Initial commit.
This commit is contained in:
commit
34ba8d408b
26 changed files with 51739 additions and 0 deletions
1
.babelrc.js
Normal file
1
.babelrc.js
Normal file
|
@ -0,0 +1 @@
|
|||
module.exports = {};
|
170
.eslintrc.js
Normal file
170
.eslintrc.js
Normal 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
5
.firebaserc
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"projects": {
|
||||
"default": "built-to-spell"
|
||||
}
|
||||
}
|
66
.gitignore
vendored
Normal file
66
.gitignore
vendored
Normal 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
21
LICENSE
Normal 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
45
dist/custom.d5cc83c2.css
vendored
Normal 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
1
dist/custom.d5cc83c2.css.map
vendored
Normal 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
397
dist/custom.d5cc83c2.js
vendored
Normal 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
1
dist/custom.d5cc83c2.js.map
vendored
Normal 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
14
dist/index.html
vendored
Normal 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
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
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
16
firebase.json
Normal 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
19728
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
38
package.json
Normal file
38
package.json
Normal 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
27
src/app.tsx
Normal 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
42
src/custom.css
Normal 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
97
src/game-view.tsx
Normal 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
22
src/hexagon.tsx
Normal 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
69
src/honeycomb.tsx
Normal 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
14
src/index.html
Normal 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
14
src/index.tsx
Normal 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
103
src/setup-view.tsx
Normal 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
137
src/store.tsx
Normal 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
1
src/twl06.json
Normal file
File diff suppressed because one or more lines are too long
42
tsconfig.json
Normal file
42
tsconfig.json
Normal 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"
|
||||
]
|
||||
}
|
Loading…
Add table
Reference in a new issue