import React, { useState, useEffect } from 'react';
import {
BookOpen,
Layers,
Award,
Play,
ChevronRight,
CheckCircle,
AlertCircle,
RotateCcw,
Menu,
X,
Check,
HelpCircle,
TrendingUp,
BookMarked,
User,
Lock,
Users,
UserPlus,
LogOut,
Shield,
Search,
Eye,
EyeOff
} from 'lucide-react';
// Firebase Imports
import { initializeApp } from 'firebase/app';
import {
getAuth,
signInWithCustomToken,
signInAnonymously,
onAuthStateChanged
} from 'firebase/auth';
import {
getFirestore,
collection,
doc,
setDoc,
updateDoc,
onSnapshot
} from 'firebase/firestore';
// ==========================================
// SAFE-GUARD FIREBASE SCRIPT FOR EXTERNAL COPIES
// ==========================================
let app = null;
let auth = null;
let db = null;
let appId = 'math-lab-kelas8-v2';
// Deteksi apakah konfigurasi Firebase dari platform tersedia
const hasFirebaseConfig = typeof __firebase_config !== 'undefined' && __firebase_config;
if (hasFirebaseConfig) {
try {
const firebaseConfig = JSON.parse(__firebase_config);
app = initializeApp(firebaseConfig);
auth = getAuth(app);
db = getFirestore(app);
appId = typeof __app_id !== 'undefined' ? __app_id : 'math-lab-kelas8-v2';
} catch (err) {
console.error("Gagal menginisialisasi Firebase Cloud:", err);
}
}
// ==========================================
// DATA MATERI (10 PERTEMUAN)
// ==========================================
const PERTEMUAN_DATA = [
{
id: 1,
subBab: "A",
title: "Pertemuan 1: Pengenalan Konsep Himpunan",
deskripsi: "Memahami apa itu himpunan, cara membedakan himpunan dan bukan himpunan, serta lambang keanggotaan.",
konten: (
Selamat datang di Bab 4! Sebelum masuk ke Relasi dan Fungsi, kita harus memahami dasar dari segalanya, yaitu Himpunan.
Definisi Himpunan
Himpunan adalah kumpulan objek atau benda yang memiliki definisi yang jelas sehingga dapat ditentukan dengan pasti apakah suatu objek termasuk dalam kumpulan tersebut atau tidak.
Contoh HIMPUNAN (Definisi Jelas)
- Kumpulan siswa kelas 8 yang memakai kacamata.
- Kumpulan bilangan genap antara 1 sampai 10.
- Kumpulan hewan berkaki empat.
Contoh BUKAN HIMPUNAN (Relatif/Subjektif)
- Kumpulan lukisan yang indah (indah itu relatif).
- Kumpulan siswa yang pintar (pintar tidak ada batas ukurnya).
- Kumpulan makanan yang lezat.
Lambang Keanggotaan
Anggota himpunan dilambangkan dengan elemen (∈). Sedangkan yang bukan anggota dilambangkan dengan (∉).
Misal, Himpunan A = Himpunan bilangan ganjil kurang dari 10.
A = {'{'}1, 3, 5, 7, 9{'}'}
Maka: 3 ∈ A (3 adalah anggota A), tetapi 4 ∉ A (4 bukan anggota A).
)
},
{
id: 2,
subBab: "A",
title: "Pertemuan 2: Menyatakan Himpunan & Himpunan Bagian",
deskripsi: "Mempelajari 3 cara menyatakan himpunan dan memahami konsep himpunan bagian.",
konten: (
Ada tiga cara utama untuk menyajikan atau menuliskan suatu himpunan secara formal dalam matematika:
1
Metode Deskripsi (Kata-kata)
Menyebutkan syarat atau sifat keanggotaannya.
Contoh: P = Himpunan bilangan prima kurang dari 10
2
Metode Tabulasi (Pendaftaran Anggota)
Menuliskan semua anggota di dalam kurung kurawal {'{ }'}.
Contoh: P = {'{'}2, 3, 5, 7{'}'}
3
Notasi Pembentuk Himpunan
Menggunakan variabel peubah dan simbol matematika.
Contoh: P = {'{'} x | x < 10, x ∈ bilangan prima {'}'}
Himpunan Bagian (Subset)
Himpunan A merupakan himpunan bagian dari B (ditulis A ⊂ B) jika setiap anggota A juga merupakan anggota B.
Misal A = {'{'}1, 2{'}'} dan B = {'{'}1, 2, 3, 4{'}'}.
Karena semua anggota A (1 dan 2) ada di B, maka A ⊂ B.
)
},
{
id: 3,
subBab: "B",
title: "Pertemuan 3: Konsep Relasi & Diagram Panah",
deskripsi: "Memahami apa itu relasi antara dua himpunan dan menyajikannya lewat Diagram Panah.",
konten: (
Sekarang kita masuk ke topik utama: Relasi. Dalam matematika, relasi menyatakan hubungan antara dua himpunan.
Definisi Relasi
Relasi dari himpunan A ke himpunan B adalah suatu aturan yang memasangkan anggota-anggota himpunan A dengan anggota-anggota himpunan B.
Menyajikan Relasi: 1. Diagram Panah
Diagram panah adalah cara paling visual dengan menggunakan lingkaran/oval yang mewakili himpunan and garis panah sebagai arah relasinya.
{/* Visual Diagram Panah */}
CONTOH RELASI: "Menyukai Makanan"
Himpunan A (Anak)
Ali
Siti
Budi
Himpunan B (Makanan)
Bakso
Soto
Rujak
Ali menyukai Bakso & Rujak, Siti menyukai Soto, Budi menyukai Bakso.
)
},
{
id: 4,
subBab: "B",
title: "Pertemuan 4: Diagram Kartesius & Himpunan Pasangan Berurutan",
deskripsi: "Mempelajari dua cara lain untuk menyajikan relasi matematika.",
konten: (
Selain Diagram Panah, kita juga dapat menyajikan relasi dengan dua metode yang sangat penting:
1. Himpunan Pasangan Berurutan
Kita memasangkan setiap anggota himpunan pertama (x) dengan anggota himpunan kedua (y) lalu menulisnya dalam format koordinat (x, y) di dalam kurung kurawal.
Contoh:
R = {'{'} (Ali, Bakso), (Ali, Rujak), (Siti, Soto), (Budi, Bakso) {'}'}
2. Diagram Kartesius
Anggota himpunan A diletakkan pada sumbu mendatar (horizontal/X), dan anggota himpunan B pada sumbu tegak (vertikal/Y). Setiap pasangan ditandai dengan noktah (titik hitam).
Soto
-
●
-
Bakso
●
-
●
Rujak
●
-
-
Ali
Siti
Budi
)
},
{
id: 5,
subBab: "C",
title: "Pertemuan 5: Pengenalan Fungsi (Pemetaan)",
deskripsi: "Menemukan perbedaan mendasar antara relasi biasa dengan fungsi, serta mengenal Domain, Kodomain, dan Range.",
konten: (
Apakah semua relasi itu adalah Fungsi? Jawabannya adalah Tidak. Fungsi adalah jenis relasi yang istimewa!
Definisi Fungsi (Pemetaan)
Fungsi dari himpunan A ke B adalah relasi khusus yang memasangkan setiap anggota himpunan A dengan tepat satu anggota himpunan B.
Syarat Mutlak Fungsi (Fokus pada Himpunan A/Domain):
- Semua anggota A harus punya pasangan (tidak boleh jomblo).
- Anggota A hanya boleh dipasangkan satu kali (tidak boleh selingkuh).
- Catatan: Anggota B bebas memilih, boleh bercabang atau tidak punya pasangan.
Istilah Penting Fungsi
Domain (Daerah Asal)
Seluruh anggota himpunan pertama (A).
Kodomain (Daerah Kawan)
Seluruh anggota himpunan kedua (B).
Range (Daerah Hasil)
Anggota B yang benar-benar terpilih oleh pasangan dari A.
)
},
{
id: 6,
subBab: "C",
title: "Pertemuan 6: Notasi & Rumus Fungsi",
deskripsi: "Mempelajari bagaimana merumuskan fungsi secara matematika dan menghitung nilai fungsinya.",
konten: (
Fungsi biasanya dinotasikan dengan huruf kecil seperti f, g, h.
Notasi vs Rumus Fungsi
Notasi Fungsi
f : x → ax + b
Dibaca: fungsi f memetakan x ke ax + b
Rumus Fungsi
f(x) = ax + b
Digunakan langsung untuk menghitung nilai.
Contoh Cara Menghitung Nilai Fungsi
Diketahui fungsi f(x) = 3x - 2. Tentukan nilai f(4)!
Langkah Jawab:
1. f(x) = 3x - 2
2. Ganti nilai x dengan 4
3. f(4) = 3(4) - 2
4. f(4) = 12 - 2 = 10
Jadi, nilai f(4) adalah 10.
)
},
{
id: 7,
subBab: "C",
title: "Pertemuan 7: Grafik Fungsi pada Koordinat Kartesius",
deskripsi: "Menggambar grafik dari suatu rumus fungsi linear ke dalam koordinat kartesius.",
konten: (
Grafik fungsi diperoleh dengan menghubungkan titik-titik koordinat hasil perhitungan rumus fungsi pada bidang Kartesius.
Tahapan Menggambar Grafik Fungsi:
- Tentukan domain (jika belum ditentukan, pilih beberapa bilangan bulat, misal x = -1, 0, 1, 2).
- Hitung nilai f(x) untuk setiap x yang dipilih (sebagai koordinat Y).
- Buat tabel pasangan berurutan (x, f(x)).
- Gambarkan titik-titik tersebut pada koordinat Kartesius lalu tarik garis lurus.
Simulasi Tabel Nilai untuk f(x) = 2x + 1
x
f(x) = 2x + 1
Koordinat (x, y)
0
2(0) + 1 = 1
(0, 1)
1
2(1) + 1 = 3
(1, 3)
2
2(2) + 1 = 5
(2, 5)
*Jika digambarkan di koordinat Kartesius, titik (0,1), (1,3), dan (2,5) akan membentuk satu garis lurus diagonal naik ke kanan.
)
},
{
id: 8,
subBab: "D",
title: "Pertemuan 8: Konsep Korespondensi Satu-satu",
deskripsi: "Mengenal jenis relasi paling eksklusif, yaitu Korespondensi Satu-satu.",
konten: (
Mari kita tingkatkan pemahaman kita ke tingkat tertinggi dalam relasi: Korespondensi Satu-Satu.
Definisi Korespondensi Satu-Satu
Dua himpunan A dan B dikatakan memiliki hubungan korespondensi satu-satu jika setiap anggota A dipasangkan dengan tepat satu anggota B, dan sebaliknya setiap anggota B dipasangkan dengan tepat satu anggota A.
Syarat Mutlak Korespondensi Satu-Satu:
- Jumlah anggota himpunan A harus sama persis dengan jumlah anggota himpunan B. n(A) = n(B).
- Tidak boleh ada satu pun anggota dari kedua himpunan yang tidak memiliki pasangan atau memiliki pasangan ganda.
Contoh Nyata Korespondensi Satu-satu:
- Himpunan Negara dengan himpunan Ibu Kota Negara (setiap negara hanya punya 1 ibukota, dan sebaliknya).
- Himpunan Siswa di kelas dengan himpunan Nomor Induk Siswa Nasional / NISN.
- Himpunan Kendaraan dengan himpunan Nomor Plat Polisi.
)
},
{
id: 9,
subBab: "D",
title: "Pertemuan 9: Menghitung Banyak Korespondensi Satu-satu",
deskripsi: "Mempelajari rumus faktorial untuk menentukan banyaknya kemungkinan korespondensi satu-satu.",
konten: (
Berapa banyak susunan korespondensi satu-satu yang bisa kita buat dari dua himpunan yang jumlah anggotanya sama?
Rumus Banyak Korespondensi Satu-satu
Jika n(A) = n(B) = n, maka banyak korespondensi satu-satu yang mungkin adalah:
n! = n × (n - 1) × (n - 2) × ... × 3 × 2 × 1
*(n! dibaca: n faktorial)
Contoh Perhitungan
Jika A = {'{'}a, b, c{'}'} dan B = {'{'}1, 2, 3{'}'}. Tentukan banyak korespondensi satu-satu yang mungkin terbentuk!
1. Tentukan n: n(A) = 3 dan n(B) = 3, maka n = 3.
2. Gunakan rumus: 3! = 3 × 2 × 1
3. Hitung hasil: 3 × 2 = 6, lalu 6 × 1 = 6.
Jadi, ada 6 kemungkinan susunan korespondensi satu-satu.
)
},
{
id: 10,
subBab: "Rangkuman",
title: "Pertemuan 10: Rangkuman & Peta Konsep Bab 4",
deskripsi: "Mengkaji ulang semua materi dari Konsep Himpunan hingga Korespondensi Satu-satu sebelum tes.",
konten: (
Selamat! Kamu telah menyelesaikan seluruh rangkaian materi Bab 4. Mari kita rangkum poin pentingnya:
A. Konsep Himpunan
Kumpulan berdefinisi jelas. Dapat dinyatakan dengan deskripsi, tabulasi anggota, dan notasi himpunan.
B. Relasi
Hubungan/pemasangan anggota himpunan A ke B. Bebas dipasangkan tanpa syarat ketat. Disajikan lewat diagram panah, Kartesius, & pasangan berurutan.
C. Fungsi (Pemetaan)
Relasi khusus di mana setiap anggota Domain (A) wajib dipasangkan tepat satu ke Kodomain (B). Memiliki rumus $f(x) = ax + b$.
D. Korespondensi Satu-satu
Relasi super eksklusif antara dua himpunan dengan $n(A) = n(B)$, di mana setiap anggota saling memiliki tepat satu pasangan unik tanpa ada sisa. Banyaknya kemungkinan dihitung dengan rumus $n!$.
🎉 Kamu Siap Mengambil Ujian!
Silakan pilih menu kuis per sub-bab atau langsung uji kemampuanmu di Tes Sumatif!
)
}
];
// ==========================================
// DATA SOAL KUIS (PER SUB-BAB & SUMATIF)
// ==========================================
const QUIZ_DATA = {
"A": {
title: "Kuis Sub-Bab A: Konsep Himpunan",
questions: [
{
question: "Di bawah ini yang merupakan contoh HIMPUNAN yang terdefinisi dengan jelas adalah...",
options: [
"Kumpulan lukisan yang bernilai seni tinggi",
"Kumpulan siswa yang bertubuh tinggi di atas 165 cm",
"Kumpulan makanan daerah yang rasanya enak",
"Kumpulan kota besar yang menyenangkan untuk ditinggali"
],
answer: 1,
explanation: "Pilihan B terdefinisi jelas karena menggunakan batas angka pasti (di atas 165 cm). Pilihan lain seperti tinggi, enak, dan menyenangkan bersifat relatif."
},
{
question: "Jika Himpunan K = { x | 2 < x ≤ 10, x ∈ bilangan genap }, maka anggota himpunan K yang terdaftar adalah...",
options: [
"{2, 4, 6, 8, 10}",
"{4, 6, 8}",
"{4, 6, 8, 10}",
"{2, 4, 6, 8}"
],
answer: 2,
explanation: "Karena syaratnya adalah 2 < x ≤ 10, angka 2 tidak termasuk, tetapi angka 10 termasuk. Bilangan genapnya adalah {4, 6, 8, 10}."
},
{
question: "Diketahui A = {a, b}. Tentukan banyaknya semua himpunan bagian dari A yang mungkin!",
options: [
"2",
"4",
"8",
"16"
],
answer: 1,
explanation: "Rumus banyaknya himpunan bagian adalah 2ⁿ. Karena n(A) = 2, maka 2² = 4."
}
]
},
"B": {
title: "Kuis Sub-Bab B: Relasi",
questions: [
{
question: "Dua himpunan A = {2, 3, 4} dan B = {4, 6, 8, 10}. Relasi yang tepat memasangkan A ke B untuk pasangan (2,4), (3,6), (4,8) adalah...",
options: [
"Faktor dari",
"Kurang dari",
"Setengah dari",
"Dua kali dari"
],
answer: 2,
explanation: "2 adalah setengah dari 4, 3 setengah dari 6, dan 4 setengah dari 8. Jadi relasi yang tepat adalah 'Setengah dari'."
},
{
question: "Himpunan pasangan berurutan yang menyatakan relasi 'kuadrat dari' dari himpunan P = {1, 2, 3, 4} ke Q = {1, 4, 9, 16, 25} adalah...",
options: [
"{ (1,1), (2,4), (3,9), (4,16) }",
"{ (1,1), (4,2), (9,3), (16,4) }",
"{ (1,2), (2,4), (3,6), (4,8) }",
"{ (2,1), (4,2), (6,3), (8,4) }"
],
answer: 0,
explanation: "Kuadrat dari P ke Q berarti anggota P kuadratnya adalah anggota Q. 1²=1, 2²=4, 3²=9, 4²=16."
}
]
},
"C": {
title: "Kuis Sub-Bab C: Fungsi (Pemetaan)",
questions: [
{
question: "Suatu relasi disebut Fungsi jika...",
options: [
"Setiap anggota kodomain memiliki tepat satu pasangan di domain",
"Setiap anggota domain memiliki pasangan di kodomain",
"Setiap anggota domain dipasangkan dengan tepat satu anggota kodomain",
"Jumlah anggota domain sama dengan jumlah anggota kodomain"
],
answer: 2,
explanation: "Syarat utama fungsi adalah setiap anggota domain harus dipasangkan tepat satu dengan anggota di kodomain."
},
{
question: "Diketahui rumus fungsi f(x) = 5x - 7. Nilai dari f(3) adalah...",
options: [
"8",
"15",
"22",
"10"
],
answer: 0,
explanation: "Substitusi x = 3 ke rumus f(x): f(3) = 5(3) - 7 = 15 - 7 = 8."
},
{
question: "Jika f(x) = ax + b, dengan f(2) = 7 dan f(1) = 4, maka rumus fungsi f(x) adalah...",
options: [
"f(x) = 2x + 3",
"f(x) = 3x + 1",
"f(x) = 4x - 1",
"f(x) = 3x + 4"
],
answer: 1,
explanation: "Sistem persamaan: 2a + b = 7 dan a + b = 4. Kurangi kedua persamaan, didapat a = 3, b = 1. Jadi f(x) = 3x + 1."
}
]
},
"D": {
title: "Kuis Sub-Bab D: Korespondensi Satu-satu",
questions: [
{
question: "Pasangan himpunan berikut yang MUNGKIN membentuk korespondensi satu-satu adalah...",
options: [
"A = {warna lampu lalu lintas} dan B = {nama hari}",
"P = {huruf vokal} dan Q = {angka jari pada satu tangan utuh}",
"K = {faktor dari 6} dan L = {negara ASEAN}",
"X = {provinsi di pulau Jawa} dan Y = {ibukota negara}"
],
answer: 1,
explanation: "P = {a,i,u,e,o} memiliki 5 anggota, dan Q = {Ibu jari, Telunjuk, Tengah, Manis, Kelingking} juga memiliki 5 anggota. Karena n(P) = n(Q), mereka bisa membentuk korespondensi satu-satu."
},
{
question: "Diketahui himpunan M = {1, 2, 3, 4} dan N = {a, b, c, d}. Banyaknya korespondensi satu-satu yang mungkin terbentuk dari M ke N adalah...",
options: [
"16",
"24",
"12",
"4"
],
answer: 1,
explanation: "Banyak anggota n = 4. Rumus korespondensi satu-satu: 4! = 4 × 3 × 2 × 1 = 24."
}
]
},
"SUMATIF": {
title: "Tes Sumatif BAB 4: Relasi dan Fungsi",
questions: [
{
question: "Manakah di bawah ini yang dikategorikan sebagai himpunan kosong?",
options: [
"Himpunan bilangan prima genap",
"Himpunan nama bulan yang diawali huruf 'Z'",
"Himpunan bilangan cacah kurang dari 1",
"Himpunan siswa berambut keriting"
],
answer: 1,
explanation: "Tidak ada nama bulan yang berawalan huruf 'Z', sehingga anggotanya kosong."
},
{
question: "Diberikan himpunan A = {1, 2, 3} dan B = {a, b}. Relasi dari A ke B disajikan dengan himpunan pasangan berurutan { (1,a), (2,a), (3,b) }. Relasi tersebut merupakan...",
options: [
"Relasi biasa tetapi bukan Fungsi",
"Fungsi (Pemetaan)",
"Korespondensi satu-satu",
"Bukan Relasi maupun Fungsi"
],
answer: 1,
explanation: "Setiap anggota A {1, 2, 3} muncul tepat satu kali di sebelah kiri pasangan. Jadi ini adalah Fungsi."
},
{
question: "Pada fungsi f: x → 4x - 5, bayangan (hasil) dari -2 adalah...",
options: [
"-13",
"-3",
"3",
"13"
],
answer: 0,
explanation: "f(-2) = 4(-2) - 5 = -8 - 5 = -13."
},
{
question: "Diketahui fungsi g(x) = ax + b. Jika g(5) = 17 dan g(2) = 8, nilai dari g(10) adalah...",
options: [
"32",
"26",
"30",
"20"
],
answer: 0,
explanation: "Didapat a = 3 dan b = 2. Rumus fungsi: g(x) = 3x + 2. Nilai g(10) = 3(10) + 2 = 32."
},
{
question: "Dari diagram panah relasi berikut, mana yang merupakan KORESPONDENSI SATU-SATU?",
options: [
"Setiap anggota domain bercabang ke kodomain yang sama",
"Satu anggota domain tidak memiliki pasangan di kodomain",
"Setiap anggota domain dipasangkan tepat satu dengan anggota kodomain, dan sebaliknya setiap anggota kodomain tepat satu dengan domain",
"Anggota kodomain boleh tidak memiliki pasangan asal domain terisi semua"
],
answer: 2,
explanation: "Korespondensi satu-satu mensyaratkan hubungan dua arah yang seimbang: satu-satu tanpa sisa di kedua belah pihak."
},
{
question: "Jika n(P) = n(Q) = 5, berapakah jumlah seluruh kemungkinan korespondensi satu-satu yang dapat dibuat?",
options: [
"120",
"25",
"60",
"10"
],
answer: 0,
explanation: "5! = 5 × 4 × 3 × 2 × 1 = 120 kemungkinan."
},
{
question: "Suatu fungsi didefinisikan f(x) = 7 - 3x. Jika f(k) = -8, maka nilai k adalah...",
options: [
"3",
"-5",
"5",
"-3"
],
answer: 2,
explanation: "7 - 3k = -8 -> -3k = -15 -> k = 5."
},
{
question: "Himpunan daerah hasil (Range) dari relasi { (1,2), (2,4), (3,6), (4,8) } adalah...",
options: [
"{1, 2, 3, 4}",
"{2, 4, 6, 8}",
"{1, 2, 3, 4, 6, 8}",
"{2, 4, 8}"
],
answer: 1,
explanation: "Range adalah seluruh nilai ordinat (sebelah kanan) dari pasangan berurutan, yaitu {2, 4, 6, 8}."
},
{
question: "Bila A = {faktor dari 10} dan B = {bilangan prima kurang dari 7}. Maka jumlah anggota n(A) dan n(B) berturut-turut adalah...",
options: [
"4 dan 3",
"3 dan 3",
"4 dan 4",
"2 dan 3"
],
answer: 0,
explanation: "A = {1, 2, 5, 10} -> n(A) = 4. B = {2, 3, 5} -> n(B) = 3."
},
{
question: "Sebuah fungsi f(x) = x² - 2x. Nilai f(-3) adalah...",
options: [
"3",
"15",
"-15",
"9"
],
answer: 1,
explanation: "f(-3) = (-3)² - 2(-3) = 9 + 6 = 15."
}
]
}
};
export default function App() {
// ==========================================
// STATE MANAGEMENT
// ==========================================
const [fbUser, setFbUser] = useState(null);
const [allStudents, setAllStudents] = useState([]);
const [isLocalMode, setIsLocalMode] = useState(!db); // Flag jika Firebase tidak aktif
// App Navigation & Session
const [activeTab, setActiveTab] = useState('login'); // 'login', 'dashboard', 'materi', 'playground', 'tes', 'admin-panel'
const [loginUser, setLoginUser] = useState(null); // { id, name, username, role: 'siswa'|'admin', scores, completedPertemuan }
// Login Form States
const [loginRole, setLoginRole] = useState('siswa'); // 'siswa' | 'admin'
const [inputUsername, setInputUsername] = useState('');
const [inputPassword, setInputPassword] = useState('');
const [showPassword, setShowPassword] = useState(false);
const [loginError, setLoginError] = useState('');
// Admin: Tambah Siswa Form States
const [newSiswaName, setNewSiswaName] = useState('');
const [newSiswaUsername, setNewSiswaUsername] = useState('');
const [newSiswaPassword, setNewSiswaPassword] = useState('');
const [adminMessage, setAdminMessage] = useState({ type: '', text: '' });
const [adminSearchQuery, setAdminSearchQuery] = useState('');
// Course States
const [selectedPertemuan, setSelectedPertemuan] = useState(1);
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
// Active Quiz States
const [activeQuizKey, setActiveQuizKey] = useState("A");
const [quizStep, setQuizStep] = useState(0);
const [selectedOption, setSelectedOption] = useState(null);
const [showExplanation, setShowExplanation] = useState(false);
const [quizCorrectCount, setQuizCorrectCount] = useState(0);
const [quizFinished, setQuizFinished] = useState(false);
// Playground States
const [playgroundSetA, setPlaygroundSetA] = useState(["1", "2", "3"]);
const [playgroundSetB, setPlaygroundSetB] = useState(["A", "B", "C"]);
const [playgroundPairs, setPlaygroundPairs] = useState([]);
const [activeConnectingFrom, setActiveConnectingFrom] = useState(null);
// ==========================================
// EFFECTS & DATABASE INITIALIZATION
// ==========================================
// 1. Auth Init & Listener (Rule 3)
useEffect(() => {
if (!auth) {
// Beralih ke Local Mode jika dijalankan secara eksternal (No auth object)
setFbUser({ uid: "local_sandbox_user" });
setIsLocalMode(true);
return;
}
const initAuth = async () => {
try {
if (typeof __initial_auth_token !== 'undefined' && __initial_auth_token) {
await signInWithCustomToken(auth, __initial_auth_token);
} else {
await signInAnonymously(auth);
}
} catch (err) {
console.error("Firebase auth error:", err);
}
};
initAuth();
const unsubscribe = onAuthStateChanged(auth, (user) => {
setFbUser(user);
});
return () => unsubscribe();
}, []);
// 2. Real-Time Sync / Local Fallback Loader
useEffect(() => {
if (isLocalMode || !db) {
// MEMBACA DARI LOCALSTORAGE JIKA DICOPY KE WEB EKSTERNAL
const localData = localStorage.getItem('math_lab_students');
if (localData) {
setAllStudents(JSON.parse(localData));
} else {
const defaultStudents = [
{
id: "siswa_1_default",
name: "Siswa Contoh 1",
username: "siswa1",
password: "123",
completedPertemuan: [1],
scores: {
A: null,
B: null,
C: null,
D: null,
SUMATIF: null
},
createdAt: new Date().toISOString()
}
];
setAllStudents(defaultStudents);
localStorage.setItem('math_lab_students', JSON.stringify(defaultStudents));
}
return;
}
if (!fbUser) return;
const studentsCollectionRef = collection(db, 'artifacts', appId, 'public', 'data', 'students');
const unsubscribe = onSnapshot(studentsCollectionRef, (snapshot) => {
const studentList = [];
snapshot.forEach((doc) => {
studentList.push({ id: doc.id, ...doc.data() });
});
setAllStudents(studentList);
// Auto seed default data if database is empty
if (studentList.length === 0) {
seedInitialData();
}
}, (error) => {
console.error("Firestore loading error, switching to Local Storage Mode:", error);
setIsLocalMode(true);
});
return () => unsubscribe();
}, [fbUser, isLocalMode]);
// Sinkronisasi data user aktif saat data AllStudents terupdate
useEffect(() => {
if (loginUser && loginUser.role === 'siswa') {
const freshData = allStudents.find(s => s.id === loginUser.id);
if (freshData) {
setLoginUser({
...loginUser,
name: freshData.name,
username: freshData.username,
scores: freshData.scores || {},
completedPertemuan: freshData.completedPertemuan || [1]
});
}
}
}, [allStudents]);
// Seeder helper untuk Firestore
const seedInitialData = async () => {
if (!db) return;
try {
const defaultStudentId = "siswa_1_default";
const studentDocRef = doc(db, 'artifacts', appId, 'public', 'data', 'students', defaultStudentId);
await setDoc(studentDocRef, {
name: "Siswa Contoh 1",
username: "siswa1",
password: "123",
completedPertemuan: [1],
scores: {
A: null,
B: null,
C: null,
D: null,
SUMATIF: null
},
createdAt: new Date().toISOString()
});
} catch (err) {
console.error("Error seeding initial student:", err);
}
};
// ==========================================
// AUTHENTICATION LOGIC (STUDENT & ADMIN)
// ==========================================
const handleLogin = (e) => {
e.preventDefault();
setLoginError('');
if (!inputUsername.trim() || !inputPassword.trim()) {
setLoginError('Username dan Password wajib diisi.');
return;
}
if (loginRole === 'admin') {
// Admin static credentials
if (inputUsername.toLowerCase() === 'admin' && inputPassword === 'admin123') {
const adminSession = {
id: 'admin_session',
name: 'Administrator Guru',
username: 'admin',
role: 'admin'
};
setLoginUser(adminSession);
setActiveTab('admin-panel');
clearLoginForm();
} else {
setLoginError('Kredensial Admin salah. (Gunakan: admin / admin123)');
}
} else {
// Student Dynamic Credential Matching
const matchedStudent = allStudents.find(
s => s.username.toLowerCase() === inputUsername.toLowerCase() && s.password === inputPassword
);
if (matchedStudent) {
setLoginUser({
id: matchedStudent.id,
name: matchedStudent.name,
username: matchedStudent.username,
role: 'siswa',
scores: matchedStudent.scores || {},
completedPertemuan: matchedStudent.completedPertemuan || [1]
});
setActiveTab('dashboard');
clearLoginForm();
} else {
setLoginError('Username atau Password siswa salah. Silakan hubungi Admin/Guru.');
}
}
};
const handleLogout = () => {
setLoginUser(null);
setActiveTab('login');
setMobileMenuOpen(false);
};
const clearLoginForm = () => {
setInputUsername('');
setInputPassword('');
setLoginError('');
};
// ==========================================
// STUDENT REGISTER LOGIC (ADMIN ROLE ONLY)
// ==========================================
const handleAddSiswa = async (e) => {
e.preventDefault();
setAdminMessage({ type: '', text: '' });
if (!newSiswaName.trim() || !newSiswaUsername.trim() || !newSiswaPassword.trim()) {
setAdminMessage({ type: 'error', text: 'Semua kolom wajib diisi untuk mendaftarkan siswa.' });
return;
}
const usernameClean = newSiswaUsername.trim().toLowerCase();
// Check if username already exists
const exists = allStudents.some(s => s.username.toLowerCase() === usernameClean);
if (exists || usernameClean === 'admin') {
setAdminMessage({ type: 'error', text: 'Username sudah digunakan. Pilih username lain.' });
return;
}
const newSiswaId = "siswa_" + Date.now();
const newSiswaData = {
id: newSiswaId,
name: newSiswaName.trim(),
username: usernameClean,
password: newSiswaPassword,
completedPertemuan: [1],
scores: {
A: null,
B: null,
C: null,
D: null,
SUMATIF: null
},
createdAt: new Date().toISOString()
};
if (isLocalMode || !db) {
// Simpan ke Local Storage jika berjalan eksternal
const updatedList = [...allStudents, newSiswaData];
setAllStudents(updatedList);
localStorage.setItem('math_lab_students', JSON.stringify(updatedList));
setAdminMessage({ type: 'success', text: `Siswa "${newSiswaName}" berhasil ditambahkan ke database offline!` });
setNewSiswaName('');
setNewSiswaUsername('');
setNewSiswaPassword('');
} else {
// Simpan ke Firestore cloud
try {
const studentDocRef = doc(db, 'artifacts', appId, 'public', 'data', 'students', newSiswaId);
await setDoc(studentDocRef, newSiswaData);
setAdminMessage({ type: 'success', text: `Siswa "${newSiswaName}" berhasil ditambahkan!` });
setNewSiswaName('');
setNewSiswaUsername('');
setNewSiswaPassword('');
} catch (err) {
setAdminMessage({ type: 'error', text: 'Gagal menyimpan siswa ke server cloud.' });
}
}
};
// ==========================================
// CLASS PROGRESS SYSTEM
// ==========================================
const completeCurrentPertemuan = async (id) => {
if (!loginUser || loginUser.role !== 'siswa') {
if (id < 10) setSelectedPertemuan(id + 1);
return;
}
const currentCompleted = loginUser.completedPertemuan || [1];
if (!currentCompleted.includes(id)) {
const updatedCompleted = [...currentCompleted, id];
// Update local state terlebih dahulu agar interaksi instan
setLoginUser({ ...loginUser, completedPertemuan: updatedCompleted });
if (isLocalMode || !db) {
const updatedStudents = allStudents.map(s =>
s.id === loginUser.id ? { ...s, completedPertemuan: updatedCompleted } : s
);
setAllStudents(updatedStudents);
localStorage.setItem('math_lab_students', JSON.stringify(updatedStudents));
} else {
// Save to cloud
try {
const studentDocRef = doc(db, 'artifacts', appId, 'public', 'data', 'students', loginUser.id);
await updateDoc(studentDocRef, {
completedPertemuan: updatedCompleted
});
} catch (err) {
console.error("Gagal menyimpan progres belajar ke cloud:", err);
}
}
}
if (id < 10) {
setSelectedPertemuan(id + 1);
}
};
// ==========================================
// PLAYGROUND SIMULATOR DETECTS
// ==========================================
const analyzePlaygroundRelation = () => {
if (playgroundPairs.length === 0) return "Tidak ada relasi yang terbentuk";
const domainsMapped = playgroundPairs.map(p => p.from);
const codomainsMapped = playgroundPairs.map(p => p.to);
const domainCounts = {};
playgroundPairs.forEach(p => {
domainCounts[p.from] = (domainCounts[p.from] || 0) + 1;
});
const isFunction = playgroundSetA.every(elem => domainCounts[elem] === 1);
const codomainCounts = {};
playgroundPairs.forEach(p => {
codomainCounts[p.to] = (codomainCounts[p.to] || 0) + 1;
});
const isOneToOne = isFunction &&
playgroundSetB.every(elem => codomainCounts[elem] === 1) &&
playgroundSetA.length === playgroundSetB.length;
if (isOneToOne) {
return "Korespondensi Satu-satu (Jenis Pemetaan Paling Sempurna!)";
} else if (isFunction) {
return "Fungsi / Pemetaan (Setiap Domain dipasangkan tepat satu)";
} else {
return "Relasi Biasa (Beberapa Domain bercabang atau kosong)";
}
};
const handlePlaygroundPairToggle = (from, to) => {
const exists = playgroundPairs.find(p => p.from === from && p.to === to);
if (exists) {
setPlaygroundPairs(playgroundPairs.filter(p => !(p.from === from && p.to === to)));
} else {
setPlaygroundPairs([...playgroundPairs, { from, to }]);
}
setActiveConnectingFrom(null);
};
// ==========================================
// INTERACTIVE KUIS FLOW
// ==========================================
const startQuiz = (key) => {
setActiveQuizKey(key);
setQuizStep(0);
setSelectedOption(null);
setShowExplanation(false);
setQuizCorrectCount(0);
setQuizFinished(false);
setActiveTab('tes-aktif');
};
const handleOptionSelect = (optionIdx) => {
if (showExplanation) return;
setSelectedOption(optionIdx);
};
const submitAnswer = () => {
if (selectedOption === null) return;
const currentQuiz = QUIZ_DATA[activeQuizKey];
if (selectedOption === currentQuiz.questions[quizStep].answer) {
setQuizCorrectCount(quizCorrectCount + 1);
}
setShowExplanation(true);
};
const nextQuestion = async () => {
const currentQuiz = QUIZ_DATA[activeQuizKey];
if (quizStep + 1 < currentQuiz.questions.length) {
setQuizStep(quizStep + 1);
setSelectedOption(null);
setShowExplanation(false);
} else {
const finalScore = Math.round((quizCorrectCount / currentQuiz.questions.length) * 100);
if (loginUser && loginUser.role === 'siswa') {
const updatedScores = {
...loginUser.scores,
[activeQuizKey]: finalScore
};
setLoginUser({
...loginUser,
scores: updatedScores
});
if (isLocalMode || !db) {
const updatedStudents = allStudents.map(s =>
s.id === loginUser.id ? { ...s, scores: updatedScores } : s
);
setAllStudents(updatedStudents);
localStorage.setItem('math_lab_students', JSON.stringify(updatedStudents));
} else {
// Save to cloud
try {
const studentDocRef = doc(db, 'artifacts', appId, 'public', 'data', 'students', loginUser.id);
await updateDoc(studentDocRef, {
[`scores.${activeQuizKey}`]: finalScore
});
} catch (err) {
console.error("Gagal menyimpan hasil nilai ke cloud:", err);
}
}
}
setQuizFinished(true);
}
};
const getOverallProgress = () => {
if (!loginUser || loginUser.role !== 'siswa') return 0;
const completedCount = loginUser.completedPertemuan?.length || 0;
const quizCount = Object.values(loginUser.scores || {}).filter(v => v !== null).length;
return Math.min(100, Math.round(((completedCount + quizCount) / 15) * 100)); // 10 meetings + 5 quizzes
};
const filteredStudents = allStudents.filter(student =>
student.name.toLowerCase().includes(adminSearchQuery.toLowerCase()) ||
student.username.toLowerCase().includes(adminSearchQuery.toLowerCase())
);
return (
{/* HEADER BAR */}
MATH-LAB: Relasi & Fungsi
Kelas 8 • {isLocalMode ? 'Mode Offline / Lokal' : 'Mode Cloud Connected'}
{/* Desktop Navigation */}
{loginUser && (
)}
{/* Mobile Menu Toggle */}
{loginUser && (
)}
{/* Mobile Drawer */}
{mobileMenuOpen && loginUser && (
{loginUser.role === 'admin' ? (
<>
>
) : (
<>
>
)}
{loginUser.name}
{loginUser.role}
)}
{/* CORE WRAPPER */}
{/* ==========================================
TAB: LOGIN SCREEN
========================================== */}
{activeTab === 'login' && (
{/* Header Title */}
Portal Belajar MATH-LAB
Masuk untuk menyimpan progres belajar dan mengakses materi Relasi & Fungsi Kelas 8.
{/* Role Selector Tabs */}
{/* Login Error Notification */}
{loginError && (
{loginError}
)}
{/* Input Form */}
{/* Credential Reference for Ease of Testing */}
🔑 Akun Default untuk Pengujian:
• Admin: admin / Password: admin123
• Siswa: siswa1 / Password: 123
Admin dapat menambahkan akun siswa baru melalui dashboard admin.
)}
{/* ==========================================
TAB: ADMIN PANEL (MANAGE STUDENTS)
========================================== */}
{activeTab === 'admin-panel' && loginUser && loginUser.role === 'admin' && (
{/* Header Admin */}
Role: Guru Admin
Dashboard Manajemen Siswa
Pantau progres belajar siswa Anda secara real-time dan kelola akun kelas di bawah ini.
Total Terdaftar
{allStudents.length} Siswa
{isLocalMode && (
Aplikasi Berjalan dalam Mode Sandbox / Lokal
Karena dijalankan secara eksternal (di luar platform), database beralih menggunakan memori browser lokal (Local Storage) agar tidak crash. Anda tetap bisa mendaftarkan siswa dan menguji aplikasi secara penuh.
)}
{/* Form Tambah Siswa Baru */}
Daftarkan Siswa Baru
{adminMessage.text && (
{adminMessage.type === 'success' ? : }
{adminMessage.text}
)}
{/* Tabel Progres Nilai Siswa */}
Daftar & Nilai Siswa Kelas
{/* Search Input */}
setAdminSearchQuery(e.target.value)}
className="w-full pl-8 pr-3 py-1.5 rounded-lg border border-slate-200 text-xs focus:ring-1 focus:ring-indigo-500"
/>
{filteredStudents.length === 0 ? (
Tidak ada data siswa ditemukan.
) : (
Nama (Username)
Progres Bab
Kuis A
Kuis B
Kuis C
Kuis D
Sumatif
{filteredStudents.map((siswa) => {
const completedCount = siswa.completedPertemuan?.length || 0;
return (
{siswa.name}
User: {siswa.username} | Pass: {siswa.password}
{completedCount}/10
{siswa.scores?.A !== undefined && siswa.scores?.A !== null ? `${siswa.scores.A}` : '-'}
{siswa.scores?.B !== undefined && siswa.scores?.B !== null ? `${siswa.scores.B}` : '-'}
{siswa.scores?.C !== undefined && siswa.scores?.C !== null ? `${siswa.scores.C}` : '-'}
{siswa.scores?.D !== undefined && siswa.scores?.D !== null ? `${siswa.scores.D}` : '-'}
{siswa.scores?.SUMATIF !== undefined && siswa.scores?.SUMATIF !== null ? `${siswa.scores.SUMATIF}%` : '-'}
);
})}
)}
)}
{/* ==========================================
TAB: STUDENT DASHBOARD
========================================== */}
{activeTab === 'dashboard' && loginUser && (
{/* Selamat Datang Card */}
Dashboard Siswa
Selamat Belajar, {loginUser.name}! 👋
Ayo tuntaskan 10 pertemuan dan kuis interaktif di bawah. Progres belajar Anda tersimpan secara otomatis dan dipantau oleh Guru Kelas Anda.
Total Progres
{getOverallProgress()}%
{/* Progress Stats */}
Pertemuan Selesai
{(loginUser.completedPertemuan || [1]).length} / 10 Pertemuan
Kuis Terselesaikan
{Object.values(loginUser.scores || {}).filter(v => v !== null && v !== undefined).length} / 5 Evaluasi
Nilai Tes Sumatif
{loginUser.scores?.SUMATIF !== null && loginUser.scores?.SUMATIF !== undefined
? `${loginUser.scores.SUMATIF}%`
: "Belum Ujian"}
{/* Peta Kurikulum Visual */}
Alur Pembelajaran Bab 4
{/* Sub-Bab A */}
Sub-Bab A: Himpunan
P 1-2
Mempelajari konsep dasar pembentuk relasi dan cara menyatakannya.
{/* Sub-Bab B */}
Sub-Bab B: Relasi
P 3-4
Memetakan hubungan dua kelompok objek matematika dengan diagram kartesius.
{/* Sub-Bab C */}
Sub-Bab C: Fungsi
P 5-7
Relasi teratur dengan aturan tepat satu pasangan dan cara menghitung rumusnya.
{/* Sub-Bab D */}
Sub-Bab D: Korespondensi
P 8-10
Pasangan satu-lawan-satu seimbang sempurna menggunakan rumus faktorial.
)}
{/* ==========================================
TAB: MATERI PEMBELAJARAN (10 PERTEMUAN)
========================================== */}
{activeTab === 'materi' && (
{/* Sidebar Daftar Pertemuan */}
Daftar 10 Pertemuan
{PERTEMUAN_DATA.map((p) => {
const isCompleted = loginUser?.completedPertemuan?.includes(p.id);
const isActive = selectedPertemuan === p.id;
return (
);
})}
{/* Panel Konten Utama Pertemuan */}
BAB 4: Relasi dan Fungsi
Sub-Bab {PERTEMUAN_DATA[selectedPertemuan - 1].subBab}
{PERTEMUAN_DATA[selectedPertemuan - 1].title}
{PERTEMUAN_DATA[selectedPertemuan - 1].deskripsi}
{PERTEMUAN_DATA[selectedPertemuan - 1].konten}
{/* Tombol Navigasi Pembelajaran */}
)}
{/* ==========================================
TAB: PLAYGROUND SIMULATOR (RELASI & FUNGSI)
========================================== */}
{activeTab === 'playground' && (
Playground Relasi Interaktif
Hubungkan anggota Himpunan A ke Himpunan B untuk bereksperimen. Program otomatis mendeteksi apakah hubunganmu merupakan Relasi Biasa, Fungsi, atau Korespondensi Satu-satu!
{/* Kolom Himpunan A */}
Himpunan A (Domain)
{playgroundSetA.map((element) => {
const isConnecting = activeConnectingFrom === element;
return (
Anggota {element}
);
})}
{/* Garis / Koneksi Tengah */}
Relasi Terbentuk
{playgroundPairs.length === 0 ? (
Klik tombol "Hubungkan" pada anggota A lalu pilih tujuannya di B.
) : (
{playgroundPairs.map((pair, index) => (
{pair.from} → {pair.to}
))}
)}
{/* Kolom Himpunan B */}
Himpunan B (Kodomain)
{playgroundSetB.map((element) => {
const isValidTarget = activeConnectingFrom !== null;
return (
Anggota {element}
);
})}
{/* Panel Analisis */}
Hasil Analisis Relasi:
{analyzePlaygroundRelation()}
Ingat: Fungsi mengharuskan setiap A memiliki tepat 1 cabang ke B. Korespondensi Satu-satu mengharuskan setiap A dan B memiliki tepat 1 cabang dan jumlah anggota A = B.
)}
{/* ==========================================
TAB: LIST EVALUASI & TES
========================================== */}
{activeTab === 'tes' && (
Evaluasi & Pengukuran Pemahaman
Uji pemahamanmu setelah membaca materi. Nilai Anda akan tersimpan langsung ke akun Anda.
{/* Kuis per Sub Bab A */}
Sub-Bab A
{loginUser?.scores?.A !== undefined && loginUser?.scores?.A !== null && (
Skor: {loginUser.scores.A}%
)}
Kuis Konsep Himpunan
3 Pertanyaan pilihan ganda seputar definisi himpunan formal dan himpunan bagian.
{/* Kuis per Sub Bab B */}
Sub-Bab B
{loginUser?.scores?.B !== undefined && loginUser?.scores?.B !== null && (
Skor: {loginUser.scores.B}%
)}
Kuis Relasi
2 Pertanyaan seputar penyajian relasi lewat diagram, koordinat Kartesius.
{/* Kuis per Sub Bab C */}
Sub-Bab C
{loginUser?.scores?.C !== undefined && loginUser?.scores?.C !== null && (
Skor: {loginUser.scores.C}%
)}
Kuis Fungsi / Pemetaan
3 Pertanyaan mendalam seputar formula f(x), mencari nilai x, dan diagram fungsi.
{/* Kuis per Sub Bab D */}
Sub-Bab D
{loginUser?.scores?.D !== undefined && loginUser?.scores?.D !== null && (
Skor: {loginUser.scores.D}%
)}
Kuis Korespondensi Satu-satu
2 Pertanyaan rumus faktorial dan pengenalan korespondensi seimbang.
{/* Tes Sumatif Final Banner */}
SUMATIF AKHIR
Ujian Sumatif Komprehensif BAB 4
Mengujikan seluruh materi (10 pertanyaan acak terbaik). Selesaikan kuis ini untuk menguasai materi Relasi & Fungsi.
{loginUser?.scores?.SUMATIF !== undefined && loginUser?.scores?.SUMATIF !== null && (
Skor Terakhir: {loginUser.scores.SUMATIF}%
)}
)}
{/* ==========================================
TAB: ACTIVE QUIZ WORKSPACE
========================================== */}
{activeTab === 'tes-aktif' && (
{/* Quiz Header */}
{QUIZ_DATA[activeQuizKey].title}
Pertanyaan {quizStep + 1} dari {QUIZ_DATA[activeQuizKey].questions.length}
{/* Progress Bar */}
{/* Normal Quiz Flow */}
{!quizFinished ? (
{/* Pertanyaan */}
{QUIZ_DATA[activeQuizKey].questions[quizStep].question}
{/* Pilihan Ganda */}
{QUIZ_DATA[activeQuizKey].questions[quizStep].options.map((option, idx) => {
const isSelected = selectedOption === idx;
const isCorrect = idx === QUIZ_DATA[activeQuizKey].questions[quizStep].answer;
let optionStyle = "border-slate-200 bg-white hover:bg-slate-50";
if (showExplanation) {
if (isCorrect) {
optionStyle = "border-emerald-500 bg-emerald-50 text-emerald-900";
} else if (isSelected) {
optionStyle = "border-rose-500 bg-rose-50 text-rose-950";
} else {
optionStyle = "border-slate-100 bg-slate-50 text-slate-400";
}
} else if (isSelected) {
optionStyle = "border-indigo-600 bg-indigo-50/50 text-indigo-950";
}
return (
);
})}
{/* Panel Penjelasan / Pembahasan */}
{showExplanation && (
{selectedOption === QUIZ_DATA[activeQuizKey].questions[quizStep].answer ? (
Jawabanmu Benar!
) : (
Jawabanmu Kurang Tepat
)}
Pembahasan: {QUIZ_DATA[activeQuizKey].questions[quizStep].explanation}
)}
{/* Tombol Aksi */}
{!showExplanation ? (
) : (
)}
) : (
// Hasil Kuis Selesai
Kuis Selesai! 🎉
Nilai yang kamu dapatkan untuk topik ini:
{Math.round((quizCorrectCount / QUIZ_DATA[activeQuizKey].questions.length) * 100)}%
Kamu berhasil menjawab {quizCorrectCount} dari {QUIZ_DATA[activeQuizKey].questions.length} pertanyaan dengan benar.
)}
)}
{/* FOOTER */}
);
}
Blog
tes
FORMULIR PENDAFTARAN HOSTING
expired
ppdbmtselqodar
How Many Times to Brush in Day
There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by injected humour, or randomised words which don’t look even slightly believable. If you are going to use a passage of Lorem Ipsum, you need to be sure there isn’t anything embarrassing hidden in the middle of text. All the Lorem Ipsum generators on the Internet tend to repeat predefined chunks as necessary, making this the first true generator on the Internet. It uses a dictionary of over 200 Latin words, combined with a handful of model sentence structures, to generate Lorem Ipsum which looks reasonable. The generated Lorem Ipsum is therefore always free from repetition, injected humour, or non-characteristic words etc.
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Nunc vel dolor ut velit euismod condimentum.
Sed ut libero ut massa imperdiet fringilla a quis nisi.
Proin ac velit ultrices, tempus velit bibendum, porta ipsum.
Aliquam dapibus purus at vulputate ultricies.
Nunc sit amet mi sit amet purus tincidunt tincidunt et sit amet ex.
Outstanding Dental Care
There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by injected humour, or randomised words which don’t look even slightly believable. If you are going to use a passage of Lorem Ipsum, you need to be sure there isn’t anything embarrassing hidden in the middle of text. All the Lorem Ipsum generators on the Internet tend to repeat predefined chunks as necessary, making this the first true generator on the Internet. It uses a dictionary of over 200 Latin words, combined with a handful of model sentence structures, to generate Lorem Ipsum which looks reasonable. The generated Lorem Ipsum is therefore always free from repetition, injected humour, or non-characteristic words etc.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc vel dolor ut velit euismod condimentum. Sed ut libero ut massa imperdiet fringilla a quis nisi. Proin ac velit ultrices, tempus velit bibendum, porta ipsum. Aliquam dapibus purus at vulputate ultricies. Nunc sit amet mi sit amet purus tincidunt tincidunt et sit amet ex.Steps to Prevent Dental Caries
There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by injected humour, or randomised words which don’t look even slightly believable. If you are going to use a passage of Lorem Ipsum, you need to be sure there isn’t anything embarrassing hidden in the middle of text. All the Lorem Ipsum generators on the Internet tend to repeat predefined chunks as necessary, making this the first true generator on the Internet. It uses a dictionary of over 200 Latin words, combined with a handful of model sentence structures, to generate Lorem Ipsum which looks reasonable. The generated Lorem Ipsum is therefore always free from repetition, injected humour, or non-characteristic words etc.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc vel dolor ut velit euismod condimentum. Sed ut libero ut massa imperdiet fringilla a quis nisi. Proin ac velit ultrices, tempus velit bibendum, porta ipsum. Aliquam dapibus purus at vulputate ultricies. Nunc sit amet mi sit amet purus tincidunt tincidunt et sit amet ex.Hello world!
Welcome to WordPress. This is your first post. Edit or delete it, then start writing!







