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
5 changes: 4 additions & 1 deletion .github/workflows/frontend-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ jobs:
- name: Run TypeScript type-check
run: npm run type-check

- name: Run unit tests
run: npm run test

- name: Run Next.js build
env:
NEXT_PUBLIC_API_URL: http://localhost:6003/api
Expand Down Expand Up @@ -91,4 +94,4 @@ jobs:
with:
name: playwright-traces
path: frontend/test-results
retention-days: 7
retention-days: 7
49 changes: 38 additions & 11 deletions frontend/app/scan/[assetId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

import { useParams, useRouter } from "next/navigation";
import { useState, useEffect } from "react";
import { Package, ExternalLink, AlertTriangle, LogIn } from "lucide-react";
import { Package, AlertTriangle, LogIn, WifiOff } from "lucide-react";
import { Button } from "@/components/ui/button";
import { StatusBadge } from "@/components/assets/status-badge";
import axios from "axios";

interface PublicAsset {
id: string;
Expand All @@ -17,23 +18,29 @@ interface PublicAsset {
contactEmail?: string;
}

const API_BASE = process.env.NEXT_PUBLIC_API_URL || "http://localhost:6003/api";

export default function ScanPage() {
const params = useParams();
const router = useRouter();
const assetId = params.assetId as string;
const [asset, setAsset] = useState<PublicAsset | null>(null);
const [loading, setLoading] = useState(true);
const [notFound, setNotFound] = useState(false);
const [error, setError] = useState(false);

useEffect(() => {
setLoading(true);
fetch(`/api/assets/scan/${assetId}`)
.then((r) => {
if (!r.ok) throw new Error("Not found");
return r.json();
axios
.get<PublicAsset>(`${API_BASE}/assets/scan/${assetId}`)
.then((r) => setAsset(r.data?.data ?? r.data))
.catch((err) => {
if (err.response?.status === 404) {
setNotFound(true);
} else {
setError(true);
}
})
.then((body) => setAsset(body?.data ?? body))
.catch(() => setNotFound(true))
.finally(() => setLoading(false));
}, [assetId]);

Expand All @@ -45,6 +52,25 @@ export default function ScanPage() {
);
}

if (error) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 p-4">
<div className="text-center max-w-sm">
<div className="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4">
<WifiOff className="w-8 h-8 text-red-400" />
</div>
<h1 className="text-xl font-semibold text-gray-900 mb-2">Connection Error</h1>
<p className="text-gray-500 text-sm mb-4">
Could not reach the server. Please check your connection and try again.
</p>
<Button variant="outline" onClick={() => window.location.reload()}>
Retry
</Button>
</div>
</div>
);
}

if (notFound || !asset) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 p-4">
Expand All @@ -56,9 +82,12 @@ export default function ScanPage() {
<p className="text-gray-500 text-sm mb-4">
This QR code does not match any registered asset in our system.
</p>
<p className="text-xs text-gray-400">
<p className="text-xs text-gray-400 mb-4">
If you found this asset, please contact the owner for assistance.
</p>
<Button variant="outline" size="sm" onClick={() => router.push("/login")}>
<LogIn className="w-4 h-4 mr-2" /> Log in for more options
</Button>
</div>
</div>
);
Expand All @@ -67,7 +96,6 @@ export default function ScanPage() {
return (
<div className="min-h-screen bg-gray-50 p-4">
<div className="max-w-md mx-auto">
{/* Public view — minimal safe info */}
<div className="bg-white rounded-2xl shadow-sm border p-6 mb-4">
{asset.imageUrl && (
<img src={asset.imageUrl} alt={asset.name} className="w-full h-48 object-cover rounded-xl mb-4" />
Expand All @@ -90,7 +118,6 @@ export default function ScanPage() {
)}
</div>

{/* Quick actions */}
<div className="bg-white rounded-2xl shadow-sm border p-4 space-y-2">
<Button variant="outline" className="w-full justify-start" onClick={() => router.push("/login")}>
<LogIn className="w-4 h-4 mr-2" /> Log in for more options
Expand All @@ -110,4 +137,4 @@ export default function ScanPage() {
</div>
</div>
);
}
}
Loading
Loading