diff --git a/app/firestore.rules b/app/firestore.rules deleted file mode 120000 index b10e7e2d..00000000 --- a/app/firestore.rules +++ /dev/null @@ -1 +0,0 @@ -../crossword/firestore.rules \ No newline at end of file diff --git a/app/firestore.rules b/app/firestore.rules new file mode 100644 index 00000000..d227de13 --- /dev/null +++ b/app/firestore.rules @@ -0,0 +1,85 @@ +rules_version = '2'; +service cloud.firestore { + match /databases/{database}/documents { + function isAdmin() { + return request.auth.token.admin; + } + function isNonAnonymous() { + return request.auth.token.firebase.sign_in_provider != 'anonymous'; + } + match /{document=**} { + allow read, write: if false; + } + function isAuthor() { + return request.auth.uid != null && request.auth.uid == resource.data.a; + } + function authorSetCorrectly() { + return request.auth.uid != null && request.auth.uid == request.resource.data.a; + } + function isPublished() { + return resource.data.p == null || request.time > resource.data.p; + } + function validCrossword() { + return (request.resource.data.g.size() == (request.resource.data.w * request.resource.data.h)) + && (request.resource.data.ac.size() == request.resource.data.an.size()) + && (request.resource.data.dc.size() == request.resource.data.dn.size()); + } + match /c/{crosswordId} { + allow get: if isAdmin() || isAuthor() + || (resource.data.m == true && isPublished()); + + // TODO figure out exactly what is using this query + allow list: if isAdmin() || isAuthor() + || (resource.data.c != null && request.time + duration.time(1, 0, 0, 0) >= resource.data.p); + + // This supports the homepage 'recent puzzles' list + allow list: if resource.data.m + && request.query.limit <= 30 + && request.time + duration.time(1, 0, 0, 0) >= resource.data.p; + + allow update: if isAdmin(); + + allow create: if isAdmin() && authorSetCorrectly() + && validCrossword() + && request.resource.data.c == null + && request.resource.data.p == null + && request.resource.data.m == false; + } + match /uc/{userId} { + allow read: if request.auth.uid == userId; + allow write: if request.auth.uid == userId; + } + match /up/{userId} { + allow read: if request.auth.uid == userId; + allow write: if request.auth.uid == userId; + } + match /p/{playId} { + allow read: if request.auth.uid != null + && (resource == null || request.auth.uid == resource.data.u); + + allow write: if request.auth.uid != null + && request.auth.uid == request.resource.data.u + && playId == (request.resource.data.c + '-' + request.resource.data.u); + + allow delete: if request.auth.uid != null + && request.auth.uid == resource.data.u; + } + match /s/{crosswordId} { + allow read: if isAdmin() || request.auth.uid == resource.data.a; + } + match /ds/{dateString} { + allow read: if isAdmin(); + } + match /categories/{category} { + allow get: if true; + allow write: if isAdmin(); + } + match /cfm/{commentId} { + allow write: if request.auth.uid != null + && isNonAnonymous() + && request.auth.uid == request.resource.data.a; + allow read: if isAdmin(); + allow delete: if isAdmin(); + } + } +} diff --git a/crossword/admin_scripts/src/makeAdmin.ts b/app/scripts/makeAdmin.ts similarity index 100% rename from crossword/admin_scripts/src/makeAdmin.ts rename to app/scripts/makeAdmin.ts diff --git a/crossword/admin_scripts/src/migrate.ts b/app/scripts/migrate.ts similarity index 100% rename from crossword/admin_scripts/src/migrate.ts rename to app/scripts/migrate.ts diff --git a/crossword/.env.development b/crossword/.env.development deleted file mode 100644 index 0e798443..00000000 --- a/crossword/.env.development +++ /dev/null @@ -1 +0,0 @@ -HOST=md.local diff --git a/crossword/.firebaserc b/crossword/.firebaserc deleted file mode 100644 index af04a056..00000000 --- a/crossword/.firebaserc +++ /dev/null @@ -1,5 +0,0 @@ -{ - "projects": { - "default": "mdcrosshare" - } -} diff --git a/crossword/.gitignore b/crossword/.gitignore deleted file mode 100644 index ad121706..00000000 --- a/crossword/.gitignore +++ /dev/null @@ -1,26 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -node_modules -/.pnp -.pnp.js - -# testing -/coverage - -# production -/build - -# misc -.DS_Store -.env.local -.env.development.local -.env.test.local -.env.production.local - -npm-debug.log* -yarn-debug.log* -yarn-error.log* -dist -.sentryclirc -firestore-debug.log \ No newline at end of file diff --git a/crossword/README.md b/crossword/README.md deleted file mode 100644 index a70bc254..00000000 --- a/crossword/README.md +++ /dev/null @@ -1,36 +0,0 @@ -## Available Scripts - -In the project directory, you can run: - -### `yarn start` - -Runs the app in the development mode.
-Open [http://localhost:3000](http://localhost:3000) to view it in the browser. - -The page will reload if you make edits.
-You will also see any lint errors in the console. - -### `yarn test` - -Launches the test runner in the interactive watch mode.
-See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. - -### `yarn build` - -Builds the app for production to the `build` folder.
-It correctly bundles React in production mode and optimizes the build for the best performance. - -The build is minified and the filenames include the hashes.
-Your app is ready to be deployed! - -See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. - -### `yarn eject` - -**Note: this is a one-way operation. Once you `eject`, you can’t go back!** - -If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. - -Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. - -You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. diff --git a/crossword/__mocks__/workerMock.ts b/crossword/__mocks__/workerMock.ts deleted file mode 100644 index 0210a729..00000000 --- a/crossword/__mocks__/workerMock.ts +++ /dev/null @@ -1 +0,0 @@ -module.exports = Object.create(null); diff --git a/crossword/admin_scripts/package-lock.json b/crossword/admin_scripts/package-lock.json deleted file mode 100644 index a1be2014..00000000 --- a/crossword/admin_scripts/package-lock.json +++ /dev/null @@ -1,1488 +0,0 @@ -{ - "name": "crosshare-scripts", - "version": "0.1.0", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "@firebase/app-types": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.6.0.tgz", - "integrity": "sha512-ld6rzjXk/SUauHiQZJkeuSJpxIZ5wdnWuF5fWBFQNPaxsaJ9kyYg9GqEvwZ1z2e6JP5cU9gwRBlfW1WkGtGDYA==" - }, - "@firebase/auth-interop-types": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.1.4.tgz", - "integrity": "sha512-CLKNS84KGAv5lRnHTQZFWoR11Ti7gIPFirDDXWek/fSU+TdYdnxJFR5XSD4OuGyzUYQ3Dq7aVj5teiRdyBl9hA==" - }, - "@firebase/component": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.1.8.tgz", - "integrity": "sha512-kzuCF+NVympQk3gcsHldOmDRVPVndECi6O9Wvd47HTEQYO9HsZWfOM1fHUvvHAijSzNi16p4NSM7UziuBQBL4w==", - "requires": { - "@firebase/util": "0.2.43", - "tslib": "1.11.1" - } - }, - "@firebase/database": { - "version": "0.5.24", - "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.5.24.tgz", - "integrity": "sha512-9whAQzU8cxDUKGBWCT/aHVmqfyzCP2RkGhbZi2oHpMrmvht7cuBtXtUbDD5R8WomniCOUP8rtQfmCFI7V9ehYw==", - "requires": { - "@firebase/auth-interop-types": "0.1.4", - "@firebase/component": "0.1.8", - "@firebase/database-types": "0.4.14", - "@firebase/logger": "0.2.0", - "@firebase/util": "0.2.43", - "faye-websocket": "0.11.3", - "tslib": "1.11.1" - } - }, - "@firebase/database-types": { - "version": "0.4.14", - "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.4.14.tgz", - "integrity": "sha512-+D41HWac0HcvwMi+0dezEdSOZHpVjPKPNmpQiW2GDuS5kk27/v1jxc9v7F4ALLtpxbVcn16UZl5PqEkcS9H2Xg==", - "requires": { - "@firebase/app-types": "0.6.0" - } - }, - "@firebase/logger": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.2.0.tgz", - "integrity": "sha512-qOMnAh1JY9NkYUEy3iFviiFq0dCvk6qN2DsRy2Y7eAhHR6RqwA47l1kI+0MIXmSzlJ9akXjWAXxV5ijzr68Big==" - }, - "@firebase/util": { - "version": "0.2.43", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.2.43.tgz", - "integrity": "sha512-4gGlvcoOJ48xO6PH59UOHLjvImdYXANF/1d0ao60fbiJDIKxJqMksXw3UF2zsUrRkyCOqIDLeiVuF18vffXP+g==", - "requires": { - "tslib": "1.11.1" - } - }, - "@google-cloud/common": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-2.4.0.tgz", - "integrity": "sha512-zWFjBS35eI9leAHhjfeOYlK5Plcuj/77EzstnrJIZbKgF/nkqjcQuGiMCpzCwOfPyUbz8ZaEOYgbHa759AKbjg==", - "optional": true, - "requires": { - "@google-cloud/projectify": "^1.0.0", - "@google-cloud/promisify": "^1.0.0", - "arrify": "^2.0.0", - "duplexify": "^3.6.0", - "ent": "^2.2.0", - "extend": "^3.0.2", - "google-auth-library": "^5.5.0", - "retry-request": "^4.0.0", - "teeny-request": "^6.0.0" - } - }, - "@google-cloud/firestore": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-3.7.1.tgz", - "integrity": "sha512-2zDGr3wnzgMf/sn+wgqLJoakKbchqrn1F05O0CrXdr3pmOpRCTDWD+ua/k73JG/fqWGkoLw+uuDQew980ZHlvw==", - "optional": true, - "requires": { - "deep-equal": "^2.0.0", - "functional-red-black-tree": "^1.0.1", - "google-gax": "^1.13.0", - "readable-stream": "^3.4.0", - "through2": "^3.0.0" - } - }, - "@google-cloud/paginator": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-2.0.3.tgz", - "integrity": "sha512-kp/pkb2p/p0d8/SKUu4mOq8+HGwF8NPzHWkj+VKrIPQPyMRw8deZtrO/OcSiy9C/7bpfU5Txah5ltUNfPkgEXg==", - "optional": true, - "requires": { - "arrify": "^2.0.0", - "extend": "^3.0.2" - } - }, - "@google-cloud/projectify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-1.0.4.tgz", - "integrity": "sha512-ZdzQUN02eRsmTKfBj9FDL0KNDIFNjBn/d6tHQmA/+FImH5DO6ZV8E7FzxMgAUiVAUq41RFAkb25p1oHOZ8psfg==", - "optional": true - }, - "@google-cloud/promisify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-1.0.4.tgz", - "integrity": "sha512-VccZDcOql77obTnFh0TbNED/6ZbbmHDf8UMNnzO1d5g9V0Htfm4k5cllY8P1tJsRKC3zWYGRLaViiupcgVjBoQ==", - "optional": true - }, - "@google-cloud/storage": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-4.6.0.tgz", - "integrity": "sha512-ubhbLAnj+hrp32x5gI+JajKU0kvhApA6PsLOLkuOj4Cz4b6MNsyhSWZ5rq2W7TylqfNNW8M9QxPCKWg3Sb0IbA==", - "optional": true, - "requires": { - "@google-cloud/common": "^2.1.1", - "@google-cloud/paginator": "^2.0.0", - "@google-cloud/promisify": "^1.0.0", - "arrify": "^2.0.0", - "compressible": "^2.0.12", - "concat-stream": "^2.0.0", - "date-and-time": "^0.12.0", - "duplexify": "^3.5.0", - "extend": "^3.0.2", - "gaxios": "^2.0.1", - "gcs-resumable-upload": "^2.2.4", - "hash-stream-validation": "^0.2.2", - "mime": "^2.2.0", - "mime-types": "^2.0.8", - "onetime": "^5.1.0", - "p-limit": "^2.2.0", - "pumpify": "^2.0.0", - "readable-stream": "^3.4.0", - "snakeize": "^0.1.0", - "stream-events": "^1.0.1", - "through2": "^3.0.0", - "xdg-basedir": "^4.0.0" - } - }, - "@grpc/grpc-js": { - "version": "0.6.18", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-0.6.18.tgz", - "integrity": "sha512-uAzv/tM8qpbf1vpx1xPMfcUMzbfdqJtdCYAqY/LsLeQQlnTb4vApylojr+wlCyr7bZeg3AFfHvtihnNOQQt/nA==", - "optional": true, - "requires": { - "semver": "^6.2.0" - } - }, - "@grpc/proto-loader": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.5.3.tgz", - "integrity": "sha512-8qvUtGg77G2ZT2HqdqYoM/OY97gQd/0crSG34xNmZ4ZOsv3aQT/FQV9QfZPazTGna6MIoyUd+u6AxsoZjJ/VMQ==", - "optional": true, - "requires": { - "lodash.camelcase": "^4.3.0", - "protobufjs": "^6.8.6" - } - }, - "@protobufjs/aspromise": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=", - "optional": true - }, - "@protobufjs/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", - "optional": true - }, - "@protobufjs/codegen": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", - "optional": true - }, - "@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=", - "optional": true - }, - "@protobufjs/fetch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", - "optional": true, - "requires": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" - } - }, - "@protobufjs/float": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=", - "optional": true - }, - "@protobufjs/inquire": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=", - "optional": true - }, - "@protobufjs/path": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=", - "optional": true - }, - "@protobufjs/pool": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=", - "optional": true - }, - "@protobufjs/utf8": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=", - "optional": true - }, - "@tootallnate/once": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.0.0.tgz", - "integrity": "sha512-KYyTT/T6ALPkIRd2Ge080X/BsXvy9O0hcWTtMWkPvwAwF99+vn6Dv4GzrFT/Nn1LePr+FFDbRXXlqmsy9lw2zA==", - "optional": true - }, - "@types/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-UoOfVEzAUpeSPmjm7h1uk5MH6KZma2z2O7a75onTGjnNvAvMVrPzPL/vBbT65iIGHWj6rokwfmYcmxmlSf2uwg==", - "optional": true, - "requires": { - "@types/node": "*" - } - }, - "@types/long": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", - "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==", - "optional": true - }, - "@types/node": { - "version": "8.10.59", - "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.59.tgz", - "integrity": "sha512-8RkBivJrDCyPpBXhVZcjh7cQxVBSmRk9QM7hOketZzp6Tg79c0N8kkpAIito9bnJ3HCVCHVYz+KHTEbfQNfeVQ==" - }, - "abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "optional": true, - "requires": { - "event-target-shim": "^5.0.0" - } - }, - "agent-base": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.0.tgz", - "integrity": "sha512-j1Q7cSCqN+AwrmDd+pzgqc0/NpC655x2bUf5ZjRIO77DcNBFmh+OgRNzF6OKdCC9RSCb19fGd99+bhXFdkRNqw==", - "optional": true, - "requires": { - "debug": "4" - } - }, - "arrify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", - "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", - "optional": true - }, - "base64-js": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", - "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==", - "optional": true - }, - "bignumber.js": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-7.2.1.tgz", - "integrity": "sha512-S4XzBk5sMB+Rcb/LNcpzXr57VRTxgAvaAEDAl1AwRx27j00hT84O6OkteE7u8UB3NuaaygCRrEpqox4uDOrbdQ==", - "optional": true - }, - "buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" - }, - "buffer-from": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", - "optional": true - }, - "compressible": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", - "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", - "optional": true, - "requires": { - "mime-db": ">= 1.43.0 < 2" - } - }, - "concat-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", - "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", - "optional": true, - "requires": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.0.2", - "typedarray": "^0.0.6" - } - }, - "configstore": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", - "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", - "optional": true, - "requires": { - "dot-prop": "^5.2.0", - "graceful-fs": "^4.1.2", - "make-dir": "^3.0.0", - "unique-string": "^2.0.0", - "write-file-atomic": "^3.0.0", - "xdg-basedir": "^4.0.0" - } - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "optional": true - }, - "crypto-random-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", - "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", - "optional": true - }, - "date-and-time": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/date-and-time/-/date-and-time-0.12.0.tgz", - "integrity": "sha512-n2RJIAp93AucgF/U/Rz5WRS2Hjg5Z+QxscaaMCi6pVZT1JpJKRH+C08vyH/lRR1kxNXnPxgo3lWfd+jCb/UcuQ==", - "optional": true - }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "optional": true, - "requires": { - "ms": "^2.1.1" - } - }, - "deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.0.1.tgz", - "integrity": "sha512-7Et6r6XfNW61CPPCIYfm1YPGSmh6+CliYeL4km7GWJcpX5LTAflGF8drLLR+MZX+2P3NZfAfSduutBbSWqER4g==", - "optional": true, - "requires": { - "es-abstract": "^1.16.3", - "es-get-iterator": "^1.0.1", - "is-arguments": "^1.0.4", - "is-date-object": "^1.0.1", - "is-regex": "^1.0.4", - "isarray": "^2.0.5", - "object-is": "^1.0.1", - "object-keys": "^1.1.1", - "regexp.prototype.flags": "^1.2.0", - "side-channel": "^1.0.1", - "which-boxed-primitive": "^1.0.1", - "which-collection": "^1.0.0" - } - }, - "define-properties": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "optional": true, - "requires": { - "object-keys": "^1.0.12" - } - }, - "dicer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.3.0.tgz", - "integrity": "sha512-MdceRRWqltEG2dZqO769g27N/3PXfcKl04VhYnBlo2YhH7zPi88VebsjTKclaOyiuMaGU72hTfw3VkUitGcVCA==", - "requires": { - "streamsearch": "0.1.2" - } - }, - "dot-prop": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.2.0.tgz", - "integrity": "sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A==", - "optional": true, - "requires": { - "is-obj": "^2.0.0" - } - }, - "duplexify": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", - "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", - "optional": true, - "requires": { - "end-of-stream": "^1.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.0.0", - "stream-shift": "^1.0.0" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "optional": true - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "optional": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "optional": true - } - } - }, - "ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "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==", - "optional": true, - "requires": { - "once": "^1.4.0" - } - }, - "ent": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", - "integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0=", - "optional": true - }, - "es-abstract": { - "version": "1.17.4", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.4.tgz", - "integrity": "sha512-Ae3um/gb8F0mui/jPL+QiqmglkUsaQf7FwBEHYIFkztkneosu9imhqHpBzQ3h1vit8t5iQ74t6PEVvphBZiuiQ==", - "optional": true, - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.1.5", - "is-regex": "^1.0.5", - "object-inspect": "^1.7.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.0", - "string.prototype.trimleft": "^2.1.1", - "string.prototype.trimright": "^2.1.1" - } - }, - "es-get-iterator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.0.tgz", - "integrity": "sha512-UfrmHuWQlNMTs35e1ypnvikg6jCz3SK8v8ImvmDsh36fCVUR1MqoFDiyn0/k52C8NqO3YsO8Oe0azeesNuqSsQ==", - "optional": true, - "requires": { - "es-abstract": "^1.17.4", - "has-symbols": "^1.0.1", - "is-arguments": "^1.0.4", - "is-map": "^2.0.1", - "is-set": "^2.0.1", - "is-string": "^1.0.5", - "isarray": "^2.0.5" - } - }, - "es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", - "optional": true, - "requires": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - } - }, - "event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "optional": true - }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "optional": true - }, - "fast-text-encoding": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.1.tgz", - "integrity": "sha512-x4FEgaz3zNRtJfLFqJmHWxkMDDvXVtaznj2V9jiP8ACUJrUgist4bP9FmDL2Vew2Y9mEQI/tG4GqabaitYp9CQ==", - "optional": true - }, - "faye-websocket": { - "version": "0.11.3", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.3.tgz", - "integrity": "sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA==", - "requires": { - "websocket-driver": ">=0.5.1" - } - }, - "firebase-admin": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-8.10.0.tgz", - "integrity": "sha512-QzJZ1sBh9xzKjb44aP6m1duy0Xe1ixexwh0eaOt1CkJYCOq2b6bievK4GNWMl5yGQ7FFBEbZO6hyDi+5wrctcg==", - "requires": { - "@firebase/database": "^0.5.17", - "@google-cloud/firestore": "^3.0.0", - "@google-cloud/storage": "^4.1.2", - "@types/node": "^8.10.59", - "dicer": "^0.3.0", - "jsonwebtoken": "8.1.0", - "node-forge": "0.7.4" - } - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "optional": true - }, - "functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", - "optional": true - }, - "gaxios": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-2.3.2.tgz", - "integrity": "sha512-K/+py7UvKRDaEwEKlLiRKrFr+wjGjsMz5qH7Vs549QJS7cpSCOT/BbWL7pzqECflc46FcNPipjSfB+V1m8PAhw==", - "optional": true, - "requires": { - "abort-controller": "^3.0.0", - "extend": "^3.0.2", - "https-proxy-agent": "^5.0.0", - "is-stream": "^2.0.0", - "node-fetch": "^2.3.0" - } - }, - "gcp-metadata": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-3.5.0.tgz", - "integrity": "sha512-ZQf+DLZ5aKcRpLzYUyBS3yo3N0JSa82lNDO8rj3nMSlovLcz2riKFBsYgDzeXcv75oo5eqB2lx+B14UvPoCRnA==", - "optional": true, - "requires": { - "gaxios": "^2.1.0", - "json-bigint": "^0.3.0" - } - }, - "gcs-resumable-upload": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/gcs-resumable-upload/-/gcs-resumable-upload-2.3.3.tgz", - "integrity": "sha512-sf896I5CC/1AxeaGfSFg3vKMjUq/r+A3bscmVzZm10CElyRanN0XwPu/MxeIO4LSP+9uF6yKzXvNsaTsMXUG6Q==", - "optional": true, - "requires": { - "abort-controller": "^3.0.0", - "configstore": "^5.0.0", - "gaxios": "^2.0.0", - "google-auth-library": "^5.0.0", - "pumpify": "^2.0.0", - "stream-events": "^1.0.4" - } - }, - "google-auth-library": { - "version": "5.10.1", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-5.10.1.tgz", - "integrity": "sha512-rOlaok5vlpV9rSiUu5EpR0vVpc+PhN62oF4RyX/6++DG1VsaulAFEMlDYBLjJDDPI6OcNOCGAKy9UVB/3NIDXg==", - "optional": true, - "requires": { - "arrify": "^2.0.0", - "base64-js": "^1.3.0", - "ecdsa-sig-formatter": "^1.0.11", - "fast-text-encoding": "^1.0.0", - "gaxios": "^2.1.0", - "gcp-metadata": "^3.4.0", - "gtoken": "^4.1.0", - "jws": "^4.0.0", - "lru-cache": "^5.0.0" - } - }, - "google-gax": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-1.15.1.tgz", - "integrity": "sha512-1T1PwSZWnbdRusA+NCZMSe56iU6swGvuZuy54eYl9vEHiRXTLYbQmUkWY2CqgYD9Fd/T4WBkUl22+rZG80unyw==", - "optional": true, - "requires": { - "@grpc/grpc-js": "^0.6.18", - "@grpc/proto-loader": "^0.5.1", - "@types/fs-extra": "^8.0.1", - "@types/long": "^4.0.0", - "abort-controller": "^3.0.0", - "duplexify": "^3.6.0", - "google-auth-library": "^5.0.0", - "is-stream-ended": "^0.1.4", - "lodash.at": "^4.6.0", - "lodash.has": "^4.5.2", - "node-fetch": "^2.6.0", - "protobufjs": "^6.8.9", - "retry-request": "^4.0.0", - "semver": "^6.0.0", - "walkdir": "^0.4.0" - } - }, - "google-p12-pem": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-2.0.4.tgz", - "integrity": "sha512-S4blHBQWZRnEW44OcR7TL9WR+QCqByRvhNDZ/uuQfpxywfupikf/miba8js1jZi6ZOGv5slgSuoshCWh6EMDzg==", - "optional": true, - "requires": { - "node-forge": "^0.9.0" - }, - "dependencies": { - "node-forge": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.1.tgz", - "integrity": "sha512-G6RlQt5Sb4GMBzXvhfkeFmbqR6MzhtnT7VTHuLadjkii3rdYHNdw0m8zA4BTxVIh68FicCQ2NSUANpsqkr9jvQ==", - "optional": true - } - } - }, - "graceful-fs": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", - "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==", - "optional": true - }, - "gtoken": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-4.1.4.tgz", - "integrity": "sha512-VxirzD0SWoFUo5p8RDP8Jt2AGyOmyYcT/pOUgDKJCK+iSw0TMqwrVfY37RXTNmoKwrzmDHSk0GMT9FsgVmnVSA==", - "optional": true, - "requires": { - "gaxios": "^2.1.0", - "google-p12-pem": "^2.0.0", - "jws": "^4.0.0", - "mime": "^2.2.0" - } - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "optional": true, - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-symbols": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", - "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", - "optional": true - }, - "hash-stream-validation": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/hash-stream-validation/-/hash-stream-validation-0.2.2.tgz", - "integrity": "sha512-cMlva5CxWZOrlS/cY0C+9qAzesn5srhFA8IT1VPiHc9bWWBLkJfEUIZr7MWoi89oOOGmpg8ymchaOjiArsGu5A==", - "optional": true, - "requires": { - "through2": "^2.0.0" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "optional": true - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "optional": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "optional": true - }, - "through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "optional": true, - "requires": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - } - } - }, - "http-parser-js": { - "version": "0.4.10", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.4.10.tgz", - "integrity": "sha1-ksnBN0w1CF912zWexWzCV8u5P6Q=" - }, - "http-proxy-agent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", - "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", - "optional": true, - "requires": { - "@tootallnate/once": "1", - "agent-base": "6", - "debug": "4" - } - }, - "https-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", - "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", - "optional": true, - "requires": { - "agent-base": "6", - "debug": "4" - } - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "optional": true - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "optional": true - }, - "is-arguments": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz", - "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==", - "optional": true - }, - "is-bigint": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.0.tgz", - "integrity": "sha512-t5mGUXC/xRheCK431ylNiSkGGpBp8bHENBcENTkDT6ppwPzEVxNGZRvgvmOEfbWkFhA7D2GEuE2mmQTr78sl2g==", - "optional": true - }, - "is-boolean-object": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.0.1.tgz", - "integrity": "sha512-TqZuVwa/sppcrhUCAYkGBk7w0yxfQQnxq28fjkO53tnK9FQXmdwz2JS5+GjsWQ6RByES1K40nI+yDic5c9/aAQ==", - "optional": true - }, - "is-callable": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", - "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==", - "optional": true - }, - "is-date-object": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", - "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==", - "optional": true - }, - "is-map": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.1.tgz", - "integrity": "sha512-T/S49scO8plUiAOA2DBTBG3JHpn1yiw0kRp6dgiZ0v2/6twi5eiB0rHtHFH9ZIrvlWc6+4O+m4zg5+Z833aXgw==", - "optional": true - }, - "is-number-object": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.4.tgz", - "integrity": "sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw==", - "optional": true - }, - "is-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", - "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", - "optional": true - }, - "is-regex": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", - "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", - "optional": true, - "requires": { - "has": "^1.0.3" - } - }, - "is-set": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.1.tgz", - "integrity": "sha512-eJEzOtVyenDs1TMzSQ3kU3K+E0GUS9sno+F0OBT97xsgcJsF9nXMBtkT9/kut5JEpM7oL7X/0qxR17K3mcwIAA==", - "optional": true - }, - "is-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", - "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", - "optional": true - }, - "is-stream-ended": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-stream-ended/-/is-stream-ended-0.1.4.tgz", - "integrity": "sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw==", - "optional": true - }, - "is-string": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz", - "integrity": "sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==", - "optional": true - }, - "is-symbol": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", - "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", - "optional": true, - "requires": { - "has-symbols": "^1.0.1" - } - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", - "optional": true - }, - "is-weakmap": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", - "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", - "optional": true - }, - "is-weakset": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.1.tgz", - "integrity": "sha512-pi4vhbhVHGLxohUw7PhGsueT4vRGFoXhP7+RGN0jKIv9+8PWYCQTqtADngrxOm2g46hoH0+g8uZZBzMrvVGDmw==", - "optional": true - }, - "isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "optional": true - }, - "json-bigint": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-0.3.0.tgz", - "integrity": "sha1-DM2RLEuCcNBfBW+9E4FLU9OCWx4=", - "optional": true, - "requires": { - "bignumber.js": "^7.0.0" - } - }, - "jsonwebtoken": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.1.0.tgz", - "integrity": "sha1-xjl80uX9WD1lwAeoPce7eOaYK4M=", - "requires": { - "jws": "^3.1.4", - "lodash.includes": "^4.3.0", - "lodash.isboolean": "^3.0.3", - "lodash.isinteger": "^4.0.4", - "lodash.isnumber": "^3.0.3", - "lodash.isplainobject": "^4.0.6", - "lodash.isstring": "^4.0.1", - "lodash.once": "^4.0.0", - "ms": "^2.0.0", - "xtend": "^4.0.1" - }, - "dependencies": { - "jwa": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", - "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", - "requires": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", - "requires": { - "jwa": "^1.4.1", - "safe-buffer": "^5.0.1" - } - } - } - }, - "jwa": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", - "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", - "optional": true, - "requires": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "jws": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", - "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", - "optional": true, - "requires": { - "jwa": "^2.0.0", - "safe-buffer": "^5.0.1" - } - }, - "lodash.at": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.at/-/lodash.at-4.6.0.tgz", - "integrity": "sha1-k83OZk8KGZTqM9181A4jr9EbD/g=", - "optional": true - }, - "lodash.camelcase": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=", - "optional": true - }, - "lodash.has": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/lodash.has/-/lodash.has-4.5.2.tgz", - "integrity": "sha1-0Z9NwQlQWMzL4rDN9O4P5Ko3yGI=", - "optional": true - }, - "lodash.includes": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", - "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" - }, - "lodash.isboolean": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", - "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" - }, - "lodash.isinteger": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", - "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" - }, - "lodash.isnumber": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", - "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=" - }, - "lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" - }, - "lodash.isstring": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" - }, - "lodash.once": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" - }, - "long": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", - "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", - "optional": true - }, - "lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "optional": true, - "requires": { - "yallist": "^3.0.2" - } - }, - "make-dir": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.2.tgz", - "integrity": "sha512-rYKABKutXa6vXTXhoV18cBE7PaewPXHe/Bdq4v+ZLMhxbWApkFFplT0LcbMW+6BbjnQXzZ/sAvSE/JdguApG5w==", - "optional": true, - "requires": { - "semver": "^6.0.0" - } - }, - "mime": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz", - "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==", - "optional": true - }, - "mime-db": { - "version": "1.43.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz", - "integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==", - "optional": true - }, - "mime-types": { - "version": "2.1.26", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz", - "integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==", - "optional": true, - "requires": { - "mime-db": "1.43.0" - } - }, - "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "optional": true - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node-fetch": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", - "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==", - "optional": true - }, - "node-forge": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.4.tgz", - "integrity": "sha512-8Df0906+tq/omxuCZD6PqhPaQDYuyJ1d+VITgxoIA8zvQd1ru+nMJcDChHH324MWitIgbVkAkQoGEEVJNpn/PA==" - }, - "object-inspect": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", - "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==", - "optional": true - }, - "object-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.0.2.tgz", - "integrity": "sha512-Epah+btZd5wrrfjkJZq1AOB9O6OxUQto45hzFd7lXGrpHPGE0W1k+426yrZV+k6NJOzLNNW/nVsmZdIWsAqoOQ==", - "optional": true - }, - "object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "optional": true - }, - "object.assign": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", - "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", - "optional": true, - "requires": { - "define-properties": "^1.1.2", - "function-bind": "^1.1.1", - "has-symbols": "^1.0.0", - "object-keys": "^1.0.11" - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "optional": true, - "requires": { - "wrappy": "1" - } - }, - "onetime": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz", - "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==", - "optional": true, - "requires": { - "mimic-fn": "^2.1.0" - } - }, - "p-limit": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.2.tgz", - "integrity": "sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==", - "optional": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "optional": true - }, - "process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "optional": true - }, - "protobufjs": { - "version": "6.8.9", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.8.9.tgz", - "integrity": "sha512-j2JlRdUeL/f4Z6x4aU4gj9I2LECglC+5qR2TrWb193Tla1qfdaNQTZ8I27Pt7K0Ajmvjjpft7O3KWTGciz4gpw==", - "optional": true, - "requires": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", - "@types/long": "^4.0.0", - "@types/node": "^10.1.0", - "long": "^4.0.0" - }, - "dependencies": { - "@types/node": { - "version": "10.17.17", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.17.tgz", - "integrity": "sha512-gpNnRnZP3VWzzj5k3qrpRC6Rk3H/uclhAVo1aIvwzK5p5cOrs9yEyQ8H/HBsBY0u5rrWxXEiVPQ0dEB6pkjE8Q==", - "optional": true - } - } - }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "optional": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "pumpify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-2.0.1.tgz", - "integrity": "sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw==", - "optional": true, - "requires": { - "duplexify": "^4.1.1", - "inherits": "^2.0.3", - "pump": "^3.0.0" - }, - "dependencies": { - "duplexify": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.1.tgz", - "integrity": "sha512-DY3xVEmVHTv1wSzKNbwoU6nVjzI369Y6sPoqfYr0/xlx3IdX2n94xIszTcjPO8W8ZIv0Wb0PXNcjuZyT4wiICA==", - "optional": true, - "requires": { - "end-of-stream": "^1.4.1", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1", - "stream-shift": "^1.0.0" - } - } - } - }, - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "optional": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "regexp.prototype.flags": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz", - "integrity": "sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ==", - "optional": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.0-next.1" - } - }, - "retry-request": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-4.1.1.tgz", - "integrity": "sha512-BINDzVtLI2BDukjWmjAIRZ0oglnCAkpP2vQjM3jdLhmT62h0xnQgciPwBRDAvHqpkPT2Wo1XuUyLyn6nbGrZQQ==", - "optional": true, - "requires": { - "debug": "^4.1.1", - "through2": "^3.0.1" - } - }, - "safe-buffer": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", - "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "optional": true - }, - "side-channel": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.2.tgz", - "integrity": "sha512-7rL9YlPHg7Ancea1S96Pa8/QWb4BtXL/TZvS6B8XFetGBeuhAsfmUspK6DokBeZ64+Kj9TCNRD/30pVz1BvQNA==", - "optional": true, - "requires": { - "es-abstract": "^1.17.0-next.1", - "object-inspect": "^1.7.0" - } - }, - "signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", - "optional": true - }, - "snakeize": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/snakeize/-/snakeize-0.1.0.tgz", - "integrity": "sha1-EMCI2LWOsHazIpu1oE4jLOEmQi0=", - "optional": true - }, - "stream-events": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", - "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", - "optional": true, - "requires": { - "stubs": "^3.0.0" - } - }, - "stream-shift": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", - "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==", - "optional": true - }, - "streamsearch": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", - "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=" - }, - "string.prototype.trimleft": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz", - "integrity": "sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==", - "optional": true, - "requires": { - "define-properties": "^1.1.3", - "function-bind": "^1.1.1" - } - }, - "string.prototype.trimright": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz", - "integrity": "sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==", - "optional": true, - "requires": { - "define-properties": "^1.1.3", - "function-bind": "^1.1.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "optional": true, - "requires": { - "safe-buffer": "~5.1.0" - }, - "dependencies": { - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "optional": true - } - } - }, - "stubs": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", - "integrity": "sha1-6NK6H6nJBXAwPAMLaQD31fiavls=", - "optional": true - }, - "teeny-request": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-6.0.3.tgz", - "integrity": "sha512-TZG/dfd2r6yeji19es1cUIwAlVD8y+/svB1kAC2Y0bjEyysrfbO8EZvJBRwIE6WkwmUoB7uvWLwTIhJbMXZ1Dw==", - "optional": true, - "requires": { - "http-proxy-agent": "^4.0.0", - "https-proxy-agent": "^5.0.0", - "node-fetch": "^2.2.0", - "stream-events": "^1.0.5", - "uuid": "^7.0.0" - } - }, - "through2": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.1.tgz", - "integrity": "sha512-M96dvTalPT3YbYLaKaCuwu+j06D/8Jfib0o/PxbVt6Amhv3dUAtW6rTV1jPgJSBG83I/e04Y6xkVdVhSRhi0ww==", - "optional": true, - "requires": { - "readable-stream": "2 || 3" - } - }, - "tslib": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz", - "integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==" - }, - "typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", - "optional": true - }, - "typedarray-to-buffer": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", - "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", - "optional": true, - "requires": { - "is-typedarray": "^1.0.0" - } - }, - "unique-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", - "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", - "optional": true, - "requires": { - "crypto-random-string": "^2.0.0" - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "optional": true - }, - "uuid": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.2.tgz", - "integrity": "sha512-vy9V/+pKG+5ZTYKf+VcphF5Oc6EFiu3W8Nv3P3zIh0EqVI80ZxOzuPfe9EHjkFNvf8+xuTHVeei4Drydlx4zjw==", - "optional": true - }, - "walkdir": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/walkdir/-/walkdir-0.4.1.tgz", - "integrity": "sha512-3eBwRyEln6E1MSzcxcVpQIhRG8Q1jLvEqRmCZqS3dsfXEDR/AhOF4d+jHg1qvDCpYaVRZjENPQyrVxAkQqxPgQ==", - "optional": true - }, - "websocket-driver": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.3.tgz", - "integrity": "sha512-bpxWlvbbB459Mlipc5GBzzZwhoZgGEZLuqPaR0INBGnPAY1vdBX6hPnoFXiw+3yWxDuHyQjO2oXTMyS8A5haFg==", - "requires": { - "http-parser-js": ">=0.4.0 <0.4.11", - "safe-buffer": ">=5.1.0", - "websocket-extensions": ">=0.1.1" - } - }, - "websocket-extensions": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", - "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==" - }, - "which-boxed-primitive": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.1.tgz", - "integrity": "sha512-7BT4TwISdDGBgaemWU0N0OU7FeAEJ9Oo2P1PHRm/FCWoEi2VLWC9b6xvxAA3C/NMpxg3HXVgi0sMmGbNUbNepQ==", - "optional": true, - "requires": { - "is-bigint": "^1.0.0", - "is-boolean-object": "^1.0.0", - "is-number-object": "^1.0.3", - "is-string": "^1.0.4", - "is-symbol": "^1.0.2" - } - }, - "which-collection": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", - "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", - "optional": true, - "requires": { - "is-map": "^2.0.1", - "is-set": "^2.0.1", - "is-weakmap": "^2.0.1", - "is-weakset": "^2.0.1" - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "optional": true - }, - "write-file-atomic": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", - "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", - "optional": true, - "requires": { - "imurmurhash": "^0.1.4", - "is-typedarray": "^1.0.0", - "signal-exit": "^3.0.2", - "typedarray-to-buffer": "^3.1.5" - } - }, - "xdg-basedir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", - "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", - "optional": true - }, - "xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" - }, - "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "optional": true - } - } -} diff --git a/crossword/admin_scripts/package.json b/crossword/admin_scripts/package.json deleted file mode 100644 index 3b028cc4..00000000 --- a/crossword/admin_scripts/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "crosshare-scripts", - "version": "0.1.0", - "private": true, - "scripts": { - "makeAdmin": "node dist/scripts/src/makeAdmin.js", - "build": "npm run build-ts && npm run tslint", - "build-ts": "tsc", - "tslint": "tslint -c tslint.json -p tsconfig.json" - }, - "dependencies": { - "firebase-admin": "^8.10.0" - } -} diff --git a/crossword/admin_scripts/sentryRelease.js b/crossword/admin_scripts/sentryRelease.js deleted file mode 100644 index ee64ac88..00000000 --- a/crossword/admin_scripts/sentryRelease.js +++ /dev/null @@ -1,24 +0,0 @@ -const SentryCli = require('@sentry/cli'); -async function createReleaseAndUpload() { - const release = process.env.REACT_APP_SENTRY_RELEASE; - if (!release) { - console.warn('REACT_APP_SENTRY_RELEASE is not set'); - return; - } - const cli = new SentryCli(); - try { - console.log('Creating sentry release ' + release); - await cli.releases.new(release); - console.log('Uploading source maps'); - await cli.releases.uploadSourceMaps(release, { - include: ['build/static/js'], - urlPrefix: '~/static/js', - rewrite: false, - }); - console.log('Finalizing release'); - await cli.releases.finalize(release); - } catch (e) { - console.error('Source maps uploading failed:', e); - } -} -createReleaseAndUpload(); diff --git a/crossword/admin_scripts/tsconfig.json b/crossword/admin_scripts/tsconfig.json deleted file mode 100644 index aac0444b..00000000 --- a/crossword/admin_scripts/tsconfig.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "compilerOptions": { - "target": "es6", - "module": "commonjs", - "sourceMap": true, - "outDir": "dist", - "strict": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true, - "baseUrl": "." - }, - "include": [ - "src/**/*" - ] -} diff --git a/crossword/admin_scripts/tslint.json b/crossword/admin_scripts/tslint.json deleted file mode 100644 index 85598d7b..00000000 --- a/crossword/admin_scripts/tslint.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "extends": "tslint:latest", - "rules": { - "interface-name": false, - "max-line-length": [true, 120], - "member-ordering": [true, - "public-before-private", - "static-before-instance", - "variables-before-functions" - ], - "no-console": false, - "no-implicit-dependencies": [true, "dev"], - "no-var-requires": false, - "object-literal-sort-keys": false, - "quotemark": [true, "single", "avoid-escape"], - "variable-name": [true, "ban-keywords", "check-format"] - } -} diff --git a/crossword/firestore.indexes.json b/crossword/firestore.indexes.json deleted file mode 100644 index d9142a12..00000000 --- a/crossword/firestore.indexes.json +++ /dev/null @@ -1,192 +0,0 @@ -{ - "indexes": [ - { - "collectionGroup": "c", - "queryScope": "COLLECTION", - "fields": [ - { - "fieldPath": "c", - "order": "ASCENDING" - }, - { - "fieldPath": "p", - "order": "ASCENDING" - } - ] - }, - { - "collectionGroup": "c", - "queryScope": "COLLECTION", - "fields": [ - { - "fieldPath": "c", - "order": "ASCENDING" - }, - { - "fieldPath": "p", - "order": "DESCENDING" - } - ] - }, - { - "collectionGroup": "p", - "queryScope": "COLLECTION", - "fields": [ - { - "fieldPath": "f", - "order": "ASCENDING" - }, - { - "fieldPath": "ua", - "order": "ASCENDING" - } - ] - }, - { - "collectionGroup": "p", - "queryScope": "COLLECTION", - "fields": [ - { - "fieldPath": "u", - "order": "ASCENDING" - }, - { - "fieldPath": "ua", - "order": "DESCENDING" - } - ] - } - ], - "fieldOverrides": [ - { - "collectionGroup": "c", - "fieldPath": "ac", - "indexes": [] - }, - { - "collectionGroup": "c", - "fieldPath": "an", - "indexes": [] - }, - { - "collectionGroup": "c", - "fieldPath": "dc", - "indexes": [] - }, - { - "collectionGroup": "c", - "fieldPath": "dn", - "indexes": [] - }, - { - "collectionGroup": "c", - "fieldPath": "g", - "indexes": [] - }, - { - "collectionGroup": "c", - "fieldPath": "h", - "indexes": [] - }, - { - "collectionGroup": "c", - "fieldPath": "hs", - "indexes": [] - }, - { - "collectionGroup": "c", - "fieldPath": "n", - "indexes": [] - }, - { - "collectionGroup": "c", - "fieldPath": "s", - "indexes": [] - }, - { - "collectionGroup": "c", - "fieldPath": "t", - "indexes": [] - }, - { - "collectionGroup": "c", - "fieldPath": "w", - "indexes": [] - }, - { - "collectionGroup": "p", - "fieldPath": "ch", - "indexes": [] - }, - { - "collectionGroup": "p", - "fieldPath": "ct", - "indexes": [] - }, - { - "collectionGroup": "p", - "fieldPath": "f", - "indexes": [] - }, - { - "collectionGroup": "p", - "fieldPath": "g", - "indexes": [] - }, - { - "collectionGroup": "p", - "fieldPath": "n", - "indexes": [] - }, - { - "collectionGroup": "p", - "fieldPath": "rc", - "indexes": [] - }, - { - "collectionGroup": "p", - "fieldPath": "t", - "indexes": [] - }, - { - "collectionGroup": "p", - "fieldPath": "uc", - "indexes": [] - }, - { - "collectionGroup": "p", - "fieldPath": "vc", - "indexes": [] - }, - { - "collectionGroup": "p", - "fieldPath": "wc", - "indexes": [] - }, - { - "collectionGroup": "p", - "fieldPath": "we", - "indexes": [] - }, - { - "collectionGroup": "s", - "fieldPath": "a", - "indexes": [] - }, - { - "collectionGroup": "s", - "fieldPath": "ua", - "indexes": [] - }, - { - "collectionGroup": "s", - "fieldPath": "ct", - "indexes": [] - }, - { - "collectionGroup": "s", - "fieldPath": "uc", - "indexes": [] - } - ] -} diff --git a/crossword/firestore.rules b/crossword/firestore.rules deleted file mode 100644 index d227de13..00000000 --- a/crossword/firestore.rules +++ /dev/null @@ -1,85 +0,0 @@ -rules_version = '2'; -service cloud.firestore { - match /databases/{database}/documents { - function isAdmin() { - return request.auth.token.admin; - } - function isNonAnonymous() { - return request.auth.token.firebase.sign_in_provider != 'anonymous'; - } - match /{document=**} { - allow read, write: if false; - } - function isAuthor() { - return request.auth.uid != null && request.auth.uid == resource.data.a; - } - function authorSetCorrectly() { - return request.auth.uid != null && request.auth.uid == request.resource.data.a; - } - function isPublished() { - return resource.data.p == null || request.time > resource.data.p; - } - function validCrossword() { - return (request.resource.data.g.size() == (request.resource.data.w * request.resource.data.h)) - && (request.resource.data.ac.size() == request.resource.data.an.size()) - && (request.resource.data.dc.size() == request.resource.data.dn.size()); - } - match /c/{crosswordId} { - allow get: if isAdmin() || isAuthor() - || (resource.data.m == true && isPublished()); - - // TODO figure out exactly what is using this query - allow list: if isAdmin() || isAuthor() - || (resource.data.c != null && request.time + duration.time(1, 0, 0, 0) >= resource.data.p); - - // This supports the homepage 'recent puzzles' list - allow list: if resource.data.m - && request.query.limit <= 30 - && request.time + duration.time(1, 0, 0, 0) >= resource.data.p; - - allow update: if isAdmin(); - - allow create: if isAdmin() && authorSetCorrectly() - && validCrossword() - && request.resource.data.c == null - && request.resource.data.p == null - && request.resource.data.m == false; - } - match /uc/{userId} { - allow read: if request.auth.uid == userId; - allow write: if request.auth.uid == userId; - } - match /up/{userId} { - allow read: if request.auth.uid == userId; - allow write: if request.auth.uid == userId; - } - match /p/{playId} { - allow read: if request.auth.uid != null - && (resource == null || request.auth.uid == resource.data.u); - - allow write: if request.auth.uid != null - && request.auth.uid == request.resource.data.u - && playId == (request.resource.data.c + '-' + request.resource.data.u); - - allow delete: if request.auth.uid != null - && request.auth.uid == resource.data.u; - } - match /s/{crosswordId} { - allow read: if isAdmin() || request.auth.uid == resource.data.a; - } - match /ds/{dateString} { - allow read: if isAdmin(); - } - match /categories/{category} { - allow get: if true; - allow write: if isAdmin(); - } - match /cfm/{commentId} { - allow write: if request.auth.uid != null - && isNonAnonymous() - && request.auth.uid == request.resource.data.a; - allow read: if isAdmin(); - allow delete: if isAdmin(); - } - } -} diff --git a/crossword/package.json b/crossword/package.json deleted file mode 100644 index 1e52fbfb..00000000 --- a/crossword/package.json +++ /dev/null @@ -1,79 +0,0 @@ -{ - "name": "crosshare", - "version": "0.1.1", - "private": true, - "dependencies": { - "@emotion/core": "^10.0.28", - "@modern-dev/jsbn": "^1.2.5", - "@reach/router": "^1.3.3", - "@sentry/browser": "^5.15.4", - "@sentry/cli": "^1.52.1", - "@testing-library/jest-dom": "^4.2.4", - "@testing-library/react": "^9.5.0", - "@testing-library/user-event": "^7.1.2", - "@types/jest": "^25.2.1", - "@types/lz-string": "^1.3.34", - "@types/node": "^13.11.1", - "@types/reach__router": "^1.3.4", - "@types/react": "^16.9.34", - "@types/react-dom": "^16.9.6", - "@types/react-window": "^1.8.1", - "@use-it/event-listener": "^0.1.3", - "eslint-plugin-flowtype": "^4.7.0", - "firebase": "^7.13.2", - "fp-ts": "^2.5.3", - "io-ts": "^2.1.3", - "localforage": "^1.7.3", - "lz-string": "^1.4.4", - "react": "^16.13.1", - "react-device-detect": "^1.11.14", - "react-dom": "^16.13.1", - "react-firebase-hooks": "^2.1.1", - "react-helmet-async": "^1.0.4", - "react-icons": "^3.9.0", - "react-scripts": "^3.4.1", - "react-toastify": "^5.5.0", - "react-window": "^1.8.5", - "source-map-explorer": "^2.4.2", - "typescript": "^3.8.3", - "worker-loader": "^2.0.0" - }, - "scripts": { - "analyze": "source-map-explorer 'build/static/js/*.js'", - "dev:firebase": "firebase serve -p 4000", - "dev:react": "react-scripts start", - "build": "react-scripts build", - "test": "react-scripts test", - "eject": "react-scripts eject", - "release": "(export REACT_APP_SENTRY_RELEASE=$(git rev-parse --short HEAD); react-scripts build && node admin_scripts/sentryRelease.js)" - }, - "eslintConfig": { - "extends": "react-app" - }, - "browserslist": { - "production": [ - ">0.2%", - "not dead", - "not op_mini all" - ], - "development": [ - "last 1 chrome version", - "last 1 firefox version", - "last 1 safari version" - ] - }, - "proxy": "http://localhost:4000", - "devDependencies": { - "@firebase/testing": "^0.19.2", - "@types/jest-in-case": "^1.0.2", - "jest-emotion": "^10.0.32", - "jest-in-case": "^1.0.2", - "prettier": "^2.0.5", - "react-devtools": "^4.6.0" - }, - "jest": { - "moduleNameMapper": { - "\\.worker.ts":"/__mocks__/workerMock.ts" - } - } -} diff --git a/crossword/public/apple-icon-120.png b/crossword/public/apple-icon-120.png deleted file mode 100644 index 212aad1d..00000000 Binary files a/crossword/public/apple-icon-120.png and /dev/null differ diff --git a/crossword/public/apple-icon-152.png b/crossword/public/apple-icon-152.png deleted file mode 100644 index 1e33eea2..00000000 Binary files a/crossword/public/apple-icon-152.png and /dev/null differ diff --git a/crossword/public/apple-icon-167.png b/crossword/public/apple-icon-167.png deleted file mode 100644 index 305ef2be..00000000 Binary files a/crossword/public/apple-icon-167.png and /dev/null differ diff --git a/crossword/public/apple-icon-180.png b/crossword/public/apple-icon-180.png deleted file mode 100644 index eda522e0..00000000 Binary files a/crossword/public/apple-icon-180.png and /dev/null differ diff --git a/crossword/public/apple-splash-1125-2436.png b/crossword/public/apple-splash-1125-2436.png deleted file mode 100644 index e21da4b1..00000000 Binary files a/crossword/public/apple-splash-1125-2436.png and /dev/null differ diff --git a/crossword/public/apple-splash-1136-640.png b/crossword/public/apple-splash-1136-640.png deleted file mode 100644 index 385f6002..00000000 Binary files a/crossword/public/apple-splash-1136-640.png and /dev/null differ diff --git a/crossword/public/apple-splash-1242-2208.png b/crossword/public/apple-splash-1242-2208.png deleted file mode 100644 index 04f6e51d..00000000 Binary files a/crossword/public/apple-splash-1242-2208.png and /dev/null differ diff --git a/crossword/public/apple-splash-1242-2688.png b/crossword/public/apple-splash-1242-2688.png deleted file mode 100644 index 4ddebf18..00000000 Binary files a/crossword/public/apple-splash-1242-2688.png and /dev/null differ diff --git a/crossword/public/apple-splash-1334-750.png b/crossword/public/apple-splash-1334-750.png deleted file mode 100644 index b643fd49..00000000 Binary files a/crossword/public/apple-splash-1334-750.png and /dev/null differ diff --git a/crossword/public/apple-splash-1536-2048.png b/crossword/public/apple-splash-1536-2048.png deleted file mode 100644 index f22612bb..00000000 Binary files a/crossword/public/apple-splash-1536-2048.png and /dev/null differ diff --git a/crossword/public/apple-splash-1668-2224.png b/crossword/public/apple-splash-1668-2224.png deleted file mode 100644 index d4e28b3d..00000000 Binary files a/crossword/public/apple-splash-1668-2224.png and /dev/null differ diff --git a/crossword/public/apple-splash-1668-2388.png b/crossword/public/apple-splash-1668-2388.png deleted file mode 100644 index 4b720e01..00000000 Binary files a/crossword/public/apple-splash-1668-2388.png and /dev/null differ diff --git a/crossword/public/apple-splash-1792-828.png b/crossword/public/apple-splash-1792-828.png deleted file mode 100644 index 215965c5..00000000 Binary files a/crossword/public/apple-splash-1792-828.png and /dev/null differ diff --git a/crossword/public/apple-splash-2048-1536.png b/crossword/public/apple-splash-2048-1536.png deleted file mode 100644 index 84fa1f8d..00000000 Binary files a/crossword/public/apple-splash-2048-1536.png and /dev/null differ diff --git a/crossword/public/apple-splash-2048-2732.png b/crossword/public/apple-splash-2048-2732.png deleted file mode 100644 index 4727f536..00000000 Binary files a/crossword/public/apple-splash-2048-2732.png and /dev/null differ diff --git a/crossword/public/apple-splash-2208-1242.png b/crossword/public/apple-splash-2208-1242.png deleted file mode 100644 index 1dd90509..00000000 Binary files a/crossword/public/apple-splash-2208-1242.png and /dev/null differ diff --git a/crossword/public/apple-splash-2224-1668.png b/crossword/public/apple-splash-2224-1668.png deleted file mode 100644 index d89e330b..00000000 Binary files a/crossword/public/apple-splash-2224-1668.png and /dev/null differ diff --git a/crossword/public/apple-splash-2388-1668.png b/crossword/public/apple-splash-2388-1668.png deleted file mode 100644 index 915f4e3b..00000000 Binary files a/crossword/public/apple-splash-2388-1668.png and /dev/null differ diff --git a/crossword/public/apple-splash-2436-1125.png b/crossword/public/apple-splash-2436-1125.png deleted file mode 100644 index c0a6b9b4..00000000 Binary files a/crossword/public/apple-splash-2436-1125.png and /dev/null differ diff --git a/crossword/public/apple-splash-2688-1242.png b/crossword/public/apple-splash-2688-1242.png deleted file mode 100644 index ecd0c884..00000000 Binary files a/crossword/public/apple-splash-2688-1242.png and /dev/null differ diff --git a/crossword/public/apple-splash-2732-2048.png b/crossword/public/apple-splash-2732-2048.png deleted file mode 100644 index d7103dda..00000000 Binary files a/crossword/public/apple-splash-2732-2048.png and /dev/null differ diff --git a/crossword/public/apple-splash-640-1136.png b/crossword/public/apple-splash-640-1136.png deleted file mode 100644 index 991deef2..00000000 Binary files a/crossword/public/apple-splash-640-1136.png and /dev/null differ diff --git a/crossword/public/apple-splash-750-1334.png b/crossword/public/apple-splash-750-1334.png deleted file mode 100644 index 226a1390..00000000 Binary files a/crossword/public/apple-splash-750-1334.png and /dev/null differ diff --git a/crossword/public/apple-splash-828-1792.png b/crossword/public/apple-splash-828-1792.png deleted file mode 100644 index 96c9ff8e..00000000 Binary files a/crossword/public/apple-splash-828-1792.png and /dev/null differ diff --git a/crossword/public/demos/mini.xw b/crossword/public/demos/mini.xw deleted file mode 100644 index 7c75b939..00000000 --- a/crossword/public/demos/mini.xw +++ /dev/null @@ -1,15 +0,0 @@ -{ - "author": "Mike D", - "title": "Mini example", - "size": { - "rows": 5, - "cols": 5 - }, - "highlighted": [], - "highlight": "circle", - "clues": { - "across": ["5. Good thing to see on a report card, or a hint to the vowels in this puzzle", "6. Indian bread", "7. Cable alternative", "4. Org. for boomers, now", "1. Witch"], - "down": ["1. Islamic analogue of kosher", "2. Ireland's ___ Islands", "3. Savior of lost souls, for short?", "5. What's more", "4. \"So it goes\""] - }, - "grid": [".", ".", "H", "A", "G", ".", "A", "A", "R", "P", "A", "L", "L", "A", "S", "N", "A", "A", "N", ".", "D", "S", "L", ".", "."] -} diff --git a/crossword/public/demos/mini2.xw b/crossword/public/demos/mini2.xw deleted file mode 100644 index 966db25e..00000000 --- a/crossword/public/demos/mini2.xw +++ /dev/null @@ -1,13 +0,0 @@ -{ - "author": "Mike D", - "title": "Untitled", - "size": { - "rows": 5, - "cols": 5 - }, - "clues": { - "across": ["1. It may be + or -", "4. Sci-fi weapon setting", "8. Prefix with globin", "9. Szyslak of \"The Simpsons\"", "6. Pairs are seen in it"], - "down": ["3. Duke of video gaming", "1. Necessity for going online, in brief", "2. \"Then again...,\" in textspeak", "5. Fish in a Disney film", "7. School of the future?"] - }, - "grid": ["I", "O", "N", ".", ".", "S", "T", "U", "N", ".", "P", "O", "K", "E", "R", ".", "H", "E", "M", "O", ".", ".", "M", "O", "E"] -} diff --git a/crossword/public/demos/presidential_appts.xw b/crossword/public/demos/presidential_appts.xw deleted file mode 100644 index ee92da08..00000000 --- a/crossword/public/demos/presidential_appts.xw +++ /dev/null @@ -1,15 +0,0 @@ -{ - "author": "Michael Dirolf", - "title": "Presidential Appointments", - "size": { - "rows": 15, - "cols": 15 - }, - "highlighted": [], - "highlight": "circle", - "clues": { - "across": ["1. Swamp menace, for short", "14. Time out?", "17. First word of Carroll's \"Jabberwocky\"", "20. Presidential architect who designed the New York Times building", "25. Presidential skateboarder who completed the first 900", "34. Hubbub", "37. Insurance company symbol", "43. German cipher machine of W.W. II", "48. Presidential musician who composed the \"The Lion King\" soundtrack", "54. Presidential singer-songwriter who became a congressman", "63. \"____ can survive anything but a misprint\": Oscar Wilde", "66. Type of badge for a scout", "69. \"Moneyball\" subject Billy", "5. Key changer, on a guitar", "9. Implied", "15. Cab alternative", "16. Old Olds", "18. Member of Clinton's cabinet for all eight years", "19. Popular antianxiety drug", "23. Presidential nickname", "24. Iran-contra org.", "35. Leaves after dinner?", "36. Essence", "40. Sport competed in barefoot, in brief", "42. German wine valley", "45. Under the weather", "47. Former telecom giant", "52. Was in front", "53. Is in another form?", "64. Pantry acronym", "65. Last word before dinner?", "67. One of the seven deadly sins", "68. Like most dorms nowadays", "70. Hook's helper", "71. Celebratory dance"], - "down": ["1. Means of surveillance, for short", "2. Seats by the orchestra pit, perhaps", "3. Neighbor of Yemen", "4. The Clash's \"Rock the ___\"", "5. It varies inversely to resistance", "6. Last words of an Ali boast", "7. William with a state named after him", "8. \"The Epic of American Civilization\" muralist", "9. You become one upon receiving your first paycheck", "10. Half court game?", "11. Wrestler / actor John", "12. \"Argo\" setting", "13. Prefix with -plasmosis", "21. Andrew Yang's big policy proposal, in brief", "22. Barnyard sound", "25. Hazard", "26. Glandular prefix", "27. Prefix with -pathy", "28. Beauty", "29. Sri Lankan language", "30. Queen Eliz., e.g.", "31. Courage, in Cádiz", "32. Elizabethan barmaid", "33. Last name in fashion", "38. Metric wts.", "39. Flipped breakfast", "41. PC key", "44. Swear", "46. Something a tuba hits", "49. Ford flops", "50. Aye's opposite", "51. \"The Well-Tempered Clavier\" composer", "54. Vertical part of a window frame", "55. Foil", "56. Roman marketplaces", "57. Sinn ___", "58. Egg cell", "59. It knows, proverbially", "60. Melville's second book", "61. Not e'en once", "62. Wave in italiano"] - }, - "grid": ["C", "R", "O", "C", ".", "C", "A", "P", "O", ".", "T", "A", "C", "I", "T", "C", "O", "M", "A", ".", "U", "B", "E", "R", ".", "A", "L", "E", "R", "O", "T", "W", "A", "S", ".", "R", "E", "N", "O", ".", "X", "A", "N", "A", "X", "V", "A", "N", "B", "U", "R", "E", "N", "Z", "O", "P", "I", "A", "N", "O", ".", ".", ".", "A", "B", "E", ".", ".", "C", "I", "A", ".", ".", ".", ".", "W", "A", "S", "H", "I", "N", "G", "T", "O", "N", "Y", "H", "A", "W", "K", "A", "D", "O", ".", ".", "T", "E", "A", ".", "K", "E", "R", "N", "E", "L", "G", "E", "C", "K", "O", ".", "M", "M", "A", ".", "R", "H", "I", "N", "E", "E", "N", "I", "G", "M", "A", ".", "I", "L", "L", ".", ".", "M", "C", "I", "R", "O", "O", "S", "E", "V", "E", "L", "T", "O", "N", "J", "O", "H", "N", ".", ".", ".", ".", "L", "E", "D", ".", ".", "W", "A", "S", ".", ".", ".", "J", "E", "F", "F", "E", "R", "S", "O", "N", "N", "Y", "B", "O", "N", "O", "A", "P", "O", "E", "T", ".", "E", "V", "O", "O", ".", "A", "M", "E", "N", "M", "E", "R", "I", "T", ".", "L", "U", "S", "T", ".", "C", "O", "E", "D", "B", "E", "A", "N", "E", ".", "S", "M", "E", "E", ".", "H", "O", "R", "A"] -} diff --git a/crossword/public/favicon-32x32.png b/crossword/public/favicon-32x32.png deleted file mode 100644 index dbf764ee..00000000 Binary files a/crossword/public/favicon-32x32.png and /dev/null differ diff --git a/crossword/public/favicon.ico b/crossword/public/favicon.ico deleted file mode 100644 index bc6e13d8..00000000 Binary files a/crossword/public/favicon.ico and /dev/null differ diff --git a/crossword/public/index.html b/crossword/public/index.html deleted file mode 100644 index 14969a5b..00000000 --- a/crossword/public/index.html +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - - Crosshare - crossword community - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - diff --git a/crossword/public/keypress.mp3 b/crossword/public/keypress.mp3 deleted file mode 100644 index 502890c7..00000000 Binary files a/crossword/public/keypress.mp3 and /dev/null differ diff --git a/crossword/public/manifest-icon-192.png b/crossword/public/manifest-icon-192.png deleted file mode 100644 index 572c145b..00000000 Binary files a/crossword/public/manifest-icon-192.png and /dev/null differ diff --git a/crossword/public/manifest-icon-512.png b/crossword/public/manifest-icon-512.png deleted file mode 100644 index 9a6bdeed..00000000 Binary files a/crossword/public/manifest-icon-512.png and /dev/null differ diff --git a/crossword/public/manifest.json b/crossword/public/manifest.json deleted file mode 100644 index ca8d09e0..00000000 --- a/crossword/public/manifest.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "short_name": "Crosshare", - "name": "Crosshare", - "icons": [ - { - "src": "manifest-icon-192.png", - "sizes": "192x192", - "type": "image/png", - "purpose": "maskable any" - }, - { - "src": "manifest-icon-512.png", - "sizes": "512x512", - "type": "image/png", - "purpose": "maskable any" - }, - { - "src": "favicon.ico", - "sizes": "64x64 32x32 24x24 16x16", - "type": "image/x-icon" - } - ], - "start_url": "https://crosshare.org", - "display": "standalone", - "theme_color": "#EB984E", - "background_color": "#ffffff" -} diff --git a/crossword/public/robots.txt b/crossword/public/robots.txt deleted file mode 100644 index 01b0f9a1..00000000 --- a/crossword/public/robots.txt +++ /dev/null @@ -1,2 +0,0 @@ -# https://www.robotstxt.org/robotstxt.html -User-agent: * diff --git a/crossword/public/staging.robots.txt b/crossword/public/staging.robots.txt deleted file mode 100644 index b21f0887..00000000 --- a/crossword/public/staging.robots.txt +++ /dev/null @@ -1,3 +0,0 @@ -# https://www.robotstxt.org/robotstxt.html -User-agent: * -Disallow: / diff --git a/crossword/public/success.mp3 b/crossword/public/success.mp3 deleted file mode 100644 index aa24d6e5..00000000 Binary files a/crossword/public/success.mp3 and /dev/null differ diff --git a/crossword/src/AccountPage.tsx b/crossword/src/AccountPage.tsx deleted file mode 100644 index c414052a..00000000 --- a/crossword/src/AccountPage.tsx +++ /dev/null @@ -1,149 +0,0 @@ -/** @jsx jsx */ -import { jsx } from '@emotion/core'; - -import * as React from 'react'; -import { Link, RouteComponentProps } from "@reach/router"; - -import { requiresAuth, AuthProps } from './App'; -import { UserPlaysV, AuthoredPuzzlesV } from './common/dbtypes'; -import { getFromSessionOrDB } from './dbUtils'; -import { timeString } from './utils' -import { getFirebaseApp } from './firebase'; - -import { Page } from './Page'; - -export const PlayListItem = (props: UserPlay) => { - return ( -
  • {props.title} {props.didComplete ? "completed " + (props.didCheat ? "with helpers" : "without helpers") : "unfinished"} {timeString(props.playTime, false)}
  • - ); -} - -export const AuthoredListItem = (props: AuthoredPuzzle) => { - return ( -
  • {props.title}
  • - ); -} - -export const getDisplayName = (user: firebase.User) => { - return user.displayName || "Anonymous Crossharer"; -} - -export const DisplayNameForm = ({ user, onChange, onCancel }: { user: firebase.User, onChange: (newName: string) => void, onCancel?: () => void }) => { - function sanitize(input: string) { - return input.replace(/[^0-9a-zA-Z ]/g, ''); - } - const [newDisplayName, setNewDisplayName] = React.useState(sanitize(getDisplayName(user))); - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - if (newDisplayName.trim()) { - user.updateProfile({ displayName: newDisplayName.trim() }).then(() => { - if (!user.displayName) { - throw new Error("something went wrong"); - } - onChange(user.displayName); - }); - } - } - - return ( -
    - - - {onCancel ? - - : ''} -
    - ); -}; - -interface AuthoredPuzzle { - id: string, - createdAt: firebase.firestore.Timestamp, - title: string, -} - -interface UserPlay { - id: string, - updatedAt: firebase.firestore.Timestamp, - playTime: number, - didCheat: boolean, - didComplete: boolean, - title: string, -} - -export const AccountPage = requiresAuth(({ user }: RouteComponentProps & AuthProps) => { - const [authoredPuzzles, setAuthoredPuzzles] = React.useState | null>(null); - const [plays, setPlays] = React.useState | null>(null); - const [error, setError] = React.useState(false); - const [displayName, setDisplayName] = React.useState(getDisplayName(user)); - - React.useEffect(() => { - console.log("loading authored puzzles and plays"); - // TODO pagination on both of these - Promise.all([ - getFromSessionOrDB('uc', user.uid, AuthoredPuzzlesV, -1), - getFromSessionOrDB('up', user.uid, UserPlaysV, -1) - ]) - .then(([authoredResult, playsResult]) => { - if (authoredResult === null) { - setAuthoredPuzzles([]); - } else { - const authored = Object.entries(authoredResult).map(([id, val]) => { - const [createdAt, title] = val; - return { id, createdAt, title }; - }) - // Sort in reverse order by createdAt - authored.sort((a, b) => b.createdAt.toMillis() - a.createdAt.toMillis()); - setAuthoredPuzzles(authored); - } - if (playsResult === null) { - setPlays([]); - } else { - const plays = Object.entries(playsResult).map(([id, val]) => { - const [updatedAt, playTime, didCheat, didComplete, title] = val; - return { id, updatedAt, playTime, didCheat, didComplete, title }; - }) - // Sort in reverse order by updatedAt - plays.sort((a, b) => b.updatedAt.toMillis() - a.updatedAt.toMillis()); - setPlays(plays); - } - }).catch(reason => { - console.error(reason); - setError(true); - }); - }, [user]); - - if (error) { - return Error loading plays / authored puzzles; - } - return ( - -
    -

    Account

    -

    You're logged in as {user.email}.

    -

    Your display name - {displayName} - is displayed next to any comments you make or puzzles you create.

    - - {plays && plays.length ? - -

    Recent Plays

    -
      {plays.map(PlayListItem)}
    -
    - : - "" - } - {authoredPuzzles && authoredPuzzles.length ? - -

    Authored Puzzles

    -
      {authoredPuzzles.map(AuthoredListItem)}
    -
    - : - "" - } -
    -
    - ); -}); diff --git a/crossword/src/Admin.tsx b/crossword/src/Admin.tsx deleted file mode 100644 index 642d8d60..00000000 --- a/crossword/src/Admin.tsx +++ /dev/null @@ -1,209 +0,0 @@ -/** @jsx jsx */ -import { jsx } from '@emotion/core'; - -import * as React from 'react'; - -import { navigate, Link, RouteComponentProps } from "@reach/router"; - -import { requiresAdmin, AuthProps } from './App'; -import { Page } from './Page'; -import { PuzzleResult, puzzleFromDB, puzzleTitle } from './types'; -import { - TimestampedPuzzleT, DailyStatsT, DailyStatsV, DBPuzzleV, getDateString, - CategoryIndexT, CategoryIndexV, prettifyDateString, DBPuzzleT, - CommentForModerationWithIdT, CommentForModerationV, CommentWithRepliesT -} from './common/dbtypes'; -import { getFromDB, getFromSessionOrDB, mapEachResult } from './dbUtils'; -import { getFirebaseApp, getTimestampClass } from './firebase'; -import { UpcomingMinisCalendar } from './UpcomingMinisCalendar'; - -const PuzzleListItem = (props: PuzzleResult) => { - return ( -
  • {puzzleTitle(props)} by {props.authorName}
  • - ); -} - -export const Admin = requiresAdmin((_: RouteComponentProps & AuthProps) => { - const [unmoderated, setUnmoderated] = React.useState | null>(null); - const [commentsForModeration, setCommentsForModeration] = React.useState | null>(null); - const [minis, setMinis] = React.useState(null); - const [stats, setStats] = React.useState(null); - const [error, setError] = React.useState(false); - const [commentIdsForDeletion, setCommentIdsForDeletion] = React.useState>(new Set()); - - React.useEffect(() => { - console.log("loading admin content"); - const db = getFirebaseApp().firestore(); - const now = new Date(); - const dateString = getDateString(now); - Promise.all([ - getFromSessionOrDB('ds', dateString, DailyStatsV, 1000 * 60 * 30), - getFromSessionOrDB('categories', 'dailymini', CategoryIndexV, 24 * 60 * 60 * 1000), - mapEachResult(db.collection('c').where("m", "==", false), DBPuzzleV, (dbpuzz, docId) => { - const forStorage: TimestampedPuzzleT = { downloadedAt: getTimestampClass().now(), data: dbpuzz } - sessionStorage.setItem('c/' + docId, JSON.stringify(forStorage)); - return { ...puzzleFromDB(dbpuzz), id: docId }; - }), - mapEachResult(db.collection('cfm'), CommentForModerationV, (cfm, docId) => { - return { ...cfm, i: docId }; - }) - ]) - .then(([stats, minis, unmoderated, cfm]) => { - setStats(stats); - setMinis(minis); - setUnmoderated(unmoderated); - setCommentsForModeration(cfm); - }) - .catch(reason => { - console.error(reason); - setError(true); - }); - }, []); - - const goToPuzzle = React.useCallback((_date: Date, puzzle: string | null) => { - if (puzzle) { - navigate("/crosswords/" + puzzle); - } - }, []); - - if (error) { - return Error loading admin content; - } - if (unmoderated === null || commentsForModeration === null) { - return Loading admin content...; - } - - function titleForId(crosswordId: string): string { - if (minis) { - const dateString = Object.keys(minis).find(key => minis[key] === crosswordId); - if (dateString) { - return "Daily mini for " + prettifyDateString(dateString); - } - } - return crosswordId; - } - - function findCommentById(comments: Array, id: string): CommentWithRepliesT | null { - for (const comment of comments) { - if (comment.i === id) { - return comment; - } - if (comment.r !== undefined) { - const res = findCommentById(comment.r, id); - if (res !== null) { - return res; - } - } - } - return null; - } - - async function moderateComments(e: React.FormEvent) { - e.preventDefault(); - const db = getFirebaseApp().firestore(); - const puzzles: Record = {}; - if (commentsForModeration) { - for (const comment of commentsForModeration) { - // Don't need to do anything for any comment that has been marked for deletion - if (!commentIdsForDeletion.has(comment.i)) { - let puzzle: DBPuzzleT; - if (puzzles.hasOwnProperty(comment.pid)) { - puzzle = puzzles[comment.pid]; - } else { - puzzle = await getFromDB('c', comment.pid, DBPuzzleV); - puzzles[comment.pid] = puzzle; - } - if (puzzle.cs === undefined) { - puzzle.cs = []; - } - if (comment.rt === null) { - puzzle.cs.push(comment); - } else { - const parent = findCommentById(puzzle.cs, comment.rt); - if (parent === null) { - throw new Error('parent comment not found'); - } - if (parent.r) { - parent.r.push(comment); - } else { - parent.r = [comment]; - } - } - } - await db.collection('cfm').doc(comment.i).delete(); - } - } - - // Now we've merged in all the comments, so update the puzzles: - for (const [puzzleId, dbPuzzle] of Object.entries(puzzles)) { - console.log("updating", puzzleId, dbPuzzle.cs); - await db.collection('c').doc(puzzleId).update({ cs: dbPuzzle.cs }); - } - - setCommentsForModeration([]); - } - - function setCommentForDeletion(commentId: string, checked: boolean) { - if (!checked) { - commentIdsForDeletion.delete(commentId); - } else { - commentIdsForDeletion.add(commentId); - } - setCommentIdsForDeletion(new Set(commentIdsForDeletion)); - } - - return ( - -
    -

    Comment Moderation

    - {commentsForModeration.length === 0 ? -
    No comments are currently awaiting moderation.
    - : -
    -

    Check comments to disallow them

    -
      - {commentsForModeration.map((cfm) => -
    • - -
    • - )} -
    - -
    - } -

    Unmoderated

    - {unmoderated.length === 0 ? -
    No puzzles are currently awaiting moderation.
    - : -
      {unmoderated.map(PuzzleListItem)}
    - } - {stats ? - -

    Today's Stats

    -
    Total completions: {stats.n}
    -
    Users w/ completions: {stats.u.length}
    -
    Top Puzzles
    -
      - {Object.entries(stats.c).map(([crosswordId, count]) => { - return ( -
    • - {titleForId(crosswordId)}: {count} - (stats) -
    • - ); - })} -
    -
    - : ""} -

    Upcoming Minis

    - - -
    -
    - ); -}); diff --git a/crossword/src/App.test.tsx b/crossword/src/App.test.tsx deleted file mode 100644 index a2f686a3..00000000 --- a/crossword/src/App.test.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react'; -import { render } from '@testing-library/react'; -import App from './App'; -import { initFirebaseForTesting } from './testUtils'; - -initFirebaseForTesting(); - -test('renders todays mini link', () => { - const { getByText } = render(); - const linkElement = getByText(/today's daily mini crossword/i); - expect(linkElement).toBeInTheDocument(); -}); diff --git a/crossword/src/App.tsx b/crossword/src/App.tsx deleted file mode 100644 index b9d927f2..00000000 --- a/crossword/src/App.tsx +++ /dev/null @@ -1,369 +0,0 @@ -/** @jsx jsx */ -import { jsx } from '@emotion/core'; -import * as React from 'react'; - -import { WindowLocation, Location, Router, RouteComponentProps } from "@reach/router"; -import { Helmet, HelmetProvider } from 'react-helmet-async'; -import { ToastContainer } from 'react-toastify'; -import 'react-toastify/dist/ReactToastify.min.css'; - -import { ErrorBoundary } from './ErrorBoundary'; -import { PuzzleLoader } from './Puzzle'; -import { PuzzleStats } from './PuzzleStats'; -import { Page, SquareTest, TwoColTest } from './Page'; -import { AccountPage } from './AccountPage'; -import { IconTestPage } from './Icons'; -import { Admin } from './Admin'; -import { Home } from './Home'; -import { Category } from './Category'; -import { PlayV, UserPlayT, UserPlaysV } from './common/dbtypes'; -import { getValidatedAndDelete, setInCache, updateInCache } from './dbUtils'; -import { useAuthState, getFirebaseApp, getGoogleAuthProvider } from './firebase'; -import { BuilderDBLoader } from './Builder'; - -import googlesignin from './googlesignin.png'; - -interface AuthContextValue { - user: firebase.User | undefined, - isAdmin: boolean, - loadingUser: boolean, - error: string | undefined -} -export const AuthContext = React.createContext({ user: undefined, loadingUser: false, error: "using default context" } as AuthContextValue); - -type CrosshareAudioContextValue = [AudioContext | null, () => void]; -export const CrosshareAudioContext = React.createContext([null, () => { }]); - -type Optionalize = Omit; - -export interface AuthProps { - isAdmin: boolean, - user: firebase.User, -}; - -export const GoogleSignInButton = () => { - function signin() { - getFirebaseApp().auth().signInWithPopup(getGoogleAuthProvider()) - .then(() => { - getFirebaseApp().analytics().logEvent("login", { method: 'google' }); - }) - } - return ( - - ); -} - -export const GoogleLinkButton = ({ user }: { user: firebase.User }) => { - function signin() { - user.linkWithPopup(getGoogleAuthProvider()) - .then(() => { - console.log("linked w/o needing a merge") - getFirebaseApp().analytics().logEvent("login", { method: 'google' }); - }) - .catch(async (error: any) => { - if (error.code !== 'auth/credential-already-in-use') { - console.log(error); - return; - } - // Get anonymous user plays - const db = getFirebaseApp().firestore(); - await db.collection('up').doc(user.uid).delete(); - const plays = await getValidatedAndDelete(db.collection('p').where('u', '==', user.uid), PlayV); - return getFirebaseApp().auth().signInWithCredential(error.credential).then(async (value: firebase.auth.UserCredential) => { - console.log("signed in as new user " + value.user ?.uid); - const newUser = value.user; - if (!newUser) { - throw new Error('missing new user after link'); - } - await Promise.all(plays.map((play) => { - play.u = newUser.uid; - console.log("Updating play " + play.c + '-' + play.u) - const userPlay: UserPlayT = [play.ua, play.t, play.ch, play.f, 'Crossword']; - return Promise.all([ - setInCache('p', play.c + "-" + newUser.uid, play, PlayV, true), - updateInCache('up', newUser.uid, { [play.c]: userPlay }, UserPlaysV, true) - ]); - })); - user.delete(); - console.log("linked and merged plays"); - }); - }); - } - return ( - - ); -} - -/* Ensure we have at least an anonymous user */ -export function ensureUser(WrappedComponent: React.ComponentType) { - return (props: Optionalize) => { - const { user, isAdmin, loadingUser, error } = React.useContext(AuthContext); - if (loadingUser) { - return Loading user...; - } - if (error) { - return Error loading user: {error}; - } - if (!user) { - getFirebaseApp().auth().signInAnonymously().then(() => { - getFirebaseApp().analytics().logEvent("login", { method: 'anonymous' }); - }) - return Loading user...; - }; - return - } -} - -/* Ensure we have a non-anonymous user, upgrading an anonymous user if we have one. */ -export function requiresAuth(WrappedComponent: React.ComponentType) { - return (props: Optionalize) => { - const { user, isAdmin, loadingUser, error } = React.useContext(AuthContext); - if (loadingUser) { - return Loading user...; - } - if (error) { - return Error loading user: {error}; - } - if (!user) { - return ( - -
    -

    Please sign-in with your Google account to continue. We use your account to keep track of the puzzles you've played and your solve streaks.

    - -
    -
    - ); - } - if (user.isAnonymous) { - return ( - -
    -

    Please sign-in with your Google account to continue. We use your account to keep track of the puzzles you've played and your solve streaks.

    - -
    -
    - ); - } - return - } -} - -/* Ensure we have an admin user, upgrading an anonymous user if we have one. */ -export function requiresAdmin(WrappedComponent: React.ComponentType) { - return (props: Optionalize) => { - const { user, isAdmin, loadingUser, error } = React.useContext(AuthContext); - if (loadingUser) { - return Loading user...; - } - if (error) { - return Error loading user: {error}; - } - if (user && user.email) { - if (!isAdmin) { - return Must be an admin to view this page.; - } - return - } - return ( - -
    -

    Please sign-in to continue. You must be an admin to view this page.

    - -
    -
    - ); - } -} - -const ErrorTest = requiresAdmin((_: RouteComponentProps & AuthProps) => { - if (1) - throw new Error("testing"); - return
    error
    ; -}); - -const NotFound = (_: RouteComponentProps) => { - return not found :(; -} - -const TermsOfService = (_: RouteComponentProps) => { - return ( - -

    Crosshare Terms of Service

    -

    1. Terms

    -

    By accessing the website at https://crosshare.org, you are agreeing to be bound by these terms of service, all applicable laws and regulations, and agree that you are responsible for compliance with any applicable local laws. If you do not agree with any of these terms, you are prohibited from using or accessing this site. The materials contained in this website are protected by applicable copyright and trademark law.

    -

    2. Use License

    -

    Permission is granted to temporarily download one copy of the materials (information or software) on Crosshare's website for personal, non-commercial transitory viewing only. This is the grant of a license, not a transfer of title, and under this license you may not:

    -

    - modify or copy the materials;

    -

    - use the materials for any commercial purpose, or for any public display (commercial or non-commercial);

    -

    - attempt to decompile or reverse engineer any software contained on Crosshare's website;

    -

    - remove any copyright or other proprietary notations from the materials; or

    -

    - transfer the materials to another person or "mirror" the materials on any other server.

    -

    This license shall automatically terminate if you violate any of these restrictions and may be terminated by Crosshare at any time. Upon terminating your viewing of these materials or upon the termination of this license, you must destroy any downloaded materials in your possession whether in electronic or printed format.

    -

    3. Disclaimer

    -

    The materials on Crosshare's website are provided on an 'as is' basis. Crosshare makes no warranties, expressed or implied, and hereby disclaims and negates all other warranties including, without limitation, implied warranties or conditions of merchantability, fitness for a particular purpose, or non-infringement of intellectual property or other violation of rights.

    -

    Further, Crosshare does not warrant or make any representations concerning the accuracy, likely results, or reliability of the use of the materials on its website or otherwise relating to such materials or on any sites linked to this site.

    -

    4. Limitations

    -

    In no event shall Crosshare or its suppliers be liable for any damages (including, without limitation, damages for loss of data or profit, or due to business interruption) arising out of the use or inability to use the materials on Crosshare's website, even if Crosshare or a Crosshare authorized representative has been notified orally or in writing of the possibility of such damage. Because some jurisdictions do not allow limitations on implied warranties, or limitations of liability for consequential or incidental damages, these limitations may not apply to you.

    -

    5. Accuracy of materials

    -

    The materials appearing on Crosshare's website could include technical, typographical, or photographic errors. Crosshare does not warrant that any of the materials on its website are accurate, complete or current. Crosshare may make changes to the materials contained on its website at any time without notice. However Crosshare does not make any commitment to update the materials.

    -

    6. Links

    -

    Crosshare has not reviewed all of the sites linked to its website and is not responsible for the contents of any such linked site. The inclusion of any link does not imply endorsement by Crosshare of the site. Use of any such linked website is at the user's own risk.

    -

    7. Modifications

    -

    Crosshare may revise these terms of service for its website at any time without notice. By using this website you are agreeing to be bound by the then current version of these terms of service.

    -

    8. Governing Law

    -

    These terms and conditions are governed by and construed in accordance with the laws of New York and you irrevocably submit to the exclusive jurisdiction of the courts in that State or location.

    -
    - ); -} - -const PrivacyPolicy = (_: RouteComponentProps) => { - return ( - -

    Privacy Policy

    -

    Your privacy is important to us. It is Crosshare's policy to respect your privacy regarding any information we may collect from you across our website, https://crosshare.org, and other sites we own and operate.

    -

    We only ask for personal information when we truly need it to provide a service to you. We collect it by fair and lawful means, with your knowledge and consent. We also let you know why we’re collecting it and how it will be used.

    -

    We only retain collected information for as long as necessary to provide you with your requested service. What data we store, we’ll protect within commercially acceptable means to prevent loss and theft, as well as unauthorized access, disclosure, copying, use or modification.

    -

    We don’t share any personally identifying information publicly or with third-parties, except when required to by law.

    -

    Our website may link to external sites that are not operated by us. Please be aware that we have no control over the content and practices of these sites, and cannot accept responsibility or liability for their respective privacy policies.

    -

    You are free to refuse our request for your personal information, with the understanding that we may be unable to provide you with some of your desired services.

    -

    Your continued use of our website will be regarded as acceptance of our practices around privacy and personal information. If you have any questions about how we handle user data and personal information, feel free to contact us.

    -

    This policy is effective as of 10 March 2020.

    -
    - ); -} - -const Construct = (_: RouteComponentProps) => { - let size = 5; - let grid = [ - " ", " ", " ", " ", " ", - " ", " ", " ", " ", " ", - " ", " ", " ", " ", " ", - " ", " ", " ", " ", " ", - " ", " ", " ", " ", " ", - ]; - // size = 15; - // grid = [ - // " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", - // " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", - // " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", - // " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", - // " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", - // " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", - // " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", - // " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", - // " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", - // " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", - // " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", - // " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", - // " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", - // " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", - // " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", - // ]; - // size = 15; - // grid = [ - // " . . ", - // " . . ", - // " . . ", - // "VANBURENZOPIANO", - // "... .. ....", - // "WASHINGTONYHAWK", - // " .. . ", - // " . . ", - // " . .. ", - // "ROOSEVELTONJOHN", - // ".... .. ...", - // "JEFFERSONNYBONO", - // " . . ", - // " . . ", - // " . . " - // ]; - // grid = grid.map(s => s.split("")).flat(); - const props = { - "size": { - "rows": size, - "cols": size - }, - "grid": grid - } - - return ; -}; - -const PageViewTracker = ({ location }: { location: WindowLocation }) => { - React.useEffect(() => { - try { - getFirebaseApp().analytics().logEvent("screen_view", { app_name: 'react', screen_name: location.pathname }); - } catch (e) { - if (!(e instanceof TypeError)) { - throw e; - } - } - }, [location]); - return ; -} - -const App = () => { - const [user, loadingUser, error] = useAuthState(); - const [isAdmin, setIsAdmin] = React.useState(false); - React.useEffect(() => { - if (user && user.email) { - user.getIdTokenResult() - .then((idTokenResult) => { - if (!!idTokenResult.claims.admin) { - setIsAdmin(true); - } else { - setIsAdmin(false); - } - }) - .catch((error) => { - setIsAdmin(false); - console.error(error); - }); - } - }, [user]); - - const [audioContext, setAudioContext] = React.useState(null); - const initAudioContext = React.useCallback(() => { - if (!audioContext) { - const constructor = window.AudioContext || (window as any).webkitAudioContext; - setAudioContext(new constructor()); - } - }, [audioContext, setAudioContext]); - return ( - - - - - - - {({ location }) => ( - - )} - - Loading...}> - - - - - - - - - - - - - - - - - - - - - - - ); -} - -export default App; diff --git a/crossword/src/AutoSizer.tsx b/crossword/src/AutoSizer.tsx deleted file mode 100644 index b45872b1..00000000 --- a/crossword/src/AutoSizer.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import * as React from 'react'; -import createDetectElementResize from './vendor/detectElementResize'; - -declare global { - interface Window { - HTMLElement: typeof HTMLElement; - } -} - -interface Size { - height: number, - width: number, -}; - -interface Props { - /** Function responsible for rendering children. */ - children: (size: Size) => React.ReactNode; -} - -interface State { - height: number, - width: number, -}; - -type ResizeHandler = (element: HTMLElement, onResize: () => void) => void; - -interface DetectElementResize { - addResizeListener: ResizeHandler, - removeResizeListener: ResizeHandler, -}; - -export default class AutoSizer extends React.PureComponent { - static defaultProps = { - onResize: () => { }, - disableHeight: false, - disableWidth: false, - style: {}, - }; - - state = { - height: 0, - width: 0, - }; - - _parentNode?: HTMLElement; - _autoSizer?: HTMLElement; - _detectElementResize?: DetectElementResize; - - componentDidMount() { - if ( - this._autoSizer && - this._autoSizer.parentNode && - this._autoSizer.parentNode.ownerDocument && - this._autoSizer.parentNode.ownerDocument.defaultView && - this._autoSizer.parentNode instanceof - this._autoSizer.parentNode.ownerDocument.defaultView.HTMLElement - ) { - // Delay access of parentNode until mount. - // This handles edge-cases where the component has already been unmounted before its ref has been set, - // As well as libraries like react-lite which have a slightly different lifecycle. - this._parentNode = this._autoSizer.parentNode; - - // Defer requiring resize handler in order to support server-side rendering. - // See issue #41 - this._detectElementResize = createDetectElementResize(null); - if (this._parentNode) { - this._detectElementResize.addResizeListener( - this._parentNode, - this._onResize, - ); - } - - this._onResize(); - } - } - - componentWillUnmount() { - if (this._detectElementResize && this._parentNode) { - this._detectElementResize.removeResizeListener( - this._parentNode, - this._onResize, - ); - } - } - - render() { - const { height, width } = this.state; - - // Outer div should not force width/height since that may prevent containers from shrinking. - // Inner component should overflow and use calculated width/height. - // See issue #68 for more information. - const outerStyle: React.CSSProperties = { width: 0, height: 0, overflow: 'visible' }; - const childParams: Size = {width: width, height: height}; - - return ( -
    - {width !== 0 && height !== 0 && this.props.children(childParams)} -
    - ); - } - - _onResize = () => { - if (this._parentNode) { - // Guard against AutoSizer component being removed from the DOM immediately after being added. - // This can result in invalid style values which can result in NaN values if we don't handle them. - // See issue #150 for more context. - - const height = this._parentNode.offsetHeight || 0; - const width = this._parentNode.offsetWidth || 0; - - const style = window.getComputedStyle(this._parentNode) || {}; - const paddingLeft = parseInt(style.paddingLeft, 10) || 0; - const paddingRight = parseInt(style.paddingRight, 10) || 0; - const paddingTop = parseInt(style.paddingTop, 10) || 0; - const paddingBottom = parseInt(style.paddingBottom, 10) || 0; - - let newHeight = height - paddingTop - paddingBottom; - let newWidth = width - paddingLeft - paddingRight; - - if ( - (this.state.height !== newHeight) || - (this.state.width !== newWidth) - ) { - this.setState({ - height: newHeight, - width: newWidth, - }); - } - } - }; - - _setRef = (autoSizer: HTMLDivElement) => { - this._autoSizer = autoSizer; - }; -} diff --git a/crossword/src/Autofiller.ts b/crossword/src/Autofiller.ts deleted file mode 100644 index f8849580..00000000 --- a/crossword/src/Autofiller.ts +++ /dev/null @@ -1,309 +0,0 @@ -import { valAt, getCrosses } from './gridBase'; -import { - AutofillGrid, fromTemplate, minGridCost, stableSubsets, - numMatchesForEntry, gridWithEntryDecided -} from './autofillGrid'; -import * as WordDB from './WordDB'; - -enum ResultTag { - Recur, - Value -} -interface Result { - type: ResultTag -} -interface Recur extends Result { - type: ResultTag.Recur, - grid: AutofillGrid, - discrep: number, - pitched: Set, - subset: Set | null, - cont: Cont -} -function isRecur(result: Result): result is Recur { - return result.type === ResultTag.Recur; -} -function recur(grid: AutofillGrid, discrep: number, pitched: Set, subset: Set | null, cont: Cont): Recur { - return { type: ResultTag.Recur, grid, discrep, pitched, subset, cont }; -} -interface Value extends Result { - type: ResultTag.Value, - result: AutofillGrid | null -} -function isValue(result: Result): result is Value { - return result.type === ResultTag.Value; -} -function value(x: AutofillGrid | null) { - return { type: ResultTag.Value, result: x } -} - -type Cont = (x: AutofillGrid | null) => Result; - -export class Autofiller { - public initialGrid: AutofillGrid; - public completed: boolean; - public solnGrid: AutofillGrid | null; - public solnCost: number | null; - public nextStep: Result; - public postedSoln: boolean; - public startTime: number; - - constructor( - public readonly grid: string[], - public readonly width: number, - public readonly height: number, - public onResult: (input: string[], result: string[]) => void, - public onComplete: () => void - ) { - this.initialGrid = fromTemplate(this.grid, this.width, this.height); - this.completed = false; - this.solnCost = null; - this.solnGrid = null; - this.postedSoln = false; - this.startTime = new Date().getTime(); - - // Our initial step is a call to recur - this.nextStep = recur(this.initialGrid, 5, new Set(), null, (result) => { return value(result) }); - }; - - step() { - if (!WordDB.dbTransformed) { - console.error("Worker has no db but attempting autofill"); - this.completed = true; - return; - } - if (this.completed) { - console.log("Calling step but already completed"); - return; - } - - /* Take as many steps as we can w/in 50ms. After that we need to take a - * break so that we have a chance to get any new input from the user. */ - const start = new Date().getTime(); - while (isRecur(this.nextStep) && new Date().getTime() - start < 50) { - this.nextStep = this._solve( - this.nextStep.grid, - this.nextStep.discrep, - this.nextStep.pitched, - this.nextStep.subset, - this.nextStep.cont); - } - - // If it's a "value" (not a "recur") we're done. - if (isValue(this.nextStep)) { - console.log("Finished: " + ((new Date().getTime() - this.startTime) / 1000).toPrecision(4) + "s"); - this.completed = true; - this.onComplete(); - } - - // If we have a solution that hasn't been posted yet, post it. - if (this.solnGrid && !this.postedSoln) { - this.postedSoln = true; - this.onResult(this.grid, this.solnGrid.cells); - } - }; - - /* Fill out a grid or a subset of a grid */ - _solve(grid: AutofillGrid, discrep: number, pitched: Set, subset: Set | null, cont: Cont): Result { - const baseCost = minGridCost(grid); - - // We already have a solution that's better than this grid could possibly get - if (this.solnCost && baseCost > this.solnCost) { - return cont(null); - } - - let entriesToConsider = grid.entries.filter((e) => !e.completedWord); - // There are no entries left to consider, this grid must be a new best solution - if (entriesToConsider.length === 0) { - this.solnGrid = grid - this.solnCost = baseCost - this.postedSoln = false; - return cont(grid) - } - - if (subset !== null) { - entriesToConsider = entriesToConsider.filter((e) => subset.has(e.index)); - } - // There are no entries left in this subset, so we're done with this subsection - if (entriesToConsider.length === 0) { - return cont(grid); - } - - // See if there are any stable subsets out of the entries we're considering - const subsets = stableSubsets(grid, subset); - if (subsets.length > 1) { - subsets.sort((a, b) => a.size - b.size); - // Attempt to solve the smallest subset - return recur(grid, discrep, pitched, subsets[0], subsolved => { - if (subsolved === null) { - // The subset couldn't be solved, so this grid is a failure - return cont(null); - } else { - // Solve the rest of the grid - return recur(subsolved, discrep, pitched, subset, cont); - } - }); - } - - // Consider entries in order of possible matches - entriesToConsider.sort((e1, e2) => numMatchesForEntry(e1) - numMatchesForEntry(e2)); - let successor: [AutofillGrid, number, string] | null = null; - let successorDiff: number | null = null; - - for (const entry of entriesToConsider) { - const crosses = getCrosses(grid, entry) - let bestGrid: [AutofillGrid, number, string] | null = null; - let bestCost: number | null = null; - let secondBestCost: number | null = null; - - let skipEntry = false; - const failingLetters: Array = []; - entry.cells.forEach(() => { failingLetters.push("") }); - const succeedingLetters: Array = []; - entry.cells.forEach(() => { succeedingLetters.push("") }); - for (const [word, score] of WordDB.matchingWords(entry.length, entry.bitmap)) { - if (pitched.has(entry.index + ":" + word)) { - continue; - } - if (grid.usedWords.has(word)) { - continue; - } - - // If we have a secondBestCost for this entry we know it's lower than existing soln cost - const costToBeat = secondBestCost !== null ? secondBestCost : this.solnCost; - - // Fail fast based on score change due to this entry alone - if (costToBeat !== null && ((baseCost - entry.minCost + 1 / score) > costToBeat)) { - continue; - } - - // Fail fast based on score change due to any crosses - let failFast = false; - let j = -1; - for (let i = 0; i < entry.cells.length; i += 1) { - j += 1; - const cell = valAt(grid, entry.cells[i]); - if (cell !== ' ') { // Don't need to check cross - j += cell.length - 1; - continue; - } - const crossIndex = crosses[i].entryIndex; - if (crossIndex === null) { - continue; - } - if (failingLetters[i].indexOf(word[j]) !== -1) { - failFast = true; - break; - } - } - if (!failFast) { - let j = -1; - for (let i = 0; i < entry.cells.length; i += 1) { - j += 1; - const cell = valAt(grid, entry.cells[i]); - if (cell !== ' ') { // Don't need to check cross - j += cell.length - 1; - continue; - } - const crossIndex = crosses[i].entryIndex; - if (crossIndex === null) { - continue; - } - if (succeedingLetters[i].indexOf(word[j]) !== -1) { - continue; - } - const cross = grid.entries[crossIndex]; - const crossLength = cross.length; - const newBitmap = WordDB.updateBitmap(crossLength, cross.bitmap, crosses[i].wordIndex, word[j]); - if (newBitmap.isZero()) { - failingLetters[i] += word[j]; - failFast = true; - break; - } - const newCost = baseCost - cross.minCost + WordDB.minCost(crossLength, newBitmap); - if (costToBeat !== null && newCost > costToBeat) { - failingLetters[i] += word[j]; - failFast = true; - break; - } - succeedingLetters[i] += word[j]; - } - } - if (failFast) { - continue; - } - - const newgrid = gridWithEntryDecided(grid, entry.index, word, score); - if (newgrid === null) { - continue; - } - - const newCost = minGridCost(newgrid); - - // Check overall score - if (costToBeat && newCost > costToBeat) { - continue; - } - - if (bestGrid === null || bestCost === null) { - bestGrid = [newgrid, entry.index, word]; - bestCost = newCost; - } else if (newCost < bestCost) { - bestGrid = [newgrid, entry.index, word]; - secondBestCost = bestCost; - bestCost = newCost; - } else if (secondBestCost === null || newCost < secondBestCost) { - secondBestCost = newCost; - if (successorDiff && secondBestCost - baseCost < successorDiff) { - skipEntry = true; - break; - } - } - } - - if (skipEntry) { - break; - } - - if (bestGrid === null || bestCost === null) { // No valid option for this entry, bad grid - return cont(null); - } - - if (secondBestCost === null) { // No backup option, so this entry is forced - successor = bestGrid; - break; - } - - const costDiff = secondBestCost - bestCost; - if (successorDiff === null || costDiff > successorDiff) { // No successor or this one has higher cost differential - successor = bestGrid; - successorDiff = costDiff; - } - } - - if (successor === null) { - throw new Error("successor was null"); - } - const suc = successor; // weird hack around type system not realizing successor isn't null - let nextSubset = null; - if (subset !== null) { - nextSubset = new Set(Array.from(subset).filter(e => e !== suc[1])) - } - - if (!discrep || pitched.size >= discrep) { - return recur(successor[0], discrep, pitched, nextSubset, cont); - } - - return recur(successor[0], discrep, pitched, nextSubset, - result => { - const newPitched = new Set(pitched.values()); - newPitched.add(suc[1] + ":" + suc[2]); - return recur(grid, discrep, newPitched, subset, - res2 => { - return cont((res2 !== null && (result === null || minGridCost(res2) < minGridCost(result))) ? res2 : result); - } - ); - } - ) - }; -} diff --git a/crossword/src/Builder.tsx b/crossword/src/Builder.tsx deleted file mode 100644 index 1e30c318..00000000 --- a/crossword/src/Builder.tsx +++ /dev/null @@ -1,567 +0,0 @@ -/** @jsx jsx */ -import { jsx } from '@emotion/core'; - -import * as React from 'react'; - -import { isMobile, isTablet, isIPad13 } from "react-device-detect"; -import { - FaRegNewspaper, FaUser, FaListOl, FaRegCircle, FaRegCheckCircle, FaTabletAlt, - FaKeyboard, FaEllipsisH, FaVolumeUp, FaVolumeMute, FaFillDrip, FaUserLock -} from 'react-icons/fa'; -import { IoMdStats } from 'react-icons/io'; -import useEventListener from '@use-it/event-listener'; -import { Helmet } from "react-helmet-async"; -import { FixedSizeList as List } from "react-window"; -import { navigate } from '@reach/router'; - -import AutoSizer from "./AutoSizer"; -import { - Rebus, SpinnerWorking, SpinnerFinished, SpinnerFailed, SpinnerDisabled, - SymmetryIcon, SymmetryRotational, SymmetryVertical, SymmetryHorizontal, SymmetryNone, - EscapeKey, BacktickKey -} from './Icons'; -import { requiresAdmin, AuthProps } from './App'; -import { getFirebaseApp, getTimestampClass } from './firebase'; -import { GridView } from './Grid'; -import { getCrosses, valAt, entryAndCrossAtPosition } from './gridBase'; -import { fromCells, getClueMap } from './viewableGrid'; -import { TimestampedPuzzleT, AuthoredPuzzleT, AuthoredPuzzlesV } from './common/dbtypes' -import { updateInCache } from './dbUtils'; -import { Direction, PuzzleT } from './types'; -import { - Symmetry, BuilderState, BuilderEntry, builderReducer, validateGrid, - KeypressAction, SetClueAction, SymmetryAction, ClickedFillAction, PuzzleAction, - SetTitleAction, SetHighlightAction, PublishAction -} from './reducer'; -import { TopBarLink, TopBar, TopBarDropDownLink, TopBarDropDown } from './TopBar'; -import { SquareAndCols, TinyNav, Page } from './Page'; -import { RebusOverlay } from './Puzzle'; -import AutofillWorker from 'worker-loader!./autofill.worker.ts'; // eslint-disable-line import/no-webpack-loader-syntax -import { isAutofillCompleteMessage, isAutofillResultMessage, WorkerMessage, LoadDBMessage, AutofillMessage } from './types'; -import * as WordDB from './WordDB'; -import { Overlay } from './Overlay'; -import { usePersistedBoolean } from './hooks'; -import { buttonAsLink } from './style'; - -let worker: Worker; - -type WithOptional = Omit & Partial>; -type BuilderProps = WithOptional, "clues" | "title" | "highlighted" | "highlight"> - -export const BuilderDBLoader = requiresAdmin((props: BuilderProps & AuthProps) => { - const [ready, setReady] = React.useState(false); - WordDB.initializeOrBuild(setReady); - if (ready) { - return ; - } - return Loading word database... -}); - -interface PotentialFillItemProps { - isGoodSuggestion: (entryIndex: number, word: string) => boolean, - entryIndex: number, - value: [string, number], - dispatch: React.Dispatch, -} -const PotentialFillItem = (props: PotentialFillItemProps) => { - function click(e: React.MouseEvent) { - e.preventDefault(); - props.dispatch({ type: 'CLICKEDFILL', entryIndex: props.entryIndex, value: props.value[0] }); - } - return ( -
    - {props.value[0]} -
    - ); -} - -interface PotentialFillListProps { - isGoodSuggestion: (entryIndex: number, word: string) => boolean, - header?: string, - entryIndex: number, - values: Array<[string, number]>, - dispatch: React.Dispatch, -} -const PotentialFillList = (props: PotentialFillListProps) => { - const listRef = React.useRef(null); - React.useEffect(() => { - if (listRef.current !== null) { - listRef.current.scrollToItem(0); - } - }, [props.entryIndex, props.values]); - return ( -
    {props.header ? -
    {props.header}
    : ""} -
    - - {({ height, width }) => { - return ( - {({ index, style }) => ( -
    - -
    - )}
    ) - }} -
    -
    -
    - ); -} - -const ClueRow = (props: { dispatch: React.Dispatch, entry: BuilderEntry, clues: Map }) => { - if (props.entry.completedWord === null) { - throw new Error("shouldn't ever get here"); - } - return ( - - {props.entry.completedWord} - { - const sca: SetClueAction = { type: "SETCLUE", word: props.entry.completedWord || "", clue: e.target.value }; - props.dispatch(sca); - }} /> - - ); -} - -interface ClueModeProps { - title: string | null, - exitClueMode: () => void, - completedEntries: Array, - clues: Map, - dispatch: React.Dispatch, -} -const ClueMode = (props: ClueModeProps) => { - const topbar = ( - - } text="Back to Grid" onClick={props.exitClueMode} /> - - ); - const clueRows = props.completedEntries.map(e => ); - return ( - -
    -

    Title

    - { - const sta: SetTitleAction = { type: "SETTITLE", value: e.target.value }; - props.dispatch(sta); - }} /> -

    Clues

    - {props.completedEntries.length ? - - - {clueRows} - -
    - : - -

    This where you come to set clues for your puzzle, but you don't have any completed fill words yet!

    -

    Go back to and fill in one or more words completely. Then come back here and make some clues.

    -
    - } -
    -
    - ) -} - -export const Builder = (props: BuilderProps & AuthProps) => { - const initialGrid = fromCells({ - mapper: (e) => e, - width: props.size.cols, - height: props.size.rows, - cells: props.grid, - allowBlockEditing: true, - highlighted: new Set(props.highlighted), - highlight: props.highlight || "circle", - }); - const [state, dispatch] = React.useReducer(builderReducer, { - type: 'builder', - title: props.title || null, - active: { col: 0, row: 0, dir: Direction.Across }, - grid: initialGrid, - showKeyboard: isMobile || isIPad13, - isTablet: isTablet || isIPad13, - showExtraKeyLayout: false, - isEnteringRebus: false, - rebusValue: '', - gridIsComplete: false, - repeats: new Set(), - hasNoShortWords: false, - isEditable: () => true, - symmetry: Symmetry.Rotational, - clues: getClueMap(initialGrid, props.clues || []), - publishErrors: [], - toPublish: null, - authorId: props.user.uid, - authorName: props.user.displayName || "Anonymous", - postEdit(_cellIndex) { - return validateGrid(this); - } - }, validateGrid); - - const [autofilledGrid, setAutofilledGrid] = React.useState([]); - const [autofillInProgress, setAutofillInProgress] = React.useState(false); - - // TODO should we actually disable autofill? Right now it just turns off display - const [autofillEnabled, setAutofillEnabled] = React.useState(true); - - // We need a ref to the current grid so we can verify it in worker.onmessage - const currentCells = React.useRef(state.grid.cells); - React.useEffect(() => { - if (!WordDB.dbEncoded) { - throw new Error("missing db!"); - } - currentCells.current = state.grid.cells; - setAutofilledGrid([]); - if (!worker) { - console.log("initializing worker"); - worker = new AutofillWorker(); - worker.onmessage = e => { - const data = e.data as WorkerMessage; - if (isAutofillResultMessage(data)) { - if (currentCells.current.every((c, i) => c === data.input[i])) { - setAutofilledGrid(data.result); - } - } else if (isAutofillCompleteMessage(data)) { - setAutofillInProgress(false); - } else { - console.error("unhandled msg in builder: ", e.data); - } - }; - let loaddb: LoadDBMessage = { type: 'loaddb', db: WordDB.dbEncoded }; - worker.postMessage(loaddb); - } - let autofill: AutofillMessage = { - type: 'autofill', - grid: state.grid.cells, - width: state.grid.width, - height: state.grid.height - }; - setAutofillInProgress(true); - worker.postMessage(autofill); - }, [state.grid.cells, state.grid.width, state.grid.height]); - - const [clueMode, setClueMode] = React.useState(false); - if (clueMode) { - return e.completedWord)} exitClueMode={() => setClueMode(false)} /> - } - return -} - -interface GridModeProps { - user: firebase.User, - isAdmin: boolean, - autofillEnabled: boolean, - setAutofillEnabled: (val: boolean) => void, - autofilledGrid: string[], - autofillInProgress: boolean, - state: BuilderState, - dispatch: React.Dispatch, - setClueMode: (val: boolean) => void, -} -const GridMode = ({ state, dispatch, setClueMode, ...props }: GridModeProps) => { - const [muted, setMuted] = usePersistedBoolean("muted", false); - - const physicalKeyboardHandler = React.useCallback((e: React.KeyboardEvent) => { - if (e.metaKey || e.altKey || e.ctrlKey) { - return; // This way you can still do apple-R and such - } - const kpa: KeypressAction = { type: "KEYPRESS", key: e.key, shift: e.shiftKey }; - dispatch(kpa); - e.preventDefault(); - }, [dispatch]); - useEventListener('keydown', physicalKeyboardHandler); - - let left = ; - let right = ; - let tiny = ; - const doCrossesWork = React.useCallback((entryIndex: number, word: string) => { - let successFailureEntries = new Map>(); - const entry = state.grid.entries[entryIndex]; - let successFailure = successFailureEntries.get(entryIndex); - if (successFailure === undefined) { - successFailure = new Map(); - successFailureEntries.set(entryIndex, successFailure); - } - const crosses = getCrosses(state.grid, entry) - let j = -1; - for (let i = 0; i < entry.cells.length; i += 1) { - j += 1; - const cell = valAt(state.grid, entry.cells[i]); - if (cell.length > 1) { - // Handle rebuses - j += cell.length - 1; - continue; - } - if (!entry.completedWord && cell !== ' ') { - continue; - } - const crossIndex = crosses[i].entryIndex; - if (crossIndex === null) { - continue; - } - const cross = state.grid.entries[crossIndex]; - if (cross.completedWord) { - continue; - } - let crossSuccessFailure = successFailure.get(i); - if (crossSuccessFailure === undefined) { - crossSuccessFailure = ["", ""]; - } - const [succeeding, failing] = crossSuccessFailure; - if (failing.indexOf(word[j]) !== -1) { - return false; - } - if (succeeding.indexOf(word[j]) !== -1) { - continue; - } - let crossPattern = ''; - for (let crossCell of cross.cells) { - if (crossCell.row === entry.cells[i].row && crossCell.col === entry.cells[i].col) { - crossPattern += word[j]; - } else { - crossPattern += valAt(state.grid, crossCell); - } - } - const newBitmap = WordDB.matchingBitmap(crossPattern); - if (newBitmap ?.isZero()) { - successFailure.set(i, [succeeding, failing + word[j]]); - return false; - } else { - successFailure.set(i, [succeeding + word[j], failing]); - } - } - return true; - }, [state.grid]); - for (let entry of entryAndCrossAtPosition(state.grid, state.active)) { - if (entry !== null) { - let matches: Array<[string, number]>; - let pattern = ""; - let crosses = getCrosses(state.grid, entry); - for (let index = 0; index < entry.cells.length; index += 1) { - const cell = entry.cells[index]; - const val = valAt(state.grid, cell); - const cross = crosses[index]; - // If complete, remove any cells whose crosses aren't complete and show that - if (entry.completedWord && - val.length === 1 && - cross.entryIndex !== null && - !state.grid.entries[cross.entryIndex].completedWord) { - pattern += " "; - } else { - pattern += val; - } - } - matches = WordDB.matchingWords(pattern.length, WordDB.matchingBitmap(pattern)); - if (entry.direction === state.active.dir) { - tiny = ; - } - if (entry.direction === Direction.Across) { - left = ; - } else { - right = ; - } - } - } - - const { autofillEnabled, setAutofillEnabled } = props; - const toggleAutofillEnabled = React.useCallback(() => { - setAutofillEnabled(!autofillEnabled); - }, [autofillEnabled, setAutofillEnabled]); - - - let totalLength = 0; - state.grid.entries.forEach((e) => totalLength += e.cells.length); - const numEntries = state.grid.entries.length; - const averageLength = totalLength / numEntries; - - const publishInProgress = React.useRef(false); - React.useEffect(() => { - if (state.toPublish === null || publishInProgress.current) { - return; - } - const dbpuzzle = state.toPublish; - publishInProgress.current = true; - console.log("Uploading"); - console.log(dbpuzzle); - const db = getFirebaseApp().firestore(); - db.collection("c").add(dbpuzzle).then(async (ref) => { - console.log("Uploaded", ref.id); - - const authoredPuzzle: AuthoredPuzzleT = [dbpuzzle.ca, dbpuzzle.t]; - await updateInCache('uc', props.user.uid, { [ref.id]: authoredPuzzle }, AuthoredPuzzlesV, true); - - const forStorage: TimestampedPuzzleT = { downloadedAt: getTimestampClass().now(), data: dbpuzzle } - sessionStorage.setItem('c/' + ref.id, JSON.stringify(forStorage)); - navigate("/crosswords/" + ref.id); - }); - }, [state.toPublish, props.user.uid]); - - const keyboardHandler = React.useCallback((key: string) => { - const kpa: KeypressAction = { type: "KEYPRESS", key: key, shift: false }; - dispatch(kpa); - }, [dispatch]); - - const topBar = React.useMemo(() => { - let autofillIcon = ; - let autofillText = "Autofill disabled"; - if (props.autofillEnabled) { - if (props.autofillInProgress) { - autofillIcon = ; - autofillText = "Autofill in progress"; - } else if (props.autofilledGrid.length) { - autofillIcon = ; - autofillText = "Autofill complete"; - } else { - autofillIcon = ; - autofillText = "Couldn't autofill this grid"; - } - } - return - - } text="Clues" onClick={() => setClueMode(true)} /> - } text="Stats"> -

    Grid status

    -
    {state.gridIsComplete ? : } All cells should be filled
    -
    {state.hasNoShortWords ? : } All words should be at least three letters
    -
    {state.repeats.size > 0 ? ({Array.from(state.repeats).sort().join(", ")}) : } No words should be repeated
    -

    Fill

    -
    Number of words: {numEntries}
    -
    Mean word length: {averageLength.toPrecision(3)}
    -
    - } text="Symmetry"> - } text="Rotational Symmetry" onClick={() => { - const a: SymmetryAction = { type: "CHANGESYMMETRY", symmetry: Symmetry.Rotational }; - dispatch(a); - }} /> - } text="Horizontal Symmetry" onClick={() => { - const a: SymmetryAction = { type: "CHANGESYMMETRY", symmetry: Symmetry.Horizontal }; - dispatch(a); - }} /> - } text="Vertical Symmetry" onClick={() => { - const a: SymmetryAction = { type: "CHANGESYMMETRY", symmetry: Symmetry.Vertical }; - dispatch(a); - }} /> - } text="No Symmetry" onClick={() => { - const a: SymmetryAction = { type: "CHANGESYMMETRY", symmetry: Symmetry.None }; - dispatch(a); - }} /> - - } text="More"> - } text="Publish Puzzle" onClick={() => { - const a: PublishAction = { type: 'PUBLISH', publishTimestamp: getTimestampClass().now() }; - dispatch(a); - }} /> - } text="Enter Rebus" shortcutHint={} onClick={() => { - const a: KeypressAction = { type: "KEYPRESS", key: 'Escape', shift: false }; - dispatch(a); - }} /> - : } text="Toggle Square Highlight" shortcutHint={} onClick={() => { - const a: KeypressAction = { type: "KEYPRESS", key: '`', shift: false }; - dispatch(a); - }} /> - : } text={state.grid.highlight === "circle" ? "Use Shade for Highlights" : "Use Circle for Highlights"} onClick={() => { - const a: SetHighlightAction = { type: "SETHIGHLIGHT", highlight: state.grid.highlight === "circle" ? "shade" : "circle" }; - dispatch(a); - }} /> - { - muted ? - } text="Unmute" onClick={() => setMuted(false)} /> - : - } text="Mute" onClick={() => setMuted(true)} /> - } - { - props.isAdmin ? - - } text="Toggle Keyboard" onClick={() => dispatch({ type: "TOGGLEKEYBOARD" })} /> - } text="Toggle Tablet" onClick={() => dispatch({ type: "TOGGLETABLET" })} /> - } text="Admin" onClick={() => navigate('/admin')} /> - - : - "" - } - } text="Account" onClick={() => navigate('/account')} /> - -
    - }, [props.autofillEnabled, props.autofillInProgress, props.autofilledGrid.length, averageLength, dispatch, muted, numEntries, props.isAdmin, setClueMode, setMuted, state.grid.highlight, state.gridIsComplete, state.hasNoShortWords, state.repeats, state.symmetry, toggleAutofillEnabled]); - - return ( - - - Constructor - - - {topBar} - - {state.isEnteringRebus ? - : ""} - {state.publishErrors.length ? - dispatch({ type: "CLEARPUBLISHERRORS" })}> - -
    Please fix the following errors and try publishing again:
    -
      - {state.publishErrors.map((s, i) =>
    • {s}
    • )} -
    -
    -
    : ""} - { - return - } - } - left={left} - right={right} - tinyColumn={{tiny}} - /> -
    - ) -} diff --git a/crossword/src/Calendar.tsx b/crossword/src/Calendar.tsx deleted file mode 100644 index c780150f..00000000 --- a/crossword/src/Calendar.tsx +++ /dev/null @@ -1,110 +0,0 @@ -/** @jsx jsx */ -import { jsx } from '@emotion/core'; - -import * as React from 'react'; - -const daysToDisplay = 42; -const dayLabels = ['Sun', 'Mon', 'Tue', 'Wed', 'Thur', 'Fri', 'Sat']; -const monthLabels = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; - -interface CalendarProps { - selected?: Date, - onClick: (d: Date) => void, - dateIsDisabled: (d: Date) => boolean -} - -export function Calendar(props: CalendarProps) { - const today = new Date(); - const selected = props.selected || today; - const [monthToShow, setMonthToShow] = React.useState(selected); - const year = monthToShow.getUTCFullYear(); - const month = monthToShow.getUTCMonth(); - const firstMonthDay = new Date(year, month, 0); - const firstMonthDayNumber = firstMonthDay.getDay(); - const firstDayToDisplay = new Date(year, month, -firstMonthDayNumber); - - function changeMonth(incr: number) { - const changed = new Date(monthToShow); - changed.setUTCMonth(changed.getUTCMonth() + incr) - setMonthToShow(changed); - } - - function sameDate(d1: Date, d2: Date) { - return d1.getUTCFullYear() === d2.getUTCFullYear() && - d1.getUTCMonth() === d2.getUTCMonth() && - d1.getUTCDate() === d2.getUTCDate(); - } - - return ( -
    -
    -
    changeMonth(-1)}> - {'<'} -
    -
    {monthLabels[month]} {year}
    -
    changeMonth(1)}> - {'>'} -
    -
    -
    - {dayLabels.map(label =>
    {label}
    )} - {Array(daysToDisplay).fill(0).map((_, i) => { - const d = new Date(firstDayToDisplay); - d.setDate(d.getDate() + i); - const isDisabled = props.dateIsDisabled(d); - const isToday = sameDate(d, today); - const isSelected = sameDate(d, selected); - return
    { - if (!isDisabled) { - props.onClick(d); - } - }} - > - {d.getDate()} -
    - } - )} -
    -
    - ) -} diff --git a/crossword/src/Category.test.tsx b/crossword/src/Category.test.tsx deleted file mode 100644 index ed737645..00000000 --- a/crossword/src/Category.test.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import React from 'react'; -import { render, initFirebaseForTesting } from './testUtils'; -import { Category } from './Category'; -import * as firebase from '@firebase/testing'; - -var adminApp = firebase.initializeAdminApp({ - projectId: "mdcrosshare", -}); - -initFirebaseForTesting(); - -test('basic category page test', async () => { - window.HTMLElement.prototype.scrollIntoView = function() { }; - - await adminApp.firestore().collection('categories').doc('dailymini').set({ '2020-4-10': 'foobar' }); - - const { findByText } = render(); - - const link = await findByText('Daily Mini for 5/10/2020'); - expect(link).toBeInTheDocument(); - expect(link).toHaveAttribute('href', '/crosswords/foobar'); -}); diff --git a/crossword/src/Category.tsx b/crossword/src/Category.tsx deleted file mode 100644 index d089c969..00000000 --- a/crossword/src/Category.tsx +++ /dev/null @@ -1,100 +0,0 @@ -/** @jsx jsx */ -import { jsx } from '@emotion/core'; -import * as React from 'react'; - -import { Link, RouteComponentProps } from "@reach/router"; - -import { ensureUser, AuthProps } from './App'; -import { Page } from './Page'; -import { - CategoryIndexT, CategoryIndexV, UserPlaysT, UserPlaysV, - getDateString, prettifyDateString -} from './common/dbtypes'; -import { getFromSessionOrDB } from './dbUtils'; - -interface CategoryProps extends RouteComponentProps, AuthProps { - categoryId?: string -} - -const CategoryNames: { [key: string]: string } = { - dailymini: "Daily Mini" -} - -export const Category = ensureUser(({ user, categoryId }: CategoryProps) => { - const [plays, setPlays] = React.useState(null); - const [puzzles, setPuzzles] = React.useState(null); - const [error, setError] = React.useState(false); - - React.useEffect(() => { - if (!categoryId || !CategoryNames.hasOwnProperty(categoryId)) { - console.log("Missing category id") - setError(true); - return; - } - Promise.all([ - getFromSessionOrDB('categories', categoryId, CategoryIndexV, 24 * 60 * 60 * 1000), - getFromSessionOrDB('up', user.uid, UserPlaysV, -1) - ]) - .then(([puzzles, plays]) => { - setPuzzles(puzzles); - setPlays(plays); - }).catch(reason => { - console.error(reason); - setError(true); - }); - }, [categoryId, user.uid]); - - if (!puzzles) { - return Loading...; - } - - if (!categoryId || !CategoryNames.hasOwnProperty(categoryId) || !puzzles || error) { - return Error loading category page; - } - - const today = new Date() - today.setHours(12); - const ds = addZeros(getDateString(today)); - - function addZeros(dateString: string) { - const groups = dateString.match(/^(\d+)-(\d+)-(\d+)$/); - if (!groups) { - throw new Error("Bad date string: " + dateString); - } - const year = groups[1]; - let month = groups[2]; - if (month.length === 1) { - month = '0' + month; - } - let date = groups[3]; - if (date.length === 1) { - date = '0' + date; - } - return year + '-' + month + '-' + date; - } - - return ( - -
      - {Object.entries(puzzles) - .map(([k, v]) => [addZeros(k), v]) - .filter(([k, _v]) => k <= ds) - .sort((a, b) => a[0] > b[0] ? -1 : 1) - .map(([dateString, puzzleId]) => { - const play = plays && plays[puzzleId]; - return (
    • - {CategoryNames[categoryId] + ' for ' + prettifyDateString(dateString)} -
      {play ?.[3] ? 'completed ' + (play ?.[2] ? 'with helpers' : 'without helpers') : (play ? 'unfinished' : '')}
      -
    • ); - })} -
    -
    - ); -}); diff --git a/crossword/src/Cell.tsx b/crossword/src/Cell.tsx deleted file mode 100644 index 54ebefad..00000000 --- a/crossword/src/Cell.tsx +++ /dev/null @@ -1,130 +0,0 @@ -/** @jsx jsx */ -import { jsx } from '@emotion/core'; - -import * as React from 'react'; - -import { FaSlash, FaEye } from 'react-icons/fa'; - -import { - notSelectable, PRIMARY, SECONDARY, ERROR_COLOR, -} from './style'; - -type CellProps = { - autofill: string, - showingKeyboard: boolean, - gridWidth: number, - gridSize: number, - isBlock: boolean, - active: boolean, - entryCell: boolean, - highlight: "circle" | "shade" | undefined, - value: string, - number: string, - row: number, - column: number, - onClick: (pos: { row: number, col: number }) => void, - isVerified: boolean | undefined, - isWrong: boolean | undefined, - wasRevealed: boolean | undefined, -} - -export const Cell = React.memo(function Cell(props: CellProps) { - let bg = "var(--white)"; - if (props.isBlock && props.active) { - bg = "repeating-linear-gradient(-45deg,var(--cell-wall),var(--cell-wall) 10px," + PRIMARY + " 10px," + PRIMARY + " 20px);" - } else if (props.isBlock) { - bg = "var(--cell-wall)"; - } else if (props.active) { - bg = PRIMARY; - } else if (props.entryCell) { - bg = SECONDARY; - } - - const cellSize = props.gridSize / props.gridWidth; - const value = props.value.trim() ? props.value : props.autofill; - - return ( -
    -
    props.onClick({ row: props.row, col: props.column })} css={[notSelectable, { - position: 'absolute', - width: '100%', - height: '100%', - borderRight: '1px solid var(--cell-wall)', - borderBottom: '1px solid var(--cell-wall)', - borderTop: (props.row === 0) ? '1px solid var(--cell-wall)' : 0, - borderLeft: (props.column === 0) ? '1px solid var(--cell-wall)' : 0, - background: bg, - - }]}> - {!props.isBlock ? - -
    - {props.wasRevealed ? -
    : ""} - {props.number}
    -
    - {props.isWrong ? -
    : ""} - {props.highlight === 'circle' ? -
    : ""} - {props.highlight === 'shade' ? -
    : ""} -
    {value}
    -
    -
    - : ""} -
    -
    - ); -}); diff --git a/crossword/src/Comments.test.tsx b/crossword/src/Comments.test.tsx deleted file mode 100644 index afc1d040..00000000 --- a/crossword/src/Comments.test.tsx +++ /dev/null @@ -1,265 +0,0 @@ -import React from "react"; -import { Comments } from "./Comments"; -import { anonymousUser, render, initFirebaseForTesting } from "./testUtils"; -import { getTimestampClass } from "./firebase"; -import * as firebase from "@firebase/testing"; - -initFirebaseForTesting(); - -const testComments = [ - { - i: "comment-id", - c: "my first comment", - a: "comment-author-id", - n: "Mike D", - t: 55.4, - ch: false, - p: getTimestampClass().now(), - }, -]; - -test("basic comment display", () => { - const { getByText, container } = render( - - ); - expect(getByText(/my first comment/i)).toBeVisible(); - expect(container.firstChild).toMatchInlineSnapshot(` - .emotion-5 { - margin-top: 1em; - } - - .emotion-0 { - border-bottom: 1px solid var(--black); - } - - .emotion-4 { - list-style-type: none; - margin: 2em 0 0 0; - padding: 0; - } - - .emotion-3 { - margin-top: 1em; - } - - .emotion-1 { - vertical-align: text-bottom; - } - - .emotion-2 { - font-size: 0.75em; - background-color: var(--caption); - color: white; - border-radius: 5px; - padding: 0.1em 0.2em; - } - -
    -

    - Comments -

    -
    - Sign in with google (above) to leave a comment of your own -
    -
      -
    • -
      -
      - - - - - - - - - - - - - - - - - - - - - - - Mike D - - - - 🤓 - - - 55s - -
      -
      - my first comment -
      - - -
      -
    • -
    -
    - `); -}); - -test("security rules should only allow commenting as onesself", async () => { - var app = firebase.initializeTestApp({ - projectId: "mdcrosshare", - auth: { - uid: "mike", - admin: false, - firebase: { - sign_in_provider: "google.com", - }, - }, - }); - - await firebase.assertFails( - app.firestore().collection("cfm").add({ c: "comment text" }) - ); - await firebase.assertFails( - app.firestore().collection("cfm").add({ c: "comment text", a: "jared" }) - ); - await firebase.assertSucceeds( - app.firestore().collection("cfm").add({ c: "comment text", a: "mike" }) - ); -}); - -test("security rules should only allow commenting if non-anonymous", async () => { - var app = firebase.initializeTestApp({ - projectId: "mdcrosshare", - auth: { - uid: "mike", - admin: false, - firebase: { - sign_in_provider: "anonymous", - }, - }, - }); - - await firebase.assertFails( - app.firestore().collection("cfm").add({ c: "comment text" }) - ); - await firebase.assertFails( - app.firestore().collection("cfm").add({ c: "comment text", a: "jared" }) - ); - await firebase.assertFails( - app.firestore().collection("cfm").add({ c: "comment text", a: "mike" }) - ); -}); diff --git a/crossword/src/Comments.tsx b/crossword/src/Comments.tsx deleted file mode 100644 index 5e739a89..00000000 --- a/crossword/src/Comments.tsx +++ /dev/null @@ -1,300 +0,0 @@ -/** @jsx jsx */ -import { jsx } from '@emotion/core'; -import * as React from 'react'; -import * as t from "io-ts"; -import { isRight } from 'fp-ts/lib/Either'; -import { PathReporter } from "io-ts/lib/PathReporter"; - -import { Identicon } from './Icons'; -import { timeString } from './utils'; -import { Emoji } from './Emoji'; -import { buttonAsLink } from './style'; -import { DisplayNameForm, getDisplayName } from './AccountPage'; -import { getFirebaseApp, getTimestampClass } from './firebase'; -import { - CommentWithOrWithoutRepliesT, CommentForModerationT, CommentForModerationWithIdT, CommentWithRepliesT, CommentT, - CommentForModerationWithIdV, -} from './common/dbtypes'; - -const COMMENT_LENGTH_LIMIT = 140; - -interface CommentProps { - puzzleAuthorId: string, - comment: CommentWithOrWithoutRepliesT, - children?: React.ReactNode -} -const Comment = (props: CommentProps) => { - return ( -
    -
    -
    {props.comment.c}
    - {props.children} -
    - ); -} - -const CommentWithReplies = (props: CommentFormProps & { comment: CommentWithOrWithoutRepliesT }) => { - const [showingForm, setShowingForm] = React.useState(false); - return ( - - {(props.user.isAnonymous || props.comment.i === undefined) ? - '' - : - (showingForm ? -
    - setShowingForm(false)} replyToId={props.comment.i} {...props} /> -
    - : -
    - ) - } - {props.comment.r ? -
      - {props.comment.r.map((a, i) =>
    • )} -
    - : - '' - } -
    - ) -} - -function commentsKey(puzzleId: string) { - return 'comments/' + puzzleId; -} - -function commentsFromStorage(puzzleId: string): Array { - const inSession = localStorage.getItem(commentsKey(puzzleId)); - if (inSession) { - const res = t.array(CommentForModerationWithIdV).decode(JSON.parse(inSession)); - if (isRight(res)) { - return res.right; - } else { - console.error("Couldn't parse object in local storage"); - console.error(PathReporter.report(res).join(",")); - } - } - return []; -} - -interface CommentFlairProps { - solveTime: number, - didCheat: boolean, - displayName: string, - userId: string, - puzzleAuthorId: string, -} -const CommentFlair = (props: CommentFlairProps) => { - return ( - - - {props.displayName} - {(props.userId === props.puzzleAuthorId) ? - constructor - : - - {props.didCheat ? : } - < span css={{ - fontSize: '0.75em', - backgroundColor: 'var(--caption)', - color: 'white', - borderRadius: 5, - padding: '0.1em 0.2em', - }}>{timeString(props.solveTime, false)} - - } - - ); -} - -interface CommentFormProps { - displayName: string, - setDisplayName: (name: string) => void, - puzzleAuthorId: string, - user: firebase.User, - solveTime: number, - didCheat: boolean, - puzzleId: string, - replyToId?: string, -} - -const CommentForm = ({ onCancel, ...props }: CommentFormProps & { onCancel?: () => void }) => { - const [commentText, setCommentText] = React.useState(''); - const [editingDisplayName, setEditingDisplayName] = React.useState(false); - const [submittedComment, setSubmittedComment] = React.useState(null); - - function sanitize(input: string) { - return input.substring(0, COMMENT_LENGTH_LIMIT); - } - - function submitComment(event: React.FormEvent) { - event.preventDefault(); - const textToSubmit = commentText.trim(); - if (!textToSubmit) { - return; - } - const comment: CommentForModerationT = { - c: textToSubmit, - a: props.user.uid, - n: props.displayName, - t: props.solveTime, - ch: props.didCheat, - p: getTimestampClass().now(), - pid: props.puzzleId, - rt: props.replyToId !== undefined ? props.replyToId : null - } - console.log("Submitting comment", comment); - const db = getFirebaseApp().firestore(); - // Add to moderation queue for long term - db.collection('cfm').add(comment).then((ref) => { - console.log("Uploaded", ref.id); - - // Replace this form w/ the comment for the short term - setSubmittedComment(comment); - // Add the comment to localStorage for the medium term - const forSession = commentsFromStorage(props.puzzleId); - forSession.push({ i: ref.id, ...comment }); - localStorage.setItem(commentsKey(props.puzzleId), JSON.stringify(forSession)); - }); - } - - if (submittedComment) { - return - } - - return ( - -
    -