basic usability complete
This commit is contained in:
parent
1e71255371
commit
a416f66a56
8 changed files with 118 additions and 45 deletions
10
package-lock.json
generated
10
package-lock.json
generated
|
@ -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",
|
||||||
|
|
|
@ -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"
|
||||||
},
|
},
|
||||||
|
|
11
src/App.css
11
src/App.css
|
@ -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 {
|
||||||
|
|
|
@ -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>
|
||||||
|
<Show when={nextUnsolvedId() !== undefined}>
|
||||||
|
<a class={styles.nextPuzzle} href={`/puzzle/${nextUnsolvedId()}`}>Next puzzle: #{nextUnsolvedId()}</a>
|
||||||
|
</Show>
|
||||||
<div class={styles.calendar}>
|
<div class={styles.calendar}>
|
||||||
|
<div class={`${styles.entry} ${styles.entryBlank}`}></div>
|
||||||
<For each={connections}>
|
<For each={connections}>
|
||||||
{(item) => {
|
{(item) => {
|
||||||
const isSolved = store.solutions[item.id] !== undefined;
|
const isSolved = store.solutions[item.id] !== undefined;
|
||||||
return (
|
return (
|
||||||
<A
|
<a
|
||||||
href={`/puzzle/${item.id}`}
|
href={`/puzzle/${item.id}`}
|
||||||
{...stylex.props(styles.entry(isSolved))}
|
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>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue