Selasa, 05 Mei 2026

React - Deploy

 





















Web - HTML Project

 








Android - Room Database

 


Room Database adalah bagian dari Android Jetpack yang menyediakan lapisan abstraksi di atas SQLite. Tujuannya adalah untuk mempermudah pengelolaan database lokal pada aplikasi Android dengan tetap memanfaatkan kekuatan penuh dari SQLite secara lebih aman dan efisien.

Secara sederhana, Room bertindak sebagai perantara antara kode Kotlin/Java Anda dengan database SQLite yang ada di sistem Android.


🧩 3 Komponen Utama Room

Ada tiga komponen penting yang harus ada agar Room bisa bekerja:

1. Entity

Entity adalah representasi dari tabel di dalam database. Anda mendefinisikan sebuah Data Class (di Kotlin) dan menandainya dengan anotasi @Entity. Setiap properti dalam kelas tersebut akan menjadi kolom di tabel database.

  • Anotasi Utama: @PrimaryKey (wajib ada satu), @ColumnInfo (untuk mengubah nama kolom).

2. DAO (Data Access Object)

DAO adalah antarmuka (interface) yang digunakan untuk mendefinisikan metode akses data. Di sinilah Anda menulis query SQL (seperti SELECT, INSERT, DELETE, UPDATE).

  • Kelebihan: Room akan memeriksa syntax SQL Anda saat proses kompilasi (compile-time). Jika ada typo pada nama tabel, aplikasi tidak akan bisa di-build, sehingga mencegah error saat aplikasi dijalankan.

3. Room Database

Ini adalah titik akses utama dan pemegang koneksi ke database. Kelas ini harus berupa kelas abstrak yang mengekstensi RoomDatabase. Di sinilah Anda mendaftarkan Entity dan DAO.


🚀 Mengapa Menggunakan Room? (Kelebihan)

  1. Keamanan Compile-time: Room mengecek query SQL Anda sebelum aplikasi dijalankan. Jika query salah, Room akan memberitahu Anda lewat pesan error saat kompilasi.

  2. Boilerplate Code Berkurang: Anda tidak perlu lagi menulis kode panjang seperti ContentValues atau Cursor secara manual seperti pada SQLite tradisional.

  3. Integrasi dengan Library Lain: Room mendukung penuh Flow (Kotlin) atau LiveData untuk pembaruan data secara real-time (otomatis memperbarui UI saat data di DB berubah).

  4. Migrasi yang Terstruktur: Room menyediakan sistem yang jelas untuk mengubah skema database (misalnya menambah kolom baru) tanpa menghapus data pengguna yang sudah ada.


🛠️ Contoh Implementasi Sederhana

Jika kita menerapkan Room pada aplikasi Marketplace Siswa yang Anda buat sebelumnya, berikut adalah gambaran kodenya:

Step 1: Entity (Tabel Produk)

Kotlin
@Entity(tableName = "produk_table")
data class Produk(
    @PrimaryKey(autoGenerate = true) val id: Int = 0,
    val nama: String,
    val harga: Int,
    val deskripsi: String
)

Step 2: DAO (Akses Data)

Kotlin
@Dao
interface ProdukDao {
    @Query("SELECT * FROM produk_table ORDER BY id DESC")
    fun getAllProduk(): Flow<List<Produk>>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertProduk(produk: Produk)

    @Delete
    suspend fun deleteProduk(produk: Produk)
}

Step 3: Database Instance

Kotlin
@Database(entities = [Produk::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
    abstract fun produkDao(): ProdukDao
}

⚠️ Aturan Penting Room

  • Jangan di Main Thread: Operasi database (Insert, Update, Delete) tidak boleh dilakukan di Main Thread (thread UI) karena dapat menyebabkan aplikasi membeku (ANR - App Not Responding). Gunakan Coroutines (suspend functions) untuk menjalankan operasi di background.

  • Singleton Pattern: Sangat disarankan untuk membuat instance database sebagai Singleton agar tidak memakan banyak memori karena pembuatan koneksi database adalah proses yang "mahal".

Dengan Room, aplikasi Marketplace Anda nantinya tidak akan kehilangan data saat HP dimatikan atau aplikasi ditutup, karena data tersimpan permanen di penyimpanan internal ponsel.


entu, mari kita tingkatkan aplikasi MarketSiswa agar lebih tangguh. Dengan integrasi Room Database, data produk yang ditambahkan siswa akan tersimpan secara permanen di memori ponsel.

Berikut adalah perubahan kode yang terstruktur menjadi 3 bagian utama: Dependensi, Arsitektur Room, dan Update UI.

1. Tambahkan Dependensi

Buka file build.gradle.kts (Module: app) dan tambahkan baris berikut agar fitur Room bisa digunakan:

dependencies {

    val room_version = "2.6.1"

    implementation("androidx.room:room-runtime:$room_version")

    implementation("androidx.room:room-ktx:$room_version")

    kapt("androidx.room:room-compiler:$room_version") // Gunakan 'ksp' jika proyek Anda mendukung KSP

}

2. Implementasi Arsitektur Room

Kita akan mendefinisikan tabel, cara akses data, dan koneksi database.

import androidx.room.*

import kotlinx.coroutines.flow.Flow


// --- ENTITY (Tabel) ---

@Entity(tableName = "products")

data class ProductEntity(

    @PrimaryKey(autoGenerate = true) val id: Long = 0,

    val name: String,

    val price: String,

    val description: String

)


// --- DAO (Data Access Object) ---

@Dao

interface ProductDao {

    @Query("SELECT * FROM products ORDER BY id DESC")

    fun getAllProducts(): Flow<List<ProductEntity>>


    @Insert(onConflict = OnConflictStrategy.REPLACE)

    suspend fun insertProduct(product: ProductEntity)

}


// --- DATABASE INSTANCE ---

@Database(entities = [ProductEntity::class], version = 1)

abstract class AppDatabase : RoomDatabase() {

    abstract fun productDao(): ProductDao


    companion object {

        @Volatile

        private var INSTANCE: AppDatabase? = null


        fun getDatabase(context: android.content.Context): AppDatabase {

            return INSTANCE ?: synchronized(this) {

                val instance = Room.databaseBuilder(

                    context.applicationContext,

                    AppDatabase::class.java,

                    "marketplace_db"

                ).build()

                INSTANCE = instance

                instance

            }

        }

    }

}

3. Update UI (MainActivity.kt)

Sekarang kita hubungkan UI dengan Database menggunakan collectAsState agar daftar produk otomatis terupdate saat ada data baru masuk.

@OptIn(ExperimentalMaterial3Api::class)

@Composable

fun MainScreen() {

    val context = androidx.compose.ui.platform.LocalContext.current

    val db = remember { AppDatabase.getDatabase(context) }

    val productDao = db.productDao()

    

    // Mengambil data dari database secara real-time

    val productList by productDao.getAllProducts().collectAsState(initial = emptyList())

    

    var currentScreen by remember { mutableStateOf("home") }

    val snackbarHostState = remember { SnackbarHostState() }

    val scope = rememberCoroutineScope()


    Scaffold(

        snackbarHost = { SnackbarHost(snackbarHostState) },

        // ... (TopBar & BottomBar sama seperti sebelumnya)

        floatingActionButton = {

            if (currentScreen == "home") {

                ExtendedFloatingActionButton(

                    onClick = { currentScreen = "add" },

                    containerColor = MaterialTheme.colorScheme.primaryContainer

                ) {

                    Icon(Icons.Default.Add, contentDescription = null)

                    Text("Jual")

                }

            }

        }

    ) { innerPadding ->

        Box(modifier = Modifier.padding(innerPadding)) {

            when (currentScreen) {

                "home" -> HomeScreen(productList)

                "add" -> AddProductScreen(

                    onProductAdded = { name, price, desc ->

                        scope.launch {

                            // Simpan ke database

                            productDao.insertProduct(

                                ProductEntity(name = name, price = price, description = desc)

                            )

                            currentScreen = "home"

                            snackbarHostState.showSnackbar("Produk tersimpan permanen!")

                        }

                    }

                )

                "profile" -> ProfileScreen()

            }

        }

    }

}


@Composable

fun HomeScreen(products: List<ProductEntity>) {

    LazyColumn(

        modifier = Modifier.fillMaxSize(),

        contentPadding = PaddingValues(16.dp),

        verticalArrangement = Arrangement.spacedBy(12.dp)

    ) {

        item {

            Text("Katalog Siswa", fontSize = 24.sp, fontWeight = FontWeight.ExtraBold)

            Spacer(Modifier.height(8.dp))

        }

        items(products) { product ->

            Card(

                modifier = Modifier.fillMaxWidth(),

                shape = RoundedCornerShape(16.dp)

            ) {

                Column(modifier = Modifier.padding(16.dp)) {

                    Text(product.name, fontWeight = FontWeight.Bold, fontSize = 18.sp)

                    Text("Rp ${product.price}", color = MaterialTheme.colorScheme.primary)

                    Text(product.description, fontSize = 14.sp, color = Color.Gray)

                }

            }

        }

    }

}


@Composable

fun AddProductScreen(onProductAdded: (String, String, String) -> Unit) {

    var name by remember { mutableStateOf("") }

    var price by remember { mutableStateOf("") }

    var desc by remember { mutableStateOf("") }

    var isLoading by remember { mutableStateOf(false) }

    val scope = rememberCoroutineScope()


    Column(modifier = Modifier.padding(24.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {

        OutlinedTextField(value = name, onValueChange = { name = it }, label = { Text("Nama") }, modifier = Modifier.fillMaxWidth())

        OutlinedTextField(value = price, onValueChange = { price = it }, label = { Text("Harga") }, modifier = Modifier.fillMaxWidth())

        OutlinedTextField(value = desc, onValueChange = { desc = it }, label = { Text("Deskripsi") }, modifier = Modifier.fillMaxWidth())


        Button(

            onClick = {

                isLoading = true

                scope.launch {

                    delay(800) // Simulasi UX loading

                    onProductAdded(name, price, desc)

                    isLoading = false

                }

            },

            modifier = Modifier.fillMaxWidth(),

            enabled = name.isNotBlank() && !isLoading

        ) {

            if (isLoading) CircularProgressIndicator(size = 20.dp) else Text("Simpan")

        }

    }

}


Apa yang berubah?

  1. Penggunaan Flow: Di ProductDao, kita menggunakan Flow<List<ProductEntity>>. Ini membuat UI "berlangganan" pada database. Begitu data di DB berubah, UI otomatis update tanpa perlu refresh manual.

  2. Coroutine Scope: Proses simpan (insertProduct) ditandai sebagai suspend, jadi kita menjalankannya di dalam scope.launch agar tidak mengganggu kelancaran UI.

  3. Data Persisten: Sekarang, silakan coba tambah produk, lalu tutup aplikasi sepenuhnya (bahkan hapus dari Recent Apps). Saat dibuka kembali, produk Anda akan tetap ada di sana!

Apakah Anda ingin menambahkan fitur Hapus Produk (Delete) dengan cara menggeser (swipe) kartu produk?</List


https://gemini.google.com/app/56c46549a5bc1461

React - Deploy Aplikasi

 


Panduan Deploy Aplikasi Marketplace Siswa

Dokumen ini menjelaskan langkah-langkah untuk mempublikasikan (deploy) aplikasi React Anda ke internet agar dapat diakses secara publik.

1. Persiapan Awal

Pastikan Anda sudah memiliki lingkungan pengembangan lokal yang siap:

  • Node.js terinstall di komputer.

  • Git terinstall untuk mengelola kode.

  • Akun di GitHub, Vercel, atau Netlify.

2. Membuat Proyek React Lokal

Jika Anda belum memiliki struktur proyek yang lengkap, gunakan Vite (rekomendasi terbaru):

  1. Buka terminal/CMD dan jalankan:

    npm create vite@latest marketplace-siswa -- --template react
    cd marketplace-siswa
    npm install
    
  2. Install dependensi yang dibutuhkan oleh aplikasi:

    npm install lucide-react
    npm install -D tailwindcss border-color postcss autoprefixer
    npx tailwindcss init -p
    
  3. Salin kode dari file marketplace_siswa.jsx ke dalam src/App.jsx di proyek baru Anda.

3. Konfigurasi Tailwind CSS

Agar tampilan sesuai, pastikan file tailwind.config.js Anda diatur seperti ini:

/** @type {import('tailwindcss').Config} */
export default {
  content: [
    "./index.html",
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

Dan tambahkan direktif Tailwind di src/index.css:

@tailwind base;
@tailwind components;
@tailwind utilities;

4. Opsi Deployment

Opsi A: Menggunakan Vercel (Paling Mudah)

  1. Buat repositori baru di GitHub dan unggah (push) kode Anda ke sana.

  2. Buka Vercel.

  3. Klik "Add New" > "Project".

  4. Hubungkan akun GitHub Anda dan pilih repositori marketplace-siswa.

  5. Klik "Deploy". Vercel akan mendeteksi Vite secara otomatis.

Opsi B: Menggunakan Netlify

  1. Buka Netlify.

  2. Login dan pilih "Add new site" > "Import from existing project".

  3. Pilih GitHub dan tentukan repositorinya.

  4. Pastikan Build Command adalah npm run build dan Publish directory adalah dist.

  5. Klik "Deploy site".

Opsi C: Drag & Drop (Tanpa GitHub)

  1. Jalankan perintah build di terminal lokal Anda:

    npm run build
    
  2. Folder bernama dist akan muncul di proyek Anda.

  3. Buka Netlify, masuk ke menu Sites, lalu tarik dan lepas (drag & drop) folder dist tersebut ke area yang disediakan.

5. Tips Tambahan

  • Custom Domain: Baik Vercel maupun Netlify memberikan subdomain gratis (contoh: marketplace-siswa.vercel.app). Anda bisa menghubungkan domain .com atau .id milik sendiri di menu pengaturan.

  • Update Kode: Jika Anda menggunakan metode GitHub, setiap kali Anda melakukan git push, situs Anda akan diperbarui secara otomatis.

Dibuat untuk mempermudah siswa dalam mempublikasikan karya mereka.

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;

React - Masterclass

  https://www.youtube.com/watch?v=MHn66JJH5zs&list=PLSsAz5wf2lkJhEVN7fG8DfnQInknICI9K&index=1 https://www.youtube.com/watch?v=yOIO5h...