Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"dependencies": {
"@phosphor-icons/react": "^2.1.10",
"@rsbuild/plugin-typed-css-modules": "^1.1.1",
"html-to-image": "^1.11.13",
"nanoid": "^5.1.6",
"openmeteo": "^1.2.1",
"react": "^19.1.1",
Expand Down
96 changes: 76 additions & 20 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,39 @@
import React, { createContext, useEffect, useState } from "react";
import React, { createContext, useEffect, useRef, useState } from "react";
import Widget, { WidgetState } from "./Widget";
import WidgetMap from "./WidgetMap";
import Header from "./Header";
import Menu from "./Menu";
import { Grid } from "./Grid";
import { nanoid } from "nanoid";
import { toPng } from "html-to-image";
import styles from "./App.css";

export interface Template {
name: string;
image: string;
widgets: WidgetState<any>[];
// settings:
// theme:
}

interface AppContextType {
widgets: WidgetState<any>[];
templates: Template[];
activeTemplate: number;
editing: boolean;
deleting: boolean;
hidden: boolean;
menuOpen: boolean;

setWidgets: React.Dispatch<React.SetStateAction<WidgetState<any>[]>>;
setTemplates: React.Dispatch<React.SetStateAction<Template[]>>;
setEditing: React.Dispatch<React.SetStateAction<boolean>>;
setDeleting: React.Dispatch<React.SetStateAction<boolean>>;
setHidden: React.Dispatch<React.SetStateAction<boolean>>;
setMenuOpen: React.Dispatch<React.SetStateAction<boolean>>;

saveTemplate: () => void;
loadTemplate: () => boolean;
saveTemplate: (name?: string) => void;
loadTemplate: (index?: number) => void;

addWidget: (type: string) => WidgetState<any>;
removeWidget: (id: string) => void;
Expand Down Expand Up @@ -47,28 +61,67 @@ const FallbackTemplate: WidgetState<any>[] = [
const App = () => {
const [editing, setEditing] = useState(false);
const [deleting, setDeleting] = useState(false);
const [hidden, setHidden] = useState(false);
const [menuOpen, setMenuOpen] = useState(false);
const [widgets, setWidgets] = useState<WidgetState<any>[]>([]);
const [templates, setTemplates] = useState<Template[]>([]);
const [activeTemplate, setActiveTemplate] = useState(0);

const gridRef = useRef(null);

async function saveTemplate(name?: string) {
const template = {
name: name ?? templates[activeTemplate]?.name ?? "Default",
image: await toPng(gridRef.current, {
canvasWidth: 240,
canvasHeight: 135,
}),
widgets: structuredClone(widgets),
};

// TODO: This system will eventually keep an
// array of templates and store the index of
// the active template. For the time being,
// this uses a single template structure.
if (name === null || name === undefined) {
const _templates = [...templates];
_templates[activeTemplate] = template;
setTemplates([..._templates]);
} else {
setTemplates([...templates, template]);
setActiveTemplate(templates.length);
}

function saveTemplate() {
localStorage.setItem("template", JSON.stringify(widgets));
writeTemplates();
}

function loadTemplate(): boolean {
const template = localStorage.getItem("template");
function loadTemplate(index?: number) {
const i = index ?? activeTemplate;
if (i >= templates.length) return;
setWidgets(templates[i].widgets);
setActiveTemplate(i);
localStorage.setItem("activeTemplate", JSON.stringify(i));
}

if (template === null) {
console.warn("No template stored");
return false;
function readTemplates() {
const _templates: Template[] =
JSON.parse(localStorage.getItem("templates")) ?? [];
const _activeTemplate: number =
JSON.parse(localStorage.getItem("activeTemplate")) ?? 0;

if (_templates.length === 0) {
_templates.push({
name: "Default",
image: null,
widgets: structuredClone(FallbackTemplate),
});
}

setWidgets(JSON.parse(template) as WidgetState<any>[]);
return true;
setTemplates(_templates);
setActiveTemplate(_activeTemplate);

setWidgets(_templates[_activeTemplate].widgets);
}

function writeTemplates() {
localStorage.setItem("templates", JSON.stringify(templates));
localStorage.setItem("activeTemplate", JSON.stringify(activeTemplate));
}

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

useEffect(() => {
if (!loadTemplate()) {
setWidgets(FallbackTemplate);
}
readTemplates();
}, []);

return (
Expand All @@ -109,13 +160,18 @@ const App = () => {
<AppContext.Provider
value={{
widgets,
templates,
activeTemplate,
editing,
deleting,
hidden,
menuOpen,

setWidgets,
setTemplates,
setEditing,
setDeleting,
setHidden,
setMenuOpen,

saveTemplate,
Expand All @@ -126,7 +182,7 @@ const App = () => {
}}
>
<Header></Header>
<Grid width={24} height={12}>
<Grid width={24} height={12} ref={gridRef}>
{widgets.map((state) => {
const map = WidgetMap[state.type];
const Component = map.component;
Expand Down
11 changes: 11 additions & 0 deletions src/Grid.css
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@
position: relative;
width: 100%;
height: 100%;
opacity: 1;
transition: opacity 0.25s;
}

.grid.hidden {
opacity: 0;
pointer-events: none;
}

.grid.hidden * {
pointer-events: none;
}

.grid-slots {
Expand Down
13 changes: 10 additions & 3 deletions src/Grid.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, {
createContext,
RefObject,
useContext,
useEffect,
useMemo,
Expand Down Expand Up @@ -100,6 +101,8 @@ export function GridItem({
}

function handleDrag(x: number, y: number, drag: boolean) {
if (deleting) return;

let [gridX, gridY] = pixelToGrid(x, y);
gridX = Math.min(ctx.width - size.width, Math.max(0, gridX));
gridY = Math.min(ctx.height - size.height, Math.max(0, gridY));
Expand Down Expand Up @@ -232,14 +235,15 @@ export function Grid({
width = 16,
height = 8,
children,
ref,
}: {
width: number;
height: number;
children?: React.ReactNode;
ref?: React.RefObject<HTMLDivElement>;
}) {
const ref = useRef<HTMLDivElement>(null);
const [cellSize, setCellSize] = useState(0);
const { editing, deleting } = useContext(AppContext);
const { editing, deleting, hidden } = useContext(AppContext);

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

return (
<div className={styles.grid} ref={ref}>
<div
className={[styles.grid, hidden ? styles.hidden : ""].join(" ")}
ref={ref}
>
{editing && (
<div
className={styles.gridSlots}
Expand Down
22 changes: 21 additions & 1 deletion src/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useContext } from "react";
import React, { useContext, useState } from "react";
import { AppContext } from "./App";
import styles from "./App.css";

Expand All @@ -10,6 +10,8 @@ import {
ListIcon,
WrenchIcon,
EraserIcon,
EyeIcon,
EyeClosedIcon,
ArrowsOutCardinalIcon,
} from "@phosphor-icons/react";

Expand All @@ -27,12 +29,15 @@ export default function Header() {
const {
editing,
deleting,
hidden,
setEditing,
setDeleting,
setHidden,
menuOpen,
setMenuOpen,
saveTemplate,
loadTemplate,
templates,
} = useContext(AppContext);

return (
Expand All @@ -42,6 +47,7 @@ export default function Header() {
<button
className={[styles.container, styles.button].join(" ")}
onClick={() => {
setHidden(false);
setEditing(true);
}}
>
Expand Down Expand Up @@ -103,6 +109,20 @@ export default function Header() {
<ListIcon weight="bold"></ListIcon>
Settings
</button>

<button
className={[styles.container, styles.button].join(" ")}
style={{ padding: "6px" }}
onClick={() => {
setHidden(!hidden);
}}
>
{hidden ? (
<EyeClosedIcon weight="bold"></EyeClosedIcon>
) : (
<EyeIcon weight="bold"></EyeIcon>
)}
</button>
</div>
</div>
);
Expand Down
52 changes: 50 additions & 2 deletions src/Menu.css
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
.menu > span {
color: #fff8;
font-weight: 500;
font-size: 0.9rem;
text-align: center;
}

Expand Down Expand Up @@ -76,7 +77,7 @@
.item-name {
color: #fffa;
font-weight: 500;
font-size: 0.8rem;
font-size: 0.9rem;
margin-left: 8px;
}

Expand Down Expand Up @@ -123,16 +124,63 @@
}

.widget-thumb {
display: flex;
align-items: center;
justify-content: center;
padding: 8px;
max-height: 96px !important;
zoom: 0.75;
cursor: pointer;

transition:
background-color 0.15s,
border-color 0.15s;
}

.widget-thumb:hover {
background-color: transparent;
border-color: #fff4;
}

.widget-thumb > div > * {
.widget-thumb > * {
pointer-events: none;
user-select: none;
}

.template-item {
display: flex;
flex-flow: column nowrap;
height: min-content;
/*padding: 4px;*/
cursor: pointer;

transition:
background-color 0.15s,
border-color 0.15s;
}

.template-item:hover,
.template-item.active {
background-color: transparent;
border-color: #fff4;
}

.template-item.active span {
font-weight: 500;
color: #fffc;
}

.template-header {
display: flex;
flex-flow: row nowrap;
padding: 4px;
align-items: center;
justify-content: space-between;
height: min-content;
}

.save-button {
font-size: 0.9rem;
margin: 0 auto;
height: 32px;
}
Loading