Initial commit
This commit is contained in:
79
src/components/Cell.vue
Normal file
79
src/components/Cell.vue
Normal file
@@ -0,0 +1,79 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
state: {
|
||||
type: Number,
|
||||
required: true,
|
||||
validator: (v) => [0, 1, 2].includes(v)
|
||||
},
|
||||
r: Number,
|
||||
c: Number
|
||||
});
|
||||
|
||||
const emit = defineEmits(['start-drag', 'enter-cell']);
|
||||
|
||||
const cellClass = computed(() => {
|
||||
switch (props.state) {
|
||||
case 1: return 'filled';
|
||||
case 2: return 'cross';
|
||||
default: return 'empty';
|
||||
}
|
||||
});
|
||||
|
||||
const handleMouseDown = (e) => {
|
||||
// 0 = left, 2 = right
|
||||
if (e.button === 0) emit('start-drag', props.r, props.c, false);
|
||||
if (e.button === 2) emit('start-drag', props.r, props.c, true);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="cell"
|
||||
:class="cellClass"
|
||||
@mousedown.prevent="handleMouseDown"
|
||||
@mouseenter="emit('enter-cell', props.r, props.c)"
|
||||
@contextmenu.prevent
|
||||
>
|
||||
<span v-if="props.state === 2" class="cross-mark">×</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.cell {
|
||||
width: var(--cell-size);
|
||||
height: var(--cell-size);
|
||||
background-color: var(--cell-empty);
|
||||
border: 1px solid var(--glass-border);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transition: background-color 0.1s ease, box-shadow 0.1s ease;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.cell:hover {
|
||||
background-color: var(--cell-hover);
|
||||
box-shadow: 0 0 10px rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.cell.filled {
|
||||
background: var(--cell-filled-gradient);
|
||||
box-shadow: 0 0 15px var(--accent-cyan);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.cell.cross {
|
||||
color: var(--cell-x-color);
|
||||
font-size: 1.5rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* Guide Lines Logic (handled via CSS classes passed from parent usually, but here simpler to use nth-child or props) */
|
||||
/* Actually, user wants guide lines every 5 cells.
|
||||
We can do this in GameBoard via classes on cells or border manipulation.
|
||||
Let's do it in GameBoard style or pass a prop 'isGuideRight', 'isGuideBottom'.
|
||||
*/
|
||||
</style>
|
||||
129
src/components/CustomGameModal.vue
Normal file
129
src/components/CustomGameModal.vue
Normal file
@@ -0,0 +1,129 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { usePuzzleStore } from '@/stores/puzzle';
|
||||
|
||||
const emit = defineEmits(['close']);
|
||||
const store = usePuzzleStore();
|
||||
|
||||
const customSize = ref(10);
|
||||
const errorMsg = ref('');
|
||||
|
||||
const confirm = () => {
|
||||
const size = parseInt(customSize.value);
|
||||
if (isNaN(size) || size < 5 || size > 100) {
|
||||
errorMsg.value = 'Rozmiar musi być między 5 a 100!';
|
||||
return;
|
||||
}
|
||||
|
||||
store.initCustomGame(size);
|
||||
emit('close');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="modal-overlay" @click.self="emit('close')">
|
||||
<div class="modal glass-panel">
|
||||
<h2>GRA WŁASNA</h2>
|
||||
<p>Wprowadź rozmiar siatki (5 - 100):</p>
|
||||
|
||||
<div class="input-group">
|
||||
<input
|
||||
type="number"
|
||||
v-model="customSize"
|
||||
min="5"
|
||||
max="100"
|
||||
@keyup.enter="confirm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p v-if="errorMsg" class="error">{{ errorMsg }}</p>
|
||||
|
||||
<div class="actions">
|
||||
<button class="btn-neon secondary" @click="emit('close')">Anuluj</button>
|
||||
<button class="btn-neon" @click="confirm">Start</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
backdrop-filter: blur(5px);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 2000;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
.modal {
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
max-width: 400px;
|
||||
width: 90%;
|
||||
border: 1px solid var(--accent-cyan);
|
||||
box-shadow: 0 0 50px rgba(0, 242, 255, 0.2);
|
||||
animation: slideUp 0.3s ease;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
color: var(--accent-cyan);
|
||||
margin: 0 0 20px 0;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: var(--text-color);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
input {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid var(--glass-border);
|
||||
color: #fff;
|
||||
padding: 10px;
|
||||
font-size: 1.2rem;
|
||||
border-radius: 8px;
|
||||
width: 100px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-cyan);
|
||||
box-shadow: 0 0 10px rgba(0, 242, 255, 0.3);
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #ff4d4d;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from { transform: translateY(50px); opacity: 0; }
|
||||
to { transform: translateY(0); opacity: 1; }
|
||||
}
|
||||
</style>
|
||||
136
src/components/FixedBar.vue
Normal file
136
src/components/FixedBar.vue
Normal file
@@ -0,0 +1,136 @@
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { usePuzzleStore } from '@/stores/puzzle';
|
||||
import { useTimer } from '@/composables/useTimer';
|
||||
|
||||
const store = usePuzzleStore();
|
||||
const { formatTime } = useTimer();
|
||||
|
||||
const isVisible = ref(false);
|
||||
const isProgressVisible = ref(true); // Toggle for progress percentage
|
||||
|
||||
// Logic to show bar on scroll
|
||||
window.addEventListener('scroll', () => {
|
||||
isVisible.value = window.scrollY > 100;
|
||||
});
|
||||
|
||||
const progressText = computed(() => {
|
||||
const percent = store.progressPercentage;
|
||||
if (typeof percent !== 'number') return '0.0%';
|
||||
return isProgressVisible.value
|
||||
? `${percent.toFixed(1)}%`
|
||||
: '???';
|
||||
});
|
||||
|
||||
const formattedTime = computed(() => formatTime(store.elapsedTime));
|
||||
|
||||
const toggleVisibility = () => {
|
||||
isProgressVisible.value = !isProgressVisible.value;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="fixed-bar" :class="{ visible: isVisible }">
|
||||
<div class="fixed-content">
|
||||
<div class="fixed-stat">
|
||||
<span>Czas:</span>
|
||||
<span>{{ formattedTime }}</span>
|
||||
</div>
|
||||
|
||||
<div class="fixed-stat">
|
||||
<span>Postęp:</span>
|
||||
<span style="min-width: 60px; text-align: right;">{{ progressText }}</span>
|
||||
<button class="btn-eye" @click="toggleVisibility" :title="isProgressVisible ? 'Ukryj' : 'Pokaż'">
|
||||
<span v-if="isProgressVisible">👁️</span>
|
||||
<span v-else>🔒</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="progress-line-container">
|
||||
<div class="progress-line-fill" :style="{ width: `${store.progressPercentage || 0}%` }"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
#fixed-bar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 60px;
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
backdrop-filter: blur(10px);
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
transform: translateY(-100%);
|
||||
transition: transform 0.3s ease;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
#fixed-bar.visible {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.fixed-content {
|
||||
display: flex;
|
||||
gap: 40px;
|
||||
align-items: center;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 300;
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.fixed-stat {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.fixed-stat span:first-child {
|
||||
opacity: 0.7;
|
||||
font-size: 0.9rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.progress-line-container {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.progress-line-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--accent-cyan), var(--accent-purple));
|
||||
width: 0%;
|
||||
transition: width 0.3s ease;
|
||||
box-shadow: 0 0 10px var(--accent-cyan);
|
||||
}
|
||||
|
||||
.btn-eye {
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 1.2rem;
|
||||
padding: 0 5px;
|
||||
color: var(--text-color);
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.btn-eye:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
47
src/components/GameActions.vue
Normal file
47
src/components/GameActions.vue
Normal file
@@ -0,0 +1,47 @@
|
||||
<script setup>
|
||||
import { usePuzzleStore } from '@/stores/puzzle';
|
||||
|
||||
const store = usePuzzleStore();
|
||||
|
||||
function handleNewRandom() {
|
||||
// If currently custom, regenerate custom.
|
||||
// If not custom, switch to custom with current size?
|
||||
// Or maybe just re-init current level if it's not custom?
|
||||
// "NOWA LOSOWA" implies random.
|
||||
// If user is on Easy/Medium/Hard, "Random" might mean "Random predefined" or "Random generated".
|
||||
// Let's assume it generates a new random grid of current size.
|
||||
store.initCustomGame(store.size);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="game-actions">
|
||||
<button class="btn-neon secondary" @click="store.resetGame">RESET</button>
|
||||
<button class="btn-neon secondary" @click="handleNewRandom">NOWA LOSOWA</button>
|
||||
<button class="btn-neon secondary" @click="store.undo">COFNIJ</button>
|
||||
<button class="btn-neon secondary" @click="store.checkWin">SPRAWDŹ</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.game-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
justify-content: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.btn-neon.secondary {
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-size: 0.9rem;
|
||||
padding: 10px 25px;
|
||||
}
|
||||
|
||||
.btn-neon.secondary:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-color: #fff;
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
107
src/components/GameBoard.vue
Normal file
107
src/components/GameBoard.vue
Normal file
@@ -0,0 +1,107 @@
|
||||
<script setup>
|
||||
import { onMounted, onUnmounted, computed } from 'vue';
|
||||
import { usePuzzleStore } from '@/stores/puzzle';
|
||||
import { useHints } from '@/composables/useHints';
|
||||
import { useNonogram } from '@/composables/useNonogram';
|
||||
import Cell from './Cell.vue';
|
||||
import Hints from './Hints.vue';
|
||||
|
||||
const store = usePuzzleStore();
|
||||
const { rowHints, colHints } = useHints(computed(() => store.solution));
|
||||
const { startDrag, onMouseEnter, stopDrag } = useNonogram();
|
||||
|
||||
// Global mouseup to stop dragging even if mouse leaves grid
|
||||
const handleGlobalMouseUp = () => {
|
||||
stopDrag();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('mouseup', handleGlobalMouseUp);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('mouseup', handleGlobalMouseUp);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="game-board-wrapper">
|
||||
<div class="game-container">
|
||||
<div class="corner-spacer"></div>
|
||||
|
||||
<!-- Column Hints -->
|
||||
<Hints :hints="colHints" orientation="col" />
|
||||
|
||||
<!-- Row Hints -->
|
||||
<Hints :hints="rowHints" orientation="row" />
|
||||
|
||||
<!-- Grid -->
|
||||
<div
|
||||
class="grid"
|
||||
:style="{
|
||||
gridTemplateColumns: `repeat(${store.size}, var(--cell-size))`,
|
||||
gridTemplateRows: `repeat(${store.size}, var(--cell-size))`
|
||||
}"
|
||||
@mouseleave="stopDrag"
|
||||
>
|
||||
<template v-for="(row, r) in store.playerGrid" :key="r">
|
||||
<Cell
|
||||
v-for="(state, c) in row"
|
||||
:key="`${r}-${c}`"
|
||||
:state="state"
|
||||
:r="r"
|
||||
:c="c"
|
||||
:class="{
|
||||
'guide-right': (c + 1) % 5 === 0 && c !== store.size - 1,
|
||||
'guide-bottom': (r + 1) % 5 === 0 && r !== store.size - 1
|
||||
}"
|
||||
@start-drag="startDrag"
|
||||
@enter-cell="onMouseEnter"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.game-board-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.game-container {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
grid-template-rows: auto auto;
|
||||
gap: 0;
|
||||
padding: 20px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 0 30px rgba(0, 0, 0, 0.3);
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.corner-spacer {
|
||||
width: 100px; /* Must match Row Hints width */
|
||||
height: auto; /* Adapts to Col Hints height */
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
gap: var(--gap-size);
|
||||
padding: 5px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Guide Lines */
|
||||
:deep(.cell.guide-right) {
|
||||
border-right: 2px solid rgba(0, 242, 255, 0.5) !important;
|
||||
}
|
||||
|
||||
:deep(.cell.guide-bottom) {
|
||||
border-bottom: 2px solid rgba(0, 242, 255, 0.5) !important;
|
||||
}
|
||||
</style>
|
||||
73
src/components/GuidePanel.vue
Normal file
73
src/components/GuidePanel.vue
Normal file
@@ -0,0 +1,73 @@
|
||||
<script setup>
|
||||
import { useSolver } from '@/composables/useSolver';
|
||||
|
||||
const {
|
||||
isPlaying,
|
||||
speedLabel,
|
||||
statusText,
|
||||
step,
|
||||
togglePlay,
|
||||
changeSpeed
|
||||
} = useSolver();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="guide-panel">
|
||||
<div class="status-text">{{ statusText }}</div>
|
||||
|
||||
<div class="guide-controls">
|
||||
<button class="btn-neon small" @click="togglePlay" :class="{ active: isPlaying }">
|
||||
{{ isPlaying ? 'PAUSE' : 'PLAY' }}
|
||||
</button>
|
||||
|
||||
<button class="btn-neon small" @click="step" :disabled="isPlaying">
|
||||
STEP
|
||||
</button>
|
||||
|
||||
<button class="btn-neon small" @click="changeSpeed">
|
||||
SPEED: {{ speedLabel }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.guide-panel {
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
border: 1px solid rgba(0, 242, 254, 0.3);
|
||||
border-radius: 12px;
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
|
||||
.status-text {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
margin-bottom: 10px;
|
||||
font-size: 0.9rem;
|
||||
color: #ccc;
|
||||
min-height: 20px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.guide-controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.btn-neon.small {
|
||||
padding: 5px 15px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
</style>
|
||||
.btn-neon.small {
|
||||
padding: 8px 16px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
</style>
|
||||
95
src/components/Hints.vue
Normal file
95
src/components/Hints.vue
Normal file
@@ -0,0 +1,95 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
hints: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
orientation: {
|
||||
type: String,
|
||||
required: true,
|
||||
validator: (v) => ['row', 'col'].includes(v)
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="hints-container" :class="orientation">
|
||||
<div
|
||||
v-for="(group, index) in hints"
|
||||
:key="index"
|
||||
class="hint-group"
|
||||
:class="{ 'hint-alt': index % 2 !== 0 }"
|
||||
>
|
||||
<span
|
||||
v-for="(num, idx) in group"
|
||||
:key="idx"
|
||||
class="hint-num"
|
||||
>
|
||||
{{ num }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.hints-container {
|
||||
display: flex;
|
||||
gap: var(--gap-size);
|
||||
}
|
||||
|
||||
.hints-container.col {
|
||||
flex-direction: row;
|
||||
margin-bottom: 5px;
|
||||
align-items: flex-end;
|
||||
padding: 0 5px; /* Match grid padding */
|
||||
}
|
||||
|
||||
.hints-container.row {
|
||||
flex-direction: column;
|
||||
margin-right: 5px;
|
||||
align-items: flex-end;
|
||||
padding: 5px 0; /* Match grid padding */
|
||||
}
|
||||
|
||||
.hint-group {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 4px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.col .hint-group {
|
||||
flex-direction: column;
|
||||
width: var(--cell-size);
|
||||
padding: 4px 2px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.row .hint-group {
|
||||
flex-direction: row;
|
||||
height: var(--cell-size);
|
||||
padding: 2px 8px;
|
||||
width: 100px; /* Stała szerokość */
|
||||
}
|
||||
|
||||
.hint-num {
|
||||
font-size: 0.85rem;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
/* Alternating Colors */
|
||||
.hint-group.hint-alt .hint-num {
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
/* Hover effect for readability */
|
||||
.hint-group:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-color: var(--accent-cyan);
|
||||
}
|
||||
</style>
|
||||
64
src/components/LevelSelector.vue
Normal file
64
src/components/LevelSelector.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<script setup>
|
||||
import { usePuzzleStore } from '@/stores/puzzle';
|
||||
|
||||
const store = usePuzzleStore();
|
||||
|
||||
const levels = [
|
||||
{ id: 'easy', label: 'ŁATWY 5X5' },
|
||||
{ id: 'medium', label: 'ŚREDNI 10X10' },
|
||||
{ id: 'hard', label: 'TRUDNY 15X15' }
|
||||
];
|
||||
|
||||
const emit = defineEmits(['open-custom', 'toggle-guide']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="level-selector">
|
||||
<button
|
||||
v-for="lvl in levels"
|
||||
:key="lvl.id"
|
||||
class="btn-neon"
|
||||
:class="{ active: store.currentLevelId === lvl.id }"
|
||||
@click="store.initGame(lvl.id)"
|
||||
>
|
||||
{{ lvl.label }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="btn-neon"
|
||||
:class="{ active: store.currentLevelId === 'custom' }"
|
||||
@click="emit('open-custom')"
|
||||
>
|
||||
WŁASNY
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="btn-neon guide-btn"
|
||||
@click="emit('toggle-guide')"
|
||||
>
|
||||
GUIDE ❓
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.level-selector {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
justify-content: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.btn-neon {
|
||||
padding: 10px 20px;
|
||||
border-radius: 20px;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.9rem;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.guide-btn {
|
||||
/* Specific styling for guide if needed */
|
||||
}
|
||||
</style>
|
||||
86
src/components/StatusPanel.vue
Normal file
86
src/components/StatusPanel.vue
Normal file
@@ -0,0 +1,86 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { usePuzzleStore } from '@/stores/puzzle';
|
||||
import { useTimer } from '@/composables/useTimer';
|
||||
|
||||
const store = usePuzzleStore();
|
||||
const { formatTime } = useTimer();
|
||||
|
||||
const formattedTime = computed(() => formatTime(store.elapsedTime));
|
||||
const progressText = computed(() => `${store.progressPercentage.toFixed(3)}%`);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="status-panel glass-panel">
|
||||
<div class="stat-item">
|
||||
<span class="label">CZAS</span>
|
||||
<span class="value">{{ formattedTime }}</span>
|
||||
</div>
|
||||
|
||||
<div class="stat-item">
|
||||
<span class="label">RUCHY</span>
|
||||
<span class="value">{{ store.moves }}</span>
|
||||
</div>
|
||||
|
||||
<div class="stat-item">
|
||||
<span class="label">POSTĘP</span>
|
||||
<div class="progress-wrapper">
|
||||
<span class="value small">{{ progressText }}</span>
|
||||
<span class="eye-icon">👁️</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.status-panel {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
padding: 20px 40px;
|
||||
border-radius: 15px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
|
||||
margin-bottom: 30px;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 0.8rem;
|
||||
text-transform: uppercase;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 1.8rem;
|
||||
color: #fff;
|
||||
font-weight: 300;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.value.small {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.progress-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.eye-icon {
|
||||
opacity: 0.7;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
91
src/components/WinModal.vue
Normal file
91
src/components/WinModal.vue
Normal file
@@ -0,0 +1,91 @@
|
||||
<script setup>
|
||||
import { usePuzzleStore } from '@/stores/puzzle';
|
||||
|
||||
const store = usePuzzleStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="modal-overlay">
|
||||
<div class="modal glass-panel">
|
||||
<h2>GRATULACJE!</h2>
|
||||
<p>Rozwiązałeś zagadkę!</p>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat">
|
||||
<span>Czas:</span>
|
||||
<strong>{{ store.elapsedTime }}s</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button class="btn-neon" @click="store.resetGame">Zagraj Ponownie</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
backdrop-filter: blur(5px);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
animation: fadeIn 0.5s ease;
|
||||
}
|
||||
|
||||
.modal {
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
max-width: 400px;
|
||||
width: 90%;
|
||||
border: 1px solid var(--primary-accent);
|
||||
box-shadow: 0 0 50px rgba(0, 242, 255, 0.2);
|
||||
animation: slideUp 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 2.5rem;
|
||||
color: var(--primary-accent);
|
||||
margin: 0 0 10px 0;
|
||||
text-shadow: 0 0 20px var(--primary-accent);
|
||||
}
|
||||
|
||||
p {
|
||||
color: var(--text-secondary);
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.stats {
|
||||
margin-bottom: 30px;
|
||||
padding: 20px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.stat {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.stat strong {
|
||||
color: #fff;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from { transform: translateY(50px); opacity: 0; }
|
||||
to { transform: translateY(0); opacity: 1; }
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user