mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-01 11:03:25 +00:00
9432bb26f8
## Description: fixes border around clans ui <img width="67" height="705" alt="image" src="https://github.com/user-attachments/assets/5ee35eb5-b406-4403-b9b4-324769faf061" /> also fixes weird padding: <img width="134" height="244" alt="image" src="https://github.com/user-attachments/assets/32a84074-afa6-4e9a-98f1-e45aabe4aa2a" /> what it should be: <img width="140" height="206" alt="image" src="https://github.com/user-attachments/assets/b72b480e-c972-4495-b9da-5c3b411bf590" /> ## 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
200 lines
6.9 KiB
TypeScript
200 lines
6.9 KiB
TypeScript
import { html, LitElement } from "lit";
|
|
import { customElement, property, state } from "lit/decorators.js";
|
|
import {
|
|
approveClanRequest,
|
|
type ClanInfo,
|
|
type ClanJoinRequest,
|
|
denyClanRequest,
|
|
fetchClanRequests,
|
|
} from "../../ClanApi";
|
|
import { translateText } from "../../Utils";
|
|
import "../CopyButton";
|
|
import {
|
|
filterRequestsBySearch,
|
|
formatClanDate,
|
|
renderLoadingSpinner,
|
|
renderMemberSearchInput,
|
|
renderServerPagination,
|
|
showToast,
|
|
} from "./ClanShared";
|
|
|
|
@customElement("clan-requests-view")
|
|
export class ClanRequestsView extends LitElement {
|
|
createRenderRoot() {
|
|
return this;
|
|
}
|
|
|
|
@property() clanTag = "";
|
|
@property({ type: Object }) selectedClan: ClanInfo | null = null;
|
|
@state() private requests: ClanJoinRequest[] = [];
|
|
@state() private requestsTotal = 0;
|
|
@state() private requestsPage = 1;
|
|
@state() private requestsLimit = 20;
|
|
@state() private memberActionPending = false;
|
|
@state() private loading = false;
|
|
private memberSearch = "";
|
|
private memberSearchDebounce: ReturnType<typeof setTimeout> | null = null;
|
|
connectedCallback() {
|
|
super.connectedCallback();
|
|
this.loadRequests(1);
|
|
}
|
|
|
|
disconnectedCallback() {
|
|
if (this.memberSearchDebounce) clearTimeout(this.memberSearchDebounce);
|
|
super.disconnectedCallback();
|
|
}
|
|
private async loadRequests(page: number, showLoading = true) {
|
|
if (showLoading) this.loading = true;
|
|
else this.memberActionPending = true;
|
|
try {
|
|
const data = await fetchClanRequests(this.clanTag, page);
|
|
if (!data) {
|
|
if (showLoading)
|
|
showToast(translateText("clan_modal.failed_to_load_requests"), "red");
|
|
return;
|
|
}
|
|
this.requests = data.results;
|
|
this.requestsTotal = data.total;
|
|
this.requestsLimit = data.limit;
|
|
this.requestsPage = page;
|
|
} finally {
|
|
if (showLoading) this.loading = false;
|
|
else this.memberActionPending = false;
|
|
}
|
|
}
|
|
|
|
private async handleApprove(publicId: string) {
|
|
if (this.memberActionPending) return;
|
|
this.memberActionPending = true;
|
|
try {
|
|
const result = await approveClanRequest(this.clanTag, publicId);
|
|
if (result !== true) {
|
|
showToast(translateText(result.error), "red");
|
|
return;
|
|
}
|
|
this.requests = this.requests.filter((r) => r.publicId !== publicId);
|
|
this.requestsTotal--;
|
|
this.dispatchEvent(
|
|
new CustomEvent("request-approved", {
|
|
detail: { publicId },
|
|
bubbles: true,
|
|
composed: true,
|
|
}),
|
|
);
|
|
showToast(translateText("clan_modal.request_approved"), "green");
|
|
} finally {
|
|
this.memberActionPending = false;
|
|
}
|
|
}
|
|
|
|
private async handleDeny(publicId: string) {
|
|
if (this.memberActionPending) return;
|
|
this.memberActionPending = true;
|
|
try {
|
|
const result = await denyClanRequest(this.clanTag, publicId);
|
|
if (result !== true) {
|
|
showToast(translateText(result.error), "red");
|
|
return;
|
|
}
|
|
this.requests = this.requests.filter((r) => r.publicId !== publicId);
|
|
this.requestsTotal--;
|
|
this.dispatchEvent(
|
|
new CustomEvent("request-denied", {
|
|
detail: { publicId },
|
|
bubbles: true,
|
|
composed: true,
|
|
}),
|
|
);
|
|
showToast(translateText("clan_modal.request_denied"), "green");
|
|
} finally {
|
|
this.memberActionPending = false;
|
|
}
|
|
}
|
|
|
|
private onSearchInput(e: Event) {
|
|
if (this.memberSearchDebounce) clearTimeout(this.memberSearchDebounce);
|
|
this.memberSearchDebounce = setTimeout(() => {
|
|
this.memberSearch = (e.target as HTMLInputElement).value;
|
|
this.requestUpdate();
|
|
}, 200);
|
|
}
|
|
|
|
render() {
|
|
if (this.loading) return renderLoadingSpinner();
|
|
|
|
const totalPages = Math.ceil(this.requestsTotal / this.requestsLimit);
|
|
const filtered = filterRequestsBySearch(this.requests, this.memberSearch);
|
|
return html`
|
|
<div>
|
|
<div
|
|
class="text-[10px] font-bold uppercase tracking-wider text-white/40 mb-2"
|
|
>
|
|
${translateText("clan_modal.pending_requests_count", {
|
|
count: this.requestsTotal,
|
|
})}
|
|
</div>
|
|
${renderMemberSearchInput(
|
|
(e) => this.onSearchInput(e),
|
|
"clan_modal.search_requests_placeholder",
|
|
)}
|
|
${filtered.length === 0
|
|
? html`<div
|
|
class="flex flex-col items-center justify-center p-12 text-center"
|
|
>
|
|
<p class="text-white/40 text-sm">
|
|
${translateText("clan_modal.no_requests")}
|
|
</p>
|
|
</div>`
|
|
: html`
|
|
<div class="space-y-3">
|
|
${filtered.map(
|
|
(req) => html`
|
|
<div
|
|
class="flex items-center gap-3 bg-white/5 rounded-xl border border-white/10 p-4"
|
|
>
|
|
<div class="flex-1 min-w-0">
|
|
<copy-button
|
|
compact
|
|
.copyText=${req.publicId}
|
|
.displayText=${req.publicId}
|
|
.showVisibilityToggle=${false}
|
|
.showCopyIcon=${false}
|
|
></copy-button>
|
|
<span class="text-white/30 text-[10px]">
|
|
${translateText("clan_modal.requested_on", {
|
|
tag: this.selectedClan?.tag ?? this.clanTag,
|
|
date: formatClanDate(req.createdAt),
|
|
})}
|
|
</span>
|
|
</div>
|
|
<div class="flex items-center gap-2 shrink-0">
|
|
<button
|
|
@click=${() => this.handleApprove(req.publicId)}
|
|
?disabled=${this.memberActionPending}
|
|
class="px-3 py-1.5 text-[10px] font-bold uppercase tracking-wider rounded-lg bg-green-500/20 text-green-400 border border-green-500/30 hover:bg-green-500/30 transition-all disabled:opacity-50 disabled:pointer-events-none"
|
|
>
|
|
${translateText("clan_modal.approve")}
|
|
</button>
|
|
<button
|
|
@click=${() => this.handleDeny(req.publicId)}
|
|
?disabled=${this.memberActionPending}
|
|
class="px-3 py-1.5 text-[10px] font-bold uppercase tracking-wider rounded-lg bg-red-500/20 text-red-400 border border-red-500/30 hover:bg-red-500/30 transition-all disabled:opacity-50 disabled:pointer-events-none"
|
|
>
|
|
${translateText("clan_modal.deny")}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`,
|
|
)}
|
|
</div>
|
|
`}
|
|
${totalPages > 1
|
|
? renderServerPagination(this.requestsPage, totalPages, (p) =>
|
|
this.loadRequests(p, false),
|
|
)
|
|
: ""}
|
|
</div>
|
|
`;
|
|
}
|
|
}
|