feat(profile): "set your city" gamification box → one-time 500-coin reward
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 28s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m10s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 1m6s

- New searchable city picker (src/lib/iran-cities.ts, ~60 Iranian cities,
  fa/en search) shown as a gold reward card at the top of the profile Basic tab.
- First time a non-empty city is set, the player earns 500 coins (CITY_REWARD),
  granted server-authoritatively. Collapses to a compact summary afterwards with
  a "change city" option (no re-reward).
- Frontend: UserProfile.city + cityRewardClaimed; mock-service grants on first
  set; session/service updateProfile accept `city`; celebratory toast + sfx.
- Backend (.NET): ProfileDto.City/CityRewardClaimed (JSON blob → no migration);
  ProfileService.Update grants +500 once and writes a "city" ledger entry.
- i18n: city.* keys (fa + en).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-11 18:11:45 +03:30
parent efefbcec3d
commit ad5b42db06
10 changed files with 304 additions and 5 deletions
+8 -1
View File
@@ -6,6 +6,7 @@ import {
ACHIEVEMENTS,
CARD_BACKS,
CARD_FRONTS,
CITY_REWARD,
REACTION_PACKS,
STICKER_PACKS,
TITLES,
@@ -362,7 +363,13 @@ export class MockOnlineService implements OnlineService {
async updateProfile(patch: Parameters<OnlineService["updateProfile"]>[0]) {
const p = await this.getProfile();
this.profile = { ...p, ...patch };
const next = { ...p, ...patch };
// One-time reward: first time the player sets a (non-empty) city → +500 coins.
if (patch.city && patch.city.trim() && !p.cityRewardClaimed) {
next.coins = p.coins + CITY_REWARD;
next.cityRewardClaimed = true;
}
this.profile = next;
this.saveProfile();
return this.profile;
}