Pinia Stores Reference
Overview
CS2Inspect uses Pinia for state management. This document provides comprehensive reference for all Pinia stores in the application.
Available Stores
- loadoutStore - Manages user loadouts and item customizations
- adminStore - Manages admin panel state, user management, and analytics
- tutorialStore - Manages interactive tutorial state and step progression
loadoutStore
Location: /stores/loadoutStore.ts
Purpose: Centralized state management for user loadouts, weapons, knives, gloves, agents, music kits, and pins. Handles all CRUD operations and API communication for loadout management.
Import
import { useLoadoutStore } from '~/stores/loadoutStore'Usage
<script setup>
const loadoutStore = useLoadoutStore()
// Access state
const loadouts = loadoutStore.loadouts
const selectedLoadoutId = loadoutStore.selectedLoadoutId
// Access getters
const selectedLoadout = loadoutStore.selectedLoadout
const hasLoadouts = loadoutStore.hasLoadouts
// Call actions
await loadoutStore.fetchLoadouts(steamId)
await loadoutStore.createLoadout(steamId, 'My Loadout')
</script>State
loadouts
DBLoadout[]Array of all user loadouts.
DBLoadout Structure:
interface DBLoadout {
id: number
steamid: string
name: string
selected_knife_t: number | null
selected_knife_ct: number | null
selected_glove_t: number | null
selected_glove_ct: number | null
selected_agent_ct: number | null
selected_agent_t: number | null
selected_music: number | null
active: boolean | number
is_default: boolean | number
created_at: string
updated_at: string
}currentSkins
IEnhancedItem[] | IEnhancedKnife[] | IEnhancedWeapon[]Currently loaded items for the selected loadout (weapons, knives, gloves, etc.).
selectedLoadoutId
LoadoutId | nullID of the currently selected loadout (branded type for type safety).
isLoading
booleanLoading state for async operations.
error
string | nullError message from last operation.
Getters
selectedLoadout
ComputedRef<DBLoadout | undefined>Returns the currently selected loadout object.
Example:
<script setup>
const loadoutStore = useLoadoutStore()
const loadout = loadoutStore.selectedLoadout
watchEffect(() => {
if (loadout) {
console.log('Active loadout:', loadout.name)
}
})
</script>hasLoadouts
ComputedRef<boolean>Whether the user has any loadouts.
Example:
<template>
<div v-if="!loadoutStore.hasLoadouts">
<p>No loadouts yet. Create your first loadout!</p>
</div>
</template>loadoutSkins
ComputedRef<IEnhancedItem[]>Alias for currentSkins state.
Actions
Loadout Management
fetchLoadouts(steamId)
Fetch all loadouts for a user.
Parameters:
steamId: SteamId- User's Steam ID (branded type)
Returns: Promise<void>
Side Effects:
- Updates
loadoutsstate - Auto-selects active/first loadout if none selected
- Validates selected loadout still exists
Example:
import { toSteamId } from '~/types/core/branded'
const steamId = toSteamId('76561198012345678')
await loadoutStore.fetchLoadouts(steamId)Features:
- Prevents duplicate concurrent requests using
fetchPromisesmap - Handles both old and new API response formats
- Auto-selects active loadout or falls back to first loadout
- Validates selected loadout after fetch
createLoadout(steamId, name)
Create a new loadout.
Parameters:
steamId: SteamId- User's Steam IDname: string- Loadout name
Returns: Promise<void>
Side Effects:
- Creates new loadout in database
- Activates the newly created loadout
- Refreshes loadouts list
Validation:
- Name required
- Name must be unique per user (enforced by API)
Example:
await loadoutStore.createLoadout(steamId, 'Competitive Setup')updateLoadout(id, steamId, newName)
Update a loadout's name.
Parameters:
id: LoadoutId- Loadout ID to updatesteamId: SteamId- User's Steam IDnewName: string- New loadout name
Returns: Promise<void>
Validation:
- Name length: 1-20 characters
- Throws error if validation fails
Example:
const loadoutId = toLoadoutId(1)
await loadoutStore.updateLoadout(loadoutId, steamId, 'Casual Setup')deleteLoadout(steamId, id)
Delete a loadout.
Parameters:
steamId: SteamId- User's Steam IDid: LoadoutId- Loadout ID to delete
Returns: Promise<void>
Side Effects:
- Deletes loadout from database
- Refreshes loadouts list
- Auto-selects another loadout if deleted was selected
Restrictions:
- Cannot delete if it's the only loadout (enforced by API)
Example:
await loadoutStore.deleteLoadout(steamId, loadoutId)selectLoadout(id)
Select a loadout (client-side only, doesn't persist).
Parameters:
id: LoadoutId- Loadout ID to select
Returns: void
Example:
loadoutStore.selectLoadout(toLoadoutId(2))activateLoadout(id, steamId)
Activate a loadout (persists to database, sets as active).
Parameters:
id: LoadoutId- Loadout ID to activatesteamId: SteamId- User's Steam ID
Returns: Promise<void>
Side Effects:
- Sets loadout as active in database
- Deactivates all other loadouts
- Updates local state
- Selects the loadout
Example:
await loadoutStore.activateLoadout(loadoutId, steamId)duplicateLoadout(steamId, loadoutId)
Duplicate an existing loadout.
Parameters:
steamId: SteamId- User's Steam IDloadoutId: LoadoutId- Loadout ID to duplicate
Returns: Promise<void>
Side Effects:
- Creates copy of loadout with all items
- Refreshes loadouts list
Example:
await loadoutStore.duplicateLoadout(steamId, loadoutId)setLoadoutAsDefault(steamId, loadoutId)
Set a loadout as default (used by CS2 plugin).
Parameters:
steamId: SteamId- User's Steam IDloadoutId: LoadoutId- Loadout ID to set as default
Returns: Promise<void>
Side Effects:
- Sets
is_default = 1for this loadout - Unsets default flag for all other loadouts
- Refreshes loadouts list
Example:
await loadoutStore.setLoadoutAsDefault(steamId, loadoutId)clearLoadout(steamId, loadoutId, categories?)
Clear items from a loadout.
Parameters:
steamId: SteamId- User's Steam IDloadoutId: LoadoutId- Loadout ID to clearcategories?: string[]- Optional categories to clear (default: all)
Returns: Promise<void>
Categories: ['weapons', 'knives', 'gloves', 'agents', 'music', 'pins']
Side Effects:
- Clears specified categories from loadout
- Updates
currentSkinsif selected loadout
Example:
// Clear all items
await loadoutStore.clearLoadout(steamId, loadoutId)
// Clear only weapons and knives
await loadoutStore.clearLoadout(steamId, loadoutId, ['weapons', 'knives'])Sharing & Import
shareLoadout(steamId, loadoutId)
Generate a share code for a loadout.
Parameters:
steamId: SteamId- User's Steam IDloadoutId: LoadoutId- Loadout ID to share
Returns: Promise<string> - Share code
Example:
const shareCode = await loadoutStore.shareLoadout(steamId, loadoutId)
console.log(`Share code: ${shareCode}`)
// Use share code to share loadout with othersimportLoadout(steamId, shareCode)
Import a loadout from a share code.
Parameters:
steamId: SteamId- User's Steam IDshareCode: string- Share code from another user
Returns: Promise<void>
Side Effects:
- Creates new loadout from share code
- Refreshes loadouts list
- Auto-selects the imported loadout
Example:
await loadoutStore.importLoadout(steamId, 'ABC123XYZ')Item Fetching
fetchLoadoutWeaponSkins(type, steamId)
Fetch weapons for current loadout.
Parameters:
type: string- Weapon category ('rifles','pistols','smgs','heavys')steamId: SteamId- User's Steam ID
Returns: Promise<void>
Side Effects: Updates currentSkins with weapons
Example:
await loadoutStore.fetchLoadoutWeaponSkins('rifles', steamId)fetchLoadoutKnives(steamId)
Fetch knives for current loadout.
Parameters:
steamId: SteamId- User's Steam ID
Returns: Promise<void>
Example:
await loadoutStore.fetchLoadoutKnives(steamId)fetchLoadoutGloves(steamId)
Fetch gloves for current loadout.
Parameters:
steamId: SteamId- User's Steam ID
Returns: Promise<void>
fetchLoadoutMusicKits(steamId)
Fetch music kits for current loadout.
Parameters:
steamId: SteamId- User's Steam ID
Returns: Promise<void>
fetchLoadoutPins(steamId)
Fetch pins for current loadout.
Parameters:
steamId: SteamId- User's Steam ID
Returns: Promise<void>
Usage Patterns
Initial Load
<script setup>
const loadoutStore = useLoadoutStore()
const { user } = useAuth()
onMounted(async () => {
if (user.value?.steamId) {
await loadoutStore.fetchLoadouts(toSteamId(user.value.steamId))
}
})
</script>Creating a Loadout
<script setup>
const loadoutStore = useLoadoutStore()
const { user } = useAuth()
const message = useMessage()
const createNewLoadout = async (name: string) => {
try {
await loadoutStore.createLoadout(
toSteamId(user.value.steamId),
name
)
message.success('Loadout created successfully!')
} catch (error) {
message.error('Failed to create loadout')
}
}
</script>Switching Loadouts
<script setup>
const loadoutStore = useLoadoutStore()
const { user } = useAuth()
const switchLoadout = async (loadoutId: LoadoutId) => {
// Client-side selection
loadoutStore.selectLoadout(loadoutId)
// Fetch items for this loadout
await loadoutStore.fetchLoadoutWeaponSkins('rifles', toSteamId(user.value.steamId))
// Optionally activate (persist to DB)
await loadoutStore.activateLoadout(loadoutId, toSteamId(user.value.steamId))
}
</script>Displaying Loadouts
<template>
<div v-if="loadoutStore.isLoading">
Loading loadouts...
</div>
<div v-else-if="loadoutStore.error">
Error: {{ loadoutStore.error }}
</div>
<div v-else-if="!loadoutStore.hasLoadouts">
No loadouts yet. Create one to get started!
</div>
<div v-else>
<div v-for="loadout in loadoutStore.loadouts" :key="loadout.id">
<div
:class="{ active: loadout.id === loadoutStore.selectedLoadoutId }"
@click="switchLoadout(toLoadoutId(loadout.id))"
>
{{ loadout.name }}
<span v-if="loadout.is_default">⭐ Default</span>
<span v-if="loadout.active">✓ Active</span>
</div>
</div>
</div>
</template>Error Handling
<script setup>
const loadoutStore = useLoadoutStore()
const message = useMessage()
const handleAction = async (action: () => Promise<void>) => {
try {
await action()
message.success('Operation completed successfully')
} catch (error) {
if (error instanceof Error) {
message.error(error.message)
} else {
message.error('An unexpected error occurred')
}
}
}
// Usage
await handleAction(() =>
loadoutStore.deleteLoadout(steamId, loadoutId)
)
</script>Advanced Patterns
Reactive Loadout Display
<script setup>
const loadoutStore = useLoadoutStore()
// Automatically updates when selection changes
const activeLoadout = computed(() => loadoutStore.selectedLoadout)
watchEffect(() => {
if (activeLoadout.value) {
console.log('Active loadout changed:', activeLoadout.value.name)
// Trigger any side effects
}
})
</script>Optimistic Updates
// Optimistically update UI before server response
const quickSelectLoadout = (id: LoadoutId) => {
// Immediate UI update
loadoutStore.selectLoadout(id)
// Server sync in background
loadoutStore.activateLoadout(id, steamId).catch((error) => {
// Revert on error
message.error('Failed to activate loadout')
})
}Batch Operations
// Clear and reload loadout
const resetLoadout = async (steamId: SteamId, loadoutId: LoadoutId) => {
await loadoutStore.clearLoadout(steamId, loadoutId)
await loadoutStore.fetchLoadoutWeaponSkins('rifles', steamId)
await loadoutStore.fetchLoadoutKnives(steamId)
await loadoutStore.fetchLoadoutGloves(steamId)
}API Response Formats
The store handles both legacy and new API response formats:
Legacy Format
{
"loadouts": [...],
"skins": [...]
}New Format
{
"data": {
"loadouts": [...],
"skins": [...]
},
"meta": {
"loadoutId": 1,
"steamId": "76561198012345678",
"rows": 10
}
}The store automatically adapts to both formats for backward compatibility.
Type Safety
The store uses branded types for enhanced type safety:
import { toSteamId, toLoadoutId } from '~/types/core/branded'
// These prevent accidental mixing of IDs
const steamId = toSteamId('76561198012345678') // SteamId brand
const loadoutId = toLoadoutId(1) // LoadoutId brand
// TypeScript will catch errors:
// loadoutStore.fetchLoadouts(loadoutId) // ❌ Error: LoadoutId is not SteamId
// loadoutStore.deleteLoadout(steamId, steamId) // ❌ Error: SteamId is not LoadoutIdPerformance Considerations
Deduplication
The store prevents duplicate concurrent fetches:
// Multiple calls will share the same promise
await Promise.all([
loadoutStore.fetchLoadouts(steamId), // Fetches
loadoutStore.fetchLoadouts(steamId), // Waits for first
loadoutStore.fetchLoadouts(steamId) // Waits for first
])State Updates
State updates trigger minimal re-renders:
<script setup>
// Only re-renders when selectedLoadout changes
const selectedLoadout = computed(() => loadoutStore.selectedLoadout)
// Not when other loadouts update
watch(selectedLoadout, (newLoadout) => {
console.log('Selected loadout changed:', newLoadout)
})
</script>Debugging
Enable Logging
The store includes console logging for debugging:
// Logs are automatically added for:
// - Fetch operations with counts
// - State updates
// - Error conditionsDevTools
Use Vue DevTools to inspect store state:
- Open Vue DevTools
- Navigate to Pinia tab
- Select
loadoutstore - Inspect state, getters, actions
Best Practices
1. Always Handle Errors
try {
await loadoutStore.fetchLoadouts(steamId)
} catch (error) {
// Always handle errors
console.error('Failed to fetch loadouts:', error)
message.error('Could not load loadouts')
}2. Use Branded Types
// Good: Type-safe
const steamId = toSteamId(user.steamId)
await loadoutStore.fetchLoadouts(steamId)
// Bad: Not type-safe
await loadoutStore.fetchLoadouts(user.steamId)3. Check Loading States
<template>
<div v-if="loadoutStore.isLoading">
<Spinner />
</div>
<div v-else>
<!-- Content -->
</div>
</template>4. Validate Before Actions
if (!loadoutStore.hasLoadouts) {
message.warning('Create a loadout first')
return
}
await loadoutStore.deleteLoadout(steamId, loadoutId)5. Use Computed for Derived State
// Good: Reactive
const activeLoadoutName = computed(() =>
loadoutStore.selectedLoadout?.name ?? 'No loadout'
)
// Bad: Not reactive
const activeLoadoutName = loadoutStore.selectedLoadout?.nameTesting
Unit Testing
import { setActivePinia, createPinia } from 'pinia'
import { useLoadoutStore } from '~/stores/loadoutStore'
describe('loadoutStore', () => {
beforeEach(() => {
setActivePinia(createPinia())
})
it('should initialize with empty state', () => {
const store = useLoadoutStore()
expect(store.loadouts).toEqual([])
expect(store.selectedLoadoutId).toBeNull()
expect(store.hasLoadouts).toBe(false)
})
it('should update state after fetch', async () => {
const store = useLoadoutStore()
// Mock API response
await store.fetchLoadouts(toSteamId('123'))
expect(store.loadouts.length).toBeGreaterThan(0)
})
})Related Documentation
- Composables Reference - Vue composables
- API Reference - Backend API documentation
- TypeScript Types - Type definitions
- Components - Component documentation
Migration Guide
From Direct API Calls
// Old: Direct API calls
const response = await fetch('/api/loadouts')
const loadouts = await response.json()
// New: Use store
const loadoutStore = useLoadoutStore()
await loadoutStore.fetchLoadouts(steamId)
const loadouts = loadoutStore.loadoutsFrom Local State
<!-- Old: Local component state -->
<script setup>
const loadouts = ref([])
const fetchLoadouts = async () => {
const response = await fetch('/api/loadouts')
loadouts.value = await response.json()
}
</script>
<!-- New: Use store -->
<script setup>
const loadoutStore = useLoadoutStore()
onMounted(() => loadoutStore.fetchLoadouts(steamId))
</script>Future Enhancements
Planned improvements for the loadout store:
- Offline Support: Cache loadouts in IndexedDB
- Real-time Updates: WebSocket integration for multi-device sync
- Undo/Redo: Action history for reverting changes
- Conflict Resolution: Handle concurrent edits
- Performance: Virtual scrolling for large loadout lists
Contributing
When modifying the loadout store:
- Maintain Type Safety: Use branded types
- Handle Both Formats: Support legacy and new API responses
- Add Error Handling: Catch and handle all errors
- Update Tests: Add tests for new functionality
- Document Changes: Update this documentation
adminStore
Location: /stores/adminStore.ts
Purpose: Centralized state management for the admin panel. Handles dashboard statistics, user management, settings, activity logs, and admin user management with built-in caching (5-minute TTL).
Import
import { useAdminStore } from '~/stores/adminStore'Usage
<script setup>
const adminStore = useAdminStore()
// Check admin status
await adminStore.checkAdminStatus()
// Fetch dashboard data
await adminStore.fetchOverviewStats()
// Manage users
await adminStore.fetchUsers({ search: '', page: 1 })
await adminStore.banUser(steamId, 'Violation of TOS', 72)
</script>State
interface AdminState {
// Auth
isAdmin: boolean
adminRole: 'admin' | 'superadmin' | null
adminPermissions: string[]
// Dashboard data
overviewStats: AdminOverviewStats | null
users: AdminUserSummary[]
usersTotal: number
activityData: AdminActivityData | null
topUsers: AdminTopUser[]
settings: AdminSetting[]
activityLog: AdminActivityLogEntry[]
activityLogTotal: number
adminUsers: AdminInfo[]
// Loading states
isLoading: boolean
isLoadingStats: boolean
isLoadingUsers: boolean
isLoadingSettings: boolean
isLoadingActivity: boolean
// Error & cache
error: string | null
lastUsersQuery: string | null
lastActivityLogQuery: string | null
lastActivityRange: '7d' | '30d' | '90d' | null
lastFetch: {
adminStatus: number | null
stats: number | null
users: number | null
settings: number | null
activity: number | null
activityLog: number | null
adminUsers: number | null
}
}Getters
isSuperAdmin
ComputedRef<boolean>Whether the current admin has superadmin role.
isStatsCacheStale
ComputedRef<boolean>Whether overview stats cache is older than 5 minutes.
isUsersCacheStale
ComputedRef<boolean>Whether users data cache is older than 5 minutes.
isSettingsCacheStale
ComputedRef<boolean>Whether settings cache is older than 5 minutes.
totalItems
ComputedRef<number>Sum of all item categories from overview stats.
Actions
Authentication
checkAdminStatus()
Verify current user's admin status and fetch role/permissions.
Returns: Promise<boolean> - Whether the user is an admin.
fetchCurrentAdminInfo(forceRefresh?)
Fetch current admin's detailed role and permissions.
Parameters:
forceRefresh?: boolean- Bypass cache
Returns: Promise<void>
Statistics
fetchOverviewStats(forceRefresh?)
Fetch dashboard overview statistics (total users, active users, loadouts, items, banned count).
Parameters:
forceRefresh?: boolean- Bypass cache
Returns: Promise<void>
fetchActivityData(range, force?)
Fetch activity chart data for a time range.
Parameters:
range: '7d' | '30d' | '90d'- Time rangeforce?: boolean- Bypass cache
Returns: Promise<void>
fetchTopUsers(limit?)
Fetch top users for leaderboard display.
Parameters:
limit?: number- Max users (default: 10)
Returns: Promise<void>
User Management
fetchUsers(params)
Fetch paginated user list with optional search.
Parameters:
params: { search?, page?, limit?, force? }
Returns: Promise<void>
fetchUserDetails(steamId)
Fetch detailed information for a single user.
Parameters:
steamId: string
Returns: Promise<AdminUserDetails | null>
banUser(steamId, reason, durationHours?, options?)
Ban a user. Logs action in activity log.
Parameters:
steamId: stringreason: stringdurationHours?: number- Omit for permanent banoptions?: { refreshUsers?: boolean }
Returns: Promise<void>
unbanUser(steamId, options?)
Unban a user. Logs action in activity log.
Parameters:
steamId: stringoptions?: { refreshUsers?: boolean }
Returns: Promise<void>
deleteUserData(steamId)
Delete all data for a user. Logs action in activity log.
Parameters:
steamId: string
Returns: Promise<void>
Settings (Superadmin)
fetchSettings(forceRefresh?)
Fetch all application settings.
Returns: Promise<void>
updateSetting(key, value)
Update a single application setting. Logs action in activity log.
Parameters:
key: stringvalue: string | number | boolean
Returns: Promise<void>
Activity Log
fetchActivityLog(params)
Fetch paginated admin activity audit log.
Parameters:
params: { page?, limit?, action?, force? }
Returns: Promise<void>
Admin Management (Superadmin)
fetchAdminUsers(forceRefresh?)
Fetch all admin users.
Returns: Promise<void>
addAdmin(steamId, role)
Grant admin privileges to a user. Logs action.
Parameters:
steamId: stringrole: 'admin' | 'superadmin'
Returns: Promise<void>
removeAdmin(steamId)
Revoke admin privileges. Logs action.
Parameters:
steamId: string
Returns: Promise<void>
Utilities
clearCache()
Clear all cached data (keeps auth state).
reset()
Reset store to initial state (clears everything including auth).
Caching Behavior
The admin store caches all fetched data with a 5-minute TTL (CACHE_DURATION = 300000). Each data category has its own cache timestamp. Actions with a forceRefresh parameter bypass the cache when set to true.
// Uses cache if fresh
await adminStore.fetchOverviewStats()
// Always fetches from server
await adminStore.fetchOverviewStats(true)Related Documentation
- Admin Panel - Admin panel overview
- Admin API - API endpoint documentation
- useAdminAuth - Admin auth composable
- useAdminStats - Admin stats composable
tutorialStore
Location: /stores/tutorialStore.ts
Purpose: Manages the interactive tutorial system state including active tutorial tracking, step progression, completion persistence, and cross-component modal control via the pendingAction mechanism.
Import
import { useTutorialStore } from '~/stores/tutorialStore'State
interface TutorialState {
activeTutorialId: string | null // ID of the running tutorial
currentStepIndex: number // zero-based step index
isActive: boolean // whether a tutorial is running
completedTutorials: string[] // IDs of completed tutorials
targetRect: DOMRect | null // bounding rect of the current target element
pendingAction: TutorialAction // signal for components to open/close modals
}
type TutorialAction =
| 'open-weapon-modal'
| 'close-weapon-modal'
| 'open-loadout-create'
| 'close-loadout-create'
| nullGetters
| Getter | Return Type | Description |
|---|---|---|
activeTutorial | TutorialDefinition | null | The full tutorial definition for the active tutorial |
currentStep | TutorialStep | null | The current step object |
totalSteps | number | Total steps in the active tutorial |
progressLabel | string | Formatted string like "3 / 10" |
isLastStep | boolean | Whether the current step is the final one |
isTutorialCompleted | (id: string) => boolean | Check if a specific tutorial was completed |
Actions
| Action | Parameters | Description |
|---|---|---|
startTutorial | tutorialId: string | Start a tutorial, navigate to its start route |
nextStep | none | Advance forward (runs afterStep hook, completes on last step) |
previousStep | none | Go back one step (runs afterStep hook) |
stopTutorial | none | Stop the tutorial, reset state, clear highlights |
completeTutorial | tutorialId: string | Mark a tutorial as completed and persist |
updateTargetRect | rect: DOMRect | null | Update the stored target element rect |
requestAction | action: TutorialAction | Signal a component to perform an action (e.g. open modal) |
clearAction | none | Clear the pending action after it's been handled |
loadPersistedState | none | Load completed tutorials from localStorage |
persistState | none | Save completed tutorials to localStorage |
pendingAction Pattern
The pendingAction field enables tutorials to programmatically control modals in other components:
// In tutorialDefinitions.ts — a step hook requests an action
beforeStep: () => {
const store = useTutorialStore()
store.requestAction('open-weapon-modal')
}
// In the target component — a watcher responds
watch(() => tutorialStore.pendingAction, (action) => {
if (action === 'open-weapon-modal') {
tutorialStore.clearAction()
// Open the modal
}
})Persistence
Completed tutorial IDs are stored in localStorage under the key cs2inspect_tutorial_completions as a JSON array of strings.
Related
- Tutorial System Guide - Full architecture documentation
- useTutorial - Composable wrapper