Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 35 additions & 38 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,31 @@ import {
Send, Menu, X, ChevronLeft, ChevronRight, Bot, User, Paperclip, Camera, Video,
MessageSquare, Newspaper, FileText, CheckCircle2, Circle, AlertTriangle,
Info, Plus, LogOut, UploadCloud, Image as ImageIcon, ShieldCheck,
Sparkles, Signal, Wifi, BatteryFull
Sparkles
} from 'lucide-react';

// Branded gradient + icon header images, generated inline so news always
// shows a header image without depending on an external image host.
const newsImg = (from, to, icon) => `data:image/svg+xml,${encodeURIComponent(
`<svg xmlns="http://www.w3.org/2000/svg" width="400" height="200" viewBox="0 0 400 200">` +
`<defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1">` +
`<stop offset="0%" stop-color="${from}"/><stop offset="100%" stop-color="${to}"/>` +
`</linearGradient></defs>` +
`<rect width="400" height="200" fill="url(#g)"/>` +
`<circle cx="200" cy="100" r="46" fill="#fff" opacity="0.16"/>` +
icon +
`</svg>`
)}`;

const NEWS_IMG_APD = newsImg('#fbbf24', '#d97706',
'<path d="M170,106 a30,26 0 0 1 60,0 z" fill="#fff"/><rect x="166" y="104" width="68" height="9" rx="3" fill="#fff"/><rect x="166" y="104" width="68" height="3" fill="#fde68a"/>');
const NEWS_IMG_FIRE = newsImg('#38bdf8', '#0369a1',
'<path d="M200,70 C214,86 220,98 220,110 C220,124 211,132 200,132 C189,132 180,124 180,110 C180,99 185,90 193,82 C194,92 199,93 200,85 C202,78 199,75 200,70 Z" fill="#fff"/>');
const NEWS_IMG_AWARD = newsImg('#34d399', '#0d9488',
'<path d="M182,78 h36 v18 c0,14 -8,24 -18,24 c-10,0 -18,-10 -18,-24 z" fill="#fff"/><path d="M182,82 c-11,0 -18,7 -18,15 c0,8 6,14 14,15" stroke="#fff" stroke-width="4" fill="none" stroke-linecap="round"/><path d="M218,82 c11,0 18,7 18,15 c0,8 -6,14 -14,15" stroke="#fff" stroke-width="4" fill="none" stroke-linecap="round"/><rect x="193" y="120" width="14" height="10" fill="#fff"/><rect x="184" y="130" width="32" height="7" rx="2" fill="#fff"/>');
const NEWS_IMG_DEFAULT = newsImg('#10b981', '#047857',
'<path d="M200,72 c-14,0 -22,12 -22,28 v14 l-8,10 h60 l-8,-10 v-14 c0,-16 -8,-28 -22,-28 z" fill="#fff"/><circle cx="200" cy="132" r="7" fill="#fff"/>');

const NEWS_ITEMS = [
{
id: 1,
Expand All @@ -15,10 +37,10 @@ const NEWS_ITEMS = [
type: "warning",
summary: "Kewajiban penggunaan helm keselamatan (Safety Helmet) Kelas E terbaru.",
content: "Berdasarkan evaluasi keselamatan semester pertama, manajemen menetapkan bahwa mulai 1 Juli 2026, seluruh personel yang memasuki area operasional zona kuning dan merah wajib menggunakan Helm Keselamatan (Safety Helmet) Kelas E dengan pelindung benturan samping.\n\nPedoman lengkap telah diperbarui dalam dokumen Sistem Manajemen HSSE. Harap seluruh pekerja dan kontraktor menyesuaikan dengan standar terbaru ini. Inspeksi akan diperketat mulai bulan depan.",
image: "https://images.unsplash.com/photo-1541888081-3642eb657736?auto=format&fit=crop&q=80&w=400&h=200",
image: NEWS_IMG_APD,
carousel: [
{ url: "https://images.unsplash.com/photo-1504328345606-18bbc8c9d7d1?auto=format&fit=crop&q=80&w=400&h=300", caption: "Helm standar kelas E" },
{ url: "https://images.unsplash.com/photo-1581092160562-40aa08e78837?auto=format&fit=crop&q=80&w=400&h=300", caption: "Inspeksi kepatuhan di lapangan" }
{ url: NEWS_IMG_APD, caption: "Helm standar kelas E" },
{ url: NEWS_IMG_APD, caption: "Inspeksi kepatuhan di lapangan" }
]
},
{
Expand All @@ -29,9 +51,9 @@ const NEWS_ITEMS = [
type: "info",
summary: "Jadwal dan rute evakuasi untuk simulasi hari Jumat.",
content: "Sebagai bagian dari kesiapsiagaan tanggap darurat, simulasi evakuasi kebakaran (Fire Drill) akan dilaksanakan pada:\n\nHari/Tanggal: Jumat, 26 Juni 2026\nWaktu: 09.00 WIB - Selesai\nLokasi: Seluruh area Gedung Utama dan Fasilitas Produksi A\n\nMohon seluruh personel berpartisipasi aktif dan mengikuti arahan dari Floor Warden di masing-masing lantai.",
image: "https://images.unsplash.com/photo-1614064506922-442cd26588a5?auto=format&fit=crop&q=80&w=400&h=200",
image: NEWS_IMG_FIRE,
carousel: [
{ url: "https://images.unsplash.com/photo-1542385151-efd9000785a0?auto=format&fit=crop&q=80&w=400&h=300", caption: "Rute dan titik kumpul evakuasi" }
{ url: NEWS_IMG_FIRE, caption: "Rute dan titik kumpul evakuasi" }
]
},
{
Expand All @@ -42,9 +64,9 @@ const NEWS_ITEMS = [
type: "success",
summary: "Fasilitas Produksi B berhasil mencapai 1 juta jam kerja aman.",
content: "Apresiasi setinggi-tingginya kepada seluruh tim di Fasilitas Produksi B atas pencapaian luar biasa: 1 Juta Jam Kerja Tanpa Kecelakaan Kerja (Zero LTI) yang tercapai pada pertengahan tahun 2026 ini.\n\nPencapaian ini adalah bukti komitmen kita bersama terhadap budaya HSSE 'Safety First'. Mari kita pertahankan dan tingkatkan kinerja keselamatan di seluruh area operasi.",
image: "https://images.unsplash.com/photo-1581092580497-e0d23cbdf1dc?auto=format&fit=crop&q=80&w=400&h=200",
image: NEWS_IMG_AWARD,
carousel: [
{ url: "https://images.unsplash.com/photo-1504917595217-d4dc5ebe6122?auto=format&fit=crop&q=80&w=400&h=300", caption: "Penghargaan Tim Produksi B" }
{ url: NEWS_IMG_AWARD, caption: "Penghargaan Tim Produksi B" }
]
}
];
Expand Down Expand Up @@ -124,13 +146,6 @@ export default function App() {
// State for Admin/Contributor Popup
const [showInfoPopup, setShowInfoPopup] = useState(false);

// Live clock for the device status bar
const [clock, setClock] = useState(new Date());
useEffect(() => {
const t = setInterval(() => setClock(new Date()), 30000);
return () => clearInterval(t);
}, []);

const chatContainerRef = useRef(null);

useEffect(() => {
Expand Down Expand Up @@ -282,7 +297,7 @@ export default function App() {
type: newsType,
summary: newsForm.summary,
content: newsForm.summary,
image: newsForm.headerImage || "https://images.unsplash.com/photo-1504917595217-d4dc5ebe6122?auto=format&fit=crop&q=80&w=400&h=200",
image: newsForm.headerImage || NEWS_IMG_DEFAULT,
carousel: newsForm.carousel.filter(item => item.url) // Hanya simpan lampiran yang telah diisi foto
};

Expand All @@ -292,20 +307,6 @@ export default function App() {
setActiveTab('news');
};

const clockLabel = clock.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });

// ---- Device status bar (top of the phone frame) ----
const StatusBar = ({ dark = false }) => (
<div className={`flex items-center justify-between px-6 pt-2 pb-1.5 text-[13px] font-bold shrink-0 ${dark ? 'text-white' : 'text-slate-900'}`}>
<span className="tracking-tight">{clockLabel}</span>
<div className="flex items-center space-x-1.5">
<Signal size={15} strokeWidth={2.5} />
<Wifi size={15} strokeWidth={2.5} />
<BatteryFull size={20} strokeWidth={2} />
</div>
</div>
);

// ---- Login ----
const LoginView = () => (
<div className="flex flex-col h-full bg-gradient-to-br from-emerald-800 via-emerald-900 to-teal-950 justify-center items-center px-6 relative overflow-hidden">
Expand Down Expand Up @@ -373,7 +374,7 @@ export default function App() {
<span className="absolute -bottom-0.5 -right-0.5 w-3.5 h-3.5 bg-green-400 border-2 border-emerald-600 rounded-full"></span>
</div>
<div>
<h1 className="font-bold text-base leading-tight">Bot K3 Pertamina</h1>
<h1 className="font-bold text-base leading-tight">HSSE Bot</h1>
<p className="text-emerald-100 text-xs flex items-center gap-1.5">
<span className="w-1.5 h-1.5 bg-green-300 rounded-full animate-pulse"></span>
Online · Selalu aktif
Expand Down Expand Up @@ -685,7 +686,7 @@ export default function App() {
</div>
<div>
<h1 className="font-bold text-base leading-tight">Tabah Darma · Security</h1>
<p className="text-slate-300 text-[11px] font-medium tracking-wide">Contributor Panel • HSSE Shipping & Marine</p>
<p className="text-slate-300 text-[11px] font-medium tracking-wide">HSSE Shipping & Marine</p>
</div>
</div>
<button
Expand Down Expand Up @@ -785,7 +786,7 @@ export default function App() {

if (!isLoggedIn) {
return (
<div className="flex justify-center items-center h-screen bg-gradient-to-br from-slate-300 via-slate-200 to-slate-300 sm:p-4">
<div className="flex justify-center items-center h-[100dvh] overflow-hidden bg-gradient-to-br from-slate-300 via-slate-200 to-slate-300 sm:p-4">
<div className="w-full sm:w-[400px] h-full sm:h-[820px] overflow-hidden flex flex-col relative sm:rounded-[2.8rem] bg-slate-900 sm:border-[10px] border-slate-900 sm:shadow-[0_30px_70px_-15px_rgba(15,23,42,0.5)] sm:ring-1 sm:ring-slate-300">
{LoginView()}
</div>
Expand All @@ -794,11 +795,8 @@ export default function App() {
}

return (
<div className="flex justify-center items-center h-screen bg-gradient-to-br from-slate-300 via-slate-200 to-slate-300 sm:p-4">
<div className="flex justify-center items-center h-[100dvh] overflow-hidden bg-gradient-to-br from-slate-300 via-slate-200 to-slate-300 sm:p-4">
<div className="w-full sm:w-[400px] h-full sm:h-[820px] bg-white overflow-hidden flex flex-col relative sm:rounded-[2.8rem] border-slate-900 sm:border-[10px] sm:shadow-[0_30px_70px_-15px_rgba(15,23,42,0.5)] sm:ring-1 sm:ring-slate-300">
{/* Device status bar */}
<StatusBar />

{/* Main Content Area */}
<div className="flex-1 overflow-hidden relative">
{activeTab === 'chat' && ChatView()}
Expand All @@ -809,7 +807,6 @@ export default function App() {
{/* Global News Create Modal */}
{showNewsModal && (
<div className="absolute inset-0 z-[60] bg-white flex flex-col animate-slide-up">
<StatusBar dark />
<div className="bg-gradient-to-r from-emerald-600 to-teal-600 px-4 py-3 flex items-center justify-between text-white shrink-0 shadow-md relative z-10">
<div className="flex items-center space-x-2">
<button onClick={() => setShowNewsModal(false)} className="p-1 hover:bg-white/15 rounded-lg transition-colors"><X size={20} /></button>
Expand Down