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/plugin-opener": "^2",
"@tauri-apps/plugin-store": "^2.2.0",
"solid-icons": "^1.1.0",
"solid-js": "^1.9.3",
"vite-plugin-inline-css-modules": "^0.0.8"
},
@ -1803,6 +1804,15 @@
"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": {
"version": "1.9.5",
"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/plugin-opener": "^2",
"@tauri-apps/plugin-store": "^2.2.0",
"solid-icons": "^1.1.0",
"solid-js": "^1.9.3",
"vite-plugin-inline-css-modules": "^0.0.8"
},

View file

@ -83,9 +83,12 @@
.puzzle-header {
display: flex;
align-items: center;
align-items: baseline;
flex-direction: row;
> h1 {
margin-bottom: 1em;
> h2 {
font-size: 2em;
line-height: 1;
flex-grow: 1;
text-align: left;
margin: 0;
@ -96,6 +99,10 @@
display: flex;
flex-direction: row;
gap: calc(1 * var(--unit));
& button {
display: flex;
align-items: center;
}
}
.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 { 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",
.calendar {
column-width: 7em;
column-gap: 0.3em;
}
.entry {
width: 0.8em;
margin: 0.1em;
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() {
const { connections, store } = useAppModel();
const nextUnsolvedId = createMemo(() => {
return connections.find(x => store.solutions[x.id] === undefined)?.id
})
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>
<Show when={nextUnsolvedId() !== undefined}>
<a class={styles.nextPuzzle} href={`/puzzle/${nextUnsolvedId()}`}>Next puzzle: #{nextUnsolvedId()}</a>
</Show>
<div class={styles.calendar}>
<div class={`${styles.entry} ${styles.entryBlank}`}></div>
<For each={connections}>
{(item) => {
const isSolved = store.solutions[item.id] !== undefined;
return (
<a
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>
);
}

View file

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

View file

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

View file

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

View file

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