Docker, API proxy, Drone CI, .env
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+163
@@ -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);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user