mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-06-21 05:10:41 +00:00
Add new payment logo SVGs for Discover, JCB, Maestro, Mastercard, UnionPay, and Visa
- Added discover.svg for Discover card logo. - Added jcb.svg for JCB card logo. - Added maestro.svg for Maestro card logo. - Added mastercard.svg for Mastercard logo. - Added unionpay.svg for UnionPay logo. - Added visa.svg for Visa card logo.
This commit is contained in:
@@ -9,6 +9,7 @@ import {
|
||||
MOBILE_LAYOUT_QUERY,
|
||||
VAULT_LIST_OVERSCAN,
|
||||
VAULT_LIST_ROW_HEIGHT,
|
||||
cardListSubtitle,
|
||||
FOLDER_SORT_STORAGE_KEY,
|
||||
VAULT_SORT_STORAGE_KEY,
|
||||
cipherTypeKey,
|
||||
@@ -501,6 +502,9 @@ const folderName = useCallback((id: string | null | undefined): string => {
|
||||
if (Number(cipher.type || 1) === 1) {
|
||||
return cipher.login?.decUsername || cipherMetaById.get(cipher.id)?.firstUri || '';
|
||||
}
|
||||
if (Number(cipher.type || 1) === 3) {
|
||||
return cardListSubtitle(cipher);
|
||||
}
|
||||
return cipherTypeLabel(Number(cipher.type || 1));
|
||||
}, [cipherMetaById]);
|
||||
|
||||
|
||||
@@ -5,10 +5,12 @@ import { useDialogLifecycle } from '@/components/ConfirmDialog';
|
||||
import type { Cipher } from '@/lib/types';
|
||||
import { t } from '@/lib/i18n';
|
||||
import {
|
||||
CardBrandIcon,
|
||||
TOTP_PERIOD_SECONDS,
|
||||
TOTP_RING_CIRCUMFERENCE,
|
||||
VaultListIcon,
|
||||
copyToClipboard,
|
||||
displayCardBrand,
|
||||
formatAttachmentSize,
|
||||
formatHistoryTime,
|
||||
formatTotp,
|
||||
@@ -246,7 +248,13 @@ export default function VaultDetailView(props: VaultDetailViewProps) {
|
||||
<h4>{t('txt_card_details')}</h4>
|
||||
<div className="kv-line"><span>{t('txt_cardholder_name')}</span><strong>{props.selectedCipher.card.decCardholderName || ''}</strong></div>
|
||||
<div className="kv-line"><span>{t('txt_number')}</span><strong>{props.selectedCipher.card.decNumber || ''}</strong></div>
|
||||
<div className="kv-line"><span>{t('txt_brand')}</span><strong>{props.selectedCipher.card.decBrand || ''}</strong></div>
|
||||
<div className="kv-line">
|
||||
<span>{t('txt_brand')}</span>
|
||||
<strong className="card-brand-detail">
|
||||
<CardBrandIcon brand={props.selectedCipher.card.decBrand} />
|
||||
{displayCardBrand(props.selectedCipher.card.decBrand)}
|
||||
</strong>
|
||||
</div>
|
||||
<div className="kv-line"><span>{t('txt_expiry')}</span><strong>{`${props.selectedCipher.card.decExpMonth || ''}/${props.selectedCipher.card.decExpYear || ''}`}</strong></div>
|
||||
<div className="kv-line"><span>{t('txt_security_code')}</span><strong>{props.selectedCipher.card.decCode || ''}</strong></div>
|
||||
</div>
|
||||
|
||||
@@ -5,13 +5,17 @@ import { useEffect, useRef, useState } from 'preact/hooks';
|
||||
import { useDialogLifecycle } from '@/components/ConfirmDialog';
|
||||
import type { Cipher, Folder, VaultDraft, VaultDraftField } from '@/lib/types';
|
||||
import { t } from '@/lib/i18n';
|
||||
import { cardBrand } from '@/lib/import-format-shared';
|
||||
import {
|
||||
CARD_BRAND_OPTIONS,
|
||||
CardBrandIcon,
|
||||
cipherTypeLabel,
|
||||
createEmptyLoginUri,
|
||||
formatAttachmentSize,
|
||||
formatHistoryTime,
|
||||
getCreateTypeOptions,
|
||||
getWebsiteMatchOptions,
|
||||
normalizeCardBrand,
|
||||
toBooleanFieldValue,
|
||||
} from '@/components/vault/vault-page-helpers';
|
||||
|
||||
@@ -126,6 +130,10 @@ function WebsiteRow(props: WebsiteRowProps) {
|
||||
|
||||
export default function VaultEditor(props: VaultEditorProps) {
|
||||
const createTypeOptions = getCreateTypeOptions();
|
||||
const normalizedDraftCardBrand = normalizeCardBrand(props.draft.cardBrand);
|
||||
const cardBrandOptions = normalizedDraftCardBrand && !CARD_BRAND_OPTIONS.includes(normalizedDraftCardBrand as any)
|
||||
? [...CARD_BRAND_OPTIONS, normalizedDraftCardBrand]
|
||||
: CARD_BRAND_OPTIONS;
|
||||
const totpQrVideoRef = useRef<HTMLVideoElement | null>(null);
|
||||
const totpQrFileRef = useRef<HTMLInputElement | null>(null);
|
||||
const totpQrStreamRef = useRef<MediaStream | null>(null);
|
||||
@@ -435,8 +443,37 @@ export default function VaultEditor(props: VaultEditorProps) {
|
||||
<h4>{t('txt_card_details')}</h4>
|
||||
<div className="field-grid">
|
||||
<label className="field"><span>{t('txt_cardholder_name')}</span><input className="input" value={props.draft.cardholderName} onInput={(e) => props.onUpdateDraft({ cardholderName: (e.currentTarget as HTMLInputElement).value })} /></label>
|
||||
<label className="field"><span>{t('txt_number')}</span><input className="input" value={props.draft.cardNumber} onInput={(e) => props.onUpdateDraft({ cardNumber: (e.currentTarget as HTMLInputElement).value })} /></label>
|
||||
<label className="field"><span>{t('txt_brand')}</span><input className="input" value={props.draft.cardBrand} onInput={(e) => props.onUpdateDraft({ cardBrand: (e.currentTarget as HTMLInputElement).value })} /></label>
|
||||
<label className="field">
|
||||
<span>{t('txt_number')}</span>
|
||||
<input
|
||||
className="input"
|
||||
value={props.draft.cardNumber}
|
||||
onInput={(e) => {
|
||||
const value = (e.currentTarget as HTMLInputElement).value;
|
||||
const detectedBrand = normalizeCardBrand(cardBrand(value) || '');
|
||||
props.onUpdateDraft({
|
||||
cardNumber: value,
|
||||
...(props.draft.cardBrand ? {} : { cardBrand: detectedBrand }),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<label className="field">
|
||||
<span>{t('txt_brand')}</span>
|
||||
<div className="card-brand-select-row">
|
||||
<CardBrandIcon brand={normalizedDraftCardBrand} />
|
||||
<select
|
||||
className="input card-brand-select"
|
||||
value={normalizedDraftCardBrand}
|
||||
onInput={(e) => props.onUpdateDraft({ cardBrand: (e.currentTarget as HTMLSelectElement).value })}
|
||||
>
|
||||
<option value="">{t('txt_select')}</option>
|
||||
{cardBrandOptions.map((brand) => (
|
||||
<option key={brand} value={brand}>{brand}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</label>
|
||||
<label className="field"><span>{t('txt_security_code_cvv')}</span><input className="input" value={props.draft.cardCode} onInput={(e) => props.onUpdateDraft({ cardCode: (e.currentTarget as HTMLInputElement).value })} /></label>
|
||||
<label className="field"><span>{t('txt_expiry_month')}</span><input className="input" value={props.draft.cardExpMonth} onInput={(e) => props.onUpdateDraft({ cardExpMonth: (e.currentTarget as HTMLInputElement).value })} /></label>
|
||||
<label className="field"><span>{t('txt_expiry_year')}</span><input className="input" value={props.draft.cardExpYear} onInput={(e) => props.onUpdateDraft({ cardExpYear: (e.currentTarget as HTMLInputElement).value })} /></label>
|
||||
|
||||
@@ -92,7 +92,7 @@ const CipherListItem = memo(function CipherListItem(props: CipherListItemProps)
|
||||
onInput={(e) => props.onToggleSelected(props.cipher.id, (e.currentTarget as HTMLInputElement).checked)}
|
||||
/>
|
||||
<button type="button" className="row-main" onClick={() => props.onSelectCipher(props.cipher.id)}>
|
||||
<div className="list-icon-wrap">
|
||||
<div className={`list-icon-wrap ${Number(props.cipher.type || 1) === 3 ? 'card-list-icon-wrap' : ''}`}>
|
||||
<VaultListIcon cipher={props.cipher} />
|
||||
</div>
|
||||
<div className="list-text">
|
||||
|
||||
@@ -28,6 +28,89 @@ interface TypeOption {
|
||||
label: string;
|
||||
}
|
||||
|
||||
export const CARD_BRAND_OPTIONS = [
|
||||
'Visa',
|
||||
'Mastercard',
|
||||
'American Express',
|
||||
'Discover',
|
||||
'Diners Club',
|
||||
'JCB',
|
||||
'Maestro',
|
||||
'UnionPay',
|
||||
'RuPay',
|
||||
] as const;
|
||||
|
||||
type CardBrand = typeof CARD_BRAND_OPTIONS[number];
|
||||
|
||||
const CARD_BRAND_ALIASES: Record<string, CardBrand> = {
|
||||
amex: 'American Express',
|
||||
'american express': 'American Express',
|
||||
americanexpress: 'American Express',
|
||||
discover: 'Discover',
|
||||
diners: 'Diners Club',
|
||||
'diners club': 'Diners Club',
|
||||
dinersclub: 'Diners Club',
|
||||
jcb: 'JCB',
|
||||
maestro: 'Maestro',
|
||||
mastercard: 'Mastercard',
|
||||
master: 'Mastercard',
|
||||
rupay: 'RuPay',
|
||||
unionpay: 'UnionPay',
|
||||
'union pay': 'UnionPay',
|
||||
visa: 'Visa',
|
||||
};
|
||||
|
||||
const CARD_BRAND_LOGO_SLUGS: Partial<Record<CardBrand, string>> = {
|
||||
'American Express': 'american-express',
|
||||
'Diners Club': 'diners',
|
||||
Discover: 'discover',
|
||||
JCB: 'jcb',
|
||||
Maestro: 'maestro',
|
||||
Mastercard: 'mastercard',
|
||||
UnionPay: 'unionpay',
|
||||
Visa: 'visa',
|
||||
};
|
||||
|
||||
export function normalizeCardBrand(value: string | null | undefined): string {
|
||||
const normalized = String(value || '').trim();
|
||||
if (!normalized) return '';
|
||||
return CARD_BRAND_ALIASES[normalized.toLowerCase().replace(/\s+/g, ' ')] || normalized;
|
||||
}
|
||||
|
||||
export function displayCardBrand(value: string | null | undefined): string {
|
||||
return normalizeCardBrand(value);
|
||||
}
|
||||
|
||||
export function cardLast4(value: string | null | undefined): string {
|
||||
const digits = String(value || '').replace(/\D/g, '');
|
||||
return digits.length >= 4 ? digits.slice(-4) : '';
|
||||
}
|
||||
|
||||
export function cardListSubtitle(cipher: Cipher): string {
|
||||
const brand = displayCardBrand(cipher.card?.decBrand ?? cipher.card?.brand);
|
||||
const last4 = cardLast4(cipher.card?.decNumber ?? cipher.card?.number);
|
||||
if (brand && last4) return `${brand}, *${last4}`;
|
||||
if (brand) return brand;
|
||||
if (last4) return `*${last4}`;
|
||||
return cipherTypeLabel(3);
|
||||
}
|
||||
|
||||
export function CardBrandIcon({ brand }: { brand?: string | null }) {
|
||||
const display = displayCardBrand(brand);
|
||||
const key = display.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'generic';
|
||||
const label = display || t('txt_card');
|
||||
const logoSlug = CARD_BRAND_LOGO_SLUGS[display as CardBrand];
|
||||
return (
|
||||
<span className={`card-brand-icon card-brand-${key}`} aria-label={label} title={label}>
|
||||
{logoSlug ? (
|
||||
<img src={`/payment-logos/cards/${logoSlug}.svg`} alt="" loading="lazy" decoding="async" />
|
||||
) : (
|
||||
<CreditCard size={18} />
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export function getCreateTypeOptions(): TypeOption[] {
|
||||
return [
|
||||
{ type: 1, label: t('txt_login') },
|
||||
@@ -323,7 +406,7 @@ export function draftFromCipher(cipher: Cipher): VaultDraft {
|
||||
if (cipher.card) {
|
||||
draft.cardholderName = cipher.card.decCardholderName || '';
|
||||
draft.cardNumber = cipher.card.decNumber || '';
|
||||
draft.cardBrand = cipher.card.decBrand || '';
|
||||
draft.cardBrand = normalizeCardBrand(cipher.card.decBrand || '');
|
||||
draft.cardExpMonth = cipher.card.decExpMonth || '';
|
||||
draft.cardExpYear = cipher.card.decExpYear || '';
|
||||
draft.cardCode = cipher.card.decCode || '';
|
||||
@@ -425,6 +508,9 @@ export function firstPasskeyCreationTime(cipher: Cipher | null): string | null {
|
||||
}
|
||||
|
||||
export function VaultListIcon({ cipher }: { cipher: Cipher }) {
|
||||
if (Number(cipher.type || 1) === 3) {
|
||||
return <CardBrandIcon brand={cipher.card?.decBrand ?? cipher.card?.brand} />;
|
||||
}
|
||||
return <WebsiteIcon cipher={cipher} fallback={<TypeIcon type={Number(cipher.type || 1)} />} />;
|
||||
}
|
||||
|
||||
|
||||
@@ -710,6 +710,7 @@ const en: Record<string, string> = {
|
||||
"txt_security_code": "Security Code",
|
||||
"txt_security_code_cvv": "Security Code (CVV)",
|
||||
"txt_select_all": "Select All",
|
||||
"txt_select": "Select",
|
||||
"txt_select_duplicate_items": "Select Duplicates",
|
||||
"txt_select_an_item": "Select an item",
|
||||
"txt_send_created": "Send created",
|
||||
|
||||
@@ -710,6 +710,7 @@ const es: Record<string, string> = {
|
||||
"txt_security_code": "Código de seguridad",
|
||||
"txt_security_code_cvv": "Código de seguridad (CVV)",
|
||||
"txt_select_all": "Seleccionar todo",
|
||||
"txt_select": "Seleccionar",
|
||||
"txt_select_duplicate_items": "Seleccionar duplicados",
|
||||
"txt_select_an_item": "Seleccione un elemento",
|
||||
"txt_send_created": "Envío creado",
|
||||
|
||||
@@ -710,6 +710,7 @@ const ru: Record<string, string> = {
|
||||
"txt_security_code": "Код безопасности",
|
||||
"txt_security_code_cvv": "Код безопасности (CVV)",
|
||||
"txt_select_all": "Выбрать все",
|
||||
"txt_select": "Выбрать",
|
||||
"txt_select_duplicate_items": "Выберите дубликаты",
|
||||
"txt_select_an_item": "Выберите элемент",
|
||||
"txt_send_created": "Отправить создано",
|
||||
|
||||
@@ -602,7 +602,7 @@ const zhCN: Record<string, string> = {
|
||||
"txt_note": "笔记",
|
||||
"txt_notes": "备注",
|
||||
"txt_replace_device_name_with_note": "为这台设备设置自定义名称,不会改变系统识别到的设备类型。",
|
||||
"txt_number": "数字",
|
||||
"txt_number": "号码",
|
||||
"txt_open": "打开",
|
||||
"txt_opera_browser": "Opera 浏览器",
|
||||
"txt_opera_extension": "Opera 扩展",
|
||||
@@ -710,6 +710,7 @@ const zhCN: Record<string, string> = {
|
||||
"txt_security_code": "安全码",
|
||||
"txt_security_code_cvv": "安全码 (CVV)",
|
||||
"txt_select_all": "全选",
|
||||
"txt_select": "请选择",
|
||||
"txt_select_duplicate_items": "选择重复项",
|
||||
"txt_select_an_item": "请选择一个项目",
|
||||
"txt_send_created": "Send 已创建",
|
||||
|
||||
@@ -602,7 +602,7 @@ const zhTW: Record<string, string> = {
|
||||
"txt_note": "筆記",
|
||||
"txt_notes": "備註",
|
||||
"txt_replace_device_name_with_note": "為這臺設備設置自定義名稱,不會改變系統識別到的設備類型。",
|
||||
"txt_number": "數字",
|
||||
"txt_number": "號碼",
|
||||
"txt_open": "打開",
|
||||
"txt_opera_browser": "Opera 瀏覽器",
|
||||
"txt_opera_extension": "Opera 擴展",
|
||||
@@ -710,6 +710,7 @@ const zhTW: Record<string, string> = {
|
||||
"txt_security_code": "安全碼",
|
||||
"txt_security_code_cvv": "安全碼 (CVV)",
|
||||
"txt_select_all": "全選",
|
||||
"txt_select": "請選擇",
|
||||
"txt_select_duplicate_items": "選擇重複項",
|
||||
"txt_select_an_item": "請選擇一個項目",
|
||||
"txt_send_created": "Send 已創建",
|
||||
|
||||
@@ -198,6 +198,27 @@
|
||||
box-shadow: 0 10px 28px rgba(0, 0, 0, 0.24), 0 0 0 1px rgba(139, 184, 255, 0.12);
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .card-brand-icon {
|
||||
color: #bfdbfe;
|
||||
background: linear-gradient(180deg, #1f2937 0%, #111827 100%);
|
||||
border-color: color-mix(in srgb, var(--primary) 30%, var(--line));
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .card-brand-icon:has(img) {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .card-brand-american-express {
|
||||
color: #fff;
|
||||
background: #2563eb;
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .card-brand-mastercard,
|
||||
:root[data-theme='dark'] .card-brand-maestro,
|
||||
:root[data-theme='dark'] .card-brand-discover {
|
||||
color: #f8fafc;
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .mobile-sidebar-sheet,
|
||||
:root[data-theme='dark'] .mobile-sidebar-close,
|
||||
:root[data-theme='dark'] .nav-layout-menu,
|
||||
|
||||
@@ -379,6 +379,101 @@
|
||||
transition: transform 240ms var(--ease-out-soft), filter 240ms var(--ease-out-soft);
|
||||
}
|
||||
|
||||
.card-list-icon-wrap {
|
||||
@apply w-9;
|
||||
}
|
||||
|
||||
.card-brand-icon {
|
||||
@apply inline-grid h-6 w-9 shrink-0 place-items-center rounded border text-[7px] font-black uppercase leading-none;
|
||||
letter-spacing: 0;
|
||||
color: #1e3a8a;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #f4f7fb 100%);
|
||||
border-color: rgba(96, 125, 169, 0.34);
|
||||
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
.card-brand-icon span {
|
||||
@apply block max-w-full overflow-hidden text-ellipsis whitespace-nowrap px-0.5;
|
||||
}
|
||||
|
||||
.card-brand-icon img {
|
||||
@apply block h-full w-full object-contain;
|
||||
}
|
||||
|
||||
.card-brand-icon svg {
|
||||
@apply h-[18px] w-[18px];
|
||||
}
|
||||
|
||||
.card-brand-icon:has(img) {
|
||||
color: inherit;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.card-brand-visa {
|
||||
color: #1a4db3;
|
||||
}
|
||||
|
||||
.card-brand-mastercard {
|
||||
color: #111827;
|
||||
background:
|
||||
radial-gradient(circle at 38% 50%, rgba(235, 0, 27, 0.88) 0 24%, transparent 25%),
|
||||
radial-gradient(circle at 62% 50%, rgba(247, 158, 27, 0.88) 0 24%, transparent 25%),
|
||||
#fff;
|
||||
}
|
||||
|
||||
.card-brand-american-express {
|
||||
color: #fff;
|
||||
background: #2e77bb;
|
||||
border-color: rgba(46, 119, 187, 0.45);
|
||||
}
|
||||
|
||||
.card-brand-discover {
|
||||
color: #172554;
|
||||
background:
|
||||
radial-gradient(circle at 72% 50%, rgba(245, 130, 32, 0.9) 0 26%, transparent 27%),
|
||||
#fff;
|
||||
}
|
||||
|
||||
.card-brand-diners-club {
|
||||
color: #0f5fa8;
|
||||
}
|
||||
|
||||
.card-brand-jcb {
|
||||
color: #0f766e;
|
||||
}
|
||||
|
||||
.card-brand-maestro {
|
||||
color: #1e40af;
|
||||
background:
|
||||
radial-gradient(circle at 38% 50%, rgba(0, 85, 164, 0.86) 0 24%, transparent 25%),
|
||||
radial-gradient(circle at 62% 50%, rgba(237, 28, 36, 0.82) 0 24%, transparent 25%),
|
||||
#fff;
|
||||
}
|
||||
|
||||
.card-brand-unionpay {
|
||||
color: #0f766e;
|
||||
}
|
||||
|
||||
.card-brand-rupay {
|
||||
color: #1e3a8a;
|
||||
}
|
||||
|
||||
.card-brand-select-row {
|
||||
@apply flex items-center gap-2;
|
||||
}
|
||||
|
||||
.card-brand-select-row .card-brand-icon {
|
||||
@apply h-8 w-11;
|
||||
}
|
||||
|
||||
.card-brand-select {
|
||||
@apply min-w-0 flex-1;
|
||||
}
|
||||
|
||||
.card-brand-detail {
|
||||
@apply inline-flex min-w-0 items-center gap-2;
|
||||
}
|
||||
|
||||
.list-icon {
|
||||
@apply h-6 w-6 rounded-md;
|
||||
opacity: 0;
|
||||
|
||||
Reference in New Issue
Block a user