Lovable Migrator

Analisis mendalam ZIP Lovable → output siap upload ke GitHub repo, tanpa manual edit

⚡ v5 — powerful, share-link ready, username-safe
① Analisis & Bersihkan
② Supabase
③ Checklist Deploy
④ Template Kode
📦 Upload ZIP project Lovable apa pun

Game, web app, landing page — tool akan analisis otomatis dan tampilkan apa yang perlu diubah

Kapan butuh tab ini? Kalau game pakai Supabase (database, realtime, auth). Supabase lama milik Lovable tidak bisa diakses setelah migrasi — perlu buat project baru di akun sendiri.
📋 Langkah setup Supabase baru
supabase.com → New project → nama sesuai game → Region: Southeast Asia (Singapore) → Create
SQL Editor → New query → paste SQL dari tab ④ sesuai game → Run → pastikan "Success"
Klik Connect (hijau, navbar atas) → Framework → React → catat VITE_SUPABASE_URL dan VITE_SUPABASE_PUBLISHABLE_KEY
Workers project → Settings → Build → Variables and secrets → Add (tipe Text, bukan Secret/Encrypt)
Buka game → coba buat room/login → kalau berhasil, Supabase tersambung
⚠️ Error umum Supabase
Normal — Supabase lama ada di akun Lovable. Buat project baru di akun sendiri.
Valid! Key format baru Supabase dimulai sb_publishable_, bukan eyJ.... Keduanya berfungsi normal.
Pastikan SQL sudah jalankan alter publication supabase_realtime add table public.nama_tabel untuk semua tabel yang perlu realtime.
Pastikan RLS policy sudah dibuat: create policy "allow all" on public.tabel for all using (true) with check (true);
0 / 0 selesai
📦 Persiapan kode
🐙 GitHub
☁️ Cloudflare — Deploy
Jangan pakai npx wrangler versions upload — hanya upload versi, tidak langsung live
Untuk Supabase: VITE_SUPABASE_URL dan VITE_SUPABASE_PUBLISHABLE_KEY
🔧 Error umum & solusi
→ Hapus bun.lockb dari GitHub, commit, retry build
→ Hapus "sideEffects": false dari package.json
→ vite.config.ts salah — cloudflare({"{"}viteEnvironment:{"{"}name:"ssr"{"}"}{"}"}}) harus SEBELUM tanstackStart()
→ wrangler.jsonc belum ada atau salah — pastikan ada "assets": {"{"}"directory": "./dist"{"}"}
→ Hapus file public/_redirects dari repo
→ Copy isi yang disarankan → paste ke wrangler.jsonc di GitHub → commit
🌐 Domain custom
Kalau error "already has DNS records" → hapus dulu CNAME di DNS Cloudflare, baru Add Domain lagi
🔗 Standar alur share link & username (semua game multiplayer)
Share Link: domain.web.id?room=XXXXX (SPA) atau /room/XXXXX (TanStack). Copy Kode: salin kode room saja.
Klik link → [1] cek kode akses (bila belum) → [2] tampil form nama (prefill dari terakhir, wajib diisi/konfirmasi) → [3] masuk room yang dituju
SPA: simpan di sessionStorage lalu baca di HomeScreen. TanStack: pakai validateSearch + useSearch di setiap route yang dilalui.
Nama terakhir boleh di-prefill (getPlayerName()) tapi pemain harus klik tombol join/masuk secara sadar. Ini mencegah join dengan nama lama yang tidak sesuai.
Agar tidak perlu ketik ulang di sesi berikutnya, tapi tetap bisa diubah.
Tampilkan pesan error yang jelas kalau nama tidak valid sebelum join diproses.
✅ Finalisasi
Kalau preview lama → cache WhatsApp belum habis, tunggu 1–24 jam. Test di opengraph.xyz dulu.

Template siap pakai — sesuaikan dengan stack dan nama project Anda.

wrangler.jsonc — Static Assets (Vite React) Vite React
Untuk project Vite React biasa tanpa SSR. Ganti nama-project dan tanggal hari ini.
{ "$schema": "node_modules/wrangler/config-schema.json", "name": "nama-project", "compatibility_date": "2026-07-03", "assets": { "directory": "./dist" } }
wrangler.jsonc — TanStack Start SSR TanStack Start
{ "$schema": "node_modules/wrangler/config-schema.json", "name": "nama-project", "compatibility_date": "2026-07-03", "compatibility_flags": ["nodejs_compat"], "main": "src/server.ts", "observability": { "enabled": true } }
vite.config.ts — TanStack Start + Cloudflare TanStack Start
Urutan plugin kritis! cloudflare() harus SEBELUM tanstackStart(). Kalau tidak ada server.ts, hapus baris server: {"{"} entry: "server" {"}"}.
import { defineConfig } from "vite"; import { tanstackStart } from "@tanstack/react-start/plugin/vite"; import { cloudflare } from "@cloudflare/vite-plugin"; import react from "@vitejs/plugin-react"; import tailwindcss from "@tailwindcss/vite"; import tsconfigPaths from "vite-tsconfig-paths"; export default defineConfig({ plugins: [ cloudflare({ viteEnvironment: { name: "ssr" } }), tanstackStart({ server: { entry: "server" } }), react(), tailwindcss(), tsconfigPaths(), ], });
SQL Supabase — Sketsa Berantai (skeber) Supabase
create table public.rooms ( id uuid primary key default gen_random_uuid(), code text not null, created_at timestamptz default now(), current_step integer default 0, host_id uuid, num_players integer default 1, status text default 'waiting' ); create table public.players ( id uuid primary key default gen_random_uuid(), created_at timestamptz default now(), nickname text not null, room_id uuid not null references public.rooms(id) on delete cascade, seat integer default 0 ); alter table public.rooms add constraint rooms_host_id_fkey foreign key (host_id) references public.players(id) on delete set null; create table public.steps ( id uuid primary key default gen_random_uuid(), created_at timestamptz default now(), author_id uuid not null references public.players(id) on delete cascade, chain_owner_id uuid not null references public.players(id) on delete cascade, drawing_data text, kind text not null, room_id uuid not null references public.rooms(id) on delete cascade, step_index integer not null, text_content text ); alter publication supabase_realtime add table public.rooms; alter publication supabase_realtime add table public.players; alter publication supabase_realtime add table public.steps; alter table public.rooms enable row level security; alter table public.players enable row level security; alter table public.steps enable row level security; create policy "allow all" on public.rooms for all using (true) with check (true); create policy "allow all" on public.players for all using (true) with check (true); create policy "allow all" on public.steps for all using (true) with check (true);
SQL Supabase — Tekongan Supabase
create table public.rooms ( id uuid primary key default gen_random_uuid(), created_at timestamptz default now(), host_id text not null, host_name text not null, status text default 'waiting', seeker_id text, seeker_lives integer default 3, venue_id text, venue_name text, hiding_ends_at timestamptz ); create table public.room_players ( id uuid primary key default gen_random_uuid(), room_id uuid not null references public.rooms(id) on delete cascade, player_id text not null, name text not null, role text default 'hider', status text default 'hiding', joined_at timestamptz default now() ); create table public.room_hidden_spots ( id uuid primary key default gen_random_uuid(), room_id uuid not null references public.rooms(id) on delete cascade, player_id text not null, spot_id text not null, created_at timestamptz default now() ); create table public.duels ( id uuid primary key default gen_random_uuid(), room_id uuid not null references public.rooms(id) on delete cascade, seeker_id text not null, hider_id text not null, spot_id text not null, started_at timestamptz default now(), seeker_tapped_at timestamptz, hider_tapped_at timestamptz, winner_id text ); create table public.emotes ( id uuid primary key default gen_random_uuid(), room_id uuid not null references public.rooms(id) on delete cascade, player_id text not null, emote text not null, created_at timestamptz default now() ); create table public.chat_messages ( id uuid primary key default gen_random_uuid(), room_id uuid not null references public.rooms(id) on delete cascade, player_id text not null, name text not null, content text not null, created_at timestamptz default now() ); alter publication supabase_realtime add table public.rooms; alter publication supabase_realtime add table public.room_players; alter publication supabase_realtime add table public.room_hidden_spots; alter publication supabase_realtime add table public.duels; alter publication supabase_realtime add table public.emotes; alter publication supabase_realtime add table public.chat_messages; alter table public.rooms enable row level security; alter table public.room_players enable row level security; alter table public.room_hidden_spots enable row level security; alter table public.duels enable row level security; alter table public.emotes enable row level security; alter table public.chat_messages enable row level security; create policy "allow all" on public.rooms for all using (true) with check (true); create policy "allow all" on public.room_players for all using (true) with check (true); create policy "allow all" on public.room_hidden_spots for all using (true) with check (true); create policy "allow all" on public.duels for all using (true) with check (true); create policy "allow all" on public.emotes for all using (true) with check (true); create policy "allow all" on public.chat_messages for all using (true) with check (true);
Tombol Game Hub — standar Arsepat semua game
Wajib pakai <a href>, bukan button+Dialog+iframe. Ganti URL sesuai domain Hub.
Helper nama player — standar semua game semua game
Tambahkan ke file lib/game.ts atau lib/player.ts. Pakai di HomeScreen/Lobby untuk prefill nama yang bisa diubah.
// ── Player name helpers (simpan nama antar sesi, selalu bisa diubah) ── const KEY_NAME = "player_name"; export function getPlayerName(): string { return localStorage.getItem(KEY_NAME) ?? ""; } export function setPlayerName(name: string) { if (name.trim()) localStorage.setItem(KEY_NAME, name.trim()); } export function clearPlayerName() { localStorage.removeItem(KEY_NAME); } // ── Cara pakai di HomeScreen/Lobby ── // const [name, setName] = useState(() => getPlayerName()); // prefill dari localStorage // → setelah join berhasil: setPlayerName(name.trim()); // → nama SELALU ditampilkan di form — tidak pernah auto-join tanpa konfirmasi
Share Link — SPA (React Router) Vite React
Tambahkan ke LobbyScreen/RoomScreen. Pakai ?room=KODE di URL.
// Di LobbyScreen — fungsi share link function shareLink() { const url = `${window.location.origin}?room=${roomCode}`; if (navigator.share) { navigator.share({ title: "Yuk main bareng!", text: `Gabung di ruang ${roomCode}!`, url, }).catch(() => {}); } else { navigator.clipboard.writeText(url); toast.success("Link ruang disalin!"); } } // Di Index.tsx — baca ?room= dari URL saat load function getRoomFromUrl(): string { return new URLSearchParams(window.location.search).get("room") ?? ""; } const PENDING = "pending_room"; // Saat load: simpan room dari URL const roomFromUrl = getRoomFromUrl(); if (roomFromUrl) sessionStorage.setItem(PENDING, roomFromUrl); // Saat sudah unlock: baca dan forward ke HomeScreen const pending = sessionStorage.getItem(PENDING) ?? ""; // Di HomeScreen: prefill roomCode dan set mode join // const [roomCode, setRoomCode] = useState(pending); // const [mode, setMode] = useState(pending ? "join" : "choose");
Share Link — TanStack Start (file-based routing) TanStack Start
Pakai validateSearch + useSearch di setiap route yang dilalui redirect.
// room.$id.tsx — redirect ke / dengan membawa room ID if (!isAuthorized()) { nav({ to: "/", search: { redirect: id } }); return; } // index.tsx (gateway) — tambah validateSearch export const Route = createFileRoute("/")({ validateSearch: (search) => ({ redirect: (search.redirect as string) ?? "" }), component: () => , }); // Di Gateway: useSearch({ from: "/" }) → forward redirect ke /lobby // lobby.tsx — ambil redirect dan prefill room code export const Route = createFileRoute("/lobby")({ validateSearch: (search) => ({ redirect: (search.redirect as string) ?? "" }), component: () => , }); // Di Lobby: const { redirect } = useSearch({ from: "/lobby" }); // const [room, setRoom] = useState(redirect ?? ""); // prefill room code // JANGAN auto-join — biarkan pemain konfirmasi nama dulu
register-sw.ts — bersih tanpa Lovable PWA
export function registerServiceWorker() { if (typeof window === "undefined") return; if (!("serviceWorker" in navigator)) return; const inIframe = (() => { try { return window.self !== window.top; } catch { return true; } })(); const isDevHost = ["localhost","127.0.0.1"].includes(window.location.hostname); if (inIframe || isDevHost) { navigator.serviceWorker.getRegistrations().then(r => r.forEach(s => s.unregister())); return; } window.addEventListener("load", () => { navigator.serviceWorker.register("/sw.js").catch(() => {}); }); }