improving the table layout

This commit is contained in:
2026-03-05 22:56:24 -05:00
parent f910d07b48
commit 010a92999a
8 changed files with 90 additions and 11 deletions

View File

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

View File

@@ -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 ─────────────────────────────────────────────────────

View File

@@ -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' },

View File

@@ -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 },
] ]

View File

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

View File

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

View File

@@ -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'

View File

@@ -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
} }