Files
OpenFrontIO/src/client/components/clan/ClanRequestsView.ts
T
Ryan 9432bb26f8 [bugfix] fixes border around clans ui (#3873)
## 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
2026-05-06 16:06:33 -06:00

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>
`;
}
}