Skip to content

Commit 434c607

Browse files
committed
Graphs, Languages, Themes
- remove the grafana stuff completely and replace it with HASS-style charts - add a language switcher, so it works no matter the fauna language - add a dark mode, so people hating white backgrounds don't use extensions which ens***fy the experience (esp. irt the graphs)
1 parent 82da5f9 commit 434c607

13 files changed

Lines changed: 195 additions & 95 deletions

File tree

public/locales/bg.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ views:
124124
colibri_message_contact: |
125125
Ако имате проблем с ползването му, или е нужно да заявите достъп за нови служители, моля свържете се с Инит Лаб на
126126
имейл us@initlab.org.
127-
colibri_message_emergency: При неотложни случаи, моля обадете се на тел. 0883 433 990 (Венцислав).
127+
colibri_message_emergency: При неотложни случаи, моля обадете се на тел. 02 422 54 36.
128128
devices:
129129
lock: заключи
130130
unlock: отключи
@@ -170,6 +170,8 @@ views:
170170
lights: Осветление
171171
hvac: Климатици
172172
sensors: Графики
173+
dark_mode: Тъмна тема
174+
language: Език
173175
oauth_application_management: OAuth интеграция
174176
oauth_token_management: Упълномощени приложения
175177
registrations:

public/locales/en.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ views:
117117
colibri_message_contact: |
118118
In case of technical difficulties, or you need to request access for new employees, please contact init Lab
119119
at us@initlab.org.
120-
colibri_message_emergency: In case of major malfunction, please call +359 883 433 990 (Vencislav).
120+
colibri_message_emergency: In case of major malfunction, please call +359 2 422 54 36.
121121
devices:
122122
lock: lock
123123
unlock: unlock
@@ -163,6 +163,8 @@ views:
163163
lights: Lights
164164
hvac: HVAC
165165
sensors: Sensors
166+
dark_mode: Dark Mode
167+
language: Language
166168
oauth_application_management: OAuth integration
167169
oauth_token_management: Authorized applications
168170
registrations:

src/App.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {createElement} from 'react';
1+
import {createElement, useEffect} from 'react';
22
import {Container} from 'react-bootstrap';
33
import {Route, Routes} from 'react-router-dom';
44

@@ -14,10 +14,13 @@ import {useVariant} from './hooks/useVariant.ts';
1414
import {getDoorActions, getHvacActions, getLightActions} from "./utils/device.ts";
1515
import Devices from "./pages/Devices.tsx";
1616
import {useDocumentTitle} from '@uidotdev/usehooks';
17+
import {useTheme} from './hooks/useTheme.ts';
1718

1819
function App() {
1920
const variant = useVariant();
2021
useDocumentTitle(variant.title);
22+
const [theme] = useTheme();
23+
useEffect(() => document.documentElement.setAttribute('data-bs-theme', theme), [theme]);
2124

2225
return (<>
2326
<NavBar/>

src/hooks/useEndpoints.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import useSWR, {type Fetcher, type SWRConfiguration} from 'swr';
22
import {authenticatedFetcher, fetcher} from '../utils/swr.js';
33
import {useAuthStorage} from './useAuthStorage.ts';
44
import type {FaunaPresentUser, FaunaUser} from "../fauna-types";
5-
import type {PortierActionLogEntry, PortierDevice, RawMqttReading} from "../portier-types";
5+
import type {MqttSensorHistory, PortierActionLogEntry, PortierDevice, RawMqttReading} from "../portier-types";
66

77
function useAuthSWR<TResult>(key: URL | string, config?: SWRConfiguration) {
88
const {accessToken} = useAuthStorage();
@@ -34,7 +34,9 @@ export function useMqttStatus(config?: SWRConfiguration) {
3434
}
3535

3636
export function useMqttHistory(config?: SWRConfiguration) {
37-
return useCheckSWR<{ [sensor: string]: {[metric: string]: [[number, number]]} }>(import.meta.env.MQTT_PROXY_URL.concat('sensors/'), config);
37+
return useCheckSWR<{
38+
[sensor: string]: MqttSensorHistory
39+
}>(import.meta.env.MQTT_PROXY_URL.concat('sensors/'), config);
3840
}
3941

4042
export function useActionLog(config?: SWRConfiguration) {

src/hooks/useLocale.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import {type Dispatch, useEffect} from 'react';
2+
import {useLocalStorage} from '@uidotdev/usehooks';
3+
import {useCurrentUser} from './useEndpoints.ts';
4+
import {useTranslation} from "react-i18next";
5+
6+
const LOCALE_KEY = 'locale';
7+
8+
export function useLocale(): [string | undefined, Dispatch<string>] {
9+
const {data: user} = useCurrentUser();
10+
const [locale, setStoredLocale] = useLocalStorage<string>(LOCALE_KEY);
11+
12+
const {i18n} = useTranslation();
13+
14+
useEffect(() => {
15+
i18n.changeLanguage(locale ?? user?.locale ?? 'bg').then(() => {})
16+
}, [i18n, locale, user?.locale]);
17+
18+
19+
return [locale, setStoredLocale];
20+
}

src/hooks/useTheme.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import {useLocalStorage, useMediaQuery} from '@uidotdev/usehooks';
2+
3+
const THEME_KEY = 'theme';
4+
5+
export function useTheme() {
6+
const darkMode = useMediaQuery('(prefers-color-scheme: dark)') ? 'dark' : 'light';
7+
return useLocalStorage<string>(THEME_KEY, darkMode);
8+
}

src/i18n.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import i18n from 'i18next';
22
import Backend from 'i18next-http-backend';
3-
import { initReactI18next } from 'react-i18next';
4-
import { load } from 'js-yaml';
3+
import {initReactI18next} from 'react-i18next';
4+
import {load} from 'js-yaml';
55

66
i18n
77
.use(Backend)
88
.use(initReactI18next)
99
.init({
10-
fallbackLng: 'bg',
1110
backend: {
1211
loadPath: import.meta.env.BASE_URL + 'locales/{{lng}}.yaml',
1312
parse: (data: any) => load(data),

src/layout/NavBar.tsx

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,36 @@
1-
import {useEffect} from 'react';
21
import {NavLink, useLocation} from 'react-router-dom';
32
import {Container, Image, Nav, Navbar, NavDropdown} from 'react-bootstrap';
43
import {useTranslation} from 'react-i18next';
54

65
import './NavBar.css';
7-
import i18n from '../i18n.ts';
86
import {useVariant} from '../hooks/useVariant.ts';
97
import {useCurrentUser} from '../hooks/useEndpoints.ts';
8+
import {useTheme} from '../hooks/useTheme.ts';
109
import RequireRole from '../widgets/RequireRole.tsx';
1110
import RequireVariant from "../widgets/RequireVariant.tsx";
11+
import {useLocale} from '../hooks/useLocale.ts';
1212

1313
const NavBar = () => {
1414
const {t} = useTranslation();
15+
const [locale, setLocale] = useLocale();
1516
const backendUrl = import.meta.env.OIDC_AUTHORITY_URL;
1617
const {
1718
data: user,
1819
} = useCurrentUser();
1920
const variant = useVariant();
2021

21-
useEffect(function () {
22-
if (user?.locale) {
23-
i18n.changeLanguage(user.locale).then(() => {
24-
});
25-
}
26-
}, [user]);
22+
const [theme, setTheme] = useTheme();
23+
const changeLanguage = async () =>
24+
setLocale((!locale || locale == 'bg') ? 'en' : 'bg');
25+
26+
const changeTheme = () => setTheme(theme == 'light' ? 'dark' : 'light');
27+
28+
// useEffect(function () {
29+
// if (user?.locale) {
30+
// i18n.changeLanguage(user.locale).then(() => {
31+
// });
32+
// }
33+
// }, [user]);
2734

2835
const location = useLocation();
2936

@@ -71,6 +78,12 @@ const NavBar = () => {
7178
{t('views.navigation.labbers')}
7279
</Nav.Link>
7380
</RequireRole>
81+
</Nav>
82+
<Nav>
83+
<Nav.Link onClick={changeLanguage}><i className="fa-solid fa-language"/>{' '}<span
84+
className={'d-lg-none'}>{t('views.navigation.language')}</span></Nav.Link>
85+
<Nav.Link onClick={changeTheme}><i className="fa-solid fa-circle-half-stroke"/>{' '}<span
86+
className={'d-lg-none'}>{t('views.navigation.dark_mode')}</span></Nav.Link>
7487
{user ? <NavDropdown title={<>
7588
<i className="fa-solid fa-user"/>{' '}
7689
{t('views.navigation.account')}

src/pages/Devices.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import type {PortierDevice} from "../portier-types";
1212
const Devices = ({
1313
deviceGroup,
1414
deviceActionMapper,
15-
}: {deviceGroup: string, deviceActionMapper: (d: PortierDevice) => string[]}) => {
15+
}: { deviceGroup: string, deviceActionMapper: (d: PortierDevice) => string[] }) => {
1616
const {t} = useTranslation();
1717

1818
const {
@@ -36,11 +36,11 @@ const Devices = ({
3636
{devices && <>
3737
{filteredDevices.length > 0 ? filteredDevices.map(device => {
3838
const deviceActions = deviceActionMapper(device);
39-
const isUnavailable = device?.statuses?.available === false;
40-
const isOpen = device?.statuses?.open === true;
39+
const isUnavailable = !device?.statuses?.available;
40+
const isOpen = !!device?.statuses?.open;
4141

42-
return (<Col key={device.id} className={`col-xl-${2*deviceActions.length}`}>
43-
<Card>
42+
return (<Col key={device.id} className={`col-xl-${2 * deviceActions.length}`}>
43+
<Card border="secondary">
4444
<Card.Header
4545
className={'text-start' + (variantName === "initlab" ? ' bg-primary text-light' : '')}>
4646
{device.name}

src/pages/Sensors.tsx

Lines changed: 4 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import {Col, Row} from 'react-bootstrap';
22
import {useTranslation} from 'react-i18next';
3-
import {sensors} from '../config.ts';
43
import {useMqttHistory} from "../hooks/useEndpoints.ts";
5-
import EChartsReact from "echarts-for-react";
4+
import SensorGraph from "../widgets/SensorReadings/SensorGraph.tsx";
65

76
const Sensors = () => {
87
const {t} = useTranslation();
98
const {data} = useMqttHistory();
109

11-
if(!data) return <></>;
10+
if (!data) return <></>;
1211

1312
return <>
1413
<Row>
@@ -17,73 +16,8 @@ const Sensors = () => {
1716
</Col>
1817
</Row>
1918
<Row className="row-cols row-cols-1 row-cols-xxl-2">
20-
21-
{Object.entries(data).map(([sensor, metrics]) => {
22-
23-
const temperature = metrics['sensor_temperature'];
24-
const humidity = metrics['sensor_humidity'];
25-
26-
const matchedSensor = Object.keys(sensors).find(n => n.includes(sensor));
27-
const label = sensors[matchedSensor??''].label ?? sensor;
28-
29-
const yAxis = [];
30-
const series = [];
31-
32-
const parse = (d: [number, number]): [number, number] => [d[0]*1000, d[1]];
33-
34-
if(temperature) {
35-
yAxis.push({
36-
name: t('views.sensors.temperature'),
37-
type: 'value',
38-
interval: 2.5,
39-
axisLabel: {formatter: '{value} °C'},
40-
min: (v: any) => Math.floor(v.min / 5) * 5,
41-
max: (v: any) => Math.ceil(v.max / 5) * 5,
42-
});
43-
series.push({
44-
name: t('views.sensors.temperature'),
45-
type: 'line',
46-
smooth: true,
47-
symbol: 'none',
48-
data: temperature.map(parse),
49-
yAxisIndex: yAxis.length - 1,
50-
tooltip: {valueFormatter: (v: any) => `${v} °C`}
51-
});
52-
}
53-
if(humidity) {
54-
yAxis.push({
55-
name: t('views.sensors.humidity'),
56-
type: 'value',
57-
interval: 2.5,
58-
axisLabel: {formatter: '{value} %'},
59-
min: (v: any) => Math.floor(v.min / 5) * 5,
60-
max: (v: any) => Math.ceil(v.max / 5) * 5,
61-
});
62-
series.push({
63-
name: t('views.sensors.humidity'),
64-
type: 'line',
65-
smooth: true,
66-
symbol: 'none',
67-
data: humidity.map(parse),
68-
yAxisIndex: yAxis.length - 1,
69-
tooltip: {valueFormatter: (v: any) => `${v} %`}
70-
});
71-
}
72-
73-
74-
return <Col key={sensor}><EChartsReact option={{
75-
title: {text: t(`rooms.${label}`), left: 'left'},
76-
tooltip: {
77-
trigger: 'axis'
78-
},
79-
legend: {type: 'plain', left: 'left'},
80-
xAxis: {type: 'time',max:Date.now()},
81-
grid: { left: 0, right: 0, top: 70, bottom: 70 },
82-
yAxis: yAxis,
83-
series: series,
84-
animation: false,
85-
}}/></Col>;
86-
}
19+
{Object.entries(data).map(([sensor, metrics]) =>
20+
<Col key={sensor}><SensorGraph sensor={sensor} metrics={metrics}/></Col>
8721
)}
8822
</Row>
8923
</>;

0 commit comments

Comments
 (0)