From 66b79fa2db90ac3dd9693a8085145cbc3e7c4e75 Mon Sep 17 00:00:00 2001 From: "[Valentino Heman Budiarto]" <[hemanvalentino@gmail.com]> Date: Mon, 26 Oct 2026 14:23:38 +0700 Subject: [PATCH] . --- app/api/trajectory/route.js | 43 +++--- app/page.tsx | 270 +++++++++++++++++++----------------- 2 files changed, 168 insertions(+), 145 deletions(-) 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 (
-
-
- - - -
+
+
PI4 WD ROVER -
- - - -
+
); @@ -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 */} - Robot Feed { 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 */} + Robot Feed { 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
+
+
+
L
+
+
R
+
+
+ )} +
+ + {/* 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 */} -