added steambot, trades and trasctions.

This commit is contained in:
2026-01-10 05:31:01 +00:00
parent 232968de1e
commit b90cdd59df
10 changed files with 3113 additions and 138 deletions

View File

@@ -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",