Compare commits

...

2 Commits

Author SHA1 Message Date
eb12f138b0 Perbaikan tampilan 2026-01-12 23:13:51 +07:00
6c2d32af30 Perbaikan UI/UX 2026-01-12 16:18:35 +07:00
6 changed files with 946 additions and 847 deletions

1
.idea/.name generated Normal file
View File

@ -0,0 +1 @@
PPB_Kelompok2

View File

@ -4,6 +4,14 @@
<selectionStates> <selectionStates>
<SelectionState runConfigName="app"> <SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" /> <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> </SelectionState>
</selectionStates> </selectionStates>
</component> </component>

View File

@ -2,6 +2,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<application <application
android:allowBackup="true" android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
@ -10,12 +12,14 @@
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.PPB_Kelompok2"> android:theme="@style/Theme.PPB_Kelompok2"
android:usesCleartextTraffic="true">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
android:label="@string/app_name" android:label="@string/app_name"
android:theme="@style/Theme.PPB_Kelompok2"> android:theme="@style/Theme.PPB_Kelompok2"
android:configChanges="orientation|screenSize|keyboardHidden">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />

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

View File

@ -1,868 +1,61 @@
package com.example.ppb_kelompok2 package com.example.ppb_kelompok2
// Yoseph
import android.annotation.SuppressLint
import android.os.Bundle 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.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.compose.animation.animateColorAsState import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.animation.core.tween import androidx.compose.material3.Surface
import androidx.compose.foundation.background import androidx.compose.runtime.Composable
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.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate import androidx.compose.ui.viewinterop.AndroidView
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
// --- Main Activity ---
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
enableEdgeToEdge() enableEdgeToEdge()
setContent { setContent {
PPB_Kelompok2Theme { Surface(modifier = Modifier.fillMaxSize()) {
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) { WebViewContainer()
AppNavigationGraph()
}
} }
} }
} }
} }
// --- Navigation Graph --- @SuppressLint("SetJavaScriptEnabled")
@Composable @Composable
fun AppNavigationGraph() { fun WebViewContainer() {
val navController = rememberNavController() AndroidView(
NavHost(navController = navController, startDestination = "login") { factory = { context ->
composable("login") { LoginScreen(navController = navController) } WebView(context).apply {
composable("main") { MainAppScreen() } layoutParams = ViewGroup.LayoutParams(
} ViewGroup.LayoutParams.MATCH_PARENT,
} ViewGroup.LayoutParams.MATCH_PARENT
// --- 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()
}
) )
}
item { // Konfigurasi WebView agar React berjalan lancar
Button( settings.apply {
onClick = { /* TODO: Implement finish logic */ }, javaScriptEnabled = true
modifier = Modifier.fillMaxWidth() domStorageEnabled = true
) { allowFileAccess = true
Text("Selesai") allowContentAccess = true
} loadWithOverviewMode = true
} useWideViewPort = true
} cacheMode = WebSettings.LOAD_DEFAULT
} mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
}
@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")
}
}
} }
MemoryGameState.PLAYING, MemoryGameState.FINISHED -> { webViewClient = WebViewClient()
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceAround) {
Text("Gerakan: $moves", style = MaterialTheme.typography.bodyLarge) // Memuat file index.html dari folder assets
bestScore?.let { loadUrl("file:///android_asset/index.html")
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")
}
}
}
}
} }
}
}
}
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), modifier = Modifier.fillMaxSize()
label = "ReactionBackgroundColor"
) )
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)
}
}
}
} }

View File

@ -1,5 +1,5 @@
[versions] [versions]
agp = "8.13.1" agp = "8.13.2"
kotlin = "2.0.21" kotlin = "2.0.21"
coreKtx = "1.10.1" coreKtx = "1.10.1"
junit = "4.13.2" junit = "4.13.2"