Files
bamort/frontend/src/components/CharacterCreation.vue
T

372 lines
12 KiB
Vue

<template>
<div class="character-creation">
<div class="creation-header">
<h1>Create New Character</h1>
<div class="progress-indicator">
<div
v-for="step in steps"
:key="step.number"
:class="['step', {
active: currentStep === step.number,
completed: currentStep > step.number,
clickable: currentStep > step.number || currentStep === step.number
}]"
@click="navigateToStep(step.number)"
>
<span class="step-number">{{ step.number }}</span>
<span class="step-title">{{ step.title }}</span>
</div>
</div>
</div>
<div class="creation-content">
<!-- Step 1: Basic Information -->
<CharacterBasicInfo
v-if="currentStep === 1"
:session-data="sessionData"
@next="handleNext"
@save="saveProgress"
/>
<!-- Step 2: Attributes -->
<CharacterAttributes
v-if="currentStep === 2"
:session-data="sessionData"
@next="handleNext"
@previous="handlePrevious"
@save="saveProgress"
/>
<!-- Step 3: Derived Values -->
<CharacterDerivedValues
v-if="currentStep === 3"
:session-data="sessionData"
@next="handleNext"
@previous="handlePrevious"
@save="saveProgress"
/>
<!-- Step 4: Skills -->
<CharacterSkills
v-if="currentStep === 4"
:session-data="sessionData"
:skill-categories="skillCategories"
@previous="handlePrevious"
@next="handleNext"
@save="saveProgress"
/>
<!-- Step 5: Spells -->
<CharacterSpells
v-if="currentStep === 5"
:session-data="sessionData"
:skill-categories="skillCategories"
@previous="handlePrevious"
@finalize="handleFinalize"
@save="saveProgress"
/>
</div>
<!-- Session Info -->
<div class="session-info">
<p>Session expires: {{ formatDate(sessionData.expires_at) }}</p>
<button @click="deleteDraft" class="delete-btn">Delete Draft</button>
</div>
</div>
</template>
<script>
import API from '../utils/api'
import CharacterBasicInfo from './CharacterCreation/CharacterBasicInfo.vue'
import CharacterAttributes from './CharacterCreation/CharacterAttributes.vue'
import CharacterDerivedValues from './CharacterCreation/CharacterDerivedValues.vue'
import CharacterSkills from './CharacterCreation/CharacterSkills.vue'
import CharacterSpells from './CharacterCreation/CharacterSpells.vue'
export default {
name: 'CharacterCreation',
components: {
CharacterBasicInfo,
CharacterAttributes,
CharacterDerivedValues,
CharacterSkills,
CharacterSpells,
},
props: {
sessionId: {
type: String,
required: true,
}
},
data() {
return {
currentStep: 1,
sessionData: {
id: '',
name: '',
rasse: '',
typ: '',
herkunft: '',
glaube: '',
geschlecht: '',
stand: '',
attributes: {},
derived_values: {},
skills: [],
spells: [],
skill_points: {},
spell_points: {},
expires_at: null,
current_step: 1,
},
steps: [
{ number: 1, title: 'Basic Info' },
{ number: 2, title: 'Attributes' },
{ number: 3, title: 'Derived Values' },
{ number: 4, title: 'Skills' },
{ number: 5, title: 'Spells' },
],
skillCategories: [],
}
},
async created() {
await this.loadSession()
await this.loadSkillCategories()
},
methods: {
async loadSession(preserveCurrentStep = false) {
try {
const token = localStorage.getItem('token')
const response = await API.get(`/api/characters/create-session/${this.sessionId}`, {
headers: { Authorization: `Bearer ${token}` },
})
this.sessionData = response.data
// Only update currentStep if not preserving it
if (!preserveCurrentStep) {
this.currentStep = response.data.current_step || 1
}
} catch (error) {
console.error('Error loading session:', error)
this.$router.push('/dashboard')
}
},
async loadSkillCategories() {
try {
const token = localStorage.getItem('token')
const response = await API.get('/api/characters/skill-categories', {
headers: { Authorization: `Bearer ${token}` },
})
this.skillCategories = response.data.categories || []
} catch (error) {
console.error('Error loading skill categories:', error)
// Fallback dummy data
this.skillCategories = [
{ name: 'körperlich', display_name: 'Körperliche Fertigkeiten', max_points: 200, points: 200 },
{ name: 'gesellschaftlich', display_name: 'Gesellschaftliche Fertigkeiten', max_points: 150, points: 150 },
{ name: 'natur', display_name: 'Natur Fertigkeiten', max_points: 100, points: 100 },
{ name: 'wissen', display_name: 'Wissens Fertigkeiten', max_points: 180, points: 180 },
{ name: 'handwerk', display_name: 'Handwerks Fertigkeiten', max_points: 120, points: 120 },
{ name: 'zauber', display_name: 'Zauber', max_points: 300, points: 300 },
]
}
},
async handleNext(data) {
try {
// Merge the new data
this.sessionData = { ...this.sessionData, ...data }
// Save progress for current step before moving to next
await this.saveProgressForStep(this.currentStep, data)
// Reload session data from backend to ensure consistent state
// Preserve currentStep as we'll increment it after
await this.loadSession(true)
// Move to next step
this.currentStep++
} catch (error) {
console.error('Failed to save progress before moving to next step:', error)
// Don't move to next step if save failed
}
},
async saveProgressForStep(step, data) {
try {
const token = localStorage.getItem('token')
let endpoint = ''
let payload = {}
switch (step) {
case 1:
endpoint = `/api/characters/create-session/${this.sessionId}/basic`
// Handle both old format and new basic_info format
const basicInfo = data.basic_info || data
payload = {
name: basicInfo.name || this.sessionData.name || '',
geschlecht: basicInfo.geschlecht || this.sessionData.geschlecht || '',
rasse: basicInfo.rasse || this.sessionData.rasse || '',
typ: basicInfo.typ || this.sessionData.typ || '',
herkunft: basicInfo.herkunft || this.sessionData.herkunft || '',
stand: basicInfo.stand || this.sessionData.stand || '',
glaube: basicInfo.glaube || this.sessionData.glaube || '',
}
// Validate that all required fields are present
if (!payload.name || !payload.geschlecht || !payload.rasse || !payload.typ || !payload.herkunft || !payload.stand) {
throw new Error(`Missing required fields: name=${payload.name}, geschlecht=${payload.geschlecht}, rasse=${payload.rasse}, typ=${payload.typ}, herkunft=${payload.herkunft}, stand=${payload.stand}`)
}
break
case 2:
endpoint = `/api/characters/create-session/${this.sessionId}/attributes`
payload = data.attributes || data
break
case 3:
endpoint = `/api/characters/create-session/${this.sessionId}/derived`
payload = data.derived_values || data
break
case 4:
endpoint = `/api/characters/create-session/${this.sessionId}/skills`
payload = {
skills: data.skills || this.sessionData.skills,
spells: data.spells || this.sessionData.spells,
skill_points: data.skill_points || this.sessionData.skill_points,
}
break
}
if (endpoint) {
const response = await API.put(endpoint, payload, {
headers: { Authorization: `Bearer ${token}` },
})
}
} catch (error) {
console.error('Error saving progress for step', step, ':', error)
// Provide more specific error messages
if (error.response && error.response.status === 401) {
alert('Your session has expired. Please log in again.')
} else if (error.response && error.response.status === 400) {
const errorMsg = error.response.data?.error || 'Invalid data submitted'
//alert(`Error saving character data: ${errorMsg}`)
} //else {
//alert('Failed to save character data. Please try again.')
//}
throw error // Re-throw to handle in calling function
}
},
handlePrevious() {
this.currentStep--
},
navigateToStep(stepNumber) {
// Only allow navigation to current step or previously completed steps
if (stepNumber <= this.currentStep) {
this.currentStep = stepNumber
// Save current progress before switching steps (no data parameter needed here)
this.saveProgress().catch(error => {
console.error('Failed to save progress during navigation:', error)
})
}
},
async saveProgress(data = null) {
try {
// Use provided data or current sessionData as fallback
const dataToSave = data || this.sessionData
// Update sessionData with new data if provided
if (data) {
this.sessionData = { ...this.sessionData, ...data }
}
// Save progress for current step
await this.saveProgressForStep(this.currentStep, dataToSave)
} catch (error) {
console.error('Failed to save progress:', error)
throw error
}
},
getUserIdFromToken() {
try {
const token = localStorage.getItem('token')
if (!token) return null
// Decode JWT token to get user ID
const base64Url = token.split('.')[1]
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/')
const jsonPayload = decodeURIComponent(window.atob(base64).split('').map(function(c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
}).join(''))
const payload = JSON.parse(jsonPayload)
return payload.user_id || payload.userID || payload.sub || null
} catch (error) {
console.error('Error decoding token:', error)
return null
}
},
async handleFinalize() {
try {
const token = localStorage.getItem('token')
const userId = this.getUserIdFromToken()
const requestBody = {}
if (userId) {
requestBody.user_id = userId
}
const response = await API.post(`/api/characters/create-session/${this.sessionId}/finalize`, requestBody, {
headers: { Authorization: `Bearer ${token}` },
})
const characterId = response.data.character_id
// Success message
//alert('Character successfully created!')
// Navigate to character view or back to character list
this.$router.push(`/character/${characterId}`)
} catch (error) {
console.error('Error finalizing character:', error)
if (error.response?.data?.error) {
alert(`Error: ${error.response.data.error}`)
} else {
alert('Fehler beim Abschließen der Charakter-Erstellung')
}
}
},
async deleteDraft() {
if (confirm('Are you sure you want to delete this character draft?')) {
try {
const token = localStorage.getItem('token')
await API.delete(`/api/characters/create-session/${this.sessionId}`, {
headers: { Authorization: `Bearer ${token}` },
})
this.$router.push('/dashboard')
} catch (error) {
console.error('Error deleting session:', error)
}
}
},
formatDate(dateString) {
if (!dateString) return ''
return new Date(dateString).toLocaleDateString()
},
},
}
</script>
<style>
/* All styles moved to main.css */
</style>