diff --git a/apps/web/package.json b/apps/web/package.json index 2c741ef..e8cc382 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@repo/ui": "workspace:*", + "@repo/utils": "workspace:*", "@tailwindcss/vite": "^4.1.18", "react": "^19.2.3", "react-dom": "^19.2.3", diff --git a/apps/web/src/apps/example/date-example.tsx b/apps/web/src/apps/example/date-example.tsx new file mode 100644 index 0000000..38ec2d0 --- /dev/null +++ b/apps/web/src/apps/example/date-example.tsx @@ -0,0 +1,279 @@ +import { DateService } from '@repo/utils'; +import React, { useState, useEffect, useMemo } from 'react'; + +// Mock Data untuk simulasi list tugas/tagihan +// Kita gunakan relative time agar mock data selalu relevan saat dijalankan +const MOCK_TASKS = [ + { + id: 1, + title: 'Bayar Server (Lusa)', + dueDate: DateService.now().add(2, 'day').toISOString(), + }, + { + id: 2, + title: 'Laporan Bulanan (Lewat 5 Hari)', + dueDate: DateService.now().subtract(5, 'day').toISOString(), + }, + { + id: 3, + title: 'Meeting Tahunan (Bulan Depan)', + dueDate: DateService.now().add(1, 'month').toISOString(), + }, + { + id: 4, + title: 'Deadline Besok Pagi', + dueDate: DateService.now().add(1, 'day').startOf('day').toISOString(), + }, +]; + +export const DateServiceExample: React.FC = () => { + // 1. STATE: Waktu sekarang (untuk jam digital) + const [currentTime, setCurrentTime] = useState(DateService.now()); + + // 2. STATE: Manipulasi tanggal interaktif + const [selectedDate, setSelectedDate] = useState(DateService.now()); + + // 3. STATE: Khusus Demo Logic Calendar (Besok jam 00:00 vs Sekarang) + // Ini untuk menjawab kenapa diff kadang 0 padahal beda hari + const [calendarTarget] = useState(() => DateService.now().add(1, 'day').startOf('day')); + + // 4. MEMO: Nilai Epoch Static (Hanya diambil saat component mount) + // Gunakan ini jika ingin nilai 'kapan user membuka halaman' + const staticDate = useMemo(() => new DateService(), []); + + // Effect untuk update jam setiap detik + useEffect(() => { + const timer = setInterval(() => { + setCurrentTime(DateService.now()); + }, 1000); + return () => clearInterval(timer); + }, []); + + // Handler untuk tombol tambah/kurang hari + const handleAddDay = (days: number) => { + setSelectedDate((prev) => prev.add(days, 'day')); + }; + + return ( +
+

DateService Feature Showcase

+ +
+ {/* --- CARD 1: REALTIME CLOCK --- */} +
+
+

Realtime Clock

+
{currentTime.format('HH:mm:ss')}
+
{currentTime.format('dddd, DD MMMM YYYY')}
+
+
+ ISO: {currentTime.toISOString()} +
+
+ + {/* --- CARD 2: EPOCH & TIMESTAMPS --- */} +
+

Epoch / Timestamp Data

+ +
+ {/* STATIC SECTION */} +
+
STATIC (Load Once)
+
Captured via useMemo([]), tidak berubah.
+
+
+
Epoch Millis
+
{staticDate.epochMillis}
+
+
+
Epoch Seconds
+
{staticDate.epochSeconds}
+
+
+
+ + {/* LIVE SECTION */} +
+
LIVE (Reactive)
+
Updated every second (new instance).
+
+
+
Epoch Millis
+
{new DateService().epochMillis}
+
+
+
Epoch Seconds
+
{new DateService().epochSeconds}
+
+
+
+
+
+ + {/* --- CARD 3: CALENDAR LOGIC DEMO (Standard vs Calendar Diff) --- */} +
+

+ 🗓️ Calendar Logic Demo (The "Zero Day" Issue) +

+
+ {/* Info Section */} +
+

+ Compare Current Time vs Tomorrow 00:00. +

+
+
+ Now: + {currentTime.format('HH:mm')} +
+
+ Target: + {calendarTarget.format('DD MMM HH:mm')} +
+
+
+ + {/* Wrong Way */} +
+
Standard .diff('day')
+
+ {calendarTarget.diff(currentTime, 'day')}{' '} + Days +
+

+ Hasil 0 jika jarak < 24 jam. Kurang cocok untuk deadline "H-1". +

+
+ + {/* Right Way */} +
+
New .diffCalendarDay()
+
+ {calendarTarget.diffCalendarDay(currentTime)}{' '} + Days +
+

Hasil 1. Menghitung selisih tanggal kalender (ignore jam).

+
+
+
+ + {/* --- CARD 4: MANIPULATION --- */} +
+

+ Interactive Manipulation +

+ +
+ + +
+ {selectedDate.format('DD MMMM YYYY')} + Selected Date State +
+ + +
+ +
+
+ Precise Diff (Float) + + {selectedDate.diff(currentTime, 'day', true).toFixed(2)} days + +
+
+ Calendar Diff (Int) + + {selectedDate.diffCalendarDay(currentTime)} days + +
+
+
+
+ + {/* --- CARD 5: TASK LIST (IMPLEMENTATION REAL) --- */} +
+

Task List Implementation

+ +
+ + + + + + + + + + + {MOCK_TASKS.map((task) => { + const dueDate = new DateService(task.dueDate); + const now = DateService.now(); // Mengambil waktu 'live' dari state currentTime akan menyebabkan re-render tiap detik, tapi disini kita instantiate baru agar logic per baris bersih. + + // Logic 1: Apakah sudah lewat? (Check absolute timestamp) + const isOverdue = now.isAfter(dueDate); + + // Logic 2: Gunakan CALENDAR DIFF untuk countdown yang enak dibaca user + // Jika pakai diff() biasa, tugas besok pagi bisa dianggap '0 days left' + const daysDiff = dueDate.diffCalendarDay(now); + + // Logic 3: Warning jika kurang dari 3 hari lagi (dan belum overdue) + const isDueSoon = !isOverdue && daysDiff <= 3; + + let statusBadge; + const rowClass = 'border-b border-gray-50 hover:bg-gray-50 transition-colors'; + + if (isOverdue) { + statusBadge = ( + + OVERDUE + + ); + } else if (isDueSoon) { + statusBadge = ( + + DUE SOON + + ); + } else { + statusBadge = ( + + ACTIVE + + ); + } + + return ( + + + + + + + ); + })} + +
Task NameDue DateStatus BadgeHuman Readable Countdown
{task.title}{dueDate.format('DD MMM YYYY HH:mm')}{statusBadge} + {isOverdue ? ( + `${Math.abs(daysDiff)} days late` // Gunakan Math.abs karena diff negatif + ) : daysDiff === 0 ? ( + Today! + ) : ( + `${daysDiff} days left` + )} +
+
+
+
+ ); +}; diff --git a/apps/web/src/apps/example/encryption-demo.tsx b/apps/web/src/apps/example/encryption-demo.tsx new file mode 100644 index 0000000..c734eb2 --- /dev/null +++ b/apps/web/src/apps/example/encryption-demo.tsx @@ -0,0 +1,179 @@ +import { EncryptionService } from '@repo/utils'; +import React, { useState } from 'react'; + +export const EncryptionDemoApps: React.FC = () => { + // --- 1. SETUP INSTANCE --- + // Kita gunakan Singleton agar hemat memory + const cryptoService = EncryptionService.getInstance(); + + // --- 2. STATE --- + const [plainText, setPlainText] = useState(''); + const [encryptedResult, setEncryptedResult] = useState(''); + const [decryptedResult, setDecryptedResult] = useState(''); + + // State untuk demo LocalStorage + const [storageKey] = useState('my_secure_token'); + const [storageValue, setStorageValue] = useState(''); + const [savedStatus, setSavedStatus] = useState(''); + + // --- 3. HANDLERS --- + + const handleEncrypt = () => { + // Panggil method encrypt dari class + const result = cryptoService.encrypt(plainText); + setEncryptedResult(result); + + // Reset decrypt result biar user harus klik decrypt lagi untuk validasi + setDecryptedResult(''); + }; + + const handleDecrypt = () => { + // Panggil method decrypt dari class + const original = cryptoService.decrypt(encryptedResult); + setDecryptedResult(original); + }; + + // --- DEMO LOCALSTORAGE --- + const saveToStorage = () => { + try { + // Encrypt sebelum simpan + const secureData = cryptoService.encrypt(storageValue); + localStorage.setItem(storageKey, secureData); + setSavedStatus(`Saved securely! Cipher: ${secureData.substring(0, 15)}...`); + } catch (error) { + setSavedStatus('Error saving data'); + } + }; + + const loadFromStorage = () => { + try { + const storedData = localStorage.getItem(storageKey); + if (!storedData) { + setSavedStatus('No data found in storage'); + return; + } + // Decrypt setelah ambil + const originalData = cryptoService.decrypt(storedData); + setSavedStatus(`Loaded & Decrypted: "${originalData}"`); + } catch (error) { + setSavedStatus('Failed to decrypt storage data'); + } + }; + + return ( +
+

Encryption Service Demo

+ +
+ {/* === SECTION 1: INTERACTIVE PLAYGROUND === */} +
+

🔐 Live Playground

+ + {/* INPUT */} +
+ + setPlainText(e.target.value)} + placeholder="Type sensitive data here..." + className="w-full p-2 border border-slate-300 rounded focus:ring-2 focus:ring-indigo-500 outline-none" + /> +
+ + {/* ACTIONS */} +
+ + +
+ + {/* OUTPUT: ENCRYPTED */} +
+ +