feat: marketplace saved searches, quick offers, safety

This commit is contained in:
SkateSpots 2026-10-29 10:27:02 +07:00
parent 68647716d3
commit 8b895a4858
28 changed files with 586 additions and 3 deletions

View file

@ -127,6 +127,26 @@ export function OfferDialog({
</p>
</div>
<div className="space-y-2">
<p className="text-[10px] font-black uppercase tracking-widest text-gray-500">
{t('marketplaceExtrasUi.offerTemplates')}
</p>
<div className="flex flex-wrap gap-1.5">
{[
t('marketplaceExtrasUi.offerDiscount'),
t('marketplaceExtrasUi.offerSwap'),
t('marketplaceExtrasUi.offerPickup'),
t('marketplaceExtrasUi.offerShipping'),
].map((template) => (
<button
key={template}
type="button"
onClick={() => setNote(template)}
className="min-h-0 rounded-full border border-orange-500/25 bg-orange-500/5 px-2.5 py-1 text-[11px] text-orange-200 hover:bg-orange-500/15"
>
{template}
</button>
))}
</div>
<Label htmlFor="offer-note" className="text-xs uppercase tracking-wide text-gray-400">
{t('marketUi.offer.messageLabel')}
</Label>

View file

@ -1373,6 +1373,22 @@ const batch1 = {
trust: "Confiança",
noResults: "Cap resultat per a «{{query}}».",
},
marketplaceExtrasUi: {
savedSearches: "Cerques desades",
saveSearch: "Desa la cerca",
searchName: "Posa nom a aquesta cerca…",
deleteSearch: "Elimina la cerca desada",
safetyTitle: "Compra i ven amb seguretat",
safetyMeet: "Trobeu-vos en un lloc públic i reviseu el material abans de pagar.",
safetyKeys: "No comparteixis mai frases seed ni claus privades. Mantén el tracte al xat de l'aplicació.",
safetyEscrow: "Per als enviaments, llegeix primer les condicions de l'escrow: l'escrow Cashu és només per a l'app nativa i no està verificat.",
safetyMore: "Llegeix les normes del mercat",
offerTemplates: "Notes ràpides",
offerDiscount: "Pots fer un 10 % de descompte?",
offerSwap: "Consideraries un intercanvi?",
offerPickup: "Puc recollir-ho aquesta setmana.",
offerShipping: "Pots agrupar l'enviament?",
},
};
export default batch1;

View file

@ -451,6 +451,22 @@ const drift = {
trust: "Důvěra",
noResults: "Žádné výsledky pro „{{query}}“.",
},
marketplaceExtrasUi: {
savedSearches: "Uložená hledání",
saveSearch: "Uložit hledání",
searchName: "Pojmenuj toto hledání…",
deleteSearch: "Smazat uložené hledání",
safetyTitle: "Nakupuj a prodávej bezpečně",
safetyMeet: "Sejděte se na veřejném místě a vybavení si před platbou prohlédněte.",
safetyKeys: "Nikdy nesdílej seed fráze ani soukromé klíče. Drž obchod v chatu v aplikaci.",
safetyEscrow: "U zásilek si nejdřív přečti podmínky escrow: Cashu escrow je jen pro nativní aplikaci a je neověřené.",
safetyMore: "Přečíst pravidla tržiště",
offerTemplates: "Rychlé poznámky",
offerDiscount: "Můžeš dát slevu 10 %?",
offerSwap: "Nepřemýšlel bys o výměně?",
offerPickup: "Tento týden si to můžu vyzvednout.",
offerShipping: "Můžeš spojit dopravu?",
},
};
export default drift;

View file

@ -453,6 +453,22 @@ const drift = {
trust: "Vertrauen",
noResults: "Keine Ergebnisse für „{{query}}“.",
},
marketplaceExtrasUi: {
savedSearches: "Gespeicherte Suchen",
saveSearch: "Suche speichern",
searchName: "Diese Suche benennen…",
deleteSearch: "Gespeicherte Suche löschen",
safetyTitle: "Sicher kaufen & verkaufen",
safetyMeet: "Trefft euch an einem öffentlichen Ort und prüft die Ausrüstung, bevor ihr bezahlt.",
safetyKeys: "Teile niemals Seed-Phrasen oder private Schlüssel. Wickle den Deal im In-App-Chat ab.",
safetyEscrow: "Bei Versandgeschäften lies zuerst die Escrow-Bedingungen: Cashu-Escrow gibt es nur in der nativen App, und sie ist nicht verifiziert.",
safetyMore: "Marktplatz-Richtlinien lesen",
offerTemplates: "Schnellnotizen",
offerDiscount: "Gehen 10 % Rabatt?",
offerSwap: "Würdest du über einen Tausch nachdenken?",
offerPickup: "Ich kann es diese Woche abholen.",
offerShipping: "Kannst du den Versand bündeln?",
},
};
export default drift;

View file

@ -455,6 +455,22 @@ const drift = {
trust: "Εμπιστοσύνη",
noResults: "Κανένα αποτέλεσμα για «{{query}}».",
},
marketplaceExtrasUi: {
savedSearches: "Αποθηκευμένες αναζητήσεις",
saveSearch: "Αποθήκευση αναζήτησης",
searchName: "Ονόμασε αυτή την αναζήτηση…",
deleteSearch: "Διαγραφή αποθηκευμένης αναζήτησης",
safetyTitle: "Αγόρασε και πούλα με ασφάλεια",
safetyMeet: "Συναντηθείτε σε δημόσιο σημείο και ελέγξτε τον εξοπλισμό πριν πληρώσετε.",
safetyKeys: "Ποτέ μη μοιράζεσαι seed phrases ή ιδιωτικά κλειδιά. Κράτα τη συναλλαγή στο chat της εφαρμογής.",
safetyEscrow: "Για αποστολές, διάβασε πρώτα τους όρους του escrow: το Cashu escrow είναι μόνο για την εγγενή εφαρμογή και δεν είναι επαληθευμένο.",
safetyMore: "Διάβασε τους κανόνες της αγοράς",
offerTemplates: "Γρήγορες σημειώσεις",
offerDiscount: "Μπορείς να κάνεις 10% έκπτωση;",
offerSwap: "Θα σκεφτόσουν μια ανταλλαγή;",
offerPickup: "Μπορώ να το παραλάβω αυτή την εβδομάδα.",
offerShipping: "Μπορείς να συνδυάσεις τα μεταφορικά;",
},
};
export default drift;

View file

@ -1373,6 +1373,22 @@ const batch1 = {
trust: "Konfiantza",
noResults: "Ez dago emaitzarik «{{query}}» bilaketarako.",
},
marketplaceExtrasUi: {
savedSearches: "Gordetako bilaketak",
saveSearch: "Gorde bilaketa",
searchName: "Eman izena bilaketa honi…",
deleteSearch: "Ezabatu gordetako bilaketa",
safetyTitle: "Erosi eta saldu segurtasunez",
safetyMeet: "Elkartu leku publiko batean eta aztertu materiala ordaindu aurretik.",
safetyKeys: "Inoiz ez partekatu seed esaldiak edo gako pribatuak. Egin tratuak aplikazioko txatean.",
safetyEscrow: "Bidalketetan, irakurri lehenik escrow baldintzak: Cashu escrow aplikazio natiboan soilik dago eta ez dago egiaztatuta.",
safetyMore: "Irakurri azokaren jarraibideak",
offerTemplates: "Ohar azkarrak",
offerDiscount: "%10eko deskontua egin dezakezu?",
offerSwap: "Trukea kontuan hartuko zenuke?",
offerPickup: "Aste honetan jaso dezaket.",
offerShipping: "Bidalketa batu dezakezu?",
},
};
export default batch1;

View file

@ -453,6 +453,22 @@ const drift = {
trust: "Confiance",
noResults: "Aucun résultat pour « {{query}} ».",
},
marketplaceExtrasUi: {
savedSearches: "Recherches enregistrées",
saveSearch: "Enregistrer la recherche",
searchName: "Nommer cette recherche…",
deleteSearch: "Supprimer la recherche enregistrée",
safetyTitle: "Acheter et vendre en toute sécurité",
safetyMeet: "Retrouvez-vous dans un lieu public et inspectez le matériel avant de payer.",
safetyKeys: "Ne partagez jamais de phrases de récupération ni de clés privées. Gardez la transaction dans le chat de l'application.",
safetyEscrow: "Pour les envois, lisez d'abord les conditions de l'escrow : l'escrow Cashu est réservé à l'application native et n'est pas vérifié.",
safetyMore: "Lire les règles du marché",
offerTemplates: "Notes rapides",
offerDiscount: "Peux-tu faire 10 % de remise ?",
offerSwap: "Envisagerais-tu un échange ?",
offerPickup: "Je peux venir le chercher cette semaine.",
offerShipping: "Peux-tu regrouper l'expédition ?",
},
};
export default drift;

View file

@ -1373,6 +1373,22 @@ const batch1 = {
trust: "Confianza",
noResults: "Sen resultados para «{{query}}».",
},
marketplaceExtrasUi: {
savedSearches: "Buscas gardadas",
saveSearch: "Gardar busca",
searchName: "Ponlle nome a esta busca…",
deleteSearch: "Eliminar busca gardada",
safetyTitle: "Compra e vende con seguridade",
safetyMeet: "Quedade nun lugar público e revisade o material antes de pagar.",
safetyKeys: "Nunca compartas frases seed nin claves privadas. Mantén o trato na chat da aplicación.",
safetyEscrow: "Para os envíos, le primeiro as condicións do escrow: o escrow Cashu é só para a app nativa e non está verificado.",
safetyMore: "Ler as normas do mercado",
offerTemplates: "Notas rápidas",
offerDiscount: "Podes facer un 10 % de desconto?",
offerSwap: "Considerarías un intercambio?",
offerPickup: "Podo recollerlo esta semana.",
offerShipping: "Podes agrupar o envío?",
},
};
export default batch1;

View file

@ -1373,6 +1373,22 @@ const batch1 = {
trust: "भरोसा",
noResults: "“{{query}}” के लिए कोई परिणाम नहीं।",
},
marketplaceExtrasUi: {
savedSearches: "सहेजी गई खोजें",
saveSearch: "खोज सहेजें",
searchName: "इस खोज को नाम दें…",
deleteSearch: "सहेजी गई खोज हटाएं",
safetyTitle: "सुरक्षित रूप से खरीदें और बेचें",
safetyMeet: "किसी सार्वजनिक जगह पर मिलें और भुगतान से पहले सामान जांच लें।",
safetyKeys: "सीड वाक्यांश या निजी कुंजियां कभी साझा न करें। सौदा ऐप की चैट में ही रखें।",
safetyEscrow: "शिपिंग वाले सौदों के लिए पहले एस्क्रो की शर्तें पढ़ें: Cashu एस्क्रो केवल नेटिव ऐप में है और सत्यापित नहीं है।",
safetyMore: "मार्केटप्लेस दिशानिर्देश पढ़ें",
offerTemplates: "झटपट नोट्स",
offerDiscount: "क्या आप 10% छूट दे सकते हैं?",
offerSwap: "क्या आप अदला-बदली पर विचार करेंगे?",
offerPickup: "मैं इसे इस हफ्ते खुद ले सकता/सकती हूं।",
offerShipping: "क्या आप शिपिंग जोड़ सकते हैं?",
},
};
export default batch1;

View file

@ -447,6 +447,22 @@ const driftStrings = {
trust: "Kepercayaan",
noResults: "Tidak ada hasil untuk “{{query}}”.",
},
marketplaceExtrasUi: {
savedSearches: "Pencarian tersimpan",
saveSearch: "Simpan pencarian",
searchName: "Beri nama pencarian ini…",
deleteSearch: "Hapus pencarian tersimpan",
safetyTitle: "Beli & jual dengan aman",
safetyMeet: "Bertemulah di tempat umum dan periksa perlengkapannya sebelum membayar.",
safetyKeys: "Jangan pernah membagikan frasa seed atau kunci privat. Lakukan transaksi di chat dalam aplikasi.",
safetyEscrow: "Untuk pengiriman, baca dulu ketentuan escrow: escrow Cashu hanya untuk aplikasi native dan belum terverifikasi.",
safetyMore: "Baca panduan marketplace",
offerTemplates: "Catatan cepat",
offerDiscount: "Bisa diskon 10%?",
offerSwap: "Mau mempertimbangkan tukar tambah?",
offerPickup: "Saya bisa ambil sendiri minggu ini.",
offerShipping: "Bisa gabung ongkos kirimnya?",
},
};
export default driftStrings;

View file

@ -453,6 +453,22 @@ const drift = {
trust: "Fiducia",
noResults: "Nessun risultato per «{{query}}».",
},
marketplaceExtrasUi: {
savedSearches: "Ricerche salvate",
saveSearch: "Salva ricerca",
searchName: "Dai un nome a questa ricerca…",
deleteSearch: "Elimina ricerca salvata",
safetyTitle: "Compra e vendi in sicurezza",
safetyMeet: "Incontratevi in un luogo pubblico e controllate l'attrezzatura prima di pagare.",
safetyKeys: "Non condividere mai frasi seed o chiavi private. Tieni lo scambio nella chat dell'app.",
safetyEscrow: "Per le spedizioni, leggi prima i termini dell'escrow: l'escrow Cashu è solo per l'app nativa e non è verificato.",
safetyMore: "Leggi le linee guida del marketplace",
offerTemplates: "Note rapide",
offerDiscount: "Puoi fare uno sconto del 10%?",
offerSwap: "Prenderesti in considerazione uno scambio?",
offerPickup: "Posso ritirarlo questa settimana.",
offerShipping: "Puoi raggruppare la spedizione?",
},
};
export default drift;

View file

@ -463,6 +463,22 @@ const drift: Dict = {
trust: "信頼",
noResults: "「{{query}}」の結果はありません。",
},
marketplaceExtrasUi: {
savedSearches: "保存した検索",
saveSearch: "検索を保存",
searchName: "この検索に名前を付ける…",
deleteSearch: "保存した検索を削除",
safetyTitle: "安全に売買する",
safetyMeet: "人目のある場所で待ち合わせ、支払う前にギアを確認しましょう。",
safetyKeys: "シードフレーズや秘密鍵は絶対に共有しないでください。取引はアプリ内チャットで進めましょう。",
safetyEscrow: "発送の取引では、まずエスクローの条件を読んでください。Cashu エスクローはネイティブアプリ専用で、未検証です。",
safetyMore: "マーケットプレイスのガイドラインを読む",
offerTemplates: "クイックメモ",
offerDiscount: "10% オフにできますか?",
offerSwap: "交換は検討できますか?",
offerPickup: "今週中に受け取れます。",
offerShipping: "送料をまとめられますか?",
},
};
export default drift;

View file

@ -463,6 +463,22 @@ const drift: Dict = {
trust: "신뢰",
noResults: "“{{query}}”에 대한 결과가 없습니다.",
},
marketplaceExtrasUi: {
savedSearches: "저장된 검색",
saveSearch: "검색 저장",
searchName: "이 검색 이름 지정…",
deleteSearch: "저장된 검색 삭제",
safetyTitle: "안전하게 사고팔기",
safetyMeet: "공공장소에서 만나 결제 전에 장비를 확인하세요.",
safetyKeys: "시드 문구나 개인 키를 절대 공유하지 마세요. 거래는 앱 내 채팅에서 진행하세요.",
safetyEscrow: "배송 거래라면 먼저 에스크로 약관을 읽으세요. Cashu 에스크로는 네이티브 앱 전용이며 검증되지 않았습니다.",
safetyMore: "마켓플레이스 가이드라인 읽기",
offerTemplates: "빠른 메모",
offerDiscount: "10% 할인해 주실 수 있나요?",
offerSwap: "교환도 고려해 보시겠어요?",
offerPickup: "이번 주에 직접 수령할 수 있어요.",
offerShipping: "배송비를 묶을 수 있나요?",
},
};
export default drift;

View file

@ -6,6 +6,7 @@ import notify from './notify';
import misc from './misc';
import cordnUi from './cordnUi';
import communityUi from './communityUi';
import marketplaceExtrasUi from './marketplaceExtrasUi';
import globalSearchUi from './globalSearchUi';
import signerPromptUi from './signerPromptUi';
import newsletterDraftUi from './newsletterDraftUi';
@ -24,6 +25,7 @@ export const legacyEn = {
legacyMiscUi: misc.en,
cordnUi: cordnUi.en,
communityUi: communityUi.en,
marketplaceExtrasUi: marketplaceExtrasUi.en,
globalSearchUi: globalSearchUi.en,
signerPromptUi: signerPromptUi.en,
newsletterDraftUi: newsletterDraftUi.en,
@ -38,6 +40,7 @@ export const legacyEs = {
legacyMiscUi: misc.es,
cordnUi: cordnUi.es,
communityUi: communityUi.es,
marketplaceExtrasUi: marketplaceExtrasUi.es,
globalSearchUi: globalSearchUi.es,
signerPromptUi: signerPromptUi.es,
newsletterDraftUi: newsletterDraftUi.es,

View file

@ -0,0 +1,35 @@
const marketplaceExtrasUi = {
en: {
savedSearches: 'Saved searches',
saveSearch: 'Save search',
searchName: 'Name this search…',
deleteSearch: 'Delete saved search',
safetyTitle: 'Buy & sell safely',
safetyMeet: 'Meet in a public spot and inspect the gear before paying.',
safetyKeys: 'Never share seed phrases or private keys. Keep the deal in the in-app chat.',
safetyEscrow: 'For shipped deals, read the escrow terms first: Cashu escrow is native-only and unverified.',
safetyMore: 'Read the marketplace guidelines',
offerTemplates: 'Quick notes',
offerDiscount: 'Can you do 10% off?',
offerSwap: 'Would you consider a swap?',
offerPickup: 'I can pick up this week.',
offerShipping: 'Can you bundle shipping?',
},
es: {
savedSearches: 'Búsquedas guardadas',
saveSearch: 'Guardar búsqueda',
searchName: 'Ponle nombre a esta búsqueda…',
deleteSearch: 'Eliminar búsqueda guardada',
safetyTitle: 'Compra y vende con seguridad',
safetyMeet: 'Quedad en un sitio público y revisa el material antes de pagar.',
safetyKeys: 'Nunca compartas frases semilla ni claves privadas. Mantén el trato en el chat de la app.',
safetyEscrow: 'Para envíos, lee primero las condiciones del depósito: el escrow Cashu es solo para la app nativa y no está verificado.',
safetyMore: 'Leer las normas del mercado',
offerTemplates: 'Notas rápidas',
offerDiscount: '¿Puedes hacer un 10 % de descuento?',
offerSwap: '¿Te plantearías un intercambio?',
offerPickup: 'Puedo recogerlo esta semana.',
offerShipping: '¿Puedes combinar el envío?',
},
};
export default marketplaceExtrasUi;

View file

@ -451,6 +451,22 @@ const drift = {
trust: "Vertrouwen",
noResults: "Geen resultaten voor ‘{{query}}’.",
},
marketplaceExtrasUi: {
savedSearches: "Opgeslagen zoekopdrachten",
saveSearch: "Zoekopdracht opslaan",
searchName: "Geef deze zoekopdracht een naam…",
deleteSearch: "Opgeslagen zoekopdracht verwijderen",
safetyTitle: "Veilig kopen & verkopen",
safetyMeet: "Spreek af op een openbare plek en bekijk de spullen voordat je betaalt.",
safetyKeys: "Deel nooit seed phrases of privésleutels. Houd de deal in de chat in de app.",
safetyEscrow: "Lees bij verzending eerst de escrowvoorwaarden: Cashu-escrow is alleen in de native app en niet geverifieerd.",
safetyMore: "Lees de marktplaatsrichtlijnen",
offerTemplates: "Snelle notities",
offerDiscount: "Kun je 10% korting geven?",
offerSwap: "Zou je een ruil overwegen?",
offerPickup: "Ik kan het deze week ophalen.",
offerShipping: "Kun je de verzendkosten bundelen?",
},
};
export default drift;

View file

@ -451,6 +451,22 @@ const drift = {
trust: "Zaufanie",
noResults: "Brak wyników dla „{{query}}”.",
},
marketplaceExtrasUi: {
savedSearches: "Zapisane wyszukiwania",
saveSearch: "Zapisz wyszukiwanie",
searchName: "Nazwij to wyszukiwanie…",
deleteSearch: "Usuń zapisane wyszukiwanie",
safetyTitle: "Kupuj i sprzedawaj bezpiecznie",
safetyMeet: "Spotkajcie się w miejscu publicznym i obejrzyjcie sprzęt przed zapłatą.",
safetyKeys: "Nigdy nie udostępniaj fraz seed ani kluczy prywatnych. Prowadź transakcję w czacie w aplikacji.",
safetyEscrow: "W przypadku wysyłki najpierw przeczytaj warunki escrow: escrow Cashu działa tylko w aplikacji natywnej i nie jest zweryfikowany.",
safetyMore: "Przeczytaj zasady marketplace",
offerTemplates: "Szybkie notatki",
offerDiscount: "Czy możesz dać 10% zniżki?",
offerSwap: "Czy rozważysz zamianę?",
offerPickup: "Mogę odebrać w tym tygodniu.",
offerShipping: "Czy możesz połączyć wysyłkę?",
},
};
export default drift;

View file

@ -453,6 +453,22 @@ const drift = {
trust: "Confiança",
noResults: "Nenhum resultado para “{{query}}”.",
},
marketplaceExtrasUi: {
savedSearches: "Pesquisas salvas",
saveSearch: "Salvar pesquisa",
searchName: "Dê um nome a esta pesquisa…",
deleteSearch: "Excluir pesquisa salva",
safetyTitle: "Compre e venda com segurança",
safetyMeet: "Encontrem-se em um local público e inspecionem o equipamento antes de pagar.",
safetyKeys: "Nunca compartilhe frases seed ou chaves privadas. Mantenha a negociação no chat do app.",
safetyEscrow: "Para envios, leia primeiro os termos do escrow: o escrow Cashu é apenas para o app nativo e não é verificado.",
safetyMore: "Ler as diretrizes do marketplace",
offerTemplates: "Notas rápidas",
offerDiscount: "Você consegue 10% de desconto?",
offerSwap: "Você consideraria uma troca?",
offerPickup: "Consigo retirar esta semana.",
offerShipping: "Você consegue agrupar o frete?",
},
};
export default drift;

View file

@ -451,6 +451,22 @@ const drift = {
trust: "Încredere",
noResults: "Niciun rezultat pentru „{{query}}”.",
},
marketplaceExtrasUi: {
savedSearches: "Căutări salvate",
saveSearch: "Salvează căutarea",
searchName: "Denumește această căutare…",
deleteSearch: "Șterge căutarea salvată",
safetyTitle: "Cumpără și vinde în siguranță",
safetyMeet: "Întâlniți-vă într-un loc public și verificați echipamentul înainte de plată.",
safetyKeys: "Nu partaja niciodată fraze seed sau chei private. Ține tranzacția în chatul din aplicație.",
safetyEscrow: "Pentru livrări, citește mai întâi termenii escrow: escrow-ul Cashu este doar pentru aplicația nativă și nu este verificat.",
safetyMore: "Citește regulile marketplace-ului",
offerTemplates: "Note rapide",
offerDiscount: "Poți face 10% reducere?",
offerSwap: "Ai lua în calcul un schimb?",
offerPickup: "Pot ridica personal săptămâna asta.",
offerShipping: "Poți combina transportul?",
},
};
export default drift;

View file

@ -455,6 +455,22 @@ const drift = {
trust: "Доверие",
noResults: "Нет результатов по запросу «{{query}}».",
},
marketplaceExtrasUi: {
savedSearches: "Сохранённые поиски",
saveSearch: "Сохранить поиск",
searchName: "Назовите этот поиск…",
deleteSearch: "Удалить сохранённый поиск",
safetyTitle: "Покупайте и продавайте безопасно",
safetyMeet: "Встречайтесь в общественном месте и осматривайте снаряжение до оплаты.",
safetyKeys: "Никогда не делитесь seed-фразами или приватными ключами. Ведите сделку в чате приложения.",
safetyEscrow: "При доставке сначала прочитайте условия эскроу: Cashu-эскроу работает только в нативном приложении и не проверен.",
safetyMore: "Прочитать правила маркетплейса",
offerTemplates: "Быстрые заметки",
offerDiscount: "Можете сделать скидку 10%?",
offerSwap: "Рассмотрите обмен?",
offerPickup: "Могу забрать на этой неделе.",
offerShipping: "Можете объединить доставку?",
},
};
export default drift;

View file

@ -455,6 +455,22 @@ const drift = {
trust: "Förtroende",
noResults: "Inga resultat för ”{{query}}”.",
},
marketplaceExtrasUi: {
savedSearches: "Sparade sökningar",
saveSearch: "Spara sökning",
searchName: "Namnge den här sökningen…",
deleteSearch: "Ta bort sparad sökning",
safetyTitle: "Köp och sälj säkert",
safetyMeet: "Mötas på en offentlig plats och granska utrustningen innan ni betalar.",
safetyKeys: "Dela aldrig seed-fraser eller privata nycklar. Håll affären i appens chatt.",
safetyEscrow: "För skickade affärer, läs escrow-villkoren först: Cashu-escrow finns bara i den inbyggda appen och är overifierad.",
safetyMore: "Läs marknadsplatsens riktlinjer",
offerTemplates: "Snabba anteckningar",
offerDiscount: "Kan du ge 10 % rabatt?",
offerSwap: "Skulle du överväga ett byte?",
offerPickup: "Jag kan hämta den här veckan.",
offerShipping: "Kan du samla frakten?",
},
};
export default drift;

View file

@ -447,6 +447,22 @@ const driftStrings = {
trust: "ความน่าเชื่อถือ",
noResults: "ไม่พบผลลัพธ์สำหรับ “{{query}}”",
},
marketplaceExtrasUi: {
savedSearches: "การค้นหาที่บันทึกไว้",
saveSearch: "บันทึกการค้นหา",
searchName: "ตั้งชื่อการค้นหานี้…",
deleteSearch: "ลบการค้นหาที่บันทึกไว้",
safetyTitle: "ซื้อและขายอย่างปลอดภัย",
safetyMeet: "นัดพบในที่สาธารณะและตรวจสอบอุปกรณ์ก่อนชำระเงิน",
safetyKeys: "อย่าแชร์วลี seed หรือคีย์ส่วนตัวเด็ดขาด ให้คุยดีลในแชทของแอป",
safetyEscrow: "สำหรับการจัดส่ง โปรดอ่านเงื่อนไข escrow ก่อน: escrow ของ Cashu ใช้ได้เฉพาะแอปเนทีฟและยังไม่ผ่านการตรวจสอบ",
safetyMore: "อ่านแนวทางของตลาด",
offerTemplates: "โน้ตด่วน",
offerDiscount: "ลด 10% ได้ไหม?",
offerSwap: "สนใจแลกเปลี่ยนไหม?",
offerPickup: "ฉันรับเองได้ภายในสัปดาห์นี้",
offerShipping: "รวมค่าส่งได้ไหม?",
},
};
export default driftStrings;

View file

@ -447,6 +447,22 @@ const driftStrings = {
trust: "Güven",
noResults: "“{{query}}” için sonuç yok.",
},
marketplaceExtrasUi: {
savedSearches: "Kayıtlı aramalar",
saveSearch: "Aramayı kaydet",
searchName: "Bu aramayı adlandır…",
deleteSearch: "Kayıtlı aramayı sil",
safetyTitle: "Güvenle al ve sat",
safetyMeet: "Herkese açık bir yerde buluşun ve ödeme yapmadan önce ekipmanı inceleyin.",
safetyKeys: "Seed ifadelerini veya özel anahtarları asla paylaşmayın. Anlaşmayı uygulama içi sohbette sürdürün.",
safetyEscrow: "Kargolu anlaşmalarda önce escrow koşullarını okuyun: Cashu escrow yalnızca yerel uygulamada çalışır ve doğrulanmamıştır.",
safetyMore: "Pazar yeri kurallarını oku",
offerTemplates: "Hızlı notlar",
offerDiscount: "%10 indirim yapabilir misin?",
offerSwap: "Takas düşünür müsün?",
offerPickup: "Bu hafta kendim alabilirim.",
offerShipping: "Kargo ücretini birleştirebilir misin?",
},
};
export default driftStrings;

View file

@ -455,6 +455,22 @@ const drift = {
trust: "Довіра",
noResults: "Немає результатів для «{{query}}».",
},
marketplaceExtrasUi: {
savedSearches: "Збережені пошуки",
saveSearch: "Зберегти пошук",
searchName: "Назвіть цей пошук…",
deleteSearch: "Видалити збережений пошук",
safetyTitle: "Купуйте й продавайте безпечно",
safetyMeet: "Зустрічайтеся в публічному місці та огляньте спорядження до оплати.",
safetyKeys: "Ніколи не діліться seed-фразами чи приватними ключами. Тримайте угоду в чаті застосунку.",
safetyEscrow: "Для доставки спершу прочитайте умови ескроу: Cashu-ескроу працює лише в нативному застосунку й не перевірений.",
safetyMore: "Прочитати правила маркетплейсу",
offerTemplates: "Швидкі нотатки",
offerDiscount: "Можете зробити знижку 10%?",
offerSwap: "Розглянете обмін?",
offerPickup: "Можу забрати цього тижня.",
offerShipping: "Можете об'єднати доставку?",
},
};
export default drift;

View file

@ -447,6 +447,22 @@ const driftStrings = {
trust: "Tin cậy",
noResults: "Không có kết quả cho “{{query}}”.",
},
marketplaceExtrasUi: {
savedSearches: "Tìm kiếm đã lưu",
saveSearch: "Lưu tìm kiếm",
searchName: "Đặt tên cho tìm kiếm này…",
deleteSearch: "Xóa tìm kiếm đã lưu",
safetyTitle: "Mua bán an toàn",
safetyMeet: "Gặp nhau ở nơi công cộng và kiểm tra đồ trước khi trả tiền.",
safetyKeys: "Không bao giờ chia sẻ cụm seed hay khóa riêng tư. Giữ giao dịch trong chat của ứng dụng.",
safetyEscrow: "Với đơn gửi hàng, hãy đọc điều kiện escrow trước: escrow Cashu chỉ dùng cho ứng dụng gốc và chưa được xác minh.",
safetyMore: "Đọc hướng dẫn chợ",
offerTemplates: "Ghi chú nhanh",
offerDiscount: "Bạn giảm 10% được không?",
offerSwap: "Bạn có cân nhắc đổi đồ không?",
offerPickup: "Tôi có thể tự lấy trong tuần này.",
offerShipping: "Bạn gộp phí ship được không?",
},
};
export default driftStrings;

View file

@ -463,6 +463,22 @@ const drift: Dict = {
trust: "信任",
noResults: "未找到“{{query}}”的结果。",
},
marketplaceExtrasUi: {
savedSearches: "已保存的搜索",
saveSearch: "保存搜索",
searchName: "为此搜索命名…",
deleteSearch: "删除已保存的搜索",
safetyTitle: "安全买卖",
safetyMeet: "在公共场所见面,付款前先检查装备。",
safetyKeys: "切勿分享助记词或私钥。交易请留在应用内聊天中。",
safetyEscrow: "寄送交易请先阅读托管条款:Cashu 托管仅限原生应用,且尚未验证。",
safetyMore: "阅读市场指南",
offerTemplates: "快捷留言",
offerDiscount: "能优惠 10% 吗?",
offerSwap: "可以考虑交换吗?",
offerPickup: "我这周可以自取。",
offerShipping: "可以合并运费吗?",
},
};
export default drift;

View file

@ -463,6 +463,22 @@ const drift: Dict = {
trust: "信任",
noResults: "找不到「{{query}}」的結果。",
},
marketplaceExtrasUi: {
savedSearches: "已儲存的搜尋",
saveSearch: "儲存搜尋",
searchName: "為此搜尋命名…",
deleteSearch: "刪除已儲存的搜尋",
safetyTitle: "安心買賣",
safetyMeet: "在公共場所見面,付款前先檢查裝備。",
safetyKeys: "切勿分享助記詞或私鑰。交易請留在應用程式內聊天中。",
safetyEscrow: "寄送交易請先閱讀託管條款:Cashu 託管僅限原生應用程式,且尚未驗證。",
safetyMore: "閱讀市場指南",
offerTemplates: "快速留言",
offerDiscount: "能優惠 10% 嗎?",
offerSwap: "可以考慮交換嗎?",
offerPickup: "我這週可以自取。",
offerShipping: "可以合併運費嗎?",
},
};
export default drift;

View file

@ -1,4 +1,4 @@
import { useState, useMemo } from 'react';
import { useMemo, useState } from 'react';
import { useSeoMeta } from '@unhead/react';
import { Link } from 'react-router-dom';
import { PageHeader } from '@/components/PageHeader';
@ -6,6 +6,8 @@ import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Input } from '@/components/ui/input';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Label } from '@/components/ui/label';
import { Skeleton } from '@/components/ui/skeleton';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { DropdownMenu, DropdownMenuContent, DropdownMenuCheckboxItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
@ -20,12 +22,25 @@ import { GEAR_CATEGORIES } from '@/lib/types';
import { GEAR_CATEGORY_KEYS } from '@/lib/labelKeys';
import { getLastLocation } from '@/lib/lastLocation';
import { itemDistanceKm } from '@/lib/sortOptions';
import { ShoppingCart, Plus, Search, Filter, ArrowUpDown, Users, ShieldCheck, UserCheck, AlertCircle, MapPin } from 'lucide-react';
import { ShoppingCart, Plus, Search, Filter, ArrowUpDown, Users, ShieldCheck, UserCheck, AlertCircle, MapPin, Bookmark, X } from 'lucide-react';
import { useTranslation } from '@/contexts/language';
import { useLocalStorage } from '@/hooks/useLocalStorage';
type SortOption = 'newest' | 'price_low' | 'price_high' | 'trust' | 'distance';
type TrustFilter = 'all' | 'verified' | 'trusted' | 'following';
interface SavedSearch {
id: string;
name: string;
query: string;
category: string;
sort: SortOption;
fulfilment: 'all' | 'pickup' | 'ships';
trust: TrustFilter;
wtb: boolean;
selling: boolean;
}
export default function GearSwapPage() {
const { t } = useTranslation();
useSeoMeta({
@ -45,6 +60,9 @@ export default function GearSwapPage() {
const [trustFilter, setTrustFilter] = useState<TrustFilter>('all');
const [showOnlyWTB, setShowOnlyWTB] = useState(false);
const [showOnlySelling, setShowOnlySelling] = useState(false);
const [savedSearches, setSavedSearches] = useLocalStorage<SavedSearch[]>('skatespots:gear-saved-searches', []);
const [saveSearchOpen, setSaveSearchOpen] = useState(false);
const [saveSearchName, setSaveSearchName] = useState('');
// Get unique seller pubkeys for batch queries
const sellerPubkeys = useMemo(() => {
@ -182,6 +200,41 @@ export default function GearSwapPage() {
return filtered;
}, [items, searchQuery, selectedCategory, sortBy, trustFilter, showOnlyWTB, showOnlySelling, fulfilmentFilter, trustScores, followingSet]);
const applySavedSearch = (saved: SavedSearch) => {
setSearchQuery(saved.query);
setSelectedCategory(saved.category);
setSortBy(saved.sort);
setFulfilmentFilter(saved.fulfilment);
setTrustFilter(saved.trust);
setShowOnlyWTB(saved.wtb);
setShowOnlySelling(saved.selling);
};
const saveCurrentSearch = () => {
const name = saveSearchName.trim();
if (!name) return;
const saved: SavedSearch = {
id: `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`,
name,
query: searchQuery,
category: selectedCategory,
sort: sortBy,
fulfilment: fulfilmentFilter,
trust: trustFilter,
wtb: showOnlyWTB,
selling: showOnlySelling,
};
setSavedSearches((current) => [saved, ...current.filter((item) => item.name !== name)].slice(0, 12));
setSaveSearchOpen(false);
setSaveSearchName('');
};
const safetyItems = [
t('marketplaceExtrasUi.safetyMeet'),
t('marketplaceExtrasUi.safetyKeys'),
t('marketplaceExtrasUi.safetyEscrow'),
];
const categoryOptions = [
{ value: 'all', label: t('marketUi.gearSwap.allCategories') },
...Object.entries(GEAR_CATEGORIES).map(([key]) => ({
@ -212,6 +265,22 @@ export default function GearSwapPage() {
}
/>
<details className="group mb-6 rounded-2xl border border-orange-500/20 bg-black/70 px-4 py-3">
<summary className="flex cursor-pointer list-none items-center gap-2 text-sm font-bold text-orange-300">
<ShieldCheck className="h-4 w-4" />
{t('marketplaceExtrasUi.safetyTitle')}
<span className="ml-auto text-[10px] font-normal uppercase tracking-wider text-gray-500 group-open:hidden">{t('common.learnMore')}</span>
</summary>
<ul className="mt-3 space-y-1.5 text-xs leading-relaxed text-gray-400">
{safetyItems.map((item) => (
<li key={item} className="flex gap-2"><span className="text-orange-400">•</span><span>{item}</span></li>
))}
</ul>
<Link to="/marketplace-guidelines" className="mt-2 inline-block text-xs font-bold text-orange-400 hover:underline">
{t('marketplaceExtrasUi.safetyMore')}
</Link>
</details>
{/* Search & Filters */}
<Card className="bg-black/90 border-2 border-orange-500/20 mb-6">
<CardContent className="p-4">
@ -375,14 +444,54 @@ export default function GearSwapPage() {
</CardContent>
</Card>
{savedSearches.length > 0 && (
<div className="mb-4 flex flex-wrap items-center gap-2">
<span className="flex items-center gap-1.5 text-[10px] font-black uppercase tracking-widest text-gray-500">
<Bookmark className="h-3.5 w-3.5" /> {t('marketplaceExtrasUi.savedSearches')}
</span>
{savedSearches.map((saved) => (
<span key={saved.id} className="inline-flex items-center overflow-hidden rounded-full border border-orange-500/30 bg-orange-500/10">
<button
type="button"
onClick={() => applySavedSearch(saved)}
className="min-h-0 px-2.5 py-1 text-xs font-bold text-orange-200 hover:bg-orange-500/15"
>
{saved.name}
</button>
<button
type="button"
onClick={() => setSavedSearches((current) => current.filter((item) => item.id !== saved.id))}
aria-label={t('marketplaceExtrasUi.deleteSearch')}
title={t('marketplaceExtrasUi.deleteSearch')}
className="min-h-0 px-1.5 py-1 text-orange-300/60 hover:bg-red-500/20 hover:text-red-300"
>
<X className="h-3 w-3" />
</button>
</span>
))}
</div>
)}
{/* Results Count */}
{!isLoading && (
<div className="flex items-center justify-between mb-4">
<div className="mb-4 flex flex-wrap items-center justify-between gap-2">
<p className="text-sm text-gray-400">
{filteredItems.length === 1
? t('marketUi.gearSwap.listingFound', { count: filteredItems.length })
: t('marketUi.gearSwap.listingsFound', { count: filteredItems.length })}
</p>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => {
setSaveSearchName(searchQuery.trim().slice(0, 40));
setSaveSearchOpen(true);
}}
className="h-8 min-h-0 gap-1.5 border-orange-500/30 text-orange-300 hover:bg-orange-500/10"
>
<Bookmark className="h-3.5 w-3.5" /> {t('marketplaceExtrasUi.saveSearch')}
</Button>
</div>
)}
@ -472,6 +581,38 @@ export default function GearSwapPage() {
</div>
)}
</div>
<Dialog open={saveSearchOpen} onOpenChange={setSaveSearchOpen}>
<DialogContent className="mx-4 max-w-[calc(100vw-2rem)] border-2 border-orange-500/30 bg-black sm:max-w-sm">
<DialogHeader>
<DialogTitle className="flex items-center gap-2 text-white">
<Bookmark className="h-5 w-5 text-orange-400" /> {t('marketplaceExtrasUi.saveSearch')}
</DialogTitle>
</DialogHeader>
<div className="space-y-3">
<Label htmlFor="saved-search-name" className="text-xs uppercase tracking-wide text-gray-400">
{t('marketplaceExtrasUi.savedSearches')}
</Label>
<Input
id="saved-search-name"
value={saveSearchName}
onChange={(event) => setSaveSearchName(event.target.value)}
onKeyDown={(event) => { if (event.key === 'Enter') saveCurrentSearch(); }}
placeholder={t('marketplaceExtrasUi.searchName')}
className="h-11 border-gray-700 bg-gray-900 text-white"
autoFocus
/>
<div className="flex gap-2">
<Button variant="outline" onClick={() => setSaveSearchOpen(false)} className="flex-1 border-white/15">
{t('common.cancel')}
</Button>
<Button onClick={saveCurrentSearch} disabled={!saveSearchName.trim()} className="flex-1 bg-orange-500 font-bold text-black hover:bg-orange-500/90">
{t('common.save')}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
</>
);
}