Initial commit.

This commit is contained in:
Brent Schroeter 2021-01-04 22:20:46 -05:00
commit 34ba8d408b
26 changed files with 51739 additions and 0 deletions

1
.babelrc.js Normal file
View file

@ -0,0 +1 @@
module.exports = {};

170
.eslintrc.js Normal file
View file

@ -0,0 +1,170 @@
module.exports = {
env: {
browser: true,
es2021: true,
},
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaFeatures: { jsx: true },
ecmaVersion: 12,
project: ['./tsconfig.json'],
sourceType: 'module',
},
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking',
'plugin:jsx-a11y/recommended',
'plugin:react/recommended',
'airbnb',
],
plugins: [
'@typescript-eslint',
'import',
'jsx-a11y',
'react',
'react-hooks',
],
rules: {
'@typescript-eslint/brace-style': ['error'],
'@typescript-eslint/comma-dangle': ['error', 'always-multiline'],
'@typescript-eslint/consistent-type-definitions': ['error', 'interface'],
'@typescript-eslint/keyword-spacing': ['error'],
'@typescript-eslint/member-delimiter-style': ['error'],
'@typescript-eslint/method-signature-style': ['warn', 'method'],
'@typescript-eslint/naming-convention': [
'warn',
{
selector: 'default',
format: ['camelCase', 'PascalCase'],
leadingUnderscore: 'forbid',
trailingUnderscore: 'forbid',
filter: {
// skip names requiring quotes, e.g. HTTP headers
regex: '[- ]',
match: false,
},
},
{
selector: ['enumMember'],
format: ['UPPER_CASE'],
},
{
selector: ['property', 'parameter', 'parameterProperty'],
format: ['camelCase', 'PascalCase'],
leadingUnderscore: 'allow',
trailingUnderscore: 'forbid',
filter: {
// skip names requiring quotes, e.g. HTTP headers
regex: '[- ]',
match: false,
},
},
{
selector: 'typeLike',
format: ['PascalCase'],
},
],
'@typescript-eslint/no-duplicate-imports': ['error'],
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-floating-promises': ['error'],
'@typescript-eslint/no-misused-promises': ['error'],
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-shadow': ['error'],
'@typescript-eslint/no-unnecessary-condition': ['error', { allowConstantLoopConditions: true }],
'@typescript-eslint/no-unnecessary-boolean-literal-compare': ['error'],
'@typescript-eslint/no-unused-expressions': ['error'],
'@typescript-eslint/no-unused-vars': ['error'],
'@typescript-eslint/no-use-before-define': ['error'],
'@typescript-eslint/prefer-nullish-coalescing': ['warn'],
'@typescript-eslint/quotes': ['error', 'single', { avoidEscape: true }],
'@typescript-eslint/semi': ['error'],
'@typescript-eslint/space-before-function-paren': ['error', {
anonymous: 'never',
named: 'never',
asyncArrow: 'always',
}],
'@typescript-eslint/switch-exhaustiveness-check': ['error'],
'@typescript-eslint/type-annotation-spacing': ['warn', {
before: false,
after: true,
overrides: {
arrow: { before: true },
},
}],
'brace-style': 'off',
camelcase: 'off',
'class-methods-use-this': 'off',
'comma-dangle': 'off',
'keyword-spacing': 'off',
'no-duplicate-imports': 'off',
'no-shadow': 'off',
'no-unused-expressions': 'off',
'no-unused-vars': 'off',
'no-use-before-define': 'off',
quotes: 'off',
semi: 'off',
'space-before-function-paren': 'off',
'implicit-arrow-linebreak': 'off',
'import/extensions': [
'error',
'ignorePackages',
{
js: 'never',
jsx: 'never',
ts: 'never',
tsx: 'never',
},
],
indent: ['error', 2, {
CallExpression: { arguments: 'first' },
FunctionDeclaration: { parameters: 'first' },
FunctionExpression: { parameters: 'first' },
SwitchCase: 1,
VariableDeclarator: 'first',
offsetTernaryExpressions: true,
}],
'jsx-a11y/label-has-associated-control': ['warn', {
assert: 'either',
}],
'max-classes-per-file': 'off',
'max-len': ['warn', { code: 100, tabWidth: 2 }],
'no-console': 'off',
'no-underscore-dangle': 'off',
'prefer-arrow-callback': ['error'],
'react/function-component-definition': ['error', {
namedComponents: 'arrow-function',
unnamedComponents: 'arrow-function',
}],
'react/jsx-curly-newline': ['error', { multiline: 'forbid', singleline: 'forbid' }],
'react/jsx-filename-extension': ['error', { extensions: ['.jsx', '.tsx'] }],
'react/jsx-first-prop-new-line': ['error', 'multiline'],
'react/jsx-fragments': 'off',
'react/jsx-indent': ['error', 2, { checkAttributes: true, indentLogicalExpressions: true }],
'react/jsx-indent-props': ['error', 2],
'react/jsx-key': ['error', { checkFragmentShorthand: true }],
'react/jsx-max-props-per-line': ['error', { maximum: 3, when: 'always' }],
'react/jsx-no-target-blank': ['error'],
'react/jsx-wrap-multilines': ['error', {
declaration: 'parens-new-line',
assignment: 'parens-new-line',
return: 'parens-new-line',
arrow: 'parens-new-line',
condition: 'parens-new-line',
logical: 'parens-new-line',
prop: 'parens-new-line',
}],
'react/prop-types': 'off',
'react/no-deprecated': ['error'],
'react/no-multi-comp': ['error'],
'react/no-unused-prop-types': ['error'],
'react/prefer-stateless-function': ['error'],
'react/react-in-jsx-scope': 'off',
'react-hooks/rules-of-hooks': ['error'],
'react-hooks/exhaustive-deps': ['warn'],
},
settings: {
'import/resolver': {
node: { extensions: ['.js', '.jsx', '.ts', '.tsx'] },
},
},
};

5
.firebaserc Normal file
View file

@ -0,0 +1,5 @@
{
"projects": {
"default": "built-to-spell"
}
}

66
.gitignore vendored Normal file
View file

@ -0,0 +1,66 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
firebase-debug.log*
firebase-debug.*.log*
# Firebase cache
.firebase/
# Firebase config
# Uncomment this if you'd like others to create their own Firebase project.
# For a team working on the same Firebase project(s), it is recommended to leave
# it commented so all members can deploy to the same project(s) in .firebaserc.
# .firebaserc
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Brent Schroeter
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

45
dist/custom.d5cc83c2.css vendored Normal file
View file

@ -0,0 +1,45 @@
div.hexagon {
overflow: hidden;
padding: 0 0 60px 0;
position: relative;
-o-transform: skewX(30deg);
-moz-transform: skewX(30deg);
-webkit-transform: skewX(30deg);
-ms-transform: skewX(30deg);
transform: skewX(30deg);
visibility: hidden;
width: 70px;
}
div.hexagon>div {
height: 100%;
left: 0;
overflow: hidden;
position: absolute;
top: 0;
-o-transform: skewY(-50deg);
-moz-transform: skewY(-50deg);
-webkit-transform: skewY(-50deg);
-ms-transform: skewY(-50deg);
transform: skewX(-50deg);
visibility: hidden;
width: 100%;
}
div.hexagon>div>button {
height: 100%;
left: 0;
overflow: hidden;
position: absolute;
-o-transform: skewY(20deg);
-moz-transform: skewY(20deg);
-webkit-transform: skewY(20deg);
-ms-transform: skewY(20deg);
top: 0;
transform: skewX(30deg);
visibility: visible;
width: 100%;
}
/*# sourceMappingURL=/custom.d5cc83c2.css.map */

1
dist/custom.d5cc83c2.css.map vendored Normal file
View file

@ -0,0 +1 @@
{"version":3,"sources":["custom.css"],"names":[],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"custom.d5cc83c2.css","sourceRoot":"../src","sourcesContent":["div.hexagon {\n overflow: hidden;\n padding: 0 0 60px 0;\n position: relative;\n -o-transform: skewX(30deg);\n -moz-transform: skewX(30deg);\n -webkit-transform: skewX(30deg);\n -ms-transform: skewX(30deg);\n transform: skewX(30deg);\n visibility: hidden;\n width: 70px;\n}\n\ndiv.hexagon>div {\n height: 100%;\n left: 0;\n overflow: hidden;\n position: absolute;\n top: 0;\n -o-transform: skewY(-50deg);\n -moz-transform: skewY(-50deg);\n -webkit-transform: skewY(-50deg);\n -ms-transform: skewY(-50deg);\n transform: skewX(-50deg);\n visibility: hidden;\n width: 100%;\n}\n\ndiv.hexagon>div>button {\n height: 100%;\n left: 0;\n overflow: hidden;\n position: absolute;\n -o-transform: skewY(20deg);\n -moz-transform: skewY(20deg);\n -webkit-transform: skewY(20deg);\n -ms-transform: skewY(20deg);\n top: 0;\n transform: skewX(30deg);\n visibility: visible;\n width: 100%;\n}\n"]}

397
dist/custom.d5cc83c2.js vendored Normal file
View file

@ -0,0 +1,397 @@
// modules are defined as an array
// [ module function, map of requires ]
//
// map of requires is short require name -> numeric require
//
// anything defined in a previous bundle is accessed via the
// orig method which is the require for previous bundles
parcelRequire = (function (modules, cache, entry, globalName) {
// Save the require from previous bundle to this closure if any
var previousRequire = typeof parcelRequire === 'function' && parcelRequire;
var nodeRequire = typeof require === 'function' && require;
function newRequire(name, jumped) {
if (!cache[name]) {
if (!modules[name]) {
// if we cannot find the module within our internal map or
// cache jump to the current global require ie. the last bundle
// that was added to the page.
var currentRequire = typeof parcelRequire === 'function' && parcelRequire;
if (!jumped && currentRequire) {
return currentRequire(name, true);
}
// If there are other bundles on this page the require from the
// previous one is saved to 'previousRequire'. Repeat this as
// many times as there are bundles until the module is found or
// we exhaust the require chain.
if (previousRequire) {
return previousRequire(name, true);
}
// Try the node require function if it exists.
if (nodeRequire && typeof name === 'string') {
return nodeRequire(name);
}
var err = new Error('Cannot find module \'' + name + '\'');
err.code = 'MODULE_NOT_FOUND';
throw err;
}
localRequire.resolve = resolve;
localRequire.cache = {};
var module = cache[name] = new newRequire.Module(name);
modules[name][0].call(module.exports, localRequire, module, module.exports, this);
}
return cache[name].exports;
function localRequire(x){
return newRequire(localRequire.resolve(x));
}
function resolve(x){
return modules[name][1][x] || x;
}
}
function Module(moduleName) {
this.id = moduleName;
this.bundle = newRequire;
this.exports = {};
}
newRequire.isParcelRequire = true;
newRequire.Module = Module;
newRequire.modules = modules;
newRequire.cache = cache;
newRequire.parent = previousRequire;
newRequire.register = function (id, exports) {
modules[id] = [function (require, module) {
module.exports = exports;
}, {}];
};
var error;
for (var i = 0; i < entry.length; i++) {
try {
newRequire(entry[i]);
} catch (e) {
// Save first error but execute all entries
if (!error) {
error = e;
}
}
}
if (entry.length) {
// Expose entry point to Node, AMD or browser globals
// Based on https://github.com/ForbesLindesay/umd/blob/master/template.js
var mainExports = newRequire(entry[entry.length - 1]);
// CommonJS
if (typeof exports === "object" && typeof module !== "undefined") {
module.exports = mainExports;
// RequireJS
} else if (typeof define === "function" && define.amd) {
define(function () {
return mainExports;
});
// <script>
} else if (globalName) {
this[globalName] = mainExports;
}
}
// Override the current require with this new one
parcelRequire = newRequire;
if (error) {
// throw error from earlier, _after updating parcelRequire_
throw error;
}
return newRequire;
})({"../node_modules/parcel-bundler/src/builtins/bundle-url.js":[function(require,module,exports) {
var bundleURL = null;
function getBundleURLCached() {
if (!bundleURL) {
bundleURL = getBundleURL();
}
return bundleURL;
}
function getBundleURL() {
// Attempt to find the URL of the current script and use that as the base URL
try {
throw new Error();
} catch (err) {
var matches = ('' + err.stack).match(/(https?|file|ftp|chrome-extension|moz-extension):\/\/[^)\n]+/g);
if (matches) {
return getBaseURL(matches[0]);
}
}
return '/';
}
function getBaseURL(url) {
return ('' + url).replace(/^((?:https?|file|ftp|chrome-extension|moz-extension):\/\/.+)\/[^/]+$/, '$1') + '/';
}
exports.getBundleURL = getBundleURLCached;
exports.getBaseURL = getBaseURL;
},{}],"../node_modules/parcel-bundler/src/builtins/css-loader.js":[function(require,module,exports) {
var bundle = require('./bundle-url');
function updateLink(link) {
var newLink = link.cloneNode();
newLink.onload = function () {
link.remove();
};
newLink.href = link.href.split('?')[0] + '?' + Date.now();
link.parentNode.insertBefore(newLink, link.nextSibling);
}
var cssTimeout = null;
function reloadCSS() {
if (cssTimeout) {
return;
}
cssTimeout = setTimeout(function () {
var links = document.querySelectorAll('link[rel="stylesheet"]');
for (var i = 0; i < links.length; i++) {
if (bundle.getBaseURL(links[i].href) === bundle.getBundleURL()) {
updateLink(links[i]);
}
}
cssTimeout = null;
}, 50);
}
module.exports = reloadCSS;
},{"./bundle-url":"../node_modules/parcel-bundler/src/builtins/bundle-url.js"}],"custom.css":[function(require,module,exports) {
var reloadCSS = require('_css_loader');
module.hot.dispose(reloadCSS);
module.hot.accept(reloadCSS);
},{"_css_loader":"../node_modules/parcel-bundler/src/builtins/css-loader.js"}],"../node_modules/parcel-bundler/src/builtins/hmr-runtime.js":[function(require,module,exports) {
var global = arguments[3];
var OVERLAY_ID = '__parcel__error__overlay__';
var OldModule = module.bundle.Module;
function Module(moduleName) {
OldModule.call(this, moduleName);
this.hot = {
data: module.bundle.hotData,
_acceptCallbacks: [],
_disposeCallbacks: [],
accept: function (fn) {
this._acceptCallbacks.push(fn || function () {});
},
dispose: function (fn) {
this._disposeCallbacks.push(fn);
}
};
module.bundle.hotData = null;
}
module.bundle.Module = Module;
var checkedAssets, assetsToAccept;
var parent = module.bundle.parent;
if ((!parent || !parent.isParcelRequire) && typeof WebSocket !== 'undefined') {
var hostname = "" || location.hostname;
var protocol = location.protocol === 'https:' ? 'wss' : 'ws';
var ws = new WebSocket(protocol + '://' + hostname + ':' + "51210" + '/');
ws.onmessage = function (event) {
checkedAssets = {};
assetsToAccept = [];
var data = JSON.parse(event.data);
if (data.type === 'update') {
var handled = false;
data.assets.forEach(function (asset) {
if (!asset.isNew) {
var didAccept = hmrAcceptCheck(global.parcelRequire, asset.id);
if (didAccept) {
handled = true;
}
}
}); // Enable HMR for CSS by default.
handled = handled || data.assets.every(function (asset) {
return asset.type === 'css' && asset.generated.js;
});
if (handled) {
console.clear();
data.assets.forEach(function (asset) {
hmrApply(global.parcelRequire, asset);
});
assetsToAccept.forEach(function (v) {
hmrAcceptRun(v[0], v[1]);
});
} else if (location.reload) {
// `location` global exists in a web worker context but lacks `.reload()` function.
location.reload();
}
}
if (data.type === 'reload') {
ws.close();
ws.onclose = function () {
location.reload();
};
}
if (data.type === 'error-resolved') {
console.log('[parcel] ✨ Error resolved');
removeErrorOverlay();
}
if (data.type === 'error') {
console.error('[parcel] 🚨 ' + data.error.message + '\n' + data.error.stack);
removeErrorOverlay();
var overlay = createErrorOverlay(data);
document.body.appendChild(overlay);
}
};
}
function removeErrorOverlay() {
var overlay = document.getElementById(OVERLAY_ID);
if (overlay) {
overlay.remove();
}
}
function createErrorOverlay(data) {
var overlay = document.createElement('div');
overlay.id = OVERLAY_ID; // html encode message and stack trace
var message = document.createElement('div');
var stackTrace = document.createElement('pre');
message.innerText = data.error.message;
stackTrace.innerText = data.error.stack;
overlay.innerHTML = '<div style="background: black; font-size: 16px; color: white; position: fixed; height: 100%; width: 100%; top: 0px; left: 0px; padding: 30px; opacity: 0.85; font-family: Menlo, Consolas, monospace; z-index: 9999;">' + '<span style="background: red; padding: 2px 4px; border-radius: 2px;">ERROR</span>' + '<span style="top: 2px; margin-left: 5px; position: relative;">🚨</span>' + '<div style="font-size: 18px; font-weight: bold; margin-top: 20px;">' + message.innerHTML + '</div>' + '<pre>' + stackTrace.innerHTML + '</pre>' + '</div>';
return overlay;
}
function getParents(bundle, id) {
var modules = bundle.modules;
if (!modules) {
return [];
}
var parents = [];
var k, d, dep;
for (k in modules) {
for (d in modules[k][1]) {
dep = modules[k][1][d];
if (dep === id || Array.isArray(dep) && dep[dep.length - 1] === id) {
parents.push(k);
}
}
}
if (bundle.parent) {
parents = parents.concat(getParents(bundle.parent, id));
}
return parents;
}
function hmrApply(bundle, asset) {
var modules = bundle.modules;
if (!modules) {
return;
}
if (modules[asset.id] || !bundle.parent) {
var fn = new Function('require', 'module', 'exports', asset.generated.js);
asset.isNew = !modules[asset.id];
modules[asset.id] = [fn, asset.deps];
} else if (bundle.parent) {
hmrApply(bundle.parent, asset);
}
}
function hmrAcceptCheck(bundle, id) {
var modules = bundle.modules;
if (!modules) {
return;
}
if (!modules[id] && bundle.parent) {
return hmrAcceptCheck(bundle.parent, id);
}
if (checkedAssets[id]) {
return;
}
checkedAssets[id] = true;
var cached = bundle.cache[id];
assetsToAccept.push([bundle, id]);
if (cached && cached.hot && cached.hot._acceptCallbacks.length) {
return true;
}
return getParents(global.parcelRequire, id).some(function (id) {
return hmrAcceptCheck(global.parcelRequire, id);
});
}
function hmrAcceptRun(bundle, id) {
var cached = bundle.cache[id];
bundle.hotData = {};
if (cached) {
cached.hot.data = bundle.hotData;
}
if (cached && cached.hot && cached.hot._disposeCallbacks.length) {
cached.hot._disposeCallbacks.forEach(function (cb) {
cb(bundle.hotData);
});
}
delete bundle.cache[id];
bundle(id);
cached = bundle.cache[id];
if (cached && cached.hot && cached.hot._acceptCallbacks.length) {
cached.hot._acceptCallbacks.forEach(function (cb) {
cb();
});
return true;
}
}
},{}]},{},["../node_modules/parcel-bundler/src/builtins/hmr-runtime.js"], null)
//# sourceMappingURL=/custom.d5cc83c2.js.map

1
dist/custom.d5cc83c2.js.map vendored Normal file
View file

@ -0,0 +1 @@
{"version":3,"sources":["../node_modules/parcel-bundler/src/builtins/bundle-url.js","../node_modules/parcel-bundler/src/builtins/css-loader.js"],"names":["bundleURL","getBundleURLCached","getBundleURL","Error","err","matches","stack","match","getBaseURL","url","replace","exports","bundle","require","updateLink","link","newLink","cloneNode","onload","remove","href","split","Date","now","parentNode","insertBefore","nextSibling","cssTimeout","reloadCSS","setTimeout","links","document","querySelectorAll","i","length","module"],"mappings":"AAAA,ACAA,IDAIA,ACAAY,MAAM,GDAG,ACAAC,GDAG,IAAhB,ACAoB,CAAC,cAAD,CAApB;;ADCA,ACCA,SDDSZ,ACCAa,UAAT,CAAoBC,IAApB,EAA0B,CDD1B,GAA8B;AAC5B,ACCA,MDDI,ACCAC,CDDChB,MCCM,GDDX,ACCce,EDDE,ECCE,CAACE,SAAL,EAAd;ADAEjB,IAAAA,SAAS,GAAGE,YAAY,EAAxB;AACD,ACADc,EAAAA,OAAO,CAACE,MAAR,GAAiB,YAAY;AAC3BH,IAAAA,IAAI,CAACI,MAAL;ADCF,ACAC,GAFD,MDEOnB,SAAP;AACD;ACACgB,EAAAA,OAAO,CAACI,IAAR,GAAeL,IAAI,CAACK,IAAL,CAAUC,KAAV,CAAgB,GAAhB,EAAqB,CAArB,IAA0B,GAA1B,GAAgCC,IAAI,CAACC,GAAL,EAA/C;ADEF,ACDER,EAAAA,IAAI,CAACS,EDCEtB,QCDP,CAAgBuB,GDClB,GAAwB,MCDtB,CAA6BT,OAA7B,EAAsCD,IAAI,CAACW,WAA3C;ADEA,ACDD;ADEC,MAAI;AACF,ACDJ,IAAIC,MDCM,IAAIxB,ACDA,GAAG,EDCP,EAAN,ACDJ;ADEG,GAFD,CAEE,OAAOC,GAAP,EAAY;AACZ,ACFJ,QDEQC,CCFCuB,MDEM,GAAG,ACFlB,CDEmB,ECFE,GDEGxB,GAAG,CAACE,KAAV,EAAiBC,KAAjB,CAAuB,+DAAvB,CAAd;ACDF,MAAIoB,UAAJ,EAAgB;ADEd,ACDA,QDCItB,OAAJ,EAAa;AACX,ACDH,aDCUG,UAAU,CAACH,OAAO,CAAC,CAAD,CAAR,CAAjB;AACD;AACF,ACDDsB,EAAAA,UAAU,GAAGE,UAAU,CAAC,YAAY;AAClC,QAAIC,KAAK,GAAGC,QAAQ,CAACC,gBAAT,CAA0B,wBAA1B,CAAZ;ADEF,SAAO,GAAP;AACD,ACFG,SAAK,IAAIC,CAAC,GAAG,CAAb,EAAgBA,CAAC,GAAGH,KAAK,CAACI,MAA1B,EAAkCD,CAAC,EAAnC,EAAuC;AACrC,UAAIrB,MAAM,CAACJ,UAAP,CAAkBsB,KAAK,CAACG,CAAD,CAAL,CAASb,IAA3B,MAAqCR,MAAM,CAACV,YAAP,EAAzC,EAAgE;ADGtE,ACFQY,QAAAA,CDECN,SCFS,CDElB,ACFmBsB,CDECrB,GAApB,CCFwB,CDEC,ACFAwB,CAAD,CAAN,CAAV;ADGN,ACFK,SDEE,CAAC,KAAKxB,GAAN,EAAWC,OAAX,CAAmB,sEAAnB,EAA2F,IAA3F,IAAmG,GAA1G;AACD,ACFI;;ADILC,ACFIgB,IAAAA,GDEG,CAACzB,MCFM,GAAG,GDEjB,CCFI,EDEmBD,kBAAvB;AACAU,ACFG,GATsB,EASpB,EDEE,ACXkB,CDWjBH,ACXN,UDWF,GAAqBA,UAArB;ACDC;;AAED2B,MAAM,CAACxB,OAAP,GAAiBiB,SAAjB","file":"custom.d5cc83c2.js","sourceRoot":"../src","sourcesContent":["var bundleURL = null;\nfunction getBundleURLCached() {\n if (!bundleURL) {\n bundleURL = getBundleURL();\n }\n\n return bundleURL;\n}\n\nfunction getBundleURL() {\n // Attempt to find the URL of the current script and use that as the base URL\n try {\n throw new Error;\n } catch (err) {\n var matches = ('' + err.stack).match(/(https?|file|ftp|chrome-extension|moz-extension):\\/\\/[^)\\n]+/g);\n if (matches) {\n return getBaseURL(matches[0]);\n }\n }\n\n return '/';\n}\n\nfunction getBaseURL(url) {\n return ('' + url).replace(/^((?:https?|file|ftp|chrome-extension|moz-extension):\\/\\/.+)\\/[^/]+$/, '$1') + '/';\n}\n\nexports.getBundleURL = getBundleURLCached;\nexports.getBaseURL = getBaseURL;\n","var bundle = require('./bundle-url');\n\nfunction updateLink(link) {\n var newLink = link.cloneNode();\n newLink.onload = function () {\n link.remove();\n };\n newLink.href = link.href.split('?')[0] + '?' + Date.now();\n link.parentNode.insertBefore(newLink, link.nextSibling);\n}\n\nvar cssTimeout = null;\nfunction reloadCSS() {\n if (cssTimeout) {\n return;\n }\n\n cssTimeout = setTimeout(function () {\n var links = document.querySelectorAll('link[rel=\"stylesheet\"]');\n for (var i = 0; i < links.length; i++) {\n if (bundle.getBaseURL(links[i].href) === bundle.getBundleURL()) {\n updateLink(links[i]);\n }\n }\n\n cssTimeout = null;\n }, 50);\n}\n\nmodule.exports = reloadCSS;\n"]}

14
dist/index.html vendored Normal file
View file

@ -0,0 +1,14 @@
<!doctype HTML>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet">
<link href="/custom.d5cc83c2.css" rel="stylesheet">
<title>Built to Spell</title>
<script src="/custom.d5cc83c2.js"></script></head>
<body class="bg-gray-50">
<div id="app-root" class="w-screen h-screen"></div>
<script type="text/javascript" src="/src.f69400ca.js"></script>
</body>
</html>

30667
dist/src.f69400ca.js vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/src.f69400ca.js.map vendored Normal file

File diff suppressed because one or more lines are too long

16
firebase.json Normal file
View file

@ -0,0 +1,16 @@
{
"hosting": {
"public": "dist",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
],
"rewrites": [
{
"source": "**",
"destination": "/index.html"
}
]
}
}

19728
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

38
package.json Normal file
View file

@ -0,0 +1,38 @@
{
"name": "built-to-spell",
"version": "0.0.1",
"description": "NYT Spelling Bee simulator",
"main": "index.js",
"scripts": {
"lint": "eslint src",
"deploy": "rm -r .cache ; parcel build -d dist src/index.html && firebase deploy --only=hosting",
"start": "rm -r .cache ; parcel -d dist src/index.html"
},
"repository": {
"type": "git",
"url": "git+ssh://git@gitlab.com/brentschroeter/built-to-spell.git"
},
"author": "Brent Schroeter <brent@brentschroeter.com>",
"license": "MIT",
"bugs": {
"url": "https://gitlab.com/brentschroeter/built-to-spell/issues"
},
"homepage": "https://gitlab.com/brentschroeter/built-to-spell#readme",
"dependencies": {
"react": "^17.0.1",
"react-dom": "^17.0.1"
},
"devDependencies": {
"@babel/core": "^7.12.10",
"@types/node": "^14.14.20",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"@typescript-eslint/eslint-plugin": "^4.11.1",
"eslint": "^7.17.0",
"eslint-config-airbnb": "^18.2.1",
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-react": "^7.22.0",
"parcel-bundler": "^1.12.4",
"typescript": "^4.1.3"
}
}

27
src/app.tsx Normal file
View file

@ -0,0 +1,27 @@
import React, {
FunctionComponent,
useContext,
} from 'react';
import GameView from './game-view.tsx';
import SetupView from './setup-view';
import { StoreContext } from './store';
const App: FunctionComponent = () => {
const { state } = useContext(StoreContext);
const { view } = state;
return (
<div className="mx-auto max-w-sm min-h-screen p-2 bg-white shadow-lg">
{view === 'GAME' && (
<GameView />
)}
{view === 'SETUP' && (
<SetupView />
)}
</div>
);
};
export default App;

42
src/custom.css Normal file
View file

@ -0,0 +1,42 @@
div.hexagon {
overflow: hidden;
padding: 0 0 60px 0;
position: relative;
-o-transform: skewX(30deg);
-moz-transform: skewX(30deg);
-webkit-transform: skewX(30deg);
-ms-transform: skewX(30deg);
transform: skewX(30deg);
visibility: hidden;
width: 70px;
}
div.hexagon>div {
height: 100%;
left: 0;
overflow: hidden;
position: absolute;
top: 0;
-o-transform: skewY(-50deg);
-moz-transform: skewY(-50deg);
-webkit-transform: skewY(-50deg);
-ms-transform: skewY(-50deg);
transform: skewX(-50deg);
visibility: hidden;
width: 100%;
}
div.hexagon>div>button {
height: 100%;
left: 0;
overflow: hidden;
position: absolute;
-o-transform: skewY(20deg);
-moz-transform: skewY(20deg);
-webkit-transform: skewY(20deg);
-ms-transform: skewY(20deg);
top: 0;
transform: skewX(30deg);
visibility: visible;
width: 100%;
}

97
src/game-view.tsx Normal file
View file

@ -0,0 +1,97 @@
import React, {
FunctionComponent,
useContext,
} from 'react';
import Honeycomb from './honeycomb';
import { StoreContext } from './store';
const GameView: FunctionComponent = () => {
const { dispatch, state } = useContext(StoreContext);
const {
currentWord,
letters,
score,
words,
} = state;
return (
<>
<div className="mb-6 flex justify-end items-center">
<button
className="px-6 py-4 text-xl"
onClick={() => dispatch({ type: 'SWITCH_VIEW', payload: 'SETUP' })}
type="button"
>
</button>
</div>
<div className="my-6 flex justify-center">
<div
className="w-8 h-8 rounded-full bg-yellow-400 text-xs flex items-center justify-center"
>
<div>
{score}
</div>
</div>
</div>
<div
className="m-6 px-2 py-1 border border-gray-200 rounded font-light text-sm flex flex-wrap"
>
{words.length === 0 && (
<div className="m-1 text-gray-400">
Your words...
</div>
)}
{words.map((word) => (
<div
className="m-1"
key={word}
>
{word}
</div>
))}
</div>
<div className="mb-6 mt-16 h-7 font-bold text-center text-lg">
{currentWord.split('').map((ch, i) => (
<span
className={ch === letters[0] ? 'text-yellow-400' : undefined}
key={i /* eslint-disable-line react/no-array-index-key */}
>
{ch}
</span>
))}
<span className="text-yellow-400 animate-pulse text-2xl font-light relative">
|
</span>
</div>
<Honeycomb />
<div className="my-6 flex justify-center items-center space-x-3">
<button
className="h-10 w-20 rounded-full border border-gray-200 text-sm"
onClick={() => dispatch({ type: 'DELETE_LETTER' })}
type="button"
>
Delete
</button>
<button
className="h-10 w-10 rounded-full border border-gray-200 text-xl pb-1"
onClick={() => dispatch({ type: 'SHUFFLE_LETTERS' })}
type="button"
>
</button>
<button
className="h-10 w-20 rounded-full border border-gray-200 text-sm"
onClick={() => dispatch({ type: 'ENTER_WORD' })}
type="button"
>
Enter
</button>
</div>
</>
);
};
export default GameView;

22
src/hexagon.tsx Normal file
View file

@ -0,0 +1,22 @@
import React, { SFC } from 'react';
interface Props {
accent?: boolean;
onClick(): void;
}
const Hexagon: SFC<Props> = ({ accent, children, onClick }) => (
<div className="hexagon">
<div>
<button
className={`${accent ? 'bg-yellow-400' : 'bg-gray-200'} font-bold text-xl`}
onClick={onClick}
type="button"
>
{children}
</button>
</div>
</div>
);
export default Hexagon;

69
src/honeycomb.tsx Normal file
View file

@ -0,0 +1,69 @@
import React, {
CSSProperties,
FunctionComponent,
useContext,
} from 'react';
import Hexagon from './hexagon';
import { StoreContext } from './store';
const hexagonSizePx = 60;
const positions: CSSProperties[] = [{
position: 'absolute',
marginLeft: hexagonSizePx * 1.1,
marginTop: hexagonSizePx * 1.0,
visibility: 'hidden',
}];
for (let i = 0; i < 6; i += 1) {
positions.push({
position: 'absolute',
marginLeft: hexagonSizePx * (1.1 + 1.1 * Math.cos((Math.PI * (i + 0.5)) / 3)),
marginTop: hexagonSizePx * (1.0 + 1.1 * Math.sin((Math.PI * (i + 0.5)) / 3)),
visibility: 'hidden',
});
}
const containerStyle: CSSProperties = {
margin: '0 auto',
height: hexagonSizePx * 3.2,
position: 'relative',
width: hexagonSizePx * 3.2,
};
const Honeycomb: FunctionComponent = () => {
const { dispatch, state } = useContext(StoreContext);
const { letters } = state;
if (letters.length !== 7) {
return (
<div>
Waiting for letters...
</div>
);
}
return (
<div
className="mx-auto"
style={containerStyle}
>
{letters.map((ch, i) => (
<div
key={ch}
style={positions[i]}
>
<Hexagon
accent={i === 0}
onClick={() => dispatch({ type: 'ENTER_LETTER', payload: ch })}
>
{ch}
</Hexagon>
</div>
))}
</div>
);
};
export default Honeycomb;

14
src/index.html Normal file
View file

@ -0,0 +1,14 @@
<!doctype HTML>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet">
<link href="/custom.css" rel="stylesheet">
<title>Built to Spell</title>
</head>
<body class="bg-gray-50">
<div id="app-root" class="w-screen h-screen"></div>
<script type="text/javascript" src="./index.tsx"></script>
</body>
</html>

14
src/index.tsx Normal file
View file

@ -0,0 +1,14 @@
import React from 'react';
import { render } from 'react-dom';
import App from './app';
import { StoreProvider } from './store';
render(
(
<StoreProvider>
<App />
</StoreProvider>
),
document.getElementById('app-root'),
);

103
src/setup-view.tsx Normal file
View file

@ -0,0 +1,103 @@
import React, {
ChangeEventHandler,
FunctionComponent,
useContext,
useEffect,
} from 'react';
import { StoreContext } from './store';
const range7 = [0, 1, 2, 3, 4, 5, 6];
const isValidLetter = (ch: string): boolean => ch.length === 1 && ch >= 'A' && ch <= 'Z';
const SetupView: FunctionComponent = () => {
const { dispatch, state } = useContext(StoreContext);
const { letters } = state;
useEffect(() => {
const firstInput = document.getElementById('setup-view-letter-selector-0');
if (firstInput) {
firstInput.focus();
}
}, []);
const handleLetterChange = (i: number): ChangeEventHandler<HTMLInputElement> => (ev) => {
let newLetter = ev.currentTarget.value.slice(-1).toUpperCase();
if (letters.length > i && newLetter === letters[i]) {
newLetter = ev.currentTarget.value[0].toUpperCase();
}
if (isValidLetter(newLetter)) {
let idx = letters.indexOf(newLetter);
if (idx === -1) {
idx = i;
}
const newLetters = [...letters];
if (newLetters.length > idx) {
newLetters.splice(idx, 1, newLetter);
} else {
newLetters.push(newLetter);
}
dispatch({ type: 'SET_LETTERS', payload: newLetters });
let nextIdx = idx + 1;
if (nextIdx > newLetters.length) {
nextIdx = newLetters.length;
}
nextIdx = Math.min(nextIdx, 6);
const nextInput = document.getElementById(`setup-view-letter-selector-${nextIdx}`);
if (nextInput) {
nextInput.focus();
}
}
};
return (
<div>
<div className="flex items-center justify-between">
<button
className="p-3 text-lg disabled:text-gray-400"
disabled={letters.length !== 7}
onClick={() => dispatch({ type: 'SWITCH_VIEW', payload: 'GAME' })}
type="button"
>
</button>
</div>
<h2 className="mt-6 text-center text-gray-600 font-light">
Choose letters:
</h2>
<div className="my-3 flex justify-center space-x-2">
{range7.map((i) => (
<input
className={`
w-10 py-2 appearance-none outline-none
border-2 ${i === 0 ? 'border-yellow-400' : 'border-gray-200'}
rounded text-center
`}
key={i}
id={`setup-view-letter-selector-${i}`}
onChange={handleLetterChange(i)}
type="text"
value={letters.length > i ? letters[i] : ''}
/>
))}
</div>
<div className="flex justify-center">
<button
className={`
my-6 px-4 py-2 rounded font-bold text-sm
bg-yellow-400 hover:bg-yellow-500 transition-all
${letters.length < 7 ? 'opacity-0 cursor-default' : ''}
`}
disabled={letters.length !== 7}
onClick={() => dispatch({ type: 'SWITCH_VIEW', payload: 'GAME' })}
type="button"
>
Play Built to Spell
</button>
</div>
</div>
);
};
export default SetupView;

137
src/store.tsx Normal file
View file

@ -0,0 +1,137 @@
import React, {
createContext,
Dispatch,
FunctionComponent,
Reducer,
useMemo,
useReducer,
} from 'react';
import twl06 = require('./twl06.json');
export interface State {
currentWord: string;
letters: string[];
score: number;
view: 'GAME' | 'SETUP';
words: string[];
}
const initialState: State = {
currentWord: '',
letters: [],
score: 0,
view: 'SETUP',
words: [],
};
export interface DeleteLetterAction {
type: 'DELETE_LETTER';
}
export interface EnterLetterAction {
type: 'ENTER_LETTER';
payload: string;
}
export interface EnterWordAction {
type: 'ENTER_WORD';
}
export interface SetLettersAction {
type: 'SET_LETTERS';
payload: string[];
}
export interface ShuffleLettersAction {
type: 'SHUFFLE_LETTERS';
}
export interface SwitchViewAction {
type: 'SWITCH_VIEW';
payload: State['view'];
}
type Action = DeleteLetterAction
| EnterLetterAction
| EnterWordAction
| SetLettersAction
| ShuffleLettersAction
| SwitchViewAction;
const shuffle = (letters: string[]): string[] => {
const newLetters = letters.slice(0, 1);
const choices = letters.slice(1);
for (let i = 1; i < letters.length; i += 1) {
const choice = Math.floor(Math.random() * choices.length);
newLetters.push(choices.splice(choice, 1)[0]);
}
return newLetters;
};
const canSubmitWord = (state: State): boolean => {
const { currentWord, words } = state;
return currentWord.length > 3 && !words.includes(currentWord)
&& currentWord.split('').includes(state.letters[0])
&& twl06.includes(currentWord.toLowerCase());
};
const scoreWord = (word: string): number => (word.length === 4 ? 1 : word.length);
const enterWordReducer: Reducer<State, EnterWordAction> = (state) => {
const valid = canSubmitWord(state);
return {
...state,
currentWord: '',
score: state.score + (valid ? scoreWord(state.currentWord) : 0),
words: valid ? [...state.words, state.currentWord] : state.words,
};
};
const reducer: Reducer<State, Action> = (state, action) => {
switch (action.type) {
case 'DELETE_LETTER':
return { ...state, currentWord: state.currentWord.slice(0, -1) };
case 'ENTER_LETTER':
return { ...state, currentWord: `${state.currentWord}${action.payload}` };
case 'ENTER_WORD':
return enterWordReducer(state, action);
case 'SET_LETTERS':
return action.payload.length === state.letters.length
&& action.payload.every((ch, i) => state.letters[i] === ch)
? state
: {
...state,
currentWord: '',
letters: action.payload,
score: 0,
words: [],
};
case 'SHUFFLE_LETTERS':
return { ...state, letters: shuffle(state.letters) };
case 'SWITCH_VIEW':
return { ...state, view: action.payload };
default:
throw new Error();
}
};
interface ContextValue {
state: State;
dispatch: Dispatch<Action>;
}
export const StoreContext = createContext({
state: initialState,
dispatch: () => undefined,
} as ContextValue);
export const StoreProvider: FunctionComponent = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState);
const ctxVal = useMemo(() => ({ state, dispatch }), [state, dispatch]);
return (
<StoreContext.Provider value={ctxVal}>
{children}
</StoreContext.Provider>
);
};

1
src/twl06.json Normal file

File diff suppressed because one or more lines are too long

42
tsconfig.json Normal file
View file

@ -0,0 +1,42 @@
{
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"alwaysStrict": true,
"emitDecoratorMetadata": true,
"esModuleInterop": true,
"experimentalDecorators": true,
"jsx": "react",
"lib": [
"es7",
"dom"
],
"module": "commonjs",
"moduleResolution": "node",
"noImplicitAny": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"sourceMap": true,
"strict": true,
"strictFunctionTypes": true,
"strictNullChecks": true,
"strictPropertyInitialization": true,
"target": "es6",
"baseUrl": "./",
"allowJs": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"isolatedModules": true
},
"include": [
"src/**/*",
".eslintrc.js",
],
"exclude": [
"node_modules"
]
}