diff --git a/package.json b/package.json index 607b709..1112cd7 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,9 @@ "react-router-dom": "^7.11.0", "sass": "^1.97.0", "swr": "^2.3.8", - "vite-plugin-compression": "^0.5.1" + "vite-plugin-compression": "^0.5.1", + "echarts": "^6.0.0", + "echarts-for-react": "^3.0.5" }, "devDependencies": { "typescript":">=4.0.0", diff --git a/public/locales/bg.yaml b/public/locales/bg.yaml index ff79441..111adbf 100644 --- a/public/locales/bg.yaml +++ b/public/locales/bg.yaml @@ -109,6 +109,11 @@ network_devices: new_network_device: Ново мрежово устройство edit: edit_network_device: Редакция на мрежово устройство +rooms: + big-room: Голяма зала + small-room: Малка зала + kitchen: Кухня + outside: Навън views: dashboard: sensor_readings: Температура и влажност на въздуха @@ -119,7 +124,7 @@ views: colibri_message_contact: | Ако имате проблем с ползването му, или е нужно да заявите достъп за нови служители, моля свържете се с Инит Лаб на имейл us@initlab.org. - colibri_message_emergency: При неотложни случаи, моля обадете се на тел. 0883 433 990 (Венцислав). + colibri_message_emergency: При неотложни случаи, моля обадете се на тел. 02 422 54 36. devices: lock: заключи unlock: отключи @@ -165,6 +170,8 @@ views: lights: Осветление hvac: Климатици sensors: Графики + dark_mode: Тъмна тема + language: Език oauth_application_management: OAuth интеграция oauth_token_management: Упълномощени приложения registrations: @@ -227,6 +234,8 @@ views: are_you_sure: 'Сигурни ли сте?' sensors: title: Показания на сензорите + temperature: Температура + humidity: Влажност action_log: title: Лог на действията columns: diff --git a/public/locales/en.yaml b/public/locales/en.yaml index 36c32e0..3882310 100644 --- a/public/locales/en.yaml +++ b/public/locales/en.yaml @@ -103,6 +103,11 @@ network_devices: new_network_device: New network device edit: edit_network_device: Edit network device +rooms: + big-room: Big Room + small-room: Small Room + kitchen: Kitchen + outside: Outside views: dashboard: sensor_readings: Air temperature and humidity @@ -112,7 +117,7 @@ views: colibri_message_contact: | In case of technical difficulties, or you need to request access for new employees, please contact init Lab at us@initlab.org. - colibri_message_emergency: In case of major malfunction, please call +359 883 433 990 (Vencislav). + colibri_message_emergency: In case of major malfunction, please call +359 2 422 54 36. devices: lock: lock unlock: unlock @@ -158,6 +163,8 @@ views: lights: Lights hvac: HVAC sensors: Sensors + dark_mode: Dark Mode + language: Language oauth_application_management: OAuth integration oauth_token_management: Authorized applications registrations: @@ -219,6 +226,8 @@ views: are_you_sure: 'Are you shnur?' sensors: title: Sensor readings + temperature: Temperature + humidity: Humidity action_log: title: Action logs columns: diff --git a/src/App.tsx b/src/App.tsx index 5c56a58..eedd1cf 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import {createElement} from 'react'; +import {createElement, useEffect} from 'react'; import {Container} from 'react-bootstrap'; import {Route, Routes} from 'react-router-dom'; @@ -14,10 +14,13 @@ import {useVariant} from './hooks/useVariant.ts'; import {getDoorActions, getHvacActions, getLightActions} from "./utils/device.ts"; import Devices from "./pages/Devices.tsx"; import {useDocumentTitle} from '@uidotdev/usehooks'; +import {useTheme} from './hooks/useTheme.ts'; function App() { const variant = useVariant(); useDocumentTitle(variant.title); + const [theme] = useTheme(); + useEffect(() => document.documentElement.setAttribute('data-bs-theme', theme), [theme]); return (<> diff --git a/src/config.ts b/src/config.ts index dea8439..1c29a3b 100644 --- a/src/config.ts +++ b/src/config.ts @@ -13,10 +13,10 @@ export const oidc = { export type MqttConfig = {[topic: string]: {label: string}} export const sensors: MqttConfig = { - 'sensors/big-room': {label: 'Big room',}, - 'sensors/small-room': {label: 'Small room',}, - 'sensors/kitchen': {label: 'Kitchen',}, - 'sensors/outside': {label: 'Outside',}, + 'sensors/big-room/test': {label: 'big-room'}, + 'sensors/small-room/test': {label: 'small-room',}, + 'sensors/kitchen/test': {label: 'kitchen',}, + 'sensors/outside/test': {label: 'outside',}, }; export const variantHosts: {[hostname: string]: string} = { @@ -45,12 +45,4 @@ export const variants: {[variant: string]: VariantConfig} = { logo: {url: colibriLogo, alt: 'colibri logo',}, title: 'Casa Libri', }, -} - -export const grafana = { - urls: [ - 'https://stats.initlab.org/d-solo/SGAb0ZXMk/temperature-and-humidity?orgId=1&refresh=1m&panelId=4', - 'https://stats.initlab.org/d-solo/SGAb0ZXMk/temperature-and-humidity?orgId=1&refresh=1m&panelId=5', - 'https://stats.initlab.org/d-solo/SGAb0ZXMk/temperature-and-humidity?orgId=1&refresh=1m&panelId=10', - ] -}; +} \ No newline at end of file diff --git a/src/hooks/useEndpoints.ts b/src/hooks/useEndpoints.ts index 0c41aa6..edc4549 100644 --- a/src/hooks/useEndpoints.ts +++ b/src/hooks/useEndpoints.ts @@ -2,7 +2,7 @@ import useSWR, {type Fetcher, type SWRConfiguration} from 'swr'; import {authenticatedFetcher, fetcher} from '../utils/swr.js'; import {useAuthStorage} from './useAuthStorage.ts'; import type {FaunaPresentUser, FaunaUser} from "../fauna-types"; -import type {PortierActionLogEntry, PortierDevice, RawMqttReading} from "../portier-types"; +import type {MqttSensorHistory, PortierActionLogEntry, PortierDevice, RawMqttReading} from "../portier-types"; function useAuthSWR(key: URL | string, config?: SWRConfiguration) { const {accessToken} = useAuthStorage(); @@ -33,6 +33,12 @@ export function useMqttStatus(config?: SWRConfiguration) { return useCheckSWR<{ [topic: string]: RawMqttReading }>(import.meta.env.MQTT_PROXY_URL.concat('status'), config); } +export function useMqttHistory(config?: SWRConfiguration) { + return useCheckSWR<{ + [sensor: string]: MqttSensorHistory + }>(import.meta.env.MQTT_PROXY_URL.concat('sensors/'), config); +} + export function useActionLog(config?: SWRConfiguration) { return useAuthSWR(import.meta.env.PORTIER_URL.concat('api/actionLog/0/0'), config); } diff --git a/src/hooks/useLocale.ts b/src/hooks/useLocale.ts new file mode 100644 index 0000000..9039d7b --- /dev/null +++ b/src/hooks/useLocale.ts @@ -0,0 +1,20 @@ +import {type Dispatch, useEffect} from 'react'; +import {useLocalStorage} from '@uidotdev/usehooks'; +import {useCurrentUser} from './useEndpoints.ts'; +import {useTranslation} from "react-i18next"; + +const LOCALE_KEY = 'locale'; + +export function useLocale(): [string | undefined, Dispatch] { + const {data: user} = useCurrentUser(); + const [locale, setStoredLocale] = useLocalStorage(LOCALE_KEY); + + const {i18n} = useTranslation(); + + useEffect(() => { + i18n.changeLanguage(locale ?? user?.locale ?? 'bg').then(() => {}) + }, [i18n, locale, user?.locale]); + + + return [locale, setStoredLocale]; +} diff --git a/src/hooks/useTheme.ts b/src/hooks/useTheme.ts new file mode 100644 index 0000000..e9987ef --- /dev/null +++ b/src/hooks/useTheme.ts @@ -0,0 +1,8 @@ +import {useLocalStorage, useMediaQuery} from '@uidotdev/usehooks'; + +const THEME_KEY = 'theme'; + +export function useTheme() { + const darkMode = useMediaQuery('(prefers-color-scheme: dark)') ? 'dark' : 'light'; + return useLocalStorage(THEME_KEY, darkMode); +} diff --git a/src/i18n.ts b/src/i18n.ts index 6b17d6d..4a2cf2f 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -1,13 +1,12 @@ import i18n from 'i18next'; import Backend from 'i18next-http-backend'; -import { initReactI18next } from 'react-i18next'; -import { load } from 'js-yaml'; +import {initReactI18next} from 'react-i18next'; +import {load} from 'js-yaml'; i18n .use(Backend) .use(initReactI18next) .init({ - fallbackLng: 'bg', backend: { loadPath: import.meta.env.BASE_URL + 'locales/{{lng}}.yaml', parse: (data: any) => load(data), diff --git a/src/layout/NavBar.tsx b/src/layout/NavBar.tsx index 0d7f3ff..8cd97b1 100644 --- a/src/layout/NavBar.tsx +++ b/src/layout/NavBar.tsx @@ -1,29 +1,36 @@ -import {useEffect} from 'react'; import {NavLink, useLocation} from 'react-router-dom'; import {Container, Image, Nav, Navbar, NavDropdown} from 'react-bootstrap'; import {useTranslation} from 'react-i18next'; import './NavBar.css'; -import i18n from '../i18n.ts'; import {useVariant} from '../hooks/useVariant.ts'; import {useCurrentUser} from '../hooks/useEndpoints.ts'; +import {useTheme} from '../hooks/useTheme.ts'; import RequireRole from '../widgets/RequireRole.tsx'; import RequireVariant from "../widgets/RequireVariant.tsx"; +import {useLocale} from '../hooks/useLocale.ts'; const NavBar = () => { const {t} = useTranslation(); + const [locale, setLocale] = useLocale(); const backendUrl = import.meta.env.OIDC_AUTHORITY_URL; const { data: user, } = useCurrentUser(); const variant = useVariant(); - useEffect(function () { - if (user?.locale) { - i18n.changeLanguage(user.locale).then(() => { - }); - } - }, [user]); + const [theme, setTheme] = useTheme(); + const changeLanguage = async () => + setLocale((!locale || locale == 'bg') ? 'en' : 'bg'); + + const changeTheme = () => setTheme(theme == 'light' ? 'dark' : 'light'); + +// useEffect(function () { +// if (user?.locale) { +// i18n.changeLanguage(user.locale).then(() => { +// }); +// } +// }, [user]); const location = useLocation(); @@ -71,6 +78,12 @@ const NavBar = () => { {t('views.navigation.labbers')} + +