mirror of
https://github.com/SirBlobby/Hoya26.git
synced 2026-02-04 03:34:34 -05:00
UI Rework
This commit is contained in:
1
frontend/src/app.css
Normal file
1
frontend/src/app.css
Normal file
@@ -0,0 +1 @@
|
||||
@import "tailwindcss";
|
||||
@@ -1,13 +1,15 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Tauri + SvelteKit + Typescript App</title>
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -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>
|
||||
|
||||
@@ -3,24 +3,20 @@
|
||||
</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"
|
||||
@@ -31,24 +27,20 @@
|
||||
<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"
|
||||
@@ -66,7 +58,7 @@
|
||||
}
|
||||
|
||||
.cloud-content-wrapper {
|
||||
background: #ffffff; /* White */
|
||||
background: #ffffff;
|
||||
padding: 20px 60px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
@@ -92,7 +84,6 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Mobile adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.cloud-content-wrapper {
|
||||
padding: 20px 20px;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import Icon from "@iconify/svelte";
|
||||
|
||||
interface Props {
|
||||
currentRoute: string;
|
||||
@@ -9,139 +10,88 @@
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
name: "Mission",
|
||||
name: "Goals",
|
||||
route: "/community",
|
||||
icon: "people",
|
||||
activeColor: "#f472b6",
|
||||
icon: "ri:flag-fill",
|
||||
activeColor: "#34d399",
|
||||
},
|
||||
{
|
||||
name: "History",
|
||||
route: "/",
|
||||
icon: "time",
|
||||
activeColor: "#1ed760",
|
||||
route: "/",
|
||||
icon: "ri:time-fill",
|
||||
activeColor: "#34d399",
|
||||
},
|
||||
{
|
||||
name: "Home",
|
||||
route: "/", // Hidden - for center button only
|
||||
icon: "home",
|
||||
activeColor: "#4ade80",
|
||||
name: "Scan",
|
||||
route: "/",
|
||||
icon: "ri:camera-lens-fill",
|
||||
isCenter: true,
|
||||
},
|
||||
{
|
||||
name: "Report",
|
||||
route: "/report",
|
||||
icon: "megaphone",
|
||||
icon: "ri:alarm-warning-fill",
|
||||
activeColor: "#ef4444",
|
||||
},
|
||||
{
|
||||
name: "Ask AI",
|
||||
name: "Chat",
|
||||
route: "/chat",
|
||||
icon: "chatbubble-ellipses",
|
||||
activeColor: "#4ade80",
|
||||
icon: "ri:chat-3-fill",
|
||||
activeColor: "#60a5fa",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
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"
|
||||
{#each tabs as tab, index}
|
||||
{#if !tab.isCenter}
|
||||
<button
|
||||
class="tab-item"
|
||||
class:active={currentRoute === tab.route}
|
||||
onclick={() => navigateToTab(tab.route)}
|
||||
aria-label={tab.name}
|
||||
>
|
||||
<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"
|
||||
<Icon
|
||||
icon={tab.icon}
|
||||
width="24"
|
||||
style="color: {currentRoute === tab.route
|
||||
? tab.activeColor
|
||||
: '#6b7280'};"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<span
|
||||
class="tab-name"
|
||||
style="color: {currentRoute === tab.route
|
||||
? tab.activeColor
|
||||
: '#6b7280'};"
|
||||
>
|
||||
{tab.name}
|
||||
</span>
|
||||
</button>
|
||||
{/if}
|
||||
{#if index === 1}
|
||||
<button
|
||||
class="scan-button"
|
||||
onclick={handleScan}
|
||||
aria-label="Scan product"
|
||||
>
|
||||
<Icon
|
||||
icon="ri:camera-lens-fill"
|
||||
width="28"
|
||||
style="color: white;"
|
||||
/>
|
||||
</button>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -151,130 +101,55 @@
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding-bottom: 20px;
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
padding: 0;
|
||||
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;
|
||||
justify-content: space-around;
|
||||
height: 80px;
|
||||
background: #0d2e25;
|
||||
border-top: 1px solid #1f473b;
|
||||
padding: 0 16px 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);
|
||||
gap: 4px;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.tab-name {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
|
||||
.scan-button {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
background: #10b981;
|
||||
border-radius: 50%;
|
||||
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);
|
||||
cursor: pointer;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||
transition: transform 0.15s;
|
||||
border: 4px solid #051f18;
|
||||
}
|
||||
|
||||
.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;
|
||||
.scan-button:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import Icon from "@iconify/svelte";
|
||||
|
||||
interface RecentItem {
|
||||
id: number;
|
||||
@@ -17,71 +18,129 @@
|
||||
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");
|
||||
}
|
||||
|
||||
function getImpactColor(impact: string): string {
|
||||
switch (impact.toLowerCase()) {
|
||||
case "low":
|
||||
return "#22c55e";
|
||||
case "medium":
|
||||
return "#f59e0b";
|
||||
case "high":
|
||||
return "#ef4444";
|
||||
default:
|
||||
return "#6b7280";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="home-container">
|
||||
<div class="bg-gradient"></div>
|
||||
|
||||
<div class="safe-area">
|
||||
<div class="header">
|
||||
<div>
|
||||
<h1 class="app-name">Ethix</h1>
|
||||
<p class="tagline">Truth in every scan.</p>
|
||||
<div class="header-left">
|
||||
<div class="logo-icon">
|
||||
<Icon icon="ri:leaf-fill" width="24" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="app-name">Ethix</h1>
|
||||
<p class="tagline">Truth in every scan</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="profile-avatar">
|
||||
<button class="profile-avatar">
|
||||
<span>YO</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="scroll-content">
|
||||
<button class="card scan-card" on:click={onToggleCamera}>
|
||||
<button class="card scan-card" onclick={onToggleCamera}>
|
||||
<div class="scan-card-glow"></div>
|
||||
<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="scan-icon-wrapper">
|
||||
<Icon icon="ri:camera-lens-fill" width="36" />
|
||||
</div>
|
||||
<div class="card-text-container">
|
||||
<h2 class="card-title">Start Scanning</h2>
|
||||
<p class="card-text">Identify harmful products</p>
|
||||
<p class="card-text">
|
||||
Identify harmful products instantly
|
||||
</p>
|
||||
</div>
|
||||
<div class="arrow-icon">
|
||||
<Icon icon="ri:arrow-right-s-line" width="28" />
|
||||
</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="stats-row">
|
||||
<div class="stat-item">
|
||||
<div class="stat-icon green">
|
||||
<Icon icon="ri:scan-2-line" width="20" />
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<span class="stat-number">47</span>
|
||||
<span class="stat-label">Scans</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-divider"></div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-icon blue">
|
||||
<Icon icon="ri:leaf-line" width="20" />
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<span class="stat-number">32</span>
|
||||
<span class="stat-label">Eco Picks</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-divider"></div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-icon orange">
|
||||
<Icon icon="ri:fire-line" width="20" />
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<span class="stat-number">78%</span>
|
||||
<span class="stat-label">Score</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="recent-section">
|
||||
<h2 class="section-title">Recent Activity</h2>
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">Recent Activity</h2>
|
||||
<button class="see-all-btn">See all</button>
|
||||
</div>
|
||||
|
||||
{#if recentItems.length === 0}
|
||||
<p class="no-items">No recent scans.</p>
|
||||
<div class="empty-state">
|
||||
<Icon
|
||||
icon="ri:scan-line"
|
||||
width="48"
|
||||
style="color: rgba(255,255,255,0.3);"
|
||||
/>
|
||||
<p>No recent scans yet</p>
|
||||
<span>Start scanning to see your history</span>
|
||||
</div>
|
||||
{: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>
|
||||
<button
|
||||
class="recent-card"
|
||||
onclick={() => navigateToDetails(item)}
|
||||
>
|
||||
<div
|
||||
class="recent-icon-container"
|
||||
style="background: {getImpactColor(
|
||||
item.impact,
|
||||
)}20;"
|
||||
>
|
||||
<Icon
|
||||
icon="ri:box-3-line"
|
||||
width="22"
|
||||
style="color: {getImpactColor(
|
||||
item.impact,
|
||||
)};"
|
||||
/>
|
||||
</div>
|
||||
<div class="recent-info">
|
||||
<h3 class="recent-title">{item.title}</h3>
|
||||
@@ -89,8 +148,13 @@
|
||||
</div>
|
||||
<div
|
||||
class="impact-badge"
|
||||
class:high-impact={item.impact === "High"}
|
||||
class:low-impact={item.impact === "Low"}
|
||||
style="background: {getImpactColor(
|
||||
item.impact,
|
||||
)}15; color: {getImpactColor(
|
||||
item.impact,
|
||||
)}; border: 1px solid {getImpactColor(
|
||||
item.impact,
|
||||
)}25;"
|
||||
>
|
||||
{item.impact}
|
||||
</div>
|
||||
@@ -104,12 +168,30 @@
|
||||
|
||||
<style>
|
||||
.home-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
background-color: #0f172a;
|
||||
min-height: 100vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.bg-gradient {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(
|
||||
165deg,
|
||||
#064e3b 0%,
|
||||
#065f46 15%,
|
||||
#047857 30%,
|
||||
#0f766e 50%,
|
||||
#134e4a 70%,
|
||||
#1e3a3a 100%
|
||||
);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.safe-area {
|
||||
padding-top: 50px;
|
||||
padding-bottom: 140px;
|
||||
@@ -118,71 +200,114 @@
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 24px;
|
||||
padding: 20px 24px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: linear-gradient(135deg, #4ade80, #22c55e);
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #052e16;
|
||||
box-shadow: 0 4px 16px rgba(74, 222, 128, 0.4);
|
||||
}
|
||||
|
||||
.app-name {
|
||||
font-size: 28px;
|
||||
font-size: 26px;
|
||||
font-weight: 800;
|
||||
color: white;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.tagline {
|
||||
color: #94a3b8;
|
||||
margin: 0;
|
||||
margin-top: 4px;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
margin: 2px 0 0 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.profile-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background-color: #334155;
|
||||
border-radius: 20px;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(8px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.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);
|
||||
padding: 0 20px 140px;
|
||||
}
|
||||
|
||||
.scan-card {
|
||||
margin-bottom: 24px;
|
||||
position: relative;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(74, 222, 128, 0.2),
|
||||
rgba(34, 197, 94, 0.1)
|
||||
);
|
||||
padding: 24px;
|
||||
border-radius: 24px;
|
||||
border: 1px solid rgba(74, 222, 128, 0.3);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
margin-bottom: 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.scan-card-glow {
|
||||
position: absolute;
|
||||
top: -20px;
|
||||
right: -20px;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
background: radial-gradient(
|
||||
circle,
|
||||
rgba(74, 222, 128, 0.3) 0%,
|
||||
transparent 70%
|
||||
);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.scan-card:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.card-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.scan-icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
.scan-icon-wrapper {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
background: linear-gradient(135deg, #4ade80, #22c55e);
|
||||
border-radius: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #052e16;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 6px 20px rgba(74, 222, 128, 0.4);
|
||||
}
|
||||
|
||||
.card-text-container {
|
||||
@@ -191,105 +316,193 @@
|
||||
|
||||
.card-title {
|
||||
color: white;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.card-text {
|
||||
color: #94a3b8;
|
||||
font-size: 14px;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 13px;
|
||||
margin: 4px 0 0 0;
|
||||
}
|
||||
|
||||
.arrow-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
flex-shrink: 0;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
backdrop-filter: blur(8px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 20px;
|
||||
padding: 16px 20px;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.stat-icon.green {
|
||||
background: rgba(74, 222, 128, 0.2);
|
||||
color: #4ade80;
|
||||
}
|
||||
|
||||
.stat-icon.blue {
|
||||
background: rgba(96, 165, 250, 0.2);
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
.stat-icon.orange {
|
||||
background: rgba(251, 146, 60, 0.2);
|
||||
color: #fb923c;
|
||||
}
|
||||
|
||||
.stat-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
color: white;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 11px;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.stat-divider {
|
||||
width: 1px;
|
||||
height: 36px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.recent-section {
|
||||
margin-top: 24px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
margin-bottom: 16px;
|
||||
font-weight: 700;
|
||||
font-size: 18px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.no-items {
|
||||
color: #64748b;
|
||||
.see-all-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #4ade80;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border-radius: 20px;
|
||||
border: 1px dashed rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-weight: 600;
|
||||
margin: 12px 0 4px;
|
||||
}
|
||||
|
||||
.empty-state span {
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.recent-card {
|
||||
background-color: rgba(30, 41, 59, 0.9);
|
||||
padding: 16px;
|
||||
border-radius: 24px;
|
||||
border: 1px solid #334155;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
backdrop-filter: blur(8px);
|
||||
padding: 14px 16px;
|
||||
border-radius: 18px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 12px;
|
||||
gap: 14px;
|
||||
margin-bottom: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
transition: all 0.2s ease;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.recent-card:hover {
|
||||
transform: translateX(4px);
|
||||
background-color: rgba(30, 41, 59, 1);
|
||||
.recent-card:active {
|
||||
transform: scale(0.98);
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.recent-icon-container {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background-color: #334155;
|
||||
border-radius: 12px;
|
||||
border-radius: 14px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.recent-icon-container svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.recent-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.recent-title {
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.recent-date {
|
||||
color: #94a3b8;
|
||||
font-size: 13px;
|
||||
margin: 4px 0 0 0;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
font-size: 12px;
|
||||
margin: 3px 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;
|
||||
padding: 6px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
</style>
|
||||
|
||||
533
frontend/src/lib/components/MobileHomePage.svelte
Normal file
533
frontend/src/lib/components/MobileHomePage.svelte
Normal file
@@ -0,0 +1,533 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import Icon from "@iconify/svelte";
|
||||
|
||||
const scanHistory = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Plastic Water Bottle",
|
||||
date: "Today, 10:45 AM",
|
||||
severity: "High",
|
||||
icon: "ri:drop-fill",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Organic Banana",
|
||||
date: "Yesterday, 3:20 PM",
|
||||
severity: "Low",
|
||||
icon: "ri:plant-fill",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Aluminum Soda Can",
|
||||
date: "Mon, 12:15 PM",
|
||||
severity: "Low",
|
||||
icon: "ri:cup-fill",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "Takeout Container",
|
||||
date: "Sun, 8:30 PM",
|
||||
severity: "Medium",
|
||||
icon: "ri:box-3-fill",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "Shampoo Bottle",
|
||||
date: "Sat, 11:00 AM",
|
||||
severity: "High",
|
||||
icon: "ri:flask-fill",
|
||||
},
|
||||
];
|
||||
|
||||
let greeting = $state("Hello");
|
||||
|
||||
onMount(() => {
|
||||
const hour = new Date().getHours();
|
||||
if (hour < 12) greeting = "Good morning";
|
||||
else if (hour < 17) greeting = "Good afternoon";
|
||||
else greeting = "Good evening";
|
||||
});
|
||||
|
||||
function getSeverityClass(severity: string): string {
|
||||
return severity.toLowerCase();
|
||||
}
|
||||
|
||||
function getSeverityColor(severity: string): string {
|
||||
switch (severity.toLowerCase()) {
|
||||
case "low":
|
||||
return "#34d399";
|
||||
case "medium":
|
||||
return "#fb923c";
|
||||
case "high":
|
||||
return "#f87171";
|
||||
default:
|
||||
return "#9ca3af";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="mobile-home">
|
||||
<header class="header">
|
||||
<div class="header-left">
|
||||
<div class="avatar">
|
||||
<Icon
|
||||
icon="ri:seedling-fill"
|
||||
width="24"
|
||||
style="color: #34d399;"
|
||||
/>
|
||||
</div>
|
||||
<div class="header-text">
|
||||
<span class="greeting">{greeting}</span>
|
||||
<h1 class="username">Eco Warrior</h1>
|
||||
</div>
|
||||
</div>
|
||||
<button class="notification-btn" aria-label="Notifications">
|
||||
<Icon
|
||||
icon="ri:notification-3-line"
|
||||
width="24"
|
||||
style="color: #9ca3af;"
|
||||
/>
|
||||
<span class="notif-badge">2</span>
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<section class="impact-hero">
|
||||
<div class="impact-main">
|
||||
<span class="impact-label">Your Carbon Savings</span>
|
||||
<div class="impact-value">
|
||||
<span class="impact-number">23</span>
|
||||
<span class="impact-unit">kg CO₂</span>
|
||||
</div>
|
||||
<div class="impact-comparison">
|
||||
<Icon icon="ri:car-fill" width="16" style="color: #6ee7b7;" />
|
||||
<span>≈ driving 92 km less</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="impact-visual">
|
||||
<div class="tree-ring">
|
||||
<Icon icon="ri:tree-fill" width="32" style="color: #10b981;" />
|
||||
</div>
|
||||
<span class="tree-count">2 trees planted</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="stats-row">
|
||||
<div class="stat-pill">
|
||||
<div class="stat-icon">
|
||||
<Icon
|
||||
icon="ri:qr-scan-2-line"
|
||||
width="20"
|
||||
style="color: #34d399;"
|
||||
/>
|
||||
</div>
|
||||
<span class="stat-value">47</span>
|
||||
<span class="stat-label">scans</span>
|
||||
</div>
|
||||
<div class="stat-pill">
|
||||
<div class="stat-icon eco">
|
||||
<Icon
|
||||
icon="ri:checkbox-circle-fill"
|
||||
width="20"
|
||||
style="color: #34d399;"
|
||||
/>
|
||||
</div>
|
||||
<span class="stat-value">32</span>
|
||||
<span class="stat-label">eco picks</span>
|
||||
</div>
|
||||
<div class="stat-pill">
|
||||
<div class="stat-icon score">
|
||||
<Icon icon="ri:star-fill" width="20" style="color: #fbbf24;" />
|
||||
</div>
|
||||
<span class="stat-value">78%</span>
|
||||
<span class="stat-label">score</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="recent-section">
|
||||
<div class="section-header">
|
||||
<h2>Recent Scans</h2>
|
||||
<a href="/history" class="see-all">
|
||||
See all
|
||||
<Icon icon="ri:arrow-right-s-line" width="16" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="scan-list">
|
||||
{#each scanHistory as item (item.id)}
|
||||
<button class="scan-item" aria-label="View {item.name}">
|
||||
<div
|
||||
class="item-icon"
|
||||
style="color: {getSeverityColor(item.severity)};"
|
||||
>
|
||||
<Icon icon={item.icon} width="24" />
|
||||
</div>
|
||||
<div class="item-info">
|
||||
<span class="item-name">{item.name}</span>
|
||||
<span class="item-date">{item.date}</span>
|
||||
</div>
|
||||
<span
|
||||
class="severity-tag {getSeverityClass(item.severity)}"
|
||||
>
|
||||
{item.severity}
|
||||
</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="tip-card">
|
||||
<div class="tip-icon">
|
||||
<Icon
|
||||
icon="ri:lightbulb-flash-fill"
|
||||
width="22"
|
||||
style="color: #fbbf24;"
|
||||
/>
|
||||
</div>
|
||||
<div class="tip-text">
|
||||
<strong>Daily Tip:</strong> Bring reusable bags shopping to cut 500 plastic
|
||||
bags per year!
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.mobile-home {
|
||||
padding: 16px 20px 120px;
|
||||
min-height: 100vh;
|
||||
background: #051f18;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: #0d2e25;
|
||||
border: 1px solid #1f473b;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.header-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.greeting {
|
||||
font-size: 13px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.username {
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
color: white;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.notification-btn {
|
||||
position: relative;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
background: #0d2e25;
|
||||
border: 1px solid #1f473b;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.notif-badge {
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
right: -2px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background: #ef4444;
|
||||
color: white;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 2px solid #051f18;
|
||||
}
|
||||
|
||||
.impact-hero {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: #0d2e25;
|
||||
border: 1px solid #1f473b;
|
||||
border-radius: 20px;
|
||||
padding: 24px;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.impact-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.impact-label {
|
||||
font-size: 11px;
|
||||
color: #6ee7b7;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.impact-value {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.impact-number {
|
||||
font-size: 36px;
|
||||
font-weight: 900;
|
||||
color: white;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.impact-unit {
|
||||
font-size: 16px;
|
||||
color: #9ca3af;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.impact-comparison {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
color: #d1fae5;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.impact-visual {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.tree-ring {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
border: 1px solid rgba(16, 185, 129, 0.3);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.tree-count {
|
||||
font-size: 11px;
|
||||
color: #34d399;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.stat-pill {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 16px 8px;
|
||||
background: #0d2e25;
|
||||
border: 1px solid #1f473b;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 10px;
|
||||
color: #9ca3af;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.section-header h2 {
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
color: white;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.see-all {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
font-size: 13px;
|
||||
color: #34d399;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.scan-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.scan-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 12px;
|
||||
background: #0d2e25;
|
||||
border: 1px solid #1f473b;
|
||||
border-radius: 16px;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
transition: transform 0.1s;
|
||||
}
|
||||
|
||||
.scan-item:active {
|
||||
transform: scale(0.98);
|
||||
background: #1f473b;
|
||||
}
|
||||
|
||||
.item-icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #051f18;
|
||||
}
|
||||
|
||||
.item-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.item-name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.item-date {
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.severity-tag {
|
||||
padding: 4px 10px;
|
||||
border-radius: 20px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.severity-tag.low {
|
||||
background: rgba(52, 211, 153, 0.1);
|
||||
color: #34d399;
|
||||
border: 1px solid rgba(52, 211, 153, 0.2);
|
||||
}
|
||||
|
||||
.severity-tag.medium {
|
||||
background: rgba(251, 146, 60, 0.1);
|
||||
color: #fb923c;
|
||||
border: 1px solid rgba(251, 146, 60, 0.2);
|
||||
}
|
||||
|
||||
.severity-tag.high {
|
||||
background: rgba(248, 113, 113, 0.1);
|
||||
color: #f87171;
|
||||
border: 1px solid rgba(248, 113, 113, 0.2);
|
||||
}
|
||||
|
||||
.tip-card {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
padding: 20px;
|
||||
background: #0d2e25;
|
||||
border: 1px solid #1f473b;
|
||||
border-radius: 16px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.tip-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: rgba(251, 191, 36, 0.1);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tip-text {
|
||||
font-size: 13px;
|
||||
color: #d1d5db;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.tip-text strong {
|
||||
color: white;
|
||||
font-weight: 700;
|
||||
}
|
||||
</style>
|
||||
@@ -1,24 +0,0 @@
|
||||
<nav class="mobile-tab-bar">
|
||||
<a href="/">Home</a>
|
||||
<a href="/profile">Profile</a>
|
||||
<a href="/settings">Settings</a>
|
||||
</nav>
|
||||
|
||||
<style>
|
||||
.mobile-tab-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
padding: 1rem;
|
||||
background-color: #222;
|
||||
color: white;
|
||||
box-shadow: 0 -2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
a {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
</style>
|
||||
@@ -1,17 +0,0 @@
|
||||
<script>
|
||||
import { onMount } from "svelte";
|
||||
import { isTauri } from "@tauri-apps/api/core";
|
||||
|
||||
let { children } = $props();
|
||||
let isNative = $state(false);
|
||||
let mounted = $state(false);
|
||||
|
||||
onMount(() => {
|
||||
isNative = isTauri();
|
||||
mounted = true;
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if mounted && isNative}
|
||||
{@render children()}
|
||||
{/if}
|
||||
@@ -1,17 +0,0 @@
|
||||
<script>
|
||||
import { onMount } from "svelte";
|
||||
import { isTauri } from "@tauri-apps/api/core";
|
||||
|
||||
let { children } = $props();
|
||||
let isNative = $state(false);
|
||||
let mounted = $state(false);
|
||||
|
||||
onMount(() => {
|
||||
isNative = isTauri();
|
||||
mounted = true;
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if mounted && !isNative}
|
||||
{@render children()}
|
||||
{/if}
|
||||
@@ -1,188 +1,267 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { onMount, onDestroy } from "svelte";
|
||||
import { page } from "$app/stores";
|
||||
import {
|
||||
drawLandscape,
|
||||
type ParallaxState,
|
||||
type SceneType,
|
||||
} from "$lib/ts/parallax";
|
||||
|
||||
interface SceneConfig {
|
||||
sceneType: SceneType;
|
||||
staticScene: boolean;
|
||||
progress?: number;
|
||||
scenes?: SceneType[];
|
||||
}
|
||||
|
||||
const PATH_CONFIG: Record<string, SceneConfig> = {
|
||||
"/": { sceneType: "transition", staticScene: false },
|
||||
"/chat": { sceneType: "oilRig", staticScene: true },
|
||||
"/community": { sceneType: "forest", staticScene: true },
|
||||
"/report": { sceneType: "industrial", staticScene: true },
|
||||
"/catalogue": {
|
||||
sceneType: "transition",
|
||||
staticScene: false,
|
||||
scenes: ["eco", "ocean"],
|
||||
},
|
||||
"/news": { sceneType: "ocean", staticScene: true },
|
||||
};
|
||||
|
||||
const DEFAULT_CONFIG: SceneConfig = {
|
||||
sceneType: "transition",
|
||||
staticScene: false,
|
||||
};
|
||||
|
||||
interface Props {
|
||||
onProgressChange?: (progress: number) => void;
|
||||
staticScene?: boolean;
|
||||
sceneType?: SceneType;
|
||||
sceneProgress?: number;
|
||||
scenes?: SceneType[];
|
||||
}
|
||||
let {
|
||||
onProgressChange,
|
||||
staticScene,
|
||||
sceneType,
|
||||
sceneProgress,
|
||||
scenes,
|
||||
}: Props = $props();
|
||||
|
||||
let scrollY = 0;
|
||||
let innerHeight = 0;
|
||||
let innerHeight = 1;
|
||||
let scrollHeight = 1;
|
||||
let mouseX = 0;
|
||||
let mouseY = 0;
|
||||
let canvas: HTMLCanvasElement;
|
||||
let ctx: CanvasRenderingContext2D | null = null;
|
||||
let animationId: number = 0;
|
||||
let scrollContainer: HTMLElement | null = null;
|
||||
let currentPath = $state("/");
|
||||
let currentProgress = $state(0);
|
||||
|
||||
let activeConfig = $derived(() => {
|
||||
const pathConfig = PATH_CONFIG[currentPath] || DEFAULT_CONFIG;
|
||||
return {
|
||||
sceneType: sceneType ?? pathConfig.sceneType,
|
||||
staticScene: staticScene ?? pathConfig.staticScene,
|
||||
progress: sceneProgress ?? pathConfig.progress ?? 0,
|
||||
scenes: scenes ?? pathConfig.scenes,
|
||||
};
|
||||
});
|
||||
|
||||
interface SceneBlend {
|
||||
fromScene: SceneType;
|
||||
toScene: SceneType;
|
||||
blendProgress: number;
|
||||
}
|
||||
|
||||
function getSceneBlend(
|
||||
scrollProgress: number,
|
||||
scenesList: SceneType[],
|
||||
): SceneBlend {
|
||||
if (scenesList.length === 0) {
|
||||
return { fromScene: "eco", toScene: "eco", blendProgress: 0 };
|
||||
}
|
||||
if (scenesList.length === 1) {
|
||||
return {
|
||||
fromScene: scenesList[0],
|
||||
toScene: scenesList[0],
|
||||
blendProgress: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const numTransitions = scenesList.length - 1;
|
||||
const segmentSize = 1 / numTransitions;
|
||||
const segmentIndex = Math.min(
|
||||
Math.floor(scrollProgress / segmentSize),
|
||||
numTransitions - 1,
|
||||
);
|
||||
|
||||
const segmentProgress =
|
||||
(scrollProgress - segmentIndex * segmentSize) / segmentSize;
|
||||
|
||||
return {
|
||||
fromScene: scenesList[segmentIndex],
|
||||
toScene:
|
||||
scenesList[Math.min(segmentIndex + 1, scenesList.length - 1)],
|
||||
blendProgress: Math.min(Math.max(segmentProgress, 0), 1),
|
||||
};
|
||||
}
|
||||
|
||||
function calculateProgress(): number {
|
||||
const config = activeConfig();
|
||||
|
||||
if (config.staticScene && !config.scenes) {
|
||||
return Math.min(Math.max(config.progress, 0), 1);
|
||||
}
|
||||
|
||||
const scrollableHeight = scrollHeight - innerHeight;
|
||||
if (scrollableHeight <= 0) return config.progress;
|
||||
return Math.min(Math.max(scrollY / scrollableHeight, 0), 1);
|
||||
}
|
||||
|
||||
function updateMeasurements() {
|
||||
const config = activeConfig();
|
||||
if (config.staticScene && !config.scenes) return;
|
||||
|
||||
if (scrollContainer) {
|
||||
scrollY = scrollContainer.scrollTop;
|
||||
innerHeight = scrollContainer.clientHeight;
|
||||
scrollHeight = scrollContainer.scrollHeight;
|
||||
} else {
|
||||
scrollY = window.scrollY;
|
||||
innerHeight = window.innerHeight;
|
||||
scrollHeight = Math.max(
|
||||
document.body.scrollHeight,
|
||||
document.documentElement.scrollHeight,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function animate() {
|
||||
if (!ctx || !canvas) return;
|
||||
|
||||
updateMeasurements();
|
||||
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
|
||||
const canvasWidth = Math.floor(rect.width * dpr);
|
||||
const canvasHeight = Math.floor(rect.height * dpr);
|
||||
|
||||
if (canvas.width !== canvasWidth || canvas.height !== canvasHeight) {
|
||||
canvas.width = canvasWidth;
|
||||
canvas.height = canvasHeight;
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
}
|
||||
|
||||
const progress = calculateProgress();
|
||||
const config = activeConfig();
|
||||
|
||||
if (currentProgress !== progress) {
|
||||
currentProgress = progress;
|
||||
onProgressChange?.(progress);
|
||||
}
|
||||
|
||||
let sceneBlend: SceneBlend | undefined;
|
||||
if (config.scenes && config.scenes.length > 1) {
|
||||
sceneBlend = getSceneBlend(progress, config.scenes);
|
||||
}
|
||||
|
||||
const state: ParallaxState = {
|
||||
scrollY: config.staticScene && !config.scenes ? 0 : scrollY,
|
||||
innerHeight,
|
||||
mouseX,
|
||||
mouseY,
|
||||
progress: sceneBlend ? sceneBlend.blendProgress : progress,
|
||||
sceneType: sceneBlend ? sceneBlend.fromScene : config.sceneType,
|
||||
blendToScene: sceneBlend?.toScene,
|
||||
blendProgress: sceneBlend?.blendProgress,
|
||||
};
|
||||
|
||||
drawLandscape(ctx, rect.width, rect.height, state);
|
||||
animationId = requestAnimationFrame(animate);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
const handleScroll = () => {
|
||||
scrollY = window.scrollY;
|
||||
};
|
||||
const unsubscribe = page.subscribe((p) => {
|
||||
currentPath = p.url.pathname;
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
window.addEventListener("mousemove", handleMouseMove, {
|
||||
passive: true,
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
const config = activeConfig();
|
||||
|
||||
if (!config.staticScene || config.scenes) {
|
||||
scrollContainer = document.querySelector(
|
||||
".app-container",
|
||||
) as HTMLElement;
|
||||
if (!scrollContainer) {
|
||||
scrollContainer = document.querySelector(
|
||||
".content-wrapper",
|
||||
) as HTMLElement;
|
||||
}
|
||||
if (!scrollContainer) {
|
||||
scrollContainer = document.querySelector(
|
||||
"main",
|
||||
) as HTMLElement;
|
||||
}
|
||||
updateMeasurements();
|
||||
}
|
||||
|
||||
if (canvas) {
|
||||
ctx = canvas.getContext("2d");
|
||||
if (ctx) {
|
||||
animate();
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("scroll", handleScroll);
|
||||
unsubscribe();
|
||||
window.removeEventListener("mousemove", handleMouseMove);
|
||||
if (animationId) {
|
||||
cancelAnimationFrame(animationId);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// Calculate opacity for transition effects
|
||||
$: progress = Math.min(Math.max(scrollY / innerHeight, 0), 1);
|
||||
onDestroy(() => {
|
||||
if (animationId) {
|
||||
cancelAnimationFrame(animationId);
|
||||
}
|
||||
});
|
||||
</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 class="parallax-landscape">
|
||||
<canvas bind:this={canvas} class="parallax-canvas"></canvas>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.parallax-container {
|
||||
.parallax-landscape {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
z-index: 0;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.layer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
.parallax-canvas {
|
||||
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 */
|
||||
}
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,234 +0,0 @@
|
||||
<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>
|
||||
@@ -1,174 +0,0 @@
|
||||
<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>
|
||||
@@ -1,12 +0,0 @@
|
||||
<footer class="web-footer">
|
||||
<p>© 2026 My Website. All rights reserved.</p>
|
||||
</footer>
|
||||
|
||||
<style>
|
||||
.web-footer {
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
background-color: #f4f4f4;
|
||||
margin-top: auto;
|
||||
}
|
||||
</style>
|
||||
650
frontend/src/lib/components/WebHomePage.svelte
Normal file
650
frontend/src/lib/components/WebHomePage.svelte
Normal file
@@ -0,0 +1,650 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { fly, fade } from "svelte/transition";
|
||||
import ParallaxLandscape from "$lib/components/ParallaxLandscape.svelte";
|
||||
|
||||
interface Props {
|
||||
onProgressChange?: (progress: number) => void;
|
||||
}
|
||||
let { onProgressChange }: Props = $props();
|
||||
|
||||
const news = [
|
||||
{
|
||||
id: 1,
|
||||
title: "Ocean Cleanup Hits 500 Tons",
|
||||
desc: "Major milestone reached in the Pacific cleanup initiative.",
|
||||
date: "2h ago",
|
||||
icon: "ri:ship-line",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Plastic Ban Starts Today",
|
||||
desc: "Big cities are saying no to single-use plastics.",
|
||||
date: "5h ago",
|
||||
icon: "ri:prohibited-line",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "Ethix Launches Globally",
|
||||
desc: "Our platform is now available worldwide.",
|
||||
date: "1d ago",
|
||||
icon: "ri:global-line",
|
||||
},
|
||||
];
|
||||
|
||||
const features = [
|
||||
{
|
||||
title: "Scan It",
|
||||
desc: "Point your camera at any product.",
|
||||
icon: "ri:scan-2-line",
|
||||
},
|
||||
{
|
||||
title: "Get Real Info",
|
||||
desc: "AI breaks down the real impact.",
|
||||
icon: "ri:information-line",
|
||||
},
|
||||
{
|
||||
title: "Find Better",
|
||||
desc: "See eco-friendly swaps instantly.",
|
||||
icon: "ri:leaf-line",
|
||||
},
|
||||
{
|
||||
title: "Call It Out",
|
||||
desc: "Report misleading green claims.",
|
||||
icon: "ri:megaphone-line",
|
||||
},
|
||||
];
|
||||
|
||||
const stats = [
|
||||
{ value: "50K+", label: "Scans" },
|
||||
{ value: "12K", label: "Users" },
|
||||
{ value: "98%", label: "Accuracy" },
|
||||
{ value: "24/7", label: "Support" },
|
||||
];
|
||||
|
||||
let sceneProgress = $state(0);
|
||||
let isEcoTheme = $derived(sceneProgress < 0.5);
|
||||
|
||||
function handleProgressChange(progress: number) {
|
||||
sceneProgress = progress;
|
||||
onProgressChange?.(progress);
|
||||
}
|
||||
|
||||
let scoreIndex = $state(0);
|
||||
const scores = [
|
||||
{
|
||||
label: "Fiji Water",
|
||||
score: "94/100",
|
||||
color: "#1ed760",
|
||||
image: "/water-bottle.png",
|
||||
scale: 0.7,
|
||||
},
|
||||
{
|
||||
label: "Plastic Bag",
|
||||
score: "12/100",
|
||||
color: "#e91429",
|
||||
image: "/plastic-bag.png",
|
||||
scale: 0.75,
|
||||
},
|
||||
{
|
||||
label: "Starbucks",
|
||||
score: "65/100",
|
||||
color: "#f59b23",
|
||||
image: "/coffee-cup.png",
|
||||
scale: 1,
|
||||
},
|
||||
];
|
||||
|
||||
onMount(() => {
|
||||
const interval = setInterval(() => {
|
||||
scoreIndex = (scoreIndex + 1) % scores.length;
|
||||
}, 4000);
|
||||
return () => clearInterval(interval);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="web-home" class:industrial-theme={!isEcoTheme}>
|
||||
<ParallaxLandscape onProgressChange={handleProgressChange} />
|
||||
|
||||
<section class="hero">
|
||||
<div class="glass-card hero-content">
|
||||
<div class="hero-badge">
|
||||
<iconify-icon icon="ri:eye-line" width="16"></iconify-icon>
|
||||
<span>See the real impact</span>
|
||||
</div>
|
||||
<h1 class="hero-title">
|
||||
Know What <br /> You Buy.
|
||||
</h1>
|
||||
<p class="hero-desc">
|
||||
Scan a product. See if it's actually good for the planet. Find
|
||||
better alternatives if it's not. Simple as that.
|
||||
</p>
|
||||
<div class="hero-actions">
|
||||
<a href="/catalogue" class="cta-primary">
|
||||
<iconify-icon icon="ri:store-2-fill" width="20"
|
||||
></iconify-icon>
|
||||
<span>Browse Database</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hero-visual">
|
||||
<div class="visual-container">
|
||||
<div class="hero-image">
|
||||
{#key scoreIndex}
|
||||
<div
|
||||
class="product-wrapper"
|
||||
in:fly={{ x: 100, duration: 500, opacity: 0 }}
|
||||
out:fly={{ x: -100, duration: 500, opacity: 0 }}
|
||||
>
|
||||
<img
|
||||
src={scores[scoreIndex].image}
|
||||
alt={scores[scoreIndex].label}
|
||||
class="product-image"
|
||||
style="transform: scale({scores[scoreIndex]
|
||||
.scale});"
|
||||
/>
|
||||
</div>
|
||||
{/key}
|
||||
</div>
|
||||
<div class="orbit orbit-1"></div>
|
||||
<div class="orbit orbit-2"></div>
|
||||
|
||||
<div class="floating-card glass-card">
|
||||
{#key scoreIndex}
|
||||
<div class="score-content" in:fade={{ duration: 300 }}>
|
||||
<iconify-icon
|
||||
icon="ri:checkbox-circle-fill"
|
||||
width="24"
|
||||
style="color: {scores[scoreIndex].color};"
|
||||
></iconify-icon>
|
||||
<div>
|
||||
<div class="card-label">
|
||||
{scores[scoreIndex].label}
|
||||
</div>
|
||||
<div class="card-value">
|
||||
{scores[scoreIndex].score}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/key}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="spacer"></div>
|
||||
|
||||
<section class="content-section">
|
||||
<div class="glass-card stats-grid">
|
||||
{#each stats as stat}
|
||||
<div class="stat-item">
|
||||
<div class="stat-value">{stat.value}</div>
|
||||
<div class="stat-label">{stat.label}</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="content-section">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">How It Works</h2>
|
||||
<p class="section-desc">Tools to help you shop smarter.</p>
|
||||
</div>
|
||||
<div class="features-grid">
|
||||
{#each features as feature}
|
||||
<div class="glass-card feature-card">
|
||||
<div class="feature-icon">
|
||||
<iconify-icon icon={feature.icon} width="24"
|
||||
></iconify-icon>
|
||||
</div>
|
||||
<h3 class="feature-title">{feature.title}</h3>
|
||||
<p class="feature-desc">{feature.desc}</p>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="content-section">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">Latest News</h2>
|
||||
<p class="section-desc">
|
||||
Updates from the world of sustainability.
|
||||
</p>
|
||||
</div>
|
||||
<div class="news-grid">
|
||||
{#each news as item (item.id)}
|
||||
<article class="glass-card news-card">
|
||||
<div class="news-icon">
|
||||
<iconify-icon icon={item.icon} width="24"
|
||||
></iconify-icon>
|
||||
</div>
|
||||
<div class="news-meta">{item.date}</div>
|
||||
<h3 class="news-title">{item.title}</h3>
|
||||
<p class="news-desc">{item.desc}</p>
|
||||
<a href="/news/{item.id}" class="news-link">
|
||||
Read more
|
||||
<iconify-icon icon="ri:arrow-right-line" width="16"
|
||||
></iconify-icon>
|
||||
</a>
|
||||
</article>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="footer-spacer"></div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.web-home {
|
||||
width: 100%;
|
||||
overflow-x: hidden;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.glass-card {
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 24px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
|
||||
transition: all 0.4s ease;
|
||||
}
|
||||
|
||||
.glass-card:hover {
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.hero {
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 60px;
|
||||
align-items: center;
|
||||
padding: 120px 60px;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
padding: 48px;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.hero-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
color: #34d399;
|
||||
padding: 10px 20px;
|
||||
border-radius: 50px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 24px;
|
||||
border: 1px solid rgba(16, 185, 129, 0.2);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1.5px;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 72px;
|
||||
font-weight: 900;
|
||||
line-height: 1.05;
|
||||
color: white;
|
||||
margin: 0 0 28px 0;
|
||||
letter-spacing: -2px;
|
||||
}
|
||||
|
||||
.hero-desc {
|
||||
font-size: 18px;
|
||||
line-height: 1.7;
|
||||
color: #d1d5db;
|
||||
margin: 0 0 36px 0;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.cta-primary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 18px 36px;
|
||||
border-radius: 50px;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
background: #10b981;
|
||||
color: white;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
text-decoration: none;
|
||||
box-shadow: 0 8px 24px rgba(16, 185, 129, 0.25);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.cta-primary:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 16px 40px rgba(16, 185, 129, 0.35);
|
||||
background: #059669;
|
||||
}
|
||||
|
||||
.hero-visual {
|
||||
position: relative;
|
||||
height: 500px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.visual-container {
|
||||
position: relative;
|
||||
width: 420px;
|
||||
height: 420px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.hero-image {
|
||||
width: 360px;
|
||||
height: 360px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.product-wrapper {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.product-image {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
object-fit: contain;
|
||||
filter: drop-shadow(0 10px 30px rgba(0, 0, 0, 0.3));
|
||||
}
|
||||
|
||||
.orbit {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 50%;
|
||||
z-index: 1;
|
||||
animation: spin 25s linear infinite;
|
||||
}
|
||||
|
||||
.orbit-1 {
|
||||
width: 340px;
|
||||
height: 340px;
|
||||
}
|
||||
.orbit-2 {
|
||||
width: 480px;
|
||||
height: 480px;
|
||||
opacity: 0.4;
|
||||
animation-duration: 45s;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: translate(-50%, -50%) rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: translate(-50%, -50%) rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.floating-card {
|
||||
position: absolute;
|
||||
right: -20px;
|
||||
bottom: 80px;
|
||||
padding: 16px 24px;
|
||||
min-width: 200px;
|
||||
z-index: 10;
|
||||
animation: float 4s ease-in-out infinite;
|
||||
background: rgba(5, 31, 24, 0.8);
|
||||
border: 1px solid rgba(52, 211, 153, 0.2);
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-12px);
|
||||
}
|
||||
}
|
||||
|
||||
.score-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.card-label {
|
||||
font-size: 11px;
|
||||
color: #9ca3af;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.card-value {
|
||||
font-size: 22px;
|
||||
color: white;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
height: 25vh;
|
||||
}
|
||||
.footer-spacer {
|
||||
height: 15vh;
|
||||
}
|
||||
|
||||
.content-section {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 60px 40px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
text-align: center;
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 42px;
|
||||
font-weight: 800;
|
||||
color: white;
|
||||
margin: 0 0 12px 0;
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
|
||||
.section-desc {
|
||||
font-size: 18px;
|
||||
color: #d1d5db;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 32px;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 48px;
|
||||
font-weight: 900;
|
||||
color: #34d399;
|
||||
margin-bottom: 4px;
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #9ca3af;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
padding: 32px 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
border-radius: 16px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #34d399;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.feature-title {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
|
||||
.feature-desc {
|
||||
font-size: 14px;
|
||||
color: #9ca3af;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.news-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 28px;
|
||||
}
|
||||
|
||||
.news-card {
|
||||
padding: 28px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.news-icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
border-radius: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #34d399;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.news-meta {
|
||||
font-size: 12px;
|
||||
color: #34d399;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.news-title {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
margin: 0 0 10px 0;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.news-desc {
|
||||
font-size: 14px;
|
||||
color: #d1d5db;
|
||||
line-height: 1.6;
|
||||
flex-grow: 1;
|
||||
margin: 0 0 20px 0;
|
||||
}
|
||||
|
||||
.news-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #34d399;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.news-link:hover {
|
||||
gap: 12px;
|
||||
color: #6ee7b7;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.hero {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 40px;
|
||||
padding: 100px 30px;
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
max-width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 52px;
|
||||
}
|
||||
.hero-actions {
|
||||
justify-content: center;
|
||||
}
|
||||
.hero-visual {
|
||||
height: 400px;
|
||||
}
|
||||
.features-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
.news-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,23 +0,0 @@
|
||||
<nav class="web-navbar">
|
||||
<h1>My Website</h1>
|
||||
<ul>
|
||||
<li><a href="/">Home</a></li>
|
||||
<li><a href="/about">About</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<style>
|
||||
.web-navbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 1rem;
|
||||
background-color: #333;
|
||||
color: white;
|
||||
}
|
||||
ul {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
list-style: none;
|
||||
}
|
||||
a { color: white; text-decoration: none; }
|
||||
</style>
|
||||
182
frontend/src/lib/ts/parallax/colors.ts
Normal file
182
frontend/src/lib/ts/parallax/colors.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
import type { SceneColors, SceneType, ParallaxState } from './types';
|
||||
|
||||
export const SCENE_COLORS: Record<Exclude<SceneType, 'transition'>, SceneColors> = {
|
||||
eco: {
|
||||
skyTop: '#87CEEB',
|
||||
skyBottom: '#E0F7FA',
|
||||
sun: '#FFD54F',
|
||||
sunGlow: 'rgba(255, 213, 79, 0.3)',
|
||||
mountainFar: '#81C784',
|
||||
mountainMid: '#66BB6A',
|
||||
hillFront: '#4CAF50',
|
||||
treeDark: '#2E7D32',
|
||||
treeLight: '#66BB6A',
|
||||
ground: '#8BC34A',
|
||||
water: '#4FC3F7',
|
||||
cloud: 'rgba(255, 255, 255, 0.9)',
|
||||
accent: '#E91E63',
|
||||
},
|
||||
industrial: {
|
||||
skyTop: '#4A4A4A',
|
||||
skyBottom: '#757575',
|
||||
sun: '#FFA726',
|
||||
sunGlow: 'rgba(255, 167, 38, 0.2)',
|
||||
mountainFar: '#616161',
|
||||
mountainMid: '#757575',
|
||||
hillFront: '#5D4037',
|
||||
treeDark: '#4E342E',
|
||||
treeLight: '#6D4C41',
|
||||
ground: '#795548',
|
||||
water: '#546E7A',
|
||||
cloud: 'rgba(120, 120, 120, 0.7)',
|
||||
accent: '#D32F2F',
|
||||
},
|
||||
forest: {
|
||||
skyTop: '#64B5F6',
|
||||
skyBottom: '#B3E5FC',
|
||||
sun: '#FFD54F',
|
||||
sunGlow: 'rgba(255, 213, 79, 0.4)',
|
||||
mountainFar: '#1B5E20',
|
||||
mountainMid: '#2E7D32',
|
||||
hillFront: '#388E3C',
|
||||
treeDark: '#1B5E20',
|
||||
treeLight: '#4CAF50',
|
||||
ground: '#33691E',
|
||||
water: '#26A69A',
|
||||
cloud: 'rgba(255, 255, 255, 0.85)',
|
||||
accent: '#8BC34A',
|
||||
},
|
||||
deforestation: {
|
||||
skyTop: '#8D6E63',
|
||||
skyBottom: '#BCAAA4',
|
||||
sun: '#FF8A65',
|
||||
sunGlow: 'rgba(255, 138, 101, 0.3)',
|
||||
mountainFar: '#6D4C41',
|
||||
mountainMid: '#8D6E63',
|
||||
hillFront: '#A1887F',
|
||||
treeDark: '#5D4037',
|
||||
treeLight: '#8D6E63',
|
||||
ground: '#4E342E',
|
||||
water: '#78909C',
|
||||
cloud: 'rgba(180, 160, 140, 0.8)',
|
||||
accent: '#FF5722',
|
||||
},
|
||||
ocean: {
|
||||
skyTop: '#039BE5',
|
||||
skyBottom: '#81D4FA',
|
||||
sun: '#FFF176',
|
||||
sunGlow: 'rgba(255, 241, 118, 0.4)',
|
||||
mountainFar: '#0277BD',
|
||||
mountainMid: '#0288D1',
|
||||
hillFront: '#03A9F4',
|
||||
treeDark: '#00897B',
|
||||
treeLight: '#26A69A',
|
||||
ground: '#4DB6AC',
|
||||
water: '#00ACC1',
|
||||
cloud: 'rgba(255, 255, 255, 0.95)',
|
||||
accent: '#00BCD4',
|
||||
},
|
||||
oilRig: {
|
||||
skyTop: '#37474F',
|
||||
skyBottom: '#546E7A',
|
||||
sun: '#FF6F00',
|
||||
sunGlow: 'rgba(255, 111, 0, 0.25)',
|
||||
mountainFar: '#263238',
|
||||
mountainMid: '#37474F',
|
||||
hillFront: '#455A64',
|
||||
treeDark: '#263238',
|
||||
treeLight: '#37474F',
|
||||
ground: '#1C313A',
|
||||
water: '#1C313A',
|
||||
cloud: 'rgba(80, 80, 80, 0.8)',
|
||||
accent: '#FF5722',
|
||||
},
|
||||
city: {
|
||||
skyTop: '#42A5F5',
|
||||
skyBottom: '#90CAF9',
|
||||
sun: '#FFEB3B',
|
||||
sunGlow: 'rgba(255, 235, 59, 0.35)',
|
||||
mountainFar: '#78909C',
|
||||
mountainMid: '#90A4AE',
|
||||
hillFront: '#B0BEC5',
|
||||
treeDark: '#4CAF50',
|
||||
treeLight: '#81C784',
|
||||
ground: '#ECEFF1',
|
||||
water: '#26C6DA',
|
||||
cloud: 'rgba(255, 255, 255, 0.9)',
|
||||
accent: '#2196F3',
|
||||
},
|
||||
pollutedCity: {
|
||||
skyTop: '#424242',
|
||||
skyBottom: '#616161',
|
||||
sun: '#EF6C00',
|
||||
sunGlow: 'rgba(239, 108, 0, 0.2)',
|
||||
mountainFar: '#424242',
|
||||
mountainMid: '#616161',
|
||||
hillFront: '#757575',
|
||||
treeDark: '#4E342E',
|
||||
treeLight: '#5D4037',
|
||||
ground: '#3E2723',
|
||||
water: '#37474F',
|
||||
cloud: 'rgba(90, 90, 90, 0.85)',
|
||||
accent: '#F44336',
|
||||
},
|
||||
};
|
||||
|
||||
export function lerpColor(color1: string, color2: string, t: number): string {
|
||||
const parse = (c: string) => {
|
||||
if (c.startsWith('#')) {
|
||||
const hex = c.slice(1);
|
||||
return {
|
||||
r: parseInt(hex.slice(0, 2), 16),
|
||||
g: parseInt(hex.slice(2, 4), 16),
|
||||
b: parseInt(hex.slice(4, 6), 16),
|
||||
a: 1,
|
||||
};
|
||||
}
|
||||
const match = c.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
|
||||
if (match) {
|
||||
return {
|
||||
r: parseInt(match[1]),
|
||||
g: parseInt(match[2]),
|
||||
b: parseInt(match[3]),
|
||||
a: parseFloat(match[4] ?? '1'),
|
||||
};
|
||||
}
|
||||
return { r: 0, g: 0, b: 0, a: 1 };
|
||||
};
|
||||
|
||||
const c1 = parse(color1);
|
||||
const c2 = parse(color2);
|
||||
|
||||
const r = Math.round(c1.r + (c2.r - c1.r) * t);
|
||||
const g = Math.round(c1.g + (c2.g - c1.g) * t);
|
||||
const b = Math.round(c1.b + (c2.b - c1.b) * t);
|
||||
const a = c1.a + (c2.a - c1.a) * t;
|
||||
|
||||
return `rgba(${r}, ${g}, ${b}, ${a})`;
|
||||
}
|
||||
|
||||
function blendColors(colors1: SceneColors, colors2: SceneColors, t: number): SceneColors {
|
||||
const result = {} as SceneColors;
|
||||
for (const key of Object.keys(colors1) as (keyof SceneColors)[]) {
|
||||
result[key] = lerpColor(colors1[key], colors2[key], t);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function getSceneColors(state: ParallaxState): SceneColors {
|
||||
const { sceneType, progress, blendToScene, blendProgress } = state;
|
||||
|
||||
if (sceneType === 'transition') {
|
||||
return blendColors(SCENE_COLORS.eco, SCENE_COLORS.industrial, progress);
|
||||
}
|
||||
|
||||
if (blendToScene && blendProgress !== undefined && blendToScene !== 'transition') {
|
||||
const fromColors = SCENE_COLORS[sceneType as keyof typeof SCENE_COLORS] || SCENE_COLORS.eco;
|
||||
const toColors = SCENE_COLORS[blendToScene as keyof typeof SCENE_COLORS] || SCENE_COLORS.eco;
|
||||
return blendColors(fromColors, toColors, blendProgress);
|
||||
}
|
||||
|
||||
return SCENE_COLORS[sceneType as keyof typeof SCENE_COLORS] || SCENE_COLORS.eco;
|
||||
}
|
||||
151
frontend/src/lib/ts/parallax/drawBase.ts
Normal file
151
frontend/src/lib/ts/parallax/drawBase.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import type { DrawContext } from './types';
|
||||
import { getSceneColors } from './colors';
|
||||
|
||||
export function drawSky(dc: DrawContext): void {
|
||||
const { ctx, width, height, state } = dc;
|
||||
const colors = getSceneColors(state);
|
||||
|
||||
const gradient = ctx.createLinearGradient(0, 0, 0, height);
|
||||
gradient.addColorStop(0, colors.skyTop);
|
||||
gradient.addColorStop(1, colors.skyBottom);
|
||||
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
}
|
||||
|
||||
export function drawSun(dc: DrawContext): void {
|
||||
const { ctx, width, height, state } = dc;
|
||||
const colors = getSceneColors(state);
|
||||
|
||||
const sunX = width * 0.85 + state.mouseX * 0.3 + state.scrollY * 0.02;
|
||||
const sunY = height * 0.2 + state.mouseY * 0.3 + state.scrollY * 0.05;
|
||||
const sunRadius = Math.min(width, height) * 0.08;
|
||||
|
||||
for (let i = 4; i >= 0; i--) {
|
||||
ctx.beginPath();
|
||||
ctx.arc(sunX, sunY, sunRadius * (1 + i * 0.4), 0, Math.PI * 2);
|
||||
ctx.fillStyle = colors.sunGlow;
|
||||
ctx.globalAlpha = 0.2 - i * 0.03;
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
ctx.globalAlpha = 1;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(sunX, sunY, sunRadius, 0, Math.PI * 2);
|
||||
ctx.fillStyle = "#FDB813";
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
export function drawClouds(dc: DrawContext): void {
|
||||
const { ctx, width, height, state } = dc;
|
||||
const colors = getSceneColors(state);
|
||||
|
||||
const drawCloud = (x: number, y: number, scale: number) => {
|
||||
ctx.fillStyle = colors.cloud;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(x, y, 60 * scale, 35 * scale, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(x + 50 * scale, y + 10 * scale, 50 * scale, 30 * scale, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(x - 40 * scale, y + 5 * scale, 45 * scale, 25 * scale, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(x + 20 * scale, y - 15 * scale, 40 * scale, 25 * scale, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
};
|
||||
|
||||
const cloudOffsetX = state.mouseX * 0.5 - state.scrollY * 0.03;
|
||||
const cloudOffsetY = state.scrollY * 0.1;
|
||||
|
||||
drawCloud(width * 0.15 + cloudOffsetX, height * 0.15 + cloudOffsetY, 1.2);
|
||||
drawCloud(width * 0.55 + cloudOffsetX * 0.7, height * 0.12 + cloudOffsetY * 0.8, 0.9);
|
||||
drawCloud(width * 0.85 + cloudOffsetX * 0.5, height * 0.2 + cloudOffsetY * 0.6, 1.0);
|
||||
}
|
||||
|
||||
export function drawMountains(dc: DrawContext): void {
|
||||
const { ctx, width, height, state } = dc;
|
||||
const colors = getSceneColors(state);
|
||||
const parallaxFar = state.mouseX * 0.2 + state.scrollY * 0.15;
|
||||
const parallaxMid = state.mouseX * 0.35 + state.scrollY * 0.25;
|
||||
|
||||
ctx.fillStyle = colors.mountainFar;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-100 + parallaxFar, height);
|
||||
ctx.lineTo(width * 0.2 + parallaxFar, height * 0.45);
|
||||
ctx.lineTo(width * 0.35 + parallaxFar, height * 0.55);
|
||||
ctx.lineTo(width * 0.5 + parallaxFar, height * 0.4);
|
||||
ctx.lineTo(width * 0.7 + parallaxFar, height * 0.5);
|
||||
ctx.lineTo(width * 0.85 + parallaxFar, height * 0.35);
|
||||
ctx.lineTo(width + 100 + parallaxFar, height * 0.5);
|
||||
ctx.lineTo(width + 100, height);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = colors.mountainMid;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-100 + parallaxMid, height);
|
||||
ctx.lineTo(width * 0.1 + parallaxMid, height * 0.55);
|
||||
ctx.lineTo(width * 0.25 + parallaxMid, height * 0.62);
|
||||
ctx.lineTo(width * 0.4 + parallaxMid, height * 0.5);
|
||||
ctx.lineTo(width * 0.6 + parallaxMid, height * 0.58);
|
||||
ctx.lineTo(width * 0.75 + parallaxMid, height * 0.48);
|
||||
ctx.lineTo(width * 0.9 + parallaxMid, height * 0.55);
|
||||
ctx.lineTo(width + 100, height);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
export function drawHills(dc: DrawContext): void {
|
||||
const { ctx, width, height, state } = dc;
|
||||
const colors = getSceneColors(state);
|
||||
const parallax = state.mouseX * 0.5 + state.scrollY * 0.35;
|
||||
|
||||
ctx.fillStyle = colors.hillFront;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-100, height);
|
||||
|
||||
for (let x = -100; x <= width + 100; x += 100) {
|
||||
const y = height * 0.65 + Math.sin((x + parallax) * 0.01) * 40;
|
||||
ctx.lineTo(x, y);
|
||||
}
|
||||
|
||||
ctx.lineTo(width + 100, height);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = colors.ground;
|
||||
ctx.fillRect(-100, height * 0.8, width + 200, height * 0.2);
|
||||
}
|
||||
|
||||
export function drawWater(dc: DrawContext): void {
|
||||
const { ctx, width, height, state } = dc;
|
||||
const colors = getSceneColors(state);
|
||||
const time = Date.now() * 0.001;
|
||||
const waveOffset = state.scrollY * 0.1;
|
||||
|
||||
ctx.fillStyle = colors.water;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-100, height);
|
||||
|
||||
for (let x = -100; x <= width + 100; x += 20) {
|
||||
const y = height * 0.88 + Math.sin((x + time * 50 + waveOffset) * 0.02) * 5;
|
||||
ctx.lineTo(x, y);
|
||||
}
|
||||
|
||||
ctx.lineTo(width + 100, height);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
|
||||
ctx.lineWidth = 2;
|
||||
for (let i = 0; i < 5; i++) {
|
||||
ctx.beginPath();
|
||||
const startX = (width / 5) * i + Math.sin(time + i) * 20;
|
||||
ctx.moveTo(startX, height * 0.9 + i * 5);
|
||||
ctx.lineTo(startX + 40, height * 0.9 + i * 5);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
95
frontend/src/lib/ts/parallax/index.ts
Normal file
95
frontend/src/lib/ts/parallax/index.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import type { ParallaxState, DrawContext, SceneType } from './types';
|
||||
import { drawSky, drawSun, drawClouds, drawMountains, drawHills, drawWater } from './drawBase';
|
||||
import { drawEcoScene } from './scenes/eco';
|
||||
import { drawForestScene } from './scenes/forest';
|
||||
import { drawIndustrialScene } from './scenes/industrial';
|
||||
import { drawDeforestationScene } from './scenes/deforestation';
|
||||
import { drawOceanScene, drawOceanWaves } from './scenes/ocean';
|
||||
import { drawOilRigScene } from './scenes/oilRig';
|
||||
import { drawCityScene } from './scenes/city';
|
||||
import { drawPollutedCityScene } from './scenes/pollutedCity';
|
||||
|
||||
export type { ParallaxState, SceneType };
|
||||
|
||||
const SCENE_ELEMENTS: Record<Exclude<SceneType, 'transition'>, (dc: DrawContext) => void> = {
|
||||
eco: drawEcoScene,
|
||||
forest: drawForestScene,
|
||||
industrial: drawIndustrialScene,
|
||||
deforestation: drawDeforestationScene,
|
||||
ocean: drawOceanScene,
|
||||
oilRig: drawOilRigScene,
|
||||
city: drawCityScene,
|
||||
pollutedCity: drawPollutedCityScene,
|
||||
};
|
||||
|
||||
const CUSTOM_WATER_SCENES: SceneType[] = ['ocean', 'oilRig'];
|
||||
const NO_TERRAIN_SCENES: SceneType[] = ['ocean', 'oilRig'];
|
||||
|
||||
export function drawLandscape(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
width: number,
|
||||
height: number,
|
||||
state: ParallaxState
|
||||
): void {
|
||||
const dc: DrawContext = { ctx, width, height, state };
|
||||
const { sceneType, blendToScene, blendProgress } = state;
|
||||
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
|
||||
drawSky(dc);
|
||||
drawSun(dc);
|
||||
drawClouds(dc);
|
||||
|
||||
const skipTerrain = NO_TERRAIN_SCENES.includes(sceneType) ||
|
||||
(blendToScene && NO_TERRAIN_SCENES.includes(blendToScene) && (blendProgress ?? 0) > 0.5);
|
||||
|
||||
if (!skipTerrain) {
|
||||
drawMountains(dc);
|
||||
drawHills(dc);
|
||||
}
|
||||
|
||||
if (sceneType === 'transition') {
|
||||
const ecoOpacity = 1 - state.progress;
|
||||
const industrialOpacity = state.progress;
|
||||
|
||||
if (ecoOpacity > 0.1) {
|
||||
ctx.globalAlpha = ecoOpacity;
|
||||
drawEcoScene(dc);
|
||||
}
|
||||
if (industrialOpacity > 0.1) {
|
||||
ctx.globalAlpha = industrialOpacity;
|
||||
drawIndustrialScene(dc);
|
||||
}
|
||||
ctx.globalAlpha = 1;
|
||||
} else if (blendToScene && blendProgress !== undefined && blendToScene !== 'transition') {
|
||||
const fromOpacity = 1 - blendProgress;
|
||||
const toOpacity = blendProgress;
|
||||
|
||||
const fromDrawer = SCENE_ELEMENTS[sceneType as keyof typeof SCENE_ELEMENTS];
|
||||
const toDrawer = SCENE_ELEMENTS[blendToScene as keyof typeof SCENE_ELEMENTS];
|
||||
|
||||
if (fromDrawer && fromOpacity > 0.1) {
|
||||
ctx.globalAlpha = fromOpacity;
|
||||
fromDrawer(dc);
|
||||
}
|
||||
if (toDrawer && toOpacity > 0.1) {
|
||||
ctx.globalAlpha = toOpacity;
|
||||
toDrawer(dc);
|
||||
}
|
||||
ctx.globalAlpha = 1;
|
||||
} else {
|
||||
const drawer = SCENE_ELEMENTS[sceneType as keyof typeof SCENE_ELEMENTS];
|
||||
if (drawer) {
|
||||
drawer(dc);
|
||||
}
|
||||
}
|
||||
|
||||
const useCustomWater = CUSTOM_WATER_SCENES.includes(sceneType) ||
|
||||
(blendToScene && CUSTOM_WATER_SCENES.includes(blendToScene) && (blendProgress ?? 0) > 0.5);
|
||||
|
||||
if (sceneType === 'ocean' || (blendToScene === 'ocean' && (blendProgress ?? 0) > 0.5)) {
|
||||
drawOceanWaves(dc);
|
||||
} else if (!useCustomWater) {
|
||||
drawWater(dc);
|
||||
}
|
||||
}
|
||||
189
frontend/src/lib/ts/parallax/scenes/city.ts
Normal file
189
frontend/src/lib/ts/parallax/scenes/city.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
import type { DrawContext } from '../types';
|
||||
|
||||
export function drawCityBuildings(dc: DrawContext): void {
|
||||
const { ctx, width, height, state } = dc;
|
||||
const parallax = state.mouseX * 0.35;
|
||||
|
||||
const drawBuilding = (x: number, bWidth: number, bHeight: number, color: string, hasSpire: boolean = false) => {
|
||||
const baseY = height * 0.8;
|
||||
|
||||
ctx.fillStyle = color;
|
||||
ctx.fillRect(x + parallax, baseY - bHeight, bWidth, bHeight);
|
||||
|
||||
const gradient = ctx.createLinearGradient(x + parallax, 0, x + parallax + bWidth, 0);
|
||||
gradient.addColorStop(0, 'rgba(255, 255, 255, 0.1)');
|
||||
gradient.addColorStop(0.5, 'rgba(255, 255, 255, 0.2)');
|
||||
gradient.addColorStop(1, 'rgba(255, 255, 255, 0.05)');
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fillRect(x + parallax, baseY - bHeight, bWidth, bHeight);
|
||||
|
||||
ctx.fillStyle = 'rgba(255, 255, 200, 0.8)';
|
||||
const windowCols = Math.floor(bWidth / 18);
|
||||
const windowRows = Math.floor(bHeight / 25);
|
||||
|
||||
for (let row = 0; row < windowRows; row++) {
|
||||
for (let col = 0; col < windowCols; col++) {
|
||||
if (Math.random() > 0.15) {
|
||||
ctx.fillStyle = Math.random() > 0.7 ? 'rgba(255, 255, 200, 0.9)' : 'rgba(200, 220, 255, 0.6)';
|
||||
ctx.fillRect(
|
||||
x + parallax + col * 18 + 5,
|
||||
baseY - bHeight + row * 25 + 8,
|
||||
10, 12
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasSpire) {
|
||||
ctx.fillStyle = '#90A4AE';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + parallax + bWidth / 2, baseY - bHeight - 40);
|
||||
ctx.lineTo(x + parallax + bWidth / 2 - 8, baseY - bHeight);
|
||||
ctx.lineTo(x + parallax + bWidth / 2 + 8, baseY - bHeight);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
}
|
||||
};
|
||||
|
||||
drawBuilding(width * 0.02, 55, height * 0.28, '#607D8B');
|
||||
drawBuilding(width * 0.08, 70, height * 0.42, '#78909C', true);
|
||||
drawBuilding(width * 0.18, 50, height * 0.32, '#546E7A');
|
||||
drawBuilding(width * 0.25, 85, height * 0.55, '#455A64', true);
|
||||
drawBuilding(width * 0.38, 60, height * 0.38, '#607D8B');
|
||||
drawBuilding(width * 0.48, 75, height * 0.48, '#78909C', true);
|
||||
drawBuilding(width * 0.58, 55, height * 0.35, '#546E7A');
|
||||
drawBuilding(width * 0.68, 90, height * 0.52, '#455A64', true);
|
||||
drawBuilding(width * 0.8, 65, height * 0.4, '#607D8B');
|
||||
drawBuilding(width * 0.9, 50, height * 0.3, '#78909C');
|
||||
}
|
||||
|
||||
export function drawStreet(dc: DrawContext): void {
|
||||
const { ctx, width, height } = dc;
|
||||
const time = Date.now() * 0.001;
|
||||
|
||||
ctx.fillStyle = '#424242';
|
||||
ctx.fillRect(0, height * 0.82, width, height * 0.08);
|
||||
|
||||
ctx.strokeStyle = '#FFEB3B';
|
||||
ctx.lineWidth = 3;
|
||||
ctx.setLineDash([30, 20]);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, height * 0.86);
|
||||
ctx.lineTo(width, height * 0.86);
|
||||
ctx.stroke();
|
||||
ctx.setLineDash([]);
|
||||
|
||||
const drawCar = (baseX: number, y: number, color: string, direction: number) => {
|
||||
const x = ((baseX + time * 50 * direction) % (width + 100)) - 50;
|
||||
|
||||
ctx.fillStyle = color;
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(x, y, 40, 15, 3);
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = color;
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(x + 8, y - 10, 24, 12, 3);
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = '#81D4FA';
|
||||
ctx.fillRect(x + 10, y - 8, 9, 8);
|
||||
ctx.fillRect(x + 21, y - 8, 9, 8);
|
||||
|
||||
ctx.fillStyle = '#212121';
|
||||
ctx.beginPath();
|
||||
ctx.arc(x + 10, y + 15, 5, 0, Math.PI * 2);
|
||||
ctx.arc(x + 30, y + 15, 5, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
};
|
||||
|
||||
drawCar(width * 0.1, height * 0.83, '#E53935', 1);
|
||||
drawCar(width * 0.5, height * 0.83, '#1E88E5', 1);
|
||||
drawCar(width * 0.3, height * 0.87, '#43A047', -1);
|
||||
drawCar(width * 0.8, height * 0.87, '#FDD835', -1);
|
||||
}
|
||||
|
||||
export function drawAirplane(dc: DrawContext): void {
|
||||
const { ctx, width, height, state } = dc;
|
||||
const time = Date.now() * 0.0003;
|
||||
const parallax = state.mouseX * 0.1;
|
||||
|
||||
const x = (time * width) % (width + 200) - 100;
|
||||
const y = height * 0.15 + Math.sin(time * 5) * 10;
|
||||
|
||||
ctx.save();
|
||||
ctx.translate(x + parallax, y);
|
||||
|
||||
ctx.fillStyle = '#ECEFF1';
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(0, 0, 40, 8, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(45, 0, 12, 6, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = '#B0BEC5';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-10, 0);
|
||||
ctx.lineTo(-30, -35);
|
||||
ctx.lineTo(10, -35);
|
||||
ctx.lineTo(15, 0);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-10, 0);
|
||||
ctx.lineTo(-30, 35);
|
||||
ctx.lineTo(10, 35);
|
||||
ctx.lineTo(15, 0);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = '#E53935';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-35, 0);
|
||||
ctx.lineTo(-50, -20);
|
||||
ctx.lineTo(-30, 0);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
|
||||
ctx.lineWidth = 3;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-50, 0);
|
||||
ctx.lineTo(-150, 3);
|
||||
ctx.stroke();
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
export function drawParks(dc: DrawContext): void {
|
||||
const { ctx, width, height } = dc;
|
||||
|
||||
ctx.fillStyle = '#81C784';
|
||||
|
||||
const parks = [
|
||||
{ x: width * 0.05, y: height * 0.81, rx: 25, ry: 8 },
|
||||
{ x: width * 0.35, y: height * 0.81, rx: 30, ry: 10 },
|
||||
{ x: width * 0.75, y: height * 0.81, rx: 35, ry: 9 },
|
||||
];
|
||||
|
||||
parks.forEach(p => {
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(p.x, p.y, p.rx, p.ry, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = '#4CAF50';
|
||||
ctx.beginPath();
|
||||
ctx.arc(p.x - 10, p.y - 5, 8, 0, Math.PI * 2);
|
||||
ctx.arc(p.x + 10, p.y - 3, 6, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
});
|
||||
}
|
||||
|
||||
export function drawCityScene(dc: DrawContext): void {
|
||||
drawAirplane(dc);
|
||||
drawCityBuildings(dc);
|
||||
drawParks(dc);
|
||||
drawStreet(dc);
|
||||
}
|
||||
137
frontend/src/lib/ts/parallax/scenes/deforestation.ts
Normal file
137
frontend/src/lib/ts/parallax/scenes/deforestation.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import type { DrawContext } from '../types';
|
||||
|
||||
export function drawStumps(dc: DrawContext): void {
|
||||
const { ctx, width, height, state } = dc;
|
||||
const parallax = state.mouseX * 0.7 + state.scrollY * 0.5;
|
||||
|
||||
const drawStump = (x: number, y: number, scale: number) => {
|
||||
ctx.fillStyle = '#5D4037';
|
||||
ctx.fillRect(x - 12 * scale + parallax, y - 8 * scale, 24 * scale, 25 * scale);
|
||||
|
||||
ctx.fillStyle = '#8D6E63';
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(x + parallax, y - 8 * scale, 15 * scale, 8 * scale, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
ctx.strokeStyle = '#5D4037';
|
||||
ctx.lineWidth = 1;
|
||||
for (let ring = 1; ring <= 3; ring++) {
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(x + parallax, y - 8 * scale, ring * 4 * scale, ring * 2.5 * scale, 0, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
}
|
||||
};
|
||||
|
||||
const stumpPositions = [
|
||||
{ x: width * 0.08, y: height * 0.78, scale: 1.2 },
|
||||
{ x: width * 0.18, y: height * 0.82, scale: 0.9 },
|
||||
{ x: width * 0.28, y: height * 0.76, scale: 1.4 },
|
||||
{ x: width * 0.42, y: height * 0.8, scale: 1.0 },
|
||||
{ x: width * 0.55, y: height * 0.78, scale: 1.3 },
|
||||
{ x: width * 0.68, y: height * 0.82, scale: 0.8 },
|
||||
{ x: width * 0.78, y: height * 0.77, scale: 1.5 },
|
||||
{ x: width * 0.9, y: height * 0.8, scale: 1.1 },
|
||||
];
|
||||
|
||||
stumpPositions.forEach(s => drawStump(s.x, s.y, s.scale));
|
||||
}
|
||||
|
||||
export function drawFallenLogs(dc: DrawContext): void {
|
||||
const { ctx, width, height, state } = dc;
|
||||
const parallax = state.mouseX * 0.6;
|
||||
|
||||
const drawLog = (x: number, y: number, length: number, angle: number) => {
|
||||
ctx.save();
|
||||
ctx.translate(x + parallax, y);
|
||||
ctx.rotate(angle);
|
||||
|
||||
ctx.fillStyle = '#6D4C41';
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(0, 0, length, 12, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = '#8D6E63';
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(length - 5, 0, 8, 12, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
ctx.strokeStyle = '#4E342E';
|
||||
ctx.lineWidth = 2;
|
||||
for (let i = -length + 20; i < length - 20; i += 25) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(i, -10);
|
||||
ctx.lineTo(i + 10, 10);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
};
|
||||
|
||||
drawLog(width * 0.2, height * 0.85, 80, 0.1);
|
||||
drawLog(width * 0.5, height * 0.86, 100, -0.15);
|
||||
drawLog(width * 0.75, height * 0.84, 70, 0.2);
|
||||
}
|
||||
|
||||
export function drawLoggingTruck(dc: DrawContext): void {
|
||||
const { ctx, width, height, state } = dc;
|
||||
const parallax = state.mouseX * 0.4;
|
||||
const x = width * 0.65 + parallax;
|
||||
const y = height * 0.75;
|
||||
|
||||
ctx.fillStyle = '#FF9800';
|
||||
ctx.fillRect(x, y, 50, 35);
|
||||
ctx.fillStyle = '#F57C00';
|
||||
ctx.fillRect(x, y, 50, 8);
|
||||
|
||||
ctx.fillStyle = '#81D4FA';
|
||||
ctx.fillRect(x + 5, y + 10, 40, 15);
|
||||
|
||||
ctx.fillStyle = '#616161';
|
||||
ctx.fillRect(x + 55, y + 5, 90, 30);
|
||||
|
||||
ctx.fillStyle = '#8D6E63';
|
||||
for (let i = 0; i < 3; i++) {
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(x + 75 + i * 25, y + 5, 12, 20, Math.PI / 2, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
ctx.fillStyle = '#212121';
|
||||
ctx.beginPath();
|
||||
ctx.arc(x + 15, y + 38, 12, 0, Math.PI * 2);
|
||||
ctx.arc(x + 80, y + 38, 12, 0, Math.PI * 2);
|
||||
ctx.arc(x + 120, y + 38, 12, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = '#757575';
|
||||
ctx.beginPath();
|
||||
ctx.arc(x + 15, y + 38, 6, 0, Math.PI * 2);
|
||||
ctx.arc(x + 80, y + 38, 6, 0, Math.PI * 2);
|
||||
ctx.arc(x + 120, y + 38, 6, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
export function drawDirtPatches(dc: DrawContext): void {
|
||||
const { ctx, width, height } = dc;
|
||||
|
||||
ctx.fillStyle = '#4E342E';
|
||||
|
||||
const patches = [
|
||||
{ x: width * 0.15, y: height * 0.83, rx: 40, ry: 15 },
|
||||
{ x: width * 0.45, y: height * 0.85, rx: 60, ry: 20 },
|
||||
{ x: width * 0.7, y: height * 0.82, rx: 50, ry: 18 },
|
||||
];
|
||||
|
||||
patches.forEach(p => {
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(p.x, p.y, p.rx, p.ry, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
});
|
||||
}
|
||||
|
||||
export function drawDeforestationScene(dc: DrawContext): void {
|
||||
drawDirtPatches(dc);
|
||||
drawStumps(dc);
|
||||
drawFallenLogs(dc);
|
||||
drawLoggingTruck(dc);
|
||||
}
|
||||
145
frontend/src/lib/ts/parallax/scenes/eco.ts
Normal file
145
frontend/src/lib/ts/parallax/scenes/eco.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import type { DrawContext } from '../types';
|
||||
import { getSceneColors } from '../colors';
|
||||
|
||||
const FLOWER_COLORS = ['#E91E63', '#FF5722', '#FFEB3B', '#9C27B0', '#FF4081'];
|
||||
|
||||
export function drawEcoTrees(dc: DrawContext): void {
|
||||
const { ctx, width, height, state } = dc;
|
||||
const colors = getSceneColors(state);
|
||||
const parallax = state.mouseX * 0.8 + state.scrollY * 0.5;
|
||||
|
||||
const drawTree = (x: number, y: number, scale: number) => {
|
||||
ctx.fillStyle = '#5D4037';
|
||||
ctx.fillRect(x - 8 * scale + parallax, y, 16 * scale, 40 * scale);
|
||||
|
||||
ctx.fillStyle = colors.treeDark;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + parallax, y - 80 * scale);
|
||||
ctx.lineTo(x - 35 * scale + parallax, y);
|
||||
ctx.lineTo(x + 35 * scale + parallax, y);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = colors.treeLight;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + parallax, y - 100 * scale);
|
||||
ctx.lineTo(x - 25 * scale + parallax, y - 30 * scale);
|
||||
ctx.lineTo(x + 25 * scale + parallax, y - 30 * scale);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
};
|
||||
|
||||
const treePositions = [
|
||||
{ x: width * 0.1, y: height * 0.75, scale: 1.3 },
|
||||
{ x: width * 0.2, y: height * 0.78, scale: 1.0 },
|
||||
{ x: width * 0.35, y: height * 0.73, scale: 1.5 },
|
||||
{ x: width * 0.65, y: height * 0.76, scale: 1.2 },
|
||||
{ x: width * 0.8, y: height * 0.74, scale: 1.4 },
|
||||
{ x: width * 0.92, y: height * 0.77, scale: 1.1 },
|
||||
];
|
||||
|
||||
treePositions.forEach((t) => drawTree(t.x, t.y, t.scale));
|
||||
}
|
||||
|
||||
export function drawFlowers(dc: DrawContext): void {
|
||||
const { ctx, width, height, state } = dc;
|
||||
const parallax = state.mouseX * 1.0 + state.scrollY * 0.6;
|
||||
const time = Date.now() * 0.001;
|
||||
|
||||
const drawFlower = (x: number, y: number, color: string, size: number) => {
|
||||
const sway = Math.sin(time + x * 0.01) * 3;
|
||||
ctx.strokeStyle = '#2E7D32';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + parallax, y + 15);
|
||||
ctx.quadraticCurveTo(x + parallax + sway, y + 7, x + parallax, y);
|
||||
ctx.stroke();
|
||||
|
||||
ctx.fillStyle = color;
|
||||
for (let i = 0; i < 5; i++) {
|
||||
ctx.beginPath();
|
||||
const angle = (i * Math.PI * 2) / 5;
|
||||
ctx.ellipse(
|
||||
x + Math.cos(angle) * size * 0.5 + parallax,
|
||||
y + Math.sin(angle) * size * 0.5,
|
||||
size * 0.4, size * 0.25, angle, 0, Math.PI * 2
|
||||
);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
ctx.fillStyle = '#FFEB3B';
|
||||
ctx.beginPath();
|
||||
ctx.arc(x + parallax, y, size * 0.3, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
};
|
||||
|
||||
for (let i = 0; i < 25; i++) {
|
||||
const x = (i * width) / 25 + (i % 3) * 15;
|
||||
const y = height * 0.82 + Math.sin(i * 1.5) * 12;
|
||||
const color = FLOWER_COLORS[i % FLOWER_COLORS.length];
|
||||
drawFlower(x, y, color, 6 + (i % 4) * 2);
|
||||
}
|
||||
}
|
||||
|
||||
export function drawBirds(dc: DrawContext): void {
|
||||
const { ctx, width, height, state } = dc;
|
||||
ctx.strokeStyle = '#37474F';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.lineCap = 'round';
|
||||
|
||||
const parallax = state.mouseX * 0.15 - state.scrollY * 0.02;
|
||||
const time = Date.now() * 0.002;
|
||||
|
||||
const drawBird = (x: number, y: number, size: number, phase: number) => {
|
||||
const wingOffset = Math.sin(time + phase) * 5;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + parallax - 10 * size, y + wingOffset);
|
||||
ctx.quadraticCurveTo(x + parallax, y - 5 * size, x + parallax + 10 * size, y + wingOffset);
|
||||
ctx.stroke();
|
||||
};
|
||||
|
||||
drawBird(width * 0.3, height * 0.25, 0.8, 0);
|
||||
drawBird(width * 0.35, height * 0.28, 0.6, 1);
|
||||
drawBird(width * 0.45, height * 0.22, 0.7, 2);
|
||||
drawBird(width * 0.7, height * 0.18, 0.9, 0.5);
|
||||
drawBird(width * 0.75, height * 0.21, 0.5, 1.5);
|
||||
}
|
||||
|
||||
export function drawButterflies(dc: DrawContext): void {
|
||||
const { ctx, width, height, state } = dc;
|
||||
const time = Date.now() * 0.003;
|
||||
const parallax = state.mouseX * 0.3;
|
||||
|
||||
const drawButterfly = (x: number, y: number, color: string, phase: number) => {
|
||||
const wingFlap = Math.sin(time * 5 + phase) * 0.3 + 0.7;
|
||||
const floatY = Math.sin(time + phase) * 10;
|
||||
const floatX = Math.cos(time * 0.5 + phase) * 15;
|
||||
|
||||
ctx.save();
|
||||
ctx.translate(x + parallax + floatX, y + floatY);
|
||||
|
||||
ctx.fillStyle = color;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(-8 * wingFlap, 0, 8, 12, -0.3, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(8 * wingFlap, 0, 8, 12, 0.3, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = '#1a1a1a';
|
||||
ctx.fillRect(-1, -8, 2, 16);
|
||||
|
||||
ctx.restore();
|
||||
};
|
||||
|
||||
drawButterfly(width * 0.25, height * 0.6, '#E91E63', 0);
|
||||
drawButterfly(width * 0.55, height * 0.55, '#FFEB3B', 1.5);
|
||||
drawButterfly(width * 0.75, height * 0.62, '#9C27B0', 3);
|
||||
}
|
||||
|
||||
export function drawEcoScene(dc: DrawContext): void {
|
||||
drawEcoTrees(dc);
|
||||
drawFlowers(dc);
|
||||
drawBirds(dc);
|
||||
drawButterflies(dc);
|
||||
}
|
||||
168
frontend/src/lib/ts/parallax/scenes/forest.ts
Normal file
168
frontend/src/lib/ts/parallax/scenes/forest.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import type { DrawContext } from '../types';
|
||||
import { getSceneColors } from '../colors';
|
||||
|
||||
export function drawForestTrees(dc: DrawContext): void {
|
||||
const { ctx, width, height, state } = dc;
|
||||
const colors = getSceneColors(state);
|
||||
const parallax = state.mouseX * 0.8 + state.scrollY * 0.5;
|
||||
|
||||
const drawPineTree = (x: number, y: number, scale: number, dark: boolean) => {
|
||||
ctx.fillStyle = dark ? '#3E2723' : '#5D4037';
|
||||
ctx.fillRect(x - 6 * scale + parallax, y, 12 * scale, 35 * scale);
|
||||
|
||||
const foliageColor = dark ? colors.treeDark : colors.treeLight;
|
||||
ctx.fillStyle = foliageColor;
|
||||
|
||||
for (let layer = 0; layer < 4; layer++) {
|
||||
const layerY = y + 5 * scale - layer * 22 * scale;
|
||||
const layerWidth = (35 - layer * 5) * scale;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + parallax, layerY - 25 * scale);
|
||||
ctx.lineTo(x - layerWidth + parallax, layerY);
|
||||
ctx.lineTo(x + layerWidth + parallax, layerY);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
}
|
||||
};
|
||||
|
||||
const treePositions = [
|
||||
{ x: width * 0.05, y: height * 0.68, scale: 0.7, dark: true },
|
||||
{ x: width * 0.15, y: height * 0.66, scale: 0.8, dark: true },
|
||||
{ x: width * 0.28, y: height * 0.67, scale: 0.6, dark: true },
|
||||
{ x: width * 0.42, y: height * 0.65, scale: 0.75, dark: true },
|
||||
{ x: width * 0.58, y: height * 0.68, scale: 0.65, dark: true },
|
||||
{ x: width * 0.72, y: height * 0.66, scale: 0.7, dark: true },
|
||||
{ x: width * 0.88, y: height * 0.67, scale: 0.8, dark: true },
|
||||
{ x: width * 0.95, y: height * 0.68, scale: 0.6, dark: true },
|
||||
{ x: width * 0.08, y: height * 0.76, scale: 1.4, dark: false },
|
||||
{ x: width * 0.22, y: height * 0.78, scale: 1.2, dark: false },
|
||||
{ x: width * 0.38, y: height * 0.74, scale: 1.6, dark: false },
|
||||
{ x: width * 0.52, y: height * 0.77, scale: 1.3, dark: false },
|
||||
{ x: width * 0.68, y: height * 0.75, scale: 1.5, dark: false },
|
||||
{ x: width * 0.82, y: height * 0.78, scale: 1.1, dark: false },
|
||||
{ x: width * 0.94, y: height * 0.76, scale: 1.4, dark: false },
|
||||
];
|
||||
|
||||
treePositions.forEach((t) => drawPineTree(t.x, t.y, t.scale, t.dark));
|
||||
}
|
||||
|
||||
export function drawMushrooms(dc: DrawContext): void {
|
||||
const { ctx, width, height, state } = dc;
|
||||
const parallax = state.mouseX * 0.9 + state.scrollY * 0.6;
|
||||
|
||||
const drawMushroom = (x: number, y: number, scale: number, isRed: boolean) => {
|
||||
ctx.fillStyle = '#F5F5DC';
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(x + parallax, y + 8 * scale, 6 * scale, 10 * scale, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = isRed ? '#D32F2F' : '#8D6E63';
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(x + parallax, y - 2 * scale, 12 * scale, 8 * scale, 0, Math.PI, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
if (isRed) {
|
||||
ctx.fillStyle = '#FFFFFF';
|
||||
ctx.beginPath();
|
||||
ctx.arc(x + parallax - 4, y - 4 * scale, 2 * scale, 0, Math.PI * 2);
|
||||
ctx.arc(x + parallax + 5, y - 3 * scale, 1.5 * scale, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
};
|
||||
|
||||
drawMushroom(width * 0.15, height * 0.84, 1.0, true);
|
||||
drawMushroom(width * 0.35, height * 0.86, 0.8, false);
|
||||
drawMushroom(width * 0.52, height * 0.83, 1.2, true);
|
||||
drawMushroom(width * 0.78, height * 0.85, 0.9, false);
|
||||
drawMushroom(width * 0.88, height * 0.84, 1.1, true);
|
||||
}
|
||||
|
||||
export function drawDeer(dc: DrawContext): void {
|
||||
const { ctx, width, height, state } = dc;
|
||||
const time = Date.now() * 0.001;
|
||||
const parallax = state.mouseX * 0.5;
|
||||
|
||||
const x = width * 0.6 + parallax;
|
||||
const y = height * 0.72;
|
||||
const headBob = Math.sin(time * 2) * 3;
|
||||
|
||||
ctx.fillStyle = '#8D6E63';
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(x, y, 35, 25, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = '#6D4C41';
|
||||
ctx.fillRect(x - 20, y + 15, 8, 30);
|
||||
ctx.fillRect(x - 5, y + 18, 8, 28);
|
||||
ctx.fillRect(x + 10, y + 15, 8, 30);
|
||||
ctx.fillRect(x + 25, y + 18, 8, 28);
|
||||
|
||||
ctx.fillStyle = '#8D6E63';
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(x + 40, y - 15 + headBob, 12, 18, 0.3, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(x + 55, y - 25 + headBob, 10, 12, 0.2, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
ctx.strokeStyle = '#5D4037';
|
||||
ctx.lineWidth = 3;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + 50, y - 35 + headBob);
|
||||
ctx.lineTo(x + 45, y - 50 + headBob);
|
||||
ctx.lineTo(x + 40, y - 45 + headBob);
|
||||
ctx.moveTo(x + 45, y - 50 + headBob);
|
||||
ctx.lineTo(x + 50, y - 55 + headBob);
|
||||
ctx.stroke();
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + 60, y - 35 + headBob);
|
||||
ctx.lineTo(x + 65, y - 50 + headBob);
|
||||
ctx.lineTo(x + 70, y - 45 + headBob);
|
||||
ctx.moveTo(x + 65, y - 50 + headBob);
|
||||
ctx.lineTo(x + 60, y - 55 + headBob);
|
||||
ctx.stroke();
|
||||
|
||||
ctx.fillStyle = '#1a1a1a';
|
||||
ctx.beginPath();
|
||||
ctx.arc(x + 60, y - 25 + headBob, 3, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
export function drawLightRays(dc: DrawContext): void {
|
||||
const { ctx, width, height, state } = dc;
|
||||
const time = Date.now() * 0.0005;
|
||||
|
||||
ctx.save();
|
||||
ctx.globalAlpha = 0.15 + Math.sin(time) * 0.05;
|
||||
|
||||
const gradient = ctx.createLinearGradient(width * 0.7, 0, width * 0.5, height * 0.7);
|
||||
gradient.addColorStop(0, 'rgba(255, 255, 200, 0.8)');
|
||||
gradient.addColorStop(1, 'rgba(255, 255, 200, 0)');
|
||||
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(width * 0.75, 0);
|
||||
ctx.lineTo(width * 0.85, 0);
|
||||
ctx.lineTo(width * 0.55, height * 0.7);
|
||||
ctx.lineTo(width * 0.45, height * 0.7);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(width * 0.55, 0);
|
||||
ctx.lineTo(width * 0.62, 0);
|
||||
ctx.lineTo(width * 0.35, height * 0.65);
|
||||
ctx.lineTo(width * 0.28, height * 0.65);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
export function drawForestScene(dc: DrawContext): void {
|
||||
drawLightRays(dc);
|
||||
drawForestTrees(dc);
|
||||
drawMushrooms(dc);
|
||||
drawDeer(dc);
|
||||
}
|
||||
130
frontend/src/lib/ts/parallax/scenes/industrial.ts
Normal file
130
frontend/src/lib/ts/parallax/scenes/industrial.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import type { DrawContext } from '../types';
|
||||
|
||||
export function drawFactories(dc: DrawContext): void {
|
||||
const { ctx, width, height, state } = dc;
|
||||
const parallax = state.mouseX * 0.4 + state.scrollY * 0.3;
|
||||
|
||||
const drawFactory = (x: number, baseY: number, factoryWidth: number, factoryHeight: number) => {
|
||||
ctx.fillStyle = '#424242';
|
||||
ctx.fillRect(x + parallax, baseY - factoryHeight, factoryWidth, factoryHeight);
|
||||
|
||||
ctx.fillStyle = '#2E2E2E';
|
||||
ctx.fillRect(x + parallax, baseY - factoryHeight, factoryWidth * 0.1, factoryHeight);
|
||||
|
||||
ctx.fillStyle = 'rgba(255, 200, 100, 0.6)';
|
||||
const windowRows = 3;
|
||||
const windowCols = 4;
|
||||
const windowW = factoryWidth * 0.12;
|
||||
const windowH = factoryHeight * 0.08;
|
||||
const spacingX = (factoryWidth - windowCols * windowW) / (windowCols + 1);
|
||||
const spacingY = (factoryHeight * 0.6 - windowRows * windowH) / (windowRows + 1);
|
||||
|
||||
for (let row = 0; row < windowRows; row++) {
|
||||
for (let col = 0; col < windowCols; col++) {
|
||||
const wx = x + parallax + spacingX * (col + 1) + windowW * col;
|
||||
const wy = baseY - factoryHeight + spacingY * (row + 1) + windowH * row + factoryHeight * 0.3;
|
||||
ctx.fillRect(wx, wy, windowW, windowH);
|
||||
}
|
||||
}
|
||||
|
||||
const chimneyWidth = factoryWidth * 0.15;
|
||||
const chimneyHeight = factoryHeight * 0.6;
|
||||
ctx.fillStyle = '#263238';
|
||||
ctx.fillRect(x + parallax + factoryWidth * 0.7, baseY - factoryHeight - chimneyHeight, chimneyWidth, chimneyHeight);
|
||||
|
||||
ctx.fillStyle = '#D32F2F';
|
||||
ctx.fillRect(x + parallax + factoryWidth * 0.7, baseY - factoryHeight - chimneyHeight, chimneyWidth, 10);
|
||||
ctx.fillRect(x + parallax + factoryWidth * 0.7, baseY - factoryHeight - chimneyHeight * 0.5, chimneyWidth, 10);
|
||||
|
||||
drawSmoke(ctx, x + parallax + factoryWidth * 0.7 + chimneyWidth / 2, baseY - factoryHeight - chimneyHeight);
|
||||
};
|
||||
|
||||
drawFactory(width * 0.08, height * 0.8, width * 0.18, height * 0.28);
|
||||
drawFactory(width * 0.35, height * 0.8, width * 0.22, height * 0.38);
|
||||
drawFactory(width * 0.7, height * 0.8, width * 0.15, height * 0.3);
|
||||
}
|
||||
|
||||
function drawSmoke(ctx: CanvasRenderingContext2D, x: number, baseY: number): void {
|
||||
const time = Date.now() * 0.001;
|
||||
|
||||
ctx.fillStyle = 'rgba(100, 100, 100, 0.5)';
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const smokeY = baseY - i * 22 - Math.sin(time + i) * 8;
|
||||
const smokeRadius = 12 + i * 10 + Math.sin(time * 2 + i) * 4;
|
||||
const offsetX = Math.sin(time + i * 0.5) * 20;
|
||||
const alpha = 0.5 - i * 0.07;
|
||||
|
||||
ctx.globalAlpha = alpha;
|
||||
ctx.beginPath();
|
||||
ctx.arc(x + offsetX, smokeY, smokeRadius, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
|
||||
export function drawPowerLines(dc: DrawContext): void {
|
||||
const { ctx, width, height, state } = dc;
|
||||
const parallax = state.mouseX * 0.3;
|
||||
|
||||
ctx.fillStyle = '#37474F';
|
||||
const polePositions = [width * 0.25, width * 0.6, width * 0.95];
|
||||
|
||||
polePositions.forEach(px => {
|
||||
ctx.fillRect(px + parallax - 4, height * 0.5, 8, height * 0.35);
|
||||
ctx.fillRect(px + parallax - 25, height * 0.52, 50, 6);
|
||||
});
|
||||
|
||||
ctx.strokeStyle = '#1a1a1a';
|
||||
ctx.lineWidth = 2;
|
||||
|
||||
for (let wire = 0; wire < 3; wire++) {
|
||||
ctx.beginPath();
|
||||
const yOffset = wire * 8;
|
||||
ctx.moveTo(polePositions[0] + parallax - 20, height * 0.52 + yOffset);
|
||||
|
||||
const midX = (polePositions[0] + polePositions[1]) / 2 + parallax;
|
||||
ctx.quadraticCurveTo(midX, height * 0.58 + yOffset, polePositions[1] + parallax + 20, height * 0.52 + yOffset);
|
||||
ctx.stroke();
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(polePositions[1] + parallax - 20, height * 0.52 + yOffset);
|
||||
const midX2 = (polePositions[1] + polePositions[2]) / 2 + parallax;
|
||||
ctx.quadraticCurveTo(midX2, height * 0.58 + yOffset, polePositions[2] + parallax + 20, height * 0.52 + yOffset);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
export function drawDeadTrees(dc: DrawContext): void {
|
||||
const { ctx, width, height, state } = dc;
|
||||
const parallax = state.mouseX * 0.6;
|
||||
|
||||
const drawDeadTree = (x: number, y: number, scale: number) => {
|
||||
ctx.strokeStyle = '#4E342E';
|
||||
ctx.lineWidth = 8 * scale;
|
||||
ctx.lineCap = 'round';
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + parallax, y + 40 * scale);
|
||||
ctx.lineTo(x + parallax, y - 30 * scale);
|
||||
ctx.stroke();
|
||||
|
||||
ctx.lineWidth = 4 * scale;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + parallax, y - 10 * scale);
|
||||
ctx.lineTo(x + parallax - 25 * scale, y - 40 * scale);
|
||||
ctx.moveTo(x + parallax, y - 20 * scale);
|
||||
ctx.lineTo(x + parallax + 30 * scale, y - 45 * scale);
|
||||
ctx.moveTo(x + parallax, y - 30 * scale);
|
||||
ctx.lineTo(x + parallax - 15 * scale, y - 55 * scale);
|
||||
ctx.stroke();
|
||||
};
|
||||
|
||||
drawDeadTree(width * 0.02, height * 0.78, 1.0);
|
||||
drawDeadTree(width * 0.88, height * 0.76, 1.2);
|
||||
}
|
||||
|
||||
export function drawIndustrialScene(dc: DrawContext): void {
|
||||
drawPowerLines(dc);
|
||||
drawFactories(dc);
|
||||
drawDeadTrees(dc);
|
||||
}
|
||||
197
frontend/src/lib/ts/parallax/scenes/ocean.ts
Normal file
197
frontend/src/lib/ts/parallax/scenes/ocean.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
import type { DrawContext } from '../types';
|
||||
import { getSceneColors } from '../colors';
|
||||
|
||||
export function drawOceanWaves(dc: DrawContext): void {
|
||||
const { ctx, width, height, state } = dc;
|
||||
const colors = getSceneColors(state);
|
||||
const time = Date.now() * 0.001;
|
||||
|
||||
for (let layer = 0; layer < 3; layer++) {
|
||||
const alpha = 0.3 + layer * 0.25;
|
||||
const yBase = height * (0.55 + layer * 0.12);
|
||||
const waveHeight = 15 - layer * 3;
|
||||
const speed = 40 + layer * 20;
|
||||
|
||||
ctx.fillStyle = layer === 2 ? colors.water : `rgba(0, 150, 200, ${alpha})`;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-100, height);
|
||||
|
||||
for (let x = -100; x <= width + 100; x += 10) {
|
||||
const y = yBase + Math.sin((x + time * speed) * 0.02) * waveHeight;
|
||||
ctx.lineTo(x, y);
|
||||
}
|
||||
|
||||
ctx.lineTo(width + 100, height);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.6)';
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const x = (width / 8) * i + Math.sin(time + i * 2) * 30;
|
||||
const y = height * 0.58 + Math.sin(time * 1.5 + i) * 8;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(x, y, 20 + i * 3, 5, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
|
||||
export function drawBoats(dc: DrawContext): void {
|
||||
const { ctx, width, height, state } = dc;
|
||||
const time = Date.now() * 0.001;
|
||||
const parallax = state.mouseX * 0.3;
|
||||
|
||||
const drawSailboat = (x: number, baseY: number, scale: number, phase: number) => {
|
||||
const bob = Math.sin(time * 2 + phase) * 5;
|
||||
const y = baseY + bob;
|
||||
|
||||
ctx.fillStyle = '#5D4037';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + parallax - 30 * scale, y);
|
||||
ctx.lineTo(x + parallax + 30 * scale, y);
|
||||
ctx.lineTo(x + parallax + 20 * scale, y + 15 * scale);
|
||||
ctx.lineTo(x + parallax - 20 * scale, y + 15 * scale);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = '#8D6E63';
|
||||
ctx.fillRect(x + parallax - 2 * scale, y - 60 * scale, 4 * scale, 60 * scale);
|
||||
|
||||
ctx.fillStyle = '#FFFFFF';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + parallax, y - 55 * scale);
|
||||
ctx.lineTo(x + parallax + 25 * scale, y - 10 * scale);
|
||||
ctx.lineTo(x + parallax, y - 10 * scale);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = '#E91E63';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + parallax, y - 60 * scale);
|
||||
ctx.lineTo(x + parallax + 12 * scale, y - 55 * scale);
|
||||
ctx.lineTo(x + parallax, y - 50 * scale);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
};
|
||||
|
||||
drawSailboat(width * 0.2, height * 0.58, 1.0, 0);
|
||||
drawSailboat(width * 0.7, height * 0.62, 1.3, 1.5);
|
||||
drawSailboat(width * 0.9, height * 0.56, 0.8, 3);
|
||||
}
|
||||
|
||||
export function drawDolphins(dc: DrawContext): void {
|
||||
const { ctx, width, height } = dc;
|
||||
const time = Date.now() * 0.001;
|
||||
|
||||
const drawDolphin = (baseX: number, baseY: number, scale: number, phase: number) => {
|
||||
const cycleLength = 3;
|
||||
const cycleProgress = ((time + phase) % cycleLength) / cycleLength;
|
||||
|
||||
let jumpProgress = 0;
|
||||
if (cycleProgress > 0.2 && cycleProgress < 0.8) {
|
||||
jumpProgress = (cycleProgress - 0.2) / 0.6;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
const jumpHeight = 80 * scale * Math.sin(jumpProgress * Math.PI);
|
||||
const x = baseX + (jumpProgress - 0.5) * 150 * scale;
|
||||
const y = baseY - jumpHeight;
|
||||
|
||||
const rotation = Math.cos(jumpProgress * Math.PI) * -0.8;
|
||||
|
||||
ctx.save();
|
||||
ctx.translate(x, y);
|
||||
ctx.rotate(rotation);
|
||||
|
||||
ctx.fillStyle = '#546E7A';
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(0, 0, 30 * scale, 12 * scale, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-5 * scale, -10 * scale);
|
||||
ctx.lineTo(5 * scale, -25 * scale);
|
||||
ctx.lineTo(12 * scale, -8 * scale);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-28 * scale, 0);
|
||||
ctx.lineTo(-45 * scale, -12 * scale);
|
||||
ctx.lineTo(-35 * scale, 0);
|
||||
ctx.lineTo(-45 * scale, 12 * scale);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(35 * scale, 0, 12 * scale, 6 * scale, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = '#1a1a1a';
|
||||
ctx.beginPath();
|
||||
ctx.arc(28 * scale, -3 * scale, 2 * scale, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
ctx.restore();
|
||||
};
|
||||
|
||||
drawDolphin(width * 0.25, height * 0.75, 1.0, 0);
|
||||
drawDolphin(width * 0.5, height * 0.78, 0.8, 1);
|
||||
}
|
||||
|
||||
export function drawSeagulls(dc: DrawContext): void {
|
||||
const { ctx, width, height, state } = dc;
|
||||
const time = Date.now() * 0.003;
|
||||
const parallax = state.mouseX * 0.2;
|
||||
|
||||
const drawSeagull = (x: number, y: number, scale: number, phase: number) => {
|
||||
const wingFlap = Math.sin(time * 3 + phase) * 0.4;
|
||||
|
||||
ctx.save();
|
||||
ctx.translate(x + parallax, y);
|
||||
|
||||
ctx.fillStyle = '#FFFFFF';
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(0, 0, 10 * scale, 5 * scale, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
ctx.strokeStyle = '#FFFFFF';
|
||||
ctx.lineWidth = 3 * scale;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-12 * scale, 0);
|
||||
ctx.quadraticCurveTo(-18 * scale, -15 * scale * (1 + wingFlap), -25 * scale, -5 * scale);
|
||||
ctx.stroke();
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(12 * scale, 0);
|
||||
ctx.quadraticCurveTo(18 * scale, -15 * scale * (1 + wingFlap), 25 * scale, -5 * scale);
|
||||
ctx.stroke();
|
||||
|
||||
ctx.fillStyle = '#FFFFFF';
|
||||
ctx.beginPath();
|
||||
ctx.arc(12 * scale, -2 * scale, 4 * scale, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = '#FF9800';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(16 * scale, -2 * scale);
|
||||
ctx.lineTo(22 * scale, 0);
|
||||
ctx.lineTo(16 * scale, 2 * scale);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
ctx.restore();
|
||||
};
|
||||
|
||||
drawSeagull(width * 0.15, height * 0.2, 1.0, 0);
|
||||
drawSeagull(width * 0.35, height * 0.15, 0.8, 1);
|
||||
drawSeagull(width * 0.55, height * 0.22, 1.2, 2);
|
||||
drawSeagull(width * 0.8, height * 0.18, 0.9, 3);
|
||||
}
|
||||
|
||||
export function drawOceanScene(dc: DrawContext): void {
|
||||
drawSeagulls(dc);
|
||||
drawBoats(dc);
|
||||
drawDolphins(dc);
|
||||
}
|
||||
162
frontend/src/lib/ts/parallax/scenes/oilRig.ts
Normal file
162
frontend/src/lib/ts/parallax/scenes/oilRig.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import type { DrawContext } from '../types';
|
||||
|
||||
export function drawOilRigs(dc: DrawContext): void {
|
||||
const { ctx, width, height, state } = dc;
|
||||
const parallax = state.mouseX * 0.3;
|
||||
|
||||
const drawRig = (x: number, baseY: number, scale: number) => {
|
||||
const time = Date.now() * 0.005;
|
||||
|
||||
ctx.fillStyle = '#37474F';
|
||||
ctx.fillRect(x - 50 * scale + parallax, baseY, 10 * scale, 80 * scale);
|
||||
ctx.fillRect(x + 40 * scale + parallax, baseY, 10 * scale, 80 * scale);
|
||||
|
||||
ctx.strokeStyle = '#455A64';
|
||||
ctx.lineWidth = 4 * scale;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x - 45 * scale + parallax, baseY + 20 * scale);
|
||||
ctx.lineTo(x + 45 * scale + parallax, baseY + 60 * scale);
|
||||
ctx.moveTo(x + 45 * scale + parallax, baseY + 20 * scale);
|
||||
ctx.lineTo(x - 45 * scale + parallax, baseY + 60 * scale);
|
||||
ctx.stroke();
|
||||
|
||||
ctx.fillStyle = '#546E7A';
|
||||
ctx.fillRect(x - 70 * scale + parallax, baseY - 25 * scale, 140 * scale, 25 * scale);
|
||||
|
||||
ctx.fillStyle = '#455A64';
|
||||
ctx.fillRect(x - 40 * scale + parallax, baseY - 60 * scale, 40 * scale, 35 * scale);
|
||||
|
||||
ctx.fillStyle = 'rgba(255, 200, 100, 0.7)';
|
||||
ctx.fillRect(x - 35 * scale + parallax, baseY - 50 * scale, 12 * scale, 10 * scale);
|
||||
ctx.fillRect(x - 18 * scale + parallax, baseY - 50 * scale, 12 * scale, 10 * scale);
|
||||
|
||||
ctx.fillStyle = '#37474F';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + 20 * scale + parallax, baseY - 25 * scale);
|
||||
ctx.lineTo(x + 10 * scale + parallax, baseY - 120 * scale);
|
||||
ctx.lineTo(x + 50 * scale + parallax, baseY - 120 * scale);
|
||||
ctx.lineTo(x + 40 * scale + parallax, baseY - 25 * scale);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = '#FF5722';
|
||||
ctx.fillRect(x + 20 * scale + parallax, baseY - 100 * scale, 70 * scale, 6 * scale);
|
||||
|
||||
const flameX = x + 90 * scale + parallax;
|
||||
const flameY = baseY - 100 * scale;
|
||||
|
||||
ctx.fillStyle = '#263238';
|
||||
ctx.fillRect(x + 85 * scale + parallax, baseY - 25 * scale, 10 * scale, -75 * scale);
|
||||
|
||||
ctx.fillStyle = '#FF6F00';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(flameX - 5 * scale, flameY);
|
||||
ctx.quadraticCurveTo(flameX + Math.sin(time) * 8, flameY - 35 * scale, flameX + 5 * scale, flameY);
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = '#FFEB3B';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(flameX - 3 * scale, flameY);
|
||||
ctx.quadraticCurveTo(flameX + Math.sin(time * 1.5) * 5, flameY - 25 * scale, flameX + 3 * scale, flameY);
|
||||
ctx.fill();
|
||||
};
|
||||
|
||||
drawRig(width * 0.25, height * 0.6, 1.0);
|
||||
drawRig(width * 0.75, height * 0.55, 1.3);
|
||||
}
|
||||
|
||||
export function drawOilTanker(dc: DrawContext): void {
|
||||
const { ctx, width, height, state } = dc;
|
||||
const time = Date.now() * 0.001;
|
||||
const parallax = state.mouseX * 0.4;
|
||||
|
||||
const x = width * 0.5 + parallax;
|
||||
const y = height * 0.72 + Math.sin(time) * 3;
|
||||
|
||||
ctx.fillStyle = '#1C313A';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x - 100, y);
|
||||
ctx.lineTo(x + 100, y);
|
||||
ctx.lineTo(x + 80, y + 25);
|
||||
ctx.lineTo(x - 80, y + 25);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = '#B71C1C';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x - 90, y + 15);
|
||||
ctx.lineTo(x + 90, y + 15);
|
||||
ctx.lineTo(x + 80, y + 25);
|
||||
ctx.lineTo(x - 80, y + 25);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = '#37474F';
|
||||
ctx.fillRect(x - 95, y - 8, 190, 8);
|
||||
|
||||
ctx.fillStyle = '#455A64';
|
||||
for (let i = 0; i < 5; i++) {
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(x - 70 + i * 35, y - 15, 15, 20, 0, Math.PI, 0);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
ctx.fillStyle = '#263238';
|
||||
ctx.fillRect(x + 60, y - 35, 30, 30);
|
||||
ctx.fillStyle = 'rgba(255, 200, 100, 0.6)';
|
||||
ctx.fillRect(x + 65, y - 30, 8, 8);
|
||||
ctx.fillRect(x + 78, y - 30, 8, 8);
|
||||
|
||||
ctx.fillStyle = '#37474F';
|
||||
ctx.fillRect(x + 70, y - 50, 10, 20);
|
||||
|
||||
ctx.fillStyle = 'rgba(60, 60, 60, 0.4)';
|
||||
for (let i = 0; i < 3; i++) {
|
||||
ctx.beginPath();
|
||||
ctx.arc(x + 75 + Math.sin(time + i) * 8, y - 55 - i * 15, 8 + i * 5, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
|
||||
export function drawOilSlicks(dc: DrawContext): void {
|
||||
const { ctx, width, height } = dc;
|
||||
const time = Date.now() * 0.0005;
|
||||
|
||||
ctx.fillStyle = '#1C313A';
|
||||
ctx.fillRect(0, height * 0.75, width, height * 0.25);
|
||||
|
||||
const slicks = [
|
||||
{ x: width * 0.15, y: height * 0.82, rx: 80, ry: 25 },
|
||||
{ x: width * 0.45, y: height * 0.85, rx: 120, ry: 30 },
|
||||
{ x: width * 0.75, y: height * 0.8, rx: 90, ry: 28 },
|
||||
];
|
||||
|
||||
slicks.forEach((slick, i) => {
|
||||
ctx.fillStyle = 'rgba(20, 20, 20, 0.7)';
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(slick.x, slick.y, slick.rx, slick.ry, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
const gradient = ctx.createRadialGradient(
|
||||
slick.x - slick.rx * 0.3, slick.y,
|
||||
0,
|
||||
slick.x, slick.y,
|
||||
slick.rx
|
||||
);
|
||||
gradient.addColorStop(0, 'rgba(128, 0, 128, 0.3)');
|
||||
gradient.addColorStop(0.3, 'rgba(0, 100, 200, 0.2)');
|
||||
gradient.addColorStop(0.6, 'rgba(0, 200, 100, 0.15)');
|
||||
gradient.addColorStop(1, 'rgba(200, 200, 0, 0.1)');
|
||||
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(slick.x + Math.sin(time + i) * 5, slick.y, slick.rx * 0.9, slick.ry * 0.9, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
});
|
||||
}
|
||||
|
||||
export function drawOilRigScene(dc: DrawContext): void {
|
||||
drawOilSlicks(dc);
|
||||
drawOilRigs(dc);
|
||||
drawOilTanker(dc);
|
||||
}
|
||||
176
frontend/src/lib/ts/parallax/scenes/pollutedCity.ts
Normal file
176
frontend/src/lib/ts/parallax/scenes/pollutedCity.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import type { DrawContext } from '../types';
|
||||
|
||||
export function drawSmoggyBuildings(dc: DrawContext): void {
|
||||
const { ctx, width, height, state } = dc;
|
||||
const parallax = state.mouseX * 0.35;
|
||||
|
||||
const drawBuilding = (x: number, bWidth: number, bHeight: number) => {
|
||||
const baseY = height * 0.8;
|
||||
|
||||
ctx.fillStyle = '#37474F';
|
||||
ctx.fillRect(x + parallax, baseY - bHeight, bWidth, bHeight);
|
||||
|
||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.2)';
|
||||
ctx.fillRect(x + parallax, baseY - bHeight * 0.8, bWidth * 0.5, bHeight * 0.3);
|
||||
ctx.fillRect(x + parallax + bWidth * 0.6, baseY - bHeight * 0.5, bWidth * 0.3, bHeight * 0.2);
|
||||
|
||||
const windowCols = Math.floor(bWidth / 18);
|
||||
const windowRows = Math.floor(bHeight / 25);
|
||||
|
||||
for (let row = 0; row < windowRows; row++) {
|
||||
for (let col = 0; col < windowCols; col++) {
|
||||
const isBroken = Math.random() > 0.85;
|
||||
const isLit = Math.random() > 0.5;
|
||||
|
||||
if (!isBroken) {
|
||||
ctx.fillStyle = isLit ? 'rgba(255, 180, 100, 0.5)' : 'rgba(50, 50, 50, 0.8)';
|
||||
ctx.fillRect(
|
||||
x + parallax + col * 18 + 5,
|
||||
baseY - bHeight + row * 25 + 8,
|
||||
10, 12
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
drawBuilding(width * 0.02, 60, height * 0.35);
|
||||
drawBuilding(width * 0.1, 75, height * 0.48);
|
||||
drawBuilding(width * 0.22, 55, height * 0.38);
|
||||
drawBuilding(width * 0.32, 90, height * 0.55);
|
||||
drawBuilding(width * 0.48, 65, height * 0.42);
|
||||
drawBuilding(width * 0.58, 80, height * 0.52);
|
||||
drawBuilding(width * 0.72, 55, height * 0.36);
|
||||
drawBuilding(width * 0.82, 70, height * 0.45);
|
||||
}
|
||||
|
||||
export function drawSmokestacks(dc: DrawContext): void {
|
||||
const { ctx, width, height, state } = dc;
|
||||
const parallax = state.mouseX * 0.4;
|
||||
const time = Date.now() * 0.001;
|
||||
|
||||
const drawSmokestack = (x: number, stackHeight: number) => {
|
||||
const baseY = height * 0.8;
|
||||
|
||||
ctx.fillStyle = '#263238';
|
||||
ctx.fillRect(x + parallax - 12, baseY - stackHeight, 24, stackHeight);
|
||||
|
||||
ctx.fillStyle = '#B71C1C';
|
||||
ctx.fillRect(x + parallax - 12, baseY - stackHeight, 24, 15);
|
||||
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const smokeY = baseY - stackHeight - i * 20 - Math.sin(time + i * 0.5) * 10;
|
||||
const smokeRadius = 15 + i * 12;
|
||||
const offsetX = Math.sin(time * 0.8 + i * 0.3) * 25 * (i / 4);
|
||||
const alpha = 0.6 - i * 0.06;
|
||||
|
||||
ctx.fillStyle = `rgba(60, 60, 60, ${alpha})`;
|
||||
ctx.beginPath();
|
||||
ctx.arc(x + parallax + offsetX, smokeY, smokeRadius, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
};
|
||||
|
||||
drawSmokestack(width * 0.15, height * 0.3);
|
||||
drawSmokestack(width * 0.55, height * 0.35);
|
||||
drawSmokestack(width * 0.85, height * 0.28);
|
||||
}
|
||||
|
||||
export function drawSmog(dc: DrawContext): void {
|
||||
const { ctx, width, height } = dc;
|
||||
const time = Date.now() * 0.0003;
|
||||
|
||||
for (let layer = 0; layer < 3; layer++) {
|
||||
const gradient = ctx.createLinearGradient(0, height * 0.3, 0, height * 0.7);
|
||||
const alpha = 0.15 + layer * 0.08;
|
||||
gradient.addColorStop(0, `rgba(80, 80, 80, 0)`);
|
||||
gradient.addColorStop(0.5, `rgba(100, 90, 80, ${alpha})`);
|
||||
gradient.addColorStop(1, `rgba(80, 80, 80, 0)`);
|
||||
|
||||
ctx.fillStyle = gradient;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-100, height * 0.6);
|
||||
for (let x = -100; x <= width + 100; x += 50) {
|
||||
const y = height * 0.45 + Math.sin((x + time * 100 + layer * 500) * 0.005) * 40;
|
||||
ctx.lineTo(x, y);
|
||||
}
|
||||
ctx.lineTo(width + 100, height * 0.7);
|
||||
ctx.lineTo(-100, height * 0.7);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
|
||||
export function drawTrafficJam(dc: DrawContext): void {
|
||||
const { ctx, width, height } = dc;
|
||||
|
||||
ctx.fillStyle = '#2E2E2E';
|
||||
ctx.fillRect(0, height * 0.82, width, height * 0.08);
|
||||
|
||||
ctx.strokeStyle = 'rgba(200, 180, 100, 0.4)';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.setLineDash([20, 30]);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, height * 0.86);
|
||||
ctx.lineTo(width, height * 0.86);
|
||||
ctx.stroke();
|
||||
ctx.setLineDash([]);
|
||||
|
||||
const drawCar = (x: number, y: number, color: string) => {
|
||||
ctx.fillStyle = color;
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(x, y, 35, 14, 2);
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = 'rgba(100, 100, 100, 0.5)';
|
||||
ctx.fillRect(x + 8, y - 7, 19, 8);
|
||||
|
||||
ctx.fillStyle = '#1a1a1a';
|
||||
ctx.beginPath();
|
||||
ctx.arc(x + 8, y + 14, 4, 0, Math.PI * 2);
|
||||
ctx.arc(x + 27, y + 14, 4, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
};
|
||||
|
||||
const carColors = ['#616161', '#424242', '#757575', '#546E7A', '#455A64'];
|
||||
for (let i = 0; i < 15; i++) {
|
||||
const lane = i % 2;
|
||||
const x = i * 55 + 20;
|
||||
const y = height * 0.83 + lane * 35;
|
||||
drawCar(x, y, carColors[i % carColors.length]);
|
||||
}
|
||||
|
||||
ctx.fillStyle = 'rgba(255, 0, 0, 0.4)';
|
||||
for (let i = 0; i < 15; i++) {
|
||||
const x = i * 55 + 48;
|
||||
const y = height * 0.835 + (i % 2) * 35 + 7;
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, 3, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
|
||||
export function drawDebris(dc: DrawContext): void {
|
||||
const { ctx, width, height } = dc;
|
||||
|
||||
ctx.fillStyle = '#5D4037';
|
||||
|
||||
for (let i = 0; i < 12; i++) {
|
||||
const x = Math.random() * width;
|
||||
const y = height * 0.81 + Math.random() * 8;
|
||||
const size = 3 + Math.random() * 5;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.rect(x, y, size, size * 0.7);
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
|
||||
export function drawPollutedCityScene(dc: DrawContext): void {
|
||||
drawSmog(dc);
|
||||
drawSmoggyBuildings(dc);
|
||||
drawSmokestacks(dc);
|
||||
drawDebris(dc);
|
||||
drawTrafficJam(dc);
|
||||
}
|
||||
44
frontend/src/lib/ts/parallax/types.ts
Normal file
44
frontend/src/lib/ts/parallax/types.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
export interface ParallaxState {
|
||||
scrollY: number;
|
||||
innerHeight: number;
|
||||
mouseX: number;
|
||||
mouseY: number;
|
||||
progress: number;
|
||||
sceneType: SceneType;
|
||||
blendToScene?: SceneType;
|
||||
blendProgress?: number;
|
||||
}
|
||||
|
||||
export type SceneType =
|
||||
| 'eco'
|
||||
| 'industrial'
|
||||
| 'forest'
|
||||
| 'deforestation'
|
||||
| 'ocean'
|
||||
| 'oilRig'
|
||||
| 'city'
|
||||
| 'pollutedCity'
|
||||
| 'transition';
|
||||
|
||||
export interface DrawContext {
|
||||
ctx: CanvasRenderingContext2D;
|
||||
width: number;
|
||||
height: number;
|
||||
state: ParallaxState;
|
||||
}
|
||||
|
||||
export interface SceneColors {
|
||||
skyTop: string;
|
||||
skyBottom: string;
|
||||
sun: string;
|
||||
sunGlow: string;
|
||||
mountainFar: string;
|
||||
mountainMid: string;
|
||||
hillFront: string;
|
||||
treeDark: string;
|
||||
treeLight: string;
|
||||
ground: string;
|
||||
water: string;
|
||||
cloud: string;
|
||||
accent: string;
|
||||
}
|
||||
@@ -1,13 +1,16 @@
|
||||
<script lang="ts">
|
||||
import "../app.css";
|
||||
import { onMount } from "svelte";
|
||||
import { isTauri } from "@tauri-apps/api/core";
|
||||
import CustomTabBar from "$lib/components/CustomTabBar.svelte";
|
||||
import CameraScreen from "$lib/components/CameraScreen.svelte";
|
||||
import { page } from "$app/stores";
|
||||
import { goto } from "$app/navigation";
|
||||
import Icon from "@iconify/svelte";
|
||||
|
||||
let { children } = $props();
|
||||
let isApp = $state(false);
|
||||
let isMobile = $state(false);
|
||||
let isCameraActive = $state(false);
|
||||
let recentItems = $state([
|
||||
{
|
||||
@@ -26,8 +29,20 @@
|
||||
},
|
||||
]);
|
||||
|
||||
function handleScanComplete(item: any) {
|
||||
recentItems = [item, ...recentItems];
|
||||
isCameraActive = false;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
const checkMobile = () => {
|
||||
isMobile = window.innerWidth < 768;
|
||||
};
|
||||
isApp = isTauri();
|
||||
|
||||
checkMobile();
|
||||
window.addEventListener("resize", checkMobile);
|
||||
|
||||
if (isApp) {
|
||||
document.body.classList.add("platform-native");
|
||||
} else {
|
||||
@@ -40,6 +55,7 @@
|
||||
window.addEventListener("scan", handleScan);
|
||||
return () => {
|
||||
window.removeEventListener("scan", handleScan);
|
||||
window.removeEventListener("resize", checkMobile);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -55,25 +71,37 @@
|
||||
];
|
||||
</script>
|
||||
|
||||
{#if isApp}
|
||||
<main class="app-container">
|
||||
{@render children()}
|
||||
</main>
|
||||
<CustomTabBar currentRoute={$page.route.id || "/"} />
|
||||
<svelte:head>
|
||||
<title>Ethix - Truth in every scan</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Scan products to reveal their true environmental impact. Join the community of eco-conscious shoppers making a difference."
|
||||
/>
|
||||
</svelte:head>
|
||||
|
||||
{#if isMobile}
|
||||
<div class="mobile-container">
|
||||
<main class="app-container">
|
||||
<div class="content-wrapper">
|
||||
{@render children()}
|
||||
</div>
|
||||
</main>
|
||||
<CustomTabBar currentRoute={$page.route.id || "/"} />
|
||||
</div>
|
||||
{:else}
|
||||
<nav class="desktop-nav">
|
||||
<div class="nav-container">
|
||||
<a href="/" class="nav-brand">
|
||||
<div class="brand-icon">
|
||||
<a href="/" class="logo-container">
|
||||
<div class="logo-icon">
|
||||
<img
|
||||
src="/ethix-logo.png"
|
||||
alt="Ethix Logo"
|
||||
class="brand-logo-img"
|
||||
class="logo-img"
|
||||
/>
|
||||
</div>
|
||||
<div class="brand-content">
|
||||
<h1 class="brand-title">Ethix</h1>
|
||||
<p class="brand-tagline">Truth in every scan</p>
|
||||
<div class="logo-text-wrapper">
|
||||
<span class="logo-text">Ethix</span>
|
||||
<span class="logo-subtitle">Truth in every scan</span>
|
||||
</div>
|
||||
</a>
|
||||
<div class="nav-links">
|
||||
@@ -83,51 +111,16 @@
|
||||
class="nav-link"
|
||||
class:active={$page.route.id === link.route}
|
||||
>
|
||||
<iconify-icon icon={link.icon} width="20"
|
||||
></iconify-icon>
|
||||
<Icon icon={link.icon} width="20" />
|
||||
<span>{link.name}</span>
|
||||
</a>
|
||||
{/each}
|
||||
<a href="/catalogue" class="catalogue-button">
|
||||
<iconify-icon icon="ri:search-line" width="20"
|
||||
></iconify-icon>
|
||||
<Icon icon="ri:search-line" width="20" />
|
||||
<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">
|
||||
@@ -137,6 +130,13 @@
|
||||
</main>
|
||||
{/if}
|
||||
|
||||
{#if isCameraActive}
|
||||
<CameraScreen
|
||||
onClose={() => (isCameraActive = false)}
|
||||
onScanComplete={handleScanComplete}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
:global(body) {
|
||||
margin: 0;
|
||||
@@ -146,15 +146,16 @@
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
sans-serif;
|
||||
background-color: #000000;
|
||||
background-color: #0c0c0c;
|
||||
color: white;
|
||||
overflow: hidden;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.web-container {
|
||||
min-height: 80vh;
|
||||
max-width: 1200px;
|
||||
padding: 20px;
|
||||
.mobile-container {
|
||||
height: 100vh;
|
||||
overflow-y: auto;
|
||||
background-color: #051f18;
|
||||
}
|
||||
|
||||
.app-container {
|
||||
@@ -208,51 +209,47 @@
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.nav-brand {
|
||||
.logo-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
text-decoration: none;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.brand-icon {
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
padding: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
.logo-container:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.brand-logo-img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.brand-content {
|
||||
.logo-text-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.brand-title {
|
||||
.logo-text {
|
||||
font-size: 24px;
|
||||
font-weight: 900;
|
||||
color: #1a1a1a;
|
||||
margin: 0;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
.brand-tagline {
|
||||
font-size: 12px;
|
||||
color: #555;
|
||||
margin: 4px 0 0 0;
|
||||
.logo-subtitle {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #4b5563;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -303,43 +300,9 @@
|
||||
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);
|
||||
}
|
||||
.logo-img {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
object-fit: contain;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,895 +1,39 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { fly, fade } from "svelte/transition";
|
||||
import ParallaxLandscape from "$lib/components/ParallaxLandscape.svelte";
|
||||
import CloudSection from "$lib/components/CloudSection.svelte";
|
||||
|
||||
const news = [
|
||||
{
|
||||
id: 1,
|
||||
title: "Ocean Cleanup Hits 500 Tons",
|
||||
desc: "Major milestone reached in the Pacific.",
|
||||
date: "2h ago",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Plastic Ban Starts Today",
|
||||
desc: "Big cities are saying no to single-use.",
|
||||
date: "5h ago",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "Ethix Launches Globally",
|
||||
desc: "Now available worldwide.",
|
||||
date: "1d ago",
|
||||
},
|
||||
];
|
||||
|
||||
const scanHistory = [
|
||||
{
|
||||
name: "Plastic Water Bottle",
|
||||
date: "Today, 10:45 AM",
|
||||
severity: "High",
|
||||
},
|
||||
{ name: "Organic Banana", date: "Yesterday, 3:20 PM", severity: "Low" },
|
||||
{ name: "Aluminum Soda Can", date: "Mon, 12:15 PM", severity: "Low" },
|
||||
{ name: "Takeout Container", date: "Sun, 8:30 PM", severity: "Medium" },
|
||||
{ name: "Shampoo Bottle", date: "Sat, 11:00 AM", severity: "High" },
|
||||
];
|
||||
|
||||
const features = [
|
||||
{
|
||||
title: "Scan It",
|
||||
desc: "Point your camera at any product.",
|
||||
customSvg:
|
||||
"M13 9h5l-5-5zM6 22q-.825 0-1.412-.587T4 20v-3h16v3q0 .825-.587 1.413T18 22zm-5-7v-2h22v2zm3-4V4q0-.825.588-1.412T6 2h8l6 6v3z",
|
||||
},
|
||||
{
|
||||
title: "Get Real Info",
|
||||
desc: "AI breaks down the real impact.",
|
||||
customSvg:
|
||||
"M11 17h2v-6h-2zm1-8q.425 0 .713-.288T13 8t-.288-.712T12 7t-.712.288T11 8t.288.713T12 9m0 13q-2.075 0-3.9-.788t-3.175-2.137T2.788 15.9T2 12t.788-3.9t2.137-3.175T8.1 2.788T12 2t3.9.788t3.175 2.137T21.213 8.1T22 12t-.788 3.9t-2.137 3.175t-3.175 2.138T12 22m0-2q3.35 0 5.675-2.325T20 12t-2.325-5.675T12 4T6.325 6.325T4 12t2.325 5.675T12 20m0-8",
|
||||
},
|
||||
{
|
||||
title: "Find Better",
|
||||
desc: "See eco-friendly swaps instantly.",
|
||||
customSvg:
|
||||
"M7 21v-2h4v-3.1q-1.225-.275-2.187-1.037T7.4 12.95q-1.875-.225-3.137-1.637T3 8V5h4V3h10v2h4v3q0 1.9-1.263 3.313T16.6 12.95q-.45 1.15-1.412 1.913T13 15.9V19h4v2zm0-10.2V7H5v1q0 .95.55 1.713T7 10.8m5 3.2q1.25 0 2.125-.875T15 11V5H9v6q0 1.25.875 2.125T12 14m5-3.2q.9-.325 1.45-1.088T19 8V7h-2zm-5-1.3",
|
||||
},
|
||||
{
|
||||
title: "Call It Out",
|
||||
desc: "Report misleading green claims.",
|
||||
customSvg:
|
||||
"M3 22V2h13v4.675L10.675 12H7v8h3.675l2 2zM9.5 7q.425 0 .713-.288T10.5 6t-.288-.712T9.5 5t-.712.288T8.5 6t.288.713T9.5 7M15 21.5L11.5 18H9v-4h2.5l3.5-3.5zm2 1.425v-2q1.75-.375 2.875-1.75T21 16t-1.125-3.175T17 11.1v-2q2.6.35 4.3 2.313T23 16t-1.7 4.588t-4.3 2.337m0-4.125v-5.625q.875.3 1.438 1.075T19 16t-.562 1.738T17 18.8",
|
||||
},
|
||||
];
|
||||
|
||||
const stats = [
|
||||
{ value: "50K+", label: "Scans" },
|
||||
{ value: "12K", label: "Users" },
|
||||
{ value: "98%", label: "Accuracy" },
|
||||
{ value: "24/7", label: "Support" },
|
||||
];
|
||||
|
||||
let scrollY = $state(0);
|
||||
|
||||
// Score Rotation Logic with 2D images
|
||||
let scoreIndex = $state(0);
|
||||
const scores = [
|
||||
{
|
||||
label: "Fiji Water",
|
||||
score: "94/100",
|
||||
color: "#1ed760",
|
||||
image: "/water-bottle.png",
|
||||
blendMode: "multiply",
|
||||
scale: 0.7,
|
||||
},
|
||||
{
|
||||
label: "Plastic Bag",
|
||||
score: "12/100",
|
||||
color: "#e91429",
|
||||
image: "/plastic-bag.png",
|
||||
blendMode: "multiply",
|
||||
scale: 0.75,
|
||||
},
|
||||
{
|
||||
label: "Starbucks",
|
||||
score: "65/100",
|
||||
color: "#f59b23",
|
||||
image: "/coffee-cup.png",
|
||||
blendMode: "multiply",
|
||||
scale: 1,
|
||||
},
|
||||
];
|
||||
|
||||
onMount(() => {
|
||||
const handleScroll = () => {
|
||||
scrollY = window.scrollY;
|
||||
};
|
||||
|
||||
window.addEventListener("scroll", handleScroll);
|
||||
|
||||
const interval = setInterval(() => {
|
||||
scoreIndex = (scoreIndex + 1) % scores.length;
|
||||
}, 4000); // 4 seconds per item
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("scroll", handleScroll);
|
||||
clearInterval(interval);
|
||||
};
|
||||
});
|
||||
import WebHomePage from "$lib/components/WebHomePage.svelte";
|
||||
import MobileHomePage from "$lib/components/MobileHomePage.svelte";
|
||||
</script>
|
||||
|
||||
<!-- Desktop Web View -->
|
||||
<div class="web-page">
|
||||
<!-- Cartoon Parallax Landscape -->
|
||||
<ParallaxLandscape />
|
||||
<svelte:head>
|
||||
<title>Ethix - Home</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Ethix helps you make sustainable choices. Scan products, track your carbon footprint, and find eco-friendly alternatives."
|
||||
/>
|
||||
</svelte:head>
|
||||
|
||||
<!-- Hero Section -->
|
||||
<section class="hero">
|
||||
<div
|
||||
class="hero-content"
|
||||
style="transform: translateY({scrollY * 0.15}px)"
|
||||
>
|
||||
<div class="hero-badge">
|
||||
<iconify-icon icon="ri:eye-line" width="16"></iconify-icon>
|
||||
<span>See the real impact</span>
|
||||
</div>
|
||||
<h1 class="hero-title">
|
||||
Know What <br /> You Buy.
|
||||
</h1>
|
||||
<p class="hero-desc">
|
||||
Scan a product. See if it's actually good for the planet. Find
|
||||
better alternatives if it's not. Simple as that.
|
||||
</p>
|
||||
<div class="hero-actions">
|
||||
<a
|
||||
href="/catalogue"
|
||||
class="cta-primary"
|
||||
style="text-decoration: none;"
|
||||
>
|
||||
<iconify-icon icon="ri:store-2-fill" width="20"
|
||||
></iconify-icon>
|
||||
<span>Browse Database</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2D Product Carousel -->
|
||||
<div
|
||||
class="hero-visual"
|
||||
style="transform: translateY({scrollY * -0.2}px)"
|
||||
>
|
||||
<div class="visual-container">
|
||||
<div class="hero-image">
|
||||
{#key scoreIndex}
|
||||
<div
|
||||
class="product-image-wrapper"
|
||||
in:fly={{ x: 100, duration: 500, opacity: 0 }}
|
||||
out:fly={{ x: -100, duration: 500, opacity: 0 }}
|
||||
>
|
||||
<img
|
||||
src={scores[scoreIndex].image}
|
||||
alt={scores[scoreIndex].label}
|
||||
class="product-image"
|
||||
style="mix-blend-mode: {scores[scoreIndex]
|
||||
.blendMode}; transform: scale({scores[
|
||||
scoreIndex
|
||||
].scale});"
|
||||
/>
|
||||
</div>
|
||||
{/key}
|
||||
</div>
|
||||
<div class="orbit orbit-1"></div>
|
||||
<div class="orbit orbit-2"></div>
|
||||
|
||||
<!-- Floating Status Card (Simple Fade) -->
|
||||
<div class="floating-card">
|
||||
{#key scoreIndex}
|
||||
<div class="score-content" in:fade={{ duration: 300 }}>
|
||||
<iconify-icon
|
||||
icon="ri:checkbox-circle-fill"
|
||||
width="24"
|
||||
style="color: {scores[scoreIndex].color};"
|
||||
></iconify-icon>
|
||||
<div>
|
||||
<div class="card-label">
|
||||
{scores[scoreIndex].label}
|
||||
</div>
|
||||
<div class="card-value">
|
||||
{scores[scoreIndex].score}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/key}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Journey Spacer -->
|
||||
<div class="spacer-section" style="height: 30vh;"></div>
|
||||
|
||||
<!-- Stats Section with Cartoony Cloud -->
|
||||
<div style="transform: translateY({scrollY * 0.05}px)">
|
||||
<CloudSection>
|
||||
<div class="stats-content">
|
||||
{#each stats as stat}
|
||||
<div class="stat-item">
|
||||
<div class="stat-value">{stat.value}</div>
|
||||
<div class="stat-label">{stat.label}</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</CloudSection>
|
||||
</div>
|
||||
|
||||
<!-- Features Section with Cartoony Cloud -->
|
||||
<CloudSection>
|
||||
<div class="section-header">
|
||||
<h2 class="section-title cloud-title">How It Works</h2>
|
||||
<p class="section-desc cloud-desc">
|
||||
Tools to help you shop smarter.
|
||||
</p>
|
||||
</div>
|
||||
<div class="features-grid">
|
||||
{#each features as feature}
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="32"
|
||||
height="32"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path fill="currentColor" d={feature.customSvg} />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="feature-title">{feature.title}</h3>
|
||||
<p class="feature-desc">{feature.desc}</p>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</CloudSection>
|
||||
|
||||
<!-- News Section -->
|
||||
<CloudSection>
|
||||
<div class="section-header">
|
||||
<h2 class="section-title cloud-title">Latest News</h2>
|
||||
<p class="section-desc cloud-desc">
|
||||
Updates from the world of sustainability.
|
||||
</p>
|
||||
</div>
|
||||
<div class="news-grid">
|
||||
{#each news as item (item.id)}
|
||||
<article class="news-card">
|
||||
<div class="news-image">
|
||||
<iconify-icon icon="ri:article-line" width="48"
|
||||
></iconify-icon>
|
||||
</div>
|
||||
<div class="news-meta">{item.date}</div>
|
||||
<h3 class="news-title">{item.title}</h3>
|
||||
<p class="news-desc">{item.desc}</p>
|
||||
<a href="#" class="news-link">
|
||||
Read
|
||||
<iconify-icon icon="ri:arrow-right-line" width="16"
|
||||
></iconify-icon>
|
||||
</a>
|
||||
</article>
|
||||
{/each}
|
||||
</div>
|
||||
</CloudSection>
|
||||
<div class="web-view">
|
||||
<WebHomePage />
|
||||
</div>
|
||||
|
||||
<!-- Mobile View (Scan History) -->
|
||||
<div class="mobile-page">
|
||||
<div class="safe-area">
|
||||
<div class="header">
|
||||
<h1 class="page-title">Scan History</h1>
|
||||
<p class="subtitle">Your recent findings</p>
|
||||
</div>
|
||||
|
||||
<div class="scroll-content">
|
||||
{#each scanHistory as item}
|
||||
<div class="history-item">
|
||||
<div class="history-icon">
|
||||
<iconify-icon icon="ri:barcode-box-line" width="24"
|
||||
></iconify-icon>
|
||||
</div>
|
||||
<div class="history-details">
|
||||
<h3 class="history-title">{item.name}</h3>
|
||||
<p class="history-time">{item.date}</p>
|
||||
</div>
|
||||
<div class="severity-badge {item.severity.toLowerCase()}">
|
||||
{item.severity}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mobile-view">
|
||||
<MobileHomePage />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Hide mobile view on desktop, hide web view on mobile */
|
||||
.web-page {
|
||||
.web-view {
|
||||
display: none;
|
||||
}
|
||||
.mobile-page {
|
||||
|
||||
.mobile-view {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.web-page {
|
||||
.web-view {
|
||||
display: block;
|
||||
}
|
||||
.mobile-page {
|
||||
.mobile-view {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== WEB PAGE DESIGN ===== */
|
||||
.web-page {
|
||||
width: 100%;
|
||||
overflow-x: hidden;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* Common Section Container */
|
||||
.section-container {
|
||||
background: #121212;
|
||||
padding: 40px;
|
||||
border-radius: 32px; /* ROUNDED STYLING */
|
||||
border: none;
|
||||
box-shadow: 0 8px 40px rgba(0, 0, 0, 0.5); /* Better shadow depth */
|
||||
}
|
||||
|
||||
/* Hero Section */
|
||||
.hero {
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 80px;
|
||||
align-items: center;
|
||||
padding: 120px 60px;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
background: rgba(0, 0, 0, 0.4); /* Highly transparent black square */
|
||||
backdrop-filter: blur(8px); /* Subtle blur for readability */
|
||||
padding: 40px;
|
||||
border-radius: 32px; /* Rounded corners */
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
|
||||
max-width: 650px; /* Constrain width slightly */
|
||||
}
|
||||
|
||||
.hero-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
color: #22c55e;
|
||||
padding: 8px 16px;
|
||||
border-radius: 50px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 24px;
|
||||
border: 1px solid rgba(34, 197, 94, 0.2);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 80px;
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
color: white;
|
||||
margin: 0 0 32px 0;
|
||||
letter-spacing: -3px;
|
||||
/* Gradient Text for Aesthetics */
|
||||
background: linear-gradient(135deg, #ffffff 0%, #b3b3b3 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.hero-desc {
|
||||
font-size: 20px;
|
||||
line-height: 1.6;
|
||||
color: #b3b3b3;
|
||||
margin: 0 0 40px 0;
|
||||
max-width: 540px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.cta-primary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 20px 48px;
|
||||
border-radius: 500px;
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); /* Bouncy transition */
|
||||
border: none;
|
||||
background: #22c55e;
|
||||
color: #000000;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
box-shadow: 0 10px 40px rgba(34, 197, 94, 0.3);
|
||||
}
|
||||
|
||||
.cta-primary:hover {
|
||||
transform: scale(1.05) translateY(-2px);
|
||||
background-color: #16a34a;
|
||||
box-shadow: 0 20px 60px rgba(34, 197, 94, 0.5);
|
||||
}
|
||||
|
||||
/* Visual & Earth */
|
||||
.hero-visual {
|
||||
position: relative;
|
||||
height: 500px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.visual-container {
|
||||
position: relative;
|
||||
width: 450px;
|
||||
height: 450px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: radial-gradient(
|
||||
circle,
|
||||
rgba(255, 255, 255, 0.05) 0%,
|
||||
rgba(0, 0, 0, 0) 60%
|
||||
);
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.hero-image {
|
||||
width: 380px;
|
||||
height: 380px;
|
||||
background: transparent;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #22c55e;
|
||||
/* No animation here, wrapper animates */
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.product-icon-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.product-image-wrapper {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.product-image {
|
||||
width: 320px;
|
||||
height: 320px;
|
||||
object-fit: contain;
|
||||
filter: drop-shadow(0 10px 40px rgba(34, 197, 94, 0.3));
|
||||
}
|
||||
|
||||
.orbit {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 50%;
|
||||
z-index: 1;
|
||||
animation: spin 20s linear infinite;
|
||||
}
|
||||
|
||||
.orbit-1 {
|
||||
width: 350px;
|
||||
height: 350px;
|
||||
}
|
||||
.orbit-2 {
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
opacity: 0.3;
|
||||
animation-duration: 40s;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: translate(-50%, -50%) rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: translate(-50%, -50%) rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Floating Cards */
|
||||
.floating-card {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 60px;
|
||||
background: rgba(0, 0, 0, 0.6); /* Transparent background */
|
||||
backdrop-filter: blur(8px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
padding: 20px 24px;
|
||||
border-radius: 24px;
|
||||
min-width: 260px; /* Fixed width to prevent jumping */
|
||||
height: 80px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start; /* items align start */
|
||||
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5);
|
||||
z-index: 10;
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-12px);
|
||||
}
|
||||
}
|
||||
|
||||
.score-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@keyframes floatCards {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0px) rotate(0deg);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-15px) rotate(1deg);
|
||||
}
|
||||
}
|
||||
|
||||
.card-label {
|
||||
font-size: 11px;
|
||||
color: #b3b3b3;
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
.card-value {
|
||||
font-size: 24px;
|
||||
color: white;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.stats-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 40px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 64px;
|
||||
font-weight: 900;
|
||||
color: #166534;
|
||||
margin-bottom: 8px;
|
||||
letter-spacing: -2px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 16px;
|
||||
color: #111827; /* Darker black for readability on grey */
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
/* Features Cloud Section */
|
||||
.features-cloud {
|
||||
max-width: 1400px;
|
||||
}
|
||||
|
||||
.cloud-features-inner {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.cloud-title {
|
||||
color: #000000 !important;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
color: #000000 !important;
|
||||
}
|
||||
|
||||
.cloud-desc {
|
||||
color: #555555;
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 24px;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
background: #a1a1aa; /* Darker than container #d4d4d8 */
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
padding: 40px 32px;
|
||||
border-radius: 32px;
|
||||
transition: all 0.3s ease;
|
||||
text-align: center;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.feature-card:hover {
|
||||
transform: translateY(-8px);
|
||||
background: #b0b0b9;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2);
|
||||
border-color: #166534;
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
color: #15803d;
|
||||
margin-bottom: 24px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
background: rgba(21, 128, 61, 0.15);
|
||||
border-radius: 50%; /* Circle icons */
|
||||
}
|
||||
|
||||
.feature-title {
|
||||
color: #111827;
|
||||
font-size: 22px;
|
||||
margin-bottom: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.feature-desc {
|
||||
color: #4b5563;
|
||||
line-height: 1.6;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* News Section */
|
||||
.news-section {
|
||||
padding: 60px;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto 100px;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.news-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 32px;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.news-card {
|
||||
background: #a1a1aa;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08); /* Consistent with feature cards */
|
||||
padding: 24px;
|
||||
border-radius: 32px;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.news-card:hover {
|
||||
transform: translateY(-8px);
|
||||
background: #b0b0b9;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2);
|
||||
border-color: #166534;
|
||||
}
|
||||
|
||||
.news-image {
|
||||
background: #f0fdf4;
|
||||
height: 200px;
|
||||
border-radius: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #166534;
|
||||
margin-bottom: 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.news-meta {
|
||||
color: #166534;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 12px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.news-title {
|
||||
color: #1a1a1a;
|
||||
font-weight: 800;
|
||||
font-size: 24px;
|
||||
margin: 0 0 12px 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.news-desc {
|
||||
color: #555555;
|
||||
flex-grow: 1;
|
||||
margin-bottom: 24px;
|
||||
line-height: 1.6;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.news-link {
|
||||
color: #166534;
|
||||
text-decoration: none;
|
||||
font-weight: 800;
|
||||
font-size: 14px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 12px 24px;
|
||||
border-radius: 50px;
|
||||
width: fit-content;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.news-link:hover {
|
||||
background: white;
|
||||
color: black;
|
||||
}
|
||||
|
||||
/* Section Headers */
|
||||
.section-header {
|
||||
text-align: left;
|
||||
margin-bottom: 40px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
color: white;
|
||||
margin: 0;
|
||||
font-size: 48px;
|
||||
font-weight: 900;
|
||||
letter-spacing: -2px;
|
||||
}
|
||||
|
||||
/* Mobile Styles */
|
||||
.mobile-page {
|
||||
background: #000000;
|
||||
color: white;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.safe-area {
|
||||
padding-top: 50px;
|
||||
padding-bottom: 120px;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 24px;
|
||||
padding-top: 30px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: #b3b3b3;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin: 4px 0 0 0;
|
||||
}
|
||||
|
||||
.scroll-content {
|
||||
padding: 0 20px 140px 20px;
|
||||
}
|
||||
|
||||
.history-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
background: #121212;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.history-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: #2a2a2a;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #b3b3b3;
|
||||
}
|
||||
|
||||
.history-details {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.history-title {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.history-time {
|
||||
margin: 4px 0 0 0;
|
||||
font-size: 12px;
|
||||
color: #b3b3b3;
|
||||
}
|
||||
|
||||
.severity-badge {
|
||||
padding: 4px 12px;
|
||||
border-radius: 50px;
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.severity-badge.low {
|
||||
background: rgba(30, 215, 96, 0.2);
|
||||
color: #1ed760;
|
||||
}
|
||||
|
||||
.severity-badge.medium {
|
||||
background: rgba(245, 155, 35, 0.2);
|
||||
color: #f59b23;
|
||||
}
|
||||
|
||||
.severity-badge.high {
|
||||
background: rgba(233, 20, 41, 0.2);
|
||||
color: #e91429;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import ParallaxLandscape from "$lib/components/ParallaxLandscape.svelte";
|
||||
import Icon from "@iconify/svelte";
|
||||
|
||||
const categories = ["All", "Food", "Fashion", "Tech", "Home"];
|
||||
let selectedCategory = $state("All");
|
||||
@@ -12,9 +13,8 @@
|
||||
brand: "PureLife",
|
||||
category: "Home",
|
||||
score: 92,
|
||||
grade: "A",
|
||||
image: "ri:cup-line",
|
||||
color: "#1ed760" // Spotify Green
|
||||
color: "#1ed760",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
@@ -22,9 +22,8 @@
|
||||
brand: "TrendZ",
|
||||
category: "Fashion",
|
||||
score: 35,
|
||||
grade: "D",
|
||||
image: "ri:t-shirt-2-line",
|
||||
color: "#e91429" // Spotify Red (Error)
|
||||
color: "#e91429",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
@@ -32,9 +31,8 @@
|
||||
brand: "BeanGreen",
|
||||
category: "Food",
|
||||
score: 88,
|
||||
grade: "B+",
|
||||
image: "ri:cup-fill",
|
||||
color: "#b49bc8" // Lavender
|
||||
color: "#b49bc8",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
@@ -42,9 +40,8 @@
|
||||
brand: "TechGiant",
|
||||
category: "Tech",
|
||||
score: 45,
|
||||
grade: "C",
|
||||
image: "ri:smartphone-line",
|
||||
color: "#f59b23" // Spotify Orange
|
||||
color: "#f59b23",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
@@ -52,9 +49,8 @@
|
||||
brand: "SmileEco",
|
||||
category: "Home",
|
||||
score: 98,
|
||||
grade: "A+",
|
||||
image: "ri:brush-line",
|
||||
color: "#1db954" // Slightly darker green
|
||||
color: "#1db954",
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
@@ -62,71 +58,95 @@
|
||||
brand: "SingleUse Inc",
|
||||
category: "Home",
|
||||
score: 12,
|
||||
grade: "F",
|
||||
image: "ri:forbid-2-line",
|
||||
color: "#e91429"
|
||||
}
|
||||
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());
|
||||
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;
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
function selectCategory(category: string) {
|
||||
selectedCategory = category;
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Ethix - Product Catalogue</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Browse our extensive database of sustainable products, verified eco-scores, and green alternatives."
|
||||
/>
|
||||
</svelte:head>
|
||||
|
||||
<div class="page-wrapper">
|
||||
<div class="desktop-bg">
|
||||
<ParallaxLandscape />
|
||||
<div class="bg-overlay"></div>
|
||||
</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 class="glass-header">
|
||||
<div class="header">
|
||||
<h1 class="page-title">Product Database</h1>
|
||||
<p class="subtitle">
|
||||
Search our verified sustainability ratings
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="search-container">
|
||||
<div class="search-icon-wrapper">
|
||||
<Icon icon="ri:search-line" width="20" />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
class="search-input"
|
||||
placeholder="Search for products, brands..."
|
||||
bind:value={searchQuery}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="filter-bar">
|
||||
{#each categories as category}
|
||||
<button
|
||||
class="filter-chip"
|
||||
class:active={selectedCategory === category}
|
||||
onclick={() => selectCategory(category)}
|
||||
>
|
||||
{category}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</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>
|
||||
<Icon
|
||||
icon={product.image}
|
||||
width="56"
|
||||
style="color: {product.color};"
|
||||
/>
|
||||
</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};">
|
||||
<div
|
||||
class="score-badge"
|
||||
style="background-color: {product.color};"
|
||||
>
|
||||
<span class="score-text">{product.score}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -139,7 +159,6 @@
|
||||
.page-wrapper {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
background-color: #000000;
|
||||
overflow-x: hidden;
|
||||
position: relative;
|
||||
}
|
||||
@@ -148,6 +167,10 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
.bg-overlay {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.content-container {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
@@ -156,81 +179,91 @@
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.glass-header {
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 32px;
|
||||
padding: 40px;
|
||||
margin-bottom: 40px;
|
||||
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: left;
|
||||
margin-bottom: 32px;
|
||||
padding: 0 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
color: white;
|
||||
font-size: 48px;
|
||||
font-size: 42px;
|
||||
font-weight: 900;
|
||||
margin: 0;
|
||||
letter-spacing: -2px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: #b3b3b3;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-size: 16px;
|
||||
margin: 8px 0 0 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Search Bar */
|
||||
.search-container {
|
||||
position: relative;
|
||||
margin-bottom: 24px;
|
||||
padding: 0 12px;
|
||||
max-width: 600px;
|
||||
margin: 0 auto 32px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
background: #2a2a2a;
|
||||
border: none;
|
||||
border-radius: 500px; /* Capsule */
|
||||
padding: 14px 48px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 50px;
|
||||
padding: 16px 20px 16px 52px;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
outline: none;
|
||||
transition: background 0.2s;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
background: #333333;
|
||||
box-shadow: 0 0 0 2px white; /* Focus ring */
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-color: #34d399;
|
||||
}
|
||||
|
||||
.search-input::placeholder {
|
||||
color: #b3b3b3;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
.search-icon-wrapper {
|
||||
position: absolute;
|
||||
left: 32px;
|
||||
left: 20px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #b3b3b3;
|
||||
font-size: 20px;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Filters */
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 32px;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.filter-chip {
|
||||
background: #2a2a2a; /* Pill bg */
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 500px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
padding: 10px 20px;
|
||||
border-radius: 50px;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
@@ -238,27 +271,31 @@
|
||||
}
|
||||
|
||||
.filter-chip:hover {
|
||||
background: #333333;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.filter-chip.active {
|
||||
background: #1ed760;
|
||||
color: #000000;
|
||||
background: linear-gradient(135deg, #22c55e, #16a34a);
|
||||
color: white;
|
||||
border-color: transparent;
|
||||
box-shadow: 0 4px 16px rgba(34, 197, 94, 0.3);
|
||||
}
|
||||
|
||||
/* Grid */
|
||||
.product-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 24px;
|
||||
gap: 20px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.product-card {
|
||||
background: #181818; /* Spotify Card */
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
transition: background-color 0.3s ease;
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 20px;
|
||||
padding: 20px;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
cursor: pointer;
|
||||
@@ -266,19 +303,20 @@
|
||||
}
|
||||
|
||||
.product-card:hover {
|
||||
background-color: #282828;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
.card-image-placeholder {
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
background: #333333;
|
||||
border-radius: 4px; /* Slightly rounded images */
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
.product-info {
|
||||
@@ -296,27 +334,27 @@
|
||||
}
|
||||
|
||||
.product-brand {
|
||||
color: #b3b3b3;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.score-badge {
|
||||
position: absolute;
|
||||
top: 24px;
|
||||
right: 24px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
top: 28px;
|
||||
right: 28px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.score-text {
|
||||
color: #000000;
|
||||
font-weight: 900;
|
||||
color: white;
|
||||
font-weight: 800;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@@ -330,9 +368,20 @@
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.page-wrapper {
|
||||
background: transparent;
|
||||
@media (max-width: 767px) {
|
||||
.content-container {
|
||||
padding: 60px 16px 100px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.product-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import ParallaxLandscape from "$lib/components/ParallaxLandscape.svelte";
|
||||
import CloudSection from "$lib/components/CloudSection.svelte";
|
||||
import Icon from "@iconify/svelte";
|
||||
|
||||
let messages = $state([
|
||||
{
|
||||
@@ -99,171 +99,200 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Ethix - Chat Assistant</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Chat with Ethix AI to get instant answers about recycling, sustainability, and eco-friendly product alternatives."
|
||||
/>
|
||||
</svelte:head>
|
||||
|
||||
<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 class="chat-container">
|
||||
<div class="chat-card">
|
||||
<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>
|
||||
|
||||
<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>
|
||||
<h1 class="page-title">Ethix Assistant</h1>
|
||||
<div class="powered-by">
|
||||
<Icon icon="ri:shining-fill" width="12" />
|
||||
<span>Powered by Gemini</span>
|
||||
</div>
|
||||
</div>
|
||||
</CloudSection>
|
||||
|
||||
<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"
|
||||
>
|
||||
<Icon icon="ri:send-plane-fill" width="24" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.page-wrapper {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
min-height: 100vh;
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.desktop-bg {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.chat-layout {
|
||||
.chat-container {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 100px 24px 40px;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.chat-card {
|
||||
background: #0d2e25;
|
||||
border: 1px solid #1f473b;
|
||||
border-radius: 32px;
|
||||
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.3);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
max-height: 700px;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 24px;
|
||||
padding-top: 20px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
padding: 32px;
|
||||
padding-bottom: 24px;
|
||||
border-bottom: 1px solid #1f473b;
|
||||
text-align: center;
|
||||
z-index: 20;
|
||||
background: #051f18;
|
||||
}
|
||||
|
||||
.mascot-container {
|
||||
position: relative;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin: 0 auto 12px;
|
||||
margin: 0 auto 16px;
|
||||
}
|
||||
|
||||
.mascot-canvas {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
filter: drop-shadow(0 4px 10px rgba(34, 197, 94, 0.3));
|
||||
filter: drop-shadow(0 4px 12px rgba(16, 185, 129, 0.4));
|
||||
}
|
||||
|
||||
.mascot-status-dot {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
right: 10px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: #22c55e;
|
||||
border: 2px solid white;
|
||||
bottom: 8px;
|
||||
right: 8px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background: #34d399;
|
||||
border: 3px solid #051f18;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 10px rgba(34, 197, 94, 0.5);
|
||||
box-shadow: 0 0 12px rgba(52, 211, 153, 0.6);
|
||||
}
|
||||
|
||||
.page-title {
|
||||
color: #000000;
|
||||
color: white;
|
||||
font-size: 28px;
|
||||
font-weight: 800;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.powered-by {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-top: 12px;
|
||||
padding: 4px 12px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 20px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #34d399;
|
||||
letter-spacing: 0.5px;
|
||||
border: 1px solid rgba(52, 211, 153, 0.2);
|
||||
}
|
||||
|
||||
.chat-window {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background: #0d2e25;
|
||||
}
|
||||
|
||||
.messages-container {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 24px;
|
||||
padding-bottom: 120px;
|
||||
padding-bottom: 100px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.message {
|
||||
padding: 16px 24px;
|
||||
border-radius: 24px;
|
||||
max-width: 80%;
|
||||
font-size: 16px;
|
||||
padding: 16px 20px;
|
||||
border-radius: 20px;
|
||||
max-width: 85%;
|
||||
font-size: 15px;
|
||||
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;
|
||||
background: linear-gradient(135deg, #10b981, #059669);
|
||||
border-bottom-right-radius: 6px;
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.2);
|
||||
}
|
||||
|
||||
.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);
|
||||
background: #051f18;
|
||||
border-bottom-left-radius: 6px;
|
||||
color: #e5e7eb;
|
||||
border: 1px solid #1f473b;
|
||||
}
|
||||
|
||||
.message p {
|
||||
@@ -271,34 +300,38 @@
|
||||
}
|
||||
|
||||
.input-container {
|
||||
position: relative;
|
||||
margin: 20px;
|
||||
padding: 12px;
|
||||
background-color: #ffffff;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: 40px;
|
||||
padding: 16px 20px;
|
||||
padding-bottom: 30px;
|
||||
background: #051f18;
|
||||
border-top: 1px solid #1f473b;
|
||||
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;
|
||||
background: #0d2e25;
|
||||
color: white;
|
||||
padding: 14px 20px;
|
||||
border: 1px solid #1f473b;
|
||||
border-radius: 50px;
|
||||
font-size: 15px;
|
||||
outline: none;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.message-input:focus {
|
||||
border-color: #34d399;
|
||||
background: #11382e;
|
||||
}
|
||||
|
||||
.message-input::placeholder {
|
||||
color: #9ca3af;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.send-button {
|
||||
background-color: #22c55e;
|
||||
background: #10b981;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
@@ -309,12 +342,12 @@
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.send-button:hover {
|
||||
transform: scale(1.05);
|
||||
background-color: #16a34a;
|
||||
box-shadow: 0 0 15px rgba(34, 197, 94, 0.4);
|
||||
transform: scale(1.08);
|
||||
box-shadow: 0 0 20px rgba(16, 185, 129, 0.5);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
@@ -328,35 +361,85 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.page-wrapper {
|
||||
background: transparent;
|
||||
.chat-container {
|
||||
padding-top: 100px;
|
||||
height: auto;
|
||||
max-width: 1200px;
|
||||
}
|
||||
|
||||
.chat-layout {
|
||||
margin-top: 160px;
|
||||
margin-bottom: 0;
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
height: 640px;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
.chat-card {
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 32px;
|
||||
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.3);
|
||||
height: 85vh;
|
||||
max-height: 900px;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding-top: 24px;
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
background: transparent;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.chat-window {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.ai-message {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.input-container {
|
||||
position: relative;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: 20px;
|
||||
width: auto;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
.message-input {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.message-input:focus {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
border-color: rgba(34, 197, 94, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.chat-container {
|
||||
padding: 0;
|
||||
max-width: 100%;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chat-card {
|
||||
height: 100%;
|
||||
max-height: none;
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 20px;
|
||||
padding-top: 50px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.messages-container {
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.input-container {
|
||||
padding-bottom: 90px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import ParallaxLandscape from "$lib/components/ParallaxLandscape.svelte";
|
||||
import CloudSection from "$lib/components/CloudSection.svelte";
|
||||
</script>
|
||||
|
||||
<div class="page-wrapper">
|
||||
@@ -9,107 +8,93 @@
|
||||
</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 class="header-card">
|
||||
<h1 class="page-title">Why We Exist</h1>
|
||||
<p class="subtitle">Our Mission & Goal</p>
|
||||
</div>
|
||||
|
||||
<div class="grid-layout">
|
||||
<div class="glass-card mission-card">
|
||||
<div class="icon-circle problem-icon">
|
||||
<iconify-icon
|
||||
icon="ri:error-warning-fill"
|
||||
width="32"
|
||||
style="color: #ef4444;"
|
||||
></iconify-icon>
|
||||
</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>
|
||||
|
||||
<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 class="glass-card mission-card">
|
||||
<div class="icon-circle solution-icon">
|
||||
<iconify-icon
|
||||
icon="ri:eye-fill"
|
||||
width="32"
|
||||
style="color: #4ade80;"
|
||||
></iconify-icon>
|
||||
</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>
|
||||
<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:flag-fill"
|
||||
width="32"
|
||||
style="color: #1ed760;"
|
||||
icon="ri:check-line"
|
||||
width="20"
|
||||
style="color: #4ade80;"
|
||||
></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>
|
||||
<span>Instant Fact-Checking</span>
|
||||
</li>
|
||||
<li>
|
||||
<iconify-icon
|
||||
icon="ri:check-line"
|
||||
width="20"
|
||||
style="color: #4ade80;"
|
||||
></iconify-icon>
|
||||
<span>Unbiased Eco-Ratings</span>
|
||||
</li>
|
||||
<li>
|
||||
<iconify-icon
|
||||
icon="ri:check-line"
|
||||
width="20"
|
||||
style="color: #4ade80;"
|
||||
></iconify-icon>
|
||||
<span>Real Sustainable Alternatives</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</CloudSection>
|
||||
|
||||
<div class="glass-card goal-card">
|
||||
<div class="header-row">
|
||||
<iconify-icon
|
||||
icon="ri:flag-fill"
|
||||
width="32"
|
||||
style="color: #4ade80;"
|
||||
></iconify-icon>
|
||||
<h2 class="card-title large">Our Ultimate Goal</h2>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -117,7 +102,6 @@
|
||||
.page-wrapper {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
background-color: #000000;
|
||||
overflow-x: hidden;
|
||||
position: relative;
|
||||
}
|
||||
@@ -129,19 +113,18 @@
|
||||
.content-container {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
padding: 80px 24px 120px;
|
||||
max-width: 800px;
|
||||
padding: 100px 24px 120px;
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
.header-card {
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
padding: 0 20px;
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
color: #000000;
|
||||
color: white;
|
||||
font-size: 48px;
|
||||
font-weight: 900;
|
||||
margin: 0;
|
||||
@@ -149,10 +132,10 @@
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: #4b5563;
|
||||
font-size: 16px;
|
||||
margin: 16px 0 0 0;
|
||||
font-weight: 700;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
font-size: 14px;
|
||||
margin: 12px 0 0 0;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
@@ -163,13 +146,20 @@
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #a1a1aa; /* Light Grey Card */
|
||||
padding: 40px;
|
||||
border-radius: 8px;
|
||||
box-shadow: none;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
.glass-card {
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 24px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
|
||||
padding: 36px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.glass-card:hover {
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.mission-card {
|
||||
@@ -179,7 +169,6 @@
|
||||
}
|
||||
|
||||
.goal-card {
|
||||
background: #a1a1aa;
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
@@ -187,25 +176,25 @@
|
||||
}
|
||||
|
||||
.icon-circle {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 24px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.problem-icon {
|
||||
background: rgba(233, 20, 41, 0.1);
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
}
|
||||
|
||||
.solution-icon {
|
||||
background: rgba(34, 197, 94, 0.1); /* Eco Green tint */
|
||||
background: rgba(34, 197, 94, 0.15);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
color: #1a1a1a;
|
||||
color: white;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
margin: 0 0 16px 0;
|
||||
@@ -217,33 +206,33 @@
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
color: #333333;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
margin: 0 0 24px 0;
|
||||
}
|
||||
|
||||
.stat-highlight {
|
||||
background: #374151; /* Darker grey */
|
||||
padding: 16px 24px;
|
||||
border-radius: 8px;
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||
padding: 16px 20px;
|
||||
border-radius: 16px;
|
||||
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-size: 36px;
|
||||
font-weight: 900;
|
||||
color: #e91429;
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #f3f4f6; /* Light text on dark bg */
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
@@ -258,29 +247,33 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
font-size: 16px;
|
||||
color: #1a1a1a;
|
||||
font-weight: 700;
|
||||
margin-bottom: 14px;
|
||||
font-size: 15px;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.benefit-list li:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.header-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 24px;
|
||||
gap: 14px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.goal-text {
|
||||
color: #333333;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-size: 18px;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 0;
|
||||
line-height: 1.7;
|
||||
margin: 0;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
color: #166534;
|
||||
color: #4ade80;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@@ -295,19 +288,20 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.page-wrapper {
|
||||
background: transparent;
|
||||
.grid-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.goal-card {
|
||||
grid-column: span 2;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.content-container {
|
||||
padding: 40px 20px 100px;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 0;
|
||||
margin-bottom: 30px;
|
||||
padding: 60px 20px 100px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
|
||||
@@ -1,113 +1,225 @@
|
||||
<script lang="ts">
|
||||
import ParallaxLandscape from "$lib/components/ParallaxLandscape.svelte";
|
||||
|
||||
const news = [
|
||||
{
|
||||
id: 1,
|
||||
title: "Ocean Cleanup hits milestone",
|
||||
desc: "500 tons removed from Pacific patch.",
|
||||
desc: "500 tons of plastic have been successfully removed from the Pacific garbage patch, marking a major breakthrough in ocean restoration efforts.",
|
||||
date: "2h ago",
|
||||
icon: "ri:ship-line",
|
||||
tag: "Trending",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "New Plastic Ban in effect",
|
||||
desc: "Major cities adopt strict policies.",
|
||||
desc: "Major cities across the globe adopt strict single-use plastic policies, signaling a shift toward sustainable alternatives.",
|
||||
date: "5h ago",
|
||||
icon: "ri:prohibited-line",
|
||||
tag: "Policy",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "Ethix App launches globally",
|
||||
desc: "Empowering users to make better choices.",
|
||||
desc: "Empowering users worldwide to make better choices with AI-powered sustainability scanning and verification.",
|
||||
date: "1d ago",
|
||||
icon: "ri:global-line",
|
||||
tag: "Launch",
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<div class="page-container">
|
||||
<div class="safe-area">
|
||||
<div class="page-wrapper">
|
||||
<div class="desktop-bg">
|
||||
<ParallaxLandscape />
|
||||
</div>
|
||||
|
||||
<div class="content-container">
|
||||
<div class="header">
|
||||
<h1 class="page-title">Eco News</h1>
|
||||
<p class="subtitle">Latest sustainability updates</p>
|
||||
</div>
|
||||
|
||||
<div class="scroll-content">
|
||||
<div class="news-grid">
|
||||
{#each news as item (item.id)}
|
||||
<div class="news-card">
|
||||
<div class="news-image"></div>
|
||||
<p class="news-meta">{item.date} • Trending</p>
|
||||
<article class="news-card">
|
||||
<div class="card-header">
|
||||
<div class="news-icon">
|
||||
<iconify-icon icon={item.icon} width="28"
|
||||
></iconify-icon>
|
||||
</div>
|
||||
<span class="news-tag">{item.tag}</span>
|
||||
</div>
|
||||
<div class="news-meta">{item.date}</div>
|
||||
<h2 class="news-title">{item.title}</h2>
|
||||
<p class="news-desc">{item.desc}</p>
|
||||
</div>
|
||||
<a href="/news/{item.id}" class="news-link">
|
||||
Read more
|
||||
<iconify-icon icon="ri:arrow-right-line" width="16"
|
||||
></iconify-icon>
|
||||
</a>
|
||||
</article>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.page-container {
|
||||
.page-wrapper {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
background-color: #0f172a;
|
||||
overflow-y: auto;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.safe-area {
|
||||
padding-top: 50px;
|
||||
padding-bottom: 120px;
|
||||
.desktop-bg {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.content-container {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
padding: 100px 24px 120px;
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 24px;
|
||||
padding-top: 30px;
|
||||
margin-bottom: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
color: white;
|
||||
font-size: 34px;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.5px;
|
||||
font-size: 42px;
|
||||
font-weight: 900;
|
||||
letter-spacing: -2px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: #94a3b8;
|
||||
font-size: 14px;
|
||||
margin: 4px 0 0 0;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
font-size: 16px;
|
||||
margin: 10px 0 0 0;
|
||||
}
|
||||
|
||||
.scroll-content {
|
||||
padding: 24px;
|
||||
padding-bottom: 140px;
|
||||
.news-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.news-card {
|
||||
background-color: rgba(30, 41, 59, 0.9);
|
||||
padding: 20px;
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 24px;
|
||||
border: 1px solid #334155;
|
||||
padding: 28px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.news-card:hover {
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.news-image {
|
||||
height: 120px;
|
||||
background-color: #334155;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 12px;
|
||||
.news-icon {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
background: rgba(34, 197, 94, 0.15);
|
||||
border-radius: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #4ade80;
|
||||
}
|
||||
|
||||
.news-tag {
|
||||
background: rgba(34, 197, 94, 0.2);
|
||||
color: #4ade80;
|
||||
padding: 6px 14px;
|
||||
border-radius: 50px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.news-meta {
|
||||
color: #94a3b8;
|
||||
font-size: 12px;
|
||||
margin: 0 0 4px 0;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
font-size: 13px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.news-title {
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: 18px;
|
||||
margin: 4px 0 8px 0;
|
||||
font-weight: 700;
|
||||
font-size: 22px;
|
||||
margin: 0 0 12px 0;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.news-desc {
|
||||
color: #cbd5e1;
|
||||
margin: 0;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
margin: 0 0 20px 0;
|
||||
}
|
||||
|
||||
.news-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #4ade80;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.news-link:hover {
|
||||
gap: 12px;
|
||||
color: #86efac;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.desktop-bg {
|
||||
display: block;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.news-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.news-card:first-child {
|
||||
grid-column: span 2;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.content-container {
|
||||
padding: 60px 16px 100px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 32px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import ParallaxLandscape from "$lib/components/ParallaxLandscape.svelte";
|
||||
import CloudSection from "$lib/components/CloudSection.svelte";
|
||||
|
||||
let productName = $state("");
|
||||
let description = $state("");
|
||||
@@ -54,31 +53,31 @@
|
||||
|
||||
<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 class="glass-card success-card">
|
||||
<div class="icon-circle success">
|
||||
<iconify-icon
|
||||
icon="ri:checkbox-circle-fill"
|
||||
width="60"
|
||||
style="color: #4ade80;"
|
||||
></iconify-icon>
|
||||
</div>
|
||||
</CloudSection>
|
||||
<h2 class="success-title">Report Submitted!</h2>
|
||||
<p class="success-subtitle">
|
||||
Thank you for keeping companies honest.
|
||||
</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="header">
|
||||
<div class="header-section">
|
||||
<h1 class="page-title">Report Greenwashing</h1>
|
||||
<p class="subtitle">Call out false eco-claims</p>
|
||||
</div>
|
||||
|
||||
<CloudSection>
|
||||
<div class="glass-card form-card">
|
||||
<div class="form-content">
|
||||
<div class="form-group">
|
||||
<label class="label">Product Name</label>
|
||||
<label class="label" for="productName"
|
||||
>Product Name</label
|
||||
>
|
||||
<div class="input-wrapper">
|
||||
<iconify-icon
|
||||
icon="ri:price-tag-3-line"
|
||||
@@ -86,6 +85,7 @@
|
||||
></iconify-icon>
|
||||
<input
|
||||
type="text"
|
||||
id="productName"
|
||||
class="input"
|
||||
placeholder="e.g. 'Eco-Friendly' Water Bottle"
|
||||
bind:value={productName}
|
||||
@@ -94,13 +94,16 @@
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="label">Why is it misleading?</label>
|
||||
<label class="label" for="description"
|
||||
>Why is it misleading?</label
|
||||
>
|
||||
<div class="input-wrapper textarea-wrapper">
|
||||
<iconify-icon
|
||||
icon="ri:text-wrapper"
|
||||
icon="ri:text-snippet-line"
|
||||
class="input-icon top-align"
|
||||
></iconify-icon>
|
||||
<textarea
|
||||
id="description"
|
||||
class="textarea"
|
||||
placeholder="Describe the claim and the reality..."
|
||||
bind:value={description}
|
||||
@@ -109,8 +112,12 @@
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="label">Evidence (Photo)</label>
|
||||
<button class="image-picker" on:click={pickImage}>
|
||||
<span class="label">Evidence (Photo)</span>
|
||||
<button
|
||||
class="image-picker"
|
||||
onclick={pickImage}
|
||||
type="button"
|
||||
>
|
||||
{#if image}
|
||||
<img
|
||||
src={image}
|
||||
@@ -128,7 +135,7 @@
|
||||
<iconify-icon
|
||||
icon="ri:camera-add-line"
|
||||
width="32"
|
||||
style="color: #94a3b8;"
|
||||
style="color: rgba(255,255,255,0.4);"
|
||||
></iconify-icon>
|
||||
<p>Upload Photo</p>
|
||||
</div>
|
||||
@@ -140,14 +147,15 @@
|
||||
class="submit-button"
|
||||
class:disabled={!isValid}
|
||||
disabled={!isValid}
|
||||
on:click={handleSubmit}
|
||||
onclick={handleSubmit}
|
||||
type="button"
|
||||
>
|
||||
<iconify-icon icon="ri:alert-fill" width="20"
|
||||
></iconify-icon>
|
||||
Submit Report
|
||||
</button>
|
||||
</div>
|
||||
</CloudSection>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -156,7 +164,6 @@
|
||||
.page-wrapper {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
background-color: #09090b;
|
||||
overflow-x: hidden;
|
||||
position: relative;
|
||||
}
|
||||
@@ -169,100 +176,76 @@
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
padding: 100px 24px 120px;
|
||||
max-width: 600px; /* Narrower form */
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
.header-section {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
padding: 0 20px;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
color: #000000;
|
||||
font-size: 32px;
|
||||
color: white;
|
||||
font-size: 36px;
|
||||
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;
|
||||
}
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: #166534; /* Dark Green for report subtitle */
|
||||
font-size: 16px;
|
||||
margin: 8px 0 0 0;
|
||||
color: #4ade80;
|
||||
font-size: 14px;
|
||||
margin: 10px 0 0 0;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.glass-card {
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 28px;
|
||||
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.form-card {
|
||||
padding: 36px;
|
||||
}
|
||||
|
||||
.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;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: #333333;
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
position: relative;
|
||||
background-color: #f9fafb;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 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);
|
||||
border-color: rgba(34, 197, 94, 0.5);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.input-icon {
|
||||
@@ -270,44 +253,52 @@
|
||||
left: 16px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #a1a1aa;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.top-align {
|
||||
top: 20px;
|
||||
top: 18px;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
color: #1a1a1a;
|
||||
color: white;
|
||||
padding: 16px 16px 16px 48px;
|
||||
border: none;
|
||||
font-size: 16px;
|
||||
font-size: 15px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.input::placeholder {
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
|
||||
.textarea {
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
color: #1a1a1a;
|
||||
color: white;
|
||||
padding: 16px 16px 16px 48px;
|
||||
border: none;
|
||||
font-size: 16px;
|
||||
font-size: 15px;
|
||||
outline: none;
|
||||
height: 120px;
|
||||
resize: none;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.textarea::placeholder {
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
|
||||
.image-picker {
|
||||
width: 100%;
|
||||
background-color: #f9fafb;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 16px;
|
||||
height: 160px;
|
||||
border: 2px dashed #d1d5db;
|
||||
height: 140px;
|
||||
border: 2px dashed rgba(255, 255, 255, 0.15);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
padding: 0;
|
||||
@@ -316,8 +307,8 @@
|
||||
}
|
||||
|
||||
.image-picker:hover {
|
||||
border-color: #22c55e;
|
||||
background-color: #3f3f46;
|
||||
border-color: rgba(34, 197, 94, 0.5);
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.picker-placeholder {
|
||||
@@ -326,12 +317,13 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
gap: 12px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.picker-placeholder p {
|
||||
color: #a1a1aa;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@@ -344,7 +336,7 @@
|
||||
.change-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -358,14 +350,14 @@
|
||||
}
|
||||
|
||||
.submit-button {
|
||||
margin-top: 12px;
|
||||
background: #22c55e;
|
||||
margin-top: 8px;
|
||||
background: linear-gradient(135deg, #22c55e, #16a34a);
|
||||
padding: 18px;
|
||||
border-radius: 16px;
|
||||
border-radius: 50px;
|
||||
border: none;
|
||||
color: #000000;
|
||||
font-weight: 800;
|
||||
font-size: 16px;
|
||||
color: white;
|
||||
font-weight: 700;
|
||||
font-size: 15px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
width: 100%;
|
||||
@@ -373,37 +365,31 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
box-shadow: 0 4px 20px rgba(34, 197, 94, 0.3);
|
||||
box-shadow: 0 8px 24px 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;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 12px 32px rgba(34, 197, 94, 0.45);
|
||||
}
|
||||
|
||||
.submit-button.disabled {
|
||||
background: #27272a;
|
||||
color: #52525b;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
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);
|
||||
background: rgba(34, 197, 94, 0.15);
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 50%;
|
||||
@@ -421,8 +407,8 @@
|
||||
}
|
||||
|
||||
.success-subtitle {
|
||||
color: #a1a1aa;
|
||||
font-size: 18px;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@@ -436,31 +422,19 @@
|
||||
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;
|
||||
padding: 60px 20px 100px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.form-content {
|
||||
padding: 24px;
|
||||
.form-card {
|
||||
padding: 28px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user