diff --git a/src/app/app.component.css b/src/app/app.component.css index 81ada23..e69de29 100644 --- a/src/app/app.component.css +++ b/src/app/app.component.css @@ -1,173 +0,0 @@ -.container { - max-width: 1200px; - margin: 0 auto; - padding: 20px; - font-family: Arial, sans-serif; -} - -.controls { - display: flex; - justify-content: space-between; - margin-bottom: 20px; - flex-wrap: wrap; - gap: 10px; -} - -.form-group { - margin-bottom: 10px; - flex: 1; - min-width: 200px; -} - -label { - display: block; - margin-bottom: 5px; - font-weight: bold; -} - -/* Fixed height for input/select elements */ -select, input { - width: 100%; - padding: 8px; - border: 1px solid #ccc; - border-radius: 4px; - box-sizing: border-box; - height: 40px; /* Set a fixed height for both */ - line-height: 24px; /* Consistent line height */ - font-size: 14px; /* Consistent font size */ - appearance: auto; /* Preserve native appearance but ensure consistent sizing */ -} - -.date-input { - width: 100%; - max-width: 100%; -} - -.chart-container { - background-color: #f9f9f9; - border-radius: 8px; - padding: 20px; - min-height: 400px; - margin-bottom: 20px; -} - -.loading, .error { - display: flex; - justify-content: center; - align-items: center; - height: 400px; -} - -.error { - color: #d9534f; -} - -.price-list { - margin-top: 30px; -} - -table { - width: 100%; - border-collapse: collapse; -} - -th, td { - padding: 10px; - text-align: left; - border-bottom: 1px solid #ddd; -} - -th { - background-color: #f2f2f2; -} - -tr:hover { - background-color: #f5f5f5; -} - -/* Price summary styles - similar to the image */ -.price-summary { - background-color: #f9f9f9; - border-radius: 8px; - padding: 20px; - margin-bottom: 20px; - border: 1px solid #e0e0e0; -} - -.price-heading { - margin-bottom: 10px; -} - -.price-heading h2 { - font-size: 1.2rem; - margin: 0; - color: #1d5631; -} - -.price-subheading { - font-size: 0.8rem; - color: #666; - margin: 5px 0 15px 0; - font-style: italic; -} - -.current-price { - display: flex; - align-items: baseline; - margin-bottom: 15px; -} - -.price-label { - font-size: 1rem; - color: #1d5631; - margin-right: 15px; -} - -.price-value { - font-size: 2rem; - font-weight: bold; - color: #1d5631; - margin-right: 15px; -} - -.price-unit { - font-size: 1rem; - font-weight: normal; -} - -.price-change { - font-size: 1rem; - font-weight: bold; - padding: 2px 8px; - border-radius: 4px; -} - -.price-increase { - background-color: #ff6b6b; - color: white; -} - -.price-decrease { - background-color: #51cf66; - color: white; -} - -.price-extremes { - display: flex; - flex-wrap: wrap; - gap: 20px; - font-size: 0.9rem; - color: #555; -} - -.price-high { - color: #e03131; -} - -.price-low { - color: #2b8a3e; -} - -.price-average { - color: #555; -} diff --git a/src/app/app.component.html b/src/app/app.component.html index edddf25..95cc6cb 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,100 +1,7 @@ -
-
-

{{ title }}

-
- -
-
- - -
- -
- - -
-
- - @if (!loading && !error && priceData.length > 0) { -
-
-

{{ getRegionName(selectedRegion) }} {{ formattedDisplayDate }}

-

(utan moms och andra skatter)

-
- -
-
Just nu
-
{{ currentPrice?.SEK_per_kWh || 0 | number:'1.2-2' }} kr/kWh
-
- {{ (currentPrice?.SEK_per_kWh || 0) > averagePrice ? '+' : '' }}{{ ((currentPrice?.SEK_per_kWh || 0) - averagePrice) | number:'1.2-2' }}% -
-
- -
-
- ↑ {{ highestPrice?.SEK_per_kWh || 0 | number:'1.2-2' }} kr kl {{ highestPrice?.time_start | date:'HH:mm' }} -
-
- ↓ {{ lowestPrice?.SEK_per_kWh || 0 | number:'1.2-2' }} kr kl {{ lowestPrice?.time_start | date:'HH:mm' }} -
-
- {{ averagePrice | number:'1.2-2' }} kr snitt -
-
-
- } - -
- @if (loading) { -
-

Loading energy price data...

-
- } - - @if (error) { -
-

{{ error }}

-
- } - - @if (!loading && !error) { - - } -
- - @if (!loading && !error && priceData.length > 0) { -
-

Hour-by-hour prices

- - - - - - - - - - @for (price of priceData; track price.time_start) { - - - - - - } - -
TimeSEK/kWhEUR/kWh
{{ price.time_start | date:'HH:00' }} - {{ price.time_end | date:'HH:00' }}{{ price.SEK_per_kWh | number:'1.2-4' }}{{ price.EUR_per_kWh | number:'1.2-4' }}
-
- } +
+ +
+ +
+
diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 63cf7e4..fd1c593 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,140 +1,16 @@ -import { Component, OnInit, inject } from '@angular/core'; -import { CommonModule, DatePipe } from '@angular/common'; -import { FormsModule } from '@angular/forms'; -import { EnergyPriceService, EnergyPrice } from './energy-price.service'; -import { EnergyChartComponent } from './energy-chart/energy-chart.component'; +import { Component } from '@angular/core'; +import { provideRouter } from '@angular/router'; +import { HeaderComponent } from './components/header/header.component'; +import { FooterComponent } from './components/footer/footer.component'; +import { RouterOutlet } from '@angular/router'; @Component({ selector: 'app-root', - templateUrl: './app.component.html', - styleUrls: ['./app.component.css'], standalone: true, - imports: [CommonModule, FormsModule, EnergyChartComponent], - providers: [DatePipe] + imports: [HeaderComponent, FooterComponent, RouterOutlet], + templateUrl: './app.component.html', + styleUrls: ['./app.component.css'] }) -export class AppComponent implements OnInit { +export class AppComponent { title = 'Energy Price Dashboard'; - priceData: EnergyPrice[] = []; - loading = true; - error = ''; - - regions = [ - { value: 'SE1', label: 'Luleå / Norra Sverige' }, - { value: 'SE2', label: 'Sundsvall / Norra Mellansverige' }, - { value: 'SE3', label: 'Stockholm / Södra Mellansverige' }, - { value: 'SE4', label: 'Malmö / Södra Sverige' } - ]; - - // Default values - selectedDate = new Date(); - // Default will be overridden by localStorage if available - selectedRegion = this.regions[Math.floor(Math.random() * this.regions.length)].value; - - private energyPriceService = inject(EnergyPriceService); - private datePipe = inject(DatePipe); - - ngOnInit() { - // Load saved region from localStorage if available - const savedRegion = localStorage.getItem('selectedRegion'); - if (savedRegion) { - this.selectedRegion = savedRegion; - } - - this.loadPriceData(); - } - - loadPriceData() { - this.loading = true; - this.error = ''; - - const { year, month, day } = this.energyPriceService.formatDate(this.selectedDate); - - this.energyPriceService.getPrices(year, month, day, this.selectedRegion) - .subscribe({ - next: (data) => { - this.priceData = data; - this.loading = false; - }, - error: (err) => { - this.error = 'Failed to load energy price data. Please try again later.'; - this.loading = false; - console.error('Error fetching price data:', err); - } - }); - } - - onRegionChange() { - // Save selected region to localStorage - localStorage.setItem('selectedRegion', this.selectedRegion); - this.loadPriceData(); - } - - onDateChange(event: Event) { - // Fix: properly handle date input event - const inputElement = event.target as HTMLInputElement; - if (inputElement.value) { - this.selectedDate = new Date(inputElement.value); - this.loadPriceData(); - } - } - - get maxDate(): string { - const today = new Date(); - return today.toISOString().split('T')[0]; - } - - get formattedDate(): string { - return this.selectedDate.toISOString().split('T')[0]; - } - - get formattedDisplayDate(): string { - return this.datePipe.transform(this.selectedDate, 'd MMMM yyyy') || ''; - } - - get currentPrice(): EnergyPrice | null { - if (!this.priceData || this.priceData.length === 0) { - return null; - } - - const now = new Date(); - const currentHour = now.getHours(); - - // Find the price for the current hour - return this.priceData.find(price => { - const priceHour = new Date(price.time_start).getHours(); - return priceHour === currentHour; - }) || this.priceData[0]; // Default to first price if not found - } - - get averagePrice(): number { - if (!this.priceData || this.priceData.length === 0) { - return 0; - } - - const sum = this.priceData.reduce((total, price) => total + price.SEK_per_kWh, 0); - return sum / this.priceData.length; - } - - get highestPrice(): EnergyPrice | null { - if (!this.priceData || this.priceData.length === 0) { - return null; - } - - return this.priceData.reduce((max, price) => - price.SEK_per_kWh > max.SEK_per_kWh ? price : max, this.priceData[0]); - } - - get lowestPrice(): EnergyPrice | null { - if (!this.priceData || this.priceData.length === 0) { - return null; - } - - return this.priceData.reduce((min, price) => - price.SEK_per_kWh < min.SEK_per_kWh ? price : min, this.priceData[0]); - } - - getRegionName(regionCode: string): string { - const region = this.regions.find(r => r.value === regionCode); - return region ? region.label : regionCode; - } } diff --git a/src/app/app.config.ts b/src/app/app.config.ts index 6896639..e475f53 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -2,8 +2,15 @@ import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core'; import { provideRouter } from '@angular/router'; import { routes } from './app.routes'; + +import { provideHttpClient } from '@angular/common/http'; import { provideCharts, withDefaultRegisterables } from 'ng2-charts'; export const appConfig: ApplicationConfig = { - providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes), provideCharts(withDefaultRegisterables())] + providers: [ + provideZoneChangeDetection({ eventCoalescing: true }), + provideRouter(routes), + provideCharts(withDefaultRegisterables()), + provideHttpClient(), + ] }; diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index dc39edb..afc6859 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -1,3 +1,7 @@ import { Routes } from '@angular/router'; +import { HomeComponent } from './pages/home/home.component'; -export const routes: Routes = []; +export const routes: Routes = [ + { path: '', component: HomeComponent }, + { path: '**', redirectTo: '' } +]; diff --git a/src/app/energy-chart/energy-chart.component.css b/src/app/components/energy-chart/energy-chart.component.css similarity index 100% rename from src/app/energy-chart/energy-chart.component.css rename to src/app/components/energy-chart/energy-chart.component.css diff --git a/src/app/energy-chart/energy-chart.component.html b/src/app/components/energy-chart/energy-chart.component.html similarity index 100% rename from src/app/energy-chart/energy-chart.component.html rename to src/app/components/energy-chart/energy-chart.component.html diff --git a/src/app/energy-chart/energy-chart.component.ts b/src/app/components/energy-chart/energy-chart.component.ts similarity index 97% rename from src/app/energy-chart/energy-chart.component.ts rename to src/app/components/energy-chart/energy-chart.component.ts index dc9849e..7655210 100644 --- a/src/app/energy-chart/energy-chart.component.ts +++ b/src/app/components/energy-chart/energy-chart.component.ts @@ -2,7 +2,7 @@ import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; import { CommonModule } from '@angular/common'; import { ChartConfiguration, ChartData, ChartType } from 'chart.js'; import { BaseChartDirective } from 'ng2-charts'; -import { EnergyPrice } from '../energy-price.service'; +import { EnergyPrice } from '../../services/energy-price.service'; @Component({ selector: 'app-energy-chart', diff --git a/src/app/components/footer/footer.component.html b/src/app/components/footer/footer.component.html new file mode 100644 index 0000000..8829ba6 --- /dev/null +++ b/src/app/components/footer/footer.component.html @@ -0,0 +1,73 @@ + diff --git a/src/app/components/footer/footer.component.ts b/src/app/components/footer/footer.component.ts new file mode 100644 index 0000000..1996691 --- /dev/null +++ b/src/app/components/footer/footer.component.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core'; +import { RouterModule, RouterLink } from '@angular/router'; +import { CommonModule } from '@angular/common'; + +@Component({ + selector: 'app-footer', + standalone: true, + imports: [RouterModule, RouterLink, CommonModule], + templateUrl: './footer.component.html' +}) +export class FooterComponent { + currentYear = new Date().getFullYear(); +} diff --git a/src/app/components/header/header.component.html b/src/app/components/header/header.component.html new file mode 100644 index 0000000..85a53b8 --- /dev/null +++ b/src/app/components/header/header.component.html @@ -0,0 +1,34 @@ +
+
+
+ + + + +

Energy Price Dashboard

+
+ + + + + +
+ + +
+ Home + About + FAQ + Contact +
+
+
diff --git a/src/app/components/header/header.component.ts b/src/app/components/header/header.component.ts new file mode 100644 index 0000000..5086c29 --- /dev/null +++ b/src/app/components/header/header.component.ts @@ -0,0 +1,17 @@ +import { Component } from '@angular/core'; +import { RouterModule, RouterLink, RouterLinkActive } from '@angular/router'; +import { CommonModule } from '@angular/common'; + +@Component({ + selector: 'app-header', + standalone: true, + imports: [RouterModule, RouterLink, RouterLinkActive, CommonModule], + templateUrl: './header.component.html' +}) +export class HeaderComponent { + isMenuOpen = false; + + toggleMenu() { + this.isMenuOpen = !this.isMenuOpen; + } +} diff --git a/src/app/pages/home/home.component.css b/src/app/pages/home/home.component.css new file mode 100644 index 0000000..81ada23 --- /dev/null +++ b/src/app/pages/home/home.component.css @@ -0,0 +1,173 @@ +.container { + max-width: 1200px; + margin: 0 auto; + padding: 20px; + font-family: Arial, sans-serif; +} + +.controls { + display: flex; + justify-content: space-between; + margin-bottom: 20px; + flex-wrap: wrap; + gap: 10px; +} + +.form-group { + margin-bottom: 10px; + flex: 1; + min-width: 200px; +} + +label { + display: block; + margin-bottom: 5px; + font-weight: bold; +} + +/* Fixed height for input/select elements */ +select, input { + width: 100%; + padding: 8px; + border: 1px solid #ccc; + border-radius: 4px; + box-sizing: border-box; + height: 40px; /* Set a fixed height for both */ + line-height: 24px; /* Consistent line height */ + font-size: 14px; /* Consistent font size */ + appearance: auto; /* Preserve native appearance but ensure consistent sizing */ +} + +.date-input { + width: 100%; + max-width: 100%; +} + +.chart-container { + background-color: #f9f9f9; + border-radius: 8px; + padding: 20px; + min-height: 400px; + margin-bottom: 20px; +} + +.loading, .error { + display: flex; + justify-content: center; + align-items: center; + height: 400px; +} + +.error { + color: #d9534f; +} + +.price-list { + margin-top: 30px; +} + +table { + width: 100%; + border-collapse: collapse; +} + +th, td { + padding: 10px; + text-align: left; + border-bottom: 1px solid #ddd; +} + +th { + background-color: #f2f2f2; +} + +tr:hover { + background-color: #f5f5f5; +} + +/* Price summary styles - similar to the image */ +.price-summary { + background-color: #f9f9f9; + border-radius: 8px; + padding: 20px; + margin-bottom: 20px; + border: 1px solid #e0e0e0; +} + +.price-heading { + margin-bottom: 10px; +} + +.price-heading h2 { + font-size: 1.2rem; + margin: 0; + color: #1d5631; +} + +.price-subheading { + font-size: 0.8rem; + color: #666; + margin: 5px 0 15px 0; + font-style: italic; +} + +.current-price { + display: flex; + align-items: baseline; + margin-bottom: 15px; +} + +.price-label { + font-size: 1rem; + color: #1d5631; + margin-right: 15px; +} + +.price-value { + font-size: 2rem; + font-weight: bold; + color: #1d5631; + margin-right: 15px; +} + +.price-unit { + font-size: 1rem; + font-weight: normal; +} + +.price-change { + font-size: 1rem; + font-weight: bold; + padding: 2px 8px; + border-radius: 4px; +} + +.price-increase { + background-color: #ff6b6b; + color: white; +} + +.price-decrease { + background-color: #51cf66; + color: white; +} + +.price-extremes { + display: flex; + flex-wrap: wrap; + gap: 20px; + font-size: 0.9rem; + color: #555; +} + +.price-high { + color: #e03131; +} + +.price-low { + color: #2b8a3e; +} + +.price-average { + color: #555; +} diff --git a/src/app/pages/home/home.component.html b/src/app/pages/home/home.component.html new file mode 100644 index 0000000..edddf25 --- /dev/null +++ b/src/app/pages/home/home.component.html @@ -0,0 +1,100 @@ +
+
+

{{ title }}

+
+ +
+
+ + +
+ +
+ + +
+
+ + @if (!loading && !error && priceData.length > 0) { +
+
+

{{ getRegionName(selectedRegion) }} {{ formattedDisplayDate }}

+

(utan moms och andra skatter)

+
+ +
+
Just nu
+
{{ currentPrice?.SEK_per_kWh || 0 | number:'1.2-2' }} kr/kWh
+
+ {{ (currentPrice?.SEK_per_kWh || 0) > averagePrice ? '+' : '' }}{{ ((currentPrice?.SEK_per_kWh || 0) - averagePrice) | number:'1.2-2' }}% +
+
+ +
+
+ ↑ {{ highestPrice?.SEK_per_kWh || 0 | number:'1.2-2' }} kr kl {{ highestPrice?.time_start | date:'HH:mm' }} +
+
+ ↓ {{ lowestPrice?.SEK_per_kWh || 0 | number:'1.2-2' }} kr kl {{ lowestPrice?.time_start | date:'HH:mm' }} +
+
+ {{ averagePrice | number:'1.2-2' }} kr snitt +
+
+
+ } + +
+ @if (loading) { +
+

Loading energy price data...

+
+ } + + @if (error) { +
+

{{ error }}

+
+ } + + @if (!loading && !error) { + + } +
+ + @if (!loading && !error && priceData.length > 0) { +
+

Hour-by-hour prices

+ + + + + + + + + + @for (price of priceData; track price.time_start) { + + + + + + } + +
TimeSEK/kWhEUR/kWh
{{ price.time_start | date:'HH:00' }} - {{ price.time_end | date:'HH:00' }}{{ price.SEK_per_kWh | number:'1.2-4' }}{{ price.EUR_per_kWh | number:'1.2-4' }}
+
+ } +
diff --git a/src/app/pages/home/home.component.ts b/src/app/pages/home/home.component.ts new file mode 100644 index 0000000..52f748a --- /dev/null +++ b/src/app/pages/home/home.component.ts @@ -0,0 +1,144 @@ +import { Component, OnInit, inject } from '@angular/core'; +import { CommonModule, DatePipe } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { EnergyPriceService, EnergyPrice } from '../../services/energy-price.service'; +import { EnergyChartComponent } from '../../components/energy-chart/energy-chart.component'; + +@Component({ + selector: 'app-home', + templateUrl: './home.component.html', + styleUrls: ['./home.component.css'], + standalone: true, + imports: [ + CommonModule, + FormsModule, + EnergyChartComponent, + ], + providers: [DatePipe] +}) +export class HomeComponent implements OnInit { + title = 'Energy Price Dashboard'; + priceData: EnergyPrice[] = []; + loading = true; + error = ''; + + regions = [ + { value: 'SE1', label: 'Luleå / Norra Sverige' }, + { value: 'SE2', label: 'Sundsvall / Norra Mellansverige' }, + { value: 'SE3', label: 'Stockholm / Södra Mellansverige' }, + { value: 'SE4', label: 'Malmö / Södra Sverige' } + ]; + + // Default values + selectedDate = new Date(); + // Default will be overridden by localStorage if available + selectedRegion = this.regions[Math.floor(Math.random() * this.regions.length)].value; + + private energyPriceService = inject(EnergyPriceService); + private datePipe = inject(DatePipe); + + ngOnInit() { + // Load saved region from localStorage if available + const savedRegion = localStorage.getItem('selectedRegion'); + if (savedRegion) { + this.selectedRegion = savedRegion; + } + + this.loadPriceData(); + } + + loadPriceData() { + this.loading = true; + this.error = ''; + + const { year, month, day } = this.energyPriceService.formatDate(this.selectedDate); + + this.energyPriceService.getPrices(year, month, day, this.selectedRegion) + .subscribe({ + next: (data) => { + this.priceData = data; + this.loading = false; + }, + error: (err) => { + this.error = 'Failed to load energy price data. Please try again later.'; + this.loading = false; + console.error('Error fetching price data:', err); + } + }); + } + + onRegionChange() { + // Save selected region to localStorage + localStorage.setItem('selectedRegion', this.selectedRegion); + this.loadPriceData(); + } + + onDateChange(event: Event) { + // Fix: properly handle date input event + const inputElement = event.target as HTMLInputElement; + if (inputElement.value) { + this.selectedDate = new Date(inputElement.value); + this.loadPriceData(); + } + } + + get maxDate(): string { + const today = new Date(); + return today.toISOString().split('T')[0]; + } + + get formattedDate(): string { + return this.selectedDate.toISOString().split('T')[0]; + } + + get formattedDisplayDate(): string { + return this.datePipe.transform(this.selectedDate, 'd MMMM yyyy') || ''; + } + + get currentPrice(): EnergyPrice | null { + if (!this.priceData || this.priceData.length === 0) { + return null; + } + + const now = new Date(); + const currentHour = now.getHours(); + + // Find the price for the current hour + return this.priceData.find(price => { + const priceHour = new Date(price.time_start).getHours(); + return priceHour === currentHour; + }) || this.priceData[0]; // Default to first price if not found + } + + get averagePrice(): number { + if (!this.priceData || this.priceData.length === 0) { + return 0; + } + + const sum = this.priceData.reduce((total, price) => total + price.SEK_per_kWh, 0); + return sum / this.priceData.length; + } + + get highestPrice(): EnergyPrice | null { + if (!this.priceData || this.priceData.length === 0) { + return null; + } + + return this.priceData.reduce((max, price) => + price.SEK_per_kWh > max.SEK_per_kWh ? price : max, this.priceData[0]); + } + + get lowestPrice(): EnergyPrice | null { + if (!this.priceData || this.priceData.length === 0) { + return null; + } + + return this.priceData.reduce((min, price) => + price.SEK_per_kWh < min.SEK_per_kWh ? price : min, this.priceData[0]); + } + + getRegionName(regionCode: string): string { + const region = this.regions.find(r => r.value === regionCode); + return region ? region.label : regionCode; + } +} diff --git a/src/app/energy-price.service.ts b/src/app/services/energy-price.service.ts similarity index 100% rename from src/app/energy-price.service.ts rename to src/app/services/energy-price.service.ts diff --git a/src/main.ts b/src/main.ts index 4e6be2b..83b8548 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,11 +1,6 @@ import { bootstrapApplication } from '@angular/platform-browser'; -import { provideHttpClient } from '@angular/common/http'; -import { provideCharts, withDefaultRegisterables } from 'ng2-charts'; +import { appConfig } from './app/app.config'; import { AppComponent } from './app/app.component'; -bootstrapApplication(AppComponent, { - providers: [ - provideHttpClient(), - provideCharts(withDefaultRegisterables()) - ] -}).catch(err => console.error(err)); +bootstrapApplication(AppComponent, appConfig) + .catch(err => console.error(err));