Phase 6: Translate User Pages - exchange, trades, invites, profile
- Expand exchange.json with all exchange page strings (page, steps, detailsStep, bookingStep, confirmationStep, priceDisplay) - Create trades.json translation files for es, en, ca - Create invites.json translation files for es, en, ca - Create profile.json translation files for es, en, ca - Translate exchange page and all components (ExchangeDetailsStep, BookingStep, ConfirmationStep, StepIndicator, PriceDisplay) - Translate trades page (titles, sections, buttons, status labels) - Translate invites page (titles, sections, status badges, copy button) - Translate profile page (form labels, hints, placeholders, messages) - Update IntlProvider to load all new namespaces - All frontend tests passing
This commit is contained in:
parent
7dd13292a0
commit
246553c402
22 changed files with 559 additions and 115 deletions
|
|
@ -17,11 +17,44 @@ import caExchange from "../../locales/ca/exchange.json";
|
||||||
import esAuth from "../../locales/es/auth.json";
|
import esAuth from "../../locales/es/auth.json";
|
||||||
import enAuth from "../../locales/en/auth.json";
|
import enAuth from "../../locales/en/auth.json";
|
||||||
import caAuth from "../../locales/ca/auth.json";
|
import caAuth from "../../locales/ca/auth.json";
|
||||||
|
import esTrades from "../../locales/es/trades.json";
|
||||||
|
import enTrades from "../../locales/en/trades.json";
|
||||||
|
import caTrades from "../../locales/ca/trades.json";
|
||||||
|
import esInvites from "../../locales/es/invites.json";
|
||||||
|
import enInvites from "../../locales/en/invites.json";
|
||||||
|
import caInvites from "../../locales/ca/invites.json";
|
||||||
|
import esProfile from "../../locales/es/profile.json";
|
||||||
|
import enProfile from "../../locales/en/profile.json";
|
||||||
|
import caProfile from "../../locales/ca/profile.json";
|
||||||
|
|
||||||
const messages = {
|
const messages = {
|
||||||
es: { common: esCommon, navigation: esNavigation, exchange: esExchange, auth: esAuth },
|
es: {
|
||||||
en: { common: enCommon, navigation: enNavigation, exchange: enExchange, auth: enAuth },
|
common: esCommon,
|
||||||
ca: { common: caCommon, navigation: caNavigation, exchange: caExchange, auth: caAuth },
|
navigation: esNavigation,
|
||||||
|
exchange: esExchange,
|
||||||
|
auth: esAuth,
|
||||||
|
trades: esTrades,
|
||||||
|
invites: esInvites,
|
||||||
|
profile: esProfile,
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
common: enCommon,
|
||||||
|
navigation: enNavigation,
|
||||||
|
exchange: enExchange,
|
||||||
|
auth: enAuth,
|
||||||
|
trades: enTrades,
|
||||||
|
invites: enInvites,
|
||||||
|
profile: enProfile,
|
||||||
|
},
|
||||||
|
ca: {
|
||||||
|
common: caCommon,
|
||||||
|
navigation: caNavigation,
|
||||||
|
exchange: caExchange,
|
||||||
|
auth: caAuth,
|
||||||
|
trades: caTrades,
|
||||||
|
invites: caInvites,
|
||||||
|
profile: caProfile,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
interface IntlProviderProps {
|
interface IntlProviderProps {
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { components } from "../../generated/api";
|
||||||
import { formatDate, formatTime } from "../../utils/date";
|
import { formatDate, formatTime } from "../../utils/date";
|
||||||
import { formatEur } from "../../utils/exchange";
|
import { formatEur } from "../../utils/exchange";
|
||||||
import { bannerStyles } from "../../styles/shared";
|
import { bannerStyles } from "../../styles/shared";
|
||||||
|
import { useTranslation } from "../../hooks/useTranslation";
|
||||||
|
|
||||||
type BookableSlot = components["schemas"]["BookableSlot"];
|
type BookableSlot = components["schemas"]["BookableSlot"];
|
||||||
type ExchangeResponse = components["schemas"]["ExchangeResponse"];
|
type ExchangeResponse = components["schemas"]["ExchangeResponse"];
|
||||||
|
|
@ -215,14 +216,15 @@ export function BookingStep({
|
||||||
onSlotSelect,
|
onSlotSelect,
|
||||||
onBackToDetails,
|
onBackToDetails,
|
||||||
}: BookingStepProps) {
|
}: BookingStepProps) {
|
||||||
|
const t = useTranslation("exchange");
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Trade Summary Card */}
|
{/* Trade Summary Card */}
|
||||||
<div style={styles.summaryCard}>
|
<div style={styles.summaryCard}>
|
||||||
<div style={styles.summaryHeader}>
|
<div style={styles.summaryHeader}>
|
||||||
<span style={styles.summaryTitle}>Your Exchange</span>
|
<span style={styles.summaryTitle}>{t("bookingStep.yourExchange")}</span>
|
||||||
<button onClick={onBackToDetails} style={styles.editButton}>
|
<button onClick={onBackToDetails} style={styles.editButton}>
|
||||||
Edit
|
{t("bookingStep.edit")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div style={styles.summaryDetails}>
|
<div style={styles.summaryDetails}>
|
||||||
|
|
@ -232,7 +234,7 @@ export function BookingStep({
|
||||||
color: direction === "buy" ? "#4ade80" : "#f87171",
|
color: direction === "buy" ? "#4ade80" : "#f87171",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{direction === "buy" ? "Buy" : "Sell"} BTC
|
{direction === "buy" ? t("bookingStep.buy") : t("bookingStep.sell")} BTC
|
||||||
</span>
|
</span>
|
||||||
<span style={styles.summaryDivider}>•</span>
|
<span style={styles.summaryDivider}>•</span>
|
||||||
<span>{formatEur(eurAmount)}</span>
|
<span>{formatEur(eurAmount)}</span>
|
||||||
|
|
@ -242,15 +244,17 @@ export function BookingStep({
|
||||||
</span>
|
</span>
|
||||||
<span style={styles.summaryDivider}>•</span>
|
<span style={styles.summaryDivider}>•</span>
|
||||||
<span style={styles.summaryPaymentMethod}>
|
<span style={styles.summaryPaymentMethod}>
|
||||||
{direction === "buy" ? "Receive via " : "Send via "}
|
{direction === "buy" ? t("bookingStep.receiveVia") : t("bookingStep.sendVia")}{" "}
|
||||||
{bitcoinTransferMethod === "onchain" ? "Onchain" : "Lightning"}
|
{bitcoinTransferMethod === "onchain"
|
||||||
|
? t("transferMethod.onchain")
|
||||||
|
: t("transferMethod.lightning")}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Date Selection */}
|
{/* Date Selection */}
|
||||||
<div style={styles.section}>
|
<div style={styles.section}>
|
||||||
<h2 style={styles.sectionTitle}>Select a Date</h2>
|
<h2 style={styles.sectionTitle}>{t("bookingStep.selectDate")}</h2>
|
||||||
<div style={styles.dateGrid}>
|
<div style={styles.dateGrid}>
|
||||||
{dates.map((date) => {
|
{dates.map((date) => {
|
||||||
const dateStr = formatDate(date);
|
const dateStr = formatDate(date);
|
||||||
|
|
@ -291,15 +295,13 @@ export function BookingStep({
|
||||||
{/* Warning for existing trade on selected date */}
|
{/* Warning for existing trade on selected date */}
|
||||||
{existingTradeOnSelectedDate && (
|
{existingTradeOnSelectedDate && (
|
||||||
<div style={bannerStyles.errorBanner}>
|
<div style={bannerStyles.errorBanner}>
|
||||||
<div>
|
<div>{t("bookingStep.existingTradeWarning")}</div>
|
||||||
You already have a trade booked on this day. You can only book one trade per day.
|
|
||||||
</div>
|
|
||||||
<div style={styles.errorLink}>
|
<div style={styles.errorLink}>
|
||||||
<a
|
<a
|
||||||
href={`/trades/${existingTradeOnSelectedDate.public_id}`}
|
href={`/trades/${existingTradeOnSelectedDate.public_id}`}
|
||||||
style={styles.errorLinkAnchor}
|
style={styles.errorLinkAnchor}
|
||||||
>
|
>
|
||||||
View your existing trade →
|
{t("bookingStep.viewExistingTrade")}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -309,7 +311,7 @@ export function BookingStep({
|
||||||
{selectedDate && !existingTradeOnSelectedDate && (
|
{selectedDate && !existingTradeOnSelectedDate && (
|
||||||
<div style={styles.section}>
|
<div style={styles.section}>
|
||||||
<h2 style={styles.sectionTitle}>
|
<h2 style={styles.sectionTitle}>
|
||||||
Available Slots for{" "}
|
{t("bookingStep.availableSlots")}{" "}
|
||||||
{selectedDate.toLocaleDateString("en-US", {
|
{selectedDate.toLocaleDateString("en-US", {
|
||||||
weekday: "long",
|
weekday: "long",
|
||||||
month: "long",
|
month: "long",
|
||||||
|
|
@ -318,9 +320,9 @@ export function BookingStep({
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{isLoadingSlots ? (
|
{isLoadingSlots ? (
|
||||||
<div style={styles.emptyState}>Loading slots...</div>
|
<div style={styles.emptyState}>{t("bookingStep.loadingSlots")}</div>
|
||||||
) : availableSlots.length === 0 ? (
|
) : availableSlots.length === 0 ? (
|
||||||
<div style={styles.emptyState}>No available slots for this date</div>
|
<div style={styles.emptyState}>{t("bookingStep.noSlots")}</div>
|
||||||
) : (
|
) : (
|
||||||
<div style={styles.slotGrid}>
|
<div style={styles.slotGrid}>
|
||||||
{availableSlots.map((slot) => {
|
{availableSlots.map((slot) => {
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { components } from "../../generated/api";
|
||||||
import { formatTime } from "../../utils/date";
|
import { formatTime } from "../../utils/date";
|
||||||
import { formatEur } from "../../utils/exchange";
|
import { formatEur } from "../../utils/exchange";
|
||||||
import { buttonStyles } from "../../styles/shared";
|
import { buttonStyles } from "../../styles/shared";
|
||||||
|
import { useTranslation } from "../../hooks/useTranslation";
|
||||||
|
|
||||||
type BookableSlot = components["schemas"]["BookableSlot"];
|
type BookableSlot = components["schemas"]["BookableSlot"];
|
||||||
type Direction = "buy" | "sell";
|
type Direction = "buy" | "sell";
|
||||||
|
|
@ -154,14 +155,15 @@ export function ConfirmationStep({
|
||||||
onConfirm,
|
onConfirm,
|
||||||
onBack,
|
onBack,
|
||||||
}: ConfirmationStepProps) {
|
}: ConfirmationStepProps) {
|
||||||
|
const t = useTranslation("exchange");
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Compressed Booking Summary */}
|
{/* Compressed Booking Summary */}
|
||||||
<div style={styles.compressedBookingCard}>
|
<div style={styles.compressedBookingCard}>
|
||||||
<div style={styles.compressedBookingHeader}>
|
<div style={styles.compressedBookingHeader}>
|
||||||
<span style={styles.compressedBookingTitle}>Appointment</span>
|
<span style={styles.compressedBookingTitle}>{t("confirmationStep.appointment")}</span>
|
||||||
<button onClick={onBack} style={styles.editButton}>
|
<button onClick={onBack} style={styles.editButton}>
|
||||||
Edit
|
{t("confirmationStep.edit")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div style={styles.compressedBookingDetails}>
|
<div style={styles.compressedBookingDetails}>
|
||||||
|
|
@ -181,44 +183,48 @@ export function ConfirmationStep({
|
||||||
|
|
||||||
{/* Confirmation Card */}
|
{/* Confirmation Card */}
|
||||||
<div style={styles.confirmCard}>
|
<div style={styles.confirmCard}>
|
||||||
<h3 style={styles.confirmTitle}>Confirm Trade</h3>
|
<h3 style={styles.confirmTitle}>{t("confirmationStep.confirmTrade")}</h3>
|
||||||
<div style={styles.confirmDetails}>
|
<div style={styles.confirmDetails}>
|
||||||
<div style={styles.confirmRow}>
|
<div style={styles.confirmRow}>
|
||||||
<span style={styles.confirmLabel}>Time:</span>
|
<span style={styles.confirmLabel}>{t("confirmationStep.time")}</span>
|
||||||
<span style={styles.confirmValue}>
|
<span style={styles.confirmValue}>
|
||||||
{formatTime(selectedSlot.start_time)} - {formatTime(selectedSlot.end_time)}
|
{formatTime(selectedSlot.start_time)} - {formatTime(selectedSlot.end_time)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={styles.confirmRow}>
|
<div style={styles.confirmRow}>
|
||||||
<span style={styles.confirmLabel}>Direction:</span>
|
<span style={styles.confirmLabel}>{t("confirmationStep.direction")}</span>
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
...styles.confirmValue,
|
...styles.confirmValue,
|
||||||
color: direction === "buy" ? "#4ade80" : "#f87171",
|
color: direction === "buy" ? "#4ade80" : "#f87171",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{direction === "buy" ? "Buy BTC" : "Sell BTC"}
|
{direction === "buy" ? t("direction.buyShort") : t("direction.sellShort")}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={styles.confirmRow}>
|
<div style={styles.confirmRow}>
|
||||||
<span style={styles.confirmLabel}>EUR:</span>
|
<span style={styles.confirmLabel}>{t("confirmationStep.eur")}</span>
|
||||||
<span style={styles.confirmValue}>{formatEur(eurAmount)}</span>
|
<span style={styles.confirmValue}>{formatEur(eurAmount)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={styles.confirmRow}>
|
<div style={styles.confirmRow}>
|
||||||
<span style={styles.confirmLabel}>BTC:</span>
|
<span style={styles.confirmLabel}>{t("confirmationStep.btc")}</span>
|
||||||
<span style={{ ...styles.confirmValue, ...styles.satsValue }}>
|
<span style={{ ...styles.confirmValue, ...styles.satsValue }}>
|
||||||
<SatsDisplay sats={satsAmount} />
|
<SatsDisplay sats={satsAmount} />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={styles.confirmRow}>
|
<div style={styles.confirmRow}>
|
||||||
<span style={styles.confirmLabel}>Rate:</span>
|
<span style={styles.confirmLabel}>{t("confirmationStep.rate")}</span>
|
||||||
<span style={styles.confirmValue}>{formatPrice(agreedPrice)}/BTC</span>
|
<span style={styles.confirmValue}>{formatPrice(agreedPrice)}/BTC</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={styles.confirmRow}>
|
<div style={styles.confirmRow}>
|
||||||
<span style={styles.confirmLabel}>Payment:</span>
|
<span style={styles.confirmLabel}>{t("confirmationStep.payment")}</span>
|
||||||
<span style={styles.confirmValue}>
|
<span style={styles.confirmValue}>
|
||||||
{direction === "buy" ? "Receive via " : "Send via "}
|
{direction === "buy"
|
||||||
{bitcoinTransferMethod === "onchain" ? "Onchain" : "Lightning"}
|
? t("confirmationStep.receiveVia")
|
||||||
|
: t("confirmationStep.sendVia")}{" "}
|
||||||
|
{bitcoinTransferMethod === "onchain"
|
||||||
|
? t("transferMethod.onchain")
|
||||||
|
: t("transferMethod.lightning")}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -237,13 +243,15 @@ export function ConfirmationStep({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isBooking
|
{isBooking
|
||||||
? "Booking..."
|
? t("confirmationStep.booking")
|
||||||
: isPriceStale
|
: isPriceStale
|
||||||
? "Price Stale"
|
? t("confirmationStep.priceStale")
|
||||||
: `Confirm ${direction === "buy" ? "Buy" : "Sell"}`}
|
: direction === "buy"
|
||||||
|
? t("confirmationStep.confirmBuy")
|
||||||
|
: t("confirmationStep.confirmSell")}
|
||||||
</button>
|
</button>
|
||||||
<button onClick={onBack} disabled={isBooking} style={styles.cancelButton}>
|
<button onClick={onBack} disabled={isBooking} style={styles.cancelButton}>
|
||||||
Back
|
{t("confirmationStep.back")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { SatsDisplay } from "../../components/SatsDisplay";
|
||||||
import { formatEur } from "../../utils/exchange";
|
import { formatEur } from "../../utils/exchange";
|
||||||
import { buttonStyles } from "../../styles/shared";
|
import { buttonStyles } from "../../styles/shared";
|
||||||
import constants from "../../../../shared/constants.json";
|
import constants from "../../../../shared/constants.json";
|
||||||
|
import { useTranslation } from "../../hooks/useTranslation";
|
||||||
|
|
||||||
const { lightningMaxEur: LIGHTNING_MAX_EUR } = constants.exchange;
|
const { lightningMaxEur: LIGHTNING_MAX_EUR } = constants.exchange;
|
||||||
|
|
||||||
|
|
@ -225,6 +226,7 @@ export function ExchangeDetailsStep({
|
||||||
hasPrice,
|
hasPrice,
|
||||||
onContinue,
|
onContinue,
|
||||||
}: ExchangeDetailsStepProps) {
|
}: ExchangeDetailsStepProps) {
|
||||||
|
const t = useTranslation("exchange");
|
||||||
const isLightningDisabled = eurAmount > LIGHTNING_MAX_EUR * 100;
|
const isLightningDisabled = eurAmount > LIGHTNING_MAX_EUR * 100;
|
||||||
|
|
||||||
const handleAmountChange = (value: number) => {
|
const handleAmountChange = (value: number) => {
|
||||||
|
|
@ -263,7 +265,7 @@ export function ExchangeDetailsStep({
|
||||||
...(direction === "buy" ? styles.directionBtnBuyActive : {}),
|
...(direction === "buy" ? styles.directionBtnBuyActive : {}),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Buy BTC
|
{t("direction.buyShort")}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => onDirectionChange("sell")}
|
onClick={() => onDirectionChange("sell")}
|
||||||
|
|
@ -272,14 +274,15 @@ export function ExchangeDetailsStep({
|
||||||
...(direction === "sell" ? styles.directionBtnSellActive : {}),
|
...(direction === "sell" ? styles.directionBtnSellActive : {}),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Sell BTC
|
{t("direction.sellShort")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Payment Method Selector */}
|
{/* Payment Method Selector */}
|
||||||
<div style={styles.paymentMethodSection}>
|
<div style={styles.paymentMethodSection}>
|
||||||
<div style={styles.paymentMethodLabel}>
|
<div style={styles.paymentMethodLabel}>
|
||||||
Payment Method <span style={styles.required}>*</span>
|
{t("detailsStep.paymentMethod")}{" "}
|
||||||
|
<span style={styles.required}>{t("detailsStep.required")}</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={styles.paymentMethodRow}>
|
<div style={styles.paymentMethodRow}>
|
||||||
<button
|
<button
|
||||||
|
|
@ -290,7 +293,7 @@ export function ExchangeDetailsStep({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span style={styles.paymentMethodIcon}>🔗</span>
|
<span style={styles.paymentMethodIcon}>🔗</span>
|
||||||
<span>Onchain</span>
|
<span>{t("transferMethod.onchain")}</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => onBitcoinTransferMethodChange("lightning")}
|
onClick={() => onBitcoinTransferMethodChange("lightning")}
|
||||||
|
|
@ -302,12 +305,12 @@ export function ExchangeDetailsStep({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span style={styles.paymentMethodIcon}>⚡</span>
|
<span style={styles.paymentMethodIcon}>⚡</span>
|
||||||
<span>Lightning</span>
|
<span>{t("transferMethod.lightning")}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{isLightningDisabled && (
|
{isLightningDisabled && (
|
||||||
<div style={styles.thresholdMessage}>
|
<div style={styles.thresholdMessage}>
|
||||||
Lightning payments are only available for amounts up to €{LIGHTNING_MAX_EUR}
|
{t("detailsStep.lightningThreshold", { max: LIGHTNING_MAX_EUR })}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -315,7 +318,7 @@ export function ExchangeDetailsStep({
|
||||||
{/* Amount Section */}
|
{/* Amount Section */}
|
||||||
<div style={styles.amountSection}>
|
<div style={styles.amountSection}>
|
||||||
<div style={styles.amountHeader}>
|
<div style={styles.amountHeader}>
|
||||||
<span style={styles.amountLabel}>Amount (EUR)</span>
|
<span style={styles.amountLabel}>{t("detailsStep.amount")}</span>
|
||||||
<div style={styles.amountInputWrapper}>
|
<div style={styles.amountInputWrapper}>
|
||||||
<span style={styles.amountCurrency}>€</span>
|
<span style={styles.amountCurrency}>€</span>
|
||||||
<input
|
<input
|
||||||
|
|
@ -345,18 +348,24 @@ export function ExchangeDetailsStep({
|
||||||
<div style={styles.tradeSummary}>
|
<div style={styles.tradeSummary}>
|
||||||
{direction === "buy" ? (
|
{direction === "buy" ? (
|
||||||
<p style={styles.summaryText}>
|
<p style={styles.summaryText}>
|
||||||
You buy{" "}
|
{t("detailsStep.summaryBuy").split("{sats}")[0].trim()}{" "}
|
||||||
<strong style={styles.satsValue}>
|
<strong style={styles.satsValue}>
|
||||||
<SatsDisplay sats={satsAmount} />
|
<SatsDisplay sats={satsAmount} />
|
||||||
</strong>
|
</strong>
|
||||||
, you sell <strong>{formatEur(eurAmount)}</strong>
|
{", "}
|
||||||
|
{t("detailsStep.summaryBuy").split("{sats}")[1]?.split("{eur}")[0]?.trim()}{" "}
|
||||||
|
<strong>{formatEur(eurAmount)}</strong>
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<p style={styles.summaryText}>
|
<p style={styles.summaryText}>
|
||||||
You buy <strong>{formatEur(eurAmount)}</strong>, you sell{" "}
|
{t("detailsStep.summarySell").split("{sats}")[0]?.split("{eur}")[0]?.trim()}{" "}
|
||||||
|
<strong>{formatEur(eurAmount)}</strong>
|
||||||
|
{", "}
|
||||||
|
{t("detailsStep.summarySell").split("{sats}")[0]?.split("{eur}")[1]?.trim()}{" "}
|
||||||
<strong style={styles.satsValue}>
|
<strong style={styles.satsValue}>
|
||||||
<SatsDisplay sats={satsAmount} />
|
<SatsDisplay sats={satsAmount} />
|
||||||
</strong>
|
</strong>
|
||||||
|
{t("detailsStep.summarySell").split("{sats}")[1]?.trim()}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -370,7 +379,7 @@ export function ExchangeDetailsStep({
|
||||||
...(isPriceStale || !hasPrice ? buttonStyles.buttonDisabled : {}),
|
...(isPriceStale || !hasPrice ? buttonStyles.buttonDisabled : {}),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Continue to Booking
|
{t("detailsStep.continueToBooking")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
import { CSSProperties } from "react";
|
import { CSSProperties } from "react";
|
||||||
import { components } from "../../generated/api";
|
import { components } from "../../generated/api";
|
||||||
|
import { useTranslation } from "../../hooks/useTranslation";
|
||||||
|
|
||||||
type ExchangePriceResponse = components["schemas"]["ExchangePriceResponse"];
|
type ExchangePriceResponse = components["schemas"]["ExchangePriceResponse"];
|
||||||
|
|
||||||
|
|
@ -94,6 +95,7 @@ export function PriceDisplay({
|
||||||
direction,
|
direction,
|
||||||
agreedPrice,
|
agreedPrice,
|
||||||
}: PriceDisplayProps) {
|
}: PriceDisplayProps) {
|
||||||
|
const t = useTranslation("exchange");
|
||||||
const marketPrice = priceData?.price?.market_price ?? 0;
|
const marketPrice = priceData?.price?.market_price ?? 0;
|
||||||
const premiumPercent = priceData?.price?.premium_percentage ?? 5;
|
const premiumPercent = priceData?.price?.premium_percentage ?? 5;
|
||||||
const isPriceStale = priceData?.price?.is_stale ?? false;
|
const isPriceStale = priceData?.price?.is_stale ?? false;
|
||||||
|
|
@ -101,16 +103,16 @@ export function PriceDisplay({
|
||||||
return (
|
return (
|
||||||
<div style={styles.priceCard}>
|
<div style={styles.priceCard}>
|
||||||
{isLoading && !priceData ? (
|
{isLoading && !priceData ? (
|
||||||
<div style={styles.priceLoading}>Loading price...</div>
|
<div style={styles.priceLoading}>{t("priceDisplay.loading")}</div>
|
||||||
) : error && !priceData?.price ? (
|
) : error && !priceData?.price ? (
|
||||||
<div style={styles.priceError}>{error}</div>
|
<div style={styles.priceError}>{error}</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div style={styles.priceRow}>
|
<div style={styles.priceRow}>
|
||||||
<span style={styles.priceLabel}>Market:</span>
|
<span style={styles.priceLabel}>{t("priceDisplay.market")}</span>
|
||||||
<span style={styles.priceValue}>{formatPrice(marketPrice)}</span>
|
<span style={styles.priceValue}>{formatPrice(marketPrice)}</span>
|
||||||
<span style={styles.priceDivider}>•</span>
|
<span style={styles.priceDivider}>•</span>
|
||||||
<span style={styles.priceLabel}>Our price:</span>
|
<span style={styles.priceLabel}>{t("priceDisplay.ourPrice")}</span>
|
||||||
<span style={styles.priceValue}>{formatPrice(agreedPrice)}</span>
|
<span style={styles.priceValue}>{formatPrice(agreedPrice)}</span>
|
||||||
<span style={styles.premiumBadge}>
|
<span style={styles.premiumBadge}>
|
||||||
{direction === "buy" ? "+" : "-"}
|
{direction === "buy" ? "+" : "-"}
|
||||||
|
|
@ -119,8 +121,8 @@ export function PriceDisplay({
|
||||||
</div>
|
</div>
|
||||||
{lastUpdate && (
|
{lastUpdate && (
|
||||||
<div style={styles.priceTimestamp}>
|
<div style={styles.priceTimestamp}>
|
||||||
Updated {lastUpdate.toLocaleTimeString()}
|
{t("priceDisplay.updated")} {lastUpdate.toLocaleTimeString()}
|
||||||
{isPriceStale && <span style={styles.staleWarning}> (stale)</span>}
|
{isPriceStale && <span style={styles.staleWarning}> {t("priceDisplay.stale")}</span>}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { CSSProperties } from "react";
|
import { CSSProperties } from "react";
|
||||||
|
import { useTranslation } from "../../hooks/useTranslation";
|
||||||
|
|
||||||
type WizardStep = "details" | "booking" | "confirmation";
|
type WizardStep = "details" | "booking" | "confirmation";
|
||||||
|
|
||||||
|
|
@ -58,6 +59,7 @@ const styles: Record<string, CSSProperties> = {
|
||||||
* Shows which step the user is currently on and which steps are completed.
|
* Shows which step the user is currently on and which steps are completed.
|
||||||
*/
|
*/
|
||||||
export function StepIndicator({ currentStep }: StepIndicatorProps) {
|
export function StepIndicator({ currentStep }: StepIndicatorProps) {
|
||||||
|
const t = useTranslation("exchange");
|
||||||
return (
|
return (
|
||||||
<div style={styles.stepIndicator}>
|
<div style={styles.stepIndicator}>
|
||||||
<div
|
<div
|
||||||
|
|
@ -67,7 +69,7 @@ export function StepIndicator({ currentStep }: StepIndicatorProps) {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span style={styles.stepNumber}>1</span>
|
<span style={styles.stepNumber}>1</span>
|
||||||
<span style={styles.stepLabel}>Exchange Details</span>
|
<span style={styles.stepLabel}>{t("steps.details")}</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={styles.stepDivider} />
|
<div style={styles.stepDivider} />
|
||||||
<div
|
<div
|
||||||
|
|
@ -81,7 +83,7 @@ export function StepIndicator({ currentStep }: StepIndicatorProps) {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span style={styles.stepNumber}>2</span>
|
<span style={styles.stepNumber}>2</span>
|
||||||
<span style={styles.stepLabel}>Book Appointment</span>
|
<span style={styles.stepLabel}>{t("steps.booking")}</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={styles.stepDivider} />
|
<div style={styles.stepDivider} />
|
||||||
<div
|
<div
|
||||||
|
|
@ -91,7 +93,7 @@ export function StepIndicator({ currentStep }: StepIndicatorProps) {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span style={styles.stepNumber}>3</span>
|
<span style={styles.stepNumber}>3</span>
|
||||||
<span style={styles.stepLabel}>Confirm</span>
|
<span style={styles.stepLabel}>{t("steps.confirm")}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import { StepIndicator } from "./components/StepIndicator";
|
||||||
import { ExchangeDetailsStep } from "./components/ExchangeDetailsStep";
|
import { ExchangeDetailsStep } from "./components/ExchangeDetailsStep";
|
||||||
import { BookingStep } from "./components/BookingStep";
|
import { BookingStep } from "./components/BookingStep";
|
||||||
import { ConfirmationStep } from "./components/ConfirmationStep";
|
import { ConfirmationStep } from "./components/ConfirmationStep";
|
||||||
|
import { useTranslation } from "../hooks/useTranslation";
|
||||||
|
|
||||||
type ExchangeResponse = components["schemas"]["ExchangeResponse"];
|
type ExchangeResponse = components["schemas"]["ExchangeResponse"];
|
||||||
|
|
||||||
|
|
@ -57,6 +58,7 @@ const styles = {
|
||||||
|
|
||||||
export default function ExchangePage() {
|
export default function ExchangePage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const t = useTranslation("exchange");
|
||||||
const { user, isLoading, isAuthorized } = useRequireAuth({
|
const { user, isLoading, isAuthorized } = useRequireAuth({
|
||||||
requiredPermission: Permission.CREATE_EXCHANGE,
|
requiredPermission: Permission.CREATE_EXCHANGE,
|
||||||
fallbackRedirect: "/",
|
fallbackRedirect: "/",
|
||||||
|
|
@ -281,8 +283,8 @@ export default function ExchangePage() {
|
||||||
<main style={layoutStyles.main}>
|
<main style={layoutStyles.main}>
|
||||||
<Header currentPage="exchange" />
|
<Header currentPage="exchange" />
|
||||||
<div style={styles.content}>
|
<div style={styles.content}>
|
||||||
<h1 style={typographyStyles.pageTitle}>Exchange Bitcoin</h1>
|
<h1 style={typographyStyles.pageTitle}>{t("page.title")}</h1>
|
||||||
<p style={typographyStyles.pageSubtitle}>Buy or sell Bitcoin with a 5% premium</p>
|
<p style={typographyStyles.pageSubtitle}>{t("page.subtitle")}</p>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div style={bannerStyles.errorBanner}>
|
<div style={bannerStyles.errorBanner}>
|
||||||
|
|
@ -290,7 +292,7 @@ export default function ExchangePage() {
|
||||||
{existingTradeId && (
|
{existingTradeId && (
|
||||||
<div style={styles.errorLink}>
|
<div style={styles.errorLink}>
|
||||||
<a href={`/trades/${existingTradeId}`} style={styles.errorLinkAnchor}>
|
<a href={`/trades/${existingTradeId}`} style={styles.errorLinkAnchor}>
|
||||||
View your existing trade →
|
{t("page.viewExistingTrade")}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,13 @@ import { components } from "../generated/api";
|
||||||
import constants from "../../../shared/constants.json";
|
import constants from "../../../shared/constants.json";
|
||||||
import { Permission } from "../auth-context";
|
import { Permission } from "../auth-context";
|
||||||
import { cardStyles, typographyStyles, buttonStyles } from "../styles/shared";
|
import { cardStyles, typographyStyles, buttonStyles } from "../styles/shared";
|
||||||
|
import { useTranslation } from "../hooks/useTranslation";
|
||||||
|
|
||||||
// Use generated type from OpenAPI schema
|
// Use generated type from OpenAPI schema
|
||||||
type Invite = components["schemas"]["UserInviteResponse"];
|
type Invite = components["schemas"]["UserInviteResponse"];
|
||||||
|
|
||||||
export default function InvitesPage() {
|
export default function InvitesPage() {
|
||||||
|
const t = useTranslation("invites");
|
||||||
const { user, isLoading, isAuthorized } = useRequireAuth({
|
const { user, isLoading, isAuthorized } = useRequireAuth({
|
||||||
requiredPermission: Permission.VIEW_OWN_INVITES,
|
requiredPermission: Permission.VIEW_OWN_INVITES,
|
||||||
fallbackRedirect: "/admin/trades",
|
fallbackRedirect: "/admin/trades",
|
||||||
|
|
@ -62,26 +64,21 @@ export default function InvitesPage() {
|
||||||
>
|
>
|
||||||
<div style={styles.pageCard}>
|
<div style={styles.pageCard}>
|
||||||
<div style={cardStyles.cardHeader}>
|
<div style={cardStyles.cardHeader}>
|
||||||
<h1 style={cardStyles.cardTitle}>My Invites</h1>
|
<h1 style={cardStyles.cardTitle}>{t("page.title")}</h1>
|
||||||
<p style={cardStyles.cardSubtitle}>
|
<p style={cardStyles.cardSubtitle}>{t("page.subtitle")}</p>
|
||||||
Share your invite codes with friends to let them join
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(invites?.length ?? 0) === 0 ? (
|
{(invites?.length ?? 0) === 0 ? (
|
||||||
<EmptyState
|
<EmptyState message={t("page.noInvites")} hint={t("page.noInvitesHint")} />
|
||||||
message="You don't have any invites yet."
|
|
||||||
hint="Contact an admin if you need invite codes to share."
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<div style={styles.sections}>
|
<div style={styles.sections}>
|
||||||
{/* Ready Invites */}
|
{/* Ready Invites */}
|
||||||
{readyInvites.length > 0 && (
|
{readyInvites.length > 0 && (
|
||||||
<div style={styles.section}>
|
<div style={styles.section}>
|
||||||
<h2 style={typographyStyles.sectionTitle}>Available ({readyInvites.length})</h2>
|
<h2 style={typographyStyles.sectionTitle}>
|
||||||
<p style={typographyStyles.sectionHint}>
|
{t("page.available", { count: readyInvites.length })}
|
||||||
Share these links with people you want to invite
|
</h2>
|
||||||
</p>
|
<p style={typographyStyles.sectionHint}>{t("page.availableHint")}</p>
|
||||||
<div style={styles.inviteList}>
|
<div style={styles.inviteList}>
|
||||||
{readyInvites.map((invite) => (
|
{readyInvites.map((invite) => (
|
||||||
<div key={invite.id} style={styles.inviteCard}>
|
<div key={invite.id} style={styles.inviteCard}>
|
||||||
|
|
@ -91,7 +88,7 @@ export default function InvitesPage() {
|
||||||
onClick={() => copyToClipboard(invite)}
|
onClick={() => copyToClipboard(invite)}
|
||||||
style={buttonStyles.accentButton}
|
style={buttonStyles.accentButton}
|
||||||
>
|
>
|
||||||
{copiedId === invite.id ? "Copied!" : "Copy Link"}
|
{copiedId === invite.id ? t("page.copied") : t("page.copyLink")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -103,14 +100,20 @@ export default function InvitesPage() {
|
||||||
{/* Spent Invites */}
|
{/* Spent Invites */}
|
||||||
{spentInvites.length > 0 && (
|
{spentInvites.length > 0 && (
|
||||||
<div style={styles.section}>
|
<div style={styles.section}>
|
||||||
<h2 style={typographyStyles.sectionTitle}>Used ({spentInvites.length})</h2>
|
<h2 style={typographyStyles.sectionTitle}>
|
||||||
|
{t("page.used", { count: spentInvites.length })}
|
||||||
|
</h2>
|
||||||
<div style={styles.inviteList}>
|
<div style={styles.inviteList}>
|
||||||
{spentInvites.map((invite) => (
|
{spentInvites.map((invite) => (
|
||||||
<div key={invite.id} style={styles.inviteCardSpent}>
|
<div key={invite.id} style={styles.inviteCardSpent}>
|
||||||
<div style={styles.inviteCode}>{invite.identifier}</div>
|
<div style={styles.inviteCode}>{invite.identifier}</div>
|
||||||
<div style={styles.inviteeMeta}>
|
<div style={styles.inviteeMeta}>
|
||||||
<StatusBadge variant="success">Used</StatusBadge>
|
<StatusBadge variant="success">{t("page.usedStatus")}</StatusBadge>
|
||||||
<span style={styles.inviteeEmail}>by {invite.used_by_email}</span>
|
{invite.used_by_email && (
|
||||||
|
<span style={styles.inviteeEmail}>
|
||||||
|
{t("page.usedBy", { email: invite.used_by_email })}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
@ -121,12 +124,14 @@ export default function InvitesPage() {
|
||||||
{/* Revoked Invites */}
|
{/* Revoked Invites */}
|
||||||
{revokedInvites.length > 0 && (
|
{revokedInvites.length > 0 && (
|
||||||
<div style={styles.section}>
|
<div style={styles.section}>
|
||||||
<h2 style={typographyStyles.sectionTitle}>Revoked ({revokedInvites.length})</h2>
|
<h2 style={typographyStyles.sectionTitle}>
|
||||||
|
{t("page.revoked", { count: revokedInvites.length })}
|
||||||
|
</h2>
|
||||||
<div style={styles.inviteList}>
|
<div style={styles.inviteList}>
|
||||||
{revokedInvites.map((invite) => (
|
{revokedInvites.map((invite) => (
|
||||||
<div key={invite.id} style={styles.inviteCardRevoked}>
|
<div key={invite.id} style={styles.inviteCardRevoked}>
|
||||||
<div style={styles.inviteCode}>{invite.identifier}</div>
|
<div style={styles.inviteCode}>{invite.identifier}</div>
|
||||||
<StatusBadge variant="error">Revoked</StatusBadge>
|
<StatusBadge variant="error">{t("page.revokedStatus")}</StatusBadge>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import {
|
||||||
utilityStyles,
|
utilityStyles,
|
||||||
} from "../styles/shared";
|
} from "../styles/shared";
|
||||||
import { validateProfileFields } from "../utils/validation";
|
import { validateProfileFields } from "../utils/validation";
|
||||||
|
import { useTranslation } from "../hooks/useTranslation";
|
||||||
|
|
||||||
// Use generated type from OpenAPI schema
|
// Use generated type from OpenAPI schema
|
||||||
type ProfileData = components["schemas"]["ProfileResponse"];
|
type ProfileData = components["schemas"]["ProfileResponse"];
|
||||||
|
|
@ -41,6 +42,7 @@ function toFormData(data: ProfileData): FormData {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ProfilePage() {
|
export default function ProfilePage() {
|
||||||
|
const t = useTranslation("profile");
|
||||||
const { user, isLoading, isAuthorized } = useRequireAuth({
|
const { user, isLoading, isAuthorized } = useRequireAuth({
|
||||||
requiredPermission: Permission.MANAGE_OWN_PROFILE,
|
requiredPermission: Permission.MANAGE_OWN_PROFILE,
|
||||||
fallbackRedirect: "/admin/trades",
|
fallbackRedirect: "/admin/trades",
|
||||||
|
|
@ -88,7 +90,7 @@ export default function ProfilePage() {
|
||||||
setGodfatherEmail(data.godfather_email ?? null);
|
setGodfatherEmail(data.godfather_email ?? null);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Profile load error:", err);
|
console.error("Profile load error:", err);
|
||||||
setToast({ message: "Failed to load profile", type: "error" });
|
setToast({ message: t("messages.loadError"), type: "error" });
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoadingProfile(false);
|
setIsLoadingProfile(false);
|
||||||
}
|
}
|
||||||
|
|
@ -141,16 +143,16 @@ export default function ProfilePage() {
|
||||||
const formValues = toFormData(data);
|
const formValues = toFormData(data);
|
||||||
setFormData(formValues);
|
setFormData(formValues);
|
||||||
setOriginalData(formValues);
|
setOriginalData(formValues);
|
||||||
setToast({ message: "Profile saved successfully!", type: "success" });
|
setToast({ message: t("messages.saveSuccess"), type: "success" });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Profile save error:", err);
|
console.error("Profile save error:", err);
|
||||||
const fieldErrors = extractFieldErrors(err);
|
const fieldErrors = extractFieldErrors(err);
|
||||||
if (fieldErrors?.detail?.field_errors) {
|
if (fieldErrors?.detail?.field_errors) {
|
||||||
setErrors(fieldErrors.detail.field_errors);
|
setErrors(fieldErrors.detail.field_errors);
|
||||||
setToast({ message: "Please fix the errors below", type: "error" });
|
setToast({ message: t("messages.fixErrors"), type: "error" });
|
||||||
} else {
|
} else {
|
||||||
setToast({
|
setToast({
|
||||||
message: extractApiErrorMessage(err, "Network error. Please try again."),
|
message: extractApiErrorMessage(err, t("messages.networkError")),
|
||||||
type: "error",
|
type: "error",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -181,16 +183,16 @@ export default function ProfilePage() {
|
||||||
<div style={layoutStyles.contentCentered}>
|
<div style={layoutStyles.contentCentered}>
|
||||||
<div style={styles.profileCard}>
|
<div style={styles.profileCard}>
|
||||||
<div style={cardStyles.cardHeader}>
|
<div style={cardStyles.cardHeader}>
|
||||||
<h1 style={cardStyles.cardTitle}>My Profile</h1>
|
<h1 style={cardStyles.cardTitle}>{t("page.title")}</h1>
|
||||||
<p style={cardStyles.cardSubtitle}>Manage your contact information</p>
|
<p style={cardStyles.cardSubtitle}>{t("page.subtitle")}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} style={formStyles.form}>
|
<form onSubmit={handleSubmit} style={formStyles.form}>
|
||||||
{/* Login email - read only */}
|
{/* Login email - read only */}
|
||||||
<div style={formStyles.field}>
|
<div style={formStyles.field}>
|
||||||
<label style={styles.labelWithBadge}>
|
<label style={styles.labelWithBadge}>
|
||||||
Login Email
|
{t("form.email")}
|
||||||
<span style={utilityStyles.readOnlyBadge}>Read only</span>
|
<span style={utilityStyles.readOnlyBadge}>{t("form.readOnly")}</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
|
|
@ -198,36 +200,32 @@ export default function ProfilePage() {
|
||||||
style={{ ...formStyles.input, ...formStyles.inputReadOnly }}
|
style={{ ...formStyles.input, ...formStyles.inputReadOnly }}
|
||||||
disabled
|
disabled
|
||||||
/>
|
/>
|
||||||
<span style={formStyles.hint}>
|
<span style={formStyles.hint}>{t("form.emailHint")}</span>
|
||||||
This is your login email and cannot be changed here.
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Godfather - shown if user was invited */}
|
{/* Godfather - shown if user was invited */}
|
||||||
{godfatherEmail && (
|
{godfatherEmail && (
|
||||||
<div style={formStyles.field}>
|
<div style={formStyles.field}>
|
||||||
<label style={styles.labelWithBadge}>
|
<label style={styles.labelWithBadge}>
|
||||||
Invited By
|
{t("form.invitedBy")}
|
||||||
<span style={utilityStyles.readOnlyBadge}>Read only</span>
|
<span style={utilityStyles.readOnlyBadge}>{t("form.readOnly")}</span>
|
||||||
</label>
|
</label>
|
||||||
<div style={styles.godfatherBox}>
|
<div style={styles.godfatherBox}>
|
||||||
<span style={styles.godfatherEmail}>{godfatherEmail}</span>
|
<span style={styles.godfatherEmail}>{godfatherEmail}</span>
|
||||||
</div>
|
</div>
|
||||||
<span style={formStyles.hint}>The user who invited you to join.</span>
|
<span style={formStyles.hint}>{t("form.invitedByHint")}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div style={utilityStyles.divider} />
|
<div style={utilityStyles.divider} />
|
||||||
|
|
||||||
<p style={styles.sectionLabel}>Contact Details</p>
|
<p style={styles.sectionLabel}>{t("form.contactDetails")}</p>
|
||||||
<p style={styles.sectionHint}>
|
<p style={styles.sectionHint}>{t("form.contactDetailsHint")}</p>
|
||||||
These are for communication purposes only — they won't affect your login.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* Contact email */}
|
{/* Contact email */}
|
||||||
<div style={formStyles.field}>
|
<div style={formStyles.field}>
|
||||||
<label htmlFor="contact_email" style={formStyles.label}>
|
<label htmlFor="contact_email" style={formStyles.label}>
|
||||||
Contact Email
|
{t("form.contactEmail")}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="contact_email"
|
id="contact_email"
|
||||||
|
|
@ -238,7 +236,7 @@ export default function ProfilePage() {
|
||||||
...formStyles.input,
|
...formStyles.input,
|
||||||
...(errors.contact_email ? formStyles.inputError : {}),
|
...(errors.contact_email ? formStyles.inputError : {}),
|
||||||
}}
|
}}
|
||||||
placeholder="alternate@example.com"
|
placeholder={t("placeholders.contactEmail")}
|
||||||
/>
|
/>
|
||||||
{errors.contact_email && (
|
{errors.contact_email && (
|
||||||
<span style={formStyles.errorText}>{errors.contact_email}</span>
|
<span style={formStyles.errorText}>{errors.contact_email}</span>
|
||||||
|
|
@ -248,7 +246,7 @@ export default function ProfilePage() {
|
||||||
{/* Telegram */}
|
{/* Telegram */}
|
||||||
<div style={formStyles.field}>
|
<div style={formStyles.field}>
|
||||||
<label htmlFor="telegram" style={formStyles.label}>
|
<label htmlFor="telegram" style={formStyles.label}>
|
||||||
Telegram
|
{t("form.telegram")}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="telegram"
|
id="telegram"
|
||||||
|
|
@ -259,7 +257,7 @@ export default function ProfilePage() {
|
||||||
...formStyles.input,
|
...formStyles.input,
|
||||||
...(errors.telegram ? formStyles.inputError : {}),
|
...(errors.telegram ? formStyles.inputError : {}),
|
||||||
}}
|
}}
|
||||||
placeholder="@username"
|
placeholder={t("placeholders.telegram")}
|
||||||
/>
|
/>
|
||||||
{errors.telegram && <span style={formStyles.errorText}>{errors.telegram}</span>}
|
{errors.telegram && <span style={formStyles.errorText}>{errors.telegram}</span>}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -267,7 +265,7 @@ export default function ProfilePage() {
|
||||||
{/* Signal */}
|
{/* Signal */}
|
||||||
<div style={formStyles.field}>
|
<div style={formStyles.field}>
|
||||||
<label htmlFor="signal" style={formStyles.label}>
|
<label htmlFor="signal" style={formStyles.label}>
|
||||||
Signal
|
{t("form.signal")}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="signal"
|
id="signal"
|
||||||
|
|
@ -278,7 +276,7 @@ export default function ProfilePage() {
|
||||||
...formStyles.input,
|
...formStyles.input,
|
||||||
...(errors.signal ? formStyles.inputError : {}),
|
...(errors.signal ? formStyles.inputError : {}),
|
||||||
}}
|
}}
|
||||||
placeholder="username.01"
|
placeholder={t("placeholders.signal")}
|
||||||
/>
|
/>
|
||||||
{errors.signal && <span style={formStyles.errorText}>{errors.signal}</span>}
|
{errors.signal && <span style={formStyles.errorText}>{errors.signal}</span>}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -286,7 +284,7 @@ export default function ProfilePage() {
|
||||||
{/* Nostr npub */}
|
{/* Nostr npub */}
|
||||||
<div style={formStyles.field}>
|
<div style={formStyles.field}>
|
||||||
<label htmlFor="nostr_npub" style={formStyles.label}>
|
<label htmlFor="nostr_npub" style={formStyles.label}>
|
||||||
Nostr (npub)
|
{t("form.nostrNpub")}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="nostr_npub"
|
id="nostr_npub"
|
||||||
|
|
@ -297,7 +295,7 @@ export default function ProfilePage() {
|
||||||
...formStyles.input,
|
...formStyles.input,
|
||||||
...(errors.nostr_npub ? formStyles.inputError : {}),
|
...(errors.nostr_npub ? formStyles.inputError : {}),
|
||||||
}}
|
}}
|
||||||
placeholder="npub1..."
|
placeholder={t("placeholders.nostrNpub")}
|
||||||
/>
|
/>
|
||||||
{errors.nostr_npub && <span style={formStyles.errorText}>{errors.nostr_npub}</span>}
|
{errors.nostr_npub && <span style={formStyles.errorText}>{errors.nostr_npub}</span>}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -311,7 +309,7 @@ export default function ProfilePage() {
|
||||||
}}
|
}}
|
||||||
disabled={!canSubmit}
|
disabled={!canSubmit}
|
||||||
>
|
>
|
||||||
{isSubmitting ? "Saving..." : "Save Changes"}
|
{isSubmitting ? t("form.saving") : t("form.saveChanges")}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,12 @@ import { useMutation } from "../hooks/useMutation";
|
||||||
import { formatDateTime } from "../utils/date";
|
import { formatDateTime } from "../utils/date";
|
||||||
import { formatEur } from "../utils/exchange";
|
import { formatEur } from "../utils/exchange";
|
||||||
import { typographyStyles, tradeCardStyles } from "../styles/shared";
|
import { typographyStyles, tradeCardStyles } from "../styles/shared";
|
||||||
|
import { useTranslation } from "../hooks/useTranslation";
|
||||||
|
|
||||||
export default function TradesPage() {
|
export default function TradesPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const t = useTranslation("trades");
|
||||||
|
const tExchange = useTranslation("exchange");
|
||||||
const { user, isLoading, isAuthorized } = useRequireAuth({
|
const { user, isLoading, isAuthorized } = useRequireAuth({
|
||||||
requiredPermission: Permission.VIEW_OWN_EXCHANGES,
|
requiredPermission: Permission.VIEW_OWN_EXCHANGES,
|
||||||
fallbackRedirect: "/",
|
fallbackRedirect: "/",
|
||||||
|
|
@ -70,17 +73,17 @@ export default function TradesPage() {
|
||||||
error={error}
|
error={error}
|
||||||
contentStyle={styles.content}
|
contentStyle={styles.content}
|
||||||
>
|
>
|
||||||
<h1 style={typographyStyles.pageTitle}>My Trades</h1>
|
<h1 style={typographyStyles.pageTitle}>{t("page.title")}</h1>
|
||||||
<p style={typographyStyles.pageSubtitle}>View and manage your Bitcoin trades</p>
|
<p style={typographyStyles.pageSubtitle}>{t("page.subtitle")}</p>
|
||||||
|
|
||||||
{isLoadingTrades ? (
|
{isLoadingTrades ? (
|
||||||
<EmptyState message="Loading trades..." isLoading={true} />
|
<EmptyState message={t("page.loadingTrades")} isLoading={true} />
|
||||||
) : (trades?.length ?? 0) === 0 ? (
|
) : (trades?.length ?? 0) === 0 ? (
|
||||||
<EmptyState
|
<EmptyState
|
||||||
message="You don't have any trades yet."
|
message={t("page.noTrades")}
|
||||||
action={
|
action={
|
||||||
<a href="/exchange" style={styles.emptyStateLink}>
|
<a href="/exchange" style={styles.emptyStateLink}>
|
||||||
Start trading
|
{t("page.startTrading")}
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
@ -89,7 +92,9 @@ export default function TradesPage() {
|
||||||
{/* Upcoming Trades */}
|
{/* Upcoming Trades */}
|
||||||
{upcomingTrades.length > 0 && (
|
{upcomingTrades.length > 0 && (
|
||||||
<div style={styles.section}>
|
<div style={styles.section}>
|
||||||
<h2 style={styles.sectionTitle}>Upcoming ({upcomingTrades.length})</h2>
|
<h2 style={styles.sectionTitle}>
|
||||||
|
{t("page.upcoming", { count: upcomingTrades.length })}
|
||||||
|
</h2>
|
||||||
<div style={tradeCardStyles.tradeList}>
|
<div style={tradeCardStyles.tradeList}>
|
||||||
{upcomingTrades.map((trade) => {
|
{upcomingTrades.map((trade) => {
|
||||||
const isBuy = trade.direction === "buy";
|
const isBuy = trade.direction === "buy";
|
||||||
|
|
@ -110,7 +115,7 @@ export default function TradesPage() {
|
||||||
color: isBuy ? "#4ade80" : "#f87171",
|
color: isBuy ? "#4ade80" : "#f87171",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isBuy ? "BUY BTC" : "SELL BTC"}
|
{isBuy ? tExchange("direction.buy") : tExchange("direction.sell")}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -120,8 +125,8 @@ export default function TradesPage() {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isBuy
|
{isBuy
|
||||||
? `Receive via ${trade.bitcoin_transfer_method === "onchain" ? "Onchain" : "Lightning"}`
|
? `${tExchange("bookingStep.receiveVia")} ${trade.bitcoin_transfer_method === "onchain" ? tExchange("transferMethod.onchain") : tExchange("transferMethod.lightning")}`
|
||||||
: `Send via ${trade.bitcoin_transfer_method === "onchain" ? "Onchain" : "Lightning"}`}
|
: `${tExchange("bookingStep.sendVia")} ${trade.bitcoin_transfer_method === "onchain" ? tExchange("transferMethod.onchain") : tExchange("transferMethod.lightning")}`}
|
||||||
</span>
|
</span>
|
||||||
<span style={tradeCardStyles.amount}>
|
<span style={tradeCardStyles.amount}>
|
||||||
{formatEur(trade.eur_amount)}
|
{formatEur(trade.eur_amount)}
|
||||||
|
|
@ -132,7 +137,7 @@ export default function TradesPage() {
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={tradeCardStyles.rateRow}>
|
<div style={tradeCardStyles.rateRow}>
|
||||||
<span style={tradeCardStyles.rateLabel}>Rate:</span>
|
<span style={tradeCardStyles.rateLabel}>{t("trade.rate")}</span>
|
||||||
<span style={tradeCardStyles.rateValue}>
|
<span style={tradeCardStyles.rateValue}>
|
||||||
€
|
€
|
||||||
{trade.agreed_price_eur.toLocaleString("de-DE", {
|
{trade.agreed_price_eur.toLocaleString("de-DE", {
|
||||||
|
|
@ -153,7 +158,7 @@ export default function TradesPage() {
|
||||||
onConfirm={() => handleCancel(trade.public_id)}
|
onConfirm={() => handleCancel(trade.public_id)}
|
||||||
onCancel={() => setConfirmCancelId(null)}
|
onCancel={() => setConfirmCancelId(null)}
|
||||||
onActionClick={() => setConfirmCancelId(trade.public_id)}
|
onActionClick={() => setConfirmCancelId(trade.public_id)}
|
||||||
actionLabel="Cancel"
|
actionLabel={t("trade.cancel")}
|
||||||
isLoading={cancellingId === trade.public_id}
|
isLoading={cancellingId === trade.public_id}
|
||||||
confirmVariant="danger"
|
confirmVariant="danger"
|
||||||
confirmButtonStyle={styles.confirmButton}
|
confirmButtonStyle={styles.confirmButton}
|
||||||
|
|
@ -166,7 +171,7 @@ export default function TradesPage() {
|
||||||
}}
|
}}
|
||||||
style={styles.viewDetailsButton}
|
style={styles.viewDetailsButton}
|
||||||
>
|
>
|
||||||
View Details
|
{t("trade.viewDetails")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -181,7 +186,7 @@ export default function TradesPage() {
|
||||||
{pastOrFinalTrades.length > 0 && (
|
{pastOrFinalTrades.length > 0 && (
|
||||||
<div style={styles.section}>
|
<div style={styles.section}>
|
||||||
<h2 style={typographyStyles.sectionTitleMuted}>
|
<h2 style={typographyStyles.sectionTitleMuted}>
|
||||||
History ({pastOrFinalTrades.length})
|
{t("page.history", { count: pastOrFinalTrades.length })}
|
||||||
</h2>
|
</h2>
|
||||||
<div style={tradeCardStyles.tradeList}>
|
<div style={tradeCardStyles.tradeList}>
|
||||||
{pastOrFinalTrades.map((trade) => {
|
{pastOrFinalTrades.map((trade) => {
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,70 @@
|
||||||
},
|
},
|
||||||
"direction": {
|
"direction": {
|
||||||
"buy": "COMPRAR BTC",
|
"buy": "COMPRAR BTC",
|
||||||
"sell": "VENDRE BTC"
|
"sell": "VENDRE BTC",
|
||||||
|
"buyShort": "Comprar BTC",
|
||||||
|
"sellShort": "Vendre BTC"
|
||||||
},
|
},
|
||||||
"transferMethod": {
|
"transferMethod": {
|
||||||
"onchain": "Onchain",
|
"onchain": "Onchain",
|
||||||
"lightning": "Lightning"
|
"lightning": "Lightning"
|
||||||
|
},
|
||||||
|
"page": {
|
||||||
|
"title": "Intercanviar Bitcoin",
|
||||||
|
"subtitle": "Compra o ven Bitcoin amb una prima del 5%",
|
||||||
|
"viewExistingTrade": "Veure la teva operació existent →"
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"details": "Detalls de l'Intercanvi",
|
||||||
|
"booking": "Reservar Cita",
|
||||||
|
"confirm": "Confirmar"
|
||||||
|
},
|
||||||
|
"detailsStep": {
|
||||||
|
"paymentMethod": "Mètode de Pagament",
|
||||||
|
"required": "*",
|
||||||
|
"lightningThreshold": "Els pagaments Lightning només estan disponibles per importants fins a €{max}",
|
||||||
|
"amount": "Quantitat (EUR)",
|
||||||
|
"summaryBuy": "Compres {sats}, vens {eur}",
|
||||||
|
"summarySell": "Compres {eur}, vens {sats}",
|
||||||
|
"continueToBooking": "Continuar a Reserva"
|
||||||
|
},
|
||||||
|
"bookingStep": {
|
||||||
|
"yourExchange": "El Teu Intercanvi",
|
||||||
|
"edit": "Editar",
|
||||||
|
"buy": "Comprar",
|
||||||
|
"sell": "Vendre",
|
||||||
|
"receiveVia": "Rebre via",
|
||||||
|
"sendVia": "Enviar via",
|
||||||
|
"selectDate": "Seleccionar una Data",
|
||||||
|
"existingTradeWarning": "Ja tens una operació reservada en aquest dia. Només pots reservar una operació per dia.",
|
||||||
|
"viewExistingTrade": "Veure la teva operació existent →",
|
||||||
|
"availableSlots": "Espais Disponibles per a",
|
||||||
|
"loadingSlots": "Carregant espais...",
|
||||||
|
"noSlots": "No hi ha espais disponibles per a aquesta data"
|
||||||
|
},
|
||||||
|
"confirmationStep": {
|
||||||
|
"appointment": "Cita",
|
||||||
|
"edit": "Editar",
|
||||||
|
"confirmTrade": "Confirmar Operació",
|
||||||
|
"time": "Hora:",
|
||||||
|
"direction": "Direcció:",
|
||||||
|
"eur": "EUR:",
|
||||||
|
"btc": "BTC:",
|
||||||
|
"rate": "Taxa:",
|
||||||
|
"payment": "Pagament:",
|
||||||
|
"receiveVia": "Rebre via",
|
||||||
|
"sendVia": "Enviar via",
|
||||||
|
"booking": "Reservant...",
|
||||||
|
"priceStale": "Preu Desactualitzat",
|
||||||
|
"confirmBuy": "Confirmar Compra",
|
||||||
|
"confirmSell": "Confirmar Venda",
|
||||||
|
"back": "Enrere"
|
||||||
|
},
|
||||||
|
"priceDisplay": {
|
||||||
|
"loading": "Carregant preu...",
|
||||||
|
"market": "Mercat:",
|
||||||
|
"ourPrice": "El nostre preu:",
|
||||||
|
"updated": "Actualitzat",
|
||||||
|
"stale": "(desactualitzat)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
17
frontend/locales/ca/invites.json
Normal file
17
frontend/locales/ca/invites.json
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"page": {
|
||||||
|
"title": "Les Meves Invitacions",
|
||||||
|
"subtitle": "Comparteix els teus codis d'invitació amb amics perquè s'uneixin",
|
||||||
|
"noInvites": "Encara no tens invitacions.",
|
||||||
|
"noInvitesHint": "Contacta amb un administrador si necessites codis d'invitació per compartir.",
|
||||||
|
"available": "Disponibles ({count})",
|
||||||
|
"availableHint": "Comparteix aquests enllaços amb les persones que vulguis convidar",
|
||||||
|
"used": "Usades ({count})",
|
||||||
|
"revoked": "Revocades ({count})",
|
||||||
|
"copyLink": "Copiar Enllaç",
|
||||||
|
"copied": "Copiat!",
|
||||||
|
"usedBy": "per {email}",
|
||||||
|
"usedStatus": "Usada",
|
||||||
|
"revokedStatus": "Revocada"
|
||||||
|
}
|
||||||
|
}
|
||||||
33
frontend/locales/ca/profile.json
Normal file
33
frontend/locales/ca/profile.json
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
{
|
||||||
|
"page": {
|
||||||
|
"title": "El Meu Perfil",
|
||||||
|
"subtitle": "Gestiona la teva informació de contacte"
|
||||||
|
},
|
||||||
|
"form": {
|
||||||
|
"email": "Correu Electrònic",
|
||||||
|
"emailHint": "Aquest és el teu correu d'inici de sessió i no es pot canviar aquí.",
|
||||||
|
"invitedBy": "Convidat Per",
|
||||||
|
"invitedByHint": "L'usuari que et va convidar a unir-te.",
|
||||||
|
"readOnly": "Només lectura",
|
||||||
|
"contactDetails": "Detalls de Contacte",
|
||||||
|
"contactDetailsHint": "Aquests són només per a fins de comunicació — no afectaran el teu inici de sessió.",
|
||||||
|
"contactEmail": "Correu de Contacte",
|
||||||
|
"telegram": "Telegram",
|
||||||
|
"signal": "Signal",
|
||||||
|
"nostrNpub": "Nostr (npub)",
|
||||||
|
"saving": "Desant...",
|
||||||
|
"saveChanges": "Desar Canvis"
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"loadError": "Error en carregar el perfil",
|
||||||
|
"saveSuccess": "Perfil desat amb èxit!",
|
||||||
|
"fixErrors": "Si us plau, corregeix els errors a continuació",
|
||||||
|
"networkError": "Error de xarxa. Si us plau, intenta-ho de nou."
|
||||||
|
},
|
||||||
|
"placeholders": {
|
||||||
|
"contactEmail": "alternate@example.com",
|
||||||
|
"telegram": "@username",
|
||||||
|
"signal": "username.01",
|
||||||
|
"nostrNpub": "npub1..."
|
||||||
|
}
|
||||||
|
}
|
||||||
16
frontend/locales/ca/trades.json
Normal file
16
frontend/locales/ca/trades.json
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"page": {
|
||||||
|
"title": "Les Meves Operacions",
|
||||||
|
"subtitle": "Veure i gestionar les teves operacions de Bitcoin",
|
||||||
|
"loadingTrades": "Carregant operacions...",
|
||||||
|
"noTrades": "Encara no tens operacions.",
|
||||||
|
"startTrading": "Començar a operar",
|
||||||
|
"upcoming": "Properes ({count})",
|
||||||
|
"history": "Historial ({count})"
|
||||||
|
},
|
||||||
|
"trade": {
|
||||||
|
"rate": "Taxa:",
|
||||||
|
"cancel": "Cancel·lar",
|
||||||
|
"viewDetails": "Veure Detalls"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,10 +8,70 @@
|
||||||
},
|
},
|
||||||
"direction": {
|
"direction": {
|
||||||
"buy": "BUY BTC",
|
"buy": "BUY BTC",
|
||||||
"sell": "SELL BTC"
|
"sell": "SELL BTC",
|
||||||
|
"buyShort": "Buy BTC",
|
||||||
|
"sellShort": "Sell BTC"
|
||||||
},
|
},
|
||||||
"transferMethod": {
|
"transferMethod": {
|
||||||
"onchain": "Onchain",
|
"onchain": "Onchain",
|
||||||
"lightning": "Lightning"
|
"lightning": "Lightning"
|
||||||
|
},
|
||||||
|
"page": {
|
||||||
|
"title": "Exchange Bitcoin",
|
||||||
|
"subtitle": "Buy or sell Bitcoin with a 5% premium",
|
||||||
|
"viewExistingTrade": "View your existing trade →"
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"details": "Exchange Details",
|
||||||
|
"booking": "Book Appointment",
|
||||||
|
"confirm": "Confirm"
|
||||||
|
},
|
||||||
|
"detailsStep": {
|
||||||
|
"paymentMethod": "Payment Method",
|
||||||
|
"required": "*",
|
||||||
|
"lightningThreshold": "Lightning payments are only available for amounts up to €{max}",
|
||||||
|
"amount": "Amount (EUR)",
|
||||||
|
"summaryBuy": "You buy {sats}, you sell {eur}",
|
||||||
|
"summarySell": "You buy {eur}, you sell {sats}",
|
||||||
|
"continueToBooking": "Continue to Booking"
|
||||||
|
},
|
||||||
|
"bookingStep": {
|
||||||
|
"yourExchange": "Your Exchange",
|
||||||
|
"edit": "Edit",
|
||||||
|
"buy": "Buy",
|
||||||
|
"sell": "Sell",
|
||||||
|
"receiveVia": "Receive via",
|
||||||
|
"sendVia": "Send via",
|
||||||
|
"selectDate": "Select a Date",
|
||||||
|
"existingTradeWarning": "You already have a trade booked on this day. You can only book one trade per day.",
|
||||||
|
"viewExistingTrade": "View your existing trade →",
|
||||||
|
"availableSlots": "Available Slots for",
|
||||||
|
"loadingSlots": "Loading slots...",
|
||||||
|
"noSlots": "No available slots for this date"
|
||||||
|
},
|
||||||
|
"confirmationStep": {
|
||||||
|
"appointment": "Appointment",
|
||||||
|
"edit": "Edit",
|
||||||
|
"confirmTrade": "Confirm Trade",
|
||||||
|
"time": "Time:",
|
||||||
|
"direction": "Direction:",
|
||||||
|
"eur": "EUR:",
|
||||||
|
"btc": "BTC:",
|
||||||
|
"rate": "Rate:",
|
||||||
|
"payment": "Payment:",
|
||||||
|
"receiveVia": "Receive via",
|
||||||
|
"sendVia": "Send via",
|
||||||
|
"booking": "Booking...",
|
||||||
|
"priceStale": "Price Stale",
|
||||||
|
"confirmBuy": "Confirm Buy",
|
||||||
|
"confirmSell": "Confirm Sell",
|
||||||
|
"back": "Back"
|
||||||
|
},
|
||||||
|
"priceDisplay": {
|
||||||
|
"loading": "Loading price...",
|
||||||
|
"market": "Market:",
|
||||||
|
"ourPrice": "Our price:",
|
||||||
|
"updated": "Updated",
|
||||||
|
"stale": "(stale)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
17
frontend/locales/en/invites.json
Normal file
17
frontend/locales/en/invites.json
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"page": {
|
||||||
|
"title": "My Invites",
|
||||||
|
"subtitle": "Share your invite codes with friends to let them join",
|
||||||
|
"noInvites": "You don't have any invites yet.",
|
||||||
|
"noInvitesHint": "Contact an admin if you need invite codes to share.",
|
||||||
|
"available": "Available ({count})",
|
||||||
|
"availableHint": "Share these links with people you want to invite",
|
||||||
|
"used": "Used ({count})",
|
||||||
|
"revoked": "Revoked ({count})",
|
||||||
|
"copyLink": "Copy Link",
|
||||||
|
"copied": "Copied!",
|
||||||
|
"usedBy": "by {email}",
|
||||||
|
"usedStatus": "Used",
|
||||||
|
"revokedStatus": "Revoked"
|
||||||
|
}
|
||||||
|
}
|
||||||
33
frontend/locales/en/profile.json
Normal file
33
frontend/locales/en/profile.json
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
{
|
||||||
|
"page": {
|
||||||
|
"title": "My Profile",
|
||||||
|
"subtitle": "Manage your contact information"
|
||||||
|
},
|
||||||
|
"form": {
|
||||||
|
"email": "Email",
|
||||||
|
"emailHint": "This is your login email and cannot be changed here.",
|
||||||
|
"invitedBy": "Invited By",
|
||||||
|
"invitedByHint": "The user who invited you to join.",
|
||||||
|
"readOnly": "Read only",
|
||||||
|
"contactDetails": "Contact Details",
|
||||||
|
"contactDetailsHint": "These are for communication purposes only — they won't affect your login.",
|
||||||
|
"contactEmail": "Contact Email",
|
||||||
|
"telegram": "Telegram",
|
||||||
|
"signal": "Signal",
|
||||||
|
"nostrNpub": "Nostr (npub)",
|
||||||
|
"saving": "Saving...",
|
||||||
|
"saveChanges": "Save Changes"
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"loadError": "Failed to load profile",
|
||||||
|
"saveSuccess": "Profile saved successfully!",
|
||||||
|
"fixErrors": "Please fix the errors below",
|
||||||
|
"networkError": "Network error. Please try again."
|
||||||
|
},
|
||||||
|
"placeholders": {
|
||||||
|
"contactEmail": "alternate@example.com",
|
||||||
|
"telegram": "@username",
|
||||||
|
"signal": "username.01",
|
||||||
|
"nostrNpub": "npub1..."
|
||||||
|
}
|
||||||
|
}
|
||||||
16
frontend/locales/en/trades.json
Normal file
16
frontend/locales/en/trades.json
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"page": {
|
||||||
|
"title": "My Trades",
|
||||||
|
"subtitle": "View and manage your Bitcoin trades",
|
||||||
|
"loadingTrades": "Loading trades...",
|
||||||
|
"noTrades": "You don't have any trades yet.",
|
||||||
|
"startTrading": "Start trading",
|
||||||
|
"upcoming": "Upcoming ({count})",
|
||||||
|
"history": "History ({count})"
|
||||||
|
},
|
||||||
|
"trade": {
|
||||||
|
"rate": "Rate:",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"viewDetails": "View Details"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,10 +8,70 @@
|
||||||
},
|
},
|
||||||
"direction": {
|
"direction": {
|
||||||
"buy": "COMPRAR BTC",
|
"buy": "COMPRAR BTC",
|
||||||
"sell": "VENDER BTC"
|
"sell": "VENDER BTC",
|
||||||
|
"buyShort": "Comprar BTC",
|
||||||
|
"sellShort": "Vender BTC"
|
||||||
},
|
},
|
||||||
"transferMethod": {
|
"transferMethod": {
|
||||||
"onchain": "Onchain",
|
"onchain": "Onchain",
|
||||||
"lightning": "Lightning"
|
"lightning": "Lightning"
|
||||||
|
},
|
||||||
|
"page": {
|
||||||
|
"title": "Intercambiar Bitcoin",
|
||||||
|
"subtitle": "Compra o vende Bitcoin con una prima del 5%",
|
||||||
|
"viewExistingTrade": "Ver tu operación existente →"
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"details": "Detalles del Intercambio",
|
||||||
|
"booking": "Reservar Cita",
|
||||||
|
"confirm": "Confirmar"
|
||||||
|
},
|
||||||
|
"detailsStep": {
|
||||||
|
"paymentMethod": "Método de Pago",
|
||||||
|
"required": "*",
|
||||||
|
"lightningThreshold": "Los pagos Lightning solo están disponibles para montos de hasta €{max}",
|
||||||
|
"amount": "Cantidad (EUR)",
|
||||||
|
"summaryBuy": "Compras {sats}, vendes {eur}",
|
||||||
|
"summarySell": "Compras {eur}, vendes {sats}",
|
||||||
|
"continueToBooking": "Continuar a Reserva"
|
||||||
|
},
|
||||||
|
"bookingStep": {
|
||||||
|
"yourExchange": "Tu Intercambio",
|
||||||
|
"edit": "Editar",
|
||||||
|
"buy": "Comprar",
|
||||||
|
"sell": "Vender",
|
||||||
|
"receiveVia": "Recibir vía",
|
||||||
|
"sendVia": "Enviar vía",
|
||||||
|
"selectDate": "Seleccionar una Fecha",
|
||||||
|
"existingTradeWarning": "Ya tienes una operación reservada en este día. Solo puedes reservar una operación por día.",
|
||||||
|
"viewExistingTrade": "Ver tu operación existente →",
|
||||||
|
"availableSlots": "Espacios Disponibles para",
|
||||||
|
"loadingSlots": "Cargando espacios...",
|
||||||
|
"noSlots": "No hay espacios disponibles para esta fecha"
|
||||||
|
},
|
||||||
|
"confirmationStep": {
|
||||||
|
"appointment": "Cita",
|
||||||
|
"edit": "Editar",
|
||||||
|
"confirmTrade": "Confirmar Operación",
|
||||||
|
"time": "Hora:",
|
||||||
|
"direction": "Dirección:",
|
||||||
|
"eur": "EUR:",
|
||||||
|
"btc": "BTC:",
|
||||||
|
"rate": "Tasa:",
|
||||||
|
"payment": "Pago:",
|
||||||
|
"receiveVia": "Recibir vía",
|
||||||
|
"sendVia": "Enviar vía",
|
||||||
|
"booking": "Reservando...",
|
||||||
|
"priceStale": "Precio Desactualizado",
|
||||||
|
"confirmBuy": "Confirmar Compra",
|
||||||
|
"confirmSell": "Confirmar Venta",
|
||||||
|
"back": "Atrás"
|
||||||
|
},
|
||||||
|
"priceDisplay": {
|
||||||
|
"loading": "Cargando precio...",
|
||||||
|
"market": "Mercado:",
|
||||||
|
"ourPrice": "Nuestro precio:",
|
||||||
|
"updated": "Actualizado",
|
||||||
|
"stale": "(desactualizado)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
17
frontend/locales/es/invites.json
Normal file
17
frontend/locales/es/invites.json
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"page": {
|
||||||
|
"title": "Mis Invitaciones",
|
||||||
|
"subtitle": "Comparte tus códigos de invitación con amigos para que se unan",
|
||||||
|
"noInvites": "Aún no tienes invitaciones.",
|
||||||
|
"noInvitesHint": "Contacta con un administrador si necesitas códigos de invitación para compartir.",
|
||||||
|
"available": "Disponibles ({count})",
|
||||||
|
"availableHint": "Comparte estos enlaces con las personas que quieras invitar",
|
||||||
|
"used": "Usadas ({count})",
|
||||||
|
"revoked": "Revocadas ({count})",
|
||||||
|
"copyLink": "Copiar Enlace",
|
||||||
|
"copied": "¡Copiado!",
|
||||||
|
"usedBy": "por {email}",
|
||||||
|
"usedStatus": "Usada",
|
||||||
|
"revokedStatus": "Revocada"
|
||||||
|
}
|
||||||
|
}
|
||||||
33
frontend/locales/es/profile.json
Normal file
33
frontend/locales/es/profile.json
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
{
|
||||||
|
"page": {
|
||||||
|
"title": "Mi Perfil",
|
||||||
|
"subtitle": "Gestiona tu información de contacto"
|
||||||
|
},
|
||||||
|
"form": {
|
||||||
|
"email": "Correo Electrónico",
|
||||||
|
"emailHint": "Este es tu correo de inicio de sesión y no se puede cambiar aquí.",
|
||||||
|
"invitedBy": "Invitado Por",
|
||||||
|
"invitedByHint": "El usuario que te invitó a unirte.",
|
||||||
|
"readOnly": "Solo lectura",
|
||||||
|
"contactDetails": "Detalles de Contacto",
|
||||||
|
"contactDetailsHint": "Estos son solo para fines de comunicación — no afectarán tu inicio de sesión.",
|
||||||
|
"contactEmail": "Correo de Contacto",
|
||||||
|
"telegram": "Telegram",
|
||||||
|
"signal": "Signal",
|
||||||
|
"nostrNpub": "Nostr (npub)",
|
||||||
|
"saving": "Guardando...",
|
||||||
|
"saveChanges": "Guardar Cambios"
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"loadError": "Error al cargar el perfil",
|
||||||
|
"saveSuccess": "¡Perfil guardado exitosamente!",
|
||||||
|
"fixErrors": "Por favor corrige los errores a continuación",
|
||||||
|
"networkError": "Error de red. Por favor intenta de nuevo."
|
||||||
|
},
|
||||||
|
"placeholders": {
|
||||||
|
"contactEmail": "alternate@example.com",
|
||||||
|
"telegram": "@username",
|
||||||
|
"signal": "username.01",
|
||||||
|
"nostrNpub": "npub1..."
|
||||||
|
}
|
||||||
|
}
|
||||||
16
frontend/locales/es/trades.json
Normal file
16
frontend/locales/es/trades.json
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"page": {
|
||||||
|
"title": "Mis Operaciones",
|
||||||
|
"subtitle": "Ver y gestionar tus operaciones de Bitcoin",
|
||||||
|
"loadingTrades": "Cargando operaciones...",
|
||||||
|
"noTrades": "Aún no tienes operaciones.",
|
||||||
|
"startTrading": "Empezar a operar",
|
||||||
|
"upcoming": "Próximas ({count})",
|
||||||
|
"history": "Historial ({count})"
|
||||||
|
},
|
||||||
|
"trade": {
|
||||||
|
"rate": "Tasa:",
|
||||||
|
"cancel": "Cancelar",
|
||||||
|
"viewDetails": "Ver Detalles"
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue