Compare commits

..

10 Commits

Author SHA1 Message Date
Ben Ramey
7a798c1bcd Styling 2022-08-27 16:58:01 -05:00
Ben Ramey
f46da9a619 Add _document.js 2022-08-27 16:02:34 -05:00
Ben Ramey
c45d304fdd Use tailwind css 2022-08-27 14:23:30 -05:00
696ca6a393 Progress 2022-08-27 08:14:10 -05:00
f65ae56f74 Generic api for any entity 2022-08-12 08:57:36 -05:00
d1495df5a1 A few more api call basics 2022-07-05 21:15:42 -05:00
2fa58305cb Move api to v1 folder 2022-06-27 09:08:26 -05:00
6fe79a1892 Starting on working bookmarks api 2022-06-27 09:01:36 -05:00
4c37658cd8 Auth0 working! 2022-05-11 09:23:32 -05:00
002c6a2f3d Update env variables to connect to AWS 2022-05-10 09:24:48 -05:00
33 changed files with 6107 additions and 294 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
node_modules/

454
package-lock.json generated Normal file
View File

@@ -0,0 +1,454 @@
{
"requires": true,
"lockfileVersion": 1,
"dependencies": {
"@auth0/nextjs-auth0": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@auth0/nextjs-auth0/-/nextjs-auth0-1.8.0.tgz",
"integrity": "sha512-lOI7i1khyuvVUGD/V9k+YMVxj3LB74KYqrCRQ8Uezai8YEAPHhExGMpIlFUPV7J4iPIuNNhW/4TYa45nkPNr0w==",
"requires": {
"base64url": "^3.0.1",
"cookie": "^0.5.0",
"debug": "^4.3.4",
"futoin-hkdf": "^1.5.0",
"http-errors": "^1.8.1",
"joi": "^17.6.0",
"jose": "^2.0.5",
"on-headers": "^1.0.2",
"openid-client": "^4.9.1",
"tslib": "^2.3.1",
"url-join": "^4.0.1"
}
},
"@hapi/hoek": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
"integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ=="
},
"@hapi/topo": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz",
"integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==",
"requires": {
"@hapi/hoek": "^9.0.0"
}
},
"@panva/asn1.js": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@panva/asn1.js/-/asn1.js-1.0.0.tgz",
"integrity": "sha512-UdkG3mLEqXgnlKsWanWcgb6dOjUzJ+XC5f+aWw30qrtjxeNUSfKX1cd5FBzOaXQumoe9nIqeZUvrRJS03HCCtw=="
},
"@sideway/address": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz",
"integrity": "sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==",
"requires": {
"@hapi/hoek": "^9.0.0"
}
},
"@sideway/formula": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.0.tgz",
"integrity": "sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg=="
},
"@sideway/pinpoint": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz",
"integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ=="
},
"@sindresorhus/is": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
"integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw=="
},
"@szmarczak/http-timer": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
"integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==",
"requires": {
"defer-to-connect": "^2.0.0"
}
},
"@types/cacheable-request": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.2.tgz",
"integrity": "sha512-B3xVo+dlKM6nnKTcmm5ZtY/OL8bOAOd2Olee9M1zft65ox50OzjEHW91sDiU9j6cvW8Ejg1/Qkf4xd2kugApUA==",
"requires": {
"@types/http-cache-semantics": "*",
"@types/keyv": "*",
"@types/node": "*",
"@types/responselike": "*"
}
},
"@types/http-cache-semantics": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz",
"integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ=="
},
"@types/json-buffer": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/json-buffer/-/json-buffer-3.0.0.tgz",
"integrity": "sha512-3YP80IxxFJB4b5tYC2SUPwkg0XQLiu0nWvhRgEatgjf+29IcWO9X1k8xRv5DGssJ/lCrjYTjQPcobJr2yWIVuQ=="
},
"@types/keyv": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz",
"integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==",
"requires": {
"@types/node": "*"
}
},
"@types/node": {
"version": "17.0.32",
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.32.tgz",
"integrity": "sha512-eAIcfAvhf/BkHcf4pkLJ7ECpBAhh9kcxRBpip9cTiO+hf+aJrsxYxBeS6OXvOd9WqNAJmavXVpZvY1rBjNsXmw=="
},
"@types/responselike": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz",
"integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==",
"requires": {
"@types/node": "*"
}
},
"aggregate-error": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
"integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==",
"requires": {
"clean-stack": "^2.0.0",
"indent-string": "^4.0.0"
}
},
"base64url": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz",
"integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A=="
},
"cacheable-lookup": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz",
"integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA=="
},
"cacheable-request": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.2.tgz",
"integrity": "sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==",
"requires": {
"clone-response": "^1.0.2",
"get-stream": "^5.1.0",
"http-cache-semantics": "^4.0.0",
"keyv": "^4.0.0",
"lowercase-keys": "^2.0.0",
"normalize-url": "^6.0.1",
"responselike": "^2.0.0"
}
},
"clean-stack": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
"integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A=="
},
"clone-response": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz",
"integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=",
"requires": {
"mimic-response": "^1.0.0"
}
},
"compress-brotli": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/compress-brotli/-/compress-brotli-1.3.8.tgz",
"integrity": "sha512-lVcQsjhxhIXsuupfy9fmZUFtAIdBmXA7EGY6GBdgZ++qkM9zG4YFT8iU7FoBxzryNDMOpD1HIFHUSX4D87oqhQ==",
"requires": {
"@types/json-buffer": "~3.0.0",
"json-buffer": "~3.0.1"
}
},
"cookie": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw=="
},
"debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"requires": {
"ms": "2.1.2"
}
},
"decompress-response": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
"requires": {
"mimic-response": "^3.1.0"
},
"dependencies": {
"mimic-response": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="
}
}
},
"defer-to-connect": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",
"integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg=="
},
"depd": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
"integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
},
"end-of-stream": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
"requires": {
"once": "^1.4.0"
}
},
"futoin-hkdf": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/futoin-hkdf/-/futoin-hkdf-1.5.0.tgz",
"integrity": "sha512-4CerDhtTgx4i5PKccQIpEp4T9wqmosPIP9Kep35SdCpYkQeriD3zddUVhrO1Fc4QvGhsAnd2rXyoOr5047mJEg=="
},
"get-stream": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
"integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
"requires": {
"pump": "^3.0.0"
}
},
"got": {
"version": "11.8.3",
"resolved": "https://registry.npmjs.org/got/-/got-11.8.3.tgz",
"integrity": "sha512-7gtQ5KiPh1RtGS9/Jbv1ofDpBFuq42gyfEib+ejaRBJuj/3tQFeR5+gw57e4ipaU8c/rCjvX6fkQz2lyDlGAOg==",
"requires": {
"@sindresorhus/is": "^4.0.0",
"@szmarczak/http-timer": "^4.0.5",
"@types/cacheable-request": "^6.0.1",
"@types/responselike": "^1.0.0",
"cacheable-lookup": "^5.0.3",
"cacheable-request": "^7.0.2",
"decompress-response": "^6.0.0",
"http2-wrapper": "^1.0.0-beta.5.2",
"lowercase-keys": "^2.0.0",
"p-cancelable": "^2.0.0",
"responselike": "^2.0.0"
}
},
"http-cache-semantics": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz",
"integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ=="
},
"http-errors": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz",
"integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==",
"requires": {
"depd": "~1.1.2",
"inherits": "2.0.4",
"setprototypeof": "1.2.0",
"statuses": ">= 1.5.0 < 2",
"toidentifier": "1.0.1"
}
},
"http2-wrapper": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz",
"integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==",
"requires": {
"quick-lru": "^5.1.1",
"resolve-alpn": "^1.0.0"
}
},
"indent-string": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
"integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="
},
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"joi": {
"version": "17.6.0",
"resolved": "https://registry.npmjs.org/joi/-/joi-17.6.0.tgz",
"integrity": "sha512-OX5dG6DTbcr/kbMFj0KGYxuew69HPcAE3K/sZpEV2nP6e/j/C0HV+HNiBPCASxdx5T7DMoa0s8UeHWMnb6n2zw==",
"requires": {
"@hapi/hoek": "^9.0.0",
"@hapi/topo": "^5.0.0",
"@sideway/address": "^4.1.3",
"@sideway/formula": "^3.0.0",
"@sideway/pinpoint": "^2.0.0"
}
},
"jose": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/jose/-/jose-2.0.5.tgz",
"integrity": "sha512-BAiDNeDKTMgk4tvD0BbxJ8xHEHBZgpeRZ1zGPPsitSyMgjoMWiLGYAE7H7NpP5h0lPppQajQs871E8NHUrzVPA==",
"requires": {
"@panva/asn1.js": "^1.0.0"
}
},
"json-buffer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="
},
"keyv": {
"version": "4.2.7",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.2.7.tgz",
"integrity": "sha512-HeOstD8SXvtWoQhMMBCelcUuZsiV7T7MwsADtOXT0KuwYP9nCxrSoMDeLXNDTLN3VFSuRp38JzoGbbTboq3QQw==",
"requires": {
"compress-brotli": "^1.3.8",
"json-buffer": "3.0.1"
}
},
"lowercase-keys": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
"integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="
},
"lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"requires": {
"yallist": "^4.0.0"
}
},
"make-error": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw=="
},
"mimic-response": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
"integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"normalize-url": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
"integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A=="
},
"object-hash": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz",
"integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw=="
},
"oidc-token-hash": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.1.tgz",
"integrity": "sha512-EvoOtz6FIEBzE+9q253HsLCVRiK/0doEJ2HCvvqMQb3dHZrP3WlJKYtJ55CRTw4jmYomzH4wkPuCj/I3ZvpKxQ=="
},
"on-headers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
"integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA=="
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"requires": {
"wrappy": "1"
}
},
"openid-client": {
"version": "4.9.1",
"resolved": "https://registry.npmjs.org/openid-client/-/openid-client-4.9.1.tgz",
"integrity": "sha512-DYUF07AHjI3QDKqKbn2F7RqozT4hyi4JvmpodLrq0HHoNP7t/AjeG/uqiBK1/N2PZSAQEThVjDLHSmJN4iqu/w==",
"requires": {
"aggregate-error": "^3.1.0",
"got": "^11.8.0",
"jose": "^2.0.5",
"lru-cache": "^6.0.0",
"make-error": "^1.3.6",
"object-hash": "^2.0.1",
"oidc-token-hash": "^5.0.1"
}
},
"p-cancelable": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz",
"integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg=="
},
"pump": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
"integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
"requires": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
},
"quick-lru": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
"integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="
},
"resolve-alpn": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz",
"integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="
},
"responselike": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.0.tgz",
"integrity": "sha512-xH48u3FTB9VsZw7R+vvgaKeLKzT6jOogbQhEe/jewwnZgzPcnyWui2Av6JpoYZF/91uueC+lqhWqeURw5/qhCw==",
"requires": {
"lowercase-keys": "^2.0.0"
}
},
"setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
},
"statuses": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
},
"toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="
},
"tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
},
"url-join": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz",
"integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA=="
},
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
},
"yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
}
}
}

View File

@@ -1,34 +1,5 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
# forget me not site
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file.
[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`.
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
Using
- nextjs
- tailwindcss

48
web/components/header.js Normal file
View File

@@ -0,0 +1,48 @@
import Link from "next/link";
function Header({ user, loading }) {
return (
<header className="container mx-auto flex justify-center flex-col">
<h1 className="text-4xl mx-auto">forget me not</h1>
<nav className="mx-auto mt-2">
<ul className="flex justify-center flex-row">
<li className="mx-3">
<Link href="/">
<a>Home</a>
</Link>
</li>
<li className="mx-3">
<Link href="/about">
<a>About</a>
</Link>
</li>
{!loading &&
(user ? (
<>
<li className="mx-3">
<Link href="/profile">
<a>Client-rendered profile</a>
</Link>
</li>
<li className="mx-3">
<Link href="/advanced/ssr-profile">
<a>Server rendered profile (advanced)</a>
</Link>
</li>
<li className="mx-3">
<a href="/api/v1/logout">Logout</a>
</li>
</>
) : (
<li className="mx-3">
<a href="/api/v1/login">Login</a>
</li>
))}
</ul>
</nav>
</header>
);
}
export default Header;

20
web/components/layout.js Normal file
View File

@@ -0,0 +1,20 @@
import Head from "next/head";
import Header from "./header";
function Layout({ user, loading = false, children }) {
return (
<>
<Head>
<title>forget me not</title>
</Head>
<Header user={user} loading={loading} />
<main>
<div className="container mx-auto">{children}</div>
</main>
</>
);
}
export default Layout;

View File

@@ -0,0 +1,11 @@
import Link from "next/link";
function WantToButton({ href, text }) {
return (
<Link href={href}>
<a className="py-1 px-2 mr-1 rounded border text-xl">{text}</a>
</Link>
);
}
export default WantToButton;

24
web/lib/auth0.js Normal file
View File

@@ -0,0 +1,24 @@
import { initAuth0 } from "@auth0/nextjs-auth0";
export default initAuth0({
secret: process.env.SESSION_COOKIE_SECRET,
issuerBaseURL: process.env.NEXT_PUBLIC_AUTH0_DOMAIN,
baseURL: process.env.NEXT_PUBLIC_BASE_URL,
clientID: process.env.NEXT_PUBLIC_AUTH0_CLIENT_ID,
clientSecret: process.env.AUTH0_CLIENT_SECRET,
routes: {
callback:
process.env.NEXT_PUBLIC_REDIRECT_URI ||
"http://localhost:3000/api/v1/callback",
postLogoutRedirect:
process.env.NEXT_PUBLIC_POST_LOGOUT_REDIRECT_URI ||
"http://localhost:3000",
},
authorizationParams: {
response_type: "code",
scope: process.env.NEXT_PUBLIC_AUTH0_SCOPE,
},
session: {
absoluteDuration: process.env.SESSION_COOKIE_LIFETIME,
},
});

13
web/lib/db.js Normal file
View File

@@ -0,0 +1,13 @@
const AWS = require("aws-sdk");
AWS.config.update({
accessKeyId: process.env.FMN_AWS_ACCESS_KEY_ID, // Do NOT HARD-CODE your secret credentials here
secretAccessKey: process.env.FMN_AWS_SECRET_ACCESS_KEY, // Do NOT HARD-CODE your secret credentials here
region: process.env.FMN_AWS_REGION,
});
const dynamodb = new AWS.DynamoDB.DocumentClient({
region: process.env.FMN_AWS_REGION,
});
export default dynamodb;

42
web/lib/jsonapi.js Normal file
View File

@@ -0,0 +1,42 @@
import auth0 from "./auth0";
async function jsonApi(handler) {
var unauthenticationHandler = async (req, res) => {
try {
await handler(req, res);
} catch (error) {
handleError(res, error);
}
res.end();
};
return unauthenticationHandler;
}
function authenticatedJsonApi(handler) {
var authenticatedHandler = async (req, res) => {
try {
const session = await auth0.getSession(req, res);
if (!session || !session.user) {
res.status(401);
} else {
await handler(req, res, session);
}
} catch (error) {
handleError(res, error);
}
res.end();
};
return authenticatedHandler;
}
function handleError(res, error) {
console.error(error);
res.status(error.status || 500).end(error.message);
}
export { jsonApi, authenticatedJsonApi };

View File

@@ -0,0 +1,22 @@
import Ajv from "ajv";
import addFormats from "ajv-formats";
const ajv = new Ajv();
addFormats(ajv);
const validateId = ajv.compile({
type: "string",
minLength: 1,
maxLength: 10,
});
const validatePostBody = ajv.compile({
type: "object",
properties: {
title: { type: "string" },
url: { type: "string", format: "uri" },
},
required: ["title", "url"],
});
export { validateId, validatePostBody };

3
web/lib/models/index.js Normal file
View File

@@ -0,0 +1,3 @@
export default {
bookmarks: require("./bookmarks"),
};

73
web/lib/user.js Normal file
View File

@@ -0,0 +1,73 @@
import { useState, useEffect } from "react";
export async function fetchUser(cookie = "") {
if (typeof window !== "undefined" && window.__user) {
return window.__user;
}
const res = await fetch(
"/api/v1/me",
cookie
? {
headers: {
cookie,
},
}
: {}
);
if (!res.ok) {
delete window.__user;
return null;
}
const json = await res.json();
if (typeof window !== "undefined") {
window.__user = json;
}
return json;
}
export function useFetchUser({ required } = {}) {
const [loading, setLoading] = useState(
() => !(typeof window !== "undefined" && window.__user)
);
const [user, setUser] = useState(() => {
if (typeof window === "undefined") {
return null;
}
return window.__user || null;
});
useEffect(
() => {
if (!loading && user) {
return;
}
setLoading(true);
let isMounted = true;
fetchUser().then((user) => {
// Only set the user if the component is still mounted
if (isMounted) {
// When the user is not logged in but login is required
if (required && !user) {
window.location.href = "/api/v1/login";
return;
}
setUser(user);
setLoading(false);
}
});
return () => {
isMounted = false;
};
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
return { user, loading };
}

View File

@@ -1,6 +1,23 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
async headers() {
return [
{
source: "/api/:path*",
headers: [
{
key: "Content-Type",
value: "application/vnd.api+json"
},
{
key: "x-testing",
value: "true"
}
]
}
]
}
}
module.exports = nextConfig

5089
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,14 +9,20 @@
"lint": "next lint"
},
"dependencies": {
"@auth0/nextjs-auth0": "^1.8.0",
"ajv": "^8.11.0",
"ajv-formats": "^2.1.1",
"aws-sdk": "^2.1131.0",
"next": "12.1.6",
"react": "18.1.0",
"react-dom": "18.1.0"
},
"devDependencies": {
"autoprefixer": "^10.4.8",
"eslint": "8.15.0",
"eslint-config-next": "12.1.6",
"prettier": "^2.6.2"
"postcss": "^8.4.16",
"prettier": "^2.6.2",
"tailwindcss": "^3.1.8"
}
}

13
web/pages/_document.js Normal file
View File

@@ -0,0 +1,13 @@
import { Html, Head, Main, NextScript } from "next/document";
export default function Document() {
return (
<Html dir="ltr">
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
);
}

19
web/pages/about.js Normal file
View File

@@ -0,0 +1,19 @@
import Layout from "../components/layout";
import { useFetchUser } from "../lib/user";
function About() {
const { user, loading } = useFetchUser();
return (
<Layout user={user} loading={loading}>
<h1>About</h1>
<p>
This is the about page, navigating between this page and <i>Home</i> is
always pretty fast. However, when you navigate to the <i>Profile</i>{" "}
page it takes more time because it uses SSR to fetch the user first;
</p>
</Layout>
);
}
export default About;

View File

@@ -0,0 +1,37 @@
// This import is only included in the server build, because it's only used by getServerSideProps
import auth0 from "../../lib/auth0";
import Layout from "../../components/layout";
function Profile({ user }) {
return (
<Layout user={user}>
<h1>Profile</h1>
<div>
<h3>Profile (server rendered)</h3>
<img src={user.picture} alt="user picture" />
<p>nickname: {user.nickname}</p>
<p>name: {user.name}</p>
</div>
</Layout>
);
}
export async function getServerSideProps({ req, res }) {
// Here you can check authentication status directly before rendering the page,
// however the page would be a serverless function, which is more expensive and
// slower than a static page with client side authentication
const session = await auth0.getSession(req, res);
if (!session || !session.user) {
res.writeHead(302, {
Location: "/api/v1/login",
});
res.end();
return;
}
return { props: { user: session.user } };
}
export default Profile;

View File

@@ -1,19 +0,0 @@
const AWS = require("aws-sdk");
const dynamodb = new AWS.DynamoDB.DocumentClient({
region: process.env.AWS_REGION,
});
const handler = async (req, res) => {
const params = {
TableName: process.env.db,
KeyConditionExpression: "hk = :hk",
ExpressionAttributeValues: { ":hk": "benramey@fastmail.com" },
};
let user = await dynamodb.query(params).promise();
res.status(200).json(user);
};
export default handler;

View File

@@ -0,0 +1,53 @@
import db from "../../../../lib/db";
import { authenticatedJsonApi } from "../../../../lib/jsonapi";
import models from "../../../../lib/models";
const handler = authenticatedJsonApi(async (req, res, session) => {
const { method } = req;
const { user } = session;
switch (method) {
case "GET":
await getEntity(req, res, user);
break;
default:
res.setHeader("Allow", ["GET"]);
res.status(405).send(`Method ${method} Not Allowed`);
}
});
const getEntity = async (req, res, user) => {
const { entityName, entityId } = req.query;
if (!models[entityName]) {
res.status(404);
return;
}
var model = models[entityName];
if (!model.validateId(entityId)) {
console.warn(model.validateId.errors);
res.status(404);
return;
}
const params = {
TableName: process.env.FMN_DYNAMODB_TABLENAME,
KeyConditionExpression: "hk = :hk AND sk = :sk",
ExpressionAttributeValues: {
":hk": `user#${user.sub}`,
":sk": `${entityName}#${entityId}`,
},
};
let entities = await db.query(params).promise();
if (entities.Items.length == 1) {
res.status(200).json(entities.Items[0]);
} else {
res.status(404);
}
};
export default handler;

View File

@@ -0,0 +1,73 @@
import db from "../../../../lib/db";
import { authenticatedJsonApi } from "../../../../lib/jsonapi";
import models from "../../../../lib/models";
const handler = authenticatedJsonApi(async (req, res, session) => {
const { method } = req;
const { user } = session;
switch (method) {
case "GET":
await getEntities(req, res, user);
break;
case "POST":
await createEntity(req, res, user);
break;
default:
res.setHeader("Allow", ["GET", "POST"]);
res.status(405).json(`Method ${method} Not Allowed`);
}
});
const getEntities = async (req, res, user) => {
const { entityName } = req.query;
if (!models[entityName]) {
res.status(404);
return;
}
const params = {
TableName: process.env.FMN_DYNAMODB_TABLENAME,
KeyConditionExpression: "hk = :hk AND begins_with(sk, :sk)",
ExpressionAttributeValues: {
":hk": `user#${user.sub}`,
":sk": `${entityName}#`,
},
};
let entities = await db.query(params).promise();
res.status(200).json(entities.Items);
};
const createEntity = async (req, res, user) => {
const { entityName } = req.query;
if (!models[entityName]) {
res.status(404);
return;
}
var model = models[entityName];
if (!model.validatePostBody(req.body)) {
console.warn(validatePostBody.errors);
res.status(400).json(validatePostBody.errors);
return;
}
const params = {
TableName: process.env.FMN_DYNAMODB_TABLENAME,
Item: {
hk: `user#${user.sub}`,
sk: `${entityName}#${"abc"}`,
title: req.body.title,
url: req.body.url,
},
};
await db.put(params).promise();
res.status(200).json(params.Item);
};
export default handler;

View File

@@ -0,0 +1,7 @@
import auth0 from "../../../lib/auth0";
const callback = async (req, res) => {
await auth0.handleCallback(req, res);
};
export default callback;

View File

@@ -0,0 +1,7 @@
import auth0 from "../../../lib/auth0";
const login = async (req, res) => {
await auth0.handleLogin(req, res);
};
export default login;

View File

@@ -0,0 +1,8 @@
import auth0 from "../../../lib/auth0";
import { withErrorHandling } from "../../../lib/apiHelpers";
const logout = withErrorHandling(async (req, res) => {
await auth0.handleLogout(req, res);
});
export default logout;

7
web/pages/api/v1/me.js Normal file
View File

@@ -0,0 +1,7 @@
import auth0 from "../../../lib/auth0";
const me = async (req, res) => {
await auth0.handleProfile(req, res);
};
export default me;

View File

@@ -1,69 +1,50 @@
import Head from 'next/head'
import Image from 'next/image'
import styles from '../styles/Home.module.css'
import Layout from "../components/layout";
import WantToButton from "../components/wanttobutton";
import Link from "next/link";
import { useFetchUser } from "../lib/user";
function Home() {
const { user, loading } = useFetchUser();
export default function Home() {
return (
<div className={styles.container}>
<Head>
<title>Create Next App: Testing Update 2</title>
<meta name="description" content="Generated by create next app" />
<link rel="icon" href="/favicon.ico" />
</Head>
<Layout user={user} loading={loading}>
{loading && <p>Loading login info...</p>}
<main className={styles.main}>
<h1 className={styles.title}>
Welcome to <a href="https://nextjs.org">Next.js!</a>
</h1>
{!loading && !user && (
<>
<div className="flex flex-col justify-center">
<p className="text-lg">I want to...</p>
<div className="flex flex-row mt-2">
<WantToButton href="/api/v1/login" text="login" />
<WantToButton
href="/api/v1/login"
text="stay anonymous"
/>
</div>
</div>
</>
)}
<p className={styles.description}>
Get started by editing{' '}
<code className={styles.code}>pages/index.js</code>
</p>
<div className={styles.grid}>
<a href="https://nextjs.org/docs" className={styles.card}>
<h2>Documentation &rarr;</h2>
<p>Find in-depth information about Next.js features and API.</p>
</a>
<a href="https://nextjs.org/learn" className={styles.card}>
<h2>Learn &rarr;</h2>
<p>Learn about Next.js in an interactive course with quizzes!</p>
</a>
<a
href="https://github.com/vercel/next.js/tree/canary/examples"
className={styles.card}
>
<h2>Examples &rarr;</h2>
<p>Discover and deploy boilerplate example Next.js projects.</p>
</a>
<a
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
className={styles.card}
>
<h2>Deploy &rarr;</h2>
<p>
Instantly deploy your Next.js site to a public URL with Vercel.
</p>
</a>
</div>
</main>
<footer className={styles.footer}>
<a
href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Powered by{' '}
<span className={styles.logo}>
<Image src="/vercel.svg" alt="Vercel Logo" width={72} height={16} />
</span>
</a>
</footer>
</div>
)
{user && (
<>
<h4>Rendered user info on the client</h4>
<img src={user.picture} alt="user picture" />
<p>nickname: {user.nickname}</p>
<p>name: {user.name}</p>
<p>
<form method="POST" action="/api/v1/bookmarks">
<label for="title">title: </label>
<input type="text" name="title" />
<br />
<label for="url">url:</label>
<input type="text" name="url" />
<button type="submit">Add bookmark</button>
</form>
</p>
</>
)}
</Layout>
);
}
export default Home;

31
web/pages/profile.js Normal file
View File

@@ -0,0 +1,31 @@
// This import is only needed when checking authentication status directly from getInitialProps
// import auth0 from '../lib/auth0'
import { useFetchUser } from "../lib/user";
import Layout from "../components/layout";
function ProfileCard({ user }) {
return (
<>
<h1>Profile</h1>
<div>
<h3>Profile (client rendered)</h3>
<img src={user.picture} alt="user picture" />
<p>nickname: {user.nickname}</p>
<p>name: {user.name}</p>
</div>
</>
);
}
function Profile() {
const { user, loading } = useFetchUser({ required: true });
return (
<Layout user={user} loading={loading}>
{loading ? <>Loading...</> : <ProfileCard user={user} />}
</Layout>
);
}
export default Profile;

6
web/postcss.config.js Normal file
View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -1,4 +0,0 @@
<svg width="283" height="64" viewBox="0 0 283 64" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path d="M141.04 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.46 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM248.72 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.45 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM200.24 34c0 6 3.92 10 10 10 4.12 0 7.21-1.87 8.8-4.92l7.68 4.43c-3.18 5.3-9.14 8.49-16.48 8.49-11.05 0-19-7.2-19-18s7.96-18 19-18c7.34 0 13.29 3.19 16.48 8.49l-7.68 4.43c-1.59-3.05-4.68-4.92-8.8-4.92-6.07 0-10 4-10 10zm82.48-29v46h-9V5h9zM36.95 0L73.9 64H0L36.95 0zm92.38 5l-27.71 48L73.91 5H84.3l17.32 30 17.32-30h10.39zm58.91 12v9.69c-1-.29-2.06-.49-3.2-.49-5.81 0-10 4-10 10V51h-9V17h9v9.2c0-5.08 5.91-9.2 13.2-9.2z" fill="#000"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,116 +0,0 @@
.container {
padding: 0 2rem;
}
.main {
min-height: 100vh;
padding: 4rem 0;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.footer {
display: flex;
flex: 1;
padding: 2rem 0;
border-top: 1px solid #eaeaea;
justify-content: center;
align-items: center;
}
.footer a {
display: flex;
justify-content: center;
align-items: center;
flex-grow: 1;
}
.title a {
color: #0070f3;
text-decoration: none;
}
.title a:hover,
.title a:focus,
.title a:active {
text-decoration: underline;
}
.title {
margin: 0;
line-height: 1.15;
font-size: 4rem;
}
.title,
.description {
text-align: center;
}
.description {
margin: 4rem 0;
line-height: 1.5;
font-size: 1.5rem;
}
.code {
background: #fafafa;
border-radius: 5px;
padding: 0.75rem;
font-size: 1.1rem;
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
Bitstream Vera Sans Mono, Courier New, monospace;
}
.grid {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
max-width: 800px;
}
.card {
margin: 1rem;
padding: 1.5rem;
text-align: left;
color: inherit;
text-decoration: none;
border: 1px solid #eaeaea;
border-radius: 10px;
transition: color 0.15s ease, border-color 0.15s ease;
max-width: 300px;
}
.card:hover,
.card:focus,
.card:active {
color: #0070f3;
border-color: #0070f3;
}
.card h2 {
margin: 0 0 1rem 0;
font-size: 1.5rem;
}
.card p {
margin: 0;
font-size: 1.25rem;
line-height: 1.5;
}
.logo {
height: 1em;
margin-left: 0.5rem;
}
@media (max-width: 600px) {
.grid {
width: 100%;
flex-direction: column;
}
}

View File

@@ -1,16 +1,3 @@
html,
body {
padding: 0;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
}
a {
color: inherit;
text-decoration: none;
}
* {
box-sizing: border-box;
}
@tailwind base;
@tailwind components;
@tailwind utilities;

11
web/tailwind.config.js Normal file
View File

@@ -0,0 +1,11 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./pages/**/*.js",
"./components/**/*.js"
],
theme: {
extend: {},
},
plugins: [],
}