WIP
This commit is contained in:
parent
f67606b944
commit
1e71255371
9 changed files with 389 additions and 335 deletions
82
package-lock.json
generated
82
package-lock.json
generated
|
@ -10,10 +10,12 @@
|
|||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@solid-primitives/storage": "^4.3.1",
|
||||
"@solidjs/router": "^0.15.3",
|
||||
"@tauri-apps/api": "^2",
|
||||
"@tauri-apps/plugin-opener": "^2",
|
||||
"@tauri-apps/plugin-store": "^2.2.0",
|
||||
"solid-js": "^1.9.3"
|
||||
"solid-js": "^1.9.3",
|
||||
"vite-plugin-inline-css-modules": "^0.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2",
|
||||
|
@ -299,7 +301,6 @@
|
|||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -316,7 +317,6 @@
|
|||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -333,7 +333,6 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -350,7 +349,6 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -367,7 +365,6 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -384,7 +381,6 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -401,7 +397,6 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -418,7 +413,6 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -435,7 +429,6 @@
|
|||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -452,7 +445,6 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -469,7 +461,6 @@
|
|||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -486,7 +477,6 @@
|
|||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -503,7 +493,6 @@
|
|||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -520,7 +509,6 @@
|
|||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -537,7 +525,6 @@
|
|||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -554,7 +541,6 @@
|
|||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -571,7 +557,6 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -588,7 +573,6 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -605,7 +589,6 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -622,7 +605,6 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -639,7 +621,6 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -656,7 +637,6 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -673,7 +653,6 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -690,7 +669,6 @@
|
|||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -707,7 +685,6 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -777,7 +754,6 @@
|
|||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -791,7 +767,6 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -805,7 +780,6 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -819,7 +793,6 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -833,7 +806,6 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -847,7 +819,6 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -861,7 +832,6 @@
|
|||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -875,7 +845,6 @@
|
|||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -889,7 +858,6 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -903,7 +871,6 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -917,7 +884,6 @@
|
|||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -931,7 +897,6 @@
|
|||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -945,7 +910,6 @@
|
|||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -959,7 +923,6 @@
|
|||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -973,7 +936,6 @@
|
|||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -987,7 +949,6 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -1001,7 +962,6 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -1015,7 +975,6 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -1029,7 +988,6 @@
|
|||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -1043,7 +1001,6 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -1080,6 +1037,15 @@
|
|||
"solid-js": "^1.6.12"
|
||||
}
|
||||
},
|
||||
"node_modules/@solidjs/router": {
|
||||
"version": "0.15.3",
|
||||
"resolved": "https://registry.npmjs.org/@solidjs/router/-/router-0.15.3.tgz",
|
||||
"integrity": "sha512-iEbW8UKok2Oio7o6Y4VTzLj+KFCmQPGEpm1fS3xixwFBdclFVBvaQVeibl1jys4cujfAK5Kn6+uG2uBm3lxOMw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"solid-js": "^1.8.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/api": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.4.1.tgz",
|
||||
|
@ -1374,7 +1340,6 @@
|
|||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
|
||||
"integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/babel-plugin-jsx-dom-expressions": {
|
||||
|
@ -1530,7 +1495,6 @@
|
|||
"version": "0.25.2",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz",
|
||||
"integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
|
@ -1581,7 +1545,6 @@
|
|||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
|
@ -1702,7 +1665,6 @@
|
|||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
|
@ -1741,14 +1703,12 @@
|
|||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.3",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
|
||||
"integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
|
@ -1777,7 +1737,6 @@
|
|||
"version": "4.38.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.38.0.tgz",
|
||||
"integrity": "sha512-5SsIRtJy9bf1ErAOiFMFzl64Ex9X5V7bnJ+WlFMb+zmP459OSWCEG7b0ERZ+PEU7xPt4OG3RHbrp1LJlXxYTrw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.7"
|
||||
|
@ -1874,7 +1833,6 @@
|
|||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
|
@ -1933,10 +1891,9 @@
|
|||
"license": "ISC"
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "6.2.4",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.4.tgz",
|
||||
"integrity": "sha512-veHMSew8CcRzhL5o8ONjy8gkfmFJAd5Ac16oxBUjlwgX3Gq2Wqr+qNC3TjPIpy7TPV/KporLga5GT9HqdrCizw==",
|
||||
"dev": true,
|
||||
"version": "6.2.6",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.6.tgz",
|
||||
"integrity": "sha512-9xpjNl3kR4rVDZgPNdTL0/c6ao4km69a/2ihNQbcANz8RuCOK3hQBmLSJf3bRKVQjVMda+YvizNE8AwvogcPbw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
|
@ -2004,6 +1961,15 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vite-plugin-inline-css-modules": {
|
||||
"version": "0.0.8",
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-inline-css-modules/-/vite-plugin-inline-css-modules-0.0.8.tgz",
|
||||
"integrity": "sha512-RXpGZJ0YW69HjkOBw/1xVeVk677wZ/OPmAbvln2eFG+9dFfq9AQapCb/iZZey/oK8psoHiwb5x2HXv5FM2UPcw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"vite": ">2.0.0-0"
|
||||
}
|
||||
},
|
||||
"node_modules/vite-plugin-solid": {
|
||||
"version": "2.11.6",
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-solid/-/vite-plugin-solid-2.11.6.tgz",
|
||||
|
|
|
@ -13,10 +13,12 @@
|
|||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@solid-primitives/storage": "^4.3.1",
|
||||
"@solidjs/router": "^0.15.3",
|
||||
"@tauri-apps/api": "^2",
|
||||
"@tauri-apps/plugin-opener": "^2",
|
||||
"@tauri-apps/plugin-store": "^2.2.0",
|
||||
"solid-js": "^1.9.3"
|
||||
"solid-js": "^1.9.3",
|
||||
"vite-plugin-inline-css-modules": "^0.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2",
|
||||
|
|
153
src/App.tsx
153
src/App.tsx
|
@ -1,141 +1,18 @@
|
|||
import { For } from "solid-js";
|
||||
import "./App.css";
|
||||
import useAppModel from "./useAppModel";
|
||||
import FitText from "./FitText";
|
||||
import { type JSX } from "solid-js"
|
||||
|
||||
// TODO
|
||||
// add routing
|
||||
// make overview page with calendar list of puzzles
|
||||
// show solved / aced / busted
|
||||
// make detail page with puzzle id in path
|
||||
// add nav links
|
||||
|
||||
function App() {
|
||||
const {
|
||||
connections,
|
||||
store,
|
||||
setStore,
|
||||
handleGuess,
|
||||
handlePinUnpin,
|
||||
handleSelectGame,
|
||||
handleShuffle,
|
||||
handleDeselect,
|
||||
getFromPuzzle,
|
||||
} = useAppModel();
|
||||
|
||||
return (
|
||||
<main class="container">
|
||||
<div class="puzzle">
|
||||
<header class="puzzle-header">
|
||||
<h1>Slick Connections</h1>
|
||||
<div class="puzzle-header-actions">
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => {
|
||||
handleSelectGame(store.puzzleId - 1);
|
||||
}}
|
||||
disabled={store.puzzleId === 1}
|
||||
>
|
||||
-
|
||||
</button>
|
||||
<select
|
||||
name="puzzleNumber"
|
||||
id="puzzleNumber"
|
||||
on:input={({ target: { value } }) =>
|
||||
handleSelectGame(parseInt(value, 10))
|
||||
}
|
||||
value={store.puzzleId}
|
||||
>
|
||||
<For each={connections}>
|
||||
{({ id }) => <option value={id}>{id}</option>}
|
||||
</For>
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => {
|
||||
handleSelectGame(store.puzzleId + 1);
|
||||
}}
|
||||
disabled={
|
||||
store.puzzleId === connections[connections.length - 1].id
|
||||
}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<div>Create four groups of four words!</div>
|
||||
<For each={store.solvedGroups}>
|
||||
{({ group, level, members }) => (
|
||||
<div class="puzzle-row">
|
||||
<div class="puzzle-group" data-level={level}>
|
||||
<div class="puzzle-group-name">{group}</div>
|
||||
<div class="puzzle-group-members">{members.join(", ")}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
<For each={[0, 1, 2, 3].slice(0, store.puzzle.length / 4)}>
|
||||
{(row) => (
|
||||
<div class="puzzle-row">
|
||||
{[0, 1, 2, 3].map((col) => {
|
||||
const index = 4 * row + col;
|
||||
const answer = () => getFromPuzzle(index).answer;
|
||||
return (
|
||||
<button
|
||||
classList={{
|
||||
"puzzle-item": true,
|
||||
"is-selected": store.selected.includes(index),
|
||||
}}
|
||||
style={{ "view-transition-name": `puzzle-${answer}` }}
|
||||
type="button"
|
||||
on:click={() => {
|
||||
setStore({
|
||||
selected: store.selected.includes(index)
|
||||
? store.selected.filter((x) => x !== index)
|
||||
: [...store.selected, index],
|
||||
});
|
||||
}}
|
||||
>
|
||||
<FitText body={answer} />
|
||||
{store.pinnedCount > index && <div class="badge">🔒</div>}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
<div class="puzzle-actions">
|
||||
<button
|
||||
type="button"
|
||||
disabled={store.selected.length === 0}
|
||||
on:click={handlePinUnpin}
|
||||
>
|
||||
{store.selected.length > 0 &&
|
||||
store.selected.every((x) => x < store.pinnedCount)
|
||||
? "Unpin"
|
||||
: "Pin"}
|
||||
</button>
|
||||
<button type="button" on:click={handleShuffle}>
|
||||
Shuffle
|
||||
</button>
|
||||
<button type="button" on:click={handleDeselect} disabled={
|
||||
store.selected.length === 0
|
||||
}>
|
||||
Deselect all
|
||||
</button>
|
||||
<button
|
||||
id="submitButton"
|
||||
type="button"
|
||||
on:click={handleGuess}
|
||||
disabled={store.selected.length !== 4}
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{ store.solvedGroups.length === 4 && <div class="celebration" /> }
|
||||
</main>
|
||||
);
|
||||
type AppProps = {
|
||||
children?: JSX.Element
|
||||
}
|
||||
|
||||
export default App;
|
||||
export default function App({ children }: AppProps) {
|
||||
return (
|
||||
<>
|
||||
<header>
|
||||
<h1>Slick Connections</h1>
|
||||
</header>
|
||||
<main>
|
||||
{children}
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
36
src/Dashboard.tsx
Normal file
36
src/Dashboard.tsx
Normal file
|
@ -0,0 +1,36 @@
|
|||
import { For } from "solid-js";
|
||||
import useAppModel from "./useAppModel";
|
||||
import { A } from "@solidjs/router";
|
||||
import { css } from 'vite-plugin-inline-css-modules'
|
||||
|
||||
const styles = css`
|
||||
calendar: {
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(7, 1fr)",
|
||||
},
|
||||
entry: (solved: boolean) => {
|
||||
return {
|
||||
borderRadius: "50%",
|
||||
backgroundColor: solved ? "green" : "gray",
|
||||
}
|
||||
},
|
||||
`
|
||||
|
||||
export default function Dashboard() {
|
||||
const { connections, store } = useAppModel();
|
||||
return (
|
||||
<div class={styles.calendar}>
|
||||
<For each={connections}>
|
||||
{(item) => {
|
||||
const isSolved = store.solutions[item.id] !== undefined;
|
||||
return (
|
||||
<A
|
||||
href={`/puzzle/${item.id}`}
|
||||
{...stylex.props(styles.entry(isSolved))}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
);
|
||||
}
|
134
src/Puzzle.tsx
Normal file
134
src/Puzzle.tsx
Normal file
|
@ -0,0 +1,134 @@
|
|||
import { For } from "solid-js";
|
||||
import "./App.css";
|
||||
import usePuzzleModel from "./usePuzzleModel";
|
||||
import FitText from "./FitText";
|
||||
import { useNavigate, useParams } from "@solidjs/router";
|
||||
|
||||
// TODO
|
||||
// add routing
|
||||
// make overview page with calendar list of puzzles
|
||||
// show solved / aced / busted
|
||||
// make detail page with puzzle id in path
|
||||
// add nav links
|
||||
|
||||
function Puzzle() {
|
||||
const params = useParams<{ id: string }>()
|
||||
const id = () => parseInt(params.id);
|
||||
|
||||
const {
|
||||
connections,
|
||||
store,
|
||||
setStore,
|
||||
handleGuess,
|
||||
handlePinUnpin,
|
||||
handleShuffle,
|
||||
handleDeselect,
|
||||
getFromPuzzle,
|
||||
} = usePuzzleModel(id);
|
||||
|
||||
const navigate = useNavigate()
|
||||
|
||||
return (
|
||||
<main class="container">
|
||||
<div class="puzzle">
|
||||
<header class="puzzle-header">
|
||||
<h1>Slick Connections</h1>
|
||||
<div class="puzzle-header-actions">
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => {
|
||||
navigate(`/puzzle/${id() - 1}`);
|
||||
}}
|
||||
disabled={id() === 1}
|
||||
>
|
||||
-
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => {
|
||||
navigate(`/puzzle/${id() + 1}`);
|
||||
}}
|
||||
disabled={id() === connections.length - 1}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<div>Create four groups of four words!</div>
|
||||
<For each={store.solvedGroups}>
|
||||
{({ group, level, members }) => (
|
||||
<div class="puzzle-row">
|
||||
<div class="puzzle-group" data-level={level}>
|
||||
<div class="puzzle-group-name">{group}</div>
|
||||
<div class="puzzle-group-members">{members.join(", ")}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
<For each={[0, 1, 2, 3].slice(0, store.puzzle.length / 4)}>
|
||||
{(row) => (
|
||||
<div class="puzzle-row">
|
||||
{[0, 1, 2, 3].map((col) => {
|
||||
const index = 4 * row + col;
|
||||
const answer = () => getFromPuzzle(index).answer;
|
||||
return (
|
||||
<button
|
||||
classList={{
|
||||
"puzzle-item": true,
|
||||
"is-selected": store.selected.includes(index),
|
||||
}}
|
||||
style={{ "view-transition-name": `puzzle-${answer}` }}
|
||||
type="button"
|
||||
on:click={() => {
|
||||
setStore({
|
||||
selected: store.selected.includes(index)
|
||||
? store.selected.filter((x) => x !== index)
|
||||
: [...store.selected, index],
|
||||
});
|
||||
}}
|
||||
>
|
||||
<FitText body={answer} />
|
||||
{store.pinnedCount > index && <div class="badge">🔒</div>}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
<div class="puzzle-actions">
|
||||
<button
|
||||
type="button"
|
||||
disabled={store.selected.length === 0}
|
||||
on:click={handlePinUnpin}
|
||||
>
|
||||
{store.selected.length > 0 &&
|
||||
store.selected.every((x) => x < store.pinnedCount)
|
||||
? "Unpin"
|
||||
: "Pin"}
|
||||
</button>
|
||||
<button type="button" on:click={handleShuffle}>
|
||||
Shuffle
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
on:click={handleDeselect}
|
||||
disabled={store.selected.length === 0}
|
||||
>
|
||||
Deselect all
|
||||
</button>
|
||||
<button
|
||||
id="submitButton"
|
||||
type="button"
|
||||
on:click={handleGuess}
|
||||
disabled={store.selected.length !== 4}
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{store.solvedGroups.length === 4 && <div class="celebration" />}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
export default Puzzle;
|
|
@ -1,5 +1,22 @@
|
|||
/* @refresh reload */
|
||||
import { render } from "solid-js/web";
|
||||
import App from "./App";
|
||||
import { Route, Router } from "@solidjs/router";
|
||||
import Dashboard from "./Dashboard";
|
||||
import Puzzle from "./Puzzle";
|
||||
|
||||
render(() => <App />, document.getElementById("root") as HTMLElement);
|
||||
render(
|
||||
() => (
|
||||
<Router root={App}>
|
||||
<Route path="/" component={Dashboard} />
|
||||
<Route
|
||||
path="/puzzle/:id"
|
||||
component={Puzzle}
|
||||
matchFilters={{
|
||||
id: /^\d+$/,
|
||||
}}
|
||||
/>
|
||||
</Router>
|
||||
),
|
||||
document.getElementById("root") as HTMLElement
|
||||
);
|
||||
|
|
|
@ -1,24 +1,15 @@
|
|||
import { makePersisted } from "@solid-primitives/storage";
|
||||
import connections from "./assets/connections.json";
|
||||
import { shuffleArray } from "./utils";
|
||||
import { createStore } from "solid-js/store";
|
||||
import { tauriStorage } from "@solid-primitives/storage/tauri";
|
||||
|
||||
type Connection = (typeof connections)[number];
|
||||
type Answer = Connection["answers"][number];
|
||||
|
||||
function fromIndex(index: number): [number, number] {
|
||||
const col = index % 4;
|
||||
const row = (index - col) / 4;
|
||||
return [row, col];
|
||||
type Solution = {
|
||||
id: number
|
||||
mistakes: number
|
||||
}
|
||||
|
||||
type AppStore = {
|
||||
puzzleId: number;
|
||||
pinnedCount: number;
|
||||
selected: number[];
|
||||
solvedGroups: Answer[];
|
||||
puzzle: number[];
|
||||
solutions: Solution[]
|
||||
};
|
||||
|
||||
export default function useAppModel() {
|
||||
|
@ -26,138 +17,23 @@ export default function useAppModel() {
|
|||
"__TAURI_INTERNALS__" in window ? tauriStorage("AppStore") : localStorage;
|
||||
const [store, setStore] = makePersisted(
|
||||
createStore<AppStore>({
|
||||
puzzleId: 1,
|
||||
pinnedCount: 0,
|
||||
selected: [],
|
||||
solvedGroups: [],
|
||||
puzzle: shuffleArray(Array.from({ length: 16 }, (_, i) => i)),
|
||||
solutions: []
|
||||
}),
|
||||
{
|
||||
name: "slick-connections",
|
||||
storage,
|
||||
serialize(data) {
|
||||
return JSON.stringify({
|
||||
...data,
|
||||
selected: [],
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const answers = (): Answer[] => {
|
||||
return connections.find((x) => x.id === store.puzzleId)!.answers;
|
||||
};
|
||||
|
||||
const handleSelectGame = (newId: number) => {
|
||||
setStore({
|
||||
puzzleId: newId,
|
||||
selected: [],
|
||||
pinnedCount: 0,
|
||||
puzzle: shuffleArray(Array.from({ length: 16 }, (_, i) => i)),
|
||||
solvedGroups: [],
|
||||
});
|
||||
};
|
||||
|
||||
const getFromPuzzle = (index: number) => {
|
||||
const puzzleIndex = store.puzzle[index];
|
||||
const [groupIndex, memberIndex] = fromIndex(puzzleIndex);
|
||||
const group = answers()[groupIndex];
|
||||
return {
|
||||
group: group.group,
|
||||
level: group.level,
|
||||
answer: answers()[groupIndex].members[memberIndex],
|
||||
};
|
||||
};
|
||||
|
||||
const handleGuess = () => {
|
||||
const selected = store.puzzle.length === 4 ? [0, 1, 2, 3] : store.selected;
|
||||
const selectedAnswers = selected.map((x) => getFromPuzzle(x));
|
||||
const { level } = selectedAnswers[0];
|
||||
const isCorrect = selectedAnswers.every((x) => x.level === level);
|
||||
if (!isCorrect) {
|
||||
// TODO you got it wrong
|
||||
alert("wrong");
|
||||
return;
|
||||
}
|
||||
const selectedPinnedCount = selected.reduce(
|
||||
(acc, cur) => acc + (cur < store.pinnedCount ? 1 : 0),
|
||||
0
|
||||
);
|
||||
setStore({
|
||||
pinnedCount: store.pinnedCount - selectedPinnedCount,
|
||||
puzzle: store.puzzle.filter((x) =>
|
||||
selected.every((s) => store.puzzle[s] !== x)
|
||||
),
|
||||
selected: [],
|
||||
});
|
||||
const newSolvedGroup = answers().find((x) => x.level === level);
|
||||
if (newSolvedGroup != null) {
|
||||
setStore({
|
||||
solvedGroups: [...store.solvedGroups, newSolvedGroup],
|
||||
});
|
||||
}
|
||||
if (store.puzzle.length === 0) {
|
||||
// completely solved!
|
||||
}
|
||||
};
|
||||
|
||||
const handleShuffle = () => {
|
||||
const pinned = store.puzzle.slice(0, store.pinnedCount);
|
||||
const toShuffle = store.puzzle.slice(store.pinnedCount);
|
||||
setStore({
|
||||
puzzle: [...pinned, ...shuffleArray(toShuffle)],
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeselect = () => {
|
||||
setStore({
|
||||
selected: [],
|
||||
});
|
||||
};
|
||||
|
||||
const handlePinUnpin = () => {
|
||||
if (store.selected.every((x) => x < store.pinnedCount)) {
|
||||
// we are unpinning
|
||||
const puzzleStart = Array.from({ length: store.pinnedCount }, (_, i) => i)
|
||||
.filter((x) => !store.selected.includes(x))
|
||||
.map((x) => store.puzzle[x]);
|
||||
const puzzleMiddle = store.selected.map((x) => store.puzzle[x]);
|
||||
const puzzleEnd = store.puzzle.slice(store.pinnedCount);
|
||||
const newPuzzle = [...puzzleStart, ...puzzleMiddle, ...puzzleEnd];
|
||||
setStore({
|
||||
pinnedCount: store.pinnedCount - store.selected.length,
|
||||
selected: [],
|
||||
puzzle: newPuzzle,
|
||||
});
|
||||
return;
|
||||
}
|
||||
// we are pinning
|
||||
const puzzleStart = store.puzzle.slice(0, store.pinnedCount);
|
||||
const puzzleMid = store.selected
|
||||
.filter((x) => x >= store.pinnedCount)
|
||||
.map((x) => store.puzzle[x]);
|
||||
const puzzleEnd = Array.from(
|
||||
{ length: store.puzzle.length - store.pinnedCount },
|
||||
(_, i) => i + store.pinnedCount
|
||||
)
|
||||
.filter((x) => !store.selected.includes(x))
|
||||
.map((x) => store.puzzle[x]);
|
||||
setStore({
|
||||
pinnedCount: puzzleStart.length + puzzleMid.length,
|
||||
selected: [],
|
||||
puzzle: [...puzzleStart, ...puzzleMid, ...puzzleEnd],
|
||||
});
|
||||
};
|
||||
function setSolution(id: number, mistakes: number) {
|
||||
setStore("solutions", id, {
|
||||
id,
|
||||
mistakes
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
connections,
|
||||
store,
|
||||
setStore,
|
||||
handleGuess,
|
||||
handlePinUnpin,
|
||||
handleSelectGame,
|
||||
handleShuffle,
|
||||
handleDeselect,
|
||||
getFromPuzzle,
|
||||
setSolution,
|
||||
};
|
||||
}
|
||||
|
|
145
src/usePuzzleModel.ts
Normal file
145
src/usePuzzleModel.ts
Normal file
|
@ -0,0 +1,145 @@
|
|||
import { Accessor, createEffect } from "solid-js";
|
||||
import connections from "./assets/connections.json";
|
||||
import { shuffleArray } from "./utils";
|
||||
import { createStore } from "solid-js/store";
|
||||
|
||||
type Connection = (typeof connections)[number];
|
||||
type Answer = Connection["answers"][number];
|
||||
|
||||
function fromIndex(index: number): [number, number] {
|
||||
const col = index % 4;
|
||||
const row = (index - col) / 4;
|
||||
return [row, col];
|
||||
}
|
||||
|
||||
type AppStore = {
|
||||
pinnedCount: number;
|
||||
selected: number[];
|
||||
solvedGroups: Answer[];
|
||||
puzzle: number[];
|
||||
};
|
||||
|
||||
export default function usePuzzleModel(id: Accessor<number>) {
|
||||
const [store, setStore] = createStore<AppStore>({
|
||||
pinnedCount: 0,
|
||||
selected: [],
|
||||
solvedGroups: [],
|
||||
puzzle: shuffleArray(Array.from({ length: 16 }, (_, i) => i)),
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
id()
|
||||
setStore({
|
||||
pinnedCount: 0,
|
||||
selected: [],
|
||||
solvedGroups: [],
|
||||
puzzle: shuffleArray(Array.from({ length: 16 }, (_, i) => i)),
|
||||
});
|
||||
})
|
||||
|
||||
const answers = (): Answer[] => {
|
||||
return connections.find((x) => x.id === id())!.answers;
|
||||
};
|
||||
|
||||
const getFromPuzzle = (index: number) => {
|
||||
const puzzleIndex = store.puzzle[index];
|
||||
const [groupIndex, memberIndex] = fromIndex(puzzleIndex);
|
||||
const group = answers()[groupIndex];
|
||||
return {
|
||||
group: group.group,
|
||||
level: group.level,
|
||||
answer: answers()[groupIndex].members[memberIndex],
|
||||
};
|
||||
};
|
||||
|
||||
const handleGuess = () => {
|
||||
const selected = store.puzzle.length === 4 ? [0, 1, 2, 3] : store.selected;
|
||||
const selectedAnswers = selected.map((x) => getFromPuzzle(x));
|
||||
const { level } = selectedAnswers[0];
|
||||
const isCorrect = selectedAnswers.every((x) => x.level === level);
|
||||
if (!isCorrect) {
|
||||
// TODO you got it wrong
|
||||
alert("wrong");
|
||||
return;
|
||||
}
|
||||
const selectedPinnedCount = selected.reduce(
|
||||
(acc, cur) => acc + (cur < store.pinnedCount ? 1 : 0),
|
||||
0
|
||||
);
|
||||
setStore({
|
||||
pinnedCount: store.pinnedCount - selectedPinnedCount,
|
||||
puzzle: store.puzzle.filter((x) =>
|
||||
selected.every((s) => store.puzzle[s] !== x)
|
||||
),
|
||||
selected: [],
|
||||
});
|
||||
const newSolvedGroup = answers().find((x) => x.level === level);
|
||||
if (newSolvedGroup != null) {
|
||||
setStore({
|
||||
solvedGroups: [...store.solvedGroups, newSolvedGroup],
|
||||
});
|
||||
}
|
||||
if (store.puzzle.length === 0) {
|
||||
// completely solved!
|
||||
}
|
||||
};
|
||||
|
||||
const handleShuffle = () => {
|
||||
const pinned = store.puzzle.slice(0, store.pinnedCount);
|
||||
const toShuffle = store.puzzle.slice(store.pinnedCount);
|
||||
setStore({
|
||||
puzzle: [...pinned, ...shuffleArray(toShuffle)],
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeselect = () => {
|
||||
setStore({
|
||||
selected: [],
|
||||
});
|
||||
};
|
||||
|
||||
const handlePinUnpin = () => {
|
||||
if (store.selected.every((x) => x < store.pinnedCount)) {
|
||||
// we are unpinning
|
||||
const puzzleStart = Array.from({ length: store.pinnedCount }, (_, i) => i)
|
||||
.filter((x) => !store.selected.includes(x))
|
||||
.map((x) => store.puzzle[x]);
|
||||
const puzzleMiddle = store.selected.map((x) => store.puzzle[x]);
|
||||
const puzzleEnd = store.puzzle.slice(store.pinnedCount);
|
||||
const newPuzzle = [...puzzleStart, ...puzzleMiddle, ...puzzleEnd];
|
||||
setStore({
|
||||
pinnedCount: store.pinnedCount - store.selected.length,
|
||||
selected: [],
|
||||
puzzle: newPuzzle,
|
||||
});
|
||||
return;
|
||||
}
|
||||
// we are pinning
|
||||
const puzzleStart = store.puzzle.slice(0, store.pinnedCount);
|
||||
const puzzleMid = store.selected
|
||||
.filter((x) => x >= store.pinnedCount)
|
||||
.map((x) => store.puzzle[x]);
|
||||
const puzzleEnd = Array.from(
|
||||
{ length: store.puzzle.length - store.pinnedCount },
|
||||
(_, i) => i + store.pinnedCount
|
||||
)
|
||||
.filter((x) => !store.selected.includes(x))
|
||||
.map((x) => store.puzzle[x]);
|
||||
setStore({
|
||||
pinnedCount: puzzleStart.length + puzzleMid.length,
|
||||
selected: [],
|
||||
puzzle: [...puzzleStart, ...puzzleMid, ...puzzleEnd],
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
connections,
|
||||
store,
|
||||
setStore,
|
||||
handleGuess,
|
||||
handlePinUnpin,
|
||||
handleShuffle,
|
||||
handleDeselect,
|
||||
getFromPuzzle,
|
||||
};
|
||||
}
|
|
@ -1,12 +1,13 @@
|
|||
import { defineConfig } from "vite";
|
||||
import solid from "vite-plugin-solid";
|
||||
import vitePluginInlineCSSModules from 'vite-plugin-inline-css-modules'
|
||||
|
||||
// @ts-expect-error process is a nodejs global
|
||||
const host = process.env.TAURI_DEV_HOST;
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig(async () => ({
|
||||
plugins: [solid()],
|
||||
plugins: [solid(), vitePluginInlineCSSModules()],
|
||||
|
||||
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
|
||||
//
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue