Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eb12f138b0 | |||
| 6c2d32af30 |
1
.idea/.name
generated
Normal file
1
.idea/.name
generated
Normal file
@ -0,0 +1 @@
|
||||
PPB_Kelompok2
|
||||
8
.idea/deploymentTargetSelector.xml
generated
8
.idea/deploymentTargetSelector.xml
generated
@ -4,6 +4,14 @@
|
||||
<selectionStates>
|
||||
<SelectionState runConfigName="app">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<DropdownSelection timestamp="2026-01-12T10:12:43.024416Z">
|
||||
<Target type="DEFAULT_BOOT">
|
||||
<handle>
|
||||
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\Ahmar Rafly\.android\avd\Pixel_6_Pro.avd" />
|
||||
</handle>
|
||||
</Target>
|
||||
</DropdownSelection>
|
||||
<DialogSelection />
|
||||
</SelectionState>
|
||||
</selectionStates>
|
||||
</component>
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
@ -10,12 +12,14 @@
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.PPB_Kelompok2">
|
||||
android:theme="@style/Theme.PPB_Kelompok2"
|
||||
android:usesCleartextTraffic="true">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.PPB_Kelompok2">
|
||||
android:theme="@style/Theme.PPB_Kelompok2"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
|
||||
893
app/src/main/assets/index.html
Normal file
893
app/src/main/assets/index.html
Normal file
@ -0,0 +1,893 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<title>PsyJournal React</title>
|
||||
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
|
||||
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/recharts/shadcn-recharts.js"></script>
|
||||
<script src="https://unpkg.com/lucide@latest"></script>
|
||||
<style>
|
||||
body { margin: 0; padding: 0; overflow-x: hidden; }
|
||||
.pb-safe { padding-bottom: env(safe-area-inset-bottom, 20px); }
|
||||
.animate-fade-in { animation: fadeIn 0.3s ease-out; }
|
||||
.scrollbar-hide::-webkit-scrollbar { display: none; }
|
||||
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
|
||||
input[type=range]::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
border-radius: 50%;
|
||||
background: #ffffff;
|
||||
border: 2px solid currentColor;
|
||||
margin-top: -8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
<script type="text/babel">
|
||||
// Shim for Lucide Icons in browser
|
||||
const { useState, useEffect, useRef } = React;
|
||||
|
||||
// Mocking Lucide components to work in browser without bundler
|
||||
const Icon = ({ name, className }) => {
|
||||
useEffect(() => {
|
||||
if (window.lucide) {
|
||||
window.lucide.createIcons();
|
||||
}
|
||||
}, [name]);
|
||||
return <i data-lucide={name.toLowerCase().replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()} className={className}></i>;
|
||||
};
|
||||
|
||||
const BookOpen = (props) => <Icon name="BookOpen" {...props} />;
|
||||
const Activity = (props) => <Icon name="Activity" {...props} />;
|
||||
const BrainCircuit = (props) => <Icon name="BrainCircuit" {...props} />;
|
||||
const User = (props) => <Icon name="User" {...props} />;
|
||||
const Save = (props) => <Icon name="Save" {...props} />;
|
||||
const Smile = (props) => <Icon name="Smile" {...props} />;
|
||||
const Meh = (props) => <Icon name="Meh" {...props} />;
|
||||
const Frown = (props) => <Icon name="Frown" {...props} />;
|
||||
const TrendingUp = (props) => <Icon name="TrendingUp" {...props} />;
|
||||
const AlertTriangle = (props) => <Icon name="AlertTriangle" {...props} />;
|
||||
const Award = (props) => <Icon name="Award" {...props} />;
|
||||
const Clock = (props) => <Icon name="Clock" {...props} />;
|
||||
const Phone = (props) => <Icon name="Phone" {...props} />;
|
||||
const X = (props) => <Icon name="X" {...props} />;
|
||||
const Zap = (props) => <Icon name="Zap" {...props} />;
|
||||
const Search = (props) => <Icon name="Search" {...props} />;
|
||||
const CheckCircle = (props) => <Icon name="CheckCircle" {...props} />;
|
||||
const Trash2 = (props) => <Icon name="Trash2" {...props} />;
|
||||
const Edit2 = (props) => <Icon name="Edit2" {...props} />;
|
||||
const Plus = (props) => <Icon name="Plus" {...props} />;
|
||||
const Play = (props) => <Icon name="Play" {...props} />;
|
||||
const HelpCircle = (props) => <Icon name="HelpCircle" {...props} />;
|
||||
const ArrowRight = (props) => <Icon name="ArrowRight" {...props} />;
|
||||
const Moon = (props) => <Icon name="Moon" {...props} />;
|
||||
const Sun = (props) => <Icon name="Sun" {...props} />;
|
||||
const Calendar = (props) => <Icon name="Calendar" {...props} />;
|
||||
const LogOut = (props) => <Icon name="LogOut" {...props} />;
|
||||
const Globe = (props) => <Icon name="Globe" {...props} />;
|
||||
|
||||
// Mocking Recharts for Browser (Simple version if library not fully loaded)
|
||||
const { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, AreaChart, Area } = window.Recharts || {
|
||||
ResponsiveContainer: ({children}) => <div style={{width:'100%', height:'100%'}}>{children}</div>,
|
||||
AreaChart: () => <div className="bg-slate-100 w-full h-full flex items-center justify-center text-xs text-slate-400">Chart Placeholder</div>,
|
||||
Area: () => null, XAxis: () => null, YAxis: () => null, CartesianGrid: () => null, Tooltip: () => null
|
||||
};
|
||||
|
||||
const PsychologyJournalApp = () => {
|
||||
// --- KONFIGURASI WARNA & LEVEL ---
|
||||
const COLORS = {
|
||||
normal: '#38A169', // Hijau
|
||||
mild: '#D69E2E', // Kuning/Emas
|
||||
moderate: '#DD6B20',// Orange
|
||||
severe: '#E53E3E', // Merah
|
||||
primary: '#4F6DAD', // Default Biru
|
||||
bg: '#F5F5F7'
|
||||
};
|
||||
|
||||
// --- STATE MANAGEMENT ---
|
||||
const [activeTab, setActiveTab] = useState('journal');
|
||||
const [viewMode, setViewMode] = useState('main');
|
||||
const [modalBadge, setModalBadge] = useState(null);
|
||||
const [showHelpModal, setShowHelpModal] = useState(false);
|
||||
|
||||
// User Data
|
||||
const [userProfile, setUserProfile] = useState({
|
||||
name: 'Pengguna',
|
||||
streak: 0,
|
||||
lastActiveDate: null,
|
||||
xp: 0,
|
||||
badges: []
|
||||
});
|
||||
|
||||
// Data Jurnal & Penilaian (Mulai Kosong)
|
||||
const [journals, setJournals] = useState([]);
|
||||
const [assessments, setAssessments] = useState([]);
|
||||
|
||||
// Journal Form States
|
||||
const [journalMode, setJournalMode] = useState('free');
|
||||
const [journalText, setJournalText] = useState('');
|
||||
const [availableTags, setAvailableTags] = useState(['#sedih', '#bangga', '#lelah', '#bersyukur', '#cemas']);
|
||||
const [journalTags, setJournalTags] = useState([]);
|
||||
const [tagInput, setTagInput] = useState('');
|
||||
const [reflectionAnswers, setReflectionAnswers] = useState({ q1: '', q2: '', q3: '' });
|
||||
const [autoSaveTime, setAutoSaveTime] = useState(null);
|
||||
const [showAssessmentAlert, setShowAssessmentAlert] = useState(false);
|
||||
|
||||
// Search
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
// Assessment Inputs
|
||||
const [assessmentInputs, setAssessmentInputs] = useState({
|
||||
mood: 0, stress: 0, energy: 0, focus: 0
|
||||
});
|
||||
|
||||
const [reportRange, setReportRange] = useState('mingguan');
|
||||
|
||||
// --- GAME STATES ---
|
||||
const [activeGame, setActiveGame] = useState(null);
|
||||
// Reaction
|
||||
const [reactionState, setReactionState] = useState('intro');
|
||||
const [reactionTime, setReactionTime] = useState(0);
|
||||
const reactionTimerRef = useRef(null);
|
||||
const reactionStartRef = useRef(0);
|
||||
// Simon
|
||||
const [simonSequence, setSimonSequence] = useState([]);
|
||||
const [simonUserStep, setSimonUserStep] = useState(0);
|
||||
const [simonPlaying, setSimonPlaying] = useState(false);
|
||||
const [simonFlash, setSimonFlash] = useState(null);
|
||||
const [simonScore, setSimonScore] = useState(0);
|
||||
const [simonGameOver, setSimonGameOver] = useState(false);
|
||||
// Logic
|
||||
const [logicSetup, setLogicSetup] = useState({ difficulty: 'mudah', count: 5 });
|
||||
const [logicState, setLogicState] = useState('setup');
|
||||
const [logicQuestions, setLogicQuestions] = useState([]);
|
||||
const [logicCurrentIdx, setLogicCurrentIdx] = useState(0);
|
||||
const [logicScore, setLogicScore] = useState(0);
|
||||
|
||||
// --- DATABASE LENCANA ---
|
||||
const BADGES_DB = [
|
||||
{ id: 'first_step', name: 'Langkah Awal', icon: '🚶', desc: 'Menulis jurnal pertama kali.', date: '-' },
|
||||
{ id: 'writer_routine', name: 'Penulis Rutin', icon: '✍️', desc: 'Menulis jurnal 7 hari berturut-turut.', date: '-' },
|
||||
{ id: 'self_discipline', name: 'Disiplin Diri', icon: '🔥', desc: 'Mencapai streak aktivitas 7 hari.', date: '-' },
|
||||
{ id: 'simon_master', name: 'Master Memori', icon: '🧠', desc: 'Mencapai level 8 di Tes Memori.', date: '-' },
|
||||
{ id: 'reaction_flash', name: 'Si Kilat', icon: '⚡', desc: 'Reaksi di bawah 250ms.', date: '-' },
|
||||
{ id: 'logic_einstein', name: 'Logika Einstein', icon: '🎓', desc: 'Skor 100% pada Logika Susah.', date: '-' },
|
||||
{ id: 'night_owl', name: 'Burung Hantu', icon: '🦉', desc: 'Menulis jurnal di atas jam 10 malam.', date: '-' },
|
||||
{ id: 'early_bird', name: 'Si Pagi', icon: '🌅', desc: 'Melakukan penilaian sebelum jam 7 pagi.', date: '-' },
|
||||
{ id: 'zen_mood', name: 'Emosi Zen', icon: '🧘♀️', desc: 'Skor kestabilan emosi 0 (Normal) selama 3 hari.', date: '-' },
|
||||
{ id: 'story_teller', name: 'Penyair Hati', icon: '📜', desc: 'Menulis jurnal lebih dari 500 karakter.', date: '-' },
|
||||
];
|
||||
|
||||
// --- LOGIC FUNCTIONS ---
|
||||
|
||||
useEffect(() => {
|
||||
if (!journalText && !reflectionAnswers.q1) return;
|
||||
const timer = setTimeout(() => {
|
||||
setAutoSaveTime(new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }));
|
||||
}, 30000);
|
||||
return () => clearTimeout(timer);
|
||||
}, [journalText, reflectionAnswers]);
|
||||
|
||||
const getTodayStr = () => {
|
||||
const d = new Date();
|
||||
return d.toLocaleDateString('id-ID', { day: 'numeric', month: 'short', year: 'numeric' });
|
||||
};
|
||||
|
||||
const hasAssessedToday = () => assessments.some(a => a.date === getTodayStr());
|
||||
|
||||
const updateStreak = () => {
|
||||
const today = getTodayStr();
|
||||
if (userProfile.lastActiveDate === today) return;
|
||||
|
||||
setUserProfile(prev => ({
|
||||
...prev,
|
||||
streak: prev.streak + 1,
|
||||
lastActiveDate: today,
|
||||
xp: prev.xp + 20
|
||||
}));
|
||||
|
||||
if (userProfile.streak === 0) unlockBadge('first_step');
|
||||
};
|
||||
|
||||
const checkTimeBadges = () => {
|
||||
const hour = new Date().getHours();
|
||||
if (hour >= 22 || hour < 4) unlockBadge('night_owl');
|
||||
if (hour >= 4 && hour < 7) unlockBadge('early_bird');
|
||||
};
|
||||
|
||||
const handleAddTag = () => {
|
||||
if (!tagInput.trim()) return;
|
||||
const newTag = tagInput.startsWith('#') ? tagInput.trim() : `#${tagInput.trim()}`;
|
||||
if (!availableTags.includes(newTag)) setAvailableTags([...availableTags, newTag]);
|
||||
if (!journalTags.includes(newTag)) setJournalTags([...journalTags, newTag]);
|
||||
setTagInput('');
|
||||
};
|
||||
|
||||
const handleTagClick = (tag) => {
|
||||
setSearchQuery(tag);
|
||||
setViewMode('history_full');
|
||||
};
|
||||
|
||||
const handleSaveJournal = () => {
|
||||
if (!journalText.trim() && journalMode === 'free') {
|
||||
alert("⚠️ Jurnal Bebas masih kosong.");
|
||||
return;
|
||||
}
|
||||
if ((!reflectionAnswers.q1.trim() || !reflectionAnswers.q2.trim() || !reflectionAnswers.q3.trim()) && journalMode === 'reflection') {
|
||||
alert("⚠️ Refleksi Harian belum lengkap.");
|
||||
return;
|
||||
}
|
||||
|
||||
const newEntry = {
|
||||
id: Date.now(),
|
||||
date: getTodayStr(),
|
||||
type: 'complete',
|
||||
content: {
|
||||
text: journalText,
|
||||
reflection: { ...reflectionAnswers }
|
||||
},
|
||||
tags: [...journalTags]
|
||||
};
|
||||
|
||||
if (journalText.length > 500) unlockBadge('story_teller');
|
||||
checkTimeBadges();
|
||||
|
||||
setJournals(prev => [newEntry, ...prev]);
|
||||
|
||||
setJournalText('');
|
||||
setReflectionAnswers({ q1: '', q2: '', q3: '' });
|
||||
setJournalTags([]);
|
||||
setAutoSaveTime(null);
|
||||
|
||||
updateStreak();
|
||||
|
||||
if (!hasAssessedToday()) setShowAssessmentAlert(true);
|
||||
else alert("✅ Jurnal Berhasil Disimpan!");
|
||||
};
|
||||
|
||||
const handleSaveAssessment = () => {
|
||||
const total = Object.values(assessmentInputs).reduce((a, b) => a + b, 0);
|
||||
setAssessments(prev => [{ id: Date.now(), date: getTodayStr(), scores: assessmentInputs, total }, ...prev]);
|
||||
|
||||
if (total >= 10) alert("⚠️ Skor Berat. Disarankan konsultasi.");
|
||||
checkTimeBadges();
|
||||
if (assessmentInputs.mood === 0) unlockBadge('zen_mood');
|
||||
updateStreak();
|
||||
setAssessmentInputs({ mood: 0, stress: 0, energy: 0, focus: 0 });
|
||||
setActiveTab('profile');
|
||||
};
|
||||
|
||||
const handleDeleteAccount = () => {
|
||||
if (window.confirm("Yakin hapus akun permanen?")) {
|
||||
setJournals([]);
|
||||
setAssessments([]);
|
||||
setUserProfile({ name: 'Pengguna Baru', streak: 0, lastActiveDate: null, xp: 0, badges: [] });
|
||||
setActiveTab('journal');
|
||||
}
|
||||
};
|
||||
|
||||
const unlockBadge = (badgeId) => {
|
||||
if (!userProfile.badges.includes(badgeId)) {
|
||||
setUserProfile(prev => ({ ...prev, badges: [...prev.badges, badgeId] }));
|
||||
setModalBadge(BADGES_DB.find(b => b.id === badgeId));
|
||||
}
|
||||
};
|
||||
|
||||
const getSeverityInfo = (score) => {
|
||||
if (score >= 10) return { color: COLORS.severe, bg: 'bg-red-50', text: 'text-red-700', fill: 'bg-red-500', label: 'Berat' };
|
||||
if (score >= 7) return { color: COLORS.moderate, bg: 'bg-orange-50', text: 'text-orange-700', fill: 'bg-orange-500', label: 'Sedang' };
|
||||
if (score >= 4) return { color: COLORS.mild, bg: 'bg-yellow-50', text: 'text-yellow-700', fill: 'bg-yellow-500', label: 'Ringan' };
|
||||
return { color: COLORS.normal, bg: 'bg-green-50', text: 'text-green-700', fill: 'bg-green-500', label: 'Normal' };
|
||||
};
|
||||
|
||||
const getChartData = () => {
|
||||
if (assessments.length === 0) {
|
||||
if (reportRange === 'mingguan') {
|
||||
return ['Sn', 'Sl', 'Rb', 'Km', 'Jm', 'Sb', 'Mg'].map(day => ({ name: day, score: 0 }));
|
||||
} else if (reportRange === 'bulanan') {
|
||||
return ['M1', 'M2', 'M3', 'M4'].map(week => ({ name: week, score: 0 }));
|
||||
} else {
|
||||
return ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'].map(month => ({ name: month, score: 0 }));
|
||||
}
|
||||
}
|
||||
|
||||
const latestScore = assessments[0].total;
|
||||
const jitter = (val) => Math.max(0, Math.min(12, val + (Math.random() * 2 - 1)));
|
||||
|
||||
if (reportRange === 'mingguan') {
|
||||
return ['Sn', 'Sl', 'Rb', 'Km', 'Jm', 'Sb', 'Mg'].map(day => ({ name: day, score: jitter(latestScore) }));
|
||||
} else if (reportRange === 'bulanan') {
|
||||
return ['M1', 'M2', 'M3', 'M4'].map(week => ({ name: week, score: jitter(latestScore) }));
|
||||
} else {
|
||||
return ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'].map(month => ({ name: month, score: jitter(latestScore) }));
|
||||
}
|
||||
};
|
||||
|
||||
// --- GAME LOGIC ---
|
||||
const startReactionGame = () => {
|
||||
setReactionState('waiting');
|
||||
setReactionTime(0);
|
||||
const delay = 2000 + Math.random() * 4000;
|
||||
reactionTimerRef.current = setTimeout(() => { setReactionState('ready'); reactionStartRef.current = Date.now(); }, delay);
|
||||
};
|
||||
const handleReactionClick = () => {
|
||||
if (reactionState === 'waiting') { clearTimeout(reactionTimerRef.current); setReactionState('too-soon'); }
|
||||
else if (reactionState === 'ready') {
|
||||
const time = Date.now() - reactionStartRef.current;
|
||||
setReactionTime(time); setReactionState('finished');
|
||||
if (time < 250) unlockBadge('reaction_flash');
|
||||
}
|
||||
};
|
||||
const playSimonSequence = async (sequence) => {
|
||||
setSimonPlaying(false);
|
||||
for (let i = 0; i < sequence.length; i++) {
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
setSimonFlash(sequence[i]);
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
setSimonFlash(null);
|
||||
}
|
||||
setSimonPlaying(true);
|
||||
};
|
||||
const startSimonGame = () => {
|
||||
const firstIdx = Math.floor(Math.random() * 9);
|
||||
const newSeq = [firstIdx];
|
||||
setSimonSequence(newSeq); setSimonScore(0); setSimonUserStep(0); setSimonGameOver(false);
|
||||
playSimonSequence(newSeq);
|
||||
};
|
||||
const handleSimonClick = (index) => {
|
||||
if (!simonPlaying || simonGameOver) return;
|
||||
setSimonFlash(index); setTimeout(() => setSimonFlash(null), 200);
|
||||
if (index === simonSequence[simonUserStep]) {
|
||||
if (simonUserStep === simonSequence.length - 1) {
|
||||
const nextScore = simonScore + 1; setSimonScore(nextScore); setSimonUserStep(0);
|
||||
const newSeq = [...simonSequence, Math.floor(Math.random() * 9)]; setSimonSequence(newSeq);
|
||||
if (nextScore === 8) unlockBadge('simon_master');
|
||||
setTimeout(() => playSimonSequence(newSeq), 1000);
|
||||
} else { setSimonUserStep(simonUserStep + 1); }
|
||||
} else { setSimonGameOver(true); setSimonPlaying(false); }
|
||||
};
|
||||
const LOGIC_DB = {
|
||||
mudah: [{ q: "2, 4, 6, ...?", options: ["7","8","9"], ans: 1 }, { q: "Lawan kata Panas?", options: ["Dingin","Hangat","Api"], ans: 0 }, { q: "Warna langit cerah?", options: ["Merah","Biru","Hijau"], ans: 1 }, { q: "1 + 1 x 0 = ?", options: ["0","1","2"], ans: 1 }, { q: "Bentuk Roda?", options: ["Kotak","Bulat","Segitiga"], ans: 1 }],
|
||||
sedang: [{ q: "1, 1, 2, 3, ...?", options: ["4","5","6"], ans: 1 }],
|
||||
susah: [{ q: "A>B, B>C, A...C?", options: [">","<","="], ans: 0 }]
|
||||
};
|
||||
const startLogicGame = () => {
|
||||
const pool = LOGIC_DB[logicSetup.difficulty];
|
||||
setLogicQuestions(pool); setLogicCurrentIdx(0); setLogicScore(0); setLogicState('playing');
|
||||
};
|
||||
const handleLogicAnswer = (idx) => {
|
||||
if (idx === logicQuestions[logicCurrentIdx].ans) setLogicScore(logicScore + 1);
|
||||
if (logicCurrentIdx < logicQuestions.length - 1) {
|
||||
setLogicCurrentIdx(logicCurrentIdx + 1);
|
||||
} else {
|
||||
setLogicState('finished');
|
||||
if (logicSetup.difficulty === 'susah' && logicScore === logicQuestions.length - 1) unlockBadge('logic_einstein');
|
||||
}
|
||||
};
|
||||
|
||||
// --- RENDERERS ---
|
||||
|
||||
const renderJournal = () => (
|
||||
<div className="flex flex-col h-[calc(100vh-170px)] animate-fade-in">
|
||||
<div className="bg-white p-1 rounded-xl flex shadow-sm border border-slate-100 mb-4 shrink-0">
|
||||
<button onClick={() => setJournalMode('free')} className={`flex-1 py-3 text-sm font-bold rounded-lg transition-all ${journalMode === 'free' ? 'bg-[#4F6DAD] text-white shadow-md' : 'text-slate-400 hover:bg-slate-50'}`}>Jurnal Bebas</button>
|
||||
<button onClick={() => setJournalMode('reflection')} className={`flex-1 py-3 text-sm font-bold rounded-lg transition-all ${journalMode === 'reflection' ? 'bg-[#4F6DAD] text-white shadow-md' : 'text-slate-400 hover:bg-slate-50'}`}>Refleksi Harian</button>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-6 rounded-3xl shadow-sm border border-slate-100 relative flex-1 flex flex-col">
|
||||
{journalMode === 'free' ? (
|
||||
<>
|
||||
<textarea
|
||||
value={journalText}
|
||||
onChange={(e) => setJournalText(e.target.value)}
|
||||
placeholder="Tuliskan perasaanmu..."
|
||||
className="w-full flex-1 bg-[#F5F5F7] p-4 rounded-2xl border border-slate-200 outline-none resize-none text-slate-700 focus:ring-2 focus:ring-[#4F6DAD] focus:border-transparent transition-all mb-4"
|
||||
></textarea>
|
||||
|
||||
<div className="shrink-0">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<input type="text" value={tagInput} onChange={(e) => setTagInput(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && handleAddTag()} placeholder="+ Tag..." className="text-xs p-2 rounded-lg bg-[#F5F5F7] border border-slate-200 outline-none w-36" />
|
||||
<button onClick={handleAddTag} disabled={!tagInput.trim()} className="p-2 bg-slate-100 rounded-lg text-slate-500 hover:bg-[#4F6DAD] hover:text-white disabled:opacity-50 transition-colors"><Plus className="w-3 h-3" /></button>
|
||||
</div>
|
||||
<div className="flex gap-2 mb-4 overflow-x-auto pb-2 scrollbar-hide">
|
||||
{availableTags.map(tag => (
|
||||
<button key={tag} onClick={() => setJournalTags(prev => prev.includes(tag) ? prev.filter(t=>t!==tag) : [...prev, tag])} className={`px-3 py-1 rounded-full text-xs font-bold transition-colors whitespace-nowrap ${journalTags.includes(tag) ? 'bg-[#4F6DAD] text-white' : 'bg-slate-100 text-slate-400'}`}>{tag}</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex-1 overflow-y-auto px-1 space-y-5">
|
||||
{[
|
||||
{
|
||||
q: 'Satu hal kecil yang membuatmu tersenyum hari ini?',
|
||||
key: 'q1',
|
||||
icon: Smile,
|
||||
color: 'text-yellow-600 bg-yellow-100',
|
||||
ph: 'Misal: Kopi pagi yang enak, sapaan teman...'
|
||||
},
|
||||
{
|
||||
q: 'Tantangan terbesar hari ini & solusinya?',
|
||||
key: 'q2',
|
||||
icon: Activity,
|
||||
color: 'text-red-600 bg-red-100',
|
||||
ph: 'Misal: Macet total, solusinya dengar podcast...'
|
||||
},
|
||||
{
|
||||
q: 'Satu hal yang ingin kamu perbaiki besok?',
|
||||
key: 'q3',
|
||||
icon: TrendingUp,
|
||||
color: 'text-blue-600 bg-blue-100',
|
||||
ph: 'Misal: Tidur lebih awal, kurangi sosmed...'
|
||||
}
|
||||
].map((item, idx) => (
|
||||
<div key={idx} className="group relative">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className={`p-1.5 rounded-lg ${item.color}`}>
|
||||
<item.icon className="w-4 h-4"/>
|
||||
</div>
|
||||
<label className="text-xs font-bold text-slate-700 uppercase tracking-wide">{item.q}</label>
|
||||
</div>
|
||||
<textarea
|
||||
value={reflectionAnswers[item.key]}
|
||||
onChange={(e) => setReflectionAnswers({...reflectionAnswers, [item.key]: e.target.value})}
|
||||
className="w-full p-4 bg-[#F5F5F7] rounded-2xl border border-slate-200 outline-none focus:ring-2 focus:ring-[#4F6DAD] focus:bg-white focus:border-transparent transition-all resize-none text-slate-700 text-sm placeholder:text-slate-400"
|
||||
placeholder={item.ph}
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="shrink-0 mt-3 flex flex-col gap-3 pt-3 border-t border-slate-50">
|
||||
{autoSaveTime && <div className="flex items-center justify-end gap-1.5 text-[10px] text-slate-400 font-medium animate-fade-in"><CheckCircle className="w-3 h-3 text-green-500" /> Disimpan otomatis {autoSaveTime}</div>}
|
||||
<button onClick={handleSaveJournal} className="w-full bg-[#4F6DAD] text-white py-4 rounded-2xl font-bold text-lg hover:shadow-lg hover:bg-[#3E5C9A] active:scale-[0.98] transition-all flex items-center justify-center gap-2 shadow-md"><Save className="w-5 h-5"/> Simpan Jurnal</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showAssessmentAlert && (
|
||||
<div className="fixed inset-0 bg-black/20 backdrop-blur-sm flex items-center justify-center z-50 p-6 animate-fade-in">
|
||||
<div className="bg-white p-6 rounded-3xl shadow-2xl max-w-sm w-full text-center">
|
||||
<div className="w-16 h-16 bg-yellow-100 rounded-full flex items-center justify-center mx-auto mb-4"><Activity className="w-8 h-8 text-yellow-600"/></div>
|
||||
<h3 className="text-xl font-bold text-slate-800 mb-2">Lupa Sesuatu?</h3>
|
||||
<p className="text-slate-500 mb-6">Anda belum menilai mood hari ini.</p>
|
||||
<div className="flex gap-3">
|
||||
<button onClick={() => setShowAssessmentAlert(false)} className="flex-1 py-3 text-slate-400 font-bold hover:bg-slate-50 rounded-xl">Nanti Saja</button>
|
||||
<button onClick={() => {setShowAssessmentAlert(false); setActiveTab('assessment')}} className="flex-1 py-3 bg-[#4F6DAD] text-white font-bold rounded-xl shadow-lg">Ya, Lanjut</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderAssessment = () => {
|
||||
const total = Object.values(assessmentInputs).reduce((a, b) => a + b, 0);
|
||||
const risk = getSeverityInfo(total);
|
||||
const indicators = [{ id: 'mood', label: 'Emosi' }, { id: 'stress', label: 'Stress' }, { id: 'energy', label: 'Energi' }, { id: 'focus', label: 'Fokus' }];
|
||||
|
||||
return (
|
||||
<div className="space-y-6 animate-fade-in pb-20">
|
||||
<div className="bg-white p-6 rounded-3xl shadow-sm border border-slate-100 sticky top-20 z-10">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<h2 className="text-lg font-bold text-slate-700">Skor Mental</h2>
|
||||
<span className={`px-3 py-1 rounded-lg text-xs font-bold ${risk.bg} ${risk.text}`}>{total} - {risk.label}</span>
|
||||
</div>
|
||||
<div className="w-full bg-slate-100 h-2 rounded-full mt-3 overflow-hidden">
|
||||
<div className={`h-full transition-all duration-500 ${total >= 10 ? 'bg-red-500' : total >= 7 ? 'bg-orange-500' : total >= 4 ? 'bg-yellow-500' : 'bg-[#38A169]'}`} style={{width: `${(total/12)*100}%`}}></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white p-6 rounded-3xl shadow-sm border border-slate-100 space-y-8">
|
||||
{indicators.map((item) => {
|
||||
const val = assessmentInputs[item.id];
|
||||
const getSeverity = (v) => {
|
||||
if(v===0) return {label:'Normal', hex: COLORS.normal};
|
||||
if(v===1) return {label:'Ringan', hex: COLORS.mild};
|
||||
if(v===2) return {label:'Sedang', hex: COLORS.moderate};
|
||||
return {label:'Berat', hex: COLORS.severe};
|
||||
};
|
||||
const sev = getSeverity(val);
|
||||
const percent = (val / 3) * 100;
|
||||
|
||||
return (
|
||||
<div key={item.id} className="mb-6">
|
||||
<div className="flex justify-between mb-2">
|
||||
<label className="font-bold text-slate-700">{item.label}</label>
|
||||
<span className="text-sm font-bold" style={{color: sev.hex}}>{sev.label}</span>
|
||||
</div>
|
||||
|
||||
<div className="relative h-10 flex items-center">
|
||||
<div className="absolute w-full h-3 bg-slate-200 rounded-full"></div>
|
||||
|
||||
<div
|
||||
className="absolute h-3 rounded-full transition-all duration-300"
|
||||
style={{
|
||||
width: `${percent}%`,
|
||||
background: `linear-gradient(to right, ${sev.hex}, ${sev.hex})`
|
||||
}}
|
||||
></div>
|
||||
|
||||
<div
|
||||
className="absolute h-8 w-8 rounded-full shadow-md border-4 border-white flex items-center justify-center text-xs font-bold text-white transition-all duration-300 z-10"
|
||||
style={{
|
||||
left: `${percent}%`,
|
||||
transform: `translateX(-50%)`,
|
||||
backgroundColor: sev.hex
|
||||
}}
|
||||
>
|
||||
{val}
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="3"
|
||||
step="1"
|
||||
value={val}
|
||||
onChange={(e) => setAssessmentInputs(prev => ({...prev, [item.id]: parseInt(e.target.value)}))}
|
||||
className="absolute w-full h-full opacity-0 cursor-pointer z-20"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between text-[10px] text-slate-400 -mt-1 font-medium px-1">
|
||||
<span>Normal</span><span>Berat</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<button onClick={handleSaveAssessment} className="w-full py-4 bg-[#4F6DAD] text-white rounded-xl font-bold text-lg hover:bg-[#3E5C9A] transition-all shadow-lg">Simpan Penilaian</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderCognitive = () => (
|
||||
<div className="space-y-6 animate-fade-in pb-20">
|
||||
{!activeGame && (
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
<button onClick={() => { setActiveGame('simon'); startSimonGame(); }} className="bg-white p-5 rounded-2xl border border-slate-100 shadow-sm hover:shadow-md transition-all text-left flex gap-4 items-center">
|
||||
<div className="p-3 bg-purple-100 text-purple-600 rounded-xl"><BrainCircuit className="w-6 h-6"/></div>
|
||||
<div><h3 className="font-bold text-slate-800">Tes Memori</h3><p className="text-xs text-slate-400">Ingat urutan pola grid 3x3</p></div>
|
||||
</button>
|
||||
<button onClick={() => { setActiveGame('reaction'); setReactionState('intro'); }} className="bg-white p-5 rounded-2xl border border-slate-100 shadow-sm hover:shadow-md transition-all text-left flex gap-4 items-center">
|
||||
<div className="p-3 bg-yellow-100 text-yellow-600 rounded-xl"><Zap className="w-6 h-6"/></div>
|
||||
<div><h3 className="font-bold text-slate-800">Kecepatan Reaksi</h3><p className="text-xs text-slate-400">Tes refleks visual</p></div>
|
||||
</button>
|
||||
<button onClick={() => { setActiveGame('logic'); setLogicState('setup'); }} className="bg-white p-5 rounded-2xl border border-slate-100 shadow-sm hover:shadow-md transition-all text-left flex gap-4 items-center">
|
||||
<div className="p-3 bg-blue-100 text-blue-600 rounded-xl"><Search className="w-6 h-6"/></div>
|
||||
<div><h3 className="font-bold text-slate-800">Tes Logika</h3><p className="text-xs text-slate-400">Asah pola pikir</p></div>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* GAME: REACTION */}
|
||||
{activeGame === 'reaction' && (
|
||||
<div className="bg-white p-6 rounded-3xl shadow-lg text-center h-[400px] flex flex-col justify-center relative">
|
||||
<button onClick={() => setActiveGame(null)} className="absolute top-4 right-4 p-2 bg-slate-100 rounded-full"><X className="w-4 h-4"/></button>
|
||||
<h3 className="font-bold text-xl mb-4 text-[#4F6DAD]">Tes Reaksi</h3>
|
||||
<div onClick={handleReactionClick} className={`flex-1 rounded-2xl flex flex-col items-center justify-center cursor-pointer transition-all select-none ${reactionState === 'intro' ? 'bg-slate-100' : reactionState === 'waiting' ? 'bg-red-500' : reactionState === 'ready' ? 'bg-green-500' : reactionState === 'too-soon' ? 'bg-orange-500' : 'bg-[#4F6DAD]'}`}>
|
||||
{reactionState === 'intro' && <button onClick={(e) => { e.stopPropagation(); startReactionGame(); }} className="bg-[#4F6DAD] text-white px-8 py-3 rounded-full font-bold shadow-lg">Mulai Tes</button>}
|
||||
{reactionState === 'waiting' && <p className="text-white font-bold text-2xl animate-pulse">Tunggu Hijau...</p>}
|
||||
{reactionState === 'ready' && <p className="text-white font-bold text-3xl">TEKAN!</p>}
|
||||
{reactionState === 'too-soon' && <div onClick={(e) => e.stopPropagation()}><p className="text-white font-bold mb-4">Terlalu Cepat!</p><button onClick={startReactionGame} className="bg-white/30 text-white px-4 py-2 rounded-xl">Ulangi</button></div>}
|
||||
{reactionState === 'finished' && <div onClick={(e) => e.stopPropagation()}><p className="text-white font-bold text-4xl mb-2">{reactionTime} ms</p><button onClick={startReactionGame} className="bg-white/20 text-white px-6 py-2 rounded-xl mt-4">Coba Lagi</button></div>}
|
||||
</div>
|
||||
<p className="text-xs text-slate-400 mt-4">Jeda waktu berubah setiap ronde.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* GAME: MEMORY TEST (SIMON 3x3) */}
|
||||
{activeGame === 'simon' && (
|
||||
<div className="bg-white p-6 rounded-3xl shadow-lg text-center relative">
|
||||
<button onClick={() => setActiveGame(null)} className="absolute top-4 right-4 p-2 bg-slate-100 rounded-full"><X className="w-4 h-4"/></button>
|
||||
<div className="mb-6">
|
||||
<h3 className="font-bold text-xl text-[#4F6DAD]">Tes Memori</h3>
|
||||
<p className="text-sm text-slate-500">Skor: {simonScore}</p>
|
||||
</div>
|
||||
|
||||
{simonGameOver ? (
|
||||
<div className="py-10">
|
||||
<p className="text-red-500 font-bold text-xl mb-4">Game Over!</p>
|
||||
<p className="text-slate-600 mb-6">Skor Akhir: {simonScore}</p>
|
||||
<button onClick={startSimonGame} className="bg-[#4F6DAD] text-white px-8 py-3 rounded-xl font-bold">Main Lagi</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-3 gap-3 max-w-[280px] mx-auto">
|
||||
{[...Array(9)].map((_, i) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => handleSimonClick(i)}
|
||||
className={`h-20 rounded-xl transition-all duration-150 shadow-sm border-b-4 active:border-b-0 active:translate-y-1 ${
|
||||
simonFlash === i
|
||||
? 'bg-[#4F6DAD] border-[#3b5488] brightness-110 scale-105' // Active Blue
|
||||
: 'bg-slate-200 border-slate-300 hover:bg-slate-300' // Default Gray
|
||||
}`}
|
||||
></button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{!simonPlaying && !simonGameOver && <p className="mt-6 text-sm font-bold text-slate-400 animate-pulse">Perhatikan Urutan...</p>}
|
||||
{simonPlaying && !simonGameOver && <p className="mt-6 text-sm font-bold text-[#4F6DAD]">Giliranmu!</p>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* GAME: LOGIC */}
|
||||
{activeGame === 'logic' && (
|
||||
<div className="bg-white p-6 rounded-3xl shadow-lg relative min-h-[400px] flex flex-col">
|
||||
<button onClick={() => setActiveGame(null)} className="absolute top-4 right-4 p-2 bg-slate-100 rounded-full"><X className="w-4 h-4"/></button>
|
||||
|
||||
{logicState === 'setup' && (
|
||||
<div className="flex flex-col justify-center flex-1">
|
||||
<h3 className="font-bold text-xl text-[#4F6DAD] mb-6 text-center">Pengaturan Tes Logika</h3>
|
||||
|
||||
<div className="mb-6">
|
||||
<label className="text-xs font-bold text-slate-400 uppercase mb-2 block">Tingkat Kesulitan</label>
|
||||
<div className="flex bg-slate-100 p-1 rounded-xl">
|
||||
{['mudah', 'sedang', 'susah'].map(lvl => (
|
||||
<button key={lvl} onClick={() => setLogicSetup({...logicSetup, difficulty: lvl})} className={`flex-1 py-2 rounded-lg text-xs font-bold capitalize transition-all ${logicSetup.difficulty === lvl ? 'bg-white shadow text-[#4F6DAD]' : 'text-slate-400'}`}>{lvl}</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-8">
|
||||
<label className="text-xs font-bold text-slate-400 uppercase mb-2 block">Jumlah Soal</label>
|
||||
<div className="flex gap-4">
|
||||
{[5, 10].map(count => (
|
||||
<button key={count} onClick={() => setLogicSetup({...logicSetup, count})} className={`flex-1 py-3 border-2 rounded-xl font-bold transition-all ${logicSetup.count === count ? 'border-[#4F6DAD] text-[#4F6DAD] bg-indigo-50' : 'border-slate-200 text-slate-400'}`}>{count} Soal</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button onClick={startLogicGame} className="w-full bg-[#4F6DAD] text-white py-4 rounded-xl font-bold text-lg shadow-lg">Mulai Tes</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{logicState === 'playing' && (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<span className="text-xs font-bold text-slate-400">Soal {logicCurrentIdx + 1}/{logicQuestions.length}</span>
|
||||
<span className="text-xs font-bold bg-indigo-50 text-[#4F6DAD] px-2 py-1 rounded capitalize">{logicSetup.difficulty}</span>
|
||||
</div>
|
||||
|
||||
<h4 className="font-bold text-lg text-slate-800 mb-8 leading-relaxed">{logicQuestions[logicCurrentIdx]?.q}</h4>
|
||||
|
||||
<div className="space-y-3 flex-1">
|
||||
{logicQuestions[logicCurrentIdx]?.options.map((opt, idx) => (
|
||||
<button key={idx} onClick={() => handleLogicAnswer(idx)} className="w-full text-left p-4 rounded-xl border border-slate-200 hover:bg-indigo-50 hover:border-[#4F6DAD] transition-all font-medium text-slate-600 active:scale-[0.98]">
|
||||
{opt}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{logicState === 'finished' && (
|
||||
<div className="text-center flex flex-col justify-center flex-1">
|
||||
<div className="w-20 h-20 bg-indigo-100 rounded-full flex items-center justify-center mx-auto mb-4 text-[#4F6DAD] text-4xl font-bold">{Math.round((logicScore/logicQuestions.length)*100)}%</div>
|
||||
<h3 className="font-bold text-xl text-slate-800 mb-2">Tes Selesai!</h3>
|
||||
<p className="text-slate-500 mb-8">Anda menjawab {logicScore} benar dari {logicQuestions.length} soal.</p>
|
||||
<button onClick={() => setLogicState('setup')} className="bg-[#4F6DAD] text-white px-8 py-3 rounded-xl font-bold">Ulangi Tes</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderProfile = () => {
|
||||
const chartData = getChartData();
|
||||
const avgScore = chartData.length > 0 && assessments.length > 0
|
||||
? chartData.reduce((a, b) => a + (b.score || 0), 0) / chartData.length
|
||||
: 0;
|
||||
const severityInfo = getSeverityInfo(avgScore);
|
||||
|
||||
return (
|
||||
<div className="space-y-6 animate-fade-in pb-20">
|
||||
<div className="bg-white p-6 rounded-3xl shadow-sm border border-slate-100 flex items-center justify-between">
|
||||
<div className="flex items-center gap-4"><div className="w-14 h-14 bg-[#4F6DAD] rounded-full flex items-center justify-center text-white text-xl font-bold">U</div><div><h2 className="font-bold text-lg text-slate-800">{userProfile.name}</h2><p className="text-xs text-slate-400">Bergabung Jan 2024</p></div></div>
|
||||
<div className="text-center"><div className="flex items-center gap-1 text-orange-500 font-bold text-xl">{userProfile.streak} <span className="text-2xl">🔥</span></div><p className="text-[10px] text-slate-400 uppercase font-bold tracking-wider">Streak</p></div>
|
||||
</div>
|
||||
|
||||
<button onClick={() => setShowHelpModal(true)} className="w-full bg-red-50 border border-red-100 text-red-600 py-3 rounded-xl font-bold flex items-center justify-center gap-2 hover:bg-red-100 transition-colors">
|
||||
<Phone className="w-4 h-4" /> BUTUH BANTUAN DARURAT?
|
||||
</button>
|
||||
|
||||
{/* 1. LENCANA PENCAPAIAN */}
|
||||
<div>
|
||||
<h3 className="font-bold text-slate-700 mb-3 flex items-center gap-2"><Award className="w-5 h-5 text-[#4F6DAD]"/> Lencana</h3>
|
||||
<div className="bg-white p-4 rounded-3xl shadow-sm border border-slate-100"><div className="grid grid-cols-3 gap-4 max-h-64 overflow-y-auto pr-1">{BADGES_DB.map(badge => (<div key={badge.id} onClick={() => setModalBadge(badge)} className={`aspect-square rounded-2xl flex flex-col items-center justify-center text-center p-2 cursor-pointer transition-all ${userProfile.badges.includes(badge.id) ? 'bg-[#F5F5F7] hover:bg-slate-200' : 'opacity-40 grayscale bg-slate-50'}`}><span className="text-3xl mb-1">{badge.icon}</span><span className="text-[10px] font-bold text-slate-600 leading-tight">{badge.name}</span></div>))}</div></div>
|
||||
</div>
|
||||
|
||||
{/* 2. LAPORAN KESEHATAN */}
|
||||
<div className="bg-white p-6 rounded-3xl shadow-sm border border-slate-100">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h3 className="font-bold text-slate-700">Laporan Kesehatan</h3>
|
||||
<select value={reportRange} onChange={(e) => setReportRange(e.target.value)} className="bg-[#F5F5F7] text-xs font-bold text-slate-600 py-2 px-3 rounded-lg border-none outline-none">
|
||||
<option value="mingguan">Mingguan</option>
|
||||
<option value="bulanan">Bulanan</option>
|
||||
<option value="tahunan">Tahunan</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="h-40 w-full mb-4">
|
||||
<ResponsiveContainer>
|
||||
<AreaChart data={chartData}>
|
||||
<defs>
|
||||
<linearGradient id="colorScore" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor={severityInfo.color} stopOpacity={0.3}/>
|
||||
<stop offset="95%" stopColor={severityInfo.color} stopOpacity={0}/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#f0f0f0"/>
|
||||
<XAxis dataKey="name" tick={{fontSize:10}} axisLine={false} tickLine={false}/>
|
||||
<Tooltip contentStyle={{borderRadius:'10px', border:'none'}}/>
|
||||
<Area type="monotone" dataKey="score" stroke={severityInfo.color} strokeWidth={3} fill="url(#colorScore)"/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
{assessments.length > 0 || journals.length > 0 ? (
|
||||
<div className={`${severityInfo.bg} p-4 rounded-xl flex gap-3 items-start transition-colors duration-500`}>
|
||||
<BrainCircuit className={`w-5 h-5 ${severityInfo.text} mt-0.5 flex-shrink-0`}/>
|
||||
<div>
|
||||
<h4 className={`text-xs font-bold ${severityInfo.text} uppercase mb-1`}>AI Insight: {severityInfo.label}</h4>
|
||||
<p className={`text-xs ${severityInfo.text} leading-relaxed opacity-90`}>
|
||||
{avgScore >= 10 ? "Tingkat stres sangat tinggi. Segera hubungi bantuan profesional atau orang terdekat." :
|
||||
avgScore >= 7 ? "Terdeteksi beban emosi sedang. Luangkan waktu untuk hobi atau meditasi." :
|
||||
avgScore >= 4 ? "Ada sedikit gejolak. Tetap jaga pola tidur dan aktivitas fisik." :
|
||||
"Kondisi mental Anda stabil dan sehat. Teruskan kebiasaan baik ini!"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-slate-50 p-4 rounded-xl flex gap-3 items-center text-slate-400">
|
||||
<BrainCircuit className="w-5 h-5"/>
|
||||
<p className="text-xs">Isi penilaian untuk melihat analisis AI.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 3. RIWAYAT JURNAL TERBARU (DIBAWAH) */}
|
||||
<div>
|
||||
<h3 className="font-bold text-slate-700 mb-3 flex items-center gap-2"><BookOpen className="w-5 h-5 text-[#4F6DAD]"/> Riwayat Jurnal Terbaru</h3>
|
||||
{journals.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
{journals.slice(0, 3).map(j => (
|
||||
<div key={j.id} className="bg-white p-4 rounded-2xl shadow-sm border border-slate-100">
|
||||
<div className="flex justify-between mb-1">
|
||||
<span className="text-xs font-bold text-[#4F6DAD]">{j.date}</span>
|
||||
</div>
|
||||
<p className="text-sm text-slate-600 line-clamp-1">{j.content.text}</p>
|
||||
</div>
|
||||
))}
|
||||
<button onClick={() => setViewMode('history_full')} className="w-full py-3 text-[#4F6DAD] font-bold text-xs bg-indigo-50 rounded-xl hover:bg-indigo-100">Lihat Semua</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-white p-6 rounded-2xl border border-dashed border-slate-200 text-center text-slate-400 text-sm">
|
||||
Belum ada jurnal yang disimpan.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 4. HAPUS AKUN (PALING BAWAH) */}
|
||||
<div className="pt-4">
|
||||
<button onClick={handleDeleteAccount} className="w-full py-4 text-red-500 font-bold text-sm bg-red-50 rounded-2xl border border-red-100 hover:bg-red-100 transition-colors flex items-center justify-center gap-2">
|
||||
<Trash2 className="w-4 h-4"/> Hapus Akun Permanen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderHistoryFull = () => {
|
||||
const filteredJournals = journals.filter(j => {
|
||||
const q = searchQuery.toLowerCase();
|
||||
if (j.type === 'complete' && j.content.text.toLowerCase().includes(q)) return true;
|
||||
if (j.tags && j.tags.some(t => t.toLowerCase().includes(q))) return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="animate-slide-up pb-20">
|
||||
<div className="sticky top-0 bg-[#F5F5F7] pt-4 pb-2 z-10 flex items-center gap-3 mb-4"><button onClick={() => setViewMode('main')} className="p-2 bg-white rounded-full shadow-sm"><X className="w-5 h-5"/></button><h2 className="text-xl font-bold text-slate-800">Riwayat Jurnal</h2></div>
|
||||
<div className="relative mb-6"><Search className="absolute left-4 top-3.5 w-5 h-5 text-slate-400" /><input type="text" value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} placeholder="Cari jurnal atau tag..." className="w-full pl-12 pr-4 py-3 rounded-xl border border-slate-200 focus:border-[#4F6DAD] outline-none shadow-sm"/></div>
|
||||
<div className="space-y-4">
|
||||
{filteredJournals.length > 0 ? filteredJournals.map(j => (
|
||||
<div key={j.id} className="bg-white p-5 rounded-2xl shadow-sm border border-slate-100">
|
||||
<div className="flex justify-between items-start mb-2"><span className="text-xs font-bold text-[#4F6DAD] bg-indigo-50 px-2 py-1 rounded-md">{j.date}</span></div>
|
||||
<div className="space-y-4">
|
||||
<p className="text-slate-700 text-sm leading-relaxed border-b border-slate-100 pb-3">{j.content.text}</p>
|
||||
<div className="bg-slate-50 p-4 rounded-xl space-y-3">
|
||||
<div className="flex items-center gap-2 mb-1"><div className="w-1.5 h-1.5 rounded-full bg-[#4F6DAD]"></div><span className="text-xs font-bold text-slate-500 uppercase">Hal Kecil</span></div><p className="text-sm text-slate-600 pl-3.5 italic">"{j.content.reflection.q1}"</p>
|
||||
<div className="flex items-center gap-2 mb-1 mt-3"><div className="w-1.5 h-1.5 rounded-full bg-red-400"></div><span className="text-xs font-bold text-slate-500 uppercase">Tantangan</span></div><p className="text-sm text-slate-600 pl-3.5 italic">"{j.content.reflection.q2}"</p>
|
||||
<div className="flex items-center gap-2 mb-1 mt-3"><div className="w-1.5 h-1.5 rounded-full bg-blue-400"></div><span className="text-xs font-bold text-slate-500 uppercase">Perbaikan</span></div><p className="text-sm text-slate-600 pl-3.5 italic">"{j.content.reflection.q3}"</p>
|
||||
</div>
|
||||
</div>
|
||||
{j.tags && <div className="flex gap-2 mt-3">{j.tags.map(t => <span key={t} onClick={() => handleTagClick(t)} className="text-[10px] text-slate-500 bg-slate-100 px-2 py-1 rounded-full cursor-pointer hover:bg-[#4F6DAD] hover:text-white transition-colors">{t}</span>)}</div>}
|
||||
</div>
|
||||
)) : <div className="text-center py-10 text-slate-400"><p>Tidak ditemukan.</p></div>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
if (viewMode === 'history_full') return <div className="min-h-screen bg-[#F5F5F7] font-sans text-[#2D3748] max-w-md mx-auto p-6 relative">{renderHistoryFull()}</div>;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F5F5F7] font-sans text-[#2D3748] max-w-md mx-auto relative shadow-2xl overflow-hidden">
|
||||
<header className="bg-white px-6 pt-8 pb-4 shadow-sm flex justify-between items-center sticky top-0 z-20">
|
||||
<div className="flex items-center gap-2"><div className="bg-[#4F6DAD] p-2 rounded-lg text-white"><Activity className="w-5 h-5" /></div><h1 className="text-lg font-extrabold text-[#4F6DAD]">PsyJournal</h1></div>
|
||||
<div className="w-8 h-8 rounded-full bg-slate-100 border border-slate-200 overflow-hidden"><User className="w-full h-full p-1 text-slate-400"/></div>
|
||||
</header>
|
||||
<main className="p-4">
|
||||
{activeTab === 'journal' && renderJournal()}
|
||||
{activeTab === 'assessment' && renderAssessment()}
|
||||
{activeTab === 'cognitive' && renderCognitive()}
|
||||
{activeTab === 'profile' && renderProfile()}
|
||||
</main>
|
||||
<nav className="fixed bottom-0 left-0 right-0 max-w-md mx-auto bg-white border-t border-slate-200 px-6 py-3 flex justify-between items-center z-30 pb-safe">
|
||||
{[{ id: 'journal', icon: BookOpen, label: 'Jurnal' }, { id: 'assessment', icon: Activity, label: 'Penilaian' }, { id: 'cognitive', icon: BrainCircuit, label: 'Tes' }, { id: 'profile', icon: User, label: 'Profil' }].map((item) => (
|
||||
<button key={item.id} onClick={() => setActiveTab(item.id)} className={`flex flex-col items-center gap-1 transition-all ${activeTab === item.id ? 'text-[#4F6DAD] -translate-y-1' : 'text-slate-300'}`}><item.icon className={`w-6 h-6 ${activeTab === item.id ? 'fill-current' : ''}`} /><span className="text-[10px] font-bold">{item.label}</span></button>
|
||||
))}
|
||||
</nav>
|
||||
{modalBadge && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm p-6 animate-fade-in">
|
||||
<div className="bg-white rounded-3xl p-8 w-full max-w-sm text-center relative shadow-2xl flex flex-col items-center">
|
||||
<button onClick={() => setModalBadge(null)} className="absolute top-4 right-4 text-slate-400"><X/></button>
|
||||
<div className="text-7xl mb-4 animate-bounce mt-4">{modalBadge.icon}</div>
|
||||
<h3 className="text-2xl font-bold text-[#4F6DAD] mb-1">{modalBadge.name}</h3>
|
||||
<div className="bg-slate-50 px-4 py-1.5 rounded-full mb-6 border border-slate-100 shadow-sm mt-2"><span className="text-xs text-slate-500 font-bold flex items-center gap-1"><Clock className="w-3 h-3"/> Dicapai: {modalBadge.date}</span></div>
|
||||
<p className="text-slate-600 text-sm mb-8 px-4 text-center leading-relaxed">{modalBadge.desc}</p>
|
||||
<button onClick={() => setModalBadge(null)} className="w-full bg-[#4F6DAD] text-white py-3 rounded-xl font-bold flex items-center justify-center gap-2"><ArrowRight className="w-4 h-4"/> Tutup</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showHelpModal && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-6 animate-fade-in">
|
||||
<div className="bg-white rounded-3xl p-6 w-full max-w-sm shadow-2xl relative">
|
||||
<button onClick={() => setShowHelpModal(false)} className="absolute top-4 right-4 text-slate-400 hover:text-slate-600"><X className="w-5 h-5"/></button>
|
||||
<div className="flex items-center gap-2 mb-6 text-red-600">
|
||||
<AlertTriangle className="w-6 h-6"/>
|
||||
<h3 className="text-xl font-bold">Bantuan Darurat</h3>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<a href="tel:119" className="flex items-center gap-4 p-4 bg-red-50 text-red-700 rounded-2xl border border-red-100 hover:bg-red-100 transition-colors">
|
||||
<div className="bg-red-200 p-3 rounded-full"><Phone className="w-6 h-6"/></div>
|
||||
<div>
|
||||
<div className="font-bold text-lg">Panggil Ambulans</div>
|
||||
<div className="text-xs opacity-70">Nomor Darurat: 119</div>
|
||||
</div>
|
||||
</a>
|
||||
<a href="https://www.intothelightid.org/tentang-bunuh-diri/layanan-konseling-psikolog-psikiater/" target="_blank" rel="noopener noreferrer" className="flex items-center gap-4 p-4 bg-blue-50 text-blue-700 rounded-2xl border border-blue-100 hover:bg-blue-100 transition-colors">
|
||||
<div className="bg-blue-200 p-3 rounded-full"><Globe className="w-6 h-6"/></div>
|
||||
<div>
|
||||
<div className="font-bold text-lg">Layanan Profesional</div>
|
||||
<div className="text-xs opacity-70">Konseling & Psikolog</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<p className="text-center text-xs text-slate-400 mt-6 px-4">
|
||||
Jangan ragu untuk meminta bantuan. Anda tidak sendirian.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(<PsychologyJournalApp />);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,868 +1,61 @@
|
||||
package com.example.ppb_kelompok2
|
||||
// Yoseph
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.view.ViewGroup
|
||||
import android.webkit.WebSettings
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.*
|
||||
import com.example.ppb_kelompok2.ui.theme.PPB_Kelompok2Theme
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.random.Random
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
|
||||
// --- Main Activity ---
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
setContent {
|
||||
PPB_Kelompok2Theme {
|
||||
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
|
||||
AppNavigationGraph()
|
||||
}
|
||||
Surface(modifier = Modifier.fillMaxSize()) {
|
||||
WebViewContainer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Navigation Graph ---
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
@Composable
|
||||
fun AppNavigationGraph() {
|
||||
val navController = rememberNavController()
|
||||
NavHost(navController = navController, startDestination = "login") {
|
||||
composable("login") { LoginScreen(navController = navController) }
|
||||
composable("main") { MainAppScreen() }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// --- Login Screen ---
|
||||
@Composable
|
||||
fun LoginScreen(navController: NavController) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text("MindTrack AI", style = MaterialTheme.typography.headlineLarge, fontWeight = FontWeight.Bold)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
"Lacak kesehatan mental Anda dengan kekuatan AI.",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(horizontal = 32.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
Button(
|
||||
onClick = { navController.navigate("main") {
|
||||
popUpTo("login") { inclusive = true }
|
||||
} },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 32.dp)
|
||||
) {
|
||||
Icon(Icons.Default.AccountCircle, contentDescription = "Google Icon") // Placeholder for Google Icon
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text("Masuk dengan Google")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Main App Structure (with Bottom Navigation) ---
|
||||
sealed class Screen(val route: String, val label: String, val icon: ImageVector) {
|
||||
object Journal : Screen("journal", "Jurnal", Icons.Default.Book)
|
||||
object Assessment : Screen("assessment", "Penilaian", Icons.Default.Checklist)
|
||||
object CognitiveTest : Screen("cognitive_test", "Tes Kognitif", Icons.Default.SportsEsports)
|
||||
object History : Screen("history", "Riwayat & Grafik", Icons.Default.BarChart)
|
||||
}
|
||||
|
||||
val bottomNavItems = listOf(
|
||||
Screen.Journal,
|
||||
Screen.Assessment,
|
||||
Screen.CognitiveTest,
|
||||
Screen.History
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun MainAppScreen() {
|
||||
val navController = rememberNavController()
|
||||
Scaffold(
|
||||
bottomBar = { AppBottomNavigation(navController = navController) }
|
||||
) { innerPadding ->
|
||||
AppNavHost(navController = navController, modifier = Modifier.padding(innerPadding))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AppBottomNavigation(navController: NavHostController) {
|
||||
NavigationBar {
|
||||
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||
val currentRoute = navBackStackEntry?.destination?.route
|
||||
|
||||
bottomNavItems.forEach { screen ->
|
||||
NavigationBarItem(
|
||||
icon = { Icon(screen.icon, contentDescription = screen.label) },
|
||||
label = { Text(screen.label) },
|
||||
selected = currentRoute == screen.route,
|
||||
onClick = {
|
||||
navController.navigate(screen.route) {
|
||||
popUpTo(navController.graph.startDestinationId) { saveState = true }
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AppNavHost(navController: NavHostController, modifier: Modifier = Modifier) {
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = Screen.Journal.route,
|
||||
modifier = modifier.fillMaxSize()
|
||||
) {
|
||||
composable(Screen.Journal.route) { JournalScreen() }
|
||||
composable(Screen.Assessment.route) { AssessmentScreen() }
|
||||
composable(Screen.CognitiveTest.route) { CognitiveTestScreen(navController) }
|
||||
composable(Screen.History.route) { HistoryScreen() }
|
||||
composable("memory_test") { MemoryTestScreen(navController) }
|
||||
composable("focus_test") { FocusTestScreen(navController) }
|
||||
composable("reaction_test") { ReactionSpeedTestScreen(navController) }
|
||||
}
|
||||
}
|
||||
|
||||
// --- App Screens ---
|
||||
|
||||
@Composable
|
||||
fun JournalScreen() {
|
||||
var journalText by remember { mutableStateOf("") }
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Card(modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 16.dp)) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Text("Analisis AI", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text("Sentimen: Netral")
|
||||
Text("Emosi Terdeteksi: Tenang")
|
||||
}
|
||||
}
|
||||
OutlinedTextField(
|
||||
value = journalText,
|
||||
onValueChange = { journalText = it },
|
||||
label = { Text("Tuliskan perasaanmu di sini...") },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Button(
|
||||
onClick = { /* TODO: Implement save logic */ },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text("Simpan Jurnal")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AssessmentScreen() {
|
||||
val indicators = remember {
|
||||
listOf(
|
||||
"Mood Sedih", "Rasa Bersalah", "Menarik Diri Sosial",
|
||||
"Sulit Konsentrasi", "Kelelahan", "Pikiran Bunuh Diri"
|
||||
)
|
||||
}
|
||||
|
||||
val sliderValues = remember {
|
||||
mutableStateMapOf<String, Float>().apply {
|
||||
indicators.forEach { indicator ->
|
||||
put(indicator, 0f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val totalScore = sliderValues.values.sum().toInt()
|
||||
val assessmentLevel = when (totalScore) {
|
||||
in 0..4 -> "Normal"
|
||||
in 5..9 -> "Ringan"
|
||||
in 10..14 -> "Sedang"
|
||||
else -> "Berat"
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(title = { Text("Penilaian Harian") })
|
||||
}
|
||||
) { innerPadding ->
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(innerPadding)
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
item {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
horizontalAlignment = Alignment.Start
|
||||
) {
|
||||
Text("Ringkasan Skor", style = MaterialTheme.typography.titleMedium)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text("Total Skor: $totalScore", style = MaterialTheme.typography.headlineMedium)
|
||||
Text("Tingkat: $assessmentLevel", style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
items(indicators) { indicatorName ->
|
||||
IndicatorItem(
|
||||
indicatorName = indicatorName,
|
||||
value = sliderValues[indicatorName] ?: 0f,
|
||||
onValueChange = {
|
||||
sliderValues[indicatorName] = it.roundToInt().toFloat()
|
||||
}
|
||||
fun WebViewContainer() {
|
||||
AndroidView(
|
||||
factory = { context ->
|
||||
WebView(context).apply {
|
||||
layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Button(
|
||||
onClick = { /* TODO: Implement finish logic */ },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text("Selesai")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun IndicatorItem(indicatorName: String, value: Float, onValueChange: (Float) -> Unit) {
|
||||
val description = when (value.toInt()) {
|
||||
0 -> "Tidak sama sekali"
|
||||
1 -> "Beberapa hari"
|
||||
2 -> "Lebih dari separuh hari"
|
||||
3 -> "Hampir setiap hari"
|
||||
else -> ""
|
||||
}
|
||||
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(indicatorName, style = MaterialTheme.typography.titleMedium)
|
||||
Text(
|
||||
text = value.toInt().toString(),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Slider(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
valueRange = 0f..3f,
|
||||
steps = 2,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
Text(
|
||||
text = description,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun CognitiveTestScreen(navController: NavController) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Text("Pilih Tes Kognitif", style = MaterialTheme.typography.headlineSmall)
|
||||
|
||||
@Composable
|
||||
fun TestCard(title: String, description: String, icon: ImageVector, route: String) {
|
||||
Card(
|
||||
onClick = { navController.navigate(route) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(icon, contentDescription = null, modifier = Modifier.size(40.dp))
|
||||
Spacer(Modifier.width(16.dp))
|
||||
Column {
|
||||
Text(title, style = MaterialTheme.typography.titleMedium)
|
||||
Text(description, style = MaterialTheme.typography.bodySmall, color = Color.Gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TestCard(
|
||||
title = "Tes Memori",
|
||||
description = "Uji memori jangka pendek Anda",
|
||||
icon = Icons.Default.Memory,
|
||||
route = "memory_test"
|
||||
)
|
||||
TestCard(
|
||||
title = "Tes Fokus",
|
||||
description = "Uji kemampuan fokus & atensi",
|
||||
icon = Icons.Default.CenterFocusStrong,
|
||||
route = "focus_test"
|
||||
)
|
||||
TestCard(
|
||||
title = "Tes Kecepatan Reaksi",
|
||||
description = "Uji kecepatan reaksi visual Anda",
|
||||
icon = Icons.Default.Speed,
|
||||
route = "reaction_test"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Cognitive Test Screens ---
|
||||
|
||||
data class MemoryCard(val id: Int, val icon: ImageVector, var isFaceUp: Boolean = false, var isMatched: Boolean = false)
|
||||
|
||||
enum class MemoryGameState { READY, PLAYING, FINISHED }
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun MemoryTestScreen(navController: NavController) {
|
||||
val icons = listOf(
|
||||
Icons.Default.Favorite, Icons.Default.Star, Icons.Default.ThumbUp,
|
||||
Icons.Default.Spa, Icons.Default.Cloud, Icons.Default.Anchor
|
||||
)
|
||||
var cards by remember { mutableStateOf(createShuffledCards(icons)) }
|
||||
var selectedCards by remember { mutableStateOf(listOf<MemoryCard>()) }
|
||||
var moves by remember { mutableIntStateOf(0) }
|
||||
var bestScore by remember { mutableStateOf<Int?>(null) }
|
||||
var gameState by remember { mutableStateOf(MemoryGameState.READY) }
|
||||
|
||||
LaunchedEffect(cards.all { it.isMatched }) {
|
||||
if (cards.all { it.isMatched } && gameState == MemoryGameState.PLAYING) {
|
||||
gameState = MemoryGameState.FINISHED
|
||||
if (bestScore == null || moves < bestScore!!) {
|
||||
bestScore = moves
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(selectedCards) {
|
||||
if (selectedCards.size == 2) {
|
||||
val (first, second) = selectedCards
|
||||
if (first.icon == second.icon) {
|
||||
cards = cards.map { if (it.id == first.id || it.id == second.id) it.copy(isMatched = true) else it }
|
||||
} else {
|
||||
delay(1000)
|
||||
cards = cards.map { if (it.id == first.id || it.id == second.id) it.copy(isFaceUp = false) else it }
|
||||
}
|
||||
selectedCards = listOf()
|
||||
}
|
||||
}
|
||||
|
||||
fun restartGame() {
|
||||
moves = 0
|
||||
cards = createShuffledCards(icons)
|
||||
gameState = MemoryGameState.PLAYING
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Tes Memori") },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { navController.popBackStack() }) {
|
||||
Icon(Icons.Default.ArrowBack, contentDescription = "Kembali")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) { innerPadding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(innerPadding)
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
when (gameState) {
|
||||
MemoryGameState.READY -> {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Text("Tes Memori", style = MaterialTheme.typography.headlineMedium)
|
||||
bestScore?.let {
|
||||
Text("Skor Terbaik: $it gerakan", style = MaterialTheme.typography.titleMedium)
|
||||
}
|
||||
Text(
|
||||
"Tes ini menguji memori jangka pendek Anda. Cocokkan semua kartu dengan jumlah gerakan sesedikit mungkin.",
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
Button(onClick = { gameState = MemoryGameState.PLAYING }) {
|
||||
Text("Mulai")
|
||||
}
|
||||
}
|
||||
// Konfigurasi WebView agar React berjalan lancar
|
||||
settings.apply {
|
||||
javaScriptEnabled = true
|
||||
domStorageEnabled = true
|
||||
allowFileAccess = true
|
||||
allowContentAccess = true
|
||||
loadWithOverviewMode = true
|
||||
useWideViewPort = true
|
||||
cacheMode = WebSettings.LOAD_DEFAULT
|
||||
mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
|
||||
}
|
||||
|
||||
MemoryGameState.PLAYING, MemoryGameState.FINISHED -> {
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceAround) {
|
||||
Text("Gerakan: $moves", style = MaterialTheme.typography.bodyLarge)
|
||||
bestScore?.let {
|
||||
Text("Skor Terbaik: $it", style = MaterialTheme.typography.bodyLarge)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Fixed(3),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
items(cards) { card ->
|
||||
MemoryCardView(card = card, onCardClicked = {
|
||||
if (gameState == MemoryGameState.PLAYING && !card.isFaceUp && !card.isMatched && selectedCards.size < 2) {
|
||||
cards = cards.map { if (it.id == card.id) it.copy(isFaceUp = true) else it }
|
||||
selectedCards = selectedCards + card
|
||||
if (selectedCards.size == 1) moves++
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
if (gameState == MemoryGameState.FINISHED) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(16.dp)){
|
||||
Text("Selesai!", style = MaterialTheme.typography.headlineMedium, color = MaterialTheme.colorScheme.primary)
|
||||
Button(onClick = { restartGame() }) {
|
||||
Icon(Icons.Default.Refresh, contentDescription = "Coba Lagi")
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text("Coba Lagi")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
webViewClient = WebViewClient()
|
||||
|
||||
// Memuat file index.html dari folder assets
|
||||
loadUrl("file:///android_asset/index.html")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun createShuffledCards(icons: List<ImageVector>): List<MemoryCard> {
|
||||
return (icons + icons).mapIndexed { index, icon -> MemoryCard(id = index, icon = icon) }.shuffled()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun MemoryCardView(card: MemoryCard, onCardClicked: () -> Unit) {
|
||||
Card(
|
||||
onClick = onCardClicked,
|
||||
modifier = Modifier.aspectRatio(1f),
|
||||
enabled = !card.isMatched,
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = if (card.isFaceUp || card.isMatched) MaterialTheme.colorScheme.surfaceVariant else MaterialTheme.colorScheme.primary
|
||||
)
|
||||
) {
|
||||
Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) {
|
||||
if (card.isFaceUp || card.isMatched) {
|
||||
Icon(card.icon, contentDescription = null, modifier = Modifier.size(40.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class FocusGameState { READY, PLAYING, FINISHED }
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun FocusTestScreen(navController: NavController) {
|
||||
var score by remember { mutableIntStateOf(0) }
|
||||
var highScore by remember { mutableIntStateOf(0) }
|
||||
val normalColor = MaterialTheme.colorScheme.onSurface
|
||||
var gridItems by remember { mutableStateOf(generateFocusGrid(normalColor)) }
|
||||
var gameState by remember { mutableStateOf(FocusGameState.READY) }
|
||||
var selectedDuration by remember { mutableIntStateOf(15) }
|
||||
var timeLeft by remember { mutableIntStateOf(selectedDuration) }
|
||||
|
||||
LaunchedEffect(gameState) {
|
||||
if (gameState == FocusGameState.PLAYING) {
|
||||
timeLeft = selectedDuration
|
||||
while (timeLeft > 0) {
|
||||
delay(1000)
|
||||
timeLeft--
|
||||
}
|
||||
if (timeLeft == 0) gameState = FocusGameState.FINISHED
|
||||
} else if (gameState == FocusGameState.FINISHED) {
|
||||
if (score > highScore) {
|
||||
highScore = score
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun newLevel() {
|
||||
gridItems = generateFocusGrid(normalColor)
|
||||
}
|
||||
|
||||
fun restartGame() {
|
||||
score = 0
|
||||
gameState = FocusGameState.READY
|
||||
newLevel()
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Tes Fokus") },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { navController.popBackStack() }) {
|
||||
Icon(Icons.Default.ArrowBack, contentDescription = "Kembali")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) { innerPadding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(innerPadding)
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
when (gameState) {
|
||||
FocusGameState.READY -> {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Text("Tes Fokus", style = MaterialTheme.typography.headlineMedium)
|
||||
if (highScore > 0) {
|
||||
Text("Skor Tertinggi: $highScore", style = MaterialTheme.typography.titleMedium)
|
||||
}
|
||||
Text(
|
||||
"Tes ini menguji kemampuan fokus dan atensi Anda untuk mengidentifikasi perbedaan visual dengan cepat.",
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
Text("Pilih durasi waktu:")
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
val durations = listOf(15, 30, 60, 120)
|
||||
durations.forEach { duration ->
|
||||
val isSelected = selectedDuration == duration
|
||||
OutlinedButton(
|
||||
onClick = { selectedDuration = duration },
|
||||
colors = if (isSelected) ButtonDefaults.outlinedButtonColors(containerColor = MaterialTheme.colorScheme.primaryContainer) else ButtonDefaults.outlinedButtonColors()
|
||||
) {
|
||||
Text("${duration}s")
|
||||
}
|
||||
}
|
||||
}
|
||||
Button(onClick = { gameState = FocusGameState.PLAYING }) {
|
||||
Text("Mulai")
|
||||
}
|
||||
}
|
||||
}
|
||||
FocusGameState.PLAYING -> {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceAround,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text("Skor: $score", style = MaterialTheme.typography.bodyLarge)
|
||||
Text("Waktu: $timeLeft", style = MaterialTheme.typography.bodyLarge)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Fixed(5),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
items(gridItems.indices.toList()) { index ->
|
||||
val item = gridItems[index]
|
||||
Icon(
|
||||
imageVector = item.icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.rotate(item.rotation)
|
||||
.clickable {
|
||||
if (item.isDistractor) {
|
||||
score++
|
||||
newLevel()
|
||||
} else {
|
||||
if (score > 0) score--
|
||||
}
|
||||
},
|
||||
tint = item.color
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
FocusGameState.FINISHED -> {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Text("Waktu Habis!", style = MaterialTheme.typography.headlineMedium)
|
||||
Text("Skor Akhir: $score", style = MaterialTheme.typography.bodyLarge)
|
||||
Text("Skor Tertinggi: $highScore", style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.Bold)
|
||||
Button(onClick = { restartGame() }) {
|
||||
Text("Coba Lagi")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class FocusItem(val icon: ImageVector, val color: Color, val rotation: Float, val isDistractor: Boolean)
|
||||
|
||||
private fun generateFocusGrid(normalColor: Color): List<FocusItem> {
|
||||
val gridSize = 25
|
||||
val normalIcon = Icons.Default.Circle
|
||||
val distractorIndex = Random.nextInt(gridSize)
|
||||
|
||||
val distractorType = Random.nextInt(3)
|
||||
val distractor: FocusItem
|
||||
val items = MutableList(gridSize) { FocusItem(normalIcon, normalColor, 0f, false) }
|
||||
|
||||
when (distractorType) {
|
||||
0 -> { // Different Icon
|
||||
distractor = FocusItem(Icons.Default.Star, normalColor, 0f, true)
|
||||
}
|
||||
1 -> { // Different Color
|
||||
distractor = FocusItem(normalIcon, Color.Red, 0f, true)
|
||||
}
|
||||
else -> { // Different Rotation
|
||||
distractor = FocusItem(normalIcon, normalColor, 90f, true)
|
||||
// Use an icon that shows rotation
|
||||
items.replaceAll { it.copy(icon = Icons.Default.Navigation) }
|
||||
}
|
||||
}
|
||||
items[distractorIndex] = distractor
|
||||
return items
|
||||
}
|
||||
|
||||
enum class ReactionGameState { READY, WAITING, ACTION, FINISHED }
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ReactionSpeedTestScreen(navController: NavController) {
|
||||
var state by remember { mutableStateOf(ReactionGameState.READY) }
|
||||
var startTime by remember { mutableLongStateOf(0L) }
|
||||
var reactionTime by remember { mutableLongStateOf(0L) }
|
||||
var bestTime by remember { mutableStateOf<Long?>(null) }
|
||||
|
||||
val backgroundColor by animateColorAsState(
|
||||
targetValue = when (state) {
|
||||
ReactionGameState.WAITING -> Color.Red.copy(alpha = 0.8f)
|
||||
ReactionGameState.ACTION -> Color.Green.copy(alpha = 0.8f)
|
||||
else -> MaterialTheme.colorScheme.surface
|
||||
},
|
||||
animationSpec = tween(300),
|
||||
label = "ReactionBackgroundColor"
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
|
||||
val onScreenClick = {
|
||||
when (state) {
|
||||
ReactionGameState.WAITING -> {
|
||||
reactionTime = -1 // Too soon
|
||||
state = ReactionGameState.FINISHED
|
||||
}
|
||||
ReactionGameState.ACTION -> {
|
||||
val newReactionTime = System.currentTimeMillis() - startTime
|
||||
reactionTime = newReactionTime
|
||||
if (bestTime == null || newReactionTime < bestTime!!) {
|
||||
bestTime = newReactionTime
|
||||
}
|
||||
state = ReactionGameState.FINISHED
|
||||
}
|
||||
else -> { /* Clicks handled by buttons in READY and FINISHED states */ }
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(state) {
|
||||
if (state == ReactionGameState.WAITING) {
|
||||
delay(Random.nextLong(1500, 5500))
|
||||
if (state == ReactionGameState.WAITING) { // Ensure state hasn't changed
|
||||
startTime = System.currentTimeMillis()
|
||||
state = ReactionGameState.ACTION
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Tes Kecepatan Reaksi") },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { navController.popBackStack() }) {
|
||||
Icon(Icons.Default.ArrowBack, contentDescription = "Kembali")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) { innerPadding ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(innerPadding)
|
||||
.background(backgroundColor)
|
||||
.clickable(
|
||||
enabled = state == ReactionGameState.WAITING || state == ReactionGameState.ACTION,
|
||||
onClick = onScreenClick
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
when (state) {
|
||||
ReactionGameState.READY -> {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
modifier = Modifier.padding(16.dp)
|
||||
) {
|
||||
Text("Tes Kecepatan Reaksi", style = MaterialTheme.typography.headlineMedium, color = MaterialTheme.colorScheme.onSurface)
|
||||
bestTime?.let {
|
||||
Text("Waktu Terbaik: $it ms", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.onSurface)
|
||||
}
|
||||
Text(
|
||||
"Tes ini mengukur kecepatan reaksi visual Anda. Tunggu layar berubah menjadi hijau, lalu tekan secepat mungkin.",
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
Button(onClick = { state = ReactionGameState.WAITING }) {
|
||||
Text("Mulai")
|
||||
}
|
||||
}
|
||||
}
|
||||
ReactionGameState.WAITING -> {
|
||||
Text("Tunggu sampai hijau...", fontSize = 24.sp, color = Color.White, fontWeight = FontWeight.Bold)
|
||||
}
|
||||
ReactionGameState.ACTION -> {
|
||||
Text("Tekan Sekarang!", fontSize = 24.sp, color = Color.White, fontWeight = FontWeight.Bold)
|
||||
}
|
||||
ReactionGameState.FINISHED -> {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
modifier = Modifier.padding(16.dp)
|
||||
) {
|
||||
val resultText = if (reactionTime == -1L) "Terlalu Cepat!" else "${reactionTime} ms"
|
||||
Text(resultText, fontSize = 48.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface)
|
||||
bestTime?.let {
|
||||
Text("Waktu Terbaik: $it ms", fontSize = 20.sp, color = MaterialTheme.colorScheme.onSurface)
|
||||
}
|
||||
Button(onClick = { state = ReactionGameState.READY }) {
|
||||
Text("Coba Lagi")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun HistoryScreen() {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
item {
|
||||
Text("Riwayat & Grafik", style = MaterialTheme.typography.headlineSmall)
|
||||
}
|
||||
item {
|
||||
GraphCard(title = "Tren Mood Mingguan")
|
||||
}
|
||||
item {
|
||||
GraphCard(title = "Perkembangan Skor Depresi")
|
||||
}
|
||||
item {
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Text("Insight AI", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text("AI menemukan pola bahwa mood Anda cenderung menurun di akhir pekan.", style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GraphCard(title: String) {
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Text(title, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(150.dp)
|
||||
.background(Color.LightGray.copy(alpha = 0.5f))
|
||||
) {
|
||||
Text("Area Grafik", modifier = Modifier.align(Alignment.Center), color = Color.Gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
[versions]
|
||||
agp = "8.13.1"
|
||||
agp = "8.13.2"
|
||||
kotlin = "2.0.21"
|
||||
coreKtx = "1.10.1"
|
||||
junit = "4.13.2"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user