added steambot, trades and trasctions.
This commit is contained in:
@@ -319,28 +319,37 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Confirm Sale Modal -->
|
||||
<!-- Trade Status Modal -->
|
||||
<div
|
||||
v-if="showConfirmModal"
|
||||
v-if="showTradeModal"
|
||||
class="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4"
|
||||
@click.self="showConfirmModal = false"
|
||||
@click.self="
|
||||
tradeState === 'created' || tradeState === 'error'
|
||||
? null
|
||||
: (showTradeModal = false)
|
||||
"
|
||||
>
|
||||
<div
|
||||
class="bg-surface-light rounded-lg max-w-md w-full p-6 border border-surface-lighter"
|
||||
>
|
||||
<!-- Modal Header -->
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-xl font-bold text-white">Confirm Sale</h3>
|
||||
<h3 class="text-xl font-bold text-white">{{ tradeModalTitle }}</h3>
|
||||
<button
|
||||
@click="showConfirmModal = false"
|
||||
v-if="tradeState !== 'confirming' && tradeState !== 'created'"
|
||||
@click="
|
||||
showTradeModal = false;
|
||||
currentTrade = null;
|
||||
selectedItems = [];
|
||||
"
|
||||
class="text-text-secondary hover:text-white transition-colors"
|
||||
>
|
||||
<X class="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Modal Content -->
|
||||
<div class="space-y-4 mb-6">
|
||||
<!-- Confirming State -->
|
||||
<div v-if="tradeState === 'confirming'" class="space-y-4 mb-6">
|
||||
<p class="text-text-secondary">
|
||||
You're about to sell
|
||||
<strong class="text-white">{{ selectedItems.length }}</strong>
|
||||
@@ -375,28 +384,195 @@
|
||||
<AlertCircle class="w-5 h-5 text-primary flex-shrink-0 mt-0.5" />
|
||||
<p class="text-sm text-text-secondary">
|
||||
<strong class="text-white">Important:</strong> You will receive a
|
||||
Steam trade offer shortly. Please accept it to complete the sale.
|
||||
Funds will be credited to your balance after the trade is
|
||||
accepted.
|
||||
Steam trade offer. Please verify the code before accepting.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
@click="showTradeModal = false"
|
||||
class="flex-1 px-4 py-2.5 bg-surface hover:bg-surface-lighter text-white rounded-lg transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
@click="confirmSale"
|
||||
:disabled="isProcessing"
|
||||
class="flex-1 px-4 py-2.5 bg-gradient-to-r from-primary to-primary-dark text-surface-dark font-semibold rounded-lg hover:opacity-90 transition-opacity disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
||||
>
|
||||
<Loader2 v-if="isProcessing" class="w-4 h-4 animate-spin" />
|
||||
<span>{{ isProcessing ? "Processing..." : "Confirm Sale" }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Actions -->
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
@click="showConfirmModal = false"
|
||||
class="flex-1 px-4 py-2.5 bg-surface hover:bg-surface-lighter text-white rounded-lg transition-colors"
|
||||
<!-- Trade Created State -->
|
||||
<div v-else-if="tradeState === 'created'" class="space-y-4">
|
||||
<div class="text-center py-4">
|
||||
<div
|
||||
class="w-16 h-16 bg-primary/20 rounded-full flex items-center justify-center mx-auto mb-4"
|
||||
>
|
||||
<CheckCircle class="w-8 h-8 text-primary" />
|
||||
</div>
|
||||
<p class="text-white font-semibold mb-2">Trade Offer Created!</p>
|
||||
<p class="text-text-secondary text-sm">
|
||||
Check your Steam for the trade offer
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Verification Code Display -->
|
||||
<div
|
||||
class="bg-gradient-to-br from-primary/20 to-primary-dark/20 border-2 border-primary rounded-lg p-6 text-center"
|
||||
>
|
||||
Cancel
|
||||
<p class="text-text-secondary text-sm mb-2">Verification Code</p>
|
||||
<p class="text-4xl font-bold text-white tracking-widest font-mono">
|
||||
{{ currentTrade?.verificationCode }}
|
||||
</p>
|
||||
<p class="text-text-secondary text-xs mt-2">
|
||||
Match this code with the one in your Steam trade offer
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Open Trade Link Button -->
|
||||
<a
|
||||
v-if="currentTrade?.tradeOfferUrl"
|
||||
:href="currentTrade.tradeOfferUrl"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="w-full px-6 py-3 bg-gradient-to-r from-primary to-primary-dark text-surface-dark font-semibold rounded-lg hover:opacity-90 transition-opacity flex items-center justify-center gap-2 text-center"
|
||||
>
|
||||
<ExternalLink class="w-5 h-5" />
|
||||
Open Trade in Steam
|
||||
</a>
|
||||
|
||||
<!-- Trade Details -->
|
||||
<div class="bg-surface rounded-lg p-4 space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-text-secondary">Items:</span>
|
||||
<span class="text-white font-semibold">
|
||||
{{ currentTrade?.itemCount }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-text-secondary">Value:</span>
|
||||
<span class="text-white font-semibold">
|
||||
{{ formatCurrency(currentTrade?.totalValue || 0) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-text-secondary">Status:</span>
|
||||
<span
|
||||
class="text-yellow-400 font-semibold flex items-center gap-1"
|
||||
>
|
||||
<Loader2 class="w-3 h-3 animate-spin" />
|
||||
Pending
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Instructions -->
|
||||
<div
|
||||
class="bg-primary/10 border border-primary/30 rounded-lg p-3 space-y-2"
|
||||
>
|
||||
<p class="text-white font-semibold text-sm">Next Steps:</p>
|
||||
<ol
|
||||
class="text-text-secondary text-sm space-y-1 list-decimal list-inside"
|
||||
>
|
||||
<li>Click "Open Trade in Steam" button above</li>
|
||||
<li>
|
||||
Verify the code matches:
|
||||
<span class="text-primary font-mono font-bold">{{
|
||||
currentTrade?.verificationCode
|
||||
}}</span>
|
||||
</li>
|
||||
<li>Accept the trade</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<button
|
||||
@click="
|
||||
showTradeModal = false;
|
||||
currentTrade = null;
|
||||
selectedItems = [];
|
||||
"
|
||||
class="w-full px-4 py-2.5 bg-surface hover:bg-surface-lighter text-white rounded-lg transition-colors"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Trade Accepted State -->
|
||||
<div v-else-if="tradeState === 'accepted'" class="space-y-4">
|
||||
<div class="text-center py-4">
|
||||
<div
|
||||
class="w-16 h-16 bg-green-500/20 rounded-full flex items-center justify-center mx-auto mb-4"
|
||||
>
|
||||
<CheckCircle class="w-8 h-8 text-green-500" />
|
||||
</div>
|
||||
<p class="text-white font-semibold text-lg mb-2">Trade Complete!</p>
|
||||
<p class="text-text-secondary text-sm">
|
||||
Your balance has been credited
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-surface rounded-lg p-4 space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-text-secondary">Amount Credited:</span>
|
||||
<span class="text-green-500 font-bold text-xl">
|
||||
+{{
|
||||
formatCurrency(
|
||||
currentTrade?.amount || currentTrade?.totalValue || 0
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-text-secondary">New Balance:</span>
|
||||
<span class="text-white font-semibold">
|
||||
{{
|
||||
formatCurrency(
|
||||
currentTrade?.newBalance || authStore.user?.balance || 0
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
@click="confirmSale"
|
||||
:disabled="isProcessing"
|
||||
class="flex-1 px-4 py-2.5 bg-gradient-to-r from-primary to-primary-dark text-surface-dark font-semibold rounded-lg hover:opacity-90 transition-opacity disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
||||
@click="
|
||||
showTradeModal = false;
|
||||
currentTrade = null;
|
||||
selectedItems = [];
|
||||
"
|
||||
class="w-full px-4 py-2.5 bg-gradient-to-r from-primary to-primary-dark text-surface-dark font-semibold rounded-lg hover:opacity-90 transition-opacity"
|
||||
>
|
||||
<Loader2 v-if="isProcessing" class="w-4 h-4 animate-spin" />
|
||||
<span>{{ isProcessing ? "Processing..." : "Confirm Sale" }}</span>
|
||||
Done
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Error State -->
|
||||
<div v-else-if="tradeState === 'error'" class="space-y-4">
|
||||
<div class="text-center py-4">
|
||||
<div
|
||||
class="w-16 h-16 bg-red-500/20 rounded-full flex items-center justify-center mx-auto mb-4"
|
||||
>
|
||||
<AlertCircle class="w-8 h-8 text-red-500" />
|
||||
</div>
|
||||
<p class="text-white font-semibold text-lg mb-2">Trade Failed</p>
|
||||
<p class="text-text-secondary text-sm">{{ tradeError }}</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
@click="
|
||||
showTradeModal = false;
|
||||
currentTrade = null;
|
||||
tradeState = 'confirming';
|
||||
tradeError = null;
|
||||
"
|
||||
class="w-full px-4 py-2.5 bg-surface hover:bg-surface-lighter text-white rounded-lg transition-colors"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -405,7 +581,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import { ref, computed, onMounted, onUnmounted } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import axios from "@/utils/axios";
|
||||
@@ -424,6 +600,7 @@ import {
|
||||
Settings,
|
||||
AlertTriangle,
|
||||
RefreshCw,
|
||||
ExternalLink,
|
||||
} from "lucide-vue-next";
|
||||
|
||||
const router = useRouter();
|
||||
@@ -437,7 +614,10 @@ const selectedItems = ref([]);
|
||||
const isLoading = ref(false);
|
||||
const isProcessing = ref(false);
|
||||
|
||||
const showConfirmModal = ref(false);
|
||||
const showTradeModal = ref(false);
|
||||
const tradeState = ref("confirming"); // confirming, created, accepted, error
|
||||
const currentTrade = ref(null);
|
||||
const tradeError = ref(null);
|
||||
const searchQuery = ref("");
|
||||
const selectedGame = ref("cs2");
|
||||
const sortBy = ref("price-desc");
|
||||
@@ -463,6 +643,24 @@ const totalSelectedValue = computed(() => {
|
||||
}, 0);
|
||||
});
|
||||
|
||||
const tradeModalTitle = computed(() => {
|
||||
switch (tradeState.value) {
|
||||
case "confirming":
|
||||
return "Confirm Sale";
|
||||
case "created":
|
||||
return "Trade Offer Created";
|
||||
case "accepted":
|
||||
return "Trade Complete!";
|
||||
case "error":
|
||||
return "Trade Failed";
|
||||
default:
|
||||
return "Trade Status";
|
||||
}
|
||||
});
|
||||
|
||||
// WebSocket connection for real-time updates
|
||||
let wsMessageHandler = null;
|
||||
|
||||
// Methods
|
||||
const fetchInventory = async () => {
|
||||
isLoading.value = true;
|
||||
@@ -596,7 +794,11 @@ const handleSellClick = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
showConfirmModal.value = true;
|
||||
// Reset modal state
|
||||
tradeState.value = "confirming";
|
||||
currentTrade.value = null;
|
||||
tradeError.value = null;
|
||||
showTradeModal.value = true;
|
||||
};
|
||||
|
||||
const confirmSale = async () => {
|
||||
@@ -604,7 +806,7 @@ const confirmSale = async () => {
|
||||
|
||||
if (!hasTradeUrl.value) {
|
||||
toast.error("Trade URL is required to sell items");
|
||||
showConfirmModal.value = false;
|
||||
showTradeModal.value = false;
|
||||
router.push("/profile");
|
||||
return;
|
||||
}
|
||||
@@ -613,8 +815,11 @@ const confirmSale = async () => {
|
||||
|
||||
try {
|
||||
const response = await axios.post("/api/inventory/sell", {
|
||||
tradeUrl: authStore.user?.tradeUrl,
|
||||
items: selectedItems.value.map((item) => ({
|
||||
assetid: item.assetid,
|
||||
appid: item.appid || (selectedGame.value === "cs2" ? 730 : 252490),
|
||||
contextid: item.contextid || "2",
|
||||
name: item.name,
|
||||
price: item.estimatedPrice,
|
||||
image: item.image,
|
||||
@@ -627,20 +832,11 @@ const confirmSale = async () => {
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
toast.success(
|
||||
`Successfully listed ${selectedItems.value.length} item${
|
||||
selectedItems.value.length > 1 ? "s" : ""
|
||||
} for ${formatCurrency(response.data.totalEarned)}!`
|
||||
);
|
||||
const trade = response.data.trade;
|
||||
|
||||
toast.info(
|
||||
"You will receive a Steam trade offer shortly. Please accept it to complete the sale."
|
||||
);
|
||||
|
||||
// Update balance
|
||||
if (response.data.newBalance !== undefined) {
|
||||
authStore.updateBalance(response.data.newBalance);
|
||||
}
|
||||
// Update modal state to show verification code
|
||||
currentTrade.value = trade;
|
||||
tradeState.value = "created";
|
||||
|
||||
// Remove sold items from list
|
||||
const soldAssetIds = selectedItems.value.map((item) => item.assetid);
|
||||
@@ -650,22 +846,82 @@ const confirmSale = async () => {
|
||||
filteredItems.value = filteredItems.value.filter(
|
||||
(item) => !soldAssetIds.includes(item.assetid)
|
||||
);
|
||||
|
||||
// Clear selection and close modal
|
||||
selectedItems.value = [];
|
||||
showConfirmModal.value = false;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to sell items:", err);
|
||||
console.error("Failed to create trade:", err);
|
||||
const message =
|
||||
err.response?.data?.message ||
|
||||
"Failed to complete sale. Please try again.";
|
||||
toast.error(message);
|
||||
"Failed to create trade offer. Please try again.";
|
||||
|
||||
tradeError.value = message;
|
||||
tradeState.value = "error";
|
||||
} finally {
|
||||
isProcessing.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Listen for WebSocket trade updates
|
||||
const setupWebSocketListeners = () => {
|
||||
// Get WebSocket from auth store if available
|
||||
if (authStore.ws) {
|
||||
wsMessageHandler = (event) => {
|
||||
try {
|
||||
const message = JSON.parse(event.data);
|
||||
|
||||
if (message.type === "trade_completed" && currentTrade.value) {
|
||||
// Trade was accepted and completed
|
||||
if (message.data.tradeId === currentTrade.value.tradeId) {
|
||||
currentTrade.value = {
|
||||
...currentTrade.value,
|
||||
...message.data,
|
||||
};
|
||||
tradeState.value = "accepted";
|
||||
|
||||
// Update balance
|
||||
if (message.data.newBalance !== undefined) {
|
||||
authStore.updateBalance(message.data.newBalance);
|
||||
}
|
||||
|
||||
toast.success(
|
||||
`Trade completed! +${formatCurrency(message.data.amount)}`
|
||||
);
|
||||
}
|
||||
} else if (message.type === "trade_declined" && currentTrade.value) {
|
||||
if (message.data.tradeId === currentTrade.value.tradeId) {
|
||||
tradeError.value = "Trade was declined on Steam";
|
||||
tradeState.value = "error";
|
||||
}
|
||||
} else if (message.type === "trade_expired" && currentTrade.value) {
|
||||
if (message.data.tradeId === currentTrade.value.tradeId) {
|
||||
tradeError.value = "Trade offer expired";
|
||||
tradeState.value = "error";
|
||||
}
|
||||
} else if (message.type === "balance_update") {
|
||||
authStore.updateBalance(message.data.balance);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error handling WebSocket message:", err);
|
||||
}
|
||||
};
|
||||
|
||||
authStore.ws.addEventListener("message", wsMessageHandler);
|
||||
}
|
||||
};
|
||||
|
||||
const cleanupWebSocketListeners = () => {
|
||||
if (authStore.ws && wsMessageHandler) {
|
||||
authStore.ws.removeEventListener("message", wsMessageHandler);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
setupWebSocketListeners();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
cleanupWebSocketListeners();
|
||||
});
|
||||
|
||||
const formatCurrency = (amount) => {
|
||||
return new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
|
||||
@@ -9,11 +9,67 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Pending Trades Section -->
|
||||
<div v-if="pendingTrades.length > 0" class="mb-6">
|
||||
<h2 class="text-xl font-bold text-white mb-4 flex items-center gap-2">
|
||||
<Clock class="w-5 h-5 text-yellow-400" />
|
||||
Pending Trades
|
||||
</h2>
|
||||
<div class="space-y-3">
|
||||
<div
|
||||
v-for="trade in pendingTrades"
|
||||
:key="trade._id"
|
||||
class="bg-surface-light rounded-lg border border-yellow-400/30 p-4"
|
||||
>
|
||||
<div class="flex items-center justify-between flex-wrap gap-4">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<div
|
||||
class="w-2 h-2 bg-yellow-400 rounded-full animate-pulse"
|
||||
></div>
|
||||
<span class="text-white font-medium">
|
||||
Selling {{ trade.items?.length || 0 }} item(s)
|
||||
</span>
|
||||
</div>
|
||||
<div class="h-6 w-px bg-surface-lighter"></div>
|
||||
<div class="text-text-secondary text-sm">
|
||||
Code:
|
||||
<span class="text-primary font-mono font-bold">{{
|
||||
trade.verificationCode
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="text-white font-semibold">
|
||||
{{ formatCurrency(trade.totalValue || 0) }}
|
||||
</div>
|
||||
<a
|
||||
v-if="trade.tradeOfferUrl"
|
||||
:href="trade.tradeOfferUrl"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="px-4 py-2 bg-primary hover:bg-primary-dark text-surface-dark font-medium rounded-lg transition-colors text-sm flex items-center gap-1"
|
||||
>
|
||||
<ExternalLink class="w-4 h-4" />
|
||||
Open
|
||||
</a>
|
||||
<button
|
||||
@click="viewTradeDetails(trade)"
|
||||
class="px-4 py-2 bg-primary/20 hover:bg-primary/30 text-primary rounded-lg transition-colors text-sm"
|
||||
>
|
||||
Details
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div
|
||||
class="bg-surface-light rounded-lg border border-surface-lighter p-6 mb-6"
|
||||
>
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-text-secondary mb-2">
|
||||
Type
|
||||
@@ -65,12 +121,6 @@
|
||||
<option value="year">Last Year</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex items-end">
|
||||
<button @click="resetFilters" class="btn-secondary w-full">
|
||||
Reset Filters
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -417,11 +467,112 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Trade Details Modal -->
|
||||
<div
|
||||
v-if="showTradeModal && selectedTrade"
|
||||
class="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4"
|
||||
@click.self="closeTradeModal"
|
||||
>
|
||||
<div
|
||||
class="bg-surface-light rounded-lg max-w-md w-full p-6 border border-surface-lighter"
|
||||
>
|
||||
<!-- Modal Header -->
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-xl font-bold text-white">Trade Details</h3>
|
||||
<button
|
||||
@click="closeTradeModal"
|
||||
class="text-text-secondary hover:text-white transition-colors"
|
||||
>
|
||||
<X class="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Trade Status -->
|
||||
<div class="text-center py-4 mb-4">
|
||||
<div
|
||||
class="w-16 h-16 bg-yellow-400/20 rounded-full flex items-center justify-center mx-auto mb-3"
|
||||
>
|
||||
<Clock class="w-8 h-8 text-yellow-400" />
|
||||
</div>
|
||||
<p class="text-white font-semibold mb-1">Waiting for Acceptance</p>
|
||||
<p class="text-text-secondary text-sm">
|
||||
Check your Steam trade offers
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Verification Code -->
|
||||
<div
|
||||
class="bg-gradient-to-br from-primary/20 to-primary-dark/20 border-2 border-primary rounded-lg p-6 text-center mb-4"
|
||||
>
|
||||
<p class="text-text-secondary text-sm mb-2">Verification Code</p>
|
||||
<p class="text-4xl font-bold text-white tracking-widest font-mono">
|
||||
{{ selectedTrade.verificationCode }}
|
||||
</p>
|
||||
<p class="text-text-secondary text-xs mt-2">
|
||||
Match this code with your Steam trade offer
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Open Trade Link Button -->
|
||||
<a
|
||||
v-if="selectedTrade.tradeOfferUrl"
|
||||
:href="selectedTrade.tradeOfferUrl"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="w-full px-6 py-3 bg-gradient-to-r from-primary to-primary-dark text-surface-dark font-semibold rounded-lg hover:opacity-90 transition-opacity flex items-center justify-center gap-2 text-center mb-4"
|
||||
>
|
||||
<ExternalLink class="w-5 h-5" />
|
||||
Open Trade in Steam
|
||||
</a>
|
||||
|
||||
<!-- Trade Info -->
|
||||
<div class="bg-surface rounded-lg p-4 space-y-2 mb-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-text-secondary">Items:</span>
|
||||
<span class="text-white font-semibold">{{
|
||||
selectedTrade.items?.length || 0
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-text-secondary">Value:</span>
|
||||
<span class="text-white font-semibold">{{
|
||||
formatCurrency(selectedTrade.totalValue || 0)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-text-secondary">Created:</span>
|
||||
<span class="text-white font-semibold">{{
|
||||
formatDate(selectedTrade.sentAt || selectedTrade.createdAt)
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Instructions -->
|
||||
<div class="bg-primary/10 border border-primary/30 rounded-lg p-3 mb-4">
|
||||
<p class="text-white font-semibold text-sm mb-2">Instructions:</p>
|
||||
<ol
|
||||
class="text-text-secondary text-sm space-y-1 list-decimal list-inside"
|
||||
>
|
||||
<li>Click "Open Trade in Steam" button above</li>
|
||||
<li>Verify the code matches</li>
|
||||
<li>Accept the trade</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<button
|
||||
@click="closeTradeModal"
|
||||
class="w-full px-4 py-2.5 bg-surface hover:bg-surface-lighter text-white rounded-lg transition-colors"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, watch } from "vue";
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from "vue";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import axios from "@/utils/axios";
|
||||
import { useToast } from "vue-toastification";
|
||||
@@ -443,6 +594,9 @@ import {
|
||||
RefreshCw,
|
||||
Gift,
|
||||
DollarSign,
|
||||
Clock,
|
||||
X,
|
||||
ExternalLink,
|
||||
} from "lucide-vue-next";
|
||||
|
||||
const authStore = useAuthStore();
|
||||
@@ -455,6 +609,9 @@ const currentPage = ref(1);
|
||||
const perPage = ref(10);
|
||||
const totalTransactions = ref(0);
|
||||
const expandedTransaction = ref(null);
|
||||
const pendingTrades = ref([]);
|
||||
const showTradeModal = ref(false);
|
||||
const selectedTrade = ref(null);
|
||||
|
||||
const filters = ref({
|
||||
type: "",
|
||||
@@ -462,6 +619,8 @@ const filters = ref({
|
||||
dateRange: "all",
|
||||
});
|
||||
|
||||
let wsMessageHandler = null;
|
||||
|
||||
const stats = ref({
|
||||
totalDeposits: 0,
|
||||
totalWithdrawals: 0,
|
||||
@@ -557,13 +716,83 @@ const fetchTransactions = async () => {
|
||||
console.error("Response:", error.response?.data);
|
||||
console.error("Status:", error.response?.status);
|
||||
if (error.response?.status !== 404) {
|
||||
toast.error("Failed to load transactions");
|
||||
toast.error("Failed to load transaction history");
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchPendingTrades = async () => {
|
||||
try {
|
||||
const response = await axios.get("/api/inventory/trades");
|
||||
if (response.data.success) {
|
||||
pendingTrades.value = response.data.trades.filter(
|
||||
(t) => t.state === "pending"
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to load pending trades:", err);
|
||||
}
|
||||
};
|
||||
|
||||
const viewTradeDetails = (trade) => {
|
||||
selectedTrade.value = trade;
|
||||
showTradeModal.value = true;
|
||||
};
|
||||
|
||||
const closeTradeModal = () => {
|
||||
showTradeModal.value = false;
|
||||
selectedTrade.value = null;
|
||||
};
|
||||
|
||||
const setupWebSocketListeners = () => {
|
||||
if (authStore.ws) {
|
||||
wsMessageHandler = (event) => {
|
||||
try {
|
||||
const message = JSON.parse(event.data);
|
||||
|
||||
if (message.type === "trade_completed") {
|
||||
// Remove from pending
|
||||
pendingTrades.value = pendingTrades.value.filter(
|
||||
(t) => t._id !== message.data.tradeId
|
||||
);
|
||||
// Refresh transactions
|
||||
fetchTransactions();
|
||||
toast.success("Trade completed! Balance updated.");
|
||||
} else if (
|
||||
message.type === "trade_declined" ||
|
||||
message.type === "trade_expired" ||
|
||||
message.type === "trade_canceled"
|
||||
) {
|
||||
// Remove from pending
|
||||
pendingTrades.value = pendingTrades.value.filter(
|
||||
(t) => t._id !== message.data.tradeId
|
||||
);
|
||||
if (message.type === "trade_declined") {
|
||||
toast.warning("Trade was declined");
|
||||
} else if (message.type === "trade_expired") {
|
||||
toast.warning("Trade offer expired");
|
||||
}
|
||||
} else if (message.type === "trade_created") {
|
||||
// Add to pending trades
|
||||
fetchPendingTrades();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error handling WebSocket message:", err);
|
||||
}
|
||||
};
|
||||
|
||||
authStore.ws.addEventListener("message", wsMessageHandler);
|
||||
}
|
||||
};
|
||||
|
||||
const cleanupWebSocketListeners = () => {
|
||||
if (authStore.ws && wsMessageHandler) {
|
||||
authStore.ws.removeEventListener("message", wsMessageHandler);
|
||||
}
|
||||
};
|
||||
|
||||
const nextPage = () => {
|
||||
if (hasNextPage.value) {
|
||||
currentPage.value++;
|
||||
@@ -731,5 +960,11 @@ const getSessionColor = (sessionIdShort) => {
|
||||
// Lifecycle
|
||||
onMounted(() => {
|
||||
fetchTransactions();
|
||||
fetchPendingTrades();
|
||||
setupWebSocketListeners();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
cleanupWebSocketListeners();
|
||||
});
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user