down time di findwords

This commit is contained in:
='fauz 2025-11-04 14:54:32 +07:00
parent 5a9b3ba561
commit 30a600c339
9 changed files with 652 additions and 159 deletions

15
package-lock.json generated
View File

@ -16,7 +16,8 @@
"vue": "^3.2.13",
"vue-qrcode-reader": "^5.7.3",
"vue-router": "^4.5.1",
"vue3-lottie": "^3.3.1"
"vue3-lottie": "^3.3.1",
"vuex": "^4.1.0"
},
"devDependencies": {
"@babel/core": "^7.12.16",
@ -13099,6 +13100,18 @@
"integrity": "sha512-uvhvYPC8kGPjXT3MyKMrL3JitEAmDMp30lVkuq/590Mw9ok6pWcFCwXJveo0t5uqYw1UREQHofD+jVpdjBv8wg==",
"license": "MIT"
},
"node_modules/vuex": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/vuex/-/vuex-4.1.0.tgz",
"integrity": "sha512-hmV6UerDrPcgbSy9ORAtNXDr9M4wlNP4pEFKye4ujJF8oqgFFuxDCdOLS3eNoRTtq5O3hoBDh9Doj1bQMYHRbQ==",
"license": "MIT",
"dependencies": {
"@vue/devtools-api": "^6.0.0-beta.11"
},
"peerDependencies": {
"vue": "^3.2.0"
}
},
"node_modules/watchpack": {
"version": "2.4.4",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz",

View File

@ -17,7 +17,8 @@
"vue": "^3.2.13",
"vue-qrcode-reader": "^5.7.3",
"vue-router": "^4.5.1",
"vue3-lottie": "^3.3.1"
"vue3-lottie": "^3.3.1",
"vuex": "^4.1.0"
},
"devDependencies": {
"@babel/core": "^7.12.16",

View File

@ -0,0 +1,89 @@
<template>
<!-- <div>
<h2>Daftar Item</h2>
<button @click="addItem">Tambah Item</button>
<ul>
<li v-for="item in items" :key="item.id">{{ item.name }}</li>
</ul>
</div> -->
<div
class="min-h-screen flex flex-col bg-gradient-to-b from-emerald-100 via-lime-50 to-white
text-gray-800 relative overflow-hidden mb-[-10px]"
>
<div class="w-full px-6 py-4 flex items-center justify-between backdrop-blur-md bg-white/30 border-b border-emerald-100 z-20">
<button
class="flex items-center gap-2 text-emerald-600 hover:text-emerald-800 transition-colors"
>
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
<span class="font-semibold">Kembali</span>
</button>
<!-- Breadcrumb / Path -->
<div class="text-sm text-gray-600">
<span class="text-emerald-600 font-medium cursor-pointer hover:underline">Beranda</span>
<span class="mx-2"></span>
<span class="text-gray-700 font-semibold">Manga</span>
<span class="mx-2"></span>
<span class="text-gray-700 font-semibold">Chapters</span>
</div>
</div>
<div class="text-center mt-8">
<h1 class="text-4xl font-extrabold bg-gradient-to-r from-emerald-500 to-lime-500 bg-clip-text text-transparent">
Daftar Manga
</h1>
<p class="text-gray-500 mt-2">Temukan bacaan favoritmu di sini 📖</p>
</div>
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-6 mt-10 px-6 pb-20">
<div
class="group cursor-pointer rounded-2xl bg-white/60 backdrop-blur-lg p-4 shadow-md border border-white/40
hover:shadow-2xl hover:-translate-y-2 transition-all duration-300 relative overflow-hidden"
>
<div class="mt-0 text-center">
<h3 class="font-semibold text-lg text-gray-800 truncate">Chapter</h3>
</div>
<div class="relative" >
<img
class="w-full h-48 object-cover rounded-xl group-hover:scale-105 transition-transform duration-500"
/>
<div class="absolute inset-0 bg-gradient-to-t from-black/40 to-transparent rounded-xl opacity-0 group-hover:opacity-100 transition-opacity"></div>
</div>
<div class="mt-3 text-center">
<p class="text-sm text-gray-500">
<button
class="text-gray-900 bg-white hover:bg-gray-100 border border-gray-200 focus:ring-4 focus:outline-none focus:ring-gray-100 font-medium rounded-lg text-sm px-5 py-2.5 text-center inline-flex items-center dark:focus:ring-gray-600 dark:bg-gray-800 dark:border-gray-700 dark:text-white dark:hover:bg-gray-700 me-2 mb-2">
<h3 class="font-semibold text-lg text-gray-800 truncate">📖 Synopsis</h3>
</button>
</p>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapState, mapActions } from "vuex";
export default {
computed: {
...mapState("items", ["items"])
},
methods: {
...mapActions("items", ["fetchItems", "createItem"]),
addItem() {
const newItem = { id: Date.now(), name: "Item baru" };
this.createItem(newItem);
}
},
mounted() {
this.fetchItems();
}
};
</script>

View File

@ -4,7 +4,13 @@ import './assets/tailwind.css'
import router from './router'
import { createPinia } from 'pinia'
import Vue3Lottie from 'vue3-lottie'
import store from '@/stores'
const pinia = createPinia()
createApp(App).use(pinia).use(router).use(Vue3Lottie).mount('#app')
createApp(App)
.use(pinia)
.use(router)
.use(Vue3Lottie)
.use(store)
.mount('#app')

View File

@ -0,0 +1,240 @@
<template>
<!-- Header Section -->
<div class="w-full px-6 py-4 flex items-center justify-between backdrop-blur-md bg-white/30 border-b border-emerald-100 z-20">
<!-- Back Button -->
<button
@click="goBack"
class="flex items-center gap-2 text-emerald-600 hover:text-emerald-800 transition-colors"
>
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
<span class="font-semibold">Kembali</span>
</button>
<!-- Breadcrumb / Path -->
<div class="text-sm text-gray-600">
<!-- <span class="text-emerald-600 font-medium cursor-pointer hover:underline" @click="goEntertainment">Entertainment</span>
<span class="mx-2"></span>
<span class="text-emerald-600 font-medium cursor-pointer hover:underline" @click="goBack">Mini Games</span>
<span class="mx-2"></span> -->
<span class="text-gray-700 font-semibold">Find Words</span>
</div>
</div>
<div class="flex flex-col min-h-screen bg-gradient-to-b from-green-200 to-lime-100 text-gray-900 select-none"
style="background-image:url('/images/footer.png'); background-size:cover; background-position:center;">
<!-- Header -->
<div class="text-center mt-8">
<h1 class="text-4xl font-extrabold bg-gradient-to-r from-emerald-500 to-lime-500 bg-clip-text text-transparent">
<span>🌿</span>Temukan Kata Positif
</h1>
<p class="text-gray-500 mt-2">Temukan kata kata positif yang ada di bawah ini 📖</p>
</div>
<!-- Grid Game -->
<main class="flex-1 flex flex-col items-center justify-center p-4">
<div
ref="gridRef"
class="grid gap-1"
:style="{
gridTemplateColumns: `repeat(${size}, minmax(0, 1fr))`,
width: '90vw',
maxWidth: '500px'
}"
>
<!-- per baris -->
<template v-for="(row, rowIndex) in grid" :key="rowIndex">
<!-- per sel -->
<div
v-for="(letter, colIndex) in row"
:key="`${rowIndex}-${colIndex}`"
@mousedown.prevent="startSelect(rowIndex, colIndex)"
@mouseenter="dragSelect(rowIndex, colIndex)"
@mouseup="endSelect"
@touchstart.prevent="startSelect(rowIndex, colIndex)"
@touchmove.prevent="dragSelectTouch($event)"
@touchend.prevent="endSelect"
class="flex items-center justify-center text-lg sm:text-xl font-bold rounded-xl cursor-pointer
transition-all duration-200 ease-out shadow-md"
:class="cellClass(rowIndex, colIndex)"
>
{{ letter }}
</div>
</template>
</div>
<!-- Footer Words -->
<div class="mt-6 bg-white bg-opacity-80 p-4 rounded-3xl shadow-lg w-full max-w-lg border border-lime-200">
<h2 class="font-semibold text-green-800 mb-2 text-center">Cari kata berikut:</h2>
<div class="flex flex-wrap justify-center gap-2">
<span
v-for="word in words"
:key="word"
class="px-3 py-1 rounded-full text-sm font-semibold shadow transition-all duration-200"
:class="foundWords.includes(word)
? 'bg-green-300 text-green-900 animate-bounce-glow'
: 'bg-lime-100 text-green-700 hover:bg-green-200 hover:scale-105'"
>
{{ word }}
</span>
</div>
<p class="mt-3 text-xs text-green-700 italic text-center">
Temukan semua kata positif seperti sabar, cinta, syukur, dan gembira untuk hati yang ceria!
</p>
</div>
</main>
<!-- Footer Buttons -->
<footer class="p-4 flex justify-center gap-3">
<button
@click="resetGame"
class="px-6 py-2 bg-gradient-to-r from-lime-400 to-green-500 text-white font-bold rounded-full shadow-md hover:shadow-lg hover:scale-105 active:scale-95 transition-all duration-200"
>
🔄 Main Lagi
</button>
</footer>
</div>
</template>
<script>
export default {
name: "Find-words"
};
</script>
<script setup>
import { ref, onMounted } from "vue";
const size = 10;
const allWords = [
"TENANG", "SABAR", "SYUKUR", "CINTA", "GEMBIRA", "BANGGA",
"PERCAYA", "SEMANGAT", "OPTIMIS", "IKHLAS", "BAHAGIA", "PEDULI",
"CERIA", "HARMONI", "SETIA", "JUJUR", "RAMAH", "TABAH", "TULUS",
"BERANI", "MAJU", "DAMAI", "RIANG"
];
const words = ref([]);
const grid = ref([]);
const foundWords = ref([]);
const selectedCells = ref([]);
const isSelecting = ref(false);
const wordPositions = {};
const gridRef = ref(null);
function generateGrid() {
const rand = (n) => Math.floor(Math.random() * n);
const g = Array.from({ length: size }, () => Array(size).fill(""));
const chosenWords = [...allWords].sort(() => Math.random() - 0.5).slice(0, 6);
words.value = chosenWords;
chosenWords.forEach((word) => {
let placed = false;
for (let tries = 0; tries < 100 && !placed; tries++) {
const horizontal = Math.random() > 0.5;
const row = rand(size - (horizontal ? 1 : word.length));
const col = rand(size - (horizontal ? word.length : 1));
let fits = true;
for (let i = 0; i < word.length; i++) {
const r = row + (horizontal ? 0 : i);
const c = col + (horizontal ? i : 0);
if (g[r][c] && g[r][c] !== word[i]) {
fits = false;
break;
}
}
if (fits) {
for (let i = 0; i < word.length; i++) {
const r = row + (horizontal ? 0 : i);
const c = col + (horizontal ? i : 0);
g[r][c] = word[i];
}
placed = true;
}
}
});
const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
for (let r = 0; r < size; r++) {
for (let c = 0; c < size; c++) {
if (!g[r][c]) g[r][c] = letters[Math.floor(Math.random() * letters.length)];
}
}
grid.value = g;
}
function cellClass(r, c) {
const isSelected = selectedCells.value.some((pos) => pos.r === r && pos.c === c);
const isFound = foundWords.value.some((word) =>
wordPositions[word]?.some((p) => p.r === r && p.c === c)
);
if (isFound) return "bg-green-300 text-green-900 scale-105";
if (isSelected) return "bg-yellow-200 text-yellow-900 scale-105";
return "bg-white text-green-700 hover:bg-green-100 hover:scale-105";
}
function startSelect(r, c) {
isSelecting.value = true;
selectedCells.value = [{ r, c }];
}
function dragSelect(r, c) {
if (!isSelecting.value) return;
const last = selectedCells.value[selectedCells.value.length - 1];
if (last.r !== r || last.c !== c) selectedCells.value.push({ r, c });
}
function dragSelectTouch(e) {
if (!isSelecting.value) return;
const touch = e.touches[0];
const rect = gridRef.value.getBoundingClientRect();
const cellSize = rect.width / size;
const x = Math.floor((touch.clientX - rect.left) / cellSize);
const y = Math.floor((touch.clientY - rect.top) / cellSize);
if (x >= 0 && y >= 0 && x < size && y < size) dragSelect(y, x);
}
function endSelect() {
if (!isSelecting.value) return;
const selectedWord = selectedCells.value.map(({ r, c }) => grid.value[r][c]).join("");
if (words.value.includes(selectedWord) && !foundWords.value.includes(selectedWord)) {
foundWords.value.push(selectedWord);
wordPositions[selectedWord] = [...selectedCells.value];
if (foundWords.value.length === words.value.length) {
setTimeout(() => alert("🎉 Semua kata ditemukan! Hebat sekali! 🌟"), 200);
}
}
selectedCells.value = [];
isSelecting.value = false;
}
function resetGame() {
foundWords.value = [];
selectedCells.value = [];
generateGrid();
}
// function goEntertainment(){
// window.history.back('/entertainment');
// }
function goBack(){
window.history.back('/entertainment/mini-games');
}
onMounted(() => {
resetGame();
});
</script>
<style scoped>
@keyframes bounceGlow {
0%, 100% { box-shadow: 0 0 8px rgba(72, 255, 140, 0.6); transform: scale(1); }
50% { box-shadow: 0 0 20px rgba(72, 255, 140, 1); transform: scale(1.05); }
}
.animate-bounce-glow {
animation: bounceGlow 1s infinite;
}
</style>

View File

@ -1,240 +1,339 @@
<template>
<!-- Header Section -->
<div class="w-full px-6 py-4 flex items-center justify-between backdrop-blur-md bg-white/30 border-b border-emerald-100 z-20">
<!-- Back Button -->
<button
@click="goBack"
class="flex items-center gap-2 text-emerald-600 hover:text-emerald-800 transition-colors"
>
<button @click="goBack" class="flex items-center gap-2 text-emerald-600 hover:text-emerald-800 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
<span class="font-semibold">Kembali</span>
</button>
</div>
<!-- Breadcrumb / Path -->
<div class="text-sm text-gray-600">
<!-- <span class="text-emerald-600 font-medium cursor-pointer hover:underline" @click="goEntertainment">Entertainment</span>
<span class="mx-2"></span>
<span class="text-emerald-600 font-medium cursor-pointer hover:underline" @click="goBack">Mini Games</span>
<span class="mx-2"></span> -->
<span class="text-gray-700 font-semibold">Find Words</span>
</div>
</div>
<div class="flex flex-col min-h-screen bg-gradient-to-b from-green-200 to-lime-100 text-gray-900 select-none"
style="background-image:url('/images/footer.png'); background-size:cover; background-position:center;">
<!-- Header -->
<div class="text-center mt-8">
<h1 class="text-4xl font-extrabold bg-gradient-to-r from-emerald-500 to-lime-500 bg-clip-text text-transparent">
<span>🌿</span>Temukan Kata Positif
</h1>
<h1 class="text-4xl font-extrabold bg-gradient-to-r from-emerald-500 to-lime-500 bg-clip-text text-transparent">🌿 Temukan Kata Positif</h1>
<p class="text-gray-500 mt-2">Temukan kata kata positif yang ada di bawah ini 📖</p>
</div>
<div class="text-center mt-8">
<div class="text-sm text-gray-600">Level: <span class="font-semibold text-gray-800">{{ currLevel + 1 }} / {{ levels.length }}</span></div>
<div class="text-sm mt-0.5 text-emerald-700 font-bold"> {{ timeLeft }}s</div>
</div>
<!-- Grid Game -->
<main class="flex-1 flex flex-col items-center justify-center p-4">
<div
ref="gridRef"
class="grid gap-1"
:style="{
gridTemplateColumns: `repeat(${size}, minmax(0, 1fr))`,
width: '90vw',
maxWidth: '500px'
}"
>
<!-- per baris -->
<template v-for="(row, rowIndex) in grid" :key="rowIndex">
<!-- per sel -->
<div
v-for="(letter, colIndex) in row"
:key="`${rowIndex}-${colIndex}`"
@mousedown.prevent="startSelect(rowIndex, colIndex)"
@mouseenter="dragSelect(rowIndex, colIndex)"
<div ref="gridRef" class="grid gap-1" :style="gridStyle">
<template v-for="(row, r) in grid" :key="r">
<div v-for="(letter, c) in row" :key="`${r}-${c}`"
@mousedown.prevent="startSelect(r,c)"
@mouseenter="dragSelect(r,c)"
@mouseup="endSelect"
@touchstart.prevent="startSelect(rowIndex, colIndex)"
@touchstart.prevent="startSelect(r,c)"
@touchmove.prevent="dragSelectTouch($event)"
@touchend.prevent="endSelect"
class="flex items-center justify-center text-lg sm:text-xl font-bold rounded-xl cursor-pointer
transition-all duration-200 ease-out shadow-md"
:class="cellClass(rowIndex, colIndex)"
>
class="flex items-center justify-center text-lg sm:text-xl font-bold rounded-xl cursor-pointer transition-all duration-200 ease-out shadow-md aspect-square"
:class="cellClass(r,c)">
{{ letter }}
</div>
</template>
</div>
<!-- Footer Words -->
<div class="mt-6 bg-white bg-opacity-80 p-4 rounded-3xl shadow-lg w-full max-w-lg border border-lime-200">
<h2 class="font-semibold text-green-800 mb-2 text-center">Cari kata berikut:</h2>
<div class="flex flex-wrap justify-center gap-2">
<span
v-for="word in words"
:key="word"
class="px-3 py-1 rounded-full text-sm font-semibold shadow transition-all duration-200"
:class="foundWords.includes(word)
? 'bg-green-300 text-green-900 animate-bounce-glow'
: 'bg-lime-100 text-green-700 hover:bg-green-200 hover:scale-105'"
>
{{ word }}
<span v-for="w in words" :key="w" class="px-3 py-1 rounded-full text-sm font-semibold shadow transition-all duration-200"
:class="foundWords.includes(w) ? 'bg-green-300 text-green-900 animate-bounce-glow' : 'bg-lime-100 text-green-700 hover:bg-green-200 hover:scale-105'">
{{ w }}
</span>
</div>
<p class="mt-3 text-xs text-green-700 italic text-center">
Temukan semua kata positif seperti sabar, cinta, syukur, dan gembira untuk hati yang ceria!
</p>
<p class="mt-3 text-xs text-green-700 italic text-center">Temukan semua kata positif untuk hati yang ceria!</p>
</div>
</main>
<!-- Footer Buttons -->
<footer class="p-4 flex justify-center gap-3">
<button
@click="resetGame"
class="px-6 py-2 bg-gradient-to-r from-lime-400 to-green-500 text-white font-bold rounded-full shadow-md hover:shadow-lg hover:scale-105 active:scale-95 transition-all duration-200"
>
<button @click="resetGame" class="px-6 py-2 bg-gradient-to-r from-lime-400 to-green-500 text-white font-bold rounded-full shadow-md hover:shadow-lg hover:scale-105 active:scale-95 transition-all duration-200">
🔄 Main Lagi
</button>
</footer>
</div>
</template>
<script>
export default {
name: "Find-words"
};
name: "Find-words",
}
</script>
<script setup>
import { ref, onMounted } from "vue";
import { ref, onMounted, computed, watch } from "vue";
const size = 10;
/* ---------- data kata ---------- */
const allWords = [
"TENANG", "SABAR", "SYUKUR", "CINTA", "GEMBIRA", "BANGGA",
"PERCAYA", "SEMANGAT", "OPTIMIS", "IKHLAS", "BAHAGIA", "PEDULI",
"CERIA", "HARMONI", "SETIA", "JUJUR", "RAMAH", "TABAH", "TULUS",
"BERANI", "MAJU", "DAMAI", "RIANG"
"TENANG","SABAR","SYUKUR","CINTA","GEMBIRA","BANGGA","PERCAYA","SEMANGAT","OPTIMIS","IKHLAS",
"BAHAGIA","PEDULI","CERIA","HARMONI","SETIA","JUJUR","RAMAH","TABAH","TULUS","BERANI","MAJU","DAMAI","RIANG"
];
const words = ref([]);
/* ---------- levels (tambahkan waktu tiap level) ---------- */
const levels = [
{ name: "Easy", count: 4, time: 60 },
{ name: "Medium", count: 6, time: 45 },
{ name: "Hard", count: 8, time: 30 },
{ name: "Extreme", count: 10, time: 20 }
];
const currLevel = ref(0);
const size = computed(() => levels[currLevel.value].count);
/* ---------- reactive state ---------- */
const grid = ref([]);
const words = ref([]);
const foundWords = ref([]);
const selectedCells = ref([]);
const isSelecting = ref(false);
const wordPositions = {};
const gridRef = ref(null);
const isSelecting = ref(false);
const wordPositions = {}; // diisi saat kata ditempatkan
// const placedWords = {};
const gridStyle = computed(() => ({ gridTemplateColumns: `repeat(${size.value}, minmax(0,1fr))`, width: "90vw", maxWidth: "500px" }));
/* ---------- timer ---------- */
let timer = null;
const timeLeft = ref(levels[currLevel.value].time);
function startTimer() {
if (timer) clearInterval(timer);
timeLeft.value = levels[currLevel.value].time;
timer = setInterval(() => {
timeLeft.value--;
if (timeLeft.value <= 0) {
clearInterval(timer);
timer = null;
// waktu habis -> reset level (tetap di level sama)
resetGame();
startTimer();
}
}, 1000);
}
/* ---------- helper: pilih kata yang muat ---------- */
const shuffle = (arr) => arr.slice().sort(() => Math.random() - 0.5);
function candidatePoolForSize(sz) {
return allWords.filter(w => w.length <= sz);
}
function pickWordsForLevel() {
const pool = shuffle(candidatePoolForSize(size.value));
const desired = levels[currLevel.value].count;
if (pool.length <= desired) return pool.slice(0, pool.length);
return pool.slice(0, desired);
}
/* ---------- placement utils (8 arah) ---------- */
const DIRECTIONS = [ [1,0],[-1,0],[0,1],[0,-1], [1,1],[-1,-1],[1,-1],[-1,1] ];
function canPlace(g, word, r, c, dr, dc) {
for (let i = 0; i < word.length; i++) {
const nr = r + dr * i;
const nc = c + dc * i;
if (nr < 0 || nc < 0 || nr >= size.value || nc >= size.value) return false;
if (g[nr][nc] !== "" && g[nr][nc] !== word[i]) return false;
}
return true;
}
function placeAt(g, word, r, c, dr, dc) {
const cells = [];
for (let i = 0; i < word.length; i++) {
const nr = r + dr * i;
const nc = c + dc * i;
g[nr][nc] = word[i];
cells.push({ r: nr, c: nc });
}
wordPositions[word] = cells;
}
/* 1) coba intersection lalu 2) fallback random */
function placeWordWithIntersection(g, word) {
// intersection: cari huruf yang sama di grid yg sudah terisi
for (let r = 0; r < size.value; r++) {
for (let c = 0; c < size.value; c++) {
const letterHere = g[r][c];
if (!letterHere) continue;
for (let idx = 0; idx < word.length; idx++) {
if (word[idx] !== letterHere) continue;
for (const [dr, dc] of DIRECTIONS) {
const startR = r - dr * idx;
const startC = c - dc * idx;
if (canPlace(g, word, startR, startC, dr, dc)) {
placeAt(g, word, startR, startC, dr, dc);
return true;
}
}
}
}
}
// fallback random tries
const triesMax = 300;
for (let t = 0; t < triesMax; t++) {
const [dr, dc] = DIRECTIONS[Math.floor(Math.random() * DIRECTIONS.length)];
const r = Math.floor(Math.random() * size.value);
const c = Math.floor(Math.random() * size.value);
if (canPlace(g, word, r, c, dr, dc)) {
placeAt(g, word, r, c, dr, dc);
return true;
}
}
return false;
}
/* ---------- build grid (re-try strategy) ---------- */
function fillEmpty(g) {
const abc = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
for (let r = 0; r < size.value; r++) {
for (let c = 0; c < size.value; c++) {
if (!g[r][c]) g[r][c] = abc[Math.floor(Math.random() * abc.length)];
}
}
}
function generateGrid() {
const rand = (n) => Math.floor(Math.random() * n);
const g = Array.from({ length: size }, () => Array(size).fill(""));
const chosenWords = [...allWords].sort(() => Math.random() - 0.5).slice(0, 6);
words.value = chosenWords;
const maxAttempts = 8;
const chosenWords = pickWordsForLevel();
const desiredCount = Math.min(chosenWords.length, levels[currLevel.value].count);
chosenWords.forEach((word) => {
let placed = false;
for (let tries = 0; tries < 100 && !placed; tries++) {
const horizontal = Math.random() > 0.5;
const row = rand(size - (horizontal ? 1 : word.length));
const col = rand(size - (horizontal ? word.length : 1));
for (let attempt = 0; attempt < maxAttempts; attempt++) {
// reset wordPositions
for (const k in wordPositions) delete wordPositions[k];
let fits = true;
for (let i = 0; i < word.length; i++) {
const r = row + (horizontal ? 0 : i);
const c = col + (horizontal ? i : 0);
if (g[r][c] && g[r][c] !== word[i]) {
fits = false;
break;
}
}
if (fits) {
for (let i = 0; i < word.length; i++) {
const r = row + (horizontal ? 0 : i);
const c = col + (horizontal ? i : 0);
g[r][c] = word[i];
}
placed = true;
}
}
});
const g = Array.from({ length: size.value }, () => Array(size.value).fill(""));
// coba tempatkan kata panjang dulu
const wordsToPlace = chosenWords.slice(0, desiredCount).sort((a,b) => b.length - a.length);
const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
for (let r = 0; r < size; r++) {
for (let c = 0; c < size; c++) {
if (!g[r][c]) g[r][c] = letters[Math.floor(Math.random() * letters.length)];
}
let allPlaced = true;
for (const w of wordsToPlace) {
const ok = placeWordWithIntersection(g, w);
if (!ok) { allPlaced = false; break; }
}
if (allPlaced) {
fillEmpty(g);
grid.value = g;
// words.value = wordsToPlace;
words.value = Object.keys(wordPositions);
return true;
}
// else ulangi generate
}
// fallback (jarang terjadi)
const fallbackG = Array.from({ length: size.value }, () => Array(size.value).fill(""));
fillEmpty(fallbackG);
grid.value = fallbackG;
words.value = chosenWords.slice(0, Math.min(chosenWords.length, levels[currLevel.value].count));
return false;
}
/* ---------- UI selection & validation ---------- */
// function cellClass(r, c) {
// const sel = selectedCells.value.some(p => p.r === r && p.c === c);
// const found = Object.values(wordPositions).flat().some(p => p.r === r && p.c === c);
// return found ? "bg-green-300 text-green-900 scale-105" : sel ? "bg-yellow-200 text-yellow-900 scale-105" : "bg-white text-green-700 hover:bg-green-100 hover:scale-105";
// }
function cellClass(r, c) {
const isSelected = selectedCells.value.some((pos) => pos.r === r && pos.c === c);
const isFound = foundWords.value.some((word) =>
wordPositions[word]?.some((p) => p.r === r && p.c === c)
const sel = selectedCells.value.some(p => p.r === r && p.c === c);
// highlight hanya jika kata ditemukan user (masuk ke foundWords)
const found = foundWords.value.some(word =>
wordPositions[word]?.some(p => p.r === r && p.c === c)
);
if (isFound) return "bg-green-300 text-green-900 scale-105";
if (isSelected) return "bg-yellow-200 text-yellow-900 scale-105";
return "bg-white text-green-700 hover:bg-green-100 hover:scale-105";
return found
? "bg-green-300 text-green-900 scale-105"
: sel
? "bg-yellow-200 text-yellow-900 scale-105"
: "bg-white text-green-700 hover:bg-green-100 hover:scale-105";
}
function startSelect(r, c) {
isSelecting.value = true;
selectedCells.value = [{ r, c }];
}
function dragSelect(r, c) {
if (!isSelecting.value) return;
const last = selectedCells.value[selectedCells.value.length - 1];
if (last.r !== r || last.c !== c) selectedCells.value.push({ r, c });
}
function startSelect(r,c){ isSelecting.value = true; selectedCells.value = [{r,c}]; }
function dragSelect(r,c){ if (isSelecting.value) { const last = selectedCells.value.at(-1); if(last.r !== r || last.c !== c) selectedCells.value.push({r,c}); } }
function dragSelectTouch(e){ if (!isSelecting.value) return; const t = e.touches[0]; const rect = gridRef.value.getBoundingClientRect(); const s = rect.width / size.value; const x = Math.floor((t.clientX - rect.left) / s); const y = Math.floor((t.clientY - rect.top) / s); if (x >= 0 && y >= 0 && x < size.value && y < size.value) dragSelect(y, x); }
function dragSelectTouch(e) {
if (!isSelecting.value) return;
const touch = e.touches[0];
const rect = gridRef.value.getBoundingClientRect();
const cellSize = rect.width / size;
const x = Math.floor((touch.clientX - rect.left) / cellSize);
const y = Math.floor((touch.clientY - rect.top) / cellSize);
if (x >= 0 && y >= 0 && x < size && y < size) dragSelect(y, x);
}
/* endSelect: validasi arah lalu cek kata */
function endSelect(){
if(!isSelecting.value) return;
const cells = selectedCells.value;
if (cells.length < 2) { selectedCells.value = []; isSelecting.value = false; return; }
function endSelect() {
if (!isSelecting.value) return;
const selectedWord = selectedCells.value.map(({ r, c }) => grid.value[r][c]).join("");
if (words.value.includes(selectedWord) && !foundWords.value.includes(selectedWord)) {
foundWords.value.push(selectedWord);
wordPositions[selectedWord] = [...selectedCells.value];
if (foundWords.value.length === words.value.length) {
setTimeout(() => alert("🎉 Semua kata ditemukan! Hebat sekali! 🌟"), 200);
}
const dx = Math.sign(cells[1].r - cells[0].r);
const dy = Math.sign(cells[1].c - cells[0].c);
const valid = cells.every((cell, idx) => {
if (idx === 0) return true;
return cell.r === (cells[0].r + dx * idx) && cell.c === (cells[0].c + dy * idx);
});
if (!valid) { selectedCells.value = []; isSelecting.value = false; return; }
const formed = cells.map(p => grid.value[p.r][p.c]).join("");
if (words.value.includes(formed) && !foundWords.value.includes(formed)) {
foundWords.value.push(formed);
// mark positions (already stored when placed)
// panggil checkLevelComplete untuk auto next level
checkLevelComplete();
}
selectedCells.value = [];
isSelecting.value = false;
}
/* ---------- level completion ---------- */
function checkLevelComplete() {
if (foundWords.value.length === words.value.length) {
if (timer) { clearInterval(timer); timer = null; }
// show small delay lalu naik level (jika masih ada)
setTimeout(() => {
if (currLevel.value < levels.length - 1) {
currLevel.value++;
resetGame();
startTimer();
} else {
// last level finished: bisa reset ke awal atau tampilkan pesan
alert("🌟 Semua level selesai! Selamat :)");
// reset kembali ke level 0 atau tetap
currLevel.value = 0;
resetGame();
startTimer();
}
}, 300);
}
}
/* ---------- controls ---------- */
function resetGame() {
foundWords.value = [];
selectedCells.value = [];
for (const k in wordPositions) delete wordPositions[k];
generateGrid();
}
// function goEntertainment(){
// window.history.back('/entertainment');
// }
function goBack(){
window.history.back('/entertainment/mini-games');
}
function goBack(){ window.history.back(); }
/* ---------- init ---------- */
onMounted(() => {
generateGrid();
startTimer();
});
/* optional: kalau level berubah, restart timer dan grid */
watch(currLevel, () => {
resetGame();
startTimer();
});
</script>
<style scoped>
@keyframes bounceGlow {
0%, 100% { box-shadow: 0 0 8px rgba(72, 255, 140, 0.6); transform: scale(1); }
50% { box-shadow: 0 0 20px rgba(72, 255, 140, 1); transform: scale(1.05); }
}
.animate-bounce-glow {
animation: bounceGlow 1s infinite;
}
/* Pastikan ada aspect-square (Tailwind) — jika tidak, fallback: */
.grid > * > * { aspect-ratio: 1 / 1; } /* fallback bila Tailwind aspect-square tidak tersedia */
@keyframes bounceGlow { 0%,100%{box-shadow:0 0 8px rgba(72,255,140,.6);transform:scale(1);} 50%{box-shadow:0 0 20px rgba(72,255,140,1);transform:scale(1.05);} }
.animate-bounce-glow{animation:bounceGlow 1s infinite;}
</style>

View File

@ -17,6 +17,7 @@ import MinigamePage from '@/pages/Minigame.vue'
import FlipcardPage from '@/pages/Flipcard.vue'
import WordspellsPage from '@/pages/Wordspells.vue'
import PointhistoryPage from '@/pages/Pointhistory.vue'
import itemPage from '@/components/itemPage.vue'
const routes = [
{ path: '/', name: 'home', component: HomePage , meta:{requiresAuth:true}},
@ -43,6 +44,7 @@ const routes = [
{ path: '/entertainment/mini-games/', name:'mini-games', component:MinigamePage, props:true},
{ path: '/entertainment/mini-games/flipcard/', name:'flipcard', component:FlipcardPage, props:true},
{ path: '/entertainment/mini-games/word-spells/', name:'word-spells', component:WordspellsPage, props:true},
{ path: '/items/', name:'items', component:itemPage, props:true},
]
const router = createRouter({

8
src/stores/index.js Normal file
View File

@ -0,0 +1,8 @@
import { createStore } from "vuex";
import items from "./items";
export default createStore({
modules: {
items
}
});

35
src/stores/items.js Normal file
View File

@ -0,0 +1,35 @@
export default {
namespaced: true,
// DATA TEMPAT DISIMPAN
state: () => ({
items: []
}),
// CARA MENGUBAH STATE (HARUS SINKRON)
mutations: {
SET_ITEMS(state, payload) {
state.items = payload;
},
ADD_ITEM(state, payload) {
state.items.push(payload);
}
},
// TEMPAT PANGGIL API / FUNGSI ASYNC
actions: {
fetchItems({ commit }) {
// contoh data dari API
const data = [
{ id: 1, name: "Apel" },
{ id: 2, name: "Mangga" }
];
commit("SET_ITEMS", data); // <--- mutation dipanggil
},
createItem({ commit }, item) {
commit("ADD_ITEM", item);
}
}
};