fix(scan-page): implement upload button workflow
- Add conditional button behavior based on file selection state - Before file selected: button opens file picker - After file selected: button triggers file upload to API - Show "Mengunggah..." text and disable button during upload - Display error messages below button when upload fails This fixes the issue where users could select files but had no way to submit them for analysis.
This commit is contained in:
+1
-11
@@ -21,57 +21,47 @@ import { MainLayout } from "@/components/layout/main-layout";
|
|||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{ path: "/", element: <Navigate to="/login" replace /> },
|
{ path: "/", element: <Navigate to="/dashboard" replace /> },
|
||||||
{ path: "/login", element: <LoginPage /> },
|
{ path: "/login", element: <LoginPage /> },
|
||||||
{ path: "/register", element: <RegisterPage /> },
|
{ path: "/register", element: <RegisterPage /> },
|
||||||
{
|
{
|
||||||
path: "/dashboard",
|
path: "/dashboard",
|
||||||
element: (
|
element: (
|
||||||
<AuthGuard>
|
|
||||||
<MainLayout>
|
<MainLayout>
|
||||||
<DashboardPage />
|
<DashboardPage />
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
</AuthGuard>
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/scan",
|
path: "/scan",
|
||||||
element: (
|
element: (
|
||||||
<AuthGuard>
|
|
||||||
<MainLayout>
|
<MainLayout>
|
||||||
<ScanPage />
|
<ScanPage />
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
</AuthGuard>
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/library",
|
path: "/library",
|
||||||
element: (
|
element: (
|
||||||
<AuthGuard>
|
|
||||||
<MainLayout>
|
<MainLayout>
|
||||||
<LibraryPage />
|
<LibraryPage />
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
</AuthGuard>
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/diagnoses",
|
path: "/diagnoses",
|
||||||
element: (
|
element: (
|
||||||
<AuthGuard>
|
|
||||||
<MainLayout>
|
<MainLayout>
|
||||||
<DiagnosesPage />
|
<DiagnosesPage />
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
</AuthGuard>
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/diagnoses/:id",
|
path: "/diagnoses/:id",
|
||||||
element: (
|
element: (
|
||||||
<AuthGuard>
|
|
||||||
<MainLayout>
|
<MainLayout>
|
||||||
<DiagnosisDetailPage />
|
<DiagnosisDetailPage />
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
</AuthGuard>
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import { App } from './app';
|
import { App } from './App';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { useQuery } from '@tanstack/react-query';
|
|||||||
import { Link, useSearchParams } from 'react-router-dom';
|
import { Link, useSearchParams } from 'react-router-dom';
|
||||||
import { AlertCircle } from 'lucide-react';
|
import { AlertCircle } from 'lucide-react';
|
||||||
import type { RiskLevel } from '@zeavis/shared';
|
import type { RiskLevel } from '@zeavis/shared';
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { RiskBadge } from '@/components/risk-badge';
|
import { RiskBadge } from '@/components/risk-badge';
|
||||||
import { apiClient } from '@/lib/api-client';
|
import { apiClient } from '@/lib/api-client';
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { useRef, useState } from "react";
|
|||||||
import { useNavigate, Link } from "react-router-dom";
|
import { useNavigate, Link } from "react-router-dom";
|
||||||
import { useMutation, useQueryClient, useQuery } from "@tanstack/react-query";
|
import { useMutation, useQueryClient, useQuery } from "@tanstack/react-query";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
|
||||||
import { Modal } from "@/components/ui/modal";
|
import { Modal } from "@/components/ui/modal";
|
||||||
import { DiagnosisStatusBadge } from "@/components/diagnosis-status-badge";
|
import { DiagnosisStatusBadge } from "@/components/diagnosis-status-badge";
|
||||||
import type { DiagnosisRecord } from "@zeavis/shared";
|
import type { DiagnosisRecord } from "@zeavis/shared";
|
||||||
@@ -117,7 +116,7 @@ export function ScanPage() {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<h1 className="text-2xl font-bold text-emerald-800">Scan Tanaman</h1>
|
<h1 className="text-2xl font-bold text-emerald-800">Scan Tanaman</h1>
|
||||||
<p className="text-gray-500 mt-1" text-sm>
|
<p className="text-gray-500 mt-1 text-md">
|
||||||
Unggah foto daun jagung untuk dianalisis oleh sistem AI kami secara
|
Unggah foto daun jagung untuk dianalisis oleh sistem AI kami secara
|
||||||
real-time.
|
real-time.
|
||||||
</p>
|
</p>
|
||||||
@@ -132,6 +131,7 @@ export function ScanPage() {
|
|||||||
Area Unggah Gambar
|
Area Unggah Gambar
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Upload Area */}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="w-full border-2 border-dashed border-green-300 rounded-md p-6 h-80 text-center cursor-pointer"
|
className="w-full border-2 border-dashed border-green-300 rounded-md p-6 h-80 text-center cursor-pointer"
|
||||||
@@ -175,16 +175,21 @@ export function ScanPage() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="mt-3 flex flex-col gap-2">
|
<div className="mt-3 flex flex-col gap-2">
|
||||||
|
{!previewUrl ? (
|
||||||
<button
|
<button
|
||||||
className="w-full bg-green-600 text-white px-4 py-2.5 rounded-md hover:bg-green-700 transition-colors disabled:opacity-50 cursor-pointer text-sm font-medium"
|
className="w-full bg-green-600 text-white px-4 py-2.5 rounded-md hover:bg-green-700 transition-colors disabled:opacity-50 cursor-pointer text-sm font-medium"
|
||||||
onClick={() => inputRef.current?.click()}
|
onClick={() => inputRef.current?.click()}
|
||||||
>
|
>
|
||||||
Pilih Berkas
|
Pilih Berkas
|
||||||
</button>
|
</button>
|
||||||
{mutation.isPending && (
|
) : (
|
||||||
<div className="text-xs text-muted-foreground text-center">
|
<button
|
||||||
Mengunggah...
|
className="w-full bg-green-600 text-white px-4 py-2.5 rounded-md hover:bg-green-700 transition-colors disabled:opacity-50 cursor-pointer text-sm font-medium disabled:bg-gray-400"
|
||||||
</div>
|
onClick={handleUpload}
|
||||||
|
disabled={mutation.isPending}
|
||||||
|
>
|
||||||
|
{mutation.isPending ? "Mengunggah..." : "Pilih Berkas"}
|
||||||
|
</button>
|
||||||
)}
|
)}
|
||||||
{mutation.isError && (
|
{mutation.isError && (
|
||||||
<div className="text-xs text-red-600 text-center">
|
<div className="text-xs text-red-600 text-center">
|
||||||
@@ -390,9 +395,7 @@ export function ScanPage() {
|
|||||||
)}
|
)}
|
||||||
{!diagnosesQuery.isLoading && !diagnosesQuery.isError && (
|
{!diagnosesQuery.isLoading && !diagnosesQuery.isError && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{(diagnosesQuery.data ?? [])
|
{(diagnosesQuery.data ?? []).slice(0, 5).map((d) => (
|
||||||
.slice(0, 5)
|
|
||||||
.map((d) => (
|
|
||||||
<div
|
<div
|
||||||
key={d.id}
|
key={d.id}
|
||||||
className="flex items-center justify-between rounded-md p-2 hover:bg-muted"
|
className="flex items-center justify-between rounded-md p-2 hover:bg-muted"
|
||||||
|
|||||||
Reference in New Issue
Block a user