diff --git a/app/api/trajectory/route.js b/app/api/trajectory/route.js
index 5912aa7..c474736 100644
--- a/app/api/trajectory/route.js
+++ b/app/api/trajectory/route.js
@@ -1,29 +1,30 @@
+import { NextResponse } from 'next/server';
import fs from 'fs';
import path from 'path';
-// File JSON akan disimpan di root direktori proyek Next.js
-const filePath = path.join(process.cwd(), 'perjalanan.json');
+// Lokasi penyimpanan file di dalam folder utama proyek
+const dataFilePath = path.join(process.cwd(), 'trajectory.json');
-// Menerima data dari Tombol Y (Save)
-export async function POST(req) {
- try {
- const data = await req.json();
- fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
- return Response.json({ success: true, message: 'Data perjalanan berhasil disimpan!' });
- } catch (error) {
- return Response.json({ success: false, error: 'Gagal menyimpan data' }, { status: 500 });
- }
+// Fungsi GET untuk PLAY (membaca rute)
+export async function GET() {
+ try {
+ if (!fs.existsSync(dataFilePath)) {
+ return NextResponse.json([]);
+ }
+ const fileData = fs.readFileSync(dataFilePath, 'utf8');
+ return NextResponse.json(JSON.parse(fileData));
+ } catch (error) {
+ return NextResponse.json({ error: 'Gagal membaca data' }, { status: 500 });
+ }
}
-// Membaca data untuk Tombol A & X (Playback)
-export async function GET() {
- try {
- if (!fs.existsSync(filePath)) {
- return Response.json([]); // Kembalikan array kosong jika belum ada rekaman
+// Fungsi POST untuk SAVE (menyimpan rute)
+export async function POST(request) {
+ try {
+ const data = await request.json();
+ fs.writeFileSync(dataFilePath, JSON.stringify(data, null, 2), 'utf8');
+ return NextResponse.json({ success: true });
+ } catch (error) {
+ return NextResponse.json({ error: 'Gagal menyimpan data' }, { status: 500 });
}
- const fileData = fs.readFileSync(filePath, 'utf-8');
- return Response.json(JSON.parse(fileData));
- } catch (error) {
- return Response.json({ success: false, error: 'Gagal membaca data' }, { status: 500 });
- }
}
\ No newline at end of file
diff --git a/app/page.tsx b/app/page.tsx
index 8c62102..c6ab08f 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -1,6 +1,12 @@
'use client';
import { useState, useEffect, useRef } from 'react';
+import { Pixelify_Sans } from 'next/font/google';
+
+const pixelify = Pixelify_Sans({
+ subsets: ['latin'],
+ weight: ['400', '500', '600', '700'],
+});
// ==========================================
// A. KOMPONEN LOGO PI4WD ROVER
@@ -8,19 +14,11 @@ import { useState, useEffect, useRef } from 'react';
const PiRoverLogo = ({ scale = 1 }: { scale?: number }) => {
return (
-
);
@@ -46,6 +44,8 @@ export default function RobotDashboard() {
const [gpVisuals, setGpVisuals] = useState({ lt: 0, rt: 0, steer: 0, btnA: false, btnB: false, btnX: false, btnY: false });
+ const containerRef = useRef
(null);
+
const currentTicksRef = useRef({ l: 0, r: 0 });
const lastRecordedTicksRef = useRef({ l: 0, r: 0 });
const activeMotionRef = useRef('STOP');
@@ -128,7 +128,7 @@ export default function RobotDashboard() {
activeMotionRef.current = 'STOP'; lastCommandRef.current = { x: 0, y: 0 }; stopRobot();
}
} else if (action === 'SAVE') {
- if (isRecordingRef.current) { isRecordingRef.current = false; /* Implementasi simpan API di sini */ }
+ if (isRecordingRef.current) { isRecordingRef.current = false; }
}
};
@@ -170,6 +170,11 @@ export default function RobotDashboard() {
window.addEventListener('keydown', handleKeyDown);
window.addEventListener('keyup', handleKeyUp);
+ const handleFullscreenChange = () => {
+ setIsFullscreen(!!document.fullscreenElement);
+ };
+ document.addEventListener('fullscreenchange', handleFullscreenChange);
+
const interval = setInterval(() => {
let currentX = 0; let currentY = 0;
let gpA = false, gpB = false, gpY = false, gpX = false;
@@ -228,153 +233,170 @@ export default function RobotDashboard() {
return () => {
clearTimeout(loadingTimer);
clearInterval(interval);
- window.removeEventListener('keydown', handleKeyDown); window.removeEventListener('keyup', handleKeyUp);
+ window.removeEventListener('keydown', handleKeyDown);
+ window.removeEventListener('keyup', handleKeyUp);
+ document.removeEventListener('fullscreenchange', handleFullscreenChange);
if (wsRef.current) wsRef.current.close();
};
}, [controlMode, PI_IP]);
const toggleFullscreen = () => {
- if (!document.fullscreenElement) { document.documentElement.requestFullscreen(); setIsFullscreen(true); }
- else if (document.exitFullscreen) { document.exitFullscreen(); setIsFullscreen(false); }
+ if (!document.fullscreenElement) {
+ if (containerRef.current) {
+ containerRef.current.requestFullscreen().catch((err) => console.log(err));
+ }
+ } else {
+ if (document.exitFullscreen) {
+ document.exitFullscreen();
+ }
+ }
};
return (
e.preventDefault()}
- className="min-h-screen bg-emerald-50 flex flex-col select-none touch-none overflow-hidden"
- style={{ WebkitTouchCallout: 'none', WebkitUserSelect: 'none', fontFamily: '"HK Modular", sans-serif' }}
+ className={`min-h-screen bg-emerald-50 flex flex-col select-none touch-none overflow-x-hidden ${pixelify.className}`}
+ style={{ WebkitTouchCallout: 'none', WebkitUserSelect: 'none' }}
>
{/* LOADING SCREEN */}
-
-
+
+
Menghubungkan ke Robot...
{/* HEADER LOGO */}
-
+
- {/* MAIN CAMERA FEED & OVERLAY LAYOUT */}
-
-
- {/* Stream Kamera */}
-

{ e.currentTarget.style.display = 'none'; }} />
-
- {/* Indikator Status Perekaman */}
- {(isRecordingRef.current || isPlayingRef.current) && (
-
- {isRecordingRef.current && MEREKAM}
- {isPlayingRef.current && MEMUTAR RUTE}
-
- )}
-
- {/* OVERLAY: TOP BAR */}
-
-
-
-
-
-
-
- {/* OVERLAY: FULLSCREEN */}
-
-
- {/* OVERLAY: BOTTOM LEFT (MOVEMENT CONTROLS) */}
-
- {controlMode === 'pc-mobile' ? (
-
-
-
-
-
-
-
-
- ) : (
-
-
-
0.1 ? 'bg-white text-slate-900 border-white' : 'bg-white/20 text-white border-white/50 backdrop-blur-sm'}`}>LT
-
0.1 ? 'bg-white text-slate-900 border-white' : 'bg-white/20 text-white border-white/50 backdrop-blur-sm'}`}>RT
-
-
+ {/* MAIN CAMERA FEED & OVERLAY LAYOUT - DITENGAHKAN DAN DIPERBESAR */}
+
+
+
+ {/* Stream Kamera */}
+

{ e.currentTarget.style.display = 'none'; }} />
+
+ {/* Indikator Status Perekaman */}
+ {(isRecordingRef.current || isPlayingRef.current) && (
+
+ {isRecordingRef.current && MEREKAM}
+ {isPlayingRef.current && MEMUTAR RUTE}
)}
-
- {/* OVERLAY: BOTTOM RIGHT (ACTION CONTROLS) */}
-
- {[
- { action: 'RECORD', pc: 'V', gp: 'B', color: 'bg-red-500 shadow-[0_4px_0_#b91c1c]', activeColor: 'active:bg-red-600', gpState: gpVisuals.btnB },
- { action: 'SAVE', pc: 'B', gp: 'Y', color: 'bg-amber-400 shadow-[0_4px_0_#d97706]', activeColor: 'active:bg-amber-500', gpState: gpVisuals.btnY },
- { action: 'PLAY_FWD', pc: 'N', gp: 'A', color: 'bg-emerald-500 shadow-[0_4px_0_#047857]', activeColor: 'active:bg-emerald-600', gpState: gpVisuals.btnA },
- { action: 'PLAY_REV', pc: 'M', gp: 'X', color: 'bg-blue-600 shadow-[0_4px_0_#1d4ed8]', activeColor: 'active:bg-blue-700', gpState: gpVisuals.btnX }
- ].map((btn) => (
-
-
- {btn.action === 'PLAY_FWD' ? 'PLAY' : btn.action === 'PLAY_REV' ? 'R-PLAY' : btn.action}
-
+ {/* OVERLAY: TOP BAR */}
+
+
+
- ))}
+
+
+ {/* OVERLAY: FULLSCREEN TOGGLE */}
+
+
+ {/* OVERLAY: BOTTOM LEFT (MOVEMENT CONTROLS) */}
+
+ {controlMode === 'pc-mobile' ? (
+
+
+
+
+
+
+
+
+ ) : (
+
+
+
0.1 ? 'bg-white text-slate-900 border-slate-900' : 'bg-slate-900/50 text-white border-white/50 backdrop-blur-sm'}`}>LT
+
0.1 ? 'bg-white text-slate-900 border-slate-900' : 'bg-slate-900/50 text-white border-white/50 backdrop-blur-sm'}`}>RT
+
+
+
+ )}
+
+
+ {/* OVERLAY: BOTTOM RIGHT (ACTION CONTROLS) */}
+
+ {[
+ { action: 'RECORD', pc: 'V', gp: 'B', color: 'bg-[#f87171] shadow-[4px_4px_0_#b91c1c]', activeColor: 'active:bg-red-500', gpState: gpVisuals.btnB },
+ { action: 'SAVE', pc: 'B', gp: 'Y', color: 'bg-[#fbbf24] shadow-[4px_4px_0_#d97706]', activeColor: 'active:bg-amber-500', gpState: gpVisuals.btnY },
+ { action: 'PLAY_FWD', pc: 'N', gp: 'A', color: 'bg-[#34d399] shadow-[4px_4px_0_#047857]', activeColor: 'active:bg-emerald-500', gpState: gpVisuals.btnA },
+ { action: 'PLAY_REV', pc: 'M', gp: 'X', color: 'bg-[#60a5fa] shadow-[4px_4px_0_#1d4ed8]', activeColor: 'active:bg-blue-500', gpState: gpVisuals.btnX }
+ ].map((btn) => (
+
+
+ {btn.action === 'PLAY_FWD' ? 'PLAY' : btn.action === 'PLAY_REV' ? 'R-PLAY' : btn.action}
+
+
+
+ ))}
+
{/* FOOTER TEXT */}
-