add mini games
This commit is contained in:
parent
ca902a0078
commit
2882b64d25
BIN
public/images/crossword.png
Normal file
BIN
public/images/crossword.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
BIN
public/images/games.png
Normal file
BIN
public/images/games.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
@ -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
210
src/pages/Findwords.vue
Normal 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>
|
||||
@ -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
118
src/pages/Minigame.vue
Normal 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>
|
||||
@ -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({
|
||||
|
||||
@ -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();
|
||||
// }
|
||||
Loading…
Reference in New Issue
Block a user