Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion resources/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@
"role_officer": "Officer",
"save_changes": "Save Changes",
"saving": "Saving...",
"search_members_placeholder": "Filter current page by ID or role...",
"search_members_placeholder": "Search by exact ID, username, or role...",
"search_placeholder": "Search by clan tag...",
"search_requests_placeholder": "Search by player public ID...",
"select_new_leader": "Select a new leader",
Expand Down
3 changes: 3 additions & 0 deletions src/client/ClanApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,13 +194,16 @@ export async function fetchClanMembers(
limit = 20,
sort: ClanMemberSort = "default",
order?: ClanMemberOrder,
search?: string,
): Promise<ClanMembersResponse | false> {
try {
const params = new URLSearchParams();
params.set("page", String(page));
params.set("limit", String(limit));
if (sort !== "default") params.set("sort", sort);
if (order) params.set("order", order);
const normalizedSearch = search?.trim();
if (normalizedSearch) params.set("search", normalizedSearch);
const res = await clanFetch(
`/clans/${encodeURIComponent(tag)}/members?${params}`,
);
Expand Down
1 change: 1 addition & 0 deletions src/client/components/clan/ClanBansView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ export class ClanBansView extends LitElement {
</div>
${renderMemberSearchInput(
(e) => this.onSearchInput(e),
this.memberSearch,
"clan_modal.search_members_placeholder",
)}
${filtered.length === 0
Expand Down
45 changes: 31 additions & 14 deletions src/client/components/clan/ClanDetailView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,9 @@ export class ClanDetailView extends LitElement {
@state() private actionPending = false;
@state() private allStatsExpanded = false;
@state() private membersLoadInFlight = false;
private memberSearch = "";
@state() private memberSearch = "";
private memberSearchDebounce: ReturnType<typeof setTimeout> | null = null;
private memberLoadSeq = 0;
private asyncGeneration = 0;

connectedCallback() {
Expand Down Expand Up @@ -109,6 +110,7 @@ export class ClanDetailView extends LitElement {

disconnectedCallback() {
if (this.memberSearchDebounce) clearTimeout(this.memberSearchDebounce);
this.memberLoadSeq++;
super.disconnectedCallback();
}

Expand All @@ -122,6 +124,7 @@ export class ClanDetailView extends LitElement {

private async loadDetail() {
const gen = ++this.asyncGeneration;
this.memberLoadSeq++;
this.loading = true;
this.myRole = null;
this.pendingRequestCount = 0;
Expand Down Expand Up @@ -237,25 +240,36 @@ export class ClanDetailView extends LitElement {
}
}

private async loadMemberPage(page: number) {
private async loadMemberPage(page: number, search = this.memberSearch) {
if (!this.selectedClan) return;
const res = await fetchClanMembers(
this.selectedClan.tag,
page,
this.membersPerPage,
this.memberSort,
this.memberOrder,
);
const seq = ++this.memberLoadSeq;
Comment thread
ryanbarlow97 marked this conversation as resolved.
const res = search
? await fetchClanMembers(
this.selectedClan.tag,
page,
this.membersPerPage,
this.memberSort,
this.memberOrder,
search,
)
: await fetchClanMembers(
this.selectedClan.tag,
page,
this.membersPerPage,
this.memberSort,
this.memberOrder,
);
if (seq !== this.memberLoadSeq || search !== this.memberSearch) return;
if (!res) return;
if (res.results.length === 0 && page > 1) {
await this.loadMemberPage(1);
await this.loadMemberPage(1, search);
return;
}
this.members = res.results;
this.membersTotal = res.total;
this.memberPage = page;
this.pendingRequestCount = res.pendingRequests ?? 0;
if (this.selectedClan.memberCount !== res.total) {
if (!search && this.selectedClan.memberCount !== res.total) {
this.selectedClan = { ...this.selectedClan, memberCount: res.total };
}
}
Expand Down Expand Up @@ -344,11 +358,13 @@ export class ClanDetailView extends LitElement {
}

private onSearchInput(e: Event) {
const val = (e.target as HTMLInputElement).value;
const search = (e.target as HTMLInputElement).value.trim();
if (search === this.memberSearch) return;
this.memberSearch = search;
if (this.memberSearchDebounce) clearTimeout(this.memberSearchDebounce);
this.memberSearchDebounce = setTimeout(() => {
this.memberSearch = val;
this.requestUpdate();
this.memberSearchDebounce = null;
void this.loadMemberPage(1, search);
}, 200);
}

Expand Down Expand Up @@ -539,6 +555,7 @@ export class ClanDetailView extends LitElement {
</div>
${renderMemberSearchInput(
(e: Event) => this.onSearchInput(e),
this.memberSearch,
undefined,
renderMemberSortControl(
this.memberSort,
Expand Down
47 changes: 34 additions & 13 deletions src/client/components/clan/ClanManageView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,9 @@ export class ClanManageView extends LitElement {
@state() private confirmTargetId: string | null = null;
@state() private pendingRequestCount = 0;
@state() private actionPending = false;
private memberSearch = "";
@state() private memberSearch = "";
private memberSearchDebounce: ReturnType<typeof setTimeout> | null = null;
private memberLoadSeq = 0;

connectedCallback() {
super.connectedCallback();
Expand All @@ -74,31 +75,47 @@ export class ClanManageView extends LitElement {

disconnectedCallback() {
if (this.memberSearchDebounce) clearTimeout(this.memberSearchDebounce);
this.memberLoadSeq++;
super.disconnectedCallback();
}

private async loadMembers(page: number) {
private async loadMembers(page: number, search = this.memberSearch) {
const seq = ++this.memberLoadSeq;
if (this.members.length === 0) this.loading = true;
const res = await fetchClanMembers(
this.clanTag,
page,
this.membersPerPage,
this.memberSort,
this.memberOrder,
);
const res = search
? await fetchClanMembers(
this.clanTag,
page,
this.membersPerPage,
this.memberSort,
this.memberOrder,
search,
)
: await fetchClanMembers(
this.clanTag,
page,
this.membersPerPage,
this.memberSort,
this.memberOrder,
);
if (seq !== this.memberLoadSeq || search !== this.memberSearch) return;
if (!res) {
this.loading = false;
return;
}
if (res.results.length === 0 && page > 1) {
await this.loadMembers(1);
await this.loadMembers(1, search);
return;
}
this.members = res.results;
this.membersTotal = res.total;
this.memberPage = page;
this.pendingRequestCount = res.pendingRequests ?? 0;
if (this.selectedClan && this.selectedClan.memberCount !== res.total) {
if (
!search &&
this.selectedClan &&
this.selectedClan.memberCount !== res.total
) {
this.dispatchEvent(
new CustomEvent("clan-updated", {
detail: { memberCount: res.total },
Expand Down Expand Up @@ -254,10 +271,13 @@ export class ClanManageView extends LitElement {
}

private onSearchInput(e: Event) {
const search = (e.target as HTMLInputElement).value.trim();
if (search === this.memberSearch) return;
this.memberSearch = search;
if (this.memberSearchDebounce) clearTimeout(this.memberSearchDebounce);
this.memberSearchDebounce = setTimeout(() => {
this.memberSearch = (e.target as HTMLInputElement).value;
this.requestUpdate();
this.memberSearchDebounce = null;
void this.loadMembers(1, search);
}, 200);
}

Expand Down Expand Up @@ -442,6 +462,7 @@ export class ClanManageView extends LitElement {
</h3>
${renderMemberSearchInput(
(e) => this.onSearchInput(e),
this.memberSearch,
undefined,
renderMemberSortControl(
this.memberSort,
Expand Down
1 change: 1 addition & 0 deletions src/client/components/clan/ClanRequestsView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ export class ClanRequestsView extends LitElement {
</div>
${renderMemberSearchInput(
(e) => this.onSearchInput(e),
this.memberSearch,
"clan_modal.search_requests_placeholder",
)}
${filtered.length === 0
Expand Down
2 changes: 2 additions & 0 deletions src/client/components/clan/ClanShared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,13 +152,15 @@ export function renderServerPagination(

export function renderMemberSearchInput(
onInput: (e: Event) => void,
value: string,
placeholderKey = "clan_modal.search_members_placeholder",
trailing?: TemplateResult,
): TemplateResult {
const input = html`
<div class="relative w-full sm:flex-1 sm:min-w-0">
<input
type="text"
.value=${value}
@input=${onInput}
class="w-full h-10 pl-10 pr-4 bg-white/5 border border-white/10 rounded-xl text-white placeholder-white/20 focus:outline-none focus:ring-2 focus:ring-malibu-blue/50 focus:border-malibu-blue/50 transition-all font-medium hover:bg-white/10 text-sm"
placeholder="${translateText(placeholderKey)}"
Expand Down
35 changes: 27 additions & 8 deletions src/client/components/clan/ClanTransferView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,9 @@ export class ClanTransferView extends LitElement {
@state() private loading = false;
@state() private errorMsg = "";
@state() private confirmAction: "transfer" | null = null;
private memberSearch = "";
@state() private memberSearch = "";
private memberSearchDebounce: ReturnType<typeof setTimeout> | null = null;
private memberLoadSeq = 0;

connectedCallback() {
super.connectedCallback();
Expand All @@ -48,18 +49,30 @@ export class ClanTransferView extends LitElement {

disconnectedCallback() {
if (this.memberSearchDebounce) clearTimeout(this.memberSearchDebounce);
this.memberLoadSeq++;
super.disconnectedCallback();
}

private async loadMembers(page: number) {
if (page === 1) this.loading = true;
const res = await fetchClanMembers(this.clanTag, page, this.membersPerPage);
private async loadMembers(page: number, search = this.memberSearch) {
const seq = ++this.memberLoadSeq;
if (this.members.length === 0) this.loading = true;
const res = search
? await fetchClanMembers(
this.clanTag,
page,
this.membersPerPage,
"default",
undefined,
search,
)
: await fetchClanMembers(this.clanTag, page, this.membersPerPage);
if (seq !== this.memberLoadSeq || search !== this.memberSearch) return;
if (!res) {
this.loading = false;
return;
}
if (res.results.length === 0 && page > 1) {
await this.loadMembers(1);
await this.loadMembers(1, search);
return;
}
this.members = res.results;
Expand Down Expand Up @@ -98,10 +111,13 @@ export class ClanTransferView extends LitElement {
}

private onSearchInput(e: Event) {
const search = (e.target as HTMLInputElement).value.trim();
if (search === this.memberSearch) return;
this.memberSearch = search;
if (this.memberSearchDebounce) clearTimeout(this.memberSearchDebounce);
this.memberSearchDebounce = setTimeout(() => {
this.memberSearch = (e.target as HTMLInputElement).value;
this.requestUpdate();
this.memberSearchDebounce = null;
void this.loadMembers(1, search);
}, 200);
}

Expand Down Expand Up @@ -150,7 +166,10 @@ export class ClanTransferView extends LitElement {
</p>
</div>

${renderMemberSearchInput((e) => this.onSearchInput(e))}
${renderMemberSearchInput(
(e) => this.onSearchInput(e),
this.memberSearch,
)}

<div class="space-y-2">
${filterMembersBySearch(nonLeaders, this.memberSearch).map(
Expand Down
18 changes: 18 additions & 0 deletions tests/client/clan/ClanApiQueries.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,24 @@ describe("fetchClanMembers", () => {
expect(url.searchParams.get("limit")).toBe("50");
});

it("passes a trimmed member search with pagination and sorting", async () => {
const fetchSpy = vi.fn(
(_input: string | URL | Request, _init?: RequestInit) =>
Promise.resolve(okJson(membersResponse)),
);
vi.stubGlobal("fetch", fetchSpy);

await fetchClanMembers("TEST", 3, 50, "winsTotal", "desc", " d3G1QO8Z ");

const calledUrl = fetchSpy.mock.calls[0]![0] as string;
const url = new URL(calledUrl);
expect(url.searchParams.get("page")).toBe("3");
expect(url.searchParams.get("limit")).toBe("50");
expect(url.searchParams.get("sort")).toBe("winsTotal");
expect(url.searchParams.get("order")).toBe("desc");
expect(url.searchParams.get("search")).toBe("d3G1QO8Z");
});

it("includes the optional pendingRequests field", async () => {
mockFetch(() => okJson({ ...membersResponse, pendingRequests: 5 }));
const result = await fetchClanMembers("TEST");
Expand Down
Loading
Loading