refactor: payments providers

This commit is contained in:
3252a8
2026-05-18 10:29:00 +03:00
parent fff7e90e14
commit 51c9c8b4f0
63 changed files with 7480 additions and 6843 deletions
@@ -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}
+1
View File
@@ -42,6 +42,7 @@ export {
RefreshCw,
Repeat2,
Save,
Search,
Send,
Server,
Settings,
+4 -4
View File
@@ -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",
+75
View File
@@ -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;