First pass at interactive marketplace listings
This commit is contained in:
parent
d64ba33b61
commit
0999c1f6ab
0
assets/css/styles.css
Normal file
0
assets/css/styles.css
Normal file
@ -1,216 +1,426 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<main class="marketplace-layout">
|
||||||
<!-- Filters -->
|
<section class="filters-section">
|
||||||
<details :open="showFilters" @toggle="toggleFilters">
|
<!-- Filters Toggle -->
|
||||||
<summary role="button" class="contrast" style="margin-bottom: 1rem;">
|
<details class="filters-toggle" :open="showFilters">
|
||||||
{{ showFilters ? 'Hide Filters' : 'Show Filters' }}
|
<summary @click.prevent="showFilters = !showFilters">
|
||||||
</summary>
|
Filters
|
||||||
|
<small v-if="hasActiveFilters" style="margin-left: 0.5rem;">({{ activeFilterCount }} active)</small>
|
||||||
<!-- Filters Panel -->
|
</summary>
|
||||||
<div class="container" style="background: var(--pico-card-background-color); padding: 1rem; border-radius: var(--pico-border-radius);">
|
|
||||||
|
<form @submit.prevent class="filters">
|
||||||
<label for="filter-min-cost">Min Cost:</label>
|
<fieldset>
|
||||||
<input v-model.number="marketplace.filters.minCost" id="filter-min-cost" type="number" placeholder="Min cost" />
|
<div class="grid">
|
||||||
|
<label>
|
||||||
<label for="filter-max-cost">Max Cost:</label>
|
Min Cost
|
||||||
<input v-model.number="marketplace.filters.maxCost" id="filter-max-cost" type="number" placeholder="Max cost" />
|
<input type="number" v-model.number="marketplace.minCost" inputmode="numeric" />
|
||||||
|
</label>
|
||||||
<label for="filter-rarity">Rarity:</label>
|
|
||||||
<select v-model="marketplace.filters.rarities" id="filter-rarity" multiple size="6">
|
<label>
|
||||||
<option value="Replacement">Replacement</option>
|
Max Cost
|
||||||
<option value="Reserve">Reserve</option>
|
<input type="number" v-model.number="marketplace.maxCost" inputmode="numeric" />
|
||||||
<option value="Starter">Starter</option>
|
</label>
|
||||||
<option value="All-Star">All-Star</option>
|
</div>
|
||||||
<option value="MVP">MVP</option>
|
|
||||||
<option value="Hall of Fame">Hall of Fame</option>
|
<label>
|
||||||
</select>
|
Rarity
|
||||||
|
<select v-model="marketplace.rarities" multiple size="6">
|
||||||
<button @click="applyFilters" class="contrast" style="margin-top: 1rem;">
|
<option value="Replacement">Replacement</option>
|
||||||
Apply Filters
|
<option value="Reserve">Reserve</option>
|
||||||
</button>
|
<option value="Starter">Starter</option>
|
||||||
|
<option value="All-Star">All-Star</option>
|
||||||
|
<option value="MVP">MVP</option>
|
||||||
|
<option value="Hall of Fame">Hall of Fame</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
Cardset
|
||||||
|
<select v-model="marketplace.cardset" multiple size="5">
|
||||||
|
<option v-for="name in cardsetNames" :key="name" :value="name">
|
||||||
|
{{ name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<button type="button" @click="marketplace.clearFilters" class="secondary">
|
||||||
|
Clear Filters
|
||||||
|
</button>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<!-- Active Filter Chips -->
|
||||||
|
<div v-if="hasActiveFilters" class="filter-chips">
|
||||||
|
<span v-if="marketplace.minCost" class="chip">
|
||||||
|
Min: {{ marketplace.minCost }}
|
||||||
|
<button @click="marketplace.minCost = null" aria-label="Remove Min Cost">×</button>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span v-if="marketplace.maxCost" class="chip">
|
||||||
|
Max: {{ marketplace.maxCost }}
|
||||||
|
<button @click="marketplace.maxCost = null" aria-label="Remove Max Cost">×</button>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span v-for="rar in marketplace.rarities" :key="rar" class="chip">
|
||||||
|
{{ rar }}
|
||||||
|
<button @click="removeRarity(rar)" aria-label="Remove Rarity">×</button>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span v-for="set in marketplace.cardset" :key="set" class="chip">
|
||||||
|
{{ set }}
|
||||||
|
<button @click="removeCardset(set)" aria-label="Remove Cardset">×</button>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
|
||||||
|
|
||||||
<!-- Loading -->
|
|
||||||
<div v-if="pending" class="container" style="text-align: center; padding: 2rem;">
|
|
||||||
<p>Loading players...</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- No Results -->
|
|
||||||
<div v-else-if="!playerList || playerList.length === 0" class="container" style="text-align: center; padding: 2rem;">
|
|
||||||
<p>No players found. Try adjusting your filters!</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Player Cards -->
|
|
||||||
<section v-else class="grid" style="grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1rem; text-align: center;">
|
|
||||||
<article v-for="player in playerList" :key="player.player_id" class="card player-card">
|
|
||||||
<NuxtLink :to="`/players/${player.player_id}`" style="text-decoration: none; color: inherit;">
|
|
||||||
<img :src="player?.player_headshot" alt="Player Headshot" class="headshot" />
|
|
||||||
<h2 class="name">{{ player?.player_name }}</h2>
|
|
||||||
<p class="franchise">{{ player?.franchise_name }}</p>
|
|
||||||
|
|
||||||
<img :src="player?.player_card_image" alt="Player Card" class="card-image" />
|
|
||||||
<img v-if="player?.player_card_image2" :src="player?.player_card_image2" alt="Player Card 2" class="card-image" />
|
|
||||||
|
|
||||||
<div class="rarity" :style="{ backgroundColor: '#' + player.rarity_color }">
|
|
||||||
{{ player.rarity_name }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="cardset">{{ player?.cardset_name }}</p>
|
|
||||||
<p class="cost">Cost: {{ player?.player_cost }}₼</p>
|
|
||||||
</NuxtLink>
|
|
||||||
</article>
|
|
||||||
</section>
|
</section>
|
||||||
</div>
|
|
||||||
|
<!-- Player List -->
|
||||||
|
<section class="player-list">
|
||||||
|
<div v-if="loading">
|
||||||
|
<article v-for="n in 6" :key="n" aria-busy="true">
|
||||||
|
<header><h2>Loading...</h2></header>
|
||||||
|
<p>Loading player data...</p>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<article
|
||||||
|
v-for="player in players"
|
||||||
|
:key="player.player_id"
|
||||||
|
class="player-card"
|
||||||
|
@click="toggleExpanded(player.player_id)"
|
||||||
|
:aria-expanded="expandedPlayerId === player.player_id"
|
||||||
|
>
|
||||||
|
<header class="clickable-header">
|
||||||
|
<h2 class="player-name">{{ player.player_name }}</h2>
|
||||||
|
<div class="summary-info">
|
||||||
|
<small>
|
||||||
|
{{ player.cardset_name }} • {{ player.franchise_name }} •
|
||||||
|
<span class="rarity-badge" :style="{ backgroundColor: '#' + player.rarity_color }">
|
||||||
|
{{ player.rarity_name }}
|
||||||
|
</span> • {{ player.player_cost }}₼
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<transition name="fade">
|
||||||
|
<div v-if="expandedPlayerId === player.player_id" class="expanded-content">
|
||||||
|
<img :src="player.player_headshot" alt="Player Headshot" class="headshot" />
|
||||||
|
|
||||||
|
<div class="strat-card-container">
|
||||||
|
<img :src="player.player_card_image" alt="Card Image" class="strat-card" />
|
||||||
|
<img v-if="player.player_card_image2" :src="player.player_card_image2" alt="Card Image 2" class="strat-card" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Pagination -->
|
||||||
|
<nav class="pagination-controls">
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<button @click="previousPage" :disabled="page === 1">Previous</button>
|
||||||
|
</li>
|
||||||
|
<li>Page {{ page }}</li>
|
||||||
|
<li>
|
||||||
|
<button @click="nextPage">Next</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script setup lang="ts">
|
||||||
import { useMarketplaceStore } from '~/stores/marketplace'
|
import { ref, onMounted, watch, computed } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { useMarketplaceStore } from '@/stores/marketplace'
|
||||||
import type { MarketplacePlayer } from '~/types/MarketplacePlayer'
|
import type { MarketplacePlayer } from '~/types/MarketplacePlayer'
|
||||||
|
|
||||||
const marketplace = useMarketplaceStore()
|
|
||||||
const client = useSupabaseClient()
|
|
||||||
const router = useRouter()
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const client = useSupabaseClient()
|
||||||
|
const marketplace = useMarketplaceStore()
|
||||||
|
|
||||||
|
const players = ref<MarketplacePlayer[]>([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const pageSize = ref(20)
|
||||||
|
const page = ref(1)
|
||||||
|
const expandedPlayerId = ref<number | null>(null)
|
||||||
const showFilters = ref(false)
|
const showFilters = ref(false)
|
||||||
// Track the current filters from the URL query
|
|
||||||
const currentQuery = ref(route.query)
|
|
||||||
|
|
||||||
const toggleFilters = () => {
|
// Example cardset list
|
||||||
showFilters.value = !showFilters.value
|
const cardsetNames = [
|
||||||
|
'2021 Season', '2022 Season', '2023 Season', 'Mario Super Sluggers', 'Backyard Baseball'
|
||||||
|
]
|
||||||
|
|
||||||
|
// Computed
|
||||||
|
const hasActiveFilters = computed(() => {
|
||||||
|
return (
|
||||||
|
marketplace.minCost !== null ||
|
||||||
|
marketplace.maxCost !== null ||
|
||||||
|
marketplace.rarities.length > 0 ||
|
||||||
|
marketplace.cardset.length > 0
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const activeFilterCount = computed(() => {
|
||||||
|
let count = 0
|
||||||
|
if (marketplace.minCost !== null) count++
|
||||||
|
if (marketplace.maxCost !== null) count++
|
||||||
|
count += marketplace.rarities.length
|
||||||
|
count += marketplace.cardset.length
|
||||||
|
return count
|
||||||
|
})
|
||||||
|
|
||||||
|
const fetchPlayers = async () => {
|
||||||
|
loading.value = true
|
||||||
|
const from = (page.value - 1) * pageSize.value
|
||||||
|
const to = from + pageSize.value - 1
|
||||||
|
|
||||||
|
let query = client.from('marketplace').select('*').range(from, to)
|
||||||
|
|
||||||
|
if (marketplace.minCost !== null && marketplace.minCost > 0) {
|
||||||
|
query = query.gte('player_cost', marketplace.minCost)
|
||||||
|
}
|
||||||
|
if (marketplace.maxCost !== null && marketplace.maxCost > 0) {
|
||||||
|
query = query.lte('player_cost', marketplace.maxCost)
|
||||||
|
}
|
||||||
|
if (marketplace.rarities?.length > 0) {
|
||||||
|
query = query.in('rarity_name', marketplace.rarities)
|
||||||
|
}
|
||||||
|
if (marketplace.cardset?.length > 0) {
|
||||||
|
query = query.in('cardset_name', marketplace.cardset)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data, error } = await query
|
||||||
|
if (error) console.error('Supabase error:', error)
|
||||||
|
|
||||||
|
loading.value = false
|
||||||
|
players.value = data || []
|
||||||
}
|
}
|
||||||
|
|
||||||
const applyFilters = async () => {
|
const toggleExpanded = (playerId: number) => {
|
||||||
console.log('Applying filters:', marketplace.filters)
|
expandedPlayerId.value = expandedPlayerId.value === playerId ? null : playerId
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeRarity = (rarity: string) => {
|
||||||
|
marketplace.rarities = marketplace.rarities.filter(r => r !== rarity)
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeCardset = (cardset: string) => {
|
||||||
|
marketplace.cardset = marketplace.cardset.filter(c => c !== cardset)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateRoute = () => {
|
||||||
router.replace({
|
router.replace({
|
||||||
query: {
|
query: {
|
||||||
minCost: marketplace.filters.minCost || undefined,
|
page: page.value.toString(),
|
||||||
maxCost: marketplace.filters.maxCost || undefined,
|
...(marketplace.minCost != null && { minCost: marketplace.minCost }),
|
||||||
rarities: marketplace.filters.rarities.length > 0 ? marketplace.filters.rarities : undefined
|
...(marketplace.maxCost != null && { maxCost: marketplace.maxCost }),
|
||||||
|
...(marketplace.rarities.length && { rarity: marketplace.rarities }),
|
||||||
|
...(marketplace.cardset.length && { cardset: marketplace.cardset }),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchPlayers = async () => {
|
const previousPage = () => {
|
||||||
let query = client.from('marketplace')
|
if (page.value > 1) {
|
||||||
.select('*')
|
page.value--
|
||||||
.limit(20)
|
updateRoute()
|
||||||
|
fetchPlayers()
|
||||||
// Apply filter conditions based on the store values
|
|
||||||
if (marketplace.filters.minCost > 0) {
|
|
||||||
query = query.gte('player_cost', marketplace.filters.minCost)
|
|
||||||
}
|
}
|
||||||
if (marketplace.filters.maxCost > 0) {
|
|
||||||
query = query.lte('player_cost', marketplace.filters.maxCost)
|
|
||||||
}
|
|
||||||
if (marketplace.filters.rarities.length > 0) {
|
|
||||||
query = query.in('rarity_name', marketplace.filters.rarities)
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data, error } = await query
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('Supabase error: ', error)
|
|
||||||
}
|
|
||||||
|
|
||||||
return data
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// For useAsyncData, access filters via the store
|
const nextPage = () => {
|
||||||
const { data: playerList, pending, error, refresh } = await useAsyncData<MarketplacePlayer[] | null>(
|
page.value++
|
||||||
'player-list', // Use a static key
|
updateRoute()
|
||||||
fetchPlayers, // Fetch players with current filters
|
fetchPlayers()
|
||||||
{
|
}
|
||||||
// Only refetch when filters change
|
|
||||||
lazy: true,
|
|
||||||
server: false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(async () => {
|
||||||
const { minCost, maxCost, rarities } = route.query
|
const { getNumber, getArray } = useSafeQuery()
|
||||||
|
|
||||||
|
page.value = getNumber('page') || 1
|
||||||
|
|
||||||
marketplace.setFilters({
|
marketplace.setFilters({
|
||||||
minCost: minCost ? Number(minCost) : 0,
|
minCost: getNumber('minCost'),
|
||||||
maxCost: maxCost ? Number(maxCost) : 0,
|
maxCost: getNumber('maxCost'),
|
||||||
// rarities: rarities
|
rarities: getArray('rarities'),
|
||||||
// ? Array.isArray(rarities) ? rarities : [rarities]
|
cardset: getArray('cardset'),
|
||||||
// : []
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
await fetchPlayers()
|
||||||
})
|
})
|
||||||
|
|
||||||
// Watch for changes to the query params and refetch
|
|
||||||
watch(() => route.query, async (newQuery) => {
|
|
||||||
// Update filters based on query params
|
|
||||||
if (JSON.stringify(newQuery) !== JSON.stringify(currentQuery.value)){
|
|
||||||
currentQuery.value = newQuery // Update the tracked query
|
|
||||||
|
|
||||||
marketplace.setFilters({
|
|
||||||
minCost: newQuery.minCost ? Number(newQuery.minCost) : 0,
|
|
||||||
maxCost: newQuery.maxCost ? Number(newQuery.maxCost) : 0,
|
|
||||||
rarities: newQuery.rarities ? (Array.isArray(newQuery.rarities) ? newQuery.rarities : [newQuery.rarities]) : []
|
|
||||||
})
|
|
||||||
|
|
||||||
// Trigger refetch
|
|
||||||
await refresh()
|
|
||||||
}
|
|
||||||
}, { immediate: true }) // Set immediate to true to run on initial load
|
|
||||||
|
|
||||||
|
watch(
|
||||||
|
[() => marketplace.minCost, () => marketplace.maxCost, () => marketplace.rarities, () => marketplace.cardset],
|
||||||
|
() => {
|
||||||
|
page.value = 1
|
||||||
|
updateRoute()
|
||||||
|
fetchPlayers()
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.player-card {
|
.marketplace-layout {
|
||||||
max-width: 400px;
|
display: flex;
|
||||||
background: white;
|
flex-direction: column;
|
||||||
border-radius: 12px;
|
min-height: 100vh; /* Full height of screen */
|
||||||
overflow: hidden;
|
}
|
||||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
|
||||||
padding: 16px;
|
.filters-section {
|
||||||
|
padding-left: 1rem;
|
||||||
|
padding-right: 1rem;
|
||||||
|
padding-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-list {
|
||||||
|
flex: 1 1 auto; /* Take up the leftover space */
|
||||||
|
overflow-y: auto; /* Scroll if too many players */
|
||||||
|
padding-left: 1rem;
|
||||||
|
padding-right: 1rem;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-controls {
|
||||||
|
padding: 1rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.headshot {
|
.grid {
|
||||||
width: 100px;
|
display: grid;
|
||||||
height: 100px;
|
gap: 1rem;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters-toggle summary {
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-chips {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
background: var(--pico-primary-background);
|
||||||
|
color: var(--pico-primary-contrast);
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: var(--pico-border-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip button {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: inherit;
|
||||||
|
margin-left: 0.25rem;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-card {
|
||||||
|
padding: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
border: 1px solid var(--pico-border-color);
|
||||||
|
border-radius: var(--pico-border-radius);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.3s;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-card .clickable-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap; /* Allow wrapping of content if it exceeds screen width */
|
||||||
|
gap: 0.5rem; /* Space between name and summary info */
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-card .player-name {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-card .summary-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-card .summary-info small {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--pico-muted-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-card:hover {
|
||||||
|
background: var(--pico-muted-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rarity-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.2rem 0.5rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clickable-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expanded-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strat-card-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1rem;
|
||||||
|
flex-wrap: wrap; /* Wrap on small screens */
|
||||||
|
}
|
||||||
|
|
||||||
|
img.strat-card {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
img.headshot {
|
||||||
|
min-width: 80px;
|
||||||
|
max-width: 120px;
|
||||||
|
width: 20%;
|
||||||
|
aspect-ratio: 1/1; /* Force it to be a perfect square */
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
border: 3px solid white;
|
||||||
|
background: white;
|
||||||
margin: 0 auto 8px;
|
margin: 0 auto 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.name {
|
.fade-enter-active,
|
||||||
font-size: 1.5rem;
|
.fade-leave-active {
|
||||||
margin: 0;
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
.fade-enter-from,
|
||||||
.franchise {
|
.fade-leave-to {
|
||||||
color: #666;
|
opacity: 0;
|
||||||
font-size: 1rem;
|
transform: translateY(-10px);
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-image {
|
|
||||||
width: 100%;
|
|
||||||
height: auto;
|
|
||||||
margin: 12px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rarity {
|
|
||||||
color: white;
|
|
||||||
padding: 8px;
|
|
||||||
border-radius: 8px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cardset {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: #555;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cost {
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
<p class="description">Description: {{ player?.description }}</p>
|
<p class="description">Description: {{ player?.description }}</p>
|
||||||
|
|
||||||
<p class="cost">Cost: ${{ player?.cost }}</p>
|
<p class="cost">Cost: {{ player?.cost }}₼</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -29,8 +29,8 @@ const props = defineProps<{
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.player-card {
|
.player-card {
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
background: white;
|
background: var(--pico-card-background-color);
|
||||||
border-radius: 12px;
|
border-radius: var(--pico-border-radius);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
|
|||||||
37
composables/useSafeQuery.ts
Normal file
37
composables/useSafeQuery.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
// composables/useSafeQuery.ts
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
|
||||||
|
export const useSafeQuery = () => {
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
const getNumber = (key: string): number | undefined => {
|
||||||
|
const value = route.query[key]
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const parsed = Number(value)
|
||||||
|
return isNaN(parsed) ? undefined : parsed
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const getArray = (key: string): string[] => {
|
||||||
|
const value = route.query[key]
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.map(String)
|
||||||
|
}
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return [value]
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const getString = (key: string): string | undefined => {
|
||||||
|
const value = route.query[key]
|
||||||
|
return typeof value === 'string' ? value : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
getNumber,
|
||||||
|
getArray,
|
||||||
|
getString,
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,19 +1,5 @@
|
|||||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
compatibilityDate: '2024-11-01',
|
|
||||||
|
|
||||||
devtools: {
|
|
||||||
enabled: true,
|
|
||||||
|
|
||||||
timeline: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
modules: ['@nuxtjs/supabase', '@pinia/nuxt'],
|
|
||||||
|
|
||||||
css: ['@picocss/pico'],
|
|
||||||
|
|
||||||
app: {
|
app: {
|
||||||
head: {
|
head: {
|
||||||
title: 'Paper Dynasty',
|
title: 'Paper Dynasty',
|
||||||
@ -26,13 +12,36 @@ export default defineNuxtConfig({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
compatibilityDate: '2024-11-01',
|
||||||
|
|
||||||
|
css: [
|
||||||
|
'@picocss/pico',
|
||||||
|
'/assets/css/styles.css'
|
||||||
|
],
|
||||||
|
|
||||||
|
devtools: {
|
||||||
|
enabled: true,
|
||||||
|
|
||||||
|
timeline: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
imports: {
|
||||||
|
dirs: [
|
||||||
|
'utils',
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
modules: ['@nuxtjs/supabase', '@pinia/nuxt'],
|
||||||
|
|
||||||
runtimeConfig: {
|
runtimeConfig: {
|
||||||
public: {
|
public: {
|
||||||
supabaseKey: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImNucGhwbnV2aGp2cXprY2J3emRrIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDU4MTE3ODQsImV4cCI6MjA2MTM4Nzc4NH0.k3V9c2oiG8kufPa3_a4v6UdiGI6ML6-5lH2oifStB3I',
|
supabaseKey: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImNucGhwbnV2aGp2cXprY2J3emRrIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDU4MTE3ODQsImV4cCI6MjA2MTM4Nzc4NH0.k3V9c2oiG8kufPa3_a4v6UdiGI6ML6-5lH2oifStB3I',
|
||||||
supabaseUrl: 'https://cnphpnuvhjvqzkcbwzdk.supabase.co',
|
supabaseUrl: 'https://cnphpnuvhjvqzkcbwzdk.supabase.co',
|
||||||
supabaseRestURL: 'https://cnphpnuvhjvqzkcbwzdk.supabase.co/rest/v1',
|
supabaseRestURL: 'https://cnphpnuvhjvqzkcbwzdk.supabase.co/rest/v1',
|
||||||
// webpageBaseUrl: 'http://localhost:3000',
|
webpageBaseUrl: 'http://localhost:3000',
|
||||||
webpageBaseUrl: 'https://paper-dynasty.netlify.app',
|
// webpageBaseUrl: 'https://paper-dynasty.netlify.app',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@ -1,14 +1,24 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
export const useMarketplaceStore = defineStore('marketplace', {
|
export const useMarketplaceStore = defineStore('marketplace', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
filters: {
|
minCost: null as number | null,
|
||||||
minCost: 0,
|
maxCost: null as number | null,
|
||||||
maxCost: 0,
|
rarities: [] as string[],
|
||||||
rarities: [] as string[],
|
cardset: [] as string[],
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
setFilters(newFilters: Partial<typeof this.filters>) {
|
setFilters(filters: Partial<{ minCost: number, maxCost: number, rarities: string[], cardset: string[] }>) {
|
||||||
this.filters = { ...this.filters, ...newFilters }
|
if (filters.minCost !== undefined) this.minCost = filters.minCost
|
||||||
|
if (filters.maxCost !== undefined) this.maxCost = filters.maxCost
|
||||||
|
if (filters.rarities !== undefined) this.rarities = filters.rarities
|
||||||
|
if (filters.cardset !== undefined) this.cardset = filters.cardset
|
||||||
},
|
},
|
||||||
},
|
clearFilters() {
|
||||||
})
|
this.minCost = null
|
||||||
|
this.maxCost = null
|
||||||
|
this.rarities = []
|
||||||
|
this.cardset = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|||||||
20
utils/playerMapper.ts
Normal file
20
utils/playerMapper.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import type { MarketplacePlayer } from "~/types/MarketplacePlayer";
|
||||||
|
import type { Player } from "~/types/Player";
|
||||||
|
|
||||||
|
export function mapMarketplacePlayersToPlayers(marketplacePlayers: MarketplacePlayer[]): Player[] {
|
||||||
|
return marketplacePlayers.map(mp => ({
|
||||||
|
id: mp.player_id,
|
||||||
|
name: mp.player_name,
|
||||||
|
cost: mp.player_cost,
|
||||||
|
image: mp.player_card_image,
|
||||||
|
headshot: mp.player_headshot,
|
||||||
|
description: '',
|
||||||
|
franchise: mp.franchise_name,
|
||||||
|
rarity: {
|
||||||
|
id: 0,
|
||||||
|
name: mp.rarity_name,
|
||||||
|
color: mp.rarity_color,
|
||||||
|
value: 0,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user