dep bumps, add pages feature, bug fixes, type improvements
This commit is contained in:
parent
e21d896669
commit
fec58f2465
96 changed files with 645 additions and 384 deletions
|
@ -3,6 +3,7 @@ module.exports = {
|
|||
preset: "ts-jest",
|
||||
testEnvironment: "node",
|
||||
setupFiles: ["<rootDir>/test/setup-tests.ts"],
|
||||
// TODO: update to app dir
|
||||
moduleNameMapper: {
|
||||
"@lib/(.*)": "<rootDir>/src/lib/$1",
|
||||
"@routes/(.*)": "<rootDir>/src/routes/$1"
|
||||
|
|
|
@ -26,6 +26,9 @@ const nextConfig = {
|
|||
},
|
||||
env: {
|
||||
NEXT_PUBLIC_DRIFT_URL: process.env.DRIFT_URL
|
||||
},
|
||||
typescript: {
|
||||
// ignoreBuildErrors: true,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
10
package.json
10
package.json
|
@ -14,8 +14,8 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@next-auth/prisma-adapter": "^1.0.5",
|
||||
"@next/eslint-plugin-next": "13.1.7-canary.23",
|
||||
"@next/font": "13.1.7-canary.23",
|
||||
"@next/eslint-plugin-next": "13.2.1-canary.0",
|
||||
"@next/font": "13.2.1-canary.0",
|
||||
"@prisma/client": "^4.9.0",
|
||||
"@radix-ui/react-dialog": "^1.0.2",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.2",
|
||||
|
@ -28,7 +28,7 @@
|
|||
"client-zip": "2.3.0",
|
||||
"jest": "^29.4.1",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"next": "13.1.7-canary.23",
|
||||
"next": "13.2.1-canary.0",
|
||||
"next-auth": "^4.19.2",
|
||||
"next-themes": "^0.2.1",
|
||||
"react": "18.2.0",
|
||||
|
@ -45,7 +45,7 @@
|
|||
"uuid": "^9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@next/bundle-analyzer": "13.1.6",
|
||||
"@next/bundle-analyzer": "13.1.7-canary.26",
|
||||
"@types/bcrypt": "^5.0.0",
|
||||
"@types/lodash.debounce": "^4.0.7",
|
||||
"@types/node": "18.11.18",
|
||||
|
@ -59,7 +59,7 @@
|
|||
"cross-env": "7.0.3",
|
||||
"csstype": "^3.1.1",
|
||||
"eslint": "8.33.0",
|
||||
"eslint-config-next": "13.1.6",
|
||||
"eslint-config-next": "13.1.7-canary.26",
|
||||
"next-unused": "0.0.6",
|
||||
"prettier": "2.8.3",
|
||||
"prisma": "^4.9.0",
|
||||
|
|
157
pnpm-lock.yaml
157
pnpm-lock.yaml
|
@ -2,9 +2,9 @@ lockfileVersion: 5.4
|
|||
|
||||
specifiers:
|
||||
'@next-auth/prisma-adapter': ^1.0.5
|
||||
'@next/bundle-analyzer': 13.1.6
|
||||
'@next/eslint-plugin-next': 13.1.7-canary.23
|
||||
'@next/font': 13.1.7-canary.23
|
||||
'@next/bundle-analyzer': 13.1.7-canary.26
|
||||
'@next/eslint-plugin-next': 13.2.1-canary.0
|
||||
'@next/font': 13.2.1-canary.0
|
||||
'@prisma/client': ^4.9.0
|
||||
'@radix-ui/react-dialog': ^1.0.2
|
||||
'@radix-ui/react-dropdown-menu': ^2.0.2
|
||||
|
@ -28,10 +28,10 @@ specifiers:
|
|||
cross-env: 7.0.3
|
||||
csstype: ^3.1.1
|
||||
eslint: 8.33.0
|
||||
eslint-config-next: 13.1.6
|
||||
eslint-config-next: 13.1.7-canary.26
|
||||
jest: ^29.4.1
|
||||
lodash.debounce: ^4.0.8
|
||||
next: 13.1.7-canary.23
|
||||
next: 13.2.1-canary.0
|
||||
next-auth: ^4.19.2
|
||||
next-themes: ^0.2.1
|
||||
next-unused: 0.0.6
|
||||
|
@ -55,8 +55,8 @@ specifiers:
|
|||
|
||||
dependencies:
|
||||
'@next-auth/prisma-adapter': 1.0.5_77bhi65b6v5jbrzr36rs2ojwe4
|
||||
'@next/eslint-plugin-next': 13.1.7-canary.23
|
||||
'@next/font': 13.1.7-canary.23
|
||||
'@next/eslint-plugin-next': 13.2.1-canary.0
|
||||
'@next/font': 13.2.1-canary.0
|
||||
'@prisma/client': 4.9.0_prisma@4.9.0
|
||||
'@radix-ui/react-dialog': 1.0.2_5ndqzdd6t4rivxsukjv3i3ak2q
|
||||
'@radix-ui/react-dropdown-menu': 2.0.2_5ndqzdd6t4rivxsukjv3i3ak2q
|
||||
|
@ -69,9 +69,9 @@ dependencies:
|
|||
client-zip: 2.3.0
|
||||
jest: 29.4.1_@types+node@18.11.18
|
||||
lodash.debounce: 4.0.8
|
||||
next: 13.1.7-canary.23_biqbaboplfbrettd7655fr4n2y
|
||||
next-auth: 4.19.2_de7upave4ddivcfp3gf7tpswk4
|
||||
next-themes: 0.2.1_de7upave4ddivcfp3gf7tpswk4
|
||||
next: 13.2.1-canary.0_biqbaboplfbrettd7655fr4n2y
|
||||
next-auth: 4.19.2_avkzbs57los6fogzead7rr4u74
|
||||
next-themes: 0.2.1_avkzbs57los6fogzead7rr4u74
|
||||
react: 18.2.0
|
||||
react-datepicker: 4.8.0_biqbaboplfbrettd7655fr4n2y
|
||||
react-dom: 18.2.0_react@18.2.0
|
||||
|
@ -89,7 +89,7 @@ optionalDependencies:
|
|||
sharp: 0.31.3
|
||||
|
||||
devDependencies:
|
||||
'@next/bundle-analyzer': 13.1.6
|
||||
'@next/bundle-analyzer': 13.1.7-canary.26
|
||||
'@types/bcrypt': 5.0.0
|
||||
'@types/lodash.debounce': 4.0.7
|
||||
'@types/node': 18.11.18
|
||||
|
@ -103,7 +103,7 @@ devDependencies:
|
|||
cross-env: 7.0.3
|
||||
csstype: 3.1.1
|
||||
eslint: 8.33.0
|
||||
eslint-config-next: 13.1.6_zkdaqh7it7uc4cvz2haft7rc6u
|
||||
eslint-config-next: 13.1.7-canary.26_zkdaqh7it7uc4cvz2haft7rc6u
|
||||
next-unused: 0.0.6
|
||||
prettier: 2.8.3
|
||||
prisma: 4.9.0
|
||||
|
@ -819,11 +819,11 @@ packages:
|
|||
next-auth: ^4
|
||||
dependencies:
|
||||
'@prisma/client': 4.9.0_prisma@4.9.0
|
||||
next-auth: 4.19.2_de7upave4ddivcfp3gf7tpswk4
|
||||
next-auth: 4.19.2_avkzbs57los6fogzead7rr4u74
|
||||
dev: false
|
||||
|
||||
/@next/bundle-analyzer/13.1.6:
|
||||
resolution: {integrity: sha512-rJS9CtLoGT58mL+v2ISKANosFFWP/0YKYByHQ3vTaZrbQP8b1rYRxd2QVMJmnSXaFkiP9URt1XJ6OdGyVq5b6g==}
|
||||
/@next/bundle-analyzer/13.1.7-canary.26:
|
||||
resolution: {integrity: sha512-oRAFBr3qEsOC8cwnTWjodckrx1df1pKDSIumvvj9xcp4QeymwDpFdjz2Dp3Manh1Q/kWX+adqNu6fyJUnVDI4A==}
|
||||
dependencies:
|
||||
webpack-bundle-analyzer: 4.7.0
|
||||
transitivePeerDependencies:
|
||||
|
@ -831,28 +831,28 @@ packages:
|
|||
- utf-8-validate
|
||||
dev: true
|
||||
|
||||
/@next/env/13.1.7-canary.23:
|
||||
resolution: {integrity: sha512-7AHpOQHk0xQlLVFjvIaIkAhIsQ5QbIkZRySRigZBWJJ8ycyf6clZe8ileK/sbKdRDKT8O0YeuxtD/kOO8cRMXQ==}
|
||||
/@next/env/13.2.1-canary.0:
|
||||
resolution: {integrity: sha512-6e5sszeQUWRFZpy/HXVWs18DKbjErcF/pMKS8pIsknejN1DQPRxNFEI+QuXIf76N+r87K1qR/qWQx4WKQof8Rw==}
|
||||
dev: false
|
||||
|
||||
/@next/eslint-plugin-next/13.1.6:
|
||||
resolution: {integrity: sha512-o7cauUYsXjzSJkay8wKjpKJf2uLzlggCsGUkPu3lP09Pv97jYlekTC20KJrjQKmSv5DXV0R/uks2ZXhqjNkqAw==}
|
||||
/@next/eslint-plugin-next/13.1.7-canary.26:
|
||||
resolution: {integrity: sha512-0Cs2jSO+4vmyiWPL3ryMRGIl2+UZ+rV9TFaxzGLZUfN2yQKodR2Ilt3CWmRBFNRJr6MV2N+BIdIQ5MPy/TBeRg==}
|
||||
dependencies:
|
||||
glob: 7.1.7
|
||||
dev: true
|
||||
|
||||
/@next/eslint-plugin-next/13.1.7-canary.23:
|
||||
resolution: {integrity: sha512-8Se+LIQ4iOKxelhq0Va14PXkOm2UFgPQPJJqmN8GojiiY3gVKB2WJHsqDDu6o9dPNt7OgemLtQePcJH2barw5Q==}
|
||||
/@next/eslint-plugin-next/13.2.1-canary.0:
|
||||
resolution: {integrity: sha512-OwJgZrTGj/WovColJHOUd2RB+2uju12cYL2oL5CnyNTz999GJ/z/esghfCAqL5fh4YaiQqlZCRJEbbymTsiU0Q==}
|
||||
dependencies:
|
||||
glob: 7.1.7
|
||||
dev: false
|
||||
|
||||
/@next/font/13.1.7-canary.23:
|
||||
resolution: {integrity: sha512-y5pZjah3b5n2X1yMMAImPwAMi2hFY6Maip76R6+VzLjak+pNMB5jF72ieZoRsibGxt+efCoWOWIkh5s7ZO8YrA==}
|
||||
/@next/font/13.2.1-canary.0:
|
||||
resolution: {integrity: sha512-ByXy8uwtXMihrIKmkcDZxvLgfE5i5Y0kowcg+vATw+b7ukzUzlesI+65VWTo2AHVdtrBPGAdAjyJJTNvr/q4nQ==}
|
||||
dev: false
|
||||
|
||||
/@next/swc-android-arm-eabi/13.1.7-canary.23:
|
||||
resolution: {integrity: sha512-LxaoUF1bqi0hPrOci7RChuOg4oJA2IHiC6SviAlSTzWUgi3eJuigvfptTylO1ElBX4MMSJ5mqpq1h5fNQIg5/A==}
|
||||
/@next/swc-android-arm-eabi/13.2.1-canary.0:
|
||||
resolution: {integrity: sha512-irsC3EdtmroNaHrKeqZDJTcQ15wrTWp5JJ8IIMJ6GvW/vhVENk+unnVoxD4BQLmgelCbzi7udusISDCkUUWCkg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
@ -860,8 +860,8 @@ packages:
|
|||
dev: false
|
||||
optional: true
|
||||
|
||||
/@next/swc-android-arm64/13.1.7-canary.23:
|
||||
resolution: {integrity: sha512-gsJyRGzJlBLZNNfpPwn1T1MgeT3p/Ztx3lMuc37+v+sMjgegWZy7h5HbtTp7IT+y4DAwknNcSyLIidH88qj9lA==}
|
||||
/@next/swc-android-arm64/13.2.1-canary.0:
|
||||
resolution: {integrity: sha512-3AyZ4THkuamCyOhFkrqFY38IMI+YFmxomOcFHK+67hrqfHQRkH0boL15SnHAzURlAeruI/Mw9/PuYshYXn3Usg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
@ -869,8 +869,8 @@ packages:
|
|||
dev: false
|
||||
optional: true
|
||||
|
||||
/@next/swc-darwin-arm64/13.1.7-canary.23:
|
||||
resolution: {integrity: sha512-29HDi1EQkNaiNU0yJDikIV4jAJvs52TeGqTDf9tIkHFOAvmAF50mGbaL2urcRhy1fFvubLEx3JHWV6pAobjq3g==}
|
||||
/@next/swc-darwin-arm64/13.2.1-canary.0:
|
||||
resolution: {integrity: sha512-CN5e/C9WLGa8EdAmnndGzKh14ALfMHwjoRZYHBfFmM1Z+SArz89VrXeOmAMVgpcANBNC4ShvXp4rIUD9PveDQA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
@ -878,8 +878,8 @@ packages:
|
|||
dev: false
|
||||
optional: true
|
||||
|
||||
/@next/swc-darwin-x64/13.1.7-canary.23:
|
||||
resolution: {integrity: sha512-iWU/5+myA89I25AtLHeZP03jqUh9OfUF67oQ1ZiDNcKZAEDGqYgwqVeiUbU6rBhX483YwZOAF+iUpYefV9FLQQ==}
|
||||
/@next/swc-darwin-x64/13.2.1-canary.0:
|
||||
resolution: {integrity: sha512-eP7hKzI84MrG54M3207pynGmJHqf2V6Ex0CXpht0WSMrrv7G6W9iHyLPqk18AgxzgjA5QH4lDP+FpTaoDmpdkg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
@ -887,8 +887,8 @@ packages:
|
|||
dev: false
|
||||
optional: true
|
||||
|
||||
/@next/swc-freebsd-x64/13.1.7-canary.23:
|
||||
resolution: {integrity: sha512-IuJljewmj7BNJI9qsLXzJZBC1qU7adif0Pcg2ww5Al6l8RviGy6aBWdMgt65tHTGlUqxHhLg7mW5jT2bUw1Bcw==}
|
||||
/@next/swc-freebsd-x64/13.2.1-canary.0:
|
||||
resolution: {integrity: sha512-rloppA9VtxDFruDxMQJqrISwKzOip9/1hksljlXTBg+/fu2dJyufuARwhfksDNH3fBT0fQC10esFO+WjhLEp7g==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
@ -896,8 +896,8 @@ packages:
|
|||
dev: false
|
||||
optional: true
|
||||
|
||||
/@next/swc-linux-arm-gnueabihf/13.1.7-canary.23:
|
||||
resolution: {integrity: sha512-d1I0WY/L4RW4pQcDihWjMo+s90hv9c83lzDi1fsjhzKMJnHKyOcZKc2tXB6OvHW67y6PL6bgpPcqysmS4txwjA==}
|
||||
/@next/swc-linux-arm-gnueabihf/13.2.1-canary.0:
|
||||
resolution: {integrity: sha512-Fb0d7znHY3RlaJFCc07X+x3NGQw3Sp0UOsCOtalG2LSgMKFTt0dMc47WBZez+ratWI7dc6B3H8kmsgntBd4Skw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
@ -905,8 +905,8 @@ packages:
|
|||
dev: false
|
||||
optional: true
|
||||
|
||||
/@next/swc-linux-arm64-gnu/13.1.7-canary.23:
|
||||
resolution: {integrity: sha512-zMaepi3ZZvPserWz34ZU9JKmZPnUn/1HKEe6oNuxkHSDIqR6RM2DcUIj5mwsUdQzCAHJs8ZbLpoAtiOj7x+Ipg==}
|
||||
/@next/swc-linux-arm64-gnu/13.2.1-canary.0:
|
||||
resolution: {integrity: sha512-ATkCdBfHpTtB+7VWgsib2O98sUAa9vyB7uyHTFDWPMB9qTnTXMUfPoYxFJmYXrjwB/xoW97AKrB3FGxMM+swHw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
@ -914,8 +914,8 @@ packages:
|
|||
dev: false
|
||||
optional: true
|
||||
|
||||
/@next/swc-linux-arm64-musl/13.1.7-canary.23:
|
||||
resolution: {integrity: sha512-4bMTuoCQ5cf+6O7tIP//I4FF5o4crfRq7DdUTXoZvlF81xjlnI+gCRiHU8AllUryRhci+r4kM9/2m3GQkGQdqA==}
|
||||
/@next/swc-linux-arm64-musl/13.2.1-canary.0:
|
||||
resolution: {integrity: sha512-2x2i0dgcyhE9wycHYLAZiiHkQlzambV0qdjsG7DqQhc23UhVvkqG6dB9PHPFrkxMFPq2QDLpYGlKF9sdnnEQbw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
@ -923,8 +923,8 @@ packages:
|
|||
dev: false
|
||||
optional: true
|
||||
|
||||
/@next/swc-linux-x64-gnu/13.1.7-canary.23:
|
||||
resolution: {integrity: sha512-plovQsBsXdJUaQYx7aQHiUlYtxgawlp+1a9VX6CPJPM15HR91KmJsmdfI+JW1fVbV4x+N0TDARzdGSHKQNLAHQ==}
|
||||
/@next/swc-linux-x64-gnu/13.2.1-canary.0:
|
||||
resolution: {integrity: sha512-nRQfotbWl4V9sIS+nx4ZXtJxe8/+naeUrJSoD+Wi9PrIyF3uuLE+xi1WMHS5RhGydyduvFSyIjQO2qGzZ0Lhsw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
@ -932,8 +932,8 @@ packages:
|
|||
dev: false
|
||||
optional: true
|
||||
|
||||
/@next/swc-linux-x64-musl/13.1.7-canary.23:
|
||||
resolution: {integrity: sha512-0b9BiJ2eOFypCa41gEhURxD9borWGfXBb3AnhxwmUnMAanhZZSVutBCGjxqvki3qbTPCMUv6dVUaKXw+26ylPg==}
|
||||
/@next/swc-linux-x64-musl/13.2.1-canary.0:
|
||||
resolution: {integrity: sha512-hdM2Cds8yppGSzql2A4tCYdzjVheDh/Ei45GOz94gshO95u7SSS9UQNTaOKZTWOUfRnZYZrEbAVMhDxz10qqdQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
@ -941,8 +941,8 @@ packages:
|
|||
dev: false
|
||||
optional: true
|
||||
|
||||
/@next/swc-win32-arm64-msvc/13.1.7-canary.23:
|
||||
resolution: {integrity: sha512-psVpG4pmJHrqjXMfKcXyRNO5t18tpee4pZAuTesu8DfRd+fPqg1CwB1Hp8qBxG9c80loCoQMlKFiH5nPFAvv+g==}
|
||||
/@next/swc-win32-arm64-msvc/13.2.1-canary.0:
|
||||
resolution: {integrity: sha512-2SkgCWZbDLBgKbgJ+slVpqQLNwOzp2/ja2C8nNzwUbDB4cbD9fTyWrUngZBgg8J1OaP5KR7qHOQ8JQoHqiTOjA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
@ -950,8 +950,8 @@ packages:
|
|||
dev: false
|
||||
optional: true
|
||||
|
||||
/@next/swc-win32-ia32-msvc/13.1.7-canary.23:
|
||||
resolution: {integrity: sha512-5ZW5+QoI9yibjUPcHfh/uBjB5ubcsKoD45LNbxsxWGOl9c/tXp4zvhqIEtZ57SWajdZZiuKZ0I92MGxgiDwYfw==}
|
||||
/@next/swc-win32-ia32-msvc/13.2.1-canary.0:
|
||||
resolution: {integrity: sha512-1j1BJ2wtzDnbn6uOUPsPhOgjIWESr8TE7aLUAlxtTYgsrYdPYxe4uowa9M04mEw6UxZHatrrOA6zvTVoFUtB8g==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
@ -959,8 +959,8 @@ packages:
|
|||
dev: false
|
||||
optional: true
|
||||
|
||||
/@next/swc-win32-x64-msvc/13.1.7-canary.23:
|
||||
resolution: {integrity: sha512-+90NgEZmVqLtQ+adKtZxz67XJvjF/gESaSH8Zt96ccNH9lzoZsSyX/SYHwNJQFikiBCNn0SMeAZHIlsY5dEbqQ==}
|
||||
/@next/swc-win32-x64-msvc/13.2.1-canary.0:
|
||||
resolution: {integrity: sha512-87QjJxWWYIPKBx/FoaqEd2bxPFBdTUZ6OdEnCeuBW2XjN6qZpwL1H2dIEvh6EXJMjQYW/DAhOqgr9cvpw9fu+Q==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
@ -1955,7 +1955,7 @@ packages:
|
|||
dependencies:
|
||||
'@types/react': 18.0.27
|
||||
react: 18.2.0
|
||||
tslib: 2.4.1
|
||||
tslib: 2.5.0
|
||||
dev: false
|
||||
|
||||
/aria-hidden/1.2.2_3stiutgnnbnfnf3uowm5cip22i:
|
||||
|
@ -2970,8 +2970,8 @@ packages:
|
|||
source-map: 0.6.1
|
||||
dev: true
|
||||
|
||||
/eslint-config-next/13.1.6_zkdaqh7it7uc4cvz2haft7rc6u:
|
||||
resolution: {integrity: sha512-0cg7h5wztg/SoLAlxljZ0ZPUQ7i6QKqRiP4M2+MgTZtxWwNKb2JSwNc18nJ6/kXBI6xYvPraTbQSIhAuVw6czw==}
|
||||
/eslint-config-next/13.1.7-canary.26_zkdaqh7it7uc4cvz2haft7rc6u:
|
||||
resolution: {integrity: sha512-pKLrCNiKO4uekzZJaIXthsasvlEzSiGQLsO2I2FPr+sQAjY18UhY3jl2HZznU/vbk5GHVRKs5ibrye/29KDtbQ==}
|
||||
peerDependencies:
|
||||
eslint: ^7.23.0 || ^8.0.0
|
||||
typescript: '>=3.3.1'
|
||||
|
@ -2979,7 +2979,7 @@ packages:
|
|||
typescript:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@next/eslint-plugin-next': 13.1.6
|
||||
'@next/eslint-plugin-next': 13.1.7-canary.26
|
||||
'@rushstack/eslint-patch': 1.2.0
|
||||
'@typescript-eslint/parser': 5.49.0_zkdaqh7it7uc4cvz2haft7rc6u
|
||||
eslint: 8.33.0
|
||||
|
@ -3367,7 +3367,7 @@ packages:
|
|||
resolution: {integrity: sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==}
|
||||
engines: {node: '>= 12'}
|
||||
dependencies:
|
||||
tslib: 2.4.1
|
||||
tslib: 2.5.0
|
||||
dev: false
|
||||
|
||||
/filing-cabinet/3.3.0:
|
||||
|
@ -5473,7 +5473,7 @@ packages:
|
|||
dev: true
|
||||
optional: true
|
||||
|
||||
/next-auth/4.19.2_de7upave4ddivcfp3gf7tpswk4:
|
||||
/next-auth/4.19.2_avkzbs57los6fogzead7rr4u74:
|
||||
resolution: {integrity: sha512-6V2YG3IJQVhgCAH7mvT3yopTW92gMdUrcwGX7NQ0dCreT/+axGua/JmVdarjec0C/oJukKpIYRgjMlV+L5ZQOQ==}
|
||||
peerDependencies:
|
||||
next: ^12.2.5 || ^13
|
||||
|
@ -5488,7 +5488,7 @@ packages:
|
|||
'@panva/hkdf': 1.0.2
|
||||
cookie: 0.5.0
|
||||
jose: 4.11.0
|
||||
next: 13.1.7-canary.23_biqbaboplfbrettd7655fr4n2y
|
||||
next: 13.2.1-canary.0_biqbaboplfbrettd7655fr4n2y
|
||||
oauth: 0.9.15
|
||||
openid-client: 5.3.0
|
||||
preact: 10.11.2
|
||||
|
@ -5498,14 +5498,14 @@ packages:
|
|||
uuid: 8.3.2
|
||||
dev: false
|
||||
|
||||
/next-themes/0.2.1_de7upave4ddivcfp3gf7tpswk4:
|
||||
/next-themes/0.2.1_avkzbs57los6fogzead7rr4u74:
|
||||
resolution: {integrity: sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==}
|
||||
peerDependencies:
|
||||
next: '*'
|
||||
react: '*'
|
||||
react-dom: '*'
|
||||
dependencies:
|
||||
next: 13.1.7-canary.23_biqbaboplfbrettd7655fr4n2y
|
||||
next: 13.2.1-canary.0_biqbaboplfbrettd7655fr4n2y
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0_react@18.2.0
|
||||
dev: false
|
||||
|
@ -5521,17 +5521,20 @@ packages:
|
|||
- supports-color
|
||||
dev: true
|
||||
|
||||
/next/13.1.7-canary.23_biqbaboplfbrettd7655fr4n2y:
|
||||
resolution: {integrity: sha512-kUgSfsX+f09h8ULeqitJmUff6RPkhZq2xA2X6hSnFMrZEAjxeuAiGDdK6dZXNiuuQDHTXnZOvovupq05pf2z2w==}
|
||||
/next/13.2.1-canary.0_biqbaboplfbrettd7655fr4n2y:
|
||||
resolution: {integrity: sha512-4dXz9jinbUmxOCnXxDPxcFHmwhR+YDCLRP0kiYUpu1GiTlTYbfHAb0HosmA58ZL76W1tmqS6x8/3qkiDNtZWrg==}
|
||||
engines: {node: '>=14.6.0'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@opentelemetry/api': ^1.4.0
|
||||
fibers: '>= 3.1.0'
|
||||
node-sass: ^6.0.0 || ^7.0.0
|
||||
react: ^18.2.0
|
||||
react-dom: ^18.2.0
|
||||
sass: ^1.3.0
|
||||
peerDependenciesMeta:
|
||||
'@opentelemetry/api':
|
||||
optional: true
|
||||
fibers:
|
||||
optional: true
|
||||
node-sass:
|
||||
|
@ -5539,7 +5542,7 @@ packages:
|
|||
sass:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@next/env': 13.1.7-canary.23
|
||||
'@next/env': 13.2.1-canary.0
|
||||
'@swc/helpers': 0.4.14
|
||||
caniuse-lite: 1.0.30001449
|
||||
postcss: 8.4.14
|
||||
|
@ -5547,19 +5550,19 @@ packages:
|
|||
react-dom: 18.2.0_react@18.2.0
|
||||
styled-jsx: 5.1.1_react@18.2.0
|
||||
optionalDependencies:
|
||||
'@next/swc-android-arm-eabi': 13.1.7-canary.23
|
||||
'@next/swc-android-arm64': 13.1.7-canary.23
|
||||
'@next/swc-darwin-arm64': 13.1.7-canary.23
|
||||
'@next/swc-darwin-x64': 13.1.7-canary.23
|
||||
'@next/swc-freebsd-x64': 13.1.7-canary.23
|
||||
'@next/swc-linux-arm-gnueabihf': 13.1.7-canary.23
|
||||
'@next/swc-linux-arm64-gnu': 13.1.7-canary.23
|
||||
'@next/swc-linux-arm64-musl': 13.1.7-canary.23
|
||||
'@next/swc-linux-x64-gnu': 13.1.7-canary.23
|
||||
'@next/swc-linux-x64-musl': 13.1.7-canary.23
|
||||
'@next/swc-win32-arm64-msvc': 13.1.7-canary.23
|
||||
'@next/swc-win32-ia32-msvc': 13.1.7-canary.23
|
||||
'@next/swc-win32-x64-msvc': 13.1.7-canary.23
|
||||
'@next/swc-android-arm-eabi': 13.2.1-canary.0
|
||||
'@next/swc-android-arm64': 13.2.1-canary.0
|
||||
'@next/swc-darwin-arm64': 13.2.1-canary.0
|
||||
'@next/swc-darwin-x64': 13.2.1-canary.0
|
||||
'@next/swc-freebsd-x64': 13.2.1-canary.0
|
||||
'@next/swc-linux-arm-gnueabihf': 13.2.1-canary.0
|
||||
'@next/swc-linux-arm64-gnu': 13.2.1-canary.0
|
||||
'@next/swc-linux-arm64-musl': 13.2.1-canary.0
|
||||
'@next/swc-linux-x64-gnu': 13.2.1-canary.0
|
||||
'@next/swc-linux-x64-musl': 13.2.1-canary.0
|
||||
'@next/swc-win32-arm64-msvc': 13.2.1-canary.0
|
||||
'@next/swc-win32-ia32-msvc': 13.2.1-canary.0
|
||||
'@next/swc-win32-x64-msvc': 13.2.1-canary.0
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
- babel-plugin-macros
|
||||
|
@ -6288,7 +6291,7 @@ packages:
|
|||
react: 18.2.0
|
||||
react-remove-scroll-bar: 2.3.4_3stiutgnnbnfnf3uowm5cip22i
|
||||
react-style-singleton: 2.2.1_3stiutgnnbnfnf3uowm5cip22i
|
||||
tslib: 2.4.1
|
||||
tslib: 2.5.0
|
||||
use-callback-ref: 1.3.0_3stiutgnnbnfnf3uowm5cip22i
|
||||
use-sidecar: 1.1.2_3stiutgnnbnfnf3uowm5cip22i
|
||||
dev: false
|
||||
|
@ -7143,10 +7146,6 @@ packages:
|
|||
resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
|
||||
dev: true
|
||||
|
||||
/tslib/2.4.1:
|
||||
resolution: {integrity: sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==}
|
||||
dev: false
|
||||
|
||||
/tslib/2.5.0:
|
||||
resolution: {integrity: sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import { startTransition, Suspense, useState } from "react"
|
||||
import styles from "./auth.module.css"
|
||||
import Link from "../../components/link"
|
||||
import Link from "../../../components/link"
|
||||
import { signIn } from "next-auth/react"
|
||||
import Input from "@components/input"
|
||||
import Button from "@components/button"
|
|
@ -9,7 +9,7 @@ export function ErrorQueryParamsHandler() {
|
|||
const { setToast } = useToasts()
|
||||
|
||||
useEffect(() => {
|
||||
if (queryParams.get("error")) {
|
||||
if (queryParams?.get("error")) {
|
||||
setToast({
|
||||
message: queryParams.get("error") as string,
|
||||
type: "error"
|
|
@ -59,7 +59,10 @@ function FileDropdown({
|
|||
|
||||
return (
|
||||
<Popover>
|
||||
<Popover.Trigger className={buttonStyles.button}>
|
||||
<Popover.Trigger
|
||||
className={buttonStyles.button}
|
||||
style={{ height: 40, padding: 10 }}
|
||||
>
|
||||
<div
|
||||
className={clsx(buttonStyles.icon, styles.chevron)}
|
||||
style={{ marginRight: 6 }}
|
|
@ -1,7 +1,7 @@
|
|||
"use client"
|
||||
|
||||
import * as RadixTabs from "@radix-ui/react-tabs"
|
||||
import FormattingIcons from "src/app/(posts)/new/components/edit-document-list/edit-document/formatting-icons"
|
||||
import FormattingIcons from "src/app/(drift)/(posts)/new/components/edit-document-list/edit-document/formatting-icons"
|
||||
import { ChangeEvent, ClipboardEvent, useRef } from "react"
|
||||
import TextareaMarkdown, { TextareaMarkdownRef } from "textarea-markdown-editor"
|
||||
import Preview, { StaticPreview } from "../preview"
|
|
@ -17,7 +17,7 @@ function Description({ onChange, description }: props) {
|
|||
label="Description"
|
||||
maxLength={256}
|
||||
width="100%"
|
||||
placeholder="An optional description of your post"
|
||||
placeholder="An optional description"
|
||||
/>
|
||||
</div>
|
||||
)
|
|
@ -2,7 +2,7 @@ import { ChangeEvent, ClipboardEvent, useCallback } from "react"
|
|||
import styles from "./document.module.css"
|
||||
import Button from "@components/button"
|
||||
import Input from "@components/input"
|
||||
import DocumentTabs from "src/app/(posts)/components/tabs"
|
||||
import DocumentTabs from "src/app/(drift)/(posts)/components/tabs"
|
||||
import { Trash } from "react-feather"
|
||||
|
||||
type Props = {
|
|
@ -6,10 +6,10 @@ import generateUUID from "@lib/generate-uuid"
|
|||
import styles from "./post.module.css"
|
||||
import EditDocumentList from "./edit-document-list"
|
||||
import { ChangeEvent } from "react"
|
||||
import getTitleForPostCopy from "@lib/get-title-for-post-copy"
|
||||
import getTitleForPostCopy from "src/app/lib/get-title-for-post-copy"
|
||||
import Description from "./description"
|
||||
import { PostWithFiles } from "@lib/server/prisma"
|
||||
import PasswordModal from "../../../components/password-modal"
|
||||
import PasswordModal from "../../../../components/password-modal"
|
||||
import Title from "./title"
|
||||
import FileDropzone from "./drag-and-drop"
|
||||
import Button from "@components/button"
|
||||
|
@ -35,16 +35,14 @@ export type Document = {
|
|||
}
|
||||
|
||||
function Post({
|
||||
initialPost: stringifiedInitialPost,
|
||||
initialPost,
|
||||
newPostParent
|
||||
}: {
|
||||
initialPost?: string
|
||||
initialPost?: PostWithFiles
|
||||
newPostParent?: string
|
||||
}): JSX.Element | null {
|
||||
const { isAuthenticated } = useSessionSWR()
|
||||
|
||||
const parsedPost = JSON.parse(stringifiedInitialPost || "{}") as PostWithFiles
|
||||
const initialPost = parsedPost?.id ? parsedPost : null
|
||||
const { setToast } = useToasts()
|
||||
const router = useRouter()
|
||||
const [title, setTitle] = useState(
|
|
@ -1,6 +1,10 @@
|
|||
import NewPost from "../../components/new"
|
||||
import { notFound, redirect } from "next/navigation"
|
||||
import { getPostById } from "@lib/server/prisma"
|
||||
import {
|
||||
getPostById,
|
||||
serverPostToClientPost,
|
||||
ServerPostWithFiles
|
||||
} from "@lib/server/prisma"
|
||||
import { getSession } from "@lib/server/session"
|
||||
|
||||
async function NewFromExisting({
|
||||
|
@ -21,7 +25,7 @@ async function NewFromExisting({
|
|||
return notFound()
|
||||
}
|
||||
|
||||
const post = await getPostById(id, {
|
||||
const post = (await getPostById(id, {
|
||||
select: {
|
||||
authorId: true,
|
||||
title: true,
|
||||
|
@ -35,11 +39,11 @@ async function NewFromExisting({
|
|||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})) as ServerPostWithFiles
|
||||
|
||||
const serialized = JSON.stringify(post)
|
||||
const clientPost = post ? serverPostToClientPost(post) : undefined
|
||||
|
||||
return <NewPost initialPost={serialized} newPostParent={id} />
|
||||
return <NewPost initialPost={clientPost} newPostParent={id} />
|
||||
}
|
||||
|
||||
export default NewFromExisting
|
3
src/app/(drift)/(posts)/new/layout.tsx
Normal file
3
src/app/(drift)/(posts)/new/layout.tsx
Normal file
|
@ -0,0 +1,3 @@
|
|||
export default function NewLayout({ children }: { children: React.ReactNode }) {
|
||||
return <>{children}</>
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import NewPost from "src/app/(posts)/new/components/new"
|
||||
import NewPost from "src/app/(drift)/(posts)/new/components/new"
|
||||
import "./react-datepicker.css"
|
||||
|
||||
export default function New() {
|
|
@ -2,28 +2,21 @@
|
|||
|
||||
import Button from "@components/button"
|
||||
import ButtonGroup from "@components/button-group"
|
||||
import FileDropdown from "src/app/(posts)/components/file-dropdown"
|
||||
import FileDropdown from "src/app/(drift)/(posts)/components/file-dropdown"
|
||||
import { Edit, ArrowUpCircle, Archive } from "react-feather"
|
||||
import styles from "./post-buttons.module.css"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { PostWithFiles } from "@lib/server/prisma"
|
||||
|
||||
export const PostButtons = ({
|
||||
title,
|
||||
files,
|
||||
loading,
|
||||
postId,
|
||||
parentId
|
||||
post,
|
||||
loading
|
||||
}: {
|
||||
title: string
|
||||
files?: Pick<PostWithFiles, "files">["files"]
|
||||
post?: PostWithFiles
|
||||
loading?: boolean
|
||||
postId?: string
|
||||
parentId?: string
|
||||
visibility?: string
|
||||
authorId?: string
|
||||
}) => {
|
||||
const router = useRouter()
|
||||
const { files, id: postId, parentId, title } = post || {}
|
||||
const downloadClick = async () => {
|
||||
if (!files?.length) return
|
||||
const downloadZip = (await import("client-zip")).downloadZip
|
|
@ -2,27 +2,19 @@ import CreatedAgoBadge from "@components/badges/created-ago-badge"
|
|||
import ExpirationBadge from "@components/badges/expiration-badge"
|
||||
import VisibilityBadge from "@components/badges/visibility-badge"
|
||||
import Skeleton from "@components/skeleton"
|
||||
import { PostWithFilesAndAuthor } from "@lib/server/prisma"
|
||||
import styles from "./title.module.css"
|
||||
|
||||
type TitleProps = {
|
||||
title: string
|
||||
loading?: boolean
|
||||
displayName?: string
|
||||
visibility?: string
|
||||
createdAt?: string
|
||||
expiresAt?: string
|
||||
authorId?: string
|
||||
post?: PostWithFilesAndAuthor
|
||||
}
|
||||
|
||||
export const PostTitle = ({
|
||||
title,
|
||||
displayName,
|
||||
visibility,
|
||||
createdAt,
|
||||
expiresAt,
|
||||
loading
|
||||
}: // authorId
|
||||
TitleProps) => {
|
||||
export const PostTitle = ({ post, loading }: TitleProps) => {
|
||||
const { title, author, visibility, createdAt, expiresAt } = post || {}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore displayName should be present
|
||||
const displayName = author?.displayName
|
||||
return (
|
||||
<span className={styles.title}>
|
||||
<h1
|
|
@ -8,16 +8,12 @@ import PasswordModalWrapper from "./password-modal-wrapper"
|
|||
import { PostWithFilesAndAuthor } from "@lib/server/prisma"
|
||||
|
||||
type Props = {
|
||||
post: string | PostWithFilesAndAuthor
|
||||
post: PostWithFilesAndAuthor
|
||||
isProtected?: boolean
|
||||
isAuthor?: boolean
|
||||
}
|
||||
|
||||
const PostFiles = ({ post: _initialPost }: Props) => {
|
||||
const initialPost =
|
||||
typeof _initialPost === "string"
|
||||
? (JSON.parse(_initialPost) as PostWithFilesAndAuthor)
|
||||
: _initialPost
|
||||
const PostFiles = ({ post: initialPost }: Props) => {
|
||||
const [post, setPost] = useState<PostWithFilesAndAuthor>(initialPost)
|
||||
const router = useRouter()
|
||||
|
||||
|
@ -63,15 +59,13 @@ const PostFiles = ({ post: _initialPost }: Props) => {
|
|||
gap: "var(--gap-double)"
|
||||
}}
|
||||
>
|
||||
{post?.files?.map(({ id, content, title, html }) => (
|
||||
{post?.files?.map((file) => (
|
||||
<DocumentComponent
|
||||
skeleton={false}
|
||||
key={id}
|
||||
title={title}
|
||||
key={post.id}
|
||||
initialTab={"preview"}
|
||||
id={id}
|
||||
content={content}
|
||||
preview={html}
|
||||
file={file}
|
||||
post={post}
|
||||
/>
|
||||
))}
|
||||
</main>
|
|
@ -1,6 +1,6 @@
|
|||
"use client"
|
||||
|
||||
import { Post, PostWithFilesAndAuthor } from "@lib/server/prisma"
|
||||
import { PostWithFilesAndAuthor } from "@lib/server/prisma"
|
||||
import PasswordModal from "@components/password-modal"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useCallback, useEffect, useState } from "react"
|
||||
|
@ -10,8 +10,8 @@ import { fetchWithUser } from "src/app/lib/fetch-with-user"
|
|||
|
||||
type Props = {
|
||||
setPost: (post: PostWithFilesAndAuthor) => void
|
||||
postId: Post["id"]
|
||||
authorId: Post["authorId"]
|
||||
postId: PostWithFilesAndAuthor["id"]
|
||||
authorId: PostWithFilesAndAuthor["authorId"]
|
||||
}
|
||||
|
||||
const PasswordModalWrapper = ({ setPost, postId, authorId }: Props) => {
|
||||
|
@ -20,7 +20,9 @@ const PasswordModalWrapper = ({ setPost, postId, authorId }: Props) => {
|
|||
const { session, isLoading } = useSessionSWR()
|
||||
const isAuthor = isLoading
|
||||
? undefined
|
||||
: session?.user && session?.user?.id === authorId
|
||||
: session?.user
|
||||
? session?.user?.id === authorId
|
||||
: false
|
||||
const [isPasswordModalOpen, setIsPasswordModalOpen] = useState(false)
|
||||
const onSubmit = useCallback(
|
||||
async (password: string) => {
|
||||
|
@ -64,14 +66,14 @@ const PasswordModalWrapper = ({ setPost, postId, authorId }: Props) => {
|
|||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (isAuthor) {
|
||||
if (isAuthor === true) {
|
||||
onSubmit("author")
|
||||
setToast({
|
||||
message:
|
||||
"You're the author of this post, so you automatically have access to it.",
|
||||
type: "default"
|
||||
})
|
||||
} else {
|
||||
} else if (isAuthor === false) {
|
||||
setIsPasswordModalOpen(true)
|
||||
}
|
||||
}, [isAuthor, onSubmit, setToast])
|
|
@ -1,21 +1,20 @@
|
|||
"use client"
|
||||
|
||||
import Button from "@components/button"
|
||||
import ButtonGroup from "@components/button-group"
|
||||
import Skeleton from "@components/skeleton"
|
||||
import Tooltip from "@components/tooltip"
|
||||
import DocumentTabs from "src/app/(posts)/components/tabs"
|
||||
import DocumentTabs from "src/app/(drift)/(posts)/components/tabs"
|
||||
import Link from "next/link"
|
||||
import { memo } from "react"
|
||||
import { Download, ExternalLink } from "react-feather"
|
||||
import { Download, ExternalLink, Globe } from "react-feather"
|
||||
import styles from "./document.module.css"
|
||||
import { getURLFriendlyTitle } from "src/app/lib/get-url-friendly-title"
|
||||
import { PostWithFiles, ServerPost } from "@lib/server/prisma"
|
||||
import { isAllowedVisibilityForWebpage } from "@lib/constants"
|
||||
|
||||
type SharedProps = {
|
||||
title?: string
|
||||
initialTab: "edit" | "preview"
|
||||
id?: string
|
||||
content?: string
|
||||
preview?: string
|
||||
file?: PostWithFiles["files"][0]
|
||||
post?: Pick<ServerPost, "id" | "title" | "visibility">
|
||||
}
|
||||
|
||||
type Props = (
|
||||
|
@ -28,7 +27,13 @@ type Props = (
|
|||
) &
|
||||
SharedProps
|
||||
|
||||
const DownloadButtons = ({ rawLink }: { rawLink?: string }) => {
|
||||
const DownloadButtons = ({
|
||||
rawLink,
|
||||
siteLink
|
||||
}: {
|
||||
rawLink?: string
|
||||
siteLink?: string
|
||||
}) => {
|
||||
return (
|
||||
<ButtonGroup>
|
||||
<Tooltip content="Download">
|
||||
|
@ -44,15 +49,28 @@ const DownloadButtons = ({ rawLink }: { rawLink?: string }) => {
|
|||
/>
|
||||
</Link>
|
||||
</Tooltip>
|
||||
<Tooltip content="Open raw in new tab">
|
||||
<Link href={rawLink || ""} target="_blank" rel="noopener noreferrer">
|
||||
<Button
|
||||
iconLeft={<ExternalLink color="var(--fg)" />}
|
||||
aria-label="Open raw file in new tab"
|
||||
style={{ border: "none", background: "transparent" }}
|
||||
/>
|
||||
</Link>
|
||||
</Tooltip>
|
||||
{rawLink ? (
|
||||
<Tooltip content="Open raw in new tab">
|
||||
<Link href={rawLink || ""} target="_blank" rel="noopener noreferrer">
|
||||
<Button
|
||||
iconLeft={<ExternalLink color="var(--fg)" />}
|
||||
aria-label="Open raw file in new tab"
|
||||
style={{ border: "none", background: "transparent" }}
|
||||
/>
|
||||
</Link>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
{siteLink ? (
|
||||
<Tooltip content="Open as webpage">
|
||||
<Link href={siteLink || ""} target="_blank" rel="noopener noreferrer">
|
||||
<Button
|
||||
iconLeft={<Globe color="var(--fg)" />}
|
||||
aria-label="Open as webpage"
|
||||
style={{ border: "none", background: "transparent" }}
|
||||
/>
|
||||
</Link>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</ButtonGroup>
|
||||
)
|
||||
}
|
||||
|
@ -78,14 +96,14 @@ const Document = ({ skeleton, ...props }: Props) => {
|
|||
)
|
||||
}
|
||||
|
||||
const { title, initialTab, id, content = "", preview } = props
|
||||
const { file, post } = props
|
||||
|
||||
// if the query has our title, we can use it to scroll to the file.
|
||||
// we can't use the browsers implementation because the data isn't loaded yet
|
||||
if (title && typeof window !== "undefined") {
|
||||
if (file?.title && typeof window !== "undefined") {
|
||||
const hash = window.location.hash
|
||||
if (hash && hash === `#${title}`) {
|
||||
const element = document.getElementById(title)
|
||||
if (file && hash && hash === `#${file?.title}`) {
|
||||
const element = document.getElementById(file.title)
|
||||
if (element) {
|
||||
element.scrollIntoView()
|
||||
}
|
||||
|
@ -95,28 +113,35 @@ const Document = ({ skeleton, ...props }: Props) => {
|
|||
return (
|
||||
<>
|
||||
<div className={styles.card}>
|
||||
<header id={title}>
|
||||
<header id={file?.title}>
|
||||
<Link
|
||||
href={`#${title}`}
|
||||
href={`#${file?.title}`}
|
||||
aria-label="File"
|
||||
style={{
|
||||
textDecoration: "none",
|
||||
color: "var(--fg)"
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
{file?.title}
|
||||
</Link>
|
||||
{/* TODO: switch to api once next.js bug is fixed */}
|
||||
{/* Not /api/ because of rewrites defined in next.config.mjs */}
|
||||
<DownloadButtons rawLink={`/api/file/raw/${id}`} />
|
||||
<DownloadButtons
|
||||
rawLink={`/api/file/raw/${file?.id}`}
|
||||
siteLink={
|
||||
file && post && isAllowedVisibilityForWebpage(post.visibility)
|
||||
? `/pages/${file.id}/${getURLFriendlyTitle(file?.title || "")}`
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</header>
|
||||
<div className={styles.documentContainer}>
|
||||
<DocumentTabs
|
||||
defaultTab={initialTab}
|
||||
staticPreview={preview}
|
||||
defaultTab={props.initialTab}
|
||||
staticPreview={file?.html}
|
||||
isEditing={false}
|
||||
>
|
||||
{content}
|
||||
{file?.content || ""}
|
||||
</DocumentTabs>
|
||||
</div>
|
||||
</div>
|
20
src/app/(drift)/(posts)/post/[id]/context.tsx
Normal file
20
src/app/(drift)/(posts)/post/[id]/context.tsx
Normal file
|
@ -0,0 +1,20 @@
|
|||
"use client"
|
||||
|
||||
import { createContext, useContext } from "react"
|
||||
import { getPost } from "./get-post"
|
||||
|
||||
const PostContext = createContext<
|
||||
Awaited<ReturnType<typeof getPost>> | null
|
||||
>(null)
|
||||
|
||||
export const PostProvider = PostContext.Provider
|
||||
|
||||
export const usePost = () => {
|
||||
const post = useContext(PostContext)
|
||||
|
||||
if (!post) {
|
||||
throw new Error("usePost must be used within a PostProvider")
|
||||
}
|
||||
|
||||
return post
|
||||
}
|
|
@ -41,26 +41,24 @@ export const getPost = cache(async (id: string) => {
|
|||
}
|
||||
|
||||
if (post.visibility === "public" || post.visibility === "unlisted") {
|
||||
return { post }
|
||||
return post
|
||||
}
|
||||
|
||||
if (post.visibility === "private") {
|
||||
const user = await getCurrentUser()
|
||||
if (user?.id === post.authorId || user?.role === "admin") {
|
||||
return { post }
|
||||
return post
|
||||
}
|
||||
return redirect("/new")
|
||||
}
|
||||
|
||||
if (post.visibility === "protected") {
|
||||
return {
|
||||
post: {
|
||||
visibility: "protected",
|
||||
authorId: post.authorId,
|
||||
id: post.id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { post }
|
||||
return post
|
||||
})
|
40
src/app/(drift)/(posts)/post/[id]/layout.tsx
Normal file
40
src/app/(drift)/(posts)/post/[id]/layout.tsx
Normal file
|
@ -0,0 +1,40 @@
|
|||
import {
|
||||
PostWithFilesAndAuthor,
|
||||
serverPostToClientPost,
|
||||
ServerPostWithFilesAndAuthor
|
||||
} from "@lib/server/prisma"
|
||||
import ScrollToTop from "@components/scroll-to-top"
|
||||
import { PostButtons } from "./components/header/post-buttons"
|
||||
import styles from "./layout.module.css"
|
||||
import { PostTitle } from "./components/header/title"
|
||||
import { getPost } from "./get-post"
|
||||
|
||||
export default async function PostLayout({
|
||||
children,
|
||||
params
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
params: {
|
||||
id: string
|
||||
}
|
||||
}) {
|
||||
const post = (await getPost(params.id)) as ServerPostWithFilesAndAuthor
|
||||
|
||||
// TODO: type-safe
|
||||
const clientPost = serverPostToClientPost(post) as PostWithFilesAndAuthor
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<div className={styles.header}>
|
||||
{post.visibility !== "protected" && <PostButtons post={clientPost} />}
|
||||
{post.visibility !== "protected" && <PostTitle post={clientPost} />}
|
||||
</div>
|
||||
{post.description && (
|
||||
<div>
|
||||
<p>{post.description}</p>
|
||||
</div>
|
||||
)}
|
||||
<ScrollToTop />
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -1,4 +1,9 @@
|
|||
import VisibilityControl from "@components/badges/visibility-control"
|
||||
import {
|
||||
PostWithFilesAndAuthor,
|
||||
serverPostToClientPost,
|
||||
ServerPostWithFilesAndAuthor
|
||||
} from "@lib/server/prisma"
|
||||
import PostFiles from "./components/post-files"
|
||||
import { getPost } from "./get-post"
|
||||
|
||||
|
@ -9,11 +14,12 @@ export default async function PostPage({
|
|||
id: string
|
||||
}
|
||||
}) {
|
||||
const { post } = await getPost(params.id)
|
||||
const stringifiedPost = JSON.stringify(post)
|
||||
const post = (await getPost(params.id)) as ServerPostWithFilesAndAuthor
|
||||
const clientPost = serverPostToClientPost(post) as PostWithFilesAndAuthor
|
||||
|
||||
return (
|
||||
<>
|
||||
<PostFiles post={stringifiedPost} />
|
||||
<PostFiles post={clientPost} />
|
||||
<VisibilityControl
|
||||
authorId={post.authorId}
|
||||
postId={post.id}
|
|
@ -3,7 +3,7 @@
|
|||
import Button from "@components/button"
|
||||
import { Spinner } from "@components/spinner"
|
||||
import { useToasts } from "@components/toasts"
|
||||
import { Post, User } from "@lib/server/prisma"
|
||||
import { ServerPostWithFilesAndAuthor, UserWithPosts } from "@lib/server/prisma"
|
||||
import Link from "next/link"
|
||||
import { useState } from "react"
|
||||
import { fetchWithUser } from "src/app/lib/fetch-with-user"
|
||||
|
@ -14,7 +14,7 @@ export function UserTable({
|
|||
}: {
|
||||
users?: {
|
||||
createdAt: string
|
||||
posts?: Post[]
|
||||
posts?: ServerPostWithFilesAndAuthor[]
|
||||
id: string
|
||||
email: string | null
|
||||
role: string | null
|
||||
|
@ -95,7 +95,7 @@ export function PostTable({
|
|||
posts?: {
|
||||
createdAt: string
|
||||
id: string
|
||||
author?: User | null
|
||||
author?: UserWithPosts | null
|
||||
title: string
|
||||
visibility: string
|
||||
}[]
|
|
@ -1,10 +1,11 @@
|
|||
import { getCurrentUser } from "@lib/server/session"
|
||||
import { redirect } from "next/navigation"
|
||||
import { PropsWithChildren } from "react"
|
||||
|
||||
export default async function AdminLayout({
|
||||
children
|
||||
}: PropsWithChildren<unknown>) {
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const user = await getCurrentUser()
|
||||
const isAdmin = user?.role === "admin"
|
||||
|
|
@ -1,4 +1,10 @@
|
|||
import { getAllPosts, getAllUsers } from "@lib/server/prisma"
|
||||
import {
|
||||
getAllPosts,
|
||||
getAllUsers,
|
||||
PostWithFiles,
|
||||
serverPostToClientPost,
|
||||
ServerPostWithFiles
|
||||
} from "@lib/server/prisma"
|
||||
import { PostTable, UserTable } from "./components/tables"
|
||||
|
||||
export default async function AdminPage() {
|
||||
|
@ -25,15 +31,9 @@ export default async function AdminPage() {
|
|||
|
||||
const [users, posts] = await Promise.all([usersPromise, postsPromise])
|
||||
|
||||
const serializedPosts = posts.map((post) => {
|
||||
return {
|
||||
...post,
|
||||
createdAt: post.createdAt.toISOString(),
|
||||
updatedAt: post.updatedAt.toISOString(),
|
||||
expiresAt: post.expiresAt?.toISOString(),
|
||||
deletedAt: post.deletedAt?.toISOString()
|
||||
}
|
||||
})
|
||||
const serializedPosts = posts.map((post) =>
|
||||
serverPostToClientPost(post as ServerPostWithFiles)
|
||||
) as PostWithFiles[]
|
||||
|
||||
const serializedUsers = users.map((user) => {
|
||||
return {
|
||||
|
@ -46,7 +46,8 @@ export default async function AdminPage() {
|
|||
<div>
|
||||
<h1>Admin</h1>
|
||||
<h2>Users</h2>
|
||||
<UserTable users={serializedUsers} />
|
||||
{/* @ts-expect-error Type 'unknown' is not assignable to type */}
|
||||
<UserTable users={serializedUsers as unknown} />
|
||||
<h2>Posts</h2>
|
||||
<PostTable posts={serializedPosts} />
|
||||
</div>
|
|
@ -1,5 +1,9 @@
|
|||
import PostList from "@components/post-list"
|
||||
import { getPostsByUser, getUserById } from "@lib/server/prisma"
|
||||
import {
|
||||
getPostsByUser,
|
||||
getUserById,
|
||||
serverPostToClientPost
|
||||
} from "@lib/server/prisma"
|
||||
import Image from "next/image"
|
||||
import { Suspense } from "react"
|
||||
import { User } from "react-feather"
|
||||
|
@ -11,15 +15,10 @@ async function PostListWrapper({
|
|||
posts: ReturnType<typeof getPostsByUser>
|
||||
userId: string
|
||||
}) {
|
||||
const data = (await posts).filter((post) => post.visibility === "public")
|
||||
return (
|
||||
<PostList
|
||||
userId={userId}
|
||||
initialPosts={JSON.stringify(data)}
|
||||
hideSearch
|
||||
hideActions
|
||||
/>
|
||||
)
|
||||
const data = (await posts)
|
||||
.filter((post) => post.visibility === "public")
|
||||
.map(serverPostToClientPost)
|
||||
return <PostList userId={userId} initialPosts={data} hideSearch hideActions />
|
||||
}
|
||||
|
||||
export default async function UserPage({
|
|
@ -4,14 +4,16 @@ import Layout from "@components/layout"
|
|||
import { Toasts } from "@components/toasts"
|
||||
import Header from "@components/header"
|
||||
import { Inter } from "@next/font/google"
|
||||
import { PropsWithChildren, Suspense } from "react"
|
||||
import { Suspense } from "react"
|
||||
import { Spinner } from "@components/spinner"
|
||||
|
||||
const inter = Inter({ subsets: ["latin"], variable: "--inter-font" })
|
||||
|
||||
export default async function RootLayout({
|
||||
children
|
||||
}: PropsWithChildren<unknown>) {
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
// suppressHydrationWarning is required because of next-themes
|
||||
<html lang="en" className={inter.variable} suppressHydrationWarning>
|
|
@ -1,5 +1,5 @@
|
|||
import { redirect } from "next/navigation"
|
||||
import { getPostsByUser } from "@lib/server/prisma"
|
||||
import { getPostsByUser, serverPostToClientPost } from "@lib/server/prisma"
|
||||
import PostList from "@components/post-list"
|
||||
import { getCurrentUser } from "@lib/server/session"
|
||||
import { authOptions } from "@lib/server/auth"
|
||||
|
@ -12,14 +12,12 @@ export default async function Mine() {
|
|||
return redirect(authOptions.pages?.signIn || "/new")
|
||||
}
|
||||
|
||||
const posts = await getPostsByUser(userId, true)
|
||||
|
||||
const stringifiedPosts = JSON.stringify(posts)
|
||||
const posts = (await getPostsByUser(userId, true)).map(serverPostToClientPost)
|
||||
return (
|
||||
<Suspense fallback={<PostList skeleton={true} initialPosts={[]} />}>
|
||||
<PostList
|
||||
userId={userId}
|
||||
initialPosts={stringifiedPosts}
|
||||
initialPosts={posts}
|
||||
isOwner={true}
|
||||
hideSearch={false}
|
||||
/>
|
|
@ -2,7 +2,11 @@ import Image from "next/image"
|
|||
import Card from "@components/card"
|
||||
import { getWelcomeContent } from "src/pages/api/welcome"
|
||||
import DocumentTabs from "./(posts)/components/tabs"
|
||||
import { getAllPosts } from "@lib/server/prisma"
|
||||
import {
|
||||
getAllPosts,
|
||||
serverPostToClientPost,
|
||||
ServerPostWithFilesAndAuthor
|
||||
} from "@lib/server/prisma"
|
||||
import PostList, { NoPostsFound } from "@components/post-list"
|
||||
import { cache, Suspense } from "react"
|
||||
import ErrorBoundary from "@components/error/fallback"
|
||||
|
@ -34,12 +38,7 @@ export default async function Page() {
|
|||
<ErrorBoundary>
|
||||
<Suspense
|
||||
fallback={
|
||||
<PostList
|
||||
skeleton
|
||||
hideActions
|
||||
hideSearch
|
||||
initialPosts={JSON.stringify({})}
|
||||
/>
|
||||
<PostList skeleton hideActions hideSearch initialPosts={[]} />
|
||||
}
|
||||
>
|
||||
{/* @ts-expect-error because of async RSC */}
|
||||
|
@ -67,14 +66,14 @@ async function WelcomePost() {
|
|||
}
|
||||
|
||||
async function PublicPostList() {
|
||||
const posts = await getAllPosts({
|
||||
const posts = (await getAllPosts({
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
createdAt: true,
|
||||
author: {
|
||||
select: {
|
||||
name: true
|
||||
displayName: true
|
||||
}
|
||||
},
|
||||
visibility: true,
|
||||
|
@ -92,13 +91,13 @@ async function PublicPostList() {
|
|||
orderBy: {
|
||||
createdAt: "desc"
|
||||
}
|
||||
})
|
||||
})) as unknown as ServerPostWithFilesAndAuthor[]
|
||||
|
||||
if (posts.length === 0) {
|
||||
return <NoPostsFound />
|
||||
}
|
||||
|
||||
return (
|
||||
<PostList initialPosts={JSON.stringify(posts)} hideActions hideSearch />
|
||||
)
|
||||
const clientPosts = posts.map((post) => serverPostToClientPost(post))
|
||||
|
||||
return <PostList initialPosts={clientPosts} hideActions hideSearch />
|
||||
}
|
|
@ -6,11 +6,7 @@ import { ThemeProvider } from "next-themes"
|
|||
import { PropsWithChildren } from "react"
|
||||
import { SWRConfig } from "swr"
|
||||
|
||||
export type ChildrenProps = {
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
export function Providers({ children }: ChildrenProps) {
|
||||
export function Providers({ children }: PropsWithChildren<unknown>) {
|
||||
return (
|
||||
<SessionProvider>
|
||||
<RadixTooltip.Provider delayDuration={200}>
|
|
@ -1,6 +1,6 @@
|
|||
import SettingsGroup from "../components/settings-group"
|
||||
import Profile from "src/app/settings/components/sections/profile"
|
||||
import SettingsGroup from "../../components/settings-group"
|
||||
import APIKeys from "./components/sections/api-keys"
|
||||
import Profile from "./components/sections/profile"
|
||||
|
||||
export default async function SettingsPage() {
|
||||
return (
|
|
@ -1,5 +0,0 @@
|
|||
import { ChildrenProps } from "src/app/providers"
|
||||
|
||||
export default function NewLayout({ children }: ChildrenProps) {
|
||||
return <>{children}</>
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
import { PostWithFilesAndAuthor } from "@lib/server/prisma"
|
||||
import ScrollToTop from "@components/scroll-to-top"
|
||||
import { title } from "process"
|
||||
import { PostButtons } from "./components/header/post-buttons"
|
||||
import styles from "./layout.module.css"
|
||||
import { PostTitle } from "./components/header/title"
|
||||
import { getPost } from "./get-post"
|
||||
import { PropsWithChildren } from "react"
|
||||
|
||||
export default async function PostLayout({
|
||||
children,
|
||||
params
|
||||
}: PropsWithChildren<{
|
||||
params: {
|
||||
id: string
|
||||
}
|
||||
}>) {
|
||||
const { post } = (await getPost(params.id)) as {
|
||||
post: PostWithFilesAndAuthor
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<div className={styles.header}>
|
||||
{/* post.title is not set when the post is protected */}
|
||||
{post.title && (
|
||||
<PostButtons
|
||||
parentId={post.parentId || undefined}
|
||||
postId={post.id}
|
||||
files={post.files}
|
||||
title={title}
|
||||
authorId={post.authorId}
|
||||
visibility={post.visibility}
|
||||
/>
|
||||
)}
|
||||
{post.title && (
|
||||
<PostTitle
|
||||
title={post.title}
|
||||
createdAt={post.createdAt.toString()}
|
||||
expiresAt={post.expiresAt?.toString()}
|
||||
// displayName is an optional param
|
||||
displayName={post.author?.displayName || undefined}
|
||||
visibility={post.visibility}
|
||||
authorId={post.authorId}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{post.description && (
|
||||
<div>
|
||||
<p>{post.description}</p>
|
||||
</div>
|
||||
)}
|
||||
<ScrollToTop />
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -8,11 +8,11 @@ import Badge from "../badge"
|
|||
const ExpirationBadge = ({
|
||||
postExpirationDate
|
||||
}: {
|
||||
postExpirationDate: Date | string | null
|
||||
postExpirationDate: Date | string | undefined
|
||||
onExpires?: () => void
|
||||
}) => {
|
||||
const expirationDate = useMemo(
|
||||
() => (postExpirationDate ? new Date(postExpirationDate) : null),
|
||||
() => (postExpirationDate ? new Date(postExpirationDate) : undefined),
|
||||
[postExpirationDate]
|
||||
)
|
||||
const [timeUntilString, setTimeUntil] = useState<string | null>(
|
||||
|
|
|
@ -9,6 +9,7 @@ import { Spinner } from "@components/spinner"
|
|||
import { useRouter } from "next/navigation"
|
||||
import { useSessionSWR } from "@lib/use-session-swr"
|
||||
import { fetchWithUser } from "src/app/lib/fetch-with-user"
|
||||
import FadeIn from "@components/fade-in"
|
||||
|
||||
type Props = {
|
||||
authorId: string
|
||||
|
@ -87,7 +88,7 @@ function VisibilityControl({
|
|||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<FadeIn>
|
||||
<ButtonGroup
|
||||
style={{
|
||||
maxWidth: 600,
|
||||
|
@ -128,7 +129,7 @@ function VisibilityControl({
|
|||
onClose={onClosePasswordModal}
|
||||
onSubmit={submitPassword}
|
||||
/>
|
||||
</>
|
||||
</FadeIn>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -3,8 +3,6 @@
|
|||
cursor: pointer;
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid var(--border);
|
||||
/* padding: var(--gap-half) var(--gap); */
|
||||
color: var(--darker-gray);
|
||||
}
|
||||
|
||||
.button:hover,
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
@media (prefers-reduced-motion: no-preference) {
|
||||
.fadeIn {
|
||||
animation-name: fadeInAnimation;
|
||||
animation-fill-mode: backwards;
|
||||
}
|
||||
/* @media (prefers-reduced-motion: no-preference) { */
|
||||
.fadeIn {
|
||||
animation-name: fadeInAnimation;
|
||||
animation-fill-mode: backwards;
|
||||
}
|
||||
/* } */
|
||||
|
||||
@keyframes fadeInAnimation {
|
||||
from {
|
||||
|
|
|
@ -1,8 +1,18 @@
|
|||
"use client"
|
||||
|
||||
import { PropsWithChildren } from "react"
|
||||
import clsx from "clsx"
|
||||
import styles from "./page.module.css"
|
||||
|
||||
export default function Layout({ children }: PropsWithChildren<unknown>) {
|
||||
return <div className={styles.page}>{children}</div>
|
||||
export default function Layout({
|
||||
children,
|
||||
forSites
|
||||
}: {
|
||||
forSites?: boolean
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<div className={clsx(styles.page, forSites && styles.forSites)}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -8,6 +8,10 @@
|
|||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.forSites {
|
||||
margin-top: var(--gap);
|
||||
}
|
||||
|
||||
/* 55rem == --main-content */
|
||||
@media screen and (max-width: 55rem) {
|
||||
.page {
|
||||
|
|
|
@ -13,7 +13,7 @@ import { fetchWithUser } from "src/app/lib/fetch-with-user"
|
|||
import { Stack } from "@components/stack"
|
||||
|
||||
type Props = {
|
||||
initialPosts: string | PostWithFiles[]
|
||||
initialPosts: PostWithFiles[]
|
||||
morePosts?: boolean
|
||||
hideSearch?: boolean
|
||||
hideActions?: boolean
|
||||
|
@ -24,17 +24,13 @@ type Props = {
|
|||
}
|
||||
|
||||
const PostList = ({
|
||||
initialPosts: initialPostsMaybeJSON,
|
||||
initialPosts,
|
||||
hideSearch,
|
||||
hideActions,
|
||||
isOwner,
|
||||
skeleton,
|
||||
userId
|
||||
}: Props) => {
|
||||
const initialPosts =
|
||||
typeof initialPostsMaybeJSON === "string"
|
||||
? JSON.parse(initialPostsMaybeJSON)
|
||||
: initialPostsMaybeJSON
|
||||
const [searchValue, setSearchValue] = useState("")
|
||||
const [searching, setSearching] = useState(false)
|
||||
const [posts, setPosts] = useState<PostWithFiles[]>(initialPosts)
|
||||
|
|
|
@ -8,6 +8,12 @@ import styles from "./scroll.module.css"
|
|||
|
||||
const ScrollToTop = () => {
|
||||
const [shouldShow, setShouldShow] = useState(false)
|
||||
|
||||
const isReducedMotion =
|
||||
typeof window !== "undefined"
|
||||
? window.matchMedia("(prefers-reduced-motion: reduce)").matches
|
||||
: false
|
||||
|
||||
useEffect(() => {
|
||||
// if user is scrolled, set visible
|
||||
const handleScroll = () => {
|
||||
|
@ -17,13 +23,8 @@ const ScrollToTop = () => {
|
|||
return () => window.removeEventListener("scroll", handleScroll)
|
||||
}, [])
|
||||
|
||||
const isReducedMotion =
|
||||
typeof window !== "undefined"
|
||||
? window.matchMedia("(prefers-reduced-motion: reduce)").matches
|
||||
: false
|
||||
const onClick = async (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.currentTarget.blur()
|
||||
|
||||
window.scrollTo({ top: 0, behavior: isReducedMotion ? "auto" : "smooth" })
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import Card from "@components/card"
|
||||
"use client"
|
||||
|
||||
import * as RadixTooltip from "@radix-ui/react-tooltip"
|
||||
import styles from "./tooltip.module.css"
|
||||
|
||||
|
@ -17,8 +18,9 @@ const Tooltip = ({
|
|||
<RadixTooltip.Trigger asChild className={className}>
|
||||
{children}
|
||||
</RadixTooltip.Trigger>
|
||||
|
||||
<RadixTooltip.Content>
|
||||
<Card className={styles.tooltip}>{content}</Card>
|
||||
<div className={styles.tooltip}>{content}</div>
|
||||
</RadixTooltip.Content>
|
||||
</RadixTooltip.Root>
|
||||
)
|
||||
|
|
|
@ -1,24 +1,32 @@
|
|||
.tooltip {
|
||||
animation: fadein 300ms;
|
||||
animation: fadein 300ms;
|
||||
background: var(--bg);
|
||||
border-radius: var(--radius);
|
||||
padding: 0 var(--gap);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
[data-side='top'] .tooltip{
|
||||
margin-bottom: var(--gap-quarter);
|
||||
[data-side="top"] .tooltip {
|
||||
margin-bottom: var(--gap-quarter);
|
||||
}
|
||||
|
||||
.tooltip[data-side='bottom'] {
|
||||
margin-top: var(--gap-quarter);
|
||||
.tooltip[data-side="bottom"] {
|
||||
margin-top: var(--gap-quarter);
|
||||
}
|
||||
|
||||
.tooltip[data-side='left'] {
|
||||
margin-right: var(--gap-quarter);
|
||||
.tooltip[data-side="left"] {
|
||||
margin-right: var(--gap-quarter);
|
||||
}
|
||||
|
||||
.tooltip[data-side='right'] {
|
||||
margin-left: var(--gap-quarter);
|
||||
.tooltip[data-side="right"] {
|
||||
margin-left: var(--gap-quarter);
|
||||
}
|
||||
|
||||
@keyframes fadein {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
|
3
src/app/lib/get-url-friendly-title.tsx
Normal file
3
src/app/lib/get-url-friendly-title.tsx
Normal file
|
@ -0,0 +1,3 @@
|
|||
export function getURLFriendlyTitle(title: string) {
|
||||
return title.replace(/\s/g, "-")
|
||||
}
|
25
src/app/pages/[fileId]/[fileTitle]/layout.tsx
Normal file
25
src/app/pages/[fileId]/[fileTitle]/layout.tsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
import "@styles/globals.css"
|
||||
import "@styles/markdown.css"
|
||||
|
||||
import Layout from "@components/layout"
|
||||
import { Inter } from "@next/font/google"
|
||||
import ThemeProvider from "./theme-provider"
|
||||
|
||||
const inter = Inter({ subsets: ["latin"], variable: "--inter-font" })
|
||||
|
||||
export default async function RootLayout({
|
||||
children
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
// suppressHydrationWarning is required because of next-themes
|
||||
<html lang="en" className={inter.variable} suppressHydrationWarning>
|
||||
<body>
|
||||
<ThemeProvider>
|
||||
<Layout forSites>{children}</Layout>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
89
src/app/pages/[fileId]/[fileTitle]/page.tsx
Normal file
89
src/app/pages/[fileId]/[fileTitle]/page.tsx
Normal file
|
@ -0,0 +1,89 @@
|
|||
import {
|
||||
ALLOWED_VISIBILITIES_FOR_WEBPAGE,
|
||||
isAllowedVisibilityForWebpage
|
||||
} from "@lib/constants"
|
||||
import {
|
||||
getAllPosts,
|
||||
getFileById,
|
||||
getPostById,
|
||||
ServerPostWithFiles
|
||||
} from "@lib/server/prisma"
|
||||
import { Metadata } from "next"
|
||||
import { notFound } from "next/navigation"
|
||||
import { getURLFriendlyTitle } from "src/app/lib/get-url-friendly-title"
|
||||
|
||||
export default async function FilePage({
|
||||
params
|
||||
}: {
|
||||
params: {
|
||||
fileId: string
|
||||
}
|
||||
}) {
|
||||
const { fileId: id } = params
|
||||
|
||||
const file = await getFileById(id)
|
||||
|
||||
if (!file || !isAllowedVisibilityForWebpage(file.post.visibility)) {
|
||||
return notFound()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1 style={{ color: "var(--gray)" }}>{file.title}</h1>
|
||||
<hr />
|
||||
<article
|
||||
dangerouslySetInnerHTML={{ __html: file.html.toString("utf-8") }}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const posts = (await getAllPosts({
|
||||
select: {
|
||||
id: true,
|
||||
files: {
|
||||
// include where visibility is public or unlisted
|
||||
// and title ends with .md
|
||||
where: {
|
||||
title: {
|
||||
endsWith: ".md"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
where: {
|
||||
visibility: {
|
||||
in: ALLOWED_VISIBILITIES_FOR_WEBPAGE
|
||||
}
|
||||
}
|
||||
})) as ServerPostWithFiles[]
|
||||
|
||||
return posts.flatMap((post) => {
|
||||
return post.files.map((file) => ({
|
||||
fileId: file.id,
|
||||
fileTitle: getURLFriendlyTitle(file.title)
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params
|
||||
}: {
|
||||
params: {
|
||||
fileId: string
|
||||
}
|
||||
}): Promise<Metadata> {
|
||||
const { fileId: postId } = params
|
||||
const post = await getPostById(postId, {
|
||||
select: {
|
||||
description: true,
|
||||
title: true
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
title: post?.title || "",
|
||||
description: post?.description || ""
|
||||
}
|
||||
}
|
14
src/app/pages/[fileId]/[fileTitle]/theme-provider.tsx
Normal file
14
src/app/pages/[fileId]/[fileTitle]/theme-provider.tsx
Normal file
|
@ -0,0 +1,14 @@
|
|||
"use client"
|
||||
|
||||
import { PropsWithChildren } from "react"
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes"
|
||||
|
||||
export default function ThemeProvider({
|
||||
children
|
||||
}: PropsWithChildren<unknown>) {
|
||||
return (
|
||||
<NextThemesProvider enableSystem defaultTheme="dark">
|
||||
{children}
|
||||
</NextThemesProvider>
|
||||
)
|
||||
}
|
|
@ -122,6 +122,10 @@ table th {
|
|||
|
||||
table th,
|
||||
table td {
|
||||
padding: 0.5rem 1rem;
|
||||
padding: 0.35rem 0.75rem;
|
||||
border: 1px solid var(--light-gray);
|
||||
}
|
||||
|
||||
article > :not(:first-child) {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
|
|
@ -74,7 +74,9 @@ export const config = (env: Environment): Config => {
|
|||
is_production,
|
||||
enable_admin: stringToBoolean(env.ENABLE_ADMIN),
|
||||
registration_password: env.REGISTRATION_PASSWORD ?? "",
|
||||
welcome_content: env.WELCOME_CONTENT ?? "Welcome to Drift.",
|
||||
welcome_content:
|
||||
env.WELCOME_CONTENT ??
|
||||
"## Drift is a self-hostable clone of GitHub Gist.\n\nIt is a simple way to save and share code and text snippets, with support for the following:\n\n- Render GitHub Extended Markdown\n- User authentication\n- Private, public, and password protected posts\n- Syntax highlighting and language detection\n- Drag-and-drop file uploading \n\n You can find the source code and sponsor development on [GitHub](https://github.com/MaxLeiter/drift).",
|
||||
welcome_title: env.WELCOME_TITLE ?? "Drift",
|
||||
url:
|
||||
throwIfUndefined("DRIFT_URL", true) ||
|
||||
|
|
|
@ -1,3 +1,12 @@
|
|||
import { ServerPost } from "./server/prisma"
|
||||
|
||||
// Visibilties for the webpages feature
|
||||
export const ALLOWED_VISIBILITIES_FOR_WEBPAGE = ["public", "unlisted"]
|
||||
export function isAllowedVisibilityForWebpage(visibility: ServerPost["visibility"]) {
|
||||
return ALLOWED_VISIBILITIES_FOR_WEBPAGE.includes(visibility)
|
||||
}
|
||||
|
||||
// Code files for uploading with drag and drop and syntax highlighting
|
||||
export const allowedFileTypes = [
|
||||
"application/json",
|
||||
"application/x-javascript",
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import { Gist } from "./types"
|
||||
import * as crypto from "crypto"
|
||||
import type { Post } from "@lib/server/prisma"
|
||||
import { getHtmlFromFile } from "@lib/server/get-html-from-drift-file"
|
||||
import { prisma } from "@lib/server/prisma"
|
||||
import { prisma, ServerPost } from "@lib/server/prisma"
|
||||
|
||||
export type AdditionalPostInformation = Pick<
|
||||
Post,
|
||||
ServerPost,
|
||||
"visibility" | "password" | "expiresAt"
|
||||
> & {
|
||||
userId: string
|
||||
|
@ -14,7 +13,7 @@ export type AdditionalPostInformation = Pick<
|
|||
export async function createPostFromGist(
|
||||
{ userId, visibility, password, expiresAt }: AdditionalPostInformation,
|
||||
gist: Gist
|
||||
): Promise<Post> {
|
||||
): Promise<ServerPost> {
|
||||
const files = Object.values(gist.files)
|
||||
const [title, description] = gist.description.split("\n", 1)
|
||||
|
||||
|
|
|
@ -4,11 +4,20 @@ declare global {
|
|||
}
|
||||
|
||||
import config from "@lib/config"
|
||||
import { Post, PrismaClient, User, Prisma } from "@prisma/client"
|
||||
import {
|
||||
Post as ServerPost,
|
||||
PrismaClient,
|
||||
User as ServerUser,
|
||||
Prisma,
|
||||
File as ServerFile
|
||||
} from "@prisma/client"
|
||||
import * as crypto from "crypto"
|
||||
import { cache } from "react"
|
||||
export type { User, File, Post } from "@prisma/client"
|
||||
|
||||
export type {
|
||||
User as ServerUser,
|
||||
File as ServerFile,
|
||||
Post as ServerPost
|
||||
} from "@prisma/client"
|
||||
export const prisma =
|
||||
global.prisma ||
|
||||
new PrismaClient({
|
||||
|
@ -42,29 +51,91 @@ const postWithFilesAndAuthor = Prisma.validator<Prisma.PostArgs>()({
|
|||
})
|
||||
|
||||
export type ServerPostWithFiles = Prisma.PostGetPayload<typeof postWithFiles>
|
||||
export type PostWithAuthor = Prisma.PostGetPayload<typeof postWithAuthor>
|
||||
export type ServerPostWithAuthor = Prisma.PostGetPayload<typeof postWithAuthor>
|
||||
export type ServerPostWithFilesAndAuthor = Prisma.PostGetPayload<
|
||||
typeof postWithFilesAndAuthor
|
||||
>
|
||||
|
||||
export type PostWithFiles = Omit<ServerPostWithFiles, "files"> & {
|
||||
files: (Omit<ServerPostWithFiles["files"][number], "content" | "html"> & {
|
||||
export type PostWithFiles = Omit<
|
||||
ServerPostWithFiles,
|
||||
"files" | "updatedAt" | "createdAt" | "deletedAt" | "expiresAt"
|
||||
> & {
|
||||
files: (Omit<
|
||||
ServerPostWithFiles["files"][number],
|
||||
"content" | "html" | "updatedAt" | "createdAt" | "deletedAt"
|
||||
> & {
|
||||
content: string
|
||||
html: string
|
||||
updatedAt?: string
|
||||
createdAt: string
|
||||
deletedAt?: string
|
||||
})[]
|
||||
updatedAt?: string
|
||||
createdAt: string
|
||||
deletedAt?: string
|
||||
expiresAt?: string
|
||||
}
|
||||
|
||||
export type PostWithFilesAndAuthor = Omit<
|
||||
ServerPostWithFilesAndAuthor,
|
||||
"files"
|
||||
"files" | "updatedAt" | "createdAt" | "deletedAt" | "expiresAt" | "author"
|
||||
> & {
|
||||
files: (Omit<
|
||||
ServerPostWithFilesAndAuthor["files"][number],
|
||||
"content" | "html"
|
||||
"content" | "html" | "updatedAt" | "createdAt" | "deletedAt"
|
||||
> & {
|
||||
content: string
|
||||
html: string
|
||||
updatedAt?: string
|
||||
createdAt: string
|
||||
deletedAt?: string
|
||||
})[]
|
||||
|
||||
author: Omit<
|
||||
ServerPostWithFilesAndAuthor["author"],
|
||||
"createdAt" | "updatedAt"
|
||||
> & {
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
updatedAt?: string
|
||||
createdAt: string
|
||||
deletedAt?: string
|
||||
expiresAt?: string
|
||||
}
|
||||
|
||||
export function serverPostToClientPost(
|
||||
post: ServerPostWithFiles | ServerPostWithFilesAndAuthor
|
||||
): PostWithFilesAndAuthor | PostWithFiles {
|
||||
let result: PostWithFiles | PostWithFilesAndAuthor = {
|
||||
...post,
|
||||
files: post.files?.map((file) => ({
|
||||
...file,
|
||||
content: file.content?.toString("utf-8"),
|
||||
html: file.html?.toString("utf-8"),
|
||||
updatedAt: file.updatedAt?.toISOString(),
|
||||
createdAt: file.createdAt?.toISOString(),
|
||||
deletedAt: file.deletedAt?.toISOString()
|
||||
})),
|
||||
updatedAt: post.updatedAt?.toISOString(),
|
||||
createdAt: post.createdAt?.toISOString(),
|
||||
deletedAt: post.deletedAt?.toISOString(),
|
||||
expiresAt: post.expiresAt?.toISOString()
|
||||
}
|
||||
|
||||
if ("author" in post && post.author) {
|
||||
result = {
|
||||
...result,
|
||||
author: {
|
||||
...post.author,
|
||||
createdAt: post.author.createdAt?.toISOString(),
|
||||
updatedAt: post.author.updatedAt?.toISOString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export const getFilesForPost = async (postId: string) => {
|
||||
|
@ -87,12 +158,15 @@ export async function getFilesByPost(postId: string) {
|
|||
return files
|
||||
}
|
||||
|
||||
export async function getPostsByUser(userId: string): Promise<Post[]>
|
||||
export async function getPostsByUser(userId: string): Promise<ServerPost[]>
|
||||
export async function getPostsByUser(
|
||||
userId: string,
|
||||
includeFiles: true
|
||||
): Promise<ServerPostWithFiles[]>
|
||||
export async function getPostsByUser(userId: User["id"], withFiles?: boolean) {
|
||||
export async function getPostsByUser(
|
||||
userId: ServerUser["id"],
|
||||
withFiles?: boolean
|
||||
) {
|
||||
const posts = await prisma.post.findMany({
|
||||
where: {
|
||||
authorId: userId
|
||||
|
@ -123,7 +197,7 @@ export async function getPostsByUser(userId: User["id"], withFiles?: boolean) {
|
|||
}
|
||||
|
||||
export const getUserById = async (
|
||||
userId: User["id"],
|
||||
userId: ServerUser["id"],
|
||||
selects?: Prisma.UserFindUniqueArgs["select"]
|
||||
) => {
|
||||
const user = await prisma.user.findUnique({
|
||||
|
@ -143,7 +217,7 @@ export const getUserById = async (
|
|||
return user
|
||||
}
|
||||
|
||||
export const isUserAdmin = async (userId: User["id"]) => {
|
||||
export const isUserAdmin = async (userId: ServerUser["id"]) => {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
id: userId
|
||||
|
@ -185,36 +259,24 @@ type GetPostByIdOptions = Pick<
|
|||
"include" | "rejectOnNotFound" | "select"
|
||||
>
|
||||
|
||||
export const getPostById = async (
|
||||
postId: Post["id"],
|
||||
options?: GetPostByIdOptions
|
||||
): Promise<Post | PostWithFiles | PostWithFilesAndAuthor | null> => {
|
||||
const post = await prisma.post.findUnique({
|
||||
where: {
|
||||
id: postId
|
||||
},
|
||||
...options
|
||||
})
|
||||
export const getPostById = cache(
|
||||
async (postId: ServerPost["id"], options?: GetPostByIdOptions) => {
|
||||
const post = await prisma.post.findUnique({
|
||||
where: {
|
||||
id: postId
|
||||
},
|
||||
...options
|
||||
})
|
||||
|
||||
if (post) {
|
||||
if ("files" in post) {
|
||||
// @ts-expect-error TODO: fix types so files can exist
|
||||
post.files = post.files.map((file) => ({
|
||||
...file,
|
||||
content: file.content ? file.content.toString() : undefined,
|
||||
html: file.html ? file.html.toString() : undefined
|
||||
}))
|
||||
}
|
||||
return post
|
||||
}
|
||||
|
||||
return post
|
||||
}
|
||||
)
|
||||
|
||||
export const getAllPosts = cache(
|
||||
async (
|
||||
options?: Prisma.PostFindManyArgs
|
||||
): Promise<
|
||||
Post[] | ServerPostWithFiles[] | ServerPostWithFilesAndAuthor[]
|
||||
ServerPost[] | ServerPostWithFiles[] | ServerPostWithFilesAndAuthor[]
|
||||
> => {
|
||||
const posts = await prisma.post.findMany(options)
|
||||
return posts
|
||||
|
@ -231,7 +293,7 @@ export type UserWithPosts = Prisma.UserGetPayload<typeof userWithPosts>
|
|||
|
||||
export const getAllUsers = async (
|
||||
options?: Prisma.UserFindManyArgs
|
||||
): Promise<User[] | UserWithPosts[]> => {
|
||||
): Promise<ServerUser[] | UserWithPosts[]> => {
|
||||
const users = (await prisma.user.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
|
@ -242,7 +304,7 @@ export const getAllUsers = async (
|
|||
createdAt: true
|
||||
},
|
||||
...options
|
||||
})) as User[] | UserWithPosts[]
|
||||
})) as ServerUser[] | UserWithPosts[]
|
||||
|
||||
return users
|
||||
}
|
||||
|
@ -252,7 +314,7 @@ export const searchPosts = async (
|
|||
{
|
||||
userId
|
||||
}: {
|
||||
userId?: User["id"]
|
||||
userId?: ServerUser["id"]
|
||||
} = {}
|
||||
): Promise<ServerPostWithFiles[]> => {
|
||||
const posts = await prisma.post.findMany({
|
||||
|
@ -287,7 +349,10 @@ function generateApiToken() {
|
|||
return crypto.randomBytes(32).toString("hex")
|
||||
}
|
||||
|
||||
export const createApiToken = async (userId: User["id"], name: string) => {
|
||||
export const createApiToken = async (
|
||||
userId: ServerUser["id"],
|
||||
name: string
|
||||
) => {
|
||||
const apiToken = await prisma.apiToken.create({
|
||||
data: {
|
||||
token: generateApiToken(),
|
||||
|
@ -301,3 +366,19 @@ export const createApiToken = async (userId: User["id"], name: string) => {
|
|||
|
||||
return apiToken
|
||||
}
|
||||
|
||||
export function getFileById(fileId: ServerFile["id"]) {
|
||||
return prisma.file.findUnique({
|
||||
where: {
|
||||
id: fileId
|
||||
},
|
||||
include: {
|
||||
post: {
|
||||
select: {
|
||||
id: true,
|
||||
visibility: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import { withMethods } from "@lib/api-middleware/with-methods"
|
|||
|
||||
import { prisma } from "@lib/server/prisma"
|
||||
import { NextApiRequest, NextApiResponse } from "next"
|
||||
import { File } from "@lib/server/prisma"
|
||||
import { ServerFile } from "@lib/server/prisma"
|
||||
import * as crypto from "crypto"
|
||||
import { getHtmlFromFile } from "@lib/server/get-html-from-drift-file"
|
||||
import { verifyApiUser } from "@lib/server/verify-api-user"
|
||||
|
@ -14,7 +14,7 @@ async function handlePost(req: NextApiRequest, res: NextApiResponse<unknown>) {
|
|||
return res.status(401).json({ error: "Unauthorized" })
|
||||
}
|
||||
|
||||
const files = req.body.files as (Omit<File, "content" | "html"> & {
|
||||
const files = req.body.files as (Omit<ServerFile, "content" | "html"> & {
|
||||
content: string
|
||||
html: string
|
||||
})[]
|
||||
|
|
Loading…
Reference in a new issue