Compare commits
4 Commits
07958b7f2a
...
a01e2d5706
| Author | SHA1 | Date | |
|---|---|---|---|
| a01e2d5706 | |||
| e15e09b512 | |||
| 7e2e684131 | |||
| d1efc9e78b |
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
@ -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,
|
||||
};
|
||||
|
||||
|
||||
@ -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">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user