lots of stuff
This commit is contained in:
parent
f946fbf7b8
commit
4be45f8f7c
9 changed files with 513 additions and 236 deletions
|
|
@ -35,9 +35,9 @@ export default function AdminTradesPage() {
|
|||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Action state - use Set to track multiple concurrent actions
|
||||
const [actioningIds, setActioningIds] = useState<Set<number>>(new Set());
|
||||
const [actioningIds, setActioningIds] = useState<Set<string>>(new Set());
|
||||
const [confirmAction, setConfirmAction] = useState<{
|
||||
id: number;
|
||||
id: string;
|
||||
type: "complete" | "no_show" | "cancel";
|
||||
} | null>(null);
|
||||
|
||||
|
|
@ -99,16 +99,16 @@ export default function AdminTradesPage() {
|
|||
}
|
||||
}, [user, isAuthorized, fetchUpcomingTrades, fetchPastTrades]);
|
||||
|
||||
const handleAction = async (tradeId: number, action: "complete" | "no_show" | "cancel") => {
|
||||
const handleAction = async (publicId: string, action: "complete" | "no_show" | "cancel") => {
|
||||
// Add this trade to the set of actioning trades
|
||||
setActioningIds((prev) => new Set(prev).add(tradeId));
|
||||
setActioningIds((prev) => new Set(prev).add(publicId));
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const endpoint =
|
||||
action === "no_show"
|
||||
? `/api/admin/trades/${tradeId}/no-show`
|
||||
: `/api/admin/trades/${tradeId}/${action}`;
|
||||
? `/api/admin/trades/${publicId}/no-show`
|
||||
: `/api/admin/trades/${publicId}/${action}`;
|
||||
|
||||
await api.post<AdminExchangeResponse>(endpoint, {});
|
||||
// Refetch trades - errors from fetch are informational, not critical
|
||||
|
|
@ -124,7 +124,7 @@ export default function AdminTradesPage() {
|
|||
// Remove this trade from the set of actioning trades
|
||||
setActioningIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.delete(tradeId);
|
||||
next.delete(publicId);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
|
@ -298,18 +298,18 @@ export default function AdminTradesPage() {
|
|||
|
||||
{/* Actions */}
|
||||
<div style={styles.buttonGroup}>
|
||||
{confirmAction?.id === trade.id ? (
|
||||
{confirmAction?.id === trade.public_id ? (
|
||||
<>
|
||||
<button
|
||||
onClick={() => handleAction(trade.id, confirmAction.type)}
|
||||
disabled={actioningIds.has(trade.id)}
|
||||
onClick={() => handleAction(trade.public_id, confirmAction.type)}
|
||||
disabled={actioningIds.has(trade.public_id)}
|
||||
style={
|
||||
confirmAction.type === "cancel"
|
||||
? styles.dangerButton
|
||||
: styles.successButton
|
||||
}
|
||||
>
|
||||
{actioningIds.has(trade.id) ? "..." : "Confirm"}
|
||||
{actioningIds.has(trade.public_id) ? "..." : "Confirm"}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setConfirmAction(null)}
|
||||
|
|
@ -325,7 +325,7 @@ export default function AdminTradesPage() {
|
|||
<button
|
||||
onClick={() =>
|
||||
setConfirmAction({
|
||||
id: trade.id,
|
||||
id: trade.public_id,
|
||||
type: "complete",
|
||||
})
|
||||
}
|
||||
|
|
@ -336,7 +336,7 @@ export default function AdminTradesPage() {
|
|||
<button
|
||||
onClick={() =>
|
||||
setConfirmAction({
|
||||
id: trade.id,
|
||||
id: trade.public_id,
|
||||
type: "no_show",
|
||||
})
|
||||
}
|
||||
|
|
@ -350,7 +350,7 @@ export default function AdminTradesPage() {
|
|||
<button
|
||||
onClick={() =>
|
||||
setConfirmAction({
|
||||
id: trade.id,
|
||||
id: trade.public_id,
|
||||
type: "cancel",
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ const {
|
|||
|
||||
type Direction = "buy" | "sell";
|
||||
type BitcoinTransferMethod = "onchain" | "lightning";
|
||||
type WizardStep = "details" | "booking";
|
||||
type WizardStep = "details" | "booking" | "confirmation";
|
||||
|
||||
/**
|
||||
* Format price for display
|
||||
|
|
@ -66,9 +66,14 @@ export default function ExchangePage() {
|
|||
const [datesWithAvailability, setDatesWithAvailability] = useState<Set<string>>(new Set());
|
||||
const [isLoadingAvailability, setIsLoadingAvailability] = useState(true);
|
||||
|
||||
// User trades state (for same-day booking check)
|
||||
const [userTrades, setUserTrades] = useState<ExchangeResponse[]>([]);
|
||||
const [existingTradeOnSelectedDate, setExistingTradeOnSelectedDate] =
|
||||
useState<ExchangeResponse | null>(null);
|
||||
|
||||
// UI state
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [existingTradeId, setExistingTradeId] = useState<number | null>(null);
|
||||
const [existingTradeId, setExistingTradeId] = useState<string | null>(null);
|
||||
const [isBooking, setIsBooking] = useState(false);
|
||||
|
||||
// Compute dates
|
||||
|
|
@ -167,10 +172,28 @@ export default function ExchangePage() {
|
|||
}
|
||||
}, []);
|
||||
|
||||
// Fetch availability for all dates when entering booking step
|
||||
// Fetch user trades when entering booking step
|
||||
useEffect(() => {
|
||||
if (!user || !isAuthorized || wizardStep !== "booking") return;
|
||||
|
||||
const fetchUserTrades = async () => {
|
||||
try {
|
||||
const data = await api.get<ExchangeResponse[]>("/api/trades");
|
||||
setUserTrades(data);
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch user trades:", err);
|
||||
// Don't block the UI if this fails
|
||||
}
|
||||
};
|
||||
|
||||
fetchUserTrades();
|
||||
}, [user, isAuthorized, wizardStep]);
|
||||
|
||||
// Fetch availability for all dates when entering booking or confirmation step
|
||||
useEffect(() => {
|
||||
if (!user || !isAuthorized || (wizardStep !== "booking" && wizardStep !== "confirmation"))
|
||||
return;
|
||||
|
||||
const fetchAllAvailability = async () => {
|
||||
setIsLoadingAvailability(true);
|
||||
const availabilitySet = new Set<string>();
|
||||
|
|
@ -201,9 +224,36 @@ export default function ExchangePage() {
|
|||
}
|
||||
}, [selectedDate, user, isAuthorized, fetchSlots]);
|
||||
|
||||
// Check if a date has an existing trade (only consider booked trades, not cancelled ones)
|
||||
const getExistingTradeOnDate = useCallback(
|
||||
(date: Date): 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
|
||||
);
|
||||
},
|
||||
[userTrades]
|
||||
);
|
||||
|
||||
const handleDateSelect = (date: Date) => {
|
||||
const dateStr = formatDate(date);
|
||||
if (datesWithAvailability.has(dateStr)) {
|
||||
if (!datesWithAvailability.has(dateStr)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if user already has a trade on this date
|
||||
const existingTrade = getExistingTradeOnDate(date);
|
||||
if (existingTrade) {
|
||||
setExistingTradeOnSelectedDate(existingTrade);
|
||||
setSelectedDate(null);
|
||||
setSelectedSlot(null);
|
||||
setAvailableSlots([]);
|
||||
setError(null);
|
||||
} else {
|
||||
setExistingTradeOnSelectedDate(null);
|
||||
setSelectedDate(date);
|
||||
}
|
||||
};
|
||||
|
|
@ -211,6 +261,7 @@ export default function ExchangePage() {
|
|||
const handleSlotSelect = (slot: BookableSlot) => {
|
||||
setSelectedSlot(slot);
|
||||
setError(null);
|
||||
setWizardStep("confirmation");
|
||||
};
|
||||
|
||||
const handleContinueToBooking = () => {
|
||||
|
|
@ -223,6 +274,12 @@ export default function ExchangePage() {
|
|||
setSelectedDate(null);
|
||||
setSelectedSlot(null);
|
||||
setError(null);
|
||||
setExistingTradeOnSelectedDate(null);
|
||||
};
|
||||
|
||||
const handleBackToBooking = () => {
|
||||
setWizardStep("booking");
|
||||
setError(null);
|
||||
};
|
||||
|
||||
const handleAmountChange = (value: number) => {
|
||||
|
|
@ -282,10 +339,10 @@ export default function ExchangePage() {
|
|||
}
|
||||
setError(errorMessage);
|
||||
|
||||
// Check if it's a "same day" error and extract trade ID
|
||||
const tradeIdMatch = errorMessage.match(/Trade ID: (\d+)/);
|
||||
// Check if it's a "same day" error and extract trade public_id (UUID)
|
||||
const tradeIdMatch = errorMessage.match(/Trade ID: ([a-f0-9-]{36})/i);
|
||||
if (tradeIdMatch) {
|
||||
setExistingTradeId(parseInt(tradeIdMatch[1], 10));
|
||||
setExistingTradeId(tradeIdMatch[1]);
|
||||
} else {
|
||||
setExistingTradeId(null);
|
||||
}
|
||||
|
|
@ -294,11 +351,6 @@ export default function ExchangePage() {
|
|||
}
|
||||
};
|
||||
|
||||
const cancelSlotSelection = () => {
|
||||
setSelectedSlot(null);
|
||||
setError(null);
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<main style={layoutStyles.main}>
|
||||
|
|
@ -377,12 +429,26 @@ export default function ExchangePage() {
|
|||
<div
|
||||
style={{
|
||||
...styles.step,
|
||||
...(wizardStep === "booking" ? styles.stepActive : {}),
|
||||
...(wizardStep === "booking"
|
||||
? styles.stepActive
|
||||
: wizardStep === "confirmation"
|
||||
? styles.stepCompleted
|
||||
: {}),
|
||||
}}
|
||||
>
|
||||
<span style={styles.stepNumber}>2</span>
|
||||
<span style={styles.stepLabel}>Book Appointment</span>
|
||||
</div>
|
||||
<div style={styles.stepDivider} />
|
||||
<div
|
||||
style={{
|
||||
...styles.step,
|
||||
...(wizardStep === "confirmation" ? styles.stepActive : {}),
|
||||
}}
|
||||
>
|
||||
<span style={styles.stepNumber}>3</span>
|
||||
<span style={styles.stepLabel}>Confirm</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Step 1: Exchange Details */}
|
||||
|
|
@ -553,6 +619,7 @@ export default function ExchangePage() {
|
|||
const isSelected = selectedDate && formatDate(selectedDate) === dateStr;
|
||||
const hasAvailability = datesWithAvailability.has(dateStr);
|
||||
const isDisabled = !hasAvailability || isLoadingAvailability;
|
||||
const hasExistingTrade = getExistingTradeOnDate(date) !== null;
|
||||
|
||||
return (
|
||||
<button
|
||||
|
|
@ -564,6 +631,7 @@ export default function ExchangePage() {
|
|||
...styles.dateButton,
|
||||
...(isSelected ? styles.dateButtonSelected : {}),
|
||||
...(isDisabled ? styles.dateButtonDisabled : {}),
|
||||
...(hasExistingTrade && !isDisabled ? styles.dateButtonHasTrade : {}),
|
||||
}}
|
||||
>
|
||||
<div style={styles.dateWeekday}>
|
||||
|
|
@ -575,14 +643,32 @@ export default function ExchangePage() {
|
|||
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}>
|
||||
<div>
|
||||
You already have a trade booked on this day. You can only book one trade per day.
|
||||
</div>
|
||||
<div style={styles.errorLink}>
|
||||
<a
|
||||
href={`/trades/${existingTradeOnSelectedDate.public_id}`}
|
||||
style={styles.errorLinkAnchor}
|
||||
>
|
||||
View your existing trade →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Available Slots */}
|
||||
{selectedDate && (
|
||||
{selectedDate && !existingTradeOnSelectedDate && (
|
||||
<div style={styles.section}>
|
||||
<h2 style={styles.sectionTitle}>
|
||||
Available Slots for{" "}
|
||||
|
|
@ -618,83 +704,109 @@ export default function ExchangePage() {
|
|||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Confirm Booking */}
|
||||
{selectedSlot && (
|
||||
<div style={styles.confirmCard}>
|
||||
<h3 style={styles.confirmTitle}>Confirm Trade</h3>
|
||||
<div style={styles.confirmDetails}>
|
||||
<div style={styles.confirmRow}>
|
||||
<span style={styles.confirmLabel}>Time:</span>
|
||||
<span style={styles.confirmValue}>
|
||||
{formatTime(selectedSlot.start_time)} - {formatTime(selectedSlot.end_time)}
|
||||
</span>
|
||||
</div>
|
||||
<div style={styles.confirmRow}>
|
||||
<span style={styles.confirmLabel}>Direction:</span>
|
||||
<span
|
||||
style={{
|
||||
...styles.confirmValue,
|
||||
color: direction === "buy" ? "#4ade80" : "#f87171",
|
||||
}}
|
||||
>
|
||||
{direction === "buy" ? "Buy BTC" : "Sell BTC"}
|
||||
</span>
|
||||
</div>
|
||||
<div style={styles.confirmRow}>
|
||||
<span style={styles.confirmLabel}>EUR:</span>
|
||||
<span style={styles.confirmValue}>{formatEur(eurAmount)}</span>
|
||||
</div>
|
||||
<div style={styles.confirmRow}>
|
||||
<span style={styles.confirmLabel}>BTC:</span>
|
||||
<span style={{ ...styles.confirmValue, ...styles.satsValue }}>
|
||||
<SatsDisplay sats={satsAmount} />
|
||||
</span>
|
||||
</div>
|
||||
<div style={styles.confirmRow}>
|
||||
<span style={styles.confirmLabel}>Rate:</span>
|
||||
<span style={styles.confirmValue}>{formatPrice(agreedPrice)}/BTC</span>
|
||||
</div>
|
||||
<div style={styles.confirmRow}>
|
||||
<span style={styles.confirmLabel}>Payment:</span>
|
||||
<span style={styles.confirmValue}>
|
||||
{direction === "buy" ? "Receive via " : "Send via "}
|
||||
{bitcoinTransferMethod === "onchain" ? "Onchain" : "Lightning"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={styles.buttonRow}>
|
||||
<button
|
||||
onClick={handleBook}
|
||||
disabled={isBooking || isPriceStale}
|
||||
style={{
|
||||
...styles.bookButton,
|
||||
background:
|
||||
direction === "buy"
|
||||
? "linear-gradient(135deg, #4ade80 0%, #22c55e 100%)"
|
||||
: "linear-gradient(135deg, #f87171 0%, #ef4444 100%)",
|
||||
...(isBooking || isPriceStale ? buttonStyles.buttonDisabled : {}),
|
||||
}}
|
||||
>
|
||||
{isBooking
|
||||
? "Booking..."
|
||||
: isPriceStale
|
||||
? "Price Stale"
|
||||
: `Confirm ${direction === "buy" ? "Buy" : "Sell"}`}
|
||||
</button>
|
||||
<button
|
||||
onClick={cancelSlotSelection}
|
||||
disabled={isBooking}
|
||||
style={styles.cancelButton}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Step 2: Booking (Compressed when step 3 is active) */}
|
||||
{wizardStep === "confirmation" && (
|
||||
<div style={styles.compressedBookingCard}>
|
||||
<div style={styles.compressedBookingHeader}>
|
||||
<span style={styles.compressedBookingTitle}>Appointment</span>
|
||||
<button onClick={handleBackToBooking} style={styles.editButton}>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<div style={styles.compressedBookingDetails}>
|
||||
<span>
|
||||
{selectedDate?.toLocaleDateString("en-US", {
|
||||
weekday: "short",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
})}
|
||||
</span>
|
||||
<span style={styles.summaryDivider}>•</span>
|
||||
<span>
|
||||
{selectedSlot && formatTime(selectedSlot.start_time)} -{" "}
|
||||
{selectedSlot && formatTime(selectedSlot.end_time)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 3: Confirmation */}
|
||||
{wizardStep === "confirmation" && selectedSlot && (
|
||||
<div style={styles.confirmCard}>
|
||||
<h3 style={styles.confirmTitle}>Confirm Trade</h3>
|
||||
<div style={styles.confirmDetails}>
|
||||
<div style={styles.confirmRow}>
|
||||
<span style={styles.confirmLabel}>Time:</span>
|
||||
<span style={styles.confirmValue}>
|
||||
{formatTime(selectedSlot.start_time)} - {formatTime(selectedSlot.end_time)}
|
||||
</span>
|
||||
</div>
|
||||
<div style={styles.confirmRow}>
|
||||
<span style={styles.confirmLabel}>Direction:</span>
|
||||
<span
|
||||
style={{
|
||||
...styles.confirmValue,
|
||||
color: direction === "buy" ? "#4ade80" : "#f87171",
|
||||
}}
|
||||
>
|
||||
{direction === "buy" ? "Buy BTC" : "Sell BTC"}
|
||||
</span>
|
||||
</div>
|
||||
<div style={styles.confirmRow}>
|
||||
<span style={styles.confirmLabel}>EUR:</span>
|
||||
<span style={styles.confirmValue}>{formatEur(eurAmount)}</span>
|
||||
</div>
|
||||
<div style={styles.confirmRow}>
|
||||
<span style={styles.confirmLabel}>BTC:</span>
|
||||
<span style={{ ...styles.confirmValue, ...styles.satsValue }}>
|
||||
<SatsDisplay sats={satsAmount} />
|
||||
</span>
|
||||
</div>
|
||||
<div style={styles.confirmRow}>
|
||||
<span style={styles.confirmLabel}>Rate:</span>
|
||||
<span style={styles.confirmValue}>{formatPrice(agreedPrice)}/BTC</span>
|
||||
</div>
|
||||
<div style={styles.confirmRow}>
|
||||
<span style={styles.confirmLabel}>Payment:</span>
|
||||
<span style={styles.confirmValue}>
|
||||
{direction === "buy" ? "Receive via " : "Send via "}
|
||||
{bitcoinTransferMethod === "onchain" ? "Onchain" : "Lightning"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={styles.buttonRow}>
|
||||
<button
|
||||
onClick={handleBook}
|
||||
disabled={isBooking || isPriceStale}
|
||||
style={{
|
||||
...styles.bookButton,
|
||||
background:
|
||||
direction === "buy"
|
||||
? "linear-gradient(135deg, #4ade80 0%, #22c55e 100%)"
|
||||
: "linear-gradient(135deg, #f87171 0%, #ef4444 100%)",
|
||||
...(isBooking || isPriceStale ? buttonStyles.buttonDisabled : {}),
|
||||
}}
|
||||
>
|
||||
{isBooking
|
||||
? "Booking..."
|
||||
: isPriceStale
|
||||
? "Price Stale"
|
||||
: `Confirm ${direction === "buy" ? "Buy" : "Sell"}`}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleBackToBooking}
|
||||
disabled={isBooking}
|
||||
style={styles.cancelButton}
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
|
|
@ -937,6 +1049,33 @@ const styles: Record<string, CSSProperties> = {
|
|||
padding: "1rem 1.5rem",
|
||||
marginBottom: "1.5rem",
|
||||
},
|
||||
compressedBookingCard: {
|
||||
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",
|
||||
},
|
||||
compressedBookingHeader: {
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginBottom: "0.5rem",
|
||||
},
|
||||
compressedBookingTitle: {
|
||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
||||
fontSize: "0.875rem",
|
||||
color: "rgba(255, 255, 255, 0.5)",
|
||||
},
|
||||
compressedBookingDetails: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "0.75rem",
|
||||
flexWrap: "wrap",
|
||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
||||
fontSize: "1rem",
|
||||
color: "#fff",
|
||||
},
|
||||
summaryHeader: {
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
|
|
@ -1080,6 +1219,10 @@ const styles: Record<string, CSSProperties> = {
|
|||
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,
|
||||
|
|
@ -1090,6 +1233,11 @@ const styles: Record<string, CSSProperties> = {
|
|||
color: "rgba(255, 255, 255, 0.5)",
|
||||
fontSize: "0.8rem",
|
||||
},
|
||||
dateWarning: {
|
||||
fontSize: "0.7rem",
|
||||
marginTop: "0.25rem",
|
||||
opacity: 0.8,
|
||||
},
|
||||
slotGrid: {
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
|
|
|
|||
|
|
@ -416,7 +416,27 @@ export interface paths {
|
|||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/trades/{exchange_id}/cancel": {
|
||||
"/api/trades/{public_id}": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/**
|
||||
* Get My Trade
|
||||
* @description Get a specific trade by public ID. User can only access their own trades.
|
||||
*/
|
||||
get: operations["get_my_trade_api_trades__public_id__get"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/trades/{public_id}/cancel": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
|
|
@ -429,7 +449,7 @@ export interface paths {
|
|||
* Cancel My Trade
|
||||
* @description Cancel one of the current user's exchanges.
|
||||
*/
|
||||
post: operations["cancel_my_trade_api_trades__exchange_id__cancel_post"];
|
||||
post: operations["cancel_my_trade_api_trades__public_id__cancel_post"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
|
|
@ -481,7 +501,7 @@ export interface paths {
|
|||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/admin/trades/{exchange_id}/complete": {
|
||||
"/api/admin/trades/{public_id}/complete": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
|
|
@ -494,14 +514,14 @@ export interface paths {
|
|||
* Complete Trade
|
||||
* @description Mark a trade as completed. Only possible after slot time has passed.
|
||||
*/
|
||||
post: operations["complete_trade_api_admin_trades__exchange_id__complete_post"];
|
||||
post: operations["complete_trade_api_admin_trades__public_id__complete_post"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/admin/trades/{exchange_id}/no-show": {
|
||||
"/api/admin/trades/{public_id}/no-show": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
|
|
@ -514,14 +534,14 @@ export interface paths {
|
|||
* Mark No Show
|
||||
* @description Mark a trade as no-show. Only possible after slot time has passed.
|
||||
*/
|
||||
post: operations["mark_no_show_api_admin_trades__exchange_id__no_show_post"];
|
||||
post: operations["mark_no_show_api_admin_trades__public_id__no_show_post"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/admin/trades/{exchange_id}/cancel": {
|
||||
"/api/admin/trades/{public_id}/cancel": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
|
|
@ -534,7 +554,7 @@ export interface paths {
|
|||
* Admin Cancel Trade
|
||||
* @description Cancel any trade (admin only).
|
||||
*/
|
||||
post: operations["admin_cancel_trade_api_admin_trades__exchange_id__cancel_post"];
|
||||
post: operations["admin_cancel_trade_api_admin_trades__public_id__cancel_post"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
|
|
@ -575,6 +595,8 @@ export interface components {
|
|||
AdminExchangeResponse: {
|
||||
/** Id */
|
||||
id: number;
|
||||
/** Public Id */
|
||||
public_id: string;
|
||||
/** User Id */
|
||||
user_id: number;
|
||||
/** User Email */
|
||||
|
|
@ -760,6 +782,8 @@ export interface components {
|
|||
ExchangeResponse: {
|
||||
/** Id */
|
||||
id: number;
|
||||
/** Public Id */
|
||||
public_id: string;
|
||||
/** User Id */
|
||||
user_id: number;
|
||||
/** User Email */
|
||||
|
|
@ -1685,12 +1709,43 @@ export interface operations {
|
|||
};
|
||||
};
|
||||
};
|
||||
cancel_my_trade_api_trades__exchange_id__cancel_post: {
|
||||
get_my_trade_api_trades__public_id__get: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
exchange_id: number;
|
||||
public_id: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["ExchangeResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
cancel_my_trade_api_trades__public_id__cancel_post: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
public_id: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
|
|
@ -1770,12 +1825,12 @@ export interface operations {
|
|||
};
|
||||
};
|
||||
};
|
||||
complete_trade_api_admin_trades__exchange_id__complete_post: {
|
||||
complete_trade_api_admin_trades__public_id__complete_post: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
exchange_id: number;
|
||||
public_id: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
|
|
@ -1801,12 +1856,12 @@ export interface operations {
|
|||
};
|
||||
};
|
||||
};
|
||||
mark_no_show_api_admin_trades__exchange_id__no_show_post: {
|
||||
mark_no_show_api_admin_trades__public_id__no_show_post: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
exchange_id: number;
|
||||
public_id: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
|
|
@ -1832,12 +1887,12 @@ export interface operations {
|
|||
};
|
||||
};
|
||||
};
|
||||
admin_cancel_trade_api_admin_trades__exchange_id__cancel_post: {
|
||||
admin_cancel_trade_api_admin_trades__public_id__cancel_post: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
exchange_id: number;
|
||||
public_id: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ type ExchangeResponse = components["schemas"]["ExchangeResponse"];
|
|||
export default function TradeDetailPage() {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const tradeId = params?.id ? parseInt(params.id as string, 10) : null;
|
||||
const publicId = params?.id as string | undefined;
|
||||
|
||||
const { user, isLoading, isAuthorized } = useRequireAuth({
|
||||
requiredPermission: Permission.VIEW_OWN_EXCHANGES,
|
||||
|
|
@ -36,13 +36,13 @@ export default function TradeDetailPage() {
|
|||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!user || !isAuthorized || !tradeId) return;
|
||||
if (!user || !isAuthorized || !publicId) return;
|
||||
|
||||
const fetchTrade = async () => {
|
||||
try {
|
||||
setIsLoadingTrade(true);
|
||||
setError(null);
|
||||
const data = await api.get<ExchangeResponse>(`/api/trades/${tradeId}`);
|
||||
const data = await api.get<ExchangeResponse>(`/api/trades/${publicId}`);
|
||||
setTrade(data);
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch trade:", err);
|
||||
|
|
@ -55,7 +55,7 @@ export default function TradeDetailPage() {
|
|||
};
|
||||
|
||||
fetchTrade();
|
||||
}, [user, isAuthorized, tradeId]);
|
||||
}, [user, isAuthorized, publicId]);
|
||||
|
||||
if (isLoading || isLoadingTrade) {
|
||||
return (
|
||||
|
|
@ -221,7 +221,7 @@ export default function TradeDetailPage() {
|
|||
return;
|
||||
}
|
||||
try {
|
||||
await api.post(`/api/trades/${trade.id}/cancel`, {});
|
||||
await api.post(`/api/trades/${trade.public_id}/cancel`, {});
|
||||
router.push("/trades");
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Failed to cancel trade");
|
||||
|
|
|
|||
|
|
@ -30,8 +30,8 @@ export default function TradesPage() {
|
|||
|
||||
const [trades, setTrades] = useState<ExchangeResponse[]>([]);
|
||||
const [isLoadingTrades, setIsLoadingTrades] = useState(true);
|
||||
const [cancellingId, setCancellingId] = useState<number | null>(null);
|
||||
const [confirmCancelId, setConfirmCancelId] = useState<number | null>(null);
|
||||
const [cancellingId, setCancellingId] = useState<string | null>(null);
|
||||
const [confirmCancelId, setConfirmCancelId] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchTrades = useCallback(async () => {
|
||||
|
|
@ -52,12 +52,12 @@ export default function TradesPage() {
|
|||
}
|
||||
}, [user, isAuthorized, fetchTrades]);
|
||||
|
||||
const handleCancel = async (tradeId: number) => {
|
||||
setCancellingId(tradeId);
|
||||
const handleCancel = async (publicId: string) => {
|
||||
setCancellingId(publicId);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
await api.post<ExchangeResponse>(`/api/trades/${tradeId}/cancel`, {});
|
||||
await api.post<ExchangeResponse>(`/api/trades/${publicId}/cancel`, {});
|
||||
await fetchTrades();
|
||||
setConfirmCancelId(null);
|
||||
} catch (err) {
|
||||
|
|
@ -115,14 +115,7 @@ export default function TradesPage() {
|
|||
const status = getTradeStatusDisplay(trade.status);
|
||||
const isBuy = trade.direction === "buy";
|
||||
return (
|
||||
<div
|
||||
key={trade.id}
|
||||
style={{
|
||||
...tradeCardStyles.tradeCard,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={() => router.push(`/trades/${trade.id}`)}
|
||||
>
|
||||
<div key={trade.id} style={tradeCardStyles.tradeCard}>
|
||||
<div style={tradeCardStyles.tradeHeader}>
|
||||
<div style={tradeCardStyles.tradeInfo}>
|
||||
<div style={tradeCardStyles.tradeTime}>
|
||||
|
|
@ -181,34 +174,54 @@ export default function TradesPage() {
|
|||
</span>
|
||||
</div>
|
||||
|
||||
{trade.status === "booked" && (
|
||||
<div style={tradeCardStyles.buttonGroup}>
|
||||
{confirmCancelId === trade.id ? (
|
||||
<>
|
||||
<div style={tradeCardStyles.buttonGroup}>
|
||||
{trade.status === "booked" && (
|
||||
<>
|
||||
{confirmCancelId === trade.public_id ? (
|
||||
<>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleCancel(trade.public_id);
|
||||
}}
|
||||
disabled={cancellingId === trade.public_id}
|
||||
style={styles.confirmButton}
|
||||
>
|
||||
{cancellingId === trade.public_id ? "..." : "Confirm"}
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setConfirmCancelId(null);
|
||||
}}
|
||||
style={buttonStyles.secondaryButton}
|
||||
>
|
||||
No
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => handleCancel(trade.id)}
|
||||
disabled={cancellingId === trade.id}
|
||||
style={styles.confirmButton}
|
||||
>
|
||||
{cancellingId === trade.id ? "..." : "Confirm"}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setConfirmCancelId(null)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setConfirmCancelId(trade.public_id);
|
||||
}}
|
||||
style={buttonStyles.secondaryButton}
|
||||
>
|
||||
No
|
||||
Cancel
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => setConfirmCancelId(trade.id)}
|
||||
style={buttonStyles.secondaryButton}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
router.push(`/trades/${trade.public_id}`);
|
||||
}}
|
||||
style={styles.viewDetailsButton}
|
||||
>
|
||||
View Details
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -233,9 +246,7 @@ export default function TradesPage() {
|
|||
style={{
|
||||
...tradeCardStyles.tradeCard,
|
||||
...styles.tradeCardPast,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={() => router.push(`/trades/${trade.id}`)}
|
||||
>
|
||||
<div style={tradeCardStyles.tradeTime}>
|
||||
{formatDateTime(trade.slot_start)}
|
||||
|
|
@ -269,16 +280,26 @@ export default function TradesPage() {
|
|||
<SatsDisplay sats={trade.sats_amount} />
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
style={{
|
||||
...badgeStyles.badge,
|
||||
background: status.bgColor,
|
||||
color: status.textColor,
|
||||
marginTop: "0.5rem",
|
||||
}}
|
||||
>
|
||||
{status.text}
|
||||
</span>
|
||||
<div style={tradeCardStyles.buttonGroup}>
|
||||
<span
|
||||
style={{
|
||||
...badgeStyles.badge,
|
||||
background: status.bgColor,
|
||||
color: status.textColor,
|
||||
}}
|
||||
>
|
||||
{status.text}
|
||||
</span>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
router.push(`/trades/${trade.public_id}`);
|
||||
}}
|
||||
style={styles.viewDetailsButton}
|
||||
>
|
||||
View Details
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
|
@ -330,4 +351,15 @@ const styles: Record<string, CSSProperties> = {
|
|||
color: "#a78bfa",
|
||||
textDecoration: "none",
|
||||
},
|
||||
viewDetailsButton: {
|
||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
||||
padding: "0.35rem 0.75rem",
|
||||
fontSize: "0.75rem",
|
||||
background: "rgba(167, 139, 250, 0.15)",
|
||||
border: "1px solid rgba(167, 139, 250, 0.3)",
|
||||
borderRadius: "6px",
|
||||
color: "#a78bfa",
|
||||
cursor: "pointer",
|
||||
transition: "all 0.2s",
|
||||
},
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue