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",
testEnvironment: "node",
setupFiles: ["<rootDir>/test/setup-tests.ts"],
// TODO: update to app dir
moduleNameMapper: {
"@lib/(.*)": "<rootDir>/src/lib/$1",
"@routes/(.*)": "<rootDir>/src/routes/$1"

View file

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

View file

@ -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",

View file

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

View file

@ -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"

View file

@ -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"

View file

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

View file

@ -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"

View file

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

View file

@ -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 = {

View file

@ -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(

View file

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

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"
export default function New() {

View file

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

View file

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

View file

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

View file

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

View file

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

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") {
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
})

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

View file

@ -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
}[]

View file

@ -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"

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

View file

@ -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({

View file

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

View file

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

View file

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

View file

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

View file

@ -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 (

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 = ({
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>(

View file

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

View file

@ -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,

View file

@ -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 {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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 td {
padding: 0.5rem 1rem;
padding: 0.35rem 0.75rem;
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,
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) ||

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

View file

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

View file

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

View file

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