Compare commits
10 commits
93ddcc31f9
...
7ebe53f0a4
Author | SHA1 | Date | |
---|---|---|---|
![]() |
7ebe53f0a4 | ||
![]() |
07fde82912 | ||
![]() |
314a89d185 | ||
![]() |
a9d9c1a3af | ||
![]() |
57c05a265a | ||
![]() |
18c6ab739e | ||
![]() |
b49ff9f0f4 | ||
![]() |
b73ff68d17 | ||
![]() |
5cb4eeae72 | ||
![]() |
9ff8e0ba71 |
30 changed files with 13711 additions and 15869 deletions
|
@ -1 +0,0 @@
|
||||||
module.exports = {};
|
|
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"projects": {
|
|
||||||
"default": "built-to-spell"
|
|
||||||
}
|
|
||||||
}
|
|
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -1,5 +1,8 @@
|
||||||
dist
|
dist
|
||||||
|
public
|
||||||
.cache
|
.cache
|
||||||
|
.parcel-cache
|
||||||
|
worker
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
logs
|
logs
|
||||||
|
@ -67,3 +70,6 @@ node_modules/
|
||||||
|
|
||||||
# dotenv environment variables file
|
# dotenv environment variables file
|
||||||
.env
|
.env
|
||||||
|
|
||||||
|
# Workers
|
||||||
|
transpiled
|
||||||
|
|
3
.parcelrc
Normal file
3
.parcelrc
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"extends": "@parcel/config-default",
|
||||||
|
}
|
|
@ -2,7 +2,8 @@
|
||||||
|
|
||||||
## What It Is
|
## What It Is
|
||||||
|
|
||||||
A browser-based game inspired by the New York Times Spelling Bee.
|
A browser-based game inspired by the New York Times Spelling Bee. Also, a
|
||||||
|
weekend project to expand my CSS3 skill set (and my vocabulary).
|
||||||
|
|
||||||
## What It Isn't
|
## What It Isn't
|
||||||
|
|
||||||
|
@ -12,4 +13,6 @@ your friends when you beat their scores; they might be playing by different
|
||||||
rules. (If you're playing Built to Spell, of course, you're still automatically
|
rules. (If you're playing Built to Spell, of course, you're still automatically
|
||||||
the coolest kid on the block regardless.)
|
the coolest kid on the block regardless.)
|
||||||
|
|
||||||
## [Play It](https://bts.brentschroeter.com/)
|
## Play It
|
||||||
|
|
||||||
|
[https://bts.brentschroeter.com/](https://bts.brentschroeter.com/)
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
{
|
|
||||||
"hosting": {
|
|
||||||
"public": "dist",
|
|
||||||
"ignore": [
|
|
||||||
"firebase.json",
|
|
||||||
"**/.*",
|
|
||||||
"**/node_modules/**"
|
|
||||||
],
|
|
||||||
"rewrites": [
|
|
||||||
{
|
|
||||||
"source": "**",
|
|
||||||
"destination": "/index.html"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
23320
package-lock.json
generated
23320
package-lock.json
generated
File diff suppressed because it is too large
Load diff
39
package.json
39
package.json
|
@ -2,11 +2,10 @@
|
||||||
"name": "built-to-spell",
|
"name": "built-to-spell",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"description": "NYT Spelling Bee simulator",
|
"description": "NYT Spelling Bee simulator",
|
||||||
"main": "index.js",
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "eslint src",
|
"lint": "eslint src",
|
||||||
"deploy": "rm -r .cache ; parcel build -d dist src/index.html && firebase deploy --only=hosting",
|
"build": "parcel build --no-cache --dist-dir=public src/index.html",
|
||||||
"start": "rm -r .cache ; parcel -d dist src/index.html"
|
"start": "rm -r .cache ; parcel serve --dist-dir public src/index.html"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -19,20 +18,28 @@
|
||||||
},
|
},
|
||||||
"homepage": "https://gitlab.com/brentschroeter/built-to-spell#readme",
|
"homepage": "https://gitlab.com/brentschroeter/built-to-spell#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react": "^17.0.1",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^17.0.1"
|
"react-dom": "^18.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.12.10",
|
"@babel/core": "^7.20.7",
|
||||||
"@types/node": "^14.14.20",
|
"@parcel/packager-raw-url": "^2.8.2",
|
||||||
"@types/react": "^17.0.0",
|
"@parcel/transformer-typescript-tsc": "^2.8.2",
|
||||||
"@types/react-dom": "^17.0.0",
|
"@parcel/transformer-webmanifest": "^2.8.2",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.11.1",
|
"@types/node": "^18.11.18",
|
||||||
"eslint": "^7.17.0",
|
"@types/react": "^18.0.26",
|
||||||
"eslint-config-airbnb": "^18.2.1",
|
"@types/react-dom": "^17.0.14",
|
||||||
"eslint-plugin-jsx-a11y": "^6.4.1",
|
"@typescript-eslint/eslint-plugin": "^5.47.1",
|
||||||
"eslint-plugin-react": "^7.22.0",
|
"@typescript-eslint/parser": "^5.47.1",
|
||||||
"parcel-bundler": "^1.12.4",
|
"eslint": "^8.30.0",
|
||||||
"typescript": "^4.1.3"
|
"eslint_d": "^11.1.1",
|
||||||
|
"eslint-config-airbnb": "^19.0.4",
|
||||||
|
"eslint-plugin-import": "^2.24.1",
|
||||||
|
"eslint-plugin-jsx-a11y": "^6.6.1",
|
||||||
|
"eslint-plugin-react": "^7.31.11",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"parcel": "^2.8.2",
|
||||||
|
"process": "^0.11.10",
|
||||||
|
"typescript": "^4.6.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
BIN
src/apple.png
BIN
src/apple.png
Binary file not shown.
Before Width: | Height: | Size: 654 KiB After Width: | Height: | Size: 6.5 KiB |
|
@ -4,6 +4,7 @@ import React, {
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
import Honeycomb from './honeycomb';
|
import Honeycomb from './honeycomb';
|
||||||
|
import { isPangram } from './scoring';
|
||||||
import { StoreContext } from './store';
|
import { StoreContext } from './store';
|
||||||
|
|
||||||
const GameView: FunctionComponent = () => {
|
const GameView: FunctionComponent = () => {
|
||||||
|
@ -46,7 +47,7 @@ const GameView: FunctionComponent = () => {
|
||||||
)}
|
)}
|
||||||
{words.map((word) => (
|
{words.map((word) => (
|
||||||
<div
|
<div
|
||||||
className="m-1"
|
className={`m-1 ${isPangram(letters, word) ? 'text-yellow-400' : ''}`}
|
||||||
key={word}
|
key={word}
|
||||||
>
|
>
|
||||||
{word}
|
{word}
|
||||||
|
|
BIN
src/icon-192.png
BIN
src/icon-192.png
Binary file not shown.
Before Width: | Height: | Size: 828 KiB After Width: | Height: | Size: 9.2 KiB |
BIN
src/icon-512.png
BIN
src/icon-512.png
Binary file not shown.
Before Width: | Height: | Size: 5 MiB After Width: | Height: | Size: 27 KiB |
|
@ -13,6 +13,6 @@
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-gray-50">
|
<body class="bg-gray-50">
|
||||||
<div id="app-root" class="w-screen h-screen"></div>
|
<div id="app-root" class="w-screen h-screen"></div>
|
||||||
<script type="text/javascript" src="./index.tsx"></script>
|
<script type="module" src="./index.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -2,12 +2,15 @@ import React from 'react';
|
||||||
import { render } from 'react-dom';
|
import { render } from 'react-dom';
|
||||||
|
|
||||||
import App from './app';
|
import App from './app';
|
||||||
|
import { NytProvider } from './nyt';
|
||||||
import { StoreProvider } from './store';
|
import { StoreProvider } from './store';
|
||||||
|
|
||||||
render(
|
render(
|
||||||
(
|
(
|
||||||
<StoreProvider>
|
<StoreProvider>
|
||||||
<App />
|
<NytProvider>
|
||||||
|
<App />
|
||||||
|
</NytProvider>
|
||||||
</StoreProvider>
|
</StoreProvider>
|
||||||
),
|
),
|
||||||
document.getElementById('app-root'),
|
document.getElementById('app-root'),
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"icons": [
|
"icons": [
|
||||||
{"src": "/icon-192.png", "type": "image/png", "sizes": "192x192"},
|
{"src": "./icon-192.png", "type": "image/png", "sizes": "192x192"},
|
||||||
{"src": "/icon-512.png", "type": "image/png", "sizes": "512x512"}
|
{"src": "./icon-512.png", "type": "image/png", "sizes": "512x512"}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
37
src/nyt.tsx
Normal file
37
src/nyt.tsx
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import React, {
|
||||||
|
createContext,
|
||||||
|
FC,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
interface GameData {
|
||||||
|
today: {
|
||||||
|
validLetters: string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NytData {
|
||||||
|
letters: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Context = createContext<NytData>({ letters: [] });
|
||||||
|
|
||||||
|
export const NytProvider: FC = ({ children }) => {
|
||||||
|
const [nytData, setNytData] = useState({ letters: [] } as NytData);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch('/api/game-data/latest')
|
||||||
|
.then((res) => res.json() as Promise<GameData>)
|
||||||
|
.then((body) => {
|
||||||
|
setNytData({ letters: body.today.validLetters.map((x) => x.toLocaleUpperCase('en')) });
|
||||||
|
})
|
||||||
|
.catch(console.error);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Context.Provider value={nytData}>
|
||||||
|
{children}
|
||||||
|
</Context.Provider>
|
||||||
|
);
|
||||||
|
};
|
20
src/scoring.ts
Normal file
20
src/scoring.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import twl06 = require('./twl06.json');
|
||||||
|
|
||||||
|
export const isPangram = (letters: string[], word: string): boolean => {
|
||||||
|
const chars = word.split('');
|
||||||
|
return letters.every((letter) => chars.includes(letter));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const scoreWord = (letters: string[], words: string[], word: string): number => {
|
||||||
|
if (word.length < 4 || words.includes(word) || !word.split('').includes(letters[0])
|
||||||
|
|| !twl06.includes(word.toLowerCase())) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (word.length === 4) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (isPangram(letters, word)) {
|
||||||
|
return word.length + 7;
|
||||||
|
}
|
||||||
|
return word.length;
|
||||||
|
};
|
|
@ -5,6 +5,7 @@ import React, {
|
||||||
useEffect,
|
useEffect,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
|
import { Context as NytContext } from './nyt';
|
||||||
import { StoreContext } from './store';
|
import { StoreContext } from './store';
|
||||||
|
|
||||||
const range7 = [0, 1, 2, 3, 4, 5, 6];
|
const range7 = [0, 1, 2, 3, 4, 5, 6];
|
||||||
|
@ -12,8 +13,12 @@ const range7 = [0, 1, 2, 3, 4, 5, 6];
|
||||||
const isValidLetter = (ch: string): boolean => ch.length === 1 && ch >= 'A' && ch <= 'Z';
|
const isValidLetter = (ch: string): boolean => ch.length === 1 && ch >= 'A' && ch <= 'Z';
|
||||||
|
|
||||||
const SetupView: FunctionComponent = () => {
|
const SetupView: FunctionComponent = () => {
|
||||||
const { dispatch, state } = useContext(StoreContext);
|
const {
|
||||||
const { letters } = state;
|
dispatch,
|
||||||
|
state: { letters },
|
||||||
|
} = useContext(StoreContext);
|
||||||
|
|
||||||
|
const { letters: nytLetters } = useContext(NytContext);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const firstInput = document.getElementById('setup-view-letter-selector-0');
|
const firstInput = document.getElementById('setup-view-letter-selector-0');
|
||||||
|
@ -82,20 +87,30 @@ const SetupView: FunctionComponent = () => {
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-center">
|
<button
|
||||||
<button
|
className={`
|
||||||
className={`
|
w-full my-6 px-4 py-2 rounded font-bold text-sm
|
||||||
my-6 px-4 py-2 rounded font-bold text-sm
|
bg-yellow-400 hover:bg-yellow-500 transition-all
|
||||||
bg-yellow-400 hover:bg-yellow-500 transition-all
|
${nytLetters.length === 7 ? 'bg-yellow-400 cursor-pointer' : 'bg-gray-400 cursor-default'}
|
||||||
${letters.length < 7 ? 'opacity-0 cursor-default' : ''}
|
`}
|
||||||
`}
|
disabled={nytLetters.length !== 7}
|
||||||
disabled={letters.length !== 7}
|
onClick={() => dispatch({ type: 'SET_LETTERS', payload: nytLetters })}
|
||||||
onClick={() => dispatch({ type: 'SWITCH_VIEW', payload: 'GAME' })}
|
type="button"
|
||||||
type="button"
|
>
|
||||||
>
|
{nytLetters.length === 7 ? 'Populate from NYT' : 'Loading from NYT...'}
|
||||||
Play Built to Spell
|
</button>
|
||||||
</button>
|
<button
|
||||||
</div>
|
className={`
|
||||||
|
w-full 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' : 'cursor-pointer'}
|
||||||
|
`}
|
||||||
|
disabled={letters.length !== 7}
|
||||||
|
onClick={() => dispatch({ type: 'SWITCH_VIEW', payload: 'GAME' })}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Play Built to Spell
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,7 +7,7 @@ import React, {
|
||||||
useReducer,
|
useReducer,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
import twl06 = require('./twl06.json');
|
import { scoreWord } from './scoring';
|
||||||
|
|
||||||
export interface State {
|
export interface State {
|
||||||
currentWord: string;
|
currentWord: string;
|
||||||
|
@ -69,22 +69,13 @@ const shuffle = (letters: string[]): string[] => {
|
||||||
return newLetters;
|
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 enterWordReducer: Reducer<State, EnterWordAction> = (state) => {
|
||||||
const valid = canSubmitWord(state);
|
const score = scoreWord(state.letters, state.words, state.currentWord);
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
currentWord: '',
|
currentWord: '',
|
||||||
score: state.score + (valid ? scoreWord(state.currentWord) : 0),
|
score: state.score + score,
|
||||||
words: valid ? [...state.words, state.currentWord] : state.words,
|
words: score > 0 ? [...state.words, state.currentWord] : state.words,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
"isolatedModules": true
|
"isolatedModules": true
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src/**/*",
|
"src/",
|
||||||
".eslintrc.js",
|
".eslintrc.js",
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
|
|
0
workers-site/.cargo-ok
Normal file
0
workers-site/.cargo-ok
Normal file
170
workers-site/.eslintrc.js
Normal file
170
workers-site/.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'] },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
7
workers-site/jestconfig.json
Normal file
7
workers-site/jestconfig.json
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(t|j)sx?$": "ts-jest"
|
||||||
|
},
|
||||||
|
"testRegex": "/test/.*\\.test\\.ts$",
|
||||||
|
"collectCoverageFrom": ["src/**/*.{ts,js}"]
|
||||||
|
}
|
5662
workers-site/package-lock.json
generated
Normal file
5662
workers-site/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
36
workers-site/package.json
Normal file
36
workers-site/package.json
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
{
|
||||||
|
"name": "worker",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "dist/worker.js",
|
||||||
|
"author": "",
|
||||||
|
"license": "MIT",
|
||||||
|
"scripts": {
|
||||||
|
"build": "webpack",
|
||||||
|
"format": "prettier --write '*.{json,js}' 'src/**/*.{js,ts}' 'test/**/*.{js,ts}'",
|
||||||
|
"lint": "eslint --max-warnings=0 src && prettier --check '*.{json,js}' 'src/**/*.{js,ts}' 'test/**/*.{js,ts}'",
|
||||||
|
"test": "jest --config jestconfig.json --verbose"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@cloudflare/kv-asset-handler": "~0.1.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@cloudflare/workers-types": "^3.0.0",
|
||||||
|
"@types/jest": "^26.0.23",
|
||||||
|
"@types/service-worker-mock": "^2.0.1",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^4.16.1",
|
||||||
|
"@typescript-eslint/parser": "^4.16.1",
|
||||||
|
"eslint": "^7.21.0",
|
||||||
|
"eslint-config-prettier": "^8.1.0",
|
||||||
|
"eslint-config-typescript": "^3.0.0",
|
||||||
|
"eslint_d": "^11.1.1",
|
||||||
|
"jest": "^27.0.1",
|
||||||
|
"prettier": "^2.3.0",
|
||||||
|
"service-worker-mock": "^2.0.5",
|
||||||
|
"ts-jest": "^27.0.1",
|
||||||
|
"ts-loader": "^9.2.8",
|
||||||
|
"typescript": "^4.6.3",
|
||||||
|
"webpack": "^5.70.0",
|
||||||
|
"webpack-cli": "^4.9.2"
|
||||||
|
}
|
||||||
|
}
|
5
workers-site/src/bindings.d.ts
vendored
Normal file
5
workers-site/src/bindings.d.ts
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export {};
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
const NYT_GAME_DATA: KVNamespace;
|
||||||
|
}
|
102
workers-site/src/index.ts
Normal file
102
workers-site/src/index.ts
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
import {
|
||||||
|
getAssetFromKV,
|
||||||
|
Options as GetAssetFromKVOptions,
|
||||||
|
} from '@cloudflare/kv-asset-handler';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The DEBUG flag will do two things that help during development:
|
||||||
|
* 1. we will skip caching on the edge, which makes it easier to
|
||||||
|
* debug.
|
||||||
|
* 2. we will return an error message on exception in your Response rather
|
||||||
|
* than the default 404.html page.
|
||||||
|
*/
|
||||||
|
const DEBUG = false;
|
||||||
|
|
||||||
|
addEventListener('fetch', (event) => {
|
||||||
|
try {
|
||||||
|
event.respondWith(handleFetch(event));
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error && DEBUG) {
|
||||||
|
return event.respondWith(new Response(e.message || e.toString(), { status: 500 }));
|
||||||
|
}
|
||||||
|
event.respondWith(new Response('Internal Error', { status: 500 }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
addEventListener('scheduled', (event) => {
|
||||||
|
event.waitUntil(handleCron());
|
||||||
|
});
|
||||||
|
|
||||||
|
interface DailyGameData {
|
||||||
|
printDate: string;
|
||||||
|
centerLetter: string;
|
||||||
|
outerLetters: string[];
|
||||||
|
answers: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GameData {
|
||||||
|
today: DailyGameData;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleFetch(event: FetchEvent) {
|
||||||
|
const url = new URL(event.request.url);
|
||||||
|
const options: Partial<GetAssetFromKVOptions> = {};
|
||||||
|
try {
|
||||||
|
if (DEBUG) {
|
||||||
|
options.cacheControl = { bypassCache: true };
|
||||||
|
}
|
||||||
|
let response = new Response('', {});
|
||||||
|
if (url.pathname.toLocaleLowerCase('en') === '/api/game-data/latest') {
|
||||||
|
const gameData = await NYT_GAME_DATA.get('latest');
|
||||||
|
response = new Response(gameData || '', { status: gameData ? 200 : 500 });
|
||||||
|
response.headers.set('Content-Type', 'application/json');
|
||||||
|
} else {
|
||||||
|
const page = await getAssetFromKV(event, options);
|
||||||
|
response = new Response(page.body, page);
|
||||||
|
}
|
||||||
|
response.headers.set('X-XSS-Protection', '1; mode=block');
|
||||||
|
response.headers.set('X-Content-Type-Options', 'nosniff');
|
||||||
|
response.headers.set('X-Frame-Options', 'DENY');
|
||||||
|
response.headers.set('Referrer-Policy', 'unsafe-url');
|
||||||
|
response.headers.set('Feature-Policy', 'none');
|
||||||
|
return response;
|
||||||
|
} catch (e) {
|
||||||
|
// if an error is thrown try to serve the asset at 404.html
|
||||||
|
if (e instanceof Error && DEBUG) {
|
||||||
|
return new Response(e.message || e.toString(), { status: 500 });
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
let notFoundResponse = await getAssetFromKV(event, {
|
||||||
|
mapRequestToAsset: (req) => new Request(`${new URL(req.url).origin}/404.html`, req),
|
||||||
|
});
|
||||||
|
return new Response(notFoundResponse.body, { ...notFoundResponse, status: 404 });
|
||||||
|
} catch (e) { /* no-op */ }
|
||||||
|
return new Response('Not found.', { status: 404 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCron() {
|
||||||
|
console.log('Starting cron trigger...');
|
||||||
|
const resp = await fetch('https://www.nytimes.com/puzzles/spelling-bee');
|
||||||
|
console.log('Fetched from NYT.');
|
||||||
|
let text = await resp.text();
|
||||||
|
const startStr = '<script type="text/javascript">window.gameData = {';
|
||||||
|
const startIdx = text.indexOf(startStr);
|
||||||
|
if (startIdx === -1) {
|
||||||
|
throw new Error('Unable to locate gameData.');
|
||||||
|
}
|
||||||
|
text = text.slice(startIdx + startStr.length - 1);
|
||||||
|
const endIdx = text.indexOf('</script>');
|
||||||
|
if (endIdx === -1) {
|
||||||
|
throw new Error('Unable to locate end of gameData.');
|
||||||
|
}
|
||||||
|
text = text.slice(0, endIdx);
|
||||||
|
console.log('Found gameData.');
|
||||||
|
const gameData = JSON.parse(text) as GameData;
|
||||||
|
console.log('Parsed gameData.');
|
||||||
|
await Promise.all([
|
||||||
|
NYT_GAME_DATA.put('latest', text),
|
||||||
|
NYT_GAME_DATA.put(gameData.today.printDate, text),
|
||||||
|
]);
|
||||||
|
console.log('Saved gameData.');
|
||||||
|
}
|
21
workers-site/tsconfig.json
Normal file
21
workers-site/tsconfig.json
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./dist",
|
||||||
|
"module": "commonjs",
|
||||||
|
"target": "esnext",
|
||||||
|
"lib": ["esnext"],
|
||||||
|
"alwaysStrict": true,
|
||||||
|
"strict": true,
|
||||||
|
"preserveConstEnums": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"sourceMap": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"types": [
|
||||||
|
"@cloudflare/workers-types",
|
||||||
|
"@types/jest",
|
||||||
|
"@types/service-worker-mock"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": ["node_modules", "dist", "test"]
|
||||||
|
}
|
22
workers-site/webpack.config.js
Normal file
22
workers-site/webpack.config.js
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
entry: path.join(__dirname, 'src/index.ts'),
|
||||||
|
output: {
|
||||||
|
filename: 'worker.js',
|
||||||
|
path: path.join(__dirname, 'dist'),
|
||||||
|
},
|
||||||
|
devtool: 'cheap-module-source-map',
|
||||||
|
mode: 'development',
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.ts', '.tsx', '.js'],
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.tsx?$/,
|
||||||
|
loader: 'ts-loader',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
21
workers-site/wrangler.toml
Normal file
21
workers-site/wrangler.toml
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
name = "built-to-spell"
|
||||||
|
main = "./dist/worker.js"
|
||||||
|
route = 'bts.brentsch.com/*'
|
||||||
|
account_id = '7c81d50e4fc6ec51eecd802aacd6304b'
|
||||||
|
usage_model = 'bundled'
|
||||||
|
compatibility_flags = []
|
||||||
|
workers_dev = false
|
||||||
|
compatibility_date = "2022-03-27"
|
||||||
|
|
||||||
|
[triggers]
|
||||||
|
crons = ["0 * * * *"]
|
||||||
|
|
||||||
|
[build]
|
||||||
|
command = "npm --prefix=../ run build && npm run build"
|
||||||
|
|
||||||
|
[[kv_namespaces]]
|
||||||
|
binding = "NYT_GAME_DATA"
|
||||||
|
id = "ad58b382cc884b889f42ed94cf4dbb6b"
|
||||||
|
|
||||||
|
[site]
|
||||||
|
bucket = "../public"
|
Loading…
Add table
Reference in a new issue