UI Rework

This commit is contained in:
2026-01-24 21:30:08 +00:00
parent 4183d7f122
commit 1e8b7082a4
37 changed files with 4648 additions and 2754 deletions

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import { onMount } from "svelte";
import Icon from "@iconify/svelte";
interface Props {
onClose: () => void;
@@ -8,15 +9,15 @@
let { onClose, onScanComplete }: Props = $props();
let videoElement: HTMLVideoElement;
let videoElement = $state<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;
let useCamera = $state(true);
let fileInputElement = $state<HTMLInputElement>();
onMount(() => {
const initCamera = async () => {
@@ -53,7 +54,6 @@
capturedImage = imageData;
analyzing = true;
// Simulate analysis
setTimeout(() => {
const newItem = {
id: Date.now(),
@@ -74,6 +74,7 @@
}
async function takePicture() {
if (!videoElement) return;
analyzing = true;
const canvas = document.createElement("canvas");
@@ -85,7 +86,6 @@
capturedImage = canvas.toDataURL("image/png");
}
// Simulate analysis
setTimeout(() => {
const newItem = {
id: Date.now(),
@@ -132,62 +132,45 @@
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"
<div class="upload-icon">
<Icon
icon="ri:image-add-line"
width="80"
style="color: #4ade80;"
/>
</svg>
<p class="upload-text">Click the button below to upload an image</p>
</div>
<p class="upload-text">Select an image to scan</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
class="close-btn"
onclick={handleClose}
aria-label="Close camera"
>
<Icon icon="ri:close-fill" width="32" style="color: white;" />
</button>
<!-- Toggle button between camera and upload -->
<button
class="mode-toggle-btn"
on:click={() => (useCamera = !useCamera)}
onclick={() => (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>
<Icon
icon="ri:image-line"
width="20"
style="color: white;"
/>
<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>
<Icon
icon="ri:camera-line"
width="20"
style="color: white;"
/>
<span>Use Camera</span>
{/if}
</button>
@@ -196,35 +179,36 @@
{#if analyzing}
<div class="loading-container">
<div class="spinner"></div>
<p class="analyzing-text">Analyzing...</p>
</div>
{/if}
{#if !analyzing && !showResult}
<div class="shutter-container">
{#if useCamera}
<button class="shutter-button" on:click={takePicture}
<button
class="shutter-button"
onclick={takePicture}
aria-label="Take picture"
></button>
{:else}
<input
type="file"
accept="image/*"
bind:this={fileInputElement}
on:change={handleFileUpload}
onchange={handleFileUpload}
style="display: none;"
/>
<button
class="shutter-button upload-button"
on:click={() => fileInputElement.click()}
onclick={() => fileInputElement?.click()}
aria-label="Upload image"
>
<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>
<Icon
icon="ri:upload-cloud-line"
width="32"
style="color: #0f172a;"
/>
</button>
{/if}
</div>
@@ -235,16 +219,16 @@
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
class="sheet-close-btn"
onclick={handleClose}
aria-label="Close result"
>
<Icon
icon="ri:close-circle-fill"
width="28"
style="color: #94a3b8;"
/>
</button>
<h2 class="sheet-title">{displayTitle}</h2>
@@ -252,68 +236,48 @@
<p class="alternatives-label">Top Sustainable Alternatives</p>
<div class="alternatives-scroll">
<div class="alternative-card glass-bottle">
<button 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>
<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>
</div>
</button>
<div class="alternative-card boxed-water">
<button 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>
<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>
</div>
</button>
<div class="alternative-card aluminum">
<button 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>
<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>
</div>
</button>
</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>
<button class="report-btn" onclick={handleClose}>
<Icon icon="ri:alarm-warning-fill" width="20" />
<span>Report Greenwashing</span>
</button>
</div>
@@ -349,18 +313,20 @@
.close-btn {
position: absolute;
top: 60px;
top: 50px;
left: 20px;
background: none;
background: rgba(0, 0, 0, 0.3);
backdrop-filter: blur(4px);
border-radius: 50%;
width: 44px;
height: 44px;
border: none;
cursor: pointer;
padding: 0;
z-index: 10;
}
.close-btn svg {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
}
.loading-container {
@@ -368,12 +334,22 @@
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.analyzing-text {
color: white;
font-weight: 600;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
}
.spinner {
width: 50px;
height: 50px;
border: 4px solid rgba(74, 222, 128, 0.2);
width: 60px;
height: 60px;
border: 4px solid rgba(255, 255, 255, 0.2);
border-top-color: #4ade80;
border-radius: 50%;
animation: spin 1s linear infinite;
@@ -387,7 +363,7 @@
.shutter-container {
position: absolute;
bottom: 50px;
bottom: 60px;
width: 100%;
display: flex;
justify-content: center;
@@ -396,58 +372,59 @@
.shutter-button {
width: 80px;
height: 80px;
border-radius: 40px;
border-radius: 50%;
background-color: white;
border: 6px solid #e2e8f0;
border: 6px solid rgba(255, 255, 255, 0.3);
cursor: pointer;
transition: transform 0.2s;
background-clip: padding-box;
}
.shutter-button:active {
transform: scale(0.9);
background-color: #e2e8f0;
}
.result-sheet {
position: absolute;
bottom: 0;
width: 100%;
height: 50%;
background-color: white;
height: 60vh;
background-color: #0d2e25;
border-top-left-radius: 32px;
border-top-right-radius: 32px;
padding: 32px;
transition: transform 0.5s ease-out;
padding: 24px;
transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1);
overflow-y: auto;
box-shadow: 0 -10px 40px rgba(0, 0, 0, 0.3);
border-top: 1px solid #1f473b;
}
.sheet-close-btn {
background: none;
border: none;
cursor: pointer;
padding: 0;
padding: 4px;
align-self: flex-end;
display: block;
margin-left: auto;
}
.sheet-close-btn svg {
width: 24px;
height: 24px;
}
.sheet-title {
font-size: 28px;
font-size: 24px;
font-weight: 800;
margin-top: 12px;
margin-top: 4px;
margin-bottom: 24px;
color: #0f172a;
color: white;
}
.alternatives-label {
color: #64748b;
margin-bottom: 12px;
font-weight: bold;
font-size: 14px;
color: #6ee7b7;
margin-bottom: 16px;
font-weight: 700;
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.alternatives-scroll {
@@ -458,57 +435,46 @@
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;
min-width: 160px;
background-color: #051f18;
padding: 16px;
border-radius: 16px;
border: 1px solid #e2e8f0;
border-radius: 20px;
border: 1px solid #1f473b;
text-align: left;
cursor: pointer;
transition: background 0.2s;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.02);
}
.alternative-card:active {
background-color: #0d2e25;
}
.alt-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.alt-header svg {
width: 24px;
height: 24px;
margin-bottom: 12px;
}
.rating {
font-weight: bold;
color: #f59e0b;
font-size: 14px;
font-size: 13px;
}
.alt-name {
font-weight: bold;
color: #0f172a;
font-size: 16px;
font-weight: 700;
color: white;
font-size: 15px;
margin: 4px 0;
}
.alt-price {
color: #64748b;
color: #9ca3af;
margin: 0;
font-size: 14px;
font-size: 13px;
}
.report-btn {
@@ -516,23 +482,20 @@
align-items: center;
justify-content: center;
gap: 8px;
margin: 20px auto 0;
padding: 12px 24px;
background: none;
border: none;
margin: 32px auto 0;
padding: 14px 24px;
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.2);
border-radius: 16px;
cursor: pointer;
color: #ef4444;
font-weight: bold;
font-size: 16px;
color: #f87171;
font-weight: 700;
font-size: 15px;
width: 100%;
}
.report-btn svg {
width: 20px;
height: 20px;
}
.report-btn:hover {
opacity: 0.8;
.report-btn:active {
background: rgba(239, 68, 68, 0.2);
}
.upload-placeholder {
@@ -541,57 +504,50 @@
align-items: center;
justify-content: center;
height: 100%;
background-color: #0f172a;
background-color: #051f18;
}
.upload-icon {
margin-bottom: 24px;
opacity: 1;
background: rgba(16, 185, 129, 0.1);
width: 120px;
height: 120px;
margin-bottom: 24px;
opacity: 0.8;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #34d399;
}
.upload-text {
color: #94a3b8;
font-size: 18px;
color: #9ca3af;
font-size: 16px;
text-align: center;
padding: 0 32px;
font-weight: 600;
}
.mode-toggle-btn {
position: absolute;
top: 60px;
top: 50px;
right: 20px;
background: rgba(15, 23, 42, 0.8);
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 10px 16px;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 20px;
padding: 8px 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-size: 13px;
font-weight: 600;
white-space: nowrap;
}
.upload-button {
@@ -600,9 +556,4 @@
justify-content: center;
background-color: white;
}
.upload-button svg {
width: 40px;
height: 40px;
}
</style>