diff --git a/backend/Api/BusinessLogic/DeviceLogic.cs b/backend/Api/BusinessLogic/DeviceLogic.cs index 3af9e81..ebb1d0a 100644 --- a/backend/Api/BusinessLogic/DeviceLogic.cs +++ b/backend/Api/BusinessLogic/DeviceLogic.cs @@ -79,19 +79,9 @@ namespace Api.BusinessLogic if (device == null) { return new ConflictObjectResult(new { message = "Could not find device" }); } - var logs = await _dbAccess.ReadLogs(deviceId); + var logs = await _dbAccess.ReadLogs(deviceId, dateTimeRange); - if (logs.Count == 0) { return new ConflictObjectResult(new { message = "Could not find any logs connected to the device" }); } - - if (dateTimeRange.DateTimeStart == dateTimeRange.DateTimeEnd) { return new OkObjectResult(logs); } - - List rangedLogs = new List(); - foreach (var log in logs) - { - if (log.Date <= dateTimeRange.DateTimeStart && log.Date >= dateTimeRange.DateTimeEnd) { rangedLogs.Add(log); } - } - - return new OkObjectResult(rangedLogs); + return new OkObjectResult(logs); } /// diff --git a/backend/Api/Controllers/DeviceController.cs b/backend/Api/Controllers/DeviceController.cs index ada5466..ac3693b 100644 --- a/backend/Api/Controllers/DeviceController.cs +++ b/backend/Api/Controllers/DeviceController.cs @@ -56,8 +56,8 @@ namespace Api.Controllers } else { - dateTimeRange.DateTimeStart = DateTime.Now; - dateTimeRange.DateTimeEnd = dateTimeRange.DateTimeStart; + dateTimeRange.DateTimeStart = DateTime.UnixEpoch; + dateTimeRange.DateTimeEnd = DateTime.Now; } return await _deviceLogic.GetLogs(dateTimeRange, deviceId); } diff --git a/backend/Api/DBAccess/DBAccess.cs b/backend/Api/DBAccess/DBAccess.cs index 08cab71..ad33dfd 100644 --- a/backend/Api/DBAccess/DBAccess.cs +++ b/backend/Api/DBAccess/DBAccess.cs @@ -313,16 +313,11 @@ namespace Api.DBAccess /// Returns the logs from the device /// /// Has the id for the device that the los belong too + /// Return only logs within the specified datetime range /// - public async Task> ReadLogs(int deviceId) + public async Task> ReadLogs(int deviceId, DateTimeRange range) { - var device = await _context.Devices.Include(d => d.Logs).FirstOrDefaultAsync(d => d.Id == deviceId); - - if (device == null || device.Logs == null) { return new List(); } - - var logs = device.Logs; - - return logs; + return _context.Devices.Include(d => d.Logs.Where(l => l.Date > range.DateTimeStart && l.Date < range.DateTimeEnd)).Where(d => d.Id == deviceId).FirstOrDefault().Logs; } /// diff --git a/backend/Api/DBAccess/DBContext.cs b/backend/Api/DBAccess/DBContext.cs index 7f66cdb..993a779 100644 --- a/backend/Api/DBAccess/DBContext.cs +++ b/backend/Api/DBAccess/DBContext.cs @@ -1,4 +1,5 @@ using Microsoft.EntityFrameworkCore; +using Api.Models; using Api.Models.Users; using Api.Models.Devices; diff --git a/backend/Api/Migrations/20250402113522_ChangeModelsNamespace.Designer.cs b/backend/Api/Migrations/20250402113522_ChangeModelsNamespace.Designer.cs new file mode 100644 index 0000000..1000ce4 --- /dev/null +++ b/backend/Api/Migrations/20250402113522_ChangeModelsNamespace.Designer.cs @@ -0,0 +1,140 @@ +// +using System; +using Api; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Api.Migrations +{ + [DbContext(typeof(DBContext))] + [Migration("20250402113522_ChangeModelsNamespace")] + partial class ChangeModelsNamespace + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.3"); + + modelBuilder.Entity("Api.Models.Devices.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ReferenceId") + .HasColumnType("TEXT"); + + b.Property("TempHigh") + .HasColumnType("REAL"); + + b.Property("TempLow") + .HasColumnType("REAL"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Devices"); + }); + + modelBuilder.Entity("Api.Models.TemperatureLogs", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .HasColumnType("INTEGER"); + + b.Property("TempHigh") + .HasColumnType("REAL"); + + b.Property("TempLow") + .HasColumnType("REAL"); + + b.Property("Temperature") + .HasColumnType("REAL"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.ToTable("TemperatureLogs"); + }); + + modelBuilder.Entity("Api.Models.Users.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Email") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Password") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RefreshToken") + .HasColumnType("TEXT"); + + b.Property("RefreshTokenExpiresAt") + .HasColumnType("TEXT"); + + b.Property("Salt") + .HasColumnType("TEXT"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Api.Models.Devices.Device", b => + { + b.HasOne("Api.Models.Users.User", null) + .WithMany("Devices") + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("Api.Models.TemperatureLogs", b => + { + b.HasOne("Api.Models.Devices.Device", null) + .WithMany("Logs") + .HasForeignKey("DeviceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Api.Models.Devices.Device", b => + { + b.Navigation("Logs"); + }); + + modelBuilder.Entity("Api.Models.Users.User", b => + { + b.Navigation("Devices"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/Api/Migrations/20250402113522_ChangeModelsNamespace.cs b/backend/Api/Migrations/20250402113522_ChangeModelsNamespace.cs new file mode 100644 index 0000000..671a740 --- /dev/null +++ b/backend/Api/Migrations/20250402113522_ChangeModelsNamespace.cs @@ -0,0 +1,59 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Api.Migrations +{ + /// + public partial class ChangeModelsNamespace : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_TemperatureLogs_Devices_DeviceId", + table: "TemperatureLogs"); + + migrationBuilder.AlterColumn( + name: "DeviceId", + table: "TemperatureLogs", + type: "INTEGER", + nullable: false, + defaultValue: 0, + oldClrType: typeof(int), + oldType: "INTEGER", + oldNullable: true); + + migrationBuilder.AddForeignKey( + name: "FK_TemperatureLogs_Devices_DeviceId", + table: "TemperatureLogs", + column: "DeviceId", + principalTable: "Devices", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_TemperatureLogs_Devices_DeviceId", + table: "TemperatureLogs"); + + migrationBuilder.AlterColumn( + name: "DeviceId", + table: "TemperatureLogs", + type: "INTEGER", + nullable: true, + oldClrType: typeof(int), + oldType: "INTEGER"); + + migrationBuilder.AddForeignKey( + name: "FK_TemperatureLogs_Devices_DeviceId", + table: "TemperatureLogs", + column: "DeviceId", + principalTable: "Devices", + principalColumn: "Id"); + } + } +} diff --git a/backend/Api/Migrations/20250403070932_RemovedLogsFromDbset.Designer.cs b/backend/Api/Migrations/20250403070932_RemovedLogsFromDbset.Designer.cs new file mode 100644 index 0000000..90225f9 --- /dev/null +++ b/backend/Api/Migrations/20250403070932_RemovedLogsFromDbset.Designer.cs @@ -0,0 +1,138 @@ +// +using System; +using Api; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Api.Migrations +{ + [DbContext(typeof(DBContext))] + [Migration("20250403070932_RemovedLogsFromDbset")] + partial class RemovedLogsFromDbset + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.3"); + + modelBuilder.Entity("Api.Models.Devices.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ReferenceId") + .HasColumnType("TEXT"); + + b.Property("TempHigh") + .HasColumnType("REAL"); + + b.Property("TempLow") + .HasColumnType("REAL"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Devices"); + }); + + modelBuilder.Entity("Api.Models.TemperatureLogs", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .HasColumnType("INTEGER"); + + b.Property("TempHigh") + .HasColumnType("REAL"); + + b.Property("TempLow") + .HasColumnType("REAL"); + + b.Property("Temperature") + .HasColumnType("REAL"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.ToTable("TemperatureLogs"); + }); + + modelBuilder.Entity("Api.Models.Users.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Email") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Password") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RefreshToken") + .HasColumnType("TEXT"); + + b.Property("RefreshTokenExpiresAt") + .HasColumnType("TEXT"); + + b.Property("Salt") + .HasColumnType("TEXT"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Api.Models.Devices.Device", b => + { + b.HasOne("Api.Models.Users.User", null) + .WithMany("Devices") + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("Api.Models.TemperatureLogs", b => + { + b.HasOne("Api.Models.Devices.Device", null) + .WithMany("Logs") + .HasForeignKey("DeviceId"); + }); + + modelBuilder.Entity("Api.Models.Devices.Device", b => + { + b.Navigation("Logs"); + }); + + modelBuilder.Entity("Api.Models.Users.User", b => + { + b.Navigation("Devices"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/Api/Migrations/20250403070932_RemovedLogsFromDbset.cs b/backend/Api/Migrations/20250403070932_RemovedLogsFromDbset.cs new file mode 100644 index 0000000..17c98c1 --- /dev/null +++ b/backend/Api/Migrations/20250403070932_RemovedLogsFromDbset.cs @@ -0,0 +1,59 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Api.Migrations +{ + /// + public partial class RemovedLogsFromDbset : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_TemperatureLogs_Devices_DeviceId", + table: "TemperatureLogs"); + + migrationBuilder.AlterColumn( + name: "DeviceId", + table: "TemperatureLogs", + type: "INTEGER", + nullable: true, + oldClrType: typeof(int), + oldType: "INTEGER"); + + migrationBuilder.AddForeignKey( + name: "FK_TemperatureLogs_Devices_DeviceId", + table: "TemperatureLogs", + column: "DeviceId", + principalTable: "Devices", + principalColumn: "Id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_TemperatureLogs_Devices_DeviceId", + table: "TemperatureLogs"); + + migrationBuilder.AlterColumn( + name: "DeviceId", + table: "TemperatureLogs", + type: "INTEGER", + nullable: false, + defaultValue: 0, + oldClrType: typeof(int), + oldType: "INTEGER", + oldNullable: true); + + migrationBuilder.AddForeignKey( + name: "FK_TemperatureLogs_Devices_DeviceId", + table: "TemperatureLogs", + column: "DeviceId", + principalTable: "Devices", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/backend/Api/Migrations/DBContextModelSnapshot.cs b/backend/Api/Migrations/DBContextModelSnapshot.cs index 6d96bdd..f48f59b 100644 --- a/backend/Api/Migrations/DBContextModelSnapshot.cs +++ b/backend/Api/Migrations/DBContextModelSnapshot.cs @@ -17,7 +17,7 @@ namespace Api.Migrations #pragma warning disable 612, 618 modelBuilder.HasAnnotation("ProductVersion", "9.0.3"); - modelBuilder.Entity("Api.Models.Device", b => + modelBuilder.Entity("Api.Models.Devices.Device", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -74,7 +74,7 @@ namespace Api.Migrations b.ToTable("TemperatureLogs"); }); - modelBuilder.Entity("Api.Models.User", b => + modelBuilder.Entity("Api.Models.Users.User", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -106,26 +106,26 @@ namespace Api.Migrations b.ToTable("Users"); }); - modelBuilder.Entity("Api.Models.Device", b => + modelBuilder.Entity("Api.Models.Devices.Device", b => { - b.HasOne("Api.Models.User", null) + b.HasOne("Api.Models.Users.User", null) .WithMany("Devices") .HasForeignKey("UserId"); }); modelBuilder.Entity("Api.Models.TemperatureLogs", b => { - b.HasOne("Api.Models.Device", null) + b.HasOne("Api.Models.Devices.Device", null) .WithMany("Logs") .HasForeignKey("DeviceId"); }); - modelBuilder.Entity("Api.Models.Device", b => + modelBuilder.Entity("Api.Models.Devices.Device", b => { b.Navigation("Logs"); }); - modelBuilder.Entity("Api.Models.User", b => + modelBuilder.Entity("Api.Models.Users.User", b => { b.Navigation("Devices"); }); diff --git a/docs/Domainmodel.png b/docs/Domainmodel.png new file mode 100644 index 0000000..2dec779 Binary files /dev/null and b/docs/Domainmodel.png differ diff --git a/docs/TempAlarmModelDiagram.png b/docs/TempAlarmModelDiagram.png index f607443..af323e5 100644 Binary files a/docs/TempAlarmModelDiagram.png and b/docs/TempAlarmModelDiagram.png differ diff --git a/frontend/home/index.html b/frontend/home/index.html index f40b119..59774b7 100644 --- a/frontend/home/index.html +++ b/frontend/home/index.html @@ -5,8 +5,8 @@ Temperature-Alarm-Web - - + + @@ -26,8 +26,40 @@
- +
+ +
+
+
+ + +
+
+ +
+
+ +
+
Today
+
Last 3 days
+
Last week
+
All time
+
+
+ +
+ + +
+ +
+ + +
+
+
+
diff --git a/frontend/scripts/home.js b/frontend/scripts/home.js index 44142a7..e615b9c 100644 --- a/frontend/scripts/home.js +++ b/frontend/scripts/home.js @@ -2,40 +2,7 @@ import { logout } from "../shared/utils.js"; import { getUser } from "../shared/utils.js"; import { getDevices, getLogsOnDeviceId } from "./services/devices.service.js"; -async function buildChart(data) { - const xValues = data.map((log) => - new Date(log.date).toLocaleString() - ); // Full Date labels - const yValues = data.map((log) => log.temperature); // Temperature values - new Chart("myChart", { - type: "line", - data: { - labels: xValues, - datasets: [ - { - label: "Temperature", - fill: false, - lineTension: 0.4, - backgroundColor: "rgba(0,0,255,1.0)", - borderColor: "rgba(0,0,255,0.1)", - data: yValues, - }, - ], - }, - options: { - tooltips: { - callbacks: { - title: function (tooltipItem) { - return `Date: ${tooltipItem[0].label}`; - }, - label: function (tooltipItem) { - return `Temperature: ${tooltipItem.value}°C`; - }, - }, - }, - }, - }); -} +let chart; const TABLE_PAGINATION_SIZE = 30; @@ -62,8 +29,9 @@ function buildTable(data, offset = 0) { color = "tempNormal"; } - const date = new Date(log.date).toLocaleDateString(); - const time = new Date(log.date).toLocaleTimeString(); + const parsedDate = luxon.DateTime.fromISO(log.date).setZone("Europe/Copenhagen").setLocale("gb"); + const date = parsedDate.toLocaleString(luxon.DateTime.DATE_SHORT); + const time = parsedDate.toLocaleString(luxon.DateTime.TIME_WITH_SECONDS); document.getElementById("table-body").innerHTML += ` @@ -97,14 +65,39 @@ function handleError(err) { document.getElementById("container").style.display = "none"; } -async function fetchData(startDate = null, endDate = null) { - const devices = await getDevices() - .catch(handleError); +function addDeviceToDropdown(device) { + const opt = document.createElement("option"); + opt.innerText = `${device.name} (${device.referenceId})`; + opt.value = device.id; + document.getElementById("device-selector").appendChild(opt); +} + +function randomColorChannelValue() { + return Math.floor(Math.random() * 256); +} + +function isSameDay(a, b) { + return a.getFullYear() === b.getFullYear() && + a.getMonth() === b.getMonth() && + a.getDate() === b.getDate(); +} + +function localToUTC(date) { + if (!date) return null; + + return luxon.DateTime.fromISO(date, { zone: "Europe/Copenhagen" }).setZone("UTC"); +} + +async function fetchData() { + document.body.classList.add("loading"); + + const startDate = localToUTC(document.getElementById("period-start").value); + const endDate = localToUTC(document.getElementById("period-end").value); const deviceData = []; for (const device of devices) { - const data = await getLogsOnDeviceId(device.id) + const data = await getLogsOnDeviceId(device.id, startDate, endDate) .catch(handleError); deviceData.push(data); @@ -114,47 +107,114 @@ async function fetchData(startDate = null, endDate = null) { return; } + document.getElementById("table-body").innerHTML = ""; buildTable(deviceData[0]); - new Chart("myChart", { - type: "line", - data: { - datasets: deviceData.map(dataset => ({ - label: "Temperature", - fill: false, - lineTension: 0.4, - backgroundColor: "rgba(0,0,255,1.0)", - borderColor: "rgba(0,0,255,0.1)", - data: dataset.map(log => ({ - x: new Date(log.date).getTime(), - y: log.temperature, - })), - })), - }, - options: { - parsing: false, - plugins: { - tooltip: { - callbacks: { - label: item => `Temperature: ${item.formattedValue}°C`, + if (!chart) { + chart = new Chart("chart", { + type: "line", + data: { + datasets: [], + }, + options: { + responsive: false, + parsing: false, + plugins: { + tooltip: { + callbacks: { + label: item => `Temperature: ${item.formattedValue}°C`, + }, + }, + decimation: { + enabled: true, + algorithm: "lttb", + samples: window.innerWidth / 2, }, }, - decimation: { - enabled: true, - algorithm: "lttb", - samples: window.innerWidth / 2, + scales: { + x: { + type: "time", + time: { + displayFormats: { + hour: "HH:mm", + }, + }, + adapters: { + date: { + zone: "Europe/Copenhagen", + }, + }, + }, }, }, - scales: { - x: { - type: "time", - }, - }, - }, + }); + } + + chart.options.scales.x.time.unit = isSameDay(new Date(startDate), new Date(endDate)) + ? "hour" + : "day"; + + chart.data.datasets = deviceData.map((dataset, i) => { + const color = new Array(3) + .fill(null) + .map(randomColorChannelValue) + .join(","); + + return { + label: devices[i].name, + fill: false, + lineTension: 0.4, + backgroundColor: `rgba(${color}, 1.0)`, + borderColor: `rgba(${color}, 0.1)`, + data: dataset.map(log => ({ + x: new Date(log.date).getTime(), + y: log.temperature, + })), + }; }); + + chart.update(); + + document.body.classList.remove("loading"); } -fetchData(); +function setPeriod(start, end) { + const startDate = start && new Date(start); + startDate?.setMinutes(startDate.getMinutes() - startDate.getTimezoneOffset()); + + const endDate = start && new Date(end); + endDate?.setMinutes(endDate.getMinutes() - endDate.getTimezoneOffset()); + + document.getElementById("period-start").value = startDate?.toISOString().slice(0, 16); + document.getElementById("period-end").value = endDate?.toISOString().slice(0, 16); + + fetchData(); +} + +function setPeriodLastDays(days) { + const start = new Date() + start.setDate(new Date().getDate() - days); + start.setHours(0, 0, 0, 0); + setPeriod(start, new Date().setHours(23, 59, 0, 0)); +} + +for (const elem of document.getElementsByClassName("last-x-days")) { + elem.onclick = event => setPeriodLastDays(event.target.dataset.days); +} + +for (const elem of document.querySelectorAll("#period-start, #period-end")) { + elem.onchange = fetchData; +} + +document.getElementById("all-time").onclick = () => setPeriod(null, null); document.querySelector(".logout-container").addEventListener("click", logout); +const devices = await getDevices().catch(handleError); +for (const device of devices) { + addDeviceToDropdown(device); +} + +setPeriodLastDays(3); +fetchData(); + diff --git a/frontend/scripts/services/devices.service.js b/frontend/scripts/services/devices.service.js index f5b93a3..bc756a6 100644 --- a/frontend/scripts/services/devices.service.js +++ b/frontend/scripts/services/devices.service.js @@ -20,6 +20,7 @@ export function update(deviceId, name, temphigh, tempLow) { }); } -export function getLogsOnDeviceId(id) { - return request("GET", `/device/logs/${id}`); +export function getLogsOnDeviceId(id, startDate = null, endDate = null) { + const query = startDate && endDate ? `?dateTimeStart=${startDate}&dateTimeEnd=${endDate}` : ""; + return request("GET", `/device/logs/${id}${query}`); } diff --git a/frontend/styles/home.css b/frontend/styles/home.css index b2753e2..85eea8a 100644 --- a/frontend/styles/home.css +++ b/frontend/styles/home.css @@ -4,6 +4,18 @@ body { background-color: #F9F9F9; } +.loading * { + pointer-events: none; +} + +.loading #chart, .loading table { + opacity: 50%; +} + +.loading select, .loading input { + color: rgba(0, 0, 0, 0.3); +} + #container { margin: 0 2rem; } @@ -13,16 +25,30 @@ body { border-radius: 8px; box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.1); border: 1px solid #DDD; + margin-bottom: 2rem; + background-color: white; +} + +#filters { + margin: 2rem auto; + display: flex; + justify-content: space-between; +} + +#filters > * { + display: flex; + gap: 1rem; + align-items: center; } table { font-family: arial, sans-serif; border-collapse: collapse; width: 100%; - background-color: white; color: #616161; font-family: arial, sans-serif; border-collapse: collapse; + transition: opacity ease-in 100ms; } td, @@ -50,6 +76,9 @@ table .temperature { table tr:not(:last-child) .temperature { border-bottom-color: white; } +td { + white-space: nowrap; +} .tempHigh { background-color: #ff0000; @@ -82,6 +111,12 @@ table tr:not(:last-child) .temperature { box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.1); } +#chart { + width: 100%; + height: 400px; + transition: opacity ease-in 100ms; +} + #error { margin: 2rem; } @@ -92,6 +127,7 @@ table tr:not(:last-child) .temperature { padding: 0.5rem; color: #616161; background-color: white; + font-size: 0.85rem; cursor: pointer; transition-duration: 100ms; } @@ -104,3 +140,43 @@ table tr:not(:last-child) .temperature { background-color: #FCFCFC; } +.form-control { + display: flex; + flex-direction: column; +} + +label { + font-size: 0.85rem; + padding-left: 0.2rem; + color: #616161; +} + +select, input { + background-color: white; + color: #424242; + border: 1px solid #DDD; + border-radius: 8px; + padding: 0.5rem 1rem; + transition: color ease-in 100ms; +} + +select:focus, input:focus { + border-color: #1E88E5; +} + +#period-templates { + display: flex; + gap: 1rem; +} + +.period-template { + font-size: 0.85rem; + color: #616161; + cursor: pointer; + transition-duration: 100ms; +} + +.period-template:hover { + color: #212121; +} +