This commit is contained in:
ExostFlash 2025-06-02 23:27:02 +02:00
parent 791cbe79d6
commit cf2274d49c
8 changed files with 834 additions and 536 deletions

View file

@ -1,560 +1,568 @@
.match-card { .match-card {
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
border: 2px solid #daa520; border: 2px solid #daa520;
border-radius: 16px; padding: 24px;
padding: 24px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4),
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1);
0 8px 32px rgba(0, 0, 0, 0.4), position: relative;
inset 0 1px 0 rgba(255, 255, 255, 0.1); overflow: hidden;
position: relative; transition: all 0.3s ease;
overflow: hidden; height: 80vh;
transition: all 0.3s ease; width: 100%;
height: 80vh;
width: 100%;
} }
.match-card::before { .match-card::before {
content: ''; content: "";
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
right: 0; right: 0;
height: 2px; height: 2px;
background: linear-gradient(90deg, #daa520, #ffd700, #daa520); background: linear-gradient(90deg, #daa520, #ffd700, #daa520);
background-size: 200% 100%; background-size: 200% 100%;
animation: gradientShift 15s ease infinite; animation: gradientShift 15s ease infinite;
} }
@keyframes gradientShift { @keyframes gradientShift {
0% { 0% {
background-position: 0% 50%; background-position: 0% 50%;
} }
50% { 50% {
background-position: 100% 50%; background-position: 100% 50%;
} }
100% { 100% {
background-position: 0% 50%; background-position: 0% 50%;
} }
} }
.match-card:hover { .match-card {
transform: translateY(-4px); transform: translateY(-4px);
box-shadow: box-shadow: 0 12px 48px rgba(218, 165, 32, 0.2), 0 8px 32px rgba(0, 0, 0, 0.4);
0 12px 48px rgba(218, 165, 32, 0.2),
0 8px 32px rgba(0, 0, 0, 0.4);
} }
.match-background { .match-background {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
z-index: -1; z-index: -1;
} }
.background-image { .background-image {
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
filter: grayscale(30%) brightness(0.4) contrast(1.2); filter: grayscale(30%) brightness(0.4) contrast(1.2);
transition: all 0.5s ease; transition: all 0.5s ease;
} }
.match-card:hover .background-image { .match-card:hover .background-image {
filter: grayscale(10%) brightness(0.3) contrast(1.4); filter: grayscale(10%) brightness(0.3) contrast(1.4);
transform: scale(1.05); transform: scale(1.05);
} }
.image-overlay { .image-overlay {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
background: background: linear-gradient(
linear-gradient(135deg, 135deg,
rgba(26, 26, 46, 0.85) 0%, rgba(26, 26, 46, 0.85) 0%,
rgba(0, 0, 0, 0.7) 50%, rgba(0, 0, 0, 0.7) 50%,
rgba(22, 33, 62, 0.85) 100% rgba(22, 33, 62, 0.85) 100%
); );
} }
/* Header */ /* Header */
.match-header { .match-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 20px; margin-bottom: 20px;
} }
.match-weapon { .match-weapon {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
color: #daa520; color: #daa520;
font-weight: 600; font-weight: 600;
} }
.weapon-type { .weapon-type {
font-size: 14px; font-size: 14px;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 1px; letter-spacing: 1px;
} }
/* Status */ /* Status */
.match-status { .match-status {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
padding: 6px 12px; padding: 6px 12px;
border-radius: 20px; border-radius: 20px;
font-size: 12px; font-size: 12px;
font-weight: 600; font-weight: 600;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.5px; letter-spacing: 0.5px;
} }
.status-live { .status-warning {
background: rgba(34, 197, 94, 0.2); background: rgba(34, 197, 94, 0.2);
color: #22c55e; color: #22c55e;
border: 1px solid #22c55e; border: 1px solid #22c55e;
} }
.status-finished { .status-success {
background: rgba(239, 68, 68, 0.2); background: rgba(239, 68, 68, 0.2);
color: #ef4444; color: #ef4444;
border: 1px solid #ef4444; border: 1px solid #ef4444;
} }
.status-upcoming { .status-secondary {
background: rgba(59, 130, 246, 0.2); background: rgba(59, 130, 246, 0.2);
color: #3b82f6; color: #3b82f6;
border: 1px solid #3b82f6; border: 1px solid #3b82f6;
} }
.status-dot { .status-dot {
width: 8px; width: 8px;
height: 8px; height: 8px;
border-radius: 50%; border-radius: 50%;
background: currentColor; background: currentColor;
animation: pulse 2s infinite; animation: pulse 2s infinite;
} }
@keyframes pulse { @keyframes pulse {
0%, 100% { opacity: 1; } 0%,
50% { opacity: 0.5; } 100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
} }
/* Versus Section */ /* Versus Section */
.match-versus { .match-versus {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
margin: 24px 0; margin: 24px 0;
position: relative; position: relative;
} }
.fencer { .fencer {
flex: 1; flex: 1;
text-align: center; text-align: center;
color: white; color: white;
} }
.fencer-name { .fencer-name {
font-size: 18px; font-size: 18px;
font-weight: 700; font-weight: 700;
margin-bottom: 4px; margin-bottom: 4px;
color: #daa520; color: #daa520;
} }
.fencer-club { .fencer-club {
font-size: 12px; font-size: 12px;
color: #9ca3af; color: #9ca3af;
margin-bottom: 8px; margin-bottom: 8px;
} }
.score { .score {
font-size: 32px; font-size: 32px;
font-weight: 900; font-weight: 900;
color: white; color: white;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5); text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
} }
.vs-divider { .vs-divider {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
gap: 4px; gap: 4px;
margin: 0 20px; margin: 0 20px;
} }
.vs-text { .vs-text {
font-size: 14px; font-size: 14px;
font-weight: 700; font-weight: 700;
color: #6b7280; color: #6b7280;
letter-spacing: 2px; letter-spacing: 2px;
} }
.crossed-swords { .crossed-swords {
font-size: 20px; font-size: 20px;
opacity: 0.7; opacity: 0.7;
} }
/* Details */ /* Details */
.match-details { .match-details {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 12px; gap: 12px;
margin-top: 20px; margin-top: 20px;
padding-top: 20px; padding-top: 20px;
border-top: 1px solid rgba(255, 255, 255, 0.1); border-top: 1px solid rgba(255, 255, 255, 0.1);
} }
.detail-item { .detail-item {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 12px;
font-size: 14px; font-size: 14px;
} }
.detail-item .icon { .detail-item .icon {
font-size: 16px; font-size: 16px;
width: 20px; width: 20px;
text-align: center; text-align: center;
} }
.detail-item .label { .detail-item .label {
color: #9ca3af; color: #9ca3af;
font-weight: 500; font-weight: 500;
min-width: 60px; min-width: 60px;
} }
.detail-item .value { .detail-item .value {
color: white; color: white;
font-weight: 600; font-weight: 600;
} }
/* Responsive */ /* Responsive */
@media (max-width: 768px) { @media (max-width: 768px) {
.match-card { .match-card {
padding: 16px; padding: 16px;
} }
.match-versus { .match-versus {
flex-direction: column; flex-direction: column;
gap: 16px; gap: 16px;
} }
.vs-divider { .vs-divider {
transform: rotate(90deg); transform: rotate(90deg);
margin: 0; margin: 0;
} }
.fencer { .fencer {
width: 100%; width: 100%;
} }
} }
.combat-arena { .combat-arena {
margin-top: 24px; margin-top: 24px;
background: background: linear-gradient(
linear-gradient(135deg, 135deg,
rgba(220, 38, 38, 0.1) 0%, rgba(220, 38, 38, 0.1) 0%,
rgba(0, 0, 0, 0.8) 50%, rgba(0, 0, 0, 0.8) 50%,
rgba(255, 215, 0, 0.1) 100% rgba(255, 215, 0, 0.1) 100%
); );
border: 2px solid; border: 2px solid;
border-image: linear-gradient(45deg, #dc2626, #ffd700, #dc2626) 1; border-image: linear-gradient(45deg, #dc2626, #ffd700, #dc2626) 1;
border-radius: 12px; border-radius: 12px;
padding: 20px; padding: 20px;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
} }
.combat-arena::before { .combat-arena::before {
content: ''; content: "";
position: absolute; position: absolute;
top: -50%; top: -50%;
left: -50%; left: -50%;
width: 200%; width: 200%;
height: 200%; height: 200%;
background: background: repeating-conic-gradient(
repeating-conic-gradient( from 0deg at 50% 50%,
from 0deg at 50% 50%, transparent 0deg 2deg,
transparent 0deg 2deg, rgba(220, 38, 38, 0.05) 2deg 4deg
rgba(220, 38, 38, 0.05) 2deg 4deg );
); animation: rotate 20s linear infinite;
animation: rotate 20s linear infinite; pointer-events: none;
pointer-events: none;
} }
@keyframes rotate { @keyframes rotate {
0% { transform: rotate(0deg); } 0% {
100% { transform: rotate(360deg); } transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
} }
/* Header Arena */ /* Header Arena */
.arena-header { .arena-header {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 12px; gap: 12px;
margin-bottom: 20px; margin-bottom: 20px;
position: relative; position: relative;
} }
.arena-title { .arena-title {
font-family: 'Orbitron', monospace; font-family: "Orbitron", monospace;
font-size: 16px; font-size: 16px;
font-weight: 900; font-weight: 900;
color: #ffd700; color: #ffd700;
letter-spacing: 3px; letter-spacing: 3px;
text-shadow: text-shadow: 0 0 10px #ffd700, 0 0 20px #ffd700, 0 0 30px #ffd700;
0 0 10px #ffd700, animation: titleGlow 2s ease-in-out infinite alternate;
0 0 20px #ffd700,
0 0 30px #ffd700;
animation: titleGlow 2s ease-in-out infinite alternate;
} }
@keyframes titleGlow { @keyframes titleGlow {
from { text-shadow: 0 0 10px #ffd700, 0 0 20px #ffd700, 0 0 30px #ffd700; } from {
to { text-shadow: 0 0 20px #ffd700, 0 0 30px #ffd700, 0 0 40px #ffd700; } text-shadow: 0 0 10px #ffd700, 0 0 20px #ffd700, 0 0 30px #ffd700;
}
to {
text-shadow: 0 0 20px #ffd700, 0 0 30px #ffd700, 0 0 40px #ffd700;
}
} }
.arena-pulse { .arena-pulse {
width: 10px; width: 10px;
height: 10px; height: 10px;
background: #dc2626; background: #dc2626;
border-radius: 50%; border-radius: 50%;
position: relative; position: relative;
animation: pulse-danger 1s infinite; animation: pulse-danger 1s infinite;
} }
.arena-pulse::before { .arena-pulse::before {
content: ''; content: "";
position: absolute; position: absolute;
top: -5px; top: -5px;
left: -5px; left: -5px;
right: -5px; right: -5px;
bottom: -5px; bottom: -5px;
background: #dc2626; background: #dc2626;
border-radius: 50%; border-radius: 50%;
opacity: 0.3; opacity: 0.3;
animation: pulse-ring 1s infinite; animation: pulse-ring 1s infinite;
} }
@keyframes pulse-danger { @keyframes pulse-danger {
0%, 100% { transform: scale(1); } 0%,
50% { transform: scale(1.2); } 100% {
transform: scale(1);
}
50% {
transform: scale(1.2);
}
} }
@keyframes pulse-ring { @keyframes pulse-ring {
0% { transform: scale(0.8); opacity: 0.8; } 0% {
100% { transform: scale(2); opacity: 0; } transform: scale(0.8);
opacity: 0.8;
}
100% {
transform: scale(2);
opacity: 0;
}
} }
/* Combat Grid */ /* Combat Grid */
.combat-grid { .combat-grid {
display: grid; display: grid;
grid-template-columns: 1fr; grid-template-columns: 1fr;
gap: 16px; gap: 16px;
} }
.combat-stat { .combat-stat {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 16px; gap: 16px;
padding: 16px; padding: 16px;
background: background: linear-gradient(
linear-gradient(90deg, 90deg,
rgba(0, 0, 0, 0.8) 0%, rgba(0, 0, 0, 0.8) 0%,
rgba(26, 26, 46, 0.9) 50%, rgba(26, 26, 46, 0.9) 50%,
rgba(0, 0, 0, 0.8) 100% rgba(0, 0, 0, 0.8) 100%
); );
border: 1px solid rgba(255, 215, 0, 0.3); border: 1px solid rgba(255, 215, 0, 0.3);
border-radius: 8px; border-radius: 8px;
position: relative; position: relative;
transition: all 0.3s ease; transition: all 0.3s ease;
cursor: pointer; cursor: pointer;
} }
.combat-stat:hover { .combat-stat:hover {
transform: translateX(8px); transform: translateX(8px);
border-color: #ffd700; border-color: #ffd700;
box-shadow: box-shadow: 0 4px 20px rgba(255, 215, 0, 0.3),
0 4px 20px rgba(255, 215, 0, 0.3), inset 0 1px 0 rgba(255, 215, 0, 0.1);
inset 0 1px 0 rgba(255, 215, 0, 0.1);
} }
/* Icons animés */ /* Icons animés */
.stat-icon { .stat-icon {
position: relative; position: relative;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 48px; width: 48px;
height: 48px; height: 48px;
font-size: 24px; font-size: 24px;
} }
.location-radar { .location-radar {
position: absolute; position: absolute;
width: 40px; width: 40px;
height: 40px; height: 40px;
border: 2px solid #10b981; border: 2px solid #10b981;
border-radius: 50%; border-radius: 50%;
border-top-color: transparent; border-top-color: transparent;
animation: radar-spin 2s linear infinite; animation: radar-spin 2s linear infinite;
} }
.referee-badge { .referee-badge {
position: absolute; position: absolute;
width: 35px; width: 35px;
height: 35px; height: 35px;
background: linear-gradient(45deg, #ffd700, #ffed4a); background: linear-gradient(45deg, #ffd700, #ffed4a);
clip-path: polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%); clip-path: polygon(
animation: badge-pulse 3s ease-in-out infinite; 50% 0%,
61% 35%,
98% 35%,
68% 57%,
79% 91%,
50% 70%,
21% 91%,
32% 57%,
2% 35%,
39% 35%
);
animation: badge-pulse 3s ease-in-out infinite;
} }
.time-ring { .time-ring {
position: absolute; position: absolute;
width: 36px; width: 36px;
height: 36px; height: 36px;
border: 3px solid #dc2626; border: 3px solid #dc2626;
border-radius: 50%; border-radius: 50%;
border-left-color: transparent; border-left-color: transparent;
animation: time-tick 1s linear infinite; animation: time-tick 1s linear infinite;
} }
@keyframes radar-spin { @keyframes radar-spin {
0% { transform: rotate(0deg); } 0% {
100% { transform: rotate(360deg); } transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
} }
@keyframes badge-pulse { @keyframes badge-pulse {
0%, 100% { transform: scale(1) rotate(0deg); } 0%,
50% { transform: scale(1.1) rotate(5deg); } 100% {
transform: scale(1) rotate(0deg);
}
50% {
transform: scale(1.1) rotate(5deg);
}
} }
@keyframes time-tick { @keyframes time-tick {
0% { transform: rotate(0deg); } 0% {
100% { transform: rotate(360deg); } transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
} }
/* Content */ /* Content */
.stat-content { .stat-content {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 2px; gap: 2px;
} }
.stat-label { .stat-label {
font-family: 'Orbitron', monospace; font-family: "Orbitron", monospace;
font-size: 10px; font-size: 10px;
font-weight: 700; font-weight: 700;
color: #6b7280; color: #6b7280;
letter-spacing: 1px; letter-spacing: 1px;
text-transform: uppercase; text-transform: uppercase;
} }
.stat-value { .stat-value {
font-size: 16px; font-size: 16px;
font-weight: 700; font-weight: 700;
color: #ffffff; color: #ffffff;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5); text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
} }
.stat-sub { .stat-sub {
font-size: 11px; font-size: 11px;
color: #9ca3af; color: #9ca3af;
font-weight: 500; font-weight: 500;
} }
/* Glows spécifiques */ /* Glows spécifiques */
.venue .stat-value { color: #10b981; } .venue .stat-value {
.referee .stat-value { color: #ffd700; } color: #10b981;
.timing .stat-value { color: #dc2626; } }
.referee .stat-value {
color: #ffd700;
}
.timing .stat-value {
color: #dc2626;
}
.stat-glow { .stat-glow {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
border-radius: 8px; border-radius: 8px;
opacity: 0; opacity: 0;
transition: opacity 0.3s ease; transition: opacity 0.3s ease;
pointer-events: none; pointer-events: none;
} }
.combat-stat:hover .stat-glow { .combat-stat:hover .stat-glow {
opacity: 1; opacity: 1;
} }
.venue-glow { box-shadow: inset 0 0 20px rgba(16, 185, 129, 0.2); } .venue-glow {
.referee-glow { box-shadow: inset 0 0 20px rgba(255, 215, 0, 0.2); } box-shadow: inset 0 0 20px rgba(16, 185, 129, 0.2);
.timing-glow { box-shadow: inset 0 0 20px rgba(220, 38, 38, 0.2); }
/* Footer Arena */
.arena-footer {
margin-top: 20px;
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
} }
.referee-glow {
.danger-tape { box-shadow: inset 0 0 20px rgba(255, 215, 0, 0.2);
flex: 1;
height: 4px;
background:
repeating-linear-gradient(
45deg,
#ffd700 0px,
#ffd700 10px,
#dc2626 10px,
#dc2626 20px
);
animation: tape-move 2s linear infinite;
} }
.timing-glow {
@keyframes tape-move { box-shadow: inset 0 0 20px rgba(220, 38, 38, 0.2);
0% { background-position: 0px 0px; }
100% { background-position: 28px 0px; }
}
.warning-text {
font-family: 'Orbitron', monospace;
font-size: 12px;
font-weight: 700;
color: #ffd700;
letter-spacing: 1px;
animation: warning-blink 1.5s ease-in-out infinite alternate;
}
@keyframes warning-blink {
0% { opacity: 1; }
100% { opacity: 0.6; }
} }
/* Responsive */ /* Responsive */
@media (min-width: 768px) { @media (min-width: 768px) {
.combat-grid { .combat-grid {
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
} }
.combat-stat {
flex-direction: column;
text-align: center;
gap: 12px;
}
.combat-stat:hover {
transform: translateY(-4px);
}
}
.combat-stat {
flex-direction: column;
text-align: center;
gap: 12px;
}
.combat-stat:hover {
transform: translateY(-4px);
}
}

View file

@ -1,91 +1,114 @@
<div class="match-card"> <div class="match-card" *ngIf="latestMatch">
<div class="match-background"> <div class="match-background">
<img src="./assets/duel.webp" alt="Escrime" class="background-image"> <img src="./assets/duel.webp" alt="Escrime" class="background-image" />
<div class="image-overlay"></div> <div class="image-overlay"></div>
</div>
<div class="match-header">
<div class="match-weapon">
<i class="sword-icon">⚔️</i>
<span class="weapon-type">{{ latestMatch.weapon }}</span>
</div>
<div
class="match-status"
[ngClass]="'status-' + getMatchStateColor(latestMatch.state)"
>
<span class="status-dot"></span>
{{ getMatchStateLabel(latestMatch.state) }}
</div>
</div>
<div class="match-versus">
<div class="fencer fencer-left">
<div class="fencer-name">
{{ player1?.name }} {{ player1?.firstName }}
</div>
<div class="fencer-club">{{ player1?.club }}</div>
<div class="score">{{ latestMatch.score1 }}</div>
</div> </div>
<div class="match-header"> <div class="vs-divider">
<div class="match-weapon"> <span class="vs-text">VS</span>
<i class="sword-icon">⚔️</i> <div class="crossed-swords">⚔️</div>
<span class="weapon-type">Épée</span>
</div>
<div class="match-status status-live">
<span class="status-dot"></span>
En cours
</div>
</div> </div>
<div class="match-versus"> <div class="fencer fencer-right">
<div class="fencer fencer-left"> <div class="fencer-name">
<div class="fencer-name">Martin DUBOIS</div> {{ player2?.name }} {{ player2?.firstName }}
<div class="fencer-club">CE Paris</div> </div>
<div class="score">12</div> <div class="fencer-club">{{ player2?.club }}</div>
</div> <div class="score">{{ latestMatch.score2 }}</div>
</div>
</div>
<div class="vs-divider"> <div class="combat-arena">
<span class="vs-text">VS</span> <div class="arena-header">
<div class="crossed-swords">⚔️</div> <span class="arena-title">ARENA COMBAT</span>
</div> <div class="arena-pulse"></div>
<div class="fencer fencer-right">
<div class="fencer-name">Sophie BERNARD</div>
<div class="fencer-club">Escrime Lyon</div>
<div class="score">8</div>
</div>
</div> </div>
<div class="combat-arena"> <div class="combat-grid">
<div class="arena-header"> <div class="combat-stat venue">
<span class="arena-title">ARENA COMBAT</span> <div class="stat-icon">
<div class="arena-pulse"></div> <div class="location-radar"></div>
<span>🏛️</span>
</div> </div>
<div class="stat-content">
<div class="combat-grid"> <span class="stat-label">BATTLEFIELD</span>
<div class="combat-stat venue"> <span class="stat-value">{{ latestMatch.city }}</span>
<div class="stat-icon"> <span class="stat-sub">{{ latestMatch.country }}</span>
<div class="location-radar"></div>
<span>🏛️</span>
</div>
<div class="stat-content">
<span class="stat-label">BATTLEFIELD</span>
<span class="stat-value">Gymnase Jean Moulin</span>
<span class="stat-sub">PARIS • ZONE A</span>
</div>
<div class="stat-glow venue-glow"></div>
</div>
<div class="combat-stat referee">
<div class="stat-icon">
<div class="referee-badge"></div>
<span>⚖️</span>
</div>
<div class="stat-content">
<span class="stat-label">COMBAT JUDGE</span>
<span class="stat-value">Pierre MARTIN</span>
<span class="stat-sub">OFFICIAL REFEREE</span>
</div>
<div class="stat-glow referee-glow"></div>
</div>
<div class="combat-stat timing">
<div class="stat-icon">
<div class="time-ring"></div>
<span></span>
</div>
<div class="stat-content">
<span class="stat-label">COMBAT START</span>
<span class="stat-value">14:30</span>
<span class="stat-sub">LIVE BATTLE</span>
</div>
<div class="stat-glow timing-glow"></div>
</div>
</div> </div>
<div class="stat-glow venue-glow"></div>
</div>
<div class="arena-footer"> <div class="combat-stat referee">
<div class="danger-tape"></div> <div class="stat-icon">
<span class="warning-text">⚠️ COMBAT ZONE ACTIVE ⚠️</span> <div class="referee-badge"></div>
<span>⚖️</span>
</div> </div>
<div class="stat-content">
<span class="stat-label">COMBAT JUDGE</span>
<span class="stat-value"
>{{ referee?.name }} {{ referee?.firstName }}</span
>
<span class="stat-sub">OFFICIAL REFEREE</span>
</div>
<div class="stat-glow referee-glow"></div>
</div>
<div
class="combat-stat timing"
*ngIf="latestMatch?.state === MatchState.NOT_STARTED"
>
<div class="stat-icon">
<div class="time-ring"></div>
<span></span>
</div>
<div class="stat-content">
<span class="stat-label">COMBAT START</span>
<span class="stat-value">{{ getTimeUntilMatch() }}</span>
<span class="stat-sub">LIVE BATTLE</span>
</div>
<div class="stat-glow timing-glow"></div>
</div>
<div
class="combat-stat funfact"
*ngIf="latestMatch?.state !== MatchState.NOT_STARTED"
>
<div class="stat-icon">
<span>🎯</span>
</div>
<div class="stat-content">
<span class="stat-label">FUN FACT</span>
<span class="stat-value">
Saviez-vous que l'escrime est l'un des sports les plus anciens aux
Jeux Olympiques ?
</span>
<span class="stat-sub">Inspiration et tradition au rendez-vous</span>
</div>
</div>
</div> </div>
</div>
</div> </div>
<app-sponsort></app-sponsort> <app-sponsort></app-sponsort>

View file

@ -1,9 +1,159 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { MatchesService } from '@services/matches/matches.service';
import { Matches, MatchState } from '@interfaces/matches';
import { PlayerService } from '@services/player/player.service';
import { Player } from '@interfaces/player';
import { RefereeService } from '@services/referee/referee.service';
import { Referee, RefereeLevel } from '@interfaces/referee';
@Component({ @Component({
selector: 'app-home', selector: 'app-home',
standalone: false, standalone: false,
templateUrl: './home.component.html', templateUrl: './home.component.html',
styleUrl: './home.component.css', styleUrl: './home.component.css',
}) })
export class HomeComponent {} export class HomeComponent {
latestMatch?: Matches;
player1?: Player;
player2?: Player;
referee?: Referee;
MatchState = MatchState;
intervalId?: any;
timeUntilMatch: string | null = null;
constructor(
private matchesService: MatchesService,
private playerService: PlayerService,
private refereeService: RefereeService
) {}
ngOnInit(): void {
this.getLatestMatch();
}
ngOnDestroy(): void {
if (this.intervalId) {
clearInterval(this.intervalId);
}
}
private getLatestMatch(): void {
this.matchesService.getMatches().subscribe((matches) => {
if (matches.length > 0) {
this.latestMatch = matches[matches.length - 1];
console.log(
'[HomeComponent] 🏁 Dernier match ajouté :',
this.latestMatch
);
// Récupération des données liées
this.loadRelatedEntities(this.latestMatch);
if (this.latestMatch.state === MatchState.NOT_STARTED) {
this.updateTimeUntilMatch(); // initial
this.intervalId = setInterval(() => {
this.updateTimeUntilMatch();
}, 60000); // chaque minute
}
} else {
console.warn('[HomeComponent] Aucun match disponible');
}
});
}
private updateTimeUntilMatch(): void {
if (!this.latestMatch) return;
const now = new Date();
const matchDate = new Date(this.latestMatch.date);
const diffMs = matchDate.getTime() - now.getTime();
if (diffMs <= 0) {
this.timeUntilMatch = null;
clearInterval(this.intervalId);
return;
}
const totalMinutes = Math.floor(diffMs / (1000 * 60));
const days = Math.floor(totalMinutes / (60 * 24));
const hours = Math.floor((totalMinutes % (60 * 24)) / 60);
const minutes = totalMinutes % 60;
let result = '';
if (days > 0) result += `${days}j `;
if (hours > 0 || days > 0) result += `${hours}h `;
result += `${minutes}min`;
this.timeUntilMatch = result.trim();
}
private loadRelatedEntities(match: Matches): void {
this.playerService.getPlayerById(match.player1ID).subscribe((p1) => {
this.player1 = p1!;
});
this.playerService.getPlayerById(match.player2ID).subscribe((p2) => {
this.player2 = p2!;
});
this.refereeService.getRefereeById(match.refereeID).subscribe((ref) => {
this.referee = ref!;
});
}
getMatchStateLabel(state: MatchState): string {
switch (state) {
case MatchState.NOT_STARTED:
return 'À venir';
case MatchState.ONGOING:
return 'En cours';
case MatchState.OVER:
return 'Terminé';
default:
return 'Inconnu';
}
}
getMatchStateColor(state: MatchState): string {
switch (state) {
case MatchState.NOT_STARTED:
return 'secondary';
case MatchState.ONGOING:
return 'warning';
case MatchState.OVER:
return 'success';
default:
return 'light';
}
}
getTimeUntilMatch(): string | null {
if (
!this.latestMatch ||
this.latestMatch.state !== MatchState.NOT_STARTED
) {
return null;
}
const now = new Date();
const matchDate = new Date(this.latestMatch.date);
const diffMs = matchDate.getTime() - now.getTime();
if (diffMs <= 0) return null;
const totalMinutes = Math.floor(diffMs / (1000 * 60));
const days = Math.floor(totalMinutes / (60 * 24));
const hours = Math.floor((totalMinutes % (60 * 24)) / 60);
const minutes = totalMinutes % 60;
let result = '';
if (days > 0) result += `${days}j `;
if (hours > 0 || days > 0) result += `${hours}h `;
result += `${minutes}min`;
return result.trim();
}
}

View file

@ -0,0 +1,18 @@
form fieldset {
background-color: #f8f9fa; /* gris clair */
}
form legend {
font-weight: 600;
font-size: 1.1rem;
}
form label {
font-weight: 500;
}
form button {
width: 100%;
padding: 0.6rem;
font-size: 1.1rem;
}

View file

@ -1,102 +1,171 @@
<h2>Ajouter un match</h2> <h2 class="mb-4">Ajouter un match</h2>
<form (ngSubmit)="onSubmit()"> <form (ngSubmit)="onSubmit()" class="needs-validation" novalidate>
<fieldset> <fieldset class="border p-3 mb-4 rounded">
<legend>Joueur 1</legend> <legend class="w-auto px-2">Joueur 1</legend>
<label>Prénom :</label>
<input
type="text"
[(ngModel)]="formData.player1FirstName"
name="player1FirstName"
required
/>
<label>Nom :</label> <div class="mb-3">
<input <label class="form-label">Prénom :</label>
type="text" <input
[(ngModel)]="formData.player1Name" type="text"
name="player1Name" class="form-control"
required [(ngModel)]="formData.player1FirstName"
/> name="player1FirstName"
required
/>
</div>
<label>Club :</label> <div class="mb-3">
<input <label class="form-label">Nom :</label>
type="text" <input
[(ngModel)]="formData.player1Club" type="text"
name="player1Club" class="form-control"
required [(ngModel)]="formData.player1Name"
/> name="player1Name"
required
/>
</div>
<div class="mb-3">
<label class="form-label">Club :</label>
<input
type="text"
class="form-control"
[(ngModel)]="formData.player1Club"
name="player1Club"
required
/>
</div>
</fieldset> </fieldset>
<fieldset> <fieldset class="border p-3 mb-4 rounded">
<legend>Joueur 2</legend> <legend class="w-auto px-2">Joueur 2</legend>
<label>Prénom :</label>
<input
type="text"
[(ngModel)]="formData.player2FirstName"
name="player2FirstName"
required
/>
<label>Nom :</label> <div class="mb-3">
<input <label class="form-label">Prénom :</label>
type="text" <input
[(ngModel)]="formData.player2Name" type="text"
name="player2Name" class="form-control"
required [(ngModel)]="formData.player2FirstName"
/> name="player2FirstName"
required
/>
</div>
<label>Club :</label> <div class="mb-3">
<input <label class="form-label">Nom :</label>
type="text" <input
[(ngModel)]="formData.player2Club" type="text"
name="player2Club" class="form-control"
required [(ngModel)]="formData.player2Name"
/> name="player2Name"
required
/>
</div>
<div class="mb-3">
<label class="form-label">Club :</label>
<input
type="text"
class="form-control"
[(ngModel)]="formData.player2Club"
name="player2Club"
required
/>
</div>
</fieldset> </fieldset>
<fieldset> <fieldset class="border p-3 mb-4 rounded">
<legend>Arbitre</legend> <legend class="w-auto px-2">Arbitre</legend>
<label>Prénom :</label>
<input
type="text"
[(ngModel)]="formData.refereeFirstName"
name="refereeFirstName"
required
/>
<label>Nom :</label> <div class="mb-3">
<input <label class="form-label">Prénom :</label>
type="text" <input
[(ngModel)]="formData.refereeName" type="text"
name="refereeName" class="form-control"
required [(ngModel)]="formData.refereeFirstName"
/> name="refereeFirstName"
required
/>
</div>
<label>Niveau :</label> <div class="mb-3">
<select [(ngModel)]="formData.refereelevel" name="refereelevel" required> <label class="form-label">Nom :</label>
<option [value]="0">Départemental</option> <input
<option [value]="1">Régional</option> type="text"
<option [value]="2">National</option> class="form-control"
<option [value]="3">International</option> [(ngModel)]="formData.refereeName"
</select> name="refereeName"
required
/>
</div>
<div class="mb-3">
<label class="form-label">Niveau :</label>
<select
class="form-select"
[(ngModel)]="formData.refereelevel"
name="refereelevel"
required
>
<option [value]="0">Départemental</option>
<option [value]="1">Régional</option>
<option [value]="2">National</option>
<option [value]="3">International</option>
</select>
</div>
</fieldset> </fieldset>
<fieldset> <fieldset class="border p-3 mb-4 rounded">
<legend>Match</legend> <legend class="w-auto px-2">Match</legend>
<label>Ville :</label>
<input type="text" [(ngModel)]="formData.city" name="city" required />
<label>Pays :</label> <div class="mb-3">
<input type="text" [(ngModel)]="formData.country" name="country" required /> <label class="form-label">Ville :</label>
<input
type="text"
class="form-control"
[(ngModel)]="formData.city"
name="city"
required
/>
</div>
<label>Arme :</label> <div class="mb-3">
<select [(ngModel)]="formData.weapon" name="weapon" required> <label class="form-label">Pays :</label>
<option value="Épée">Épée</option> <input
<option value="Sabre">Sabre</option> type="text"
<option value="Fleuret">Fleuret</option> class="form-control"
</select> [(ngModel)]="formData.country"
name="country"
required
/>
</div>
<div class="mb-3">
<label class="form-label">Arme :</label>
<select
class="form-select"
[(ngModel)]="formData.weapon"
name="weapon"
required
>
<option value="Épée">Épée</option>
<option value="Sabre">Sabre</option>
<option value="Fleuret">Fleuret</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">Date :</label>
<input
type="datetime-local"
class="form-control"
[(ngModel)]="formData.date"
name="date"
required
/>
</div>
</fieldset> </fieldset>
<button type="submit">Créer le match</button> <button type="submit" class="btn btn-primary">Créer le match</button>
</form> </form>

View file

@ -36,6 +36,7 @@ export class MatchesAddComponent {
city: '', city: '',
country: '', country: '',
weapon: '', weapon: '',
date: '',
}; };
constructor( constructor(
@ -82,7 +83,7 @@ export class MatchesAddComponent {
country: this.formData.country, country: this.formData.country,
city: this.formData.city, city: this.formData.city,
weapon: this.formData.weapon, weapon: this.formData.weapon,
date: new Date(), date: new Date(this.formData.date),
state: MatchState.NOT_STARTED, state: MatchState.NOT_STARTED,
}; };

View file

@ -11,6 +11,8 @@ import { Player } from '@interfaces/player';
import { RefereeService } from '@services/referee/referee.service'; import { RefereeService } from '@services/referee/referee.service';
import { Referee, RefereeLevel } from '@interfaces/referee'; import { Referee, RefereeLevel } from '@interfaces/referee';
import { getMatchStateLabel, getMatchStateColor } from '@utils/getState';
@Component({ @Component({
selector: 'app-matches-id', selector: 'app-matches-id',
standalone: false, standalone: false,

View file

@ -0,0 +1,27 @@
import { MatchState } from '@interfaces/matches';
export function getMatchStateLabel(state: MatchState): string {
switch (state) {
case MatchState.NOT_STARTED:
return 'À venir';
case MatchState.ONGOING:
return 'En cours';
case MatchState.OVER:
return 'Terminé';
default:
return 'Inconnu';
}
}
export function getMatchStateColor(state: MatchState): string {
switch (state) {
case MatchState.NOT_STARTED:
return 'secondary';
case MatchState.ONGOING:
return 'warning';
case MatchState.OVER:
return 'success';
default:
return 'light';
}
}