Sostituito frontend-angular-new con frontend-angular

This commit is contained in:
Lorenzo Sanesi
2026-08-10 13:28:05 +02:00
parent 6d52968c28
commit f4afaf0792
78 changed files with 7510 additions and 2321 deletions
+3
View File
@@ -9,3 +9,6 @@
/frontend-angular/node_modules/
/.claude/
/.ai/mcp/mcp.json
/frontend-angular-old/.angular/
/frontend-angular-old/node_modules/
/.ai/
+28 -12
View File
@@ -1,20 +1,36 @@
# Frontend Angular Dockerfile
FROM node:20-alpine
# ========================================
# Frontend Angular Dockerfile (multi-stage)
# ========================================
# Set working directory
FROM node:20-alpine AS base
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
# ========================================
# Development: ng serve con hot reload
# ========================================
FROM base AS development
RUN npm install
# Copy application code
COPY . .
# Expose port
EXPOSE 4200
# Start development server
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]
# ========================================
# Build: bundle statico di produzione
# ========================================
FROM base AS build
RUN npm install
COPY . .
RUN npm run build
# Nota: la build SSR (server.ts/main.server.ts) non è al momento agganciata
# ad angular.json ("server" builder option assente), quindi ng build produce
# solo il bundle browser in dist/app/browser/. Se in futuro si abilita la SSR,
# questo stage andrà aggiornato per servire dist/app/server con Node invece di nginx.
# ========================================
# Production: bundle statico servito da nginx
# ========================================
FROM nginx:1.27-alpine AS production
COPY --from=build /app/dist/app/browser /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
+1 -1
View File
@@ -6,7 +6,7 @@
This contains everything you need to run your app locally.
View your app in AI Studio: https://ai.studio/apps/drive/1LXZWZLxW6p8pFAQ_ECq5pcD0utESimfc
View your app in AI Studio: https://ai.studio/apps/7d951078-743c-4db5-ac20-6a2c625411e7
## Run Locally
+52 -10
View File
@@ -1,26 +1,59 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "",
"cli": {
"packageManager": "npm",
"schematicCollections": [
"angular-eslint"
]
},
"newProjectRoot": "projects",
"projects": {
"app": {
"projectType": "application",
"schematics": {},
"root": "",
"sourceRoot": "./",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular/build:application",
"options": {
"outputPath": {
"base": "./dist",
"browser": "."
"define": {
"GEMINI_API_KEY": "'YOUR_GEMINI_API_KEY'"
},
"browser": "index.tsx",
"tsConfig": "tsconfig.json"
"browser": "src/main.ts",
"tsConfig": "tsconfig.app.json",
"assets": [
{
"glob": "**/*",
"input": "public"
}
],
"styles": [
"src/styles.css"
]
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"budgets": [
{
"type": "initial",
"maximumWarning": "500kB",
"maximumError": "1MB"
},
{
"type": "anyComponentStyle",
"maximumWarning": "4kB",
"maximumError": "8kB"
}
],
"outputHashing": "all"
},
"development": {
@@ -33,9 +66,6 @@
},
"serve": {
"builder": "@angular/build:dev-server",
"options": {
"port": 4200
},
"configurations": {
"production": {
"buildTarget": "app:build:production"
@@ -45,6 +75,18 @@
}
},
"defaultConfiguration": "development"
},
"test": {
"builder": "@angular/build:unit-test"
},
"lint": {
"builder": "@angular-eslint/builder:lint",
"options": {
"lintFilePatterns": [
"src/**/*.ts",
"src/**/*.html"
]
}
}
}
}
+3 -3
View File
@@ -1,5 +1,5 @@
{
"name": "Amici Scommesse",
"description": "Un'app per creare e gestire scommesse tra amici. Tieni traccia delle scommesse, crea gruppi, e partecipa a sfide pubbliche per guadagnare punti gloria.",
"requestFramePermissions": []
"name": "ScommettiApp",
"description": "A social betting and challenge platform to compete with friends and join communities.",
"requestFramePermissions": ["geolocation"]
}
+21
View File
@@ -0,0 +1,21 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
# Asset con hash nel nome: cache lunga
location ~* "\.[0-9a-f]{8,}\.(js|css|svg|png|jpg|jpeg|gif|webp|woff2?)$" {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Fallback per il routing lato client di Angular
location / {
try_files $uri $uri/ /index.html;
}
}
+6965 -2193
View File
File diff suppressed because it is too large Load Diff
+31 -13
View File
@@ -9,20 +9,38 @@
"preview": "ng serve --configuration=production"
},
"dependencies": {
"rxjs": "^7.8.2",
"@angular/compiler": "^20.3.0",
"@angular/platform-browser": "^20.3.0",
"@angular/common": "^20.3.0",
"@angular/core": "^20.3.0",
"@angular/build": "^20.3.0",
"@angular/cli": "^20.3.0",
"@angular/compiler-cli": "^20.3.0",
"keycloak-js": "^24.0.0",
"tailwindcss": "latest"
"@angular/cdk": "^21.0.0",
"@angular/common": "^21.0.0",
"@angular/compiler": "^21.0.0",
"@angular/core": "^21.0.0",
"@angular/forms": "^21.0.0",
"@angular/material": "^21.0.0",
"@angular/platform-browser": "^21.0.0",
"@angular/platform-server": "^21.0.0",
"@angular/router": "^21.0.0",
"@angular/ssr": "^21.0.0",
"@google/genai": "^1.27.0",
"express": "^5.1.0",
"keycloak-js": "^26.2.3",
"motion": "^12.23.24",
"rxjs": "~7.8.0",
"tslib": "^2.3.0"
},
"devDependencies": {
"@types/node": "^22.14.0",
"typescript": "~5.8.2",
"vite": "^6.2.0"
"@angular/build": "^21.0.0",
"@angular/cli": "^21.0.0",
"@angular/compiler-cli": "^21.0.0",
"@tailwindcss/postcss": "^4.1.12",
"@types/express": "^5.0.1",
"@types/node": "^20.17.19",
"angular-eslint": "21.1.0",
"cross-env": "^10.1.0",
"eslint": "^9.39.1",
"jsdom": "^27.0.0",
"postcss": "^8.5.3",
"tailwindcss": "^4.1.12",
"typescript": "~5.9.2",
"typescript-eslint": "8.47.0",
"vitest": "^4.0.0"
}
}

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

@@ -0,0 +1,184 @@
<div class="min-h-screen pb-32">
<header class="sticky top-0 z-50 bg-background/80 backdrop-blur-md flex justify-between items-center px-6 py-4">
<button class="w-10 h-10 rounded-full flex items-center justify-center hover:bg-surface-container transition-colors" [routerLink]="['/']">
<mat-icon>arrow_back</mat-icon>
</button>
<button (click)="editChallenge()" class="w-10 h-10 rounded-full flex items-center justify-center hover:bg-surface-container transition-colors text-primary">
<mat-icon>edit</mat-icon>
</button>
</header>
<main class="max-w-xl mx-auto px-6 pt-8 space-y-8">
@if (isLoading()) {
<div class="flex flex-col items-center justify-center py-20">
<div class="w-12 h-12 border-4 border-primary border-t-transparent rounded-full animate-spin"></div>
<p class="mt-4 text-on-surface-variant font-medium">{{ t('challenge.loading') }}</p>
</div>
} @else if (bet(); as data) {
<!-- Hero Section -->
<section class="space-y-4">
<div class="flex justify-between items-start">
<div class="space-y-1">
<span class="inline-flex items-center px-3 py-1 rounded-full bg-secondary-container text-secondary text-xs font-semibold tracking-wide uppercase">
{{ data.status }}
</span>
<h2 class="text-3xl font-headline font-extrabold">{{ data.title }}</h2>
</div>
<div class="bg-surface-container p-3 rounded-2xl flex flex-col items-center">
<span class="text-[10px] font-bold text-on-surface-variant uppercase tracking-widest">{{ t('challenge.stake') }}</span>
<span class="text-xl font-headline font-black text-primary">{{ data.stake }}</span>
</div>
</div>
<p class="text-on-surface-variant text-lg">{{ data.description || t('challenge.no_description') }}</p>
</section>
<!-- Stats -->
<div class="grid grid-cols-2 gap-4">
<div class="bg-surface-container-low p-6 rounded-2xl flex flex-col gap-1 border border-black/5">
<span class="text-xs font-medium text-on-surface-variant">{{ t('challenge.start_date') }}</span>
<span class="text-sm font-headline font-bold">{{ data.startDate | date:'mediumDate' }}</span>
</div>
<div class="bg-surface-container-low p-6 rounded-2xl flex flex-col gap-1 border border-black/5">
<span class="text-xs font-medium text-on-surface-variant">{{ t('challenge.end_date') }}</span>
<span class="text-sm font-headline font-bold">{{ data.endDate ? (data.endDate | date:'mediumDate') : t('challenge.end_date_indefinite') }}</span>
</div>
<div class="bg-surface-container-low p-6 rounded-2xl flex flex-col gap-1 border border-black/5">
<span class="text-xs font-medium text-on-surface-variant">{{ t('challenge.visibility') }}</span>
<span class="text-sm font-headline font-bold">{{ data.isPublic ? t('challenge.visibility.public') : t('challenge.visibility.private') }}</span>
</div>
</div>
<!-- Participants -->
<section class="space-y-4">
<div class="flex items-center justify-between">
<h3 class="font-headline font-bold">{{ t('challenge.participants') }}</h3>
<span class="text-sm font-medium text-primary">{{ data.participants.length }} {{ t('challenge.joined') }}</span>
</div>
<div class="flex flex-wrap gap-3">
@for (p of data.participants; track $index) {
<div class="flex flex-col items-center gap-1">
<div class="relative">
@if (getParticipantUser(p)?.avatarUrl) {
<img [src]="getParticipantUser(p)!.avatarUrl" class="h-12 w-12 rounded-full ring-2 ring-primary/20 object-cover" [alt]="getParticipantUser(p)!.name">
} @else {
<div class="h-12 w-12 rounded-full ring-2 ring-primary/20 bg-primary-container/20 flex items-center justify-center">
<mat-icon class="text-primary">person</mat-icon>
</div>
}
@if (p.role === 'OBSERVER') {
<div class="absolute -bottom-1 -right-1 bg-surface-container rounded-full p-0.5 shadow-sm">
<mat-icon class="text-[10px] text-on-surface-variant">visibility</mat-icon>
</div>
}
</div>
<span class="text-[10px] font-medium text-on-surface-variant">{{ getParticipantUser(p)?.name || 'Utente' }}</span>
</div>
}
</div>
</section>
} @else {
<div class="flex flex-col items-center justify-center py-20 text-center">
<mat-icon class="text-6xl text-on-surface-variant/20 mb-4">search_off</mat-icon>
<h3 class="text-xl font-headline font-bold">{{ t('challenge.not_found.title') }}</h3>
<p class="text-on-surface-variant mt-2">{{ t('challenge.not_found.description') }}</p>
<button class="mt-6 text-primary font-bold" [routerLink]="['/']">{{ t('challenge.not_found.back') }}</button>
</div>
}
</main>
<!-- Action Buttons -->
<div class="fixed bottom-0 left-0 w-full p-6 bg-background/80 backdrop-blur-xl z-40 border-t border-black/5">
<div class="max-w-xl mx-auto flex flex-col gap-3">
<button
(click)="editChallenge()"
class="w-full py-4 bg-gradient-to-r from-primary to-primary-container text-white font-headline font-bold rounded-full shadow-lg active:scale-95 transition-transform">
{{ t('challenge.update_progress') }}
</button>
<button
(click)="openInviteSheet()"
class="w-full py-4 bg-surface-container-highest text-on-surface font-headline font-bold rounded-full active:scale-95 transition-transform flex items-center justify-center gap-2">
<mat-icon>person_add</mat-icon>
{{ t('challenge.invite_friends') }}
</button>
</div>
</div>
</div>
<!-- Invite Friends Bottom Sheet -->
@if (showInviteSheet()) {
<div class="fixed inset-0 z-50 flex flex-col justify-end">
<div class="absolute inset-0 bg-black/40 backdrop-blur-sm" (click)="closeInviteSheet()"></div>
<div
class="relative bg-background shadow-2xl flex flex-col transition-all duration-300 ease-out"
[class]="inviteSheetExpanded() ? 'fixed inset-0 rounded-none h-full' : 'rounded-t-3xl h-[65vh]'">
<!-- Handle -->
<div
class="flex justify-center pt-3 pb-1 cursor-grab touch-none select-none"
(touchstart)="onHandleTouchStart($event)"
(touchend)="onHandleTouchEnd($event)">
<div class="w-10 h-1.5 rounded-full bg-on-surface/20"></div>
</div>
<!-- Header -->
<div class="flex items-center justify-between px-6 py-3">
<h3 class="text-lg font-headline font-bold">{{ t('challenge.invite_friends') }} ({{ friends().length }})</h3>
<button (click)="closeInviteSheet()" class="w-8 h-8 flex items-center justify-center rounded-full hover:bg-surface-container transition-colors">
<mat-icon class="text-on-surface-variant">close</mat-icon>
</button>
</div>
<!-- Search -->
<div class="px-6 pb-4">
<div class="flex items-center gap-3 bg-surface-container rounded-2xl px-4 py-3">
<mat-icon class="text-on-surface-variant flex-none">search</mat-icon>
<input
type="text"
[placeholder]="t('common.search') + '...'"
class="flex-1 bg-transparent text-sm outline-none text-on-surface placeholder:text-on-surface-variant"
[ngModel]="friendsSearchQuery()"
(ngModelChange)="friendsSearchQuery.set($event)"
autofocus
>
@if (friendsSearchQuery().trim()) {
<button (click)="friendsSearchQuery.set('')" class="flex-none">
<mat-icon class="text-on-surface-variant text-lg">close</mat-icon>
</button>
}
</div>
</div>
<!-- Friends List -->
<div class="overflow-y-auto flex-1 px-6 pb-8 space-y-1">
@if (filteredFriends().length > 0) {
@for (friend of filteredFriends(); track friend.id) {
<div class="w-full flex items-center gap-3 p-3 rounded-2xl"
[class]="isAlreadyParticipant(friend) ? 'opacity-50' : ''">
<div class="w-11 h-11 rounded-full bg-primary-container/20 flex items-center justify-center flex-none overflow-hidden">
@if (friend.avatarUrl) {
<img [src]="friend.avatarUrl" class="w-full h-full object-cover" [alt]="friend.name">
} @else {
<mat-icon class="text-primary">person</mat-icon>
}
</div>
<div class="flex-1 min-w-0">
<p class="font-semibold text-sm truncate">{{ friend.name }}</p>
<p class="text-xs text-on-surface-variant truncate">{{ friend.email }}</p>
</div>
@if (isAlreadyParticipant(friend)) {
<mat-icon class="text-secondary flex-none text-lg">check_circle</mat-icon>
} @else {
<mat-icon class="text-primary flex-none text-lg">person_add</mat-icon>
}
</div>
}
} @else if (friendsSearchQuery().trim()) {
<p class="text-sm text-on-surface-variant text-center py-8">Nessun amico trovato</p>
} @else {
<p class="text-sm text-on-surface-variant text-center py-8">Non hai ancora nessun amico</p>
}
</div>
</div>
</div>
}
@@ -0,0 +1,117 @@
import {ChangeDetectionStrategy, Component, inject, signal, OnInit} from '@angular/core';
import {CommonModule} from '@angular/common';
import {FormsModule} from '@angular/forms';
import {MatIconModule} from '@angular/material/icon';
import {RouterModule, Router, ActivatedRoute} from '@angular/router';
import {BetService} from '../../services/bet';
import {FriendService} from '../../services/friend.service';
import {Bet, BetParticipant, User} from '../../models/data.models';
import {LanguageService} from '../../services/language';
@Component({
selector: 'app-challenge-detail',
standalone: true,
imports: [CommonModule, FormsModule, MatIconModule, RouterModule],
templateUrl: './challenge-detail.html',
styleUrl: './challenge-detail.css',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ChallengeDetail implements OnInit {
private router = inject(Router);
private route = inject(ActivatedRoute);
private betService = inject(BetService);
private friendService = inject(FriendService);
private langService = inject(LanguageService);
challengeId = this.route.snapshot.paramMap.get('id');
bet = signal<Bet | null>(null);
isLoading = signal(true);
friends = signal<User[]>([]);
showInviteSheet = signal(false);
inviteSheetExpanded = signal(false);
friendsSearchQuery = signal('');
private touchStartY = 0;
filteredFriends(): User[] {
const q = this.friendsSearchQuery().toLowerCase().trim();
if (!q) return this.friends();
return this.friends().filter(f =>
f.name.toLowerCase().includes(q) || f.email?.toLowerCase().includes(q)
);
}
isAlreadyParticipant(friend: User): boolean {
const participants = this.bet()?.participants ?? [];
return participants.some(p => {
const user = this.getParticipantUser(p);
return user?.id === friend.id;
});
}
ngOnInit() {
if (this.challengeId) {
this.betService.getBetById(this.challengeId).subscribe({
next: (data) => {
this.bet.set(data);
this.isLoading.set(false);
},
error: (err) => {
console.error('Error loading bet:', err);
this.isLoading.set(false);
}
});
}
this.friendService.getFriends().subscribe({
next: (friends) => this.friends.set(friends),
error: (err) => console.error('Error loading friends:', err),
});
}
openInviteSheet() {
this.friendsSearchQuery.set('');
this.inviteSheetExpanded.set(false);
this.showInviteSheet.set(true);
}
closeInviteSheet() {
this.showInviteSheet.set(false);
this.inviteSheetExpanded.set(false);
}
onHandleTouchStart(event: TouchEvent) {
this.touchStartY = event.touches[0].clientY;
}
onHandleTouchEnd(event: TouchEvent) {
const deltaY = this.touchStartY - event.changedTouches[0].clientY;
if (deltaY > 40) {
this.inviteSheetExpanded.set(true);
} else if (deltaY < -40) {
if (this.inviteSheetExpanded()) {
this.inviteSheetExpanded.set(false);
} else {
this.closeInviteSheet();
}
}
}
t(key: string): string {
return this.langService.translate(key);
}
getParticipantUser(p: BetParticipant): User | null {
return typeof p.user === 'object' && p.user !== null ? p.user as User : null;
}
editChallenge() {
const data = this.bet();
if (data) {
this.router.navigate(['/create-bet'], {
state: { bet: data }
});
}
}
}
@@ -25,28 +25,16 @@
<span class="text-white/80 font-medium text-sm uppercase tracking-wider">Season Performance</span>
<h2 class="text-4xl font-headline font-extrabold mt-1">{{ t('home.total_wins') }}: {{ userProfile().totalWins }}</h2>
</div>
<div class="flex items-center justify-between mt-4">
<div class="flex -space-x-3">
<div class="w-10 h-10 rounded-full border-2 border-primary bg-surface-container flex items-center justify-center text-xs text-primary font-bold">JD</div>
<div class="w-10 h-10 rounded-full border-2 border-primary overflow-hidden">
<img src="https://picsum.photos/seed/friend1/100/100" class="w-full h-full object-cover" alt="Friend Avatar">
</div>
<div class="w-10 h-10 rounded-full border-2 border-primary bg-primary-container flex items-center justify-center text-xs">+12</div>
</div>
<span class="bg-white/20 px-4 py-1.5 rounded-full text-sm font-semibold backdrop-blur-md">Top 5% Rank</span>
<div class="flex justify-end mt-4">
<span class="bg-white/20 px-4 py-1.5 rounded-full text-sm font-semibold backdrop-blur-md">Top {{ userProfile().rankPercentile }}% Rank</span>
</div>
</div>
<div class="bg-secondary-container p-6 rounded-2xl soft-shadow flex flex-col justify-between">
<div>
<span class="text-secondary font-bold text-sm">{{ t('profile.balance') }}</span>
<div class="bg-secondary-container p-6 rounded-2xl soft-shadow flex flex-col justify-center">
<span class="text-secondary font-bold text-sm">{{ t('home.score') }}</span>
<div class="flex items-baseline gap-1 mt-2">
<span class="text-3xl font-headline font-black text-on-secondary-container">${{ userProfile().balance }}</span>
</div>
</div>
<div class="bg-white/40 p-3 rounded-2xl flex items-center gap-3">
<mat-icon class="text-secondary">trending_up</mat-icon>
<p class="text-xs font-semibold text-secondary">{{ t('profile.win_streak') }}: {{ userProfile().winStreak }}</p>
<span class="text-3xl font-headline font-black text-on-secondary-container">{{ userProfile().score }}</span>
<span class="text-sm font-semibold text-secondary">pts</span>
</div>
</div>
</section>
@@ -72,35 +60,20 @@
@for (challenge of filteredChallenges(); track challenge.id) {
<div class="bg-surface p-6 rounded-2xl soft-shadow relative overflow-hidden border border-black/5" [routerLink]="['/challenge-detail', challenge.id]">
<div class="flex justify-between items-start mb-4">
<div class="flex items-center gap-3">
<div class="w-12 h-12 rounded-2xl bg-secondary/10 flex items-center justify-center">
<mat-icon class="text-secondary">
{{ challenge.goalType === 'steps' ? 'footprint' : challenge.goalType === 'tasks' ? 'task_alt' : 'trending_up' }}
</mat-icon>
</div>
<div>
<h4 class="font-bold">{{ challenge.title }}</h4>
<p class="text-xs text-on-surface-variant">{{ challenge.description }}</p>
<p class="text-xs text-on-surface-variant mt-0.5">{{ challenge.description }}</p>
</div>
</div>
<span class="px-3 py-1 rounded-full bg-secondary-container text-secondary text-[10px] font-black uppercase tracking-widest">
<span class="px-3 py-1 rounded-full bg-secondary-container text-secondary text-[10px] font-black uppercase tracking-widest shrink-0 ml-3">
{{ challenge.status }}
</span>
</div>
<div class="space-y-4">
<div>
<div class="flex justify-between text-xs font-bold mb-1.5">
<span class="text-on-surface-variant">Target</span>
<span class="text-secondary">{{ challenge.target }} {{ challenge.goalType }}</span>
</div>
<div class="w-full h-3 bg-surface-container rounded-full overflow-hidden">
<div class="h-full bg-secondary rounded-full" [style.width.%]="challenge.status === 'completed' ? 100 : challenge.status === 'in-progress' ? 65 : 0"></div>
</div>
</div>
<div class="flex justify-between items-center pt-2">
<div class="text-sm font-bold">Prize Pool</div>
<div class="text-lg font-headline font-black text-primary">${{ challenge.prizePool }}</div>
<div class="flex justify-between items-center pt-2 border-t border-black/5">
<div class="flex items-center gap-1.5 text-xs text-on-surface-variant">
<mat-icon class="text-sm">group</mat-icon>
<span>{{ challenge.participantsCount }}</span>
</div>
<div class="text-lg font-headline font-black text-primary">{{ challenge.stake }}</div>
</div>
</div>
} @empty {
@@ -27,7 +27,7 @@ export class Home {
userProfile = computed<UserProfile>(() => {
const user = this.currentUser();
if (!user) return {id: '', name: '', username: '', avatar: '', balance: 0, winStreak: 0, totalWins: 0};
if (!user) return {id: '', name: '', username: '', avatar: '', score: 0, winStreak: 0, totalWins: 0, rankPercentile: 0};
return this.mapUserToProfile(user);
});
@@ -69,9 +69,10 @@ export class Home {
name: user.name,
username: user.email,
avatar: user.avatarUrl,
balance: 1250, // Mock balance
winStreak: 3, // Mock streak
totalWins: user.score
score: user.score,
winStreak: 0,
totalWins: user.score,
rankPercentile: user.rankPercentile ?? 0
};
}
@@ -82,16 +83,13 @@ export class Home {
'COMPLETED': 'completed',
'EXPIRED': 'completed'
};
const prize = parseFloat(bet.stake.replace(/[^0-9.]/g, '')) || 0;
return {
id: bet.id,
title: bet.title,
description: bet.description ?? '',
target: 100, // Default target
deadline: bet.endDate ?? '',
goalType: 'tasks',
status: statusMap[bet.status] ?? 'pending',
prizePool: prize,
stake: bet.stake,
participantsCount: bet.participants.length,
createdAt: bet.startDate
};
@@ -19,6 +19,7 @@
"groups.create": "Create Group",
"groups.members": "Members",
"profile.balance": "Balance",
"home.score": "Score",
"profile.win_streak": "Win Streak",
"profile.streak": "Streak",
"profile.losses": "Losses",
@@ -43,5 +44,21 @@
"common.search": "Search",
"common.edit": "Edit",
"common.delete": "Delete",
"common.confirm": "Confirm"
"common.confirm": "Confirm",
"challenge.loading": "Loading details...",
"challenge.stake": "Stake",
"challenge.no_description": "No description provided.",
"challenge.start_date": "Start Date",
"challenge.end_date": "End Date",
"challenge.end_date_indefinite": "Indefinite",
"challenge.visibility": "Visibility",
"challenge.visibility.public": "Public",
"challenge.visibility.private": "Private",
"challenge.participants": "Participants",
"challenge.joined": "Joined",
"challenge.not_found.title": "Challenge Not Found",
"challenge.not_found.description": "The challenge you're looking for doesn't exist or has been removed.",
"challenge.not_found.back": "Go Back Home",
"challenge.update_progress": "Update Progress",
"challenge.invite_friends": "Invite Friends"
}
@@ -19,6 +19,7 @@
"groups.create": "Crea Gruppo",
"groups.members": "Membri",
"profile.balance": "Bilancio",
"home.score": "Punteggio",
"profile.win_streak": "Striscia Vincente",
"profile.streak": "Striscia",
"profile.losses": "Perdite",
@@ -43,5 +44,21 @@
"common.search": "Cerca",
"common.edit": "Modifica",
"common.delete": "Elimina",
"common.confirm": "Conferma"
"common.confirm": "Conferma",
"challenge.loading": "Caricamento dettagli...",
"challenge.stake": "Posta",
"challenge.no_description": "Nessuna descrizione fornita.",
"challenge.start_date": "Data Inizio",
"challenge.end_date": "Data Fine",
"challenge.end_date_indefinite": "Indefinita",
"challenge.visibility": "Visibilità",
"challenge.visibility.public": "Pubblica",
"challenge.visibility.private": "Privata",
"challenge.participants": "Partecipanti",
"challenge.joined": "Iscritti",
"challenge.not_found.title": "Sfida Non Trovata",
"challenge.not_found.description": "La sfida che stai cercando non esiste o è stata rimossa.",
"challenge.not_found.back": "Torna alla Home",
"challenge.update_progress": "Aggiorna Progressi",
"challenge.invite_friends": "Invita Amici"
}
@@ -4,11 +4,9 @@ export interface ChallengeData {
id: string;
title: string;
description: string;
target: number;
deadline: string;
goalType: 'steps' | 'tasks' | 'predictions';
status: 'in-progress' | 'completed' | 'pending';
prizePool: number;
stake: string;
participantsCount: number;
createdAt: string;
}
@@ -38,9 +36,10 @@ export interface UserProfile {
name: string;
username: string;
avatar: string;
balance: number;
score: number;
winStreak: number;
totalWins: number;
rankPercentile: number;
}
// ─── Modelli Backend (usati nei servizi HTTP) ────────────────────────────────
@@ -91,6 +90,7 @@ export interface User {
email: string;
avatarUrl: string;
score: number;
rankPercentile?: number;
}
export interface Group {
@@ -40,7 +40,7 @@ export class BetService {
}
getBetById(id: string): Observable<Bet> {
return this.http.get<SuccessResponse<Bet[]>>(`${this.base}/${id}`).pipe(
return this.http.get<SuccessResponse<Bet>>(`${this.base}/${id}`).pipe(
map(res => mapBet(res.data))
);
}
@@ -0,0 +1,9 @@
// Dominio unico con routing per path (Caddy): frontend, /api e /auth
// sono serviti dalla stessa origin, quindi bastano URL relativi.
export const environment = {
production: true,
apiUrl: '',
keycloakUrl: '/auth',
keycloakRealm: 'scommettiapp',
keycloakClientId: 'scommetti-frontend',
};
+27 -25
View File
@@ -1,33 +1,35 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"compileOnSave": false,
"compilerOptions": {
"target": "ES2022",
"experimentalDecorators": true,
"useDefineForClassFields": false,
"module": "ESNext",
"lib": [
"ES2022",
"DOM",
"DOM.Iterable"
],
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"skipLibCheck": true,
"types": [
"node"
],
"moduleResolution": "bundler",
"isolatedModules": true,
"moduleDetection": "force",
"allowJs": true,
"jsx": "react-jsx",
"paths": {
"@/*": [
"./*"
]
}
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"importHelpers": true,
"target": "ES2022",
"module": "preserve"
},
"files": [
"./index.tsx"
],
"angularCompilerOptions": {
"disableTypeScriptVersionCheck": true
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
},
"files": [],
"references": [
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}