mirror of
https://github.com/SirBlobby/Hoya26.git
synced 2026-02-04 03:34:34 -05:00
Docker Update and Fixes
This commit is contained in:
@@ -19,6 +19,8 @@
|
||||
let useCamera = $state(true);
|
||||
let fileInputElement = $state<HTMLInputElement>();
|
||||
|
||||
let alternatives = $state<any[]>([]);
|
||||
|
||||
onMount(() => {
|
||||
const initCamera = async () => {
|
||||
if (useCamera) {
|
||||
@@ -43,6 +45,36 @@
|
||||
};
|
||||
});
|
||||
|
||||
async function analyzeImage(imageBase64: string) {
|
||||
try {
|
||||
const payload = {
|
||||
product_name: "Scanned Item",
|
||||
description: "Scanned via camera",
|
||||
report_type: "product",
|
||||
image: imageBase64,
|
||||
user_id: localStorage.getItem("ethix_user_id") || "anonymous",
|
||||
is_public: false,
|
||||
};
|
||||
|
||||
const response = await fetch("/api/incidents/submit", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status === "success" && data.analysis) {
|
||||
return data;
|
||||
} else {
|
||||
throw new Error(data.message || "Analysis failed");
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Analysis error:", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function playSuccessSound() {
|
||||
const audio = new Audio("/report completed.mp3");
|
||||
audio.volume = 0.5;
|
||||
@@ -55,7 +87,7 @@
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
reader.onload = async (e) => {
|
||||
const imageData = e.target?.result as string;
|
||||
capturedImage = imageData;
|
||||
analyzing = true;
|
||||
@@ -73,7 +105,6 @@
|
||||
analyzing = false;
|
||||
showResult = true;
|
||||
resultTranslateY = 0;
|
||||
playSuccessSound();
|
||||
typeText();
|
||||
}, 1200);
|
||||
};
|
||||
@@ -88,25 +119,26 @@
|
||||
canvas.width = videoElement.videoWidth;
|
||||
canvas.height = videoElement.videoHeight;
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
||||
if (ctx) {
|
||||
ctx.drawImage(videoElement, 0, 0);
|
||||
capturedImage = canvas.toDataURL("image/png");
|
||||
}
|
||||
const imageData = canvas.toDataURL("image/png");
|
||||
capturedImage = imageData;
|
||||
|
||||
setTimeout(() => {
|
||||
const newItem = {
|
||||
id: Date.now(),
|
||||
title: "Plastic Bottle",
|
||||
date: new Date().toLocaleString(),
|
||||
impact: "High",
|
||||
imageUri: capturedImage,
|
||||
};
|
||||
|
||||
onScanComplete(newItem);
|
||||
const result = await analyzeImage(imageData);
|
||||
processResult(result, imageData);
|
||||
} else {
|
||||
analyzing = false;
|
||||
}
|
||||
}
|
||||
|
||||
function processResult(data: any, imageUri: string) {
|
||||
analyzing = false;
|
||||
if (!data) {
|
||||
displayTitle = "Scan Failed";
|
||||
alternatives = [];
|
||||
showResult = true;
|
||||
resultTranslateY = 0;
|
||||
playSuccessSound();
|
||||
typeText();
|
||||
}, 1200);
|
||||
}
|
||||
@@ -164,7 +196,26 @@
|
||||
|
||||
<button
|
||||
class="mode-toggle-btn"
|
||||
onclick={() => (useCamera = !useCamera)}
|
||||
onclick={() => {
|
||||
useCamera = !useCamera;
|
||||
if (useCamera) {
|
||||
// Re-init camera if switching back
|
||||
const initCamera = async () => {
|
||||
try {
|
||||
stream =
|
||||
await navigator.mediaDevices.getUserMedia({
|
||||
video: { facingMode: "environment" },
|
||||
});
|
||||
if (videoElement) {
|
||||
videoElement.srcObject = stream;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Camera access denied:", err);
|
||||
}
|
||||
};
|
||||
initCamera();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#if useCamera}
|
||||
<Icon
|
||||
@@ -241,53 +292,36 @@
|
||||
|
||||
<h2 class="sheet-title">{displayTitle}</h2>
|
||||
|
||||
<p class="alternatives-label">Top Sustainable Alternatives</p>
|
||||
|
||||
<div class="alternatives-scroll">
|
||||
<button class="alternative-card glass-bottle">
|
||||
<div class="alt-header">
|
||||
<Icon
|
||||
icon="ri:cup-fill"
|
||||
width="24"
|
||||
style="color: #60a5fa;"
|
||||
/>
|
||||
<span class="rating">★ 4.9</span>
|
||||
</div>
|
||||
<h3 class="alt-name">Glass Bottle</h3>
|
||||
<p class="alt-price">$2.49</p>
|
||||
</button>
|
||||
|
||||
<button class="alternative-card boxed-water">
|
||||
<div class="alt-header">
|
||||
<Icon
|
||||
icon="ri:box-3-fill"
|
||||
width="24"
|
||||
style="color: #a78bfa;"
|
||||
/>
|
||||
<span class="rating">★ 4.7</span>
|
||||
</div>
|
||||
<h3 class="alt-name">Boxed Water</h3>
|
||||
<p class="alt-price">$1.89</p>
|
||||
</button>
|
||||
|
||||
<button class="alternative-card aluminum">
|
||||
<div class="alt-header">
|
||||
<Icon
|
||||
icon="ri:beer-fill"
|
||||
width="24"
|
||||
style="color: #9ca3af;"
|
||||
/>
|
||||
<span class="rating">★ 4.5</span>
|
||||
</div>
|
||||
<h3 class="alt-name">Aluminum</h3>
|
||||
<p class="alt-price">$1.29</p>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button class="report-btn" onclick={handleClose}>
|
||||
<Icon icon="ri:alarm-warning-fill" width="20" />
|
||||
<span>Report Greenwashing</span>
|
||||
</button>
|
||||
{#if alternatives && alternatives.length > 0}
|
||||
<p class="alternatives-label">
|
||||
Sustainable Alternatives
|
||||
</p>
|
||||
<div class="alternatives-scroll">
|
||||
{#each alternatives as alt}
|
||||
<div class="alternative-card">
|
||||
<div class="alt-header">
|
||||
<Icon
|
||||
icon="ri:leaf-fill"
|
||||
width="24"
|
||||
style="color: #4ade80;"
|
||||
/>
|
||||
{#if alt.impact_reduction}
|
||||
<span class="rating">Better</span>
|
||||
{/if}
|
||||
</div>
|
||||
<p class="alt-name">{alt.name}</p>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else if displayTitle !== "Scan Failed"}
|
||||
<p class="alternatives-label" style="color: #4ade80;">
|
||||
✓ No greenwashing concerns found
|
||||
</p>
|
||||
{:else}
|
||||
<p class="alternatives-label" style="color: #f87171;">
|
||||
Could not analyze this image. Please try again.
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -382,10 +416,11 @@
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
background-color: white;
|
||||
border: 6px solid rgba(255, 255, 255, 0.3);
|
||||
border: 6px solid rgba(74, 222, 128, 0.5);
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s;
|
||||
background-clip: padding-box;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.shutter-button:active {
|
||||
|
||||
@@ -14,15 +14,174 @@
|
||||
];
|
||||
|
||||
interface ScanItem {
|
||||
id: number;
|
||||
id: string;
|
||||
name: string;
|
||||
date: string;
|
||||
severity: string;
|
||||
image: string;
|
||||
image: string | null;
|
||||
impact: string;
|
||||
alternatives: string[];
|
||||
is_greenwashing: boolean;
|
||||
is_public: boolean;
|
||||
}
|
||||
|
||||
let scanHistory = $state<ScanItem[]>([]);
|
||||
let selectedScan = $state<ScanItem | null>(null);
|
||||
let userId = $state("");
|
||||
let fileInput: HTMLInputElement;
|
||||
let isScanning = $state(false);
|
||||
|
||||
onMount(() => {
|
||||
let storedId = localStorage.getItem("ethix_user_id");
|
||||
if (!storedId) {
|
||||
storedId = crypto.randomUUID();
|
||||
localStorage.setItem("ethix_user_id", storedId);
|
||||
}
|
||||
userId = storedId;
|
||||
|
||||
fetchHistory();
|
||||
|
||||
// Listen for scan-complete event to refresh history
|
||||
const handleScanComplete = () => {
|
||||
fetchHistory();
|
||||
};
|
||||
window.addEventListener("scan-complete", handleScanComplete);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("scan-complete", handleScanComplete);
|
||||
};
|
||||
});
|
||||
|
||||
async function fetchHistory() {
|
||||
if (!userId) return;
|
||||
try {
|
||||
const res = await fetch(
|
||||
`/api/incidents/history?user_id=${userId}`,
|
||||
);
|
||||
const data = await res.json();
|
||||
if (Array.isArray(data)) {
|
||||
scanHistory = data.map((item: any) => ({
|
||||
id: item._id,
|
||||
name: item.product_name,
|
||||
date: new Date(item.created_at).toLocaleDateString(),
|
||||
severity: item.analysis?.severity || "Low",
|
||||
image: item.image_base64
|
||||
? `data:image/jpeg;base64,${item.image_base64}`
|
||||
: null,
|
||||
impact: item.analysis?.verdict || "No impact assessment",
|
||||
alternatives: item.analysis?.alternatives || [],
|
||||
is_greenwashing: item.analysis?.is_greenwashing || false,
|
||||
is_public: item.is_public || false,
|
||||
}));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to fetch history", e);
|
||||
}
|
||||
}
|
||||
|
||||
function triggerScan() {
|
||||
if (isScanning) return;
|
||||
fileInput.click();
|
||||
}
|
||||
|
||||
async function handleFileSelect(e: Event) {
|
||||
const target = e.target as HTMLInputElement;
|
||||
if (!target.files || target.files.length === 0) return;
|
||||
|
||||
const file = target.files[0];
|
||||
isScanning = true;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = async () => {
|
||||
const base64String = reader.result as string;
|
||||
await submitScan(base64String);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
|
||||
async function submitScan(imageBase64: string) {
|
||||
try {
|
||||
const payload = {
|
||||
product_name: "Scanned Item",
|
||||
description: "Scanned via mobile app",
|
||||
report_type: "product",
|
||||
image: imageBase64,
|
||||
user_id: userId,
|
||||
is_public: false, // Default private
|
||||
};
|
||||
|
||||
const res = await fetch(
|
||||
"/api/incidents/submit",
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
},
|
||||
);
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (data.status === "success") {
|
||||
await fetchHistory();
|
||||
selectedScan = {
|
||||
id: data.incident_id,
|
||||
name:
|
||||
data.detected_brand !== "Unknown"
|
||||
? data.detected_brand
|
||||
: "Scanned Product",
|
||||
date: "Just now",
|
||||
severity: data.analysis.severity,
|
||||
image: imageBase64,
|
||||
impact: data.analysis.verdict,
|
||||
alternatives: data.analysis.alternatives || [],
|
||||
is_greenwashing: data.is_greenwashing,
|
||||
is_public: false,
|
||||
};
|
||||
} else {
|
||||
alert("Analysis failed: " + (data.message || "Unknown error"));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Scan failed", e);
|
||||
alert("Analysis failed. Please try again.");
|
||||
} finally {
|
||||
isScanning = false;
|
||||
if (fileInput) fileInput.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleVisibility() {
|
||||
if (!selectedScan) return;
|
||||
const newState = !selectedScan.is_public;
|
||||
|
||||
// Optimistic UI update for modal
|
||||
selectedScan.is_public = newState;
|
||||
|
||||
// Optimistic UI update for list
|
||||
const histIndex = scanHistory.findIndex(
|
||||
(s) => s.id === selectedScan!.id,
|
||||
);
|
||||
if (histIndex !== -1) {
|
||||
scanHistory[histIndex].is_public = newState;
|
||||
}
|
||||
|
||||
try {
|
||||
await fetch(
|
||||
`/api/incidents/${selectedScan.id}/visibility`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ is_public: newState }),
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
// Revert on failure
|
||||
selectedScan.is_public = !newState;
|
||||
if (histIndex !== -1) {
|
||||
scanHistory[histIndex].is_public = !newState;
|
||||
}
|
||||
alert("Failed to update visibility");
|
||||
}
|
||||
}
|
||||
|
||||
function openScan(scan: ScanItem) {
|
||||
selectedScan = scan;
|
||||
@@ -32,69 +191,83 @@
|
||||
selectedScan = null;
|
||||
}
|
||||
|
||||
let greeting = $state("Hello");
|
||||
|
||||
onMount(() => {
|
||||
const hour = new Date().getHours();
|
||||
if (hour < 12) greeting = "Good morning";
|
||||
else if (hour < 17) greeting = "Good afternoon";
|
||||
else greeting = "Good evening";
|
||||
});
|
||||
|
||||
function getSeverityClass(severity: string): string {
|
||||
return severity.toLowerCase();
|
||||
return severity?.toLowerCase() || "low";
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="min-h-screen bg-[#051f18] text-white px-5 pt-4 pb-32">
|
||||
<section class="flex gap-3 mb-7">
|
||||
<button
|
||||
class="flex-1 flex flex-col items-center gap-2 py-4 px-2 bg-[#0d2e25] border border-emerald-500/30 rounded-2xl active:scale-95 transition-all shadow-[0_0_15px_rgba(16,185,129,0.1)] relative overflow-hidden group min-h-[100px] justify-center"
|
||||
onclick={triggerScan}
|
||||
disabled={isScanning}
|
||||
>
|
||||
<div
|
||||
class="absolute inset-0 bg-emerald-500/5 group-hover:bg-emerald-500/10 transition-colors"
|
||||
></div>
|
||||
<div
|
||||
class="w-10 h-10 bg-emerald-500/20 rounded-xl flex items-center justify-center text-emerald-400 relative z-10"
|
||||
>
|
||||
{#if isScanning}
|
||||
<Icon
|
||||
icon="ri:loader-4-line"
|
||||
width="24"
|
||||
class="animate-spin"
|
||||
/>
|
||||
{:else}
|
||||
<Icon icon="ri:camera-lens-fill" width="24" />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex flex-col items-center relative z-10">
|
||||
<span class="text-sm font-extrabold text-white"
|
||||
>{isScanning ? "Analyzing..." : "Scan Item"}</span
|
||||
>
|
||||
<span
|
||||
class="text-[10px] text-emerald-400/80 uppercase tracking-wide font-semibold"
|
||||
>AI Detection</span
|
||||
>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div
|
||||
class="flex-1 flex flex-col items-center gap-2 py-4 px-2 bg-[#0d2e25] border border-[#1f473b] rounded-2xl"
|
||||
class="flex-1 flex flex-col items-center gap-2 py-4 px-2 bg-[#0d2e25] border border-[#1f473b] rounded-2xl min-h-[100px] justify-center"
|
||||
>
|
||||
<div
|
||||
class="w-9 h-9 bg-emerald-500/10 rounded-xl flex items-center justify-center"
|
||||
>
|
||||
<Icon
|
||||
icon="ri:qr-scan-2-line"
|
||||
width="20"
|
||||
class="text-emerald-400"
|
||||
/>
|
||||
<Icon icon="ri:leaf-fill" width="20" class="text-emerald-400" />
|
||||
</div>
|
||||
<span class="text-lg font-extrabold text-white">47</span>
|
||||
<span class="text-lg font-extrabold text-white"
|
||||
>{scanHistory.length}</span
|
||||
>
|
||||
<span
|
||||
class="text-[10px] text-gray-400 uppercase tracking-wide font-semibold"
|
||||
>scans</span
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
class="flex-1 flex flex-col items-center gap-2 py-4 px-2 bg-[#0d2e25] border border-[#1f473b] rounded-2xl"
|
||||
>
|
||||
<div
|
||||
class="w-9 h-9 bg-emerald-500/10 rounded-xl flex items-center justify-center"
|
||||
>
|
||||
<Icon
|
||||
icon="ri:checkbox-circle-fill"
|
||||
width="20"
|
||||
class="text-emerald-400"
|
||||
/>
|
||||
</div>
|
||||
<span class="text-lg font-extrabold text-white">32</span>
|
||||
<span
|
||||
class="text-[10px] text-gray-400 uppercase tracking-wide font-semibold"
|
||||
>eco picks</span
|
||||
>Total Scans</span
|
||||
>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="recent-section">
|
||||
<div class="mb-4">
|
||||
<h2 class="text-base font-extrabold text-white m-0">Your Scans</h2>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2.5">
|
||||
{#if scanHistory.length === 0}
|
||||
<div class="text-center py-10 text-gray-500 text-sm">
|
||||
No scans yet. Start by scanning a product!
|
||||
</div>
|
||||
{/if}
|
||||
{#each scanHistory as item (item.id)}
|
||||
<button
|
||||
class="flex items-center gap-3.5 p-5 bg-[#0d2e25] border border-[#1f473b] rounded-2xl cursor-pointer w-full text-left transition-transform active:scale-98 active:bg-[#1f473b]"
|
||||
onclick={() => openScan(item)}
|
||||
aria-label="View {item.name}"
|
||||
>
|
||||
<div class="w-12 h-12 rounded-xl overflow-hidden shrink-0">
|
||||
<div
|
||||
class="w-12 h-12 rounded-xl overflow-hidden shrink-0 bg-black"
|
||||
>
|
||||
{#if item.image}
|
||||
<img
|
||||
src={item.image}
|
||||
@@ -110,7 +283,9 @@
|
||||
? '#fca5a5'
|
||||
: '#86efac'}"
|
||||
>
|
||||
<span class="text-lg">{item.name[0]}</span>
|
||||
<span class="text-lg"
|
||||
>{item.name ? item.name[0] : "?"}</span
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -118,7 +293,17 @@
|
||||
<span class="text-sm font-semibold text-white truncate"
|
||||
>{item.name}</span
|
||||
>
|
||||
<span class="text-xs text-gray-400">{item.date}</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs text-gray-400"
|
||||
>{item.date}</span
|
||||
>
|
||||
{#if item.is_public}
|
||||
<span
|
||||
class="text-[9px] px-1.5 py-0.5 rounded bg-emerald-500/20 text-emerald-400 font-bold border border-emerald-500/30"
|
||||
>PUBLIC</span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
class="px-2.5 py-1 rounded-full text-[10px] font-bold uppercase tracking-wide {item.severity.toLowerCase() ===
|
||||
@@ -137,19 +322,17 @@
|
||||
|
||||
{#if selectedScan}
|
||||
<div
|
||||
class="fixed inset-0 bg-black/70 backdrop-blur-sm z-50 flex items-center justify-center p-5"
|
||||
class="fixed inset-0 bg-black/70 backdrop-blur-sm z-[200] flex items-start justify-center p-4 pt-8 pb-4 overflow-y-auto"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
onkeydown={(e) => e.key === "Escape" && closeScan()}
|
||||
onclick={closeScan}
|
||||
onkeydown={(e) => e.key === "Escape" && closeScan()}
|
||||
>
|
||||
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
||||
<div
|
||||
class="bg-[#0d2e25] border border-[#1f473b] rounded-3xl w-full max-w-[340px] p-6 relative shadow-[0_20px_50px_rgba(0,0,0,0.5)]"
|
||||
class="bg-[#0d2e25] border border-[#1f473b] rounded-3xl w-full max-w-[340px] p-5 relative shadow-[0_20px_50px_rgba(0,0,0,0.5)] mb-4"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
role="document"
|
||||
tabindex="-1"
|
||||
onkeydown={(e) => e.key === "Escape" && closeScan()}
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<button
|
||||
class="absolute top-4 right-4 bg-transparent border-none text-gray-400 cursor-pointer"
|
||||
@@ -157,7 +340,7 @@
|
||||
>
|
||||
<Icon icon="ri:close-fill" width="24" />
|
||||
</button>
|
||||
<div class="text-center mb-5">
|
||||
<div class="text-center mb-4">
|
||||
<span
|
||||
class="text-xs text-gray-400 uppercase tracking-widest"
|
||||
>{selectedScan.date}</span
|
||||
@@ -165,7 +348,7 @@
|
||||
<h2 class="text-xl text-white m-0 mt-1">Scan Report</h2>
|
||||
</div>
|
||||
<div
|
||||
class="w-full h-[200px] bg-black rounded-2xl mb-5 overflow-hidden"
|
||||
class="w-full h-[160px] bg-black rounded-2xl mb-4 overflow-hidden"
|
||||
>
|
||||
{#if selectedScan.image}
|
||||
<img
|
||||
@@ -183,7 +366,9 @@
|
||||
: '#86efac'}"
|
||||
>
|
||||
<span class="text-6xl text-[#051f18] font-black"
|
||||
>{selectedScan.name[0]}</span
|
||||
>{selectedScan.name
|
||||
? selectedScan.name[0]
|
||||
: "?"}</span
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -192,19 +377,64 @@
|
||||
<h3 class="text-lg text-white m-0 mb-1">
|
||||
{selectedScan.name}
|
||||
</h3>
|
||||
<p class="text-gray-300 text-sm mb-3">
|
||||
|
||||
<div class="flex justify-center gap-2 my-4">
|
||||
<div
|
||||
class="inline-block px-3 py-1.5 rounded-full text-xs font-bold uppercase {selectedScan.severity.toLowerCase() ===
|
||||
'high'
|
||||
? 'bg-red-400/20 text-red-400'
|
||||
: selectedScan.severity.toLowerCase() ===
|
||||
'medium'
|
||||
? 'bg-orange-400/20 text-orange-400'
|
||||
: 'bg-emerald-400/20 text-emerald-400'}"
|
||||
>
|
||||
Severity: {selectedScan.severity}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Visibility Toggle -->
|
||||
<button
|
||||
class="mb-4 flex items-center justify-center gap-2 px-4 py-2 rounded-xl text-xs font-bold w-full border {selectedScan.is_public
|
||||
? 'bg-emerald-500/20 text-emerald-400 border-emerald-500/50'
|
||||
: 'bg-[#051f18] text-gray-400 border-[#1f473b]'}"
|
||||
onclick={toggleVisibility}
|
||||
>
|
||||
<div
|
||||
class="w-3 h-3 rounded-full {selectedScan.is_public
|
||||
? 'bg-emerald-400'
|
||||
: 'bg-gray-500'}"
|
||||
></div>
|
||||
{selectedScan.is_public
|
||||
? "PUBLIC REPORT (Visible to everyone)"
|
||||
: "PRIVATE REPORT (Only you)"}
|
||||
</button>
|
||||
|
||||
<p class="text-gray-300 text-sm mb-4 leading-relaxed">
|
||||
{selectedScan.impact}
|
||||
</p>
|
||||
<div
|
||||
class="inline-block px-3 py-1.5 rounded-full text-xs font-bold uppercase {selectedScan.severity.toLowerCase() ===
|
||||
'high'
|
||||
? 'bg-red-400/20 text-red-400'
|
||||
: selectedScan.severity.toLowerCase() === 'medium'
|
||||
? 'bg-orange-400/20 text-orange-400'
|
||||
: 'bg-emerald-400/20 text-emerald-400'}"
|
||||
>
|
||||
Severity: {selectedScan.severity}
|
||||
</div>
|
||||
|
||||
{#if selectedScan.alternatives && selectedScan.alternatives.length > 0}
|
||||
<div
|
||||
class="mt-6 text-left bg-emerald-900/20 p-4 rounded-xl border border-emerald-500/20 mb-2"
|
||||
>
|
||||
<h4
|
||||
class="text-emerald-400 text-xs font-bold uppercase tracking-widest mb-3 flex items-center gap-2"
|
||||
>
|
||||
<Icon icon="ri:leaf-line" />
|
||||
Sustainable Alternatives
|
||||
</h4>
|
||||
<ul class="text-sm text-gray-300 space-y-2 pl-1">
|
||||
{#each selectedScan.alternatives as alt}
|
||||
<li class="flex items-start gap-2">
|
||||
<span class="text-emerald-500 mt-1"
|
||||
>•</span
|
||||
>
|
||||
<span>{alt}</span>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
const PATH_CONFIG: Record<string, SceneConfig> = {
|
||||
"/": { sceneType: "transition", staticScene: false },
|
||||
"/chat": { sceneType: "oilRig", staticScene: true },
|
||||
"/community": { sceneType: "forest", staticScene: true },
|
||||
"/report": { sceneType: "industrial", staticScene: true },
|
||||
"/goal": { sceneType: "pollutedCity", staticScene: true },
|
||||
"/report": { sceneType: "deforestation", staticScene: true },
|
||||
"/catalogue": {
|
||||
sceneType: "transition",
|
||||
staticScene: false,
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
async function fetchStats() {
|
||||
try {
|
||||
const res = await fetch("http://localhost:5000/api/reports/stats");
|
||||
const res = await fetch("/api/reports/stats");
|
||||
const data = await res.json();
|
||||
statsData = data;
|
||||
} catch (e) {
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
.toLowerCase()
|
||||
.endsWith(".txt")}
|
||||
<iframe
|
||||
src="http://localhost:5000/api/reports/view/{report.filename}"
|
||||
src="/api/reports/view/{report.filename}"
|
||||
class="w-full h-full border-none"
|
||||
title="Report Viewer"
|
||||
></iframe>
|
||||
@@ -74,7 +74,7 @@
|
||||
/>
|
||||
<p>Preview not available for this file type.</p>
|
||||
<a
|
||||
href="http://localhost:5000/api/reports/view/{report.filename}"
|
||||
href="/api/reports/view/{report.filename}"
|
||||
download
|
||||
class="bg-emerald-400 text-slate-900 px-6 py-3 rounded-full no-underline font-bold transition-transform hover:scale-105"
|
||||
>
|
||||
|
||||
52
frontend/src/lib/ts/api.ts
Normal file
52
frontend/src/lib/ts/api.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
|
||||
export const API_BASE = '/api';
|
||||
|
||||
export function apiUrl(path: string): string {
|
||||
// Ensure path starts with /
|
||||
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
|
||||
return `${API_BASE}${normalizedPath}`;
|
||||
}
|
||||
|
||||
export async function apiFetch<T = any>(
|
||||
path: string,
|
||||
options: RequestInit = {}
|
||||
): Promise<T> {
|
||||
const url = apiUrl(path);
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API Error: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
|
||||
export async function apiGet<T = any>(path: string): Promise<T> {
|
||||
return apiFetch<T>(path, { method: 'GET' });
|
||||
}
|
||||
|
||||
|
||||
export async function apiPost<T = any>(path: string, body: any): Promise<T> {
|
||||
return apiFetch<T>(path, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
}
|
||||
|
||||
export async function apiPut<T = any>(path: string, body: any): Promise<T> {
|
||||
return apiFetch<T>(path, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
}
|
||||
|
||||
export function reportViewUrl(filename: string): string {
|
||||
return `${API_BASE}/reports/view/${encodeURIComponent(filename)}`;
|
||||
}
|
||||
@@ -22,8 +22,9 @@ const SCENE_ELEMENTS: Record<Exclude<SceneType, 'transition'>, (dc: DrawContext)
|
||||
pollutedCity: drawPollutedCityScene,
|
||||
};
|
||||
|
||||
const CUSTOM_WATER_SCENES: SceneType[] = ['ocean', 'oilRig'];
|
||||
const NO_TERRAIN_SCENES: SceneType[] = ['ocean', 'oilRig'];
|
||||
const CUSTOM_WATER_SCENES: SceneType[] = ['ocean', 'oilRig', 'deforestation'];
|
||||
const NO_MOUNTAINS_SCENES: SceneType[] = ['ocean', 'oilRig', 'deforestation'];
|
||||
const NO_HILLS_SCENES: SceneType[] = ['ocean', 'oilRig'];
|
||||
|
||||
export function drawLandscape(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
@@ -40,11 +41,16 @@ export function drawLandscape(
|
||||
drawSun(dc);
|
||||
drawClouds(dc);
|
||||
|
||||
const skipTerrain = NO_TERRAIN_SCENES.includes(sceneType) ||
|
||||
(blendToScene && NO_TERRAIN_SCENES.includes(blendToScene) && (blendProgress ?? 0) > 0.5);
|
||||
const skipMountains = NO_MOUNTAINS_SCENES.includes(sceneType) ||
|
||||
(blendToScene && NO_MOUNTAINS_SCENES.includes(blendToScene) && (blendProgress ?? 0) > 0.5);
|
||||
|
||||
if (!skipTerrain) {
|
||||
const skipHills = NO_HILLS_SCENES.includes(sceneType) ||
|
||||
(blendToScene && NO_HILLS_SCENES.includes(blendToScene) && (blendProgress ?? 0) > 0.5);
|
||||
|
||||
if (!skipMountains) {
|
||||
drawMountains(dc);
|
||||
}
|
||||
if (!skipHills) {
|
||||
drawHills(dc);
|
||||
}
|
||||
|
||||
|
||||
@@ -19,8 +19,10 @@ export function drawSmoggyBuildings(dc: DrawContext): void {
|
||||
|
||||
for (let row = 0; row < windowRows; row++) {
|
||||
for (let col = 0; col < windowCols; col++) {
|
||||
const isBroken = Math.random() > 0.85;
|
||||
const isLit = Math.random() > 0.5;
|
||||
// Deterministic "randomness" based on position to stop flashing
|
||||
const seed = x + col * 13 + row * 71;
|
||||
const isBroken = Math.abs(Math.sin(seed)) > 0.85;
|
||||
const isLit = Math.cos(seed) > 0.1;
|
||||
|
||||
if (!isBroken) {
|
||||
ctx.fillStyle = isLit ? 'rgba(255, 180, 100, 0.5)' : 'rgba(50, 50, 50, 0.8)';
|
||||
|
||||
@@ -32,7 +32,14 @@
|
||||
|
||||
function handleScanComplete(item: any) {
|
||||
recentItems = [item, ...recentItems];
|
||||
// Don't close camera here - let user view the result sheet first
|
||||
// Camera will close when user clicks close button
|
||||
}
|
||||
|
||||
function handleCameraClose() {
|
||||
isCameraActive = false;
|
||||
// Dispatch event to notify MobileHomePage to refresh
|
||||
window.dispatchEvent(new CustomEvent('scan-complete'));
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
@@ -147,7 +154,7 @@
|
||||
|
||||
{#if isCameraActive}
|
||||
<CameraScreen
|
||||
onClose={() => (isCameraActive = false)}
|
||||
onClose={handleCameraClose}
|
||||
onScanComplete={handleScanComplete}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
async function fetchReports() {
|
||||
isLoading = true;
|
||||
try {
|
||||
const res = await fetch("http://localhost:5000/api/reports/");
|
||||
const res = await fetch("/api/reports/");
|
||||
const data = await res.json();
|
||||
if (Array.isArray(data)) reports = data;
|
||||
} catch (e) {
|
||||
@@ -79,7 +79,7 @@
|
||||
async function fetchIncidents() {
|
||||
isLoading = true;
|
||||
try {
|
||||
const res = await fetch("http://localhost:5000/api/incidents/list");
|
||||
const res = await fetch("/api/incidents/list");
|
||||
const data = await res.json();
|
||||
if (Array.isArray(data)) incidents = data;
|
||||
} catch (e) {
|
||||
@@ -103,7 +103,7 @@
|
||||
isLoading = true;
|
||||
try {
|
||||
const res = await fetch(
|
||||
"http://localhost:5000/api/reports/search",
|
||||
"/api/reports/search",
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
"http://localhost:5000/api/gemini/ask",
|
||||
"/api/gemini/ask",
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
|
||||
@@ -130,7 +130,7 @@
|
||||
if (reportType === "company") {
|
||||
// Use the new upload endpoint for company reports
|
||||
const response = await fetch(
|
||||
"http://localhost:5000/api/reports/upload",
|
||||
"/api/reports/upload",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
@@ -170,7 +170,7 @@
|
||||
} else {
|
||||
// Original product incident flow
|
||||
const response = await fetch(
|
||||
"http://localhost:5000/api/incidents/submit",
|
||||
"/api/incidents/submit",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
|
||||
Reference in New Issue
Block a user