add mini games

This commit is contained in:
='fauz 2025-10-20 10:56:22 +07:00
parent ca902a0078
commit 2882b64d25
8 changed files with 373 additions and 12 deletions

BIN
public/images/crossword.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
public/images/games.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -72,7 +72,7 @@
<div
v-for="hiburan in hiburans"
:key="hiburan.name"
@click="toMangas(hiburan.name)"
@click="toContents(hiburan.name)"
class="group cursor-pointer relative flex flex-col items-center justify-center p-4 rounded-2xl
backdrop-blur-xl bg-white/30 border border-white/40 shadow-lg
hover:shadow-[0_0_25px_rgba(34,197,94,0.4)] hover:bg-white/50
@ -116,19 +116,22 @@ export default {
genres : [],
hiburans: [
{ name: "Manga", img: "/images/manga.png" , badge:"Popular"},
{ name: "Mini Game", img: "/images/char/fox.png",badge:"Popular" },
{ name: "Videos", img: "/images/char/bear.png",badge:"Popular" },
{ name: "Cerita Rakyat", img: "/images/char/fox.png",badge:"Popular" },
{ name: "Mini Game", img: "/images/games.png",badge:"New" },
{ name: "Videos", img: "/images/char/bear.png",badge:"Remaining" },
{ name: "Cerita Rakyat", img: "/images/char/fox.png",badge:"Remaining" },
],
contents : getContent(),
tiltTransforms: {}
};
},
methods: {
toMangas(title) {
toContents(title) {
if(title == 'Manga'){
router.push('/entertainment/mangas')
}
if(title == 'Mini Game'){
router.push('/entertainment/mini-games/')
}
},
async loadGenres() {
const res = await getGenre();

210
src/pages/Findwords.vue Normal file
View File

@ -0,0 +1,210 @@
<template>
<div class="flex flex-col min-h-screen bg-gradient-to-b from-indigo-400 to-blue-200 text-gray-900 select-none">
<!-- Header -->
<header class="flex items-center justify-center bg-indigo-600 text-white py-4 shadow-md">
<h1 class="text-2xl font-bold tracking-wide flex items-center gap-2">
<span>🔤</span> Temukan Kata Positif
</h1>
</header>
<!-- 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"
:class="cellClass(rowIndex, colIndex)"
>
{{ letter }}
</div>
</template>
</div>
<!-- Footer Words -->
<div class="mt-6 bg-white bg-opacity-80 p-4 rounded-2xl shadow-lg w-full max-w-lg">
<h2 class="font-semibold text-gray-700 mb-2">Cari kata berikut:</h2>
<div class="flex flex-wrap gap-2">
<span
v-for="word in words"
:key="word"
class="px-3 py-1 rounded-full text-sm font-semibold"
:class="foundWords.includes(word) ? 'bg-green-300 text-green-800 animate-pulse' : 'bg-gray-200 text-gray-600'"
>
{{ word }}
</span>
</div>
<p class="mt-3 text-xs text-gray-600 italic">
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-5 py-2 bg-indigo-600 text-white font-bold rounded-full shadow hover:shadow-lg hover:bg-indigo-700 active:scale-95 transition"
>
🔄 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;
}
}
});
// isi huruf acak
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-300 text-yellow-900 scale-105";
return "bg-white text-indigo-700 hover:bg-indigo-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 });
}
// khusus mobile (touchmove)
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();
}
onMounted(() => {
resetGame();
});
</script>
<style scoped>
@keyframes pulseGlow {
0%, 100% {
box-shadow: 0 0 10px rgba(0, 255, 127, 0.5);
}
50% {
box-shadow: 0 0 20px rgba(0, 255, 127, 0.8);
}
}
.animate-pulse {
animation: pulseGlow 1s infinite;
}
</style>

View File

@ -8,7 +8,7 @@
margin-bottom=-10px;
>
<!-- Character Preview -->
<div class="w-full max-w-md flex flex-col items-center z-60 mb-8">
<div class="w-full max-w-md flex flex-col items-center z-60">
<div class="relative flex items-end justify-center z-30 px-[10px] mt-[40px]">
<img
src="/images/logo.png"
@ -16,7 +16,28 @@
class="object-contain relative z-30 pb-0"
/>
</div>
</div>
</div>
<div class="relative w-40 h-50 flex items-end justify-center z-30 px-[10px]">
<img
:src="selectedProfile.img"
:alt="selectedCharacter.name"
class="w-40 h-60 object-contain relative z-30 pb-12"
/>
<div
class="absolute w-[30px] h-[30px] right-0 top-[100px] z-40 bg-green-600 hover:bg-green-400 transition"
style="
-webkit-mask: url('/images/icon-edit.svg') no-repeat center;
-webkit-mask-size: contain;
mask: url('/images/icon-edit.svg') no-repeat center;
mask-size: contain;
"
@click="changeAva('profile')"
>
</div>
</div>
<h2 class="mt-[-70px] mb-6 text-md font-bold text-grey-400">{{ this.savedUser?.username || 'S'}}</h2>
<div class="w-full max-w-md flex flex-col items-center z-60">
<div class="bg-lime-200 shadow-xl rounded-2xl p-6 flex flex-col items-center
relative w-full px-[10px] mx-[10px] bg-cover bg-center bg-no-repeat"
@ -367,7 +388,7 @@ export default {
misi:[],
logs:[],
authStore : useAuthStore(),
// currentUser : computed(() => this.authStore.currentUser)
savedUser : JSON.parse(localStorage.getItem('user')) || {}
};
},
methods: {
@ -416,8 +437,6 @@ export default {
const diffToStart = start - now;
const diffToEnd = end - now;
console.log("diffToStart:", diffToStart, "diffToEnd:", diffToEnd);
const pad = (n) => String(n).padStart(2, "0")
if (diffToStart > 0) {
const totalSeconds = Math.floor(diffToStart / 1000);
@ -581,6 +600,7 @@ export default {
},
computed: {
currentUser() {
console.log(this.authStore.currentUser)
return this.authStore.currentUser;
}
}

118
src/pages/Minigame.vue Normal file
View File

@ -0,0 +1,118 @@
<template>
<div
class="min-h-screen flex flex-col items-center justify-center bg-gradient-to-b from-emerald-200 via-teal-100 to-lime-200
relative overflow-hidden text-gray-800 mb-[-10px]"
style="background-image:url('/images/footer.png');
background-repeat:no-repeat;
background-position:bottom;
background-size: 100% 200px;"
margin-bottom=-10px;
>
<div class="absolute inset-0 z-0">
<div class="absolute top-10 left-10 w-72 h-72 bg-emerald-300/30 blur-3xl rounded-full animate-pulse"></div>
<div class="absolute bottom-10 right-10 w-96 h-96 bg-lime-400/30 blur-3xl rounded-full animate-pulse"></div>
</div>
<div class="w-full max-w-md flex flex-col items-center z-20 mt-10">
<img
src="/images/logo.png"
alt="Logo"
class="w-48 h-auto drop-shadow-xl hover:scale-105 transition-transform duration-500"
/>
</div>
<div
class="backdrop-blur-xl bg-white/30 border border-white/40 rounded-3xl shadow-2xl p-6 mt-10 w-[90%] max-w-md
flex flex-col items-center text-center relative overflow-hidden"
>
<div
class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-lime-400 via-emerald-300 to-cyan-300 rounded-t-3xl animate-[pulse_3s_infinite]"
></div>
<img
src="/images/games.png"
alt="Hiburan"
class="w-40 h-auto mt-6 transition-transform duration-500 hover:scale-110"
/>
<h2 class="text-3xl font-extrabold mt-4 bg-gradient-to-r from-emerald-500 to-lime-500 bg-clip-text text-transparent tracking-wide">
Mini Game
</h2>
<p class="text-gray-600 mt-2 text-sm">Pilih game favoritmu dan mainkan sesuka kamu</p>
</div>
<div class="w-[90%] max-w-md grid grid-cols-2 gap-6 mt-10 px-2 z-20 pb-[40px]"
>
<div
v-for="game in games"
:key="game.name"
@click="toContents(game)"
class="
group cursor-pointer relative flex flex-col items-center justify-center p-4 rounded-2xl
backdrop-blur-xl bg-white/30 border border-white/40 shadow-lg
hover:shadow-[0_0_25px_rgba(34,197,94,0.4)] hover:bg-white/50
transform-gpu transition-all duration-300 hover:-translate-y-2 hover:scale-105"
@mousemove="onMouseMove($event, game.name)"
@mouseleave="resetTilt(game.name)"
:style="{ transform: tiltTransforms[game.name] }"
>
<div v-if="game.badge"
class="absolute top-2 right-2 bg-emerald-500 text-white text-[10px] font-bold px-2 py-1 rounded-full shadow">
{{ game.badge }}
</div>
<div class="relative w-24 h-24 flex items-center justify-center">
<img
:src="game.img"
:alt="game.name"
class="w-full h-full object-contain drop-shadow-lg group-hover:scale-110 transition-transform duration-500"
/>
<div
class="absolute inset-0 bg-gradient-to-t from-emerald-400/20 to-transparent rounded-2xl opacity-0 group-hover:opacity-100 transition-opacity duration-500"
></div>
</div>
<p class="mt-3 text-emerald-800 font-bold tracking-wide group-hover:text-emerald-600 transition-colors duration-300">
{{ game.name }}
</p>
</div>
</div>
</div>
</template>
<script>
export default {
name: "mini-games",
data(){
return{
games: [
{ name: "Temukan kata", img: "/images/crossword.png", badge:"New" },
{ name: "Tebak Gambar", img: "/images/tebak-gambar.png", badge:"coming soon" },
],
tiltTransforms: {}
};
},
methods: {
toContents(content) {
if(content){
if(content.name == "Temukan kata")
this.$router.push({ name: "game-find-words", params: { content: content } });
}
},
resetTilt(name) {
this.tiltTransforms = {
...this.tiltTransforms,
[name]: `rotateY(0deg) rotateX(0deg)`
};
},
onMouseMove(event, name) {
const card = event.currentTarget;
const rect = card.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
const rotateY = ((x / rect.width) - 0.5) * 15;
const rotateX = ((y / rect.height) - 0.5) * -15;
// this.$set(this.tiltTransforms, name, `rotateY(${rotateY}deg) rotateX(${rotateX}deg)`);
this.tiltTransforms = {
...this.tiltTransforms,
[name]: `rotateY(${rotateY}deg) rotateX(${rotateX}deg)`
};
},
}
}
</script>

View File

@ -11,6 +11,8 @@ import ChaptersPage from '@/pages/Chapters.vue'
import ChapterListPage from '@/pages/ChapterList.vue'
import SynopsisPage from '@/pages/Synopsis.vue'
import MissionPage from '@/pages/MissionPage.vue'
import FindwordsPage from '@/pages/Findwords.vue'
import MinigamePage from '@/pages/Minigame.vue'
const routes = [
{ path: '/', name: 'home', component: HomePage , meta:{requiresAuth:true}},
@ -29,7 +31,11 @@ const routes = [
},
{ path: '/entertainment/manga/:manga_id/chapters/:chapter_id/', name:'manga-list', component:ChapterListPage, props:true},
{ path: '/entertainment/manga/:manga_id/chapters/:chapter_id/pages', name:'manga-read', component:ChapterListPage, props:true},
{ path:'/mission/quest/:id/missions', name: 'quest-missions', component:MissionPage}
{ path:'/mission/quest/:id/missions', name: 'quest-missions', component:MissionPage},
// game
{ path: '/entertainment/game/find-words/', name:'game-find-words', component:FindwordsPage, props:true},
{ path: '/entertainment/mini-games/', name:'mini-games', component:MinigamePage, props:true},
]
const router = createRouter({

View File

@ -28,3 +28,7 @@ export const updateMissionLog = async(missionId ,data = {}) =>{
headers: { "Content-Type": "multipart/form-data" },
});
}
// export const getUserPoint = async(user_id) =>{
// return await api.get();
// }