|
| 1 | +import type { Meta, StoryObj } from '@storybook/react'; |
| 2 | +import { useState, useEffect, useCallback } from 'react'; |
| 3 | +import { MantineProvider, ColorSchemeScript, localStorageColorSchemeManager } from '@mantine/core'; |
| 4 | +import { ThemeProvider } from '../context/ThemeContext'; |
| 5 | +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; |
| 6 | +import { baseTheme } from '../theme/mantineTheme'; |
| 7 | +import { AbilityManualsContext } from '../context/AbilityManualsContext'; |
| 8 | +import { ModalsProvider } from '@mantine/modals'; |
| 9 | +import { Notifications } from '@mantine/notifications'; |
| 10 | +import { useDisclosure } from '@mantine/hooks'; |
| 11 | +import { Ability } from '../models/abilities.zod'; |
| 12 | +import { useStyles } from '../hooks/useStyles'; |
| 13 | + |
| 14 | +// Import components used in the actual AbilitiesPage |
| 15 | +import { |
| 16 | + Container, |
| 17 | + Title, |
| 18 | + Text, |
| 19 | + SimpleGrid, |
| 20 | + Group, |
| 21 | + useMantineColorScheme, |
| 22 | + Loader, |
| 23 | + Center, |
| 24 | + Paper |
| 25 | +} from '@mantine/core'; |
| 26 | +import { AbilityCard } from '../components/AbilityCard'; |
| 27 | +import { AbilityDetailsModal } from '../components/AbilityDetailsModal'; |
| 28 | +import { AddToAbilityManualModal } from '../components/AddToAbilityManualModal'; |
| 29 | +import { AbilitiesFilter } from '../components/AbilitiesFilter'; |
| 30 | +import { ExportButton } from '../components/ExportButton'; |
| 31 | + |
| 32 | +// Create a mocked version of the page that doesn't require hooks |
| 33 | +function MockedAbilitiesPage({ isLoading = false, hasError = false }) { |
| 34 | + const { isDark } = useStyles(); |
| 35 | + const [selectedAbility, setSelectedAbility] = useState<Ability | null>(null); |
| 36 | + const [filteredAbilities, setFilteredAbilities] = useState<Ability[]>([]); |
| 37 | + const [detailsModalOpened, { open: openDetailsModal, close: closeDetailsModal }] = useDisclosure(false); |
| 38 | + const [abilityManualModalOpened, { open: openAbilityManualModal, close: closeAbilityManualModal }] = useDisclosure(false); // Using sample data directly from sampleAbilities// Initialize filtered abilities |
| 39 | + useEffect(() => { |
| 40 | + if (!isLoading && !hasError) { |
| 41 | + const sortedAbilities = [...sampleAbilities].sort((a, b) => a.abilityName.localeCompare(b.abilityName)); |
| 42 | + setFilteredAbilities(sortedAbilities); |
| 43 | + } |
| 44 | + }, [isLoading, hasError]); |
| 45 | + |
| 46 | + const handleViewDetails = (ability: Ability) => { |
| 47 | + setSelectedAbility(ability); |
| 48 | + openDetailsModal(); |
| 49 | + }; |
| 50 | + |
| 51 | + const handleAddToAbilityManual = (ability: Ability) => { |
| 52 | + setSelectedAbility(ability); |
| 53 | + openAbilityManualModal(); |
| 54 | + }; // Use useCallback to memoize the filter change handler |
| 55 | + const handleFilterChange = useCallback((filtered: Ability[]) => { |
| 56 | + // Create a new sorted array rather than modifying the input |
| 57 | + const sortedAbilities = [...filtered].sort((a, b) => a.abilityName.localeCompare(b.abilityName)); |
| 58 | + setFilteredAbilities(sortedAbilities); |
| 59 | + }, []); |
| 60 | + |
| 61 | + if (isLoading) { |
| 62 | + return ( |
| 63 | + <Center h="70vh"> |
| 64 | + <Loader size="xl" color={isDark ? 'blue.4' : 'blue.6'} /> |
| 65 | + </Center> |
| 66 | + ); |
| 67 | + } |
| 68 | + |
| 69 | + if (hasError) { |
| 70 | + return ( |
| 71 | + <Container size="md" py="xl"> |
| 72 | + <Paper p="xl" withBorder radius="md" bg={isDark ? 'dark.6' : 'white'}> |
| 73 | + <Title order={2} ta="center" c="red" mb="xl"> |
| 74 | + Error Loading Abilities |
| 75 | + </Title> |
| 76 | + <Text ta="center" c={isDark ? 'gray.2' : 'dark.7'}> |
| 77 | + Failed to fetch abilities. Please try again later. |
| 78 | + </Text> |
| 79 | + </Paper> |
| 80 | + </Container> |
| 81 | + ); |
| 82 | + } |
| 83 | + |
| 84 | + return ( |
| 85 | + <Container size="xl" py="xl"> |
| 86 | + <Group justify="space-between" mb="xl"> |
| 87 | + <Title order={1} c={isDark ? 'gray.1' : 'dark.8'}>Abilities Library</Title> |
| 88 | + <ExportButton abilities={filteredAbilities} label="Export Abilities" /> |
| 89 | + </Group> <AbilitiesFilter |
| 90 | + abilities={sampleAbilities} |
| 91 | + onFilterChange={handleFilterChange} |
| 92 | + /> |
| 93 | + |
| 94 | + <Text mt="md" mb="md" c={isDark ? 'gray.3' : 'dark.7'}> |
| 95 | + {filteredAbilities.length} {filteredAbilities.length === 1 ? 'ability' : 'abilities'} found |
| 96 | + </Text> |
| 97 | + |
| 98 | + <SimpleGrid |
| 99 | + cols={{ base: 1, xs: 2, md: 3, lg: 4 }} |
| 100 | + spacing="md" |
| 101 | + mt="xl" |
| 102 | + > |
| 103 | + {filteredAbilities.map((ability) => ( |
| 104 | + <AbilityCard |
| 105 | + key={`${ability.abilityName} (${ability.abilityDiscipline})`} |
| 106 | + ability={ability} |
| 107 | + onViewDetails={handleViewDetails} |
| 108 | + onAddToAbilityManual={handleAddToAbilityManual} |
| 109 | + /> |
| 110 | + ))} |
| 111 | + </SimpleGrid> |
| 112 | + |
| 113 | + <AbilityDetailsModal |
| 114 | + ability={selectedAbility} |
| 115 | + opened={detailsModalOpened} |
| 116 | + onClose={closeDetailsModal} |
| 117 | + /> |
| 118 | + |
| 119 | + <AddToAbilityManualModal |
| 120 | + ability={selectedAbility} |
| 121 | + opened={abilityManualModalOpened} |
| 122 | + onClose={closeAbilityManualModal} |
| 123 | + /> |
| 124 | + </Container> |
| 125 | + ); |
| 126 | +} |
| 127 | + |
| 128 | +// Create a client for React Query |
| 129 | +const queryClient = new QueryClient({ |
| 130 | + defaultOptions: { |
| 131 | + queries: { |
| 132 | + staleTime: Infinity, |
| 133 | + refetchOnWindowFocus: false, |
| 134 | + }, |
| 135 | + }, |
| 136 | +}); |
| 137 | + |
| 138 | +// Sample abilities for our mock data |
| 139 | +const sampleAbilities: Ability[] = [ |
| 140 | + { |
| 141 | + abilityName: "Deflecting Edge", |
| 142 | + abilityCp: 40, |
| 143 | + abilityDiscipline: "Balanced_Sword", |
| 144 | + abilityLevel: "Intermediate", |
| 145 | + abilityType: "Active", |
| 146 | + abilityDescription: "When an opponent attacks you with a melee weapon, you may spend a degree of success to attempt to redirect their blow. Make an opposed Balanced Sword roll against the attacker. If you succeed, their attack is redirected to another target within your weapon's reach, and your opponent must apply their attack result to the new target instead." |
| 147 | + }, |
| 148 | + { |
| 149 | + abilityName: "Elemental Attunement", |
| 150 | + abilityCp: 30, |
| 151 | + abilityDiscipline: "Elementalism", |
| 152 | + abilityLevel: "Basic", |
| 153 | + abilityType: "Passive", |
| 154 | + abilityDescription: "You have an intuitive understanding of elemental magic. You gain a +1 bonus to all spellcasting rolls involving elemental magic and reduce the complexity of all elemental spells by 1.", |
| 155 | + complexityPyromancy: 1, |
| 156 | + complexityHydromancy: 1, |
| 157 | + complexityAeromancy: 1, |
| 158 | + complexityGeomancy: 1 |
| 159 | + }, |
| 160 | + { |
| 161 | + abilityName: "Whirlwind Strike", |
| 162 | + abilityCp: 60, |
| 163 | + abilityDiscipline: "Unbalanced_Sword", |
| 164 | + abilityLevel: "Advanced", |
| 165 | + abilityType: "Active", |
| 166 | + abilityDescription: "You spin in a deadly arc, striking all enemies within your reach. Make a single Unbalanced Sword roll and apply the result against the defense of all targets within your weapon's reach. This ability can only be used once per scene and requires a full turn to execute." |
| 167 | + }, |
| 168 | + { |
| 169 | + abilityName: "Inner Strength", |
| 170 | + abilityCp: 25, |
| 171 | + abilityDiscipline: "Inner_Pillar", |
| 172 | + abilityLevel: "Basic", |
| 173 | + abilityType: "Passive", |
| 174 | + abilityDescription: "Through rigorous training, you have developed exceptional mental resilience. You gain a +2 bonus to all willpower checks to resist mental manipulation or control." |
| 175 | + }, |
| 176 | + { |
| 177 | + abilityName: "Strategic Command", |
| 178 | + abilityCp: 45, |
| 179 | + abilityDiscipline: "Strategist", |
| 180 | + abilityLevel: "Intermediate", |
| 181 | + abilityType: "Active", |
| 182 | + abilityDescription: "As an action, you can analyze the battlefield and provide tactical guidance to your allies. Up to three allies of your choice gain a +1 bonus to their next attack or defense roll, provided they can hear and understand you." |
| 183 | + }, |
| 184 | + { |
| 185 | + abilityName: "Shadow Step", |
| 186 | + abilityCp: 35, |
| 187 | + abilityDiscipline: "Sneak", |
| 188 | + abilityLevel: "Intermediate", |
| 189 | + abilityType: "Active", |
| 190 | + abilityDescription: "You can move silently from shadow to shadow. When in dim light or darkness, you can move up to half your movement speed without making any noise and without requiring a stealth check." |
| 191 | + } |
| 192 | +]; |
| 193 | + |
| 194 | +// Sample ability manual for context |
| 195 | +const sampleAbilityManual = { |
| 196 | + id: "123456", |
| 197 | + name: "Wizard's Spellbook", |
| 198 | + character: "Gandalf the Grey", |
| 199 | + description: "A collection of magical abilities for my wizard character", |
| 200 | + abilities: [sampleAbilities[0], sampleAbilities[1]], |
| 201 | + createdAt: new Date("2025-05-01"), |
| 202 | + updatedAt: new Date("2025-05-20"), |
| 203 | +}; |
| 204 | + |
| 205 | +// Mock for AbilityManualsContext |
| 206 | +const mockAbilityManualsContext = { |
| 207 | + AbilityManuals: [sampleAbilityManual], |
| 208 | + addAbilityManual: () => { }, |
| 209 | + updateAbilityManual: () => { }, |
| 210 | + deleteAbilityManual: () => { }, |
| 211 | + getAbilityManual: (id: string) => { |
| 212 | + if (id === "123456") { |
| 213 | + return sampleAbilityManual; |
| 214 | + } |
| 215 | + return undefined; |
| 216 | + }, |
| 217 | + addAbilityToAbilityManual: () => { }, |
| 218 | + removeAbilityFromAbilityManual: () => { }, |
| 219 | +}; |
| 220 | + |
| 221 | +// Mock abilities tags for the useAbilityTags hook |
| 222 | +queryClient.setQueryData(['abilityTags'], { |
| 223 | + data: { |
| 224 | + tags: [ |
| 225 | + { |
| 226 | + tag: "offensive", |
| 227 | + name: "Offensive", |
| 228 | + description: "Abilities focused on attacking", |
| 229 | + abilities: ["Deflecting Edge", "Whirlwind Strike"] |
| 230 | + }, |
| 231 | + { |
| 232 | + tag: "defensive", |
| 233 | + name: "Defensive", |
| 234 | + description: "Abilities focused on defense", |
| 235 | + abilities: ["Deflecting Edge"] |
| 236 | + }, |
| 237 | + { |
| 238 | + tag: "magic", |
| 239 | + name: "Magic", |
| 240 | + description: "Magic-related abilities", |
| 241 | + abilities: ["Elemental Attunement"] |
| 242 | + }, |
| 243 | + { |
| 244 | + tag: "strategy", |
| 245 | + name: "Strategy", |
| 246 | + description: "Strategic abilities", |
| 247 | + abilities: ["Strategic Command"] |
| 248 | + }, |
| 249 | + { |
| 250 | + tag: "stealth", |
| 251 | + name: "Stealth", |
| 252 | + description: "Stealth abilities", |
| 253 | + abilities: ["Shadow Step"] |
| 254 | + }, |
| 255 | + { |
| 256 | + tag: "mental", |
| 257 | + name: "Mental", |
| 258 | + description: "Mental abilities", |
| 259 | + abilities: ["Inner Strength"] |
| 260 | + } |
| 261 | + ] |
| 262 | + } |
| 263 | +}); |
| 264 | + |
| 265 | +// Mock abilities data for the useAbilities hook |
| 266 | +queryClient.setQueryData(['abilities'], sampleAbilities); |
| 267 | + |
| 268 | +const meta: Meta<typeof MockedAbilitiesPage> = { |
| 269 | + component: MockedAbilitiesPage, |
| 270 | + title: 'Pages/AbilitiesPage', |
| 271 | + tags: ['autodocs'], |
| 272 | + parameters: { |
| 273 | + layout: 'fullscreen', |
| 274 | + }, |
| 275 | + decorators: [ |
| 276 | + (Story) => { |
| 277 | + // Create a color scheme manager for storybook |
| 278 | + const colorSchemeManager = localStorageColorSchemeManager({ key: 'saga-abilities-storybook-color-scheme' }); |
| 279 | + |
| 280 | + return ( |
| 281 | + <QueryClientProvider client={queryClient}> |
| 282 | + <ColorSchemeScript defaultColorScheme="light" /> |
| 283 | + <MantineProvider theme={baseTheme} defaultColorScheme="light" colorSchemeManager={colorSchemeManager}> |
| 284 | + <Notifications position="top-right" /> |
| 285 | + <ModalsProvider> |
| 286 | + <ThemeProvider> |
| 287 | + <AbilityManualsContext.Provider value={mockAbilityManualsContext}> |
| 288 | + <Story /> |
| 289 | + </AbilityManualsContext.Provider> |
| 290 | + </ThemeProvider> |
| 291 | + </ModalsProvider> |
| 292 | + </MantineProvider> |
| 293 | + </QueryClientProvider> |
| 294 | + ); |
| 295 | + }, |
| 296 | + ], |
| 297 | +}; |
| 298 | + |
| 299 | +export default meta; |
| 300 | +type Story = StoryObj<typeof MockedAbilitiesPage>; |
| 301 | + |
| 302 | +// Default story showing the abilities page |
| 303 | +export const Default: Story = { |
| 304 | + args: { |
| 305 | + isLoading: false, |
| 306 | + hasError: false |
| 307 | + } |
| 308 | +}; |
| 309 | + |
| 310 | +// Loading state story |
| 311 | +export const Loading: Story = { |
| 312 | + args: { |
| 313 | + isLoading: true, |
| 314 | + hasError: false |
| 315 | + } |
| 316 | +}; |
| 317 | + |
| 318 | +// Error state story |
| 319 | +export const Error: Story = { |
| 320 | + args: { |
| 321 | + isLoading: false, |
| 322 | + hasError: true |
| 323 | + } |
| 324 | +}; |
| 325 | + |
| 326 | +// Dark mode variant |
| 327 | +export const DarkMode: Story = { |
| 328 | + args: { |
| 329 | + isLoading: false, |
| 330 | + hasError: false |
| 331 | + }, |
| 332 | + parameters: { |
| 333 | + backgrounds: { default: 'dark' }, |
| 334 | + }, |
| 335 | + decorators: [ |
| 336 | + (Story) => { |
| 337 | + const colorSchemeManager = localStorageColorSchemeManager({ key: 'saga-abilities-storybook-color-scheme' }); |
| 338 | + |
| 339 | + return ( |
| 340 | + <QueryClientProvider client={queryClient}> |
| 341 | + <ColorSchemeScript defaultColorScheme="dark" /> |
| 342 | + <MantineProvider theme={baseTheme} defaultColorScheme="dark" colorSchemeManager={colorSchemeManager}> |
| 343 | + <Notifications position="top-right" /> |
| 344 | + <ModalsProvider> |
| 345 | + <ThemeProvider> |
| 346 | + <AbilityManualsContext.Provider value={mockAbilityManualsContext}> |
| 347 | + <Story /> |
| 348 | + </AbilityManualsContext.Provider> |
| 349 | + </ThemeProvider> |
| 350 | + </ModalsProvider> |
| 351 | + </MantineProvider> |
| 352 | + </QueryClientProvider> |
| 353 | + ); |
| 354 | + }, |
| 355 | + ], |
| 356 | +}; |
0 commit comments