basic usability complete

This commit is contained in:
Joshua Seigler 2025-04-18 13:35:30 -04:00
parent 1e71255371
commit a416f66a56
8 changed files with 118 additions and 45 deletions

10
package-lock.json generated
View file

@ -14,6 +14,7 @@
"@tauri-apps/api": "^2", "@tauri-apps/api": "^2",
"@tauri-apps/plugin-opener": "^2", "@tauri-apps/plugin-opener": "^2",
"@tauri-apps/plugin-store": "^2.2.0", "@tauri-apps/plugin-store": "^2.2.0",
"solid-icons": "^1.1.0",
"solid-js": "^1.9.3", "solid-js": "^1.9.3",
"vite-plugin-inline-css-modules": "^0.0.8" "vite-plugin-inline-css-modules": "^0.0.8"
}, },
@ -1803,6 +1804,15 @@
"seroval": "^1.0" "seroval": "^1.0"
} }
}, },
"node_modules/solid-icons": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/solid-icons/-/solid-icons-1.1.0.tgz",
"integrity": "sha512-IesTfr/F1ElVwH2E1110s2RPXH4pujKfSs+koT8rwuTAdleO5s26lNSpqJV7D1+QHooJj18mcOiz2PIKs0ic+A==",
"license": "MIT",
"peerDependencies": {
"solid-js": "*"
}
},
"node_modules/solid-js": { "node_modules/solid-js": {
"version": "1.9.5", "version": "1.9.5",
"resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.5.tgz", "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.5.tgz",

View file

@ -17,6 +17,7 @@
"@tauri-apps/api": "^2", "@tauri-apps/api": "^2",
"@tauri-apps/plugin-opener": "^2", "@tauri-apps/plugin-opener": "^2",
"@tauri-apps/plugin-store": "^2.2.0", "@tauri-apps/plugin-store": "^2.2.0",
"solid-icons": "^1.1.0",
"solid-js": "^1.9.3", "solid-js": "^1.9.3",
"vite-plugin-inline-css-modules": "^0.0.8" "vite-plugin-inline-css-modules": "^0.0.8"
}, },

View file

@ -83,9 +83,12 @@
.puzzle-header { .puzzle-header {
display: flex; display: flex;
align-items: center; align-items: baseline;
flex-direction: row; flex-direction: row;
> h1 { margin-bottom: 1em;
> h2 {
font-size: 2em;
line-height: 1;
flex-grow: 1; flex-grow: 1;
text-align: left; text-align: left;
margin: 0; margin: 0;
@ -96,6 +99,10 @@
display: flex; display: flex;
flex-direction: row; flex-direction: row;
gap: calc(1 * var(--unit)); gap: calc(1 * var(--unit));
& button {
display: flex;
align-items: center;
}
} }
.puzzle-row { .puzzle-row {

View file

@ -1,36 +1,76 @@
import { For } from "solid-js"; import { For, Show, createMemo } from "solid-js";
import useAppModel from "./useAppModel"; import useAppModel from "./useAppModel";
import { A } from "@solidjs/router";
import { css } from 'vite-plugin-inline-css-modules' import { css } from 'vite-plugin-inline-css-modules'
const styles = css` const styles = css`
calendar: { .calendar {
display: "grid", column-width: 7em;
gridTemplateColumns: "repeat(7, 1fr)", column-gap: 0.3em;
}, }
entry: (solved: boolean) => { .entry {
return { width: 0.8em;
borderRadius: "50%", margin: 0.1em;
backgroundColor: solved ? "green" : "gray", display: inline-block;
height: 0.8em;
border-radius: 25%;
background-color: gray;
}
.entryBlank {
background: none;
}
.nextPuzzle {
display: flex;
justify-content: center;
width: 100%;
font-size: 3em;
line-height: 1;
padding: 1em;
color: var(--color-foreground);
background-color: var(--group-green);
margin-bottom: 1em;
&:hover, &:focus-visible, &:active {
color: var(--color-foreground);
outline: none;
background-color: var(--group-yellow);
} }
}, }
` `
const colorStrings = [
'var(--group-purple)',
'var(--group-blue)',
'var(--group-green)',
'var(--group-yellow)',
]
export default function Dashboard() { export default function Dashboard() {
const { connections, store } = useAppModel(); const { connections, store } = useAppModel();
const nextUnsolvedId = createMemo(() => {
return connections.find(x => store.solutions[x.id] === undefined)?.id
})
return ( return (
<div class={styles.calendar}> <div>
<For each={connections}> <Show when={nextUnsolvedId() !== undefined}>
{(item) => { <a class={styles.nextPuzzle} href={`/puzzle/${nextUnsolvedId()}`}>Next puzzle: #{nextUnsolvedId()}</a>
const isSolved = store.solutions[item.id] !== undefined; </Show>
return ( <div class={styles.calendar}>
<A <div class={`${styles.entry} ${styles.entryBlank}`}></div>
href={`/puzzle/${item.id}`} <For each={connections}>
{...stylex.props(styles.entry(isSolved))} {(item) => {
/> const isSolved = store.solutions[item.id] !== undefined;
); return (
}} <a
</For> href={`/puzzle/${item.id}`}
class={styles.entry}
style={isSolved ? {
"background-color": colorStrings[store.solutions[item.id].guesses - 4] ?? 'var(--color-foreground)'
} : {}}
title={`#${item.id}, ${item.date}, ${store.solutions[item.id]?.guesses}`}
/>
);
}}
</For>
</div>
</div> </div>
); );
} }

View file

@ -2,17 +2,16 @@ import { createEffect, createSignal, type Accessor } from "solid-js";
export default function FitText(props: { body: Accessor<string> }) { export default function FitText(props: { body: Accessor<string> }) {
let textRef: SVGTextElement | undefined; let textRef: SVGTextElement | undefined;
const [width, setWidth] = createSignal(100); const [viewBox, setViewbox] = createSignal<string | undefined>()
const [height, setHeight] = createSignal(100);
createEffect(() => { createEffect(async () => {
props.body(); props.body();
await Promise.resolve()
const bounds = textRef?.getBBox(); const bounds = textRef?.getBBox();
if (bounds === undefined) { if (bounds === undefined) {
return; return;
} }
setWidth(bounds.width); setViewbox(`0 0 ${bounds.width} ${bounds.height}`)
setHeight(bounds.height);
}); });
return ( return (
@ -21,15 +20,14 @@ export default function FitText(props: { body: Accessor<string> }) {
width: "100%", width: "100%",
"max-height": "40%", "max-height": "40%",
}} }}
viewBox={`0 0 ${width()} ${height()}`} viewBox={viewBox()}
overflow="visible"
fill="currentcolor" fill="currentcolor"
> >
<text <text
ref={textRef} ref={textRef}
x="50%" x="50%"
y="50%" y="50%"
dominant-baseline="central" dominant-baseline="middle"
text-anchor="middle" text-anchor="middle"
font-size="30px" font-size="30px"
font-family="inherit" font-family="inherit"

View file

@ -3,6 +3,7 @@ import "./App.css";
import usePuzzleModel from "./usePuzzleModel"; import usePuzzleModel from "./usePuzzleModel";
import FitText from "./FitText"; import FitText from "./FitText";
import { useNavigate, useParams } from "@solidjs/router"; import { useNavigate, useParams } from "@solidjs/router";
import { TbArrowLeft, TbArrowRight, TbArrowUp } from 'solid-icons/tb'
// TODO // TODO
// add routing // add routing
@ -32,7 +33,7 @@ function Puzzle() {
<main class="container"> <main class="container">
<div class="puzzle"> <div class="puzzle">
<header class="puzzle-header"> <header class="puzzle-header">
<h1>Slick Connections</h1> <h2>#{id()}</h2>
<div class="puzzle-header-actions"> <div class="puzzle-header-actions">
<button <button
type="button" type="button"
@ -41,7 +42,15 @@ function Puzzle() {
}} }}
disabled={id() === 1} disabled={id() === 1}
> >
- <TbArrowLeft />
</button>
<button
type="button"
on:click={() => {
navigate('/');
}}
>
<TbArrowUp />
</button> </button>
<button <button
type="button" type="button"
@ -50,7 +59,7 @@ function Puzzle() {
}} }}
disabled={id() === connections.length - 1} disabled={id() === connections.length - 1}
> >
+ <TbArrowRight />
</button> </button>
</div> </div>
</header> </header>

View file

@ -5,7 +5,7 @@ import { tauriStorage } from "@solid-primitives/storage/tauri";
type Solution = { type Solution = {
id: number id: number
mistakes: number guesses: number
} }
type AppStore = { type AppStore = {
@ -24,10 +24,10 @@ export default function useAppModel() {
storage, storage,
} }
); );
function setSolution(id: number, mistakes: number) { function setSolution(id: number, guesses: number) {
setStore("solutions", id, { setStore("solutions", id, {
id, id,
mistakes guesses
}) })
} }

View file

@ -2,6 +2,7 @@ import { Accessor, createEffect } from "solid-js";
import connections from "./assets/connections.json"; import connections from "./assets/connections.json";
import { shuffleArray } from "./utils"; import { shuffleArray } from "./utils";
import { createStore } from "solid-js/store"; import { createStore } from "solid-js/store";
import useAppModel from "./useAppModel";
type Connection = (typeof connections)[number]; type Connection = (typeof connections)[number];
type Answer = Connection["answers"][number]; type Answer = Connection["answers"][number];
@ -12,7 +13,8 @@ function fromIndex(index: number): [number, number] {
return [row, col]; return [row, col];
} }
type AppStore = { type PuzzleStore = {
guesses: number;
pinnedCount: number; pinnedCount: number;
selected: number[]; selected: number[];
solvedGroups: Answer[]; solvedGroups: Answer[];
@ -20,16 +22,22 @@ type AppStore = {
}; };
export default function usePuzzleModel(id: Accessor<number>) { export default function usePuzzleModel(id: Accessor<number>) {
const [store, setStore] = createStore<AppStore>({ const [store, setStore] = createStore<PuzzleStore>({
guesses: 0,
pinnedCount: 0, pinnedCount: 0,
selected: [], selected: [],
solvedGroups: [], solvedGroups: [],
puzzle: shuffleArray(Array.from({ length: 16 }, (_, i) => i)), puzzle: [],
}); });
const {
setSolution
} = useAppModel()
createEffect(() => { createEffect(() => {
id() id()
setStore({ setStore({
guesses: 0,
pinnedCount: 0, pinnedCount: 0,
selected: [], selected: [],
solvedGroups: [], solvedGroups: [],
@ -53,6 +61,7 @@ export default function usePuzzleModel(id: Accessor<number>) {
}; };
const handleGuess = () => { const handleGuess = () => {
setStore('guesses', x => x+1)
const selected = store.puzzle.length === 4 ? [0, 1, 2, 3] : store.selected; const selected = store.puzzle.length === 4 ? [0, 1, 2, 3] : store.selected;
const selectedAnswers = selected.map((x) => getFromPuzzle(x)); const selectedAnswers = selected.map((x) => getFromPuzzle(x));
const { level } = selectedAnswers[0]; const { level } = selectedAnswers[0];
@ -75,12 +84,11 @@ export default function usePuzzleModel(id: Accessor<number>) {
}); });
const newSolvedGroup = answers().find((x) => x.level === level); const newSolvedGroup = answers().find((x) => x.level === level);
if (newSolvedGroup != null) { if (newSolvedGroup != null) {
setStore({ setStore('solvedGroups', x => x.concat(newSolvedGroup))
solvedGroups: [...store.solvedGroups, newSolvedGroup],
});
} }
if (store.puzzle.length === 0) { if (store.puzzle.length === 0) {
// completely solved! // completely solved!
setSolution(id(), store.guesses)
} }
}; };