Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 144 additions & 0 deletions components/logistics/ImageCapture.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
'use client';

import { useCallback, useMemo, useRef, useState } from 'react';
import { Upload, Loader } from 'lucide-react';
import { useImageCompressor } from '@/hooks/useImageCompressor';
import { uploadService } from '@/services/uploadService';

export default function ImageCapture() {
const fileInputRef = useRef<HTMLInputElement | null>(null);
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
const [isUploading, setIsUploading] = useState(false);
const [uploadResult, setUploadResult] = useState<string | null>(null);
const { compress, isCompressing } = useImageCompressor();

const handleFileChange = useCallback(
async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0] ?? null;
if (!file) return;

// Create preview immediately
const url = URL.createObjectURL(file);
setPreviewUrl(url);
setSelectedFile(file);
setUploadResult(null);
},
[]
);

const compressedInfo = useMemo(() => {
if (!selectedFile) return null;
return {
name: selectedFile.name,
sizeKB: (selectedFile.size / 1024).toFixed(2),
};
}, [selectedFile]);

const handleUpload = useCallback(async () => {
if (!selectedFile) return;
setIsUploading(true);
setUploadResult(null);
try {
// Compress to under 500KB
const compressed = await compress(selectedFile, 500);

// Optionally show compressed size in result
const compressedSizeKB = (compressed.size / 1024).toFixed(2);

const response = await uploadService.uploadFile(compressed, 'proof');
if (response.success) {
setUploadResult(
`Uploaded ${response.data?.fileName ?? compressed.name} (${compressedSizeKB} KB)`
);
// cleanup preview
if (previewUrl) {
URL.revokeObjectURL(previewUrl);
setPreviewUrl(null);
}
setSelectedFile(null);
} else {
setUploadResult(
`Upload failed: ${response.message ?? 'unknown error'}`
);
}
} catch (err: any) {
setUploadResult(err?.message ?? 'Upload error');
} finally {
setIsUploading(false);
}
}, [selectedFile, compress, previewUrl]);

return (
<div className="max-w-md mx-auto p-4">
<h2 className="text-lg font-semibold mb-3">Capture Proof of Delivery</h2>

<div className="mb-3">
<input
ref={fileInputRef}
type="file"
accept="image/*"
capture="environment"
onChange={handleFileChange}
className="block"
/>
</div>

{previewUrl && (
<div className="mb-3">
<img
src={previewUrl}
alt="preview"
className="w-full rounded-md border"
/>
</div>
)}

{compressedInfo && (
<p className="text-sm text-gray-600 mb-2">
Original: {compressedInfo.name} — {compressedInfo.sizeKB} KB
</p>
)}

<div className="flex items-center gap-2">
<button
type="button"
onClick={() => fileInputRef.current?.click()}
className="inline-flex items-center gap-2 px-3 py-1.5 rounded border bg-white"
>
<Upload className="w-4 h-4" />
Choose Photo
</button>

<button
type="button"
onClick={handleUpload}
disabled={!selectedFile || isCompressing || isUploading}
className={`inline-flex items-center gap-2 px-3 py-1.5 rounded text-white ${
!selectedFile || isCompressing || isUploading
? 'bg-gray-400'
: 'bg-blue-600'
}`}
>
{isCompressing ? (
<>
<Loader className="w-4 h-4 animate-spin" />
Compressing...
</>
) : isUploading ? (
<>
<Loader className="w-4 h-4 animate-spin" />
Uploading...
</>
) : (
'Upload Proof'
)}
</button>
</div>

{uploadResult && (
<div className="mt-3 text-sm text-gray-700">{uploadResult}</div>
)}
</div>
);
}
7 changes: 1 addition & 6 deletions features/escrow/components/EscrowLock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,7 @@ export function EscrowLock({
const [state, setState] = useState<LockState>('idle');
const { isLoading, error, escrowId, transactionHash, lockEscrow, reset } =
useEscrowLock();
const {
error: toastError,
success: toastSuccess,
loading: toastLoading,
info: toastInfo,
} = useToast();
const { toast } = useToast();

const isWalletConnected = !!walletAddress;
const formattedAmount = amount.toFixed(2);
Expand Down
1 change: 1 addition & 0 deletions features/escrow/components/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { EscrowLock } from './EscrowLock';
21 changes: 21 additions & 0 deletions hooks/useImageCompressor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { useCallback, useState } from 'react';
import { imageCompressionService } from '@/services/imageCompressionService';

export function useImageCompressor() {
const [isCompressing, setIsCompressing] = useState(false);

const compress = useCallback(async (file: File, targetKB = 500) => {
setIsCompressing(true);
try {
const compressed = await imageCompressionService.compressImage(
file,
targetKB
);
return compressed;
} finally {
setIsCompressing(false);
}
}, []);

return { compress, isCompressing } as const;
}
62 changes: 45 additions & 17 deletions hooks/useNetworkCheck.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
import { useState, useCallback, useEffect, useRef } from 'react';
import { networkService, NetworkInfo } from '@/services/networkService';
import { networkService } from '@/services/networkService';

// Minimal local type for the network info returned by the backend.
// Keep this small to avoid coupling to a non-exported service type.
type NetworkInfo = {
network: string;
chainId?: number | null;
};

/** How often to re-check the network while a wallet is connected (ms). */
const POLL_INTERVAL_MS = 10_000;

export type NetworkStatus =
| 'idle' // no wallet connected
| 'loading' // first fetch in progress
| 'match' // wallet network matches .env config
| 'mismatch' // wallet is on the wrong network
| 'error'; // fetch failed
| 'idle' // no wallet connected
| 'loading' // first fetch in progress
| 'match' // wallet network matches .env config
| 'mismatch' // wallet is on the wrong network
| 'error'; // fetch failed

/**
* useNetworkCheck — polls the backend to detect whether the connected wallet
Expand All @@ -23,8 +30,7 @@ export type NetworkStatus =
* - `recheck` — manually trigger a re-check immediately
*/
export function useNetworkCheck(address: string | null) {
const expectedNetwork =
process.env.NEXT_PUBLIC_STELLAR_NETWORK ?? 'testnet';
const expectedNetwork = process.env.NEXT_PUBLIC_STELLAR_NETWORK ?? 'testnet';

const [walletNetwork, setWalletNetwork] = useState<NetworkInfo | null>(null);
const [status, setStatus] = useState<NetworkStatus>('idle');
Expand All @@ -33,9 +39,8 @@ export function useNetworkCheck(address: string | null) {

const check = useCallback(async () => {
if (!address) {

setStatus('idle');

setWalletNetwork(null);
return;
}
Expand All @@ -47,18 +52,41 @@ export function useNetworkCheck(address: string | null) {
try {
const response = await networkService.getWalletNetwork(address);
if (response.success && response.data) {
setWalletNetwork(response.data);
const incoming: NetworkInfo = response.data;

// Determine match based on configured expected network (case-insensitive).
const match =
response.data.network.toLowerCase() ===
expectedNetwork.toLowerCase();
setStatus(match ? 'match' : 'mismatch');
incoming.network?.toLowerCase() === expectedNetwork.toLowerCase();

// Only update state when something actually changed. Use functional
// updates to avoid reading stale values from the closure and to
// prevent setting new object identities when the logical data is
// identical — this prevents unnecessary re-renders (the likely
// cause of the infinite loop reported).
setWalletNetwork((prev) => {
if (
prev &&
prev.network?.toLowerCase() === incoming.network?.toLowerCase() &&
(prev.chainId ?? null) === (incoming.chainId ?? null)
) {
return prev;
}
return incoming;
});

setStatus((prev) => {
const newStatus: typeof status = match ? 'match' : 'mismatch';
if (prev === newStatus) return prev;
return newStatus;
});
} else {
setError(response.message || 'Failed to check network');
setStatus('error');
}
} catch (err: any) {
setError(
err.response?.data?.message || 'An error occurred while checking network'
err.response?.data?.message ||
'An error occurred while checking network'
);
setStatus('error');
}
Expand All @@ -71,7 +99,7 @@ export function useNetworkCheck(address: string | null) {
if (!address) {
// eslint-disable-next-line react-hooks/set-state-in-effect
setStatus('idle');

setWalletNetwork(null);
return;
}
Expand All @@ -91,4 +119,4 @@ export function useNetworkCheck(address: string | null) {
error,
recheck: check,
};
}
}
29 changes: 18 additions & 11 deletions hooks/useProofUpload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@ interface UseProofUploadReturn {
isCompressing: boolean;
uploadedProofs: UploadedProof[];
errors: string[];

handleFileCapture: (_file: File) => Promise<void>;

handleCameraCapture: (_canvas: HTMLCanvasElement) => Promise<void>;
clearUploadedProofs: () => void;

removeProof: (_filename: string) => void;
}

Expand All @@ -51,7 +51,13 @@ export function useProofUpload({
const [isCompressing, setIsCompressing] = useState(false);
const [uploadedProofs, setUploadedProofs] = useState<UploadedProof[]>([]);
const [errors, setErrors] = useState<string[]>([]);
const { error: toastError, success: toastSuccess, loading: toastLoading, info: toastInfo } = useToast();
const {
error: toastError,
success: toastSuccess,
loading: toastLoading,
info: toastInfo,
toast,
} = useToast();

const handleFileCapture = useCallback(
async (file: File) => {
Expand Down Expand Up @@ -93,8 +99,11 @@ export function useProofUpload({
},
};

const { file: compressedFile, isCompressed, originalSize } =
await proofService.compressImage(file, compressionOptions);
const {
file: compressedFile,
isCompressed,
originalSize,
} = await proofService.compressImage(file, compressionOptions);

setIsCompressing(false);
setCompressionProgress(100);
Expand Down Expand Up @@ -189,11 +198,9 @@ export function useProofUpload({

// Create File from blob
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const file = new File(
[blob],
`proof-${timestamp}.jpg`,
{ type: 'image/jpeg' }
);
const file = new File([blob], `proof-${timestamp}.jpg`, {
type: 'image/jpeg',
});

// Upload the captured image
await handleFileCapture(file);
Expand Down
Loading
Loading