mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-05 16:35:09 +00:00
df05d21fc2
## Description: Continuation from #3276 Adds the complete client-side clan UI as a Lit web component (`<clan-modal>`), a typed API client with Zod-validated responses, shared response schemas, and a reusable `<confirm-dialog>` component. ### New: `ClanModal.ts` | View | What it does | |------|-------------| | **My Clans** | Lists joined clans + pending join requests (built from `/users/@me`, no extra fetches) | | **Browse** | Search by tag (min 3 chars), paginated results, configurable per-page (10/25/50) | | **Clan Detail** | Stats, paginated + searchable member list, role badges, join/leave/request actions | | **Manage** | Edit name (max 35 chars) + description, toggle open/invite-only, disband | | **Transfer** | Leadership transfer with member selector + confirmation | | **Requests** | Approve/deny join requests (leader/officer) | | **Bans** | View and unban (leader/officer) | | **My Requests** | View and withdraw outgoing requests | ### New: `ConfirmDialog.ts` Reusable `<confirm-dialog>` Lit component — replaces native `confirm()`/`prompt()` which are blocked or broken on mobile and CrazyGames iframes. Supports danger/warning variants and an optional textarea (used for ban reasons). Fires `confirm`/`cancel` events. ### New: `ClanApi.ts` Typed API client covering all clan endpoints. Every response is Zod-validated. Auth header is always last in the spread (can't be overridden by callers). Unknown server error messages always fall back to a generic client-side string — never displayed verbatim. ### New: `ClanApiSchemas.ts` (in `src/core/`) Shared Zod schemas for clan API responses with max-length constraints on `name` (35) and `description` (200). Lives in `core/` so it can be consumed by both client code and the leaderboard table. ### Modified: `ApiSchemas.ts` - Added `clans` and `clanRequests` arrays to `UserMeResponseSchema` - Moved clan leaderboard schemas out to `ClanApiSchemas.ts` - Renamed `LeaderboardClanTagSchema` → `RequiredClanTagSchema` ### Modified: `Api.ts` - Added `invalidateUserMe()` to bust the cached `/users/me` response after mutations - Removed `fetchClanLeaderboard` (moved to `ClanApi.ts`) ### Tests - `ClanModal.test.ts` — rendering, view navigation, user actions - `ClanApiQueries.test.ts` — fetch functions, error handling, pagination - `ClanApiMutations.test.ts` — join, leave, kick, ban, promote, transfer, etc. - `ClanApiBans.test.ts` — ban/unban calls and error paths - `ClanApiSchemas.test.ts` — Zod schema validation edge cases - `LeaderboardModal.test.ts` — updated imports ## Notable design decisions - **Not-logged-in state** — shows "Sign in to join clans" instead of false "no clans" empty state - **Rate limit feedback** — reads `Retry-After` header and surfaces wait time to the user ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: w.o.n --------- Co-authored-by: evanpelle <evanpelle@gmail.com>
106 lines
3.8 KiB
TypeScript
106 lines
3.8 KiB
TypeScript
import { html, LitElement } from "lit";
|
|
import { customElement, property, state } from "lit/decorators.js";
|
|
import { invalidateUserMe } from "../../Api";
|
|
import { withdrawClanRequest } from "../../ClanApi";
|
|
import { translateText } from "../../Utils";
|
|
import { modalHeader } from "../ui/ModalHeader";
|
|
import { formatClanDate, modalContainerClass, showToast } from "./ClanShared";
|
|
|
|
@customElement("clan-my-requests-view")
|
|
export class ClanMyRequestsView extends LitElement {
|
|
createRenderRoot() {
|
|
return this;
|
|
}
|
|
|
|
@property({ type: Array }) myPendingRequests: {
|
|
tag: string;
|
|
name: string;
|
|
createdAt: string;
|
|
}[] = [];
|
|
|
|
@state() private actionPending = false;
|
|
|
|
async handleWithdrawRequest(tag: string) {
|
|
if (this.actionPending) return;
|
|
this.actionPending = true;
|
|
try {
|
|
const result = await withdrawClanRequest(tag);
|
|
if (result !== true) {
|
|
showToast(translateText(result.error), "red");
|
|
return;
|
|
}
|
|
invalidateUserMe();
|
|
showToast(translateText("clan_modal.join_request_cancelled"), "green");
|
|
this.dispatchEvent(
|
|
new CustomEvent("request-withdrawn", {
|
|
detail: { tag },
|
|
bubbles: true,
|
|
composed: true,
|
|
}),
|
|
);
|
|
} finally {
|
|
this.actionPending = false;
|
|
}
|
|
}
|
|
|
|
render() {
|
|
return html`
|
|
<div class="${modalContainerClass}">
|
|
${modalHeader({
|
|
title: translateText("clan_modal.pending_applications"),
|
|
onBack: () =>
|
|
this.dispatchEvent(
|
|
new CustomEvent("navigate-back", {
|
|
bubbles: true,
|
|
composed: true,
|
|
}),
|
|
),
|
|
ariaLabel: translateText("common.back"),
|
|
})}
|
|
<div class="flex-1 overflow-y-auto custom-scrollbar mr-1 p-4 lg:p-6">
|
|
${this.myPendingRequests.length === 0
|
|
? html`<p class="text-white/40 text-sm text-center py-8">
|
|
${translateText("clan_modal.no_pending_applications")}
|
|
</p>`
|
|
: html`
|
|
<div class="space-y-3">
|
|
${this.myPendingRequests.map(
|
|
(req) => html`
|
|
<div
|
|
class="flex items-center gap-3 bg-white/5 rounded-xl border border-white/10 p-4"
|
|
>
|
|
<div
|
|
class="w-10 h-10 rounded-xl bg-amber-500/10 flex items-center justify-center border border-amber-500/20 shrink-0"
|
|
>
|
|
<span class="text-amber-400 font-bold text-xs"
|
|
>${req.tag}</span
|
|
>
|
|
</div>
|
|
<div class="flex-1 min-w-0">
|
|
<span
|
|
class="text-white font-bold text-sm truncate block"
|
|
>${req.name}</span
|
|
>
|
|
<span class="text-white/30 text-xs">
|
|
${translateText("clan_modal.applied")}
|
|
${formatClanDate(req.createdAt)}
|
|
</span>
|
|
</div>
|
|
<button
|
|
@click=${() => this.handleWithdrawRequest(req.tag)}
|
|
?disabled=${this.actionPending}
|
|
class="text-[10px] font-bold uppercase tracking-wider px-3 py-1 rounded-full bg-red-500/15 text-red-400 border border-red-500/20 hover:bg-red-500/25 transition-all cursor-pointer disabled:opacity-50 disabled:pointer-events-none"
|
|
>
|
|
${translateText("clan_modal.cancel_request")}
|
|
</button>
|
|
</div>
|
|
`,
|
|
)}
|
|
</div>
|
|
`}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|