Frontend Update

This commit is contained in:
Yousif Ali
2026-01-24 06:16:34 -05:00
parent 20070301ca
commit d6471afab2
20 changed files with 5036 additions and 190 deletions

View File

@@ -16,17 +16,19 @@
"dependencies": { "dependencies": {
"@tauri-apps/api": "^2.9.1", "@tauri-apps/api": "^2.9.1",
"@tauri-apps/plugin-opener": "^2.5.3", "@tauri-apps/plugin-opener": "^2.5.3",
"@types/three": "^0.182.0",
"compression": "^1.8.1", "compression": "^1.8.1",
"express": "^5.2.1" "express": "^5.2.1",
"three": "^0.182.0"
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-static": "^3.0.10", "@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.50.1", "@sveltejs/kit": "^2.50.1",
"@sveltejs/vite-plugin-svelte": "^5.1.1", "@sveltejs/vite-plugin-svelte": "^5.1.1",
"@tauri-apps/cli": "^2.9.6",
"svelte": "^5.48.0", "svelte": "^5.48.0",
"svelte-check": "^4.3.5", "svelte-check": "^4.3.5",
"typescript": "~5.6.3", "typescript": "~5.6.3",
"vite": "^6.4.1", "vite": "^6.4.1"
"@tauri-apps/cli": "^2.9.6"
} }
} }

View File

@@ -0,0 +1,608 @@
<script lang="ts">
import { onMount } from "svelte";
interface Props {
onClose: () => void;
onScanComplete: (item: any) => void;
}
let { onClose, onScanComplete }: Props = $props();
let videoElement: HTMLVideoElement;
let stream: MediaStream | null = null;
let analyzing = $state(false);
let showResult = $state(false);
let capturedImage = $state<string | null>(null);
let displayTitle = $state("");
let resultTranslateY = $state(100);
let useCamera = $state(true); // Toggle between camera and file upload
let fileInputElement: HTMLInputElement;
onMount(() => {
const initCamera = async () => {
if (useCamera) {
try {
stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: "environment" },
});
if (videoElement) {
videoElement.srcObject = stream;
}
} catch (err) {
console.error("Camera access denied:", err);
}
}
};
initCamera();
return () => {
if (stream) {
stream.getTracks().forEach((track) => track.stop());
}
};
});
function handleFileUpload(event: Event) {
const input = event.target as HTMLInputElement;
const file = input.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
const imageData = e.target?.result as string;
capturedImage = imageData;
analyzing = true;
// Simulate analysis
setTimeout(() => {
const newItem = {
id: Date.now(),
title: "Plastic Bottle",
date: new Date().toLocaleString(),
impact: "High",
imageUri: capturedImage,
};
onScanComplete(newItem);
analyzing = false;
showResult = true;
resultTranslateY = 0;
typeText();
}, 1200);
};
reader.readAsDataURL(file);
}
async function takePicture() {
analyzing = true;
const canvas = document.createElement("canvas");
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");
}
// Simulate analysis
setTimeout(() => {
const newItem = {
id: Date.now(),
title: "Plastic Bottle",
date: new Date().toLocaleString(),
impact: "High",
imageUri: capturedImage,
};
onScanComplete(newItem);
analyzing = false;
showResult = true;
resultTranslateY = 0;
typeText();
}, 1200);
}
function typeText() {
let i = 0;
const txt = "Detected: Plastic Bottle";
const interval = setInterval(() => {
displayTitle = txt.substring(0, i);
i++;
if (i > txt.length) clearInterval(interval);
}, 50);
}
function handleClose() {
if (stream) {
stream.getTracks().forEach((track) => track.stop());
}
onClose();
}
</script>
<div class="camera-container">
{#if capturedImage}
<img src={capturedImage} alt="Captured" class="captured-image" />
{:else if useCamera}
<video
bind:this={videoElement}
autoplay
playsinline
class="video-element"
></video>
{:else}
<!-- File upload placeholder -->
<div class="upload-placeholder">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
fill="#4ade80"
class="upload-icon"
>
<path
d="M194.6 32H317.4C338.1 32 356.4 45.22 362.9 64.82L373.3 96H448C483.3 96 512 124.7 512 160V416C512 451.3 483.3 480 448 480H64C28.65 480 0 451.3 0 416V160C0 124.7 28.65 96 64 96H138.7L149.1 64.82C155.6 45.22 173.9 32 194.6 32H194.6zM256 384C309 384 352 341 352 288C352 234.1 309 192 256 192C202.1 192 160 234.1 160 288C160 341 202.1 384 256 384z"
/>
</svg>
<p class="upload-text">Click the button below to upload an image</p>
</div>
{/if}
<div class="camera-overlay">
{#if !showResult}
<button class="close-btn" on:click={handleClose}>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
fill="white"
>
<path
d="M256 48C141.1 48 48 141.1 48 256s93.1 208 208 208 208-93.1 208-208S370.9 48 256 48zm52.7 283.3L256 278.6l-52.7 52.7c-6.2 6.2-16.4 6.2-22.6 0-3.1-3.1-4.7-7.2-4.7-11.3 0-4.1 1.6-8.2 4.7-11.3l52.7-52.7-52.7-52.7c-3.1-3.1-4.7-7.2-4.7-11.3 0-4.1 1.6-8.2 4.7-11.3 6.2-6.2 16.4-6.2 22.6 0l52.7 52.7 52.7-52.7c6.2-6.2 16.4-6.2 22.6 0 6.2 6.2 6.2 16.4 0 22.6L278.6 256l52.7 52.7c6.2 6.2 6.2 16.4 0 22.6-6.2 6.3-16.4 6.3-22.6 0z"
/>
</svg>
</button>
<!-- Toggle button between camera and upload -->
<button
class="mode-toggle-btn"
on:click={() => (useCamera = !useCamera)}
>
{#if useCamera}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
fill="white"
>
<path
d="M0 96C0 60.7 28.7 32 64 32H448c35.3 0 64 28.7 64 64V416c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V96zM323.8 202.5c-4.5-6.6-11.9-10.5-19.8-10.5s-15.4 3.9-19.8 10.5l-87 127.6L170.7 297c-4.6-5.7-11.5-9-18.7-9s-14.2 3.3-18.7 9l-64 80c-5.8 7.2-6.9 17.1-2.9 25.4s12.4 13.6 21.6 13.6h96 32H424c8.9 0 17.1-4.9 21.2-12.8s3.6-17.4-1.4-24.7l-120-176zM112 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z"
/>
</svg>
<span>Upload File</span>
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
fill="white"
>
<path
d="M194.6 32H317.4C338.1 32 356.4 45.22 362.9 64.82L373.3 96H448C483.3 96 512 124.7 512 160V416C512 451.3 483.3 480 448 480H64C28.65 480 0 451.3 0 416V160C0 124.7 28.65 96 64 96H138.7L149.1 64.82C155.6 45.22 173.9 32 194.6 32H194.6zM256 384C309 384 352 341 352 288C352 234.1 309 192 256 192C202.1 192 160 234.1 160 288C160 341 202.1 384 256 384z"
/>
</svg>
<span>Use Camera</span>
{/if}
</button>
{/if}
{#if analyzing}
<div class="loading-container">
<div class="spinner"></div>
</div>
{/if}
{#if !analyzing && !showResult}
<div class="shutter-container">
{#if useCamera}
<button class="shutter-button" on:click={takePicture}
></button>
{:else}
<input
type="file"
accept="image/*"
bind:this={fileInputElement}
on:change={handleFileUpload}
style="display: none;"
/>
<button
class="shutter-button upload-button"
on:click={() => fileInputElement.click()}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
fill="#0f172a"
>
<path
d="M288 109.3V352c0 17.7-14.3 32-32 32s-32-14.3-32-32V109.3l-73.4 73.4c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3l128-128c12.5-12.5 32.8-12.5 45.3 0l128 128c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L288 109.3zM64 352H192c0 35.3 28.7 64 64 64s64-28.7 64-64H448c35.3 0 64 28.7 64 64v32c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V416c0-35.3 28.7-64 64-64zM432 456a24 24 0 1 0 0-48 24 24 0 1 0 0 48z"
/>
</svg>
</button>
{/if}
</div>
{/if}
{#if showResult}
<div
class="result-sheet"
style="transform: translateY({resultTranslateY}%);"
>
<button class="sheet-close-btn" on:click={handleClose}>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 320 512"
fill="#0f172a"
>
<path
d="M310.6 361.4c12.5 12.5 12.5 32.75 0 45.25C304.4 412.9 296.2 416 288 416s-16.38-3.125-22.62-9.375L160 301.3L54.63 406.6C48.38 412.9 40.19 416 32 416S15.63 412.9 9.375 406.6c-12.5-12.5-12.5-32.75 0-45.25l105.4-105.4L9.375 150.6c-12.5-12.5-12.5-32.75 0-45.25s32.75-12.5 45.25 0L160 210.8l105.4-105.4c12.5-12.5 32.75-12.5 45.25 0s12.5 32.75 0 45.25l-105.4 105.4L310.6 361.4z"
/>
</svg>
</button>
<h2 class="sheet-title">{displayTitle}</h2>
<p class="alternatives-label">Top Sustainable Alternatives</p>
<div class="alternatives-scroll">
<div class="alternative-card glass-bottle">
<div class="alt-header">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 384 512"
fill="#60a5fa"
>
<path
d="M192 512C86 512 0 426 0 320C0 228.8 130.2 57.7 166.6 11.7C172.6 4.2 181.5 0 191.1 0h1.8c9.6 0 18.5 4.2 24.5 11.7C253.8 57.7 384 228.8 384 320c0 106-86 192-192 192zM96 336c0-8.8-7.2-16-16-16s-16 7.2-16 16c0 61.9 50.1 112 112 112 8.8 0 16-7.2 16-16s-7.2-16-16-16c-44.2 0-80-35.8-80-80z"
/>
</svg>
<span class="rating">★ 4.9</span>
</div>
<h3 class="alt-name">Glass Bottle</h3>
<p class="alt-price">$2.49</p>
</div>
<div class="alternative-card boxed-water">
<div class="alt-header">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 448 512"
fill="#a78bfa"
>
<path
d="M384 480H64c-35.3 0-64-28.7-64-64V96C0 60.7 28.7 32 64 32h320c35.3 0 64 28.7 64 64v320c0 35.3-28.7 64-64 64z"
/>
</svg>
<span class="rating">★ 4.7</span>
</div>
<h3 class="alt-name">Boxed Water</h3>
<p class="alt-price">$1.89</p>
</div>
<div class="alternative-card aluminum">
<div class="alt-header">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
fill="#9ca3af"
>
<path
d="M0 192c0-35.3 28.7-64 64-64c.5 0 1.1 0 1.6 0C73 91.5 105.3 64 144 64c15 0 29 4.1 40.9 11.2C198.2 49.6 225.1 32 256 32s57.8 17.6 71.1 43.2C339 68.1 353 64 368 64c38.7 0 71 27.5 78.4 64c.5 0 1.1 0 1.6 0c35.3 0 64 28.7 64 64c0 11.7-3.1 22.6-8.6 32H8.6C3.1 214.6 0 203.7 0 192zm0 91.4C0 268.3 12.3 256 27.4 256H484.6c15.1 0 27.4 12.3 27.4 27.4c0 70.5-44.4 130.7-106.7 154.1L403.5 452c-2 16-15.6 28-31.8 28H140.2c-16.1 0-29.8-12-31.8-28l-1.8-14.4C44.4 414.1 0 353.9 0 283.4z"
/>
</svg>
<span class="rating">★ 4.5</span>
</div>
<h3 class="alt-name">Aluminum</h3>
<p class="alt-price">$1.29</p>
</div>
</div>
<button class="report-btn" on:click={handleClose}>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 448 512"
fill="#ef4444"
>
<path
d="M64 32C64 14.3 49.7 0 32 0S0 14.3 0 32V64 368 480c0 17.7 14.3 32 32 32s32-14.3 32-32V352l64.3-16.1c41.1-10.3 84.6-5.5 122.5 13.4c44.2 22.1 95.5 24.8 141.7 7.4l34.7-13c12.5-4.7 20.8-16.6 20.8-30V66.1c0-23-24.2-38-44.8-27.7l-9.6 4.8c-46.3 23.2-100.8 23.2-147.1 0c-35.1-17.6-75.4-22-113.5-12.5L64 48V32z"
/>
</svg>
<span>Report Greenwashing</span>
</button>
</div>
{/if}
</div>
</div>
<style>
.camera-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100vh;
background-color: #000;
z-index: 1000;
}
.video-element,
.captured-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.camera-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.close-btn {
position: absolute;
top: 60px;
left: 20px;
background: none;
border: none;
cursor: pointer;
padding: 0;
z-index: 10;
}
.close-btn svg {
width: 40px;
height: 40px;
}
.loading-container {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.spinner {
width: 50px;
height: 50px;
border: 4px solid rgba(74, 222, 128, 0.2);
border-top-color: #4ade80;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.shutter-container {
position: absolute;
bottom: 50px;
width: 100%;
display: flex;
justify-content: center;
}
.shutter-button {
width: 80px;
height: 80px;
border-radius: 40px;
background-color: white;
border: 6px solid #e2e8f0;
cursor: pointer;
transition: transform 0.2s;
}
.shutter-button:active {
transform: scale(0.9);
}
.result-sheet {
position: absolute;
bottom: 0;
width: 100%;
height: 50%;
background-color: white;
border-top-left-radius: 32px;
border-top-right-radius: 32px;
padding: 32px;
transition: transform 0.5s ease-out;
overflow-y: auto;
}
.sheet-close-btn {
background: none;
border: none;
cursor: pointer;
padding: 0;
align-self: flex-end;
display: block;
margin-left: auto;
}
.sheet-close-btn svg {
width: 24px;
height: 24px;
}
.sheet-title {
font-size: 28px;
font-weight: 800;
margin-top: 12px;
margin-bottom: 24px;
color: #0f172a;
}
.alternatives-label {
color: #64748b;
margin-bottom: 12px;
font-weight: bold;
font-size: 14px;
}
.alternatives-scroll {
display: flex;
gap: 12px;
overflow-x: auto;
padding-bottom: 16px;
margin-bottom: 20px;
}
.alternatives-scroll::-webkit-scrollbar {
height: 6px;
}
.alternatives-scroll::-webkit-scrollbar-track {
background: #f1f5f9;
border-radius: 3px;
}
.alternatives-scroll::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 3px;
}
.alternative-card {
min-width: 140px;
background-color: #f8fafc;
padding: 16px;
border-radius: 16px;
border: 1px solid #e2e8f0;
}
.alt-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.alt-header svg {
width: 24px;
height: 24px;
}
.rating {
font-weight: bold;
color: #f59e0b;
font-size: 14px;
}
.alt-name {
font-weight: bold;
color: #0f172a;
font-size: 16px;
margin: 4px 0;
}
.alt-price {
color: #64748b;
margin: 0;
font-size: 14px;
}
.report-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
margin: 20px auto 0;
padding: 12px 24px;
background: none;
border: none;
cursor: pointer;
color: #ef4444;
font-weight: bold;
font-size: 16px;
}
.report-btn svg {
width: 20px;
height: 20px;
}
.report-btn:hover {
opacity: 0.8;
}
.upload-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
background-color: #0f172a;
}
.upload-icon {
width: 120px;
height: 120px;
margin-bottom: 24px;
opacity: 0.8;
}
.upload-text {
color: #94a3b8;
font-size: 18px;
text-align: center;
padding: 0 32px;
}
.mode-toggle-btn {
position: absolute;
top: 60px;
right: 20px;
background: rgba(15, 23, 42, 0.8);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 10px 16px;
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
transition: all 0.3s ease;
z-index: 10;
}
.mode-toggle-btn:hover {
background: rgba(15, 23, 42, 0.9);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.mode-toggle-btn svg {
width: 20px;
height: 20px;
}
.mode-toggle-btn span {
color: white;
font-size: 14px;
font-weight: 600;
white-space: nowrap;
}
.upload-button {
display: flex;
align-items: center;
justify-content: center;
background-color: white;
}
.upload-button svg {
width: 40px;
height: 40px;
}
</style>

View File

@@ -0,0 +1,104 @@
<script>
export let className = "";
</script>
<div class="cloud-section-container {className}">
<!-- Top Layers -->
<svg
class="cloud-wave top-wave"
viewBox="0 0 1440 120"
preserveAspectRatio="none"
xmlns="http://www.w3.org/2000/svg"
>
<!-- Layer 1 (Back/Most Transparent) -->
<path
fill="rgba(255,255,255,0.3)"
d="M0,60 Q360,120 720,60 Q1080,0 1440,60 L1440,120 L0,120 Z"
/>
<!-- Layer 2 -->
<path
fill="rgba(255,255,255,0.6)"
d="M0,80 Q360,20 720,80 Q1080,140 1440,80 L1440,120 L0,120 Z"
/>
<!-- Layer 3 (Solid White - connects to content) -->
<path
fill="#ffffff"
d="M0,100 Q360,60 720,100 Q1080,140 1440,100 L1440,120 L0,120 Z"
/>
</svg>
<div class="cloud-content-wrapper">
<slot />
</div>
<!-- Bottom Layers -->
<svg
class="cloud-wave bottom-wave"
viewBox="0 0 1440 120"
preserveAspectRatio="none"
xmlns="http://www.w3.org/2000/svg"
>
<!-- Layer 1 (Solid White - connects from content) -->
<path
fill="#ffffff"
d="M0,0 L1440,0 L1440,20 Q1080,60 720,20 Q360,-20 0,20 Z"
/>
<!-- Layer 2 -->
<path
fill="rgba(255,255,255,0.6)"
d="M0,0 L1440,0 L1440,40 Q1080,0 720,40 Q360,80 0,40 Z"
/>
<!-- Layer 3 (Front/Most Transparent) -->
<path
fill="rgba(255,255,255,0.3)"
d="M0,0 L1440,0 L1440,60 Q1080,20 720,60 Q360,100 0,60 Z"
/>
</svg>
</div>
<style>
.cloud-section-container {
position: relative;
margin: 40px auto;
max-width: 1200px;
z-index: 10;
filter: drop-shadow(0 10px 30px rgba(0, 0, 0, 0.1));
}
.cloud-content-wrapper {
background: #ffffff; /* White */
padding: 20px 60px;
position: relative;
z-index: 2;
min-height: 100px;
margin-top: -2px;
margin-bottom: -2px;
}
.cloud-wave {
display: block;
width: 100%;
height: 80px;
pointer-events: none;
}
.top-wave {
z-index: 3;
position: relative;
}
.bottom-wave {
z-index: 1;
position: relative;
}
/* Mobile adjustments */
@media (max-width: 768px) {
.cloud-content-wrapper {
padding: 20px 20px;
}
.cloud-wave {
height: 50px;
}
}
</style>

View File

@@ -0,0 +1,280 @@
<script lang="ts">
import { goto } from "$app/navigation";
interface Props {
currentRoute: string;
}
let { currentRoute }: Props = $props();
const tabs = [
{
name: "Mission",
route: "/community",
icon: "people",
activeColor: "#f472b6",
},
{
name: "History",
route: "/",
icon: "time",
activeColor: "#1ed760",
},
{
name: "Home",
route: "/", // Hidden - for center button only
icon: "home",
activeColor: "#4ade80",
isCenter: true,
},
{
name: "Report",
route: "/report",
icon: "megaphone",
activeColor: "#ef4444",
},
{
name: "Ask AI",
route: "/chat",
icon: "chatbubble-ellipses",
activeColor: "#4ade80",
},
];
function navigateToTab(route: string) {
goto(route);
}
function getIconPath(icon: string, isFocused: boolean): string {
switch (icon) {
case "people":
return isFocused
? "M166.5 83.5C148.6 98.07 128 111.1 104.1 123.5C119.9 145.9 131.8 171.9 138.5 200h98.7C226.5 138.5 192 88.96 146.5 63.36C154.2 71.48 161.6 77.25 166.5 83.5zM196.7 368H159.3c6.672 28.11 18.53 54.08 34.39 76.52C209.5 422.1 226.1 393.3 237.3 368zM104.1 388.5C128 400.9 148.6 413.9 166.5 428.5C161.6 434.8 154.2 440.5 146.5 448.6C155.2 456.8 166.5 462.5 179.2 464.1C184.1 456.2 192.5 447.7 203.5 437.5C221.4 422.9 242 409.9 265.9 397.5C250.1 375.1 238.2 349.1 231.5 320H116.5C109.8 348.1 97.94 374.1 82.14 396.5zM365.9 248C359.2 219.9 347.3 193.9 331.5 171.5C307.6 183.9 287 196.9 269.1 211.5C274 217.8 281.4 223.5 289.1 231.6C280.4 239.8 269.1 245.5 256.4 247.1C251.5 239.2 243.1 230.7 232.1 220.5C214.2 205.9 193.6 192.9 169.7 180.5C185.5 158.1 197.4 132.1 204.1 104h114.1c6.672 28.11 18.53 54.08 34.39 76.52C368.5 158.1 389.1 145.1 407 130.5C402.1 124.2 394.6 118.5 386.9 110.4C395.6 102.2 406.9 96.54 419.6 94.93C424.5 102.8 432.9 111.3 443.9 121.5C461.8 136.1 482.4 149.1 506.3 161.5C490.5 183.9 478.6 209.9 471.9 238h-98.66C380.1 269.5 391.9 295.5 407.7 317.9C383.8 330.3 363.2 343.3 345.3 357.9C350.2 364.2 357.6 369.9 365.3 378C356.6 386.2 345.3 391.9 332.6 393.5C327.7 385.6 319.3 377.1 308.3 366.9C290.4 352.3 269.8 339.3 245.9 326.9C226.1 304.5 213.8 278.5 207.1 248h158.8z M288 0C422.4 0 512 89.6 512 224S422.4 448 288 448S64 358.4 64 224S153.6 0 288 0z"
: "M166.5 83.5C148.6 98.07 128 111.1 104.1 123.5C119.9 145.9 131.8 171.9 138.5 200h98.7C226.5 138.5 192 88.96 146.5 63.36C154.2 71.48 161.6 77.25 166.5 83.5zM196.7 368H159.3c6.672 28.11 18.53 54.08 34.39 76.52C209.5 422.1 226.1 393.3 237.3 368zM104.1 388.5C128 400.9 148.6 413.9 166.5 428.5C161.6 434.8 154.2 440.5 146.5 448.6C155.2 456.8 166.5 462.5 179.2 464.1C184.1 456.2 192.5 447.7 203.5 437.5C221.4 422.9 242 409.9 265.9 397.5C250.1 375.1 238.2 349.1 231.5 320H116.5C109.8 348.1 97.94 374.1 82.14 396.5zM365.9 248C359.2 219.9 347.3 193.9 331.5 171.5C307.6 183.9 287 196.9 269.1 211.5C274 217.8 281.4 223.5 289.1 231.6C280.4 239.8 269.1 245.5 256.4 247.1C251.5 239.2 243.1 230.7 232.1 220.5C214.2 205.9 193.6 192.9 169.7 180.5C185.5 158.1 197.4 132.1 204.1 104h114.1c6.672 28.11 18.53 54.08 34.39 76.52C368.5 158.1 389.1 145.1 407 130.5C402.1 124.2 394.6 118.5 386.9 110.4C395.6 102.2 406.9 96.54 419.6 94.93C424.5 102.8 432.9 111.3 443.9 121.5C461.8 136.1 482.4 149.1 506.3 161.5C490.5 183.9 478.6 209.9 471.9 238h-98.66C380.1 269.5 391.9 295.5 407.7 317.9C383.8 330.3 363.2 343.3 345.3 357.9C350.2 364.2 357.6 369.9 365.3 378C356.6 386.2 345.3 391.9 332.6 393.5C327.7 385.6 319.3 377.1 308.3 366.9C290.4 352.3 269.8 339.3 245.9 326.9C226.1 304.5 213.8 278.5 207.1 248h158.8z M288 0C422.4 0 512 89.6 512 224S422.4 448 288 448S64 358.4 64 224S153.6 0 288 0z";
case "time":
return isFocused
? "M256 0a256 256 0 1 1 0 512A256 256 0 1 1 256 0zM232 120V256c0 8 4 15.5 10.7 20l96 64c11 7.4 25.9 4.4 33.3-6.7s4.4-25.9-6.7-33.3L280 243.2V120c0-13.3-10.7-24-24-24s-24 10.7-24 24z"
: "M256 0a256 256 0 1 1 0 512A256 256 0 1 1 256 0zM232 120V256c0 8 4 15.5 10.7 20l96 64c11 7.4 25.9 4.4 33.3-6.7s4.4-25.9-6.7-33.3L280 243.2V120c0-13.3-10.7-24-24-24s-24 10.7-24 24z";
case "newspaper":
return isFocused
? "M96 0C43 0 0 43 0 96V416c0 53 43 96 96 96H384h32c17.7 0 32-14.3 32-32s-14.3-32-32-32V384c17.7 0 32-14.3 32-32V32c0-17.7-14.3-32-32-32H384 96zm0 384H352v64H96c-17.7 0-32-14.3-32-32s14.3-32 32-32zm32-240c0-8.8 7.2-16 16-16H336c8.8 0 16 7.2 16 16s-7.2 16-16 16H144c-8.8 0-16-7.2-16-16zm16 48H336c8.8 0 16 7.2 16 16s-7.2 16-16 16H144c-8.8 0-16-7.2-16-16s7.2-16 16-16z"
: "M96 96c0-35.3 28.7-64 64-64H448c35.3 0 64 28.7 64 64V416c0 35.3-28.7 64-64 64H80c-44.2 0-80-35.8-80-80V128c0-17.7 14.3-32 32-32s32 14.3 32 32V400c0 8.8 7.2 16 16 16s16-7.2 16-16V96zm64 24v80c0 13.3 10.7 24 24 24H296c13.3 0 24-10.7 24-24V120c0-13.3-10.7-24-24-24H184c-13.3 0-24 10.7-24 24zm128 0v80H192V120H288zM160 280c0-13.3 10.7-24 24-24H424c13.3 0 24 10.7 24 24s-10.7 24-24 24H184c-13.3 0-24-10.7-24-24zm0 80c0-13.3 10.7-24 24-24H424c13.3 0 24 10.7 24 24s-10.7 24-24 24H184c-13.3 0-24-10.7-24-24z";
case "megaphone":
return isFocused
? "M480 32c0-12.9-7.8-24.6-19.8-29.6s-25.7-2.2-34.9 6.9L381.7 53c-48 48-113.1 75-181 75H192 160 64c-35.3 0-64 28.7-64 64v96c0 35.3 28.7 64 64 64l0 128c0 17.7 14.3 32 32 32h64c17.7 0 32-14.3 32-32V352l8.7 0c67.9 0 133 27 181 75l43.6 43.6c9.2 9.2 22.9 11.9 34.9 6.9s19.8-16.6 19.8-29.6V300.4c18.6-8.8 32-32.5 32-60.4s-13.4-51.6-32-60.4V32zm-64 76.7V240 371.3C357.2 317.8 280.5 288 200.7 288H192V192h8.7c79.8 0 156.5-29.8 215.3-83.3z"
: "M480 32c0-12.9-7.8-24.6-19.8-29.6s-25.7-2.2-34.9 6.9L381.7 53c-48 48-113.1 75-181 75H192 160 64c-35.3 0-64 28.7-64 64v96c0 35.3 28.7 64 64 64l0 128c0 17.7 14.3 32 32 32h64c17.7 0 32-14.3 32-32V352l8.7 0c67.9 0 133 27 181 75l43.6 43.6c9.2 9.2 22.9 11.9 34.9 6.9s19.8-16.6 19.8-29.6V300.4c18.6-8.8 32-32.5 32-60.4s-13.4-51.6-32-60.4V32zM288 240c0 8.8-7.2 16-16 16H192V192h80c8.8 0 16 7.2 16 16z";
case "chatbubble-ellipses":
return isFocused
? "M256 448c141.4 0 256-93.1 256-208S397.4 32 256 32S0 125.1 0 240c0 45.1 17.7 86.8 47.7 120.9c-1.9 24.5-11.4 46.3-21.4 62.9c-5.5 9.2-11.1 16.6-15.2 21.6c-2.1 2.5-3.7 4.4-4.9 5.7c-.6 .6-1 1.1-1.3 1.4l-.3 .3 0 0 0 0 0 0 0 0c-4.6 4.6-5.9 11.4-3.4 17.4c2.5 6 8.3 9.9 14.8 9.9c28.7 0 57.6-8.9 81.6-19.3c22.9-10 42.4-21.9 54.3-30.6c31.8 11.5 67 17.9 104.1 17.9zM128 208a32 32 0 1 1 0 64 32 32 0 1 1 0-64zm128 0a32 32 0 1 1 0 64 32 32 0 1 1 0-64zm96 32a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z"
: "M256 448c141.4 0 256-93.1 256-208S397.4 32 256 32S0 125.1 0 240c0 45.1 17.7 86.8 47.7 120.9c-1.9 24.5-11.4 46.3-21.4 62.9c-5.5 9.2-11.1 16.6-15.2 21.6c-2.1 2.5-3.7 4.4-4.9 5.7c-.6 .6-1 1.1-1.3 1.4l-.3 .3 0 0 0 0 0 0 0 0c-4.6 4.6-5.9 11.4-3.4 17.4c2.5 6 8.3 9.9 14.8 9.9c28.7 0 57.6-8.9 81.6-19.3c22.9-10 42.4-21.9 54.3-30.6c31.8 11.5 67 17.9 104.1 17.9zm24-240a24 24 0 1 1 48 0 24 24 0 1 1 -48 0zm-72 0a24 24 0 1 1 48 0 24 24 0 1 1 -48 0zm-72 0a24 24 0 1 1 48 0 24 24 0 1 1 -48 0z";
default:
return "";
}
}
function handleScan() {
// This will be handled by parent
const event = new CustomEvent("scan");
window.dispatchEvent(event);
}
</script>
<div class="tab-bar-wrapper">
<!-- Main Tab Bar -->
<div class="tab-bar">
<div class="tab-bar-inner">
{#each tabs as tab, index}
{#if !tab.isCenter}
<button
class="tab-item"
class:active={currentRoute === tab.route}
on:click={() => navigateToTab(tab.route)}
>
<div
class="tab-content"
class:active={currentRoute === tab.route}
style="background-color: {currentRoute === tab.route
? `${tab.activeColor}15`
: 'transparent'};"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
fill={currentRoute === tab.route ? tab.activeColor : "#94a3b8"}
>
<path
d={getIconPath(
tab.icon,
currentRoute === tab.route
)}
/>
</svg>
<span
style="color: {currentRoute === tab.route
? tab.activeColor
: '#94a3b8'};"
>
{tab.name}
</span>
</div>
</button>
{/if}
{#if index === 1}
<div class="tab-spacer"></div>
{/if}
{/each}
</div>
</div>
<!-- Center Scan Button -->
<div class="center-button-wrapper">
<div class="center-button-ring">
<button class="center-button" on:click={handleScan}>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
fill="#052e16"
>
<path
d="M194.6 32H317.4C338.1 32 356.4 45.22 362.9 64.82L373.3 96H448C483.3 96 512 124.7 512 160V416C512 451.3 483.3 480 448 480H64C28.65 480 0 451.3 0 416V160C0 124.7 28.65 96 64 96H138.7L149.1 64.82C155.6 45.22 173.9 32 194.6 32H194.6zM256 384C309 384 352 341 352 288C352 234.1 309 192 256 192C202.1 192 160 234.1 160 288C160 341 202.1 384 256 384z"
/>
</svg>
</button>
</div>
</div>
</div>
<style>
.tab-bar-wrapper {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding-bottom: 20px;
padding-left: 12px;
padding-right: 12px;
z-index: 100;
pointer-events: none;
}
.tab-bar {
height: 85px;
border-radius: 32px;
background: rgba(30, 41, 59, 0.8);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06),
0 20px 25px -5px rgba(0, 0, 0, 0.4);
border: 1px solid rgba(255, 255, 255, 0.08);
border-top: 1.5px solid rgba(255, 255, 255, 0.15);
pointer-events: all;
}
.tab-bar-inner {
display: flex;
height: 100%;
align-items: center;
padding: 0 16px;
}
.tab-item {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 12px 0;
background: none;
border: none;
cursor: pointer;
transition: all 0.3s ease;
}
.tab-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 8px 12px;
border-radius: 18px;
transition: all 0.3s ease;
}
.tab-content svg {
width: 26px;
height: 26px;
transition: all 0.3s ease;
}
.tab-content span {
font-size: 10px;
font-weight: 500;
margin-top: 4px;
letter-spacing: 0.2px;
transition: all 0.3s ease;
}
.tab-content.active span {
font-weight: 600;
}
.tab-spacer {
width: 80px;
}
.center-button-wrapper {
position: absolute;
bottom: 48px;
left: 50%;
transform: translateX(-50%);
pointer-events: all;
}
.center-button-ring {
position: relative;
width: 90px;
height: 90px;
border-radius: 45px;
background: rgba(74, 222, 128, 0.1);
backdrop-filter: blur(15px);
-webkit-backdrop-filter: blur(15px);
border: 1.5px solid rgba(74, 222, 128, 0.3);
display: flex;
align-items: center;
justify-content: center;
}
.center-button {
width: 65px;
height: 65px;
border-radius: 33px;
background: linear-gradient(135deg, #4ade80 0%, #22c55e 50%, #16a34a 100%);
border: 2.5px solid rgba(15, 23, 42, 0.5);
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
box-shadow:
0 12px 20px rgba(74, 222, 128, 0.6),
0 4px 8px rgba(0, 0, 0, 0.2);
}
.center-button:hover {
transform: scale(1.05);
box-shadow:
0 16px 24px rgba(74, 222, 128, 0.7),
0 6px 10px rgba(0, 0, 0, 0.3);
}
.center-button:active {
transform: scale(0.98);
}
.center-button svg {
width: 30px;
height: 30px;
}
</style>

View File

@@ -0,0 +1,295 @@
<script lang="ts">
import { goto } from "$app/navigation";
interface RecentItem {
id: number;
title: string;
date: string;
impact: string;
imageUri: string | null;
}
interface Props {
recentItems: RecentItem[];
onToggleCamera: () => void;
}
let { recentItems, onToggleCamera }: Props = $props();
function navigateToDetails(item: RecentItem) {
// Store item in sessionStorage for the details page
sessionStorage.setItem("selectedItem", JSON.stringify(item));
goto("/report-details");
}
</script>
<div class="home-container">
<div class="safe-area">
<div class="header">
<div>
<h1 class="app-name">Ethix</h1>
<p class="tagline">Truth in every scan.</p>
</div>
<div class="profile-avatar">
<span>YO</span>
</div>
</div>
<div class="scroll-content">
<button class="card scan-card" on:click={onToggleCamera}>
<div class="card-inner">
<svg
class="scan-icon"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
fill="#4ade80"
>
<path
d="M156.6 384.9L125.7 353.1C117.2 345.5 114.2 333.1 117.1 321.8C120.1 312.9 124.1 301.3 129.8 288H24C15.38 288 7.414 283.4 3.146 275.9C-1.123 268.4-1.042 259.2 3.357 251.8L55.83 163.3C68.79 141.4 92.33 127.1 117.8 127.1H200C202.4 124 204.8 120.3 207.2 116.7C289.1-4.07 411.1-8.142 483.9 5.275C495.6 7.414 504.6 16.43 506.7 28.06C520.1 100.9 516.1 222.9 395.3 304.8C391.8 307.2 387.1 309.6 384 311.1V394.2C384 419.7 370.6 443.2 348.7 456.2L260.2 508.6C252.8 513 243.6 513.1 236.1 508.9C228.6 504.6 224 496.6 224 488V380.8C209.9 385.6 197.6 389.7 188.3 392.7C177.1 396.3 164.9 393.2 156.6 384.9V384.9zM384 167.1C406.1 167.1 424 150.1 424 127.1C424 105.9 406.1 87.1 384 87.1C361.9 87.1 344 105.9 344 127.1C344 150.1 361.9 167.1 384 167.1z"
/>
</svg>
<div class="card-text-container">
<h2 class="card-title">Start Scanning</h2>
<p class="card-text">Identify harmful products</p>
</div>
<svg
class="arrow-icon"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 320 512"
fill="#64748b"
>
<path
d="M96 480c-8.188 0-16.38-3.125-22.62-9.375c-12.5-12.5-12.5-32.75 0-45.25L242.8 256L73.38 86.63c-12.5-12.5-12.5-32.75 0-45.25s32.75-12.5 45.25 0l192 192c12.5 12.5 12.5 32.75 0 45.25l-192 192C112.4 476.9 104.2 480 96 480z"
/>
</svg>
</div>
</button>
<div class="recent-section">
<h2 class="section-title">Recent Activity</h2>
{#if recentItems.length === 0}
<p class="no-items">No recent scans.</p>
{:else}
{#each recentItems as item (item.id)}
<button class="recent-card" on:click={() => navigateToDetails(item)}>
<div class="recent-icon-container">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
fill="#94a3b8"
>
<path
d="M256 0C397.4 0 512 114.6 512 256C512 397.4 397.4 512 256 512C201.7 512 151.2 495 109.7 466.1C95.2 455.1 91.64 436 101.8 421.5C111.9 407 131.8 403.5 146.3 413.6C177.4 435.3 215.2 448 256 448C362 448 448 362 448 256C448 149.1 362 64 256 64C202.1 64 155 85.46 120.2 120.2L151 151C166.1 166.1 155.4 192 134.1 192H24C10.75 192 0 181.3 0 168V57.94C0 36.56 25.85 25.85 40.97 40.97L74.98 74.98C121.3 28.69 185.3 0 256 0L256 0zM256 128C269.3 128 280 138.7 280 152V246.1L344.1 311C354.3 320.4 354.3 335.6 344.1 344.1C335.6 354.3 320.4 354.3 311 344.1L239 272.1C234.5 268.5 232 262.4 232 256V152C232 138.7 242.7 128 256 128V128z"
/>
</svg>
</div>
<div class="recent-info">
<h3 class="recent-title">{item.title}</h3>
<p class="recent-date">{item.date}</p>
</div>
<div
class="impact-badge"
class:high-impact={item.impact === "High"}
class:low-impact={item.impact === "Low"}
>
{item.impact}
</div>
</button>
{/each}
{/if}
</div>
</div>
</div>
</div>
<style>
.home-container {
width: 100%;
height: 100vh;
background-color: #0f172a;
overflow-y: auto;
}
.safe-area {
padding-top: 50px;
padding-bottom: 140px;
}
.header {
display: flex;
justify-content: space-between;
padding: 24px;
align-items: center;
}
.app-name {
font-size: 28px;
font-weight: 800;
color: white;
margin: 0;
}
.tagline {
color: #94a3b8;
margin: 0;
margin-top: 4px;
}
.profile-avatar {
width: 40px;
height: 40px;
background-color: #334155;
border-radius: 20px;
display: flex;
justify-content: center;
align-items: center;
font-weight: bold;
color: white;
}
.scroll-content {
padding: 24px;
padding-bottom: 140px;
}
.card {
background-color: rgba(30, 41, 59, 0.9);
padding: 20px;
border-radius: 24px;
border: 1px solid #334155;
cursor: pointer;
transition: all 0.3s ease;
width: 100%;
border: none;
text-align: left;
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3);
}
.scan-card {
margin-bottom: 24px;
}
.card-inner {
display: flex;
align-items: center;
gap: 16px;
}
.scan-icon {
width: 56px;
height: 56px;
flex-shrink: 0;
}
.card-text-container {
flex: 1;
}
.card-title {
color: white;
font-size: 20px;
font-weight: bold;
margin: 0;
}
.card-text {
color: #94a3b8;
font-size: 14px;
margin: 4px 0 0 0;
}
.arrow-icon {
width: 24px;
height: 24px;
flex-shrink: 0;
}
.recent-section {
margin-top: 24px;
}
.section-title {
color: white;
font-weight: bold;
font-size: 20px;
margin-bottom: 16px;
}
.no-items {
color: #64748b;
}
.recent-card {
background-color: rgba(30, 41, 59, 0.9);
padding: 16px;
border-radius: 24px;
border: 1px solid #334155;
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 12px;
cursor: pointer;
transition: all 0.3s ease;
width: 100%;
text-align: left;
}
.recent-card:hover {
transform: translateX(4px);
background-color: rgba(30, 41, 59, 1);
}
.recent-icon-container {
width: 48px;
height: 48px;
background-color: #334155;
border-radius: 12px;
display: flex;
justify-content: center;
align-items: center;
flex-shrink: 0;
}
.recent-icon-container svg {
width: 24px;
height: 24px;
}
.recent-info {
flex: 1;
}
.recent-title {
color: white;
font-weight: bold;
font-size: 16px;
margin: 0;
}
.recent-date {
color: #94a3b8;
font-size: 13px;
margin: 4px 0 0 0;
}
.impact-badge {
padding: 4px 10px;
border-radius: 8px;
font-size: 12px;
font-weight: bold;
}
.high-impact {
background-color: rgba(239, 68, 68, 0.2);
color: #ef4444;
}
.low-impact {
background-color: rgba(34, 197, 94, 0.2);
color: #22c55e;
}
</style>

View File

@@ -0,0 +1,188 @@
<script lang="ts">
import { onMount } from "svelte";
let scrollY = 0;
let innerHeight = 0;
let mouseX = 0;
let mouseY = 0;
onMount(() => {
const handleScroll = () => {
scrollY = window.scrollY;
};
const handleMouseMove = (e: MouseEvent) => {
mouseX = (e.clientX - window.innerWidth / 2) * 0.05;
mouseY = (e.clientY - window.innerHeight / 2) * 0.05;
};
window.addEventListener("scroll", handleScroll);
window.addEventListener("mousemove", handleMouseMove);
innerHeight = window.innerHeight;
return () => {
window.removeEventListener("scroll", handleScroll);
window.removeEventListener("mousemove", handleMouseMove);
};
});
// Calculate opacity for transition effects
$: progress = Math.min(Math.max(scrollY / innerHeight, 0), 1);
</script>
<svelte:window bind:innerHeight />
<div class="parallax-container">
<!-- 1. Sky Gradient -->
<div
class="layer sky"
style="background: linear-gradient(to bottom,
rgb({100 + 100 * progress}, {110 + 100 * progress}, {120 + 135 * progress}),
rgb({180 + 20 * progress}, {190 + 30 * progress}, {200 + 55 * progress}));"
>
</div>
<!-- 2. Sun/Moon -->
<div
class="layer sun-layer"
style="transform: translate3d({scrollY * 0.05 + mouseX * 0.2}px, {scrollY * 0.1 + mouseY * 0.2}px, 0);"
>
<div class="sun"></div>
</div>
<!-- 3. Clouds (Back) -->
<div
class="layer clouds-back"
style="transform: translate3d({-scrollY * 0.05 + mouseX * 0.5}px, {scrollY * 0.15 + mouseY * 0.1}px, 0);"
>
<svg viewBox="0 0 1440 320" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="none">
<path fill="rgba(255,255,255,0.4)" d="M0,128L48,138.7C96,149,192,171,288,165.3C384,160,480,128,576,128C672,128,768,160,864,181.3C960,203,1056,213,1152,202.7C1248,192,1344,160,1392,144L1440,128L1440,320L1392,320C1344,320,1248,320,1152,320C1056,320,960,320,864,320C768,320,672,320,576,320C480,320,384,320,288,320C192,320,96,320,48,320L0,320Z"></path>
</svg>
</div>
<!-- 4. City Skyline (Polluted) -->
<div
class="layer city-layer"
style="
transform: translate3d({mouseX * 0.8}px, {scrollY * 0.2}px, 0);
opacity: {1 - progress * 1.5};
"
>
<svg viewBox="0 0 1440 320" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="none">
<path fill="#475569" d="M160,224L160,320L288,320L288,160L416,160L416,320L544,320L544,192L672,192L672,320L800,320L800,128L928,128L928,320L1056,320L1056,224L1184,224L1184,320L1312,320L1312,160L1440,160L1440,320L0,320L0,224Z"></path>
<!-- Factory Smoke -->
<circle cx="350" cy="140" r="20" fill="rgba(80,80,80,0.5)" />
<circle cx="370" cy="120" r="25" fill="rgba(80,80,80,0.4)" />
</svg>
</div>
<!-- 5. Mountains -->
<div
class="layer mountains"
style="transform: translate3d({mouseX * 0.3}px, {scrollY * 0.35}px, 0);"
>
<svg viewBox="0 0 1440 320" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="none">
<path fill="#3b82f6" fill-opacity="0.6" d="M0,192L60,197.3C120,203,240,213,360,202.7C480,192,600,160,720,138.7C840,117,960,107,1080,117.3C1200,128,1320,160,1380,176L1440,192L1440,320L1380,320C1320,320,1200,320,1080,320C960,320,840,320,720,320C600,320,480,320,360,320C240,320,120,320,60,320L0,320Z"></path>
</svg>
</div>
<!-- 6. Rolling Hills -->
<div
class="layer hills-mid"
style="transform: translate3d({mouseX * 0.5}px, {scrollY * 0.5}px, 0);"
>
<svg viewBox="0 0 1440 320" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="none">
<path fill="#22c55e" d="M0,256L48,229.3C96,203,192,149,288,154.7C384,160,480,224,576,234.7C672,245,768,203,864,186.7C960,171,1056,181,1152,197.3C1248,213,1344,235,1392,245.3L1440,256L1440,320L1392,320C1344,320,1248,320,1152,320C1056,320,960,320,864,320C768,320,672,320,576,320C480,320,384,320,288,320C192,320,96,320,48,320L0,320Z"></path>
</svg>
</div>
<!-- 7. Close Trees (Foreground) -->
<div
class="layer trees-front"
style="transform: translate3d({mouseX * 1.2}px, {scrollY * 0.75}px, 0);"
>
<svg viewBox="0 0 1440 320" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="none">
<path fill="#166534" d="M0,288L48,272C96,256,192,224,288,213.3C384,203,480,213,576,229.3C672,245,768,267,864,250.7C960,235,1056,181,1152,165.3C1248,149,1344,171,1392,181.3L1440,192L1440,320L1392,320C1344,320,1248,320,1152,320C1056,320,960,320,864,320C768,320,672,320,576,320C480,320,384,320,288,320C192,320,96,320,48,320L0,320Z"></path>
<!-- Simple Trees -->
<path fill="#14532d" d="M100,280 L140,150 L180,280 Z" />
<path fill="#14532d" d="M1100,300 L1150,120 L1200,300 Z" />
<circle cx="140" cy="160" r="35" fill="#16a34a" />
<circle cx="1150" cy="130" r="45" fill="#16a34a" />
</svg>
</div>
</div>
<style>
.parallax-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100vh;
z-index: 0;
overflow: hidden;
pointer-events: none;
}
.layer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
will-change: transform;
}
.sun {
width: 120px;
height: 120px;
background: radial-gradient(circle, #fef3c7 0%, #fde047 30%, #fbbf24 60%, #f59e0b 100%);
border-radius: 50%;
position: absolute;
top: 20%;
right: 12%;
box-shadow:
0 0 40px rgba(254, 243, 199, 1),
0 0 80px rgba(251, 191, 36, 0.8),
0 0 120px rgba(251, 191, 36, 0.6),
0 0 180px rgba(245, 158, 11, 0.4);
animation: sun-glow 4s ease-in-out infinite;
}
@keyframes sun-glow {
0%, 100% {
box-shadow:
0 0 40px rgba(254, 243, 199, 1),
0 0 80px rgba(251, 191, 36, 0.8),
0 0 120px rgba(251, 191, 36, 0.6),
0 0 180px rgba(245, 158, 11, 0.4);
}
50% {
box-shadow:
0 0 50px rgba(254, 243, 199, 1),
0 0 100px rgba(251, 191, 36, 0.9),
0 0 150px rgba(251, 191, 36, 0.7),
0 0 220px rgba(245, 158, 11, 0.5);
}
}
/* SVG containers aligned to bottom */
.clouds-back, .city-layer, .mountains, .hills-mid, .trees-front {
display: flex;
align-items: flex-end;
}
/* Scale SVGs to cover width, keep height proportional-ish */
svg {
width: 100%;
height: auto;
min-height: 40vh; /* Ensure they take up space */
}
/* Mobile adjustment */
@media (max-width: 768px) {
svg {
min-height: 30vh;
transform: scale(1.2); /* Zoom in for mobile */
}
}
</style>

View File

@@ -0,0 +1,234 @@
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
import * as THREE from 'three';
interface Props {
productIndex: number;
color: string;
}
let { productIndex, color }: Props = $props();
let container: HTMLDivElement;
let scene: THREE.Scene;
let camera: THREE.PerspectiveCamera;
let renderer: THREE.WebGLRenderer;
let currentMesh: THREE.Group;
let animationId: number;
// Product colors
const productColors = {
0: 0x1ed760, // Water bottle - green
1: 0xe91429, // Plastic bag - red
2: 0xf59b23 // Coffee cup - orange
};
function createWaterBottle(): THREE.Group {
const group = new THREE.Group();
const material = new THREE.MeshPhongMaterial({
color: 0x4488ff,
transparent: true,
opacity: 0.7,
shininess: 100,
});
// Main body - tall cylinder
const bodyGeometry = new THREE.CylinderGeometry(0.4, 0.45, 1.8, 32);
const body = new THREE.Mesh(bodyGeometry, material);
body.position.y = 0;
group.add(body);
// Neck - smaller cylinder
const neckGeometry = new THREE.CylinderGeometry(0.15, 0.25, 0.4, 32);
const neck = new THREE.Mesh(neckGeometry, material);
neck.position.y = 1.1;
group.add(neck);
// Cap - blue solid
const capMaterial = new THREE.MeshPhongMaterial({ color: 0x2255aa, shininess: 80 });
const capGeometry = new THREE.CylinderGeometry(0.18, 0.18, 0.15, 32);
const cap = new THREE.Mesh(capGeometry, capMaterial);
cap.position.y = 1.35;
group.add(cap);
// Label - wrap around middle
const labelMaterial = new THREE.MeshPhongMaterial({ color: 0x88ccff });
const labelGeometry = new THREE.CylinderGeometry(0.42, 0.47, 0.6, 32, 1, true);
const label = new THREE.Mesh(labelGeometry, labelMaterial);
label.position.y = 0;
group.add(label);
return group;
}
function createPlasticBag(): THREE.Group {
const group = new THREE.Group();
const material = new THREE.MeshPhongMaterial({
color: 0xffffff,
transparent: true,
opacity: 0.85,
side: THREE.DoubleSide,
});
// Main bag body - box shape
const bagGeometry = new THREE.BoxGeometry(1.2, 1.5, 0.4);
const bag = new THREE.Mesh(bagGeometry, material);
bag.position.y = -0.2;
group.add(bag);
// Left handle
const handleShape = new THREE.Shape();
handleShape.moveTo(-0.1, 0);
handleShape.lineTo(0.1, 0);
handleShape.lineTo(0.1, 0.5);
handleShape.quadraticCurveTo(0.0, 0.7, -0.1, 0.5);
handleShape.lineTo(-0.1, 0);
const extrudeSettings = { depth: 0.05, bevelEnabled: false };
const handleGeometry = new THREE.ExtrudeGeometry(handleShape, extrudeSettings);
const leftHandle = new THREE.Mesh(handleGeometry, material);
leftHandle.position.set(-0.35, 0.55, 0);
group.add(leftHandle);
const rightHandle = new THREE.Mesh(handleGeometry, material);
rightHandle.position.set(0.35, 0.55, 0);
group.add(rightHandle);
return group;
}
function createCoffeeCup(): THREE.Group {
const group = new THREE.Group();
// Cup body - tapered cylinder (brown cardboard)
const cupMaterial = new THREE.MeshPhongMaterial({ color: 0x8B4513 });
const cupGeometry = new THREE.CylinderGeometry(0.45, 0.35, 1.4, 32);
const cup = new THREE.Mesh(cupGeometry, cupMaterial);
cup.position.y = 0;
group.add(cup);
// Green sleeve
const sleeveMaterial = new THREE.MeshPhongMaterial({ color: 0x1ed760 });
const sleeveGeometry = new THREE.CylinderGeometry(0.47, 0.42, 0.5, 32, 1, true);
const sleeve = new THREE.Mesh(sleeveGeometry, sleeveMaterial);
sleeve.position.y = -0.1;
group.add(sleeve);
// White lid
const lidMaterial = new THREE.MeshPhongMaterial({ color: 0xffffff });
const lidGeometry = new THREE.CylinderGeometry(0.48, 0.45, 0.15, 32);
const lid = new THREE.Mesh(lidGeometry, lidMaterial);
lid.position.y = 0.75;
group.add(lid);
// Lid dome
const domeGeometry = new THREE.SphereGeometry(0.35, 32, 16, 0, Math.PI * 2, 0, Math.PI / 2);
const dome = new THREE.Mesh(domeGeometry, lidMaterial);
dome.position.y = 0.82;
dome.scale.y = 0.3;
group.add(dome);
return group;
}
function createProduct(index: number): THREE.Group {
switch (index) {
case 0: return createWaterBottle();
case 1: return createPlasticBag();
case 2: return createCoffeeCup();
default: return createWaterBottle();
}
}
function init() {
// Scene
scene = new THREE.Scene();
// No background - transparent
// Camera
camera = new THREE.PerspectiveCamera(45, 1, 0.1, 1000);
camera.position.z = 5;
// Renderer with transparent background
renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true // Transparent background!
});
renderer.setSize(400, 400);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setClearColor(0x000000, 0); // Fully transparent
container.appendChild(renderer.domElement);
// Lighting
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(5, 5, 5);
scene.add(directionalLight);
const backLight = new THREE.DirectionalLight(0xffffff, 0.5);
backLight.position.set(-5, -5, -5);
scene.add(backLight);
// Initial product
currentMesh = createProduct(productIndex);
scene.add(currentMesh);
animate();
}
function animate() {
animationId = requestAnimationFrame(animate);
if (currentMesh) {
currentMesh.rotation.y += 0.01;
}
renderer.render(scene, camera);
}
function updateProduct(index: number) {
if (scene && currentMesh) {
scene.remove(currentMesh);
currentMesh = createProduct(index);
scene.add(currentMesh);
}
}
$effect(() => {
if (scene) {
updateProduct(productIndex);
}
});
onMount(() => {
init();
});
onDestroy(() => {
if (animationId) {
cancelAnimationFrame(animationId);
}
if (renderer) {
renderer.dispose();
}
});
</script>
<div class="canvas-container" bind:this={container}></div>
<style>
.canvas-container {
width: 400px;
height: 400px;
display: flex;
align-items: center;
justify-content: center;
}
.canvas-container :global(canvas) {
border-radius: 50%;
}
</style>

View File

@@ -0,0 +1,174 @@
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
import * as THREE from 'three';
let container: HTMLDivElement;
let canvas: HTMLCanvasElement;
// Scene variables
let scene: THREE.Scene;
let camera: THREE.PerspectiveCamera;
let renderer: THREE.WebGLRenderer;
let particles: THREE.Points;
let geometry: THREE.BufferGeometry;
let material: THREE.PointsMaterial;
// Interaction variables
let mouseX = 0;
let mouseY = 0;
let scrollY = 0;
let targetX = 0;
let targetY = 0;
const windowHalfX = typeof window !== 'undefined' ? window.innerWidth / 2 : 0;
const windowHalfY = typeof window !== 'undefined' ? window.innerHeight / 2 : 0;
onMount(() => {
init();
animate();
window.addEventListener('resize', onWindowResize);
document.addEventListener('mousemove', onDocumentMouseMove);
window.addEventListener('scroll', onDocumentScroll);
return () => {
window.removeEventListener('resize', onWindowResize);
document.removeEventListener('mousemove', onDocumentMouseMove);
window.removeEventListener('scroll', onDocumentScroll);
// Cleanup Three.js resources
if (renderer) renderer.dispose();
if (geometry) geometry.dispose();
if (material) material.dispose();
};
});
function init() {
// 1. Scene Setup
scene = new THREE.Scene();
// Transparent background to blend with CSS gradient
scene.background = null;
// 2. Camera Setup
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 1, 10000);
camera.position.z = 1000;
// 3. Particles Setup
const particleCount = 2000; // Lots of particles
geometry = new THREE.BufferGeometry();
const positions = new Float32Array(particleCount * 3);
const colors = new Float32Array(particleCount * 3);
const color1 = new THREE.Color(0x4ade80); // Bright Green
const color2 = new THREE.Color(0x22c55e); // Darker Green
const color3 = new THREE.Color(0x0f172a); // Deep Blue/Slate
for (let i = 0; i < particleCount; i++) {
// Random positions spread out
positions[i * 3] = (Math.random() * 2 - 1) * 3000;
positions[i * 3 + 1] = (Math.random() * 2 - 1) * 3000;
positions[i * 3 + 2] = (Math.random() * 2 - 1) * 3000;
// Mixed colors for depth
const mixedColor = Math.random() > 0.5 ? color1 : (Math.random() > 0.5 ? color2 : color3);
colors[i * 3] = mixedColor.r;
colors[i * 3 + 1] = mixedColor.g;
colors[i * 3 + 2] = mixedColor.b;
}
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
// 4. Material Setup
// Creating a circular texture for particles using canvas
const sprite = new THREE.TextureLoader().load('https://raw.githubusercontent.com/mrdoob/three.js/master/examples/textures/sprites/disc.png');
material = new THREE.PointsMaterial({
size: 15,
vertexColors: true,
map: sprite,
alphaTest: 0.5,
transparent: true,
opacity: 0.8,
blending: THREE.AdditiveBlending
});
particles = new THREE.Points(geometry, material);
scene.add(particles);
// 5. Renderer Setup
renderer = new THREE.WebGLRenderer({ canvas, alpha: true, antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
}
function onWindowResize() {
const width = window.innerWidth;
const height = window.innerHeight;
camera.aspect = width / height;
camera.updateProjectionMatrix();
renderer.setSize(width, height);
}
function onDocumentMouseMove(event: MouseEvent) {
mouseX = (event.clientX - windowHalfX) * 0.5; // Scale down movement
mouseY = (event.clientY - windowHalfY) * 0.5;
}
function onDocumentScroll() {
scrollY = window.scrollY;
}
function animate() {
requestAnimationFrame(animate);
render();
}
function render() {
// Smooth mouse movement
targetX = mouseX * 0.05;
targetY = mouseY * 0.05;
// Gentle rotation based on time + mouse
particles.rotation.x += 0.0005;
particles.rotation.y += 0.0005;
// Interactive movement (Parallax)
// Move camera based on scroll (fly through effect) and mouse
camera.position.x += (mouseX - camera.position.x) * 0.02;
camera.position.y += (-mouseY - camera.position.y) * 0.02;
// Scroll effect: Move deeper into the field or rotate
camera.position.z = 1000 - (scrollY * 0.5);
camera.lookAt(scene.position);
// Wave effect on particles
const positions = particles.geometry.attributes.position.array as Float32Array;
const time = Date.now() * 0.0001;
renderer.render(scene, camera);
}
</script>
<div bind:this={container} class="three-container">
<canvas bind:this={canvas}></canvas>
</div>
<style>
.three-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100vh;
z-index: 0; /* Behind everything */
pointer-events: none; /* Let clicks pass through */
opacity: 0.6; /* Subtle blend */
}
canvas {
display: block;
}
</style>

View File

@@ -1,49 +1,159 @@
<script> <script lang="ts">
import { onMount } from "svelte"; import { onMount } from "svelte";
import { isTauri } from "@tauri-apps/api/core"; import { isTauri } from "@tauri-apps/api/core";
import WebNavbar from "$lib/components/WebNavbar.svelte"; import CustomTabBar from "$lib/components/CustomTabBar.svelte";
import WebFooter from "$lib/components/WebFooter.svelte"; import CameraScreen from "$lib/components/CameraScreen.svelte";
import MobileTabBar from "$lib/components/MobileTabBar.svelte"; import { page } from "$app/stores";
import { goto } from "$app/navigation";
let { children } = $props(); let { children } = $props();
let isApp = $state(false); let isApp = $state(false);
let isCameraActive = $state(false);
let recentItems = $state([
{
id: 101,
title: "Plastic Bottle",
date: new Date().toLocaleString(),
impact: "High",
imageUri: null,
},
{
id: 102,
title: "Aluminum Can",
date: new Date(Date.now() - 86400000).toLocaleString(),
impact: "Low",
imageUri: null,
},
]);
onMount(() => { onMount(() => {
isApp = isTauri(); isApp = isTauri();
if (isApp) { if (isApp) {
document.body.classList.add("platform-native"); document.body.classList.add("platform-native");
} else { } else {
document.body.classList.add("platform-web"); document.body.classList.add("platform-web");
} }
const handleScan = () => {
isCameraActive = true;
};
window.addEventListener("scan", handleScan);
return () => {
window.removeEventListener("scan", handleScan);
};
}); });
function addRecentItem(item: any) {
recentItems = [item, ...recentItems];
}
const navLinks = [
{ name: "Home", route: "/", icon: "ri:home-4-line" },
{ name: "Goal", route: "/community", icon: "ri:flag-2-line" },
{ name: "Chat", route: "/chat", icon: "ri:chat-3-line" },
{ name: "Report", route: "/report", icon: "ri:alarm-warning-line" },
];
</script> </script>
{#if isApp} {#if isApp}
<main class="app-container"> <main class="app-container">
{@render children()} {@render children()}
</main> </main>
<MobileTabBar /> <CustomTabBar currentRoute={$page.route.id || "/"} />
{:else} {:else}
<WebNavbar /> <nav class="desktop-nav">
<main class="web-container"> <div class="nav-container">
<a href="/" class="nav-brand">
<div class="brand-icon">
<img
src="/ethix-logo.png"
alt="Ethix Logo"
class="brand-logo-img"
/>
</div>
<div class="brand-content">
<h1 class="brand-title">Ethix</h1>
<p class="brand-tagline">Truth in every scan</p>
</div>
</a>
<div class="nav-links">
{#each navLinks as link}
<a
href={link.route}
class="nav-link"
class:active={$page.route.id === link.route}
>
<iconify-icon icon={link.icon} width="20"
></iconify-icon>
<span>{link.name}</span>
</a>
{/each}
<a href="/catalogue" class="catalogue-button">
<iconify-icon icon="ri:search-line" width="20"
></iconify-icon>
<span>Browse Catalogue</span>
</a>
</div>
</div>
<svg
class="cloud-bg cloud-far"
viewBox="0 0 2880 200"
preserveAspectRatio="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill="rgba(255,255,255,0.4)"
d="M0,0 L2880,0 L2880,150 Q2760,180 2640,155 Q2520,130 2400,160 Q2280,190 2160,165 Q2040,140 1920,170 Q1800,200 1680,170 Q1560,140 1440,165 Q1320,190 1200,160 Q1080,130 960,155 Q840,180 720,150 Q600,120 480,150 Q360,180 240,155 Q120,130 0,150 L0,0 Z"
/>
</svg>
<svg
class="cloud-bg cloud-back"
viewBox="0 0 2880 200"
preserveAspectRatio="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill="rgba(255,255,255,0.65)"
d="M0,0 L2880,0 L2880,130 Q2800,160 2720,140 Q2640,120 2560,145 Q2480,170 2400,150 Q2320,130 2240,155 Q2160,180 2080,160 Q2000,140 1920,165 Q1840,190 1760,165 Q1680,140 1600,160 Q1520,180 1440,155 Q1360,130 1280,150 Q1200,170 1120,145 Q1040,120 960,145 Q880,170 800,150 Q720,130 640,155 Q560,180 480,160 Q400,140 320,165 Q240,190 160,170 Q80,150 0,130 L0,0 Z"
/>
</svg>
<svg
class="cloud-bg cloud-front"
viewBox="0 0 2880 200"
preserveAspectRatio="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill="rgba(255,255,255,0.95)"
d="M0,0 L2880,0 L2880,100 Q2820,75 2760,95 Q2700,115 2640,90 Q2580,65 2520,90 Q2460,115 2400,95 Q2340,75 2280,100 Q2220,125 2160,105 Q2100,85 2040,110 Q1980,135 1920,110 Q1860,85 1800,105 Q1740,125 1680,100 Q1620,75 1560,95 Q1500,115 1440,90 Q1380,65 1320,90 Q1260,115 1200,95 Q1140,75 1080,100 Q1020,125 960,105 Q900,85 840,110 Q780,135 720,110 Q660,85 600,105 Q540,125 480,100 Q420,75 360,95 Q300,115 240,90 Q180,65 120,90 Q60,115 0,100 L0,0 Z"
/>
</svg>
</nav>
<main class="app-container">
<div class="content-wrapper">
{@render children()} {@render children()}
</div>
</main> </main>
<WebFooter />
{/if} {/if}
<style> <style>
:global(body) { :global(body) {
margin: 0; margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, font-family:
Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; "Circular",
"Inter",
-apple-system,
BlinkMacSystemFont,
sans-serif;
background-color: #000000;
color: white;
overflow: hidden;
} }
.web-container { .web-container {
min-height: 80vh; min-height: 80vh;
max-width: 1200px; max-width: 1200px;
margin: 0 auto;
padding: 20px; padding: 20px;
} }
@@ -65,4 +175,171 @@
padding: 12px 24px; padding: 12px 24px;
font-size: 1.1rem; font-size: 1.1rem;
} }
:global(*) {
box-sizing: border-box;
}
.desktop-nav {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: auto;
overflow: visible;
background: transparent;
display: flex;
flex-direction: column;
justify-content: flex-start;
box-shadow: none;
z-index: 100;
pointer-events: none;
}
.nav-container {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24px 40px;
max-width: 1400px;
margin: 0 auto;
width: 100%;
position: relative;
z-index: 10;
pointer-events: auto;
}
.nav-brand {
display: flex;
align-items: center;
gap: 16px;
text-decoration: none;
}
.brand-icon {
background: white;
border-radius: 50%;
padding: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
}
.brand-logo-img {
width: 32px;
height: 32px;
object-fit: contain;
}
.brand-content {
display: flex;
flex-direction: column;
}
.brand-title {
font-size: 24px;
font-weight: 900;
color: #1a1a1a;
margin: 0;
line-height: 1;
}
.brand-tagline {
font-size: 12px;
color: #555;
margin: 4px 0 0 0;
font-weight: 600;
}
.nav-links {
display: flex;
align-items: center;
gap: 32px;
}
.nav-link {
display: flex;
align-items: center;
gap: 8px;
text-decoration: none;
color: #1a1a1a;
font-weight: 700;
font-size: 16px;
padding: 8px 16px;
border-radius: 20px;
transition: all 0.2s;
}
.nav-link:hover {
background: rgba(255, 255, 255, 0.5);
color: #166534;
}
.nav-link.active {
background: white;
color: #166534;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.catalogue-button {
background: #22c55e;
color: #000000;
display: flex;
align-items: center;
gap: 8px;
padding: 12px 24px;
border-radius: 30px;
text-decoration: none;
font-weight: 700;
transition: transform 0.2s;
box-shadow: 0 4px 15px rgba(34, 197, 94, 0.3);
}
.catalogue-button:hover {
transform: translateY(-2px);
background: #16a34a;
color: white;
}
.cloud-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 150px;
z-index: 1;
pointer-events: none;
overflow: hidden;
}
.cloud-far {
z-index: 1;
opacity: 0.8;
transform-origin: center;
animation: drift 60s ease-in-out infinite alternate;
}
.cloud-back {
z-index: 2;
opacity: 0.9;
transform-origin: center;
animation: drift 45s ease-in-out infinite alternate-reverse;
}
.cloud-front {
z-index: 3;
transform-origin: center;
animation: drift 30s ease-in-out infinite alternate;
}
@keyframes drift {
0% {
transform: scaleY(1) translateX(-20px);
}
100% {
transform: scaleY(1.05) translateX(20px);
}
}
</style> </style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,338 @@
<script lang="ts">
import ParallaxLandscape from "$lib/components/ParallaxLandscape.svelte";
const categories = ["All", "Food", "Fashion", "Tech", "Home"];
let selectedCategory = $state("All");
let searchQuery = $state("");
const products = [
{
id: 1,
name: "Eco-Water Bottle",
brand: "PureLife",
category: "Home",
score: 92,
grade: "A",
image: "ri:cup-line",
color: "#1ed760" // Spotify Green
},
{
id: 2,
name: "Fast Fashion Tee",
brand: "TrendZ",
category: "Fashion",
score: 35,
grade: "D",
image: "ri:t-shirt-2-line",
color: "#e91429" // Spotify Red (Error)
},
{
id: 3,
name: "Organic Coffee",
brand: "BeanGreen",
category: "Food",
score: 88,
grade: "B+",
image: "ri:cup-fill",
color: "#b49bc8" // Lavender
},
{
id: 4,
name: "Smartphone X",
brand: "TechGiant",
category: "Tech",
score: 45,
grade: "C",
image: "ri:smartphone-line",
color: "#f59b23" // Spotify Orange
},
{
id: 5,
name: "Bamboo Toothbrush",
brand: "SmileEco",
category: "Home",
score: 98,
grade: "A+",
image: "ri:brush-line",
color: "#1db954" // Slightly darker green
},
{
id: 6,
name: "Plastic Straws",
brand: "SingleUse Inc",
category: "Home",
score: 12,
grade: "F",
image: "ri:forbid-2-line",
color: "#e91429"
}
];
let filteredProducts = $derived(
products.filter(p => {
const matchesCategory = selectedCategory === "All" || p.category === selectedCategory;
const matchesSearch = p.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
p.brand.toLowerCase().includes(searchQuery.toLowerCase());
return matchesCategory && matchesSearch;
})
);
</script>
<div class="page-wrapper">
<div class="desktop-bg">
<ParallaxLandscape />
</div>
<div class="content-container">
<div class="header">
<h1 class="page-title">Product Database</h1>
<p class="subtitle">Search our verified sustainability ratings</p>
</div>
<!-- Search Bar -->
<div class="search-container">
<iconify-icon icon="ri:search-line" class="search-icon"></iconify-icon>
<input
type="text"
class="search-input"
placeholder="Search for products, brands..."
bind:value={searchQuery}
/>
</div>
<!-- Category Filter -->
<div class="filter-bar">
{#each categories as category}
<button
class="filter-chip"
class:active={selectedCategory === category}
on:click={() => selectedCategory = category}
>
{category}
</button>
{/each}
</div>
<!-- Product Grid -->
<div class="product-grid">
{#each filteredProducts as product}
<div class="product-card">
<div class="card-image-placeholder">
<iconify-icon icon={product.image} width="64" style="color: {product.color};"></iconify-icon>
</div>
<div class="product-info">
<h3 class="product-name">{product.name}</h3>
<p class="product-brand">{product.brand}</p>
</div>
<div class="score-badge" style="background-color: {product.color};">
<span class="score-text">{product.score}</span>
</div>
</div>
{/each}
</div>
</div>
</div>
<style>
.page-wrapper {
width: 100%;
min-height: 100vh;
background-color: #000000;
overflow-x: hidden;
position: relative;
}
.desktop-bg {
display: none;
}
.content-container {
position: relative;
z-index: 10;
padding: 100px 24px 120px;
max-width: 1200px;
margin: 0 auto;
}
.header {
text-align: left;
margin-bottom: 32px;
padding: 0 12px;
}
.page-title {
color: white;
font-size: 48px;
font-weight: 900;
margin: 0;
letter-spacing: -2px;
}
.subtitle {
color: #b3b3b3;
font-size: 16px;
margin: 8px 0 0 0;
font-weight: 500;
}
/* Search Bar */
.search-container {
position: relative;
margin-bottom: 24px;
padding: 0 12px;
}
.search-input {
width: 100%;
background: #2a2a2a;
border: none;
border-radius: 500px; /* Capsule */
padding: 14px 48px;
color: white;
font-size: 16px;
font-weight: 500;
outline: none;
transition: background 0.2s;
}
.search-input:focus {
background: #333333;
box-shadow: 0 0 0 2px white; /* Focus ring */
}
.search-input::placeholder {
color: #b3b3b3;
}
.search-icon {
position: absolute;
left: 32px;
top: 50%;
transform: translateY(-50%);
color: #b3b3b3;
font-size: 20px;
pointer-events: none;
}
/* Filters */
.filter-bar {
display: flex;
gap: 12px;
margin-bottom: 32px;
flex-wrap: wrap;
padding: 0 12px;
}
.filter-chip {
background: #2a2a2a; /* Pill bg */
color: white;
border: none;
padding: 8px 16px;
border-radius: 500px;
font-weight: 600;
font-size: 14px;
cursor: pointer;
transition: all 0.2s ease;
}
.filter-chip:hover {
background: #333333;
}
.filter-chip.active {
background: #1ed760;
color: #000000;
}
/* Grid */
.product-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 24px;
padding: 0 12px;
}
.product-card {
background: #181818; /* Spotify Card */
border-radius: 8px;
padding: 16px;
transition: background-color 0.3s ease;
display: flex;
flex-direction: column;
cursor: pointer;
position: relative;
}
.product-card:hover {
background-color: #282828;
}
.card-image-placeholder {
width: 100%;
aspect-ratio: 1;
background: #333333;
border-radius: 4px; /* Slightly rounded images */
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 16px;
box-shadow: 0 8px 24px rgba(0,0,0,0.5);
}
.product-info {
flex-grow: 1;
}
.product-name {
color: white;
font-size: 16px;
font-weight: 700;
margin: 0 0 4px 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.product-brand {
color: #b3b3b3;
font-size: 14px;
margin: 0;
}
.score-badge {
position: absolute;
top: 24px;
right: 24px;
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
}
.score-text {
color: #000000;
font-weight: 900;
font-size: 12px;
}
@media (min-width: 768px) {
.desktop-bg {
display: block;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.page-wrapper {
background: transparent;
}
}
</style>

View File

@@ -0,0 +1,362 @@
<script lang="ts">
import { onMount } from "svelte";
import ParallaxLandscape from "$lib/components/ParallaxLandscape.svelte";
import CloudSection from "$lib/components/CloudSection.svelte";
let messages = $state([
{
id: 1,
text: "Hello! I'm Ethix AI. Ask me anything about recycling, sustainability, or green products.",
sender: "ai",
},
]);
let inputText = $state("");
let canvasElement = $state<HTMLCanvasElement>();
function sendMessage() {
if (!inputText.trim()) return;
const userMsg = {
id: Date.now(),
text: inputText,
sender: "user",
};
const aiResponse = generateResponse(inputText);
messages = [...messages, userMsg];
inputText = "";
setTimeout(() => {
const aiMsg = {
id: Date.now() + 1,
text: aiResponse,
sender: "ai",
};
messages = [...messages, aiMsg];
}, 1000);
}
function generateResponse(text: string): string {
const lower = text.toLowerCase();
if (lower.includes("plastic"))
return "Plastic takes 450 years to decompose. Check the resin code (triangle number) to see if you can recycle it.";
if (lower.includes("glass"))
return "Glass is 100% recyclable. You can recycle it forever without losing quality.";
if (lower.includes("aluminum"))
return "Aluminum is sustainable gold. Infinite recycling, low energy cost to reuse.";
return "Great question! Sustainable living starts with buying less, then reusing, then recycling.";
}
function handleKeyDown(event: KeyboardEvent) {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
sendMessage();
}
}
onMount(() => {
if (!canvasElement) return;
const ctx = canvasElement.getContext("2d");
if (!ctx) return;
let frame = 0;
function animate() {
frame++;
if (!ctx || !canvasElement) return;
ctx.clearRect(0, 0, 100, 100);
const yOffset = Math.sin(frame * 0.05) * 5;
ctx.fillStyle = "#e0e0e0";
ctx.beginPath();
ctx.arc(50, 50, 45, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "#22c55e";
ctx.beginPath();
ctx.arc(50, 50 + yOffset, 35, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = "#16a34a";
ctx.beginPath();
ctx.moveTo(50, 15 + yOffset);
ctx.quadraticCurveTo(70, 5 + yOffset, 60, 35 + yOffset);
ctx.closePath();
ctx.fill();
ctx.fillStyle = "white";
ctx.fillRect(35, 40 + yOffset, 8, 12);
ctx.fillRect(57, 40 + yOffset, 8, 12);
ctx.strokeStyle = "white";
ctx.lineWidth = 3;
ctx.beginPath();
ctx.arc(50, 65 + yOffset, 10, 0.2, Math.PI - 0.2);
ctx.stroke();
requestAnimationFrame(animate);
}
animate();
});
</script>
<div class="page-wrapper">
<div class="desktop-bg">
<ParallaxLandscape />
</div>
<div class="chat-wrapper-outer">
<CloudSection className="chat-cloud-section">
<div class="chat-layout">
<div class="header">
<div class="mascot-container">
<canvas
bind:this={canvasElement}
width="100"
height="100"
class="mascot-canvas"
></canvas>
<div class="mascot-status-dot"></div>
</div>
<h1 class="page-title">Ethix Assistant</h1>
</div>
<div class="chat-window">
<div class="messages-container">
{#each messages as msg (msg.id)}
<div
class="message"
class:user-message={msg.sender === "user"}
class:ai-message={msg.sender === "ai"}
>
<p>{msg.text}</p>
</div>
{/each}
</div>
<div class="input-container">
<input
type="text"
class="message-input"
placeholder="Ask about sustainability..."
bind:value={inputText}
onkeydown={handleKeyDown}
/>
<button
class="send-button"
onclick={sendMessage}
aria-label="Send message"
>
<iconify-icon
icon="ri:send-plane-fill"
width="24"
style="color: #ffffff;"
></iconify-icon>
</button>
</div>
</div>
</div>
</CloudSection>
</div>
</div>
<style>
.page-wrapper {
width: 100%;
height: 100vh;
overflow: hidden;
position: relative;
display: flex;
justify-content: center;
}
.desktop-bg {
display: none;
}
.chat-layout {
position: relative;
z-index: 10;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
.header {
padding: 24px;
padding-top: 20px;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
text-align: center;
z-index: 20;
}
.mascot-container {
position: relative;
width: 80px;
height: 80px;
margin: 0 auto 12px;
}
.mascot-canvas {
width: 80px;
height: 80px;
filter: drop-shadow(0 4px 10px rgba(34, 197, 94, 0.3));
}
.mascot-status-dot {
position: absolute;
bottom: 10px;
right: 10px;
width: 12px;
height: 12px;
background: #22c55e;
border: 2px solid white;
border-radius: 50%;
box-shadow: 0 0 10px rgba(34, 197, 94, 0.5);
}
.page-title {
color: #000000;
font-size: 28px;
font-weight: 800;
margin: 0;
}
.chat-window {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
}
.messages-container {
flex: 1;
overflow-y: auto;
padding: 24px;
padding-bottom: 120px;
display: flex;
flex-direction: column;
gap: 16px;
}
.message {
padding: 16px 24px;
border-radius: 24px;
max-width: 80%;
font-size: 16px;
line-height: 1.5;
position: relative;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.user-message {
align-self: flex-end;
background-color: #22c55e;
border-bottom-right-radius: 4px;
color: #000000;
font-weight: 600;
}
.ai-message {
align-self: flex-start;
background-color: #ffffff;
border-bottom-left-radius: 4px;
color: #1a1a1a;
border: 1px solid rgba(0, 0, 0, 0.1);
}
.message p {
margin: 0;
}
.input-container {
position: relative;
margin: 20px;
padding: 12px;
background-color: #ffffff;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 40px;
display: flex;
align-items: center;
gap: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
}
.message-input {
flex: 1;
background: transparent;
color: #1a1a1a;
padding: 12px 16px;
border: none;
font-size: 16px;
outline: none;
}
.message-input::placeholder {
color: #9ca3af;
}
.send-button {
background-color: #22c55e;
width: 48px;
height: 48px;
border-radius: 50%;
border: none;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
transition: all 0.2s ease;
flex-shrink: 0;
}
.send-button:hover {
transform: scale(1.05);
background-color: #16a34a;
box-shadow: 0 0 15px rgba(34, 197, 94, 0.4);
}
@media (min-width: 768px) {
.desktop-bg {
display: block;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.page-wrapper {
background: transparent;
}
.chat-layout {
margin-top: 160px;
margin-bottom: 0;
border-radius: 0;
border: none;
box-shadow: none;
height: 640px;
overflow: hidden;
background: transparent;
}
.header {
padding-top: 24px;
border-top-left-radius: 0;
border-top-right-radius: 0;
background: transparent;
}
.input-container {
position: relative;
bottom: 0;
left: 0;
right: 0;
margin: 20px;
width: auto;
}
}
</style>

View File

@@ -0,0 +1,317 @@
<script lang="ts">
import ParallaxLandscape from "$lib/components/ParallaxLandscape.svelte";
import CloudSection from "$lib/components/CloudSection.svelte";
</script>
<div class="page-wrapper">
<div class="desktop-bg">
<ParallaxLandscape />
</div>
<div class="content-container">
<CloudSection>
<div class="header">
<h1 class="page-title">Why We Exist</h1>
<p class="subtitle">Our Mission & Goal</p>
</div>
<div class="grid-layout">
<!-- Challenge / Problem Card -->
<div class="card mission-card">
<div class="icon-circle problem-icon">
<svg
xmlns="http://www.w3.org/2000/svg"
width="40"
height="40"
viewBox="0 0 24 24"
>
<path
fill="#e91429"
d="M7 16q.425 0 .713-.288T8 15t-.288-.712T7 14t-.712.288T6 15t.288.713T7 16m-1-3h2V8H6zm4 2h8v-2h-8zm0-4h8V9h-8zm-6 9q-.825 0-1.412-.587T2 18V6q0-.825.588-1.412T4 4h16q.825 0 1.413.588T22 6v12q0 .825-.587 1.413T20 20z"
/>
</svg>
</div>
<h2 class="card-title">The Problem</h2>
<p class="card-desc">
"Greenwashing" is everywhere. Companies spend millions
to make you believe their products are sustainable, when
often they are not.
</p>
<div class="stat-highlight">
<span class="stat-number">53%</span>
<span class="stat-label"
>of green claims are vague or misleading</span
>
</div>
</div>
<!-- Solution Card -->
<div class="card mission-card">
<div class="icon-circle solution-icon">
<svg
xmlns="http://www.w3.org/2000/svg"
width="40"
height="40"
viewBox="0 0 24 24"
>
<path
fill="#1ed760"
d="m23.5 17l-5 5l-3.5-3.5l1.5-1.5l2 2l3.5-3.5zM12 9a3 3 0 0 1 3 3a3 3 0 0 1-3 3a3 3 0 0 1-3-3a3 3 0 0 1 3-3m0-4.5c5 0 9.27 3.11 11 7.5c-.25.65-.56 1.26-.92 1.85a5.8 5.8 0 0 0-1.9-.73l.64-1.12a9.821 9.821 0 0 0-17.64 0A9.82 9.82 0 0 0 12 17.5l1.21-.07c-.14.5-.21 1.03-.21 1.57v.46l-1 .04c-5 0-9.27-3.11-11-7.5c1.73-4.39 6-7.5 11-7.5"
/>
</svg>
</div>
<h2 class="card-title">The Solution</h2>
<p class="card-desc">
We believe in radical transparency. By using AI to
analyze packaging and verify claims, we give power back
to you, the consumer.
</p>
<ul class="benefit-list">
<li>
<iconify-icon
icon="ri:check-line"
style="color: #1ed760;"
></iconify-icon>
<span>Instant Fact-Checking</span>
</li>
<li>
<iconify-icon
icon="ri:check-line"
style="color: #1ed760;"
></iconify-icon>
<span>Unbiased Eco-Ratings</span>
</li>
<li>
<iconify-icon
icon="ri:check-line"
style="color: #1ed760;"
></iconify-icon>
<span>Real Sustainable Alternatives</span>
</li>
</ul>
</div>
<!-- Goal Card -->
<div class="card goal-card">
<div class="header-row">
<h2 class="card-title large">Our Ultimate Goal</h2>
<iconify-icon
icon="ri:flag-fill"
width="32"
style="color: #1ed760;"
></iconify-icon>
</div>
<p class="goal-text">
To create a world where <span class="highlight"
>sustainability is the default</span
>, not a luxury. Where every purchase you make pushes
the industry toward a cleaner, ethical future.
</p>
</div>
</div>
</CloudSection>
</div>
</div>
<style>
.page-wrapper {
width: 100%;
min-height: 100vh;
background-color: #000000;
overflow-x: hidden;
position: relative;
}
.desktop-bg {
display: none;
}
.content-container {
position: relative;
z-index: 10;
padding: 80px 24px 120px;
max-width: 800px;
margin: 0 auto;
}
.header {
text-align: center;
margin-bottom: 40px;
padding: 0 20px;
}
.page-title {
color: #000000;
font-size: 48px;
font-weight: 900;
margin: 0;
letter-spacing: -2px;
}
.subtitle {
color: #4b5563;
font-size: 16px;
margin: 16px 0 0 0;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 2px;
}
.grid-layout {
display: flex;
flex-direction: column;
gap: 24px;
}
.card {
background: #a1a1aa; /* Light Grey Card */
padding: 40px;
border-radius: 8px;
box-shadow: none;
position: relative;
overflow: hidden;
}
.mission-card {
display: flex;
flex-direction: column;
align-items: flex-start;
}
.goal-card {
background: #a1a1aa;
text-align: center;
align-items: center;
display: flex;
flex-direction: column;
}
.icon-circle {
width: 60px;
height: 60px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 24px;
}
.problem-icon {
background: rgba(233, 20, 41, 0.1);
}
.solution-icon {
background: rgba(34, 197, 94, 0.1); /* Eco Green tint */
}
.card-title {
color: #1a1a1a;
font-size: 24px;
font-weight: 700;
margin: 0 0 16px 0;
}
.card-title.large {
font-size: 28px;
margin-bottom: 0;
}
.card-desc {
color: #333333;
font-size: 16px;
line-height: 1.6;
margin: 0 0 24px 0;
}
.stat-highlight {
background: #374151; /* Darker grey */
padding: 16px 24px;
border-radius: 8px;
display: flex;
align-items: center;
gap: 16px;
width: 100%;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.stat-number {
font-size: 32px;
font-weight: 900;
color: #e91429;
}
.stat-label {
color: #f3f4f6; /* Light text on dark bg */
font-size: 14px;
font-weight: 600;
line-height: 1.4;
}
.benefit-list {
list-style: none;
padding: 0;
margin: 0;
width: 100%;
}
.benefit-list li {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
font-size: 16px;
color: #1a1a1a;
font-weight: 700;
}
.header-row {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 24px;
}
.goal-text {
color: #333333;
font-size: 18px;
line-height: 1.6;
margin-bottom: 0;
max-width: 600px;
}
.highlight {
color: #166534;
font-weight: 700;
}
@media (min-width: 768px) {
.desktop-bg {
display: block;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.page-wrapper {
background: transparent;
}
}
@media (max-width: 767px) {
.content-container {
padding: 40px 20px 100px;
}
.header {
padding: 0;
margin-bottom: 30px;
}
.page-title {
font-size: 32px;
}
}
</style>

View File

@@ -0,0 +1,113 @@
<script lang="ts">
const news = [
{
id: 1,
title: "Ocean Cleanup hits milestone",
desc: "500 tons removed from Pacific patch.",
date: "2h ago",
},
{
id: 2,
title: "New Plastic Ban in effect",
desc: "Major cities adopt strict policies.",
date: "5h ago",
},
{
id: 3,
title: "Ethix App launches globally",
desc: "Empowering users to make better choices.",
date: "1d ago",
},
];
</script>
<div class="page-container">
<div class="safe-area">
<div class="header">
<h1 class="page-title">Eco News</h1>
<p class="subtitle">Latest sustainability updates</p>
</div>
<div class="scroll-content">
{#each news as item (item.id)}
<div class="news-card">
<div class="news-image"></div>
<p class="news-meta">{item.date} • Trending</p>
<h2 class="news-title">{item.title}</h2>
<p class="news-desc">{item.desc}</p>
</div>
{/each}
</div>
</div>
</div>
<style>
.page-container {
width: 100%;
height: 100vh;
background-color: #0f172a;
overflow-y: auto;
}
.safe-area {
padding-top: 50px;
padding-bottom: 120px;
}
.header {
padding: 24px;
padding-top: 30px;
}
.page-title {
color: white;
font-size: 34px;
font-weight: 800;
letter-spacing: -0.5px;
margin: 0;
}
.subtitle {
color: #94a3b8;
font-size: 14px;
margin: 4px 0 0 0;
}
.scroll-content {
padding: 24px;
padding-bottom: 140px;
}
.news-card {
background-color: rgba(30, 41, 59, 0.9);
padding: 20px;
border-radius: 24px;
border: 1px solid #334155;
margin-bottom: 16px;
}
.news-image {
height: 120px;
background-color: #334155;
border-radius: 12px;
margin-bottom: 12px;
}
.news-meta {
color: #94a3b8;
font-size: 12px;
margin: 0 0 4px 0;
}
.news-title {
color: white;
font-weight: bold;
font-size: 18px;
margin: 4px 0 8px 0;
}
.news-desc {
color: #cbd5e1;
margin: 0;
}
</style>

View File

@@ -0,0 +1,424 @@
<script lang="ts">
import { onMount } from "svelte";
import { goto } from "$app/navigation";
interface Item {
id: number;
title: string;
date: string;
impact: string;
imageUri: string | null;
}
let item = $state<Item | null>(null);
onMount(() => {
const storedItem = sessionStorage.getItem("selectedItem");
if (storedItem) {
item = JSON.parse(storedItem);
}
});
function navigateToReport() {
if (item) {
goto(`/report?productName=${encodeURIComponent(item.title)}`);
}
}
</script>
<div class="page-container">
{#if item}
<div class="safe-area">
<div class="header">
<button class="back-button" on:click={() => window.history.back()}>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 320 512"
fill="white"
>
<path
d="M224 480c-8.188 0-16.38-3.125-22.62-9.375l-192-192c-12.5-12.5-12.5-32.75 0-45.25l192-192c12.5-12.5 32.75-12.5 45.25 0s12.5 32.75 0 45.25L77.25 256l169.4 169.4c12.5 12.5 12.5 32.75 0 45.25C240.4 476.9 232.2 480 224 480z"
/>
</svg>
</button>
<h1 class="page-title">Scan Report</h1>
<p class="subtitle">Detailed analysis</p>
</div>
<div class="scroll-content">
<div class="card">
<div class="header-row">
<div class="title-section">
<p class="date">{item.date}</p>
<h2 class="product-title">{item.title}</h2>
</div>
<div
class="impact-badge"
class:high-impact={item.impact === "High"}
class:low-impact={item.impact === "Low"}
>
{item.impact} Impact
</div>
</div>
<div class="analysis-box">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
fill={item.impact === "High" ? "#ef4444" : "#22c55e"}
class="alert-icon"
>
<path
d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zm0-384c13.3 0 24 10.7 24 24V264c0 13.3-10.7 24-24 24s-24-10.7-24-24V152c0-13.3 10.7-24 24-24zM224 352a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z"
/>
</svg>
<div class="analysis-text">
<h3>Analysis Result</h3>
<p>
{#if item.impact === "High"}
This item takes 450+ years to decompose. Consider switching
to sustainable alternatives immediately.
{:else}
This item is eco-friendly or easily recyclable. Good choice!
{/if}
</p>
</div>
</div>
<div class="alternatives-section">
<h3 class="alternatives-title">Recommended Alternatives</h3>
<div class="alternatives-scroll">
<div class="alternative-card glass">
<div class="alt-header">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 384 512"
fill="#60a5fa"
>
<path
d="M192 512C86 512 0 426 0 320C0 228.8 130.2 57.7 166.6 11.7C172.6 4.2 181.5 0 191.1 0h1.8c9.6 0 18.5 4.2 24.5 11.7C253.8 57.7 384 228.8 384 320c0 106-86 192-192 192zM96 336c0-8.8-7.2-16-16-16s-16 7.2-16 16c0 61.9 50.1 112 112 112 8.8 0 16-7.2 16-16s-7.2-16-16-16c-44.2 0-80-35.8-80-80z"
/>
</svg>
<span class="rating">★ 4.9</span>
</div>
<h4 class="alt-name">Glass Bottle</h4>
<p class="alt-price">$2.49</p>
</div>
<div class="alternative-card boxed">
<div class="alt-header">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 448 512"
fill="#a78bfa"
>
<path
d="M384 480H64c-35.3 0-64-28.7-64-64V96C0 60.7 28.7 32 64 32h320c35.3 0 64 28.7 64 64v320c0 35.3-28.7 64-64 64z"
/>
</svg>
<span class="rating">★ 4.7</span>
</div>
<h4 class="alt-name">Boxed Water</h4>
<p class="alt-price">$1.89</p>
</div>
<div class="alternative-card aluminum">
<div class="alt-header">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
fill="#9ca3af"
>
<path
d="M0 192c0-35.3 28.7-64 64-64c.5 0 1.1 0 1.6 0C73 91.5 105.3 64 144 64c15 0 29 4.1 40.9 11.2C198.2 49.6 225.1 32 256 32s57.8 17.6 71.1 43.2C339 68.1 353 64 368 64c38.7 0 71 27.5 78.4 64c.5 0 1.1 0 1.6 0c35.3 0 64 28.7 64 64c0 11.7-3.1 22.6-8.6 32H8.6C3.1 214.6 0 203.7 0 192zm0 91.4C0 268.3 12.3 256 27.4 256H484.6c15.1 0 27.4 12.3 27.4 27.4c0 70.5-44.4 130.7-106.7 154.1L403.5 452c-2 16-15.6 28-31.8 28H140.2c-16.1 0-29.8-12-31.8-28l-1.8-14.4C44.4 414.1 0 353.9 0 283.4z"
/>
</svg>
<span class="rating">★ 4.5</span>
</div>
<h4 class="alt-name">Aluminum</h4>
<p class="alt-price">$1.29</p>
</div>
</div>
</div>
{#if item.impact !== "Low"}
<button class="report-button" on:click={navigateToReport}>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 448 512"
fill="#ef4444"
>
<path
d="M64 32C64 14.3 49.7 0 32 0S0 14.3 0 32V64 368 480c0 17.7 14.3 32 32 32s32-14.3 32-32V352l64.3-16.1c41.1-10.3 84.6-5.5 122.5 13.4c44.2 22.1 95.5 24.8 141.7 7.4l34.7-13c12.5-4.7 20.8-16.6 20.8-30V66.1c0-23-24.2-38-44.8-27.7l-9.6 4.8c-46.3 23.2-100.8 23.2-147.1 0c-35.1-17.6-75.4-22-113.5-12.5L64 48V32z"
/>
</svg>
<span>Report Greenwashing</span>
</button>
{/if}
{#if item.imageUri}
<img
src={item.imageUri}
alt="Scanned product"
class="product-image"
/>
{/if}
</div>
</div>
</div>
{:else}
<div class="loading">
<p>Loading...</p>
</div>
{/if}
</div>
<style>
.page-container {
width: 100%;
height: 100vh;
background-color: #0f172a;
overflow-y: auto;
}
.loading {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
color: white;
}
.safe-area {
padding-top: 50px;
padding-bottom: 120px;
}
.header {
padding: 24px;
padding-top: 30px;
position: relative;
}
.back-button {
background: none;
border: none;
cursor: pointer;
padding: 8px;
margin-bottom: 16px;
}
.back-button svg {
width: 24px;
height: 24px;
}
.page-title {
color: white;
font-size: 34px;
font-weight: 800;
letter-spacing: -0.5px;
margin: 0;
}
.subtitle {
color: #94a3b8;
font-size: 14px;
margin: 4px 0 0 0;
}
.scroll-content {
padding: 24px;
padding-bottom: 140px;
}
.card {
background-color: rgba(30, 41, 59, 0.9);
padding: 20px;
border-radius: 24px;
border: 1px solid #334155;
}
.header-row {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 10px;
}
.title-section {
flex: 1;
}
.date {
color: #94a3b8;
font-size: 14px;
margin: 0 0 8px 0;
}
.product-title {
color: white;
font-size: 32px;
font-weight: bold;
margin: 0;
}
.impact-badge {
padding: 8px 16px;
border-radius: 12px;
font-size: 16px;
font-weight: bold;
flex-shrink: 0;
}
.high-impact {
background-color: rgba(239, 68, 68, 0.2);
color: #ef4444;
}
.low-impact {
background-color: rgba(34, 197, 94, 0.2);
color: #22c55e;
}
.analysis-box {
margin-top: 32px;
padding: 20px;
background-color: #1e293b;
border-radius: 16px;
display: flex;
align-items: center;
gap: 16px;
}
.alert-icon {
width: 32px;
height: 32px;
flex-shrink: 0;
}
.analysis-text {
flex: 1;
}
.analysis-text h3 {
color: white;
font-weight: bold;
font-size: 16px;
margin: 0 0 4px 0;
}
.analysis-text p {
color: #94a3b8;
margin: 0;
font-size: 14px;
}
.alternatives-section {
margin-top: 32px;
}
.alternatives-title {
color: white;
font-weight: bold;
font-size: 18px;
margin: 0 0 16px 0;
}
.alternatives-scroll {
display: flex;
gap: 12px;
overflow-x: auto;
padding-bottom: 8px;
}
.alternatives-scroll::-webkit-scrollbar {
height: 6px;
}
.alternatives-scroll::-webkit-scrollbar-track {
background: #1e293b;
border-radius: 3px;
}
.alternatives-scroll::-webkit-scrollbar-thumb {
background: #334155;
border-radius: 3px;
}
.alternative-card {
min-width: 140px;
background-color: rgba(30, 41, 59, 0.8);
padding: 16px;
border-radius: 16px;
border: 1px solid #334155;
}
.alt-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.alt-header svg {
width: 24px;
height: 24px;
}
.rating {
font-weight: bold;
color: #fbbf24;
font-size: 14px;
}
.alt-name {
font-weight: bold;
color: white;
font-size: 16px;
margin: 0 0 4px 0;
}
.alt-price {
color: #94a3b8;
margin: 0;
font-size: 14px;
}
.report-button {
margin-top: 32px;
background-color: rgba(239, 68, 68, 0.2);
padding: 16px;
border-radius: 12px;
border: none;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
cursor: pointer;
transition: all 0.3s ease;
width: 100%;
}
.report-button:hover {
background-color: rgba(239, 68, 68, 0.3);
}
.report-button svg {
width: 20px;
height: 20px;
}
.report-button span {
color: #ef4444;
font-weight: bold;
}
.product-image {
width: 100%;
height: 300px;
border-radius: 16px;
margin-top: 24px;
object-fit: cover;
}
</style>

View File

@@ -0,0 +1,466 @@
<script lang="ts">
import ParallaxLandscape from "$lib/components/ParallaxLandscape.svelte";
import CloudSection from "$lib/components/CloudSection.svelte";
let productName = $state("");
let description = $state("");
let image = $state<string | null>(null);
let submitted = $state(false);
$effect(() => {
const initialName = new URLSearchParams(window.location.search).get(
"productName",
);
if (initialName) {
productName = initialName;
}
});
let isValid = $derived(
productName.trim().length > 0 && description.trim().length > 0,
);
async function pickImage() {
const input = document.createElement("input");
input.type = "file";
input.accept = "image/*";
input.onchange = (e: Event) => {
const target = e.target as HTMLInputElement;
const file = target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = (event) => {
image = event.target?.result as string;
};
reader.readAsDataURL(file);
}
};
input.click();
}
function handleSubmit() {
if (!isValid) return;
submitted = true;
setTimeout(() => {
window.history.back();
}, 2000);
}
</script>
<div class="page-wrapper">
<div class="desktop-bg">
<ParallaxLandscape />
</div>
<div class="content-container">
{#if submitted}
<CloudSection>
<div class="success-card">
<div class="icon-circle success">
<iconify-icon
icon="ri:checkbox-circle-fill"
width="60"
style="color: #4ade80;"
></iconify-icon>
</div>
<h2 class="success-title">Report Submitted!</h2>
<p class="success-subtitle">
Thank you for keeping companies honest.
</p>
</div>
</CloudSection>
{:else}
<div class="header">
<h1 class="page-title">Report Greenwashing</h1>
<p class="subtitle">Call out false eco-claims</p>
</div>
<CloudSection>
<div class="form-content">
<div class="form-group">
<label class="label">Product Name</label>
<div class="input-wrapper">
<iconify-icon
icon="ri:price-tag-3-line"
class="input-icon"
></iconify-icon>
<input
type="text"
class="input"
placeholder="e.g. 'Eco-Friendly' Water Bottle"
bind:value={productName}
/>
</div>
</div>
<div class="form-group">
<label class="label">Why is it misleading?</label>
<div class="input-wrapper textarea-wrapper">
<iconify-icon
icon="ri:text-wrapper"
class="input-icon top-align"
></iconify-icon>
<textarea
class="textarea"
placeholder="Describe the claim and the reality..."
bind:value={description}
></textarea>
</div>
</div>
<div class="form-group">
<label class="label">Evidence (Photo)</label>
<button class="image-picker" on:click={pickImage}>
{#if image}
<img
src={image}
alt="Evidence"
class="picked-image"
/>
<div class="change-overlay">
<iconify-icon
icon="ri:camera-switch-line"
width="24"
></iconify-icon>
</div>
{:else}
<div class="picker-placeholder">
<iconify-icon
icon="ri:camera-add-line"
width="32"
style="color: #94a3b8;"
></iconify-icon>
<p>Upload Photo</p>
</div>
{/if}
</button>
</div>
<button
class="submit-button"
class:disabled={!isValid}
disabled={!isValid}
on:click={handleSubmit}
>
<iconify-icon icon="ri:alert-fill" width="20"
></iconify-icon>
Submit Report
</button>
</div>
</CloudSection>
{/if}
</div>
</div>
<style>
.page-wrapper {
width: 100%;
min-height: 100vh;
background-color: #09090b;
overflow-x: hidden;
position: relative;
}
.desktop-bg {
display: none;
}
.content-container {
position: relative;
z-index: 10;
padding: 100px 24px 120px;
max-width: 600px; /* Narrower form */
margin: 0 auto;
}
.header {
text-align: center;
margin-bottom: 20px;
padding: 0 20px;
background: transparent;
box-shadow: none;
border: none;
}
.page-title {
color: #000000;
font-size: 32px;
font-weight: 900;
margin: 0;
}
/* ... */
@media (max-width: 767px) {
.content-container {
padding: 40px 20px 100px;
}
.header {
border: none;
box-shadow: none;
padding: 0;
margin-bottom: 30px;
background: transparent;
}
.page-title {
font-size: 28px;
}
.form-content {
padding: 24px;
}
}
.subtitle {
color: #166534; /* Dark Green for report subtitle */
font-size: 16px;
margin: 8px 0 0 0;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1px;
}
.form-content {
display: flex;
flex-direction: column;
gap: 24px;
}
.header {
text-align: center;
margin: 0 auto 40px;
padding: 40px 60px;
background: #d4d4d8; /* Match CloudSection grey */
border-radius: 60px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
border: none;
width: fit-content;
min-width: 300px;
}
.form-group {
display: flex;
flex-direction: column;
gap: 12px;
}
.label {
color: #333333;
font-weight: 700;
font-size: 14px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.input-wrapper {
position: relative;
background-color: #f9fafb;
border-radius: 16px;
border: 1px solid rgba(0, 0, 0, 0.1);
transition: all 0.2s;
}
.input-wrapper:focus-within {
border-color: #22c55e;
box-shadow: 0 0 0 4px rgba(34, 197, 94, 0.1);
}
.input-icon {
position: absolute;
left: 16px;
top: 50%;
transform: translateY(-50%);
color: #a1a1aa;
font-size: 20px;
}
.top-align {
top: 20px;
transform: none;
}
.input {
width: 100%;
background: transparent;
color: #1a1a1a;
padding: 16px 16px 16px 48px;
border: none;
font-size: 16px;
outline: none;
}
.textarea {
width: 100%;
background: transparent;
color: #1a1a1a;
padding: 16px 16px 16px 48px;
border: none;
font-size: 16px;
outline: none;
height: 120px;
resize: none;
font-family: inherit;
}
.image-picker {
width: 100%;
background-color: #f9fafb;
border-radius: 16px;
height: 160px;
border: 2px dashed #d1d5db;
cursor: pointer;
transition: all 0.3s ease;
padding: 0;
overflow: hidden;
position: relative;
}
.image-picker:hover {
border-color: #22c55e;
background-color: #3f3f46;
}
.picker-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
gap: 12px;
}
.picker-placeholder p {
color: #a1a1aa;
font-weight: 600;
margin: 0;
}
.picked-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.change-overlay {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.2s;
color: white;
}
.image-picker:hover .change-overlay {
opacity: 1;
}
.submit-button {
margin-top: 12px;
background: #22c55e;
padding: 18px;
border-radius: 16px;
border: none;
color: #000000;
font-weight: 800;
font-size: 16px;
cursor: pointer;
transition: all 0.3s ease;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
box-shadow: 0 4px 20px rgba(34, 197, 94, 0.3);
}
.submit-button:hover:not(.disabled) {
transform: scale(1.02);
background: #16a34a;
box-shadow: 0 8px 30px rgba(34, 197, 94, 0.5);
color: white;
}
.submit-button.disabled {
background: #27272a;
color: #52525b;
cursor: not-allowed;
box-shadow: none;
}
.success-card {
background: #18181b;
padding: 60px 40px;
border-radius: 32px;
border: 1px solid #27272a;
text-align: center;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
display: flex;
flex-direction: column;
align-items: center;
}
.icon-circle.success {
background: rgba(0, 220, 130, 0.1);
width: 100px;
height: 100px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 24px;
}
.success-title {
color: white;
font-size: 32px;
font-weight: 900;
margin: 0 0 12px 0;
}
.success-subtitle {
color: #a1a1aa;
font-size: 18px;
margin: 0;
}
@media (min-width: 768px) {
.desktop-bg {
display: block;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.page-wrapper {
background: transparent;
}
}
@media (max-width: 767px) {
.content-container {
padding: 40px 20px 100px;
}
.header {
border: none;
box-shadow: none;
padding: 0;
margin-bottom: 30px;
background: transparent;
}
.page-title {
font-size: 28px;
}
.form-content {
padding: 24px;
}
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 435 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB