Docker, API proxy, Drone CI, .env

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Rafał Miczek
2026-02-13 19:58:25 +01:00
co-authored by Cursor
commit d5d4883917
76 changed files with 15170 additions and 0 deletions
@@ -0,0 +1,163 @@
import { Component, Input } from '@angular/core';
import { CommonModule } from '@angular/common';
import { NgChartsModule } from 'ng2-charts';
import {
ChartConfiguration,
ChartData,
} from 'chart.js';
import { SportProgress } from '../../../core/models/sport-goal.model';
import { ChartCardComponent } from '../../../shared/components/chart-card/chart-card.component';
@Component({
selector: 'app-overall-progress-section',
standalone: true,
imports: [CommonModule, NgChartsModule, ChartCardComponent],
template: `
<div class="bg-gradient-to-br from-purple-50/50 via-pink-50/50 to-purple-100/50 backdrop-blur-sm rounded-2xl shadow-xl p-6 md:p-8 border-2 border-purple-200/50">
<div class="flex items-center gap-3 mb-6 pb-4 border-b-2 border-purple-200/30">
<span class="text-5xl">📊</span>
<div>
<h3 class="text-2xl font-bold text-gray-900">Overall Progress</h3>
<p class="text-sm text-gray-600">2026 year overview and combined sports progress</p>
</div>
</div>
<!-- Year Progress and Sports Progress Row -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
<!-- Year Progress Card -->
<div class="bg-white/80 backdrop-blur-md rounded-xl shadow-lg p-6 border border-purple-200/50">
<h4 class="text-lg font-semibold text-gray-900 mb-4 flex items-center gap-2">
<span class="text-2xl">📅</span>
<span>2026 Year Progress</span>
</h4>
<div class="flex flex-col items-center">
<div class="h-48 w-48 relative">
<!-- Outer ring - Optimal -->
<canvas
baseChart
[data]="yearProgressOptimalDoughnutData"
[type]="'doughnut'"
[options]="yearProgressOptimalDoughnutOptions"
class="absolute inset-0"
></canvas>
<!-- Inner ring - Actual -->
<div class="absolute" style="inset: 20%;">
<canvas
baseChart
[data]="yearProgressDoughnutData"
[type]="'doughnut'"
[options]="yearProgressDoughnutOptions"
class="w-full h-full"
></canvas>
</div>
<div class="absolute inset-0 flex items-center justify-center pointer-events-none">
<div class="text-center">
<div class="text-3xl font-bold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
{{ progress.overallPercentage.toFixed(1) }}%
</div>
<div class="text-xs text-gray-600 mt-1">Actual</div>
</div>
</div>
</div>
<!-- Legend -->
<div class="mt-4 flex flex-wrap justify-center gap-4 text-xs">
<div class="flex items-center gap-2">
<div class="w-3 h-3 rounded-full bg-blue-500"></div>
<span class="text-gray-600">Actual: {{ progress.overallPercentage.toFixed(1) }}%</span>
</div>
<div class="flex items-center gap-2">
<div class="w-3 h-3 rounded-full bg-gray-400"></div>
<span class="text-gray-600">Optimal: {{ idealProgress.toFixed(1) }}%</span>
</div>
</div>
<p class="text-xs text-gray-600 mt-3 font-medium">
<span class="text-base">⏱️</span> {{ daysElapsed }} / {{ totalDaysInYear }} days
</p>
<div *ngIf="daysBehind !== null" class="text-sm font-medium mt-3">
<span *ngIf="daysBehind > 0" class="bg-yellow-400 bg-opacity-30 px-3 py-1 rounded-full inline-block">
<span>⏰</span> {{ daysBehind.toFixed(0) }} day{{ daysBehind !== 1 ? 's' : '' }} behind schedule
</span>
<span *ngIf="daysBehind < 0" class="bg-green-400 bg-opacity-30 px-3 py-1 rounded-full inline-block">
<span>🚀</span> {{ Math.abs(daysBehind).toFixed(0) }} day{{ Math.abs(daysBehind) !== 1 ? 's' : '' }} ahead of schedule
</span>
<span *ngIf="daysBehind === 0" class="bg-green-400 bg-opacity-30 px-3 py-1 rounded-full inline-block">
<span>✨</span> Right on schedule!
</span>
</div>
</div>
</div>
<!-- Sports Progress Card -->
<div class="bg-gradient-to-r from-blue-500/90 via-purple-600/90 to-pink-600/90 backdrop-blur-md rounded-xl shadow-lg p-6 text-white relative overflow-hidden">
<div class="absolute top-0 right-0 w-32 h-32 bg-white opacity-10 rounded-full -mr-16 -mt-16"></div>
<div class="absolute bottom-0 left-0 w-24 h-24 bg-white opacity-10 rounded-full -ml-12 -mb-12"></div>
<div class="relative z-10">
<h4 class="text-lg font-semibold mb-4 flex items-center gap-2">
<span class="text-2xl">🎯</span>
<span>Sports Progress</span>
</h4>
<div class="flex flex-col items-center">
<div class="h-48 w-full">
<canvas
baseChart
[data]="sportsProgressBikeDoughnutData"
[type]="'doughnut'"
[options]="sportsProgressBikeDoughnutOptions"
></canvas>
</div>
<div class="mt-2 text-center">
<div class="text-2xl font-bold drop-shadow-lg">
{{ progress.overallPercentage.toFixed(1) }}%
</div>
<div class="text-xs opacity-90 mt-1">Overall</div>
</div>
<!-- Legend -->
<div class="mt-4 flex flex-wrap justify-center gap-4 text-xs">
<div class="flex items-center gap-2">
<div class="w-3 h-3 rounded-full bg-blue-500"></div>
<span class="opacity-90">Bike: {{ progress.bike.percentage.toFixed(1) }}%</span>
</div>
<div class="flex items-center gap-2">
<div class="w-3 h-3 rounded-full bg-green-500"></div>
<span class="opacity-90">Run: {{ progress.run.percentage.toFixed(1) }}%</span>
</div>
<div class="flex items-center gap-2">
<div class="w-3 h-3 rounded-full bg-yellow-500"></div>
<span class="opacity-90">Swim: {{ progress.swim.percentage.toFixed(1) }}%</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Combined Chart -->
<app-chart-card
title="Combined Progress Over Time"
[chartData]="combinedChartData"
[chartType]="'line'"
[chartOptions]="progressChartOptions"
></app-chart-card>
</div>
`,
})
export class OverallProgressSectionComponent {
@Input() progress!: SportProgress;
@Input() yearProgress = 0;
@Input() idealProgress = 0;
@Input() daysElapsed = 0;
@Input() totalDaysInYear = 365;
@Input() daysBehind: number | null = null;
@Input() isOnTrack = false;
@Input() yearProgressDoughnutData!: ChartData<'doughnut'>;
@Input() yearProgressOptimalDoughnutData!: ChartData<'doughnut'>;
@Input() sportsProgressBikeDoughnutData!: ChartData<'doughnut'>;
@Input() combinedChartData!: ChartData<'line'>;
@Input() yearProgressDoughnutOptions!: ChartConfiguration<'doughnut'>['options'];
@Input() yearProgressOptimalDoughnutOptions!: ChartConfiguration<'doughnut'>['options'];
@Input() sportsProgressBikeDoughnutOptions!: ChartConfiguration<'doughnut'>['options'];
@Input() progressChartOptions!: ChartConfiguration['options'];
readonly Math = Math;
}
@@ -0,0 +1,17 @@
// Component styles if needed
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,110 @@
import { Component, Input } from '@angular/core';
import { CommonModule } from '@angular/common';
import { NgChartsModule } from 'ng2-charts';
import {
ChartConfiguration,
ChartData,
} from 'chart.js';
import { SportGoal, Activity } from '../../../core/models/sport-goal.model';
import { ProgressCardComponent } from '../../../shared/components/progress-card/progress-card.component';
import { ChartCardComponent } from '../../../shared/components/chart-card/chart-card.component';
@Component({
selector: 'app-sport-section',
standalone: true,
imports: [CommonModule, NgChartsModule, ProgressCardComponent, ChartCardComponent],
template: `
<div
class="backdrop-blur-sm rounded-2xl shadow-xl p-6 md:p-8 border-2 mt-8"
[ngClass]="gradientClass"
>
<div class="flex items-center gap-3 mb-6 pb-4 border-b-2" [ngClass]="borderClass">
<span class="text-5xl">{{ emoji }}</span>
<div>
<h3 class="text-2xl font-bold text-gray-900">{{ title }}</h3>
<p class="text-sm text-gray-600">{{ description }}</p>
</div>
</div>
<div class="space-y-6">
<app-progress-card
[title]="title"
[emoji]="emoji"
[percentage]="goal.percentage"
[currentLabel]="formatDistance(goal.current) + ' km'"
[targetLabel]="goal.target + ' km'"
[description]="
'Progress: ' +
formatDistance(goal.current) +
' / ' +
goal.target +
' km'
"
[estimatedCompletion]="estimatedCompletion"
[dailyKmNeeded]="dailyKmNeeded"
[currentDailyAvg]="currentDailyAvg"
[optimalDailyAvg]="optimalDailyAvg"
[daysAheadOrBehind]="daysAheadOrBehind"
[recentActivities]="recentActivities"
></app-progress-card>
<div class="bg-white/90 backdrop-blur-md rounded-xl shadow-lg p-6 border" [ngClass]="borderColorClass">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<h4 class="text-lg font-semibold text-gray-900 mb-4 text-center">Progress</h4>
<div class="h-64">
<canvas
baseChart
[data]="doughnutData"
[type]="'doughnut'"
[options]="doughnutOptions"
></canvas>
</div>
<div class="mt-4 text-center space-y-2">
<p class="text-sm text-gray-600">
<span class="font-semibold text-gray-900">{{ goal.percentage.toFixed(1) }}%</span> Actual
</p>
<p class="text-xs text-gray-500">
Optimal: <span class="font-semibold">{{ idealProgress.toFixed(1) }}%</span>
</p>
</div>
</div>
<div>
<app-chart-card
[title]="title + ' Progress Over Time'"
[chartData]="chartData"
[chartType]="'line'"
[chartOptions]="chartOptions"
></app-chart-card>
</div>
</div>
</div>
</div>
</div>
`,
})
export class SportSectionComponent {
@Input() title = '';
@Input() emoji = '';
@Input() description = '';
@Input() goal!: SportGoal;
@Input() estimatedCompletion: string | null = null;
@Input() dailyKmNeeded: number | null = null;
@Input() currentDailyAvg: number | null = null;
@Input() optimalDailyAvg: number | null = null;
@Input() daysAheadOrBehind: number | null = null;
@Input() recentActivities: Activity[] | null = null;
@Input() idealProgress = 0;
@Input() doughnutData!: ChartData<'doughnut'>;
@Input() chartData!: ChartData<'line'>;
@Input() doughnutOptions!: ChartConfiguration<'doughnut'>['options'];
@Input() chartOptions!: ChartConfiguration['options'];
@Input() gradientClass = '';
@Input() borderClass = '';
@Input() borderColorClass = '';
readonly Math = Math;
formatDistance(km: number): string {
return km.toFixed(2);
}
}
@@ -0,0 +1,91 @@
import { Component, OnInit, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ActivatedRoute, Router } from '@angular/router';
import { StravaService } from '../../core/services/strava.service';
@Component({
selector: 'app-strava-callback',
standalone: true,
imports: [CommonModule],
template: `
<div class="flex justify-center items-center h-screen">
<div class="text-center">
<p *ngIf="loading" class="text-gray-600">Connecting to Strava...</p>
<p *ngIf="error" class="text-red-600">{{ error }}</p>
<p *ngIf="success" class="text-green-600">Successfully connected to Strava!</p>
</div>
</div>
`,
styles: [],
})
export class StravaCallbackComponent implements OnInit {
private readonly route = inject(ActivatedRoute);
private readonly router = inject(Router);
private readonly stravaService = inject(StravaService);
loading = true;
error: string | null = null;
success = false;
ngOnInit(): void {
this.route.queryParams.subscribe((params) => {
const code = params['code'];
const error = params['error'];
if (error) {
this.error = 'Authorization failed. Please try again.';
this.loading = false;
setTimeout(() => {
this.router.navigate(['/sport']);
}, 3000);
return;
}
if (code) {
this.exchangeCodeForToken(code);
} else {
this.error = 'No authorization code received.';
this.loading = false;
setTimeout(() => {
this.router.navigate(['/sport']);
}, 3000);
}
});
}
private exchangeCodeForToken(code: string): void {
this.stravaService.exchangeCodeForToken(code).subscribe({
next: () => {
this.success = true;
this.loading = false;
setTimeout(() => {
this.router.navigate(['/sport']);
}, 2000);
},
error: (err) => {
this.error = 'Failed to connect to Strava. Please try again.';
this.loading = false;
console.error('Error exchanging code for token:', err);
setTimeout(() => {
this.router.navigate(['/sport']);
}, 3000);
},
});
}
}