Skip to content

Commit 0a6b6c7

Browse files
authored
Merge pull request #37 from ChicoState/component-menu
Multiple Template support
2 parents 91cd801 + d44ad21 commit 0a6b6c7

File tree

8 files changed

+294
-60
lines changed

8 files changed

+294
-60
lines changed

package-lock.json

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"dependencies": {
1212
"@phosphor-icons/react": "^2.1.10",
1313
"@rsbuild/plugin-typed-css-modules": "^1.1.1",
14+
"html-to-image": "^1.11.13",
1415
"nanoid": "^5.1.6",
1516
"openmeteo": "^1.2.1",
1617
"react": "^19.1.1",

src/App.tsx

Lines changed: 76 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,39 @@
1-
import React, { createContext, useEffect, useState } from "react";
1+
import React, { createContext, useEffect, useRef, useState } from "react";
22
import Widget, { WidgetState } from "./Widget";
33
import WidgetMap from "./WidgetMap";
44
import Header from "./Header";
55
import Menu from "./Menu";
66
import { Grid } from "./Grid";
77
import { nanoid } from "nanoid";
8+
import { toPng } from "html-to-image";
89
import styles from "./App.css";
910

11+
export interface Template {
12+
name: string;
13+
image: string;
14+
widgets: WidgetState<any>[];
15+
// settings:
16+
// theme:
17+
}
18+
1019
interface AppContextType {
1120
widgets: WidgetState<any>[];
21+
templates: Template[];
22+
activeTemplate: number;
1223
editing: boolean;
1324
deleting: boolean;
25+
hidden: boolean;
1426
menuOpen: boolean;
1527

1628
setWidgets: React.Dispatch<React.SetStateAction<WidgetState<any>[]>>;
29+
setTemplates: React.Dispatch<React.SetStateAction<Template[]>>;
1730
setEditing: React.Dispatch<React.SetStateAction<boolean>>;
1831
setDeleting: React.Dispatch<React.SetStateAction<boolean>>;
32+
setHidden: React.Dispatch<React.SetStateAction<boolean>>;
1933
setMenuOpen: React.Dispatch<React.SetStateAction<boolean>>;
2034

21-
saveTemplate: () => void;
22-
loadTemplate: () => boolean;
35+
saveTemplate: (name?: string) => void;
36+
loadTemplate: (index?: number) => void;
2337

2438
addWidget: (type: string) => WidgetState<any>;
2539
removeWidget: (id: string) => void;
@@ -47,28 +61,67 @@ const FallbackTemplate: WidgetState<any>[] = [
4761
const App = () => {
4862
const [editing, setEditing] = useState(false);
4963
const [deleting, setDeleting] = useState(false);
64+
const [hidden, setHidden] = useState(false);
5065
const [menuOpen, setMenuOpen] = useState(false);
5166
const [widgets, setWidgets] = useState<WidgetState<any>[]>([]);
67+
const [templates, setTemplates] = useState<Template[]>([]);
68+
const [activeTemplate, setActiveTemplate] = useState(0);
69+
70+
const gridRef = useRef(null);
71+
72+
async function saveTemplate(name?: string) {
73+
const template = {
74+
name: name ?? templates[activeTemplate]?.name ?? "Default",
75+
image: await toPng(gridRef.current, {
76+
canvasWidth: 240,
77+
canvasHeight: 135,
78+
}),
79+
widgets: structuredClone(widgets),
80+
};
5281

53-
// TODO: This system will eventually keep an
54-
// array of templates and store the index of
55-
// the active template. For the time being,
56-
// this uses a single template structure.
82+
if (name === null || name === undefined) {
83+
const _templates = [...templates];
84+
_templates[activeTemplate] = template;
85+
setTemplates([..._templates]);
86+
} else {
87+
setTemplates([...templates, template]);
88+
setActiveTemplate(templates.length);
89+
}
5790

58-
function saveTemplate() {
59-
localStorage.setItem("template", JSON.stringify(widgets));
91+
writeTemplates();
6092
}
6193

62-
function loadTemplate(): boolean {
63-
const template = localStorage.getItem("template");
94+
function loadTemplate(index?: number) {
95+
const i = index ?? activeTemplate;
96+
if (i >= templates.length) return;
97+
setWidgets(templates[i].widgets);
98+
setActiveTemplate(i);
99+
localStorage.setItem("activeTemplate", JSON.stringify(i));
100+
}
64101

65-
if (template === null) {
66-
console.warn("No template stored");
67-
return false;
102+
function readTemplates() {
103+
const _templates: Template[] =
104+
JSON.parse(localStorage.getItem("templates")) ?? [];
105+
const _activeTemplate: number =
106+
JSON.parse(localStorage.getItem("activeTemplate")) ?? 0;
107+
108+
if (_templates.length === 0) {
109+
_templates.push({
110+
name: "Default",
111+
image: null,
112+
widgets: structuredClone(FallbackTemplate),
113+
});
68114
}
69115

70-
setWidgets(JSON.parse(template) as WidgetState<any>[]);
71-
return true;
116+
setTemplates(_templates);
117+
setActiveTemplate(_activeTemplate);
118+
119+
setWidgets(_templates[_activeTemplate].widgets);
120+
}
121+
122+
function writeTemplates() {
123+
localStorage.setItem("templates", JSON.stringify(templates));
124+
localStorage.setItem("activeTemplate", JSON.stringify(activeTemplate));
72125
}
73126

74127
function addWidget(type: string) {
@@ -94,9 +147,7 @@ const App = () => {
94147
}
95148

96149
useEffect(() => {
97-
if (!loadTemplate()) {
98-
setWidgets(FallbackTemplate);
99-
}
150+
readTemplates();
100151
}, []);
101152

102153
return (
@@ -109,13 +160,18 @@ const App = () => {
109160
<AppContext.Provider
110161
value={{
111162
widgets,
163+
templates,
164+
activeTemplate,
112165
editing,
113166
deleting,
167+
hidden,
114168
menuOpen,
115169

116170
setWidgets,
171+
setTemplates,
117172
setEditing,
118173
setDeleting,
174+
setHidden,
119175
setMenuOpen,
120176

121177
saveTemplate,
@@ -126,7 +182,7 @@ const App = () => {
126182
}}
127183
>
128184
<Header></Header>
129-
<Grid width={24} height={12}>
185+
<Grid width={24} height={12} ref={gridRef}>
130186
{widgets.map((state) => {
131187
const map = WidgetMap[state.type];
132188
const Component = map.component;

src/Grid.css

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,17 @@
88
position: relative;
99
width: 100%;
1010
height: 100%;
11+
opacity: 1;
12+
transition: opacity 0.25s;
13+
}
14+
15+
.grid.hidden {
16+
opacity: 0;
17+
pointer-events: none;
18+
}
19+
20+
.grid.hidden * {
21+
pointer-events: none;
1122
}
1223

1324
.grid-slots {

src/Grid.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React, {
22
createContext,
3+
RefObject,
34
useContext,
45
useEffect,
56
useMemo,
@@ -100,6 +101,8 @@ export function GridItem({
100101
}
101102

102103
function handleDrag(x: number, y: number, drag: boolean) {
104+
if (deleting) return;
105+
103106
let [gridX, gridY] = pixelToGrid(x, y);
104107
gridX = Math.min(ctx.width - size.width, Math.max(0, gridX));
105108
gridY = Math.min(ctx.height - size.height, Math.max(0, gridY));
@@ -232,14 +235,15 @@ export function Grid({
232235
width = 16,
233236
height = 8,
234237
children,
238+
ref,
235239
}: {
236240
width: number;
237241
height: number;
238242
children?: React.ReactNode;
243+
ref?: React.RefObject<HTMLDivElement>;
239244
}) {
240-
const ref = useRef<HTMLDivElement>(null);
241245
const [cellSize, setCellSize] = useState(0);
242-
const { editing, deleting } = useContext(AppContext);
246+
const { editing, deleting, hidden } = useContext(AppContext);
243247

244248
useEffect(() => {
245249
if (!ref.current) return;
@@ -267,7 +271,10 @@ export function Grid({
267271
}, [width, height]);
268272

269273
return (
270-
<div className={styles.grid} ref={ref}>
274+
<div
275+
className={[styles.grid, hidden ? styles.hidden : ""].join(" ")}
276+
ref={ref}
277+
>
271278
{editing && (
272279
<div
273280
className={styles.gridSlots}

src/Header.tsx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useContext } from "react";
1+
import React, { useContext, useState } from "react";
22
import { AppContext } from "./App";
33
import styles from "./App.css";
44

@@ -10,6 +10,8 @@ import {
1010
ListIcon,
1111
WrenchIcon,
1212
EraserIcon,
13+
EyeIcon,
14+
EyeClosedIcon,
1315
ArrowsOutCardinalIcon,
1416
} from "@phosphor-icons/react";
1517

@@ -27,12 +29,15 @@ export default function Header() {
2729
const {
2830
editing,
2931
deleting,
32+
hidden,
3033
setEditing,
3134
setDeleting,
35+
setHidden,
3236
menuOpen,
3337
setMenuOpen,
3438
saveTemplate,
3539
loadTemplate,
40+
templates,
3641
} = useContext(AppContext);
3742

3843
return (
@@ -42,6 +47,7 @@ export default function Header() {
4247
<button
4348
className={[styles.container, styles.button].join(" ")}
4449
onClick={() => {
50+
setHidden(false);
4551
setEditing(true);
4652
}}
4753
>
@@ -103,6 +109,20 @@ export default function Header() {
103109
<ListIcon weight="bold"></ListIcon>
104110
Settings
105111
</button>
112+
113+
<button
114+
className={[styles.container, styles.button].join(" ")}
115+
style={{ padding: "6px" }}
116+
onClick={() => {
117+
setHidden(!hidden);
118+
}}
119+
>
120+
{hidden ? (
121+
<EyeClosedIcon weight="bold"></EyeClosedIcon>
122+
) : (
123+
<EyeIcon weight="bold"></EyeIcon>
124+
)}
125+
</button>
106126
</div>
107127
</div>
108128
);

src/Menu.css

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
.menu > span {
3232
color: #fff8;
3333
font-weight: 500;
34+
font-size: 0.9rem;
3435
text-align: center;
3536
}
3637

@@ -76,7 +77,7 @@
7677
.item-name {
7778
color: #fffa;
7879
font-weight: 500;
79-
font-size: 0.8rem;
80+
font-size: 0.9rem;
8081
margin-left: 8px;
8182
}
8283

@@ -123,16 +124,63 @@
123124
}
124125

125126
.widget-thumb {
127+
display: flex;
128+
align-items: center;
129+
justify-content: center;
126130
padding: 8px;
127131
max-height: 96px !important;
128132
zoom: 0.75;
133+
cursor: pointer;
134+
135+
transition:
136+
background-color 0.15s,
137+
border-color 0.15s;
129138
}
130139

131140
.widget-thumb:hover {
141+
background-color: transparent;
132142
border-color: #fff4;
133143
}
134144

135-
.widget-thumb > div > * {
145+
.widget-thumb > * {
136146
pointer-events: none;
137147
user-select: none;
138148
}
149+
150+
.template-item {
151+
display: flex;
152+
flex-flow: column nowrap;
153+
height: min-content;
154+
/*padding: 4px;*/
155+
cursor: pointer;
156+
157+
transition:
158+
background-color 0.15s,
159+
border-color 0.15s;
160+
}
161+
162+
.template-item:hover,
163+
.template-item.active {
164+
background-color: transparent;
165+
border-color: #fff4;
166+
}
167+
168+
.template-item.active span {
169+
font-weight: 500;
170+
color: #fffc;
171+
}
172+
173+
.template-header {
174+
display: flex;
175+
flex-flow: row nowrap;
176+
padding: 4px;
177+
align-items: center;
178+
justify-content: space-between;
179+
height: min-content;
180+
}
181+
182+
.save-button {
183+
font-size: 0.9rem;
184+
margin: 0 auto;
185+
height: 32px;
186+
}

0 commit comments

Comments
 (0)