Spaces:
Sleeping
Sleeping
Commit ·
2477176
1
Parent(s): 9062066
feat: add credits logic
Browse files- backend/app/models/participant.py +25 -11
- backend/app/schemas/participant.py +7 -2
- backend/app/services/leaderboard_service.py +2 -0
- backend/app/services/pack_service.py +11 -10
- backend/scripts/add_pack_credits.py +52 -0
- frontend/src/components/packs/PackCard.tsx +7 -7
- frontend/src/pages/PacksPage.tsx +31 -9
- frontend/src/types/participant.ts +2 -0
backend/app/models/participant.py
CHANGED
|
@@ -69,7 +69,15 @@ class Participant(Base):
|
|
| 69 |
default=0,
|
| 70 |
nullable=False,
|
| 71 |
index=True,
|
| 72 |
-
comment="Current total points accumulated"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
)
|
| 74 |
|
| 75 |
# Pack Inventory (Phase 2 feature, prepared for future use)
|
|
@@ -125,7 +133,10 @@ class Participant(Base):
|
|
| 125 |
|
| 126 |
def add_points(self, amount: int) -> None:
|
| 127 |
"""
|
| 128 |
-
Add points to the participant's total.
|
|
|
|
|
|
|
|
|
|
| 129 |
|
| 130 |
Args:
|
| 131 |
amount: Points to add (positive integer)
|
|
@@ -134,24 +145,27 @@ class Participant(Base):
|
|
| 134 |
ValueError: If amount is negative
|
| 135 |
"""
|
| 136 |
if amount < 0:
|
| 137 |
-
raise ValueError("Cannot add negative points.
|
| 138 |
self.total_points += amount
|
|
|
|
| 139 |
|
| 140 |
-
def
|
| 141 |
"""
|
| 142 |
-
Subtract
|
|
|
|
|
|
|
| 143 |
|
| 144 |
Args:
|
| 145 |
-
amount:
|
| 146 |
|
| 147 |
Raises:
|
| 148 |
-
ValueError: If amount is negative or
|
| 149 |
"""
|
| 150 |
if amount < 0:
|
| 151 |
-
raise ValueError("Cannot subtract negative
|
| 152 |
-
if self.
|
| 153 |
-
raise ValueError("Insufficient
|
| 154 |
-
self.
|
| 155 |
|
| 156 |
def add_pack(self, pack_tier: str) -> None:
|
| 157 |
"""
|
|
|
|
| 69 |
default=0,
|
| 70 |
nullable=False,
|
| 71 |
index=True,
|
| 72 |
+
comment="Current total points accumulated (never decreases)"
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
# Pack Credits - separate from points, used to purchase packs
|
| 76 |
+
pack_credits = Column(
|
| 77 |
+
Integer,
|
| 78 |
+
default=0,
|
| 79 |
+
nullable=False,
|
| 80 |
+
comment="Credits for purchasing packs (1 point earned = 1 credit)"
|
| 81 |
)
|
| 82 |
|
| 83 |
# Pack Inventory (Phase 2 feature, prepared for future use)
|
|
|
|
| 133 |
|
| 134 |
def add_points(self, amount: int) -> None:
|
| 135 |
"""
|
| 136 |
+
Add points to the participant's total AND pack credits.
|
| 137 |
+
|
| 138 |
+
Points are used for leaderboard ranking and never decrease.
|
| 139 |
+
Pack credits are also added (1:1 ratio) and can be spent on packs.
|
| 140 |
|
| 141 |
Args:
|
| 142 |
amount: Points to add (positive integer)
|
|
|
|
| 145 |
ValueError: If amount is negative
|
| 146 |
"""
|
| 147 |
if amount < 0:
|
| 148 |
+
raise ValueError("Cannot add negative points.")
|
| 149 |
self.total_points += amount
|
| 150 |
+
self.pack_credits += amount
|
| 151 |
|
| 152 |
+
def subtract_credits(self, amount: int) -> None:
|
| 153 |
"""
|
| 154 |
+
Subtract pack credits (used for purchasing packs).
|
| 155 |
+
|
| 156 |
+
Note: This does NOT affect total_points, which never decrease.
|
| 157 |
|
| 158 |
Args:
|
| 159 |
+
amount: Credits to subtract (positive integer)
|
| 160 |
|
| 161 |
Raises:
|
| 162 |
+
ValueError: If amount is negative or insufficient credits
|
| 163 |
"""
|
| 164 |
if amount < 0:
|
| 165 |
+
raise ValueError("Cannot subtract negative credits.")
|
| 166 |
+
if self.pack_credits < amount:
|
| 167 |
+
raise ValueError(f"Insufficient credits. Need {amount}, have {self.pack_credits}")
|
| 168 |
+
self.pack_credits -= amount
|
| 169 |
|
| 170 |
def add_pack(self, pack_tier: str) -> None:
|
| 171 |
"""
|
backend/app/schemas/participant.py
CHANGED
|
@@ -104,7 +104,8 @@ class ParticipantResponse(ParticipantBase):
|
|
| 104 |
Used in GET requests and as part of other responses.
|
| 105 |
"""
|
| 106 |
id: int = Field(..., description="Unique participant ID")
|
| 107 |
-
total_points: int = Field(..., description="Current total points")
|
|
|
|
| 108 |
current_packs: dict = Field(
|
| 109 |
...,
|
| 110 |
description="Pack counts for each tier (bronze, silver, gold, ultimate)"
|
|
@@ -122,6 +123,7 @@ class ParticipantResponse(ParticipantBase):
|
|
| 122 |
"avatar_url": "https://example.com/avatars/paul.jpg",
|
| 123 |
"is_groom": True,
|
| 124 |
"total_points": 350,
|
|
|
|
| 125 |
"current_packs": {
|
| 126 |
"bronze": 2,
|
| 127 |
"silver": 1,
|
|
@@ -145,6 +147,7 @@ class ParticipantSummary(BaseModel):
|
|
| 145 |
avatar_url: Optional[str] = None
|
| 146 |
is_groom: bool
|
| 147 |
total_points: int
|
|
|
|
| 148 |
|
| 149 |
class Config:
|
| 150 |
"""Pydantic configuration."""
|
|
@@ -155,7 +158,8 @@ class ParticipantSummary(BaseModel):
|
|
| 155 |
"name": "Paul C.",
|
| 156 |
"avatar_url": "https://example.com/avatars/paul.jpg",
|
| 157 |
"is_groom": True,
|
| 158 |
-
"total_points": 350
|
|
|
|
| 159 |
}
|
| 160 |
}
|
| 161 |
|
|
@@ -182,6 +186,7 @@ class ParticipantWithRank(ParticipantSummary):
|
|
| 182 |
"avatar_url": "https://example.com/avatars/paul.jpg",
|
| 183 |
"is_groom": True,
|
| 184 |
"total_points": 350,
|
|
|
|
| 185 |
"rank": 1,
|
| 186 |
"points_today": 75
|
| 187 |
}
|
|
|
|
| 104 |
Used in GET requests and as part of other responses.
|
| 105 |
"""
|
| 106 |
id: int = Field(..., description="Unique participant ID")
|
| 107 |
+
total_points: int = Field(..., description="Current total points (never decreases)")
|
| 108 |
+
pack_credits: int = Field(..., description="Credits available for purchasing packs")
|
| 109 |
current_packs: dict = Field(
|
| 110 |
...,
|
| 111 |
description="Pack counts for each tier (bronze, silver, gold, ultimate)"
|
|
|
|
| 123 |
"avatar_url": "https://example.com/avatars/paul.jpg",
|
| 124 |
"is_groom": True,
|
| 125 |
"total_points": 350,
|
| 126 |
+
"pack_credits": 150,
|
| 127 |
"current_packs": {
|
| 128 |
"bronze": 2,
|
| 129 |
"silver": 1,
|
|
|
|
| 147 |
avatar_url: Optional[str] = None
|
| 148 |
is_groom: bool
|
| 149 |
total_points: int
|
| 150 |
+
pack_credits: int
|
| 151 |
|
| 152 |
class Config:
|
| 153 |
"""Pydantic configuration."""
|
|
|
|
| 158 |
"name": "Paul C.",
|
| 159 |
"avatar_url": "https://example.com/avatars/paul.jpg",
|
| 160 |
"is_groom": True,
|
| 161 |
+
"total_points": 350,
|
| 162 |
+
"pack_credits": 150
|
| 163 |
}
|
| 164 |
}
|
| 165 |
|
|
|
|
| 186 |
"avatar_url": "https://example.com/avatars/paul.jpg",
|
| 187 |
"is_groom": True,
|
| 188 |
"total_points": 350,
|
| 189 |
+
"pack_credits": 150,
|
| 190 |
"rank": 1,
|
| 191 |
"points_today": 75
|
| 192 |
}
|
backend/app/services/leaderboard_service.py
CHANGED
|
@@ -47,6 +47,7 @@ def get_leaderboard(db: Session, include_today_points: bool = False) -> List[Par
|
|
| 47 |
avatar_url=participant.avatar_url,
|
| 48 |
is_groom=participant.is_groom,
|
| 49 |
total_points=participant.total_points,
|
|
|
|
| 50 |
rank=rank,
|
| 51 |
points_today=points_today
|
| 52 |
)
|
|
@@ -153,6 +154,7 @@ def get_daily_leader(db: Session) -> ParticipantWithRank:
|
|
| 153 |
avatar_url=participant.avatar_url,
|
| 154 |
is_groom=participant.is_groom,
|
| 155 |
total_points=participant.total_points,
|
|
|
|
| 156 |
rank=get_participant_rank(db, participant.id),
|
| 157 |
points_today=leader_data["points_today"]
|
| 158 |
)
|
|
|
|
| 47 |
avatar_url=participant.avatar_url,
|
| 48 |
is_groom=participant.is_groom,
|
| 49 |
total_points=participant.total_points,
|
| 50 |
+
pack_credits=participant.pack_credits,
|
| 51 |
rank=rank,
|
| 52 |
points_today=points_today
|
| 53 |
)
|
|
|
|
| 154 |
avatar_url=participant.avatar_url,
|
| 155 |
is_groom=participant.is_groom,
|
| 156 |
total_points=participant.total_points,
|
| 157 |
+
pack_credits=participant.pack_credits,
|
| 158 |
rank=get_participant_rank(db, participant.id),
|
| 159 |
points_today=leader_data["points_today"]
|
| 160 |
)
|
backend/app/services/pack_service.py
CHANGED
|
@@ -330,9 +330,10 @@ def _get_animation_effects(rarity: str) -> list[str]:
|
|
| 330 |
|
| 331 |
def purchase_pack(db: Session, participant_id: int, tier: str) -> None:
|
| 332 |
"""
|
| 333 |
-
Purchase a pack using
|
| 334 |
|
| 335 |
-
Deducts
|
|
|
|
| 336 |
|
| 337 |
Args:
|
| 338 |
db: Database session
|
|
@@ -340,11 +341,11 @@ def purchase_pack(db: Session, participant_id: int, tier: str) -> None:
|
|
| 340 |
tier: Pack tier to purchase (bronze/silver/gold/ultimate)
|
| 341 |
|
| 342 |
Raises:
|
| 343 |
-
ValueError: If participant not found, invalid tier, or insufficient
|
| 344 |
|
| 345 |
Example:
|
| 346 |
>>> purchase_pack(db, 1, "bronze")
|
| 347 |
-
>>> # Deducts 100
|
| 348 |
"""
|
| 349 |
participant = db.query(Participant).filter(Participant.id == participant_id).first()
|
| 350 |
|
|
@@ -356,19 +357,19 @@ def purchase_pack(db: Session, participant_id: int, tier: str) -> None:
|
|
| 356 |
|
| 357 |
cost = PACK_COSTS[tier]
|
| 358 |
|
| 359 |
-
# Check if participant has enough
|
| 360 |
-
if participant.
|
| 361 |
-
raise ValueError(f"Insufficient
|
| 362 |
|
| 363 |
-
# Deduct points
|
| 364 |
-
participant.
|
| 365 |
|
| 366 |
# Add pack to inventory
|
| 367 |
participant.add_pack(tier)
|
| 368 |
|
| 369 |
db.commit()
|
| 370 |
|
| 371 |
-
logger.info(f"Participant {participant_id} purchased {tier} pack for {cost}
|
| 372 |
|
| 373 |
|
| 374 |
def add_free_pack(db: Session, participant_id: int, tier: str, count: int = 1) -> None:
|
|
|
|
| 330 |
|
| 331 |
def purchase_pack(db: Session, participant_id: int, tier: str) -> None:
|
| 332 |
"""
|
| 333 |
+
Purchase a pack using pack credits.
|
| 334 |
|
| 335 |
+
Deducts credits from participant and adds pack to inventory.
|
| 336 |
+
Note: This does NOT affect total_points, which never decrease.
|
| 337 |
|
| 338 |
Args:
|
| 339 |
db: Database session
|
|
|
|
| 341 |
tier: Pack tier to purchase (bronze/silver/gold/ultimate)
|
| 342 |
|
| 343 |
Raises:
|
| 344 |
+
ValueError: If participant not found, invalid tier, or insufficient credits
|
| 345 |
|
| 346 |
Example:
|
| 347 |
>>> purchase_pack(db, 1, "bronze")
|
| 348 |
+
>>> # Deducts 100 credits and adds 1 bronze pack
|
| 349 |
"""
|
| 350 |
participant = db.query(Participant).filter(Participant.id == participant_id).first()
|
| 351 |
|
|
|
|
| 357 |
|
| 358 |
cost = PACK_COSTS[tier]
|
| 359 |
|
| 360 |
+
# Check if participant has enough credits
|
| 361 |
+
if participant.pack_credits < cost:
|
| 362 |
+
raise ValueError(f"Insufficient credits. Need {cost} credits, have {participant.pack_credits}")
|
| 363 |
|
| 364 |
+
# Deduct credits (NOT points - points never decrease)
|
| 365 |
+
participant.subtract_credits(cost)
|
| 366 |
|
| 367 |
# Add pack to inventory
|
| 368 |
participant.add_pack(tier)
|
| 369 |
|
| 370 |
db.commit()
|
| 371 |
|
| 372 |
+
logger.info(f"Participant {participant_id} purchased {tier} pack for {cost} credits")
|
| 373 |
|
| 374 |
|
| 375 |
def add_free_pack(db: Session, participant_id: int, tier: str, count: int = 1) -> None:
|
backend/scripts/add_pack_credits.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Add pack_credits column to participants table.
|
| 3 |
+
|
| 4 |
+
This migration adds a new column to track credits used for purchasing packs,
|
| 5 |
+
separate from the points used for leaderboard ranking.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import sys
|
| 9 |
+
import os
|
| 10 |
+
|
| 11 |
+
# Add parent directory to path
|
| 12 |
+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
| 13 |
+
|
| 14 |
+
from app.database import engine
|
| 15 |
+
from sqlalchemy import text
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def add_pack_credits_column():
|
| 19 |
+
"""Add pack_credits column to participants table."""
|
| 20 |
+
with engine.connect() as connection:
|
| 21 |
+
# Check if column already exists
|
| 22 |
+
result = connection.execute(text(
|
| 23 |
+
"PRAGMA table_info(participants)"
|
| 24 |
+
))
|
| 25 |
+
|
| 26 |
+
columns = [row[1] for row in result]
|
| 27 |
+
|
| 28 |
+
if 'pack_credits' in columns:
|
| 29 |
+
print("[OK] Column 'pack_credits' already exists")
|
| 30 |
+
return
|
| 31 |
+
|
| 32 |
+
# Add the column with default value equal to total_points
|
| 33 |
+
# This gives existing participants credits based on their current points
|
| 34 |
+
connection.execute(text(
|
| 35 |
+
"ALTER TABLE participants ADD COLUMN pack_credits INTEGER DEFAULT 0 NOT NULL"
|
| 36 |
+
))
|
| 37 |
+
|
| 38 |
+
# Set pack_credits to total_points for existing participants
|
| 39 |
+
connection.execute(text(
|
| 40 |
+
"UPDATE participants SET pack_credits = total_points"
|
| 41 |
+
))
|
| 42 |
+
|
| 43 |
+
connection.commit()
|
| 44 |
+
|
| 45 |
+
print("[OK] Added column 'pack_credits' to participants table")
|
| 46 |
+
print("[OK] Initialized pack_credits = total_points for existing participants")
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
if __name__ == "__main__":
|
| 50 |
+
print("Adding pack_credits column to participants table...")
|
| 51 |
+
add_pack_credits_column()
|
| 52 |
+
print("\nMigration completed successfully!")
|
frontend/src/components/packs/PackCard.tsx
CHANGED
|
@@ -11,14 +11,14 @@ interface PackCardProps {
|
|
| 11 |
tier: PackTier;
|
| 12 |
count: number;
|
| 13 |
canOpen: boolean;
|
| 14 |
-
|
| 15 |
onOpen: () => void;
|
| 16 |
onPurchase: () => void;
|
| 17 |
}
|
| 18 |
|
| 19 |
-
export const PackCard: React.FC<PackCardProps> = ({ tier, count, canOpen,
|
| 20 |
const config = PACK_CONFIG[tier];
|
| 21 |
-
const canPurchase =
|
| 22 |
|
| 23 |
return (
|
| 24 |
<motion.div
|
|
@@ -71,7 +71,7 @@ export const PackCard: React.FC<PackCardProps> = ({ tier, count, canOpen, userPo
|
|
| 71 |
|
| 72 |
{/* Cost/Action */}
|
| 73 |
<div className="text-text-tertiary text-xs sm:text-sm mb-3 sm:mb-4">
|
| 74 |
-
<span className="font-semibold">{config.cost}
|
| 75 |
</div>
|
| 76 |
|
| 77 |
{/* Buttons */}
|
|
@@ -82,19 +82,19 @@ export const PackCard: React.FC<PackCardProps> = ({ tier, count, canOpen, userPo
|
|
| 82 |
</button>
|
| 83 |
) : canPurchase ? (
|
| 84 |
<button className="btn-secondary w-full" onClick={(e) => { e.stopPropagation(); onPurchase(); }}>
|
| 85 |
-
ACHETER ({config.cost}
|
| 86 |
</button>
|
| 87 |
) : (
|
| 88 |
<button
|
| 89 |
className="btn-secondary w-full opacity-40 cursor-not-allowed"
|
| 90 |
disabled
|
| 91 |
>
|
| 92 |
-
ACHETER ({config.cost}
|
| 93 |
</button>
|
| 94 |
)}
|
| 95 |
{canPurchase && !canOpen && (
|
| 96 |
<div className="text-xs text-fifa-green">
|
| 97 |
-
✓ {
|
| 98 |
</div>
|
| 99 |
)}
|
| 100 |
</div>
|
|
|
|
| 11 |
tier: PackTier;
|
| 12 |
count: number;
|
| 13 |
canOpen: boolean;
|
| 14 |
+
userCredits: number;
|
| 15 |
onOpen: () => void;
|
| 16 |
onPurchase: () => void;
|
| 17 |
}
|
| 18 |
|
| 19 |
+
export const PackCard: React.FC<PackCardProps> = ({ tier, count, canOpen, userCredits, onOpen, onPurchase }) => {
|
| 20 |
const config = PACK_CONFIG[tier];
|
| 21 |
+
const canPurchase = userCredits >= config.cost;
|
| 22 |
|
| 23 |
return (
|
| 24 |
<motion.div
|
|
|
|
| 71 |
|
| 72 |
{/* Cost/Action */}
|
| 73 |
<div className="text-text-tertiary text-xs sm:text-sm mb-3 sm:mb-4">
|
| 74 |
+
<span className="font-semibold">{config.cost} crédits</span>
|
| 75 |
</div>
|
| 76 |
|
| 77 |
{/* Buttons */}
|
|
|
|
| 82 |
</button>
|
| 83 |
) : canPurchase ? (
|
| 84 |
<button className="btn-secondary w-full" onClick={(e) => { e.stopPropagation(); onPurchase(); }}>
|
| 85 |
+
ACHETER ({config.cost} crédits)
|
| 86 |
</button>
|
| 87 |
) : (
|
| 88 |
<button
|
| 89 |
className="btn-secondary w-full opacity-40 cursor-not-allowed"
|
| 90 |
disabled
|
| 91 |
>
|
| 92 |
+
ACHETER ({config.cost} crédits)
|
| 93 |
</button>
|
| 94 |
)}
|
| 95 |
{canPurchase && !canOpen && (
|
| 96 |
<div className="text-xs text-fifa-green">
|
| 97 |
+
✓ {userCredits} crédits disponibles
|
| 98 |
</div>
|
| 99 |
)}
|
| 100 |
</div>
|
frontend/src/pages/PacksPage.tsx
CHANGED
|
@@ -19,20 +19,22 @@ export const PacksPage = () => {
|
|
| 19 |
const [selectedTier, setSelectedTier] = useState<PackTier | null>(null);
|
| 20 |
const [openResult, setOpenResult] = useState<PackOpenResult | null>(null);
|
| 21 |
const [showModal, setShowModal] = useState(false);
|
|
|
|
| 22 |
const [userPoints, setUserPoints] = useState<number>(0);
|
| 23 |
const hasShownWelcome = useRef(false);
|
| 24 |
|
| 25 |
-
// Fetch user points
|
| 26 |
useEffect(() => {
|
| 27 |
-
const
|
| 28 |
try {
|
| 29 |
const profile = await getMyProfile();
|
|
|
|
| 30 |
setUserPoints(profile.total_points);
|
| 31 |
} catch (err) {
|
| 32 |
-
console.error('Failed to fetch user
|
| 33 |
}
|
| 34 |
};
|
| 35 |
-
|
| 36 |
}, [inventory]); // Refresh when inventory changes
|
| 37 |
|
| 38 |
// Show welcome pack notification on first load if user has exactly 1 silver pack
|
|
@@ -87,6 +89,25 @@ export const PacksPage = () => {
|
|
| 87 |
<p className="text-text-secondary text-lg">
|
| 88 |
Ouvre tes packs pour obtenir des récompenses exclusives !
|
| 89 |
</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
</div>
|
| 91 |
|
| 92 |
{/* Error Display */}
|
|
@@ -156,7 +177,7 @@ export const PacksPage = () => {
|
|
| 156 |
tier="bronze"
|
| 157 |
count={inventory?.bronze || 0}
|
| 158 |
canOpen={(inventory?.bronze || 0) > 0 && !isOpening}
|
| 159 |
-
|
| 160 |
onOpen={() => handleOpenPack('bronze')}
|
| 161 |
onPurchase={() => handlePurchasePack('bronze')}
|
| 162 |
/>
|
|
@@ -164,7 +185,7 @@ export const PacksPage = () => {
|
|
| 164 |
tier="silver"
|
| 165 |
count={inventory?.silver || 0}
|
| 166 |
canOpen={(inventory?.silver || 0) > 0 && !isOpening}
|
| 167 |
-
|
| 168 |
onOpen={() => handleOpenPack('silver')}
|
| 169 |
onPurchase={() => handlePurchasePack('silver')}
|
| 170 |
/>
|
|
@@ -172,7 +193,7 @@ export const PacksPage = () => {
|
|
| 172 |
tier="gold"
|
| 173 |
count={inventory?.gold || 0}
|
| 174 |
canOpen={(inventory?.gold || 0) > 0 && !isOpening}
|
| 175 |
-
|
| 176 |
onOpen={() => handleOpenPack('gold')}
|
| 177 |
onPurchase={() => handlePurchasePack('gold')}
|
| 178 |
/>
|
|
@@ -180,7 +201,7 @@ export const PacksPage = () => {
|
|
| 180 |
tier="ultimate"
|
| 181 |
count={inventory?.ultimate || 0}
|
| 182 |
canOpen={(inventory?.ultimate || 0) > 0 && !isOpening}
|
| 183 |
-
|
| 184 |
onOpen={() => handleOpenPack('ultimate')}
|
| 185 |
onPurchase={() => handlePurchasePack('ultimate')}
|
| 186 |
/>
|
|
@@ -200,7 +221,8 @@ export const PacksPage = () => {
|
|
| 200 |
</h3>
|
| 201 |
<ul className="text-text-secondary text-left space-y-2">
|
| 202 |
<li>🎁 <strong>Packs gratuits :</strong> 2x par jour (9h et 18h)</li>
|
| 203 |
-
<li>⭐ <strong>Acheter avec des
|
|
|
|
| 204 |
<li>🏆 <strong>Récompenses :</strong> Shots, immunités, pouvoirs spéciaux et plus !</li>
|
| 205 |
</ul>
|
| 206 |
</div>
|
|
|
|
| 19 |
const [selectedTier, setSelectedTier] = useState<PackTier | null>(null);
|
| 20 |
const [openResult, setOpenResult] = useState<PackOpenResult | null>(null);
|
| 21 |
const [showModal, setShowModal] = useState(false);
|
| 22 |
+
const [userCredits, setUserCredits] = useState<number>(0);
|
| 23 |
const [userPoints, setUserPoints] = useState<number>(0);
|
| 24 |
const hasShownWelcome = useRef(false);
|
| 25 |
|
| 26 |
+
// Fetch user credits and points
|
| 27 |
useEffect(() => {
|
| 28 |
+
const fetchProfile = async () => {
|
| 29 |
try {
|
| 30 |
const profile = await getMyProfile();
|
| 31 |
+
setUserCredits(profile.pack_credits);
|
| 32 |
setUserPoints(profile.total_points);
|
| 33 |
} catch (err) {
|
| 34 |
+
console.error('Failed to fetch user profile:', err);
|
| 35 |
}
|
| 36 |
};
|
| 37 |
+
fetchProfile();
|
| 38 |
}, [inventory]); // Refresh when inventory changes
|
| 39 |
|
| 40 |
// Show welcome pack notification on first load if user has exactly 1 silver pack
|
|
|
|
| 89 |
<p className="text-text-secondary text-lg">
|
| 90 |
Ouvre tes packs pour obtenir des récompenses exclusives !
|
| 91 |
</p>
|
| 92 |
+
{/* Display Credits and Points */}
|
| 93 |
+
<div className="mt-6 flex justify-center gap-6">
|
| 94 |
+
<div className="inline-flex items-center gap-2 px-6 py-3 rounded-xl border" style={{
|
| 95 |
+
background: 'rgba(26, 41, 66, 0.75)',
|
| 96 |
+
borderColor: 'rgba(255, 255, 255, 0.1)',
|
| 97 |
+
backdropFilter: 'blur(10px)',
|
| 98 |
+
}}>
|
| 99 |
+
<span className="text-text-secondary text-sm uppercase tracking-wide">Crédits:</span>
|
| 100 |
+
<span className="font-numbers text-2xl font-bold text-fifa-gold">{userCredits}</span>
|
| 101 |
+
</div>
|
| 102 |
+
<div className="inline-flex items-center gap-2 px-6 py-3 rounded-xl border" style={{
|
| 103 |
+
background: 'rgba(26, 41, 66, 0.75)',
|
| 104 |
+
borderColor: 'rgba(255, 255, 255, 0.1)',
|
| 105 |
+
backdropFilter: 'blur(10px)',
|
| 106 |
+
}}>
|
| 107 |
+
<span className="text-text-secondary text-sm uppercase tracking-wide">Points:</span>
|
| 108 |
+
<span className="font-numbers text-2xl font-bold text-fifa-green">{userPoints}</span>
|
| 109 |
+
</div>
|
| 110 |
+
</div>
|
| 111 |
</div>
|
| 112 |
|
| 113 |
{/* Error Display */}
|
|
|
|
| 177 |
tier="bronze"
|
| 178 |
count={inventory?.bronze || 0}
|
| 179 |
canOpen={(inventory?.bronze || 0) > 0 && !isOpening}
|
| 180 |
+
userCredits={userCredits}
|
| 181 |
onOpen={() => handleOpenPack('bronze')}
|
| 182 |
onPurchase={() => handlePurchasePack('bronze')}
|
| 183 |
/>
|
|
|
|
| 185 |
tier="silver"
|
| 186 |
count={inventory?.silver || 0}
|
| 187 |
canOpen={(inventory?.silver || 0) > 0 && !isOpening}
|
| 188 |
+
userCredits={userCredits}
|
| 189 |
onOpen={() => handleOpenPack('silver')}
|
| 190 |
onPurchase={() => handlePurchasePack('silver')}
|
| 191 |
/>
|
|
|
|
| 193 |
tier="gold"
|
| 194 |
count={inventory?.gold || 0}
|
| 195 |
canOpen={(inventory?.gold || 0) > 0 && !isOpening}
|
| 196 |
+
userCredits={userCredits}
|
| 197 |
onOpen={() => handleOpenPack('gold')}
|
| 198 |
onPurchase={() => handlePurchasePack('gold')}
|
| 199 |
/>
|
|
|
|
| 201 |
tier="ultimate"
|
| 202 |
count={inventory?.ultimate || 0}
|
| 203 |
canOpen={(inventory?.ultimate || 0) > 0 && !isOpening}
|
| 204 |
+
userCredits={userCredits}
|
| 205 |
onOpen={() => handleOpenPack('ultimate')}
|
| 206 |
onPurchase={() => handlePurchasePack('ultimate')}
|
| 207 |
/>
|
|
|
|
| 221 |
</h3>
|
| 222 |
<ul className="text-text-secondary text-left space-y-2">
|
| 223 |
<li>🎁 <strong>Packs gratuits :</strong> 2x par jour (9h et 18h)</li>
|
| 224 |
+
<li>⭐ <strong>Acheter avec des crédits :</strong> Bronze (100), Silver (200), Gold (300), Ultimate (500)</li>
|
| 225 |
+
<li>💰 <strong>Crédits :</strong> Gagne 1 crédit par point marqué. Les points restent, les crédits s'utilisent !</li>
|
| 226 |
<li>🏆 <strong>Récompenses :</strong> Shots, immunités, pouvoirs spéciaux et plus !</li>
|
| 227 |
</ul>
|
| 228 |
</div>
|
frontend/src/types/participant.ts
CHANGED
|
@@ -10,6 +10,7 @@ export interface Participant {
|
|
| 10 |
avatar_url: string | null;
|
| 11 |
is_groom: boolean;
|
| 12 |
total_points: number;
|
|
|
|
| 13 |
current_packs: PackInventory;
|
| 14 |
created_at: string;
|
| 15 |
updated_at: string;
|
|
@@ -28,6 +29,7 @@ export interface ParticipantSummary {
|
|
| 28 |
avatar_url: string | null;
|
| 29 |
is_groom: boolean;
|
| 30 |
total_points: number;
|
|
|
|
| 31 |
}
|
| 32 |
|
| 33 |
export interface ParticipantWithRank extends ParticipantSummary {
|
|
|
|
| 10 |
avatar_url: string | null;
|
| 11 |
is_groom: boolean;
|
| 12 |
total_points: number;
|
| 13 |
+
pack_credits: number;
|
| 14 |
current_packs: PackInventory;
|
| 15 |
created_at: string;
|
| 16 |
updated_at: string;
|
|
|
|
| 29 |
avatar_url: string | null;
|
| 30 |
is_groom: boolean;
|
| 31 |
total_points: number;
|
| 32 |
+
pack_credits: number;
|
| 33 |
}
|
| 34 |
|
| 35 |
export interface ParticipantWithRank extends ParticipantSummary {
|