Skip to content

Commit aad096f

Browse files
authored
Merge pull request #34 from ChicoState/component-menu
Save, load & widget settings
2 parents c175d22 + 73016bd commit aad096f

File tree

17 files changed

+675
-239
lines changed

17 files changed

+675
-239
lines changed

package-lock.json

Lines changed: 19 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+
"nanoid": "^5.1.6",
1415
"openmeteo": "^1.2.1",
1516
"react": "^19.1.1",
1617
"react-dom": "^19.1.1",

src/App.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ body {
8080
gap: 4px;
8181

8282
padding: 0 12px;
83-
width: min-content;
83+
width: fit-content;
8484

8585
height: 24px;
8686
color: #fffa;

src/App.tsx

Lines changed: 140 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -1,148 +1,153 @@
1-
import React, { useState } from "react";
2-
import Widget from "./Widget";
1+
import React, { createContext, useEffect, useState } from "react";
2+
import Widget, { WidgetState } from "./Widget";
3+
import WidgetMap from "./WidgetMap";
4+
import Header from "./Header";
35
import Menu from "./Menu";
46
import { Grid } from "./Grid";
5-
import { Weather } from "./widgets/Weather";
6-
import { Clock } from "./widgets/Clock";
7-
import { ToDoList } from "./widgets/ToDoList";
8-
import { Notepad } from "./widgets/Notepad";
9-
import { Search } from "./widgets/Search";
10-
import { Shortcut } from "./widgets/Shortcut";
7+
import { nanoid } from "nanoid";
118
import styles from "./App.css";
12-
import BatteryWidget from "./widgets/BatteryWidget";
13-
import {
14-
PencilSimpleIcon,
15-
CheckIcon,
16-
XIcon,
17-
ListIcon,
18-
} from "@phosphor-icons/react";
19-
20-
const TestBox = () => {
21-
return <div className={styles.container}></div>;
22-
};
9+
10+
interface AppContextType {
11+
widgets: WidgetState<any>[];
12+
editing: boolean;
13+
deleting: boolean;
14+
menuOpen: boolean;
15+
16+
setWidgets: React.Dispatch<React.SetStateAction<WidgetState<any>[]>>;
17+
setEditing: React.Dispatch<React.SetStateAction<boolean>>;
18+
setDeleting: React.Dispatch<React.SetStateAction<boolean>>;
19+
setMenuOpen: React.Dispatch<React.SetStateAction<boolean>>;
20+
21+
saveTemplate: () => void;
22+
loadTemplate: () => boolean;
23+
24+
addWidget: (type: string) => WidgetState<any>;
25+
removeWidget: (id: string) => void;
26+
}
27+
28+
export const AppContext = createContext<AppContextType>(null);
29+
30+
const FallbackTemplate: WidgetState<any>[] = [
31+
{
32+
id: nanoid(6),
33+
type: "clock",
34+
size: { ...WidgetMap["clock"].size },
35+
position: { gridX: 10, gridY: 1 },
36+
settings: { ...WidgetMap["clock"].settings },
37+
},
38+
{
39+
id: nanoid(6),
40+
type: "search",
41+
size: { ...WidgetMap["search"].size },
42+
position: { gridX: 8, gridY: 3 },
43+
settings: { ...WidgetMap["search"].settings },
44+
},
45+
];
2346

2447
const App = () => {
2548
const [editing, setEditing] = useState(false);
49+
const [deleting, setDeleting] = useState(false);
2650
const [menuOpen, setMenuOpen] = useState(false);
51+
const [widgets, setWidgets] = useState<WidgetState<any>[]>([]);
52+
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.
57+
58+
function saveTemplate() {
59+
localStorage.setItem("template", JSON.stringify(widgets));
60+
}
61+
62+
function loadTemplate(): boolean {
63+
const template = localStorage.getItem("template");
64+
65+
if (template === null) {
66+
console.warn("No template stored");
67+
return false;
68+
}
69+
70+
setWidgets(JSON.parse(template) as WidgetState<any>[]);
71+
return true;
72+
}
73+
74+
function addWidget(type: string) {
75+
if (!Object.keys(WidgetMap).includes(type)) {
76+
console.error("Invalid widget type");
77+
return;
78+
}
79+
80+
const widget: WidgetState<any> = {
81+
id: nanoid(6),
82+
type: type,
83+
size: { ...WidgetMap[type].size },
84+
position: { gridX: 0, gridY: 0 },
85+
settings: { ...WidgetMap[type].settings },
86+
};
87+
88+
setWidgets([...widgets, widget]);
89+
return widget;
90+
}
91+
92+
function removeWidget(id: string) {
93+
setWidgets(widgets.filter((w) => w.id !== id));
94+
}
95+
96+
useEffect(() => {
97+
if (!loadTemplate()) {
98+
setWidgets(FallbackTemplate);
99+
}
100+
}, []);
27101

28102
return (
29-
<div className={styles.content}>
30-
<div className={styles.header}>
31-
<div className={styles.row}>
32-
{editing && (
33-
<button
34-
className={[styles.container, styles.button].join(" ")}
35-
onClick={() => {
36-
setEditing(!editing);
37-
}}
38-
>
39-
<XIcon weight="bold"></XIcon>
40-
Cancel
41-
</button>
42-
)}
43-
44-
<button
45-
className={[styles.container, styles.button].join(" ")}
46-
onClick={() => {
47-
setEditing(!editing);
48-
}}
49-
>
50-
{editing && <CheckIcon weight="bold"></CheckIcon>}
51-
{!editing && <PencilSimpleIcon weight="bold"></PencilSimpleIcon>}
52-
{editing ? "Done" : "Edit"}
53-
</button>
54-
55-
<button
56-
className={[styles.container, styles.button].join(" ")}
57-
onClick={() => {
58-
setMenuOpen(!menuOpen);
59-
}}
60-
>
61-
<ListIcon weight="bold"></ListIcon>
62-
Settings
63-
</button>
64-
</div>
65-
</div>
66-
67-
<Grid width={24} height={12} editing={editing}>
68-
<Widget
69-
size={{ width: 4, height: 2 }}
70-
position={{ gridX: 10, gridY: 1 }}
71-
>
72-
<Clock></Clock>
73-
</Widget>
74-
75-
<Widget
76-
size={{ width: 5, height: 5 }}
77-
position={{ gridX: 18, gridY: 1 }}
78-
>
79-
<ToDoList></ToDoList>
80-
</Widget>
81-
82-
<Widget
83-
size={{ width: 5, height: 5 }}
84-
position={{ gridX: 1, gridY: 1 }}
85-
>
86-
<Notepad></Notepad>
87-
</Widget>
88-
89-
<Widget
90-
size={{ width: 8, height: 1 }}
91-
position={{ gridX: 8, gridY: 3 }}
92-
>
93-
<Search></Search>
94-
</Widget>
95-
96-
<Widget
97-
size={{ width: 6, height: 1 }}
98-
position={{ gridX: 9, gridY: 4 }}
99-
>
100-
<Weather></Weather>
101-
</Widget>
102-
103-
<Widget
104-
size={{ width: 1, height: 1 }}
105-
position={{ gridX: 10, gridY: 5 }}
106-
resizeable={false}
107-
>
108-
<Shortcut url="https://canvas.csuchico.edu"></Shortcut>
109-
</Widget>
110-
111-
<Widget
112-
size={{ width: 1, height: 1 }}
113-
position={{ gridX: 11, gridY: 5 }}
114-
resizeable={false}
115-
>
116-
<Shortcut url="https://outlook.com"></Shortcut>
117-
</Widget>
118-
119-
<Widget
120-
size={{ width: 1, height: 1 }}
121-
position={{ gridX: 12, gridY: 5 }}
122-
resizeable={false}
123-
>
124-
<Shortcut url="https://github.com"></Shortcut>
125-
</Widget>
126-
127-
<Widget
128-
size={{ width: 1, height: 1 }}
129-
position={{ gridX: 13, gridY: 5 }}
130-
resizeable={false}
131-
>
132-
<Shortcut url="https://stackoverflow.com"></Shortcut>
133-
</Widget>
134-
135-
<Widget
136-
size={{ width: 2, height: 1 }}
137-
position={{ gridX: 20, gridY: 0 }}
138-
>
139-
<BatteryWidget />
140-
</Widget>
141-
</Grid>
142-
143-
<Menu active={menuOpen}></Menu>
103+
<div
104+
className={styles.content}
105+
onClick={(e) => {
106+
if (e.target === e.currentTarget) setMenuOpen(false);
107+
}}
108+
>
109+
<AppContext.Provider
110+
value={{
111+
widgets,
112+
editing,
113+
deleting,
114+
menuOpen,
115+
116+
setWidgets,
117+
setEditing,
118+
setDeleting,
119+
setMenuOpen,
120+
121+
saveTemplate,
122+
loadTemplate,
123+
124+
addWidget,
125+
removeWidget,
126+
}}
127+
>
128+
<Header></Header>
129+
<Grid width={24} height={12}>
130+
{widgets.map((state) => {
131+
const map = WidgetMap[state.type];
132+
const Component = map.component;
133+
return (
134+
<Widget
135+
size={state.size || map.size}
136+
position={state.position}
137+
resizeable={map.resizable}
138+
id={state.id}
139+
key={state.id}
140+
>
141+
<Component {...state}></Component>
142+
</Widget>
143+
);
144+
})}
145+
</Grid>
146+
147+
<Menu active={menuOpen}></Menu>
148+
</AppContext.Provider>
144149
</div>
145150
);
146151
};
147152

148-
export default App;
153+
export default App;

src/Drag.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ const DragContext = createContext<DragContextType>({
2020
dragStart: { x: 0, y: 0 },
2121
});
2222

23-
export function Draggable({
23+
export default function Draggable({
2424
enabled = true,
2525
onDrag = () => {},
2626
onDragRelative = () => {},
@@ -36,7 +36,7 @@ export function Draggable({
3636

3737
const [position, setPosition] = useState({ x: 0, y: 0 });
3838

39-
function handleMouseDown(e: React.MouseEvent) {
39+
function handleMouseDown(e: React.MouseEvent | MouseEvent) {
4040
if (!enabled || ctx.isDragging) return;
4141
e.preventDefault();
4242

@@ -50,7 +50,7 @@ export function Draggable({
5050
onDragRelative(0, 0, true);
5151
}
5252

53-
function handleMouseUp(e: React.MouseEvent) {
53+
function handleMouseUp(e: React.MouseEvent | MouseEvent) {
5454
if (!ctx.isDragging || ctx.activeItem !== ref.current) return;
5555
e.preventDefault();
5656

@@ -61,7 +61,7 @@ export function Draggable({
6161
onDragRelative(0, 0, false);
6262
}
6363

64-
function handleMouseMove(e: React.MouseEvent) {
64+
function handleMouseMove(e: React.MouseEvent | MouseEvent) {
6565
if (!ctx.isDragging || ctx.activeItem !== ref.current) return;
6666
e.preventDefault();
6767
setPosition({ x: position.x + e.movementX, y: position.y + e.movementY });

0 commit comments

Comments
 (0)