flipcards
This commit is contained in:
parent
a9933ccc8e
commit
5617cddcfa
BIN
public/images/cards.png
Normal file
BIN
public/images/cards.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@ -1,11 +1,13 @@
|
||||
<template>
|
||||
<div class="flex flex-col min-h-screen bg-gradient-to-b from-indigo-400 to-blue-200 text-gray-900 select-none">
|
||||
<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 -->
|
||||
<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
|
||||
<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>
|
||||
</header>
|
||||
<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">
|
||||
@ -31,7 +33,7 @@
|
||||
@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"
|
||||
transition-all duration-200 ease-out shadow-md"
|
||||
:class="cellClass(rowIndex, colIndex)"
|
||||
>
|
||||
{{ letter }}
|
||||
@ -40,19 +42,21 @@
|
||||
</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">
|
||||
<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"
|
||||
:class="foundWords.includes(word) ? 'bg-green-300 text-green-800 animate-pulse' : 'bg-gray-200 text-gray-600'"
|
||||
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-gray-600 italic">
|
||||
<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>
|
||||
@ -62,18 +66,20 @@
|
||||
<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"
|
||||
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";
|
||||
|
||||
@ -126,7 +132,6 @@ function generateGrid() {
|
||||
}
|
||||
});
|
||||
|
||||
// isi huruf acak
|
||||
const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
for (let r = 0; r < size; r++) {
|
||||
for (let c = 0; c < size; c++) {
|
||||
@ -144,8 +149,8 @@ function cellClass(r, 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";
|
||||
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) {
|
||||
@ -159,7 +164,6 @@ function dragSelect(r, c) {
|
||||
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];
|
||||
@ -196,15 +200,11 @@ onMounted(() => {
|
||||
</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);
|
||||
}
|
||||
@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-pulse {
|
||||
animation: pulseGlow 1s infinite;
|
||||
.animate-bounce-glow {
|
||||
animation: bounceGlow 1s infinite;
|
||||
}
|
||||
</style>
|
||||
|
||||
207
src/pages/Flipcard.vue
Normal file
207
src/pages/Flipcard.vue
Normal file
@ -0,0 +1,207 @@
|
||||
<template>
|
||||
<div
|
||||
class="min-h-screen flex flex-col items-center justify-start bg-gradient-to-b from-green-100 to-green-300 pb-28 pr-2 pl-2 mb-[-10px] relative overflow-hidden"
|
||||
style="background-image:url('/images/footer.png');
|
||||
background-repeat:no-repeat;
|
||||
background-position:bottom;
|
||||
background-size: 100% 200px;"
|
||||
margin-bottom=-10px;
|
||||
>
|
||||
<!-- Top Bar -->
|
||||
|
||||
<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>Game Makanan Nusantara
|
||||
</h1>
|
||||
<p class="text-gray-500 mt-2">Temukan pasangan gambar sesuai gambar daerahnya 📖</p>
|
||||
</div>
|
||||
<!-- Game Area -->
|
||||
<main class="flex-1 w-full flex flex-col items-center justify-center mt-6">
|
||||
<!-- Loading -->
|
||||
<div v-if="isLoading" class="flex flex-col items-center justify-center mt-12">
|
||||
<div class="loader mb-4"></div>
|
||||
<p class="text-green-800 font-semibold text-lg">Menyiapkan kartu...</p>
|
||||
</div>
|
||||
|
||||
<!-- Game Grid -->
|
||||
<div
|
||||
v-else
|
||||
class="grid grid-cols-3 gap-4 w-full max-w-md mt-4 px-4 animate-fade-in-up"
|
||||
>
|
||||
<div
|
||||
v-for="(card, index) in cards"
|
||||
:key="index"
|
||||
class="relative w-full aspect-square cursor-pointer perspective"
|
||||
@click="flipCard(index)"
|
||||
>
|
||||
<div
|
||||
class="transition-transform duration-700 transform-style-preserve-3d w-full h-full"
|
||||
:class="{ 'rotate-y-180': flippedIndexes.includes(index) || card.isMatched }"
|
||||
>
|
||||
<!-- Card Back -->
|
||||
<div
|
||||
class="absolute w-full h-full bg-gradient-to-br from-green-400 to-emerald-500 rounded-xl flex flex-col items-center justify-center text-white backface-hidden border-4 border-lime-200 shadow-lg hover:scale-105 transition-transform"
|
||||
>
|
||||
<span class="text-4xl font-bold drop-shadow-lg">🍀</span>
|
||||
<small class="mt-1 font-medium text-xs">Klik Aku!</small>
|
||||
</div>
|
||||
|
||||
<!-- Card Front -->
|
||||
<div
|
||||
class="absolute w-full h-full rounded-xl border-4 border-lime-300 overflow-hidden rotate-y-180 backface-hidden shadow-xl"
|
||||
:class="{ 'bg-green-200': card.isMatched }"
|
||||
>
|
||||
<img
|
||||
:src="card.content"
|
||||
alt="Card"
|
||||
class="w-full h-full object-cover"
|
||||
@error="onImageError"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Footer note -->
|
||||
<footer class="absolute bottom-4 text-sm text-green-900 opacity-80">
|
||||
© 2025 Freekake Kids — Belajar sambil bermain 🌼
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from '@/util/api';
|
||||
|
||||
export default {
|
||||
name: "FlipCardGameKids",
|
||||
data() {
|
||||
return {
|
||||
contentId: 3,
|
||||
cards: [],
|
||||
pairs: {},
|
||||
flippedIndexes: [],
|
||||
isLoading: true,
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
await this.loadData();
|
||||
},
|
||||
methods: {
|
||||
async loadData() {
|
||||
try {
|
||||
const res = await api.get(
|
||||
`/content/contents/${this.contentId}`
|
||||
);
|
||||
|
||||
const data = res.data.data || {};
|
||||
const items = data.items || [];
|
||||
const pairs = data.pairs || {};
|
||||
|
||||
this.pairs = pairs;
|
||||
|
||||
const duplicated = [...items];
|
||||
duplicated.sort(() => Math.random() - 0.5);
|
||||
|
||||
this.cards = duplicated.map((url) => ({
|
||||
content: url,
|
||||
isMatched: false,
|
||||
}));
|
||||
|
||||
this.isLoading = false;
|
||||
} catch (e) {
|
||||
console.error("Load failed", e);
|
||||
}
|
||||
},
|
||||
|
||||
flipCard(index) {
|
||||
const card = this.cards[index];
|
||||
if (card.isMatched || this.flippedIndexes.includes(index)) return;
|
||||
|
||||
this.flippedIndexes.push(index);
|
||||
|
||||
if (this.flippedIndexes.length === 2) {
|
||||
const first = this.cards[this.flippedIndexes[0]];
|
||||
const second = this.cards[this.flippedIndexes[1]];
|
||||
const isMatch =
|
||||
this.pairs[first.content] === second.content ||
|
||||
this.pairs[second.content] === first.content;
|
||||
|
||||
setTimeout(() => {
|
||||
if (isMatch) {
|
||||
first.isMatched = true;
|
||||
second.isMatched = true;
|
||||
}
|
||||
this.flippedIndexes = [];
|
||||
}, 1000);
|
||||
}
|
||||
},
|
||||
|
||||
onImageError(event) {
|
||||
event.target.src = "https://via.placeholder.com/120x120?text=🍰";
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.perspective {
|
||||
perspective: 1000px;
|
||||
}
|
||||
.transform-style-preserve-3d {
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
.rotate-y-180 {
|
||||
transform: rotateY(180deg);
|
||||
}
|
||||
.backface-hidden {
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
/* Futuristic Loader */
|
||||
.loader {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 6px solid rgba(255, 255, 255, 0.5);
|
||||
border-top-color: #22c55e;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
/* Cute animation */
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-in-up {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-in-down {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fade-in-up {
|
||||
animation: fade-in-up 0.6s ease-out;
|
||||
}
|
||||
|
||||
.animate-fade-in-down {
|
||||
animation: fade-in-down 0.6s ease-out;
|
||||
}
|
||||
</style>
|
||||
@ -82,7 +82,7 @@ export default {
|
||||
return{
|
||||
games: [
|
||||
{ name: "Temukan kata", img: "/images/crossword.png", badge:"New" },
|
||||
{ name: "Tebak Gambar", img: "/images/tebak-gambar.png", badge:"coming soon" },
|
||||
{ name: "Tebak Gambar", img: "/images/cards.png", badge:"New" },
|
||||
],
|
||||
tiltTransforms: {}
|
||||
};
|
||||
@ -90,8 +90,11 @@ export default {
|
||||
methods: {
|
||||
toContents(content) {
|
||||
if(content){
|
||||
if(content.name == "Temukan kata")
|
||||
if(content.name == "Temukan kata"){
|
||||
this.$router.push({ name: "game-find-words", params: { content: content } });
|
||||
}else if(content.name == "Tebak Gambar"){
|
||||
this.$router.push({ name: "flipcard", params: { content: content } });
|
||||
}
|
||||
}
|
||||
},
|
||||
resetTilt(name) {
|
||||
|
||||
@ -13,6 +13,7 @@ import SynopsisPage from '@/pages/Synopsis.vue'
|
||||
import MissionPage from '@/pages/MissionPage.vue'
|
||||
import FindwordsPage from '@/pages/Findwords.vue'
|
||||
import MinigamePage from '@/pages/Minigame.vue'
|
||||
import FlipcardPage from '@/pages/Flipcard.vue'
|
||||
|
||||
const routes = [
|
||||
{ path: '/', name: 'home', component: HomePage , meta:{requiresAuth:true}},
|
||||
@ -36,6 +37,7 @@ const routes = [
|
||||
// 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},
|
||||
{ path: '/entertainment/mini-games/flipcard/', name:'flipcard', component:FlipcardPage, props:true},
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
|
||||
Loading…
Reference in New Issue
Block a user