SSR theme and auth in header. Move auth check to middleware. Bump next

This commit is contained in:
Max Leiter 2023-03-27 20:16:11 -07:00
parent cb7d9ebc6b
commit 68fc679864
14 changed files with 367 additions and 313 deletions

View file

@ -1,5 +1,4 @@
{ {
"plugins": ["@typescript-eslint"],
"extends": ["next/core-web-vitals", "plugin:@typescript-eslint/recommended"], "extends": ["next/core-web-vitals", "plugin:@typescript-eslint/recommended"],
"ignorePatterns": ["node_modules/", "__tests__/"], "ignorePatterns": ["node_modules/", "__tests__/"],
"rules": { "rules": {

View file

@ -14,7 +14,7 @@
}, },
"dependencies": { "dependencies": {
"@next-auth/prisma-adapter": "^1.0.5", "@next-auth/prisma-adapter": "^1.0.5",
"@next/eslint-plugin-next": "13.2.4-canary.0", "@next/eslint-plugin-next": "13.2.5-canary.19",
"@prisma/client": "^4.10.1", "@prisma/client": "^4.10.1",
"@radix-ui/react-dialog": "^1.0.2", "@radix-ui/react-dialog": "^1.0.2",
"@radix-ui/react-dropdown-menu": "^2.0.3", "@radix-ui/react-dropdown-menu": "^2.0.3",
@ -27,10 +27,11 @@
"cmdk": "^0.1.22", "cmdk": "^0.1.22",
"jest": "^29.4.3", "jest": "^29.4.3",
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8",
"next": "13.2.4-canary.0", "next": "13.2.5-canary.19",
"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",
"react-cookie": "^4.1.1",
"react-datepicker": "4.8.0", "react-datepicker": "4.8.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-dropzone": "14.2.3", "react-dropzone": "14.2.3",

View file

@ -3,7 +3,7 @@ lockfileVersion: 5.4
specifiers: specifiers:
'@next-auth/prisma-adapter': ^1.0.5 '@next-auth/prisma-adapter': ^1.0.5
'@next/bundle-analyzer': 13.1.7-canary.26 '@next/bundle-analyzer': 13.1.7-canary.26
'@next/eslint-plugin-next': 13.2.4-canary.0 '@next/eslint-plugin-next': 13.2.5-canary.19
'@prisma/client': ^4.10.1 '@prisma/client': ^4.10.1
'@radix-ui/react-dialog': ^1.0.2 '@radix-ui/react-dialog': ^1.0.2
'@radix-ui/react-dropdown-menu': ^2.0.3 '@radix-ui/react-dropdown-menu': ^2.0.3
@ -36,7 +36,7 @@ specifiers:
jest: ^29.4.3 jest: ^29.4.3
jest-mock-extended: ^3.0.2 jest-mock-extended: ^3.0.2
lodash.debounce: ^4.0.8 lodash.debounce: ^4.0.8
next: 13.2.4-canary.0 next: 13.2.5-canary.19
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
@ -47,6 +47,7 @@ specifiers:
prettier: 2.8.3 prettier: 2.8.3
prisma: ^4.10.1 prisma: ^4.10.1
react: 18.2.0 react: 18.2.0
react-cookie: ^4.1.1
react-datepicker: 4.8.0 react-datepicker: 4.8.0
react-dom: 18.2.0 react-dom: 18.2.0
react-dropzone: 14.2.3 react-dropzone: 14.2.3
@ -65,7 +66,7 @@ specifiers:
dependencies: dependencies:
'@next-auth/prisma-adapter': 1.0.5_qpmskah7lm3ildf4stmwh4q42u '@next-auth/prisma-adapter': 1.0.5_qpmskah7lm3ildf4stmwh4q42u
'@next/eslint-plugin-next': 13.2.4-canary.0 '@next/eslint-plugin-next': 13.2.5-canary.19
'@prisma/client': 4.10.1_prisma@4.10.1 '@prisma/client': 4.10.1_prisma@4.10.1
'@radix-ui/react-dialog': 1.0.2_5ndqzdd6t4rivxsukjv3i3ak2q '@radix-ui/react-dialog': 1.0.2_5ndqzdd6t4rivxsukjv3i3ak2q
'@radix-ui/react-dropdown-menu': 2.0.3_5ndqzdd6t4rivxsukjv3i3ak2q '@radix-ui/react-dropdown-menu': 2.0.3_5ndqzdd6t4rivxsukjv3i3ak2q
@ -78,10 +79,11 @@ dependencies:
cmdk: 0.1.22_5ndqzdd6t4rivxsukjv3i3ak2q cmdk: 0.1.22_5ndqzdd6t4rivxsukjv3i3ak2q
jest: 29.4.3_@types+node@18.11.18 jest: 29.4.3_@types+node@18.11.18
lodash.debounce: 4.0.8 lodash.debounce: 4.0.8
next: 13.2.4-canary.0_biqbaboplfbrettd7655fr4n2y next: 13.2.5-canary.19_biqbaboplfbrettd7655fr4n2y
next-auth: 4.19.2_5d3uzjtamlpvvireij3yl2isni next-auth: 4.19.2_gjjimu27ie6kivuv476ljuoy44
next-themes: 0.2.1_5d3uzjtamlpvvireij3yl2isni next-themes: 0.2.1_gjjimu27ie6kivuv476ljuoy44
react: 18.2.0 react: 18.2.0
react-cookie: 4.1.1_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
react-dropzone: 14.2.3_react@18.2.0 react-dropzone: 14.2.3_react@18.2.0
@ -1017,7 +1019,7 @@ packages:
next-auth: ^4 next-auth: ^4
dependencies: dependencies:
'@prisma/client': 4.10.1_prisma@4.10.1 '@prisma/client': 4.10.1_prisma@4.10.1
next-auth: 4.19.2_5d3uzjtamlpvvireij3yl2isni next-auth: 4.19.2_gjjimu27ie6kivuv476ljuoy44
dev: false dev: false
/@next/bundle-analyzer/13.1.7-canary.26: /@next/bundle-analyzer/13.1.7-canary.26:
@ -1029,8 +1031,8 @@ packages:
- utf-8-validate - utf-8-validate
dev: true dev: true
/@next/env/13.2.4-canary.0: /@next/env/13.2.5-canary.19:
resolution: {integrity: sha512-zh3+D4qTGhDJMM6RuciIOEBA6pHeO6EE3U7LFSknX79BwVqsWpDa59yFeelmrXXh8hHCdOajrReISPICeoQRNw==} resolution: {integrity: sha512-QXO4HlOMammiqsRvFFfr29oPR5qj6bneLmwkRfceYcBE1bq8+xnB3ZpMCc26mCpAkpSFWlGgF53JzKCWWxlAxQ==}
dev: false dev: false
/@next/eslint-plugin-next/13.1.7-canary.26: /@next/eslint-plugin-next/13.1.7-canary.26:
@ -1039,32 +1041,14 @@ packages:
glob: 7.1.7 glob: 7.1.7
dev: true dev: true
/@next/eslint-plugin-next/13.2.4-canary.0: /@next/eslint-plugin-next/13.2.5-canary.19:
resolution: {integrity: sha512-opggGf9WdvkcRJFqeaJOU39MrHcbkKszBM//vht1hDYR7+g7elcW2PEMKSGuwxfF6DR4HUPJFyrDc1Nj8l9+pg==} resolution: {integrity: sha512-21kD07NHHu+a+8hK2vI14tqE+YSM5VKZuUZRF+OamC0bk/b4TzzmXhbJH6Hj3goD8MkbLW8wSMBmiswrGBadOg==}
dependencies: dependencies:
glob: 7.1.7 glob: 7.1.7
dev: false dev: false
/@next/swc-android-arm-eabi/13.2.4-canary.0: /@next/swc-darwin-arm64/13.2.5-canary.19:
resolution: {integrity: sha512-i8ZrvlgYkoUhkBNNRo+mChzMB7KhGOVoe1tWbogWngTo63ljrId0HlcPfNFNOGccggUPmSmIgkzXv5+wS1MN9Q==} resolution: {integrity: sha512-29JRAeF/dMO/cHlGmNun6t7oAKmZ1UmPyHZi6jbbSOZH4mpLAtLhyg6azqHzTYQj86TLpJZMc12g2ql4e62ldQ==}
engines: {node: '>= 10'}
cpu: [arm]
os: [android]
requiresBuild: true
dev: false
optional: true
/@next/swc-android-arm64/13.2.4-canary.0:
resolution: {integrity: sha512-IJSJyXp3rNXaMuWdMSfaSwBMejzzTkzfAq1miXVHwxQt06OP2lAlRdBj1XBFqPLgxVdhtJFeukiBLA6WbMesFw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [android]
requiresBuild: true
dev: false
optional: true
/@next/swc-darwin-arm64/13.2.4-canary.0:
resolution: {integrity: sha512-e60xAA4bX4K1WLtW61rqW7JalmCES4IZSwutLiV5+oikqPNFV2rWbkPC/otgL4wOJmNo6GO0XpkKofMnQolo+A==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
@ -1072,8 +1056,8 @@ packages:
dev: false dev: false
optional: true optional: true
/@next/swc-darwin-x64/13.2.4-canary.0: /@next/swc-darwin-x64/13.2.5-canary.19:
resolution: {integrity: sha512-QnfNzyyzxgD2SO4VZipAE7c/HnYl6pz7MKphDb3U/AbkzsncZYfFXbxxMo4GSAu1AQ4QdKwcDFAM3PBelKIppg==} resolution: {integrity: sha512-hz5pbgSg+vq2YZPFFmwwzXyXdXNffhHm97v8RWK4fUtMD9DlbZJrRUVcyOtk9486u1SkTXIMyZ8KUjE1TpZQQg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
@ -1081,26 +1065,8 @@ packages:
dev: false dev: false
optional: true optional: true
/@next/swc-freebsd-x64/13.2.4-canary.0: /@next/swc-linux-arm64-gnu/13.2.5-canary.19:
resolution: {integrity: sha512-0m8pOp3cQvGFS3Q98LrCKJL3eHiXBsl81dFbkizQaZb7h/p3LZ0LBYN6i1GVelB1JXxfO1sJA2L2eu4tASNcwQ==} resolution: {integrity: sha512-MIQJpgHhIIbr/CJyBOFSCsbr1Kv1nFB2VC2zfARjyDOOuKQQf5tlJrERQKyr8b61+/w5f+L3ZiPp7QPAUd9jmA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [freebsd]
requiresBuild: true
dev: false
optional: true
/@next/swc-linux-arm-gnueabihf/13.2.4-canary.0:
resolution: {integrity: sha512-fTKY0WHyYb7xYw9gKkW6+yHrIAj6jA5eoC41ePNpssbwqsIyHZKHDjiGHLYU7/Uq7i/GU+/O+Y9V5eRKsfGOCA==}
engines: {node: '>= 10'}
cpu: [arm]
os: [linux]
requiresBuild: true
dev: false
optional: true
/@next/swc-linux-arm64-gnu/13.2.4-canary.0:
resolution: {integrity: sha512-bKpw2Dxml5OOfvTWWmAifEc3nDgAzSLailkqPAeKzK7ag21NSizABPB3YpfixTUeBF5ftamhP0U4M36O6av4zg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
@ -1108,8 +1074,8 @@ packages:
dev: false dev: false
optional: true optional: true
/@next/swc-linux-arm64-musl/13.2.4-canary.0: /@next/swc-linux-arm64-musl/13.2.5-canary.19:
resolution: {integrity: sha512-Z8bxSnofNGiXzJZj41pnwQYoITR398ASNzEXe0Sq9kNvZuKoUn7WANpqbE9EFVrQn726czHrpSdSjujINA0aag==} resolution: {integrity: sha512-r88q1FhZwFpWwBH6lLJmm9d/WCY2MIewghUqOCKMbXLtPBLdSEvNR726PGGUBtC6pFQsRuv7Prn7CT/VMkRyXA==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
@ -1117,8 +1083,8 @@ packages:
dev: false dev: false
optional: true optional: true
/@next/swc-linux-x64-gnu/13.2.4-canary.0: /@next/swc-linux-x64-gnu/13.2.5-canary.19:
resolution: {integrity: sha512-3N8HhgKcldKv6VKE1pozPjgLi/8tEmUyqXbT/u1cG0HDDhJj6CxCnZoBPS7u6JzhbfTjqwPdvt6IcyJkevFRsA==} resolution: {integrity: sha512-Ke5aTqYMH9J3a38t6GvM58AY3MmOKbSR3RTQzaIb+96xtTUdzWjWnbAv05Lr9zlrnjTHmFR50CoOvulLTCXW/w==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
@ -1126,8 +1092,8 @@ packages:
dev: false dev: false
optional: true optional: true
/@next/swc-linux-x64-musl/13.2.4-canary.0: /@next/swc-linux-x64-musl/13.2.5-canary.19:
resolution: {integrity: sha512-G7fAzeK68piPXfbgQ4uc7zTg7RTE8Yg/FZt6Yd2S9DNvsO2PHWgKitMZZUOpHn/5OY9sg/ne1lwcegRIDmU9yA==} resolution: {integrity: sha512-GPAyzn7ksPTpY0rPoMnzBydnpMAnK9z8H8y6VEvbAqwoJY+NZN+YepnJkW9HV01mdrvmxXv5dkdPlfbxW5pRsQ==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
@ -1135,8 +1101,8 @@ packages:
dev: false dev: false
optional: true optional: true
/@next/swc-win32-arm64-msvc/13.2.4-canary.0: /@next/swc-win32-arm64-msvc/13.2.5-canary.19:
resolution: {integrity: sha512-4t3SyyqieUuEMuLvuRs+R0m+6Ocm9DiPq1xLBu5noaxOgMSYyPjUFZbhL2bfG1OQb6VyZmaeGpN4ZDfS5Mjixw==} resolution: {integrity: sha512-s+z9qlmtvWJ5uQj8yrX5d+SW4OeYN+8fdnLDRIZBa1IJZ2R6GZ0suX06KMZHR+RE2Z/vI3Nwz7kyAq3RkK9/HQ==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [win32] os: [win32]
@ -1144,8 +1110,8 @@ packages:
dev: false dev: false
optional: true optional: true
/@next/swc-win32-ia32-msvc/13.2.4-canary.0: /@next/swc-win32-ia32-msvc/13.2.5-canary.19:
resolution: {integrity: sha512-+KDwr1SMEbbCc9rMJlQSbyGJMrfcyjTfa7TpsRpYmb4g9fGFCpXfKpyS8MVStH/k1Ye27X1WQZsOsS9RIdRWGg==} resolution: {integrity: sha512-6EjXMAERpj71R1kGcocTtSefI33W4lN7L/ERtIwc1YUSw1wajsOHoWZBH0S9g6H632rxeuZi9oUfYbxOQnYsAQ==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [ia32] cpu: [ia32]
os: [win32] os: [win32]
@ -1153,8 +1119,8 @@ packages:
dev: false dev: false
optional: true optional: true
/@next/swc-win32-x64-msvc/13.2.4-canary.0: /@next/swc-win32-x64-msvc/13.2.5-canary.19:
resolution: {integrity: sha512-GMUCU4jFwHUDTMM5RZVHjLj+uR24vqUKrOhgT8SHPBVU05Rq68loq4frGU5HVl7m7jAk5TTjoIMLR6WuR8Qxmw==} resolution: {integrity: sha512-bEXNS4MbZwM/k1HjxZ3dfnkZWZZD81kQfY8yLCmuefXZ+YgcY2dh4AB6+CbgYKJ3GBCGqM5gpFu3/5fQZg+g1g==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
@ -1826,6 +1792,10 @@ packages:
'@types/node': 18.11.18 '@types/node': 18.11.18
dev: true dev: true
/@types/cookie/0.3.3:
resolution: {integrity: sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow==}
dev: false
/@types/debug/4.1.7: /@types/debug/4.1.7:
resolution: {integrity: sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==} resolution: {integrity: sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==}
dependencies: dependencies:
@ -1849,6 +1819,13 @@ packages:
'@types/unist': 2.0.6 '@types/unist': 2.0.6
dev: true dev: true
/@types/hoist-non-react-statics/3.3.1:
resolution: {integrity: sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==}
dependencies:
'@types/react': 18.0.27
hoist-non-react-statics: 3.3.2
dev: false
/@types/istanbul-lib-coverage/2.0.4: /@types/istanbul-lib-coverage/2.0.4:
resolution: {integrity: sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==} resolution: {integrity: sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==}
@ -2502,6 +2479,13 @@ packages:
base64-js: 1.5.1 base64-js: 1.5.1
ieee754: 1.2.1 ieee754: 1.2.1
/busboy/1.6.0:
resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==}
engines: {node: '>=10.16.0'}
dependencies:
streamsearch: 1.1.0
dev: false
/call-bind/1.0.2: /call-bind/1.0.2:
resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==}
dependencies: dependencies:
@ -2736,6 +2720,11 @@ packages:
/convert-source-map/2.0.0: /convert-source-map/2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
/cookie/0.4.2:
resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==}
engines: {node: '>= 0.6'}
dev: false
/cookie/0.5.0: /cookie/0.5.0:
resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
@ -4147,6 +4136,12 @@ packages:
space-separated-tokens: 2.0.1 space-separated-tokens: 2.0.1
dev: true dev: true
/hoist-non-react-statics/3.3.2:
resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
dependencies:
react-is: 16.13.1
dev: false
/html-escaper/2.0.2: /html-escaper/2.0.2:
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
@ -5743,7 +5738,7 @@ packages:
dev: true dev: true
optional: true optional: true
/next-auth/4.19.2_5d3uzjtamlpvvireij3yl2isni: /next-auth/4.19.2_gjjimu27ie6kivuv476ljuoy44:
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
@ -5758,7 +5753,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.2.4-canary.0_biqbaboplfbrettd7655fr4n2y next: 13.2.5-canary.19_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
@ -5768,14 +5763,14 @@ packages:
uuid: 8.3.2 uuid: 8.3.2
dev: false dev: false
/next-themes/0.2.1_5d3uzjtamlpvvireij3yl2isni: /next-themes/0.2.1_gjjimu27ie6kivuv476ljuoy44:
resolution: {integrity: sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==} resolution: {integrity: sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==}
peerDependencies: peerDependencies:
next: '*' next: '*'
react: '*' react: '*'
react-dom: '*' react-dom: '*'
dependencies: dependencies:
next: 13.2.4-canary.0_biqbaboplfbrettd7655fr4n2y next: 13.2.5-canary.19_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
@ -5791,12 +5786,12 @@ packages:
- supports-color - supports-color
dev: true dev: true
/next/13.2.4-canary.0_biqbaboplfbrettd7655fr4n2y: /next/13.2.5-canary.19_biqbaboplfbrettd7655fr4n2y:
resolution: {integrity: sha512-vWGAI05RK5y+qZRBtV3PylLtGEhU0vhC9RwgCJK9mxRdZdPgK6BcZMLYnQnNJNv8DGhYx3iNVfTMFaHGR4nBtw==} resolution: {integrity: sha512-PHDHdrlMaZu3YmFAKPfLGVQgl7pohUs9G7Q3HJVw9qmqEK3e1PDc6y8ny5sg6HXaIAdqrnMgzLLPyHpKAGZ6cA==}
engines: {node: '>=14.6.0'} engines: {node: '>=14.6.0'}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
'@opentelemetry/api': ^1.4.0 '@opentelemetry/api': ^1.4.1
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
@ -5812,27 +5807,24 @@ packages:
sass: sass:
optional: true optional: true
dependencies: dependencies:
'@next/env': 13.2.4-canary.0 '@next/env': 13.2.5-canary.19
'@swc/helpers': 0.4.14 '@swc/helpers': 0.4.14
busboy: 1.6.0
caniuse-lite: 1.0.30001458 caniuse-lite: 1.0.30001458
postcss: 8.4.14 postcss: 8.4.14
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
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.2.4-canary.0 '@next/swc-darwin-arm64': 13.2.5-canary.19
'@next/swc-android-arm64': 13.2.4-canary.0 '@next/swc-darwin-x64': 13.2.5-canary.19
'@next/swc-darwin-arm64': 13.2.4-canary.0 '@next/swc-linux-arm64-gnu': 13.2.5-canary.19
'@next/swc-darwin-x64': 13.2.4-canary.0 '@next/swc-linux-arm64-musl': 13.2.5-canary.19
'@next/swc-freebsd-x64': 13.2.4-canary.0 '@next/swc-linux-x64-gnu': 13.2.5-canary.19
'@next/swc-linux-arm-gnueabihf': 13.2.4-canary.0 '@next/swc-linux-x64-musl': 13.2.5-canary.19
'@next/swc-linux-arm64-gnu': 13.2.4-canary.0 '@next/swc-win32-arm64-msvc': 13.2.5-canary.19
'@next/swc-linux-arm64-musl': 13.2.4-canary.0 '@next/swc-win32-ia32-msvc': 13.2.5-canary.19
'@next/swc-linux-x64-gnu': 13.2.4-canary.0 '@next/swc-win32-x64-msvc': 13.2.5-canary.19
'@next/swc-linux-x64-musl': 13.2.4-canary.0
'@next/swc-win32-arm64-msvc': 13.2.4-canary.0
'@next/swc-win32-ia32-msvc': 13.2.4-canary.0
'@next/swc-win32-x64-msvc': 13.2.4-canary.0
transitivePeerDependencies: transitivePeerDependencies:
- '@babel/core' - '@babel/core'
- babel-plugin-macros - babel-plugin-macros
@ -6752,6 +6744,17 @@ packages:
minimist: 1.2.7 minimist: 1.2.7
strip-json-comments: 2.0.1 strip-json-comments: 2.0.1
/react-cookie/4.1.1_react@18.2.0:
resolution: {integrity: sha512-ffn7Y7G4bXiFbnE+dKhHhbP+b8I34mH9jqnm8Llhj89zF4nPxPutxHT1suUqMeCEhLDBI7InYwf1tpaSoK5w8A==}
peerDependencies:
react: '>= 16.3.0'
dependencies:
'@types/hoist-non-react-statics': 3.3.1
hoist-non-react-statics: 3.3.2
react: 18.2.0
universal-cookie: 4.0.4
dev: false
/react-datepicker/4.8.0_biqbaboplfbrettd7655fr4n2y: /react-datepicker/4.8.0_biqbaboplfbrettd7655fr4n2y:
resolution: {integrity: sha512-u69zXGHMpxAa4LeYR83vucQoUCJQ6m/WBsSxmUMu/M8ahTSVMMyiyQzauHgZA2NUr9y0FUgOAix71hGYUb6tvg==} resolution: {integrity: sha512-u69zXGHMpxAa4LeYR83vucQoUCJQ6m/WBsSxmUMu/M8ahTSVMMyiyQzauHgZA2NUr9y0FUgOAix71hGYUb6tvg==}
peerDependencies: peerDependencies:
@ -7392,6 +7395,11 @@ packages:
internal-slot: 1.0.4 internal-slot: 1.0.4
dev: true dev: true
/streamsearch/1.1.0:
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
engines: {node: '>=10.0.0'}
dev: false
/string-length/4.0.2: /string-length/4.0.2:
resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -7940,6 +7948,13 @@ packages:
unist-util-visit-parents: 5.1.1 unist-util-visit-parents: 5.1.1
dev: true dev: true
/universal-cookie/4.0.4:
resolution: {integrity: sha512-lbRVHoOMtItjWbM7TwDLdl8wug7izB0tq3/YVKhT/ahB4VDvWMyvnADfnJI8y6fSvsjh51Ix7lTGC6Tn4rMPhw==}
dependencies:
'@types/cookie': 0.3.3
cookie: 0.4.2
dev: false
/update-browserslist-db/1.0.10_browserslist@4.21.5: /update-browserslist-db/1.0.10_browserslist@4.21.5:
resolution: {integrity: sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==} resolution: {integrity: sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==}
hasBin: true hasBin: true

View file

@ -6,8 +6,10 @@ import Header from "@components/header"
import { Inter } from "next/font/google" import { Inter } from "next/font/google"
import { getMetadata } from "src/app/lib/metadata" import { getMetadata } from "src/app/lib/metadata"
import dynamic from "next/dynamic" import dynamic from "next/dynamic"
import { cookies } from "next/headers"
const inter = Inter({ subsets: ["latin"], variable: "--inter-font" }) const inter = Inter({ subsets: ["latin"], variable: "--inter-font" })
import { THEME_COOKIE, DEFAULT_THEME, SIGNED_IN_COOKIE } from "@lib/constants"
import { Suspense } from "react"
const CmdK = dynamic(() => import("@components/cmdk"), { ssr: false }) const CmdK = dynamic(() => import("@components/cmdk"), { ssr: false })
@ -16,6 +18,10 @@ export default async function RootLayout({
}: { }: {
children: React.ReactNode children: React.ReactNode
}) { }) {
const cookiesList = cookies()
const theme = cookiesList.get(THEME_COOKIE)?.value || DEFAULT_THEME
const isAuthenticated = Boolean(cookiesList.get(SIGNED_IN_COOKIE)?.value)
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>
@ -24,7 +30,9 @@ export default async function RootLayout({
<Providers> <Providers>
<Layout> <Layout>
<CmdK /> <CmdK />
<Header /> <Suspense fallback={<>Loading...</>}>
<Header theme={theme} isAuthenticated={isAuthenticated} />
</Suspense>
{children} {children}
</Layout> </Layout>
</Providers> </Providers>

View file

@ -2,6 +2,7 @@ import { Command } from "cmdk"
import { useTheme } from "next-themes" import { useTheme } from "next-themes"
import { useRouter } from "next/navigation" import { useRouter } from "next/navigation"
import { FilePlus, Moon, Search, Settings, Sun } from "react-feather" import { FilePlus, Moon, Search, Settings, Sun } from "react-feather"
import { setDriftTheme } from "src/app/lib/set-theme"
import { CmdKPage } from ".." import { CmdKPage } from ".."
import Item from "../item" import Item from "../item"
@ -41,7 +42,7 @@ export default function HomePage({
<Item <Item
shortcut="T" shortcut="T"
onSelect={() => { onSelect={() => {
setTheme(resolvedTheme === "dark" ? "light" : "dark") setDriftTheme(resolvedTheme === "dark" ? "light" : "dark", setTheme)
}} }}
icon={resolvedTheme === "dark" ? <Sun /> : <Moon />} icon={resolvedTheme === "dark" ? <Sun /> : <Moon />}
> >

View file

@ -0,0 +1,3 @@
.active {
color: var(--fg) !important;
}

View file

@ -0,0 +1,179 @@
"use client"
import { useSelectedLayoutSegment } from "next/navigation"
import FadeIn from "@components/fade-in"
import { setDriftTheme } from "src/app/lib/set-theme"
import {
Home,
Moon,
PlusCircle,
Settings,
Sun,
User,
UserX
} from "react-feather"
import { signOut } from "next-auth/react"
import Button from "@components/button"
import Link from "@components/link"
import { useSessionSWR } from "@lib/use-session-swr"
import { useTheme } from "next-themes"
import styles from "./buttons.module.css"
// constant width for sign in / sign out buttons to avoid CLS
const SIGN_IN_WIDTH = 110
type Tab = {
name: string
icon: JSX.Element
value: string
width?: number
} & (
| {
onClick: () => void
href?: undefined
}
| {
onClick?: undefined
href: string
}
)
export function HeaderButtons({
isAuthenticated,
theme
}: {
isAuthenticated: boolean
theme: string
}) {
const { isAdmin } = useSessionSWR()
return (
<>
{getButtons({
isAuthenticated,
theme,
isAdmin
})}
</>
)
}
function NavButton(tab: Tab) {
const segment = useSelectedLayoutSegment()
const isActive = segment === tab.value.toLowerCase()
const activeStyle = isActive ? styles.active : undefined
if (tab.onClick) {
return (
<Button
key={tab.value}
iconLeft={tab.icon}
onClick={tab.onClick}
className={activeStyle}
aria-label={tab.name}
aria-current={isActive ? "page" : undefined}
data-tab={tab.value}
width={tab.width}
>
{tab.name ? tab.name : undefined}
</Button>
)
} else {
return (
<Link key={tab.value} href={tab.href} data-tab={tab.value}>
<Button className={activeStyle} iconLeft={tab.icon} width={tab.width}>
{tab.name ? tab.name : undefined}
</Button>
</Link>
)
}
}
function ThemeButton({ theme }: { theme: string }) {
const { setTheme } = useTheme()
return (
<NavButton
name="Theme"
icon={theme === "dark" ? <Sun /> : <Moon />}
value="dark"
onClick={() => {
setDriftTheme(theme === "dark" ? "light" : "dark", setTheme)
}}
key="theme"
/>
)
}
/** For use by mobile */
export function getButtons({
isAuthenticated,
theme,
// mutate: mutateSession,
isAdmin
}: {
isAuthenticated: boolean
theme: string
// mutate: KeyedMutator<Session>
isAdmin?: boolean
}) {
return [
<NavButton
key="home"
name="Home"
icon={<Home />}
value="home"
href="/home"
/>,
<NavButton
key="new"
name="New"
icon={<PlusCircle />}
value="new"
href="/new"
/>,
<NavButton
key="yours"
name="Yours"
icon={<User />}
value="mine"
href="/mine"
/>,
<NavButton
name="Settings"
icon={<Settings />}
value="settings"
href="/settings"
key="settings"
/>,
<ThemeButton key="theme-button" theme={theme} />,
isAuthenticated === true ? (
<NavButton
name="Sign Out"
icon={<UserX />}
value="signout"
onClick={() => {
signOut()
}}
width={SIGN_IN_WIDTH}
/>
) : undefined,
isAuthenticated === false ? (
<NavButton
name="Sign In"
icon={<User />}
value="signin"
href="/signin"
width={SIGN_IN_WIDTH}
/>
) : undefined,
isAdmin ? (
<FadeIn>
<NavButton
name="Admin"
icon={<Settings />}
value="admin"
href="/admin"
/>
</FadeIn>
) : undefined
].filter(Boolean)
}

View file

@ -47,10 +47,6 @@
align-items: center; align-items: center;
} }
.tabs .active {
color: var(--fg);
}
.contentWrapper { .contentWrapper {
background: var(--bg); background: var(--bg);
margin-left: var(--gap); margin-left: var(--gap);

View file

@ -1,215 +1,32 @@
"use client"
import styles from "./header.module.css" import styles from "./header.module.css"
import { getButtons, HeaderButtons } from "./buttons"
// import useUserData from "@lib/hooks/use-user-data"
import Link from "@components/link"
import { usePathname } from "next/navigation"
import { signOut } from "next-auth/react"
import Button from "@components/button"
import { useTheme } from "next-themes"
import {
Home,
Loader,
Moon,
PlusCircle,
Settings,
Sun,
User,
UserX
} from "react-feather"
import { ReactNode, useEffect, useMemo, useState } from "react"
import { useSessionSWR } from "@lib/use-session-swr"
import FadeIn from "@components/fade-in"
import MobileHeader from "./mobile" import MobileHeader from "./mobile"
import { useMemo } from "react"
// constant width for sign in / sign out buttons to avoid CLS export default function Header({
const SIGN_IN_WIDTH = 110 theme,
isAuthenticated
type Tab = { }: {
name: string theme: string
icon: ReactNode isAuthenticated: boolean
value: string }) {
// onClick?: () => void const memoHeaderButtons = useMemo(
// href?: string () => (
width?: number <>
} & ( <HeaderButtons isAuthenticated={isAuthenticated} theme={theme} />
| { </>
onClick: () => void ),
href?: undefined [isAuthenticated, theme]
} )
| {
onClick?: undefined
href: string
}
)
const Header = () => {
const {
isAdmin,
isAuthenticated,
isLoading: isAuthLoading,
mutate: mutateSession
} = useSessionSWR()
const pathname = usePathname()
const { setTheme, resolvedTheme } = useTheme()
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])
// const buttons = pages.map(NavButton)
const buttons = useMemo(() => {
const NavButton = (tab: Tab) => {
const isActive = `${pathname}` === tab.href
const activeStyle = isActive ? styles.active : undefined
if (tab.onClick) {
return (
<Button
key={tab.value}
iconLeft={tab.icon}
onClick={tab.onClick}
className={activeStyle}
aria-label={tab.name}
aria-current={isActive ? "page" : undefined}
data-tab={tab.value}
width={tab.width}
>
{tab.name ? tab.name : undefined}
</Button>
)
} else {
return (
<Link key={tab.value} href={tab.href} data-tab={tab.value}>
<Button
className={activeStyle}
iconLeft={tab.icon}
width={tab.width}
>
{tab.name ? tab.name : undefined}
</Button>
</Link>
)
}
}
const NavButtonPlaceholder = ({ width }: { width: number }) => {
return (
<Button
key="placeholder"
iconLeft={<></>}
aria-current={undefined}
aria-hidden
style={{ color: "transparent" }}
width={width}
/>
)
}
const getThemeIcon = () => {
if (!mounted) {
return <Loader />
}
return <FadeIn>{resolvedTheme === "light" ? <Moon /> : <Sun />}</FadeIn>
}
return [
<NavButton
key="home"
name="Home"
icon={<Home />}
value="home"
href="/home"
/>,
<NavButton
key="new"
name="New"
icon={<PlusCircle />}
value="new"
href="/new"
/>,
<NavButton
key="yours"
name="Yours"
icon={<User />}
value="yours"
href="/mine"
/>,
<NavButton
name="Settings"
icon={<Settings />}
value="settings"
href="/settings"
key="settings"
/>,
<NavButton
name="Theme"
icon={getThemeIcon()}
value="dark"
onClick={() => {
setTheme(resolvedTheme === "light" ? "dark" : "light")
}}
key="theme"
/>,
isAuthLoading ? (
<NavButtonPlaceholder width={SIGN_IN_WIDTH} key="signin" />
) : undefined,
isAuthenticated === true ? (
<FadeIn key="signout-fade">
<NavButton
name="Sign Out"
icon={<UserX />}
value="signout"
onClick={() => {
signOut()
mutateSession(undefined)
}}
width={SIGN_IN_WIDTH}
/>
</FadeIn>
) : undefined,
isAuthenticated === false ? (
<FadeIn key="signin-fade">
<NavButton
name="Sign In"
icon={<User />}
value="signin"
href="/signin"
width={SIGN_IN_WIDTH}
/>
</FadeIn>
) : undefined,
isAdmin ? (
<FadeIn>
<NavButton
name="Admin"
icon={<Settings />}
value="admin"
href="/admin"
/>
</FadeIn>
) : undefined
].filter(Boolean)
}, [
isAuthLoading,
isAuthenticated,
isAdmin,
pathname,
mounted,
resolvedTheme,
setTheme,
mutateSession
])
return ( return (
<header className={styles.header}> <header className={styles.header}>
<div className={styles.tabs}> <div className={styles.tabs}>
<div className={styles.buttons}>{buttons}</div> <div className={styles.buttons}>
<HeaderButtons isAuthenticated={isAuthenticated} theme={theme} />
</div>
</div> </div>
<MobileHeader buttons={buttons} /> <MobileHeader isAuthenticated={isAuthenticated} theme={theme} />
</header> </header>
) )
} }
export default Header

View file

@ -1,11 +1,28 @@
"use client"
import * as DropdownMenu from "@radix-ui/react-dropdown-menu" import * as DropdownMenu from "@radix-ui/react-dropdown-menu"
import buttonStyles from "@components/button/button.module.css" import buttonStyles from "@components/button/button.module.css"
import Button from "@components/button" import Button from "@components/button"
import { Menu } from "react-feather" import { Menu } from "react-feather"
import clsx from "clsx" import clsx from "clsx"
import styles from "./mobile.module.css" import styles from "./mobile.module.css"
import { getButtons } from "./buttons"
import { useSessionSWR } from "@lib/use-session-swr"
export default function MobileHeader({
isAuthenticated,
theme
}: {
isAuthenticated: boolean
theme: string
}) {
const { isAdmin } = useSessionSWR()
const buttons = getButtons({
isAuthenticated,
theme,
isAdmin
})
export default function MobileHeader({ buttons }: { buttons: JSX.Element[] }) {
// TODO: this is a hack to close the radix ui menu when a next link is clicked // TODO: this is a hack to close the radix ui menu when a next link is clicked
const onClick = () => { const onClick = () => {
document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape" })) document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape" }))

8
src/app/lib/set-theme.ts Normal file
View file

@ -0,0 +1,8 @@
import { THEME_COOKIE } from "@lib/constants"
import { Cookies } from "react-cookie"
export function setDriftTheme(theme: string, setter: (theme: string) => void) {
setter(theme)
const cookies = new Cookies()
cookies.set(THEME_COOKIE, theme, { path: "/" })
}

View file

@ -7,8 +7,10 @@ export function isAllowedVisibilityForWebpage(
) { ) {
return ALLOWED_VISIBILITIES_FOR_WEBPAGE.includes(visibility) return ALLOWED_VISIBILITIES_FOR_WEBPAGE.includes(visibility)
} }
export const DEFAULT_THEME = "dark"
export const SIGNED_IN_COOKIE = "next-auth.session-token" export const SIGNED_IN_COOKIE = "next-auth.session-token"
export const THEME_COOKIE = "drift-theme"
// Code files for uploading with drag and drop and syntax highlighting // Code files for uploading with drag and drop and syntax highlighting
export const allowedFileTypes = [ export const allowedFileTypes = [

View file

@ -1,7 +1,7 @@
import { Session } from "next-auth" import { Session } from "next-auth"
import useSWR from "swr" import useSWR, { SWRConfiguration } from "swr"
export function useSessionSWR() { export function useSessionSWR(swrOpts: SWRConfiguration = {}) {
const { const {
data: session, data: session,
error, error,
@ -9,7 +9,9 @@ export function useSessionSWR() {
isValidating, isValidating,
mutate mutate
} = useSWR<Session>("/api/auth/session", { } = useSWR<Session>("/api/auth/session", {
fetcher: (url) => fetch(url).then((res) => res.json()) as Promise<Session> fetcher: (url) => fetch(url).then((res) => res.json()) as Promise<Session>,
revalidateOnFocus: false,
...swrOpts
}) })
return { return {

View file

@ -2,6 +2,8 @@ import { getToken } from "next-auth/jwt"
import { withAuth } from "next-auth/middleware" import { withAuth } from "next-auth/middleware"
import { NextResponse } from "next/server" import { NextResponse } from "next/server"
const PAGES_REQUIRE_AUTH = ["/new", "/settings", "/mine", "/admin"]
export default withAuth( export default withAuth(
async function middleware(req) { async function middleware(req) {
const token = await getToken({ req }) const token = await getToken({ req })
@ -11,17 +13,19 @@ export default withAuth(
req.nextUrl.pathname.startsWith("/signup") || req.nextUrl.pathname.startsWith("/signup") ||
req.nextUrl.pathname.startsWith("/signin") req.nextUrl.pathname.startsWith("/signin")
const isPageRequireAuth = PAGES_REQUIRE_AUTH.includes(req.nextUrl.pathname)
if (isAuthPage) { if (isAuthPage) {
if (isAuthed) { if (isAuthed) {
return NextResponse.redirect(new URL("/new", req.url)) return NextResponse.redirect(new URL("/new", req.url))
} }
return null return null
} } else if (isPageRequireAuth && !isAuthed) {
if (!isAuthed) {
return NextResponse.redirect(new URL("/signin", req.url)) return NextResponse.redirect(new URL("/signin", req.url))
} }
return NextResponse.next()
}, },
{ {
callbacks: { callbacks: {
@ -37,11 +41,13 @@ export default withAuth(
export const config = { export const config = {
matcher: [ matcher: [
// "/signout", /*
// "/", * Match all request paths except for the ones starting with:
"/signin", * - api (API routes)
"/signup", * - _next/static (static files)
"/new", * - _next/image (image optimization files)
"/mine", * - favicon.ico (favicon file)
*/
"/((?!api|_next/static|_next/image|favicon.ico).*)"
] ]
} }