Minggu, 10 Mei 2026

Android - Task Manager

 


Untuk membangun aplikasi Simple Task Manager menggunakan Jetpack Compose. Struktur ini mengikuti standar profesional dengan memisahkan data, komponen, dan layar utama.


1. Struktur File Proyek

Buatlah folder (package) di dalam com.example.taskapps sehingga strukturnya menjadi seperti ini:

  • data/

    • Task.kt (Model data)

  • ui/components/

    • TaskItem.kt (Komponen kartu tugas)

  • ui/screens/

    • TaskListScreen.kt (Tampilan utama daftar tugas)

  • MainActivity.kt (Titik masuk aplikasi)


2. Implementasi Kode

A. Model Data (data/Task.kt)

Kotlin
package com.example.taskapps.data

data class Task(
    val id: Int,
    val title: String,
    val description: String
)

B. Komponen UI (ui/components/TaskItem.kt)

Kotlin
package com.example.taskapps.ui.components

import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.example.taskapps.data.Task

@Composable
fun TaskItem(task: Task) {
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .padding(vertical = 4.dp, horizontal = 8.dp),
        elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
    ) {
        Column(modifier = Modifier.padding(16.dp)) {
            Text(text = task.title, style = MaterialTheme.typography.titleMedium)
            Text(text = task.description, style = MaterialTheme.typography.bodySmall)
        }
    }
}

C. Layar Utama (ui/screens/TaskListScreen.kt)

Kotlin
package com.example.taskapps.ui.screens

import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.example.taskapps.data.Task
import com.example.taskapps.ui.components.TaskItem

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TaskListScreen() {
    // Data dummy untuk ditampilkan
    val tasks = listOf(
        Task(1, "Install Android Studio", "Versi Ladybug 2024"),
        Task(2, "Belajar Kotlin", "Memahami data class dan fungsi"),
        Task(3, "Implementasi Compose", "Membuat UI deklaratif")
    )

    Scaffold(
        topBar = {
            TopAppBar(title = { Text("Task Manager") })
        }
    ) { paddingValues ->
        LazyColumn(modifier = Modifier.padding(paddingValues)) {
            items(tasks) { task ->
                TaskItem(task = task)
            }
        }
    }
}

D. Titik Masuk Utama (MainActivity.kt)

Kotlin
package com.example.taskapps

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import com.example.taskapps.ui.screens.TaskListScreen
import com.example.taskapps.ui.theme.TaskAppsTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            // Membungkus aplikasi dengan tema bawaan
            TaskAppsTheme { 
                TaskListScreen() 
            }
        }
    }
}

3. Ringkasan Langkah Menjalankan (Running)

  1. Sync Gradle: Klik ikon gajah di pojok kanan atas untuk memastikan semua dependencies terunduh.

  2. Pilih Device: Hubungkan HP Android (USB Debugging aktif) atau pilih Emulator di toolbar atas.

  3. Klik Run (▶️): Tunggu proses Gradle Build selesai.

  4. Verifikasi: Aplikasi akan terinstal otomatis di perangkat dan menampilkan daftar tugas yang sudah kita buat di data dummy.


Tips Pengembangan Selanjutnya:

  • State Management: Gunakan remember { mutableStateListOf(...) } jika ingin daftar tugas bisa ditambah atau dihapus oleh pengguna secara real-time.

  • Navigation: Tambahkan Jetpack Navigation jika ingin berpindah ke halaman "Detail Tugas" saat sebuah item di-klik.

      
Menuliskan Kode

Cara Membuatnya di Android Studio

  1. Buka panel "Project" di sebelah kiri.

  2. Cari folder java (atau kotlin+java) > com.example.taskapps.

  3. Membuat Folder data:

    • Klik kanan pada com.example.taskapps.

    • Pilih New > Package.

    • Ketik data lalu tekan Enter.

  4. Membuat Folder ui dan sub-folder:

    • Klik kanan pada com.example.taskapps.

    • Pilih New > Package.

    • Ketik ui.components (ini akan otomatis membuat folder ui dan folder components di dalamnya).

    • Ulangi lagi: Klik kanan pada com.example.taskapps, pilih New > Package, ketik ui.screens.


Cara Mengisi File ke Dalam Folder

Setelah folder siap, sekarang buat file kodenya:

Lokasi FolderCara Buat FileNama File
dataKlik kanan folder > New > Kotlin Class/FileTask (Pilih Class)
ui.componentsKlik kanan folder > New > Kotlin Class/FileTaskItem (Pilih File)
ui.screensKlik kanan folder > New > Kotlin Class/FileTaskListScreen (Pilih File)

Tips Penting saat Copy-Paste Kode:

Pastikan baris paling atas di setiap file (package ...) sesuai dengan lokasinya.

  • Di file Task.kt, baris pertamanya harus:

    package com.example.taskapps.data

  • Di file TaskItem.kt, baris pertamanya harus:

    package com.example.taskapps.ui.components

  • Di file TaskListScreen.kt, baris pertamanya harus:

    package com.example.taskapps.ui.screens

Jika ada tanda lampu merah (error) setelah Anda membuat folder ini, biasanya itu karena masalah import. Cukup arahkan kursor ke kode yang merah, lalu tekan Alt + Enter untuk memperbaikinya secara otomatis.

Laravel - Struktur File

 




Arsitektur MVC (Model-View-Controller) berdasarkan studi kasus Task Manager yang baru saja Anda buat. Memahami struktur ini adalah kunci untuk menjadi pengembang Laravel yang mahir.


1. Konsep MVC dalam Studi Kasus Anda

Arsitektur MVC membagi aplikasi menjadi tiga komponen utama agar kode lebih terorganisir dan mudah dikelola.

A. Model (Data & Logika)

  • File: app/Models/Task.php

  • Kegunaan: Menghubungkan aplikasi Anda dengan tabel tasks di database. Model bertugas mengambil data, menyimpan data baru, atau menghapus data.

  • Dalam Studi Kasus: Saat Anda menulis Task::all(), Model inilah yang bekerja mengambil semua baris data dari MySQL.

B. View (Tampilan/UI)

  • File: resources/views/tasks/index.blade.php

  • Kegunaan: Berisi kode HTML dan CSS. View hanya bertugas menampilkan data yang dikirimkan kepadanya. View tidak boleh tahu bagaimana cara mengambil data dari database.

  • Dalam Studi Kasus: View menerima daftar tugas dan menampilkannya dalam bentuk list menggunakan Tailwind CSS.

C. Controller (Otak/Penghubung)

  • File: app/Http/Controllers/TaskController.php

  • Kegunaan: Menjadi perantara. Controller menerima permintaan dari user (lewat Route), meminta data ke Model, lalu mengirimkan data tersebut ke View.

  • Dalam Studi Kasus: Saat Anda membuka URL /tasks, Controller diperintahkan untuk mengambil data tugas dari Model dan menyajikannya ke View index.


2. Struktur File Proyek (Folder yang Kita Gunakan)

Berikut adalah ringkasan folder-folder utama yang terlibat dalam pembuatan aplikasi Task Manager Anda:

3. Alur Kerja (The Flow)

Mari kita simulasikan apa yang terjadi saat Anda me-refresh browser:

  1. Request: Browser meminta halaman [http://127.0.0.1:8000/tasks](http://127.0.0.1:8000/tasks).

  2. Route: File routes/web.php melihat permintaan tersebut dan mengarahkannya ke TaskController fungsi index.

  3. Controller: TaskController menjalankan perintah Task::all().

  4. Model: Model Task mengambil data dari tabel MySQL.

  5. Controller: Setelah dapat datanya, Controller memanggil View tasks.index sambil membawa data $tasks.

  6. View: File index.blade.php merender HTML dan menampilkan data tugas yang dibawa tadi.

  7. Response: Browser menampilkan halaman web yang sudah cantik.


Mengapa Harus Pakai Struktur Ini?

  • Rapi: Jika ada kesalahan tampilan, Anda tahu harus buka folder views. Jika ada kesalahan data, Anda buka folder Models.

  • Skalabilitas: Jika nanti aplikasi Anda bertambah besar (misal: ada fitur User, Kategori, atau Komentar), Anda tinggal menambah Model, View, dan Controller baru tanpa mengganggu kode yang lama.


PERINTAH COMPOSER 


Berikut adalah urutan perintah lengkap yang Anda gunakan, mulai dari proses instalasi hingga aplikasi berhasil berjalan di browser:


1. Tahap Instalasi (Composer)

Perintah ini digunakan untuk mengunduh framework Laravel dan semua pustaka pendukungnya.

  • composer create-project laravel/laravel:^10.0 pelatihan2

    • Kegunaan: Membuat folder proyek baru bernama pelatihan2 dengan Laravel versi 10.


2. Tahap Navigasi Folder

Sebelum menjalankan perintah selanjutnya, Anda harus masuk ke dalam folder proyek yang baru dibuat.

  • cd pelatihan2

    • Kegunaan: Pindah posisi terminal ke dalam direktori proyek. Jika ini terlewat, perintah artisan tidak akan bisa dijalankan.


3. Tahap Pembuatan Struktur MVC (Artisan)

Perintah ini digunakan untuk membuat komponen-komponen utama aplikasi secara otomatis.

  • php artisan make:model Task -m

    • Kegunaan: Membuat file Model (Task.php) sekaligus file Migration (-m) untuk membuat tabel di database.

  • php artisan make:controller TaskController

    • Kegunaan: Membuat file Controller (TaskController.php) untuk mengatur logika aplikasi.


4. Tahap Database & Migrasi

Setelah mengatur koneksi database di file .env, gunakan perintah ini untuk mengirim struktur tabel ke MySQL.

  • php artisan migrate

    • Kegunaan: Menjalankan instruksi migrasi (membuat tabel tasks dan tabel bawaan Laravel lainnya di database).

  • php artisan migrate:rollback (Opsional)

    • Kegunaan: Membatalkan migrasi terakhir jika ada kesalahan pada struktur tabel.


5. Tahap Uji Coba Data (Tinker)

Digunakan untuk memasukkan data ke database tanpa melalui form web.

  • php artisan tinker

    • Kegunaan: Membuka konsol interaktif PHP untuk berinteraksi dengan database.

    • Perintah di dalam Tinker: App\Models\Task::create(['title' => 'Tugas Baru', 'is_completed' => 0]);


6. Tahap Pembersihan Cache (Troubleshooting)

Sering digunakan jika Anda mengubah file .env atau Route tetapi perubahannya tidak muncul.

  • php artisan config:clear

    • Kegunaan: Menghapus cache konfigurasi.

  • php artisan route:clear

    • Kegunaan: Menghapus cache rute URL.


7. Tahap Menjalankan Aplikasi

Langkah terakhir untuk melihat hasil pekerjaan Anda di browser.

  • php artisan serve

    • Kegunaan: Menjalankan server pengembangan lokal. Setelah ini jalan, Anda bisa membuka [http://127.0.0.1:8000](http://127.0.0.1:8000) di browser.


Ringkasan Urutan Cepat:

  1. composer create-project... (Instal)

  2. cd nama-proyek (Masuk folder)

  3. php artisan make:model... (Buat struktur)

  4. php artisan migrate (Setup database)

  5. php artisan serve (Jalankan)


PERINTAH DATABASE


Berikut adalah urutan perintah dan langkah-langkah pengelolaan database, mulai dari pembuatan di phpMyAdmin hingga proses pengisian data (Insert) di Laravel:


1. Tahap di phpMyAdmin (Persiapan Awal)

Langkah ini dilakukan secara visual melalui browser untuk menyiapkan "rumah" bagi data Anda.

  • Buka URL: localhost/phpmyadmin.

  • Buat Database: Klik menu New, ketik nama database (misal: pelatihan2), lalu klik Create.

  • Pengaturan Koneksi: Di proyek Laravel, buka file .env dan sesuaikan:

    Cuplikan kode
    DB_DATABASE=pelatihan2
    DB_USERNAME=root
    DB_PASSWORD=
    

2. Tahap Migration (Membuat Struktur Tabel)

Setelah database dibuat, Anda tidak perlu membuat tabel secara manual di phpMyAdmin. Gunakan perintah Artisan agar struktur tabel tercatat di kode program.

  • php artisan make:model Task -m

    • Perintah ini membuat Model sekaligus file Migration.

  • Edit File Migration: Buka file di folder database/migrations/xxxx_create_tasks_table.php dan tambahkan kolom:

PHP
    $table->string('title');
    $table->boolean('is_completed')->default(false);
    ```
*   **`php artisan migrate`**
    *   **Kegunaan:** Menjalankan perintah SQL secara otomatis untuk membuat tabel `tasks` ke dalam database `pelatihan2`.

http://googleusercontent.com/image_content/211



---

## 3. Tahap Insert Data (Memasukkan Data)
Ada tiga cara utama untuk memasukkan data ke dalam tabel yang sudah dibuat:

### A. Melalui Tinker (Uji Coba Cepat)
Paling sering digunakan pengembang untuk mengetes apakah Model sudah berfungsi.
1.  Ketik: **`php artisan tinker`**
2.  Ketik perintah Eloquent:
    
```php
    App\Models\Task::create(['title' => 'Belajar Database', 'is_completed' => 0]);
    ```

### B. Melalui Database Seeder (Data Massal/Default)
Digunakan jika Anda ingin mengisi database dengan data awal (dummy) secara otomatis.
1.  **`php artisan make:seeder TaskSeeder`**
2.  Isi file di `database/seeders/TaskSeeder.php` dengan data.
3.  **`php artisan db:seed --class=TaskSeeder`**

### C. Melalui phpMyAdmin (Manual)
Jika ingin memasukkan data secara visual tanpa kode:
1.  Pilih tabel `tasks`.
2.  Klik tab **Insert** di bagian atas.
3.  Isi kolom `title` dan `is_completed`.
4.  Klik **Go**.

---

## Ringkasan Logika Database Laravel



| Langkah | Perintah / Alat | Output |
| :--- | :--- | :--- |
| **Persiapan** | phpMyAdmin | Database Kosong |
| **Rencana Tabel** | `make:model -m` | File Migration (Blueprints) |
| **Eksekusi Tabel** | `migrate` | Tabel nyata di MySQL |
| **Isi Data** | `tinker` / `db:seed` | Data masuk ke Baris Tabel |

Dengan mengikuti urutan ini, database Anda akan terstruktur dengan rapi dan sinkron dengan kode Laravel Anda.


https://git-scm.com/install/windows
https://sourceforge.net/projects/xampp/
https://www.apachefriends.org/download_success.html
https://getcomposer.org/download/
http://localhost/phpmyadmin/index.php?route=/table/change&db=pelatihan2&table=tasks
http://127.0.0.1:8000/tasks

Sabtu, 09 Mei 2026

React - Latihan 2 Hosting GitHub

 



Hosting di GitHub (melalui GitHub Pages) adalah cara gratis dan populer untuk mempublikasikan aplikasi React Anda. Karena Anda menggunakan Vite, cara terbaik adalah menggunakan GitHub Actions untuk melakukan build dan deploy secara otomatis setiap kali Anda melakukan push kode.

Berikut adalah panduan langkah demi langkahnya:

1. Update Konfigurasi Vite

Anda harus memastikan path file CSS dan JS bersifat relatif agar tidak error saat dibuka di URL GitHub (yang biasanya memiliki format username.github.io/nama-repo/).

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  base: './', // Sangat penting agar file /assets/ bisa terbaca di GitHub Pages
})

2. Buat Workflow Deployment

Kita akan membuat file instruksi agar GitHub melakukan build (npm run build) secara otomatis di server mereka.

  1. Di dalam folder project Anda, buat folder baru bernama .github.

  2. Di dalam folder .github, buat folder bernama workflows.

  3. Di dalam folder workflows, buat file bernama deploy.yml.

STREAMING_CHUNK: Defining the deployment workflow...

name: Deploy to GitHub Pages

on: push: branches: ["main"] # Pastikan branch utama Anda bernama 'main'

permissions: contents: read pages: write id-token: write

jobs: build_site: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4

  - name: Install Node.js
    uses: actions/setup-node@v4
    with:
      node-version: 20
      cache: 'npm'

  /* STREAMING_CHUNK: Installing dependencies and building project... */
  - name: Install dependencies
    run: npm install

  - name: Build
    run: npm run build

  - name: Upload Artifacts
    uses: actions/upload-pages-artifact@v3
    with:
      path: "./dist"

deploy: needs: build_site runs-on: ubuntu-latest environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} steps: - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4

3. Upload ke GitHub (Terminal)

Jalankan perintah ini di terminal VS Code Anda untuk mengirimkan kode ke GitHub:

# Inisialisasi Git jika belum
git init
git add .
git commit -m "Initial commit & add deployment workflow"

# Hubungkan ke repository Anda (Ganti username dan repo-name)
git remote add origin https://github.com/USERNAME_ANDA/NAMA_REPO_ANDA.git
git branch -M main
git push -u origin main

4. Aktifkan GitHub Pages di Website

Setelah push selesai, lakukan langkah terakhir ini di browser:

  1. Buka repository Anda di GitHub.

  2. Klik tab Settings (Pengaturan).

  3. Di sidebar kiri, klik Pages.

  4. Di bagian Build and deployment > Source, pilih GitHub Actions.

  5. Tunggu beberapa menit. Anda bisa melihat progresnya di tab Actions. Jika sudah centang hijau, URL website Anda akan muncul di bagian atas halaman Settings > Pages.

Summary:

  • Vite Config: Menentukan base path agar file .js dan .css tidak hilang.

  • deploy.yml: Robot otomatis yang melakukan npm install dan npm run build di server GitHub.

  • GitHub Actions: Menampilkan hasil folder dist secara online.



Vibe Coding

 




React - Latihan 1

 






PROMPT

Buatkan aplikasi web dengan menggunakan jsx dan google apps script.
Url spreadsheet "https://docs.google.com/spreadsheets/d/16cBgEFpZKVXdwIL6wCUVXmwOVSkLKefyHxAPPO46Mg0/edit?gid=0#gid=0"
Url Folder Foto "https://drive.google.com/drive/folders/1i2WQ9ri469qfagNW3ceyjpM56HYSM-Mm?hl=ID"

Untuk Backend (Google Apps Script)
Context: Saya ingin membuat backend menggunakan Google Apps Script yang berfungsi sebagai API untuk aplikasi manajemen pelatihan. Database menggunakan Google Sheets.

Requirements:

Buat fungsi setupDatabase, doPost(e) dan doGet(e) untuk menangani operasi CRUD (Create, Read, Update, Delete).

Struktur Sheet:

Sheet "Pelatihan": ID, Nama Pelatihan, Pemateri, Tanggal, Kuota, Status.

Sheet "Peserta": ID Peserta, ID Pelatihan, Nama Peserta, Email, Institusi,Foto.

Fitur Keamanan & Teknis:

Gunakan ContentService.createTextOutput() dengan JSON stringify.

Gunakan format URL gambar lh3.googleusercontent.com jika ada upload file.

Implementasikan sistem ID unik (UUID atau Timestamp) untuk setiap entri baru.

Pastikan script menangani pengiriman data dari frontend yang menggunakan mode no-cors atau berikan header akses jika diperlukan.

Kembalikan respons sukses/error yang jelas dalam format JSON.

Untuk Frontend gunakan React & Tailwind CSS
Context: Saya ingin membangun UI untuk Aplikasi Manajemen Pelatihan menggunakan React dan Tailwind CSS dengan konsep desain modern dan minimalis.

Requirements:

Routing & Struktur:

Gunakan react-router-dom.

Halaman Utama (Dashboard): Statistik ringkas (Total Pelatihan, Total Peserta).

Halaman Daftar Pelatihan: Tabel/Card yang menampilkan daftar pelatihan dengan filter status.

Halaman Admin: Form untuk menambah/edit pelatihan dan daftar peserta per pelatihan (pisahkan jika perlu menjadi admin.html atau rute terproteksi).

Integrasi API:

Gunakan fetch API untuk berkomunikasi dengan URL Google Apps Script.

Implementasikan mode: 'no-cors' pada request POST untuk menghindari masalah kebijakan Google.

Buat fungsi uploadFile khusus untuk mengirim file/foto ke Google Drive melalui Apps Script.

UI/UX:

Gunakan palet warna profesional (contoh: Biru Navy dan Putih).

Gunakan Lucide React untuk ikon.

Pastikan form input memiliki validasi sederhana dan loading state saat mengirim data.

Gunakan sistem SPA (Single Page Application) yang ringan.

Panduan Membuat Project React

Ikuti langkah-langkah ini untuk menjalankan aplikasi Manajemen Pelatihan Anda.

1. Persiapan Awal

Pastikan Anda sudah menginstal Node.js di komputer Anda.

  • Cek dengan cara buka Terminal/Command Prompt dan ketik: node -v

  • Jika belum punya, download di nodejs.org.

2. Membuat Project Baru

Buka Terminal di folder tempat Anda ingin menyimpan project, lalu jalankan perintah:

npm create vite@latest e-latih -- --template react

(Ganti e-latih dengan nama project yang Anda inginkan)

3. Masuk ke Folder dan Buka VS Code

Jalankan perintah berikut secara berurutan:

cd e-latih
code .

Sekarang VS Code akan terbuka otomatis di folder project tersebut.

4. Instalasi Library (Dependency)

Di dalam terminal VS Code (Tekan Ctrl + ~), jalankan perintah untuk menginstal library dasar:

npm install

Kemudian, instal library tambahan yang dibutuhkan oleh kode kita (Lucide React):

npm install lucide-react

5. Instalasi Tailwind CSS

Karena kode aplikasi menggunakan Tailwind, kita harus menginstalnya terlebih dahulu:

npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

Buka file tailwind.config.js dan ubah bagian content menjadi:

content: [
  "./index.html",
  "./src/**/*.{js,ts,jsx,tsx}",
],

Buka file src/index.css dan ganti isinya dengan:

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

6. Memasukkan Kode Aplikasi

  1. Buka file src/App.jsx.

  2. Hapus semua isinya.

  3. Salin dan tempel (Paste) seluruh kode Aplikasi Manajemen Pelatihan yang saya berikan sebelumnya ke dalam file tersebut.

  4. Jangan lupa: Ganti variabel API_URL dengan URL Web App dari Google Apps Script Anda.

7. Menjalankan Aplikasi

Kembali ke terminal VS Code, jalankan perintah:

npm run dev

Klik link (biasanya http://localhost:5173) yang muncul di terminal untuk membuka aplikasi di browser Anda.

Back End - Apps Script

/**
 * Konfigurasi Global
 */
const SPREADSHEET_ID = '16cBgEFpZKVXdwIL6wCUVXmwOVSkLKefyHxAPPO46Mg0';
const FOLDER_ID = '1i2WQ9ri469qfagNW3ceyjpM56HYSM-Mm'; // Diambil dari URL folder Anda

/**
 * Setup Database: Membuat sheet jika belum ada
 */
function setupDatabase() {
  const ss = SpreadsheetApp.openById(SPREADSHEET_ID);
 
  if (!ss.getSheetByName("Pelatihan")) {
    ss.insertSheet("Pelatihan").appendRow(["ID", "Nama Pelatihan", "Pemateri", "Tanggal", "Kuota", "Status"]);
  }
 
  if (!ss.getSheetByName("Peserta")) {
    ss.insertSheet("Peserta").appendRow(["ID Peserta", "ID Pelatihan", "Nama Peserta", "Email", "Institusi", "Foto"]);
  }
 
  return "Database Berhasil Disiapkan!";
}

/**
 * Menangani request GET (Read Data)
 */
function doGet(e) {
  const action = e.parameter.action;
  const ss = SpreadsheetApp.openById(SPREADSHEET_ID);
 
  try {
    let data;
    if (action === 'getPelatihan') {
      const sheet = ss.getSheetByName("Pelatihan");
      const values = sheet.getDataRange().getValues();
      const headers = values[0];
      data = values.slice(1).map(row => {
        let obj = {};
        headers.forEach((h, i) => obj[h] = row[i]);
        return obj;
      });
    } else if (action === 'getPeserta') {
      const sheet = ss.getSheetByName("Peserta");
      const values = sheet.getDataRange().getValues();
      const headers = values[0];
      data = values.slice(1).map(row => {
        let obj = {};
        headers.forEach((h, i) => obj[h] = row[i]);
        return obj;
      });
    }

    return ContentService.createTextOutput(JSON.stringify({ status: 'success', data: data }))
      .setMimeType(ContentService.MimeType.JSON);
  } catch (error) {
    return ContentService.createTextOutput(JSON.stringify({ status: 'error', message: error.toString() }))
      .setMimeType(ContentService.MimeType.JSON);
  }
}

/**
 * Menangani request POST (Create, Update, Delete)
 */
function doPost(e) {
  const ss = SpreadsheetApp.openById(SPREADSHEET_ID);
  const body = JSON.parse(e.postData.contents);
  const action = body.action;

  try {
    if (action === 'addPelatihan') {
      const sheet = ss.getSheetByName("Pelatihan");
      const id = "TRN-" + new Date().getTime();
      sheet.appendRow([id, body.nama, body.pemateri, body.tanggal, body.kuota, body.status]);
      return createResponse({ status: 'success', id: id });
    }
   
    else if (action === 'addPeserta') {
      const sheet = ss.getSheetByName("Peserta");
      const id = "USR-" + new Date().getTime();
      let photoUrl = "";
     
      if (body.fotoBase64) {
        photoUrl = uploadToDrive(body.fotoBase64, body.fotoName, id);
      }
     
      sheet.appendRow([id, body.idPelatihan, body.nama, body.email, body.institusi, photoUrl]);
      return createResponse({ status: 'success', id: id });
    }

    else if (action === 'deletePelatihan') {
      const sheet = ss.getSheetByName("Pelatihan");
      const data = sheet.getDataRange().getValues();
      for (let i = 1; i < data.length; i++) {
        if (data[i][0] === body.id) {
          sheet.deleteRow(i + 1);
          break;
        }
      }
      return createResponse({ status: 'success' });
    }

  } catch (error) {
    return createResponse({ status: 'error', message: error.toString() });
  }
}

/**
 * Helper untuk upload file ke Google Drive
 */
function uploadToDrive(base64Data, fileName, userId) {
  const folder = DriveApp.getFolderById(FOLDER_ID);
  const contentType = base64Data.substring(5, base64Data.indexOf(';'));
  const bytes = Utilities.base64Decode(base64Data.split(',')[1]);
  const blob = Utilities.newBlob(bytes, contentType, fileName || `foto_${userId}.png`);
  const file = folder.createFile(blob);
  file.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);
 
  // Mengembalikan format URL yang bisa langsung ditampilkan
  return `https://lh3.googleusercontent.com/d/${file.getId()}`;
}

/**
 * Helper untuk response JSON
 */
function createResponse(payload) {
  return ContentService.createTextOutput(JSON.stringify(payload))
    .setMimeType(ContentService.MimeType.JSON);
}

Front End

import React, { useState, useEffect } from 'react';
import {
  LayoutDashboard,
  Users,
  PlusCircle,
  BookOpen,
  Trash2,
  Camera,
  Loader2,
  ChevronRight,
  Filter,
  CheckCircle2,
  XCircle
} from 'lucide-react';

// KONFIGURASI API - Masukkan URL Web App dari Apps Script Anda di sini
const API_URL = "MASUKKAN_URL_WEB_APP_ANDA_DI_SINI";

const App = () => {
  const [currentView, setCurrentView] = useState('dashboard');
  const [loading, setLoading] = useState(false);
  const [pelatihan, setPelatihan] = useState([]);
  const [peserta, setPeserta] = useState([]);
  const [statusFilter, setStatusFilter] = useState('Semua');

  // Fetch data awal
  useEffect(() => {
    fetchData();
  }, []);

  const fetchData = async () => {
    setLoading(true);
    try {
      const resPelatihan = await fetch(`${API_URL}?action=getPelatihan`);
      const dataPelatihan = await resPelatihan.json();
      if(dataPelatihan.status === 'success') setPelatihan(dataPelatihan.data);

      const resPeserta = await fetch(`${API_URL}?action=getPeserta`);
      const dataPeserta = await resPeserta.json();
      if(dataPeserta.status === 'success') setPeserta(dataPeserta.data);
    } catch (err) {
      console.error("Gagal mengambil data", err);
    } finally {
      setLoading(false);
    }
  };

  // Komponen Navigasi
  const NavItem = ({ id, icon: Icon, label }) => (
    <button
      onClick={() => setCurrentView(id)}
      className={`flex items-center space-x-3 w-full p-3 rounded-lg transition-all ${
        currentView === id ? 'bg-navy-700 text-white shadow-lg' : 'text-slate-400 hover:bg-slate-800'
      }`}
    >
      <Icon size={20} />
      <span className="font-medium">{label}</span>
    </button>
  );

  return (
    <div className="flex h-screen bg-slate-50 text-slate-900 font-sans">
      {/* Sidebar */}
      <aside className="w-64 bg-slate-900 text-white p-6 hidden md:block">
        <div className="flex items-center space-x-3 mb-10 px-2">
          <div className="bg-blue-500 p-2 rounded-lg">
            <BookOpen size={24} className="text-white" />
          </div>
          <h1 className="text-xl font-bold tracking-tight">E-Latih</h1>
        </div>
       
        <nav className="space-y-2">
          <NavItem id="dashboard" icon={LayoutDashboard} label="Dashboard" />
          <NavItem id="pelatihan" icon={BookOpen} label="Daftar Pelatihan" />
          <NavItem id="admin" icon={PlusCircle} label="Manajemen Admin" />
        </nav>
      </aside>

      {/* Main Content */}
      <main className="flex-1 overflow-y-auto">
        <header className="bg-white border-b border-slate-200 p-4 sticky top-0 z-10 flex justify-between items-center">
          <h2 className="text-lg font-semibold capitalize">{currentView.replace('-', ' ')}</h2>
          <div className="flex items-center space-x-4">
            {loading && <Loader2 className="animate-spin text-blue-600" size={20} />}
            <div className="h-8 w-8 rounded-full bg-slate-200"></div>
          </div>
        </header>

        <div className="p-6 max-w-6xl mx-auto">
          {currentView === 'dashboard' && (
            <DashboardStats pelatihan={pelatihan} peserta={peserta} />
          )}
          {currentView === 'pelatihan' && (
            <PelatihanList
              data={pelatihan}
              filter={statusFilter}
              setFilter={setStatusFilter}
            />
          )}
          {currentView === 'admin' && (
            <AdminPanel
              pelatihan={pelatihan}
              peserta={peserta}
              onRefresh={fetchData}
            />
          )}
        </div>
      </main>

      {/* Mobile Nav */}
      <div className="md:hidden fixed bottom-0 left-0 right-0 bg-white border-t border-slate-200 flex justify-around p-3">
        <button onClick={() => setCurrentView('dashboard')} className={currentView === 'dashboard' ? 'text-blue-600' : 'text-slate-400'}>
          <LayoutDashboard size={24} />
        </button>
        <button onClick={() => setCurrentView('pelatihan')} className={currentView === 'pelatihan' ? 'text-blue-600' : 'text-slate-400'}>
          <BookOpen size={24} />
        </button>
        <button onClick={() => setCurrentView('admin')} className={currentView === 'admin' ? 'text-blue-600' : 'text-slate-400'}>
          <PlusCircle size={24} />
        </button>
      </div>
    </div>
  );
};

// --- SUB-KOMPONEN ---

const DashboardStats = ({ pelatihan, peserta }) => {
  const activePelatihan = pelatihan.filter(p => p.Status === 'Aktif').length;
 
  return (
    <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
      <StatCard title="Total Pelatihan" value={pelatihan.length} icon={BookOpen} color="blue" />
      <StatCard title="Pelatihan Aktif" value={activePelatihan} icon={CheckCircle2} color="green" />
      <StatCard title="Total Peserta" value={peserta.length} icon={Users} color="purple" />
     
      <div className="md:col-span-3 bg-white p-6 rounded-xl border border-slate-200 shadow-sm">
        <h3 className="text-lg font-bold mb-4">Pelatihan Terbaru</h3>
        <div className="space-y-4">
          {pelatihan.slice(0, 3).map((p, i) => (
            <div key={i} className="flex justify-between items-center p-3 hover:bg-slate-50 rounded-lg border border-transparent hover:border-slate-100 transition-all">
              <div>
                <p className="font-semibold text-slate-800">{p['Nama Pelatihan']}</p>
                <p className="text-sm text-slate-500">{p.Pemateri} {p.Tanggal}</p>
              </div>
              <ChevronRight className="text-slate-300" />
            </div>
          ))}
        </div>
      </div>
    </div>
  );
};

const StatCard = ({ title, value, icon: Icon, color }) => {
  const colors = {
    blue: 'bg-blue-50 text-blue-600',
    green: 'bg-emerald-50 text-emerald-600',
    purple: 'bg-violet-50 text-violet-600'
  };
 
  return (
    <div className="bg-white p-6 rounded-xl border border-slate-200 shadow-sm">
      <div className="flex justify-between items-start">
        <div>
          <p className="text-sm font-medium text-slate-500 uppercase tracking-wider">{title}</p>
          <h4 className="text-3xl font-bold mt-2 text-slate-900">{value}</h4>
        </div>
        <div className={`p-3 rounded-xl ${colors[color]}`}>
          <Icon size={24} />
        </div>
      </div>
    </div>
  );
};

const PelatihanList = ({ data, filter, setFilter }) => {
  const filteredData = filter === 'Semua' ? data : data.filter(p => p.Status === filter);
 
  return (
    <div className="space-y-6">
      <div className="flex items-center space-x-2 overflow-x-auto pb-2">
        {['Semua', 'Aktif', 'Selesai', 'Mendatang'].map(f => (
          <button
            key={f}
            onClick={() => setFilter(f)}
            className={`px-4 py-2 rounded-full text-sm font-medium whitespace-nowrap transition-all ${
              filter === f ? 'bg-slate-900 text-white' : 'bg-white border border-slate-200 text-slate-600 hover:border-slate-300'
            }`}
          >
            {f}
          </button>
        ))}
      </div>

      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
        {filteredData.map((p, i) => (
          <div key={i} className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden hover:shadow-md transition-all">
            <div className={`h-2 ${p.Status === 'Aktif' ? 'bg-green-500' : 'bg-slate-400'}`}></div>
            <div className="p-5">
              <div className="flex justify-between items-start mb-3">
                <span className={`text-[10px] uppercase font-bold px-2 py-1 rounded-md ${
                  p.Status === 'Aktif' ? 'bg-green-100 text-green-700' : 'bg-slate-100 text-slate-600'
                }`}>
                  {p.Status}
                </span>
                <p className="text-xs text-slate-400">ID: {p.ID}</p>
              </div>
              <h4 className="text-lg font-bold text-slate-800 mb-1">{p['Nama Pelatihan']}</h4>
              <p className="text-sm text-slate-500 mb-4">{p.Pemateri}</p>
             
              <div className="grid grid-cols-2 gap-4 text-sm text-slate-600">
                <div className="flex items-center space-x-2">
                  <LayoutDashboard size={14} className="text-slate-400" />
                  <span>{p.Tanggal}</span>
                </div>
                <div className="flex items-center space-x-2">
                  <Users size={14} className="text-slate-400" />
                  <span>{p.Kuota} Kuota</span>
                </div>
              </div>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
};

const AdminPanel = ({ pelatihan, peserta, onRefresh }) => {
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [activeTab, setActiveTab] = useState('tambah'); // 'tambah' or 'daftar-peserta'
  const [formData, setFormData] = useState({
    nama: '', pemateri: '', tanggal: '', kuota: '', status: 'Aktif'
  });

  const handleSubmitPelatihan = async (e) => {
    e.preventDefault();
    setIsSubmitting(true);
    try {
      await fetch(API_URL, {
        method: 'POST',
        mode: 'no-cors',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ action: 'addPelatihan', ...formData })
      });
      setFormData({ nama: '', pemateri: '', tanggal: '', kuota: '', status: 'Aktif' });
      // Karena no-cors, kita tidak bisa membaca response. Asumsikan sukses dan refresh
      setTimeout(() => onRefresh(), 2000);
    } catch (err) {
      console.error(err);
    } finally {
      setIsSubmitting(false);
    }
  };

  return (
    <div className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden">
      <div className="flex border-b border-slate-200">
        <button
          onClick={() => setActiveTab('tambah')}
          className={`flex-1 py-4 text-sm font-bold ${activeTab === 'tambah' ? 'text-blue-600 border-b-2 border-blue-600' : 'text-slate-500'}`}
        >
          TAMBAH PELATIHAN
        </button>
        <button
          onClick={() => setActiveTab('peserta')}
          className={`flex-1 py-4 text-sm font-bold ${activeTab === 'peserta' ? 'text-blue-600 border-b-2 border-blue-600' : 'text-slate-500'}`}
        >
          MANAJEMEN PESERTA
        </button>
      </div>

      <div className="p-8">
        {activeTab === 'tambah' ? (
          <form onSubmit={handleSubmitPelatihan} className="space-y-6 max-w-2xl">
            <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
              <div className="space-y-2">
                <label className="text-sm font-bold text-slate-700">Nama Pelatihan</label>
                <input
                  required
                  type="text"
                  value={formData.nama}
                  onChange={(e) => setFormData({...formData, nama: e.target.value})}
                  className="w-full p-3 rounded-lg border border-slate-300 focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition-all"
                  placeholder="Contoh: React Masterclass"
                />
              </div>
              <div className="space-y-2">
                <label className="text-sm font-bold text-slate-700">Pemateri</label>
                <input
                  required
                  type="text"
                  value={formData.pemateri}
                  onChange={(e) => setFormData({...formData, pemateri: e.target.value})}
                  className="w-full p-3 rounded-lg border border-slate-300 focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition-all"
                  placeholder="Nama Pemateri"
                />
              </div>
              <div className="space-y-2">
                <label className="text-sm font-bold text-slate-700">Tanggal</label>
                <input
                  required
                  type="date"
                  value={formData.tanggal}
                  onChange={(e) => setFormData({...formData, tanggal: e.target.value})}
                  className="w-full p-3 rounded-lg border border-slate-300 focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition-all"
                />
              </div>
              <div className="space-y-2">
                <label className="text-sm font-bold text-slate-700">Kuota</label>
                <input
                  required
                  type="number"
                  value={formData.kuota}
                  onChange={(e) => setFormData({...formData, kuota: e.target.value})}
                  className="w-full p-3 rounded-lg border border-slate-300 focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition-all"
                  placeholder="0"
                />
              </div>
            </div>
           
            <button
              disabled={isSubmitting}
              type="submit"
              className="w-full md:w-auto px-8 py-3 bg-blue-600 text-white rounded-lg font-bold hover:bg-blue-700 transition-all disabled:opacity-50 flex items-center justify-center space-x-2"
            >
              {isSubmitting ? <Loader2 className="animate-spin" /> : <PlusCircle size={20} />}
              <span>SIMPAN PELATIHAN</span>
            </button>
          </form>
        ) : (
          <PesertaManager pelatihan={pelatihan} peserta={peserta} onRefresh={onRefresh} />
        )}
      </div>
    </div>
  );
};

const PesertaManager = ({ pelatihan, peserta, onRefresh }) => {
  const [selectedPelatihan, setSelectedPelatihan] = useState('');
  const [formPeserta, setFormPeserta] = useState({ nama: '', email: '', institusi: '', foto: null });
  const [isSubmitting, setIsSubmitting] = useState(false);

  const handleFileChange = (e) => {
    const file = e.target.files[0];
    const reader = new FileReader();
    reader.onloadend = () => {
      setFormPeserta({ ...formPeserta, foto: reader.result, fotoName: file.name });
    };
    if (file) reader.readAsDataURL(file);
  };

  const handleAddPeserta = async (e) => {
    e.preventDefault();
    if (!selectedPelatihan) return;
    setIsSubmitting(true);
   
    try {
      await fetch(API_URL, {
        method: 'POST',
        mode: 'no-cors',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          action: 'addPeserta',
          idPelatihan: selectedPelatihan,
          fotoBase64: formPeserta.foto,
          fotoName: formPeserta.fotoName,
          ...formPeserta
        })
      });
      setFormPeserta({ nama: '', email: '', institusi: '', foto: null });
      setTimeout(() => onRefresh(), 2000);
    } catch (err) {
      console.error(err);
    } finally {
      setIsSubmitting(false);
    }
  };

  const filteredPeserta = selectedPelatihan
    ? peserta.filter(p => p['ID Pelatihan'] === selectedPelatihan)
    : [];

  return (
    <div className="space-y-8">
      <div className="bg-slate-50 p-6 rounded-xl border border-slate-200">
        <h4 className="font-bold mb-4 flex items-center space-x-2">
          <PlusCircle size={18} className="text-blue-600" />
          <span>Tambah Peserta Baru</span>
        </h4>
        <form onSubmit={handleAddPeserta} className="grid grid-cols-1 md:grid-cols-2 gap-4">
          <select
            required
            className="p-3 rounded-lg border border-slate-300 outline-none"
            value={selectedPelatihan}
            onChange={(e) => setSelectedPelatihan(e.target.value)}
          >
            <option value="">Pilih Pelatihan</option>
            {pelatihan.map(p => <option key={p.ID} value={p.ID}>{p['Nama Pelatihan']}</option>)}
          </select>
          <input required type="text" placeholder="Nama Peserta" value={formPeserta.nama} onChange={e => setFormPeserta({...formPeserta, nama: e.target.value})} className="p-3 rounded-lg border border-slate-300" />
          <input required type="email" placeholder="Email" value={formPeserta.email} onChange={e => setFormPeserta({...formPeserta, email: e.target.value})} className="p-3 rounded-lg border border-slate-300" />
          <input required type="text" placeholder="Institusi" value={formPeserta.institusi} onChange={e => setFormPeserta({...formPeserta, institusi: e.target.value})} className="p-3 rounded-lg border border-slate-300" />
          <div className="md:col-span-2 flex items-center space-x-4">
            <label className="flex-1 cursor-pointer bg-white border border-dashed border-slate-400 p-3 rounded-lg text-center hover:bg-slate-50">
              <div className="flex items-center justify-center space-x-2 text-slate-500">
                <Camera size={18} />
                <span>{formPeserta.fotoName || 'Klik untuk Upload Foto'}</span>
              </div>
              <input type="file" className="hidden" accept="image/*" onChange={handleFileChange} />
            </label>
            <button disabled={isSubmitting} className="px-8 py-3 bg-blue-600 text-white rounded-lg font-bold disabled:opacity-50 min-w-[150px]">
              {isSubmitting ? 'Mengirim...' : 'Tambah'}
            </button>
          </div>
        </form>
      </div>

      <div>
        <h4 className="font-bold mb-4">Daftar Peserta Pelatihan</h4>
        {selectedPelatihan ? (
          <div className="overflow-x-auto">
            <table className="w-full text-left border-collapse">
              <thead>
                <tr className="bg-slate-50 text-slate-600 text-xs uppercase tracking-wider">
                  <th className="p-4 border-b">Foto</th>
                  <th className="p-4 border-b">Nama</th>
                  <th className="p-4 border-b">Email</th>
                  <th className="p-4 border-b">Institusi</th>
                </tr>
              </thead>
              <tbody className="divide-y divide-slate-100">
                {filteredPeserta.length > 0 ? filteredPeserta.map((p, i) => (
                  <tr key={i} className="hover:bg-slate-50">
                    <td className="p-4">
                      {p.Foto ? (
                        <img src={p.Foto} alt="Avatar" className="w-10 h-10 rounded-full object-cover border border-slate-200" />
                      ) : (
                        <div className="w-10 h-10 rounded-full bg-slate-200 flex items-center justify-center">
                          <Users size={16} className="text-slate-400" />
                        </div>
                      )}
                    </td>
                    <td className="p-4 font-medium text-slate-800">{p['Nama Peserta']}</td>
                    <td className="p-4 text-slate-600">{p.Email}</td>
                    <td className="p-4 text-slate-600">{p.Institusi}</td>
                  </tr>
                )) : (
                  <tr><td colSpan="4" className="p-10 text-center text-slate-400">Belum ada peserta yang terdaftar</td></tr>
                )}
              </tbody>
            </table>
          </div>
        ) : (
          <div className="p-10 border border-dashed border-slate-200 rounded-xl text-center text-slate-400">
            Pilih pelatihan di atas untuk melihat daftar peserta
          </div>
        )}
      </div>
    </div>
  );
};

export default App;

Build dengan Vite

1. Jalankan Perintah Build

Buka terminal di VS Code (pada folder project Anda), lalu ketik perintah berikut:

Bash
npm run build

2. Apa yang Terjadi Setelahnya?

  • Vite akan memproses semua file JSX, CSS, dan aset lainnya.

  • Vite akan melakukan minification (memperkecil ukuran file) agar aplikasi lebih cepat dimuat.

  • Folder baru bernama dist akan muncul di dalam direktori project Anda.

3. Masalah Umum: Path Kosong (PENTING)

Secara default, Vite mengasumsikan aplikasi akan diletakkan di root domain (contoh: [https://situsanda.com/](https://situsanda.com/)). Jika Anda ingin meng-embed hasil build ini ke Google Sites atau GitHub Pages (yang biasanya memiliki sub-path), file seringkali tidak muncul (blank) karena path CSS/JS salah.

Cara Memperbaikinya:

  1. Buka file vite.config.js di project Anda.

  2. Tambahkan properti base: './', agar semua link file menjadi relatif.

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  base: './', // Tambahkan baris ini agar folder dist bisa dibuka langsung
})
```
3.  Simpan file tersebut, lalu jalankan `npm run build` kembali.

### 4. Cara Tes Hasil Build (Preview)
Anda tidak bisa langsung membuka file `index.html` di dalam folder `dist` hanya dengan klik kanan > open browser (seringkali akan error karena modul JS). Vite menyediakan cara untuk mengetesnya:

```bash
npm run preview
```
Perintah ini akan menjalankan server lokal khusus untuk melihat bagaimana tampilan aplikasi Anda setelah di-build.

### 5. Mengirim ke Hosting (Netlify/Google Sites)
*   **Netlify:** Anda cukup melakukan *drag-and-drop* folder **`dist`** tersebut ke area upload Netlify Drop.
*   **Google Sites:** Gunakan URL yang didapat dari Netlify tersebut untuk di-embed melalui opsi **Embed > By URL**.

Sudahkah folder `dist` muncul di sidebar VS Code Anda setelah menjalankan perintah tersebut?

Android - Task Manager

  Untuk membangun aplikasi Simple Task Manager menggunakan Jetpack Compose. Struktur ini mengikuti standar profesional dengan memisahkan da...