- Implementierung neuer Endpunkte für die Verwaltung von Match3-Kampagnen, Levels, Objectives und Tile-Typen im Admin-Bereich. - Anpassung der Admin-Services zur Unterstützung von Benutzerberechtigungen und Fehlerbehandlung. - Einführung von neuen Modellen und Assoziationen für Match3-Levels und Tile-Typen in der Datenbank. - Verbesserung der Internationalisierung für Match3-spezifische Texte in Deutsch und Englisch. - Aktualisierung der Frontend-Routen und -Komponenten zur Verwaltung von Match3-Inhalten.
1518 lines
44 KiB
Vue
1518 lines
44 KiB
Vue
<template>
|
|
<div class="contenthidden">
|
|
<div class="contentscroll">
|
|
<div class="admin-header">
|
|
<h1>{{ $t('admin.match3.title') }}</h1>
|
|
<p>Verwalte Minigames, Level und Konfigurationen</p>
|
|
</div>
|
|
|
|
<SimpleTabs v-model="activeTab" :tabs="tabs" />
|
|
|
|
<!-- Match3 Levels Tab -->
|
|
<div v-if="activeTab === 'match3-levels'" class="match3-admin">
|
|
<div class="section-header">
|
|
<h2>{{ $t('admin.match3.title') }}</h2>
|
|
</div>
|
|
|
|
<div class="level-selection">
|
|
<div class="level-count">
|
|
<p>{{ $t('admin.match3.availableLevels', { count: levels.length }) }}</p>
|
|
</div>
|
|
|
|
<div class="level-dropdown">
|
|
<select v-model="selectedLevelId" @change="onLevelSelect" class="level-select">
|
|
<option value="new">{{ $t('admin.match3.newLevel') }}</option>
|
|
<option
|
|
v-for="level in levels"
|
|
:key="level.id"
|
|
:value="level.id"
|
|
class="level-option"
|
|
>
|
|
{{ $t('admin.match3.levelFormat', { number: level.order, name: level.name }) }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Level Details -->
|
|
<div v-if="selectedLevelId !== 'new' && selectedLevel" class="level-details">
|
|
<div class="details-header">
|
|
<h3>{{ selectedLevel.name }}</h3>
|
|
</div>
|
|
<div class="details-content">
|
|
<div class="form-group">
|
|
<label for="levelName">{{ $t('admin.match3.levelName') }}:</label>
|
|
<input
|
|
id="levelName"
|
|
v-model="levelForm.name"
|
|
type="text"
|
|
required
|
|
:placeholder="$t('admin.match3.levelName')"
|
|
>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="levelDescription">{{ $t('admin.match3.levelDescription') }}:</label>
|
|
<textarea
|
|
id="levelDescription"
|
|
v-model="levelForm.description"
|
|
required
|
|
:placeholder="$t('admin.match3.levelDescription')"
|
|
rows="3"
|
|
></textarea>
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label for="boardWidth">{{ $t('admin.match3.boardWidth') }}:</label>
|
|
<input
|
|
id="boardWidth"
|
|
v-model.number="levelForm.boardWidth"
|
|
type="number"
|
|
min="3"
|
|
max="12"
|
|
required
|
|
@change="updateBoardMatrix"
|
|
>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="boardHeight">{{ $t('admin.match3.boardHeight') }}:</label>
|
|
<input
|
|
id="boardHeight"
|
|
v-model.number="levelForm.boardHeight"
|
|
type="number"
|
|
min="3"
|
|
max="12"
|
|
required
|
|
@change="updateBoardMatrix"
|
|
>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="moveLimit">{{ $t('admin.match3.moveLimit') }}:</label>
|
|
<input
|
|
id="moveLimit"
|
|
v-model.number="levelForm.moveLimit"
|
|
type="number"
|
|
min="5"
|
|
max="100"
|
|
required
|
|
>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="levelOrder">{{ $t('admin.match3.levelOrder') }}:</label>
|
|
<input
|
|
id="levelOrder"
|
|
v-model.number="levelForm.order"
|
|
type="number"
|
|
min="1"
|
|
required
|
|
>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>{{ $t('admin.match3.boardLayout') }}:</label>
|
|
<div class="board-editor">
|
|
<div class="board-matrix" :style="boardMatrixStyle">
|
|
<div
|
|
v-for="(cell, index) in boardMatrix"
|
|
:key="index"
|
|
class="board-cell"
|
|
:class="{
|
|
'active': cell.active,
|
|
'inactive': !cell.active,
|
|
'random': cell.tileType === 'r',
|
|
'empty': cell.tileType === 'o',
|
|
'selected': selectedCellIndex === index
|
|
}"
|
|
@click="selectCell(index)"
|
|
>
|
|
<span v-if="cell.tileType === 'o'" class="cell-status">⬜</span>
|
|
<span v-else-if="cell.tileType === 'r'" class="cell-status">🎲</span>
|
|
<span v-else class="cell-status">{{ getTileSymbol(cell.tileType) }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Minimale Tile-Auswahl -->
|
|
<div v-if="selectedCellIndex !== null" class="tile-selection-minimal">
|
|
<span class="selection-label">Position {{ selectedCellIndex }}:</span>
|
|
<div class="tile-options-minimal">
|
|
<span class="tile-option-mini" @click="setTileType(selectedCellIndex, 'o')" title="Leer">⬜</span>
|
|
<span class="tile-option-mini" @click="setTileType(selectedCellIndex, 'r')" title="Zufällig">🎲</span>
|
|
<span
|
|
v-for="tileType in levelForm.tileTypes"
|
|
:key="tileType"
|
|
class="tile-option-mini"
|
|
@click="setTileType(selectedCellIndex, tileType)"
|
|
:title="tileType"
|
|
>{{ getTileSymbol(tileType) }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="board-controls">
|
|
<button type="button" class="btn btn-secondary" @click="fillAllActive">
|
|
{{ $t('admin.match3.boardControls.fillAll') }}
|
|
</button>
|
|
<button type="button" class="btn btn-secondary" @click="clearAll">
|
|
{{ $t('admin.match3.boardControls.clearAll') }}
|
|
</button>
|
|
<button type="button" class="btn btn-secondary" @click="invertBoard">
|
|
{{ $t('admin.match3.boardControls.invert') }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>{{ $t('admin.match3.tileTypes') }}:</label>
|
|
<div class="tile-types-selection">
|
|
<label v-for="tileType in availableTileTypes" :key="tileType" class="tile-type-checkbox">
|
|
<input
|
|
type="checkbox"
|
|
:value="tileType"
|
|
v-model="levelForm.tileTypes"
|
|
>
|
|
<span class="tile-symbol">{{ getTileSymbol(tileType) }}</span>
|
|
<span class="tile-name">{{ tileType }}</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-actions">
|
|
<button type="button" class="btn btn-danger" @click="deleteSelectedLevel">
|
|
{{ $t('admin.match3.delete') }}
|
|
</button>
|
|
<button type="button" class="btn btn-primary" @click="saveLevel">
|
|
{{ $t('admin.match3.update') }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Level Objectives Section (immer verfügbar) -->
|
|
<div v-if="selectedLevelId !== 'new'" class="objectives-section-container">
|
|
<div class="form-header">
|
|
<h3>{{ $t('admin.match3.levelObjectives') }}</h3>
|
|
</div>
|
|
|
|
<div class="objectives-section">
|
|
<div class="objectives-header">
|
|
<h4>{{ $t('admin.match3.objectivesTitle') }}</h4>
|
|
<button type="button" class="btn btn-secondary btn-sm" @click="addObjective">
|
|
{{ $t('admin.match3.addObjective') }}
|
|
</button>
|
|
</div>
|
|
|
|
<div v-if="levelForm.objectives && levelForm.objectives.length > 0" class="objectives-list">
|
|
<div v-for="(objective, index) in levelForm.objectives" :key="index" class="objective-item">
|
|
<div class="objective-header">
|
|
<span class="objective-number">#{{ index + 1 }}</span>
|
|
<button type="button" class="btn btn-danger btn-sm" @click="removeObjective(index)">
|
|
{{ $t('admin.match3.removeObjective') }}
|
|
</button>
|
|
</div>
|
|
|
|
<div class="objective-form">
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label>{{ $t('admin.match3.objectiveType') }}:</label>
|
|
<select v-model="objective.type" class="form-control">
|
|
<option value="score">{{ $t('admin.match3.objectiveTypeScore') }}</option>
|
|
<option value="matches">{{ $t('admin.match3.objectiveTypeMatches') }}</option>
|
|
<option value="moves">{{ $t('admin.match3.objectiveTypeMoves') }}</option>
|
|
<option value="time">{{ $t('admin.match3.objectiveTypeTime') }}</option>
|
|
<option value="special">{{ $t('admin.match3.objectiveTypeSpecial') }}</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>{{ $t('admin.match3.objectiveOperator') }}:</label>
|
|
<select v-model="objective.operator" class="form-control">
|
|
<option value=">=">{{ $t('admin.match3.operatorGreaterEqual') }}</option>
|
|
<option value="<=">{{ $t('admin.match3.operatorLessEqual') }}</option>
|
|
<option value="=">{{ $t('admin.match3.operatorEqual') }}</option>
|
|
<option value=">">{{ $t('admin.match3.operatorGreater') }}</option>
|
|
<option value="<">{{ $t('admin.match3.operatorLess') }}</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>{{ $t('admin.match3.objectiveTarget') }}:</label>
|
|
<input
|
|
v-model.number="objective.target"
|
|
type="number"
|
|
min="1"
|
|
class="form-control"
|
|
:placeholder="$t('admin.match3.objectiveTargetPlaceholder')"
|
|
>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>{{ $t('admin.match3.objectiveOrder') }}:</label>
|
|
<input
|
|
v-model.number="objective.order"
|
|
type="number"
|
|
min="1"
|
|
class="form-control"
|
|
:placeholder="$t('admin.match3.objectiveTargetPlaceholder')"
|
|
>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>{{ $t('admin.match3.objectiveDescription') }}:</label>
|
|
<input
|
|
v-model="objective.description"
|
|
type="text"
|
|
class="form-control"
|
|
:placeholder="$t('admin.match3.objectiveDescriptionPlaceholder')"
|
|
>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="checkbox-label">
|
|
<input
|
|
type="checkbox"
|
|
v-model="objective.isRequired"
|
|
>
|
|
{{ $t('admin.match3.objectiveRequired') }}
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-else class="no-objectives">
|
|
<p>{{ $t('admin.match3.noObjectives') }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Level Form (nur für neue Level) -->
|
|
<div v-if="selectedLevelId === 'new'" class="level-form">
|
|
<div class="form-header">
|
|
<h3>{{ $t('admin.match3.newLevel') }}</h3>
|
|
</div>
|
|
|
|
<form @submit.prevent="saveLevel">
|
|
<div class="form-group">
|
|
<label for="levelName">{{ $t('admin.match3.levelName') }}:</label>
|
|
<input
|
|
id="levelName"
|
|
v-model="levelForm.name"
|
|
type="text"
|
|
required
|
|
:placeholder="$t('admin.match3.levelName')"
|
|
>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="levelDescription">{{ $t('admin.match3.levelDescription') }}:</label>
|
|
<textarea
|
|
id="levelDescription"
|
|
v-model="levelForm.description"
|
|
required
|
|
:placeholder="$t('admin.match3.levelDescription')"
|
|
rows="3"
|
|
></textarea>
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label for="boardWidth">{{ $t('admin.match3.boardWidth') }}:</label>
|
|
<input
|
|
id="boardWidth"
|
|
v-model.number="levelForm.boardWidth"
|
|
type="number"
|
|
min="3"
|
|
max="12"
|
|
required
|
|
@change="updateBoardMatrix"
|
|
>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="boardHeight">{{ $t('admin.match3.boardHeight') }}:</label>
|
|
<input
|
|
id="boardHeight"
|
|
v-model.number="levelForm.boardHeight"
|
|
type="number"
|
|
min="3"
|
|
max="12"
|
|
required
|
|
@change="updateBoardMatrix"
|
|
>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="moveLimit">{{ $t('admin.match3.moveLimit') }}:</label>
|
|
<input
|
|
id="moveLimit"
|
|
v-model.number="levelForm.moveLimit"
|
|
type="number"
|
|
min="5"
|
|
max="100"
|
|
required
|
|
>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="levelOrder">{{ $t('admin.match3.levelOrder') }}:</label>
|
|
<input
|
|
id="levelOrder"
|
|
v-model.number="levelForm.order"
|
|
type="number"
|
|
min="1"
|
|
required
|
|
>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>{{ $t('admin.match3.boardLayout') }}:</label>
|
|
<div class="board-editor">
|
|
<div class="board-matrix" :style="boardMatrixStyle">
|
|
<div
|
|
v-for="(cell, index) in boardMatrix"
|
|
:key="index"
|
|
class="board-cell"
|
|
:class="{
|
|
'active': cell.active,
|
|
'inactive': !cell.active,
|
|
'random': cell.tileType === 'r',
|
|
'empty': cell.tileType === 'o',
|
|
'selected': selectedCellIndex === index
|
|
}"
|
|
@click="selectCell(index)"
|
|
>
|
|
<span v-if="cell.tileType === 'o'" class="cell-status">⬜</span>
|
|
<span v-else-if="cell.tileType === 'r'" class="cell-status">🎲</span>
|
|
<span v-else class="cell-status">{{ getTileSymbol(cell.tileType) }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Minimale Tile-Auswahl -->
|
|
<div v-if="selectedCellIndex !== null" class="tile-selection-minimal">
|
|
<span class="selection-label">Position {{ selectedCellIndex }}:</span>
|
|
<div class="tile-options-minimal">
|
|
<span class="tile-option-mini" @click="setTileType(selectedCellIndex, 'o')" title="Leer">⬜</span>
|
|
<span class="tile-option-mini" @click="setTileType(selectedCellIndex, 'r')" title="Zufällig">🎲</span>
|
|
<span
|
|
v-for="tileType in levelForm.tileTypes"
|
|
:key="tileType"
|
|
class="tile-option-mini"
|
|
@click="setTileType(selectedCellIndex, tileType)"
|
|
:title="tileType"
|
|
>{{ getTileSymbol(tileType) }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="board-controls">
|
|
<button type="button" class="btn btn-secondary" @click="fillAllActive">
|
|
{{ $t('admin.match3.boardControls.fillAll') }}
|
|
</button>
|
|
<button type="button" class="btn btn-secondary" @click="clearAll">
|
|
{{ $t('admin.match3.boardControls.clearAll') }}
|
|
</button>
|
|
<button type="button" class="btn btn-secondary" @click="invertBoard">
|
|
{{ $t('admin.match3.boardControls.invert') }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>{{ $t('admin.match3.tileTypes') }}:</label>
|
|
<div class="tile-types-selection">
|
|
<label v-for="tileType in availableTileTypes" :key="tileType" class="tile-type-checkbox">
|
|
<input
|
|
type="checkbox"
|
|
:value="tileType"
|
|
v-model="levelForm.tileTypes"
|
|
>
|
|
<span class="tile-symbol">{{ getTileSymbol(tileType) }}</span>
|
|
<span class="tile-name">{{ tileType }}</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Level Objectives Section -->
|
|
<div class="form-group">
|
|
<label>{{ $t('admin.match3.levelObjectives') }}:</label>
|
|
<div class="objectives-section">
|
|
<div class="objectives-header">
|
|
<h4>{{ $t('admin.match3.objectivesTitle') }}</h4>
|
|
<button type="button" class="btn btn-secondary btn-sm" @click="addObjective">
|
|
{{ $t('admin.match3.addObjective') }}
|
|
</button>
|
|
</div>
|
|
|
|
<div v-if="levelForm.objectives && levelForm.objectives.length > 0" class="objectives-list">
|
|
<div v-for="(objective, index) in levelForm.objectives" :key="index" class="objective-item">
|
|
<div class="objective-header">
|
|
<span class="objective-number">#{{ index + 1 }}</span>
|
|
<button type="button" class="btn btn-danger btn-sm" @click="removeObjective(index)">
|
|
{{ $t('admin.match3.removeObjective') }}
|
|
</button>
|
|
</div>
|
|
|
|
<div class="objective-form">
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label>{{ $t('admin.match3.objectiveType') }}:</label>
|
|
<select v-model="objective.type" class="form-control">
|
|
<option value="score">{{ $t('admin.match3.objectiveTypeScore') }}</option>
|
|
<option value="matches">{{ $t('admin.match3.objectiveTypeMatches') }}</option>
|
|
<option value="moves">{{ $t('admin.match3.objectiveTypeMoves') }}</option>
|
|
<option value="time">{{ $t('admin.match3.objectiveTypeTime') }}</option>
|
|
<option value="special">{{ $t('admin.match3.objectiveTypeSpecial') }}</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>{{ $t('admin.match3.objectiveOperator') }}:</label>
|
|
<select v-model="objective.operator" class="form-control">
|
|
<option value=">=">{{ $t('admin.match3.operatorGreaterEqual') }}</option>
|
|
<option value="<=">{{ $t('admin.match3.operatorLessEqual') }}</option>
|
|
<option value="=">{{ $t('admin.match3.operatorEqual') }}</option>
|
|
<option value=">">{{ $t('admin.match3.operatorGreater') }}</option>
|
|
<option value="<">{{ $t('admin.match3.operatorLess') }}</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>{{ $t('admin.match3.objectiveTarget') }}:</label>
|
|
<input
|
|
v-model.number="objective.target"
|
|
type="number"
|
|
min="1"
|
|
class="form-control"
|
|
:placeholder="$t('admin.match3.objectiveTargetPlaceholder')"
|
|
>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>{{ $t('admin.match3.objectiveOrder') }}:</label>
|
|
<input
|
|
v-model.number="objective.order"
|
|
type="number"
|
|
min="1"
|
|
class="form-control"
|
|
:placeholder="$t('admin.match3.objectiveOrderPlaceholder')"
|
|
>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>{{ $t('admin.match3.objectiveDescription') }}:</label>
|
|
<input
|
|
v-model="objective.description"
|
|
type="text"
|
|
class="form-control"
|
|
:placeholder="$t('admin.match3.objectiveDescriptionPlaceholder')"
|
|
>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="checkbox-label">
|
|
<input
|
|
type="checkbox"
|
|
v-model="objective.isRequired"
|
|
>
|
|
{{ $t('admin.match3.objectiveRequired') }}
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-else class="no-objectives">
|
|
<p>{{ $t('admin.match3.noObjectives') }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-actions">
|
|
<button type="button" class="btn btn-secondary" @click="cancelEdit">
|
|
{{ $t('admin.match3.cancel') }}
|
|
</button>
|
|
<button type="submit" class="btn btn-primary">
|
|
{{ $t('admin.match3.create') }}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import SimpleTabs from '../../components/SimpleTabs.vue';
|
|
import apiClient from '../../utils/axios.js';
|
|
|
|
export default {
|
|
name: 'AdminMinigamesView',
|
|
components: {
|
|
SimpleTabs
|
|
},
|
|
data() {
|
|
return {
|
|
activeTab: 'match3-levels',
|
|
tabs: [
|
|
{
|
|
value: 'match3-levels',
|
|
label: this.$t('admin.match3.title')
|
|
}
|
|
],
|
|
levels: [],
|
|
selectedLevelId: 'new',
|
|
editingLevel: null,
|
|
levelForm: {
|
|
name: '',
|
|
description: '',
|
|
boardWidth: 5,
|
|
boardHeight: 5,
|
|
moveLimit: 15,
|
|
order: 1,
|
|
tileTypes: ['gem', 'star', 'heart'],
|
|
objectives: []
|
|
},
|
|
boardMatrix: [],
|
|
availableTileTypes: ['gem', 'star', 'heart', 'diamond', 'circle', 'square', 'crown', 'rainbow'],
|
|
selectedCellIndex: null
|
|
};
|
|
},
|
|
computed: {
|
|
boardMatrixStyle() {
|
|
return {
|
|
gridTemplateColumns: `repeat(${this.levelForm.boardWidth}, 1fr)`,
|
|
gridTemplateRows: `repeat(${this.levelForm.boardHeight}, 1fr)`
|
|
};
|
|
},
|
|
|
|
selectedLevel() {
|
|
if (this.selectedLevelId === 'new') return null;
|
|
return this.levels.find(l => l.id === this.selectedLevelId);
|
|
}
|
|
},
|
|
|
|
watch: {
|
|
selectedLevelId: {
|
|
handler(newValue, oldValue) {
|
|
if (newValue !== oldValue) {
|
|
this.$nextTick(() => {
|
|
this.onLevelSelect();
|
|
});
|
|
}
|
|
},
|
|
immediate: true
|
|
}
|
|
},
|
|
mounted() {
|
|
// Explizite Initialisierung der Daten
|
|
this.levels = [];
|
|
this.editingLevel = null;
|
|
this.boardMatrix = [];
|
|
|
|
// Warte kurz, bis der Store geladen ist
|
|
this.$nextTick(() => {
|
|
this.loadLevels();
|
|
this.updateBoardMatrix();
|
|
});
|
|
},
|
|
|
|
methods: {
|
|
async loadLevels() {
|
|
try {
|
|
// Prüfe ob der Store geladen ist
|
|
const user = this.$store.getters.user;
|
|
if (!user || !user.authCode) {
|
|
setTimeout(() => this.loadLevels(), 100);
|
|
return;
|
|
}
|
|
|
|
const response = await apiClient.get('/api/admin/minigames/match3/levels');
|
|
this.levels = response.data;
|
|
} catch (error) {
|
|
console.error('Fehler beim Laden der Level:', error);
|
|
}
|
|
},
|
|
|
|
async onLevelSelect() {
|
|
if (this.selectedLevelId === 'new') {
|
|
this.createLevel();
|
|
} else {
|
|
const level = this.selectedLevel;
|
|
|
|
if (level) {
|
|
await this.editLevel(level);
|
|
} else {
|
|
console.warn('Kein Level gefunden für ID:', this.selectedLevelId);
|
|
}
|
|
}
|
|
},
|
|
|
|
createLevel() {
|
|
this.selectedLevelId = 'new';
|
|
this.editingLevel = null;
|
|
this.selectedCellIndex = null;
|
|
this.levelForm = {
|
|
name: '',
|
|
description: '',
|
|
boardWidth: 5,
|
|
boardHeight: 5,
|
|
moveLimit: 15,
|
|
order: 1,
|
|
tileTypes: ['gem', 'star', 'heart'],
|
|
objectives: []
|
|
};
|
|
this.updateBoardMatrix();
|
|
},
|
|
|
|
async editLevel(level) {
|
|
this.editingLevel = level;
|
|
this.selectedCellIndex = null;
|
|
this.levelForm = {
|
|
name: level.name,
|
|
description: level.description,
|
|
boardWidth: level.boardWidth,
|
|
boardHeight: level.boardHeight,
|
|
moveLimit: level.moveLimit,
|
|
order: level.order,
|
|
tileTypes: level.tileTypes || ['gem', 'star', 'heart'],
|
|
objectives: level.objectives || []
|
|
};
|
|
|
|
// Lade Objectives für dieses Level
|
|
try {
|
|
const objectives = await this.loadObjectivesForLevel(level.id);
|
|
this.levelForm.objectives = objectives;
|
|
} catch (error) {
|
|
console.error('Fehler beim Laden der Objectives:', error);
|
|
this.levelForm.objectives = [];
|
|
}
|
|
|
|
if (level.boardLayout) {
|
|
this.createBoardMatrixFromLayout(level.boardLayout);
|
|
} else {
|
|
this.updateBoardMatrix();
|
|
}
|
|
},
|
|
|
|
async deleteSelectedLevel() {
|
|
if (this.selectedLevelId && this.selectedLevelId !== 'new') {
|
|
await this.deleteLevel(this.selectedLevelId);
|
|
this.selectedLevelId = 'new';
|
|
}
|
|
},
|
|
|
|
getTileSymbol(type) {
|
|
const symbols = {
|
|
gem: '💎',
|
|
star: '⭐',
|
|
heart: '❤️',
|
|
diamond: '🔷',
|
|
circle: '⭕',
|
|
square: '🟦',
|
|
crown: '👑',
|
|
rainbow: '🌈'
|
|
};
|
|
return symbols[type] || '❓';
|
|
},
|
|
|
|
// Neue Methoden für das Tile-Auswahl-System
|
|
selectCell(index) {
|
|
this.selectedCellIndex = this.selectedCellIndex === index ? null : index;
|
|
},
|
|
|
|
setTileType(index, tileType) {
|
|
console.log('setTileType called with:', index, tileType);
|
|
if (tileType === 'o') {
|
|
// Leer
|
|
this.boardMatrix[index] = { active: false, tileType: 'o', index: index };
|
|
} else if (tileType === 'r') {
|
|
// Zufällig
|
|
this.boardMatrix[index] = { active: true, tileType: 'r', index: index };
|
|
console.log('Set random tile at index:', index, this.boardMatrix[index]);
|
|
} else {
|
|
// Spezifischer Tile-Typ
|
|
this.boardMatrix[index] = { active: true, tileType: tileType, index: index };
|
|
}
|
|
this.selectedCellIndex = null; // Auswahl aufheben
|
|
console.log('Board matrix after update:', this.boardMatrix);
|
|
},
|
|
|
|
// Mapping für Tile-Typen zu Zeichen
|
|
getTileTypeChar(tileType) {
|
|
const charMap = {
|
|
'gem': 'g',
|
|
'star': 's',
|
|
'heart': 'h',
|
|
'diamond': 'd',
|
|
'circle': 'c',
|
|
'square': 'q',
|
|
'crown': 'w',
|
|
'rainbow': 'b'
|
|
};
|
|
return charMap[tileType] || 'x';
|
|
},
|
|
|
|
// Konvertiert das Board für das Speichern
|
|
convertBoardForSave() {
|
|
return this.boardMatrix.map(cell => {
|
|
if (cell.tileType === 'o') return 'o'; // Leer
|
|
if (cell.tileType === 'r') return 'x'; // Zufällig wird als "x" gespeichert
|
|
return this.getTileTypeChar(cell.tileType); // Spezifischer Tile-Typ
|
|
});
|
|
},
|
|
|
|
cancelEdit() {
|
|
this.editingLevel = null;
|
|
this.selectedLevelId = 'new';
|
|
this.selectedCellIndex = null;
|
|
this.levelForm = {
|
|
name: '',
|
|
description: '',
|
|
boardWidth: 5,
|
|
boardHeight: 5,
|
|
moveLimit: 15,
|
|
order: 1,
|
|
tileTypes: ['gem', 'star', 'heart'],
|
|
objectives: []
|
|
};
|
|
this.updateBoardMatrix();
|
|
console.log('Bearbeitung abgebrochen, Objectives zurückgesetzt:', this.levelForm.objectives);
|
|
},
|
|
|
|
updateBoardMatrix() {
|
|
const totalCells = this.levelForm.boardWidth * this.levelForm.boardHeight;
|
|
this.boardMatrix = [];
|
|
|
|
for (let i = 0; i < totalCells; i++) {
|
|
this.boardMatrix.push({ active: false, tileType: 'o', index: i });
|
|
}
|
|
},
|
|
|
|
createBoardMatrixFromLayout(layout) {
|
|
const lines = layout.split('\n');
|
|
this.levelForm.boardHeight = lines.length;
|
|
this.levelForm.boardWidth = lines[0]?.length || 5;
|
|
|
|
this.boardMatrix = [];
|
|
let index = 0;
|
|
|
|
for (let row = 0; row < lines.length; row++) {
|
|
const line = lines[row];
|
|
for (let col = 0; col < line.length; col++) {
|
|
const char = line[col];
|
|
let tileType = 'o';
|
|
let active = false;
|
|
|
|
if (char === 'x') {
|
|
active = true;
|
|
tileType = 'r'; // x wird als zufällig interpretiert
|
|
} else if (char === 'r') {
|
|
active = true;
|
|
tileType = 'r';
|
|
} else if (char === 'g') {
|
|
active = true;
|
|
tileType = 'gem';
|
|
} else if (char === 's') {
|
|
active = true;
|
|
tileType = 'star';
|
|
} else if (char === 'h') {
|
|
active = true;
|
|
tileType = 'heart';
|
|
} else if (char === 'd') {
|
|
active = true;
|
|
tileType = 'diamond';
|
|
} else if (char === 'c') {
|
|
active = true;
|
|
tileType = 'circle';
|
|
} else if (char === 'q') {
|
|
active = true;
|
|
tileType = 'square';
|
|
} else if (char === 'w') {
|
|
active = true;
|
|
tileType = 'crown';
|
|
} else if (char === 'b') {
|
|
active = true;
|
|
tileType = 'rainbow';
|
|
}
|
|
|
|
this.boardMatrix.push({
|
|
active: active,
|
|
tileType: tileType,
|
|
index: index++
|
|
});
|
|
}
|
|
}
|
|
},
|
|
|
|
fillAllActive() {
|
|
this.boardMatrix.forEach(cell => {
|
|
cell.active = true;
|
|
cell.tileType = 'r'; // Alle auf zufällig setzen
|
|
});
|
|
},
|
|
|
|
clearAll() {
|
|
this.boardMatrix.forEach(cell => {
|
|
cell.active = false;
|
|
cell.tileType = 'o';
|
|
});
|
|
},
|
|
|
|
invertBoard() {
|
|
this.boardMatrix.forEach(cell => {
|
|
cell.active = !cell.active;
|
|
if (cell.active && cell.tileType === 'o') {
|
|
cell.tileType = 'r'; // Wenn aktiviert, auf zufällig setzen
|
|
} else if (!cell.active) {
|
|
cell.tileType = 'o';
|
|
}
|
|
});
|
|
},
|
|
|
|
generateBoardLayout() {
|
|
let layout = '';
|
|
for (let row = 0; row < this.levelForm.boardHeight; row++) {
|
|
for (let col = 0; col < this.levelForm.boardWidth; col++) {
|
|
const index = row * this.levelForm.boardWidth + col;
|
|
const cell = this.boardMatrix[index];
|
|
if (cell.tileType === 'o') {
|
|
layout += 'o'; // Leer
|
|
} else if (cell.tileType === 'r') {
|
|
layout += 'x'; // Zufällig wird als "x" gespeichert
|
|
} else {
|
|
layout += this.getTileTypeChar(cell.tileType); // Spezifischer Tile-Typ
|
|
}
|
|
}
|
|
if (row < this.levelForm.boardHeight - 1) {
|
|
layout += '\n';
|
|
}
|
|
}
|
|
return layout;
|
|
},
|
|
|
|
async saveLevel() {
|
|
try {
|
|
const levelData = {
|
|
...this.levelForm,
|
|
boardLayout: this.generateBoardLayout()
|
|
};
|
|
|
|
let savedLevel;
|
|
if (this.selectedLevelId !== 'new') {
|
|
// Level aktualisieren
|
|
const response = await apiClient.put(`/api/admin/minigames/match3/levels/${this.selectedLevelId}`, levelData);
|
|
savedLevel = response.data;
|
|
} else {
|
|
// Neues Level erstellen
|
|
const response = await apiClient.post('/api/admin/minigames/match3/levels', levelData);
|
|
savedLevel = response.data;
|
|
}
|
|
|
|
// Objectives speichern, falls vorhanden
|
|
if (this.levelForm.objectives && this.levelForm.objectives.length > 0) {
|
|
for (const objective of this.levelForm.objectives) {
|
|
const objectiveData = {
|
|
...objective,
|
|
levelId: savedLevel.id
|
|
};
|
|
|
|
if (objective.id) {
|
|
// Objective aktualisieren
|
|
await apiClient.put(`/api/admin/minigames/match3/objectives/${objective.id}`, objectiveData);
|
|
} else {
|
|
// Neues Objective erstellen
|
|
await apiClient.post('/api/admin/minigames/match3/objectives', objectiveData);
|
|
}
|
|
}
|
|
}
|
|
|
|
this.editingLevel = null;
|
|
this.selectedLevelId = 'new';
|
|
this.selectedCellIndex = null;
|
|
this.loadLevels();
|
|
} catch (error) {
|
|
console.error('Fehler beim Speichern des Levels:', error);
|
|
alert('Fehler beim Speichern des Levels');
|
|
}
|
|
},
|
|
|
|
async deleteLevel(levelId) {
|
|
if (confirm('Möchtest du dieses Level wirklich löschen?')) {
|
|
try {
|
|
await apiClient.delete(`/api/admin/minigames/match3/levels/${levelId}`);
|
|
this.loadLevels();
|
|
} catch (error) {
|
|
console.error('Fehler beim Löschen des Levels:', error);
|
|
}
|
|
}
|
|
},
|
|
|
|
// Objectives Management Methods
|
|
addObjective() {
|
|
if (!this.levelForm.objectives) {
|
|
this.levelForm.objectives = [];
|
|
}
|
|
|
|
const newObjective = {
|
|
type: 'score',
|
|
description: '',
|
|
target: 100,
|
|
operator: '>=',
|
|
order: this.levelForm.objectives.length + 1,
|
|
isRequired: true
|
|
};
|
|
|
|
this.levelForm.objectives.push(newObjective);
|
|
},
|
|
|
|
removeObjective(index) {
|
|
if (confirm('Möchtest du dieses Objective wirklich löschen?')) {
|
|
this.levelForm.objectives.splice(index, 1);
|
|
|
|
// Aktualisiere die Reihenfolge
|
|
this.levelForm.objectives.forEach((objective, idx) => {
|
|
objective.order = idx + 1;
|
|
});
|
|
}
|
|
},
|
|
|
|
async loadObjectivesForLevel(levelId) {
|
|
try {
|
|
// Objectives sind jetzt direkt in den Level-Daten enthalten
|
|
const level = this.levels.find(l => l.id == levelId);
|
|
if (level && level.objectives && Array.isArray(level.objectives)) {
|
|
return level.objectives;
|
|
}
|
|
|
|
return [];
|
|
} catch (error) {
|
|
console.error('Fehler beim Laden der Objectives:', error);
|
|
return [];
|
|
}
|
|
}
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<style scoped>
|
|
.admin-header {
|
|
text-align: center;
|
|
margin-bottom: 30px;
|
|
}
|
|
|
|
.admin-header h1 {
|
|
color: #F9A22C;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.admin-header p {
|
|
color: #666;
|
|
font-size: 16px;
|
|
}
|
|
|
|
.match3-admin {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
}
|
|
|
|
.section-header {
|
|
margin-bottom: 30px;
|
|
text-align: center;
|
|
}
|
|
|
|
.section-header h2 {
|
|
color: #F9A22C;
|
|
font-size: 24px;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
/* Level Selection */
|
|
.level-selection {
|
|
margin-bottom: 30px;
|
|
text-align: center;
|
|
}
|
|
|
|
.level-count {
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.level-count p {
|
|
font-size: 18px;
|
|
color: #333;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.level-dropdown {
|
|
display: flex;
|
|
justify-content: center;
|
|
}
|
|
|
|
.level-select {
|
|
padding: 10px 15px;
|
|
font-size: 16px;
|
|
border: 2px solid #ddd;
|
|
border-radius: 8px;
|
|
background: white;
|
|
min-width: 300px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.level-select:focus {
|
|
outline: none;
|
|
border-color: #F9A22C;
|
|
}
|
|
|
|
/* Level Details & Form */
|
|
.level-details,
|
|
.level-form {
|
|
background: white;
|
|
border: 2px solid #ddd;
|
|
border-radius: 12px;
|
|
padding: 25px;
|
|
margin-bottom: 30px;
|
|
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.details-header,
|
|
.form-header {
|
|
margin-bottom: 25px;
|
|
text-align: center;
|
|
}
|
|
|
|
.details-header h3,
|
|
.form-header h3 {
|
|
color: #F9A22C;
|
|
font-size: 22px;
|
|
margin: 0;
|
|
}
|
|
|
|
.form-group {
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.form-group label {
|
|
display: block;
|
|
margin-bottom: 8px;
|
|
font-weight: 600;
|
|
color: #333;
|
|
font-size: 16px;
|
|
}
|
|
|
|
.form-group input,
|
|
.form-group textarea {
|
|
width: 100%;
|
|
padding: 12px;
|
|
border: 2px solid #ddd;
|
|
border-radius: 8px;
|
|
font-size: 16px;
|
|
transition: border-color 0.3s ease;
|
|
}
|
|
|
|
.form-group input:focus,
|
|
.form-group textarea:focus {
|
|
outline: none;
|
|
border-color: #F9A22C;
|
|
}
|
|
|
|
.form-row {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: 20px;
|
|
}
|
|
|
|
/* Board Editor */
|
|
.board-editor {
|
|
margin-top: 10px;
|
|
}
|
|
|
|
.board-matrix {
|
|
display: grid;
|
|
gap: 2px;
|
|
margin-bottom: 15px;
|
|
border: 2px solid #333;
|
|
padding: 10px;
|
|
background: #f5f5f5;
|
|
}
|
|
|
|
.board-cell {
|
|
width: 40px;
|
|
height: 40px;
|
|
border: 1px solid #ccc;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
cursor: pointer;
|
|
background: white;
|
|
transition: all 0.2s ease;
|
|
position: relative;
|
|
}
|
|
|
|
.board-cell:hover {
|
|
border-color: #F9A22C;
|
|
transform: scale(1.05);
|
|
}
|
|
|
|
.board-cell.selected {
|
|
border: 3px solid #ff0000;
|
|
box-shadow: 0 0 10px rgba(255, 0, 0, 0.5);
|
|
}
|
|
|
|
.board-cell.active {
|
|
background: #4CAF50;
|
|
color: white;
|
|
border-color: #45a049;
|
|
}
|
|
|
|
.board-cell.inactive {
|
|
background: #f44336;
|
|
color: white;
|
|
border-color: #da190b;
|
|
}
|
|
|
|
.board-cell.random {
|
|
background: #FF9800;
|
|
color: white;
|
|
border-color: #F57C00;
|
|
}
|
|
|
|
.board-cell.empty {
|
|
background: #9E9E9E;
|
|
color: white;
|
|
border-color: #757575;
|
|
}
|
|
|
|
.cell-status {
|
|
font-size: 16px;
|
|
font-weight: bold;
|
|
}
|
|
|
|
/* Minimale Tile Selection */
|
|
.tile-selection-minimal {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 15px;
|
|
margin: 10px 0;
|
|
padding: 10px 15px;
|
|
background: #f0f0f0;
|
|
border-radius: 6px;
|
|
border: 1px solid #ddd;
|
|
}
|
|
|
|
.selection-label {
|
|
font-size: 14px;
|
|
color: #666;
|
|
font-weight: 500;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.tile-options-minimal {
|
|
display: flex;
|
|
gap: 8px;
|
|
align-items: center;
|
|
}
|
|
|
|
.tile-option-mini {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 32px;
|
|
height: 32px;
|
|
cursor: pointer;
|
|
border: 2px solid #ddd;
|
|
border-radius: 4px;
|
|
background: white;
|
|
transition: all 0.2s ease;
|
|
font-size: 18px;
|
|
}
|
|
|
|
.tile-option-mini:hover {
|
|
border-color: #F9A22C;
|
|
transform: scale(1.1);
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.board-controls {
|
|
display: flex;
|
|
gap: 10px;
|
|
justify-content: center;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.board-controls .btn {
|
|
padding: 8px 16px;
|
|
font-size: 14px;
|
|
}
|
|
|
|
/* Tile Types Selector */
|
|
.tile-types-selection {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
|
gap: 10px;
|
|
}
|
|
|
|
.tile-type-checkbox {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
cursor: pointer;
|
|
padding: 8px;
|
|
border: 1px solid #ddd;
|
|
border-radius: 4px;
|
|
background: #f9f9f9;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.tile-type-checkbox:hover {
|
|
background: #f0f0f0;
|
|
border-color: #F9A22C;
|
|
}
|
|
|
|
.tile-type-checkbox input[type="checkbox"] {
|
|
width: auto;
|
|
margin: 0;
|
|
}
|
|
|
|
.tile-symbol {
|
|
font-size: 20px;
|
|
margin-right: 8px;
|
|
}
|
|
|
|
.tile-name {
|
|
font-size: 14px;
|
|
color: #333;
|
|
font-weight: 500;
|
|
}
|
|
|
|
/* Objectives Section Container */
|
|
.objectives-section-container {
|
|
background: white;
|
|
border: 2px solid #ddd;
|
|
border-radius: 12px;
|
|
padding: 25px;
|
|
margin-bottom: 30px;
|
|
}
|
|
|
|
.objectives-section-container .form-header {
|
|
margin-bottom: 20px;
|
|
text-align: center;
|
|
}
|
|
|
|
.objectives-section-container .form-header h3 {
|
|
color: #F9A22C;
|
|
font-size: 24px;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
/* Objectives Section Styles */
|
|
.objectives-section {
|
|
margin-top: 20px;
|
|
padding: 20px;
|
|
border: 1px solid #ddd;
|
|
border-radius: 8px;
|
|
background: #f9f9f9;
|
|
}
|
|
|
|
.objectives-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 20px;
|
|
padding-bottom: 15px;
|
|
border-bottom: 1px solid #ddd;
|
|
}
|
|
|
|
.objectives-header h4 {
|
|
margin: 0;
|
|
color: #F9A22C;
|
|
font-size: 18px;
|
|
}
|
|
|
|
.btn-sm {
|
|
padding: 8px 16px;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.objectives-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 20px;
|
|
}
|
|
|
|
.objective-item {
|
|
background: white;
|
|
border: 1px solid #ddd;
|
|
border-radius: 8px;
|
|
padding: 20px;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.objective-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 15px;
|
|
padding-bottom: 10px;
|
|
border-bottom: 1px solid #eee;
|
|
}
|
|
|
|
.objective-number {
|
|
font-weight: 600;
|
|
color: #F9A22C;
|
|
font-size: 16px;
|
|
}
|
|
|
|
.objective-form .form-row {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
gap: 15px;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.objective-form .form-group {
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.objective-form label {
|
|
display: block;
|
|
margin-bottom: 5px;
|
|
font-weight: 500;
|
|
color: #333;
|
|
}
|
|
|
|
.objective-form .form-control {
|
|
width: 100%;
|
|
padding: 8px 12px;
|
|
border: 1px solid #ddd;
|
|
border-radius: 4px;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.objective-form .form-control:focus {
|
|
outline: none;
|
|
border-color: #F9A22C;
|
|
}
|
|
|
|
.checkbox-label {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.checkbox-label input[type="checkbox"] {
|
|
width: auto;
|
|
margin: 0;
|
|
}
|
|
|
|
.no-objectives {
|
|
text-align: center;
|
|
padding: 30px;
|
|
color: #666;
|
|
font-style: italic;
|
|
}
|
|
|
|
/* Form Actions */
|
|
.form-actions {
|
|
display: flex;
|
|
gap: 15px;
|
|
justify-content: center;
|
|
margin-top: 30px;
|
|
padding-top: 20px;
|
|
border-top: 1px solid #eee;
|
|
}
|
|
|
|
.btn {
|
|
padding: 12px 24px;
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
border: none;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
text-decoration: none;
|
|
display: inline-block;
|
|
text-align: center;
|
|
}
|
|
|
|
.btn-primary {
|
|
background: #F9A22C;
|
|
color: white;
|
|
}
|
|
|
|
.btn-primary:hover {
|
|
background: #e8941f;
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.btn-secondary {
|
|
background: #6c757d;
|
|
color: white;
|
|
}
|
|
|
|
.btn-secondary:hover {
|
|
background: #5a6268;
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.btn-danger {
|
|
background: #dc3545;
|
|
color: white;
|
|
}
|
|
|
|
.btn-danger:hover {
|
|
background: #c82333;
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
/* Responsive Design */
|
|
@media (max-width: 768px) {
|
|
.match3-admin {
|
|
padding: 15px;
|
|
}
|
|
|
|
.form-row {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.board-controls {
|
|
flex-direction: column;
|
|
align-items: center;
|
|
}
|
|
|
|
.form-actions {
|
|
flex-direction: column;
|
|
align-items: center;
|
|
}
|
|
|
|
.level-select {
|
|
min-width: 250px;
|
|
}
|
|
|
|
.tile-selection-minimal {
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
gap: 10px;
|
|
}
|
|
|
|
.selection-label {
|
|
font-size: 12px;
|
|
}
|
|
|
|
.tile-option-mini {
|
|
width: 28px;
|
|
height: 28px;
|
|
font-size: 16px;
|
|
}
|
|
}
|
|
</style>
|