Draft Mission and game

This commit is contained in:
='fauz 2025-10-23 14:57:16 +07:00
parent 5617cddcfa
commit 6e4decfe5c
9 changed files with 763 additions and 147 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -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();
}); });

View File

@ -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=🍰";
}, },

View File

@ -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,6 +112,7 @@
</div> </div>
</div> </div>
</div> --> </div> -->
<div v-if="missions.length > 0">
<div <div
v-for="mission in missions" v-for="mission in missions"
:key="mission.id" :key="mission.id"
@ -184,8 +185,6 @@
SELESAI SELESAI
</span> </span>
</div> </div>
<!-- Content layer ABOVE stamp -->
<div class="flex-shrink-0 mt-1 relative z-10"> <div class="flex-shrink-0 mt-1 relative z-10">
<img src="/images/qicon2.png" alt="Quest Icon" class="w-12 h-12" /> <img src="/images/qicon2.png" alt="Quest Icon" class="w-12 h-12" />
</div> </div>
@ -255,6 +254,85 @@
</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 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(() => {

View File

@ -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
View 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
View 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>

View File

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

View File

@ -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,
};
}