Compare commits

...

4 Commits

Author SHA1 Message Date
a01e2d5706 Improved device view and removed tab.
All checks were successful
homepanel/HomePanel/pipeline/head This commit looks good
2026-02-15 04:19:37 +00:00
e15e09b512 Fixed page header animation. 2026-02-15 04:13:29 +00:00
7e2e684131 Code formatting and reworded settings page. 2026-02-15 04:13:09 +00:00
d1efc9e78b Add rowGap and columnGap props to Flex component for enhanced layout control 2026-02-15 04:12:39 +00:00
8 changed files with 269 additions and 240 deletions

View File

@ -12,6 +12,7 @@ import Select from "./controls/Select";
import Form from "./controls/Form";
import Button from "./controls/Button";
import CopyButton from "./controls/CopyButton";
import DeviceQRCode from "./controls/DeviceQRCode";
import Text from "./controls/Text";
import AdminPage from "./controls/AdminPage";
import { useWebSocket } from "../../contexts/WebSocketContext";
@ -19,12 +20,7 @@ import DeleteDevice from "./DeleteDevice";
import LoadingText from "./controls/LoadingText";
import AddDevicePanel from "./AddDevicePanel";
import Flex from "./controls/Flex";
import QRCode from "react-qr-code";
import {
List,
ListItemTitle,
ListItemContent,
} from "./controls/List";
import { List, ListItemTitle, ListItemContent } from "./controls/List";
import StatusListText from "./controls/StatusListText";
import AnimatedCollapse from "./controls/AnimatedCollapse";
@ -74,17 +70,17 @@ const Device = () => {
setHoldTime(
updatedDevice.holdTime !== undefined
? updatedDevice.holdTime / 1000
: 5
: 5,
);
setTransitionDuration(
updatedDevice.transitionDuration !== undefined
? updatedDevice.transitionDuration / 1000
: 1
: 1,
);
setCarouselEnabled(
updatedDevice.carouselEnabled !== undefined
? updatedDevice.carouselEnabled
: true
: true,
);
} else {
// Still update online status and panels even if there are unsaved changes
@ -167,7 +163,7 @@ const Device = () => {
if (panel) {
const panelId = typeof panel === "string" ? panel : panel.id;
const existingIds = devicePanels.map((p) =>
typeof p === "string" ? p : p.id
typeof p === "string" ? p : p.id,
);
if (!existingIds.includes(panelId)) {
setDevicePanels([...devicePanels, panel]);
@ -181,7 +177,7 @@ const Device = () => {
devicePanels.filter((p) => {
const id = typeof p === "string" ? p : p.id;
return id !== panelId;
})
}),
);
setUnsavedChanges(true);
};
@ -197,7 +193,7 @@ const Device = () => {
const pageDisabled = deviceNotFound || deviceLoading;
const hidePanelIds = devicePanels.map((p) =>
typeof p === "string" ? p : p.id
typeof p === "string" ? p : p.id,
);
return (
@ -218,7 +214,10 @@ const Device = () => {
<Button
key="open-device"
type="primary"
disabled={online || pageDisabled}
disabled={pageDisabled}
visible={online == false}
animated={true}
animationDirection="left"
onClick={() => window.open(link, "_blank")}
icon={
<HiOutlineArrowTopRightOnSquare style={{ fontSize: "15px" }} />
@ -244,71 +243,105 @@ const Device = () => {
) : (
<Tabs defaultActiveKey="general">
<TabPane tab="General" tabKey="general">
<Form>
<Form.Item label="Device Name:">
<Input
value={name}
onChange={handleNameChange}
placeholder="Device Name"
/>
</Form.Item>
<Form.Item label="Device ID:">
<Input
value={device?.id || ""}
disabled
placeholder={device?.id || "Device ID"}
/>
</Form.Item>
<Flex style={{ width: "100%" }} wrap="wrap" columnGap={35}>
<Form style={{ flexGrow: 15 }}>
<Form.Item label="Device Name:">
<Input
value={name}
onChange={handleNameChange}
placeholder="Device Name"
/>
</Form.Item>
<Form.Item label="Device ID:">
<Input
value={device?.id || ""}
disabled
placeholder={device?.id || "Device ID"}
/>
</Form.Item>
<Form.Item label="User Agent:">
<Input
value={userAgent}
onChange={handleUserAgentChange}
placeholder="User Agent String"
disabled
/>
</Form.Item>
<Form.Item label="Transition Duration (seconds):">
<Input
type="number"
value={transitionDuration}
onChange={handleTransitionDurationChange}
placeholder="1"
min="0.1"
step="0.1"
/>
</Form.Item>
<Form.Item label="Transition Duration (seconds):">
<Input
type="number"
value={transitionDuration}
onChange={handleTransitionDurationChange}
placeholder="1"
min="0.1"
step="0.1"
/>
</Form.Item>
<Form.Item label="Carousel Enabled:">
<Select
value={carouselEnabled ? "true" : "false"}
onChange={handleCarouselEnabledChange}
options={[
{ value: "true", label: "Yes" },
{ value: "false", label: "No" },
]}
placeholder="Select carousel state"
/>
</Form.Item>
<Select
value={carouselEnabled ? "true" : "false"}
onChange={handleCarouselEnabledChange}
options={[
{ value: "true", label: "Yes" },
{ value: "false", label: "No" },
]}
placeholder="Select carousel state"
/>
</Form.Item>
<Form.Item label="Carousel Hold Time (seconds):">
<Input
type="number"
value={holdTime}
onChange={handleHoldTimeChange}
placeholder="5"
min="1"
step="1"
disabled={!carouselEnabled}
/>
</Form.Item>
<Form.Item label="Carousel Hold Time (seconds):">
<Input
type="number"
value={holdTime}
onChange={handleHoldTimeChange}
placeholder="5"
min="1"
step="1"
disabled={!carouselEnabled}
/>
</Form.Item>
<div style={{ marginTop: 16 }}>
<Button
type="primary"
onClick={handleSave}
visible={unsavedChanges}
<div style={{ marginTop: 16 }}>
<Button
type="primary"
onClick={handleSave}
visible={unsavedChanges}
animated={true}
animationDirection="left"
icon={<HiOutlineCheck style={{ fontSize: "17px" }} />}
>
Save
</Button>
</div>
</Form>
<Form style={{ flexGrow: 1, flexShrink: 1 }}>
<Form.Item label="Operating System:">
<Input
value={os}
onChange={handleOsChange}
placeholder="Operating System"
disabled
/>
</Form.Item>
<Flex vertical gap={16} style={{ marginBottom: 24 }}>
<Text>QR Code:</Text>
<DeviceQRCode value={link} blurred={online} />
</Flex>
<CopyButton
type="default"
text={link}
visible={online == false}
animated={true}
animationDirection="left"
icon={<HiOutlineCheck style={{ fontSize: "17px" }} />}
animationDirection="right"
>
Save
</Button>
</div>
</Form>
Copy Device Link
</CopyButton>
</Form>
</Flex>
</TabPane>
<TabPane tab="Panels" tabKey="panels">
{devicePanels.length === 0 ? (
@ -324,9 +357,9 @@ const Device = () => {
<List
items={devicePanels}
renderItem={(panel) => (
<Flex
align="center"
justify="left"
<Flex
align="center"
justify="left"
gap={18}
onClick={() => navigate(`/admin/panels/${panel.id}`)}
>
@ -366,59 +399,6 @@ const Device = () => {
</Button>
</Flex>
</TabPane>
<TabPane tab="Info" tabKey="info">
<Form>
<Form.Item label="Operating System:">
<Input
value={os}
onChange={handleOsChange}
placeholder="Operating System"
disabled
/>
</Form.Item>
<Form.Item label="User Agent:">
<Input
value={userAgent}
onChange={handleUserAgentChange}
placeholder="User Agent String"
disabled
/>
</Form.Item>
<AnimatedCollapse visible={online == false}>
<Flex vertical gap={12} style={{ marginBottom: 24 }}>
<Text>QR Code:</Text>
<div
style={{
backgroundColor: "white",
padding: 10,
width: "fit-content",
}}
>
<QRCode value={link} />
</div>
</Flex>
<Flex gap={8}>
<Button
type="primary"
onClick={() => window.open(link, "_blank")}
icon={
<HiOutlineArrowTopRightOnSquare
style={{ fontSize: "15px" }}
/>
}
>
Open Device
</Button>
<CopyButton
type="default"
text={link}
>
Copy Device Link
</CopyButton>
</Flex>
</AnimatedCollapse>
</Form>
</TabPane>
</Tabs>
)}
</AdminPage>

View File

@ -29,7 +29,7 @@ const Settings = () => {
// Fetch settings when component mounts and socket is connected
useEffect(() => {
if (!isConnected) return;
setIsLoading(true);
getSettings()
.then((data) => {
@ -41,7 +41,7 @@ const Settings = () => {
const initialLatitude = data.openMeteo?.latitude || "";
const initialLongitude = data.openMeteo?.longitude || "";
const initialEnabled = data.openMeteo?.enabled || false;
setSiteName(initialSiteName);
setHomeassistantHost(initialHost);
setHomeassistantToken(initialToken);
@ -105,8 +105,8 @@ const Settings = () => {
return (
<AdminPage
title="General Settings"
subtitle="Manage your system settings here."
title="System Settings"
subtitle="Manage your Home Panel, OpenMeteo and Home Assistant settings here."
onBack={false}
>
{isLoading ? (
@ -142,58 +142,55 @@ const Settings = () => {
}}
/>
</Form.Item>
<Form.Item label="OpenMeteo Weather Enabled">
<Flex gap={10} align={'flex-end'}>
<div style={{flexGrow: 1}}>
<Select
value={openMeteoEnabled}
onChange={(e) => {
setOpenMeteoEnabled(e.target.value === true);
setUnsavedChanges(true);
}}
options={openMeteoEnabledOptions}
/>
<Form.Item label="OpenMeteo Weather Enabled">
<Flex gap={10} align={"flex-end"}>
<div style={{ flexGrow: 1 }}>
<Select
value={openMeteoEnabled}
onChange={(e) => {
setOpenMeteoEnabled(e.target.value === true);
setUnsavedChanges(true);
}}
options={openMeteoEnabledOptions}
/>
</div>
<Button
<Button
icon={<HiOutlineLocationMarker style={{ fontSize: "17px" }} />}
onClick={handleGetLocation}
>
Get Location
</Button>
</Flex>
</Flex>
</Form.Item>
<Form.Item label="OpenMeteo Latitude">
<Input
type="number"
step="any"
value={openMeteoLatitude}
placeholder="Latitude (e.g., 51.5074)"
disabled={!openMeteoEnabled}
onChange={(e) => {
setOpenMeteoLatitude(e.target.value);
setUnsavedChanges(true);
}}
/>
</Form.Item>
<Form.Item label="OpenMeteo Longitude">
<Input
type="number"
step="any"
value={openMeteoLongitude}
placeholder="Longitude (e.g., -0.1278)"
disabled={!openMeteoEnabled}
onChange={(e) => {
setOpenMeteoLongitude(e.target.value);
setUnsavedChanges(true);
}}
/>
</Form.Item>
<Form.Item label="Log Level">
type="number"
step="any"
value={openMeteoLatitude}
placeholder="Latitude (e.g., 51.5074)"
disabled={!openMeteoEnabled}
onChange={(e) => {
setOpenMeteoLatitude(e.target.value);
setUnsavedChanges(true);
}}
/>
</Form.Item>
<Form.Item label="OpenMeteo Longitude">
<Input
type="number"
step="any"
value={openMeteoLongitude}
placeholder="Longitude (e.g., -0.1278)"
disabled={!openMeteoEnabled}
onChange={(e) => {
setOpenMeteoLongitude(e.target.value);
setUnsavedChanges(true);
}}
/>
</Form.Item>
<Form.Item label="Log Level">
<Select
value={logLevel}
onChange={(e) => {
@ -203,13 +200,19 @@ const Settings = () => {
options={logLevelOptions}
/>
</Form.Item>
<Button
type="primary"
onClick={handleSave}
<Button
type="primary"
onClick={handleSave}
visible={unsavedChanges}
animated={true}
style={{ marginBottom: "15px" }}
icon={<HiOutlineCheck style={{ fontSize: "15px", marginRight: "2px" }} animated={true} visible={true}/>}
icon={
<HiOutlineCheck
style={{ fontSize: "15px", marginRight: "2px" }}
animated={true}
visible={true}
/>
}
>
Save
</Button>

View File

@ -1,28 +1,41 @@
.animated-collapse {
transform: translateY(10px);
.admin-animated-collapse {
opacity: 0;
overflow: hidden;
max-height: 0;
transition:
opacity 150ms ease-in-out,
transform 150ms ease-in-out,
max-height 150ms ease-in-out,
margin 150ms ease-in-out;
}
.animated-collapse.animated {
transition: opacity 150ms ease-in-out, transform 150ms ease-in-out,
max-height 150ms ease-in-out, margin 150ms ease-in-out;
}
.animated-collapse.visible {
.admin-animated-collapse.visible {
opacity: 1;
transform: translateY(0);
transform: translateY(0) translateX(0);
}
.animated-collapse.hidden {
.admin-animated-collapse-direction-up.hidden {
transform: translateY(-10px);
}
.admin-animated-collapse-direction-down.hidden {
transform: translateY(-10px);
}
.admin-animated-collapse-direction-left.visible {
transform: translateX(10px);
}
.admin-animated-collapse-direction-right.visible {
transform: translateX(-10px);
}
.admin-animated-collapse.hidden {
opacity: 0;
transform: translateY(10px);
max-height: 0;
margin: 0 !important;
}
.animated-collapse-no-animation {
.admin-animated-collapse-no-animation {
transition: none;
}

View File

@ -1,17 +1,9 @@
import React, { useState, useRef, useEffect, useCallback } from "react";
import "./AnimatedCollapse.css";
function AnimatedCollapse({ visible, children, animateOnOpen = true, style = {}, onExited }) {
function AnimatedCollapse({ visible, children, style, direction = "down" }) {
const contentRef = useRef(null);
const [maxHeight, setMaxHeight] = useState(0);
const [shouldRender, setShouldRender] = useState(visible);
const [isAnimated, setIsAnimated] = useState(false);
const onExitedRef = useRef(onExited);
// Keep onExited ref updated without causing effect re-runs
useEffect(() => {
onExitedRef.current = onExited;
}, [onExited]);
// Measure the content height and update maxHeight
const updateHeight = useCallback(() => {
@ -23,50 +15,29 @@ function AnimatedCollapse({ visible, children, animateOnOpen = true, style = {},
// Update height when visible state changes
useEffect(() => {
if (visible) {
setShouldRender(true);
// Small delay to ensure content is rendered
const frameId = requestAnimationFrame(() => {
setTimeout(() => {
updateHeight();
if (animateOnOpen) {
setTimeout(() => {
setIsAnimated(true);
}, 10);
} else {
setIsAnimated(false);
}
}, 10);
});
return () => cancelAnimationFrame(frameId);
} else {
if (animateOnOpen) {
// Wait for animation to complete before removing from DOM
const timeoutId = setTimeout(() => {
setMaxHeight(0);
setShouldRender(false);
onExitedRef.current?.();
}, 150); // Match CSS transition duration
return () => clearTimeout(timeoutId);
} else {
setMaxHeight(0);
setShouldRender(false);
setIsAnimated(false);
onExitedRef.current?.();
}
setMaxHeight(0);
}
}, [visible, animateOnOpen, updateHeight]);
}, [visible, updateHeight]);
// Listen for height changes of the content
useEffect(() => {
if (!visible || !contentRef.current) return;
let lastHeight = contentRef.current.clientHeight;
let lastHeight = contentRef.current.scrollHeight;
let intervalId;
// Poll for height changes
const checkHeight = () => {
if (contentRef.current) {
const currentHeight = contentRef.current.clientHeight;
const currentHeight = contentRef.current.scrollHeight;
if (currentHeight !== lastHeight) {
lastHeight = currentHeight;
setMaxHeight(currentHeight);
@ -76,7 +47,7 @@ function AnimatedCollapse({ visible, children, animateOnOpen = true, style = {},
// Start polling after a short delay to ensure element is rendered
const startId = setTimeout(() => {
intervalId = setInterval(checkHeight, 150);
intervalId = setInterval(checkHeight, 100);
}, 100);
return () => {
@ -102,22 +73,9 @@ function AnimatedCollapse({ visible, children, animateOnOpen = true, style = {},
};
}, [visible, updateHeight]);
const containerClasses = [
"animated-collapse",
visible ? "visible" : "hidden",
isAnimated && "animated",
!isAnimated && "animated-collapse-no-animation",
]
.filter(Boolean)
.join(" ");
if (!shouldRender) {
return null;
}
return (
<div
className={containerClasses}
className={`admin-animated-collapse admin-animated-collapse-direction-${direction} ${visible ? "visible" : "hidden"}`}
style={{
maxHeight: visible && maxHeight > 0 ? `${maxHeight}px` : "0",
...style,

View File

@ -0,0 +1,36 @@
.qr-code-container {
background-color: white;
padding: 10px;
width: fit-content;
position: relative;
min-width: 261px;
min-height: 261px;
display: flex;
align-items: center;
justify-content: center;
}
.qr-code-container > svg {
filter: none;
transition: filter 0.25s ease;
}
.qr-code-container.blurred > svg {
filter: blur(10px);
}
.qr-code-blurred-text-container {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 1;
}
.qr-code-blurred-text-content {
background-color: rgba(0, 0, 0, 0.3);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
padding: 5px 10px;
border-radius: 12px;
}

View File

@ -0,0 +1,15 @@
import React from "react";
import QRCode from "react-qr-code";
import AnimatedCollapse from "./AnimatedCollapse";
import Text from "./Text";
import "./DeviceQRCode.css";
const DeviceQRCode = ({ value, blurred = false }) => {
return (
<div className={`qr-code-container ${blurred ? "blurred" : ""}`}>
<QRCode value={value} />
</div>
);
};
export default DeviceQRCode;

View File

@ -8,17 +8,30 @@ const Flex = ({
justify = "normal",
align = "normal",
gap = 0,
rowGap = undefined,
columnGap = undefined,
style,
className = "",
...props
}) => {
let gaps = {};
if (rowGap) {
gaps.rowGap = rowGap;
}
if (columnGap) {
gaps.columnGap = columnGap;
}
if (gap) {
gaps.gap = gap;
}
const mergedStyle = {
display: "flex",
flexDirection: vertical ? "column" : "row",
flexWrap: wrap,
justifyContent: justify,
alignItems: align,
gap: typeof gap === "number" ? `${gap}px` : gap,
...gaps,
...style,
};

View File

@ -54,7 +54,10 @@ const PageHeader = ({
};
return (
<div className={`hp-page-header ${collapsed ? "collapsed" : ""}`} ref={headerRef} >
<div
className={`hp-page-header ${collapsed ? "collapsed" : ""}`}
ref={headerRef}
>
<Flex align="center" justify="space-between">
<Flex vertical>
<Flex align="center" gap={12}>
@ -65,7 +68,15 @@ const PageHeader = ({
)}
<Title level={level}>{title}</Title>
</Flex>
{subtitle &&<AnimatedCollapse visible={!collapsed} animateOnOpen={false} style={{ marginTop: 8 }}><Text type="secondary">{subtitle}</Text></AnimatedCollapse> }
{subtitle && (
<AnimatedCollapse
visible={!collapsed}
style={{ marginTop: 8 }}
direction="up"
>
<Text type="secondary">{subtitle}</Text>
</AnimatedCollapse>
)}
</Flex>
{actions && Array.isArray(actions) && actions.length > 0 && (
<Flex gap={16} justify="flex-end" wrap="wrap">