Initial Code Commit
This commit is contained in:
100
Project/app/api/buildings/route.ts
Executable file
100
Project/app/api/buildings/route.ts
Executable file
@@ -0,0 +1,100 @@
|
||||
// @ts-nocheck
|
||||
import { NextResponse } from 'next/server';
|
||||
import { CosmosClient } from "@azure/cosmos";
|
||||
|
||||
const cosmosClient = new CosmosClient({
|
||||
endpoint: process.env.COSMOS_ENDPOINT!,
|
||||
key: process.env.COSMOS_KEY!
|
||||
});
|
||||
|
||||
const database = cosmosClient.database(process.env.COSMOS_DATABASE_ID!);
|
||||
const container = database.container(process.env.COSMOS_CONTAINER_ID!);
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const id = searchParams.get('id');
|
||||
|
||||
console.log("Received GET request with id:", id);
|
||||
|
||||
try {
|
||||
if (id) {
|
||||
// Get a single building
|
||||
console.log("Attempting to get building with id:", id);
|
||||
|
||||
const querySpec = {
|
||||
query: "SELECT * FROM c WHERE c.id = @id",
|
||||
parameters: [{ name: "@id", value: id }]
|
||||
};
|
||||
|
||||
const { resources } = await container.items.query(querySpec).fetchAll();
|
||||
console.log("Query result:", resources);
|
||||
|
||||
if (resources && resources.length > 0) {
|
||||
console.log("Returning resource for id:", id);
|
||||
return NextResponse.json(resources[0]);
|
||||
} else {
|
||||
console.log("Building not found for id:", id);
|
||||
return NextResponse.json({ message: "Building not found" }, { status: 404 });
|
||||
}
|
||||
} else {
|
||||
// Get all buildings
|
||||
console.log("Attempting to get all buildings");
|
||||
const { resources } = await container.items.readAll().fetchAll();
|
||||
console.log("Number of buildings retrieved:", resources.length);
|
||||
|
||||
return NextResponse.json(resources);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error in GET request:", error);
|
||||
return NextResponse.json({ message: "Error fetching data", error }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// function deepMerge(target: any, source: any) {
|
||||
// for (const key in source) {
|
||||
// if (Array.isArray(source[key])) {
|
||||
// if (!target[key]) target[key] = [];
|
||||
// target[key] = [...target[key], ...source[key]];
|
||||
// } else if (source[key] instanceof Object && key in target) {
|
||||
// deepMerge(target[key], source[key]);
|
||||
// } else {
|
||||
// target[key] = source[key];
|
||||
// }
|
||||
// }
|
||||
// return target;
|
||||
// }
|
||||
|
||||
export async function PATCH(request: Request) {
|
||||
try {
|
||||
const { id, operation, ...data } = await request.json();
|
||||
|
||||
// Query for the existing item
|
||||
const querySpec = {
|
||||
query: "SELECT * FROM c WHERE c.id = @id",
|
||||
parameters: [{ name: "@id", value: id }]
|
||||
};
|
||||
|
||||
const { resources } = await container.items.query(querySpec).fetchAll();
|
||||
|
||||
let existingItem = resources[0] || { id };
|
||||
|
||||
if (operation === 'deleteWasteEntry') {
|
||||
// Remove the waste entry at the specified index
|
||||
const index = data.index;
|
||||
existingItem.wasteGeneration.splice(index, 1);
|
||||
} else {
|
||||
// Deep merge the existing data with the new data
|
||||
existingItem = { ...existingItem, ...data };
|
||||
}
|
||||
|
||||
|
||||
// Upsert the item
|
||||
const { resource: result } = await container.items.upsert(existingItem);
|
||||
|
||||
console.log("Update successful. Result:", result);
|
||||
|
||||
return NextResponse.json({ message: "Building updated successfully", result });
|
||||
} catch (error) {
|
||||
return NextResponse.json({ message: "Error updating data", error }, { status: 500 });
|
||||
}
|
||||
}
|
||||
74
Project/app/api/chat/route.ts
Executable file
74
Project/app/api/chat/route.ts
Executable file
@@ -0,0 +1,74 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const { imageURL, type } = await request.json();
|
||||
|
||||
if (!imageURL) {
|
||||
return NextResponse.json({ error: "No image URL provided" }, { status: 400 });
|
||||
}
|
||||
|
||||
const payload = {
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: ` Analyze the following ${type} bill image and extract the following information:
|
||||
1. Multiple data points of usage, each with a date and ${type === 'gas' ? 'therms' : 'kWh'} used
|
||||
2. Any other relevant usage data
|
||||
|
||||
Format the output as a JSON object with an array of data points and any additional data.
|
||||
You must output valid JSON in the following format, or an empty array if no data is found:
|
||||
{
|
||||
"dataPoints": [
|
||||
{
|
||||
"date": "<ISO 8601 date string>",
|
||||
"usage": <number>
|
||||
},
|
||||
// ... more data points
|
||||
]
|
||||
}
|
||||
`
|
||||
},
|
||||
{
|
||||
type: "image_url",
|
||||
image_url: {
|
||||
url: imageURL
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
temperature: 0.4,
|
||||
top_p: 0.95,
|
||||
max_tokens: 1000
|
||||
};
|
||||
|
||||
const response = await fetch(process.env.AZURE_OPENAI_ENDPOINT as string, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'api-key': process.env.AZURE_OPENAI_KEY as string,
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
console.log('CHAT RESPONSE', response);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to generate description: ' + response.status + " " + response.statusText);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const description = data.choices[0].message.content;
|
||||
|
||||
console.log("CHAT DESCRIPTION", description);
|
||||
return NextResponse.json({ response: description });
|
||||
} catch (error) {
|
||||
console.error('Error processing chat:', error);
|
||||
return NextResponse.json({ error: (error as Error).message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
43
Project/app/api/pdf-to-image/route.ts
Executable file
43
Project/app/api/pdf-to-image/route.ts
Executable file
@@ -0,0 +1,43 @@
|
||||
// @ts-nocheck
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { toBase64 } from 'openai/core';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const formData = await request.formData();
|
||||
|
||||
let res = await fetch(process.env.PDF_URI, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
res = await res.json();
|
||||
|
||||
const pdfBuffer = await res[0];
|
||||
|
||||
let b64 = await toBase64(pdfBuffer);
|
||||
console.log(b64);
|
||||
console.log(request);
|
||||
|
||||
// Step 2: Use the image with the chat route
|
||||
const chatResponse = await fetch(process.env.PROD_URL + '/api/chat', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
imageURL: `data:image/png;base64,${b64}`,
|
||||
type: formData.get('type'),
|
||||
}),
|
||||
});
|
||||
|
||||
const chatData = await chatResponse.json();
|
||||
console.log("CHAT RESPONSE", chatData);
|
||||
|
||||
return NextResponse.json({ message: 'PDF converted successfully', response: chatData });
|
||||
} catch (error) {
|
||||
console.error('Error processing PDF:', error);
|
||||
return NextResponse.json({ error: 'Failed to process PDF' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
217
Project/app/buildings/[buildingid]/emissions/page.tsx
Executable file
217
Project/app/buildings/[buildingid]/emissions/page.tsx
Executable 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>
|
||||
);
|
||||
}
|
||||
17
Project/app/buildings/[buildingid]/layout.tsx
Executable file
17
Project/app/buildings/[buildingid]/layout.tsx
Executable 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>
|
||||
);
|
||||
}
|
||||
13
Project/app/buildings/[buildingid]/page.tsx
Executable file
13
Project/app/buildings/[buildingid]/page.tsx
Executable 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>
|
||||
);
|
||||
}
|
||||
136
Project/app/buildings/[buildingid]/trash-scanner/oldpage.tsx
Executable file
136
Project/app/buildings/[buildingid]/trash-scanner/oldpage.tsx
Executable 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;
|
||||
7
Project/app/buildings/[buildingid]/trash-scanner/page.tsx
Executable file
7
Project/app/buildings/[buildingid]/trash-scanner/page.tsx
Executable file
@@ -0,0 +1,7 @@
|
||||
// app/buildings/[buildingid]/trash-scanner/page.tsx
|
||||
|
||||
import RealtimeModel from "@/components/trashDetection";
|
||||
|
||||
export default function TrashScanner() {
|
||||
return <RealtimeModel />;
|
||||
};
|
||||
177
Project/app/buildings/[buildingid]/trash/page.tsx
Executable file
177
Project/app/buildings/[buildingid]/trash/page.tsx
Executable 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>
|
||||
);
|
||||
}
|
||||
7
Project/app/buildings/[buildingid]/trashcan-mode/page.tsx
Executable file
7
Project/app/buildings/[buildingid]/trashcan-mode/page.tsx
Executable file
@@ -0,0 +1,7 @@
|
||||
// app/buildings/[buildingid]/trashcan-mode/page.tsx
|
||||
|
||||
import TrashcanMode from "@/components/trashcanMode";
|
||||
|
||||
export default function TrashcanModePage() {
|
||||
return <TrashcanMode />;
|
||||
}
|
||||
61
Project/app/buildings/page.tsx
Executable file
61
Project/app/buildings/page.tsx
Executable file
@@ -0,0 +1,61 @@
|
||||
// app/buildings/page.tsx
|
||||
"use client";
|
||||
|
||||
import { Card, CardHeader, CardFooter, Image, Button, Skeleton } from "@nextui-org/react";
|
||||
import Link from "next/link";
|
||||
|
||||
import { useBuildingList } from "@/lib/useBuildingData";
|
||||
|
||||
|
||||
export default function BuildingsPage() {
|
||||
const { data: buildings, isLoading, error } = useBuildingList();
|
||||
|
||||
if (isLoading) return (
|
||||
<div className="grid grid-cols-12 gap-4 p-4">
|
||||
{[...Array(3)].map((_, index) => (
|
||||
<Card key={index} className="w-full h-[300px] col-span-12 sm:col-span-6 md:col-span-4">
|
||||
<Skeleton className="rounded-lg">
|
||||
<div className="h-[300px]" />
|
||||
</Skeleton>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
if (error) return <div>Error: {error.message}</div>;
|
||||
|
||||
if (buildings) return (
|
||||
<div className="grid grid-cols-12 gap-4 p-4 h-full bg-orange-900/5">
|
||||
{buildings.map(building => (
|
||||
<Card
|
||||
key={building.id}
|
||||
isFooterBlurred
|
||||
className="w-full h-[300px] col-span-12 sm:col-span-6 md:col-span-4"
|
||||
>
|
||||
<CardHeader className="absolute z-10 top-1 flex-col items-start bg-gray-800/5 backdrop-blur-lg rounded-none -mt-1">
|
||||
<h4 className="text-white font-medium text-2xl">{building.name}</h4>
|
||||
<p className="text-white/60 text-small">{building.address}</p>
|
||||
</CardHeader>
|
||||
<Image
|
||||
removeWrapper
|
||||
alt={`${building.name} image`}
|
||||
className="z-0 w-full h-full object-cover"
|
||||
src={building.imageURL}
|
||||
/>
|
||||
<CardFooter className="absolute bg-black/40 bottom-0 z-10 justify-between">
|
||||
<div>
|
||||
<p className="text-white text-tiny">Year Built: {building.yearBuilt}</p>
|
||||
<p className="text-white text-tiny">Square Footage: {building.squareFeet.toLocaleString()}</p>
|
||||
</div>
|
||||
<Link href={`/buildings/${building.id}/emissions`}>
|
||||
<Button className="text-tiny" color="primary" radius="full" size="sm">
|
||||
View Building
|
||||
</Button>
|
||||
</Link>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
return <div>No buildings found</div>;
|
||||
}
|
||||
31
Project/app/error.tsx
Executable file
31
Project/app/error.tsx
Executable file
@@ -0,0 +1,31 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
|
||||
export default function Error({
|
||||
error,
|
||||
reset,
|
||||
}: {
|
||||
error: Error;
|
||||
reset: () => void;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
// Log the error to an error reporting service
|
||||
/* eslint-disable no-console */
|
||||
console.error(error);
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>Something went wrong!</h2>
|
||||
<button
|
||||
onClick={
|
||||
// Attempt to recover by trying to re-render the segment
|
||||
() => reset()
|
||||
}
|
||||
>
|
||||
Try again
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
24
Project/app/featureBox.tsx
Executable file
24
Project/app/featureBox.tsx
Executable file
@@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
|
||||
interface FeatureBoxProps {
|
||||
title: string;
|
||||
description: string;
|
||||
theme?: string;
|
||||
}
|
||||
|
||||
const FeatureBox: React.FC<FeatureBoxProps> = ({ title, description, theme }) => {
|
||||
const isDarkMode = theme === 'dark';
|
||||
|
||||
return (
|
||||
<div className={`p-6 rounded-lg shadow-md ${isDarkMode ? 'bg-gray-800' : 'bg-white'}`}>
|
||||
<h3 className={`text-xl font-bold mb-2 ${isDarkMode ? 'text-white' : 'text-gray-800'}`}>
|
||||
{title}
|
||||
</h3>
|
||||
<p className={isDarkMode ? 'text-gray-300' : 'text-gray-600'}>
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeatureBox;
|
||||
54
Project/app/layout.tsx
Executable file
54
Project/app/layout.tsx
Executable file
@@ -0,0 +1,54 @@
|
||||
import "@/styles/globals.css";
|
||||
import { Metadata, Viewport } from "next";
|
||||
import clsx from "clsx";
|
||||
|
||||
import { Providers } from "./providers";
|
||||
|
||||
import { siteConfig } from "@/config/site";
|
||||
import { fontSans } from "@/config/fonts";
|
||||
import { Navbar } from "@/components/navbar";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
default: siteConfig.name,
|
||||
template: `%s - ${siteConfig.name}`,
|
||||
},
|
||||
description: siteConfig.description,
|
||||
icons: {
|
||||
icon: "/favicon.ico",
|
||||
},
|
||||
};
|
||||
|
||||
export const viewport: Viewport = {
|
||||
themeColor: [
|
||||
{ media: "(prefers-color-scheme: light)", color: "white" },
|
||||
{ media: "(prefers-color-scheme: dark)", color: "black" },
|
||||
],
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html suppressHydrationWarning lang="en">
|
||||
<head />
|
||||
<body
|
||||
className={clsx(
|
||||
"min-h-screen bg-background font-sans antialiased",
|
||||
fontSans.variable,
|
||||
)}
|
||||
>
|
||||
<Providers themeProps={{ attribute: "class", defaultTheme: "light" }}>
|
||||
<div className="flex flex-col min-h-screen">
|
||||
<Navbar />
|
||||
<main className="flex-1">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
</Providers>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
53
Project/app/mission/page.tsx
Executable file
53
Project/app/mission/page.tsx
Executable file
@@ -0,0 +1,53 @@
|
||||
'use client'
|
||||
|
||||
import Image from 'next/image';
|
||||
import { Button } from "@nextui-org/react";
|
||||
import Link from "next/link";
|
||||
export default function MissionPage() {
|
||||
return (
|
||||
<div className="relative min-h-screen bg-orange-900/5">
|
||||
{/* Large "About Us" text at the top */}
|
||||
<div className="absolute top-0 left-0 right-0 z-10 flex justify-center items-center h-40">
|
||||
<h1 className="text-6xl font-bold bg-gradient-to-r from-[#FF705B] to-[#FFB457] text-transparent bg-clip-text drop-shadow-lg">Our Mission</h1>
|
||||
</div>
|
||||
|
||||
<div className="relative min-h-screen">
|
||||
<Image
|
||||
src="/demo.png"
|
||||
alt="Demo image"
|
||||
width={1920}
|
||||
height={3000}
|
||||
className="w-full h-auto"
|
||||
/>
|
||||
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-background via-background to-transparent h-80 dark:from-background dark:via-background">
|
||||
{/* text at the bottom */}
|
||||
<div className="flex flex-col self-end items-center pb-16">
|
||||
<p className="mt-40 text-lg text-center max-w-2xl">
|
||||
One of the most neglected aspects of a building's carbon footprint is waste mismanagement and poor recycling practices. According to the EPA, landfills account for <span className="bg-gradient-to-r from-[#FF705B] to-[#FFB457] text-transparent bg-clip-text">15% of U.S. methane emissions</span>, with commercial buildings generating over <span className="bg-gradient-to-r from-[#FF705B] to-[#FFB457] text-transparent bg-clip-text">30% of the nation's total waste</span>.
|
||||
<br />
|
||||
<br />
|
||||
Studies show that up to <span className="bg-gradient-to-r from-[#FF705B] to-[#FFB457] text-transparent bg-clip-text">25% of items</span> in recycling bins are actually non-recyclable, contaminating entire loads. Only 32% of commercial waste is recycled, compared to a potential 75% that could be. Proper recycling can reduce a building's carbon emissions <span className="bg-gradient-to-r from-[#FF705B] to-[#FFB457] text-transparent bg-clip-text">by up to 40%</span>.
|
||||
<br />
|
||||
<br />
|
||||
<strong>Carbin</strong> uses a machine learning algorithm to identify the type of waste at the trash chute, nudging the occupants to recycle correctly with a friendly reminder. Our long term goal is to <span className="bg-gradient-to-r from-[#FF705B] to-[#FFB457] text-transparent bg-clip-text">educate building occupants</span>, something we know will truly revolutionize waste management, make efficient sorting and recycling the norm, and significantly curtail the carbon impact of our daily operations.
|
||||
</p>
|
||||
<Button
|
||||
as={Link}
|
||||
href="/buildings"
|
||||
variant="solid"
|
||||
size="lg"
|
||||
endContent={
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clipRule="evenodd" />
|
||||
</svg>
|
||||
}
|
||||
className="mb-2 mt-6 bg-orange-500 text-white"
|
||||
>
|
||||
Intrigued? Explore participating buildings
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div >
|
||||
);
|
||||
}
|
||||
86
Project/app/page.tsx
Executable file
86
Project/app/page.tsx
Executable file
@@ -0,0 +1,86 @@
|
||||
'use client';
|
||||
|
||||
import { Link } from "@nextui-org/link";
|
||||
import { button as buttonStyles } from "@nextui-org/theme";
|
||||
import { useTheme } from "next-themes";
|
||||
|
||||
import { title, subtitle } from "@/components/primitives";
|
||||
import FeatureBox from "@/app/featureBox";
|
||||
|
||||
export default function Home() {
|
||||
const { theme } = useTheme();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
<section className="flex-grow flex flex-col items-center justify-center gap-4 py-8 md:py-10 overflow-hidden relative">
|
||||
<div className="absolute inset-0 z-0">
|
||||
<video
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
playsInline
|
||||
className="object-cover w-full h-full"
|
||||
>
|
||||
<source src="/homepage-video.mp4" type="video/mp4" />
|
||||
<a href="https://www.vecteezy.com/free-videos/city-time-lapse-at-night">City Time Lapse At Night Stock Videos by Vecteezy</a>
|
||||
</video>
|
||||
<div className="absolute inset-0 bg-orange-900 opacity-60" />
|
||||
</div>
|
||||
<div className="relative z-10 text-left w-full max-w-4xl px-6">
|
||||
<div className="mb-4">
|
||||
<span className={title({ class: "text-white" })}>
|
||||
Your platform for{" "}
|
||||
</span>
|
||||
<span className={title({ color: "yellow" })}>
|
||||
building
|
||||
</span>
|
||||
<br />
|
||||
<span className={title({ class: "text-white" })}>
|
||||
a sustainable future
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className={subtitle({ class: "mt-4 text-gray-200" })}>
|
||||
Encourage <span className="text-orange-400">student participation</span> in responsible waste management with smart bins that guide proper disposal.
|
||||
</div>
|
||||
|
||||
<div className="mt-8">
|
||||
<Link
|
||||
className={buttonStyles({
|
||||
color: "warning",
|
||||
radius: "full",
|
||||
variant: "shadow",
|
||||
size: "lg",
|
||||
})}
|
||||
href="/buildings"
|
||||
>
|
||||
Get Started
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="w-full bg-white dark:bg-gray-900 py-16">
|
||||
<div className="max-w-4xl mx-auto px-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
<FeatureBox
|
||||
title="Smart Bins with Real-time Feedback"
|
||||
description="Utilize our AI-powered smart bins to guide students on proper waste disposal, and get immediate feedback."
|
||||
theme={theme}
|
||||
/>
|
||||
<FeatureBox
|
||||
title="Track Waste and Emissions"
|
||||
description="Log your building's trash and monitor emissions over time, giving you insights into your waste management efficiency."
|
||||
theme={theme}
|
||||
/>
|
||||
<FeatureBox
|
||||
title="Measure Your Impact"
|
||||
description="Track your building's emissions reduction and see the emissions saved through our smart bin system."
|
||||
theme={theme}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
26
Project/app/providers.tsx
Executable file
26
Project/app/providers.tsx
Executable file
@@ -0,0 +1,26 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { NextUIProvider } from "@nextui-org/system";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
||||
import { ThemeProviderProps } from "next-themes/dist/types";
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
|
||||
export interface ProvidersProps {
|
||||
children: React.ReactNode;
|
||||
themeProps?: ThemeProviderProps;
|
||||
}
|
||||
|
||||
export function Providers({ children, themeProps }: ProvidersProps) {
|
||||
const router = useRouter();
|
||||
const [queryClient] = React.useState(() => new QueryClient());
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<NextUIProvider navigate={router.push}>
|
||||
<NextThemesProvider {...themeProps}>{children}</NextThemesProvider>
|
||||
</NextUIProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user