feat: photo upload at level 3 + report a player (nudity avatar / chat insult)
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 2m58s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m9s
CI/CD / Deploy - local stack (db + server + web) (push) Successful in 1m0s

Photo upload:
- Lower the custom profile-photo gate from level 25 to level 3 (client const +
  i18n hint + server gate in ProfileService.Update). The level-25 "Expert" title
  is unrelated and unchanged.

Report a player:
- New ReportReason type + service.reportUser(targetId, reason, details?).
- Report entry points: a "گزارش تخلف" button + reason picker (nudity / insult /
  other) in the public-profile modal, and a flag button in the chat header
  (reports the peer for an insulting chat) with a confirmation toast.
- Mock records reports to localStorage; SignalR POSTs /api/report.
- Server: POST /api/report → ProfileService.ReportUser stores the report in the
  write-only ledger (kind="report", ref="{targetId}|{reason}|{details}") so no
  schema change is needed (server uses EnsureCreated, not migrations).
- i18n: report.* keys (fa + en).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-11 19:12:02 +03:30
parent 8033023a1f
commit 6641741669
10 changed files with 165 additions and 8 deletions
+32 -1
View File
@@ -1,12 +1,14 @@
"use client";
import { ChevronLeft, ChevronRight, MessageCircle, Send, Smile } from "lucide-react";
import { ChevronLeft, ChevronRight, Flag, MessageCircle, Send, Smile } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { useOnlineStore } from "@/lib/online-store";
import { useSessionStore } from "@/lib/session-store";
import { useUIStore } from "@/lib/ui-store";
import { useI18n } from "@/lib/i18n";
import { sound } from "@/lib/sound";
import { getService } from "@/lib/online/service";
import { pushNotification } from "@/lib/notification-store";
import { ownedReactions } from "@/lib/online/gamification";
import { avatarEmoji } from "@/lib/online/types";
import { cn } from "@/lib/cn";
@@ -23,6 +25,7 @@ export function ChatScreen() {
const viewProfile = useUIStore((s) => s.viewProfile);
const [text, setText] = useState("");
const [showEmoji, setShowEmoji] = useState(false);
const [reported, setReported] = useState(false);
const emojis = profile ? ownedReactions(profile) : [];
const endRef = useRef<HTMLDivElement>(null);
const prevLen = useRef(0);
@@ -58,6 +61,20 @@ export function ChatScreen() {
await sendChat(e);
};
const reportFriend = async () => {
if (reported || !friend) return;
setReported(true);
await getService().reportUser(friend.id, "insult");
pushNotification({
kind: "system",
titleFa: "گزارش ثبت شد",
titleEn: "Report submitted",
bodyFa: "از کمک شما برای حفظ محیط سالم ممنونیم.",
bodyEn: "Thanks for helping keep the game friendly.",
icon: "🚩",
});
};
return (
<main className="persian-pattern relative h-dvh w-full flex justify-center">
<div className="w-full max-w-3xl flex flex-col h-full">
@@ -86,6 +103,20 @@ export function ChatScreen() {
</div>
</div>
</button>
{/* report this player for insulting chat */}
<button
onClick={reportFriend}
disabled={reported}
title={t("report.insult")}
aria-label={t("report.button")}
className={cn(
"tap grid place-items-center rounded-full shrink-0 transition",
reported ? "text-teal-300" : "text-cream/40 hover:text-rose-300 hover:bg-navy-800/80"
)}
>
<Flag className="size-4" />
</button>
</header>
{/* messages */}
+1 -1
View File
@@ -28,7 +28,7 @@ import { pushNotification } from "@/lib/notification-store";
import { sound } from "@/lib/sound";
/** Level required before a player can upload a custom profile photo. */
const PHOTO_UPLOAD_MIN_LEVEL = 25;
const PHOTO_UPLOAD_MIN_LEVEL = 3;
import { AVATARS, Gender, SocialVisibility } from "@/lib/online/types";
import { GENDER_META, SOCIAL_PLATFORMS } from "@/lib/social";
import { backVisualFromDef, cardBackMotif } from "@/lib/cardBack";