mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 09:50:43 +00:00
[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
This commit is contained in:
@@ -310,6 +310,7 @@
|
||||
"ban": "Ban",
|
||||
"unban": "Unban",
|
||||
"banned_players": "Banned Players",
|
||||
"banned_players_count": "{count, plural, one {# banned player} other {# banned players}}",
|
||||
"no_bans": "No banned players.",
|
||||
"ban_reason_prompt": "Ban reason (optional, max 200 characters):",
|
||||
"confirm_ban": "Are you sure you want to ban this player? They will be removed from the clan and unable to rejoin.",
|
||||
|
||||
+64
-3
@@ -73,7 +73,7 @@ export class ClanModal extends BaseModal {
|
||||
onBack: () => this.close(),
|
||||
ariaLabel: translateText("common.back"),
|
||||
})
|
||||
: null;
|
||||
: this.renderSubViewHeader();
|
||||
return html`
|
||||
<o-modal
|
||||
id="clan-modal"
|
||||
@@ -86,11 +86,72 @@ export class ClanModal extends BaseModal {
|
||||
.onTabChange=${(key: string) => this.handleTabChange(key as Tab)}
|
||||
>
|
||||
${header ? html`<div slot="header">${header}</div>` : ""}
|
||||
${this.renderInner()}
|
||||
<div class="p-4 lg:p-[1.4rem]">${this.renderInner()}</div>
|
||||
</o-modal>
|
||||
`;
|
||||
}
|
||||
|
||||
private tagPill(tag: string) {
|
||||
return html`<span
|
||||
class="text-xs font-bold uppercase tracking-wider px-3 py-1 rounded-full bg-white/10 text-white/50 border border-white/10"
|
||||
>[${tag}]</span
|
||||
>`;
|
||||
}
|
||||
|
||||
private renderSubViewHeader() {
|
||||
const clan = this.selectedClan;
|
||||
const ariaLabel = translateText("common.back");
|
||||
if (this.view === "my-requests") {
|
||||
return modalHeader({
|
||||
title: translateText("clan_modal.pending_applications"),
|
||||
onBack: () => (this.view = "list"),
|
||||
ariaLabel,
|
||||
});
|
||||
}
|
||||
if (this.view === "manage") {
|
||||
return modalHeader({
|
||||
title: translateText("clan_modal.manage_clan"),
|
||||
onBack: () => (this.view = "detail"),
|
||||
ariaLabel,
|
||||
rightContent: clan ? this.tagPill(clan.tag) : undefined,
|
||||
});
|
||||
}
|
||||
if (this.view === "transfer") {
|
||||
return modalHeader({
|
||||
title: translateText("clan_modal.transfer_leadership"),
|
||||
onBack: () => (this.view = "manage"),
|
||||
ariaLabel,
|
||||
});
|
||||
}
|
||||
if (this.view === "requests") {
|
||||
return modalHeader({
|
||||
title: translateText("clan_modal.join_requests"),
|
||||
onBack: () => (this.view = "detail"),
|
||||
ariaLabel,
|
||||
});
|
||||
}
|
||||
if (this.view === "bans") {
|
||||
return modalHeader({
|
||||
title: translateText("clan_modal.banned_players"),
|
||||
onBack: () => (this.view = "manage"),
|
||||
ariaLabel,
|
||||
});
|
||||
}
|
||||
// Default: detail
|
||||
return modalHeader({
|
||||
title: clan?.name ?? translateText("clan_modal.title"),
|
||||
onBack: () => {
|
||||
this.view = "list";
|
||||
this.selectedClan = null;
|
||||
this.selectedClanTag = "";
|
||||
this.myRole = null;
|
||||
this.detailCache = null;
|
||||
},
|
||||
ariaLabel,
|
||||
rightContent: clan ? this.tagPill(clan.tag) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
private handleTabChange(tab: Tab) {
|
||||
this.activeTab = tab;
|
||||
this.view = "list";
|
||||
@@ -351,7 +412,7 @@ export class ClanModal extends BaseModal {
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="p-4 lg:p-6 space-y-3">
|
||||
<div class="space-y-3">
|
||||
${hasRequests ? this.renderPendingRequestsButton() : ""}
|
||||
${this.myClans.map(
|
||||
(clan) => html`
|
||||
|
||||
@@ -343,7 +343,9 @@ export class UserSettingModal extends BaseModal {
|
||||
showDivider: true,
|
||||
})}
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">${activeContent}</div>
|
||||
<div class="flex flex-col gap-2 p-4 lg:p-[1.4rem]">
|
||||
${activeContent}
|
||||
</div>
|
||||
</o-modal>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -126,9 +126,8 @@ export class OModal extends LitElement {
|
||||
!this.inline && this.maxWidth ? `max-width: ${this.maxWidth};` : "";
|
||||
|
||||
const hasTabs = this.tabs.length > 0;
|
||||
const sectionClass = hasTabs
|
||||
? "relative flex-1 min-h-0 flex flex-col text-white bg-black/70 backdrop-blur-xl lg:rounded-2xl lg:border border-white/10 overflow-hidden"
|
||||
: "relative flex-1 min-h-0 flex flex-col text-white bg-[#23232382] backdrop-blur-md lg:rounded-lg overflow-hidden";
|
||||
const sectionClass =
|
||||
"relative flex-1 min-h-0 flex flex-col text-white bg-black/70 backdrop-blur-xl lg:rounded-2xl lg:border border-white/10 overflow-hidden";
|
||||
|
||||
return html`
|
||||
<aside
|
||||
@@ -158,7 +157,7 @@ export class OModal extends LitElement {
|
||||
<section class="${sectionClass}">
|
||||
<slot name="header"></slot>
|
||||
${hasTabs ? this.renderTabs() : html``}
|
||||
<div class="flex-1 min-h-0 overflow-y-auto p-0 lg:p-[1.4rem]">
|
||||
<div class="flex-1 min-h-0 overflow-y-auto">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -3,10 +3,8 @@ import { customElement, property, state } from "lit/decorators.js";
|
||||
import { type ClanBan, fetchClanBans, unbanClanMember } from "../../ClanApi";
|
||||
import { translateText } from "../../Utils";
|
||||
import "../CopyButton";
|
||||
import { modalHeader } from "../ui/ModalHeader";
|
||||
import {
|
||||
formatClanDate,
|
||||
modalContainerClass,
|
||||
renderLoadingSpinner,
|
||||
renderMemberSearchInput,
|
||||
renderServerPagination,
|
||||
@@ -92,20 +90,7 @@ export class ClanBansView extends LitElement {
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.loading)
|
||||
return html`<div class="${modalContainerClass}">
|
||||
${modalHeader({
|
||||
title: translateText("clan_modal.banned_players"),
|
||||
onBack: () =>
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("navigate-back", {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
),
|
||||
ariaLabel: translateText("common.back"),
|
||||
})}${renderLoadingSpinner()}
|
||||
</div>`;
|
||||
if (this.loading) return renderLoadingSpinner();
|
||||
|
||||
const totalPages = Math.ceil(this.bansTotal / this.bansLimit);
|
||||
const filtered = this.memberSearch
|
||||
@@ -115,110 +100,98 @@ export class ClanBansView extends LitElement {
|
||||
: this.bans;
|
||||
|
||||
return html`
|
||||
<div class="${modalContainerClass}">
|
||||
${modalHeader({
|
||||
title: translateText("clan_modal.banned_players"),
|
||||
onBack: () =>
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("navigate-back", {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
),
|
||||
ariaLabel: translateText("common.back"),
|
||||
rightContent: html`<span
|
||||
class="text-xs font-bold uppercase tracking-wider px-3 py-1 rounded-full bg-white/10 text-white/50 border border-white/10"
|
||||
>${this.bansTotal}</span
|
||||
>`,
|
||||
})}
|
||||
<div class="flex-1 overflow-y-auto custom-scrollbar mr-1 p-4 lg:p-6">
|
||||
${renderMemberSearchInput(
|
||||
(e) => this.onSearchInput(e),
|
||||
"clan_modal.search_members_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_bans")}
|
||||
</p>
|
||||
</div>`
|
||||
: html`
|
||||
<div class="space-y-3">
|
||||
${filtered.map(
|
||||
(ban) => html`
|
||||
<div
|
||||
class="bg-white/5 rounded-xl border border-white/10 p-4 space-y-2"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<div
|
||||
class="w-8 h-8 rounded-full bg-red-500/20 flex items-center justify-center shrink-0"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-4 h-4 text-red-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<copy-button
|
||||
compact
|
||||
.copyText=${ban.publicId}
|
||||
.displayText=${ban.publicId}
|
||||
.showVisibilityToggle=${false}
|
||||
.showCopyIcon=${false}
|
||||
></copy-button>
|
||||
<span class="text-white/30 text-xs shrink-0"
|
||||
>${translateText(
|
||||
"clan_modal.banned_by_label",
|
||||
)}</span
|
||||
>
|
||||
<copy-button
|
||||
compact
|
||||
.copyText=${ban.bannedBy}
|
||||
.displayText=${ban.bannedBy}
|
||||
.showVisibilityToggle=${false}
|
||||
.showCopyIcon=${false}
|
||||
></copy-button>
|
||||
<span class="text-white/30 text-xs shrink-0"
|
||||
>${formatClanDate(ban.createdAt)}</span
|
||||
>
|
||||
<div class="flex-1"></div>
|
||||
<button
|
||||
@click=${() => this.handleUnban(ban.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 shrink-0"
|
||||
>
|
||||
${translateText("clan_modal.unban")}
|
||||
</button>
|
||||
</div>
|
||||
${ban.reason
|
||||
? html`<div class="text-white/50 text-xs pl-10">
|
||||
${translateText("clan_modal.ban_reason", {
|
||||
reason: ban.reason,
|
||||
})}
|
||||
</div>`
|
||||
: ""}
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
${totalPages > 1
|
||||
? renderServerPagination(this.bansPage, totalPages, (p) =>
|
||||
this.loadBans(p, false),
|
||||
)
|
||||
: ""}
|
||||
`}
|
||||
<div>
|
||||
<div
|
||||
class="text-[10px] font-bold uppercase tracking-wider text-white/40 mb-2"
|
||||
>
|
||||
${translateText("clan_modal.banned_players_count", {
|
||||
count: this.bansTotal,
|
||||
})}
|
||||
</div>
|
||||
${renderMemberSearchInput(
|
||||
(e) => this.onSearchInput(e),
|
||||
"clan_modal.search_members_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_bans")}
|
||||
</p>
|
||||
</div>`
|
||||
: html`
|
||||
<div class="space-y-3">
|
||||
${filtered.map(
|
||||
(ban) => html`
|
||||
<div
|
||||
class="bg-white/5 rounded-xl border border-white/10 p-4 space-y-2"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<div
|
||||
class="w-8 h-8 rounded-full bg-red-500/20 flex items-center justify-center shrink-0"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-4 h-4 text-red-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<copy-button
|
||||
compact
|
||||
.copyText=${ban.publicId}
|
||||
.displayText=${ban.publicId}
|
||||
.showVisibilityToggle=${false}
|
||||
.showCopyIcon=${false}
|
||||
></copy-button>
|
||||
<span class="text-white/30 text-xs shrink-0"
|
||||
>${translateText("clan_modal.banned_by_label")}</span
|
||||
>
|
||||
<copy-button
|
||||
compact
|
||||
.copyText=${ban.bannedBy}
|
||||
.displayText=${ban.bannedBy}
|
||||
.showVisibilityToggle=${false}
|
||||
.showCopyIcon=${false}
|
||||
></copy-button>
|
||||
<span class="text-white/30 text-xs shrink-0"
|
||||
>${formatClanDate(ban.createdAt)}</span
|
||||
>
|
||||
<div class="flex-1"></div>
|
||||
<button
|
||||
@click=${() => this.handleUnban(ban.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 shrink-0"
|
||||
>
|
||||
${translateText("clan_modal.unban")}
|
||||
</button>
|
||||
</div>
|
||||
${ban.reason
|
||||
? html`<div class="text-white/50 text-xs pl-10">
|
||||
${translateText("clan_modal.ban_reason", {
|
||||
reason: ban.reason,
|
||||
})}
|
||||
</div>`
|
||||
: ""}
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
${totalPages > 1
|
||||
? renderServerPagination(this.bansPage, totalPages, (p) =>
|
||||
this.loadBans(p, false),
|
||||
)
|
||||
: ""}
|
||||
`}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -90,8 +90,7 @@ export class ClanBrowseView extends LitElement {
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.loading && !this.browseData)
|
||||
return html`<div class="p-4 lg:p-6">${renderLoadingSpinner()}</div>`;
|
||||
if (this.loading && !this.browseData) return renderLoadingSpinner();
|
||||
|
||||
const totalPages = this.browseData
|
||||
? Math.ceil(this.browseData.total / this.browseData.limit)
|
||||
@@ -102,7 +101,7 @@ export class ClanBrowseView extends LitElement {
|
||||
);
|
||||
|
||||
return html`
|
||||
<div class="p-4 lg:p-6 space-y-4">
|
||||
<div class="space-y-4">
|
||||
<div class="relative">
|
||||
<input
|
||||
type="text"
|
||||
|
||||
@@ -16,12 +16,10 @@ import {
|
||||
import { translateText } from "../../Utils";
|
||||
import "../ConfirmDialog";
|
||||
import "../CopyButton";
|
||||
import { modalHeader } from "../ui/ModalHeader";
|
||||
import {
|
||||
type ClanRole,
|
||||
defaultOrderForSort,
|
||||
filterMembersBySearch,
|
||||
modalContainerClass,
|
||||
renderClanWL,
|
||||
renderLoadingSpinner,
|
||||
renderMemberPagination,
|
||||
@@ -282,16 +280,7 @@ export class ClanDetailView extends LitElement {
|
||||
|
||||
render() {
|
||||
if (this.loading) {
|
||||
return html`
|
||||
<div class="${modalContainerClass}">
|
||||
${modalHeader({
|
||||
title: translateText("clan_modal.title"),
|
||||
onBack: () => this.back(),
|
||||
ariaLabel: translateText("common.back"),
|
||||
})}
|
||||
${renderLoadingSpinner()}
|
||||
</div>
|
||||
`;
|
||||
return renderLoadingSpinner();
|
||||
}
|
||||
|
||||
const clan = this.selectedClan;
|
||||
@@ -306,58 +295,40 @@ export class ClanDetailView extends LitElement {
|
||||
);
|
||||
|
||||
return html`
|
||||
<div class="${modalContainerClass}">
|
||||
${modalHeader({
|
||||
title: clan.name,
|
||||
onBack: () => this.back(),
|
||||
ariaLabel: translateText("common.back"),
|
||||
rightContent: html`
|
||||
<span
|
||||
class="text-xs font-bold uppercase tracking-wider px-3 py-1 rounded-full bg-white/10 text-white/50 border border-white/10"
|
||||
>
|
||||
[${clan.tag}]
|
||||
</span>
|
||||
`,
|
||||
})}
|
||||
<div class="space-y-6">
|
||||
<div class="bg-white/5 rounded-xl border border-white/10 p-5">
|
||||
<p class="text-white/70 text-sm">
|
||||
${clan.description || translateText("clan_modal.no_description")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto custom-scrollbar mr-1 p-4 lg:p-6">
|
||||
<div class="space-y-6">
|
||||
<div class="bg-white/5 rounded-xl border border-white/10 p-5">
|
||||
<p class="text-white/70 text-sm">
|
||||
${clan.description ||
|
||||
translateText("clan_modal.no_description")}
|
||||
</p>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
${renderStat(
|
||||
translateText("clan_modal.members"),
|
||||
`${clan.memberCount ?? 0}`,
|
||||
)}
|
||||
${renderStat(
|
||||
translateText("clan_modal.status"),
|
||||
clan.isOpen
|
||||
? translateText("clan_modal.open")
|
||||
: translateText("clan_modal.invite_only"),
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
${renderStat(
|
||||
translateText("clan_modal.members"),
|
||||
`${clan.memberCount ?? 0}`,
|
||||
)}
|
||||
${renderStat(
|
||||
translateText("clan_modal.status"),
|
||||
clan.isOpen
|
||||
? translateText("clan_modal.open")
|
||||
: translateText("clan_modal.invite_only"),
|
||||
)}
|
||||
</div>
|
||||
${this.clanStats ? renderClanWL(this.clanStats) : ""}
|
||||
${canManageRequests && this.pendingRequestCount > 0
|
||||
? this.renderRequestsButton()
|
||||
: ""}
|
||||
${isMember ? this.renderMembersList() : ""}
|
||||
|
||||
${this.clanStats ? renderClanWL(this.clanStats) : ""}
|
||||
${canManageRequests && this.pendingRequestCount > 0
|
||||
? this.renderRequestsButton()
|
||||
: ""}
|
||||
${isMember ? this.renderMembersList() : ""}
|
||||
|
||||
<div class="flex flex-wrap gap-3">
|
||||
${this.renderActionButtons(
|
||||
isMember,
|
||||
isLeader,
|
||||
isOfficer,
|
||||
hasPendingRequest,
|
||||
clan,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
${this.renderActionButtons(
|
||||
isMember,
|
||||
isLeader,
|
||||
isOfficer,
|
||||
hasPendingRequest,
|
||||
clan,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -531,10 +502,4 @@ export class ClanDetailView extends LitElement {
|
||||
}
|
||||
return buttons;
|
||||
}
|
||||
|
||||
private back() {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("navigate-back", { bubbles: true, composed: true }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,13 +17,11 @@ import {
|
||||
import { translateText } from "../../Utils";
|
||||
import "../ConfirmDialog";
|
||||
import "../CopyButton";
|
||||
import { modalHeader } from "../ui/ModalHeader";
|
||||
import {
|
||||
type ClanRole,
|
||||
defaultOrderForSort,
|
||||
filterMembersBySearch,
|
||||
formatClanDate,
|
||||
modalContainerClass,
|
||||
renderLoadingSpinner,
|
||||
renderMemberPagination,
|
||||
renderMemberSearchInput,
|
||||
@@ -262,21 +260,8 @@ export class ClanManageView extends LitElement {
|
||||
this.loadMembers(1);
|
||||
}
|
||||
|
||||
private navigateDetail = () =>
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("navigate-detail", { bubbles: true, composed: true }),
|
||||
);
|
||||
|
||||
render() {
|
||||
if (this.loading)
|
||||
return html`<div class="${modalContainerClass}">
|
||||
${modalHeader({
|
||||
title: translateText("clan_modal.manage_clan"),
|
||||
onBack: this.navigateDetail,
|
||||
ariaLabel: translateText("common.back"),
|
||||
})}
|
||||
${renderLoadingSpinner()}
|
||||
</div>`;
|
||||
if (this.loading) return renderLoadingSpinner();
|
||||
|
||||
const clan = this.selectedClan;
|
||||
if (!clan) return "";
|
||||
@@ -335,187 +320,169 @@ export class ClanManageView extends LitElement {
|
||||
|
||||
private renderManageContent(clan: ClanInfo) {
|
||||
return html`
|
||||
<div class="${modalContainerClass}">
|
||||
${modalHeader({
|
||||
title: translateText("clan_modal.manage_clan"),
|
||||
onBack: this.navigateDetail,
|
||||
ariaLabel: translateText("common.back"),
|
||||
rightContent: html`<span
|
||||
class="text-xs font-bold uppercase tracking-wider px-3 py-1 rounded-full bg-white/10 text-white/50 border border-white/10"
|
||||
>[${clan.tag}]</span
|
||||
>`,
|
||||
})}
|
||||
<div class="flex-1 overflow-y-auto custom-scrollbar mr-1 p-4 lg:p-6">
|
||||
<div class="space-y-6">
|
||||
<!-- Edit Settings -->
|
||||
<div
|
||||
class="bg-white/5 rounded-2xl border border-white/10 p-6 space-y-5"
|
||||
<div class="space-y-6">
|
||||
<!-- Edit Settings -->
|
||||
<div
|
||||
class="bg-white/5 rounded-2xl border border-white/10 p-6 space-y-5"
|
||||
>
|
||||
<h3 class="text-sm font-bold text-white/60 uppercase tracking-wider">
|
||||
${translateText("clan_modal.clan_settings")}
|
||||
</h3>
|
||||
<div>
|
||||
<label
|
||||
class="block text-[10px] font-bold text-white/40 uppercase tracking-wider mb-2"
|
||||
>${translateText("clan_modal.clan_name")}</label
|
||||
>
|
||||
<h3
|
||||
class="text-sm font-bold text-white/60 uppercase tracking-wider"
|
||||
>
|
||||
${translateText("clan_modal.clan_settings")}
|
||||
</h3>
|
||||
<div>
|
||||
<label
|
||||
class="block text-[10px] font-bold text-white/40 uppercase tracking-wider mb-2"
|
||||
>${translateText("clan_modal.clan_name")}</label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
.value=${this.manageName}
|
||||
@input=${(e: Event) =>
|
||||
(this.manageName = (e.target as HTMLInputElement).value)}
|
||||
maxlength="35"
|
||||
class="w-full px-4 py-3 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"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
class="block text-[10px] font-bold text-white/40 uppercase tracking-wider mb-2"
|
||||
>${translateText("clan_modal.description")}</label
|
||||
>
|
||||
<textarea
|
||||
.value=${this.manageDescription}
|
||||
@input=${(e: Event) =>
|
||||
(this.manageDescription = (
|
||||
e.target as HTMLTextAreaElement
|
||||
).value)}
|
||||
maxlength="200"
|
||||
rows="3"
|
||||
class="w-full px-4 py-3 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 resize-none"
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<div class="text-white text-sm font-bold">
|
||||
${translateText("clan_modal.open_clan")}
|
||||
</div>
|
||||
<div class="text-white/40 text-xs">
|
||||
${translateText("clan_modal.open_clan_desc")}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
role="switch"
|
||||
aria-checked="${this.manageIsOpen}"
|
||||
aria-label="${translateText("clan_modal.open_clan")}"
|
||||
@click=${() => (this.manageIsOpen = !this.manageIsOpen)}
|
||||
class="relative w-12 h-7 rounded-full transition-all ${this
|
||||
.manageIsOpen
|
||||
? "bg-malibu-blue"
|
||||
: "bg-white/20"}"
|
||||
>
|
||||
<div
|
||||
class="absolute top-1 w-5 h-5 rounded-full bg-white shadow transition-all ${this
|
||||
.manageIsOpen
|
||||
? "left-6"
|
||||
: "left-1"}"
|
||||
></div>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
@click=${() => this.handleSaveSettings()}
|
||||
?disabled=${this.saving}
|
||||
class="w-full px-6 py-3 text-sm font-bold text-white uppercase tracking-wider bg-malibu-blue hover:bg-aquarius active:bg-malibu-blue/80 rounded-xl transition-all disabled:opacity-50"
|
||||
>
|
||||
${this.saving
|
||||
? translateText("clan_modal.saving")
|
||||
: translateText("clan_modal.save_changes")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Member Management -->
|
||||
<div
|
||||
class="bg-white/5 rounded-2xl border border-white/10 p-6 space-y-4"
|
||||
>
|
||||
<h3
|
||||
class="text-sm font-bold text-white/60 uppercase tracking-wider"
|
||||
>
|
||||
${translateText("clan_modal.members")}
|
||||
(${clan.memberCount ?? 0})
|
||||
</h3>
|
||||
${renderMemberSearchInput(
|
||||
(e) => this.onSearchInput(e),
|
||||
undefined,
|
||||
renderMemberSortControl(
|
||||
this.memberSort,
|
||||
this.memberOrder,
|
||||
(s) => this.onSortChange(s),
|
||||
() => this.onOrderToggle(),
|
||||
),
|
||||
)}
|
||||
${(() => {
|
||||
const filtered = filterMembersBySearch(
|
||||
this.members,
|
||||
this.memberSearch,
|
||||
);
|
||||
return html`
|
||||
<div class="space-y-2">
|
||||
${filtered.map((m) => this.renderManageMemberRow(m))}
|
||||
</div>
|
||||
${renderMemberPagination(
|
||||
this.memberPage,
|
||||
this.membersTotal,
|
||||
this.membersPerPage,
|
||||
(p) => this.loadMembers(p),
|
||||
(pp) => {
|
||||
this.membersPerPage = pp;
|
||||
this.loadMembers(1);
|
||||
},
|
||||
)}
|
||||
`;
|
||||
})()}
|
||||
</div>
|
||||
|
||||
<!-- Danger Zone -->
|
||||
<div
|
||||
class="bg-red-500/5 rounded-2xl border border-red-500/20 p-6 space-y-4"
|
||||
>
|
||||
<h3
|
||||
class="text-sm font-bold text-red-400/80 uppercase tracking-wider"
|
||||
>
|
||||
${translateText("clan_modal.danger_zone")}
|
||||
</h3>
|
||||
<button
|
||||
@click=${() =>
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("navigate-bans", {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
)}
|
||||
class="w-full px-6 py-3 text-sm font-bold text-red-400 uppercase tracking-wider bg-red-600/20 hover:bg-red-600/30 rounded-xl transition-all border border-red-500/30"
|
||||
>
|
||||
${translateText("clan_modal.banned_players")}
|
||||
</button>
|
||||
${this.myRole === "leader"
|
||||
? html`
|
||||
<button
|
||||
@click=${() =>
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("navigate-transfer", {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
)}
|
||||
class="w-full px-6 py-3 text-sm font-bold text-amber-400 uppercase tracking-wider bg-amber-600/20 hover:bg-amber-600/30 rounded-xl transition-all border border-amber-500/30"
|
||||
>
|
||||
${translateText("clan_modal.transfer_leadership")}
|
||||
</button>
|
||||
<button
|
||||
@click=${() => {
|
||||
this.confirmAction = "disband";
|
||||
this.confirmTargetId = null;
|
||||
}}
|
||||
?disabled=${this.confirmAction === "disband"}
|
||||
class="w-full px-6 py-3 text-sm font-bold text-red-400 uppercase tracking-wider bg-red-600/20 hover:bg-red-600/30 rounded-xl transition-all border border-red-500/30 disabled:opacity-50 disabled:pointer-events-none"
|
||||
>
|
||||
${translateText("clan_modal.disband_clan")}
|
||||
</button>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
.value=${this.manageName}
|
||||
@input=${(e: Event) =>
|
||||
(this.manageName = (e.target as HTMLInputElement).value)}
|
||||
maxlength="35"
|
||||
class="w-full px-4 py-3 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"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
class="block text-[10px] font-bold text-white/40 uppercase tracking-wider mb-2"
|
||||
>${translateText("clan_modal.description")}</label
|
||||
>
|
||||
<textarea
|
||||
.value=${this.manageDescription}
|
||||
@input=${(e: Event) =>
|
||||
(this.manageDescription = (
|
||||
e.target as HTMLTextAreaElement
|
||||
).value)}
|
||||
maxlength="200"
|
||||
rows="3"
|
||||
class="w-full px-4 py-3 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 resize-none"
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<div class="text-white text-sm font-bold">
|
||||
${translateText("clan_modal.open_clan")}
|
||||
</div>
|
||||
<div class="text-white/40 text-xs">
|
||||
${translateText("clan_modal.open_clan_desc")}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
role="switch"
|
||||
aria-checked="${this.manageIsOpen}"
|
||||
aria-label="${translateText("clan_modal.open_clan")}"
|
||||
@click=${() => (this.manageIsOpen = !this.manageIsOpen)}
|
||||
class="relative w-12 h-7 rounded-full transition-all ${this
|
||||
.manageIsOpen
|
||||
? "bg-malibu-blue"
|
||||
: "bg-white/20"}"
|
||||
>
|
||||
<div
|
||||
class="absolute top-1 w-5 h-5 rounded-full bg-white shadow transition-all ${this
|
||||
.manageIsOpen
|
||||
? "left-6"
|
||||
: "left-1"}"
|
||||
></div>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
@click=${() => this.handleSaveSettings()}
|
||||
?disabled=${this.saving}
|
||||
class="w-full px-6 py-3 text-sm font-bold text-white uppercase tracking-wider bg-malibu-blue hover:bg-aquarius active:bg-malibu-blue/80 rounded-xl transition-all disabled:opacity-50"
|
||||
>
|
||||
${this.saving
|
||||
? translateText("clan_modal.saving")
|
||||
: translateText("clan_modal.save_changes")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Member Management -->
|
||||
<div
|
||||
class="bg-white/5 rounded-2xl border border-white/10 p-6 space-y-4"
|
||||
>
|
||||
<h3 class="text-sm font-bold text-white/60 uppercase tracking-wider">
|
||||
${translateText("clan_modal.members")} (${clan.memberCount ?? 0})
|
||||
</h3>
|
||||
${renderMemberSearchInput(
|
||||
(e) => this.onSearchInput(e),
|
||||
undefined,
|
||||
renderMemberSortControl(
|
||||
this.memberSort,
|
||||
this.memberOrder,
|
||||
(s) => this.onSortChange(s),
|
||||
() => this.onOrderToggle(),
|
||||
),
|
||||
)}
|
||||
${(() => {
|
||||
const filtered = filterMembersBySearch(
|
||||
this.members,
|
||||
this.memberSearch,
|
||||
);
|
||||
return html`
|
||||
<div class="space-y-2">
|
||||
${filtered.map((m) => this.renderManageMemberRow(m))}
|
||||
</div>
|
||||
${renderMemberPagination(
|
||||
this.memberPage,
|
||||
this.membersTotal,
|
||||
this.membersPerPage,
|
||||
(p) => this.loadMembers(p),
|
||||
(pp) => {
|
||||
this.membersPerPage = pp;
|
||||
this.loadMembers(1);
|
||||
},
|
||||
)}
|
||||
`;
|
||||
})()}
|
||||
</div>
|
||||
|
||||
<!-- Danger Zone -->
|
||||
<div
|
||||
class="bg-red-500/5 rounded-2xl border border-red-500/20 p-6 space-y-4"
|
||||
>
|
||||
<h3
|
||||
class="text-sm font-bold text-red-400/80 uppercase tracking-wider"
|
||||
>
|
||||
${translateText("clan_modal.danger_zone")}
|
||||
</h3>
|
||||
<button
|
||||
@click=${() =>
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("navigate-bans", {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
)}
|
||||
class="w-full px-6 py-3 text-sm font-bold text-red-400 uppercase tracking-wider bg-red-600/20 hover:bg-red-600/30 rounded-xl transition-all border border-red-500/30"
|
||||
>
|
||||
${translateText("clan_modal.banned_players")}
|
||||
</button>
|
||||
${this.myRole === "leader"
|
||||
? html`
|
||||
<button
|
||||
@click=${() =>
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("navigate-transfer", {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
)}
|
||||
class="w-full px-6 py-3 text-sm font-bold text-amber-400 uppercase tracking-wider bg-amber-600/20 hover:bg-amber-600/30 rounded-xl transition-all border border-amber-500/30"
|
||||
>
|
||||
${translateText("clan_modal.transfer_leadership")}
|
||||
</button>
|
||||
<button
|
||||
@click=${() => {
|
||||
this.confirmAction = "disband";
|
||||
this.confirmTargetId = null;
|
||||
}}
|
||||
?disabled=${this.confirmAction === "disband"}
|
||||
class="w-full px-6 py-3 text-sm font-bold text-red-400 uppercase tracking-wider bg-red-600/20 hover:bg-red-600/30 rounded-xl transition-all border border-red-500/30 disabled:opacity-50 disabled:pointer-events-none"
|
||||
>
|
||||
${translateText("clan_modal.disband_clan")}
|
||||
</button>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -3,8 +3,7 @@ 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";
|
||||
import { formatClanDate, showToast } from "./ClanShared";
|
||||
|
||||
@customElement("clan-my-requests-view")
|
||||
export class ClanMyRequestsView extends LitElement {
|
||||
@@ -45,60 +44,47 @@ export class ClanMyRequestsView extends LitElement {
|
||||
|
||||
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>
|
||||
${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="flex items-center gap-3 bg-white/5 rounded-xl border border-white/10 p-4"
|
||||
class="w-10 h-10 rounded-xl bg-amber-500/10 flex items-center justify-center border border-amber-500/20 shrink-0"
|
||||
>
|
||||
<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
|
||||
>
|
||||
<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 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>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -9,11 +9,9 @@ import {
|
||||
} from "../../ClanApi";
|
||||
import { translateText } from "../../Utils";
|
||||
import "../CopyButton";
|
||||
import { modalHeader } from "../ui/ModalHeader";
|
||||
import {
|
||||
filterRequestsBySearch,
|
||||
formatClanDate,
|
||||
modalContainerClass,
|
||||
renderLoadingSpinner,
|
||||
renderMemberSearchInput,
|
||||
renderServerPagination,
|
||||
@@ -122,97 +120,80 @@ export class ClanRequestsView extends LitElement {
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.loading)
|
||||
return html`<div class="${modalContainerClass}">
|
||||
${modalHeader({
|
||||
title: translateText("clan_modal.join_requests"),
|
||||
onBack: () => this.back(),
|
||||
ariaLabel: translateText("common.back"),
|
||||
})}${renderLoadingSpinner()}
|
||||
</div>`;
|
||||
if (this.loading) return renderLoadingSpinner();
|
||||
|
||||
const totalPages = Math.ceil(this.requestsTotal / this.requestsLimit);
|
||||
const filtered = filterRequestsBySearch(this.requests, this.memberSearch);
|
||||
return html`
|
||||
<div class="${modalContainerClass}">
|
||||
${modalHeader({
|
||||
title: translateText("clan_modal.join_requests"),
|
||||
onBack: () => this.back(),
|
||||
ariaLabel: translateText("common.back"),
|
||||
rightContent: html`<span
|
||||
class="text-xs font-bold uppercase tracking-wider px-3 py-1 rounded-full bg-white/10 text-white/50 border border-white/10"
|
||||
>${this.requestsTotal}</span
|
||||
>`,
|
||||
})}
|
||||
<div class="flex-1 overflow-y-auto custom-scrollbar mr-1 p-4 lg:p-6">
|
||||
${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>
|
||||
<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>
|
||||
`;
|
||||
}
|
||||
|
||||
private back() {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("navigate-back", { bubbles: true, composed: true }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,9 +17,6 @@ export function defaultOrderForSort(sort: ClanMemberSort): ClanMemberOrder {
|
||||
return sort === "default" ? "asc" : "desc";
|
||||
}
|
||||
|
||||
export const modalContainerClass =
|
||||
"h-full flex flex-col overflow-hidden bg-black/70 backdrop-blur-xl lg:rounded-2xl lg:border border-white/10";
|
||||
|
||||
const dateCache = new Map<string, string>();
|
||||
|
||||
export function formatClanDate(iso: string): string {
|
||||
|
||||
@@ -10,10 +10,8 @@ import {
|
||||
import { translateText } from "../../Utils";
|
||||
import "../ConfirmDialog";
|
||||
import "../CopyButton";
|
||||
import { modalHeader } from "../ui/ModalHeader";
|
||||
import {
|
||||
filterMembersBySearch,
|
||||
modalContainerClass,
|
||||
renderLoadingSpinner,
|
||||
renderMemberSearchInput,
|
||||
renderRoleIcon,
|
||||
@@ -108,14 +106,7 @@ export class ClanTransferView extends LitElement {
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.loading)
|
||||
return html`<div class="${modalContainerClass}">
|
||||
${modalHeader({
|
||||
title: translateText("clan_modal.transfer_leadership"),
|
||||
onBack: () => this.back(),
|
||||
ariaLabel: translateText("common.back"),
|
||||
})}${renderLoadingSpinner()}
|
||||
</div>`;
|
||||
if (this.loading) return renderLoadingSpinner();
|
||||
|
||||
const nonLeaders = this.members.filter(
|
||||
(m: ClanMember) => m.role !== "leader",
|
||||
@@ -148,111 +139,94 @@ export class ClanTransferView extends LitElement {
|
||||
|
||||
private renderContent(nonLeaders: ClanMember[], totalMemberPages: number) {
|
||||
return html`
|
||||
<div class="${modalContainerClass}">
|
||||
${modalHeader({
|
||||
title: translateText("clan_modal.transfer_leadership"),
|
||||
onBack: () => this.back(),
|
||||
ariaLabel: translateText("common.back"),
|
||||
})}
|
||||
<div class="flex-1 overflow-y-auto custom-scrollbar mr-1 p-4 lg:p-6">
|
||||
<div class="space-y-6">
|
||||
${this.errorMsg
|
||||
? html`<p class="text-red-400 text-sm">${this.errorMsg}</p>`
|
||||
: ""}
|
||||
<div class="space-y-6">
|
||||
${this.errorMsg
|
||||
? html`<p class="text-red-400 text-sm">${this.errorMsg}</p>`
|
||||
: ""}
|
||||
|
||||
<div
|
||||
class="bg-amber-500/10 rounded-xl border border-amber-500/20 p-4"
|
||||
>
|
||||
<p class="text-amber-400/80 text-sm">
|
||||
${translateText("clan_modal.transfer_warning")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
${renderMemberSearchInput((e) => this.onSearchInput(e))}
|
||||
|
||||
<div class="space-y-2">
|
||||
${filterMembersBySearch(nonLeaders, this.memberSearch).map(
|
||||
(m) => html`
|
||||
<button
|
||||
@click=${() => (this.transferTarget = m.publicId)}
|
||||
class="w-full flex items-center gap-3 py-2.5 px-3 rounded-xl border cursor-pointer transition-all text-left focus:outline-none focus:ring-2 focus:ring-amber-500/50
|
||||
${this.transferTarget === m.publicId
|
||||
? "bg-amber-500/10 border-amber-500/20"
|
||||
: "bg-white/5 border-white/10 hover:bg-white/10"}"
|
||||
aria-selected=${this.transferTarget === m.publicId}
|
||||
>
|
||||
<div
|
||||
class="w-8 h-8 rounded-full bg-white/10 flex items-center justify-center text-white/50 text-xs font-bold shrink-0"
|
||||
>
|
||||
${renderRoleIcon(m.role)}
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<copy-button
|
||||
compact
|
||||
.copyText=${m.publicId}
|
||||
.displayText=${m.publicId}
|
||||
.showVisibilityToggle=${false}
|
||||
.showCopyIcon=${false}
|
||||
></copy-button>
|
||||
</div>
|
||||
<span
|
||||
class="text-[10px] font-bold uppercase tracking-wider px-2 py-0.5 rounded-full shrink-0
|
||||
${m.role === "officer"
|
||||
? "bg-purple-500/20 text-purple-400 border border-purple-500/30"
|
||||
: "bg-white/10 text-white/40 border border-white/10"}"
|
||||
>
|
||||
${translateClanRole(m.role)}
|
||||
</span>
|
||||
${this.transferTarget === m.publicId
|
||||
? html`<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-5 h-5 text-amber-400 shrink-0"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>`
|
||||
: ""}
|
||||
</button>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
|
||||
${totalMemberPages > 1
|
||||
? renderServerPagination(this.memberPage, totalMemberPages, (p) =>
|
||||
this.loadMembers(p),
|
||||
)
|
||||
: ""}
|
||||
|
||||
<button
|
||||
@click=${() => (this.confirmAction = "transfer")}
|
||||
class="w-full px-6 py-3 text-sm font-bold text-white uppercase tracking-wider rounded-xl transition-all border disabled:opacity-50 disabled:pointer-events-none
|
||||
${this.transferTarget && !this.actionPending
|
||||
? "bg-gradient-to-r from-amber-600 to-amber-700 hover:from-amber-500 hover:to-amber-600 shadow-lg hover:shadow-amber-900/40 border-white/5"
|
||||
: "bg-white/5 border-white/10 text-white/30 cursor-not-allowed"}"
|
||||
?disabled=${!this.transferTarget || this.actionPending}
|
||||
>
|
||||
${this.transferTarget
|
||||
? translateText("clan_modal.confirm_transfer", {
|
||||
name: this.transferTarget,
|
||||
})
|
||||
: translateText("clan_modal.select_new_leader")}
|
||||
</button>
|
||||
</div>
|
||||
<div class="bg-amber-500/10 rounded-xl border border-amber-500/20 p-4">
|
||||
<p class="text-amber-400/80 text-sm">
|
||||
${translateText("clan_modal.transfer_warning")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
${renderMemberSearchInput((e) => this.onSearchInput(e))}
|
||||
|
||||
<div class="space-y-2">
|
||||
${filterMembersBySearch(nonLeaders, this.memberSearch).map(
|
||||
(m) => html`
|
||||
<button
|
||||
@click=${() => (this.transferTarget = m.publicId)}
|
||||
class="w-full flex items-center gap-3 py-2.5 px-3 rounded-xl border cursor-pointer transition-all text-left focus:outline-none focus:ring-2 focus:ring-amber-500/50
|
||||
${this.transferTarget === m.publicId
|
||||
? "bg-amber-500/10 border-amber-500/20"
|
||||
: "bg-white/5 border-white/10 hover:bg-white/10"}"
|
||||
aria-selected=${this.transferTarget === m.publicId}
|
||||
>
|
||||
<div
|
||||
class="w-8 h-8 rounded-full bg-white/10 flex items-center justify-center text-white/50 text-xs font-bold shrink-0"
|
||||
>
|
||||
${renderRoleIcon(m.role)}
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<copy-button
|
||||
compact
|
||||
.copyText=${m.publicId}
|
||||
.displayText=${m.publicId}
|
||||
.showVisibilityToggle=${false}
|
||||
.showCopyIcon=${false}
|
||||
></copy-button>
|
||||
</div>
|
||||
<span
|
||||
class="text-[10px] font-bold uppercase tracking-wider px-2 py-0.5 rounded-full shrink-0
|
||||
${m.role === "officer"
|
||||
? "bg-purple-500/20 text-purple-400 border border-purple-500/30"
|
||||
: "bg-white/10 text-white/40 border border-white/10"}"
|
||||
>
|
||||
${translateClanRole(m.role)}
|
||||
</span>
|
||||
${this.transferTarget === m.publicId
|
||||
? html`<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-5 h-5 text-amber-400 shrink-0"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>`
|
||||
: ""}
|
||||
</button>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
|
||||
${totalMemberPages > 1
|
||||
? renderServerPagination(this.memberPage, totalMemberPages, (p) =>
|
||||
this.loadMembers(p),
|
||||
)
|
||||
: ""}
|
||||
|
||||
<button
|
||||
@click=${() => (this.confirmAction = "transfer")}
|
||||
class="w-full px-6 py-3 text-sm font-bold text-white uppercase tracking-wider rounded-xl transition-all border disabled:opacity-50 disabled:pointer-events-none
|
||||
${this.transferTarget && !this.actionPending
|
||||
? "bg-gradient-to-r from-amber-600 to-amber-700 hover:from-amber-500 hover:to-amber-600 shadow-lg hover:shadow-amber-900/40 border-white/5"
|
||||
: "bg-white/5 border-white/10 text-white/30 cursor-not-allowed"}"
|
||||
?disabled=${!this.transferTarget || this.actionPending}
|
||||
>
|
||||
${this.transferTarget
|
||||
? translateText("clan_modal.confirm_transfer", {
|
||||
name: this.transferTarget,
|
||||
})
|
||||
: translateText("clan_modal.select_new_leader")}
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private back() {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("navigate-back", { bubbles: true, composed: true }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user