Dokumen ini menjelaskan langkah-langkah untuk mempublikasikan (deploy) aplikasi React Anda ke internet agar dapat diakses secara publik.
import React, { useState, useEffect } from 'react';
import {
Home,
User,
Plus,
Package,
ChevronLeft,
Loader2,
CheckCircle2,
Tag,
FileText
} from 'lucide-react';
// Mock data awal untuk produk
const INITIAL_PRODUCTS = [
{ id: 1, name: 'Brownies Lumer', price: 15000, description: 'Cokelat melimpah, cocok untuk camilan sore.', category: 'Makanan' },
{ id: 2, name: 'Kaos Kelas Custom', price: 85000, description: 'Bahan katun combed 30s, adem dan nyaman.', category: 'Fashion' },
{ id: 3, name: 'Jasa Desain Poster', price: 25000, description: 'Desain kreatif untuk tugas atau acara.', category: 'Jasa' },
];
const App = () => {
// State Management
const [view, setView] = useState('home'); // 'home', 'add', 'profile'
const [products, setProducts] = useState(INITIAL_PRODUCTS);
const [loading, setLoading] = useState(false);
const [snackbar, setSnackbar] = useState({ show: false, message: '' });
// Form State
const [formData, setFormData] = useState({ name: '', price: '', description: '' });
// Fungsi untuk menampilkan snackbar secara otomatis menghilang
const showSnackbar = (message) => {
setSnackbar({ show: true, message });
setTimeout(() => setSnackbar({ show: false, message: '' }), 3000);
};
// Handler Simpan Produk
const handleAddProduct = (e) => {
e.preventDefault();
if (!formData.name || !formData.price || !formData.description) return;
setLoading(true);
// Simulasi loading/proses simpan
setTimeout(() => {
const newProduct = {
id: Date.now(),
name: formData.name,
price: parseInt(formData.price),
description: formData.description,
category: 'Umum'
};
setProducts([newProduct, ...products]);
setLoading(false);
setFormData({ name: '', price: '', description: '' });
setView('home');
showSnackbar('Produk berhasil ditambahkan!');
}, 1000);
};
// Komponen Navigasi Bawah
const BottomNav = () => (
<div className="fixed bottom-0 left-0 right-0 bg-surface-container border-t border-outline-variant flex justify-around items-center h-20 pb-4 px-4 bg-white shadow-[0_-1px_3px_rgba(0,0,0,0.1)]">
<button
onClick={() => setView('home')}
className={`flex flex-col items-center gap-1 transition-colors ${view === 'home' ? 'text-primary' : 'text-outline'}`}
>
<div className={`p-2 rounded-full ${view === 'home' ? 'bg-primary-container text-primary' : ''}`}>
<Home size={24} fill={view === 'home' ? 'currentColor' : 'none'} />
</div>
<span className="text-xs font-medium">Beranda</span>
</button>
<button
onClick={() => setView('profile')}
className={`flex flex-col items-center gap-1 transition-colors ${view === 'profile' ? 'text-primary' : 'text-outline'}`}
>
<div className={`p-2 rounded-full ${view === 'profile' ? 'bg-primary-container text-primary' : ''}`}>
<User size={24} fill={view === 'profile' ? 'currentColor' : 'none'} />
</div>
<span className="text-xs font-medium">Profil</span>
</button>
</div>
);
// Komponen Snackbar
const GlobalSnackbar = () => (
<div className={`fixed bottom-24 left-1/2 -translate-x-1/2 z-50 transition-all duration-300 transform ${snackbar.show ? 'translate-y-0 opacity-100' : 'translate-y-10 opacity-0 pointer-events-none'}`}>
<div className="bg-inverse-surface text-inverse-on-surface px-6 py-3 rounded-xl shadow-lg flex items-center gap-3 min-w-[300px]">
<CheckCircle2 size={20} className="text-green-400" />
<span className="text-sm font-medium">{snackbar.message}</span>
</div>
</div>
);
return (
<div className="min-h-screen bg-slate-50 font-sans text-slate-900 pb-24">
{/* HEADER */}
<header className="sticky top-0 z-30 bg-white/80 backdrop-blur-md border-b border-slate-100 px-6 h-16 flex items-center justify-between">
{view === 'add' ? (
<button onClick={() => setView('home')} className="p-2 -ml-2 hover:bg-slate-100 rounded-full transition-colors">
<ChevronLeft size={24} />
</button>
) : (
<div className="flex items-center gap-2">
<div className="bg-indigo-600 p-1.5 rounded-lg">
<Package size={20} className="text-white" />
</div>
<h1 className="text-xl font-bold tracking-tight text-indigo-950">MarketSiswa</h1>
</div>
)}
<div className="w-8 h-8 rounded-full bg-slate-200 overflow-hidden border border-slate-300">
{/* Placeholder Avatar */}
<div className="w-full h-full flex items-center justify-center text-[10px] font-bold text-slate-500">JS</div>
</div>
</header>
<main className="max-w-3xl mx-auto p-6">
{/* VIEW: HOME */}
{view === 'home' && (
<div className="space-y-6">
<div className="flex flex-col gap-1">
<h2 className="text-2xl font-bold text-slate-900">Halo, Siswa! 👋</h2>
<p className="text-slate-500 text-sm">Temukan produk kreatif dari teman-temanmu.</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{products.map((product) => (
<div key={product.id} className="bg-white p-4 rounded-2xl border border-slate-100 shadow-sm hover:shadow-md transition-shadow">
<div className="flex justify-between items-start mb-2">
<span className="px-2 py-0.5 bg-indigo-50 text-indigo-600 text-[10px] font-bold uppercase tracking-wider rounded-md">
{product.category}
</span>
<h3 className="text-lg font-bold text-slate-900">
Rp {product.price.toLocaleString('id-ID')}
</h3>
</div>
<h4 className="font-semibold text-slate-800 mb-1">{product.name}</h4>
<p className="text-slate-500 text-sm line-clamp-2 leading-relaxed">
{product.description}
</p>
<button className="mt-4 w-full py-2 bg-slate-50 text-slate-700 font-semibold text-sm rounded-xl hover:bg-slate-100 transition-colors border border-slate-200">
Lihat Detail
</button>
</div>
))}
</div>
{/* FAB - Floating Action Button */}
<button
onClick={() => setView('add')}
className="fixed bottom-28 right-6 w-14 h-14 bg-indigo-600 text-white rounded-2xl shadow-xl shadow-indigo-200 flex items-center justify-center hover:scale-110 active:scale-95 transition-all z-40"
>
<Plus size={32} strokeWidth={2.5} />
</button>
</div>
)}
{/* VIEW: ADD PRODUCT */}
{view === 'add' && (
<div className="max-w-md mx-auto space-y-6">
<div className="text-center space-y-2">
<h2 className="text-2xl font-bold text-slate-900">Tambah Produk Baru</h2>
<p className="text-slate-500 text-sm">Bagikan karyamu ke seluruh sekolah</p>
</div>
<form onSubmit={handleAddProduct} className="space-y-5">
{/* Input Nama */}
<div className="space-y-1.5">
<label className="text-sm font-semibold text-slate-700 ml-1 flex items-center gap-2">
<Tag size={16} /> Nama Produk
</label>
<input
type="text"
value={formData.name}
onChange={(e) => setFormData({...formData, name: e.target.value})}
placeholder="Contoh: Pisang Goreng Keju"
className="w-full px-4 py-3 rounded-xl border border-slate-200 focus:border-indigo-500 focus:ring-4 focus:ring-indigo-50/50 outline-none transition-all"
required
/>
</div>
{/* Input Harga */}
<div className="space-y-1.5">
<label className="text-sm font-semibold text-slate-700 ml-1 flex items-center gap-2">
<span className="font-bold">Rp</span> Harga Produk
</label>
<input
type="number"
value={formData.price}
onChange={(e) => setFormData({...formData, price: e.target.value})}
placeholder="Masukkan nominal harga"
className="w-full px-4 py-3 rounded-xl border border-slate-200 focus:border-indigo-500 focus:ring-4 focus:ring-indigo-50/50 outline-none transition-all"
required
/>
</div>
{/* Input Deskripsi */}
<div className="space-y-1.5">
<label className="text-sm font-semibold text-slate-700 ml-1 flex items-center gap-2">
<FileText size={16} /> Deskripsi Produk
</label>
<textarea
rows="4"
value={formData.description}
onChange={(e) => setFormData({...formData, description: e.target.value})}
placeholder="Ceritakan kelebihan produkmu..."
className="w-full px-4 py-3 rounded-xl border border-slate-200 focus:border-indigo-500 focus:ring-4 focus:ring-indigo-50/50 outline-none transition-all resize-none"
required
></textarea>
</div>
{/* Submit Button */}
<button
type="submit"
disabled={loading}
className={`w-full py-4 rounded-2xl bg-indigo-600 text-white font-bold text-lg shadow-lg shadow-indigo-100 hover:bg-indigo-700 active:scale-[0.98] transition-all flex items-center justify-center gap-3 ${loading ? 'opacity-70 cursor-not-allowed' : ''}`}
>
{loading ? (
<>
<Loader2 className="animate-spin" size={20} />
Menyimpan...
</>
) : (
'Simpan Produk'
)}
</button>
<button
type="button"
onClick={() => setView('home')}
className="w-full py-3 rounded-2xl bg-white text-slate-500 font-semibold text-sm hover:text-slate-700 transition-colors"
>
Batal
</button>
</form>
</div>
)}
{/* VIEW: PROFILE */}
{view === 'profile' && (
<div className="space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500">
<div className="flex flex-col items-center text-center space-y-4">
<div className="w-24 h-24 rounded-3xl bg-indigo-100 flex items-center justify-center border-4 border-white shadow-md">
<User size={48} className="text-indigo-600" />
</div>
<div>
<h2 className="text-2xl font-bold text-slate-900">John Siswa</h2>
<p className="text-slate-500">Kelas XII - Teknik Komputer</p>
</div>
</div>
<div className="bg-white rounded-3xl p-2 border border-slate-100 shadow-sm">
<div className="p-4 flex items-center justify-between border-b border-slate-50">
<div className="flex items-center gap-3">
<div className="p-2 bg-blue-50 text-blue-600 rounded-lg"><Package size={20}/></div>
<span className="font-semibold">Produk Saya</span>
</div>
<span className="bg-slate-100 px-3 py-1 rounded-full text-xs font-bold text-slate-600">
{products.length}
</span>
</div>
<div className="p-4 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="p-2 bg-orange-50 text-orange-600 rounded-lg"><Tag size={20}/></div>
<span className="font-semibold">Pengaturan Toko</span>
</div>
<ChevronLeft size={20} className="rotate-180 text-slate-300" />
</div>
</div>
<button className="w-full py-4 text-red-500 font-bold bg-red-50 rounded-2xl hover:bg-red-100 transition-colors">
Keluar Akun
</button>
</div>
)}
</main>
<GlobalSnackbar />
<BottomNav />
</div>
);
};
export default App;
Tidak ada komentar:
Posting Komentar