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,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
View 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 });
}
}

View 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 });
}
}

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 />;
}

61
Project/app/buildings/page.tsx Executable file
View 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
View 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
View 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
View 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
View 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&apos;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&apos;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&apos;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
View 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
View 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>
);
}