2025-12-25 19:11:23 +01:00
|
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
|
|
import { CSSProperties } from "react";
|
|
|
|
|
|
import { SatsDisplay } from "../../components/SatsDisplay";
|
|
|
|
|
|
import { components } from "../../generated/api";
|
|
|
|
|
|
import { formatDate, formatTime } from "../../utils/date";
|
|
|
|
|
|
import { formatEur } from "../../utils/exchange";
|
|
|
|
|
|
import { bannerStyles } from "../../styles/shared";
|
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
2025-12-25 22:19:13 +01:00
|
|
|
|
import { useTranslation } from "../../hooks/useTranslation";
|
2025-12-26 19:00:56 +01:00
|
|
|
|
import { useLanguage } from "../../hooks/useLanguage";
|
2025-12-25 19:11:23 +01:00
|
|
|
|
|
|
|
|
|
|
type BookableSlot = components["schemas"]["BookableSlot"];
|
|
|
|
|
|
type ExchangeResponse = components["schemas"]["ExchangeResponse"];
|
|
|
|
|
|
type Direction = "buy" | "sell";
|
|
|
|
|
|
type BitcoinTransferMethod = "onchain" | "lightning";
|
|
|
|
|
|
|
|
|
|
|
|
interface BookingStepProps {
|
|
|
|
|
|
direction: Direction;
|
|
|
|
|
|
bitcoinTransferMethod: BitcoinTransferMethod;
|
|
|
|
|
|
eurAmount: number;
|
|
|
|
|
|
satsAmount: number;
|
|
|
|
|
|
dates: Date[];
|
|
|
|
|
|
selectedDate: Date | null;
|
|
|
|
|
|
availableSlots: BookableSlot[];
|
|
|
|
|
|
selectedSlot: BookableSlot | null;
|
|
|
|
|
|
datesWithAvailability: Set<string>;
|
|
|
|
|
|
isLoadingSlots: boolean;
|
|
|
|
|
|
isLoadingAvailability: boolean;
|
|
|
|
|
|
existingTradeOnSelectedDate: ExchangeResponse | null;
|
|
|
|
|
|
userTrades: ExchangeResponse[];
|
|
|
|
|
|
onDateSelect: (date: Date) => void;
|
|
|
|
|
|
onSlotSelect: (slot: BookableSlot) => void;
|
|
|
|
|
|
onBackToDetails: () => void;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const styles: Record<string, CSSProperties> = {
|
|
|
|
|
|
summaryCard: {
|
|
|
|
|
|
background: "rgba(255, 255, 255, 0.03)",
|
|
|
|
|
|
border: "1px solid rgba(255, 255, 255, 0.08)",
|
|
|
|
|
|
borderRadius: "12px",
|
|
|
|
|
|
padding: "1rem 1.5rem",
|
|
|
|
|
|
marginBottom: "1.5rem",
|
|
|
|
|
|
},
|
|
|
|
|
|
summaryHeader: {
|
|
|
|
|
|
display: "flex",
|
|
|
|
|
|
justifyContent: "space-between",
|
|
|
|
|
|
alignItems: "center",
|
|
|
|
|
|
marginBottom: "0.5rem",
|
|
|
|
|
|
},
|
|
|
|
|
|
summaryTitle: {
|
|
|
|
|
|
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
|
|
|
|
fontSize: "0.875rem",
|
|
|
|
|
|
color: "rgba(255, 255, 255, 0.5)",
|
|
|
|
|
|
},
|
|
|
|
|
|
editButton: {
|
|
|
|
|
|
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
|
|
|
|
fontSize: "0.75rem",
|
|
|
|
|
|
color: "#a78bfa",
|
|
|
|
|
|
background: "transparent",
|
|
|
|
|
|
border: "none",
|
|
|
|
|
|
cursor: "pointer",
|
|
|
|
|
|
padding: 0,
|
|
|
|
|
|
},
|
|
|
|
|
|
summaryDetails: {
|
|
|
|
|
|
display: "flex",
|
|
|
|
|
|
alignItems: "center",
|
|
|
|
|
|
gap: "0.75rem",
|
|
|
|
|
|
flexWrap: "wrap",
|
|
|
|
|
|
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
|
|
|
|
fontSize: "1rem",
|
|
|
|
|
|
color: "#fff",
|
|
|
|
|
|
},
|
|
|
|
|
|
summaryDirection: {
|
|
|
|
|
|
fontWeight: 600,
|
|
|
|
|
|
},
|
|
|
|
|
|
summaryDivider: {
|
|
|
|
|
|
color: "rgba(255, 255, 255, 0.3)",
|
|
|
|
|
|
},
|
|
|
|
|
|
summaryPaymentMethod: {
|
|
|
|
|
|
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
|
|
|
|
fontSize: "0.875rem",
|
|
|
|
|
|
color: "rgba(255, 255, 255, 0.6)",
|
|
|
|
|
|
},
|
|
|
|
|
|
satsValue: {
|
|
|
|
|
|
fontFamily: "'DM Mono', monospace",
|
|
|
|
|
|
color: "#f7931a", // Bitcoin orange
|
|
|
|
|
|
},
|
|
|
|
|
|
section: {
|
|
|
|
|
|
marginBottom: "2rem",
|
|
|
|
|
|
},
|
|
|
|
|
|
sectionTitle: {
|
|
|
|
|
|
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
|
|
|
|
fontSize: "1.1rem",
|
|
|
|
|
|
fontWeight: 500,
|
|
|
|
|
|
color: "#fff",
|
|
|
|
|
|
marginBottom: "1rem",
|
|
|
|
|
|
},
|
|
|
|
|
|
dateGrid: {
|
|
|
|
|
|
display: "flex",
|
|
|
|
|
|
flexWrap: "wrap",
|
|
|
|
|
|
gap: "0.5rem",
|
|
|
|
|
|
},
|
|
|
|
|
|
dateButton: {
|
|
|
|
|
|
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
|
|
|
|
padding: "0.75rem 1rem",
|
|
|
|
|
|
background: "rgba(255, 255, 255, 0.03)",
|
|
|
|
|
|
border: "1px solid rgba(255, 255, 255, 0.08)",
|
|
|
|
|
|
borderRadius: "10px",
|
|
|
|
|
|
cursor: "pointer",
|
|
|
|
|
|
minWidth: "90px",
|
|
|
|
|
|
textAlign: "center" as const,
|
|
|
|
|
|
transition: "all 0.2s",
|
|
|
|
|
|
},
|
|
|
|
|
|
dateButtonSelected: {
|
|
|
|
|
|
background: "rgba(167, 139, 250, 0.15)",
|
|
|
|
|
|
border: "1px solid #a78bfa",
|
|
|
|
|
|
},
|
|
|
|
|
|
dateButtonDisabled: {
|
|
|
|
|
|
opacity: 0.4,
|
|
|
|
|
|
cursor: "not-allowed",
|
|
|
|
|
|
background: "rgba(255, 255, 255, 0.01)",
|
|
|
|
|
|
border: "1px solid rgba(255, 255, 255, 0.04)",
|
|
|
|
|
|
},
|
|
|
|
|
|
dateButtonHasTrade: {
|
|
|
|
|
|
border: "1px solid rgba(251, 146, 60, 0.5)",
|
|
|
|
|
|
background: "rgba(251, 146, 60, 0.1)",
|
|
|
|
|
|
},
|
|
|
|
|
|
dateWeekday: {
|
|
|
|
|
|
color: "#fff",
|
|
|
|
|
|
fontWeight: 500,
|
|
|
|
|
|
fontSize: "0.875rem",
|
|
|
|
|
|
marginBottom: "0.25rem",
|
|
|
|
|
|
},
|
|
|
|
|
|
dateDay: {
|
|
|
|
|
|
color: "rgba(255, 255, 255, 0.5)",
|
|
|
|
|
|
fontSize: "0.8rem",
|
|
|
|
|
|
},
|
|
|
|
|
|
dateWarning: {
|
|
|
|
|
|
fontSize: "0.7rem",
|
|
|
|
|
|
marginTop: "0.25rem",
|
|
|
|
|
|
opacity: 0.8,
|
|
|
|
|
|
},
|
|
|
|
|
|
errorLink: {
|
|
|
|
|
|
marginTop: "0.75rem",
|
|
|
|
|
|
paddingTop: "0.75rem",
|
|
|
|
|
|
borderTop: "1px solid rgba(255, 255, 255, 0.1)",
|
|
|
|
|
|
},
|
|
|
|
|
|
errorLinkAnchor: {
|
|
|
|
|
|
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
|
|
|
|
color: "#a78bfa",
|
|
|
|
|
|
textDecoration: "none",
|
|
|
|
|
|
fontWeight: 500,
|
|
|
|
|
|
fontSize: "0.9rem",
|
|
|
|
|
|
},
|
|
|
|
|
|
slotGrid: {
|
|
|
|
|
|
display: "flex",
|
|
|
|
|
|
flexWrap: "wrap",
|
|
|
|
|
|
gap: "0.5rem",
|
|
|
|
|
|
},
|
|
|
|
|
|
slotButton: {
|
|
|
|
|
|
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
|
|
|
|
padding: "0.6rem 1.25rem",
|
|
|
|
|
|
background: "rgba(255, 255, 255, 0.03)",
|
|
|
|
|
|
border: "1px solid rgba(255, 255, 255, 0.08)",
|
|
|
|
|
|
borderRadius: "8px",
|
|
|
|
|
|
color: "#fff",
|
|
|
|
|
|
cursor: "pointer",
|
|
|
|
|
|
fontSize: "0.9rem",
|
|
|
|
|
|
transition: "all 0.2s",
|
|
|
|
|
|
},
|
|
|
|
|
|
slotButtonSelected: {
|
|
|
|
|
|
background: "rgba(167, 139, 250, 0.15)",
|
|
|
|
|
|
border: "1px solid #a78bfa",
|
|
|
|
|
|
},
|
|
|
|
|
|
emptyState: {
|
|
|
|
|
|
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
|
|
|
|
color: "rgba(255, 255, 255, 0.4)",
|
|
|
|
|
|
padding: "1rem 0",
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Check if a date has an existing trade (only consider booked trades, not cancelled ones)
|
|
|
|
|
|
*/
|
|
|
|
|
|
function getExistingTradeOnDate(
|
|
|
|
|
|
date: Date,
|
|
|
|
|
|
userTrades: ExchangeResponse[]
|
|
|
|
|
|
): ExchangeResponse | null {
|
|
|
|
|
|
const dateStr = formatDate(date);
|
|
|
|
|
|
return (
|
|
|
|
|
|
userTrades.find((trade) => {
|
|
|
|
|
|
const tradeDate = formatDate(new Date(trade.slot_start));
|
|
|
|
|
|
return tradeDate === dateStr && trade.status === "booked";
|
|
|
|
|
|
}) || null
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Step 2 of the exchange wizard: Booking
|
|
|
|
|
|
* Allows user to select a date and time slot for the exchange.
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function BookingStep({
|
|
|
|
|
|
direction,
|
|
|
|
|
|
bitcoinTransferMethod,
|
|
|
|
|
|
eurAmount,
|
|
|
|
|
|
satsAmount,
|
|
|
|
|
|
dates,
|
|
|
|
|
|
selectedDate,
|
|
|
|
|
|
availableSlots,
|
|
|
|
|
|
selectedSlot,
|
|
|
|
|
|
datesWithAvailability,
|
|
|
|
|
|
isLoadingSlots,
|
|
|
|
|
|
isLoadingAvailability,
|
|
|
|
|
|
existingTradeOnSelectedDate,
|
|
|
|
|
|
userTrades,
|
|
|
|
|
|
onDateSelect,
|
|
|
|
|
|
onSlotSelect,
|
|
|
|
|
|
onBackToDetails,
|
|
|
|
|
|
}: BookingStepProps) {
|
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
2025-12-25 22:19:13 +01:00
|
|
|
|
const t = useTranslation("exchange");
|
2025-12-26 19:00:56 +01:00
|
|
|
|
const { locale } = useLanguage();
|
|
|
|
|
|
|
|
|
|
|
|
// Map locale codes to Intl locale strings
|
|
|
|
|
|
const intlLocale = locale === "es" ? "es-ES" : locale === "ca" ? "ca-ES" : "en-US";
|
2025-12-25 19:11:23 +01:00
|
|
|
|
return (
|
|
|
|
|
|
<>
|
|
|
|
|
|
{/* Trade Summary Card */}
|
|
|
|
|
|
<div style={styles.summaryCard}>
|
|
|
|
|
|
<div style={styles.summaryHeader}>
|
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
2025-12-25 22:19:13 +01:00
|
|
|
|
<span style={styles.summaryTitle}>{t("bookingStep.yourExchange")}</span>
|
2025-12-25 19:11:23 +01:00
|
|
|
|
<button onClick={onBackToDetails} style={styles.editButton}>
|
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
2025-12-25 22:19:13 +01:00
|
|
|
|
{t("bookingStep.edit")}
|
2025-12-25 19:11:23 +01:00
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style={styles.summaryDetails}>
|
|
|
|
|
|
<span
|
|
|
|
|
|
style={{
|
|
|
|
|
|
...styles.summaryDirection,
|
|
|
|
|
|
color: direction === "buy" ? "#4ade80" : "#f87171",
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
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
2025-12-25 22:19:13 +01:00
|
|
|
|
{direction === "buy" ? t("bookingStep.buy") : t("bookingStep.sell")} BTC
|
2025-12-25 19:11:23 +01:00
|
|
|
|
</span>
|
|
|
|
|
|
<span style={styles.summaryDivider}>•</span>
|
|
|
|
|
|
<span>{formatEur(eurAmount)}</span>
|
|
|
|
|
|
<span style={styles.summaryDivider}>↔</span>
|
|
|
|
|
|
<span style={styles.satsValue}>
|
|
|
|
|
|
<SatsDisplay sats={satsAmount} />
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span style={styles.summaryDivider}>•</span>
|
|
|
|
|
|
<span style={styles.summaryPaymentMethod}>
|
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
2025-12-25 22:19:13 +01:00
|
|
|
|
{direction === "buy" ? t("bookingStep.receiveVia") : t("bookingStep.sendVia")}{" "}
|
|
|
|
|
|
{bitcoinTransferMethod === "onchain"
|
|
|
|
|
|
? t("transferMethod.onchain")
|
|
|
|
|
|
: t("transferMethod.lightning")}
|
2025-12-25 19:11:23 +01:00
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Date Selection */}
|
|
|
|
|
|
<div style={styles.section}>
|
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
2025-12-25 22:19:13 +01:00
|
|
|
|
<h2 style={styles.sectionTitle}>{t("bookingStep.selectDate")}</h2>
|
2025-12-25 19:11:23 +01:00
|
|
|
|
<div style={styles.dateGrid}>
|
|
|
|
|
|
{dates.map((date) => {
|
|
|
|
|
|
const dateStr = formatDate(date);
|
|
|
|
|
|
const isSelected = selectedDate && formatDate(selectedDate) === dateStr;
|
|
|
|
|
|
const hasAvailability = datesWithAvailability.has(dateStr);
|
|
|
|
|
|
const isDisabled = !hasAvailability || isLoadingAvailability;
|
|
|
|
|
|
const hasExistingTrade = getExistingTradeOnDate(date, userTrades) !== null;
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<button
|
|
|
|
|
|
key={dateStr}
|
|
|
|
|
|
data-testid={`date-${dateStr}`}
|
|
|
|
|
|
onClick={() => onDateSelect(date)}
|
|
|
|
|
|
disabled={isDisabled}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
...styles.dateButton,
|
|
|
|
|
|
...(isSelected ? styles.dateButtonSelected : {}),
|
|
|
|
|
|
...(isDisabled ? styles.dateButtonDisabled : {}),
|
|
|
|
|
|
...(hasExistingTrade && !isDisabled ? styles.dateButtonHasTrade : {}),
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div style={styles.dateWeekday}>
|
2025-12-26 19:00:56 +01:00
|
|
|
|
{date.toLocaleDateString(intlLocale, { weekday: "short" })}
|
2025-12-25 19:11:23 +01:00
|
|
|
|
</div>
|
|
|
|
|
|
<div style={styles.dateDay}>
|
2025-12-26 19:00:56 +01:00
|
|
|
|
{date.toLocaleDateString(intlLocale, {
|
2025-12-25 19:11:23 +01:00
|
|
|
|
month: "short",
|
|
|
|
|
|
day: "numeric",
|
|
|
|
|
|
})}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{hasExistingTrade && !isDisabled && <div style={styles.dateWarning}>⚠️</div>}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Warning for existing trade on selected date */}
|
|
|
|
|
|
{existingTradeOnSelectedDate && (
|
|
|
|
|
|
<div style={bannerStyles.errorBanner}>
|
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
2025-12-25 22:19:13 +01:00
|
|
|
|
<div>{t("bookingStep.existingTradeWarning")}</div>
|
2025-12-25 19:11:23 +01:00
|
|
|
|
<div style={styles.errorLink}>
|
|
|
|
|
|
<a
|
|
|
|
|
|
href={`/trades/${existingTradeOnSelectedDate.public_id}`}
|
|
|
|
|
|
style={styles.errorLinkAnchor}
|
|
|
|
|
|
>
|
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
2025-12-25 22:19:13 +01:00
|
|
|
|
{t("bookingStep.viewExistingTrade")}
|
2025-12-25 19:11:23 +01:00
|
|
|
|
</a>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Available Slots */}
|
|
|
|
|
|
{selectedDate && !existingTradeOnSelectedDate && (
|
|
|
|
|
|
<div style={styles.section}>
|
|
|
|
|
|
<h2 style={styles.sectionTitle}>
|
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
2025-12-25 22:19:13 +01:00
|
|
|
|
{t("bookingStep.availableSlots")}{" "}
|
2025-12-26 19:00:56 +01:00
|
|
|
|
{selectedDate.toLocaleDateString(intlLocale, {
|
2025-12-25 19:11:23 +01:00
|
|
|
|
weekday: "long",
|
|
|
|
|
|
month: "long",
|
|
|
|
|
|
day: "numeric",
|
|
|
|
|
|
})}
|
|
|
|
|
|
</h2>
|
|
|
|
|
|
|
|
|
|
|
|
{isLoadingSlots ? (
|
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
2025-12-25 22:19:13 +01:00
|
|
|
|
<div style={styles.emptyState}>{t("bookingStep.loadingSlots")}</div>
|
2025-12-25 19:11:23 +01:00
|
|
|
|
) : availableSlots.length === 0 ? (
|
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
2025-12-25 22:19:13 +01:00
|
|
|
|
<div style={styles.emptyState}>{t("bookingStep.noSlots")}</div>
|
2025-12-25 19:11:23 +01:00
|
|
|
|
) : (
|
|
|
|
|
|
<div style={styles.slotGrid}>
|
|
|
|
|
|
{availableSlots.map((slot) => {
|
|
|
|
|
|
const isSelected = selectedSlot?.start_time === slot.start_time;
|
|
|
|
|
|
return (
|
|
|
|
|
|
<button
|
|
|
|
|
|
key={slot.start_time}
|
|
|
|
|
|
onClick={() => onSlotSelect(slot)}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
...styles.slotButton,
|
|
|
|
|
|
...(isSelected ? styles.slotButtonSelected : {}),
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{formatTime(slot.start_time)}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|