dep bumps, add pages feature, bug fixes, type improvements

This commit is contained in:
Max Leiter 2023-02-23 20:35:25 -08:00
parent e21d896669
commit fec58f2465
96 changed files with 645 additions and 384 deletions

View file

@ -3,6 +3,7 @@ module.exports = {
preset: "ts-jest", preset: "ts-jest",
testEnvironment: "node", testEnvironment: "node",
setupFiles: ["<rootDir>/test/setup-tests.ts"], setupFiles: ["<rootDir>/test/setup-tests.ts"],
// TODO: update to app dir
moduleNameMapper: { moduleNameMapper: {
"@lib/(.*)": "<rootDir>/src/lib/$1", "@lib/(.*)": "<rootDir>/src/lib/$1",
"@routes/(.*)": "<rootDir>/src/routes/$1" "@routes/(.*)": "<rootDir>/src/routes/$1"

View file

@ -26,6 +26,9 @@ const nextConfig = {
}, },
env: { env: {
NEXT_PUBLIC_DRIFT_URL: process.env.DRIFT_URL NEXT_PUBLIC_DRIFT_URL: process.env.DRIFT_URL
},
typescript: {
// ignoreBuildErrors: true,
} }
} }

View file

@ -14,8 +14,8 @@
}, },
"dependencies": { "dependencies": {
"@next-auth/prisma-adapter": "^1.0.5", "@next-auth/prisma-adapter": "^1.0.5",
"@next/eslint-plugin-next": "13.1.7-canary.23", "@next/eslint-plugin-next": "13.2.1-canary.0",
"@next/font": "13.1.7-canary.23", "@next/font": "13.2.1-canary.0",
"@prisma/client": "^4.9.0", "@prisma/client": "^4.9.0",
"@radix-ui/react-dialog": "^1.0.2", "@radix-ui/react-dialog": "^1.0.2",
"@radix-ui/react-dropdown-menu": "^2.0.2", "@radix-ui/react-dropdown-menu": "^2.0.2",
@ -28,7 +28,7 @@
"client-zip": "2.3.0", "client-zip": "2.3.0",
"jest": "^29.4.1", "jest": "^29.4.1",
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8",
"next": "13.1.7-canary.23", "next": "13.2.1-canary.0",
"next-auth": "^4.19.2", "next-auth": "^4.19.2",
"next-themes": "^0.2.1", "next-themes": "^0.2.1",
"react": "18.2.0", "react": "18.2.0",
@ -45,7 +45,7 @@
"uuid": "^9.0.0" "uuid": "^9.0.0"
}, },
"devDependencies": { "devDependencies": {
"@next/bundle-analyzer": "13.1.6", "@next/bundle-analyzer": "13.1.7-canary.26",
"@types/bcrypt": "^5.0.0", "@types/bcrypt": "^5.0.0",
"@types/lodash.debounce": "^4.0.7", "@types/lodash.debounce": "^4.0.7",
"@types/node": "18.11.18", "@types/node": "18.11.18",
@ -59,7 +59,7 @@
"cross-env": "7.0.3", "cross-env": "7.0.3",
"csstype": "^3.1.1", "csstype": "^3.1.1",
"eslint": "8.33.0", "eslint": "8.33.0",
"eslint-config-next": "13.1.6", "eslint-config-next": "13.1.7-canary.26",
"next-unused": "0.0.6", "next-unused": "0.0.6",
"prettier": "2.8.3", "prettier": "2.8.3",
"prisma": "^4.9.0", "prisma": "^4.9.0",

View file

@ -2,9 +2,9 @@ lockfileVersion: 5.4
specifiers: specifiers:
'@next-auth/prisma-adapter': ^1.0.5 '@next-auth/prisma-adapter': ^1.0.5
'@next/bundle-analyzer': 13.1.6 '@next/bundle-analyzer': 13.1.7-canary.26
'@next/eslint-plugin-next': 13.1.7-canary.23 '@next/eslint-plugin-next': 13.2.1-canary.0
'@next/font': 13.1.7-canary.23 '@next/font': 13.2.1-canary.0
'@prisma/client': ^4.9.0 '@prisma/client': ^4.9.0
'@radix-ui/react-dialog': ^1.0.2 '@radix-ui/react-dialog': ^1.0.2
'@radix-ui/react-dropdown-menu': ^2.0.2 '@radix-ui/react-dropdown-menu': ^2.0.2
@ -28,10 +28,10 @@ specifiers:
cross-env: 7.0.3 cross-env: 7.0.3
csstype: ^3.1.1 csstype: ^3.1.1
eslint: 8.33.0 eslint: 8.33.0
eslint-config-next: 13.1.6 eslint-config-next: 13.1.7-canary.26
jest: ^29.4.1 jest: ^29.4.1
lodash.debounce: ^4.0.8 lodash.debounce: ^4.0.8
next: 13.1.7-canary.23 next: 13.2.1-canary.0
next-auth: ^4.19.2 next-auth: ^4.19.2
next-themes: ^0.2.1 next-themes: ^0.2.1
next-unused: 0.0.6 next-unused: 0.0.6
@ -55,8 +55,8 @@ specifiers:
dependencies: dependencies:
'@next-auth/prisma-adapter': 1.0.5_77bhi65b6v5jbrzr36rs2ojwe4 '@next-auth/prisma-adapter': 1.0.5_77bhi65b6v5jbrzr36rs2ojwe4
'@next/eslint-plugin-next': 13.1.7-canary.23 '@next/eslint-plugin-next': 13.2.1-canary.0
'@next/font': 13.1.7-canary.23 '@next/font': 13.2.1-canary.0
'@prisma/client': 4.9.0_prisma@4.9.0 '@prisma/client': 4.9.0_prisma@4.9.0
'@radix-ui/react-dialog': 1.0.2_5ndqzdd6t4rivxsukjv3i3ak2q '@radix-ui/react-dialog': 1.0.2_5ndqzdd6t4rivxsukjv3i3ak2q
'@radix-ui/react-dropdown-menu': 2.0.2_5ndqzdd6t4rivxsukjv3i3ak2q '@radix-ui/react-dropdown-menu': 2.0.2_5ndqzdd6t4rivxsukjv3i3ak2q
@ -69,9 +69,9 @@ dependencies:
client-zip: 2.3.0 client-zip: 2.3.0
jest: 29.4.1_@types+node@18.11.18 jest: 29.4.1_@types+node@18.11.18
lodash.debounce: 4.0.8 lodash.debounce: 4.0.8
next: 13.1.7-canary.23_biqbaboplfbrettd7655fr4n2y next: 13.2.1-canary.0_biqbaboplfbrettd7655fr4n2y
next-auth: 4.19.2_de7upave4ddivcfp3gf7tpswk4 next-auth: 4.19.2_avkzbs57los6fogzead7rr4u74
next-themes: 0.2.1_de7upave4ddivcfp3gf7tpswk4 next-themes: 0.2.1_avkzbs57los6fogzead7rr4u74
react: 18.2.0 react: 18.2.0
react-datepicker: 4.8.0_biqbaboplfbrettd7655fr4n2y react-datepicker: 4.8.0_biqbaboplfbrettd7655fr4n2y
react-dom: 18.2.0_react@18.2.0 react-dom: 18.2.0_react@18.2.0
@ -89,7 +89,7 @@ optionalDependencies:
sharp: 0.31.3 sharp: 0.31.3
devDependencies: devDependencies:
'@next/bundle-analyzer': 13.1.6 '@next/bundle-analyzer': 13.1.7-canary.26
'@types/bcrypt': 5.0.0 '@types/bcrypt': 5.0.0
'@types/lodash.debounce': 4.0.7 '@types/lodash.debounce': 4.0.7
'@types/node': 18.11.18 '@types/node': 18.11.18
@ -103,7 +103,7 @@ devDependencies:
cross-env: 7.0.3 cross-env: 7.0.3
csstype: 3.1.1 csstype: 3.1.1
eslint: 8.33.0 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 next-unused: 0.0.6
prettier: 2.8.3 prettier: 2.8.3
prisma: 4.9.0 prisma: 4.9.0
@ -819,11 +819,11 @@ packages:
next-auth: ^4 next-auth: ^4
dependencies: dependencies:
'@prisma/client': 4.9.0_prisma@4.9.0 '@prisma/client': 4.9.0_prisma@4.9.0
next-auth: 4.19.2_de7upave4ddivcfp3gf7tpswk4 next-auth: 4.19.2_avkzbs57los6fogzead7rr4u74
dev: false dev: false
/@next/bundle-analyzer/13.1.6: /@next/bundle-analyzer/13.1.7-canary.26:
resolution: {integrity: sha512-rJS9CtLoGT58mL+v2ISKANosFFWP/0YKYByHQ3vTaZrbQP8b1rYRxd2QVMJmnSXaFkiP9URt1XJ6OdGyVq5b6g==} resolution: {integrity: sha512-oRAFBr3qEsOC8cwnTWjodckrx1df1pKDSIumvvj9xcp4QeymwDpFdjz2Dp3Manh1Q/kWX+adqNu6fyJUnVDI4A==}
dependencies: dependencies:
webpack-bundle-analyzer: 4.7.0 webpack-bundle-analyzer: 4.7.0
transitivePeerDependencies: transitivePeerDependencies:
@ -831,28 +831,28 @@ packages:
- utf-8-validate - utf-8-validate
dev: true dev: true
/@next/env/13.1.7-canary.23: /@next/env/13.2.1-canary.0:
resolution: {integrity: sha512-7AHpOQHk0xQlLVFjvIaIkAhIsQ5QbIkZRySRigZBWJJ8ycyf6clZe8ileK/sbKdRDKT8O0YeuxtD/kOO8cRMXQ==} resolution: {integrity: sha512-6e5sszeQUWRFZpy/HXVWs18DKbjErcF/pMKS8pIsknejN1DQPRxNFEI+QuXIf76N+r87K1qR/qWQx4WKQof8Rw==}
dev: false dev: false
/@next/eslint-plugin-next/13.1.6: /@next/eslint-plugin-next/13.1.7-canary.26:
resolution: {integrity: sha512-o7cauUYsXjzSJkay8wKjpKJf2uLzlggCsGUkPu3lP09Pv97jYlekTC20KJrjQKmSv5DXV0R/uks2ZXhqjNkqAw==} resolution: {integrity: sha512-0Cs2jSO+4vmyiWPL3ryMRGIl2+UZ+rV9TFaxzGLZUfN2yQKodR2Ilt3CWmRBFNRJr6MV2N+BIdIQ5MPy/TBeRg==}
dependencies: dependencies:
glob: 7.1.7 glob: 7.1.7
dev: true dev: true
/@next/eslint-plugin-next/13.1.7-canary.23: /@next/eslint-plugin-next/13.2.1-canary.0:
resolution: {integrity: sha512-8Se+LIQ4iOKxelhq0Va14PXkOm2UFgPQPJJqmN8GojiiY3gVKB2WJHsqDDu6o9dPNt7OgemLtQePcJH2barw5Q==} resolution: {integrity: sha512-OwJgZrTGj/WovColJHOUd2RB+2uju12cYL2oL5CnyNTz999GJ/z/esghfCAqL5fh4YaiQqlZCRJEbbymTsiU0Q==}
dependencies: dependencies:
glob: 7.1.7 glob: 7.1.7
dev: false dev: false
/@next/font/13.1.7-canary.23: /@next/font/13.2.1-canary.0:
resolution: {integrity: sha512-y5pZjah3b5n2X1yMMAImPwAMi2hFY6Maip76R6+VzLjak+pNMB5jF72ieZoRsibGxt+efCoWOWIkh5s7ZO8YrA==} resolution: {integrity: sha512-ByXy8uwtXMihrIKmkcDZxvLgfE5i5Y0kowcg+vATw+b7ukzUzlesI+65VWTo2AHVdtrBPGAdAjyJJTNvr/q4nQ==}
dev: false dev: false
/@next/swc-android-arm-eabi/13.1.7-canary.23: /@next/swc-android-arm-eabi/13.2.1-canary.0:
resolution: {integrity: sha512-LxaoUF1bqi0hPrOci7RChuOg4oJA2IHiC6SviAlSTzWUgi3eJuigvfptTylO1ElBX4MMSJ5mqpq1h5fNQIg5/A==} resolution: {integrity: sha512-irsC3EdtmroNaHrKeqZDJTcQ15wrTWp5JJ8IIMJ6GvW/vhVENk+unnVoxD4BQLmgelCbzi7udusISDCkUUWCkg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm] cpu: [arm]
os: [android] os: [android]
@ -860,8 +860,8 @@ packages:
dev: false dev: false
optional: true optional: true
/@next/swc-android-arm64/13.1.7-canary.23: /@next/swc-android-arm64/13.2.1-canary.0:
resolution: {integrity: sha512-gsJyRGzJlBLZNNfpPwn1T1MgeT3p/Ztx3lMuc37+v+sMjgegWZy7h5HbtTp7IT+y4DAwknNcSyLIidH88qj9lA==} resolution: {integrity: sha512-3AyZ4THkuamCyOhFkrqFY38IMI+YFmxomOcFHK+67hrqfHQRkH0boL15SnHAzURlAeruI/Mw9/PuYshYXn3Usg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [android] os: [android]
@ -869,8 +869,8 @@ packages:
dev: false dev: false
optional: true optional: true
/@next/swc-darwin-arm64/13.1.7-canary.23: /@next/swc-darwin-arm64/13.2.1-canary.0:
resolution: {integrity: sha512-29HDi1EQkNaiNU0yJDikIV4jAJvs52TeGqTDf9tIkHFOAvmAF50mGbaL2urcRhy1fFvubLEx3JHWV6pAobjq3g==} resolution: {integrity: sha512-CN5e/C9WLGa8EdAmnndGzKh14ALfMHwjoRZYHBfFmM1Z+SArz89VrXeOmAMVgpcANBNC4ShvXp4rIUD9PveDQA==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
@ -878,8 +878,8 @@ packages:
dev: false dev: false
optional: true optional: true
/@next/swc-darwin-x64/13.1.7-canary.23: /@next/swc-darwin-x64/13.2.1-canary.0:
resolution: {integrity: sha512-iWU/5+myA89I25AtLHeZP03jqUh9OfUF67oQ1ZiDNcKZAEDGqYgwqVeiUbU6rBhX483YwZOAF+iUpYefV9FLQQ==} resolution: {integrity: sha512-eP7hKzI84MrG54M3207pynGmJHqf2V6Ex0CXpht0WSMrrv7G6W9iHyLPqk18AgxzgjA5QH4lDP+FpTaoDmpdkg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
@ -887,8 +887,8 @@ packages:
dev: false dev: false
optional: true optional: true
/@next/swc-freebsd-x64/13.1.7-canary.23: /@next/swc-freebsd-x64/13.2.1-canary.0:
resolution: {integrity: sha512-IuJljewmj7BNJI9qsLXzJZBC1qU7adif0Pcg2ww5Al6l8RviGy6aBWdMgt65tHTGlUqxHhLg7mW5jT2bUw1Bcw==} resolution: {integrity: sha512-rloppA9VtxDFruDxMQJqrISwKzOip9/1hksljlXTBg+/fu2dJyufuARwhfksDNH3fBT0fQC10esFO+WjhLEp7g==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [freebsd] os: [freebsd]
@ -896,8 +896,8 @@ packages:
dev: false dev: false
optional: true optional: true
/@next/swc-linux-arm-gnueabihf/13.1.7-canary.23: /@next/swc-linux-arm-gnueabihf/13.2.1-canary.0:
resolution: {integrity: sha512-d1I0WY/L4RW4pQcDihWjMo+s90hv9c83lzDi1fsjhzKMJnHKyOcZKc2tXB6OvHW67y6PL6bgpPcqysmS4txwjA==} resolution: {integrity: sha512-Fb0d7znHY3RlaJFCc07X+x3NGQw3Sp0UOsCOtalG2LSgMKFTt0dMc47WBZez+ratWI7dc6B3H8kmsgntBd4Skw==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
@ -905,8 +905,8 @@ packages:
dev: false dev: false
optional: true optional: true
/@next/swc-linux-arm64-gnu/13.1.7-canary.23: /@next/swc-linux-arm64-gnu/13.2.1-canary.0:
resolution: {integrity: sha512-zMaepi3ZZvPserWz34ZU9JKmZPnUn/1HKEe6oNuxkHSDIqR6RM2DcUIj5mwsUdQzCAHJs8ZbLpoAtiOj7x+Ipg==} resolution: {integrity: sha512-ATkCdBfHpTtB+7VWgsib2O98sUAa9vyB7uyHTFDWPMB9qTnTXMUfPoYxFJmYXrjwB/xoW97AKrB3FGxMM+swHw==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
@ -914,8 +914,8 @@ packages:
dev: false dev: false
optional: true optional: true
/@next/swc-linux-arm64-musl/13.1.7-canary.23: /@next/swc-linux-arm64-musl/13.2.1-canary.0:
resolution: {integrity: sha512-4bMTuoCQ5cf+6O7tIP//I4FF5o4crfRq7DdUTXoZvlF81xjlnI+gCRiHU8AllUryRhci+r4kM9/2m3GQkGQdqA==} resolution: {integrity: sha512-2x2i0dgcyhE9wycHYLAZiiHkQlzambV0qdjsG7DqQhc23UhVvkqG6dB9PHPFrkxMFPq2QDLpYGlKF9sdnnEQbw==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
@ -923,8 +923,8 @@ packages:
dev: false dev: false
optional: true optional: true
/@next/swc-linux-x64-gnu/13.1.7-canary.23: /@next/swc-linux-x64-gnu/13.2.1-canary.0:
resolution: {integrity: sha512-plovQsBsXdJUaQYx7aQHiUlYtxgawlp+1a9VX6CPJPM15HR91KmJsmdfI+JW1fVbV4x+N0TDARzdGSHKQNLAHQ==} resolution: {integrity: sha512-nRQfotbWl4V9sIS+nx4ZXtJxe8/+naeUrJSoD+Wi9PrIyF3uuLE+xi1WMHS5RhGydyduvFSyIjQO2qGzZ0Lhsw==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
@ -932,8 +932,8 @@ packages:
dev: false dev: false
optional: true optional: true
/@next/swc-linux-x64-musl/13.1.7-canary.23: /@next/swc-linux-x64-musl/13.2.1-canary.0:
resolution: {integrity: sha512-0b9BiJ2eOFypCa41gEhURxD9borWGfXBb3AnhxwmUnMAanhZZSVutBCGjxqvki3qbTPCMUv6dVUaKXw+26ylPg==} resolution: {integrity: sha512-hdM2Cds8yppGSzql2A4tCYdzjVheDh/Ei45GOz94gshO95u7SSS9UQNTaOKZTWOUfRnZYZrEbAVMhDxz10qqdQ==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
@ -941,8 +941,8 @@ packages:
dev: false dev: false
optional: true optional: true
/@next/swc-win32-arm64-msvc/13.1.7-canary.23: /@next/swc-win32-arm64-msvc/13.2.1-canary.0:
resolution: {integrity: sha512-psVpG4pmJHrqjXMfKcXyRNO5t18tpee4pZAuTesu8DfRd+fPqg1CwB1Hp8qBxG9c80loCoQMlKFiH5nPFAvv+g==} resolution: {integrity: sha512-2SkgCWZbDLBgKbgJ+slVpqQLNwOzp2/ja2C8nNzwUbDB4cbD9fTyWrUngZBgg8J1OaP5KR7qHOQ8JQoHqiTOjA==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [win32] os: [win32]
@ -950,8 +950,8 @@ packages:
dev: false dev: false
optional: true optional: true
/@next/swc-win32-ia32-msvc/13.1.7-canary.23: /@next/swc-win32-ia32-msvc/13.2.1-canary.0:
resolution: {integrity: sha512-5ZW5+QoI9yibjUPcHfh/uBjB5ubcsKoD45LNbxsxWGOl9c/tXp4zvhqIEtZ57SWajdZZiuKZ0I92MGxgiDwYfw==} resolution: {integrity: sha512-1j1BJ2wtzDnbn6uOUPsPhOgjIWESr8TE7aLUAlxtTYgsrYdPYxe4uowa9M04mEw6UxZHatrrOA6zvTVoFUtB8g==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [ia32] cpu: [ia32]
os: [win32] os: [win32]
@ -959,8 +959,8 @@ packages:
dev: false dev: false
optional: true optional: true
/@next/swc-win32-x64-msvc/13.1.7-canary.23: /@next/swc-win32-x64-msvc/13.2.1-canary.0:
resolution: {integrity: sha512-+90NgEZmVqLtQ+adKtZxz67XJvjF/gESaSH8Zt96ccNH9lzoZsSyX/SYHwNJQFikiBCNn0SMeAZHIlsY5dEbqQ==} resolution: {integrity: sha512-87QjJxWWYIPKBx/FoaqEd2bxPFBdTUZ6OdEnCeuBW2XjN6qZpwL1H2dIEvh6EXJMjQYW/DAhOqgr9cvpw9fu+Q==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
@ -1955,7 +1955,7 @@ packages:
dependencies: dependencies:
'@types/react': 18.0.27 '@types/react': 18.0.27
react: 18.2.0 react: 18.2.0
tslib: 2.4.1 tslib: 2.5.0
dev: false dev: false
/aria-hidden/1.2.2_3stiutgnnbnfnf3uowm5cip22i: /aria-hidden/1.2.2_3stiutgnnbnfnf3uowm5cip22i:
@ -2970,8 +2970,8 @@ packages:
source-map: 0.6.1 source-map: 0.6.1
dev: true dev: true
/eslint-config-next/13.1.6_zkdaqh7it7uc4cvz2haft7rc6u: /eslint-config-next/13.1.7-canary.26_zkdaqh7it7uc4cvz2haft7rc6u:
resolution: {integrity: sha512-0cg7h5wztg/SoLAlxljZ0ZPUQ7i6QKqRiP4M2+MgTZtxWwNKb2JSwNc18nJ6/kXBI6xYvPraTbQSIhAuVw6czw==} resolution: {integrity: sha512-pKLrCNiKO4uekzZJaIXthsasvlEzSiGQLsO2I2FPr+sQAjY18UhY3jl2HZznU/vbk5GHVRKs5ibrye/29KDtbQ==}
peerDependencies: peerDependencies:
eslint: ^7.23.0 || ^8.0.0 eslint: ^7.23.0 || ^8.0.0
typescript: '>=3.3.1' typescript: '>=3.3.1'
@ -2979,7 +2979,7 @@ packages:
typescript: typescript:
optional: true optional: true
dependencies: dependencies:
'@next/eslint-plugin-next': 13.1.6 '@next/eslint-plugin-next': 13.1.7-canary.26
'@rushstack/eslint-patch': 1.2.0 '@rushstack/eslint-patch': 1.2.0
'@typescript-eslint/parser': 5.49.0_zkdaqh7it7uc4cvz2haft7rc6u '@typescript-eslint/parser': 5.49.0_zkdaqh7it7uc4cvz2haft7rc6u
eslint: 8.33.0 eslint: 8.33.0
@ -3367,7 +3367,7 @@ packages:
resolution: {integrity: sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==} resolution: {integrity: sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==}
engines: {node: '>= 12'} engines: {node: '>= 12'}
dependencies: dependencies:
tslib: 2.4.1 tslib: 2.5.0
dev: false dev: false
/filing-cabinet/3.3.0: /filing-cabinet/3.3.0:
@ -5473,7 +5473,7 @@ packages:
dev: true dev: true
optional: true optional: true
/next-auth/4.19.2_de7upave4ddivcfp3gf7tpswk4: /next-auth/4.19.2_avkzbs57los6fogzead7rr4u74:
resolution: {integrity: sha512-6V2YG3IJQVhgCAH7mvT3yopTW92gMdUrcwGX7NQ0dCreT/+axGua/JmVdarjec0C/oJukKpIYRgjMlV+L5ZQOQ==} resolution: {integrity: sha512-6V2YG3IJQVhgCAH7mvT3yopTW92gMdUrcwGX7NQ0dCreT/+axGua/JmVdarjec0C/oJukKpIYRgjMlV+L5ZQOQ==}
peerDependencies: peerDependencies:
next: ^12.2.5 || ^13 next: ^12.2.5 || ^13
@ -5488,7 +5488,7 @@ packages:
'@panva/hkdf': 1.0.2 '@panva/hkdf': 1.0.2
cookie: 0.5.0 cookie: 0.5.0
jose: 4.11.0 jose: 4.11.0
next: 13.1.7-canary.23_biqbaboplfbrettd7655fr4n2y next: 13.2.1-canary.0_biqbaboplfbrettd7655fr4n2y
oauth: 0.9.15 oauth: 0.9.15
openid-client: 5.3.0 openid-client: 5.3.0
preact: 10.11.2 preact: 10.11.2
@ -5498,14 +5498,14 @@ packages:
uuid: 8.3.2 uuid: 8.3.2
dev: false dev: false
/next-themes/0.2.1_de7upave4ddivcfp3gf7tpswk4: /next-themes/0.2.1_avkzbs57los6fogzead7rr4u74:
resolution: {integrity: sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==} resolution: {integrity: sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==}
peerDependencies: peerDependencies:
next: '*' next: '*'
react: '*' react: '*'
react-dom: '*' react-dom: '*'
dependencies: dependencies:
next: 13.1.7-canary.23_biqbaboplfbrettd7655fr4n2y next: 13.2.1-canary.0_biqbaboplfbrettd7655fr4n2y
react: 18.2.0 react: 18.2.0
react-dom: 18.2.0_react@18.2.0 react-dom: 18.2.0_react@18.2.0
dev: false dev: false
@ -5521,17 +5521,20 @@ packages:
- supports-color - supports-color
dev: true dev: true
/next/13.1.7-canary.23_biqbaboplfbrettd7655fr4n2y: /next/13.2.1-canary.0_biqbaboplfbrettd7655fr4n2y:
resolution: {integrity: sha512-kUgSfsX+f09h8ULeqitJmUff6RPkhZq2xA2X6hSnFMrZEAjxeuAiGDdK6dZXNiuuQDHTXnZOvovupq05pf2z2w==} resolution: {integrity: sha512-4dXz9jinbUmxOCnXxDPxcFHmwhR+YDCLRP0kiYUpu1GiTlTYbfHAb0HosmA58ZL76W1tmqS6x8/3qkiDNtZWrg==}
engines: {node: '>=14.6.0'} engines: {node: '>=14.6.0'}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
'@opentelemetry/api': ^1.4.0
fibers: '>= 3.1.0' fibers: '>= 3.1.0'
node-sass: ^6.0.0 || ^7.0.0 node-sass: ^6.0.0 || ^7.0.0
react: ^18.2.0 react: ^18.2.0
react-dom: ^18.2.0 react-dom: ^18.2.0
sass: ^1.3.0 sass: ^1.3.0
peerDependenciesMeta: peerDependenciesMeta:
'@opentelemetry/api':
optional: true
fibers: fibers:
optional: true optional: true
node-sass: node-sass:
@ -5539,7 +5542,7 @@ packages:
sass: sass:
optional: true optional: true
dependencies: dependencies:
'@next/env': 13.1.7-canary.23 '@next/env': 13.2.1-canary.0
'@swc/helpers': 0.4.14 '@swc/helpers': 0.4.14
caniuse-lite: 1.0.30001449 caniuse-lite: 1.0.30001449
postcss: 8.4.14 postcss: 8.4.14
@ -5547,19 +5550,19 @@ packages:
react-dom: 18.2.0_react@18.2.0 react-dom: 18.2.0_react@18.2.0
styled-jsx: 5.1.1_react@18.2.0 styled-jsx: 5.1.1_react@18.2.0
optionalDependencies: optionalDependencies:
'@next/swc-android-arm-eabi': 13.1.7-canary.23 '@next/swc-android-arm-eabi': 13.2.1-canary.0
'@next/swc-android-arm64': 13.1.7-canary.23 '@next/swc-android-arm64': 13.2.1-canary.0
'@next/swc-darwin-arm64': 13.1.7-canary.23 '@next/swc-darwin-arm64': 13.2.1-canary.0
'@next/swc-darwin-x64': 13.1.7-canary.23 '@next/swc-darwin-x64': 13.2.1-canary.0
'@next/swc-freebsd-x64': 13.1.7-canary.23 '@next/swc-freebsd-x64': 13.2.1-canary.0
'@next/swc-linux-arm-gnueabihf': 13.1.7-canary.23 '@next/swc-linux-arm-gnueabihf': 13.2.1-canary.0
'@next/swc-linux-arm64-gnu': 13.1.7-canary.23 '@next/swc-linux-arm64-gnu': 13.2.1-canary.0
'@next/swc-linux-arm64-musl': 13.1.7-canary.23 '@next/swc-linux-arm64-musl': 13.2.1-canary.0
'@next/swc-linux-x64-gnu': 13.1.7-canary.23 '@next/swc-linux-x64-gnu': 13.2.1-canary.0
'@next/swc-linux-x64-musl': 13.1.7-canary.23 '@next/swc-linux-x64-musl': 13.2.1-canary.0
'@next/swc-win32-arm64-msvc': 13.1.7-canary.23 '@next/swc-win32-arm64-msvc': 13.2.1-canary.0
'@next/swc-win32-ia32-msvc': 13.1.7-canary.23 '@next/swc-win32-ia32-msvc': 13.2.1-canary.0
'@next/swc-win32-x64-msvc': 13.1.7-canary.23 '@next/swc-win32-x64-msvc': 13.2.1-canary.0
transitivePeerDependencies: transitivePeerDependencies:
- '@babel/core' - '@babel/core'
- babel-plugin-macros - babel-plugin-macros
@ -6288,7 +6291,7 @@ packages:
react: 18.2.0 react: 18.2.0
react-remove-scroll-bar: 2.3.4_3stiutgnnbnfnf3uowm5cip22i react-remove-scroll-bar: 2.3.4_3stiutgnnbnfnf3uowm5cip22i
react-style-singleton: 2.2.1_3stiutgnnbnfnf3uowm5cip22i react-style-singleton: 2.2.1_3stiutgnnbnfnf3uowm5cip22i
tslib: 2.4.1 tslib: 2.5.0
use-callback-ref: 1.3.0_3stiutgnnbnfnf3uowm5cip22i use-callback-ref: 1.3.0_3stiutgnnbnfnf3uowm5cip22i
use-sidecar: 1.1.2_3stiutgnnbnfnf3uowm5cip22i use-sidecar: 1.1.2_3stiutgnnbnfnf3uowm5cip22i
dev: false dev: false
@ -7143,10 +7146,6 @@ packages:
resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
dev: true dev: true
/tslib/2.4.1:
resolution: {integrity: sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==}
dev: false
/tslib/2.5.0: /tslib/2.5.0:
resolution: {integrity: sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==} resolution: {integrity: sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==}

View file

@ -2,7 +2,7 @@
import { startTransition, Suspense, useState } from "react" import { startTransition, Suspense, useState } from "react"
import styles from "./auth.module.css" import styles from "./auth.module.css"
import Link from "../../components/link" import Link from "../../../components/link"
import { signIn } from "next-auth/react" import { signIn } from "next-auth/react"
import Input from "@components/input" import Input from "@components/input"
import Button from "@components/button" import Button from "@components/button"

View file

@ -9,7 +9,7 @@ export function ErrorQueryParamsHandler() {
const { setToast } = useToasts() const { setToast } = useToasts()
useEffect(() => { useEffect(() => {
if (queryParams.get("error")) { if (queryParams?.get("error")) {
setToast({ setToast({
message: queryParams.get("error") as string, message: queryParams.get("error") as string,
type: "error" type: "error"

View file

@ -59,7 +59,10 @@ function FileDropdown({
return ( return (
<Popover> <Popover>
<Popover.Trigger className={buttonStyles.button}> <Popover.Trigger
className={buttonStyles.button}
style={{ height: 40, padding: 10 }}
>
<div <div
className={clsx(buttonStyles.icon, styles.chevron)} className={clsx(buttonStyles.icon, styles.chevron)}
style={{ marginRight: 6 }} style={{ marginRight: 6 }}

View file

@ -1,7 +1,7 @@
"use client" "use client"
import * as RadixTabs from "@radix-ui/react-tabs" 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 { ChangeEvent, ClipboardEvent, useRef } from "react"
import TextareaMarkdown, { TextareaMarkdownRef } from "textarea-markdown-editor" import TextareaMarkdown, { TextareaMarkdownRef } from "textarea-markdown-editor"
import Preview, { StaticPreview } from "../preview" import Preview, { StaticPreview } from "../preview"

View file

@ -17,7 +17,7 @@ function Description({ onChange, description }: props) {
label="Description" label="Description"
maxLength={256} maxLength={256}
width="100%" width="100%"
placeholder="An optional description of your post" placeholder="An optional description"
/> />
</div> </div>
) )

View file

@ -2,7 +2,7 @@ import { ChangeEvent, ClipboardEvent, useCallback } from "react"
import styles from "./document.module.css" import styles from "./document.module.css"
import Button from "@components/button" import Button from "@components/button"
import Input from "@components/input" 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" import { Trash } from "react-feather"
type Props = { type Props = {

View file

@ -6,10 +6,10 @@ import generateUUID from "@lib/generate-uuid"
import styles from "./post.module.css" import styles from "./post.module.css"
import EditDocumentList from "./edit-document-list" import EditDocumentList from "./edit-document-list"
import { ChangeEvent } from "react" 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 Description from "./description"
import { PostWithFiles } from "@lib/server/prisma" import { PostWithFiles } from "@lib/server/prisma"
import PasswordModal from "../../../components/password-modal" import PasswordModal from "../../../../components/password-modal"
import Title from "./title" import Title from "./title"
import FileDropzone from "./drag-and-drop" import FileDropzone from "./drag-and-drop"
import Button from "@components/button" import Button from "@components/button"
@ -35,16 +35,14 @@ export type Document = {
} }
function Post({ function Post({
initialPost: stringifiedInitialPost, initialPost,
newPostParent newPostParent
}: { }: {
initialPost?: string initialPost?: PostWithFiles
newPostParent?: string newPostParent?: string
}): JSX.Element | null { }): JSX.Element | null {
const { isAuthenticated } = useSessionSWR() const { isAuthenticated } = useSessionSWR()
const parsedPost = JSON.parse(stringifiedInitialPost || "{}") as PostWithFiles
const initialPost = parsedPost?.id ? parsedPost : null
const { setToast } = useToasts() const { setToast } = useToasts()
const router = useRouter() const router = useRouter()
const [title, setTitle] = useState( const [title, setTitle] = useState(

View file

@ -1,6 +1,10 @@
import NewPost from "../../components/new" import NewPost from "../../components/new"
import { notFound, redirect } from "next/navigation" 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" import { getSession } from "@lib/server/session"
async function NewFromExisting({ async function NewFromExisting({
@ -21,7 +25,7 @@ async function NewFromExisting({
return notFound() return notFound()
} }
const post = await getPostById(id, { const post = (await getPostById(id, {
select: { select: {
authorId: true, authorId: true,
title: 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 export default NewFromExisting

View file

@ -0,0 +1,3 @@
export default function NewLayout({ children }: { children: React.ReactNode }) {
return <>{children}</>
}

View file

@ -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" import "./react-datepicker.css"
export default function New() { export default function New() {

View file

@ -2,28 +2,21 @@
import Button from "@components/button" import Button from "@components/button"
import ButtonGroup from "@components/button-group" 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 { Edit, ArrowUpCircle, Archive } from "react-feather"
import styles from "./post-buttons.module.css" import styles from "./post-buttons.module.css"
import { useRouter } from "next/navigation" import { useRouter } from "next/navigation"
import { PostWithFiles } from "@lib/server/prisma" import { PostWithFiles } from "@lib/server/prisma"
export const PostButtons = ({ export const PostButtons = ({
title, post,
files, loading
loading,
postId,
parentId
}: { }: {
title: string post?: PostWithFiles
files?: Pick<PostWithFiles, "files">["files"]
loading?: boolean loading?: boolean
postId?: string
parentId?: string
visibility?: string
authorId?: string
}) => { }) => {
const router = useRouter() const router = useRouter()
const { files, id: postId, parentId, title } = post || {}
const downloadClick = async () => { const downloadClick = async () => {
if (!files?.length) return if (!files?.length) return
const downloadZip = (await import("client-zip")).downloadZip const downloadZip = (await import("client-zip")).downloadZip

View file

@ -2,27 +2,19 @@ import CreatedAgoBadge from "@components/badges/created-ago-badge"
import ExpirationBadge from "@components/badges/expiration-badge" import ExpirationBadge from "@components/badges/expiration-badge"
import VisibilityBadge from "@components/badges/visibility-badge" import VisibilityBadge from "@components/badges/visibility-badge"
import Skeleton from "@components/skeleton" import Skeleton from "@components/skeleton"
import { PostWithFilesAndAuthor } from "@lib/server/prisma"
import styles from "./title.module.css" import styles from "./title.module.css"
type TitleProps = { type TitleProps = {
title: string
loading?: boolean loading?: boolean
displayName?: string post?: PostWithFilesAndAuthor
visibility?: string
createdAt?: string
expiresAt?: string
authorId?: string
} }
export const PostTitle = ({ export const PostTitle = ({ post, loading }: TitleProps) => {
title, const { title, author, visibility, createdAt, expiresAt } = post || {}
displayName, // eslint-disable-next-line @typescript-eslint/ban-ts-comment
visibility, // @ts-ignore displayName should be present
createdAt, const displayName = author?.displayName
expiresAt,
loading
}: // authorId
TitleProps) => {
return ( return (
<span className={styles.title}> <span className={styles.title}>
<h1 <h1

View file

@ -8,16 +8,12 @@ import PasswordModalWrapper from "./password-modal-wrapper"
import { PostWithFilesAndAuthor } from "@lib/server/prisma" import { PostWithFilesAndAuthor } from "@lib/server/prisma"
type Props = { type Props = {
post: string | PostWithFilesAndAuthor post: PostWithFilesAndAuthor
isProtected?: boolean isProtected?: boolean
isAuthor?: boolean isAuthor?: boolean
} }
const PostFiles = ({ post: _initialPost }: Props) => { const PostFiles = ({ post: initialPost }: Props) => {
const initialPost =
typeof _initialPost === "string"
? (JSON.parse(_initialPost) as PostWithFilesAndAuthor)
: _initialPost
const [post, setPost] = useState<PostWithFilesAndAuthor>(initialPost) const [post, setPost] = useState<PostWithFilesAndAuthor>(initialPost)
const router = useRouter() const router = useRouter()
@ -63,15 +59,13 @@ const PostFiles = ({ post: _initialPost }: Props) => {
gap: "var(--gap-double)" gap: "var(--gap-double)"
}} }}
> >
{post?.files?.map(({ id, content, title, html }) => ( {post?.files?.map((file) => (
<DocumentComponent <DocumentComponent
skeleton={false} skeleton={false}
key={id} key={post.id}
title={title}
initialTab={"preview"} initialTab={"preview"}
id={id} file={file}
content={content} post={post}
preview={html}
/> />
))} ))}
</main> </main>

View file

@ -1,6 +1,6 @@
"use client" "use client"
import { Post, PostWithFilesAndAuthor } from "@lib/server/prisma" import { PostWithFilesAndAuthor } from "@lib/server/prisma"
import PasswordModal from "@components/password-modal" import PasswordModal from "@components/password-modal"
import { useRouter } from "next/navigation" import { useRouter } from "next/navigation"
import { useCallback, useEffect, useState } from "react" import { useCallback, useEffect, useState } from "react"
@ -10,8 +10,8 @@ import { fetchWithUser } from "src/app/lib/fetch-with-user"
type Props = { type Props = {
setPost: (post: PostWithFilesAndAuthor) => void setPost: (post: PostWithFilesAndAuthor) => void
postId: Post["id"] postId: PostWithFilesAndAuthor["id"]
authorId: Post["authorId"] authorId: PostWithFilesAndAuthor["authorId"]
} }
const PasswordModalWrapper = ({ setPost, postId, authorId }: Props) => { const PasswordModalWrapper = ({ setPost, postId, authorId }: Props) => {
@ -20,7 +20,9 @@ const PasswordModalWrapper = ({ setPost, postId, authorId }: Props) => {
const { session, isLoading } = useSessionSWR() const { session, isLoading } = useSessionSWR()
const isAuthor = isLoading const isAuthor = isLoading
? undefined ? undefined
: session?.user && session?.user?.id === authorId : session?.user
? session?.user?.id === authorId
: false
const [isPasswordModalOpen, setIsPasswordModalOpen] = useState(false) const [isPasswordModalOpen, setIsPasswordModalOpen] = useState(false)
const onSubmit = useCallback( const onSubmit = useCallback(
async (password: string) => { async (password: string) => {
@ -64,14 +66,14 @@ const PasswordModalWrapper = ({ setPost, postId, authorId }: Props) => {
} }
useEffect(() => { useEffect(() => {
if (isAuthor) { if (isAuthor === true) {
onSubmit("author") onSubmit("author")
setToast({ setToast({
message: message:
"You're the author of this post, so you automatically have access to it.", "You're the author of this post, so you automatically have access to it.",
type: "default" type: "default"
}) })
} else { } else if (isAuthor === false) {
setIsPasswordModalOpen(true) setIsPasswordModalOpen(true)
} }
}, [isAuthor, onSubmit, setToast]) }, [isAuthor, onSubmit, setToast])

View file

@ -1,21 +1,20 @@
"use client"
import Button from "@components/button" import Button from "@components/button"
import ButtonGroup from "@components/button-group" import ButtonGroup from "@components/button-group"
import Skeleton from "@components/skeleton" import Skeleton from "@components/skeleton"
import Tooltip from "@components/tooltip" 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 Link from "next/link"
import { memo } from "react" import { memo } from "react"
import { Download, ExternalLink } from "react-feather" import { Download, ExternalLink, Globe } from "react-feather"
import styles from "./document.module.css" 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 = { type SharedProps = {
title?: string
initialTab: "edit" | "preview" initialTab: "edit" | "preview"
id?: string file?: PostWithFiles["files"][0]
content?: string post?: Pick<ServerPost, "id" | "title" | "visibility">
preview?: string
} }
type Props = ( type Props = (
@ -28,7 +27,13 @@ type Props = (
) & ) &
SharedProps SharedProps
const DownloadButtons = ({ rawLink }: { rawLink?: string }) => { const DownloadButtons = ({
rawLink,
siteLink
}: {
rawLink?: string
siteLink?: string
}) => {
return ( return (
<ButtonGroup> <ButtonGroup>
<Tooltip content="Download"> <Tooltip content="Download">
@ -44,15 +49,28 @@ const DownloadButtons = ({ rawLink }: { rawLink?: string }) => {
/> />
</Link> </Link>
</Tooltip> </Tooltip>
<Tooltip content="Open raw in new tab"> {rawLink ? (
<Link href={rawLink || ""} target="_blank" rel="noopener noreferrer"> <Tooltip content="Open raw in new tab">
<Button <Link href={rawLink || ""} target="_blank" rel="noopener noreferrer">
iconLeft={<ExternalLink color="var(--fg)" />} <Button
aria-label="Open raw file in new tab" iconLeft={<ExternalLink color="var(--fg)" />}
style={{ border: "none", background: "transparent" }} aria-label="Open raw file in new tab"
/> style={{ border: "none", background: "transparent" }}
</Link> />
</Tooltip> </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> </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. // 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 // 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 const hash = window.location.hash
if (hash && hash === `#${title}`) { if (file && hash && hash === `#${file?.title}`) {
const element = document.getElementById(title) const element = document.getElementById(file.title)
if (element) { if (element) {
element.scrollIntoView() element.scrollIntoView()
} }
@ -95,28 +113,35 @@ const Document = ({ skeleton, ...props }: Props) => {
return ( return (
<> <>
<div className={styles.card}> <div className={styles.card}>
<header id={title}> <header id={file?.title}>
<Link <Link
href={`#${title}`} href={`#${file?.title}`}
aria-label="File" aria-label="File"
style={{ style={{
textDecoration: "none", textDecoration: "none",
color: "var(--fg)" color: "var(--fg)"
}} }}
> >
{title} {file?.title}
</Link> </Link>
{/* TODO: switch to api once next.js bug is fixed */} {/* TODO: switch to api once next.js bug is fixed */}
{/* Not /api/ because of rewrites defined in next.config.mjs */} {/* 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> </header>
<div className={styles.documentContainer}> <div className={styles.documentContainer}>
<DocumentTabs <DocumentTabs
defaultTab={initialTab} defaultTab={props.initialTab}
staticPreview={preview} staticPreview={file?.html}
isEditing={false} isEditing={false}
> >
{content} {file?.content || ""}
</DocumentTabs> </DocumentTabs>
</div> </div>
</div> </div>

View 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
}

View file

@ -41,26 +41,24 @@ export const getPost = cache(async (id: string) => {
} }
if (post.visibility === "public" || post.visibility === "unlisted") { if (post.visibility === "public" || post.visibility === "unlisted") {
return { post } return post
} }
if (post.visibility === "private") { if (post.visibility === "private") {
const user = await getCurrentUser() const user = await getCurrentUser()
if (user?.id === post.authorId || user?.role === "admin") { if (user?.id === post.authorId || user?.role === "admin") {
return { post } return post
} }
return redirect("/new") return redirect("/new")
} }
if (post.visibility === "protected") { if (post.visibility === "protected") {
return { return {
post: {
visibility: "protected", visibility: "protected",
authorId: post.authorId, authorId: post.authorId,
id: post.id id: post.id
}
} }
} }
return { post } return post
}) })

View 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>
)
}

View file

@ -1,4 +1,9 @@
import VisibilityControl from "@components/badges/visibility-control" import VisibilityControl from "@components/badges/visibility-control"
import {
PostWithFilesAndAuthor,
serverPostToClientPost,
ServerPostWithFilesAndAuthor
} from "@lib/server/prisma"
import PostFiles from "./components/post-files" import PostFiles from "./components/post-files"
import { getPost } from "./get-post" import { getPost } from "./get-post"
@ -9,11 +14,12 @@ export default async function PostPage({
id: string id: string
} }
}) { }) {
const { post } = await getPost(params.id) const post = (await getPost(params.id)) as ServerPostWithFilesAndAuthor
const stringifiedPost = JSON.stringify(post) const clientPost = serverPostToClientPost(post) as PostWithFilesAndAuthor
return ( return (
<> <>
<PostFiles post={stringifiedPost} /> <PostFiles post={clientPost} />
<VisibilityControl <VisibilityControl
authorId={post.authorId} authorId={post.authorId}
postId={post.id} postId={post.id}

View file

@ -3,7 +3,7 @@
import Button from "@components/button" import Button from "@components/button"
import { Spinner } from "@components/spinner" import { Spinner } from "@components/spinner"
import { useToasts } from "@components/toasts" 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 Link from "next/link"
import { useState } from "react" import { useState } from "react"
import { fetchWithUser } from "src/app/lib/fetch-with-user" import { fetchWithUser } from "src/app/lib/fetch-with-user"
@ -14,7 +14,7 @@ export function UserTable({
}: { }: {
users?: { users?: {
createdAt: string createdAt: string
posts?: Post[] posts?: ServerPostWithFilesAndAuthor[]
id: string id: string
email: string | null email: string | null
role: string | null role: string | null
@ -95,7 +95,7 @@ export function PostTable({
posts?: { posts?: {
createdAt: string createdAt: string
id: string id: string
author?: User | null author?: UserWithPosts | null
title: string title: string
visibility: string visibility: string
}[] }[]

View file

@ -1,10 +1,11 @@
import { getCurrentUser } from "@lib/server/session" import { getCurrentUser } from "@lib/server/session"
import { redirect } from "next/navigation" import { redirect } from "next/navigation"
import { PropsWithChildren } from "react"
export default async function AdminLayout({ export default async function AdminLayout({
children children
}: PropsWithChildren<unknown>) { }: {
children: React.ReactNode
}) {
const user = await getCurrentUser() const user = await getCurrentUser()
const isAdmin = user?.role === "admin" const isAdmin = user?.role === "admin"

View file

@ -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" import { PostTable, UserTable } from "./components/tables"
export default async function AdminPage() { export default async function AdminPage() {
@ -25,15 +31,9 @@ export default async function AdminPage() {
const [users, posts] = await Promise.all([usersPromise, postsPromise]) const [users, posts] = await Promise.all([usersPromise, postsPromise])
const serializedPosts = posts.map((post) => { const serializedPosts = posts.map((post) =>
return { serverPostToClientPost(post as ServerPostWithFiles)
...post, ) as PostWithFiles[]
createdAt: post.createdAt.toISOString(),
updatedAt: post.updatedAt.toISOString(),
expiresAt: post.expiresAt?.toISOString(),
deletedAt: post.deletedAt?.toISOString()
}
})
const serializedUsers = users.map((user) => { const serializedUsers = users.map((user) => {
return { return {
@ -46,7 +46,8 @@ export default async function AdminPage() {
<div> <div>
<h1>Admin</h1> <h1>Admin</h1>
<h2>Users</h2> <h2>Users</h2>
<UserTable users={serializedUsers} /> {/* @ts-expect-error Type 'unknown' is not assignable to type */}
<UserTable users={serializedUsers as unknown} />
<h2>Posts</h2> <h2>Posts</h2>
<PostTable posts={serializedPosts} /> <PostTable posts={serializedPosts} />
</div> </div>

View file

@ -1,5 +1,9 @@
import PostList from "@components/post-list" 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 Image from "next/image"
import { Suspense } from "react" import { Suspense } from "react"
import { User } from "react-feather" import { User } from "react-feather"
@ -11,15 +15,10 @@ async function PostListWrapper({
posts: ReturnType<typeof getPostsByUser> posts: ReturnType<typeof getPostsByUser>
userId: string userId: string
}) { }) {
const data = (await posts).filter((post) => post.visibility === "public") const data = (await posts)
return ( .filter((post) => post.visibility === "public")
<PostList .map(serverPostToClientPost)
userId={userId} return <PostList userId={userId} initialPosts={data} hideSearch hideActions />
initialPosts={JSON.stringify(data)}
hideSearch
hideActions
/>
)
} }
export default async function UserPage({ export default async function UserPage({

View file

@ -4,14 +4,16 @@ import Layout from "@components/layout"
import { Toasts } from "@components/toasts" import { Toasts } from "@components/toasts"
import Header from "@components/header" import Header from "@components/header"
import { Inter } from "@next/font/google" import { Inter } from "@next/font/google"
import { PropsWithChildren, Suspense } from "react" import { Suspense } from "react"
import { Spinner } from "@components/spinner" import { Spinner } from "@components/spinner"
const inter = Inter({ subsets: ["latin"], variable: "--inter-font" }) const inter = Inter({ subsets: ["latin"], variable: "--inter-font" })
export default async function RootLayout({ export default async function RootLayout({
children children
}: PropsWithChildren<unknown>) { }: {
children: React.ReactNode
}) {
return ( return (
// suppressHydrationWarning is required because of next-themes // suppressHydrationWarning is required because of next-themes
<html lang="en" className={inter.variable} suppressHydrationWarning> <html lang="en" className={inter.variable} suppressHydrationWarning>

View file

@ -1,5 +1,5 @@
import { redirect } from "next/navigation" 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 PostList from "@components/post-list"
import { getCurrentUser } from "@lib/server/session" import { getCurrentUser } from "@lib/server/session"
import { authOptions } from "@lib/server/auth" import { authOptions } from "@lib/server/auth"
@ -12,14 +12,12 @@ export default async function Mine() {
return redirect(authOptions.pages?.signIn || "/new") return redirect(authOptions.pages?.signIn || "/new")
} }
const posts = await getPostsByUser(userId, true) const posts = (await getPostsByUser(userId, true)).map(serverPostToClientPost)
const stringifiedPosts = JSON.stringify(posts)
return ( return (
<Suspense fallback={<PostList skeleton={true} initialPosts={[]} />}> <Suspense fallback={<PostList skeleton={true} initialPosts={[]} />}>
<PostList <PostList
userId={userId} userId={userId}
initialPosts={stringifiedPosts} initialPosts={posts}
isOwner={true} isOwner={true}
hideSearch={false} hideSearch={false}
/> />

View file

@ -2,7 +2,11 @@ import Image from "next/image"
import Card from "@components/card" import Card from "@components/card"
import { getWelcomeContent } from "src/pages/api/welcome" import { getWelcomeContent } from "src/pages/api/welcome"
import DocumentTabs from "./(posts)/components/tabs" 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 PostList, { NoPostsFound } from "@components/post-list"
import { cache, Suspense } from "react" import { cache, Suspense } from "react"
import ErrorBoundary from "@components/error/fallback" import ErrorBoundary from "@components/error/fallback"
@ -34,12 +38,7 @@ export default async function Page() {
<ErrorBoundary> <ErrorBoundary>
<Suspense <Suspense
fallback={ fallback={
<PostList <PostList skeleton hideActions hideSearch initialPosts={[]} />
skeleton
hideActions
hideSearch
initialPosts={JSON.stringify({})}
/>
} }
> >
{/* @ts-expect-error because of async RSC */} {/* @ts-expect-error because of async RSC */}
@ -67,14 +66,14 @@ async function WelcomePost() {
} }
async function PublicPostList() { async function PublicPostList() {
const posts = await getAllPosts({ const posts = (await getAllPosts({
select: { select: {
id: true, id: true,
title: true, title: true,
createdAt: true, createdAt: true,
author: { author: {
select: { select: {
name: true displayName: true
} }
}, },
visibility: true, visibility: true,
@ -92,13 +91,13 @@ async function PublicPostList() {
orderBy: { orderBy: {
createdAt: "desc" createdAt: "desc"
} }
}) })) as unknown as ServerPostWithFilesAndAuthor[]
if (posts.length === 0) { if (posts.length === 0) {
return <NoPostsFound /> return <NoPostsFound />
} }
return ( const clientPosts = posts.map((post) => serverPostToClientPost(post))
<PostList initialPosts={JSON.stringify(posts)} hideActions hideSearch />
) return <PostList initialPosts={clientPosts} hideActions hideSearch />
} }

View file

@ -6,11 +6,7 @@ import { ThemeProvider } from "next-themes"
import { PropsWithChildren } from "react" import { PropsWithChildren } from "react"
import { SWRConfig } from "swr" import { SWRConfig } from "swr"
export type ChildrenProps = { export function Providers({ children }: PropsWithChildren<unknown>) {
children?: React.ReactNode
}
export function Providers({ children }: ChildrenProps) {
return ( return (
<SessionProvider> <SessionProvider>
<RadixTooltip.Provider delayDuration={200}> <RadixTooltip.Provider delayDuration={200}>

View file

@ -1,6 +1,6 @@
import SettingsGroup from "../components/settings-group" import SettingsGroup from "../../components/settings-group"
import Profile from "src/app/settings/components/sections/profile"
import APIKeys from "./components/sections/api-keys" import APIKeys from "./components/sections/api-keys"
import Profile from "./components/sections/profile"
export default async function SettingsPage() { export default async function SettingsPage() {
return ( return (

View file

@ -1,5 +0,0 @@
import { ChildrenProps } from "src/app/providers"
export default function NewLayout({ children }: ChildrenProps) {
return <>{children}</>
}

View file

@ -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>
)
}

View file

@ -8,11 +8,11 @@ import Badge from "../badge"
const ExpirationBadge = ({ const ExpirationBadge = ({
postExpirationDate postExpirationDate
}: { }: {
postExpirationDate: Date | string | null postExpirationDate: Date | string | undefined
onExpires?: () => void onExpires?: () => void
}) => { }) => {
const expirationDate = useMemo( const expirationDate = useMemo(
() => (postExpirationDate ? new Date(postExpirationDate) : null), () => (postExpirationDate ? new Date(postExpirationDate) : undefined),
[postExpirationDate] [postExpirationDate]
) )
const [timeUntilString, setTimeUntil] = useState<string | null>( const [timeUntilString, setTimeUntil] = useState<string | null>(

View file

@ -9,6 +9,7 @@ import { Spinner } from "@components/spinner"
import { useRouter } from "next/navigation" import { useRouter } from "next/navigation"
import { useSessionSWR } from "@lib/use-session-swr" import { useSessionSWR } from "@lib/use-session-swr"
import { fetchWithUser } from "src/app/lib/fetch-with-user" import { fetchWithUser } from "src/app/lib/fetch-with-user"
import FadeIn from "@components/fade-in"
type Props = { type Props = {
authorId: string authorId: string
@ -87,7 +88,7 @@ function VisibilityControl({
} }
return ( return (
<> <FadeIn>
<ButtonGroup <ButtonGroup
style={{ style={{
maxWidth: 600, maxWidth: 600,
@ -128,7 +129,7 @@ function VisibilityControl({
onClose={onClosePasswordModal} onClose={onClosePasswordModal}
onSubmit={submitPassword} onSubmit={submitPassword}
/> />
</> </FadeIn>
) )
} }

View file

@ -3,8 +3,6 @@
cursor: pointer; cursor: pointer;
border-radius: var(--radius); border-radius: var(--radius);
border: 1px solid var(--border); border: 1px solid var(--border);
/* padding: var(--gap-half) var(--gap); */
color: var(--darker-gray);
} }
.button:hover, .button:hover,

View file

@ -1,9 +1,9 @@
@media (prefers-reduced-motion: no-preference) { /* @media (prefers-reduced-motion: no-preference) { */
.fadeIn { .fadeIn {
animation-name: fadeInAnimation; animation-name: fadeInAnimation;
animation-fill-mode: backwards; animation-fill-mode: backwards;
}
} }
/* } */
@keyframes fadeInAnimation { @keyframes fadeInAnimation {
from { from {

View file

@ -1,8 +1,18 @@
"use client" "use client"
import { PropsWithChildren } from "react" import clsx from "clsx"
import styles from "./page.module.css" import styles from "./page.module.css"
export default function Layout({ children }: PropsWithChildren<unknown>) { export default function Layout({
return <div className={styles.page}>{children}</div> children,
forSites
}: {
forSites?: boolean
children: React.ReactNode
}) {
return (
<div className={clsx(styles.page, forSites && styles.forSites)}>
{children}
</div>
)
} }

View file

@ -8,6 +8,10 @@
margin: 0 auto; margin: 0 auto;
} }
.forSites {
margin-top: var(--gap);
}
/* 55rem == --main-content */ /* 55rem == --main-content */
@media screen and (max-width: 55rem) { @media screen and (max-width: 55rem) {
.page { .page {

View file

@ -13,7 +13,7 @@ import { fetchWithUser } from "src/app/lib/fetch-with-user"
import { Stack } from "@components/stack" import { Stack } from "@components/stack"
type Props = { type Props = {
initialPosts: string | PostWithFiles[] initialPosts: PostWithFiles[]
morePosts?: boolean morePosts?: boolean
hideSearch?: boolean hideSearch?: boolean
hideActions?: boolean hideActions?: boolean
@ -24,17 +24,13 @@ type Props = {
} }
const PostList = ({ const PostList = ({
initialPosts: initialPostsMaybeJSON, initialPosts,
hideSearch, hideSearch,
hideActions, hideActions,
isOwner, isOwner,
skeleton, skeleton,
userId userId
}: Props) => { }: Props) => {
const initialPosts =
typeof initialPostsMaybeJSON === "string"
? JSON.parse(initialPostsMaybeJSON)
: initialPostsMaybeJSON
const [searchValue, setSearchValue] = useState("") const [searchValue, setSearchValue] = useState("")
const [searching, setSearching] = useState(false) const [searching, setSearching] = useState(false)
const [posts, setPosts] = useState<PostWithFiles[]>(initialPosts) const [posts, setPosts] = useState<PostWithFiles[]>(initialPosts)

View file

@ -8,6 +8,12 @@ import styles from "./scroll.module.css"
const ScrollToTop = () => { const ScrollToTop = () => {
const [shouldShow, setShouldShow] = useState(false) const [shouldShow, setShouldShow] = useState(false)
const isReducedMotion =
typeof window !== "undefined"
? window.matchMedia("(prefers-reduced-motion: reduce)").matches
: false
useEffect(() => { useEffect(() => {
// if user is scrolled, set visible // if user is scrolled, set visible
const handleScroll = () => { const handleScroll = () => {
@ -17,13 +23,8 @@ const ScrollToTop = () => {
return () => window.removeEventListener("scroll", handleScroll) 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>) => { const onClick = async (e: React.MouseEvent<HTMLButtonElement>) => {
e.currentTarget.blur() e.currentTarget.blur()
window.scrollTo({ top: 0, behavior: isReducedMotion ? "auto" : "smooth" }) window.scrollTo({ top: 0, behavior: isReducedMotion ? "auto" : "smooth" })
} }

View file

@ -1,4 +1,5 @@
import Card from "@components/card" "use client"
import * as RadixTooltip from "@radix-ui/react-tooltip" import * as RadixTooltip from "@radix-ui/react-tooltip"
import styles from "./tooltip.module.css" import styles from "./tooltip.module.css"
@ -17,8 +18,9 @@ const Tooltip = ({
<RadixTooltip.Trigger asChild className={className}> <RadixTooltip.Trigger asChild className={className}>
{children} {children}
</RadixTooltip.Trigger> </RadixTooltip.Trigger>
<RadixTooltip.Content> <RadixTooltip.Content>
<Card className={styles.tooltip}>{content}</Card> <div className={styles.tooltip}>{content}</div>
</RadixTooltip.Content> </RadixTooltip.Content>
</RadixTooltip.Root> </RadixTooltip.Root>
) )

View file

@ -1,24 +1,32 @@
.tooltip { .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{ [data-side="top"] .tooltip {
margin-bottom: var(--gap-quarter); margin-bottom: var(--gap-quarter);
} }
.tooltip[data-side='bottom'] { .tooltip[data-side="bottom"] {
margin-top: var(--gap-quarter); margin-top: var(--gap-quarter);
} }
.tooltip[data-side='left'] { .tooltip[data-side="left"] {
margin-right: var(--gap-quarter); margin-right: var(--gap-quarter);
} }
.tooltip[data-side='right'] { .tooltip[data-side="right"] {
margin-left: var(--gap-quarter); margin-left: var(--gap-quarter);
} }
@keyframes fadein { @keyframes fadein {
from { opacity: 0; } from {
to { opacity: 1; } opacity: 0;
}
to {
opacity: 1;
}
} }

View file

@ -0,0 +1,3 @@
export function getURLFriendlyTitle(title: string) {
return title.replace(/\s/g, "-")
}

View 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>
)
}

View 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 || ""
}
}

View 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>
)
}

View file

@ -122,6 +122,10 @@ table th {
table th, table th,
table td { table td {
padding: 0.5rem 1rem; padding: 0.35rem 0.75rem;
border: 1px solid var(--light-gray); border: 1px solid var(--light-gray);
} }
article > :not(:first-child) {
margin-top: 1.5rem;
}

View file

@ -74,7 +74,9 @@ export const config = (env: Environment): Config => {
is_production, is_production,
enable_admin: stringToBoolean(env.ENABLE_ADMIN), enable_admin: stringToBoolean(env.ENABLE_ADMIN),
registration_password: env.REGISTRATION_PASSWORD ?? "", 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", welcome_title: env.WELCOME_TITLE ?? "Drift",
url: url:
throwIfUndefined("DRIFT_URL", true) || throwIfUndefined("DRIFT_URL", true) ||

View file

@ -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 = [ export const allowedFileTypes = [
"application/json", "application/json",
"application/x-javascript", "application/x-javascript",

View file

@ -1,11 +1,10 @@
import { Gist } from "./types" import { Gist } from "./types"
import * as crypto from "crypto" import * as crypto from "crypto"
import type { Post } from "@lib/server/prisma"
import { getHtmlFromFile } from "@lib/server/get-html-from-drift-file" 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< export type AdditionalPostInformation = Pick<
Post, ServerPost,
"visibility" | "password" | "expiresAt" "visibility" | "password" | "expiresAt"
> & { > & {
userId: string userId: string
@ -14,7 +13,7 @@ export type AdditionalPostInformation = Pick<
export async function createPostFromGist( export async function createPostFromGist(
{ userId, visibility, password, expiresAt }: AdditionalPostInformation, { userId, visibility, password, expiresAt }: AdditionalPostInformation,
gist: Gist gist: Gist
): Promise<Post> { ): Promise<ServerPost> {
const files = Object.values(gist.files) const files = Object.values(gist.files)
const [title, description] = gist.description.split("\n", 1) const [title, description] = gist.description.split("\n", 1)

View file

@ -4,11 +4,20 @@ declare global {
} }
import config from "@lib/config" 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 * as crypto from "crypto"
import { cache } from "react" 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 = export const prisma =
global.prisma || global.prisma ||
new PrismaClient({ new PrismaClient({
@ -42,29 +51,91 @@ const postWithFilesAndAuthor = Prisma.validator<Prisma.PostArgs>()({
}) })
export type ServerPostWithFiles = Prisma.PostGetPayload<typeof postWithFiles> 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< export type ServerPostWithFilesAndAuthor = Prisma.PostGetPayload<
typeof postWithFilesAndAuthor typeof postWithFilesAndAuthor
> >
export type PostWithFiles = Omit<ServerPostWithFiles, "files"> & { export type PostWithFiles = Omit<
files: (Omit<ServerPostWithFiles["files"][number], "content" | "html"> & { ServerPostWithFiles,
"files" | "updatedAt" | "createdAt" | "deletedAt" | "expiresAt"
> & {
files: (Omit<
ServerPostWithFiles["files"][number],
"content" | "html" | "updatedAt" | "createdAt" | "deletedAt"
> & {
content: string content: string
html: string html: string
updatedAt?: string
createdAt: string
deletedAt?: string
})[] })[]
updatedAt?: string
createdAt: string
deletedAt?: string
expiresAt?: string
} }
export type PostWithFilesAndAuthor = Omit< export type PostWithFilesAndAuthor = Omit<
ServerPostWithFilesAndAuthor, ServerPostWithFilesAndAuthor,
"files" "files" | "updatedAt" | "createdAt" | "deletedAt" | "expiresAt" | "author"
> & { > & {
files: (Omit< files: (Omit<
ServerPostWithFilesAndAuthor["files"][number], ServerPostWithFilesAndAuthor["files"][number],
"content" | "html" "content" | "html" | "updatedAt" | "createdAt" | "deletedAt"
> & { > & {
content: string content: string
html: 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) => { export const getFilesForPost = async (postId: string) => {
@ -87,12 +158,15 @@ export async function getFilesByPost(postId: string) {
return files return files
} }
export async function getPostsByUser(userId: string): Promise<Post[]> export async function getPostsByUser(userId: string): Promise<ServerPost[]>
export async function getPostsByUser( export async function getPostsByUser(
userId: string, userId: string,
includeFiles: true includeFiles: true
): Promise<ServerPostWithFiles[]> ): 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({ const posts = await prisma.post.findMany({
where: { where: {
authorId: userId authorId: userId
@ -123,7 +197,7 @@ export async function getPostsByUser(userId: User["id"], withFiles?: boolean) {
} }
export const getUserById = async ( export const getUserById = async (
userId: User["id"], userId: ServerUser["id"],
selects?: Prisma.UserFindUniqueArgs["select"] selects?: Prisma.UserFindUniqueArgs["select"]
) => { ) => {
const user = await prisma.user.findUnique({ const user = await prisma.user.findUnique({
@ -143,7 +217,7 @@ export const getUserById = async (
return user return user
} }
export const isUserAdmin = async (userId: User["id"]) => { export const isUserAdmin = async (userId: ServerUser["id"]) => {
const user = await prisma.user.findUnique({ const user = await prisma.user.findUnique({
where: { where: {
id: userId id: userId
@ -185,36 +259,24 @@ type GetPostByIdOptions = Pick<
"include" | "rejectOnNotFound" | "select" "include" | "rejectOnNotFound" | "select"
> >
export const getPostById = async ( export const getPostById = cache(
postId: Post["id"], async (postId: ServerPost["id"], options?: GetPostByIdOptions) => {
options?: GetPostByIdOptions const post = await prisma.post.findUnique({
): Promise<Post | PostWithFiles | PostWithFilesAndAuthor | null> => { where: {
const post = await prisma.post.findUnique({ id: postId
where: { },
id: postId ...options
}, })
...options
})
if (post) { return 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
}
export const getAllPosts = cache( export const getAllPosts = cache(
async ( async (
options?: Prisma.PostFindManyArgs options?: Prisma.PostFindManyArgs
): Promise< ): Promise<
Post[] | ServerPostWithFiles[] | ServerPostWithFilesAndAuthor[] ServerPost[] | ServerPostWithFiles[] | ServerPostWithFilesAndAuthor[]
> => { > => {
const posts = await prisma.post.findMany(options) const posts = await prisma.post.findMany(options)
return posts return posts
@ -231,7 +293,7 @@ export type UserWithPosts = Prisma.UserGetPayload<typeof userWithPosts>
export const getAllUsers = async ( export const getAllUsers = async (
options?: Prisma.UserFindManyArgs options?: Prisma.UserFindManyArgs
): Promise<User[] | UserWithPosts[]> => { ): Promise<ServerUser[] | UserWithPosts[]> => {
const users = (await prisma.user.findMany({ const users = (await prisma.user.findMany({
select: { select: {
id: true, id: true,
@ -242,7 +304,7 @@ export const getAllUsers = async (
createdAt: true createdAt: true
}, },
...options ...options
})) as User[] | UserWithPosts[] })) as ServerUser[] | UserWithPosts[]
return users return users
} }
@ -252,7 +314,7 @@ export const searchPosts = async (
{ {
userId userId
}: { }: {
userId?: User["id"] userId?: ServerUser["id"]
} = {} } = {}
): Promise<ServerPostWithFiles[]> => { ): Promise<ServerPostWithFiles[]> => {
const posts = await prisma.post.findMany({ const posts = await prisma.post.findMany({
@ -287,7 +349,10 @@ function generateApiToken() {
return crypto.randomBytes(32).toString("hex") 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({ const apiToken = await prisma.apiToken.create({
data: { data: {
token: generateApiToken(), token: generateApiToken(),
@ -301,3 +366,19 @@ export const createApiToken = async (userId: User["id"], name: string) => {
return apiToken return apiToken
} }
export function getFileById(fileId: ServerFile["id"]) {
return prisma.file.findUnique({
where: {
id: fileId
},
include: {
post: {
select: {
id: true,
visibility: true
}
}
}
})
}

View file

@ -2,7 +2,7 @@ import { withMethods } from "@lib/api-middleware/with-methods"
import { prisma } from "@lib/server/prisma" import { prisma } from "@lib/server/prisma"
import { NextApiRequest, NextApiResponse } from "next" import { NextApiRequest, NextApiResponse } from "next"
import { File } from "@lib/server/prisma" import { ServerFile } from "@lib/server/prisma"
import * as crypto from "crypto" import * as crypto from "crypto"
import { getHtmlFromFile } from "@lib/server/get-html-from-drift-file" import { getHtmlFromFile } from "@lib/server/get-html-from-drift-file"
import { verifyApiUser } from "@lib/server/verify-api-user" 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" }) 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 content: string
html: string html: string
})[] })[]