flipcards

This commit is contained in:
='fauz 2025-10-20 13:26:26 +07:00
parent a9933ccc8e
commit 5617cddcfa
5 changed files with 244 additions and 32 deletions

BIN
public/images/cards.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -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,20 +42,22 @@
</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">
Temukan semua kata positif seperti sabar, cinta, syukur, dan gembira untuk hati yang ceria!
<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>
@ -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
View 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>

View File

@ -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")
this.$router.push({ name: "game-find-words", params: { content: content } });
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) {

View File

@ -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({