Initial Code Commit

This commit is contained in:
2025-10-24 02:07:59 -04:00
commit f099f36838
63 changed files with 4425 additions and 0 deletions

View File

@@ -0,0 +1,217 @@
// app/buildings/[buildingid]/emissions/page.tsx
"use client";
import { useState, useEffect } from "react";
import { CalendarDate } from "@internationalized/date";
import { Button } from "@nextui-org/button";
import { Divider } from "@nextui-org/divider";
import { ButtonGroup } from "@nextui-org/button";
import { Card, CardHeader, CardBody } from "@nextui-org/react";
import { Input } from "@nextui-org/input";
import { Popover, PopoverTrigger, PopoverContent } from "@nextui-org/popover";
import { Calendar, DateValue } from "@nextui-org/calendar";
import AddDataButton from "@/components/addDataButton";
import { useBuilding } from "@/lib/useBuildingData";
import EmissionsGraph from "@/components/emissionsGraph";
interface EmissionsPageProps {
params: { buildingid: string };
}
export default function EmissionsPage({ params }: EmissionsPageProps) {
const { data: buildingData } = useBuilding(params.buildingid);
// State for filters
const [startDate, setStartDate] = useState<DateValue | null>(null);
const [endDate, setEndDate] = useState<DateValue | null>(null);
const [showWaste, setShowWaste] = useState(true);
const [showElectricity, setShowElectricity] = useState(true);
const [showGas, setShowGas] = useState(true);
const [graphType, setGraphType] = useState<'line' | 'area' | 'pie'>('line');
useEffect(() => {
if (buildingData) {
const allDates = [
...buildingData.electricityUsage.map(d => d.timestamp),
...buildingData.naturalGasUsage.map(d => d.timestamp),
...buildingData.wasteGeneration.map(d => d.timestamp)
];
if (allDates.length > 0) {
const earliestDate = new Date(Math.min(...allDates.map(d => (d as any).seconds * 1000)));
const latestDate = new Date(Math.max(...allDates.map(d => (d as any).seconds * 1000)));
earliestDate.setDate(earliestDate.getDate() - 1);
latestDate.setDate(latestDate.getDate() + 1);
setStartDate(new CalendarDate(earliestDate.getFullYear(), earliestDate.getMonth() + 1, earliestDate.getDate()));
setEndDate(new CalendarDate(latestDate.getFullYear(), latestDate.getMonth() + 1, latestDate.getDate()));
}
}
}, [buildingData]);
const handlePdfToImage = async () => {
try {
const formData = new FormData();
const pdfResponse = await fetch('/electricity-sample-bill.pdf');
const pdfBlob = await pdfResponse.blob();
formData.append('pdf', pdfBlob, 'electricity-sample-bill.pdf');
const response = await fetch('/api/pdf-to-image', {
method: 'POST',
body: { ...formData, type: 'electricity' }
});
if (!response.ok) {
throw new Error('Failed to convert PDF to image');
}
const result = await response.json();
console.log('PDF to Image conversion result:', result);
// Handle the result as needed
} catch (error) {
console.error('Error converting PDF to image:', error);
// Handle the error (e.g., show an error message to the user)
}
};
const handleStartDateChange = (date: DateValue) => {
setStartDate(date);
};
const handleEndDateChange = (date: DateValue) => {
setEndDate(date);
};
return (
<div className="flex flex-col items-center h-full p-4">
{/* Tab Title */}
<h1 className="text-6xl text-left self-start font-bold pt-8">
{`Emissions`}
</h1>
{/* Group for filters plus graph */}
<div className="flex flex-col justify-center w-full h-full">
{/* Horizontal group for adding data and filters */}
<AddDataButton buildingid={params.buildingid} />
<div className="flex gap-4 mt-4">
{/* Data Type Selection Card */}
<Card className="flex-1">
<CardHeader>
<h3 className="text-lg font-semibold">Data Types</h3>
</CardHeader>
<CardBody>
<div className="flex gap-2">
<Button
color={showElectricity ? "primary" : "default"}
onClick={() => setShowElectricity(!showElectricity)}
>
Electricity
</Button>
<Button
color={showGas ? "primary" : "default"}
onClick={() => setShowGas(!showGas)}
>
Natural Gas
</Button>
<Button
color={showWaste ? "primary" : "default"}
onClick={() => setShowWaste(!showWaste)}
>
Waste
</Button>
</div>
</CardBody>
</Card>
{/* Chart Type Selection Card */}
<Card className="flex-1">
<CardHeader>
<h3 className="text-lg font-semibold">Chart Type</h3>
</CardHeader>
<CardBody>
<ButtonGroup>
<Button
color={graphType === 'line' ? "primary" : "default"}
onClick={() => setGraphType('line')}
>
Line
</Button>
<Button
color={graphType === 'area' ? "primary" : "default"}
onClick={() => setGraphType('area')}
>
Area
</Button>
<Button
color={graphType === 'pie' ? "primary" : "default"}
onClick={() => setGraphType('pie')}
>
Pie
</Button>
</ButtonGroup>
</CardBody>
</Card>
{/* Date Range Selection Card */}
<Card className="flex-1">
<CardHeader>
<h3 className="text-lg font-semibold">Date Range</h3>
</CardHeader>
<CardBody>
<div className="flex gap-2">
<Popover placement="bottom">
<PopoverTrigger>
<Input
readOnly
label="Start Date"
value={startDate ? startDate.toString() : ''}
/>
</PopoverTrigger>
<PopoverContent>
<Calendar
showMonthAndYearPickers
value={startDate}
onChange={handleStartDateChange}
/>
</PopoverContent>
</Popover>
<Popover placement="bottom">
<PopoverTrigger>
<Input
readOnly
label="End Date"
value={endDate ? endDate.toString() : ''}
/>
</PopoverTrigger>
<PopoverContent>
<Calendar
showMonthAndYearPickers
value={endDate}
onChange={handleEndDateChange}
/>
</PopoverContent>
</Popover>
</div>
</CardBody>
</Card>
</div>
<Divider className="mt-6" />
{/* Render emissions graph */}
<EmissionsGraph
buildingid={params.buildingid}
filters={{ startDate: startDate ? startDate.toDate('UTC') : null, endDate: endDate ? endDate.toDate('UTC') : null, showWaste, showElectricity, showGas }}
graphType={graphType}
/>
</div>
</div>
);
}

View File

@@ -0,0 +1,17 @@
// app/buildings/[buildingid]/layout.tsx
import Sidebar from "../../../components/sidebar";
export default function BuildingLayout({
children,
params
}: {
children: React.ReactNode;
params: { buildingid: string };
}) {
return (
<div className="flex h-screen w-full">
<Sidebar buildingid={params.buildingid} />
<main className="flex-1 max-h-screen overflow-y-auto">{children}</main>
</div>
);
}

View File

@@ -0,0 +1,13 @@
// app/buildings/[buildingid]/page.tsx
interface BuildingPageProps {
params: { buildingid: string };
}
export default function BuildingPage({ params }: BuildingPageProps) {
return (
<div className="flex items-center justify-center text-center h-full">
Select a tab to view information about this building.
</div>
);
}

View File

@@ -0,0 +1,136 @@
"use client";
import React, { useRef, useState, useCallback, useEffect } from 'react';
import Webcam from 'react-webcam';
import { Card, CardBody } from '@nextui-org/card';
import { Button } from '@nextui-org/button';
import screenfull from 'screenfull';
const TrashScanner: React.FC = () => {
const webcamRef = useRef<Webcam>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
const [isDrawing, setIsDrawing] = useState(false);
const [startPos, setStartPos] = useState({ x: 0, y: 0 });
const [endPos, setEndPos] = useState({ x: 0, y: 0 });
const containerRef = useRef<HTMLDivElement>(null);
//canvas size
const setCanvasSize = useCallback(() => {
const video = webcamRef.current?.video;
const canvas = canvasRef.current;
if (video && canvas) {
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
}
}, []);
// listener to set canvas size when video is ready
useEffect(() => {
const video = webcamRef.current?.video;
if (video) {
video.addEventListener('loadedmetadata', setCanvasSize);
}
return () => {
if (video) {
video.removeEventListener('loadedmetadata', setCanvasSize);
}
};
}, [setCanvasSize]);
const handleMouseDown = useCallback((e: React.MouseEvent<HTMLCanvasElement>) => {
const canvas = canvasRef.current;
if (!canvas) return;
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const x = (e.clientX - rect.left) * scaleX;
const y = (e.clientY - rect.top) * scaleY;
setIsDrawing(true);
setStartPos({ x, y });
setEndPos({ x, y });
}, []);
const handleMouseMove = useCallback((e: React.MouseEvent<HTMLCanvasElement>) => {
if (!isDrawing) return;
const canvas = canvasRef.current;
if (!canvas) return;
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const x = (e.clientX - rect.left) * scaleX;
const y = (e.clientY - rect.top) * scaleY;
setEndPos({ x, y });
}, [isDrawing]);
const handleMouseUp = useCallback(() => {
setIsDrawing(false);
}, []);
const drawBox = useCallback(() => {
const canvas = canvasRef.current;
const ctx = canvas?.getContext('2d');
if (!ctx || !canvas) return;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.strokeStyle = 'red';
ctx.lineWidth = 2;
ctx.strokeRect(
Math.min(startPos.x, endPos.x),
Math.min(startPos.y, endPos.y),
Math.abs(endPos.x - startPos.x),
Math.abs(endPos.y - startPos.y)
);
}, [startPos, endPos]);
React.useEffect(() => {
drawBox();
}, [drawBox]);
const toggleFullScreen = useCallback(() => {
if (containerRef.current && screenfull.isEnabled) {
screenfull.toggle(containerRef.current);
}
}, []);
return (
<div className="container-fluid p-4">
<h1 className="text-2xl font-bold text-center mb-4">Trash Scanner</h1>
<div className="flex flex-col items-center">
<Card className="w-full md:w-auto md:max-w-[640px] mb-4">
<CardBody className="p-0">
<div ref={containerRef} className="relative aspect-video">
<Webcam
audio={false}
ref={webcamRef}
screenshotFormat="image/jpeg"
className="w-full h-full object-cover rounded-lg"
/>
<canvas
ref={canvasRef}
className="absolute top-0 left-0 w-full h-full rounded-lg"
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseUp}
/>
</div>
</CardBody>
</Card>
<Button
onClick={toggleFullScreen}
className="w-full md:w-auto md:max-w-[640px]"
>
Toggle Fullscreen
</Button>
</div>
</div>
);
};
export default TrashScanner;

View File

@@ -0,0 +1,7 @@
// app/buildings/[buildingid]/trash-scanner/page.tsx
import RealtimeModel from "@/components/trashDetection";
export default function TrashScanner() {
return <RealtimeModel />;
};

View File

@@ -0,0 +1,177 @@
"use client";
import { useParams } from "next/navigation";
import { useState } from "react";
import { Button } from "@nextui-org/button";
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter } from "@nextui-org/modal";
import { Input } from "@nextui-org/input";
import { Timestamp } from "firebase/firestore";
import { Table, TableHeader, TableColumn, TableBody, TableRow, TableCell } from "@nextui-org/table";
import { Select, SelectItem } from "@nextui-org/react";
import { useBuilding, WasteDataPoint } from "@/lib/useBuildingData";
import { trashItems } from "@/components/trashcanMode";
export default function TrashPage() {
const { buildingid } = useParams();
const { data: building, isLoading, error, updateBuilding } = useBuilding(buildingid as string);
const [isModalOpen, setIsModalOpen] = useState(false);
const [newEntry, setNewEntry] = useState({
timestamp: new Date().toISOString().slice(0, 16),
type: "",
trashcanID: "",
wasteCategory: "",
emissions: 0,
});
const [sortConfig, setSortConfig] = useState<{ key: keyof WasteDataPoint; direction: 'ascending' | 'descending' } | null>(null);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
if (!building) return <div>Building not found</div>;
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
const { name, value } = e.target;
if (name === 'emissions') {
const inputValue = parseFloat(value);
const scaledValue = isNaN(inputValue) ? 0 : inputValue / 1e+3;
setNewEntry(prev => ({ ...prev, [name]: scaledValue }));
} else {
setNewEntry(prev => ({ ...prev, [name]: value }));
}
};
const handleSubmit = () => {
const updatedWasteGeneration = [
...building!.wasteGeneration,
{ ...newEntry, timestamp: Timestamp.fromDate(new Date(newEntry.timestamp)), emissions: Number(newEntry.emissions) }
];
updateBuilding({ wasteGeneration: updatedWasteGeneration as WasteDataPoint[] });
setIsModalOpen(false);
};
const handleSort = (key: keyof WasteDataPoint) => {
let direction: 'ascending' | 'descending' = 'ascending';
if (sortConfig && sortConfig.key === key && sortConfig.direction === 'ascending') {
direction = 'descending';
}
setSortConfig({ key, direction });
};
const sortedWasteGeneration = [...building.wasteGeneration].sort((a, b) => {
if (!sortConfig) return 0;
const { key, direction } = sortConfig;
if (a[key] < b[key]) return direction === 'ascending' ? -1 : 1;
if (a[key] > b[key]) return direction === 'ascending' ? 1 : -1;
return 0;
});
const handleDelete = (index: number) => {
updateBuilding({ operation: 'deleteWasteEntry', index });
};
return (
<div className="container mx-auto p-4">
<h1 className="text-2xl font-bold mb-4">Waste Data for {building?.name}</h1>
<Table aria-label="Waste data table">
<TableHeader>
<TableColumn key="timestamp" onClick={() => handleSort('timestamp')}>Timestamp</TableColumn>
<TableColumn key="wasteCategory" onClick={() => handleSort('wasteCategory')}>Name</TableColumn>
<TableColumn key="type" onClick={() => handleSort('type')}>Trash Category</TableColumn>
<TableColumn key="trashcanID" onClick={() => handleSort('trashcanID')}>Trashcan ID</TableColumn>
<TableColumn key="emissions" onClick={() => handleSort('emissions')}>Emissions (kg ofCO2e)</TableColumn>
<TableColumn key="actions">Actions</TableColumn>
</TableHeader>
<TableBody>
{sortedWasteGeneration.map((wastePoint, index) => (
<TableRow key={index}>
<TableCell>{new Date(wastePoint.timestamp.seconds * 1000).toLocaleString()}</TableCell>
<TableCell>{wastePoint.wasteCategory}</TableCell>
<TableCell>{wastePoint.type}</TableCell>
<TableCell>{wastePoint.trashcanID}</TableCell>
<TableCell>{(wastePoint.emissions * 1e+3).toFixed(0)}</TableCell>
<TableCell>
<Button color="danger" size="sm" onPress={() => handleDelete(index)}>Delete</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<Button className="mt-4" onPress={() => setIsModalOpen(true)}>
Add New Entry
</Button>
<Modal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)}>
<ModalContent>
<ModalHeader>
<h2 className="text-lg font-semibold">Add New Waste Entry</h2>
</ModalHeader>
<ModalBody>
<Input
label="Timestamp"
name="timestamp"
type="datetime-local"
value={newEntry.timestamp}
onChange={handleInputChange}
/>
<Select
className="w-full"
label="Type"
name="type"
selectedKeys={[newEntry.type]}
onChange={handleInputChange}
>
{trashItems.map((item) => (
<SelectItem key={item.id} value={item.id}>
{item.name}
</SelectItem>
))}
</Select>
<Input
label="Trashcan ID"
name="trashcanID"
value={newEntry.trashcanID}
onChange={handleInputChange}
/>
<Select
label="Waste Category"
name="wasteCategory"
selectedKeys={[newEntry.wasteCategory]}
onChange={handleInputChange}
>
<SelectItem key="Landfill" value="Landfill">
Landfill
</SelectItem>
<SelectItem key="Recycling" value="Recycling">
Recycling
</SelectItem>
<SelectItem key="Compost" value="Compost">
Compost
</SelectItem>
</Select>
<Input
label="Emissions (grams of CO2e)"
name="emissions"
type="number"
value={(newEntry.emissions * 1e+3).toString()} // Multiply by 1e+3 for display
onChange={handleInputChange}
/>
</ModalBody>
<ModalFooter>
<Button color="danger" variant="light" onPress={() => setIsModalOpen(false)}>
Cancel
</Button>
<Button color="primary" onPress={handleSubmit}>
Add Entry
</Button>
</ModalFooter>
</ModalContent>
</Modal>
</div>
);
}

View File

@@ -0,0 +1,7 @@
// app/buildings/[buildingid]/trashcan-mode/page.tsx
import TrashcanMode from "@/components/trashcanMode";
export default function TrashcanModePage() {
return <TrashcanMode />;
}