refactor: payments providers
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
<script>
|
||||
import { ChevronRight, Eye, EyeOff, X } from "$components/ui/icons.js";
|
||||
import { ChevronRight, Eye, EyeOff, Search, X } from "$components/ui/icons.js";
|
||||
import * as UiIcons from "$components/ui/icons.js";
|
||||
import { Accordion, Switch } from "$components/ui/primitives.js";
|
||||
import Dialog from "$components/ui/dialog.svelte";
|
||||
import {
|
||||
AdminBadge,
|
||||
AdminButton,
|
||||
@@ -22,10 +24,18 @@
|
||||
let settingsOpenSections = [];
|
||||
let settingsOpenSubsections = {};
|
||||
let revealedSecrets = new Set();
|
||||
let iconPickerField = null;
|
||||
let iconPickerSearch = "";
|
||||
|
||||
$: settingsAllOpen =
|
||||
visibleSettingsSections.length > 0 &&
|
||||
settingsOpenSections.length === visibleSettingsSections.length;
|
||||
$: iconOptions = Object.keys(UiIcons)
|
||||
.filter((name) => /^[A-Z]/.test(name))
|
||||
.sort((a, b) => a.localeCompare(b));
|
||||
$: filteredIconOptions = iconOptions.filter((name) =>
|
||||
name.toLowerCase().includes(iconPickerSearch.trim().toLowerCase())
|
||||
);
|
||||
|
||||
onMount(() => {
|
||||
settingsStore.loadSettings().then(() => {
|
||||
@@ -75,6 +85,27 @@
|
||||
return field.placeholder || at("settings_secret_empty", {}, "Not set");
|
||||
}
|
||||
|
||||
function iconComponent(name) {
|
||||
const key = String(name || "").trim();
|
||||
return key ? UiIcons[key] || null : null;
|
||||
}
|
||||
|
||||
function openIconPicker(field) {
|
||||
iconPickerField = field;
|
||||
iconPickerSearch = "";
|
||||
}
|
||||
|
||||
function closeIconPicker() {
|
||||
iconPickerField = null;
|
||||
iconPickerSearch = "";
|
||||
}
|
||||
|
||||
function selectIcon(name) {
|
||||
if (!iconPickerField) return;
|
||||
settingsStore.markDirty(iconPickerField.key, name);
|
||||
closeIconPicker();
|
||||
}
|
||||
|
||||
function groupSectionFields(section) {
|
||||
const groups = new Map();
|
||||
for (const field of section.fields || []) {
|
||||
@@ -171,14 +202,37 @@
|
||||
class="admin-color"
|
||||
type="color"
|
||||
value={valueFor(field) || "#00fe7a"}
|
||||
on:input={(e) => settingsStore.markDirty(field.key, e.currentTarget.value)}
|
||||
oninput={(e) => settingsStore.markDirty(field.key, e.currentTarget.value)}
|
||||
/>
|
||||
<input
|
||||
class="input"
|
||||
type="text"
|
||||
value={valueFor(field) || ""}
|
||||
on:input={(e) => settingsStore.markDirty(field.key, e.currentTarget.value)}
|
||||
oninput={(e) => settingsStore.markDirty(field.key, e.currentTarget.value)}
|
||||
/>
|
||||
{:else if field.type === "icon"}
|
||||
{@const selectedIconName = valueFor(field) || ""}
|
||||
{@const SelectedIcon = iconComponent(selectedIconName)}
|
||||
<AdminButton
|
||||
class="admin-icon-picker-trigger"
|
||||
variant="ghost"
|
||||
onclick={() => openIconPicker(field)}
|
||||
>
|
||||
{#if SelectedIcon}
|
||||
<svelte:component this={SelectedIcon} size={16} />
|
||||
{/if}
|
||||
<span>{selectedIconName || at("settings_icon_empty", {}, "Default icon")}</span>
|
||||
</AdminButton>
|
||||
{#if selectedIconName}
|
||||
<AdminButton
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onclick={() => settingsStore.markDirty(field.key, "")}
|
||||
>
|
||||
<X size={12} />
|
||||
{at("clear", {}, "Clear")}
|
||||
</AdminButton>
|
||||
{/if}
|
||||
{:else if field.choices && field.choices.length > 0}
|
||||
<AdminSelect
|
||||
class="admin-setting-select"
|
||||
@@ -195,7 +249,7 @@
|
||||
step={field.type === "float" ? "0.1" : "1"}
|
||||
placeholder={field.placeholder}
|
||||
value={valueFor(field) ?? ""}
|
||||
on:input={(e) => settingsStore.markDirty(field.key, e.currentTarget.value)}
|
||||
oninput={(e) => settingsStore.markDirty(field.key, e.currentTarget.value)}
|
||||
/>
|
||||
{:else if field.secret}
|
||||
<input
|
||||
@@ -204,7 +258,7 @@
|
||||
placeholder={secretPlaceholder(field)}
|
||||
autocomplete="off"
|
||||
value={valueFor(field) ?? ""}
|
||||
on:input={(e) => settingsStore.markDirty(field.key, e.currentTarget.value)}
|
||||
oninput={(e) => settingsStore.markDirty(field.key, e.currentTarget.value)}
|
||||
/>
|
||||
<AdminButton
|
||||
size="sm"
|
||||
@@ -220,7 +274,7 @@
|
||||
type="text"
|
||||
placeholder={field.placeholder}
|
||||
value={valueFor(field) ?? ""}
|
||||
on:input={(e) => settingsStore.markDirty(field.key, e.currentTarget.value)}
|
||||
oninput={(e) => settingsStore.markDirty(field.key, e.currentTarget.value)}
|
||||
/>
|
||||
{/if}
|
||||
{#if isOverridden(field) || settingsDirty[field.key]}
|
||||
@@ -360,3 +414,40 @@
|
||||
{/each}
|
||||
</Accordion.Root>
|
||||
{/if}
|
||||
|
||||
<Dialog
|
||||
open={Boolean(iconPickerField)}
|
||||
title={at("settings_icon_picker_title", {}, "Choose icon")}
|
||||
description={iconPickerField ? fieldLabelText(iconPickerField) : ""}
|
||||
closeLabel={at("close", {}, "Close")}
|
||||
onclose={closeIconPicker}
|
||||
class="admin-icon-picker-dialog"
|
||||
>
|
||||
<div class="admin-icon-picker-body">
|
||||
<label class="admin-icon-picker-search">
|
||||
<Search size={15} />
|
||||
<input
|
||||
bind:value={iconPickerSearch}
|
||||
class="input"
|
||||
type="text"
|
||||
placeholder={at("search", {}, "Search")}
|
||||
/>
|
||||
</label>
|
||||
<div class="admin-icon-picker-grid">
|
||||
{#each filteredIconOptions as iconName}
|
||||
{@const Icon = iconComponent(iconName)}
|
||||
<button
|
||||
class:active={iconPickerField && valueFor(iconPickerField) === iconName}
|
||||
class="admin-icon-picker-option"
|
||||
type="button"
|
||||
onclick={() => selectIcon(iconName)}
|
||||
>
|
||||
{#if Icon}
|
||||
<svelte:component this={Icon} size={18} />
|
||||
{/if}
|
||||
<span>{iconName}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
@@ -1,33 +1,24 @@
|
||||
<script>
|
||||
import { Bitcoin, CreditCard } from "$components/ui/icons.js";
|
||||
import * as Icons from "$components/ui/icons.js";
|
||||
|
||||
export let methods = [];
|
||||
export let selectedMethod = "";
|
||||
export let t = (key) => key;
|
||||
export let onSelect = () => {};
|
||||
|
||||
function methodMeta(method) {
|
||||
const id = String(method?.id || "").toLowerCase();
|
||||
if (id.includes("platega_sbp"))
|
||||
return { title: t("wa_method_platega_sbp_card"), icon: CreditCard };
|
||||
if (id.includes("platega_crypto"))
|
||||
return { title: t("wa_method_platega_crypto"), icon: Bitcoin };
|
||||
if (id.includes("yookassa") || id.includes("card"))
|
||||
return { title: t("pay_with_yookassa_button"), icon: null };
|
||||
if (id.includes("severpay")) return { title: t("pay_with_severpay_button"), icon: null };
|
||||
if (id.includes("wata")) return { title: t("pay_with_wata_button"), icon: null };
|
||||
if (id.includes("freekassa")) return { title: t("pay_with_sbp_button"), icon: null };
|
||||
if (id.includes("cryptopay") || id.includes("crypto"))
|
||||
return { title: t("pay_with_cryptopay_button"), icon: null };
|
||||
if (id.includes("stars")) return { title: t("pay_with_stars_button"), icon: null };
|
||||
if (id.includes("sbp")) return { title: t("pay_with_sbp_button"), icon: null };
|
||||
return { title: t("wa_method_other_title"), icon: null };
|
||||
function methodTitle(method) {
|
||||
return method?.name || t("wa_method_other_title");
|
||||
}
|
||||
|
||||
function methodIcon(method) {
|
||||
const iconName = String(method?.icon || "").trim();
|
||||
return iconName ? Icons[iconName] || null : null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="method-grid">
|
||||
{#each methods as method}
|
||||
{@const meta = methodMeta(method)}
|
||||
{@const icon = methodIcon(method)}
|
||||
<button
|
||||
class:active={selectedMethod === method.id}
|
||||
class="method-card"
|
||||
@@ -35,10 +26,10 @@
|
||||
onclick={() => onSelect(method.id)}
|
||||
>
|
||||
<span class="method-card-main">
|
||||
{#if meta.icon}
|
||||
<svelte:component this={meta.icon} size={19} />
|
||||
{#if icon}
|
||||
<svelte:component this={icon} size={19} />
|
||||
{/if}
|
||||
<strong>{meta.title}</strong>
|
||||
<strong>{methodTitle(method)}</strong>
|
||||
</span>
|
||||
</button>
|
||||
{/each}
|
||||
|
||||
@@ -42,6 +42,7 @@ export {
|
||||
RefreshCw,
|
||||
Repeat2,
|
||||
Save,
|
||||
Search,
|
||||
Send,
|
||||
Server,
|
||||
Settings,
|
||||
|
||||
@@ -162,10 +162,10 @@ export const DEV_MOCK = {
|
||||
{ months: 12, price: 2690, currency: "RUB", title: "12 месяцев" },
|
||||
],
|
||||
payment_methods: [
|
||||
{ id: "yookassa", name: "Карта" },
|
||||
{ id: "platega_sbp", name: "Telegram Pay" },
|
||||
{ id: "cryptopay", name: "Криптовалюта" },
|
||||
{ id: "freekassa", name: "Другие способы" },
|
||||
{ id: "yookassa", name: "Карта", icon: "CreditCard" },
|
||||
{ id: "platega_sbp", name: "Telegram Pay", icon: "CreditCard" },
|
||||
{ id: "cryptopay", name: "Криптовалюта", icon: "Bitcoin" },
|
||||
{ id: "freekassa", name: "Другие способы", icon: "Smartphone" },
|
||||
],
|
||||
referral: {
|
||||
code: "ABCD1234",
|
||||
|
||||
@@ -1880,6 +1880,81 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.admin-icon-picker-trigger {
|
||||
min-width: 180px;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.admin-icon-picker-trigger span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.admin-icon-picker-dialog {
|
||||
width: min(100%, 720px);
|
||||
}
|
||||
|
||||
.admin-icon-picker-body {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.admin-icon-picker-search {
|
||||
position: relative;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.admin-icon-picker-search > svg {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--admin-muted);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.admin-icon-picker-search .input {
|
||||
width: 100%;
|
||||
padding-left: 36px;
|
||||
}
|
||||
|
||||
.admin-icon-picker-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(128px, 1fr));
|
||||
gap: 8px;
|
||||
max-height: min(52vh, 460px);
|
||||
overflow-y: auto;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.admin-icon-picker-option {
|
||||
display: grid;
|
||||
grid-template-columns: 20px minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-height: 42px;
|
||||
border: 1px solid var(--admin-border);
|
||||
border-radius: 8px;
|
||||
background: var(--surface-muted);
|
||||
color: var(--admin-text);
|
||||
padding: 9px 10px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.admin-icon-picker-option span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.admin-icon-picker-option:hover,
|
||||
.admin-icon-picker-option.active {
|
||||
border-color: color-mix(in srgb, var(--accent) 45%, var(--admin-border));
|
||||
background: color-mix(in srgb, var(--accent) 12%, var(--surface-muted));
|
||||
}
|
||||
|
||||
.admin-setting-control .admin-color {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
|
||||
Reference in New Issue
Block a user