Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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