Reafactor: for routing, Add: navbar & footer
This commit is contained in:
parent
32851bead5
commit
ebdfd2f5e3
16 changed files with 586 additions and 243 deletions
|
@ -1,100 +1,7 @@
|
|||
<div class="container">
|
||||
<header class="text-center m-6">
|
||||
<h1 class="text-3xl font-bold">{{ title }}</h1>
|
||||
</header>
|
||||
|
||||
<div class="controls">
|
||||
<div class="form-group">
|
||||
<label for="region">Region:</label>
|
||||
<select id="region" [(ngModel)]="selectedRegion" (change)="onRegionChange()">
|
||||
@for (region of regions; track region.value) {
|
||||
<option [value]="region.value">
|
||||
{{ region.label }}
|
||||
</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="date">Date:</label>
|
||||
<input
|
||||
type="date"
|
||||
id="date"
|
||||
[value]="formattedDate"
|
||||
(change)="onDateChange($event)"
|
||||
[max]="maxDate"
|
||||
class="date-input">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (!loading && !error && priceData.length > 0) {
|
||||
<div class="price-summary">
|
||||
<div class="price-heading">
|
||||
<h2>{{ getRegionName(selectedRegion) }} {{ formattedDisplayDate }}</h2>
|
||||
<p class="price-subheading">(utan moms och andra skatter)</p>
|
||||
</div>
|
||||
|
||||
<div class="current-price">
|
||||
<div class="price-label">Just nu</div>
|
||||
<div class="price-value">{{ currentPrice?.SEK_per_kWh || 0 | number:'1.2-2' }} <span class="price-unit">kr/kWh</span></div>
|
||||
<div class="price-change" [ngClass]="{'price-increase': (currentPrice?.SEK_per_kWh || 0) > averagePrice, 'price-decrease': (currentPrice?.SEK_per_kWh || 0) < averagePrice}">
|
||||
{{ (currentPrice?.SEK_per_kWh || 0) > averagePrice ? '+' : '' }}{{ ((currentPrice?.SEK_per_kWh || 0) - averagePrice) | number:'1.2-2' }}%
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="price-extremes">
|
||||
<div class="price-high">
|
||||
↑ {{ highestPrice?.SEK_per_kWh || 0 | number:'1.2-2' }} kr kl {{ highestPrice?.time_start | date:'HH:mm' }}
|
||||
</div>
|
||||
<div class="price-low">
|
||||
↓ {{ lowestPrice?.SEK_per_kWh || 0 | number:'1.2-2' }} kr kl {{ lowestPrice?.time_start | date:'HH:mm' }}
|
||||
</div>
|
||||
<div class="price-average">
|
||||
{{ averagePrice | number:'1.2-2' }} kr snitt
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="chart-container">
|
||||
@if (loading) {
|
||||
<div class="loading">
|
||||
<p>Loading energy price data...</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (error) {
|
||||
<div class="error">
|
||||
<p>{{ error }}</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!loading && !error) {
|
||||
<app-energy-chart [priceData]="priceData"></app-energy-chart>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (!loading && !error && priceData.length > 0) {
|
||||
<div class="price-list">
|
||||
<h2>Hour-by-hour prices</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Time</th>
|
||||
<th>SEK/kWh</th>
|
||||
<th>EUR/kWh</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (price of priceData; track price.time_start) {
|
||||
<tr>
|
||||
<td>{{ price.time_start | date:'HH:00' }} - {{ price.time_end | date:'HH:00' }}</td>
|
||||
<td>{{ price.SEK_per_kWh | number:'1.2-4' }}</td>
|
||||
<td>{{ price.EUR_per_kWh | number:'1.2-4' }}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
<div class="flex flex-col min-h-screen">
|
||||
<app-header></app-header>
|
||||
<main class="container mx-auto px-4 py-8 flex-grow">
|
||||
<router-outlet />
|
||||
</main>
|
||||
<app-footer></app-footer>
|
||||
</div>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
]
|
||||
};
|
||||
|
|
|
@ -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: '' }
|
||||
];
|
||||
|
|
|
@ -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',
|
73
src/app/components/footer/footer.component.html
Normal file
73
src/app/components/footer/footer.component.html
Normal file
|
@ -0,0 +1,73 @@
|
|||
<footer class="bg-gray-800 text-white mt-auto">
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
<!-- Company Info -->
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold mb-4">Energy Price Dashboard</h3>
|
||||
<p class="text-gray-300 mb-4">
|
||||
Track and analyze energy prices across different regions in Sweden.
|
||||
Make informed decisions about your energy consumption.
|
||||
</p>
|
||||
<div class="flex space-x-4">
|
||||
<a href="#" class="text-gray-300 hover:text-white transition-colors">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M24 4.557c-.883.392-1.832.656-2.828.775 1.017-.609 1.798-1.574 2.165-2.724-.951.564-2.005.974-3.127 1.195-.897-.957-2.178-1.555-3.594-1.555-3.179 0-5.515 2.966-4.797 6.045-4.091-.205-7.719-2.165-10.148-5.144-1.29 2.213-.669 5.108 1.523 6.574-.806-.026-1.566-.247-2.229-.616-.054 2.281 1.581 4.415 3.949 4.89-.693.188-1.452.232-2.224.084.626 1.956 2.444 3.379 4.6 3.419-2.07 1.623-4.678 2.348-7.29 2.04 2.179 1.397 4.768 2.212 7.548 2.212 9.142 0 14.307-7.721 13.995-14.646.962-.695 1.797-1.562 2.457-2.549z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="#" class="text-gray-300 hover:text-white transition-colors">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="#" class="text-gray-300 hover:text-white transition-colors">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M19 0h-14c-2.761 0-5 2.239-5 5v14c0 2.761 2.239 5 5 5h14c2.762 0 5-2.239 5-5v-14c0-2.761-2.238-5-5-5zm-11 19h-3v-11h3v11zm-1.5-12.268c-.966 0-1.75-.79-1.75-1.764s.784-1.764 1.75-1.764 1.75.79 1.75 1.764-.783 1.764-1.75 1.764zm13.5 12.268h-3v-5.604c0-3.368-4-3.113-4 0v5.604h-3v-11h3v1.765c1.396-2.586 7-2.777 7 2.476v6.759z"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Links -->
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold mb-4">Quick Links</h3>
|
||||
<ul class="space-y-2">
|
||||
<li><a routerLink="/" class="text-gray-300 hover:text-white transition-colors">Home</a></li>
|
||||
<li><a routerLink="/about" class="text-gray-300 hover:text-white transition-colors">About</a></li>
|
||||
<li><a routerLink="/faq" class="text-gray-300 hover:text-white transition-colors">FAQ</a></li>
|
||||
<li><a routerLink="/privacy" class="text-gray-300 hover:text-white transition-colors">Privacy Policy</a></li>
|
||||
<li><a routerLink="/terms" class="text-gray-300 hover:text-white transition-colors">Terms of Service</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Contact Info -->
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold mb-4">Contact Us</h3>
|
||||
<address class="not-italic text-gray-300">
|
||||
<div class="mb-2 flex items-start">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2 mt-0.5 text-green-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
<span>Energivägen 123, 11122 Stockholm, Sweden</span>
|
||||
</div>
|
||||
<div class="mb-2 flex items-start">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2 mt-0.5 text-green-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<span>contact@energypricedashboard.se</span>
|
||||
</div>
|
||||
<div class="flex items-start">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2 mt-0.5 text-green-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
|
||||
</svg>
|
||||
<span>+46 123 456 789</span>
|
||||
</div>
|
||||
</address>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-gray-700 mt-8 pt-6 text-center text-gray-400 text-sm">
|
||||
<p>© {{ currentYear }} Energy Price Dashboard. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
13
src/app/components/footer/footer.component.ts
Normal file
13
src/app/components/footer/footer.component.ts
Normal file
|
@ -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();
|
||||
}
|
34
src/app/components/header/header.component.html
Normal file
34
src/app/components/header/header.component.html
Normal file
|
@ -0,0 +1,34 @@
|
|||
<header class="bg-gradient-to-r from-green-700 via-green-600 to-green-500 shadow-md">
|
||||
<div class="container mx-auto px-4 py-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-white mr-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
<h1 class="text-2xl font-bold text-white">Energy Price Dashboard</h1>
|
||||
</div>
|
||||
|
||||
<nav class="hidden md:flex space-x-6">
|
||||
<a routerLink="/" routerLinkActive="text-green-100" [routerLinkActiveOptions]="{exact: true}" class="text-white hover:text-green-100 transition-colors font-medium">Home</a>
|
||||
<a routerLink="/about" routerLinkActive="text-green-100" class="text-white hover:text-green-100 transition-colors font-medium">About</a>
|
||||
<a routerLink="/faq" routerLinkActive="text-green-100" class="text-white hover:text-green-100 transition-colors font-medium">FAQ</a>
|
||||
<a routerLink="/contact" routerLinkActive="text-green-100" class="text-white hover:text-green-100 transition-colors font-medium">Contact</a>
|
||||
</nav>
|
||||
|
||||
<!-- Mobile menu button -->
|
||||
<button (click)="toggleMenu()" class="md:hidden bg-green-800 text-white p-2 rounded-md focus:outline-none" aria-label="Menu">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Mobile menu (hidden by default) -->
|
||||
<div class="md:hidden" [ngClass]="{'hidden': !isMenuOpen, 'block': isMenuOpen}" class="mt-4 pb-2">
|
||||
<a routerLink="/" routerLinkActive="text-green-100" [routerLinkActiveOptions]="{exact: true}" class="block py-2 text-white hover:text-green-100">Home</a>
|
||||
<a routerLink="/about" routerLinkActive="text-green-100" class="block py-2 text-white hover:text-green-100">About</a>
|
||||
<a routerLink="/faq" routerLinkActive="text-green-100" class="block py-2 text-white hover:text-green-100">FAQ</a>
|
||||
<a routerLink="/contact" routerLinkActive="text-green-100" class="block py-2 text-white hover:text-green-100">Contact</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
17
src/app/components/header/header.component.ts
Normal file
17
src/app/components/header/header.component.ts
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
173
src/app/pages/home/home.component.css
Normal file
173
src/app/pages/home/home.component.css
Normal file
|
@ -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;
|
||||
}
|
100
src/app/pages/home/home.component.html
Normal file
100
src/app/pages/home/home.component.html
Normal file
|
@ -0,0 +1,100 @@
|
|||
<div class="container">
|
||||
<header class="text-center m-6">
|
||||
<h1 class="text-3xl font-bold">{{ title }}</h1>
|
||||
</header>
|
||||
|
||||
<div class="controls">
|
||||
<div class="form-group">
|
||||
<label for="region">Region:</label>
|
||||
<select id="region" [(ngModel)]="selectedRegion" (change)="onRegionChange()">
|
||||
@for (region of regions; track region.value) {
|
||||
<option [value]="region.value">
|
||||
{{ region.label }}
|
||||
</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="date">Date:</label>
|
||||
<input
|
||||
type="date"
|
||||
id="date"
|
||||
[value]="formattedDate"
|
||||
(change)="onDateChange($event)"
|
||||
[max]="maxDate"
|
||||
class="date-input">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (!loading && !error && priceData.length > 0) {
|
||||
<div class="price-summary">
|
||||
<div class="price-heading">
|
||||
<h2>{{ getRegionName(selectedRegion) }} {{ formattedDisplayDate }}</h2>
|
||||
<p class="price-subheading">(utan moms och andra skatter)</p>
|
||||
</div>
|
||||
|
||||
<div class="current-price">
|
||||
<div class="price-label">Just nu</div>
|
||||
<div class="price-value">{{ currentPrice?.SEK_per_kWh || 0 | number:'1.2-2' }} <span class="price-unit">kr/kWh</span></div>
|
||||
<div class="price-change" [ngClass]="{'price-increase': (currentPrice?.SEK_per_kWh || 0) > averagePrice, 'price-decrease': (currentPrice?.SEK_per_kWh || 0) < averagePrice}">
|
||||
{{ (currentPrice?.SEK_per_kWh || 0) > averagePrice ? '+' : '' }}{{ ((currentPrice?.SEK_per_kWh || 0) - averagePrice) | number:'1.2-2' }}%
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="price-extremes">
|
||||
<div class="price-high">
|
||||
↑ {{ highestPrice?.SEK_per_kWh || 0 | number:'1.2-2' }} kr kl {{ highestPrice?.time_start | date:'HH:mm' }}
|
||||
</div>
|
||||
<div class="price-low">
|
||||
↓ {{ lowestPrice?.SEK_per_kWh || 0 | number:'1.2-2' }} kr kl {{ lowestPrice?.time_start | date:'HH:mm' }}
|
||||
</div>
|
||||
<div class="price-average">
|
||||
{{ averagePrice | number:'1.2-2' }} kr snitt
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="chart-container">
|
||||
@if (loading) {
|
||||
<div class="loading">
|
||||
<p>Loading energy price data...</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (error) {
|
||||
<div class="error">
|
||||
<p>{{ error }}</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!loading && !error) {
|
||||
<app-energy-chart [priceData]="priceData"></app-energy-chart>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (!loading && !error && priceData.length > 0) {
|
||||
<div class="price-list">
|
||||
<h2>Hour-by-hour prices</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Time</th>
|
||||
<th>SEK/kWh</th>
|
||||
<th>EUR/kWh</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (price of priceData; track price.time_start) {
|
||||
<tr>
|
||||
<td>{{ price.time_start | date:'HH:00' }} - {{ price.time_end | date:'HH:00' }}</td>
|
||||
<td>{{ price.SEK_per_kWh | number:'1.2-4' }}</td>
|
||||
<td>{{ price.EUR_per_kWh | number:'1.2-4' }}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</div>
|
144
src/app/pages/home/home.component.ts
Normal file
144
src/app/pages/home/home.component.ts
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
11
src/main.ts
11
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));
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue