From c79d45f91e775b0d2eed070263021d0cffe695c9 Mon Sep 17 00:00:00 2001
From: Denis Papec <denis.papec@gmail.com>
Date: Sun, 16 Apr 2023 00:05:50 +0100
Subject: [PATCH] Add optional boxed styling and error component to information
 widgets

Signed-off-by: Denis Papec <denis.papec@gmail.com>
---
 src/components/widgets/datetime/datetime.jsx  |  8 +++--
 src/components/widgets/error.jsx              | 23 +++++++++++++
 src/components/widgets/glances/glances.jsx    | 30 +++++++---------
 src/components/widgets/greeting/greeting.jsx  |  7 +++-
 .../widgets/kubernetes/kubernetes.jsx         | 29 +++++++---------
 src/components/widgets/logo/logo.jsx          |  7 +++-
 src/components/widgets/longhorn/longhorn.jsx  | 25 +++++++-------
 .../widgets/openmeteo/openmeteo.jsx           | 34 +++++++++----------
 .../widgets/openweathermap/weather.jsx        | 33 +++++++++---------
 .../widgets/resources/resources.jsx           |  7 +++-
 src/components/widgets/search/search.jsx      | 11 +++---
 .../widgets/unifi_console/unifi_console.jsx   | 26 +++++++-------
 src/components/widgets/weather/weather.jsx    | 33 +++++++++---------
 src/pages/api/widgets/longhorn.js             |  2 +-
 14 files changed, 153 insertions(+), 122 deletions(-)
 create mode 100644 src/components/widgets/error.jsx

diff --git a/src/components/widgets/datetime/datetime.jsx b/src/components/widgets/datetime/datetime.jsx
index 86983473..fc883ec3 100644
--- a/src/components/widgets/datetime/datetime.jsx
+++ b/src/components/widgets/datetime/datetime.jsx
@@ -1,5 +1,6 @@
 import { useState, useEffect } from "react";
 import { useTranslation } from "next-i18next";
+import classNames from "classnames";
 
 const textSizes = {
   "4xl": "text-4xl",
@@ -17,7 +18,7 @@ export default function DateTime({ options }) {
   const { i18n } = useTranslation();
   const [date, setDate] = useState("");
   const dateLocale = locale ?? i18n.language;
-  
+
   useEffect(() => {
     const dateFormat = new Intl.DateTimeFormat(dateLocale, { ...format });
     const interval = setInterval(() => {
@@ -27,7 +28,10 @@ export default function DateTime({ options }) {
   }, [date, setDate, dateLocale, format]);
 
   return (
-    <div className="flex flex-col justify-center first:ml-0 ml-4">
+    <div className={classNames(
+      "flex flex-col justify-center first:ml-0 ml-4",
+      options?.styleBoxed === true && " mt-2 m:mb-0 rounded-md shadow-md shadow-theme-900/10 dark:shadow-theme-900/20 bg-theme-100/20 dark:bg-white/5 p-3",
+    )}>
       <div className="flex flex-row items-center grow justify-end">
         <span className={`text-theme-800 dark:text-theme-200 tabular-nums ${textSizes[textSize || "lg"]}`}>
           {date}
diff --git a/src/components/widgets/error.jsx b/src/components/widgets/error.jsx
new file mode 100644
index 00000000..92e0076a
--- /dev/null
+++ b/src/components/widgets/error.jsx
@@ -0,0 +1,23 @@
+import { useTranslation } from "react-i18next";
+import { BiError } from "react-icons/bi";
+import classNames from "classnames";
+
+export default function Error({ options }) {
+  const { t } = useTranslation();
+
+  return (
+    <div className={classNames(
+      "flex flex-col justify-center first:ml-0 ml-4 mr-2",
+      options?.styleBoxed === true && " ml-4 mt-2 m:mb-0 rounded-md shadow-md shadow-theme-900/10 dark:shadow-theme-900/20 bg-theme-100/20 dark:bg-white/5 p-3",
+    )}>
+      <div className="flex flex-row items-center justify-end">
+        <div className="flex flex-col items-center">
+          <BiError className="w-8 h-8 text-theme-800 dark:text-theme-200" />
+          <div className="flex flex-col ml-3 text-left">
+            <span className="text-theme-800 dark:text-theme-200 text-sm">{t("widget.api_error")}</span>
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+}
diff --git a/src/components/widgets/glances/glances.jsx b/src/components/widgets/glances/glances.jsx
index 85dd44c0..b6daba7b 100644
--- a/src/components/widgets/glances/glances.jsx
+++ b/src/components/widgets/glances/glances.jsx
@@ -1,11 +1,12 @@
 import useSWR from "swr";
 import { useContext } from "react";
-import { BiError } from "react-icons/bi";
 import { FaMemory, FaRegClock, FaThermometerHalf } from "react-icons/fa";
 import { FiCpu, FiHardDrive } from "react-icons/fi";
 import { useTranslation } from "next-i18next";
+import classNames from "classnames";
 
 import UsageBar from "../resources/usage-bar";
+import Error from "../error";
 
 import { SettingsContext } from "utils/contexts/settings";
 
@@ -26,23 +27,15 @@ export default function Widget({ options }) {
   );
 
   if (error || data?.error) {
-    return (
-      <div className="flex flex-col justify-center first:ml-0 ml-4">
-        <div className="flex flex-row items-center justify-end">
-          <div className="flex flex-row items-center">
-            <BiError className="w-8 h-8 text-theme-800 dark:text-theme-200" />
-            <div className="flex flex-col ml-3 text-left">
-              <span className="text-theme-800 dark:text-theme-200 text-sm">{t("widget.api_error")}</span>
-            </div>
-          </div>
-        </div>
-      </div>
-    );
+    return <Error options={options} />
   }
 
   if (!data) {
     return (
-      <div className="flex flex-col max-w:full sm:basis-auto self-center grow-0 flex-wrap ml-4">
+      <div className={classNames(
+        "flex flex-col max-w:full sm:basis-auto self-center grow-0 flex-wrap ml-4",
+        options?.styleBoxed === true && " mb-0 sm:mb-0 rounded-md shadow-md shadow-theme-900/10 dark:shadow-theme-900/20 bg-theme-100/20 dark:bg-white/5 p-3",
+      )}>
         <div className="flex flex-row self-center flex-wrap justify-between">
            <div className="flex-none flex flex-row items-center mr-3 py-1.5">
             <FiCpu className="text-theme-800 dark:text-theme-200 w-5 h-5" />
@@ -101,7 +94,10 @@ export default function Widget({ options }) {
   }
 
   return (
-    <a href={options.url} target={settings.target ?? "_blank"} className="flex flex-col max-w:full sm:basis-auto self-center grow-0 flex-wrap">
+    <a href={options.url} target={settings.target ?? "_blank"}  className={classNames(
+      "flex flex-col max-w:full sm:basis-auto self-center grow-0 flex-wrap",
+      options?.styleBoxed === true && " mb-0 mt-2 sm:mb-0 rounded-md shadow-md shadow-theme-900/10 dark:shadow-theme-900/20 bg-theme-100/20 dark:bg-white/5 p-3",
+    )}>
       <div className="flex flex-row self-center flex-wrap justify-between">
          <div className="flex-none flex flex-row items-center mr-3 py-1.5">
           <FiCpu className="text-theme-800 dark:text-theme-200 w-5 h-5" />
@@ -184,7 +180,7 @@ export default function Widget({ options }) {
             <div className="flex flex-col ml-3 text-left min-w-[85px]">
               <span className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
                 <div className="pl-0.5">
-                  {t("common.number", { 
+                  {t("common.number", {
                     value: mainTemp,
                     maximumFractionDigits: 1,
                     style: "unit",
@@ -196,7 +192,7 @@ export default function Widget({ options }) {
               {options.expanded && (
                 <span className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
                   <div className="pl-0.5 pr-1">
-                  {t("common.number", { 
+                  {t("common.number", {
                     value: maxTemp,
                     maximumFractionDigits: 1,
                     style: "unit",
diff --git a/src/components/widgets/greeting/greeting.jsx b/src/components/widgets/greeting/greeting.jsx
index da0f063d..2e129560 100644
--- a/src/components/widgets/greeting/greeting.jsx
+++ b/src/components/widgets/greeting/greeting.jsx
@@ -1,3 +1,5 @@
+import classNames from "classnames";
+
 const textSizes = {
   "4xl": "text-4xl",
   "3xl": "text-3xl",
@@ -12,7 +14,10 @@ const textSizes = {
 export default function Greeting({ options }) {
   if (options.text) {
     return (
-      <div className="flex flex-row items-center justify-start">
+      <div className={classNames(
+        "flex flex-row items-center justify-start",
+        options?.styleBoxed === true && " ml-4 mt-2 m:mb-0 rounded-md shadow-md shadow-theme-900/10 dark:shadow-theme-900/20 bg-theme-100/20 dark:bg-white/5 p-3",
+      )}>
         <span className={`text-theme-800 dark:text-theme-200 mr-3 ${textSizes[options.text_size || "xl"]}`}>
           {options.text}
         </span>
diff --git a/src/components/widgets/kubernetes/kubernetes.jsx b/src/components/widgets/kubernetes/kubernetes.jsx
index 78c4caaf..514993da 100644
--- a/src/components/widgets/kubernetes/kubernetes.jsx
+++ b/src/components/widgets/kubernetes/kubernetes.jsx
@@ -1,12 +1,14 @@
 import useSWR from "swr";
-import { BiError } from "react-icons/bi";
 import { useTranslation } from "next-i18next";
+import classNames from "classnames";
+
+import Error from "../error";
 
 import Node from "./node";
 
 export default function Widget({ options }) {
   const { cluster, nodes } = options;
-  const { t, i18n } = useTranslation();
+  const { i18n } = useTranslation();
 
   const defaultData = {
     cpu: {
@@ -29,23 +31,15 @@ export default function Widget({ options }) {
   );
 
   if (error || data?.error) {
-    return (
-      <div className="flex flex-col justify-center first:ml-0 ml-4">
-        <div className="flex flex-row items-center justify-end">
-          <div className="flex flex-row items-center">
-            <BiError className="w-8 h-8 text-theme-800 dark:text-theme-200" />
-            <div className="flex flex-col ml-3 text-left">
-              <span className="text-theme-800 dark:text-theme-200 text-sm">{t("widget.api_error")}</span>
-            </div>
-          </div>
-        </div>
-      </div>
-    );
+    return <Error options={options} />
   }
 
   if (!data) {
     return (
-      <div className="flex flex-col max-w:full sm:basis-auto self-center grow-0 flex-wrap">
+      <div className={classNames(
+        "flex flex-col max-w:full sm:basis-auto self-center grow-0 flex-wrap",
+        options?.styleBoxed === true && " ml-4 mt-2 m:mb-0 rounded-md shadow-md shadow-theme-900/10 dark:shadow-theme-900/20 bg-theme-100/20 dark:bg-white/5 p-3",
+      )}>
         <div className="flex flex-row self-center flex-wrap justify-between">
           {cluster.show &&
             <Node type="cluster" key="cluster" options={options.cluster} data={defaultData} />
@@ -59,7 +53,10 @@ export default function Widget({ options }) {
   }
 
   return (
-    <div className="flex flex-col max-w:full sm:basis-auto self-center grow-0 flex-wrap">
+    <div className={classNames(
+      "flex flex-col max-w:full sm:basis-auto self-center grow-0 flex-wrap",
+      options?.styleBoxed === true && " ml-4 mt-2 m:mb-0 rounded-md shadow-md shadow-theme-900/10 dark:shadow-theme-900/20 bg-theme-100/20 dark:bg-white/5 p-3",
+    )}>
       <div className="flex flex-row self-center flex-wrap justify-between">
         {cluster.show &&
           <Node key="cluster" type="cluster" options={options.cluster} data={data.cluster} />
diff --git a/src/components/widgets/logo/logo.jsx b/src/components/widgets/logo/logo.jsx
index 96e8569f..6cba17bf 100644
--- a/src/components/widgets/logo/logo.jsx
+++ b/src/components/widgets/logo/logo.jsx
@@ -1,8 +1,13 @@
+import classNames from "classnames";
+
 import ResolvedIcon from "components/resolvedicon"
 
 export default function Logo({ options }) {
   return (
-    <div className="w-12 h-12 flex flex-row items-center align-middle mr-3 self-center">
+    <div className={classNames(
+      "w-12 h-12 flex flex-row items-center align-middle mr-3 self-center",
+      options?.styleBoxed === true && " ml-4 mt-2 m:mb-0 rounded-md shadow-md shadow-theme-900/10 dark:shadow-theme-900/20 bg-theme-100/20 dark:bg-white/5 p-3",
+    )}>
       {options.icon ?
         <ResolvedIcon icon={options.icon} width={48} height={48} /> :
         // fallback to homepage logo
diff --git a/src/components/widgets/longhorn/longhorn.jsx b/src/components/widgets/longhorn/longhorn.jsx
index 9fcb21b4..5139f00a 100644
--- a/src/components/widgets/longhorn/longhorn.jsx
+++ b/src/components/widgets/longhorn/longhorn.jsx
@@ -1,37 +1,36 @@
 import useSWR from "swr";
-import { BiError } from "react-icons/bi";
-import { useTranslation } from "next-i18next";
+import classNames from "classnames";
+
+import Error from "../error";
 
 import Node from "./node";
 
 export default function Longhorn({ options }) {
   const { expanded, total, labels, include, nodes } = options;
-  const { t } = useTranslation();
   const { data, error } = useSWR(`/api/widgets/longhorn`, {
     refreshInterval: 1500
   });
 
   if (error || data?.error) {
-    return (
-      <div className="flex flex-col max-w:full sm:basis-auto self-center grow-0 flex-wrap">
-        <BiError className="text-theme-800 dark:text-theme-200 w-5 h-5" />
-        <div className="flex flex-col ml-3 text-left">
-          <span className="text-theme-800 dark:text-theme-200 text-xs">{t("widget.api_error")}</span>
-        </div>
-      </div>
-    );
+    return <Error options={options} />
   }
 
   if (!data) {
     return (
-      <div className="flex flex-col max-w:full sm:basis-auto self-center grow-0 flex-wrap">
+      <div className={classNames(
+        "flex flex-col max-w:full sm:basis-auto self-center grow-0 flex-wrap",
+        options?.styleBoxed === true && " ml-4 mt-2 m:mb-0 rounded-md shadow-md shadow-theme-900/10 dark:shadow-theme-900/20 bg-theme-100/20 dark:bg-white/5 p-3",
+      )}>
         <div className="flex flex-row self-center flex-wrap justify-between" />
       </div>
     );
   }
 
   return (
-    <div className="flex flex-col max-w:full sm:basis-auto self-center grow-0 flex-wrap">
+    <div className={classNames(
+      "flex flex-col max-w:full sm:basis-auto self-center grow-0 flex-wrap",
+      options?.styleBoxed === true && " ml-4 mt-2 m:mb-0 rounded-md shadow-md shadow-theme-900/10 dark:shadow-theme-900/20 bg-theme-100/20 dark:bg-white/5 p-3",
+    )}>
       <div className="flex flex-row self-center flex-wrap justify-between">
         {data.nodes
           .filter((node) => {
diff --git a/src/components/widgets/openmeteo/openmeteo.jsx b/src/components/widgets/openmeteo/openmeteo.jsx
index 0d29aef5..1381cc55 100644
--- a/src/components/widgets/openmeteo/openmeteo.jsx
+++ b/src/components/widgets/openmeteo/openmeteo.jsx
@@ -1,9 +1,11 @@
 import useSWR from "swr";
 import { useState } from "react";
-import { BiError } from "react-icons/bi";
 import { WiCloudDown } from "react-icons/wi";
 import { MdLocationDisabled, MdLocationSearching } from "react-icons/md";
 import { useTranslation } from "next-i18next";
+import classNames from "classnames";
+
+import Error from "../error";
 
 import Icon from "./icon";
 
@@ -15,24 +17,15 @@ function Widget({ options }) {
   );
 
   if (error || data?.error) {
-    return (
-      <div className="flex flex-col justify-center first:ml-0 ml-4 mr-2">
-        <div className="flex flex-row items-center justify-end">
-          <div className="flex flex-col items-center">
-            <BiError className="w-8 h-8 text-theme-800 dark:text-theme-200" />
-            <div className="flex flex-col ml-3 text-left">
-              <span className="text-theme-800 dark:text-theme-200 text-sm">{t("widget.api_error")}</span>
-              <span className="text-theme-800 dark:text-theme-200 text-xs">-</span>
-            </div>
-          </div>
-        </div>
-      </div>
-    );
+    return <Error options={options} />
   }
 
   if (!data) {
     return (
-      <div className="flex flex-col justify-center first:ml-0 ml-4 mr-2">
+      <div className={classNames(
+        "flex flex-col justify-center first:ml-0 ml-4 mr-2",
+        options?.styleBoxed === true && " ml-4 mt-2 m:mb-0 rounded-md shadow-md shadow-theme-900/10 dark:shadow-theme-900/20 bg-theme-100/20 dark:bg-white/5 p-3",
+      )}>
         <div className="flex flex-row items-center justify-end">
           <div className="flex flex-col items-center">
             <WiCloudDown className="w-8 h-8 text-theme-800 dark:text-theme-200" />
@@ -50,7 +43,10 @@ function Widget({ options }) {
   const timeOfDay = data.current_weather.time > data.daily.sunrise[0] && data.current_weather.time < data.daily.sunset[0] ? "day" : "night";
 
   return (
-    <div className="flex flex-col justify-center first:ml-0 ml-4 mr-2">
+    <div className={classNames(
+      "flex flex-col justify-center first:ml-0 ml-4 mr-2",
+      options?.styleBoxed === true && " ml-4 mt-2 m:mb-0 rounded-md shadow-md shadow-theme-900/10 dark:shadow-theme-900/20 bg-theme-100/20 dark:bg-white/5 p-3",
+    )}>
       <div className="flex flex-row items-center justify-end">
         <div className="flex flex-col items-center">
           <Icon condition={data.current_weather.weathercode} timeOfDay={timeOfDay} />
@@ -107,8 +103,10 @@ export default function OpenMeteo({ options }) {
       <button
         type="button"
         onClick={() => requestLocation()}
-        className="flex flex-col justify-center first:ml-0 ml-4 mr-2"
-      >
+        className={classNames(
+          "flex flex-col justify-center first:ml-0 ml-4 mr-2",
+          options?.styleBoxed === true && " ml-4 mt-2 m:mb-0 rounded-md shadow-md shadow-theme-900/10 dark:shadow-theme-900/20 bg-theme-100/20 dark:bg-white/5 p-3",
+      )}>
         <div className="flex flex-row items-center justify-end">
           <div className="flex flex-col items-center">
             {requesting ? (
diff --git a/src/components/widgets/openweathermap/weather.jsx b/src/components/widgets/openweathermap/weather.jsx
index 49f428a0..b404039f 100644
--- a/src/components/widgets/openweathermap/weather.jsx
+++ b/src/components/widgets/openweathermap/weather.jsx
@@ -1,9 +1,11 @@
 import useSWR from "swr";
 import { useState } from "react";
-import { BiError } from "react-icons/bi";
 import { WiCloudDown } from "react-icons/wi";
 import { MdLocationDisabled, MdLocationSearching } from "react-icons/md";
 import { useTranslation } from "next-i18next";
+import classNames from "classnames";
+
+import Error from "../error";
 
 import Icon from "./icon";
 
@@ -15,24 +17,15 @@ function Widget({ options }) {
   );
 
   if (error || data?.cod === 401 || data?.error) {
-    return (
-      <div className="flex flex-col justify-center first:ml-auto ml-4 mr-2">
-        <div className="flex flex-row items-center justify-end">
-          <div className="hidden sm:flex flex-col items-center">
-            <BiError className="w-8 h-8 text-theme-800 dark:text-theme-200" />
-            <div className="flex flex-col ml-3 text-left">
-              <span className="text-theme-800 dark:text-theme-200 text-sm">{t("widget.api_error")}</span>
-              <span className="text-theme-800 dark:text-theme-200 text-xs">-</span>
-            </div>
-          </div>
-        </div>
-      </div>
-    );
+    return <Error options={options} />
   }
 
   if (!data) {
     return (
-      <div className="flex flex-col justify-center first:ml-auto ml-4 mr-2">
+      <div className={classNames(
+        "flex flex-col justify-center first:ml-auto ml-4 mr-2",
+        options?.styleBoxed === true && " ml-4 mt-2 m:mb-0 rounded-md shadow-md shadow-theme-900/10 dark:shadow-theme-900/20 bg-theme-100/20 dark:bg-white/5 p-3",
+      )}>
         <div className="flex flex-row items-center justify-end">
           <div className="hidden sm:flex flex-col items-center">
             <WiCloudDown className="w-8 h-8 text-theme-800 dark:text-theme-200" />
@@ -49,7 +42,10 @@ function Widget({ options }) {
   const unit = options.units === "metric" ? "celsius" : "fahrenheit";
 
   return (
-    <div className="flex flex-col justify-center first:ml-auto ml-2 mr-2">
+    <div className={classNames(
+      "flex flex-col justify-center first:ml-auto ml-2 mr-2",
+      options?.styleBoxed === true && " ml-4 mt-2 m:mb-0 rounded-md shadow-md shadow-theme-900/10 dark:shadow-theme-900/20 bg-theme-100/20 dark:bg-white/5 p-3",
+    )}>
       <div className="flex flex-row items-center justify-end">
         <div className="hidden sm:flex flex-col items-center">
           <Icon
@@ -105,7 +101,10 @@ export default function OpenWeatherMap({ options }) {
       <button
         type="button"
         onClick={() => requestLocation()}
-        className="flex flex-col justify-center first:ml-auto ml-4 mr-2"
+        className={classNames(
+          "flex flex-col justify-center first:ml-auto ml-4 mr-2",
+          options?.styleBoxed === true && " ml-4 mt-2 m:mb-0 rounded-md shadow-md shadow-theme-900/10 dark:shadow-theme-900/20 bg-theme-100/20 dark:bg-white/5 p-3",
+        )}
       >
         <div className="flex flex-row items-center justify-end">
           <div className="hidden sm:flex flex-col items-center">
diff --git a/src/components/widgets/resources/resources.jsx b/src/components/widgets/resources/resources.jsx
index 4ff0c81c..5727a2a0 100644
--- a/src/components/widgets/resources/resources.jsx
+++ b/src/components/widgets/resources/resources.jsx
@@ -1,3 +1,5 @@
+import classNames from "classnames";
+
 import Disk from "./disk";
 import Cpu from "./cpu";
 import Memory from "./memory";
@@ -7,7 +9,10 @@ import Uptime from "./uptime";
 export default function Resources({ options }) {
   const { expanded, units } = options;
   return (
-    <div className="flex flex-col max-w:full sm:basis-auto self-center grow-0 flex-wrap">
+    <div className={classNames(
+      "flex flex-col max-w:full sm:basis-auto self-center grow-0 flex-wrap",
+      options?.styleBoxed === true && " ml-4 mt-2 m:mb-0 rounded-md shadow-md shadow-theme-900/10 dark:shadow-theme-900/20 bg-theme-100/20 dark:bg-white/5 p-3",
+    )}>
       <div className="flex flex-row self-center flex-wrap justify-between">
         {options.cpu && <Cpu expanded={expanded} />}
         {options.memory && <Memory expanded={expanded} />}
diff --git a/src/components/widgets/search/search.jsx b/src/components/widgets/search/search.jsx
index 4689567f..bca3eb58 100644
--- a/src/components/widgets/search/search.jsx
+++ b/src/components/widgets/search/search.jsx
@@ -76,7 +76,7 @@ export default function Search({ options }) {
       setSelectedProvider(storedProvider);
     }
   }, [availableProviderIds]);
-  
+
   if (!availableProviderIds) {
     return null;
   }
@@ -102,7 +102,10 @@ export default function Search({ options }) {
   }
 
   return (
-    <form className="flex-col relative h-8 my-4 min-w-fit grow first:ml-0 ml-4" onSubmit={handleSubmit}>
+    <form className={classNames(
+      "flex-col relative h-8 my-4 min-w-fit grow first:ml-0 ml-4",
+      options?.styleBoxed === true && " h-14 ml-4 mt-4 m:mb-0 rounded-md shadow-md shadow-theme-900/10 dark:shadow-theme-900/20 bg-theme-100/20 dark:bg-white/5 p-3",
+    )} onSubmit={handleSubmit}>
       <div className="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none w-full text-theme-800 dark:text-white" />
       <input
         type="text"
@@ -146,8 +149,8 @@ export default function Search({ options }) {
           leaveTo="transform opacity-0 scale-95"
         >
           <Listbox.Options
-            className="absolute right-0 z-10 mt-1 origin-top-right rounded-md 
-            bg-theme-100 dark:bg-theme-600 shadow-lg 
+            className="absolute right-0 z-10 mt-1 origin-top-right rounded-md
+            bg-theme-100 dark:bg-theme-600 shadow-lg
             ring-1 ring-black ring-opacity-5 focus:outline-none"
           >
             <div className="flex flex-col">
diff --git a/src/components/widgets/unifi_console/unifi_console.jsx b/src/components/widgets/unifi_console/unifi_console.jsx
index 13c90bd4..1896771f 100644
--- a/src/components/widgets/unifi_console/unifi_console.jsx
+++ b/src/components/widgets/unifi_console/unifi_console.jsx
@@ -2,6 +2,9 @@ import { BiError, BiWifi, BiCheckCircle, BiXCircle, BiNetworkChart } from "react
 import { MdSettingsEthernet } from "react-icons/md";
 import { useTranslation } from "next-i18next";
 import { SiUbiquiti } from "react-icons/si";
+import classNames from "classnames";
+
+import Error from "../error";
 
 import useWidgetAPI from "utils/proxy/use-widget-api";
 
@@ -13,25 +16,17 @@ export default function Widget({ options }) {
   const { data: statsData, error: statsError } = useWidgetAPI(options, "stat/sites", { index: options.index });
 
   if (statsError) {
-    return (
-      <div className="flex flex-col justify-center first:ml-0 ml-4">
-        <div className="flex flex-row items-center justify-end">
-          <div className="flex flex-col items-center">
-            <BiError className="w-8 h-8 text-theme-800 dark:text-theme-200" />
-            <div className="flex flex-col ml-3 text-left">
-              <span className="text-theme-800 dark:text-theme-200 text-sm">{t("widget.api_error")}</span>
-            </div>
-          </div>
-        </div>
-      </div>
-    );
+    return <Error options={options} />
   }
 
   const defaultSite = options.site ? statsData?.data.find(s => s.desc === options.site) : statsData?.data?.find(s => s.name === "default");
 
   if (!defaultSite) {
     return (
-      <div className="flex flex-col justify-center first:ml-0 ml-4">
+      <div className={classNames(
+        "flex flex-col justify-center first:ml-0 ml-4",
+        options?.styleBoxed === true && " ml-4 mt-2 m:mb-0 rounded-md shadow-md shadow-theme-900/10 dark:shadow-theme-900/20 bg-theme-100/20 dark:bg-white/5 p-3",
+      )}>
         <div className="flex flex-row items-center justify-end">
           <div className="flex flex-col items-center">
             <SiUbiquiti className="w-5 h-5 text-theme-800 dark:text-theme-200" />
@@ -57,7 +52,10 @@ export default function Widget({ options }) {
   const dataEmpty = !(wan.show || lan.show || wlan.show || uptime);
 
   return (
-    <div className="flex-none flex flex-row items-center mr-3 py-1.5">
+    <div className={classNames(
+      "flex-none flex flex-row items-center mr-3 py-1.5",
+      options?.styleBoxed === true && " ml-4 mt-2 m:mb-0 rounded-md shadow-md shadow-theme-900/10 dark:shadow-theme-900/20 bg-theme-100/20 dark:bg-white/5 p-3",
+    )}>
       <div className="flex flex-col">
         <div className="flex flex-row ml-3 mb-0.5">
           <SiUbiquiti className="text-theme-800 dark:text-theme-200 w-3 h-3 mr-1" />
diff --git a/src/components/widgets/weather/weather.jsx b/src/components/widgets/weather/weather.jsx
index 20bf3dec..51801455 100644
--- a/src/components/widgets/weather/weather.jsx
+++ b/src/components/widgets/weather/weather.jsx
@@ -1,9 +1,11 @@
 import useSWR from "swr";
 import { useState } from "react";
-import { BiError } from "react-icons/bi";
 import { WiCloudDown } from "react-icons/wi";
 import { MdLocationDisabled, MdLocationSearching } from "react-icons/md";
 import { useTranslation } from "next-i18next";
+import classNames from "classnames";
+
+import Error from "../error";
 
 import Icon from "./icon";
 
@@ -15,24 +17,15 @@ function Widget({ options }) {
   );
 
   if (error || data?.error) {
-    return (
-      <div className="flex flex-col justify-center first:ml-0 ml-4 mr-2">
-        <div className="flex flex-row items-center justify-end">
-          <div className="flex flex-col items-center">
-            <BiError className="w-8 h-8 text-theme-800 dark:text-theme-200" />
-            <div className="flex flex-col ml-3 text-left">
-              <span className="text-theme-800 dark:text-theme-200 text-sm">{t("widget.api_error")}</span>
-              <span className="text-theme-800 dark:text-theme-200 text-xs">-</span>
-            </div>
-          </div>
-        </div>
-      </div>
-    );
+    return <Error options={options} />
   }
 
   if (!data) {
     return (
-      <div className="flex flex-col justify-center first:ml-0 ml-4 mr-2">
+      <div className={classNames(
+        "flex flex-col justify-center first:ml-0 ml-4 mr-2",
+        options?.styleBoxed === true && " ml-4 mt-2 m:mb-0 rounded-md shadow-md shadow-theme-900/10 dark:shadow-theme-900/20 bg-theme-100/20 dark:bg-white/5 p-3",
+      )}>
         <div className="flex flex-row items-center justify-end">
           <div className="flex flex-col items-center">
             <WiCloudDown className="w-8 h-8 text-theme-800 dark:text-theme-200" />
@@ -49,7 +42,10 @@ function Widget({ options }) {
   const unit = options.units === "metric" ? "celsius" : "fahrenheit";
 
   return (
-    <div className="flex flex-col justify-center first:ml-0 ml-4 mr-2">
+    <div className={classNames(
+      "flex flex-col justify-center first:ml-0 ml-4 mr-2",
+      options?.styleBoxed === true && " ml-4 mt-2 m:mb-0 rounded-md shadow-md shadow-theme-900/10 dark:shadow-theme-900/20 bg-theme-100/20 dark:bg-white/5 p-3",
+    )}>
       <div className="flex flex-row items-center justify-end">
         <div className="flex flex-col items-center">
           <Icon condition={data.current.condition.code} timeOfDay={data.current.is_day ? "day" : "night"} />
@@ -106,7 +102,10 @@ export default function WeatherApi({ options }) {
       <button
         type="button"
         onClick={() => requestLocation()}
-        className="flex flex-col justify-center first:ml-0 ml-4 mr-2"
+        className={classNames(
+          "flex flex-col justify-center first:ml-0 ml-4 mr-2",
+          options?.styleBoxed === true && " ml-4 mt-2 m:mb-0 rounded-md shadow-md shadow-theme-900/10 dark:shadow-theme-900/20 bg-theme-100/20 dark:bg-white/5 p-3",
+        )}
       >
         <div className="flex flex-row items-center justify-end">
           <div className="flex flex-col items-center">
diff --git a/src/pages/api/widgets/longhorn.js b/src/pages/api/widgets/longhorn.js
index a6b6781c..d23a7f61 100644
--- a/src/pages/api/widgets/longhorn.js
+++ b/src/pages/api/widgets/longhorn.js
@@ -46,7 +46,7 @@ function parseLonghornData(data) {
 
 export default async function handler(req, res) {
   const settings = getSettings();
-  const longhornSettings = settings?.providers?.longhorn;
+  const longhornSettings = settings?.providers?.longhorn || {};
   const {url, username, password} = longhornSettings;
 
   if (!url) {