Draft Mission and game
This commit is contained in:
parent
5617cddcfa
commit
6e4decfe5c
BIN
public/images/word-spells.png
Normal file
BIN
public/images/word-spells.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
@ -1,4 +1,26 @@
|
|||||||
<template>
|
<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"
|
<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;">
|
style="background-image:url('/images/footer.png'); background-size:cover; background-position:center;">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
@ -194,6 +216,14 @@ function resetGame() {
|
|||||||
generateGrid();
|
generateGrid();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function goEntertainment(){
|
||||||
|
window.history.back('/entertainment');
|
||||||
|
}
|
||||||
|
|
||||||
|
function goBack(){
|
||||||
|
window.history.back('/entertainment/mini-games');
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
resetGame();
|
resetGame();
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,4 +1,26 @@
|
|||||||
<template>
|
<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">Pasangan Gambar</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div
|
<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"
|
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');
|
style="background-image:url('/images/footer.png');
|
||||||
@ -8,7 +30,6 @@
|
|||||||
margin-bottom=-10px;
|
margin-bottom=-10px;
|
||||||
>
|
>
|
||||||
<!-- Top Bar -->
|
<!-- Top Bar -->
|
||||||
|
|
||||||
<div class="text-center mt-8">
|
<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">
|
<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
|
<span>🌿</span>Game Makanan Nusantara
|
||||||
@ -137,6 +158,14 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
goEntertainment(){
|
||||||
|
window.history.back('/entertainment');
|
||||||
|
},
|
||||||
|
|
||||||
|
goBack(){
|
||||||
|
window.history.back('/entertainment/mini-games');
|
||||||
|
},
|
||||||
|
|
||||||
onImageError(event) {
|
onImageError(event) {
|
||||||
event.target.src = "https://via.placeholder.com/120x120?text=🍰";
|
event.target.src = "https://via.placeholder.com/120x120?text=🍰";
|
||||||
},
|
},
|
||||||
|
|||||||
@ -54,13 +54,13 @@
|
|||||||
<span class="text-xs font-bold text-gray-800 items-center text-center">{{ 'KOIN' }}</span>
|
<span class="text-xs font-bold text-gray-800 items-center text-center">{{ 'KOIN' }}</span>
|
||||||
<div class="flex items-end justify-center gap-2 p-1">
|
<div class="flex items-end justify-center gap-2 p-1">
|
||||||
<img src="/images/token-poin.svg" alt="coin" class="w-6 h-6" />
|
<img src="/images/token-poin.svg" alt="coin" class="w-6 h-6" />
|
||||||
<span>100</span>
|
<span>{{ this.points }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-white w-[90px] h-[60px] rounded-xl p-1 shadow-lg flex flex-col bottom-0">
|
<div class="bg-white w-[90px] h-[60px] rounded-xl p-1 shadow-lg flex flex-col bottom-0">
|
||||||
<span class="text-xs font-bold text-gray-800 items-center text-center">{{ 'Dayly Quest' }}</span>
|
<span class="text-xs font-bold text-gray-800 items-center text-center">{{ 'Dayly Quest' }}</span>
|
||||||
<div class="flex items-end justify-center gap-2 p-1">
|
<div class="flex items-end justify-center gap-2 p-1">
|
||||||
<span>3/10</span>
|
<span>{{ this.totalDone }}/{{ this.totalTask }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -74,7 +74,7 @@
|
|||||||
|
|
||||||
<div class="logo w-full max-w-md flex flex-col items-center mt-4">
|
<div class="logo w-full max-w-md flex flex-col items-center mt-4">
|
||||||
<div class="bg-grey shadow-xl rounded-2xl px-16 py-1 flex flex-col items-center">
|
<div class="bg-grey shadow-xl rounded-2xl px-16 py-1 flex flex-col items-center">
|
||||||
<h2 class="mt-4 mb-4 text-md font-bold text-grey-400">Level 10 - Hari ke-2</h2>
|
<h2 class="mt-4 mb-4 text-md font-bold text-grey-400">Mission List</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -112,148 +112,226 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div> -->
|
</div> -->
|
||||||
<div
|
<div v-if="missions.length > 0">
|
||||||
v-for="mission in missions"
|
<div
|
||||||
:key="mission.id"
|
v-for="mission in missions"
|
||||||
|
:key="mission.id"
|
||||||
class="w-full max-w-md flex flex-col items-center bg-white-grey py-[0px] mx-2.5 md:mx-5
|
|
||||||
transform transition-transform duration-300 hover:scale-105"
|
class="w-full max-w-md flex flex-col items-center bg-white-grey py-[0px] mx-2.5 md:mx-5
|
||||||
>
|
transform transition-transform duration-300 hover:scale-105"
|
||||||
<!-- <div class="bg-white rounded-3xl p-6 shadow-2xl-soft flex items-start gap-4 w-full py-[10px] my-[20px]">
|
|
||||||
<div class="flex-shrink-0 mt-1">
|
|
||||||
<img src="/images/qicon2.png" alt="Quest Icon" class="w-12 h-12" />
|
|
||||||
</div>
|
|
||||||
<div class="flex-1">
|
|
||||||
<h2 class="text-xl font-bold text-gray-800">{{ mission.name }}</h2>
|
|
||||||
<p class="text-sm text-gray-600 mb-2">{{ mission.description }}</p>
|
|
||||||
<p class="text-xs font-semibold"
|
|
||||||
:class="{
|
|
||||||
'text-green-600': mission.userStatus === 'completed',
|
|
||||||
'text-yellow-600': mission.userStatus === 'in_progress',
|
|
||||||
'text-gray-500': mission.userStatus === 'not_started'
|
|
||||||
}">
|
|
||||||
Status: mission.userStatus
|
|
||||||
</p>
|
|
||||||
<div class="flex items-center gap-4 text-xs text-gray-700">
|
|
||||||
<span class="flex items-center gap-1">
|
|
||||||
<svg class="w-4 h-4 text-gray-500" fill="currentColor" viewBox="0 0 20 20">
|
|
||||||
<path d="M10 2a8 8 0 100 16 8 8 0 000-16zM9 11V5a1 1 0 012 0v6a1 1 0 01-2 0zM10 13a1 1 0 100 2 1 1 0 000-2z"/>
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
</span>
|
|
||||||
<span class="flex items-center gap-1">
|
|
||||||
<svg class="w-4 h-4 text-yellow-500" fill="currentColor" viewBox="0 0 20 20"><path d="M10 15a5 5 0 100-10 5 5 0 000 10z"/><path fill-rule="evenodd" d="M10 0a10 10 0 100 20 10 10 0 000-20zm0 18a8 8 0 100-16 8 8 0 000 16z" clip-rule="evenodd"/></svg>
|
|
||||||
+ {{ mission.coin }} koin
|
|
||||||
</span>
|
|
||||||
<span class="flex items-center gap-1">
|
|
||||||
<svg class="w-4 h-4 text-green-600 transform rotate-180" fill="currentColor" viewBox="0 0 20 20"><path d="M10 14a1 1 0 01-.707-.293l-4-4a1 1 0 011.414-1.414L10 11.586l3.293-3.293a1 1 0 011.414 1.414l-4 4A1 1 0 0110 14z"/></svg>
|
|
||||||
+{{ '100' }}mm
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-4 text-xs text-gray-700">
|
|
||||||
<button v-if="!mission.userStatus ==='completed'" class="w-full mt-4 py-2 px-4 rounded-lg bg-red-600 text-white font-bold
|
|
||||||
hover:bg-green-700 transition"
|
|
||||||
@click="handleMissionClick(mission)"
|
|
||||||
>
|
|
||||||
{{ mission.userStatus === 'completed' ? 'SELESAI' :
|
|
||||||
mission.userStatus === 'in_progress' ? 'LANJUTKAN' :
|
|
||||||
'MULAI QUEST' }}
|
|
||||||
</button>
|
|
||||||
<span v-else >Selesai</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div> -->
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="relative rounded-3xl p-6 shadow-2xl-soft flex items-start gap-4 w-full py-[10px] my-[20px] overflow-hidden"
|
|
||||||
:class="{
|
|
||||||
'bg-green-100 pointer-events-none opacity-95': mission.userStatus === 'completed',
|
|
||||||
'bg-yellow-100': mission.userStatus === 'in_progress',
|
|
||||||
'bg-white hover:scale-105': mission.userStatus === 'not_started'
|
|
||||||
}"
|
|
||||||
>
|
>
|
||||||
<!-- ✅ Stamp layer BEHIND content -->
|
<!-- <div class="bg-white rounded-3xl p-6 shadow-2xl-soft flex items-start gap-4 w-full py-[10px] my-[20px]">
|
||||||
<div
|
<div class="flex-shrink-0 mt-1">
|
||||||
v-if="mission.userStatus === 'completed'"
|
<img src="/images/qicon2.png" alt="Quest Icon" class="w-12 h-12" />
|
||||||
class="absolute inset-0 flex justify-end items-center pr-6 z-0"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="text-[64px] font-extrabold text-green-600/15 rotate-[-20deg] select-none pointer-events-none"
|
|
||||||
style="mix-blend-mode: multiply;"
|
|
||||||
>
|
|
||||||
SELESAI
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ✅ Content layer ABOVE stamp -->
|
|
||||||
<div class="flex-shrink-0 mt-1 relative z-10">
|
|
||||||
<img src="/images/qicon2.png" alt="Quest Icon" class="w-12 h-12" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex-1 relative z-10">
|
|
||||||
<h2 class="text-xl font-bold text-gray-800">{{ mission.name }}</h2>
|
|
||||||
<p class="text-sm text-gray-600 mb-2">{{ mission.description }}</p>
|
|
||||||
|
|
||||||
<p
|
|
||||||
class="text-xs font-semibold border-b pb-2"
|
|
||||||
:class="{
|
|
||||||
'text-green-600': mission.userStatus === 'completed',
|
|
||||||
'text-yellow-600': mission.userStatus === 'in_progress',
|
|
||||||
'text-gray-500': mission.userStatus === 'not_started'
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<!-- Status: {{ mission.userStatus }} -->
|
|
||||||
</p>
|
|
||||||
<div class="flex items-center gap-4 text-xs text-gray-700 pt-2">
|
|
||||||
<span class="flex items-center gap-1">
|
|
||||||
<svg class="w-4 h-4 text-gray-500" fill="currentColor" viewBox="0 0 20 20">
|
|
||||||
<path
|
|
||||||
d="M10 2a8 8 0 100 16 8 8 0 000-16zM9 11V5a1 1 0 012 0v6a1 1 0 01-2 0zM10 13a1 1 0 100 2 1 1 0 000-2z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
{{ timeLeftDH(mission.date_time_from_valid,mission.date_time_to_valid) }}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span class="flex items-center gap-1">
|
|
||||||
<svg class="w-4 h-4 text-yellow-500" fill="currentColor" viewBox="0 0 20 20">
|
|
||||||
<path d="M10 15a5 5 0 100-10 5 5 0 000 10z" />
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M10 0a10 10 0 100 20 10 10 0 000-20zm0 18a8 8 0 100-16 8 8 0 000 16z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
+{{ mission.coin }} koin
|
|
||||||
</span>
|
|
||||||
<span class="flex items-center gap-1">
|
|
||||||
<svg
|
|
||||||
class="w-4 h-4 text-green-600 transform rotate-180"
|
|
||||||
fill="currentColor"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M10 14a1 1 0 01-.707-.293l-4-4a1 1 0 011.414-1.414L10 11.586l3.293-3.293a1 1 0 011.414 1.414l-4 4A1 1 0 0110 14z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
+100mm
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex-1">
|
||||||
<!-- ✅ Button or empty block -->
|
<h2 class="text-xl font-bold text-gray-800">{{ mission.name }}</h2>
|
||||||
<div class="flex items-center gap-4 text-xs text-gray-700">
|
<p class="text-sm text-gray-600 mb-2">{{ mission.description }}</p>
|
||||||
<button
|
<p class="text-xs font-semibold"
|
||||||
v-if="mission.userStatus !== 'completed'"
|
:class="{
|
||||||
:disabled="!isMissionActive(mission.date_time_from_valid, mission.date_time_to_valid)"
|
'text-green-600': mission.userStatus === 'completed',
|
||||||
class="w-full mt-4 py-2 px-4 rounded-lg bg-red-600 text-white font-bold hover:bg-green-700 transition"
|
'text-yellow-600': mission.userStatus === 'in_progress',
|
||||||
|
'text-gray-500': mission.userStatus === 'not_started'
|
||||||
|
}">
|
||||||
|
Status: mission.userStatus
|
||||||
|
</p>
|
||||||
|
<div class="flex items-center gap-4 text-xs text-gray-700">
|
||||||
|
<span class="flex items-center gap-1">
|
||||||
|
<svg class="w-4 h-4 text-gray-500" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path d="M10 2a8 8 0 100 16 8 8 0 000-16zM9 11V5a1 1 0 012 0v6a1 1 0 01-2 0zM10 13a1 1 0 100 2 1 1 0 000-2z"/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
</span>
|
||||||
|
<span class="flex items-center gap-1">
|
||||||
|
<svg class="w-4 h-4 text-yellow-500" fill="currentColor" viewBox="0 0 20 20"><path d="M10 15a5 5 0 100-10 5 5 0 000 10z"/><path fill-rule="evenodd" d="M10 0a10 10 0 100 20 10 10 0 000-20zm0 18a8 8 0 100-16 8 8 0 000 16z" clip-rule="evenodd"/></svg>
|
||||||
|
+ {{ mission.coin }} koin
|
||||||
|
</span>
|
||||||
|
<span class="flex items-center gap-1">
|
||||||
|
<svg class="w-4 h-4 text-green-600 transform rotate-180" fill="currentColor" viewBox="0 0 20 20"><path d="M10 14a1 1 0 01-.707-.293l-4-4a1 1 0 011.414-1.414L10 11.586l3.293-3.293a1 1 0 011.414 1.414l-4 4A1 1 0 0110 14z"/></svg>
|
||||||
|
+{{ '100' }}mm
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-4 text-xs text-gray-700">
|
||||||
|
<button v-if="!mission.userStatus ==='completed'" class="w-full mt-4 py-2 px-4 rounded-lg bg-red-600 text-white font-bold
|
||||||
|
hover:bg-green-700 transition"
|
||||||
@click="handleMissionClick(mission)"
|
@click="handleMissionClick(mission)"
|
||||||
|
>
|
||||||
|
{{ mission.userStatus === 'completed' ? 'SELESAI' :
|
||||||
|
mission.userStatus === 'in_progress' ? 'LANJUTKAN' :
|
||||||
|
'MULAI QUEST' }}
|
||||||
|
</button>
|
||||||
|
<span v-else >Selesai</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="relative rounded-3xl p-6 shadow-2xl-soft flex items-start gap-4 w-full py-[10px] my-[20px] overflow-hidden"
|
||||||
|
:class="{
|
||||||
|
'bg-green-100 pointer-events-none opacity-95': mission.userStatus === 'completed',
|
||||||
|
'bg-yellow-100': mission.userStatus === 'in_progress',
|
||||||
|
'bg-white hover:scale-105': mission.userStatus === 'not_started'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<!-- ✅ Stamp layer BEHIND content -->
|
||||||
|
<div
|
||||||
|
v-if="mission.userStatus === 'completed'"
|
||||||
|
class="absolute inset-0 flex justify-end items-center pr-6 z-0"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="text-[64px] font-extrabold text-green-600/15 rotate-[-20deg] select-none pointer-events-none"
|
||||||
|
style="mix-blend-mode: multiply;"
|
||||||
>
|
>
|
||||||
{{ mission.userStatus === 'in_progress' ? 'LANJUTKAN' : 'MULAI QUEST' }}
|
SELESAI
|
||||||
</button>
|
</span>
|
||||||
<div v-else class="w-full mt-4 py-4 px-4"></div>
|
</div>
|
||||||
|
<div class="flex-shrink-0 mt-1 relative z-10">
|
||||||
|
<img src="/images/qicon2.png" alt="Quest Icon" class="w-12 h-12" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1 relative z-10">
|
||||||
|
<h2 class="text-xl font-bold text-gray-800">{{ mission.name }}</h2>
|
||||||
|
<p class="text-sm text-gray-600 mb-2">{{ mission.description }}</p>
|
||||||
|
|
||||||
|
<p
|
||||||
|
class="text-xs font-semibold border-b pb-2"
|
||||||
|
:class="{
|
||||||
|
'text-green-600': mission.userStatus === 'completed',
|
||||||
|
'text-yellow-600': mission.userStatus === 'in_progress',
|
||||||
|
'text-gray-500': mission.userStatus === 'not_started'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<!-- Status: {{ mission.userStatus }} -->
|
||||||
|
</p>
|
||||||
|
<div class="flex items-center gap-4 text-xs text-gray-700 pt-2">
|
||||||
|
<span class="flex items-center gap-1">
|
||||||
|
<svg class="w-4 h-4 text-gray-500" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path
|
||||||
|
d="M10 2a8 8 0 100 16 8 8 0 000-16zM9 11V5a1 1 0 012 0v6a1 1 0 01-2 0zM10 13a1 1 0 100 2 1 1 0 000-2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{{ timeLeftDH(mission.date_time_from_valid,mission.date_time_to_valid) }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="flex items-center gap-1">
|
||||||
|
<svg class="w-4 h-4 text-yellow-500" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path d="M10 15a5 5 0 100-10 5 5 0 000 10z" />
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M10 0a10 10 0 100 20 10 10 0 000-20zm0 18a8 8 0 100-16 8 8 0 000 16z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
+{{ mission.coin }} koin
|
||||||
|
</span>
|
||||||
|
<span class="flex items-center gap-1">
|
||||||
|
<svg
|
||||||
|
class="w-4 h-4 text-green-600 transform rotate-180"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M10 14a1 1 0 01-.707-.293l-4-4a1 1 0 011.414-1.414L10 11.586l3.293-3.293a1 1 0 011.414 1.414l-4 4A1 1 0 0110 14z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
+100mm
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ✅ Button or empty block -->
|
||||||
|
<div class="flex items-center gap-4 text-xs text-gray-700">
|
||||||
|
<button
|
||||||
|
v-if="mission.userStatus !== 'completed'"
|
||||||
|
:disabled="!isMissionActive(mission.date_time_from_valid, mission.date_time_to_valid)"
|
||||||
|
class="w-full mt-4 py-2 px-4 rounded-lg bg-red-600 text-white font-bold hover:bg-green-700 transition"
|
||||||
|
@click="handleMissionClick(mission)"
|
||||||
|
>
|
||||||
|
{{ mission.userStatus === 'in_progress' ? 'LANJUTKAN' : 'MULAI QUEST' }}
|
||||||
|
</button>
|
||||||
|
<div v-else class="w-full mt-4 py-4 px-4"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<div
|
||||||
|
class="w-full max-w-md flex flex-col items-center bg-white-grey py-[0px] mx-2.5 md:mx-5
|
||||||
|
transform transition-transform duration-300 hover:scale-105"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="relative rounded-3xl p-6 shadow-2xl-soft flex items-start gap-4 w-full py-[10px] my-[20px] overflow-hidden bg-white hover:scale-105"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 flex justify-end items-center pr-6 z-0"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="text-[30px] font-extrabold text-green-600/15 rotate-[-20deg] select-none pointer-events-none"
|
||||||
|
style="mix-blend-mode: multiply;"
|
||||||
|
>
|
||||||
|
Coming Soon
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex-shrink-0 mt-1 relative z-10">
|
||||||
|
<img src="/images/qicon2.png" alt="Quest Icon" class="w-12 h-12" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1 relative z-10">
|
||||||
|
<h2 class="text-xl font-bold text-gray-800">{{ 'Mission Draft' }}</h2>
|
||||||
|
<p class="text-sm text-gray-600 mb-2">{{ '-----------------------------------------------------------' }}</p>
|
||||||
|
|
||||||
|
<p
|
||||||
|
class="text-xs font-semibold border-b pb-2 text-gray-500"
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
<div class="flex items-center gap-4 text-xs text-gray-700 pt-2">
|
||||||
|
<span class="flex items-center gap-1">
|
||||||
|
<svg class="w-4 h-4 text-gray-500" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path
|
||||||
|
d="M10 2a8 8 0 100 16 8 8 0 000-16zM9 11V5a1 1 0 012 0v6a1 1 0 01-2 0zM10 13a1 1 0 100 2 1 1 0 000-2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{{ '----' }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="flex items-center gap-1">
|
||||||
|
<svg class="w-4 h-4 text-yellow-500" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path d="M10 15a5 5 0 100-10 5 5 0 000 10z" />
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M10 0a10 10 0 100 20 10 10 0 000-20zm0 18a8 8 0 100-16 8 8 0 000 16z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
----
|
||||||
|
</span>
|
||||||
|
<span class="flex items-center gap-1">
|
||||||
|
<svg
|
||||||
|
class="w-4 h-4 text-green-600 transform rotate-180"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M10 14a1 1 0 01-.707-.293l-4-4a1 1 0 011.414-1.414L10 11.586l3.293-3.293a1 1 0 011.414 1.414l-4 4A1 1 0 0110 14z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
---
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ✅ Button or empty block -->
|
||||||
|
<div class="flex items-center gap-4 text-xs text-gray-700">
|
||||||
|
<button
|
||||||
|
disabled
|
||||||
|
class="w-full mt-4 py-2 px-4 rounded-lg bg-red-200 text-white font-bold hover:bg-green-700 transition"
|
||||||
|
>
|
||||||
|
Coming Soon
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- <div class="w-full max-w-md flex flex-col items-center bg-green-grey py-[0px] mx-2.5 md:mx-5
|
<!-- <div class="w-full max-w-md flex flex-col items-center bg-green-grey py-[0px] mx-2.5 md:mx-5
|
||||||
transform transition-transform duration-300 hover:scale-105
|
transform transition-transform duration-300 hover:scale-105
|
||||||
@ -331,6 +409,16 @@
|
|||||||
<span>Logout</span>
|
<span>Logout</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="col-span-3 flex justify-center mt-2">
|
||||||
|
<button
|
||||||
|
@click="$router.push('/profile/mission-history')"
|
||||||
|
class="bg-yellow-400 text-indigo-900 font-bold px-4 py-2 rounded-full shadow-md hover:bg-yellow-300 active:scale-95 transition-all"
|
||||||
|
>
|
||||||
|
⭐ Lihat Riwayat Poin
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-3 gap-4" v-if="modalType === 'character'">
|
<div class="grid grid-cols-3 gap-4" v-if="modalType === 'character'">
|
||||||
<div
|
<div
|
||||||
@ -352,7 +440,7 @@
|
|||||||
<script>
|
<script>
|
||||||
// import { computed } from "vue"
|
// import { computed } from "vue"
|
||||||
import { getContent } from '@/services/content';
|
import { getContent } from '@/services/content';
|
||||||
import { getMissions,getMissionLogs, createMissionLog } from '@/services/missions';
|
import { getMissions,getMissionLogs, createMissionLog, getUserPoint } from '@/services/missions';
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import { nextTick } from 'vue';
|
import { nextTick } from 'vue';
|
||||||
|
|
||||||
@ -389,7 +477,10 @@ export default {
|
|||||||
misi:[],
|
misi:[],
|
||||||
logs:[],
|
logs:[],
|
||||||
authStore : useAuthStore(),
|
authStore : useAuthStore(),
|
||||||
savedUser : JSON.parse(localStorage.getItem('user')) || {}
|
savedUser : JSON.parse(localStorage.getItem('user')) || {},
|
||||||
|
points :0,
|
||||||
|
totalTask : 0,
|
||||||
|
totalDone :0,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@ -418,10 +509,16 @@ export default {
|
|||||||
const now = new Date();
|
const now = new Date();
|
||||||
return now >= start && now <= end;
|
return now >= start && now <= end;
|
||||||
},
|
},
|
||||||
|
async myPoints(){
|
||||||
|
const points = await getUserPoint();
|
||||||
|
console.log(points)
|
||||||
|
this.points = points.myPoints;
|
||||||
|
this.totalTask = points.taskCount;
|
||||||
|
this.totalDone = points.myTaskCount;
|
||||||
|
},
|
||||||
async getMission() {
|
async getMission() {
|
||||||
const res = await getMissions();
|
const res = await getMissions();
|
||||||
this.missions = res.results;
|
this.missions = res.results;
|
||||||
console.log(this.missions);
|
|
||||||
},
|
},
|
||||||
async getLogs(){
|
async getLogs(){
|
||||||
const resLog = await getMissionLogs();
|
const resLog = await getMissionLogs();
|
||||||
@ -545,7 +642,6 @@ export default {
|
|||||||
this.missions = this.misi
|
this.missions = this.misi
|
||||||
},
|
},
|
||||||
handleMissionClick(mission) {
|
handleMissionClick(mission) {
|
||||||
//convert to array task
|
|
||||||
const missionWithTask = {
|
const missionWithTask = {
|
||||||
...mission,
|
...mission,
|
||||||
tasks:[
|
tasks:[
|
||||||
@ -598,6 +694,8 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
// this.getLogs();
|
||||||
|
this.myPoints();
|
||||||
this.getMission();
|
this.getMission();
|
||||||
this.getMissions();
|
this.getMissions();
|
||||||
this.timer = setInterval(() => {
|
this.timer = setInterval(() => {
|
||||||
|
|||||||
@ -1,4 +1,24 @@
|
|||||||
<template>
|
<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="goBack">Entertainment</span>
|
||||||
|
<span class="mx-2">›</span>
|
||||||
|
<span class="text-gray-700 font-semibold">Mini Game</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div
|
<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
|
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]"
|
relative overflow-hidden text-gray-800 mb-[-10px]"
|
||||||
@ -83,6 +103,7 @@ export default {
|
|||||||
games: [
|
games: [
|
||||||
{ name: "Temukan kata", img: "/images/crossword.png", badge:"New" },
|
{ name: "Temukan kata", img: "/images/crossword.png", badge:"New" },
|
||||||
{ name: "Tebak Gambar", img: "/images/cards.png", badge:"New" },
|
{ name: "Tebak Gambar", img: "/images/cards.png", badge:"New" },
|
||||||
|
{ name: "Word Spells", img: "/images/word-spells.png", badge:"New" },
|
||||||
],
|
],
|
||||||
tiltTransforms: {}
|
tiltTransforms: {}
|
||||||
};
|
};
|
||||||
@ -94,6 +115,8 @@ export default {
|
|||||||
this.$router.push({ name: "game-find-words", params: { content: content } });
|
this.$router.push({ name: "game-find-words", params: { content: content } });
|
||||||
}else if(content.name == "Tebak Gambar"){
|
}else if(content.name == "Tebak Gambar"){
|
||||||
this.$router.push({ name: "flipcard", params: { content: content } });
|
this.$router.push({ name: "flipcard", params: { content: content } });
|
||||||
|
}else if(content.name == "Word Spells"){
|
||||||
|
this.$router.push({name: "word-spells", params: {content: content}})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -116,6 +139,9 @@ export default {
|
|||||||
[name]: `rotateY(${rotateY}deg) rotateX(${rotateX}deg)`
|
[name]: `rotateY(${rotateY}deg) rotateX(${rotateX}deg)`
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
goBack(){
|
||||||
|
window.history.back('/entertainment');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
131
src/pages/Pointhistory.vue
Normal file
131
src/pages/Pointhistory.vue
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
<template>
|
||||||
|
<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">History</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="min-h-screen bg-gradient-to-b from-green-100 to-green-300 text-white font-sans flex flex-col">
|
||||||
|
<!-- 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 pb-4">
|
||||||
|
<span></span>Point History
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<!-- Summary Card -->
|
||||||
|
<div class="p-4 flex justify-center">
|
||||||
|
<div class="bg-gradient-to-b from-gray-400 to-slate-500 rounded-2xl p-6 text-center w-full max-w-sm shadow-lg backdrop-blur-md animate-[pulse_3s_infinite]">
|
||||||
|
<p class="text-md text-black font-bold">Total Poin </p>
|
||||||
|
<h2 class="text-4xl font-bold text-yellow-300">{{ totalPoints }}</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filter Section -->
|
||||||
|
<div class="flex justify-center space-x-3 mt-2 mb-4">
|
||||||
|
<button
|
||||||
|
v-for="cat in categories"
|
||||||
|
:key="cat"
|
||||||
|
class="px-3 py-1 rounded-full text-sm font-semibold transition-all"
|
||||||
|
:class="selectedCategory === cat
|
||||||
|
? 'bg-yellow-400 text-indigo-900'
|
||||||
|
: 'bg-white bg-opacity-20 text-black hover:bg-opacity-40'"
|
||||||
|
@click="selectedCategory = cat"
|
||||||
|
>
|
||||||
|
{{ cat }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Point History List -->
|
||||||
|
<main class="flex-1 overflow-y-auto px-4 pb-6">
|
||||||
|
<transition-group name="fade" tag="div" class="space-y-3 max-w-xl mx-auto">
|
||||||
|
<div
|
||||||
|
v-for="(entry, index) in filteredHistory"
|
||||||
|
:key="entry.id"
|
||||||
|
class="flex justify-between items-center bg-white bg-opacity-15 backdrop-blur-md rounded-xl p-4 shadow hover:bg-opacity-25 transition-all"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-semibold text-black">{{ entry.activity }} {{ index }}</h3>
|
||||||
|
<p class="text-sm text-black">{{ entry.source }}</p>
|
||||||
|
<p class="text-xs text-black">{{ entry.date }}</p>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
class="font-bold text-lg"
|
||||||
|
:class="entry.points > 0 ? 'text-orange-300' : 'text-red-300'"
|
||||||
|
>
|
||||||
|
{{ entry.points > 0 ? '+' : '' }}{{ entry.points }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</transition-group>
|
||||||
|
|
||||||
|
<p v-if="filteredHistory.length === 0" class="text-center mt-8 text-indigo-200 italic">
|
||||||
|
Belum ada riwayat poin untuk kategori ini 🌱
|
||||||
|
</p>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<!-- <footer class="py-4 text-center bg-indigo-700 text-sm">
|
||||||
|
🌟 Terus kumpulkan poin dari aktivitas positifmu!
|
||||||
|
</footer> -->
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default{
|
||||||
|
name:'point-history'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from "vue";
|
||||||
|
|
||||||
|
// Data contoh (bisa diganti dari backend)
|
||||||
|
const pointHistory = ref([
|
||||||
|
{ id: 1, activity: "Menang Game Word Spells", source: "Game", points: 30, date: "2025-10-21 10:22" },
|
||||||
|
{ id: 2, activity: "Membaca artikel motivasi", source: "Content", points: 10, date: "2025-10-21 12:00" },
|
||||||
|
{ id: 3, activity: "Membaca manga positif", source: "Manga", points: 15, date: "2025-10-22 09:15" },
|
||||||
|
{ id: 4, activity: "Kehilangan streak harian", source: "Penalty", points: -5, date: "2025-10-22 09:20" },
|
||||||
|
{ id: 5, activity: "Menjawab quiz refleksi diri", source: "Quiz", points: 25, date: "2025-10-22 11:30" },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Filter kategori
|
||||||
|
const categories = ["Semua", "Game", "Content", "Manga", "Quiz", "Penalty"];
|
||||||
|
const selectedCategory = ref("Semua");
|
||||||
|
|
||||||
|
// Hitung total poin
|
||||||
|
const totalPoints = computed(() =>
|
||||||
|
pointHistory.value.reduce((sum, e) => sum + e.points, 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Filter sesuai kategori
|
||||||
|
const filteredHistory = computed(() => {
|
||||||
|
if (selectedCategory.value === "Semua") return pointHistory.value;
|
||||||
|
return pointHistory.value.filter(
|
||||||
|
e => e.source.toLowerCase() === selectedCategory.value.toLowerCase()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.fade-enter-active, .fade-leave-active {
|
||||||
|
transition: all 0.4s ease;
|
||||||
|
}
|
||||||
|
.fade-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
266
src/pages/Wordspells.vue
Normal file
266
src/pages/Wordspells.vue
Normal file
@ -0,0 +1,266 @@
|
|||||||
|
<template>
|
||||||
|
<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
|
||||||
|
@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 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">Temukan Kata</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<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;
|
||||||
|
>
|
||||||
|
<main class="flex-1 flex flex-col items-center justify-center relative">
|
||||||
|
<canvas ref="canvas" class="absolute inset-0 pointer-events-none"></canvas>
|
||||||
|
<div class="bg-white text-gray-800 px-5 py-2 rounded-full shadow-lg text-2xl font-bold mb-8 min-h-[50px] flex items-center justify-center">
|
||||||
|
{{ currentWord || "..." }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
ref="circleRef"
|
||||||
|
class="relative flex items-center justify-center"
|
||||||
|
style="width: 300px; height: 300px;"
|
||||||
|
@touchend="endSelect"
|
||||||
|
@mouseup="endSelect"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(letter, i) in letters"
|
||||||
|
:key="i"
|
||||||
|
class="absolute flex items-center justify-center bg-white text-indigo-700 rounded-full font-bold shadow-lg transition-all duration-200 active:scale-90 select-none"
|
||||||
|
:style="letterPosition(i)"
|
||||||
|
:class="[
|
||||||
|
totalSizeClass,
|
||||||
|
selectedIndexes.includes(i) ? 'bg-yellow-300 text-yellow-900 scale-105' : ''
|
||||||
|
]"
|
||||||
|
@mousedown="startSelect(i)"
|
||||||
|
@touchstart.prevent="startSelect(i)"
|
||||||
|
@mouseenter="dragSelect(i)"
|
||||||
|
>
|
||||||
|
{{ letter }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-8 bg-white bg-opacity-20 p-4 rounded-2xl w-full max-w-md text-center shadow-md">
|
||||||
|
<h2 class="font-semibold mb-2 text-lg">✨ Kata yang Kamu Temukan:</h2>
|
||||||
|
<div class="flex flex-wrap justify-center gap-2 min-h-[40px]">
|
||||||
|
<span
|
||||||
|
v-for="word in foundWords"
|
||||||
|
:key="word"
|
||||||
|
class="px-3 py-1 rounded-full text-sm font-semibold bg-green-300 text-green-900 animate-pulse"
|
||||||
|
>
|
||||||
|
{{ word }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span v-if="foundWords.length === 0" class="text-gray-200 italic">
|
||||||
|
Belum ada kata ditemukan 🌱
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="resetGame"
|
||||||
|
class="mt-8 px-6 py-2 bg-pink-500 rounded-full font-bold text-white shadow hover:bg-pink-600 active:scale-95"
|
||||||
|
>
|
||||||
|
🔄 Main Lagi
|
||||||
|
</button>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "word-spells"
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, nextTick, computed } from "vue";
|
||||||
|
|
||||||
|
const allWords = [
|
||||||
|
"TENANG", "SABAR", "SYUKUR", "CINTA", "GEMBIRA", "BANGGA",
|
||||||
|
"IKHLAS", "CERIA", "SETIA", "JUJUR", "RAMAH", "TABAH", "TULUS", "BERANI", "DAMAI"
|
||||||
|
];
|
||||||
|
|
||||||
|
const words = ref([]);
|
||||||
|
const letters = ref([]);
|
||||||
|
const foundWords = ref([]);
|
||||||
|
const selectedIndexes = ref([]);
|
||||||
|
const currentWord = ref("");
|
||||||
|
const isSelecting = ref(false);
|
||||||
|
const circleRef = ref(null);
|
||||||
|
const canvas = ref(null);
|
||||||
|
|
||||||
|
function shuffle(array) {
|
||||||
|
return array.sort(() => Math.random() - 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalSizeClass = computed(() => {
|
||||||
|
const total = letters.value.length;
|
||||||
|
if (total <= 7) return "w-16 h-16 text-3xl";
|
||||||
|
if (total <= 9) return "w-14 h-14 text-2xl";
|
||||||
|
return "w-12 h-12 text-xl";
|
||||||
|
});
|
||||||
|
|
||||||
|
function initGame() {
|
||||||
|
const chosen = shuffle([...allWords]).slice(0, 4);
|
||||||
|
words.value = chosen;
|
||||||
|
const maxCounts = {};
|
||||||
|
chosen.forEach(word => {
|
||||||
|
const temp = {};
|
||||||
|
for (const ch of word) temp[ch] = (temp[ch] || 0) + 1;
|
||||||
|
for (const [ch, cnt] of Object.entries(temp)) {
|
||||||
|
maxCounts[ch] = Math.max(maxCounts[ch] || 0, cnt);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let fullLetterSet = Object.entries(maxCounts).flatMap(([ch, cnt]) => Array(cnt).fill(ch));
|
||||||
|
const MIN_LETTERS = 7;
|
||||||
|
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("");
|
||||||
|
if (fullLetterSet.length < MIN_LETTERS) {
|
||||||
|
const fillers = shuffle(alphabet.filter(a => !fullLetterSet.includes(a)))
|
||||||
|
.slice(0, MIN_LETTERS - fullLetterSet.length);
|
||||||
|
fullLetterSet = fullLetterSet.concat(fillers);
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_LETTERS = 12;
|
||||||
|
if (fullLetterSet.length > MAX_LETTERS) {
|
||||||
|
fullLetterSet = fullLetterSet.slice(0, MAX_LETTERS);
|
||||||
|
}
|
||||||
|
|
||||||
|
letters.value = shuffle(fullLetterSet);
|
||||||
|
foundWords.value = [];
|
||||||
|
selectedIndexes.value = [];
|
||||||
|
currentWord.value = "";
|
||||||
|
drawConnections();
|
||||||
|
}
|
||||||
|
|
||||||
|
function letterPosition(index) {
|
||||||
|
const total = letters.value.length;
|
||||||
|
const containerSize = 300;
|
||||||
|
const center = containerSize / 2;
|
||||||
|
const radius = (containerSize / 2) - 40;
|
||||||
|
const angle = (index / total) * (2 * Math.PI) - Math.PI / 2;
|
||||||
|
const x = center + radius * Math.cos(angle);
|
||||||
|
const y = center + radius * Math.sin(angle);
|
||||||
|
const baseSize = total <= 8 ? 60 : total <= 10 ? 52 : 46;
|
||||||
|
|
||||||
|
return {
|
||||||
|
left: `${x}px`,
|
||||||
|
top: `${y}px`,
|
||||||
|
width: `${baseSize}px`,
|
||||||
|
height: `${baseSize}px`,
|
||||||
|
transform: "translate(-50%, -50%)"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function startSelect(index) {
|
||||||
|
isSelecting.value = true;
|
||||||
|
selectedIndexes.value = [index];
|
||||||
|
currentWord.value = letters.value[index];
|
||||||
|
drawConnections();
|
||||||
|
}
|
||||||
|
|
||||||
|
function dragSelect(index) {
|
||||||
|
if (!isSelecting.value) return;
|
||||||
|
if (!selectedIndexes.value.includes(index)) {
|
||||||
|
selectedIndexes.value.push(index);
|
||||||
|
currentWord.value += letters.value[index];
|
||||||
|
drawConnections();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function endSelect() {
|
||||||
|
if (!isSelecting.value) return;
|
||||||
|
const attempt = currentWord.value.toUpperCase();
|
||||||
|
|
||||||
|
if (words.value.includes(attempt)) {
|
||||||
|
if (!foundWords.value.includes(attempt)) {
|
||||||
|
foundWords.value.push(attempt);
|
||||||
|
alert(`🎉 Hebat! Kamu menemukan kata ${attempt}!`);
|
||||||
|
} else {
|
||||||
|
alert("⚡ Kata ini sudah kamu temukan!");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alert("😅 Belum benar, coba lagi!");
|
||||||
|
}
|
||||||
|
|
||||||
|
isSelecting.value = false;
|
||||||
|
selectedIndexes.value = [];
|
||||||
|
currentWord.value = "";
|
||||||
|
drawConnections();
|
||||||
|
|
||||||
|
if (foundWords.value.length === words.value.length) {
|
||||||
|
setTimeout(() => alert("🌟 Semua kata ditemukan! Kamu luar biasa!"), 300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawConnections() {
|
||||||
|
const ctx = canvas.value?.getContext("2d");
|
||||||
|
if (!ctx) return;
|
||||||
|
ctx.clearRect(0, 0, canvas.value.width, canvas.value.height);
|
||||||
|
|
||||||
|
if (selectedIndexes.value.length < 2) return;
|
||||||
|
|
||||||
|
const canvasBox = canvas.value.getBoundingClientRect();
|
||||||
|
const nodes = selectedIndexes.value.map((i) => {
|
||||||
|
const el = circleRef.value.children[i];
|
||||||
|
const box = el.getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
x: box.left + box.width / 2 - canvasBox.left,
|
||||||
|
y: box.top + box.height / 2 - canvasBox.top,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.strokeStyle = "rgba(255,255,255,0.9)";
|
||||||
|
ctx.lineWidth = 5;
|
||||||
|
ctx.lineJoin = "round";
|
||||||
|
ctx.lineCap = "round";
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(nodes[0].x, nodes[0].y);
|
||||||
|
for (let i = 1; i < nodes.length; i++) ctx.lineTo(nodes[i].x, nodes[i].y);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetGame() {
|
||||||
|
initGame();
|
||||||
|
}
|
||||||
|
|
||||||
|
function goEntertainment(){
|
||||||
|
window.history.back('/entertainment');
|
||||||
|
}
|
||||||
|
|
||||||
|
function goBack(){
|
||||||
|
window.history.back('/entertainment/mini-games');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await nextTick();
|
||||||
|
const rect = circleRef.value.getBoundingClientRect();
|
||||||
|
canvas.value.width = rect.width;
|
||||||
|
canvas.value.height = rect.height;
|
||||||
|
initGame();
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@keyframes pulseGlow {
|
||||||
|
0%, 100% { box-shadow: 0 0 10px rgba(255,255,255,0.6); }
|
||||||
|
50% { box-shadow: 0 0 20px rgba(255,255,255,0.9); }
|
||||||
|
}
|
||||||
|
.animate-pulse {
|
||||||
|
animation: pulseGlow 1s infinite;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -14,6 +14,8 @@ import MissionPage from '@/pages/MissionPage.vue'
|
|||||||
import FindwordsPage from '@/pages/Findwords.vue'
|
import FindwordsPage from '@/pages/Findwords.vue'
|
||||||
import MinigamePage from '@/pages/Minigame.vue'
|
import MinigamePage from '@/pages/Minigame.vue'
|
||||||
import FlipcardPage from '@/pages/Flipcard.vue'
|
import FlipcardPage from '@/pages/Flipcard.vue'
|
||||||
|
import WordspellsPage from '@/pages/Wordspells.vue'
|
||||||
|
import PointhistoryPage from '@/pages/Pointhistory.vue'
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{ path: '/', name: 'home', component: HomePage , meta:{requiresAuth:true}},
|
{ path: '/', name: 'home', component: HomePage , meta:{requiresAuth:true}},
|
||||||
@ -33,11 +35,13 @@ 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/', name:'manga-list', component:ChapterListPage, props:true},
|
||||||
{ path: '/entertainment/manga/:manga_id/chapters/:chapter_id/pages', name:'manga-read', 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},
|
||||||
|
{ path: '/profile/mission-history', name:'mission-history', component:PointhistoryPage},
|
||||||
|
|
||||||
// game
|
// game
|
||||||
{ path: '/entertainment/game/find-words/', name:'game-find-words', component:FindwordsPage, props:true},
|
{ 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/', name:'mini-games', component:MinigamePage, props:true},
|
||||||
{ path: '/entertainment/mini-games/flipcard/', name:'flipcard', component:FlipcardPage, 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},
|
||||||
]
|
]
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
|
|||||||
@ -1,7 +1,23 @@
|
|||||||
import api from "@/util/api";
|
import api from "@/util/api";
|
||||||
|
import { useAuthStore } from "../stores/auth";
|
||||||
|
|
||||||
export const getMissions = async () => {
|
export const getMissions = async () => {
|
||||||
return await api.get("/mission/missions").then((res) => res.data);
|
const res = await api.get("/mission/missions");
|
||||||
|
const data = res.data;
|
||||||
|
const missions = Array.isArray(data.results) ? data.results : [];
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
|
||||||
|
const activeMissions = missions.filter(
|
||||||
|
(mission) => mission.status === 'published'
|
||||||
|
&& new Date(mission.date_time_from_valid) <= now
|
||||||
|
&& new Date(mission.date_time_to_valid) >= now
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
count: activeMissions.length,
|
||||||
|
results: activeMissions,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getMissionLogs = async () => {
|
export const getMissionLogs = async () => {
|
||||||
@ -29,6 +45,22 @@ export const updateMissionLog = async(missionId ,data = {}) =>{
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// export const getUserPoint = async(user_id) =>{
|
export const getUserPoint = async() =>{
|
||||||
// return await api.get();
|
const auth = useAuthStore();
|
||||||
// }
|
const user = auth.user
|
||||||
|
console.log(`User Data: ${JSON.stringify(user)}`)
|
||||||
|
|
||||||
|
// getting points
|
||||||
|
const resPoint = await getMissionLogs();
|
||||||
|
console.log(`Missions Logs: ${JSON.stringify(resPoint)}`)
|
||||||
|
const userLogs = resPoint.results.filter((log) => log.user_id === user.id);
|
||||||
|
const myPoints = userLogs.reduce((total, log) => total + log.point, 0);
|
||||||
|
|
||||||
|
return {
|
||||||
|
task: resPoint,
|
||||||
|
taskCount: resPoint.count,
|
||||||
|
myTask: userLogs,
|
||||||
|
myTaskCount: userLogs.length,
|
||||||
|
myPoints,
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user