improving the table layout
This commit is contained in:
@@ -20,7 +20,8 @@
|
|||||||
<FilterBar :filters="filters" :result-count="profitResults.length" @set-name-filter="setNameFilter"
|
<FilterBar :filters="filters" :result-count="profitResults.length" @set-name-filter="setNameFilter"
|
||||||
@toggle-tier="toggleTier" @set-selected-item-types="setSelectedItemTypes"
|
@toggle-tier="toggleTier" @set-selected-item-types="setSelectedItemTypes"
|
||||||
@toggle-enchantment="toggleEnchantment" @reset-enchantments="resetEnchantments"
|
@toggle-enchantment="toggleEnchantment" @reset-enchantments="resetEnchantments"
|
||||||
@toggle-variant="toggleVariant" @reset-variants="resetVariants" />
|
@toggle-variant="toggleVariant" @reset-variants="resetVariants"
|
||||||
|
@toggle-station="toggleStation" @reset-stations="resetStations" />
|
||||||
|
|
||||||
<ProfitTable :results="profitResults" :sort-state="sortState" @sort="handleSort" />
|
<ProfitTable :results="profitResults" :sort-state="sortState" @sort="handleSort" />
|
||||||
</div>
|
</div>
|
||||||
@@ -73,6 +74,8 @@ const {
|
|||||||
resetEnchantments,
|
resetEnchantments,
|
||||||
toggleVariant,
|
toggleVariant,
|
||||||
resetVariants,
|
resetVariants,
|
||||||
|
toggleStation,
|
||||||
|
resetStations,
|
||||||
} = useFilters()
|
} = useFilters()
|
||||||
|
|
||||||
// Profit calculation
|
// Profit calculation
|
||||||
|
|||||||
@@ -131,6 +131,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Station toggles -->
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<span class="text-xs text-gray-400">Station</span>
|
||||||
|
<div class="flex gap-0.5">
|
||||||
|
<button class="px-2 py-1 rounded text-xs font-semibold transition-colors" :class="filters.stations === null
|
||||||
|
? 'bg-amber-600 text-amber-100'
|
||||||
|
: 'bg-gray-800 text-gray-500 hover:bg-gray-700'" @click="$emit('reset-stations')">All</button>
|
||||||
|
<button v-for="s in ALL_STATIONS" :key="s"
|
||||||
|
class="px-2 py-1 rounded text-xs font-semibold transition-colors"
|
||||||
|
:class="isStationActive(s) ? STATION_ACTIVE_CLASS[s] : 'bg-gray-800 text-gray-600 hover:bg-gray-700'"
|
||||||
|
@click="$emit('toggle-station', s)">{{ STATION_LABEL[s] }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Result count -->
|
<!-- Result count -->
|
||||||
<span class="ml-auto text-xs text-gray-500 whitespace-nowrap">{{ resultCount }} results</span>
|
<span class="ml-auto text-xs text-gray-500 whitespace-nowrap">{{ resultCount }} results</span>
|
||||||
|
|
||||||
@@ -142,8 +156,8 @@
|
|||||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||||
import { tierStyle, enchantStyle } from '../../utils/formatting'
|
import { tierStyle, enchantStyle } from '../../utils/formatting'
|
||||||
import type { FilterState, VariantType } from '../../types/filters'
|
import type { FilterState, VariantType } from '../../types/filters'
|
||||||
import { ALL_VARIANTS } from '../../types/filters'
|
import { ALL_VARIANTS, ALL_STATIONS } from '../../types/filters'
|
||||||
import type { Tier, Enchantment } from '../../types/crafting'
|
import type { Tier, Enchantment, JournalType } from '../../types/crafting'
|
||||||
import { TIERS, ENCHANTMENTS } from '../../data/constants'
|
import { TIERS, ENCHANTMENTS } from '../../data/constants'
|
||||||
import { ITEM_TREE, ALL_ITEM_NAMES, getLeaves } from '../../data/itemTree'
|
import { ITEM_TREE, ALL_ITEM_NAMES, getLeaves } from '../../data/itemTree'
|
||||||
import type { TreeNode } from '../../data/itemTree'
|
import type { TreeNode } from '../../data/itemTree'
|
||||||
@@ -161,6 +175,8 @@ const emit = defineEmits<{
|
|||||||
'reset-enchantments': []
|
'reset-enchantments': []
|
||||||
'toggle-variant': [v: VariantType]
|
'toggle-variant': [v: VariantType]
|
||||||
'reset-variants': []
|
'reset-variants': []
|
||||||
|
'toggle-station': [s: JournalType]
|
||||||
|
'reset-stations': []
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
// ─── Category tree ────────────────────────────────────────────────────────────
|
// ─── Category tree ────────────────────────────────────────────────────────────
|
||||||
@@ -287,6 +303,23 @@ function isVariantActive(v: VariantType): boolean {
|
|||||||
return s === null || s.has(v)
|
return s === null || s.has(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Station helpers ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const STATION_LABEL: Record<JournalType, string> = {
|
||||||
|
warrior: 'Warrior', hunter: 'Hunter', mage: 'Mage', toolmaker: 'Toolmaker',
|
||||||
|
}
|
||||||
|
|
||||||
|
const STATION_ACTIVE_CLASS: Record<JournalType, string> = {
|
||||||
|
warrior: 'bg-red-900/60 text-red-300',
|
||||||
|
hunter: 'bg-green-900/60 text-green-300',
|
||||||
|
mage: 'bg-blue-900/60 text-blue-300',
|
||||||
|
toolmaker: 'bg-orange-900/60 text-orange-300',
|
||||||
|
}
|
||||||
|
|
||||||
|
function isStationActive(s: JournalType): boolean {
|
||||||
|
return props.filters.stations === null || props.filters.stations.has(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ─── Dropdown open states ─────────────────────────────────────────────────────
|
// ─── Dropdown open states ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -26,10 +26,17 @@
|
|||||||
class="w-12 h-12 -my-2 rounded flex-shrink-0"
|
class="w-12 h-12 -my-2 rounded flex-shrink-0"
|
||||||
/>
|
/>
|
||||||
<span class="text-sm font-medium text-gray-200">{{ result.recipe.displayName.replace(/^T\d+\.\d+\s/, '') }}</span>
|
<span class="text-sm font-medium text-gray-200">{{ result.recipe.displayName.replace(/^T\d+\.\d+\s/, '') }}</span>
|
||||||
<span class="text-xs text-gray-500">{{ result.recipe.category }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
<!-- Station badge -->
|
||||||
|
<td class="px-4 py-3">
|
||||||
|
<span class="inline-flex items-center justify-center px-2 h-6 rounded text-xs font-semibold whitespace-nowrap"
|
||||||
|
:class="STATION[result.recipe.journalType].cls">
|
||||||
|
{{ STATION[result.recipe.journalType].label }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
<!-- Tier badge -->
|
<!-- Tier badge -->
|
||||||
<td class="px-4 py-3">
|
<td class="px-4 py-3">
|
||||||
<span
|
<span
|
||||||
@@ -156,7 +163,7 @@
|
|||||||
|
|
||||||
<!-- Expanded detail row -->
|
<!-- Expanded detail row -->
|
||||||
<tr v-if="expanded" class="border-b border-gray-700/50 bg-gray-900/60">
|
<tr v-if="expanded" class="border-b border-gray-700/50 bg-gray-900/60">
|
||||||
<td colspan="13" class="px-6 py-4">
|
<td colspan="14" class="px-6 py-4">
|
||||||
<div class="flex gap-8">
|
<div class="flex gap-8">
|
||||||
|
|
||||||
<!-- Ingredients breakdown -->
|
<!-- Ingredients breakdown -->
|
||||||
@@ -251,7 +258,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import type { ProfitResult, Tier } from '../../types/crafting'
|
import type { ProfitResult, Tier, JournalType } from '../../types/crafting'
|
||||||
import { formatSilver, formatLastUpdated, tierEnchantStyle, itemImageUrl } from '../../utils/formatting'
|
import { formatSilver, formatLastUpdated, tierEnchantStyle, itemImageUrl } from '../../utils/formatting'
|
||||||
import { useAlbionPrices } from '../../composables/useAlbionPrices'
|
import { useAlbionPrices } from '../../composables/useAlbionPrices'
|
||||||
import { useFilters } from '../../composables/useFilters'
|
import { useFilters } from '../../composables/useFilters'
|
||||||
@@ -267,6 +274,13 @@ const { getManualEntry, setManualPrice, clearManualPrice } = useAlbionPrices()
|
|||||||
const { filters } = useFilters()
|
const { filters } = useFilters()
|
||||||
const { upsert, remove, getQty, inOrder } = useProductionOrder()
|
const { upsert, remove, getQty, inOrder } = useProductionOrder()
|
||||||
|
|
||||||
|
const STATION: Record<JournalType, { label: string; cls: string }> = {
|
||||||
|
warrior: { label: 'Warrior', cls: 'bg-red-900/30 border border-red-700/40 text-red-400' },
|
||||||
|
hunter: { label: 'Hunter', cls: 'bg-green-900/30 border border-green-700/40 text-green-400' },
|
||||||
|
mage: { label: 'Mage', cls: 'bg-blue-900/30 border border-blue-700/40 text-blue-400' },
|
||||||
|
toolmaker: { label: 'Toolmaker', cls: 'bg-orange-900/30 border border-orange-700/40 text-orange-400' },
|
||||||
|
}
|
||||||
|
|
||||||
const VARIANT_INFO: Record<string, { label: string; cls: string }> = {
|
const VARIANT_INFO: Record<string, { label: string; cls: string }> = {
|
||||||
avalon: { label: 'Avalon', cls: 'bg-violet-600/30 text-violet-300 border border-violet-600/40' },
|
avalon: { label: 'Avalon', cls: 'bg-violet-600/30 text-violet-300 border border-violet-600/40' },
|
||||||
crystal: { label: 'Crystal', cls: 'bg-cyan-600/30 text-cyan-300 border border-cyan-600/40' },
|
crystal: { label: 'Crystal', cls: 'bg-cyan-600/30 text-cyan-300 border border-cyan-600/40' },
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<thead class="sticky top-0 z-10">
|
<thead class="sticky top-0 z-10">
|
||||||
<!-- Group row -->
|
<!-- Group row -->
|
||||||
<tr class="bg-gray-800 border-b border-gray-700/50">
|
<tr class="bg-gray-800 border-b border-gray-700/50">
|
||||||
<th colspan="4" class="bg-gray-800" />
|
<th colspan="5" class="bg-gray-800" />
|
||||||
<th colspan="4" class="px-4 py-1.5 text-center text-[10px] font-semibold text-gray-500 uppercase tracking-wider border-l border-gray-700/60">
|
<th colspan="4" class="px-4 py-1.5 text-center text-[10px] font-semibold text-gray-500 uppercase tracking-wider border-l border-gray-700/60">
|
||||||
No Focus
|
No Focus
|
||||||
</th>
|
</th>
|
||||||
@@ -68,6 +68,7 @@ import type { SortField, SortState } from '../../types/crafting'
|
|||||||
const leftCols = [
|
const leftCols = [
|
||||||
{ field: 'variantType' as SortField, label: 'Variant', sortable: true },
|
{ field: 'variantType' as SortField, label: 'Variant', sortable: true },
|
||||||
{ field: 'displayName' as SortField, label: 'Item', sortable: true },
|
{ field: 'displayName' as SortField, label: 'Item', sortable: true },
|
||||||
|
{ field: 'station' as SortField, label: 'Station', sortable: true },
|
||||||
{ field: 'tier' as SortField, label: 'Tier', sortable: true },
|
{ field: 'tier' as SortField, label: 'Tier', sortable: true },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ export function useCraftingProfit(
|
|||||||
if (!f.tiers.has(recipe.tier)) continue
|
if (!f.tiers.has(recipe.tier)) continue
|
||||||
if (f.enchantments !== null && !f.enchantments.has(recipe.enchantment)) continue
|
if (f.enchantments !== null && !f.enchantments.has(recipe.enchantment)) continue
|
||||||
if (f.variants !== null && !f.variants.has(variantOf(recipe.outputItemId))) continue
|
if (f.variants !== null && !f.variants.has(variantOf(recipe.outputItemId))) continue
|
||||||
|
if (f.stations !== null && !f.stations.has(recipe.journalType)) continue
|
||||||
const baseName = recipe.displayName.replace(/^T\d+\.\d /, '')
|
const baseName = recipe.displayName.replace(/^T\d+\.\d /, '')
|
||||||
if (f.selectedItemTypes !== null && !f.selectedItemTypes.has(baseName)) continue
|
if (f.selectedItemTypes !== null && !f.selectedItemTypes.has(baseName)) continue
|
||||||
if (nameLower && !recipe.displayName.toLowerCase().includes(nameLower)) continue
|
if (nameLower && !recipe.displayName.toLowerCase().includes(nameLower)) continue
|
||||||
@@ -127,6 +128,10 @@ export function useCraftingProfit(
|
|||||||
aVal = variantRank(a.recipe.outputItemId)
|
aVal = variantRank(a.recipe.outputItemId)
|
||||||
bVal = variantRank(b.recipe.outputItemId)
|
bVal = variantRank(b.recipe.outputItemId)
|
||||||
break
|
break
|
||||||
|
case 'station':
|
||||||
|
aVal = a.recipe.journalType
|
||||||
|
bVal = b.recipe.journalType
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
aVal = a.effectiveMaterialCost
|
aVal = a.effectiveMaterialCost
|
||||||
bVal = b.effectiveMaterialCost
|
bVal = b.effectiveMaterialCost
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
import type { FilterState, VariantType } from '../types/filters'
|
import type { FilterState, VariantType } from '../types/filters'
|
||||||
import { ALL_VARIANTS } from '../types/filters'
|
import { ALL_VARIANTS, ALL_STATIONS } from '../types/filters'
|
||||||
import type { Tier, Enchantment } from '../types/crafting'
|
import type { Tier, Enchantment, JournalType } from '../types/crafting'
|
||||||
import type { AlbionCity } from '../types/api'
|
import type { AlbionCity } from '../types/api'
|
||||||
import { ENCHANTMENTS, TIERS } from '../data/constants'
|
import { ENCHANTMENTS, TIERS } from '../data/constants'
|
||||||
|
|
||||||
@@ -14,6 +14,7 @@ interface StoredFilters {
|
|||||||
tiers?: Tier[]
|
tiers?: Tier[]
|
||||||
enchantments?: Enchantment[] | null
|
enchantments?: Enchantment[] | null
|
||||||
variants?: VariantType[] | null
|
variants?: VariantType[] | null
|
||||||
|
stations?: JournalType[] | null
|
||||||
selectedItemTypes?: string[] | null
|
selectedItemTypes?: string[] | null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,6 +34,7 @@ function buildInitialState(): FilterState {
|
|||||||
tiers: s.tiers ? new Set(s.tiers) : new Set(TIERS),
|
tiers: s.tiers ? new Set(s.tiers) : new Set(TIERS),
|
||||||
enchantments: s.enchantments === undefined ? null : (s.enchantments === null ? null : new Set(s.enchantments)),
|
enchantments: s.enchantments === undefined ? null : (s.enchantments === null ? null : new Set(s.enchantments)),
|
||||||
variants: s.variants === undefined ? null : (s.variants === null ? null : new Set(s.variants)),
|
variants: s.variants === undefined ? null : (s.variants === null ? null : new Set(s.variants)),
|
||||||
|
stations: s.stations === undefined ? null : (s.stations === null ? null : new Set(s.stations)),
|
||||||
selectedItemTypes: s.selectedItemTypes === undefined ? null : (s.selectedItemTypes === null ? null : new Set(s.selectedItemTypes)),
|
selectedItemTypes: s.selectedItemTypes === undefined ? null : (s.selectedItemTypes === null ? null : new Set(s.selectedItemTypes)),
|
||||||
nameFilter: '',
|
nameFilter: '',
|
||||||
}
|
}
|
||||||
@@ -49,6 +51,7 @@ watch(filters, () => {
|
|||||||
tiers: [...f.tiers],
|
tiers: [...f.tiers],
|
||||||
enchantments: f.enchantments === null ? null : [...f.enchantments],
|
enchantments: f.enchantments === null ? null : [...f.enchantments],
|
||||||
variants: f.variants === null ? null : [...f.variants],
|
variants: f.variants === null ? null : [...f.variants],
|
||||||
|
stations: f.stations === null ? null : [...f.stations],
|
||||||
selectedItemTypes: f.selectedItemTypes === null ? null : [...f.selectedItemTypes],
|
selectedItemTypes: f.selectedItemTypes === null ? null : [...f.selectedItemTypes],
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -110,6 +113,21 @@ function resetVariants() {
|
|||||||
filters.value.variants = null
|
filters.value.variants = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleStation(station: JournalType) {
|
||||||
|
const current = filters.value.stations
|
||||||
|
const next = current === null ? new Set(ALL_STATIONS) : new Set(current)
|
||||||
|
if (next.has(station)) {
|
||||||
|
next.delete(station)
|
||||||
|
} else {
|
||||||
|
next.add(station)
|
||||||
|
}
|
||||||
|
filters.value.stations = next.size === ALL_STATIONS.length ? null : next
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetStations() {
|
||||||
|
filters.value.stations = null
|
||||||
|
}
|
||||||
|
|
||||||
export function useFilters() {
|
export function useFilters() {
|
||||||
return {
|
return {
|
||||||
filters,
|
filters,
|
||||||
@@ -121,5 +139,7 @@ export function useFilters() {
|
|||||||
resetEnchantments,
|
resetEnchantments,
|
||||||
toggleVariant,
|
toggleVariant,
|
||||||
resetVariants,
|
resetVariants,
|
||||||
|
toggleStation,
|
||||||
|
resetStations,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export type JournalType = 'warrior' | 'mage' | 'hunter' | 'toolmaker'
|
|||||||
|
|
||||||
export type FameType = 'twoHanded' | 'oneHanded' | 'armorChest' | 'small'
|
export type FameType = 'twoHanded' | 'oneHanded' | 'armorChest' | 'small'
|
||||||
|
|
||||||
export type SortField = 'materialCost' | 'displayName' | 'tier' | 'variantType'
|
export type SortField = 'materialCost' | 'displayName' | 'tier' | 'variantType' | 'station'
|
||||||
|
|
||||||
export type SortDirection = 'asc' | 'desc'
|
export type SortDirection = 'asc' | 'desc'
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import type { AlbionCity } from './api'
|
import type { AlbionCity } from './api'
|
||||||
import type { Tier, Enchantment } from './crafting'
|
import type { Tier, Enchantment, JournalType } from './crafting'
|
||||||
|
|
||||||
export type VariantType = 'basic' | 'artifact' | 'avalon' | 'crystal'
|
export type VariantType = 'basic' | 'artifact' | 'avalon' | 'crystal'
|
||||||
|
|
||||||
export const ALL_VARIANTS: VariantType[] = ['basic', 'artifact', 'avalon', 'crystal']
|
export const ALL_VARIANTS: VariantType[] = ['basic', 'artifact', 'avalon', 'crystal']
|
||||||
|
|
||||||
|
export const ALL_STATIONS: JournalType[] = ['warrior', 'hunter', 'mage', 'toolmaker']
|
||||||
|
|
||||||
export interface FilterState {
|
export interface FilterState {
|
||||||
city: AlbionCity
|
city: AlbionCity
|
||||||
tiers: Set<Tier>
|
tiers: Set<Tier>
|
||||||
@@ -12,4 +14,5 @@ export interface FilterState {
|
|||||||
nameFilter: string
|
nameFilter: string
|
||||||
enchantments: Set<Enchantment> | null
|
enchantments: Set<Enchantment> | null
|
||||||
variants: Set<VariantType> | null
|
variants: Set<VariantType> | null
|
||||||
|
stations: Set<JournalType> | null
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user