first commit

This commit is contained in:
2026-01-10 04:57:43 +00:00
parent 16a76a2cd6
commit 232968de1e
131 changed files with 43262 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,118 @@
<script setup>
import { ref } from 'vue'
import { Wallet, CreditCard, DollarSign } from 'lucide-vue-next'
const amount = ref(10)
const paymentMethod = ref('card')
const quickAmounts = [10, 25, 50, 100, 250, 500]
</script>
<template>
<div class="deposit-page min-h-screen py-8">
<div class="container-custom max-w-3xl">
<h1 class="text-3xl font-display font-bold text-white mb-2">Deposit Funds</h1>
<p class="text-gray-400 mb-8">Add funds to your account balance</p>
<div class="card card-body space-y-6">
<!-- Quick Amount Selection -->
<div>
<label class="input-label mb-3">Select Amount</label>
<div class="grid grid-cols-3 sm:grid-cols-6 gap-3">
<button
v-for="quickAmount in quickAmounts"
:key="quickAmount"
@click="amount = quickAmount"
:class="[
'py-3 rounded-lg font-semibold transition-all',
amount === quickAmount
? 'bg-primary-500 text-white shadow-glow'
: 'bg-surface-light text-gray-300 hover:bg-surface-lighter border border-surface-lighter'
]"
>
${{ quickAmount }}
</button>
</div>
</div>
<!-- Custom Amount -->
<div>
<label class="input-label mb-2">Custom Amount</label>
<div class="relative">
<DollarSign class="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-500" />
<input
v-model.number="amount"
type="number"
min="5"
max="10000"
class="input pl-10"
placeholder="Enter amount"
/>
</div>
<p class="text-xs text-gray-500 mt-2">Minimum: $5 Maximum: $10,000</p>
</div>
<!-- Payment Method -->
<div>
<label class="input-label mb-3">Payment Method</label>
<div class="space-y-3">
<label class="flex items-center gap-3 p-4 bg-surface-light rounded-lg border border-surface-lighter hover:border-primary-500/50 cursor-pointer transition-colors">
<input
type="radio"
v-model="paymentMethod"
value="card"
class="w-4 h-4"
/>
<CreditCard class="w-5 h-5 text-gray-400" />
<div class="flex-1">
<div class="text-white font-medium">Credit/Debit Card</div>
<div class="text-sm text-gray-400">Visa, Mastercard, Amex</div>
</div>
</label>
<label class="flex items-center gap-3 p-4 bg-surface-light rounded-lg border border-surface-lighter hover:border-primary-500/50 cursor-pointer transition-colors">
<input
type="radio"
v-model="paymentMethod"
value="crypto"
class="w-4 h-4"
/>
<Wallet class="w-5 h-5 text-gray-400" />
<div class="flex-1">
<div class="text-white font-medium">Cryptocurrency</div>
<div class="text-sm text-gray-400">BTC, ETH, USDT</div>
</div>
</label>
</div>
</div>
<!-- Summary -->
<div class="p-4 bg-surface-dark rounded-lg space-y-2">
<div class="flex justify-between text-sm">
<span class="text-gray-400">Amount</span>
<span class="text-white font-medium">${{ amount.toFixed(2) }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-gray-400">Processing Fee</span>
<span class="text-white font-medium">$0.00</span>
</div>
<div class="divider"></div>
<div class="flex justify-between">
<span class="text-white font-semibold">Total</span>
<span class="text-xl font-bold text-primary-500">${{ amount.toFixed(2) }}</span>
</div>
</div>
<!-- Submit Button -->
<button class="btn btn-primary w-full btn-lg">
Continue to Payment
</button>
<!-- Info Notice -->
<div class="p-4 bg-accent-blue/10 border border-accent-blue/30 rounded-lg text-sm text-gray-400">
<p>💡 Deposits are processed instantly. Your balance will be updated immediately after payment confirmation.</p>
</div>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,457 @@
<template>
<div class="min-h-screen bg-surface py-8">
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<!-- Header -->
<div class="mb-8">
<h1 class="text-3xl font-bold text-white mb-2">🔍 Authentication Diagnostic</h1>
<p class="text-text-secondary">
Use this page to diagnose cookie and authentication issues
</p>
</div>
<!-- Quick Info -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<div class="bg-surface-light rounded-lg border border-surface-lighter p-4">
<div class="text-text-secondary text-sm mb-1">Login Status</div>
<div class="text-xl font-bold" :class="isLoggedIn ? 'text-success' : 'text-danger'">
{{ isLoggedIn ? '✅ Logged In' : '❌ Not Logged In' }}
</div>
</div>
<div class="bg-surface-light rounded-lg border border-surface-lighter p-4">
<div class="text-text-secondary text-sm mb-1">Browser Cookies</div>
<div class="text-xl font-bold" :class="hasBrowserCookies ? 'text-success' : 'text-danger'">
{{ hasBrowserCookies ? '✅ Present' : '❌ Missing' }}
</div>
</div>
<div class="bg-surface-light rounded-lg border border-surface-lighter p-4">
<div class="text-text-secondary text-sm mb-1">Backend Sees Cookies</div>
<div class="text-xl font-bold" :class="backendHasCookies ? 'text-success' : 'text-danger'">
{{ backendHasCookies === null ? '⏳ Testing...' : backendHasCookies ? '✅ Yes' : '❌ No' }}
</div>
</div>
</div>
<!-- Test Results -->
<div class="space-y-6">
<!-- Browser Cookies Test -->
<div class="bg-surface-light rounded-lg border border-surface-lighter p-6">
<div class="flex items-center justify-between mb-4">
<h2 class="text-xl font-bold text-white flex items-center gap-2">
<span>1</span> Browser Cookies Check
</h2>
<button @click="checkBrowserCookies" class="btn-secondary text-sm">
Refresh
</button>
</div>
<div v-if="browserCookies" class="space-y-2">
<div class="flex items-start gap-2">
<span :class="browserCookies.hasAccessToken ? 'text-success' : 'text-danger'">
{{ browserCookies.hasAccessToken ? '✅' : '❌' }}
</span>
<div class="flex-1">
<span class="text-white font-medium">accessToken:</span>
<span class="text-text-secondary ml-2">
{{ browserCookies.hasAccessToken ? 'Present (' + browserCookies.accessTokenLength + ' chars)' : 'Missing' }}
</span>
</div>
</div>
<div class="flex items-start gap-2">
<span :class="browserCookies.hasRefreshToken ? 'text-success' : 'text-danger'">
{{ browserCookies.hasRefreshToken ? '✅' : '❌' }}
</span>
<div class="flex-1">
<span class="text-white font-medium">refreshToken:</span>
<span class="text-text-secondary ml-2">
{{ browserCookies.hasRefreshToken ? 'Present (' + browserCookies.refreshTokenLength + ' chars)' : 'Missing' }}
</span>
</div>
</div>
<div v-if="!browserCookies.hasAccessToken" class="mt-4 p-4 bg-danger/10 border border-danger/30 rounded-lg">
<p class="text-danger font-medium mb-2"> No cookies found in browser!</p>
<p class="text-sm text-text-secondary">
You need to log in via Steam. Click "Login with Steam" in the navigation bar.
</p>
</div>
</div>
</div>
<!-- Backend Cookie Check -->
<div class="bg-surface-light rounded-lg border border-surface-lighter p-6">
<div class="flex items-center justify-between mb-4">
<h2 class="text-xl font-bold text-white flex items-center gap-2">
<span>2</span> Backend Cookie Check
</h2>
<button @click="checkBackendCookies" class="btn-secondary text-sm" :disabled="loadingBackend">
<Loader v-if="loadingBackend" class="w-4 h-4 animate-spin" />
<span v-else>Test Now</span>
</button>
</div>
<div v-if="backendDebug" class="space-y-3">
<div class="flex items-start gap-2">
<span :class="backendDebug.hasAccessToken ? 'text-success' : 'text-danger'">
{{ backendDebug.hasAccessToken ? '✅' : '❌' }}
</span>
<div class="flex-1">
<span class="text-white font-medium">Backend received accessToken:</span>
<span class="text-text-secondary ml-2">{{ backendDebug.hasAccessToken ? 'Yes' : 'No' }}</span>
</div>
</div>
<div class="flex items-start gap-2">
<span :class="backendDebug.hasRefreshToken ? 'text-success' : 'text-danger'">
{{ backendDebug.hasRefreshToken ? '✅' : '❌' }}
</span>
<div class="flex-1">
<span class="text-white font-medium">Backend received refreshToken:</span>
<span class="text-text-secondary ml-2">{{ backendDebug.hasRefreshToken ? 'Yes' : 'No' }}</span>
</div>
</div>
<div class="mt-4 pt-4 border-t border-surface-lighter">
<h3 class="text-white font-medium mb-2">Backend Configuration:</h3>
<div class="space-y-1 text-sm">
<div class="flex justify-between">
<span class="text-text-secondary">Cookie Domain:</span>
<span class="text-white font-mono">{{ backendDebug.config?.cookieDomain || 'N/A' }}</span>
</div>
<div class="flex justify-between">
<span class="text-text-secondary">Cookie Secure:</span>
<span class="text-white font-mono">{{ backendDebug.config?.cookieSecure }}</span>
</div>
<div class="flex justify-between">
<span class="text-text-secondary">Cookie SameSite:</span>
<span class="text-white font-mono">{{ backendDebug.config?.cookieSameSite || 'N/A' }}</span>
</div>
<div class="flex justify-between">
<span class="text-text-secondary">CORS Origin:</span>
<span class="text-white font-mono">{{ backendDebug.config?.corsOrigin || 'N/A' }}</span>
</div>
</div>
</div>
<div v-if="hasBrowserCookies && !backendDebug.hasAccessToken" class="mt-4 p-4 bg-danger/10 border border-danger/30 rounded-lg">
<p class="text-danger font-medium mb-2">🚨 PROBLEM DETECTED!</p>
<p class="text-sm text-text-secondary mb-2">
Browser has cookies but backend is NOT receiving them!
</p>
<p class="text-sm text-text-secondary mb-2">Likely causes:</p>
<ul class="text-sm text-text-secondary list-disc list-inside space-y-1 ml-2">
<li>Cookie Domain mismatch (should be "localhost", not "127.0.0.1")</li>
<li>Cookie Secure flag is true on HTTP connection</li>
<li>Cookie SameSite is too restrictive</li>
</ul>
<p class="text-sm text-white mt-3 font-medium">🔧 Fix:</p>
<p class="text-sm text-text-secondary">Update backend <code class="px-1 py-0.5 bg-surface rounded">config/index.js</code> or <code class="px-1 py-0.5 bg-surface rounded">.env</code>:</p>
<pre class="mt-2 p-2 bg-surface rounded text-xs text-white overflow-x-auto">COOKIE_DOMAIN=localhost
COOKIE_SECURE=false
COOKIE_SAME_SITE=lax</pre>
</div>
</div>
<div v-else class="text-text-secondary text-center py-4">
Click "Test Now" to check if backend receives cookies
</div>
</div>
<!-- Authentication Test -->
<div class="bg-surface-light rounded-lg border border-surface-lighter p-6">
<div class="flex items-center justify-between mb-4">
<h2 class="text-xl font-bold text-white flex items-center gap-2">
<span>3</span> Authentication Test
</h2>
<button @click="testAuth" class="btn-secondary text-sm" :disabled="loadingAuth">
<Loader v-if="loadingAuth" class="w-4 h-4 animate-spin" />
<span v-else>Test Now</span>
</button>
</div>
<div v-if="authTest" class="space-y-2">
<div class="flex items-start gap-2">
<span :class="authTest.success ? 'text-success' : 'text-danger'">
{{ authTest.success ? '✅' : '❌' }}
</span>
<div class="flex-1">
<span class="text-white font-medium">/auth/me endpoint:</span>
<span class="text-text-secondary ml-2">{{ authTest.message }}</span>
</div>
</div>
<div v-if="authTest.success && authTest.user" class="mt-4 p-4 bg-success/10 border border-success/30 rounded-lg">
<p class="text-success font-medium mb-2"> Successfully authenticated!</p>
<div class="text-sm space-y-1">
<div class="flex justify-between">
<span class="text-text-secondary">Username:</span>
<span class="text-white">{{ authTest.user.username }}</span>
</div>
<div class="flex justify-between">
<span class="text-text-secondary">Steam ID:</span>
<span class="text-white font-mono">{{ authTest.user.steamId }}</span>
</div>
<div class="flex justify-between">
<span class="text-text-secondary">Balance:</span>
<span class="text-white">${{ authTest.user.balance.toFixed(2) }}</span>
</div>
</div>
</div>
</div>
<div v-else class="text-text-secondary text-center py-4">
Click "Test Now" to verify authentication
</div>
</div>
<!-- Sessions Test -->
<div class="bg-surface-light rounded-lg border border-surface-lighter p-6">
<div class="flex items-center justify-between mb-4">
<h2 class="text-xl font-bold text-white flex items-center gap-2">
<span>4</span> Sessions Endpoint Test
</h2>
<button @click="testSessions" class="btn-secondary text-sm" :disabled="loadingSessions">
<Loader v-if="loadingSessions" class="w-4 h-4 animate-spin" />
<span v-else>Test Now</span>
</button>
</div>
<div v-if="sessionsTest" class="space-y-2">
<div class="flex items-start gap-2">
<span :class="sessionsTest.success ? 'text-success' : 'text-danger'">
{{ sessionsTest.success ? '✅' : '❌' }}
</span>
<div class="flex-1">
<span class="text-white font-medium">/user/sessions endpoint:</span>
<span class="text-text-secondary ml-2">{{ sessionsTest.message }}</span>
</div>
</div>
<div v-if="sessionsTest.success && sessionsTest.sessions" class="mt-4 space-y-2">
<p class="text-success">Found {{ sessionsTest.sessions.length }} active session(s)</p>
<div v-for="(session, i) in sessionsTest.sessions" :key="i" class="p-3 bg-surface rounded border border-surface-lighter">
<div class="text-sm space-y-1">
<div class="text-white font-medium">{{ session.browser }} on {{ session.os }}</div>
<div class="text-text-secondary">Device: {{ session.device }}</div>
<div class="text-text-secondary">IP: {{ session.ip }}</div>
</div>
</div>
</div>
<div v-else-if="sessionsTest.error" class="mt-4 p-4 bg-danger/10 border border-danger/30 rounded-lg">
<p class="text-danger font-medium mb-2"> Sessions endpoint failed!</p>
<p class="text-sm text-text-secondary">{{ sessionsTest.error }}</p>
</div>
</div>
<div v-else class="text-text-secondary text-center py-4">
Click "Test Now" to test sessions endpoint (this is what's failing for you)
</div>
</div>
<!-- 2FA Test -->
<div class="bg-surface-light rounded-lg border border-surface-lighter p-6">
<div class="flex items-center justify-between mb-4">
<h2 class="text-xl font-bold text-white flex items-center gap-2">
<span>5⃣</span> 2FA Setup Endpoint Test
</h2>
<button @click="test2FA" class="btn-secondary text-sm" :disabled="loading2FA">
<Loader v-if="loading2FA" class="w-4 h-4 animate-spin" />
<span v-else>Test Now</span>
</button>
</div>
<div v-if="twoFATest" class="space-y-2">
<div class="flex items-start gap-2">
<span :class="twoFATest.success ? 'text-success' : 'text-danger'">
{{ twoFATest.success ? '' : '' }}
</span>
<div class="flex-1">
<span class="text-white font-medium">/user/2fa/setup endpoint:</span>
<span class="text-text-secondary ml-2">{{ twoFATest.message }}</span>
</div>
</div>
<div v-if="twoFATest.success" class="mt-4 p-4 bg-success/10 border border-success/30 rounded-lg">
<p class="text-success font-medium">✅ 2FA setup endpoint works!</p>
<p class="text-sm text-text-secondary mt-1">QR code and secret were generated successfully</p>
</div>
<div v-else-if="twoFATest.error" class="mt-4 p-4 bg-danger/10 border border-danger/30 rounded-lg">
<p class="text-danger font-medium mb-2">❌ 2FA setup failed!</p>
<p class="text-sm text-text-secondary">{{ twoFATest.error }}</p>
</div>
</div>
<div v-else class="text-text-secondary text-center py-4">
Click "Test Now" to test 2FA setup endpoint
</div>
</div>
<!-- Summary -->
<div class="bg-gradient-to-r from-primary/20 to-primary/10 rounded-lg border border-primary/30 p-6">
<h2 class="text-xl font-bold text-white mb-4">📋 Summary & Next Steps</h2>
<div class="space-y-2 text-sm">
<p class="text-text-secondary" v-if="!hasBrowserCookies">
<strong class="text-white">Step 1:</strong> Log in via Steam to get authentication cookies.
</p>
<p class="text-text-secondary" v-else-if="!backendHasCookies">
<strong class="text-white">Step 2:</strong> Fix cookie configuration so backend receives them. See the red warning box above for details.
</p>
<p class="text-text-secondary" v-else-if="backendHasCookies && !authTest?.success">
<strong class="text-white">Step 3:</strong> Run the authentication test to verify your token is valid.
</p>
<p class="text-text-secondary" v-else-if="authTest?.success && !sessionsTest?.success">
<strong class="text-white">Step 4:</strong> Test the sessions endpoint - this should work now!
</p>
<p class="text-success font-medium" v-else-if="sessionsTest?.success">
✅ Everything is working! You can now use sessions and 2FA features.
</p>
</div>
<div class="mt-4 pt-4 border-t border-primary/20">
<p class="text-text-secondary text-xs">
For more detailed troubleshooting, see <code class="px-1 py-0.5 bg-surface rounded">TurboTrades/TROUBLESHOOTING_AUTH.md</code>
</p>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { Loader } from 'lucide-vue-next'
import axios from '@/utils/axios'
// State
const browserCookies = ref(null)
const backendDebug = ref(null)
const authTest = ref(null)
const sessionsTest = ref(null)
const twoFATest = ref(null)
const loadingBackend = ref(false)
const loadingAuth = ref(false)
const loadingSessions = ref(false)
const loading2FA = ref(false)
// Computed
const hasBrowserCookies = computed(() => {
return browserCookies.value?.hasAccessToken || false
})
const backendHasCookies = computed(() => {
if (!backendDebug.value) return null
return backendDebug.value.hasAccessToken
})
const isLoggedIn = computed(() => {
return hasBrowserCookies.value && backendHasCookies.value && authTest.value?.success
})
// Methods
const checkBrowserCookies = () => {
const cookies = document.cookie
const accessToken = cookies.split(';').find(c => c.trim().startsWith('accessToken='))
const refreshToken = cookies.split(';').find(c => c.trim().startsWith('refreshToken='))
browserCookies.value = {
hasAccessToken: !!accessToken,
hasRefreshToken: !!refreshToken,
accessTokenLength: accessToken ? accessToken.split('=')[1].length : 0,
refreshTokenLength: refreshToken ? refreshToken.split('=')[1].length : 0,
}
}
const checkBackendCookies = async () => {
loadingBackend.value = true
try {
const response = await axios.get('/api/auth/debug-cookies', {
withCredentials: true
})
backendDebug.value = response.data
} catch (error) {
console.error('Backend cookie check failed:', error)
backendDebug.value = {
hasAccessToken: false,
hasRefreshToken: false,
error: error.message
}
} finally {
loadingBackend.value = false
}
}
const testAuth = async () => {
loadingAuth.value = true
try {
const response = await axios.get('/api/auth/me', {
withCredentials: true
})
authTest.value = {
success: true,
message: 'Successfully authenticated',
user: response.data.user
}
} catch (error) {
authTest.value = {
success: false,
message: error.response?.data?.message || error.message,
error: error.response?.data?.error || 'Error'
}
} finally {
loadingAuth.value = false
}
}
const testSessions = async () => {
loadingSessions.value = true
try {
const response = await axios.get('/api/user/sessions', {
withCredentials: true
})
sessionsTest.value = {
success: true,
message: 'Sessions retrieved successfully',
sessions: response.data.sessions
}
} catch (error) {
sessionsTest.value = {
success: false,
message: error.response?.data?.message || error.message,
error: error.response?.data?.message || error.message
}
} finally {
loadingSessions.value = false
}
}
const test2FA = async () => {
loading2FA.value = true
try {
const response = await axios.post('/api/user/2fa/setup', {}, {
withCredentials: true
})
twoFATest.value = {
success: true,
message: '2FA setup successful',
data: response.data
}
} catch (error) {
twoFATest.value = {
success: false,
message: error.response?.data?.message || error.message,
error: error.response?.data?.message || error.message
}
} finally {
loading2FA.value = false
}
}
const runAllTests = async () => {
checkBrowserCookies()
await checkBackendCookies()
if (backendHasCookies.value) {
await testAuth()
if (authTest.value?.success) {
await testSessions()
}
}
}
// Lifecycle
onMounted(() => {
checkBrowserCookies()
checkBackendCookies()
})
</script>
<style scoped>
code {
font-family: 'Courier New', monospace;
}
</style>

View File

@@ -0,0 +1,127 @@
<script setup>
import { ref } from 'vue'
import { ChevronDown } from 'lucide-vue-next'
const faqs = ref([
{
id: 1,
question: 'How do I buy items?',
answer: 'Browse the marketplace, click on an item you like, and click "Buy Now". Make sure you have sufficient balance and your trade URL is set in your profile.',
open: false
},
{
id: 2,
question: 'How do I sell items?',
answer: 'Go to the "Sell" page, select items from your Steam inventory, set your price, and list them on the marketplace.',
open: false
},
{
id: 3,
question: 'How long does delivery take?',
answer: 'Delivery is instant! Once you purchase an item, you will receive a Steam trade offer automatically within seconds.',
open: false
},
{
id: 4,
question: 'What payment methods do you accept?',
answer: 'We accept various payment methods including credit/debit cards, PayPal, and cryptocurrency. You can deposit funds in the Deposit page.',
open: false
},
{
id: 5,
question: 'How do I withdraw my balance?',
answer: 'Go to the Withdraw page, enter the amount you want to withdraw, and select your preferred withdrawal method. Processing typically takes 1-3 business days.',
open: false
},
{
id: 6,
question: 'Is trading safe?',
answer: 'Yes! We use bank-grade security, SSL encryption, and automated trading bots to ensure safe and secure transactions.',
open: false
},
{
id: 7,
question: 'What are the fees?',
answer: 'We charge a small marketplace fee of 5% on sales. There are no fees for buying items.',
open: false
},
{
id: 8,
question: 'Can I cancel a listing?',
answer: 'Yes, you can cancel your listings anytime from your inventory page before they are sold.',
open: false
}
])
const toggleFaq = (id) => {
const faq = faqs.value.find(f => f.id === id)
if (faq) {
faq.open = !faq.open
}
}
</script>
<template>
<div class="faq-page min-h-screen py-12">
<div class="container-custom max-w-4xl">
<!-- Header -->
<div class="text-center mb-12">
<h1 class="text-4xl sm:text-5xl font-display font-bold text-white mb-4">
Frequently Asked Questions
</h1>
<p class="text-lg text-gray-400">
Find answers to common questions about TurboTrades
</p>
</div>
<!-- FAQ List -->
<div class="space-y-4">
<div
v-for="faq in faqs"
:key="faq.id"
class="card overflow-hidden"
>
<button
@click="toggleFaq(faq.id)"
class="w-full flex items-center justify-between p-6 text-left hover:bg-surface-light transition-colors"
>
<span class="text-lg font-semibold text-white pr-4">{{ faq.question }}</span>
<ChevronDown
:class="['w-5 h-5 text-gray-400 transition-transform flex-shrink-0', faq.open ? 'rotate-180' : '']"
/>
</button>
<Transition name="slide-down">
<div v-if="faq.open" class="px-6 pb-6">
<p class="text-gray-400 leading-relaxed">{{ faq.answer }}</p>
</div>
</Transition>
</div>
</div>
<!-- Contact Support -->
<div class="mt-12 p-8 bg-surface rounded-xl border border-surface-lighter text-center">
<h3 class="text-2xl font-bold text-white mb-3">Still have questions?</h3>
<p class="text-gray-400 mb-6">
Our support team is here to help you with any questions or concerns.
</p>
<router-link to="/support" class="btn btn-primary">
Contact Support
</router-link>
</div>
</div>
</div>
</template>
<style scoped>
.slide-down-enter-active,
.slide-down-leave-active {
transition: all 0.3s ease;
max-height: 200px;
}
.slide-down-enter-from,
.slide-down-leave-to {
opacity: 0;
max-height: 0;
}
</style>

View File

@@ -0,0 +1,419 @@
<script setup>
import { ref, onMounted } from "vue";
import { useRouter } from "vue-router";
import { useMarketStore } from "@/stores/market";
import { useAuthStore } from "@/stores/auth";
import {
TrendingUp,
Shield,
Zap,
Users,
ArrowRight,
Sparkles,
ChevronRight,
} from "lucide-vue-next";
const router = useRouter();
const marketStore = useMarketStore();
const authStore = useAuthStore();
const featuredItems = ref([]);
const recentSales = ref([]);
const isLoading = ref(true);
const stats = ref({
totalUsers: "50,000+",
totalTrades: "1M+",
avgTradeTime: "< 2 min",
activeListings: "25,000+",
});
const features = [
{
icon: Zap,
title: "Instant Trading",
description: "Lightning-fast transactions with automated trade bot system",
},
{
icon: Shield,
title: "Secure & Safe",
description: "Bank-grade security with SSL encryption and fraud protection",
},
{
icon: TrendingUp,
title: "Best Prices",
description: "Competitive marketplace pricing with real-time market data",
},
{
icon: Users,
title: "Active Community",
description: "Join thousands of traders in our vibrant marketplace",
},
];
onMounted(async () => {
isLoading.value = true;
await Promise.all([
marketStore.fetchFeaturedItems(),
marketStore.fetchRecentSales(6),
]);
featuredItems.value = marketStore.featuredItems.slice(0, 8);
recentSales.value = marketStore.recentSales;
isLoading.value = false;
});
const navigateToMarket = () => {
router.push("/market");
};
const navigateToSell = () => {
if (authStore.isAuthenticated) {
router.push("/sell");
} else {
authStore.login();
}
};
const formatPrice = (price) => {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(price);
};
const formatTimeAgo = (timestamp) => {
const seconds = Math.floor((Date.now() - new Date(timestamp)) / 1000);
if (seconds < 60) return "Just now";
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
return `${Math.floor(seconds / 86400)}d ago`;
};
const getRarityColor = (rarity) => {
const colors = {
common: "text-gray-400",
uncommon: "text-green-400",
rare: "text-blue-400",
mythical: "text-purple-400",
legendary: "text-amber-400",
ancient: "text-red-400",
exceedingly: "text-orange-400",
};
return colors[rarity] || "text-gray-400";
};
</script>
<template>
<div class="home-page">
<!-- Hero Section -->
<section class="relative py-20 lg:py-32 overflow-hidden">
<!-- Background Effects -->
<div class="absolute inset-0 opacity-30">
<div
class="absolute top-1/4 left-1/4 w-96 h-96 bg-primary-500/20 rounded-full blur-3xl"
></div>
<div
class="absolute bottom-1/4 right-1/4 w-96 h-96 bg-accent-blue/20 rounded-full blur-3xl"
></div>
</div>
<div class="container-custom relative z-10">
<div class="max-w-4xl mx-auto text-center">
<!-- Badge -->
<div
class="inline-flex items-center gap-2 px-4 py-2 bg-surface-light/50 backdrop-blur-sm border border-primary-500/30 rounded-full mb-6 animate-fade-in"
>
<Sparkles class="w-4 h-4 text-primary-500" />
<span class="text-sm font-medium text-gray-300"
>Premium CS2 & Rust Marketplace</span
>
</div>
<!-- Heading -->
<h1
class="text-4xl sm:text-5xl lg:text-7xl font-display font-bold mb-6 animate-slide-up"
>
<span class="text-white">Trade Your Skins</span>
<br />
<span class="gradient-text">Lightning Fast</span>
</h1>
<!-- Description -->
<p
class="text-lg sm:text-xl text-gray-400 mb-10 max-w-2xl mx-auto animate-slide-up"
style="animation-delay: 0.1s"
>
Buy, sell, and trade CS2 and Rust skins with instant delivery. Join
thousands of traders in the most trusted marketplace.
</p>
<!-- CTA Buttons -->
<div
class="flex flex-col sm:flex-row items-center justify-center gap-4 animate-slide-up"
style="animation-delay: 0.2s"
>
<button
@click="navigateToMarket"
class="btn btn-primary btn-lg group"
>
Browse Market
<ArrowRight
class="w-5 h-5 group-hover:translate-x-1 transition-transform"
/>
</button>
<button @click="navigateToSell" class="btn btn-outline btn-lg">
Start Selling
</button>
</div>
<!-- Stats -->
<div
class="grid grid-cols-2 md:grid-cols-4 gap-6 mt-16 animate-slide-up"
style="animation-delay: 0.3s"
>
<div
v-for="(value, key) in stats"
:key="key"
class="p-6 bg-surface/50 backdrop-blur-sm rounded-xl border border-surface-lighter"
>
<div class="text-2xl sm:text-3xl font-bold text-primary-500 mb-1">
{{ value }}
</div>
<div class="text-sm text-gray-400 capitalize">
{{ key.replace(/([A-Z])/g, " $1").trim() }}
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Features Section -->
<section class="py-16 lg:py-24 bg-surface/30">
<div class="container-custom">
<div class="text-center mb-12">
<h2
class="text-3xl sm:text-4xl font-display font-bold text-white mb-4"
>
Why Choose TurboTrades?
</h2>
<p class="text-lg text-gray-400 max-w-2xl mx-auto">
Experience the best trading platform with industry-leading features
and security
</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div
v-for="feature in features"
:key="feature.title"
class="p-6 bg-surface rounded-xl border border-surface-lighter hover:border-primary-500/50 transition-all group"
>
<div
class="w-12 h-12 bg-primary-500/10 rounded-lg flex items-center justify-center mb-4 group-hover:bg-primary-500/20 transition-colors"
>
<component :is="feature.icon" class="w-6 h-6 text-primary-500" />
</div>
<h3 class="text-xl font-semibold text-white mb-2">
{{ feature.title }}
</h3>
<p class="text-gray-400 text-sm">{{ feature.description }}</p>
</div>
</div>
</div>
</section>
<!-- Featured Items Section -->
<section class="py-16 lg:py-24">
<div class="container-custom">
<div class="flex items-center justify-between mb-8">
<div>
<h2
class="text-3xl sm:text-4xl font-display font-bold text-white mb-2"
>
Featured Items
</h2>
<p class="text-gray-400">Hand-picked premium skins</p>
</div>
<button @click="navigateToMarket" class="btn btn-ghost group">
View All
<ChevronRight
class="w-4 h-4 group-hover:translate-x-1 transition-transform"
/>
</button>
</div>
<!-- Loading State -->
<div
v-if="isLoading"
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6"
>
<div v-for="i in 8" :key="i" class="card">
<div class="aspect-square skeleton"></div>
<div class="card-body space-y-3">
<div class="h-4 skeleton w-3/4"></div>
<div class="h-3 skeleton w-1/2"></div>
</div>
</div>
</div>
<!-- Items Grid -->
<div
v-else
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6"
>
<div
v-for="item in featuredItems"
:key="item.id"
@click="router.push(`/item/${item.id}`)"
class="item-card group"
>
<!-- Image -->
<div class="relative">
<img :src="item.image" :alt="item.name" class="item-card-image" />
<div v-if="item.wear" class="item-card-wear">
{{ item.wear }}
</div>
<div
v-if="item.statTrak"
class="absolute top-2 right-2 badge badge-warning"
>
StatTrak
</div>
</div>
<!-- Content -->
<div class="p-4 space-y-3">
<div>
<h3 class="font-semibold text-white text-sm line-clamp-2 mb-1">
{{ item.name }}
</h3>
<p
:class="['text-xs font-medium', getRarityColor(item.rarity)]"
>
{{ item.rarity }}
</p>
</div>
<div class="flex items-center justify-between">
<span class="text-lg font-bold text-primary-500">
{{ formatPrice(item.price) }}
</span>
<button class="btn btn-sm btn-primary">Buy Now</button>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Recent Sales Section -->
<section class="py-16 lg:py-24 bg-surface/30">
<div class="container-custom">
<div class="text-center mb-12">
<h2
class="text-3xl sm:text-4xl font-display font-bold text-white mb-4"
>
Recent Sales
</h2>
<p class="text-lg text-gray-400">Live marketplace activity</p>
</div>
<div class="max-w-4xl mx-auto space-y-3">
<div
v-for="sale in recentSales"
:key="sale.id"
class="flex items-center gap-4 p-4 bg-surface rounded-lg border border-surface-lighter hover:border-primary-500/30 transition-colors"
>
<img
:src="sale.itemImage"
:alt="sale.itemName"
class="w-16 h-16 object-contain bg-surface-light rounded-lg"
/>
<div class="flex-1 min-w-0">
<h4 class="font-medium text-white text-sm truncate">
{{ sale.itemName }}
</h4>
<p class="text-xs text-gray-400">{{ sale.wear }}</p>
</div>
<div class="text-right">
<div class="font-bold text-accent-green">
{{ formatPrice(sale.price) }}
</div>
<div class="text-xs text-gray-500">
{{ formatTimeAgo(sale.soldAt) }}
</div>
</div>
</div>
</div>
</div>
</section>
<!-- CTA Section -->
<section class="py-16 lg:py-24">
<div class="container-custom">
<div
class="relative overflow-hidden rounded-2xl bg-gradient-to-r from-primary-600 to-primary-800 p-12 text-center"
>
<!-- Background Pattern -->
<div class="absolute inset-0 opacity-10">
<div
class="absolute top-0 left-1/4 w-72 h-72 bg-white rounded-full blur-3xl"
></div>
<div
class="absolute bottom-0 right-1/4 w-72 h-72 bg-white rounded-full blur-3xl"
></div>
</div>
<div class="relative z-10 max-w-3xl mx-auto">
<h2
class="text-3xl sm:text-5xl font-display font-bold text-white mb-6"
>
Ready to Start Trading?
</h2>
<p class="text-lg text-primary-100 mb-8">
Join TurboTrades today and experience the fastest, most secure way
to trade gaming skins
</p>
<div
class="flex flex-col sm:flex-row items-center justify-center gap-4"
>
<button
v-if="!authStore.isAuthenticated"
@click="authStore.login"
class="btn btn-lg bg-white text-primary-600 hover:bg-gray-100"
>
<img
src="https://community.cloudflare.steamstatic.com/public/images/signinthroughsteam/sits_01.png"
alt="Sign in through Steam"
class="h-6"
/>
</button>
<button
v-else
@click="navigateToMarket"
class="btn btn-lg bg-white text-primary-600 hover:bg-gray-100"
>
Browse Market
<ArrowRight class="w-5 h-5" />
</button>
</div>
</div>
</div>
</div>
</section>
</div>
</template>
<style scoped>
.home-page {
min-height: 100vh;
}
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>

View File

@@ -0,0 +1,21 @@
<script setup>
import { computed } from 'vue'
import { useAuthStore } from '@/stores/auth'
import { Package } from 'lucide-vue-next'
const authStore = useAuthStore()
const hasItems = computed(() => false) // Placeholder
</script>
<template>
<div class="inventory-page min-h-screen py-8">
<div class="container-custom">
<h1 class="text-3xl font-display font-bold text-white mb-8">My Inventory</h1>
<div class="text-center py-20">
<Package class="w-16 h-16 text-gray-500 mx-auto mb-4" />
<h3 class="text-xl font-semibold text-gray-400 mb-2">No items yet</h3>
<p class="text-gray-500">Purchase items from the marketplace to see them here</p>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,304 @@
<script setup>
import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useMarketStore } from '@/stores/market'
import { useAuthStore } from '@/stores/auth'
import {
ArrowLeft,
ShoppingCart,
Heart,
Share2,
AlertCircle,
CheckCircle,
Loader2,
TrendingUp,
Clock,
Package
} from 'lucide-vue-next'
const route = useRoute()
const router = useRouter()
const marketStore = useMarketStore()
const authStore = useAuthStore()
const item = ref(null)
const isLoading = ref(true)
const isPurchasing = ref(false)
const isFavorite = ref(false)
onMounted(async () => {
const itemId = route.params.id
item.value = await marketStore.getItemById(itemId)
isLoading.value = false
})
const formatPrice = (price) => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(price)
}
const handlePurchase = async () => {
if (!authStore.isAuthenticated) {
authStore.login()
return
}
if (authStore.balance < item.value.price) {
alert('Insufficient balance')
return
}
isPurchasing.value = true
const success = await marketStore.purchaseItem(item.value.id)
if (success) {
router.push('/inventory')
}
isPurchasing.value = false
}
const toggleFavorite = () => {
isFavorite.value = !isFavorite.value
}
const shareItem = () => {
navigator.clipboard.writeText(window.location.href)
alert('Link copied to clipboard!')
}
const goBack = () => {
router.back()
}
const getRarityColor = (rarity) => {
const colors = {
common: 'text-gray-400 border-gray-400/30',
uncommon: 'text-green-400 border-green-400/30',
rare: 'text-blue-400 border-blue-400/30',
mythical: 'text-purple-400 border-purple-400/30',
legendary: 'text-amber-400 border-amber-400/30',
ancient: 'text-red-400 border-red-400/30',
exceedingly: 'text-orange-400 border-orange-400/30'
}
return colors[rarity] || 'text-gray-400 border-gray-400/30'
}
</script>
<template>
<div class="item-details-page min-h-screen py-8">
<div class="container-custom">
<!-- Back Button -->
<button @click="goBack" class="btn btn-ghost mb-6 flex items-center gap-2">
<ArrowLeft class="w-4 h-4" />
Back to Market
</button>
<!-- Loading State -->
<div v-if="isLoading" class="flex items-center justify-center py-20">
<Loader2 class="w-12 h-12 text-primary-500 animate-spin" />
</div>
<!-- Item Details -->
<div v-else-if="item" class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<!-- Left Column - Image -->
<div class="space-y-4">
<div class="card overflow-hidden">
<div class="aspect-square bg-gradient-to-br from-surface-light to-surface p-8 flex items-center justify-center relative">
<img
:src="item.image"
:alt="item.name"
class="w-full h-full object-contain"
/>
<div v-if="item.wear" class="absolute top-4 left-4 badge badge-primary">
{{ item.wear }}
</div>
<div v-if="item.statTrak" class="absolute top-4 right-4 badge badge-warning">
StatTrak
</div>
</div>
</div>
<!-- Item Stats -->
<div class="card card-body space-y-3">
<h3 class="text-white font-semibold">Item Information</h3>
<div class="space-y-2 text-sm">
<div class="flex justify-between">
<span class="text-gray-400">Game</span>
<span class="text-white font-medium">{{ item.game?.toUpperCase() || 'N/A' }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-400">Category</span>
<span class="text-white font-medium capitalize">{{ item.category }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-400">Rarity</span>
<span :class="['font-medium capitalize', getRarityColor(item.rarity).split(' ')[0]]">
{{ item.rarity }}
</span>
</div>
<div v-if="item.float" class="flex justify-between">
<span class="text-gray-400">Float Value</span>
<span class="text-white font-medium">{{ item.float }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-400">Listed</span>
<span class="text-white font-medium">{{ new Date(item.listedAt).toLocaleDateString() }}</span>
</div>
</div>
</div>
</div>
<!-- Right Column - Details & Purchase -->
<div class="space-y-6">
<!-- Title & Actions -->
<div>
<div class="flex items-start justify-between mb-4">
<div class="flex-1">
<h1 class="text-3xl font-display font-bold text-white mb-2">
{{ item.name }}
</h1>
<p class="text-gray-400">{{ item.description || 'No description available' }}</p>
</div>
<div class="flex gap-2">
<button
@click="toggleFavorite"
:class="['btn btn-ghost p-2', isFavorite ? 'text-accent-red' : 'text-gray-400']"
>
<Heart :class="['w-5 h-5', isFavorite ? 'fill-current' : '']" />
</button>
<button @click="shareItem" class="btn btn-ghost p-2">
<Share2 class="w-5 h-5" />
</button>
</div>
</div>
<!-- Rarity Badge -->
<div :class="['inline-flex items-center gap-2 px-3 py-1.5 rounded-lg border', getRarityColor(item.rarity)]">
<div class="w-2 h-2 rounded-full bg-current"></div>
<span class="text-sm font-medium capitalize">{{ item.rarity }}</span>
</div>
</div>
<!-- Price & Purchase -->
<div class="card card-body space-y-4">
<div>
<div class="text-sm text-gray-400 mb-1">Current Price</div>
<div class="text-4xl font-bold text-primary-500">
{{ formatPrice(item.price) }}
</div>
</div>
<!-- Purchase Button -->
<button
@click="handlePurchase"
:disabled="isPurchasing || (authStore.isAuthenticated && authStore.balance < item.price)"
class="btn btn-primary w-full btn-lg"
>
<Loader2 v-if="isPurchasing" class="w-5 h-5 animate-spin" />
<template v-else>
<ShoppingCart class="w-5 h-5" />
<span v-if="!authStore.isAuthenticated">Login to Purchase</span>
<span v-else-if="authStore.balance < item.price">Insufficient Balance</span>
<span v-else>Buy Now</span>
</template>
</button>
<!-- Balance Check -->
<div v-if="authStore.isAuthenticated" class="p-3 bg-surface-light rounded-lg">
<div class="flex items-center justify-between text-sm">
<span class="text-gray-400">Your Balance</span>
<span class="font-semibold text-white">{{ formatPrice(authStore.balance) }}</span>
</div>
<div v-if="authStore.balance < item.price" class="flex items-center gap-2 mt-2 text-accent-red text-xs">
<AlertCircle class="w-4 h-4" />
<span>Insufficient funds. Please deposit more to continue.</span>
</div>
</div>
</div>
<!-- Seller Info -->
<div class="card card-body">
<h3 class="text-white font-semibold mb-4">Seller Information</h3>
<div class="flex items-center gap-3">
<img
:src="item.seller?.avatar || 'https://via.placeholder.com/40'"
:alt="item.seller?.username"
class="w-12 h-12 rounded-full"
/>
<div class="flex-1">
<div class="font-medium text-white">{{ item.seller?.username || 'Anonymous' }}</div>
<div class="text-sm text-gray-400">{{ item.seller?.totalSales || 0 }} successful trades</div>
</div>
<router-link
v-if="item.seller?.steamId"
:to="`/profile/${item.seller.steamId}`"
class="btn btn-sm btn-secondary"
>
View Profile
</router-link>
</div>
</div>
<!-- Features -->
<div class="grid grid-cols-3 gap-4">
<div class="card card-body text-center">
<CheckCircle class="w-6 h-6 text-accent-green mx-auto mb-2" />
<div class="text-xs text-gray-400">Instant</div>
<div class="text-sm font-medium text-white">Delivery</div>
</div>
<div class="card card-body text-center">
<Package class="w-6 h-6 text-accent-blue mx-auto mb-2" />
<div class="text-xs text-gray-400">Secure</div>
<div class="text-sm font-medium text-white">Trading</div>
</div>
<div class="card card-body text-center">
<TrendingUp class="w-6 h-6 text-primary-500 mx-auto mb-2" />
<div class="text-xs text-gray-400">Market</div>
<div class="text-sm font-medium text-white">Price</div>
</div>
</div>
<!-- Trade Offer Notice -->
<div class="p-4 bg-accent-blue/10 border border-accent-blue/30 rounded-lg">
<div class="flex gap-3">
<AlertCircle class="w-5 h-5 text-accent-blue flex-shrink-0 mt-0.5" />
<div class="text-sm">
<div class="font-medium text-white mb-1">Trade Offer Information</div>
<p class="text-gray-400">
After purchase, you will receive a Steam trade offer automatically.
Please make sure your trade URL is set in your profile settings.
</p>
</div>
</div>
</div>
</div>
</div>
<!-- Item Not Found -->
<div v-else class="text-center py-20">
<AlertCircle class="w-16 h-16 text-gray-500 mx-auto mb-4" />
<h2 class="text-2xl font-bold text-white mb-2">Item Not Found</h2>
<p class="text-gray-400 mb-6">This item may have been sold or removed.</p>
<button @click="router.push('/market')" class="btn btn-primary">
Browse Market
</button>
</div>
</div>
</div>
</template>
<style scoped>
.item-details-page {
animation: fadeIn 0.3s ease-in;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
</style>

View File

@@ -0,0 +1,624 @@
<template>
<div class="min-h-screen bg-surface py-8">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<!-- Header -->
<div class="mb-8">
<h1 class="text-3xl font-bold text-white mb-2">Marketplace</h1>
<p class="text-text-secondary">
Browse and purchase CS2 and Rust skins
</p>
</div>
<div class="flex gap-6">
<!-- Permanent Sidebar Filters -->
<aside class="w-64 flex-shrink-0 space-y-6">
<!-- Search -->
<div
class="bg-surface-light rounded-lg border border-surface-lighter p-4"
>
<h3 class="text-white font-semibold mb-3">Search</h3>
<div class="relative">
<input
v-model="marketStore.filters.search"
@input="handleSearch"
type="text"
placeholder="Search items..."
class="w-full pl-10 pr-4 py-2 bg-surface rounded-lg border border-surface-lighter text-white placeholder-text-secondary focus:outline-none focus:border-primary transition-colors"
/>
<Search
class="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-text-secondary"
/>
</div>
</div>
<!-- Game Filter -->
<div
class="bg-surface-light rounded-lg border border-surface-lighter p-4"
>
<h3 class="text-white font-semibold mb-3">Game</h3>
<div class="space-y-2">
<button
v-for="game in gameOptions"
:key="game.value"
@click="handleGameChange(game.value)"
class="w-full px-4 py-2 rounded-lg text-sm font-medium transition-colors text-left"
:class="
marketStore.filters.game === game.value
? 'bg-primary text-surface-dark'
: 'bg-surface text-text-secondary hover:bg-surface-lighter hover:text-white'
"
>
{{ game.label }}
</button>
</div>
</div>
<!-- Wear Filter (CS2 only) -->
<div
v-if="marketStore.filters.game === 'cs2'"
class="bg-surface-light rounded-lg border border-surface-lighter p-4"
>
<h3 class="text-white font-semibold mb-3">Wear</h3>
<div class="space-y-2">
<button
v-for="wear in wearOptions"
:key="wear.value"
@click="handleWearChange(wear.value)"
class="w-full px-4 py-2 rounded-lg text-sm font-medium transition-colors text-left flex items-center justify-between"
:class="
marketStore.filters.wear === wear.value
? 'bg-primary text-surface-dark'
: 'bg-surface text-text-secondary hover:bg-surface-lighter hover:text-white'
"
>
<span>{{ wear.label }}</span>
<span
v-if="marketStore.filters.wear === wear.value"
class="text-xs"
></span
>
</button>
</div>
</div>
<!-- Rarity Filter (Rust only) -->
<div
v-if="marketStore.filters.game === 'rust'"
class="bg-surface-light rounded-lg border border-surface-lighter p-4"
>
<h3 class="text-white font-semibold mb-3">Rarity</h3>
<div class="space-y-2">
<button
v-for="rarity in rarityOptions"
:key="rarity.value"
@click="handleRarityChange(rarity.value)"
class="w-full px-4 py-2 rounded-lg text-sm font-medium transition-colors text-left flex items-center justify-between"
:class="
marketStore.filters.rarity === rarity.value
? 'bg-primary text-surface-dark'
: 'bg-surface text-text-secondary hover:bg-surface-lighter hover:text-white'
"
>
<span>{{ rarity.label }}</span>
<span
v-if="marketStore.filters.rarity === rarity.value"
class="text-xs"
></span
>
</button>
</div>
</div>
<!-- Price Range -->
<div
class="bg-surface-light rounded-lg border border-surface-lighter p-4"
>
<h3 class="text-white font-semibold mb-3">Price Range</h3>
<div class="space-y-3">
<div class="flex gap-2">
<div class="flex-1">
<input
v-model.number="marketStore.filters.minPrice"
type="number"
placeholder="Min"
min="0"
class="w-full px-3 py-2 bg-surface rounded-lg border border-surface-lighter text-white text-sm focus:outline-none focus:border-primary"
/>
</div>
<span class="text-text-secondary self-center">-</span>
<div class="flex-1">
<input
v-model.number="marketStore.filters.maxPrice"
type="number"
placeholder="Max"
min="0"
class="w-full px-3 py-2 bg-surface rounded-lg border border-surface-lighter text-white text-sm focus:outline-none focus:border-primary"
/>
</div>
</div>
<button
@click="applyPriceRange"
class="w-full px-4 py-2 bg-primary hover:bg-primary-dark text-surface-dark font-medium rounded-lg transition-colors text-sm"
>
Apply
</button>
</div>
</div>
<!-- Special Filters (CS2 only) -->
<div
v-if="marketStore.filters.game === 'cs2'"
class="bg-surface-light rounded-lg border border-surface-lighter p-4"
>
<h3 class="text-white font-semibold mb-3">Special</h3>
<div class="space-y-3">
<label class="flex items-center gap-3 cursor-pointer">
<input
v-model="marketStore.filters.statTrak"
type="checkbox"
class="w-4 h-4 rounded border-surface-lighter bg-surface text-primary focus:ring-primary focus:ring-offset-0"
/>
<span class="text-sm text-text-secondary">StatTrak</span>
</label>
<label class="flex items-center gap-3 cursor-pointer">
<input
v-model="marketStore.filters.souvenir"
type="checkbox"
class="w-4 h-4 rounded border-surface-lighter bg-surface text-primary focus:ring-primary focus:ring-offset-0"
/>
<span class="text-sm text-text-secondary">Souvenir</span>
</label>
</div>
</div>
<!-- Clear Filters -->
<button
v-if="activeFiltersCount > 0"
@click="clearFilters"
class="w-full px-4 py-2 bg-surface-lighter hover:bg-surface text-text-secondary hover:text-white rounded-lg transition-colors text-sm font-medium flex items-center justify-center gap-2"
>
<X class="w-4 h-4" />
Clear All Filters
</button>
</aside>
<!-- Main Content -->
<div class="flex-1 min-w-0">
<!-- Top Bar -->
<div class="flex items-center justify-between mb-6">
<div class="text-sm text-text-secondary">
<span v-if="!marketStore.loading">
{{ marketStore.items.length }} items found
</span>
</div>
<div class="flex items-center gap-3">
<!-- Sort -->
<select
v-model="marketStore.filters.sort"
@change="handleSortChange"
class="px-4 py-2 bg-surface-light rounded-lg border border-surface-lighter text-white text-sm focus:outline-none focus:border-primary"
>
<option
v-for="option in sortOptions"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</option>
</select>
<!-- View Mode -->
<div
class="flex items-center bg-surface-light rounded-lg border border-surface-lighter p-1"
>
<button
@click="viewMode = 'grid'"
class="p-2 rounded transition-colors"
:class="
viewMode === 'grid'
? 'bg-primary text-surface-dark'
: 'text-text-secondary hover:text-white'
"
title="Grid View"
>
<Grid3x3 class="w-4 h-4" />
</button>
<button
@click="viewMode = 'list'"
class="p-2 rounded transition-colors"
:class="
viewMode === 'list'
? 'bg-primary text-surface-dark'
: 'text-text-secondary hover:text-white'
"
title="List View"
>
<List class="w-4 h-4" />
</button>
</div>
</div>
</div>
<!-- Loading State -->
<div
v-if="marketStore.isLoading"
class="flex flex-col items-center justify-center py-20"
>
<Loader2 class="w-12 h-12 animate-spin text-primary mb-4" />
<p class="text-text-secondary">Loading items...</p>
</div>
<!-- Empty State -->
<div
v-else-if="marketStore.items.length === 0"
class="text-center py-20"
>
<div
class="inline-flex items-center justify-center w-16 h-16 bg-surface-light rounded-full mb-4"
>
<Search class="w-8 h-8 text-text-secondary" />
</div>
<h3 class="text-xl font-semibold text-white mb-2">
No items found
</h3>
<p class="text-text-secondary mb-6">
Try adjusting your filters or search terms
</p>
<button
@click="clearFilters"
class="px-6 py-3 bg-primary hover:bg-primary-dark text-surface-dark font-semibold rounded-lg transition-colors"
>
Clear Filters
</button>
</div>
<!-- Grid View -->
<div
v-else-if="viewMode === 'grid'"
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"
>
<div
v-for="item in marketStore.items"
:key="item._id"
@click="navigateToItem(item._id)"
class="bg-surface-light rounded-lg border border-surface-lighter overflow-hidden hover:border-primary/50 transition-all duration-300 cursor-pointer group"
>
<!-- Image -->
<div class="relative overflow-hidden">
<img
:src="item.image"
:alt="item.name"
class="w-full h-48 object-cover group-hover:scale-110 transition-transform duration-300"
@error="handleImageError"
/>
<div
class="absolute top-2 left-2 px-2 py-1 bg-black/75 rounded text-xs font-medium text-white"
>
{{ item.game.toUpperCase() }}
</div>
<div
v-if="item.featured"
class="absolute top-2 right-2 px-2 py-1 bg-primary/90 rounded text-xs font-bold text-surface-dark"
>
FEATURED
</div>
</div>
<!-- Content -->
<div class="p-4">
<h3
class="text-white font-semibold mb-2 truncate"
:title="item.name"
>
{{ item.name }}
</h3>
<!-- Item Details -->
<div class="flex items-center gap-2 mb-3 text-xs flex-wrap">
<!-- CS2: Show wear (capitalized) -->
<span
v-if="item.game === 'cs2' && item.exterior"
class="px-2 py-1 bg-surface rounded text-text-secondary uppercase font-medium"
>
{{ item.exterior }}
</span>
<!-- Rust: Show rarity -->
<span
v-if="item.game === 'rust' && item.rarity"
class="px-2 py-1 rounded capitalize font-medium"
:style="{
backgroundColor: getRarityColor(item.rarity) + '20',
color: getRarityColor(item.rarity),
}"
>
{{ item.rarity }}
</span>
<span
v-if="item.statTrak"
class="px-2 py-1 bg-primary/20 text-primary rounded font-medium"
>
ST
</span>
</div>
<!-- Price and Button -->
<div class="flex items-center justify-between">
<div>
<p class="text-xs text-text-secondary mb-1">Price</p>
<p class="text-xl font-bold text-primary">
{{ formatPrice(item.price) }}
</p>
</div>
<button
class="px-4 py-2 bg-primary hover:bg-primary-dark text-surface-dark font-medium rounded-lg transition-colors"
>
Buy Now
</button>
</div>
</div>
</div>
</div>
<!-- List View -->
<div v-else-if="viewMode === 'list'" class="space-y-4">
<div
v-for="item in marketStore.items"
:key="item._id"
@click="navigateToItem(item._id)"
class="bg-surface-light rounded-lg border border-surface-lighter overflow-hidden hover:border-primary/50 transition-all duration-300 cursor-pointer flex"
>
<img
:src="item.image"
:alt="item.name"
class="w-48 h-32 object-cover flex-shrink-0"
@error="handleImageError"
/>
<div class="flex-1 p-4 flex items-center justify-between">
<div class="flex-1">
<h3 class="text-white font-semibold mb-1">
{{ item.name }}
</h3>
<div class="flex items-center gap-2 text-xs">
<span class="text-text-secondary">{{
item.game.toUpperCase()
}}</span>
<!-- CS2: Show wear (capitalized) -->
<span
v-if="item.game === 'cs2' && item.exterior"
class="text-text-secondary"
>
{{ item.exterior.toUpperCase() }}
</span>
<!-- Rust: Show rarity -->
<span
v-if="item.game === 'rust' && item.rarity"
class="capitalize"
:style="{ color: getRarityColor(item.rarity) }"
>
{{ item.rarity }}
</span>
<span v-if="item.statTrak" class="text-primary"
> StatTrak</span
>
</div>
</div>
<div class="flex items-center gap-4">
<div class="text-right">
<p class="text-xs text-text-secondary mb-1">Price</p>
<p class="text-xl font-bold text-primary">
{{ formatPrice(item.price) }}
</p>
</div>
<button
class="px-6 py-2 bg-primary hover:bg-primary-dark text-surface-dark font-medium rounded-lg transition-colors"
>
Buy Now
</button>
</div>
</div>
</div>
</div>
<!-- Load More -->
<div
v-if="marketStore.hasMore && !marketStore.loading"
class="mt-8 text-center"
>
<button
@click="marketStore.loadMore"
class="px-8 py-3 bg-surface-light hover:bg-surface-lighter border border-surface-lighter text-white font-medium rounded-lg transition-colors"
>
<Loader2
v-if="marketStore.loadingMore"
class="w-5 h-5 animate-spin inline-block mr-2"
/>
<span v-else>Load More</span>
</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch, onMounted } from "vue";
import { useRoute, useRouter } from "vue-router";
import { useMarketStore } from "@/stores/market";
import { Search, X, Grid3x3, List, Loader2 } from "lucide-vue-next";
const route = useRoute();
const router = useRouter();
const marketStore = useMarketStore();
const viewMode = ref("grid");
const sortOptions = [
{ value: "price_asc", label: "Price: Low to High" },
{ value: "price_desc", label: "Price: High to Low" },
{ value: "name_asc", label: "Name: A-Z" },
{ value: "name_desc", label: "Name: Z-A" },
{ value: "date_new", label: "Newest First" },
{ value: "date_old", label: "Oldest First" },
];
const gameOptions = [
{ value: null, label: "All Games" },
{ value: "cs2", label: "Counter-Strike 2" },
{ value: "rust", label: "Rust" },
];
const wearOptions = [
{ value: null, label: "All Wear" },
{ value: "fn", label: "Factory New" },
{ value: "mw", label: "Minimal Wear" },
{ value: "ft", label: "Field-Tested" },
{ value: "ww", label: "Well-Worn" },
{ value: "bs", label: "Battle-Scarred" },
];
const rarityOptions = [
{ value: null, label: "All Rarities" },
{ value: "common", label: "Common" },
{ value: "uncommon", label: "Uncommon" },
{ value: "rare", label: "Rare" },
{ value: "mythical", label: "Mythical" },
{ value: "legendary", label: "Legendary" },
];
onMounted(async () => {
if (route.query.search) {
marketStore.updateFilter("search", route.query.search);
}
if (route.query.game) {
marketStore.updateFilter("game", route.query.game);
}
if (route.query.category) {
marketStore.updateFilter("category", route.query.category);
}
await marketStore.fetchItems();
});
watch(
() => route.query,
(newQuery) => {
if (newQuery.search) {
marketStore.updateFilter("search", newQuery.search);
}
},
{ deep: true }
);
watch(
() => marketStore.filters,
async () => {
await marketStore.fetchItems();
},
{ deep: true }
);
const activeFiltersCount = computed(() => {
let count = 0;
if (marketStore.filters.game) count++;
if (marketStore.filters.rarity) count++;
if (marketStore.filters.wear) count++;
if (marketStore.filters.category && marketStore.filters.category !== "all")
count++;
if (marketStore.filters.minPrice !== null) count++;
if (marketStore.filters.maxPrice !== null) count++;
if (marketStore.filters.statTrak) count++;
if (marketStore.filters.souvenir) count++;
return count;
});
const handleSearch = () => {
marketStore.fetchItems();
};
const handleRarityChange = (rarityId) => {
if (marketStore.filters.rarity === rarityId) {
marketStore.updateFilter("rarity", null);
} else {
marketStore.updateFilter("rarity", rarityId);
}
};
const handleWearChange = (wearId) => {
if (marketStore.filters.wear === wearId) {
marketStore.updateFilter("wear", null);
} else {
marketStore.updateFilter("wear", wearId);
}
};
const handleGameChange = (gameId) => {
marketStore.updateFilter("game", gameId);
// Clear game-specific filters when switching games
marketStore.updateFilter("wear", null);
marketStore.updateFilter("rarity", null);
marketStore.updateFilter("statTrak", false);
marketStore.updateFilter("souvenir", false);
};
const handleSortChange = () => {
marketStore.fetchItems();
};
const applyPriceRange = () => {
marketStore.fetchItems();
};
const clearFilters = () => {
marketStore.clearFilters();
};
const formatPrice = (price) => {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(price);
};
const getRarityColor = (rarity) => {
const colors = {
common: "#b0c3d9",
uncommon: "#5e98d9",
rare: "#4b69ff",
mythical: "#8847ff",
legendary: "#d32ce6",
ancient: "#eb4b4b",
exceedingly: "#e4ae39",
};
return colors[rarity?.toLowerCase()] || colors.common;
};
const navigateToItem = (itemId) => {
router.push(`/item/${itemId}`);
};
const handleImageError = (event) => {
event.target.src = "https://via.placeholder.com/400x300?text=No+Image";
};
</script>
<style scoped>
/* Custom scrollbar for sidebar */
aside::-webkit-scrollbar {
width: 6px;
}
aside::-webkit-scrollbar-track {
background: transparent;
}
aside::-webkit-scrollbar-thumb {
background: #1f2a3c;
border-radius: 3px;
}
aside::-webkit-scrollbar-thumb:hover {
background: #2d3748;
}
</style>

View File

@@ -0,0 +1,77 @@
<script setup>
import { useRouter } from 'vue-router'
import { Home, ArrowLeft } from 'lucide-vue-next'
const router = useRouter()
const goHome = () => {
router.push('/')
}
const goBack = () => {
router.back()
}
</script>
<template>
<div class="not-found-page min-h-screen flex items-center justify-center py-12">
<div class="container-custom">
<div class="max-w-2xl mx-auto text-center">
<!-- 404 Animation -->
<div class="relative mb-8">
<div class="text-9xl font-display font-bold text-transparent bg-clip-text bg-gradient-to-r from-primary-500 to-primary-700 animate-pulse-slow">
404
</div>
<div class="absolute inset-0 flex items-center justify-center">
<div class="w-64 h-64 bg-primary-500/10 rounded-full blur-3xl animate-pulse"></div>
</div>
</div>
<!-- Content -->
<h1 class="text-3xl sm:text-4xl font-display font-bold text-white mb-4">
Page Not Found
</h1>
<p class="text-lg text-gray-400 mb-8 max-w-md mx-auto">
The page you're looking for doesn't exist or has been moved.
Let's get you back on track.
</p>
<!-- Actions -->
<div class="flex flex-col sm:flex-row items-center justify-center gap-4">
<button @click="goHome" class="btn btn-primary btn-lg group">
<Home class="w-5 h-5" />
Go to Homepage
</button>
<button @click="goBack" class="btn btn-secondary btn-lg group">
<ArrowLeft class="w-5 h-5 group-hover:-translate-x-1 transition-transform" />
Go Back
</button>
</div>
<!-- Quick Links -->
<div class="mt-12 pt-12 border-t border-surface-lighter">
<p class="text-sm text-gray-500 mb-4">Or try these popular pages:</p>
<div class="flex flex-wrap items-center justify-center gap-4">
<router-link to="/market" class="text-sm text-primary-500 hover:text-primary-400 transition-colors">
Browse Market
</router-link>
<span class="text-gray-600"></span>
<router-link to="/faq" class="text-sm text-primary-500 hover:text-primary-400 transition-colors">
FAQ
</router-link>
<span class="text-gray-600"></span>
<router-link to="/support" class="text-sm text-primary-500 hover:text-primary-400 transition-colors">
Support
</router-link>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.not-found-page {
background: linear-gradient(135deg, rgba(15, 25, 35, 0.95) 0%, rgba(21, 29, 40, 0.98) 50%, rgba(26, 35, 50, 0.95) 100%);
}
</style>

View File

@@ -0,0 +1,48 @@
<script setup>
</script>
<template>
<div class="privacy-page min-h-screen py-8">
<div class="container-custom max-w-4xl">
<h1 class="text-3xl sm:text-4xl font-display font-bold text-white mb-8">Privacy Policy</h1>
<div class="card card-body prose prose-invert max-w-none">
<div class="space-y-6 text-gray-300">
<section>
<h2 class="text-2xl font-semibold text-white mb-4">1. Information We Collect</h2>
<p>We collect information you provide directly to us, including when you create an account, make a purchase, or contact us for support.</p>
</section>
<section>
<h2 class="text-2xl font-semibold text-white mb-4">2. How We Use Your Information</h2>
<p>We use the information we collect to provide, maintain, and improve our services, process transactions, and communicate with you.</p>
</section>
<section>
<h2 class="text-2xl font-semibold text-white mb-4">3. Information Sharing</h2>
<p>We do not sell, trade, or otherwise transfer your personal information to third parties without your consent, except as described in this policy.</p>
</section>
<section>
<h2 class="text-2xl font-semibold text-white mb-4">4. Data Security</h2>
<p>We implement appropriate security measures to protect your personal information from unauthorized access, alteration, disclosure, or destruction.</p>
</section>
<section>
<h2 class="text-2xl font-semibold text-white mb-4">5. Your Rights</h2>
<p>You have the right to access, update, or delete your personal information. Contact us to exercise these rights.</p>
</section>
<section>
<h2 class="text-2xl font-semibold text-white mb-4">6. Contact Us</h2>
<p>If you have any questions about this Privacy Policy, please contact us at <a href="mailto:privacy@turbotrades.com" class="text-primary-500 hover:underline">privacy@turbotrades.com</a></p>
</section>
<div class="text-sm text-gray-500 mt-8 pt-8 border-t border-surface-lighter">
Last updated: {{ new Date().toLocaleDateString() }}
</div>
</div>
</div>
</div>
</div>
</template>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,28 @@
<script setup>
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { User, Star, TrendingUp, Package } from 'lucide-vue-next'
const route = useRoute()
const user = ref(null)
const isLoading = ref(true)
onMounted(async () => {
// Fetch user profile by steamId
const steamId = route.params.steamId
// TODO: Implement API call
isLoading.value = false
})
</script>
<template>
<div class="public-profile-page min-h-screen py-8">
<div class="container-custom max-w-4xl">
<div class="text-center py-20">
<User class="w-16 h-16 text-gray-500 mx-auto mb-4" />
<h3 class="text-xl font-semibold text-gray-400 mb-2">User Profile</h3>
<p class="text-gray-500">Profile view coming soon</p>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,727 @@
<template>
<div class="min-h-screen bg-surface py-8">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<!-- Header -->
<div class="mb-8">
<div class="flex items-center gap-3 mb-3">
<div class="p-3 bg-primary/10 rounded-lg">
<TrendingUp class="w-8 h-8 text-primary" />
</div>
<div>
<h1 class="text-3xl font-bold text-white">Sell Your Items</h1>
<p class="text-text-secondary">
Sell your CS2 and Rust skins directly to TurboTrades for instant
cash
</p>
</div>
</div>
<!-- Trade URL Warning -->
<div
v-if="!hasTradeUrl"
class="bg-warning/10 border border-warning/30 rounded-lg p-4 flex items-start gap-3 mb-4"
>
<AlertTriangle class="w-5 h-5 text-warning flex-shrink-0 mt-0.5" />
<div class="text-sm flex-1">
<p class="text-white font-medium mb-1">Trade URL Required</p>
<p class="text-text-secondary mb-3">
You must set your Steam Trade URL before selling items. This
allows us to send you trade offers.
</p>
<router-link
to="/profile"
class="inline-flex items-center gap-2 px-4 py-2 bg-warning hover:bg-warning/90 text-surface-dark font-medium rounded-lg transition-colors text-sm"
>
<Settings class="w-4 h-4" />
Set Trade URL in Profile
</router-link>
</div>
</div>
<!-- Info Banner -->
<div
class="bg-primary/10 border border-primary/30 rounded-lg p-4 flex items-start gap-3"
>
<Info class="w-5 h-5 text-primary flex-shrink-0 mt-0.5" />
<div class="text-sm">
<p class="text-white font-medium mb-1">How it works:</p>
<p class="text-text-secondary mb-2">
1. Select items from your Steam inventory<br />
2. We'll calculate an instant offer price<br />
3. Accept the trade offer we send to your Steam account<br />
4. Funds will be added to your balance once the trade is completed
</p>
<p class="text-xs text-text-secondary mt-2">
Note: Your Steam inventory must be public for us to fetch your
items.
</p>
</div>
</div>
</div>
<!-- Filters and Search -->
<div class="mb-6 grid grid-cols-1 md:grid-cols-4 gap-4">
<!-- Search -->
<div class="md:col-span-2">
<div class="relative">
<Search
class="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-text-secondary"
/>
<input
v-model="searchQuery"
type="text"
placeholder="Search your items..."
class="w-full pl-10 pr-4 py-2.5 bg-surface-light rounded-lg border border-surface-lighter text-white placeholder-text-secondary focus:outline-none focus:border-primary transition-colors"
@input="filterItems"
/>
</div>
</div>
<!-- Game Filter -->
<select
v-model="selectedGame"
@change="handleGameChange"
class="px-4 py-2.5 bg-surface-light rounded-lg border border-surface-lighter text-white focus:outline-none focus:border-primary transition-colors"
>
<option value="cs2">Counter-Strike 2</option>
<option value="rust">Rust</option>
</select>
<!-- Sort -->
<select
v-model="sortBy"
@change="sortItems"
class="px-4 py-2.5 bg-surface-light rounded-lg border border-surface-lighter text-white focus:outline-none focus:border-primary transition-colors"
>
<option value="price-desc">Price: High to Low</option>
<option value="price-asc">Price: Low to High</option>
<option value="name-asc">Name: A-Z</option>
<option value="name-desc">Name: Z-A</option>
</select>
</div>
<!-- Selected Items Summary -->
<div
v-if="selectedItems.length > 0"
class="mb-6 bg-surface-light rounded-lg border border-primary/50 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">
<CheckCircle class="w-5 h-5 text-primary" />
<span class="text-white font-medium">
{{ selectedItems.length }} item{{
selectedItems.length > 1 ? "s" : ""
}}
selected
</span>
</div>
<div class="h-6 w-px bg-surface-lighter"></div>
<div class="text-white font-bold text-lg">
Total: {{ formatCurrency(totalSelectedValue) }}
</div>
</div>
<div class="flex items-center gap-3">
<button
@click="clearSelection"
class="px-4 py-2 text-sm text-text-secondary hover:text-white transition-colors"
>
Clear
</button>
<button
@click="handleSellClick"
:disabled="!hasTradeUrl"
class="px-6 py-2 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"
>
Sell Selected Items
</button>
</div>
</div>
</div>
<!-- Loading State -->
<div
v-if="isLoading"
class="flex flex-col justify-center items-center py-20"
>
<Loader2 class="w-12 h-12 animate-spin text-primary mb-4" />
<p class="text-text-secondary">Loading your Steam inventory...</p>
</div>
<!-- Error State -->
<div v-else-if="error" class="text-center py-20">
<AlertCircle class="w-16 h-16 text-error mx-auto mb-4 opacity-50" />
<h3 class="text-xl font-semibold text-white mb-2">
Failed to Load Inventory
</h3>
<p class="text-text-secondary mb-6">{{ error }}</p>
<button
@click="fetchInventory"
class="inline-flex items-center gap-2 px-6 py-3 bg-primary hover:bg-primary-dark text-surface-dark font-semibold rounded-lg transition-colors"
>
<RefreshCw class="w-5 h-5" />
Retry
</button>
</div>
<!-- Empty State -->
<div
v-else-if="filteredItems.length === 0 && !isLoading"
class="text-center py-20"
>
<Package
class="w-16 h-16 text-text-secondary mx-auto mb-4 opacity-50"
/>
<h3 class="text-xl font-semibold text-white mb-2">No Items Found</h3>
<p class="text-text-secondary mb-6">
{{
searchQuery
? "Try adjusting your search or filters"
: items.length === 0
? `You don't have any ${
selectedGame === "cs2" ? "CS2" : "Rust"
} items in your inventory`
: "No items match your current filters"
}}
</p>
<div class="flex items-center justify-center gap-4">
<button
@click="handleGameChange"
class="inline-flex items-center gap-2 px-6 py-3 bg-surface-light hover:bg-surface-lighter text-white font-semibold rounded-lg transition-colors"
>
<RefreshCw class="w-5 h-5" />
Switch Game
</button>
<router-link
to="/market"
class="inline-flex items-center gap-2 px-6 py-3 bg-primary hover:bg-primary-dark text-surface-dark font-semibold rounded-lg transition-colors"
>
<ShoppingCart class="w-5 h-5" />
Browse Market
</router-link>
</div>
</div>
<!-- Items Grid -->
<div
v-else
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"
>
<div
v-for="item in paginatedItems"
:key="item.assetid"
@click="toggleSelection(item)"
:class="[
'bg-surface-light rounded-lg overflow-hidden cursor-pointer transition-all border-2',
isSelected(item.assetid)
? 'border-primary ring-2 ring-primary/50'
: 'border-transparent hover:border-primary/30',
]"
>
<!-- Item Image -->
<div class="relative aspect-video bg-surface p-4">
<img
:src="item.image"
:alt="item.name"
class="w-full h-full object-contain"
@error="handleImageError"
/>
<!-- Selection Indicator -->
<div
v-if="isSelected(item.assetid)"
class="absolute top-2 right-2 w-6 h-6 bg-primary rounded-full flex items-center justify-center"
>
<Check class="w-4 h-4 text-surface-dark" />
</div>
<!-- Price Badge -->
<div
v-if="item.estimatedPrice"
class="absolute bottom-2 left-2 px-2 py-1 bg-surface-dark/90 rounded text-xs font-bold text-primary"
>
{{ formatCurrency(item.estimatedPrice) }}
</div>
</div>
<!-- Item Details -->
<div class="p-4">
<h3
class="font-semibold text-white mb-2 line-clamp-2 text-sm"
:title="item.name"
>
{{ item.name }}
</h3>
<!-- Tags -->
<div class="flex items-center gap-2 flex-wrap text-xs mb-3">
<span
v-if="item.wearName"
class="px-2 py-1 bg-surface rounded text-text-secondary"
>
{{ item.wearName }}
</span>
<span
v-if="item.rarity"
class="px-2 py-1 rounded text-white"
:style="{
backgroundColor: getRarityColor(item.rarity) + '40',
color: getRarityColor(item.rarity),
}"
>
{{ formatRarity(item.rarity) }}
</span>
<span
v-if="item.statTrak"
class="px-2 py-1 bg-warning/20 rounded text-warning"
>
StatTrak™
</span>
</div>
<!-- Price Info -->
<div class="flex items-center justify-between">
<div>
<p class="text-xs text-text-secondary mb-1">You Get</p>
<p class="text-lg font-bold text-primary">
{{
item.estimatedPrice
? formatCurrency(item.estimatedPrice)
: "Price unavailable"
}}
</p>
</div>
</div>
</div>
</div>
</div>
<!-- Pagination -->
<div
v-if="totalPages > 1"
class="mt-8 flex items-center justify-center gap-2"
>
<button
@click="currentPage--"
:disabled="currentPage === 1"
class="px-4 py-2 bg-surface-light rounded-lg text-white disabled:opacity-50 disabled:cursor-not-allowed hover:bg-surface-lighter transition-colors"
>
Previous
</button>
<span class="px-4 py-2 text-text-secondary">
Page {{ currentPage }} of {{ totalPages }}
</span>
<button
@click="currentPage++"
:disabled="currentPage === totalPages"
class="px-4 py-2 bg-surface-light rounded-lg text-white disabled:opacity-50 disabled:cursor-not-allowed hover:bg-surface-lighter transition-colors"
>
Next
</button>
</div>
</div>
<!-- Confirm Sale Modal -->
<div
v-if="showConfirmModal"
class="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4"
@click.self="showConfirmModal = 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>
<button
@click="showConfirmModal = false"
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">
<p class="text-text-secondary">
You're about to sell
<strong class="text-white">{{ selectedItems.length }}</strong>
item{{ selectedItems.length > 1 ? "s" : "" }} to TurboTrades.
</p>
<div class="bg-surface rounded-lg p-4 space-y-2">
<div class="flex items-center justify-between">
<span class="text-text-secondary">Items Selected:</span>
<span class="text-white font-semibold">
{{ selectedItems.length }}
</span>
</div>
<div class="flex items-center justify-between">
<span class="text-text-secondary">Total Value:</span>
<span class="text-white font-semibold">
{{ formatCurrency(totalSelectedValue) }}
</span>
</div>
<div class="border-t border-surface-lighter pt-2"></div>
<div class="flex items-center justify-between">
<span class="text-white font-bold">You Will Receive:</span>
<span class="text-primary font-bold text-xl">
{{ formatCurrency(totalSelectedValue) }}
</span>
</div>
</div>
<div
class="bg-primary/10 border border-primary/30 rounded-lg p-3 flex items-start gap-2"
>
<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.
</p>
</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"
>
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>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from "vue";
import { useRouter } from "vue-router";
import { useAuthStore } from "@/stores/auth";
import axios from "@/utils/axios";
import { useToast } from "vue-toastification";
import {
TrendingUp,
Search,
Package,
Loader2,
Check,
CheckCircle,
X,
AlertCircle,
Info,
ShoppingCart,
Settings,
AlertTriangle,
RefreshCw,
} from "lucide-vue-next";
const router = useRouter();
const authStore = useAuthStore();
const toast = useToast();
// State
const items = ref([]);
const filteredItems = ref([]);
const selectedItems = ref([]);
const isLoading = ref(false);
const isProcessing = ref(false);
const showConfirmModal = ref(false);
const searchQuery = ref("");
const selectedGame = ref("cs2");
const sortBy = ref("price-desc");
const currentPage = ref(1);
const itemsPerPage = 20;
const error = ref(null);
const hasTradeUrl = ref(false);
// Computed
const totalPages = computed(() => {
return Math.ceil(filteredItems.value.length / itemsPerPage);
});
const paginatedItems = computed(() => {
const start = (currentPage.value - 1) * itemsPerPage;
const end = start + itemsPerPage;
return filteredItems.value.slice(start, end);
});
const totalSelectedValue = computed(() => {
return selectedItems.value.reduce((total, item) => {
return total + (item.estimatedPrice || 0);
}, 0);
});
// Methods
const fetchInventory = async () => {
isLoading.value = true;
error.value = null;
try {
// Check if user has trade URL set
hasTradeUrl.value = !!authStore.user?.tradeUrl;
// Fetch Steam inventory (now includes prices!)
const response = await axios.get("/api/inventory/steam", {
params: { game: selectedGame.value },
});
if (response.data.success) {
// Items already have marketPrice from backend
items.value = (response.data.items || []).map((item) => ({
...item,
estimatedPrice: item.marketPrice || null,
hasPriceData: item.hasPriceData || false,
}));
filteredItems.value = [...items.value];
sortItems();
if (items.value.length === 0) {
toast.info(
`No ${
selectedGame.value === "cs2" ? "CS2" : "Rust"
} items found in your inventory`
);
}
}
} catch (err) {
console.error("Failed to fetch inventory:", err);
if (err.response?.status === 403) {
error.value =
"Your Steam inventory is private. Please make it public in your Steam settings.";
toast.error("Steam inventory is private");
} else if (err.response?.status === 404) {
error.value = "Steam profile not found or inventory is empty.";
} else if (err.response?.data?.message) {
error.value = err.response.data.message;
toast.error(err.response.data.message);
} else {
error.value = "Failed to load inventory. Please try again.";
toast.error("Failed to load inventory");
}
} finally {
isLoading.value = false;
}
};
// Removed: Prices now come directly from inventory endpoint
// No need for separate pricing call - instant loading!
const filterItems = () => {
let filtered = [...items.value];
// Filter by search query
if (searchQuery.value.trim()) {
const query = searchQuery.value.toLowerCase();
filtered = filtered.filter((item) =>
item.name.toLowerCase().includes(query)
);
}
filteredItems.value = filtered;
sortItems();
currentPage.value = 1;
};
const sortItems = () => {
const sorted = [...filteredItems.value];
switch (sortBy.value) {
case "price-desc":
sorted.sort((a, b) => (b.estimatedPrice || 0) - (a.estimatedPrice || 0));
break;
case "price-asc":
sorted.sort((a, b) => (a.estimatedPrice || 0) - (b.estimatedPrice || 0));
break;
case "name-asc":
sorted.sort((a, b) => a.name.localeCompare(b.name));
break;
case "name-desc":
sorted.sort((a, b) => b.name.localeCompare(a.name));
break;
}
filteredItems.value = sorted;
};
const handleGameChange = async () => {
selectedItems.value = [];
items.value = [];
filteredItems.value = [];
error.value = null;
await fetchInventory();
};
const toggleSelection = (item) => {
if (!item.estimatedPrice) {
toast.warning("Price not calculated yet");
return;
}
const index = selectedItems.value.findIndex(
(i) => i.assetid === item.assetid
);
if (index > -1) {
selectedItems.value.splice(index, 1);
} else {
selectedItems.value.push(item);
}
};
const isSelected = (assetid) => {
return selectedItems.value.some((item) => item.assetid === assetid);
};
const clearSelection = () => {
selectedItems.value = [];
};
const handleSellClick = () => {
if (!hasTradeUrl.value) {
toast.warning("Please set your Steam Trade URL in your profile first");
router.push("/profile");
return;
}
showConfirmModal.value = true;
};
const confirmSale = async () => {
if (selectedItems.value.length === 0) return;
if (!hasTradeUrl.value) {
toast.error("Trade URL is required to sell items");
showConfirmModal.value = false;
router.push("/profile");
return;
}
isProcessing.value = true;
try {
const response = await axios.post("/api/inventory/sell", {
items: selectedItems.value.map((item) => ({
assetid: item.assetid,
name: item.name,
price: item.estimatedPrice,
image: item.image,
wear: item.wear,
rarity: item.rarity,
category: item.category,
statTrak: item.statTrak,
souvenir: item.souvenir,
})),
});
if (response.data.success) {
toast.success(
`Successfully listed ${selectedItems.value.length} item${
selectedItems.value.length > 1 ? "s" : ""
} for ${formatCurrency(response.data.totalEarned)}!`
);
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);
}
// Remove sold items from list
const soldAssetIds = selectedItems.value.map((item) => item.assetid);
items.value = items.value.filter(
(item) => !soldAssetIds.includes(item.assetid)
);
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);
const message =
err.response?.data?.message ||
"Failed to complete sale. Please try again.";
toast.error(message);
} finally {
isProcessing.value = false;
}
};
const formatCurrency = (amount) => {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(amount);
};
const formatRarity = (rarity) => {
if (!rarity) return "";
const rarityMap = {
Rarity_Common: "Common",
Rarity_Uncommon: "Uncommon",
Rarity_Rare: "Rare",
Rarity_Mythical: "Mythical",
Rarity_Legendary: "Legendary",
Rarity_Ancient: "Ancient",
Rarity_Contraband: "Contraband",
};
return rarityMap[rarity] || rarity;
};
const getRarityColor = (rarity) => {
const colors = {
Rarity_Common: "#b0c3d9",
Rarity_Uncommon: "#5e98d9",
Rarity_Rare: "#4b69ff",
Rarity_Mythical: "#8847ff",
Rarity_Legendary: "#d32ce6",
Rarity_Ancient: "#eb4b4b",
Rarity_Contraband: "#e4ae39",
};
return colors[rarity] || "#b0c3d9";
};
const handleImageError = (event) => {
event.target.src = "https://via.placeholder.com/400x300?text=No+Image";
};
onMounted(() => {
if (!authStore.isAuthenticated) {
router.push("/");
return;
}
fetchInventory();
});
</script>
<style scoped>
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>

View File

@@ -0,0 +1,28 @@
<script setup>
import { Mail, MessageCircle } from 'lucide-vue-next'
</script>
<template>
<div class="support-page min-h-screen py-8">
<div class="container-custom max-w-4xl">
<h1 class="text-3xl font-display font-bold text-white mb-4">Support Center</h1>
<p class="text-gray-400 mb-8">Need help? We're here to assist you.</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="card card-body">
<MessageCircle class="w-12 h-12 text-primary-500 mb-4" />
<h3 class="text-xl font-semibold text-white mb-2">Live Chat</h3>
<p class="text-gray-400 mb-4">Chat with our support team in real-time</p>
<button class="btn btn-primary">Start Chat</button>
</div>
<div class="card card-body">
<Mail class="w-12 h-12 text-accent-blue mb-4" />
<h3 class="text-xl font-semibold text-white mb-2">Email Support</h3>
<p class="text-gray-400 mb-4">Send us an email and we'll respond within 24 hours</p>
<a href="mailto:support@turbotrades.com" class="btn btn-secondary">Send Email</a>
</div>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,75 @@
<script setup>
</script>
<template>
<div class="terms-page min-h-screen py-8">
<div class="container-custom max-w-4xl">
<h1 class="text-4xl font-display font-bold text-white mb-8">Terms of Service</h1>
<div class="card card-body space-y-6 text-gray-300">
<section>
<h2 class="text-2xl font-semibold text-white mb-4">1. Acceptance of Terms</h2>
<p class="mb-4">
By accessing and using TurboTrades, you accept and agree to be bound by the terms and provision of this agreement.
</p>
</section>
<section>
<h2 class="text-2xl font-semibold text-white mb-4">2. Use License</h2>
<p class="mb-4">
Permission is granted to temporarily use TurboTrades for personal, non-commercial transitory viewing only.
</p>
</section>
<section>
<h2 class="text-2xl font-semibold text-white mb-4">3. User Accounts</h2>
<p class="mb-4">
You are responsible for maintaining the confidentiality of your account and password. You agree to accept responsibility for all activities that occur under your account.
</p>
</section>
<section>
<h2 class="text-2xl font-semibold text-white mb-4">4. Trading and Transactions</h2>
<p class="mb-4">
All trades are final. We reserve the right to cancel any transaction that we deem suspicious or fraudulent.
</p>
</section>
<section>
<h2 class="text-2xl font-semibold text-white mb-4">5. Prohibited Activities</h2>
<ul class="list-disc list-inside space-y-2 mb-4">
<li>Using the service for any illegal purpose</li>
<li>Attempting to interfere with the proper working of the service</li>
<li>Using bots or automated tools without permission</li>
<li>Engaging in fraudulent activities</li>
</ul>
</section>
<section>
<h2 class="text-2xl font-semibold text-white mb-4">6. Limitation of Liability</h2>
<p class="mb-4">
TurboTrades shall not be liable for any indirect, incidental, special, consequential or punitive damages resulting from your use of the service.
</p>
</section>
<section>
<h2 class="text-2xl font-semibold text-white mb-4">7. Changes to Terms</h2>
<p class="mb-4">
We reserve the right to modify these terms at any time. Your continued use of the service following any changes indicates your acceptance of the new terms.
</p>
</section>
<section>
<h2 class="text-2xl font-semibold text-white mb-4">8. Contact</h2>
<p>
If you have any questions about these Terms, please contact us at support@turbotrades.com
</p>
</section>
<div class="pt-6 border-t border-surface-lighter text-sm text-gray-500">
Last updated: {{ new Date().toLocaleDateString() }}
</div>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,735 @@
<template>
<div class="min-h-screen bg-surface py-8">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<!-- Header -->
<div class="mb-8">
<h1 class="text-3xl font-bold text-white mb-2">Transaction History</h1>
<p class="text-text-secondary">
View all your deposits, withdrawals, purchases, and sales
</p>
</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>
<label class="block text-sm font-medium text-text-secondary mb-2">
Type
</label>
<select
v-model="filters.type"
class="w-full px-4 py-2 bg-surface rounded-lg border border-surface-lighter text-text-primary focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
>
<option value="">All Types</option>
<option value="deposit">Deposits</option>
<option value="withdrawal">Withdrawals</option>
<option value="purchase">Purchases</option>
<option value="sale">Sales</option>
<option value="trade">Trades</option>
<option value="bonus">Bonuses</option>
<option value="refund">Refunds</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-text-secondary mb-2">
Status
</label>
<select
v-model="filters.status"
class="w-full px-4 py-2 bg-surface rounded-lg border border-surface-lighter text-text-primary focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
>
<option value="">All Status</option>
<option value="completed">Completed</option>
<option value="pending">Pending</option>
<option value="processing">Processing</option>
<option value="failed">Failed</option>
<option value="cancelled">Cancelled</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-text-secondary mb-2">
Date Range
</label>
<select
v-model="filters.dateRange"
class="w-full px-4 py-2 bg-surface rounded-lg border border-surface-lighter text-text-primary focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
>
<option value="all">All Time</option>
<option value="today">Today</option>
<option value="week">Last 7 Days</option>
<option value="month">Last 30 Days</option>
<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>
<!-- Stats Summary -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<div
class="bg-surface-light rounded-lg border border-surface-lighter p-4"
>
<div class="text-sm text-text-secondary mb-1">Total Deposits</div>
<div class="text-2xl font-bold text-success">
{{ formatCurrency(stats.totalDeposits) }}
</div>
<div class="text-xs text-text-secondary mt-1">
{{ stats.depositCount }} transactions
</div>
</div>
<div
class="bg-surface-light rounded-lg border border-surface-lighter p-4"
>
<div class="text-sm text-text-secondary mb-1">Total Withdrawals</div>
<div class="text-2xl font-bold text-danger">
{{ formatCurrency(stats.totalWithdrawals) }}
</div>
<div class="text-xs text-text-secondary mt-1">
{{ stats.withdrawalCount }} transactions
</div>
</div>
<div
class="bg-surface-light rounded-lg border border-surface-lighter p-4"
>
<div class="text-sm text-text-secondary mb-1">Total Spent</div>
<div class="text-2xl font-bold text-warning">
{{ formatCurrency(stats.totalPurchases) }}
</div>
<div class="text-xs text-text-secondary mt-1">
{{ stats.purchaseCount }} purchases
</div>
</div>
<div
class="bg-surface-light rounded-lg border border-surface-lighter p-4"
>
<div class="text-sm text-text-secondary mb-1">Total Earned</div>
<div class="text-2xl font-bold text-success">
{{ formatCurrency(stats.totalSales) }}
</div>
<div class="text-xs text-text-secondary mt-1">
{{ stats.saleCount }} sales
</div>
</div>
</div>
<!-- Transactions List -->
<div class="bg-surface-light rounded-lg border border-surface-lighter">
<div class="p-6 border-b border-surface-lighter">
<h2 class="text-xl font-bold text-white flex items-center gap-2">
<History class="w-6 h-6 text-primary" />
Transactions
</h2>
</div>
<div v-if="loading" class="text-center py-12">
<Loader class="w-8 h-8 animate-spin mx-auto text-primary" />
<p class="text-text-secondary mt-4">Loading transactions...</p>
</div>
<div
v-else-if="filteredTransactions.length === 0"
class="text-center py-12"
>
<History class="w-16 h-16 text-text-secondary/50 mx-auto mb-4" />
<h3 class="text-xl font-semibold text-text-secondary mb-2">
No transactions found
</h3>
<p class="text-text-secondary">
{{
filters.type || filters.status
? "Try changing your filters"
: "Your transaction history will appear here"
}}
</p>
</div>
<div v-else class="divide-y divide-surface-lighter">
<div
v-for="transaction in paginatedTransactions"
:key="transaction.id"
class="p-6 hover:bg-surface/50 transition-colors"
>
<div class="flex items-start justify-between gap-4">
<!-- Left side: Icon/Image, Type, Description -->
<div class="flex items-start gap-4 flex-1 min-w-0">
<!-- Item Image (for purchases/sales) or Icon (for other types) -->
<div
v-if="
transaction.itemImage &&
(transaction.type === 'purchase' ||
transaction.type === 'sale')
"
class="flex-shrink-0"
>
<img
:src="transaction.itemImage"
:alt="transaction.itemName"
class="w-16 h-16 rounded-lg object-cover border-2 border-surface-lighter"
@error="$event.target.style.display = 'none'"
/>
</div>
<!-- Icon (for non-item transactions) -->
<div
v-else
:class="[
'p-3 rounded-lg flex-shrink-0',
getTransactionIconBg(transaction.type),
]"
>
<component
:is="getTransactionIcon(transaction.type)"
:class="[
'w-6 h-6',
getTransactionIconColor(transaction.type),
]"
/>
</div>
<!-- Details -->
<div class="flex-1 min-w-0">
<!-- Title and Status -->
<div class="flex items-center gap-2 flex-wrap mb-1">
<h3 class="text-white font-semibold">
{{ getTransactionTitle(transaction) }}
</h3>
<span
:class="[
'text-xs px-2 py-0.5 rounded-full font-medium',
getStatusClass(transaction.status),
]"
>
{{ transaction.status }}
</span>
</div>
<!-- Description -->
<p class="text-sm text-text-secondary mb-2">
{{
transaction.description ||
getTransactionDescription(transaction)
}}
</p>
<!-- Meta Information -->
<div
class="flex flex-wrap items-center gap-3 text-xs text-text-secondary"
>
<span class="flex items-center gap-1">
<Calendar class="w-3 h-3" />
{{ formatDate(transaction.createdAt) }}
</span>
<span
v-if="transaction.sessionIdShort"
class="flex items-center gap-1"
>
<Monitor class="w-3 h-3" />
Session:
<span
:style="{
backgroundColor: getSessionColor(
transaction.sessionIdShort
),
}"
class="px-2 py-0.5 rounded text-white font-mono text-[10px]"
:title="`Session ID: ${transaction.sessionIdShort}`"
>
{{ transaction.sessionIdShort }}
</span>
</span>
</div>
</div>
</div>
<!-- Right side: Amount -->
<div class="text-right flex-shrink-0">
<div
:class="[
'text-xl font-bold',
transaction.direction === '+'
? 'text-success'
: 'text-danger',
]"
>
{{ transaction.direction
}}{{ formatCurrency(transaction.amount) }}
</div>
<div
v-if="transaction.fee > 0"
class="text-xs text-text-secondary mt-1"
>
Fee: {{ formatCurrency(transaction.fee) }}
</div>
</div>
</div>
<!-- Additional Details (expandable) -->
<div
v-if="expandedTransaction === transaction.id"
class="mt-4 pt-4 border-t border-surface-lighter"
>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
<div>
<span class="text-text-secondary">Transaction ID:</span>
<span class="text-white ml-2 font-mono text-xs">{{
transaction.id
}}</span>
</div>
<div v-if="transaction.balanceBefore !== undefined">
<span class="text-text-secondary">Balance Before:</span>
<span class="text-white ml-2">{{
formatCurrency(transaction.balanceBefore)
}}</span>
</div>
<div v-if="transaction.balanceAfter !== undefined">
<span class="text-text-secondary">Balance After:</span>
<span class="text-white ml-2">{{
formatCurrency(transaction.balanceAfter)
}}</span>
</div>
<div v-if="transaction.paymentMethod">
<span class="text-text-secondary">Payment Method:</span>
<span class="text-white ml-2 capitalize">{{
transaction.paymentMethod
}}</span>
</div>
</div>
</div>
<!-- Toggle Details Button -->
<button
@click="toggleExpanded(transaction.id)"
class="mt-3 text-xs text-primary hover:text-primary-hover flex items-center gap-1"
>
<ChevronDown
:class="[
'w-4 h-4 transition-transform',
expandedTransaction === transaction.id ? 'rotate-180' : '',
]"
/>
{{ expandedTransaction === transaction.id ? "Hide" : "Show" }}
Details
</button>
</div>
</div>
<!-- Pagination -->
<div
v-if="filteredTransactions.length > perPage"
class="p-6 border-t border-surface-lighter"
>
<div class="flex items-center justify-between flex-wrap gap-4">
<!-- Page info -->
<div class="text-sm text-text-secondary">
Showing {{ (currentPage - 1) * perPage + 1 }} to
{{ Math.min(currentPage * perPage, filteredTransactions.length) }}
of {{ filteredTransactions.length }} transactions
</div>
<!-- Page controls -->
<div class="flex items-center gap-2">
<!-- Previous button -->
<button
@click="prevPage"
:disabled="!hasPrevPage"
class="px-3 py-2 rounded-lg border border-surface-lighter bg-surface text-text-primary hover:bg-surface-light disabled:opacity-50 disabled:cursor-not-allowed transition-all"
title="Previous page"
>
<ChevronLeft class="w-4 h-4" />
</button>
<!-- Page numbers -->
<div class="flex gap-1">
<!-- First page -->
<button
v-if="currentPage > 3"
@click="goToPage(1)"
class="px-3 py-2 rounded-lg border border-surface-lighter bg-surface text-text-primary hover:bg-surface-light transition-all"
>
1
</button>
<span
v-if="currentPage > 3"
class="px-2 py-2 text-text-secondary"
>
...
</span>
<!-- Nearby pages -->
<button
v-for="page in [
currentPage - 1,
currentPage,
currentPage + 1,
].filter((p) => p >= 1 && p <= totalPages)"
:key="page"
@click="goToPage(page)"
:class="[
'px-3 py-2 rounded-lg border transition-all',
page === currentPage
? 'bg-primary border-primary text-white font-semibold'
: 'border-surface-lighter bg-surface text-text-primary hover:bg-surface-light',
]"
>
{{ page }}
</button>
<!-- Last page -->
<span
v-if="currentPage < totalPages - 2"
class="px-2 py-2 text-text-secondary"
>
...
</span>
<button
v-if="currentPage < totalPages - 2"
@click="goToPage(totalPages)"
class="px-3 py-2 rounded-lg border border-surface-lighter bg-surface text-text-primary hover:bg-surface-light transition-all"
>
{{ totalPages }}
</button>
</div>
<!-- Next button -->
<button
@click="nextPage"
:disabled="!hasNextPage"
class="px-3 py-2 rounded-lg border border-surface-lighter bg-surface text-text-primary hover:bg-surface-light disabled:opacity-50 disabled:cursor-not-allowed transition-all"
title="Next page"
>
<ChevronRight class="w-4 h-4" />
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, watch } from "vue";
import { useAuthStore } from "@/stores/auth";
import axios from "@/utils/axios";
import { useToast } from "vue-toastification";
import {
History,
Loader,
Calendar,
Monitor,
Smartphone,
Tablet,
Laptop,
ChevronDown,
ChevronLeft,
ChevronRight,
ArrowDownCircle,
ArrowUpCircle,
ShoppingCart,
Tag,
RefreshCw,
Gift,
DollarSign,
} from "lucide-vue-next";
const authStore = useAuthStore();
const toast = useToast();
// State
const transactions = ref([]);
const loading = ref(false);
const currentPage = ref(1);
const perPage = ref(10);
const totalTransactions = ref(0);
const expandedTransaction = ref(null);
const filters = ref({
type: "",
status: "",
dateRange: "all",
});
const stats = ref({
totalDeposits: 0,
totalWithdrawals: 0,
totalPurchases: 0,
totalSales: 0,
depositCount: 0,
withdrawalCount: 0,
purchaseCount: 0,
saleCount: 0,
});
// Computed
const filteredTransactions = computed(() => {
let filtered = [...transactions.value];
if (filters.value.type) {
filtered = filtered.filter((t) => t.type === filters.value.type);
}
if (filters.value.status) {
filtered = filtered.filter((t) => t.status === filters.value.status);
}
if (filters.value.dateRange !== "all") {
const now = new Date();
const filterDate = new Date();
switch (filters.value.dateRange) {
case "today":
filterDate.setHours(0, 0, 0, 0);
break;
case "week":
filterDate.setDate(now.getDate() - 7);
break;
case "month":
filterDate.setDate(now.getDate() - 30);
break;
case "year":
filterDate.setFullYear(now.getFullYear() - 1);
break;
}
filtered = filtered.filter((t) => new Date(t.createdAt) >= filterDate);
}
return filtered;
});
// Paginated transactions
const paginatedTransactions = computed(() => {
const start = (currentPage.value - 1) * perPage.value;
const end = start + perPage.value;
return filteredTransactions.value.slice(start, end);
});
const totalPages = computed(() => {
return Math.ceil(filteredTransactions.value.length / perPage.value);
});
const hasNextPage = computed(() => {
return currentPage.value < totalPages.value;
});
const hasPrevPage = computed(() => {
return currentPage.value > 1;
});
// Methods
const fetchTransactions = async () => {
loading.value = true;
try {
console.log("🔄 Fetching transactions...");
const response = await axios.get("/api/user/transactions", {
withCredentials: true,
params: {
limit: 1000, // Fetch all, we'll paginate on frontend
},
});
console.log("✅ Transaction response:", response.data);
if (response.data.success) {
transactions.value = response.data.transactions;
totalTransactions.value = response.data.transactions.length;
stats.value = response.data.stats || stats.value;
console.log(`📊 Loaded ${transactions.value.length} transactions`);
console.log("Stats:", stats.value);
} else {
console.warn("⚠️ Response success is false");
}
} catch (error) {
console.error("❌ Failed to fetch transactions:", error);
console.error("Response:", error.response?.data);
console.error("Status:", error.response?.status);
if (error.response?.status !== 404) {
toast.error("Failed to load transactions");
}
} finally {
loading.value = false;
}
};
const nextPage = () => {
if (hasNextPage.value) {
currentPage.value++;
window.scrollTo({ top: 0, behavior: "smooth" });
}
};
const prevPage = () => {
if (hasPrevPage.value) {
currentPage.value--;
window.scrollTo({ top: 0, behavior: "smooth" });
}
};
const goToPage = (page) => {
if (page >= 1 && page <= totalPages.value) {
currentPage.value = page;
window.scrollTo({ top: 0, behavior: "smooth" });
}
};
const resetFilters = () => {
filters.value = {
type: "",
status: "",
dateRange: "all",
};
currentPage.value = 1;
};
// Watch for filter changes and reset to page 1
watch(
[
() => filters.value.type,
() => filters.value.status,
() => filters.value.dateRange,
],
() => {
currentPage.value = 1;
}
);
const toggleExpanded = (id) => {
expandedTransaction.value = expandedTransaction.value === id ? null : id;
};
// Helper functions
const formatCurrency = (amount) => {
if (amount === undefined || amount === null) return "$0.00";
return `$${Math.abs(amount).toFixed(2)}`;
};
const formatDate = (date) => {
if (!date) return "N/A";
const d = new Date(date);
const now = new Date();
const diff = now - d;
const hours = Math.floor(diff / 3600000);
const days = Math.floor(diff / 86400000);
if (hours < 1) return "Just now";
if (hours < 24) return `${hours}h ago`;
if (days < 7) return `${days}d ago`;
return d.toLocaleDateString();
};
const getTransactionIcon = (type) => {
const icons = {
deposit: ArrowDownCircle,
withdrawal: ArrowUpCircle,
purchase: ShoppingCart,
sale: Tag,
trade: RefreshCw,
bonus: Gift,
refund: DollarSign,
};
return icons[type] || DollarSign;
};
const getTransactionIconColor = (type) => {
const colors = {
deposit: "text-success",
withdrawal: "text-danger",
purchase: "text-primary",
sale: "text-warning",
trade: "text-info",
bonus: "text-success",
refund: "text-warning",
};
return colors[type] || "text-text-secondary";
};
const getTransactionIconBg = (type) => {
const backgrounds = {
deposit: "bg-success/20",
withdrawal: "bg-danger/20",
purchase: "bg-primary/20",
sale: "bg-warning/20",
trade: "bg-info/20",
bonus: "bg-success/20",
refund: "bg-warning/20",
};
return backgrounds[type] || "bg-surface-lighter";
};
const getTransactionTitle = (transaction) => {
const titles = {
deposit: "Deposit",
withdrawal: "Withdrawal",
purchase: "Item Purchase",
sale: "Item Sale",
trade: "Trade",
bonus: "Bonus",
refund: "Refund",
};
return titles[transaction.type] || "Transaction";
};
const getTransactionDescription = (transaction) => {
if (transaction.itemName) {
return `${transaction.type === "purchase" ? "Purchased" : "Sold"} ${
transaction.itemName
}`;
}
return `${
transaction.type.charAt(0).toUpperCase() + transaction.type.slice(1)
} of ${formatCurrency(transaction.amount)}`;
};
const getStatusClass = (status) => {
const classes = {
completed: "bg-success/20 text-success",
pending: "bg-warning/20 text-warning",
processing: "bg-info/20 text-info",
failed: "bg-danger/20 text-danger",
cancelled: "bg-text-secondary/20 text-text-secondary",
};
return classes[status] || "bg-surface-lighter text-text-secondary";
};
const getDeviceIcon = (device) => {
const icons = {
Mobile: Smartphone,
Tablet: Tablet,
Desktop: Laptop,
};
return icons[device] || Laptop;
};
const getSessionColor = (sessionIdShort) => {
if (!sessionIdShort) return "#64748b";
let hash = 0;
for (let i = 0; i < sessionIdShort.length; i++) {
hash = sessionIdShort.charCodeAt(i) + ((hash << 5) - hash);
}
const hue = Math.abs(hash) % 360;
const saturation = 60 + (Math.abs(hash) % 20);
const lightness = 45 + (Math.abs(hash) % 15);
return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
};
// Lifecycle
onMounted(() => {
fetchTransactions();
});
</script>

View File

@@ -0,0 +1,77 @@
<script setup>
import { ref } from 'vue'
import { useAuthStore } from '@/stores/auth'
import { DollarSign, AlertCircle } from 'lucide-vue-next'
const authStore = useAuthStore()
const amount = ref(0)
const method = ref('paypal')
</script>
<template>
<div class="withdraw-page min-h-screen py-8">
<div class="container-custom max-w-2xl">
<h1 class="text-3xl font-display font-bold text-white mb-2">Withdraw Funds</h1>
<p class="text-gray-400 mb-8">Withdraw your balance to your preferred payment method</p>
<div class="card card-body space-y-6">
<!-- Available Balance -->
<div class="p-4 bg-surface-light rounded-lg">
<div class="text-sm text-gray-400 mb-1">Available Balance</div>
<div class="text-3xl font-bold text-primary-500">
{{ new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(authStore.balance) }}
</div>
</div>
<!-- Withdrawal Amount -->
<div class="input-group">
<label class="input-label">Withdrawal Amount</label>
<div class="relative">
<DollarSign class="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-500" />
<input
v-model.number="amount"
type="number"
placeholder="0.00"
class="input pl-10"
min="5"
:max="authStore.balance"
step="0.01"
/>
</div>
<p class="input-hint">Minimum withdrawal: $5.00</p>
</div>
<!-- Payment Method -->
<div class="input-group">
<label class="input-label">Payment Method</label>
<select v-model="method" class="input">
<option value="paypal">PayPal</option>
<option value="bank">Bank Transfer</option>
<option value="crypto">Cryptocurrency</option>
</select>
</div>
<!-- Notice -->
<div class="p-4 bg-accent-blue/10 border border-accent-blue/30 rounded-lg">
<div class="flex gap-3">
<AlertCircle class="w-5 h-5 text-accent-blue flex-shrink-0 mt-0.5" />
<div class="text-sm">
<div class="font-medium text-white mb-1">Processing Time</div>
<p class="text-gray-400">
Withdrawals are typically processed within 24-48 hours. You'll receive an email confirmation once your withdrawal is complete.
</p>
</div>
</div>
</div>
<!-- Submit Button -->
<button
class="btn btn-primary w-full btn-lg"
:disabled="amount < 5 || amount > authStore.balance"
>
Request Withdrawal
</button>
</div>
</div>
</div>
</template>