diff --git a/app/ewaste-26/page.css b/app/ewaste-26/page.css index 3416a2d..45c67a7 100644 --- a/app/ewaste-26/page.css +++ b/app/ewaste-26/page.css @@ -1 +1,186 @@ -/* Placeholder CSS for ewaste-26 page */ +/* ========================================== + PAAVNI MASCOT - E-Waste '26 Page Only + ========================================== + All styles scoped to mascot elements only. + Does NOT affect any other page or component. + ========================================== */ + +/* Absolute-position mascot container - right side of hero area, scrolls with the page */ +.paavni-mascot-container { + position: absolute; + right: 1vw; + top: 58%; + transform: translateY(-50%); + width: 155px; + z-index: 20; + + pointer-events: auto; + /* <-- IMPORTANT */ + cursor: pointer; +} + +/* The mascot image itself */ +.paavni-mascot-image { + pointer-events: none; +} + + + +/* ========================================== + RESPONSIVE BREAKPOINTS + ========================================== */ + +/* Extra large desktops (>= 1600px) */ +@media (min-width: 1600px) { + .paavni-mascot-container { + width: 180px; + right: 3vw; + } +} + +/* Large desktops (1440px - 1599px) */ +@media (min-width: 1440px) and (max-width: 1599px) { + .paavni-mascot-container { + width: 165px; + right: 2.5vw; + } +} + +/* Standard desktop (1024px - 1439px) - default values apply */ + +/* Tablet landscape (768px - 1023px) */ +@media (min-width: 768px) and (max-width: 1023px) { + .paavni-mascot-container { + width: 120px; + right: 8px; + } +} + +/* Tablet portrait (640px - 767px) */ +@media (min-width: 640px) and (max-width: 767px) { + .paavni-mascot-container { + width: 95px; + right: 6px; + } +} + +/* Mobile (< 640px) */ +@media (max-width: 639px) { + .paavni-mascot-container { + width: 56px; + right: 8px; + top: 84%; + transform: translateY(-50%); + } + + .paavni-popup { + width: 160px; + right: 105%; + top: 12%; + } +} + +/* Very small mobile (< 380px) */ +@media (max-width: 379px) { + .paavni-mascot-container { + width: 48px; + right: 6px; + top: 87%; + transform: translateY(-50%); + } + + .paavni-popup { + width: 145px; + right: 102%; + top: 10%; + } +} + +.paavni-popup { + + position: absolute; + + right: 108%; + + top: 18%; + + width: 225px; + + background: rgba(20, 20, 20, .92); + + border: 1px solid rgba(60, 255, 160, .25); + + border-radius: 14px; + + padding: 14px 18px; + + color: white; + + backdrop-filter: blur(14px); + + box-shadow: 0 10px 30px rgba(0, 0, 0, .35); + + pointer-events: none; + + z-index: 30; +} + +.popup-title { + + color: #32d583; + + font-weight: 700; + + margin-bottom: 6px; + + padding-left: 8px; + +} + +.popup-text { + + color: #d7d7d7; + + line-height: 1.5; + + font-size: .92rem; + + padding-left: 8px; + +} + +.paavni-popup::after { + content: ""; + + position: absolute; + + right: -8px; + + top: 34px; + + width: 16px; + + height: 16px; + + background: rgba(20, 20, 20, .92); + + transform: rotate(45deg); + + border-right: 1px solid rgba(60, 255, 160, .25); + + border-top: 1px solid rgba(60, 255, 160, .25); +} + +@keyframes popupFade { + + from { + opacity: 0; + transform: translateX(15px); + } + + to { + opacity: 1; + transform: translateX(0); + } + +} \ No newline at end of file diff --git a/app/ewaste-26/page.tsx b/app/ewaste-26/page.tsx index a0e7ca8..ed8c3a0 100644 --- a/app/ewaste-26/page.tsx +++ b/app/ewaste-26/page.tsx @@ -1,5 +1,5 @@ "use client"; -import React, { useState, useEffect, useMemo } from "react"; +import React, { useState, useEffect, useMemo, useRef } from "react"; import { motion, AnimatePresence } from "framer-motion"; import { Recycle, @@ -38,6 +38,7 @@ import { Badge } from "@/components/ui/badge"; import { SegmentedControl } from "@/components/ui/segmented-control"; import { galleryImages } from "@/lib/gallery"; import ImagesScrollingGallery from "@/components/ui/images-scrolling-gallery"; +import "./page.css"; // ========================================== // CHART CONFIGURATION @@ -97,8 +98,8 @@ function CountUp({ end, duration = 1500, suffix = "" }: { end: number; duration? }, [end, duration]); const formattedValue = Number.isInteger(end) - ? Math.round(count).toLocaleString() - : count.toFixed(1); + ? Math.round(count).toLocaleString() + : count.toFixed(1); return {formattedValue}{suffix}; } @@ -109,11 +110,11 @@ function CountUp({ end, duration = 1500, suffix = "" }: { end: number; duration? const getInitials = (name: string) => { if (!name) return ""; return name - .split(" ") - .map((n) => n[0]) - .join("") - .toUpperCase() - .slice(0, 2); + .split(" ") + .map((n) => n[0]) + .join("") + .toUpperCase() + .slice(0, 2); }; const getCategoryIcon = (category: string) => { @@ -134,6 +135,132 @@ const getCategoryIcon = (category: string) => { } }; +// ========================================== +// PAAVNI MASCOT COMPONENT +// ========================================== +const ECO_FACTS = [ + "Thank you for supporting a greener campus.", + "Every recycled device helps reduce landfill waste.", + "Recycling one laptop saves valuable natural resources.", + "Batteries should never be thrown into household waste.", + "E-waste contains valuable metals that can be recovered.", + "Small recycling efforts create a big environmental impact.", + "Always erase personal data before recycling your device.", + "Responsible recycling protects both people and the planet.", + "Reusing electronics extends their life and reduces waste.", + "Every contribution brings us closer to a cleaner campus." +]; +function PaavniMascot() { + const [isVisible, setIsVisible] = useState(true); + const [currentFact, setCurrentFact] = useState( + "Thank you for supporting a greener campus." + ); + const [canClick, setCanClick] = useState(false); + + const timerRef = useRef(null); + const hasShownWelcome = useRef(false); + const lastIndexRef = useRef(-1); + + useEffect(() => { + if (hasShownWelcome.current) return; + hasShownWelcome.current = true; + + setIsVisible(false); + + const introTimer = setTimeout(() => { + setIsVisible(true); + + timerRef.current = setTimeout(() => { + setIsVisible(false); + setCanClick(true); + }, 6000); + }, 1200); + + return () => { + clearTimeout(introTimer); + + if (timerRef.current) clearTimeout(timerRef.current); + }; + }, []); + + const handleClick = () => { + if (!canClick) return; + + if (timerRef.current) { + clearTimeout(timerRef.current); + } + + let randomIndex; + + do { + randomIndex = Math.floor(Math.random() * ECO_FACTS.length); + } while ( + ECO_FACTS.length > 1 && + randomIndex === lastIndexRef.current + ); + + lastIndexRef.current = randomIndex; + + setCurrentFact(ECO_FACTS[randomIndex]); + setIsVisible(true); + + timerRef.current = setTimeout(() => { + setIsVisible(false); + }, 6000); + }; + + return ( + + Paavni Mascot + + + {isVisible && ( + +
+ 💚 Paavni says +
+ +
+ {currentFact} +
+
+ )} +
+
+ ); +} + // ========================================== // MAIN APP COMPONENT // ========================================== @@ -151,22 +278,22 @@ export default function EWasteDrive2026() { // LIVE CONVEX DATA // ========================================== const individualsData = useQuery( - api.ewaste.getIndividualLeaderboard, - { limit: 100 } + api.ewaste.getIndividualLeaderboard, + { limit: 100 } ); const organizationsData = useQuery( - api.ewaste.getOrganizationLeaderboard, - { limit: 100 } + api.ewaste.getOrganizationLeaderboard, + { limit: 100 } ); const statsData = useQuery(api.ewaste.getEwasteStats); // Convex returns undefined while a query is loading. const isLeaderboardLoading = - individualsData === undefined || - organizationsData === undefined || - statsData === undefined; + individualsData === undefined || + organizationsData === undefined || + statsData === undefined; // Convert live individual records into the format used by the existing UI. const individuals = useMemo(() => { @@ -198,13 +325,13 @@ export default function EWasteDrive2026() { }, [organizationsData]); const stats = useMemo( - () => - statsData ?? { - totalWeight: 0, - individualCount: 0, - organizationCount: 0, - }, - [statsData] + () => + statsData ?? { + totalWeight: 0, + individualCount: 0, + organizationCount: 0, + }, + [statsData] ); // Build the device-type chart from live individual records. @@ -216,9 +343,9 @@ export default function EWasteDrive2026() { for (const item of individualsData ?? []) { const wasteTypes = - item.wasteTypes && item.wasteTypes.length > 0 - ? item.wasteTypes - : ["Other"]; + item.wasteTypes && item.wasteTypes.length > 0 + ? item.wasteTypes + : ["Other"]; const distributedWeight = item.wasteWeight / wasteTypes.length; @@ -229,34 +356,34 @@ export default function EWasteDrive2026() { } const sorted = Array.from(totals.entries()) - .sort((a, b) => b[1] - a[1]) - .slice(0, 5); + .sort((a, b) => b[1] - a[1]) + .slice(0, 5); const totalWeight = sorted.reduce((sum, [, weight]) => sum + weight, 0); return sorted.map(([name, weight], index) => ({ name, value: - totalWeight > 0 - ? Number(((weight / totalWeight) * 100).toFixed(1)) - : 0, + totalWeight > 0 + ? Number(((weight / totalWeight) * 100).toFixed(1)) + : 0, color: colors[index % colors.length], })); }, [individualsData]); // Keep the same horizontal bar-chart design using live Convex totals. const departmentData = useMemo( - () => [ - { - name: "Individuals", - weight: individuals.reduce((sum, item) => sum + item.weight, 0), - }, - { - name: "Organizations", - weight: organizations.reduce((sum, item) => sum + item.weight, 0), - }, - ], - [individuals, organizations] + () => [ + { + name: "Individuals", + weight: individuals.reduce((sum, item) => sum + item.weight, 0), + }, + { + name: "Organizations", + weight: organizations.reduce((sum, item) => sum + item.weight, 0), + }, + ], + [individuals, organizations] ); // Build a lightweight live trend using the dates returned by Convex. @@ -274,15 +401,15 @@ export default function EWasteDrive2026() { } return Array.from(dailyTotals.entries()) - .sort(([dateA], [dateB]) => dateA.localeCompare(dateB)) - .slice(-8) - .map(([date, weight]) => ({ - date: new Date(`${date}T00:00:00`).toLocaleDateString("en-IN", { - day: "numeric", - month: "short", - }), - weight: Number(weight.toFixed(1)), - })); + .sort(([dateA], [dateB]) => dateA.localeCompare(dateB)) + .slice(-8) + .map(([date, weight]) => ({ + date: new Date(`${date}T00:00:00`).toLocaleDateString("en-IN", { + day: "numeric", + month: "short", + }), + weight: Number(weight.toFixed(1)), + })); }, [individuals, organizations]); // Extract Podium & List items @@ -303,514 +430,517 @@ export default function EWasteDrive2026() { }, [activeTab, individuals, organizations]); return ( -
- {/* Modern Premium Background Layer System */} -