Initial Code Commit
This commit is contained in:
32
Project/.dockerignore
Executable file
32
Project/.dockerignore
Executable file
@@ -0,0 +1,32 @@
|
||||
# Ignore node_modules directory
|
||||
node_modules
|
||||
|
||||
# Ignore npm debug log
|
||||
npm-debug.log
|
||||
|
||||
# Ignore Dockerfile and .dockerignore file
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
|
||||
# Ignore build output
|
||||
/build
|
||||
/dist
|
||||
|
||||
# Ignore logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Ignore temporary files
|
||||
tmp
|
||||
temp
|
||||
*.tmp
|
||||
*.temp
|
||||
|
||||
# Ignore coverage directory
|
||||
coverage
|
||||
|
||||
# Ignore .git directory and related files
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
|
||||
20
Project/.eslintignore
Executable file
20
Project/.eslintignore
Executable file
@@ -0,0 +1,20 @@
|
||||
.now/*
|
||||
*.css
|
||||
.changeset
|
||||
dist
|
||||
esm/*
|
||||
public/*
|
||||
tests/*
|
||||
scripts/*
|
||||
*.config.js
|
||||
.DS_Store
|
||||
node_modules
|
||||
coverage
|
||||
.next
|
||||
build
|
||||
!.commitlintrc.cjs
|
||||
!.lintstagedrc.cjs
|
||||
!jest.config.js
|
||||
!plopfile.js
|
||||
!react-shim.js
|
||||
!tsup.config.ts
|
||||
122
Project/.eslintrc.json
Executable file
122
Project/.eslintrc.json
Executable file
@@ -0,0 +1,122 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/eslintrc.json",
|
||||
"env": {
|
||||
"browser": false,
|
||||
"es2021": true,
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"next",
|
||||
"plugin:react/recommended",
|
||||
"plugin:prettier/recommended",
|
||||
"plugin:react-hooks/recommended",
|
||||
"plugin:jsx-a11y/recommended"
|
||||
],
|
||||
"plugins": [
|
||||
"react",
|
||||
"unused-imports",
|
||||
"import",
|
||||
"@typescript-eslint",
|
||||
"jsx-a11y",
|
||||
"prettier"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
},
|
||||
"ecmaVersion": 12,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "detect"
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"react/no-unescaped-entities": "off",
|
||||
"@next/next/no-page-custom-font": "off",
|
||||
"no-console": "warn",
|
||||
"react/prop-types": "off",
|
||||
"react/jsx-uses-react": "off",
|
||||
"react/react-in-jsx-scope": "off",
|
||||
"react-hooks/exhaustive-deps": "off",
|
||||
"jsx-a11y/click-events-have-key-events": "warn",
|
||||
"jsx-a11y/interactive-supports-focus": "warn",
|
||||
"prettier/prettier": "off",
|
||||
"no-unused-vars": "off",
|
||||
"unused-imports/no-unused-vars": "off",
|
||||
"unused-imports/no-unused-imports": "warn",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
"args": "after-used",
|
||||
"ignoreRestSiblings": false,
|
||||
"argsIgnorePattern": "^_.*?$"
|
||||
}
|
||||
],
|
||||
"import/order": [
|
||||
"warn",
|
||||
{
|
||||
"groups": [
|
||||
"type",
|
||||
"builtin",
|
||||
"object",
|
||||
"external",
|
||||
"internal",
|
||||
"parent",
|
||||
"sibling",
|
||||
"index"
|
||||
],
|
||||
"pathGroups": [
|
||||
{
|
||||
"pattern": "~/**",
|
||||
"group": "external",
|
||||
"position": "after"
|
||||
}
|
||||
],
|
||||
"newlines-between": "always"
|
||||
}
|
||||
],
|
||||
"react/self-closing-comp": "warn",
|
||||
"react/jsx-sort-props": [
|
||||
"warn",
|
||||
{
|
||||
"callbacksLast": true,
|
||||
"shorthandFirst": true,
|
||||
"noSortAlphabetically": false,
|
||||
"reservedFirst": true
|
||||
}
|
||||
],
|
||||
"padding-line-between-statements": [
|
||||
"warn",
|
||||
{
|
||||
"blankLine": "always",
|
||||
"prev": "*",
|
||||
"next": "return"
|
||||
},
|
||||
{
|
||||
"blankLine": "always",
|
||||
"prev": [
|
||||
"const",
|
||||
"let",
|
||||
"var"
|
||||
],
|
||||
"next": "*"
|
||||
},
|
||||
{
|
||||
"blankLine": "any",
|
||||
"prev": [
|
||||
"const",
|
||||
"let",
|
||||
"var"
|
||||
],
|
||||
"next": [
|
||||
"const",
|
||||
"let",
|
||||
"var"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
38
Project/.gitignore
vendored
Executable file
38
Project/.gitignore
vendored
Executable file
@@ -0,0 +1,38 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
.env
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
bun.lockb
|
||||
.env
|
||||
25
Project/Dockerfile
Executable file
25
Project/Dockerfile
Executable file
@@ -0,0 +1,25 @@
|
||||
|
||||
# Use the official Node.js image as the base image
|
||||
FROM node:20
|
||||
|
||||
# Set the working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package.json and package-lock.json
|
||||
COPY package*.json ./
|
||||
|
||||
# Install dependencies
|
||||
RUN npm install bun -g
|
||||
RUN bun install
|
||||
|
||||
# Copy the rest of the application code
|
||||
COPY . .
|
||||
|
||||
# Build the Next.js application
|
||||
RUN bun run build
|
||||
|
||||
# Expose the port the app runs on
|
||||
EXPOSE 3000
|
||||
|
||||
# Start the Next.js application
|
||||
CMD ["bun", "start"]
|
||||
1
Project/README.md
Executable file
1
Project/README.md
Executable file
@@ -0,0 +1 @@
|
||||
An app for tracking the carbon footprint of buildings.
|
||||
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>
|
||||
);
|
||||
}
|
||||
37
Project/components/addDataButton.tsx
Executable file
37
Project/components/addDataButton.tsx
Executable file
@@ -0,0 +1,37 @@
|
||||
// components/addDataButton.tsx
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Button } from "@nextui-org/react";
|
||||
|
||||
import { PlusIcon } from "./icons";
|
||||
import { UploadDataModal } from "./uploadDataModal";
|
||||
|
||||
import { useBuilding } from "@/lib/useBuildingData";
|
||||
|
||||
interface AddDataButtonProps {
|
||||
buildingid: string;
|
||||
}
|
||||
|
||||
export default function AddDataButton({ buildingid }: AddDataButtonProps) {
|
||||
const { updateBuilding } = useBuilding(buildingid);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
className="w-fit"
|
||||
startContent={<PlusIcon size={16} />}
|
||||
onPress={() => setIsModalOpen(true)}
|
||||
>
|
||||
Upload new data
|
||||
</Button>
|
||||
<UploadDataModal
|
||||
buildingid={buildingid}
|
||||
isOpen={isModalOpen}
|
||||
updateBuilding={updateBuilding}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
218
Project/components/emissionsGraph.tsx
Executable file
218
Project/components/emissionsGraph.tsx
Executable file
@@ -0,0 +1,218 @@
|
||||
// components/emissionsGraph.tsx
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { LineChart, Line, AreaChart, Area, PieChart, Pie, Cell, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
|
||||
|
||||
import { useBuilding, Building, ElectricityDataPoint, NaturalGasDataPoint, WasteDataPoint } from '@/lib/useBuildingData';
|
||||
|
||||
export type EmissionGraphFilters = {
|
||||
startDate?: Date | null;
|
||||
endDate?: Date | null;
|
||||
showWaste?: boolean;
|
||||
showElectricity?: boolean;
|
||||
showGas?: boolean;
|
||||
}
|
||||
|
||||
interface EmissionsGraphProps {
|
||||
buildingid: string;
|
||||
filters: EmissionGraphFilters;
|
||||
graphType: 'line' | 'area' | 'pie';
|
||||
}
|
||||
|
||||
type ChartDataPoint = {
|
||||
date: string;
|
||||
electricity: number;
|
||||
gas: number;
|
||||
waste: number;
|
||||
};
|
||||
|
||||
const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884D8'];
|
||||
|
||||
export default function EmissionsGraph({ buildingid, filters, graphType }: EmissionsGraphProps) {
|
||||
const { data: building, isLoading, error } = useBuilding(buildingid);
|
||||
|
||||
const chartData = useMemo(() => {
|
||||
if (!building) return [];
|
||||
|
||||
const dataMap = new Map<string, Partial<ChartDataPoint>>();
|
||||
|
||||
const addDataPoint = (date: Date, type: 'electricity' | 'gas' | 'waste', value: number) => {
|
||||
const dateString = date.toISOString().split('T')[0];
|
||||
const existingData = dataMap.get(dateString) || { date: dateString };
|
||||
existingData[type] = value;
|
||||
dataMap.set(dateString, existingData);
|
||||
};
|
||||
|
||||
// Collect all unique dates and data points
|
||||
const allDates = new Set<string>();
|
||||
const typedDataPoints: { [key: string]: { date: string, value: number }[] } = {
|
||||
electricity: [],
|
||||
gas: [],
|
||||
waste: []
|
||||
};
|
||||
|
||||
if (filters.showElectricity) {
|
||||
building.electricityUsage.forEach((point: ElectricityDataPoint) => {
|
||||
const date = new Date(point.timestamp.seconds * 1000);
|
||||
const dateString = date.toISOString().split('T')[0];
|
||||
allDates.add(dateString);
|
||||
typedDataPoints.electricity.push({ date: dateString, value: point.emissions });
|
||||
});
|
||||
}
|
||||
|
||||
if (filters.showGas) {
|
||||
building.naturalGasUsage.forEach((point: NaturalGasDataPoint) => {
|
||||
const date = new Date(point.timestamp.seconds * 1000);
|
||||
const dateString = date.toISOString().split('T')[0];
|
||||
allDates.add(dateString);
|
||||
typedDataPoints.gas.push({ date: dateString, value: point.emissions });
|
||||
});
|
||||
}
|
||||
|
||||
if (filters.showWaste) {
|
||||
building.wasteGeneration.forEach((point: WasteDataPoint) => {
|
||||
const date = new Date(point.timestamp.seconds * 1000);
|
||||
const dateString = date.toISOString().split('T')[0];
|
||||
allDates.add(dateString);
|
||||
typedDataPoints.waste.push({ date: dateString, value: point.emissions });
|
||||
});
|
||||
}
|
||||
|
||||
// Sort dates and data points
|
||||
const sortedDates = Array.from(allDates).sort();
|
||||
Object.values(typedDataPoints).forEach(points => points.sort((a, b) => a.date.localeCompare(b.date)));
|
||||
|
||||
// Interpolate missing values
|
||||
const interpolateValue = (date: string, points: { date: string, value: number }[]) => {
|
||||
const index = points.findIndex(p => p.date >= date);
|
||||
if (index === -1) return points[points.length - 1]?.value || 0;
|
||||
if (index === 0) return points[0].value;
|
||||
const prev = points[index - 1];
|
||||
const next = points[index];
|
||||
const totalDays = (new Date(next.date).getTime() - new Date(prev.date).getTime()) / (1000 * 60 * 60 * 24);
|
||||
const daysSincePrev = (new Date(date).getTime() - new Date(prev.date).getTime()) / (1000 * 60 * 60 * 24);
|
||||
return Number((prev.value + (next.value - prev.value) * (daysSincePrev / totalDays)).toFixed(3));
|
||||
};
|
||||
|
||||
// Fill in all data points
|
||||
sortedDates.forEach(date => {
|
||||
const point: Partial<ChartDataPoint> = { date };
|
||||
if (filters.showElectricity) point.electricity = interpolateValue(date, typedDataPoints.electricity);
|
||||
if (filters.showGas) point.gas = interpolateValue(date, typedDataPoints.gas);
|
||||
if (filters.showWaste) point.waste = interpolateValue(date, typedDataPoints.waste);
|
||||
dataMap.set(date, point);
|
||||
});
|
||||
|
||||
// Modify the return statement to truncate values
|
||||
return Array.from(dataMap.values())
|
||||
.filter(point => {
|
||||
const date = new Date(point.date || '');
|
||||
return (!filters.startDate || date >= filters.startDate) &&
|
||||
(!filters.endDate || date <= filters.endDate);
|
||||
})
|
||||
.map(point => ({
|
||||
...point,
|
||||
electricity: point.electricity ? Number(point.electricity.toFixed(3)) : undefined,
|
||||
gas: point.gas ? Number(point.gas.toFixed(3)) : undefined,
|
||||
waste: point.waste ? Number(point.waste.toFixed(3)) : undefined,
|
||||
}));
|
||||
}, [building, filters]);
|
||||
|
||||
const pieChartData = useMemo(() => {
|
||||
if (!building || !filters.showWaste) return [];
|
||||
|
||||
const wasteTypes = new Map<string, number>();
|
||||
|
||||
building.wasteGeneration.forEach((point: WasteDataPoint) => {
|
||||
const type = point.wasteCategory.toLowerCase();
|
||||
wasteTypes.set(type, (wasteTypes.get(type) || 0) + point.emissions);
|
||||
});
|
||||
|
||||
return Array.from(wasteTypes, ([name, value]) => ({ name, value: Number(value.toFixed(3)) }));
|
||||
}, [building, filters.showWaste]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="w-full h-96 bg-gray-200 animate-pulse rounded-lg">
|
||||
{/* Skeleton content */}
|
||||
<div className="h-full flex items-center justify-center">
|
||||
<span className="text-gray-400">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (error) return <div>Error: {error.message}</div>;
|
||||
|
||||
const renderLineChart = () => (
|
||||
<LineChart data={chartData}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="date" />
|
||||
<YAxis label={{ value: 'Emissions (metric tons CO2e)', angle: -90, position: 'insideLeft', dy: 96 }} />
|
||||
<Tooltip formatter={(value) => Number(value).toFixed(3)} />
|
||||
<Legend />
|
||||
{filters.showElectricity && building && building.electricityUsage.length > 0 &&
|
||||
<Line type="monotone" dataKey="electricity" stroke="#8884d8" name="Electricity" connectNulls />}
|
||||
{filters.showGas && building && building.naturalGasUsage.length > 0 &&
|
||||
<Line type="monotone" dataKey="gas" stroke="#82ca9d" name="Natural Gas" connectNulls />}
|
||||
{filters.showWaste && building && building.wasteGeneration.length > 0 &&
|
||||
<Line type="monotone" dataKey="waste" stroke="#ffc658" name="Waste" connectNulls />}
|
||||
</LineChart>
|
||||
);
|
||||
|
||||
const renderAreaChart = () => (
|
||||
<AreaChart data={chartData}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="date" />
|
||||
<YAxis label={{ value: 'Emissions (metric tons CO2e)', angle: -90, position: 'insideLeft' }} />
|
||||
<Tooltip formatter={(value) => Number(value).toFixed(3)} />
|
||||
<Legend />
|
||||
{filters.showElectricity && building && building.electricityUsage.length > 0 &&
|
||||
<Area type="monotone" dataKey="electricity" stackId="1" stroke="#8884d8" fill="#8884d8" name="Electricity" connectNulls />}
|
||||
{filters.showGas && building && building.naturalGasUsage.length > 0 &&
|
||||
<Area type="monotone" dataKey="gas" stackId="1" stroke="#82ca9d" fill="#82ca9d" name="Natural Gas" connectNulls />}
|
||||
{filters.showWaste && building && building.wasteGeneration.length > 0 &&
|
||||
<Area type="monotone" dataKey="waste" stackId="1" stroke="#ffc658" fill="#ffc658" name="Waste" connectNulls />}
|
||||
</AreaChart>
|
||||
);
|
||||
|
||||
const renderPieChart = () => (
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={pieChartData}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
labelLine={false}
|
||||
outerRadius={80}
|
||||
fill="#8884d8"
|
||||
dataKey="value"
|
||||
label={({ name, percent }) => `${name} ${(percent * 100).toFixed(0)}%`}
|
||||
>
|
||||
{pieChartData.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip />
|
||||
<Legend />
|
||||
</PieChart>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="w-full h-96">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
{(() => {
|
||||
switch (graphType) {
|
||||
case 'line':
|
||||
return renderLineChart();
|
||||
case 'area':
|
||||
return renderAreaChart();
|
||||
case 'pie':
|
||||
return renderPieChart();
|
||||
default:
|
||||
return <></>;
|
||||
}
|
||||
})()}
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
50
Project/components/face.tsx
Executable file
50
Project/components/face.tsx
Executable file
@@ -0,0 +1,50 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
interface FaceProps {
|
||||
bin: string;
|
||||
isVisible: boolean;
|
||||
itemPosition: { x: number; y: number } | null;
|
||||
videoDimensions: { width: number; height: number };
|
||||
facePosition: { x: number; y: number };
|
||||
}
|
||||
|
||||
const Face: React.FC<FaceProps> = ({ bin, isVisible, itemPosition, videoDimensions, facePosition }) => {
|
||||
// Calculate eye rotation based on item position
|
||||
const [eyeRotation, setEyeRotation] = useState<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (itemPosition && isVisible) {
|
||||
const dx = itemPosition.x - facePosition.x;
|
||||
const dy = itemPosition.y - facePosition.y;
|
||||
const angle = Math.atan2(dy, dx) * (180 / Math.PI);
|
||||
|
||||
setEyeRotation(angle);
|
||||
}
|
||||
}, [itemPosition, isVisible, facePosition]);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
animate={{ y: isVisible ? 0 : 100 }} // Animate up when visible
|
||||
className="face-container"
|
||||
initial={{ y: 100 }} // Start below the screen
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<div className="face">
|
||||
{/* Face SVG or Graphics */}
|
||||
<div className="eyes">
|
||||
<div
|
||||
className="eye left-eye"
|
||||
style={{ transform: `rotate(${eyeRotation}deg)` }}
|
||||
/>
|
||||
<div
|
||||
className="eye right-eye"
|
||||
style={{ transform: `rotate(${eyeRotation}deg)` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Face;
|
||||
230
Project/components/icons.tsx
Executable file
230
Project/components/icons.tsx
Executable file
@@ -0,0 +1,230 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { IconSvgProps } from "@/types";
|
||||
|
||||
export const GithubIcon: React.FC<IconSvgProps> = ({
|
||||
size = 24,
|
||||
width,
|
||||
height,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
height={size || height}
|
||||
viewBox="0 0 24 24"
|
||||
width={size || width}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
clipRule="evenodd"
|
||||
d="M12.026 2c-5.509 0-9.974 4.465-9.974 9.974 0 4.406 2.857 8.145 6.821 9.465.499.09.679-.217.679-.481 0-.237-.008-.865-.011-1.696-2.775.602-3.361-1.338-3.361-1.338-.452-1.152-1.107-1.459-1.107-1.459-.905-.619.069-.605.069-.605 1.002.07 1.527 1.028 1.527 1.028.89 1.524 2.336 1.084 2.902.829.091-.645.351-1.085.635-1.334-2.214-.251-4.542-1.107-4.542-4.93 0-1.087.389-1.979 1.024-2.675-.101-.253-.446-1.268.099-2.64 0 0 .837-.269 2.742 1.021a9.582 9.582 0 0 1 2.496-.336 9.554 9.554 0 0 1 2.496.336c1.906-1.291 2.742-1.021 2.742-1.021.545 1.372.203 2.387.099 2.64.64.696 1.024 1.587 1.024 2.675 0 3.833-2.33 4.675-4.552 4.922.355.308.675.916.675 1.846 0 1.334-.012 2.41-.012 2.737 0 .267.178.577.687.479C19.146 20.115 22 16.379 22 11.974 22 6.465 17.535 2 12.026 2z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const MoonFilledIcon = ({
|
||||
size = 24,
|
||||
width,
|
||||
height,
|
||||
...props
|
||||
}: IconSvgProps) => (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
height={size || height}
|
||||
role="presentation"
|
||||
viewBox="0 0 24 24"
|
||||
width={size || width}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M21.53 15.93c-.16-.27-.61-.69-1.73-.49a8.46 8.46 0 01-1.88.13 8.409 8.409 0 01-5.91-2.82 8.068 8.068 0 01-1.44-8.66c.44-1.01.13-1.54-.09-1.76s-.77-.55-1.83-.11a10.318 10.318 0 00-6.32 10.21 10.475 10.475 0 007.04 8.99 10 10 0 002.89.55c.16.01.32.02.48.02a10.5 10.5 0 008.47-4.27c.67-.93.49-1.519.32-1.79z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const SunFilledIcon = ({
|
||||
size = 24,
|
||||
width,
|
||||
height,
|
||||
...props
|
||||
}: IconSvgProps) => (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
height={size || height}
|
||||
role="presentation"
|
||||
viewBox="0 0 24 24"
|
||||
width={size || width}
|
||||
{...props}
|
||||
>
|
||||
<g fill="currentColor">
|
||||
<path d="M19 12a7 7 0 11-7-7 7 7 0 017 7z" />
|
||||
<path d="M12 22.96a.969.969 0 01-1-.96v-.08a1 1 0 012 0 1.038 1.038 0 01-1 1.04zm7.14-2.82a1.024 1.024 0 01-.71-.29l-.13-.13a1 1 0 011.41-1.41l.13.13a1 1 0 010 1.41.984.984 0 01-.7.29zm-14.28 0a1.024 1.024 0 01-.71-.29 1 1 0 010-1.41l.13-.13a1 1 0 011.41 1.41l-.13.13a1 1 0 01-.7.29zM22 13h-.08a1 1 0 010-2 1.038 1.038 0 011.04 1 .969.969 0 01-.96 1zM2.08 13H2a1 1 0 010-2 1.038 1.038 0 011.04 1 .969.969 0 01-.96 1zm16.93-7.01a1.024 1.024 0 01-.71-.29 1 1 0 010-1.41l.13-.13a1 1 0 011.41 1.41l-.13.13a.984.984 0 01-.7.29zm-14.02 0a1.024 1.024 0 01-.71-.29l-.13-.14a1 1 0 011.41-1.41l.13.13a1 1 0 010 1.41.97.97 0 01-.7.3zM12 3.04a.969.969 0 01-1-.96V2a1 1 0 012 0 1.038 1.038 0 01-1 1.04z" />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const HeartFilledIcon = ({
|
||||
size = 24,
|
||||
width,
|
||||
height,
|
||||
...props
|
||||
}: IconSvgProps) => (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
height={size || height}
|
||||
role="presentation"
|
||||
viewBox="0 0 24 24"
|
||||
width={size || width}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M12.62 20.81c-.34.12-.9.12-1.24 0C8.48 19.82 2 15.69 2 8.69 2 5.6 4.49 3.1 7.56 3.1c1.82 0 3.43.88 4.44 2.24a5.53 5.53 0 0 1 4.44-2.24C19.51 3.1 22 5.6 22 8.69c0 7-6.48 11.13-9.38 12.12Z"
|
||||
fill="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const SearchIcon = (props: IconSvgProps) => (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
role="presentation"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M11.5 21C16.7467 21 21 16.7467 21 11.5C21 6.25329 16.7467 2 11.5 2C6.25329 2 2 6.25329 2 11.5C2 16.7467 6.25329 21 11.5 21Z"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<path
|
||||
d="M22 22L20 20"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const LoadingCircleIcon = ({
|
||||
size = 24,
|
||||
className,
|
||||
...props
|
||||
}: IconSvgProps & { size?: number; className?: string }) => (
|
||||
<svg
|
||||
className={`animate-spin text-current ${className}`}
|
||||
fill="none"
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
width={size}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<circle
|
||||
className="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
/>
|
||||
<path
|
||||
className="opacity-75"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const LeftArrowIcon = ({
|
||||
size = 24,
|
||||
width,
|
||||
height,
|
||||
...props
|
||||
}: IconSvgProps) => (
|
||||
<svg
|
||||
height={size || height}
|
||||
viewBox="0 0 24 24"
|
||||
width={size || width}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M19 12H5M12 19L5 12L12 5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const SettingsIcon = ({
|
||||
size = 24,
|
||||
width,
|
||||
height,
|
||||
...props
|
||||
}: IconSvgProps) => (
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height={size || height}
|
||||
viewBox="0 0 1024 1024"
|
||||
width={size || width}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M861.227 511.915a284.16 284.16 0 0 0-2.304-36.95 40.79 40.79 0 0 1 19.072-38.4l47.488-26.752a25.515 25.515 0 0 0 9.685-35.242l-90.07-152.406a26.837 26.837 0 0 0-36.095-9.429l-47.446 26.795a43.384 43.384 0 0 1-43.648-2.987 313.856 313.856 0 0 0-65.109-36.693 41.515 41.515 0 0 1-24.405-35.414v-53.333a26.155 26.155 0 0 0-26.368-25.77H421.845a26.155 26.155 0 0 0-26.325 25.727v53.632a41.515 41.515 0 0 1-24.448 35.584 315.35 315.35 0 0 0-65.067 36.566 43.264 43.264 0 0 1-43.69 2.986l-47.446-26.752a26.795 26.795 0 0 0-36.01 9.472L88.832 374.912a25.557 25.557 0 0 0 9.643 35.243l47.402 26.709c13.142 8.15 20.566 23.04 19.115 38.4a295.424 295.424 0 0 0 0 73.515 40.79 40.79 0 0 1-19.03 38.4l-47.487 26.709a25.515 25.515 0 0 0-9.643 35.2l90.027 152.448a26.88 26.88 0 0 0 36.053 9.515l47.403-26.795c14.037-6.997 30.72-5.888 43.648 2.944a315.596 315.596 0 0 0 65.066 36.523 41.515 41.515 0 0 1 24.491 35.584v53.546a26.197 26.197 0 0 0 26.283 25.814h180.181a26.155 26.155 0 0 0 26.368-25.728v-53.846a41.472 41.472 0 0 1 24.32-35.498 312.15 312.15 0 0 0 65.067-36.608A43.264 43.264 0 0 1 761.472 784l47.488 26.88a26.795 26.795 0 0 0 36.053-9.515l89.942-152.405a25.515 25.515 0 0 0-9.6-35.243l-47.446-26.752a40.79 40.79 0 0 1-19.072-38.4c1.622-12.117 2.39-24.405 2.39-36.693v.043zM511.7 654.25a142.25 142.25 0 1 1 .598-284.459 142.25 142.25 0 0 1-.598 284.459z"
|
||||
fill="currentColor"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="0"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
|
||||
export const PlusIcon = ({
|
||||
size = 24,
|
||||
width,
|
||||
height,
|
||||
...props
|
||||
}: IconSvgProps) => (
|
||||
<svg
|
||||
height={size || height}
|
||||
viewBox="0 0 24 24"
|
||||
width={size || width}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M12 5V19M5 12H19"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
|
||||
|
||||
80
Project/components/navbar.tsx
Executable file
80
Project/components/navbar.tsx
Executable file
@@ -0,0 +1,80 @@
|
||||
// components/navbar.tsx
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Navbar as NextUINavbar,
|
||||
NavbarContent,
|
||||
NavbarBrand,
|
||||
NavbarItem,
|
||||
} from "@nextui-org/navbar";
|
||||
import { Button } from "@nextui-org/button";
|
||||
import { Link } from "@nextui-org/link";
|
||||
import NextLink from "next/link";
|
||||
import { usePathname } from 'next/navigation';
|
||||
import Image from "next/image";
|
||||
|
||||
import { siteConfig } from "@/config/site";
|
||||
import { ThemeSwitch } from "@/components/theme-switch";
|
||||
import { GithubIcon } from "@/components/icons";
|
||||
|
||||
export const Navbar = () => {
|
||||
const pathname = usePathname();
|
||||
|
||||
if (pathname.includes("/buildings/")) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<NextUINavbar maxWidth="xl" position="sticky">
|
||||
<NavbarContent className="basis-1/5 sm:basis-full" justify="start">
|
||||
<NavbarBrand as="li" className="gap-3">
|
||||
<NextLink className="flex justify-start items-center" href="/">
|
||||
<Image
|
||||
alt="Carbin Logo"
|
||||
className="h-8 w-8"
|
||||
height={48}
|
||||
src="/carbin.png"
|
||||
width={48}
|
||||
/>
|
||||
|
||||
<p className="text-2xl font-bold font-baskerville">Carbin</p>
|
||||
</NextLink>
|
||||
</NavbarBrand>
|
||||
</NavbarContent>
|
||||
|
||||
<NavbarContent className="hidden sm:flex basis-1/5 sm:basis-full" justify="center">
|
||||
<NavbarItem>
|
||||
<Button
|
||||
as={NextLink}
|
||||
href="/buildings"
|
||||
className="bg-orange-500 text-white min-w-[120px]"
|
||||
>
|
||||
Buildings
|
||||
</Button>
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<Button
|
||||
as={NextLink}
|
||||
href="/mission"
|
||||
className="text-orange-500 min-w-[120px]"
|
||||
variant="ghost"
|
||||
>
|
||||
Our Mission
|
||||
</Button>
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
|
||||
<NavbarContent
|
||||
className="hidden sm:flex basis-1/5 sm:basis-full"
|
||||
justify="end"
|
||||
>
|
||||
<NavbarItem className="hidden sm:flex gap-2">
|
||||
<Link isExternal aria-label="Github" href={siteConfig.links.github}>
|
||||
<GithubIcon className="text-default-500" />
|
||||
</Link>
|
||||
<ThemeSwitch />
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
</NextUINavbar>
|
||||
);
|
||||
};
|
||||
53
Project/components/primitives.ts
Executable file
53
Project/components/primitives.ts
Executable file
@@ -0,0 +1,53 @@
|
||||
import { tv } from "tailwind-variants";
|
||||
|
||||
export const title = tv({
|
||||
base: "tracking-tight inline font-semibold",
|
||||
variants: {
|
||||
color: {
|
||||
violet: "from-[#FF1CF7] to-[#b249f8]",
|
||||
yellow: "from-[#FF705B] to-[#FFB457]",
|
||||
blue: "from-[#5EA2EF] to-[#0072F5]",
|
||||
cyan: "from-[#00b7fa] to-[#01cfea]",
|
||||
green: "from-[#6FEE8D] to-[#17c964]",
|
||||
pink: "from-[#FF72E1] to-[#F54C7A]",
|
||||
foreground: "dark:from-[#FFFFFF] dark:to-[#4B4B4B]",
|
||||
},
|
||||
size: {
|
||||
sm: "text-3xl lg:text-4xl",
|
||||
md: "text-[2.3rem] lg:text-5xl leading-9",
|
||||
lg: "text-4xl lg:text-6xl",
|
||||
},
|
||||
fullWidth: {
|
||||
true: "w-full block",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: "md",
|
||||
},
|
||||
compoundVariants: [
|
||||
{
|
||||
color: [
|
||||
"violet",
|
||||
"yellow",
|
||||
"blue",
|
||||
"cyan",
|
||||
"green",
|
||||
"pink",
|
||||
"foreground",
|
||||
],
|
||||
class: "bg-clip-text text-transparent bg-gradient-to-b",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export const subtitle = tv({
|
||||
base: "w-full md:w-1/2 my-2 text-lg lg:text-xl text-default-600 block max-w-full",
|
||||
variants: {
|
||||
fullWidth: {
|
||||
true: "!w-full",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
fullWidth: true,
|
||||
},
|
||||
});
|
||||
102
Project/components/sidebar.tsx
Executable file
102
Project/components/sidebar.tsx
Executable file
@@ -0,0 +1,102 @@
|
||||
// app/buildings/[buildingid]/sidebar/sidebar.tsx
|
||||
"use client";
|
||||
|
||||
import { Link } from "@nextui-org/link";
|
||||
import { Avatar } from "@nextui-org/avatar";
|
||||
import { useState } from "react";
|
||||
import { Button, Skeleton } from "@nextui-org/react";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
import { ThemeSwitch } from "./theme-switch";
|
||||
|
||||
import { useBuilding } from "@/lib/useBuildingData";
|
||||
import { GithubIcon, LeftArrowIcon } from "@/components/icons";
|
||||
import { siteConfig } from "@/config/site";
|
||||
|
||||
|
||||
interface SidebarProps {
|
||||
buildingid: string;
|
||||
}
|
||||
|
||||
export default function Sidebar({ buildingid }: SidebarProps) {
|
||||
const { data: buildingData, error, isLoading } = useBuilding(buildingid);
|
||||
const [isExpanded, setIsExpanded] = useState(true);
|
||||
const pathname = usePathname();
|
||||
|
||||
if (pathname.includes("trashcan-mode")) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`flex flex-col items-center p-4 space-y-4 h-full ${isExpanded ? "w-64" : "w-16"}`}>
|
||||
|
||||
{/* Top section with info about building */}
|
||||
<div className="flex flex-col items-center space-y-4 min-h-64 max-h-64">
|
||||
|
||||
{/* Back to all buildings */}
|
||||
<Link href="/buildings">
|
||||
<Button startContent={<LeftArrowIcon size={16} />} variant="light">
|
||||
{"Back to all buildings"}
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
{/* Photo of building */}
|
||||
{isLoading ? (
|
||||
<Skeleton className="w-24 h-24 rounded-full">
|
||||
<div className="w-24 h-24 rounded-full bg-default-300" />
|
||||
</Skeleton>
|
||||
) : error ? (
|
||||
<div>Error: {error.message}</div>
|
||||
) : !buildingData ? (
|
||||
<div>No building found</div>
|
||||
) : (
|
||||
<Avatar
|
||||
alt={buildingData.name}
|
||||
className="w-24 h-24"
|
||||
src={buildingData.imageURL}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Name of building and settings button*/}
|
||||
{isLoading ? (
|
||||
<Skeleton className="w-40 h-8 mb-4">
|
||||
<div className="w-40 h-8 bg-default-300" />
|
||||
</Skeleton>
|
||||
) : buildingData ? (
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<h2 className="text-xl font-bold mb-4">{buildingData.name}</h2>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{/* Middle section with navigation links */}
|
||||
<nav className="flex flex-col space-y-6 h-full">
|
||||
<Link color="primary" href={`/buildings/${buildingid}/emissions`}>
|
||||
{pathname === `/buildings/${buildingid}/emissions` ? <strong>Emissions</strong> : "Emissions"}
|
||||
</Link>
|
||||
<Link color="primary" href={`/buildings/${buildingid}/trash`}>
|
||||
{pathname === `/buildings/${buildingid}/trash` ? <strong>Trash Log</strong> : "Trash Log"}
|
||||
</Link>
|
||||
<Link color="primary" href={`/buildings/${buildingid}/trash-scanner`}>
|
||||
{pathname === `/buildings/${buildingid}/trash-scanner` ? <strong>Trash Scanner</strong> : "Trash Scanner"}
|
||||
</Link>
|
||||
<Link color="primary" href={`/buildings/${buildingid}/trashcan-mode`}>
|
||||
{pathname === `/buildings/${buildingid}/trashcan-mode` ? <strong>Trashcan Mode</strong> : "Trashcan Mode"}
|
||||
</Link>
|
||||
</nav>
|
||||
|
||||
{/* Bottom section with quick actions */}
|
||||
<div className="flex items-center space-x-2 bg-default-100 rounded-full p-2">
|
||||
<ThemeSwitch />
|
||||
<div className="w-px h-6 bg-divider" /> {/* Vertical divider */}
|
||||
<Link isExternal aria-label="Github" className="p-0" href={siteConfig.links.github}>
|
||||
<GithubIcon className="text-default-500" />
|
||||
</Link>
|
||||
{/* <div className="w-px h-6 bg-divider" />
|
||||
<Link aria-label="Settings" className="p-0" href={"/settings"}>
|
||||
<SettingsIcon className="text-default-500" />
|
||||
</Link> */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
81
Project/components/theme-switch.tsx
Executable file
81
Project/components/theme-switch.tsx
Executable file
@@ -0,0 +1,81 @@
|
||||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import { VisuallyHidden } from "@react-aria/visually-hidden";
|
||||
import { SwitchProps, useSwitch } from "@nextui-org/switch";
|
||||
import { useTheme } from "next-themes";
|
||||
import { useIsSSR } from "@react-aria/ssr";
|
||||
import clsx from "clsx";
|
||||
|
||||
import { SunFilledIcon, MoonFilledIcon } from "@/components/icons";
|
||||
|
||||
export interface ThemeSwitchProps {
|
||||
className?: string;
|
||||
classNames?: SwitchProps["classNames"];
|
||||
}
|
||||
|
||||
export const ThemeSwitch: FC<ThemeSwitchProps> = ({
|
||||
className,
|
||||
classNames,
|
||||
}) => {
|
||||
const { theme, setTheme } = useTheme();
|
||||
const isSSR = useIsSSR();
|
||||
|
||||
const onChange = () => {
|
||||
theme === "light" ? setTheme("dark") : setTheme("light");
|
||||
};
|
||||
|
||||
const {
|
||||
Component,
|
||||
slots,
|
||||
isSelected,
|
||||
getBaseProps,
|
||||
getInputProps,
|
||||
getWrapperProps,
|
||||
} = useSwitch({
|
||||
isSelected: theme === "light" || isSSR,
|
||||
"aria-label": `Switch to ${theme === "light" || isSSR ? "dark" : "light"} mode`,
|
||||
onChange,
|
||||
});
|
||||
|
||||
return (
|
||||
<Component
|
||||
{...getBaseProps({
|
||||
className: clsx(
|
||||
"px-px transition-opacity hover:opacity-80 cursor-pointer",
|
||||
className,
|
||||
classNames?.base,
|
||||
),
|
||||
})}
|
||||
>
|
||||
<VisuallyHidden>
|
||||
<input {...getInputProps()} />
|
||||
</VisuallyHidden>
|
||||
<div
|
||||
{...getWrapperProps()}
|
||||
className={slots.wrapper({
|
||||
class: clsx(
|
||||
[
|
||||
"w-auto h-auto",
|
||||
"bg-transparent",
|
||||
"rounded-lg",
|
||||
"flex items-center justify-center",
|
||||
"group-data-[selected=true]:bg-transparent",
|
||||
"!text-default-500",
|
||||
"pt-px",
|
||||
"px-0",
|
||||
"mx-0",
|
||||
],
|
||||
classNames?.wrapper,
|
||||
),
|
||||
})}
|
||||
>
|
||||
{!isSelected || isSSR ? (
|
||||
<SunFilledIcon size={22} />
|
||||
) : (
|
||||
<MoonFilledIcon size={22} />
|
||||
)}
|
||||
</div>
|
||||
</Component>
|
||||
);
|
||||
};
|
||||
175
Project/components/trashDetection.tsx
Executable file
175
Project/components/trashDetection.tsx
Executable file
@@ -0,0 +1,175 @@
|
||||
// trashDetection.tsx
|
||||
/* eslint-disable no-console */
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useRef, useState, useMemo } from "react";
|
||||
import { InferenceEngine, CVImage } from "inferencejs";
|
||||
|
||||
function RealtimeModel() {
|
||||
const inferEngine = useMemo(() => new InferenceEngine(), []);
|
||||
const [modelWorkerId, setModelWorkerId] = useState<string | null>(null);
|
||||
const modelWorkerIdRef = useRef<string | null>(null);
|
||||
const [modelLoading, setModelLoading] = useState(false);
|
||||
const [predictions, setPredictions] = useState<any[]>([]);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
// References to manage media stream and timeouts
|
||||
const mediaStreamRef = useRef<MediaStream | null>(null);
|
||||
const detectFrameTimeoutRef = useRef<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
console.log("Component mounted");
|
||||
setModelLoading(true);
|
||||
|
||||
inferEngine
|
||||
.startWorker("trash-detection-kkthk", 7, "rf_1nBQDUSClLUApDgPjG78qMbBH602")
|
||||
.then((id) => {
|
||||
setModelWorkerId(id);
|
||||
modelWorkerIdRef.current = id;
|
||||
startWebcam();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error starting model worker:", error);
|
||||
});
|
||||
|
||||
// Cleanup function to stop the model worker and webcam when the component unmounts
|
||||
return () => {
|
||||
console.log("Component unmounting, stopping model worker and webcam");
|
||||
if (modelWorkerIdRef.current) {
|
||||
inferEngine.stopWorker(modelWorkerIdRef.current);
|
||||
console.log(`Stopped model worker with ID: ${modelWorkerIdRef.current}`);
|
||||
}
|
||||
stopWebcam();
|
||||
if (detectFrameTimeoutRef.current) {
|
||||
clearTimeout(detectFrameTimeoutRef.current);
|
||||
detectFrameTimeoutRef.current = null;
|
||||
console.log("Cleared detectFrameTimeoutRef");
|
||||
}
|
||||
};
|
||||
}, [inferEngine]);
|
||||
|
||||
const startWebcam = () => {
|
||||
const constraints = {
|
||||
audio: false,
|
||||
video: {
|
||||
facingMode: "environment",
|
||||
},
|
||||
};
|
||||
|
||||
navigator.mediaDevices
|
||||
.getUserMedia(constraints)
|
||||
.then((stream) => {
|
||||
mediaStreamRef.current = stream; // Store the stream reference
|
||||
if (videoRef.current && containerRef.current) {
|
||||
videoRef.current.srcObject = stream;
|
||||
videoRef.current.onloadedmetadata = () => {
|
||||
videoRef.current?.play();
|
||||
};
|
||||
|
||||
videoRef.current.onplay = () => {
|
||||
if (canvasRef.current && videoRef.current && containerRef.current) {
|
||||
detectFrame();
|
||||
}
|
||||
};
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error accessing webcam:", error);
|
||||
});
|
||||
};
|
||||
|
||||
const stopWebcam = () => {
|
||||
if (mediaStreamRef.current) {
|
||||
console.log("Stopping webcam...");
|
||||
mediaStreamRef.current.getTracks().forEach((track) => {
|
||||
track.stop();
|
||||
console.log(`Stopped track: ${track.kind}`);
|
||||
});
|
||||
mediaStreamRef.current = null;
|
||||
} else {
|
||||
console.log("No media stream to stop.");
|
||||
}
|
||||
|
||||
if (videoRef.current) {
|
||||
videoRef.current.pause();
|
||||
videoRef.current.srcObject = null;
|
||||
console.log("Video paused and srcObject cleared.");
|
||||
}
|
||||
};
|
||||
|
||||
const detectFrame = () => {
|
||||
if (!modelWorkerIdRef.current) {
|
||||
detectFrameTimeoutRef.current = window.setTimeout(detectFrame, 1000 / 3);
|
||||
return;
|
||||
}
|
||||
|
||||
if (videoRef.current && canvasRef.current) {
|
||||
const img = new CVImage(videoRef.current);
|
||||
|
||||
inferEngine.infer(modelWorkerIdRef.current, img).then((newPredictions) => {
|
||||
const ctx = canvasRef.current!.getContext("2d")!;
|
||||
|
||||
// Clear the canvas
|
||||
ctx.clearRect(0, 0, canvasRef.current!.width, canvasRef.current!.height);
|
||||
|
||||
// Get the scaling factors
|
||||
const scaleX = canvasRef.current!.width / (videoRef.current!.videoWidth ?? 1);
|
||||
const scaleY = canvasRef.current!.height / (videoRef.current!.videoHeight ?? 1);
|
||||
|
||||
newPredictions.forEach((prediction: any) => {
|
||||
const x = (prediction.bbox.x - prediction.bbox.width / 2) * scaleX;
|
||||
const y = (prediction.bbox.y - prediction.bbox.height / 2) * scaleY;
|
||||
const width = prediction.bbox.width * scaleX;
|
||||
const height = prediction.bbox.height * scaleY;
|
||||
|
||||
// Draw bounding box
|
||||
ctx.strokeStyle = prediction.color;
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeRect(x, y, width, height);
|
||||
});
|
||||
|
||||
setPredictions(newPredictions);
|
||||
detectFrameTimeoutRef.current = window.setTimeout(detectFrame, 1000 / 3);
|
||||
}).catch((error) => {
|
||||
console.error("Error during inference:", error);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="w-full h-screen relative overflow-hidden">
|
||||
<div className="absolute inset-0">
|
||||
<video
|
||||
ref={videoRef}
|
||||
muted
|
||||
playsInline
|
||||
className="absolute top-0 left-0 w-full h-full transform scale-x-[-1]"
|
||||
>
|
||||
<track kind="captions" />
|
||||
</video>
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className="absolute top-0 left-0 w-full h-full transform scale-x-[-1]"
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute bottom-4 left-1/2 transform -translate-x-1/2 w-11/12 max-w-2xl">
|
||||
<div className="bg-black bg-opacity-50 backdrop-filter backdrop-blur-md rounded-xl p-4 text-white text-center">
|
||||
{predictions.length > 0 ? (
|
||||
predictions.map((prediction, index) => (
|
||||
<div key={index} className="text-lg">
|
||||
{`${prediction.class} - ${Math.round(prediction.confidence * 100)}%`}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="text-lg">No Item Detected</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default RealtimeModel;
|
||||
681
Project/components/trashcanMode.tsx
Executable file
681
Project/components/trashcanMode.tsx
Executable file
@@ -0,0 +1,681 @@
|
||||
/* eslint-disable no-console */
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useRef, useState, useMemo } from "react";
|
||||
import { InferenceEngine, CVImage } from "inferencejs";
|
||||
import { motion } from "framer-motion";
|
||||
import { Timestamp } from 'firebase/firestore';
|
||||
import { useParams } from "next/navigation";
|
||||
|
||||
import { useBuilding, WasteDataPoint } from '@/lib/useBuildingData';
|
||||
import { Card } from "@nextui-org/react";
|
||||
|
||||
export const trashItems = [
|
||||
{
|
||||
id: "Aluminum-Can",
|
||||
name: "Aluminum Can",
|
||||
bin: "Recycling",
|
||||
co2e: 170,
|
||||
},
|
||||
{
|
||||
id: "Aluminum-Foil",
|
||||
name: "Aluminum Foil",
|
||||
bin: "Recycling",
|
||||
note: "Please rinse and flatten",
|
||||
co2e: 10,
|
||||
},
|
||||
{
|
||||
id: "Bio-Plastic-Cup",
|
||||
name: "Bio-Plastic Cup",
|
||||
bin: "Compost",
|
||||
co2e: 70,
|
||||
},
|
||||
{
|
||||
id: "Cardboard",
|
||||
name: "Cardboard",
|
||||
bin: "Recycling",
|
||||
note: "Please flatten all cardboard",
|
||||
co2e: 80,
|
||||
},
|
||||
{
|
||||
id: "Food",
|
||||
name: "Food",
|
||||
bin: "Compost",
|
||||
co2e: 1000,
|
||||
},
|
||||
{
|
||||
id: "Food-Wrapper",
|
||||
name: "Food Wrapper",
|
||||
bin: "Landfill",
|
||||
co2e: 6,
|
||||
},
|
||||
{
|
||||
id: "Paper",
|
||||
name: "Paper",
|
||||
bin: "Recycling",
|
||||
co2e: 8,
|
||||
},
|
||||
{
|
||||
id: "Paper-Cup",
|
||||
name: "Paper Cup",
|
||||
bin: "Recycling",
|
||||
co2e: 11,
|
||||
},
|
||||
{
|
||||
id: "Paper-Plate",
|
||||
name: "Paper Plate",
|
||||
bin: "Compost",
|
||||
co2e: 15,
|
||||
},
|
||||
{
|
||||
id: "Paper-Soft",
|
||||
name: "Soft Paper",
|
||||
bin: "Recycling",
|
||||
co2e: 5,
|
||||
},
|
||||
{
|
||||
id: "Plastic-Bag",
|
||||
name: "Plastic Bag",
|
||||
bin: "Landfill",
|
||||
co2e: 33,
|
||||
},
|
||||
{
|
||||
id: "Plastic-Bottle",
|
||||
name: "Plastic Bottle",
|
||||
bin: "Recycling",
|
||||
note: "Only hard number 1 or 2 bottles",
|
||||
co2e: 82,
|
||||
},
|
||||
{
|
||||
id: "Plastic-Container",
|
||||
name: "Plastic Container",
|
||||
bin: "Recycling",
|
||||
note: "Only hard plastics number 1 or 2",
|
||||
co2e: 100,
|
||||
},
|
||||
{
|
||||
id: "Plastic-Cup",
|
||||
name: "Plastic Cup",
|
||||
bin: "Recycling",
|
||||
note: "Only hard plastics number 1 or 2",
|
||||
co2e: 30,
|
||||
},
|
||||
{
|
||||
id: "Plastic-Utensil",
|
||||
name: "Plastic Utensil",
|
||||
bin: "Landfill",
|
||||
co2e: 8,
|
||||
},
|
||||
{
|
||||
id: "Styrofoam",
|
||||
name: "Styrofoam",
|
||||
bin: "Landfill",
|
||||
co2e: 45,
|
||||
},
|
||||
];
|
||||
|
||||
interface BBox {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
interface Prediction {
|
||||
class: string;
|
||||
confidence: number;
|
||||
bbox: BBox;
|
||||
color: string;
|
||||
}
|
||||
|
||||
interface Detection {
|
||||
className: string;
|
||||
lastSeen: number;
|
||||
framesSeen: number;
|
||||
bbox: BBox;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
function TrashcanMode() {
|
||||
// Initialize the inference engine and state variables
|
||||
const inferEngine = useMemo(() => new InferenceEngine(), []);
|
||||
const [modelWorkerId, setModelWorkerId] = useState<string | null>(null);
|
||||
const [modelLoading, setModelLoading] = useState(false);
|
||||
const [currentItem, setCurrentItem] = useState<any | null>(null); // Current detected item
|
||||
const [thrownItems, setThrownItems] = useState<string[]>([]); // List of items estimated to be thrown away
|
||||
const [showCelebration, setShowCelebration] = useState(false); // State to trigger celebration
|
||||
const [showCamera, setShowCamera] = useState(false); // Default to false as per your preference
|
||||
const [isHovering, setIsHovering] = useState(false); // State to detect hover over the switch area
|
||||
|
||||
// state variables for ripple effect
|
||||
const [rippleActive, setRippleActive] = useState(false);
|
||||
const [rippleColor, setRippleColor] = useState<string>('');
|
||||
const [ripplePosition, setRipplePosition] = useState<{ x: string; y: string }>({ x: '50%', y: '50%' });
|
||||
|
||||
// References to DOM elements
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Tracking detections over time
|
||||
const detectionsRef = useRef<{ [className: string]: Detection }>({}); // Ref to store detection history
|
||||
|
||||
// Introduce a ref to keep track of the last active item and its timestamp
|
||||
const lastActiveItemRef = useRef<{ itemDetails: any | null; timestamp: number }>({
|
||||
itemDetails: null,
|
||||
timestamp: 0,
|
||||
});
|
||||
|
||||
// Inside the component, get the building data
|
||||
const { buildingid } = useParams();
|
||||
const { data: building, isLoading, error, updateBuilding } = useBuilding(buildingid as string);
|
||||
|
||||
// Helper function to get bin emoji
|
||||
const getBinEmoji = (bin: string) => {
|
||||
switch (bin) {
|
||||
case "Recycling":
|
||||
return "♻️";
|
||||
case "Compost":
|
||||
return "🌿";
|
||||
case "Landfill":
|
||||
return "🗑️";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to get item emoji
|
||||
const getItemEmoji = (itemId: string) => {
|
||||
switch (itemId) {
|
||||
case "Aluminum-Can":
|
||||
return "🥫";
|
||||
case "Aluminum-Foil":
|
||||
return "🥄";
|
||||
case "Bio-Plastic-Cup":
|
||||
return "🥤";
|
||||
case "Cardboard":
|
||||
return "📦";
|
||||
case "Food":
|
||||
return "🍎";
|
||||
case "Food-Wrapper":
|
||||
return "🍬";
|
||||
case "Paper":
|
||||
return "📄";
|
||||
case "Paper-Cup":
|
||||
return "☕";
|
||||
case "Paper-Plate":
|
||||
return "🍽️";
|
||||
case "Paper-Soft":
|
||||
return "📃";
|
||||
case "Plastic-Bag":
|
||||
return "🛍️";
|
||||
case "Plastic-Bottle":
|
||||
return "🍼";
|
||||
case "Plastic-Container":
|
||||
return "🍱";
|
||||
case "Plastic-Cup":
|
||||
return "🥛";
|
||||
case "Plastic-Utensil":
|
||||
return "🍴";
|
||||
case "Styrofoam":
|
||||
return "📦";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
// helper function for ripple start position
|
||||
const getBinRippleStartPosition = (bin: string) => {
|
||||
switch (bin) {
|
||||
case "Recycling":
|
||||
return { x: '100%', y: '100%' }; // Bottom-right corner
|
||||
case "Compost":
|
||||
return { x: '50%', y: '100%' }; // Bottom-center
|
||||
case "Landfill":
|
||||
return { x: '0%', y: '100%' }; // Bottom-left corner
|
||||
default:
|
||||
return { x: '50%', y: '50%' }; // Center
|
||||
}
|
||||
};
|
||||
|
||||
// Effect to start the model worker
|
||||
useEffect(() => {
|
||||
if (!modelLoading) {
|
||||
setModelLoading(true);
|
||||
inferEngine
|
||||
.startWorker("trash-detection-kkthk", 7, "rf_1nBQDUSClLUApDgPjG78qMbBH602")
|
||||
.then((id) => setModelWorkerId(id))
|
||||
.catch((error) => {
|
||||
console.error("Error starting model worker:", error);
|
||||
});
|
||||
}
|
||||
}, [inferEngine, modelLoading]);
|
||||
|
||||
// Effect to start the webcam when the model worker is ready
|
||||
useEffect(() => {
|
||||
if (modelWorkerId) {
|
||||
startWebcam();
|
||||
}
|
||||
}, [modelWorkerId]);
|
||||
|
||||
// Function to initialize and start the webcam
|
||||
const startWebcam = () => {
|
||||
const constraints = {
|
||||
audio: false,
|
||||
video: {
|
||||
width: { ideal: 640 },
|
||||
height: { ideal: 480 },
|
||||
facingMode: "environment",
|
||||
},
|
||||
};
|
||||
|
||||
navigator.mediaDevices
|
||||
.getUserMedia(constraints)
|
||||
.then((stream) => {
|
||||
if (videoRef.current) {
|
||||
videoRef.current.srcObject = stream;
|
||||
videoRef.current.onloadedmetadata = () => {
|
||||
videoRef.current?.play();
|
||||
};
|
||||
|
||||
videoRef.current.onplay = () => {
|
||||
detectFrame();
|
||||
};
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error accessing webcam:", error);
|
||||
});
|
||||
};
|
||||
|
||||
// Function to detect objects in each video frame
|
||||
const detectFrame = () => {
|
||||
if (!modelWorkerId || !videoRef.current) {
|
||||
setTimeout(detectFrame, 1000 / 3);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const img = new CVImage(videoRef.current);
|
||||
|
||||
inferEngine.infer(modelWorkerId, img).then((predictions: unknown) => {
|
||||
const typedPredictions = predictions as Prediction[];
|
||||
|
||||
const videoWidth = videoRef.current?.videoWidth ?? 640;
|
||||
const videoHeight = videoRef.current?.videoHeight ?? 480;
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
// Filter predictions above confidence threshold
|
||||
const validPredictions = typedPredictions.filter((pred) => pred.confidence >= 0.2);
|
||||
|
||||
if (showCamera && canvasRef.current) {
|
||||
const ctx = canvasRef.current.getContext("2d")!;
|
||||
const canvasWidth = canvasRef.current.width;
|
||||
const canvasHeight = canvasRef.current.height;
|
||||
|
||||
// Clear the canvas
|
||||
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
|
||||
|
||||
// Draw trash can regions
|
||||
drawTrashcanRegions(ctx, videoWidth, videoHeight, canvasWidth, canvasHeight);
|
||||
|
||||
// Get scaling factors
|
||||
const scaleX = canvasWidth / (videoWidth ?? 1);
|
||||
const scaleY = canvasHeight / (videoHeight ?? 1);
|
||||
|
||||
validPredictions.forEach((pred: Prediction) => {
|
||||
// Draw bounding box and center point
|
||||
drawBoundingBox(ctx, pred, scaleX, scaleY);
|
||||
});
|
||||
}
|
||||
|
||||
validPredictions.forEach((pred: Prediction) => {
|
||||
const className = pred.class;
|
||||
const bbox = pred.bbox;
|
||||
|
||||
// Initialize tracking for this class if not present
|
||||
if (!detectionsRef.current[className]) {
|
||||
detectionsRef.current[className] = {
|
||||
className: className,
|
||||
lastSeen: now,
|
||||
framesSeen: 1,
|
||||
bbox: bbox,
|
||||
isActive: false,
|
||||
};
|
||||
} else {
|
||||
// Update tracking info
|
||||
const detection = detectionsRef.current[className];
|
||||
|
||||
detection.lastSeen = now;
|
||||
detection.framesSeen += 1;
|
||||
detection.bbox = bbox;
|
||||
|
||||
// Mark as active if seen consistently over 3 frames
|
||||
if (detection.framesSeen >= 3 && !detection.isActive) {
|
||||
detection.isActive = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Remove stale detections and check if any active detections are present
|
||||
let activeDetections = Object.values(detectionsRef.current).filter((detection) => {
|
||||
const timeSinceLastSeen = now - detection.lastSeen;
|
||||
|
||||
if (timeSinceLastSeen > 1000) {
|
||||
// Remove stale detections
|
||||
if (detection.isActive) {
|
||||
// Determine if last known position was near the correct trashcan area
|
||||
const itemDetails = trashItems.find((item) => item.id === detection.className);
|
||||
|
||||
if (itemDetails) {
|
||||
const isNearCorrectTrashcan = checkIfNearTrashcanArea(
|
||||
detection.bbox,
|
||||
itemDetails.bin,
|
||||
videoWidth,
|
||||
videoHeight
|
||||
);
|
||||
|
||||
if (isNearCorrectTrashcan) {
|
||||
// Item was likely thrown away in the correct bin
|
||||
setThrownItems((prevItems) => [...prevItems, detection.className]);
|
||||
setShowCelebration(true); // Trigger celebration
|
||||
setTimeout(() => setShowCelebration(false), 3000); // Stop celebration after 3 seconds
|
||||
|
||||
// Trigger the ripple effect
|
||||
setRippleColor(getBinColor(itemDetails.bin));
|
||||
setRipplePosition(getBinRippleStartPosition(itemDetails.bin));
|
||||
setRippleActive(true);
|
||||
setTimeout(() => setRippleActive(false), 3000); // Ripple lasts 3 seconds
|
||||
|
||||
const adjustedEmissions = itemDetails.co2e / 1e+3; // Convert kg to tons
|
||||
const newWasteDataPoint: WasteDataPoint = {
|
||||
timestamp: Timestamp.now(),
|
||||
type: itemDetails.id,
|
||||
trashcanID: '1', // Use trashcan ID 1
|
||||
wasteCategory: itemDetails.bin,
|
||||
emissions: adjustedEmissions,
|
||||
};
|
||||
|
||||
// Update the building's waste generation data
|
||||
const updatedWasteGeneration = [
|
||||
...(building?.wasteGeneration || []),
|
||||
newWasteDataPoint,
|
||||
];
|
||||
|
||||
updateBuilding({ wasteGeneration: updatedWasteGeneration });
|
||||
} else {
|
||||
// Incorrect bin, do not trigger celebration
|
||||
setCurrentItem(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
delete detectionsRef.current[detection.className];
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return detection.isActive;
|
||||
});
|
||||
|
||||
// Update the current item for display based on active detections
|
||||
if (activeDetections.length > 0) {
|
||||
// Find the most recently seen active detection
|
||||
activeDetections.sort((a, b) => b.lastSeen - a.lastSeen);
|
||||
const mostRecentDetection = activeDetections[0];
|
||||
const itemDetails = trashItems.find((item) => item.id === mostRecentDetection.className);
|
||||
|
||||
// Update last active item reference
|
||||
lastActiveItemRef.current = { itemDetails, timestamp: now };
|
||||
setCurrentItem(itemDetails);
|
||||
} else {
|
||||
// If no active detections, retain the last item for a short duration
|
||||
if (now - lastActiveItemRef.current.timestamp < 1000) {
|
||||
setCurrentItem(lastActiveItemRef.current.itemDetails);
|
||||
} else {
|
||||
setCurrentItem(null);
|
||||
lastActiveItemRef.current = { itemDetails: null, timestamp: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(detectFrame, 1000 / 3);
|
||||
});
|
||||
};
|
||||
|
||||
// Helper function to draw bounding box and center point
|
||||
const drawBoundingBox = (ctx: CanvasRenderingContext2D, prediction: Prediction, scaleX: number, scaleY: number) => {
|
||||
const x = (prediction.bbox.x - prediction.bbox.width / 2) * scaleX;
|
||||
const y = (prediction.bbox.y - prediction.bbox.height / 2) * scaleY;
|
||||
const width = prediction.bbox.width * scaleX;
|
||||
const height = prediction.bbox.height * scaleY;
|
||||
|
||||
// Draw bounding box
|
||||
ctx.strokeStyle = prediction.color || "#FF0000";
|
||||
ctx.lineWidth = 2;
|
||||
ctx.strokeRect(x, y, width, height);
|
||||
|
||||
// Draw center point
|
||||
ctx.fillStyle = prediction.color || "#FF0000";
|
||||
ctx.beginPath();
|
||||
ctx.arc(x + width / 2, y + height / 2, 5, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
};
|
||||
|
||||
// Helper function to draw trashcan regions
|
||||
const drawTrashcanRegions = (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
videoWidth: number,
|
||||
videoHeight: number,
|
||||
canvasWidth: number,
|
||||
canvasHeight: number
|
||||
) => {
|
||||
const trashcanAreas = getTrashcanAreas(videoWidth, videoHeight);
|
||||
|
||||
const scaleX = canvasWidth / (videoWidth ?? 1);
|
||||
const scaleY = canvasHeight / (videoHeight ?? 1);
|
||||
|
||||
Object.entries(trashcanAreas).forEach(([bin, area]) => {
|
||||
const x = area.x * scaleX;
|
||||
const y = area.y * scaleY;
|
||||
const width = area.width * scaleX;
|
||||
const height = area.height * scaleY;
|
||||
|
||||
ctx.strokeStyle = getBinColor(bin);
|
||||
ctx.lineWidth = 2;
|
||||
ctx.strokeRect(x, y, width, height);
|
||||
|
||||
// Optionally, fill the area with transparent color
|
||||
ctx.fillStyle = getBinColor(bin) + "33"; // Add transparency
|
||||
ctx.fillRect(x, y, width, height);
|
||||
});
|
||||
};
|
||||
|
||||
// Helper function to check if the bounding box is near the correct trashcan area
|
||||
const checkIfNearTrashcanArea = (
|
||||
bbox: BBox,
|
||||
correctBin: string,
|
||||
videoWidth: number,
|
||||
videoHeight: number
|
||||
): boolean => {
|
||||
const centerX = bbox.x;
|
||||
const centerY = bbox.y;
|
||||
|
||||
// Define areas for each trashcan
|
||||
const trashcanAreas = getTrashcanAreas(videoWidth, videoHeight);
|
||||
|
||||
// Check if the center point is within any trashcan area
|
||||
for (const [bin, area] of Object.entries(trashcanAreas)) {
|
||||
if (
|
||||
centerX >= area.x &&
|
||||
centerX <= area.x + area.width &&
|
||||
centerY >= area.y &&
|
||||
centerY <= area.y + area.height
|
||||
) {
|
||||
const isCorrect = bin === correctBin;
|
||||
|
||||
return isCorrect;
|
||||
}
|
||||
}
|
||||
|
||||
// If not near any bin
|
||||
return false;
|
||||
};
|
||||
|
||||
// Helper function to define trashcan areas
|
||||
const getTrashcanAreas = (videoWidth: number, videoHeight: number) => {
|
||||
const areaWidth = (videoWidth * 2) / 5; // 2/5 of the screen width
|
||||
const areaHeight = videoHeight / 2; // 1/2 of the screen height
|
||||
|
||||
return {
|
||||
Recycling: {
|
||||
x: 0,
|
||||
y: videoHeight / 2,
|
||||
width: areaWidth,
|
||||
height: areaHeight,
|
||||
},
|
||||
Compost: {
|
||||
x: (videoWidth - areaWidth) / 2,
|
||||
y: videoHeight / 2,
|
||||
width: areaWidth,
|
||||
height: areaHeight,
|
||||
},
|
||||
Landfill: {
|
||||
x: videoWidth - areaWidth,
|
||||
y: videoHeight / 2,
|
||||
width: areaWidth,
|
||||
height: areaHeight,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
// Helper function to get bin color
|
||||
const getBinColor = (bin: string) => {
|
||||
switch (bin) {
|
||||
case "Recycling":
|
||||
return "#00aaff"; // Blue
|
||||
case "Compost":
|
||||
return "#33cc33"; // Green
|
||||
case "Landfill":
|
||||
return "#aaaaaa"; // Gray
|
||||
default:
|
||||
return "#ffffff"; // White
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to get arrow symbol
|
||||
const getArrow = (bin: string) => {
|
||||
switch (bin) {
|
||||
case "Recycling":
|
||||
return "→"; // Right arrow
|
||||
case "Compost":
|
||||
return "↓"; // Down arrow
|
||||
case "Landfill":
|
||||
return "←"; // Left arrow
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
// Render the component
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="w-full h-screen relative overflow-hidden bg-gradient-to-b from-gray-50 to-gray-100 dark:from-gray-800 dark:to-gray-900"
|
||||
>
|
||||
{/* Hidden video element for capturing webcam feed */}
|
||||
<video ref={videoRef} muted playsInline className="hidden">
|
||||
<track kind="captions" />
|
||||
</video>
|
||||
|
||||
{/* Video and canvas elements for display */}
|
||||
{showCamera && (
|
||||
<div className="absolute inset-0">
|
||||
<video
|
||||
ref={videoRef}
|
||||
muted
|
||||
playsInline
|
||||
className="absolute top-0 left-0 w-full h-full object-cover opacity-30"
|
||||
style={{ transform: "scaleX(-1)" }}
|
||||
>
|
||||
<track kind="captions" />
|
||||
</video>
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className="absolute top-0 left-0 w-full h-full object-cover pointer-events-none"
|
||||
style={{ transform: "scaleX(-1)" }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Ripple Effect Overlay */}
|
||||
{rippleActive && (
|
||||
<div
|
||||
className="ripple-effect"
|
||||
style={{
|
||||
backgroundColor: rippleColor,
|
||||
left: ripplePosition.x,
|
||||
top: ripplePosition.y,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Main content */}
|
||||
<div className="relative z-10 flex flex-col justify-center items-center w-full h-full p-8">
|
||||
{showCelebration ? (
|
||||
<motion.div
|
||||
className="flex flex-col items-center"
|
||||
initial={{ scale: 0 }}
|
||||
animate={{ scale: 1 }}
|
||||
transition={{ duration: 0.5, type: "spring", stiffness: 100 }}
|
||||
>
|
||||
<span aria-label="Check Mark" className="text-9xl mb-4" role="img">
|
||||
✅
|
||||
</span>
|
||||
<h2 className="text-4xl font-bold text-green-600 dark:text-green-400">
|
||||
Great job!
|
||||
</h2>
|
||||
</motion.div>
|
||||
) : currentItem ? (
|
||||
<Card className="w-full max-w-2xl p-8 bg-white dark:bg-gray-800 shadow-lg">
|
||||
<h1 className="text-5xl font-bold text-center mb-8 text-gray-800 dark:text-gray-200">
|
||||
{getBinEmoji(currentItem.bin)} {currentItem.bin} {getArrow(currentItem.bin)}
|
||||
</h1>
|
||||
<h2 className="text-3xl text-center mb-4 text-gray-700 dark:text-gray-300">
|
||||
{getItemEmoji(currentItem.id)} {currentItem.name}
|
||||
</h2>
|
||||
{currentItem.note && (
|
||||
<p className="text-xl text-center text-gray-600 dark:text-gray-400">
|
||||
{currentItem.note}
|
||||
</p>
|
||||
)}
|
||||
</Card>
|
||||
) : (
|
||||
<Card className="w-full max-w-2xl p-8 bg-white dark:bg-gray-800 shadow-lg">
|
||||
<h1 className="text-4xl font-bold text-center mb-8 text-gray-800 dark:text-gray-200">
|
||||
No Item Detected
|
||||
</h1>
|
||||
{thrownItems.length > 0 && (
|
||||
<div className="text-center">
|
||||
<h2 className="text-xl font-semibold mb-2 text-gray-700 dark:text-gray-300">
|
||||
Recently Thrown Items:
|
||||
</h2>
|
||||
<p className="text-lg text-gray-600 dark:text-gray-400">
|
||||
{Object.entries(
|
||||
thrownItems.slice(-5).reduce((acc, item) => {
|
||||
acc[item] = (acc[item] || 0) + 1;
|
||||
return acc;
|
||||
}, {} as Record<string, number>)
|
||||
)
|
||||
.map(([item, count]) => (count > 1 ? `${item} (${count}x)` : item))
|
||||
.join(", ")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default TrashcanMode;
|
||||
415
Project/components/uploadDataModal.tsx
Executable file
415
Project/components/uploadDataModal.tsx
Executable file
@@ -0,0 +1,415 @@
|
||||
// components/uploadDataModal.tsx
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Button, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter } from "@nextui-org/react";
|
||||
import { Accordion, AccordionItem } from "@nextui-org/react";
|
||||
import { AzureKeyCredential, DocumentAnalysisClient } from "@azure/ai-form-recognizer";
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { ElectricityDataPoint, NaturalGasDataPoint } from "../lib/useBuildingData";
|
||||
|
||||
interface UploadDataModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
buildingid: string;
|
||||
updateBuilding: (newData: any) => void;
|
||||
}
|
||||
|
||||
const EMISSIONS_FACTOR = 0.5;
|
||||
const key = process.env.NEXT_PUBLIC_FORM_RECOGNIZER_KEY;
|
||||
const endpoint = process.env.NEXT_PUBLIC_FORM_RECOGNIZER_ENDPOINT;
|
||||
|
||||
export function UploadDataModal({ isOpen, onClose, buildingid, updateBuilding }: UploadDataModalProps) {
|
||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||
const [gasFile, setGasFile] = useState<File | null>(null);
|
||||
const [electricityFile, setElectricityFile] = useState<File | null>(null);
|
||||
const [gasFileUrl, setGasFileUrl] = useState<string | null>(null);
|
||||
const [electricityFileUrl, setElectricityFileUrl] = useState<string | null>(null);
|
||||
const [extractionStatus, setExtractionStatus] = useState<'idle' | 'loading' | 'complete'>('idle');
|
||||
const [aiExtractionStatus, setAiExtractionStatus] = useState<'idle' | 'loading' | 'complete'>('idle');
|
||||
const [dataPreview, setDataPreview] = useState<any>(null);
|
||||
const router = useRouter();
|
||||
|
||||
const handleFileUpload = (type: 'gas' | 'electricity', file: File) => {
|
||||
if (type === 'gas') {
|
||||
setGasFile(file);
|
||||
setGasFileUrl(URL.createObjectURL(file));
|
||||
} else if (type === 'electricity') {
|
||||
setElectricityFile(file);
|
||||
setElectricityFileUrl(URL.createObjectURL(file));
|
||||
}
|
||||
setIsSubmitted(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (gasFileUrl) URL.revokeObjectURL(gasFileUrl);
|
||||
if (electricityFileUrl) URL.revokeObjectURL(electricityFileUrl);
|
||||
};
|
||||
}, [gasFileUrl, electricityFileUrl]);
|
||||
|
||||
const extractDataFromPDF = async (file: File, type: 'gas' | 'electricity') => {
|
||||
const client = new DocumentAnalysisClient(endpoint!, new AzureKeyCredential(key!));
|
||||
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const poller = await client.beginAnalyzeDocument("prebuilt-document", arrayBuffer);
|
||||
const { keyValuePairs } = await poller.pollUntilDone();
|
||||
|
||||
if (!keyValuePairs) return [];
|
||||
|
||||
const dataPoints: (ElectricityDataPoint | NaturalGasDataPoint)[] = [];
|
||||
let extractedDate: Date | null = null;
|
||||
|
||||
const monthMap: { [key: string]: number } = {
|
||||
'jan': 0, 'january': 0, 'feb': 1, 'february': 1, 'mar': 2, 'march': 2,
|
||||
'apr': 3, 'april': 3, 'may': 4, 'jun': 5, 'june': 5, 'jul': 6, 'july': 6,
|
||||
'aug': 7, 'august': 7, 'sep': 8, 'september': 8, 'oct': 9, 'october': 9,
|
||||
'nov': 10, 'november': 10, 'dec': 11, 'december': 11
|
||||
};
|
||||
|
||||
for (const { key, value } of keyValuePairs) {
|
||||
console.log("KEY:", key.content, "VALUE:", value?.content);
|
||||
if (!value) continue;
|
||||
|
||||
const keyLower = key.content.toLowerCase();
|
||||
const valueLower = value.content.toLowerCase();
|
||||
|
||||
// Extract date information
|
||||
if (keyLower.includes('date') || keyLower.includes('period')) {
|
||||
console.log("DATE IDENTIFIED:", valueLower);
|
||||
const dateMatch = valueLower.match(/(\d{1,2})\s*(?:st|nd|rd|th)?\s*(?:of)?\s*([a-z]+)?\s*(\d{4})?/i);
|
||||
|
||||
console.log("DATE MATCH:", dateMatch);
|
||||
|
||||
if (dateMatch) {
|
||||
const day = 1; // Always assume 1st of the month
|
||||
const month = dateMatch[2] ? monthMap[dateMatch[2].toLowerCase()] : new Date().getMonth();
|
||||
const year = dateMatch[3] ? parseInt(dateMatch[3]) : new Date().getFullYear();
|
||||
|
||||
if (year >= 1900 && year <= 2100) {
|
||||
extractedDate = new Date(year, month, day);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (type === 'electricity' && keyLower.includes('kwh')) {
|
||||
const kwh = parseFloat(value.content || '0');
|
||||
|
||||
if (kwh !== 0) {
|
||||
const timestamp = extractedDate || new Date();
|
||||
|
||||
timestamp.setHours(0, 0, 0, 0); // Set to midnight
|
||||
|
||||
const existingDataIndex = dataPoints.findIndex(point =>
|
||||
point.timestamp.seconds === timestamp.getTime() / 1000
|
||||
);
|
||||
|
||||
if (existingDataIndex === -1) {
|
||||
dataPoints.push({
|
||||
timestamp: { seconds: timestamp.getTime() / 1000, nanoseconds: 0 },
|
||||
kwh: kwh,
|
||||
emissions: kwh * EMISSIONS_FACTOR / 1000,
|
||||
});
|
||||
} else {
|
||||
dataPoints[existingDataIndex] = {
|
||||
...dataPoints[existingDataIndex],
|
||||
kwh: kwh,
|
||||
emissions: kwh * EMISSIONS_FACTOR / 1000,
|
||||
};
|
||||
}
|
||||
}
|
||||
} else if (type === 'gas' && keyLower.includes('therm')) {
|
||||
const therms = parseFloat(value.content || '0');
|
||||
|
||||
if (therms !== 0) {
|
||||
const timestamp = extractedDate || new Date();
|
||||
|
||||
timestamp.setHours(0, 0, 0, 0); // Set to midnight
|
||||
|
||||
const existingDataIndex = dataPoints.findIndex(point =>
|
||||
point.timestamp.seconds === timestamp.getTime() / 1000
|
||||
);
|
||||
|
||||
if (existingDataIndex === -1) {
|
||||
dataPoints.push({
|
||||
timestamp: { seconds: timestamp.getTime() / 1000, nanoseconds: 0 },
|
||||
therms: therms,
|
||||
emissions: therms * 5.3 / 1000, // approx CO2 emissions for natural gas (5.3 kg CO2 per therm, measured in tons)
|
||||
});
|
||||
} else {
|
||||
dataPoints[existingDataIndex] = {
|
||||
...dataPoints[existingDataIndex],
|
||||
therms: therms,
|
||||
emissions: therms * 5.3 / 1000,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return dataPoints;
|
||||
};
|
||||
|
||||
const handleExtraction = async () => {
|
||||
setExtractionStatus('loading');
|
||||
try {
|
||||
let newData: any = {};
|
||||
|
||||
if (gasFile) {
|
||||
const gasData = await extractDataFromPDF(gasFile, 'gas');
|
||||
|
||||
console.log("Gas data:");
|
||||
gasData.forEach(dataPoint => {
|
||||
console.log("Date:", new Date(dataPoint.timestamp.seconds * 1000).toLocaleDateString(), "Therms:", (dataPoint as NaturalGasDataPoint).therms);
|
||||
});
|
||||
newData.naturalGasUsage = gasData;
|
||||
}
|
||||
|
||||
if (electricityFile) {
|
||||
const electricityData = await extractDataFromPDF(electricityFile, 'electricity');
|
||||
|
||||
console.log("Electricity data:");
|
||||
electricityData.forEach(dataPoint => {
|
||||
console.log("Date:", new Date(dataPoint.timestamp.seconds * 1000).toLocaleDateString(), "kWh:", (dataPoint as ElectricityDataPoint).kwh);
|
||||
});
|
||||
newData.electricityUsage = electricityData;
|
||||
}
|
||||
|
||||
setDataPreview(newData);
|
||||
setExtractionStatus('complete');
|
||||
|
||||
// Update the building data
|
||||
updateBuilding(newData);
|
||||
} catch (error) {
|
||||
console.error("Error during extraction:", error);
|
||||
setExtractionStatus('idle');
|
||||
}
|
||||
};
|
||||
|
||||
const handleAIExtraction = async () => {
|
||||
setAiExtractionStatus('loading');
|
||||
try {
|
||||
let newData: any = {};
|
||||
|
||||
if (gasFile) {
|
||||
const gasData = await extractDataUsingAI(gasFile, 'gas');
|
||||
newData.naturalGasUsage = gasData;
|
||||
}
|
||||
|
||||
if (electricityFile) {
|
||||
const electricityData = await extractDataUsingAI(electricityFile, 'electricity');
|
||||
newData.electricityUsage = electricityData;
|
||||
}
|
||||
|
||||
setDataPreview(newData);
|
||||
setAiExtractionStatus('complete');
|
||||
|
||||
// Update the building data
|
||||
updateBuilding(newData);
|
||||
} catch (error) {
|
||||
console.error("Error during AI extraction:", error);
|
||||
setAiExtractionStatus('idle');
|
||||
}
|
||||
};
|
||||
|
||||
const extractDataUsingAI = async (file: File, type: 'gas' | 'electricity') => {
|
||||
// Step 1: Convert PDF to image
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append('pdf', file, file.name);
|
||||
formData.append('type', type);
|
||||
|
||||
const pdfToImageResponse = await fetch('/api/pdf-to-image', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!pdfToImageResponse.ok) {
|
||||
throw new Error('Failed to convert PDF to image');
|
||||
}
|
||||
|
||||
const { response } = await pdfToImageResponse.json();
|
||||
console.log("PDF TO IMAGE RESPONSE", response);
|
||||
|
||||
// Parse the JSON response
|
||||
const parsedData: string = response.response;
|
||||
|
||||
//Trim the string to remove the "anything before first {" and "and after last }"
|
||||
const trimmedData = parsedData.replace(/^[^{]*|[^}]*$/g, '');
|
||||
|
||||
const parsedTrimmedData = JSON.parse(trimmedData);
|
||||
console.log("PARSED TRIMMED DATA", parsedTrimmedData);
|
||||
|
||||
// Convert the parsed data to the format expected by the application
|
||||
return parsedTrimmedData.dataPoints.map((point: any) => ({
|
||||
timestamp: {
|
||||
seconds: new Date(point.date).getTime() / 1000,
|
||||
nanoseconds: 0
|
||||
},
|
||||
[type === 'gas' ? 'therms' : 'kwh']: point.usage,
|
||||
emissions: point.usage * (type === 'gas' ? 5.3 : EMISSIONS_FACTOR) / 1000,
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal backdrop="blur" isOpen={isOpen} size="xl" onClose={onClose}>
|
||||
<ModalContent>
|
||||
{!isSubmitted ? (
|
||||
<>
|
||||
<ModalHeader>Upload New Data</ModalHeader>
|
||||
<ModalBody>
|
||||
<div className="flex space-x-6">
|
||||
<div
|
||||
aria-label="Upload gas data"
|
||||
className="w-full h-40 border-2 border-dashed border-gray-300 rounded-lg flex items-center justify-center cursor-pointer"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => document.getElementById('gas-upload')?.click()}
|
||||
onDragOver={(e) => e.preventDefault()}
|
||||
onDrop={(e) => {
|
||||
e.preventDefault();
|
||||
const file = e.dataTransfer.files[0];
|
||||
|
||||
if (file) handleFileUpload('gas', file);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if ((e.key === 'Enter' || e.key === ' ') && gasFile) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<p className="text-center p-4">Click or drag to upload gas bill PDF</p>
|
||||
<input
|
||||
accept=".pdf"
|
||||
className="hidden"
|
||||
id="gas-upload"
|
||||
type="file"
|
||||
onChange={(e) => {
|
||||
const file = e.target.files?.[0];
|
||||
|
||||
if (file) handleFileUpload('gas', file);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="w-full h-40 border-2 border-dashed border-gray-300 rounded-lg flex items-center justify-center cursor-pointer"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => document.getElementById('electricity-upload')?.click()}
|
||||
onDragOver={(e) => e.preventDefault()}
|
||||
onDrop={(e) => {
|
||||
e.preventDefault();
|
||||
const file = e.dataTransfer.files[0];
|
||||
|
||||
if (file) handleFileUpload('electricity', file);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if ((e.key === 'Enter' || e.key === ' ') && electricityFile) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<p className="text-center p-4">Click or drag to upload electricity bill PDF</p>
|
||||
<input
|
||||
accept=".pdf"
|
||||
className="hidden"
|
||||
id="electricity-upload"
|
||||
type="file"
|
||||
onChange={(e) => {
|
||||
const file = e.target.files?.[0];
|
||||
|
||||
if (file) handleFileUpload('electricity', file);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ModalHeader>Data Uploaded</ModalHeader>
|
||||
<ModalBody>
|
||||
<div className="flex flex-col items-center">
|
||||
<p className="text-4xl">✅</p>
|
||||
<p className="text-center mt-4">
|
||||
Your file has been successfully uploaded! Please wait while we extract the data.
|
||||
</p>
|
||||
{(gasFile || electricityFile) && (
|
||||
<Accordion className="w-full mt-4">
|
||||
<AccordionItem key="1" aria-label="File Preview" title="File Preview">
|
||||
{gasFile && gasFileUrl && (
|
||||
<div>
|
||||
<p>Gas Bill:</p>
|
||||
<p>Name: {gasFile.name}</p>
|
||||
<p>Type: {gasFile.type}</p>
|
||||
<p>Size: {(gasFile.size / 1024).toFixed(2)} KB</p>
|
||||
<embed
|
||||
className="mt-2"
|
||||
height="500px"
|
||||
src={gasFileUrl}
|
||||
type="application/pdf"
|
||||
width="100%"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{electricityFile && electricityFileUrl && (
|
||||
<div className="mt-4">
|
||||
<p>Electricity Bill:</p>
|
||||
<p>Name: {electricityFile.name}</p>
|
||||
<p>Type: {electricityFile.type}</p>
|
||||
<p>Size: {(electricityFile.size / 1024).toFixed(2)} KB</p>
|
||||
<embed
|
||||
className="mt-2"
|
||||
height="500px"
|
||||
src={electricityFileUrl}
|
||||
type="application/pdf"
|
||||
width="100%"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</AccordionItem>
|
||||
<AccordionItem key="2" aria-label="Data Extraction" title="Data Extraction">
|
||||
{extractionStatus === 'idle' && aiExtractionStatus === 'idle' && (
|
||||
<div className="flex space-x-4">
|
||||
<Button
|
||||
color="primary"
|
||||
onPress={handleExtraction}
|
||||
>
|
||||
Start Form Recognizer Extraction
|
||||
</Button>
|
||||
<Button
|
||||
color="secondary"
|
||||
onPress={handleAIExtraction}
|
||||
>
|
||||
Start AI-Powered Extraction
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{extractionStatus === 'loading' && <p>Extracting data using Form Recognizer...</p>}
|
||||
{aiExtractionStatus === 'loading' && <p>Extracting data using AI...</p>}
|
||||
{extractionStatus === 'complete' && <p>Form Recognizer extraction complete!</p>}
|
||||
{aiExtractionStatus === 'complete' && <p>AI-powered extraction complete!</p>}
|
||||
</AccordionItem>
|
||||
<AccordionItem key="3" aria-label="Data Preview" title="Data Preview">
|
||||
{dataPreview ? (
|
||||
<pre>{JSON.stringify(dataPreview, null, 2)}</pre>
|
||||
) : (
|
||||
<p>No data available. Please complete extraction first.</p>
|
||||
)}
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
)}
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="primary" onPress={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</>
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
11
Project/config/fonts.ts
Executable file
11
Project/config/fonts.ts
Executable file
@@ -0,0 +1,11 @@
|
||||
import { Fira_Code as FontMono, Inter as FontSans } from "next/font/google";
|
||||
|
||||
export const fontSans = FontSans({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-sans",
|
||||
});
|
||||
|
||||
export const fontMono = FontMono({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-mono",
|
||||
});
|
||||
9
Project/config/site.ts
Executable file
9
Project/config/site.ts
Executable file
@@ -0,0 +1,9 @@
|
||||
export type SiteConfig = typeof siteConfig;
|
||||
|
||||
export const siteConfig = {
|
||||
name: "Carbin",
|
||||
description: "Smart bins to help track your building's carbon footprint and encourage responsible waste management.",
|
||||
links: {
|
||||
github: "https://github.com/elibullockpapa/patriotHacks2024",
|
||||
},
|
||||
};
|
||||
104
Project/config/trashItems.ts
Executable file
104
Project/config/trashItems.ts
Executable file
@@ -0,0 +1,104 @@
|
||||
|
||||
export const trashItems = [
|
||||
{
|
||||
id: "Aluminum-Can",
|
||||
name: "Aluminum Can",
|
||||
bin: "Recycling",
|
||||
co2e: 170,
|
||||
},
|
||||
{
|
||||
id: "Aluminum-Foil",
|
||||
name: "Aluminum Foil",
|
||||
bin: "Recycling",
|
||||
note: "Please rinse and flatten",
|
||||
co2e: 10,
|
||||
},
|
||||
{
|
||||
id: "Bio-Plastic-Cup",
|
||||
name: "Bio-Plastic Cup",
|
||||
bin: "Compost",
|
||||
co2e: 70,
|
||||
},
|
||||
{
|
||||
id: "Cardboard",
|
||||
name: "Cardboard",
|
||||
bin: "Recycling",
|
||||
note: "Please flatten all cardboard",
|
||||
co2e: 80,
|
||||
},
|
||||
{
|
||||
id: "Food",
|
||||
name: "Food",
|
||||
bin: "Compost",
|
||||
co2e: 1000,
|
||||
},
|
||||
{
|
||||
id: "Food-Wrapper",
|
||||
name: "Food Wrapper",
|
||||
bin: "Landfill",
|
||||
co2e: 6,
|
||||
},
|
||||
{
|
||||
id: "Paper",
|
||||
name: "Paper",
|
||||
bin: "Recycling",
|
||||
co2e: 8,
|
||||
},
|
||||
{
|
||||
id: "Paper-Cup",
|
||||
name: "Paper Cup",
|
||||
bin: "Recycling",
|
||||
co2e: 11,
|
||||
},
|
||||
{
|
||||
id: "Paper-Plate",
|
||||
name: "Paper Plate",
|
||||
bin: "Compost",
|
||||
co2e: 15,
|
||||
},
|
||||
{
|
||||
id: "Paper-Soft",
|
||||
name: "Soft Paper",
|
||||
bin: "Recycling",
|
||||
co2e: 5,
|
||||
},
|
||||
{
|
||||
id: "Plastic-Bag",
|
||||
name: "Plastic Bag",
|
||||
bin: "Landfill",
|
||||
co2e: 33,
|
||||
},
|
||||
{
|
||||
id: "Plastic-Bottle",
|
||||
name: "Plastic Bottle",
|
||||
bin: "Recycling",
|
||||
note: "Only hard number 1 or 2 bottles",
|
||||
co2e: 82,
|
||||
},
|
||||
{
|
||||
id: "Plastic-Container",
|
||||
name: "Plastic Container",
|
||||
bin: "Recycling",
|
||||
note: "Only hard plastics number 1 or 2",
|
||||
co2e: 100,
|
||||
},
|
||||
{
|
||||
id: "Plastic-Cup",
|
||||
name: "Plastic Cup",
|
||||
bin: "Recycling",
|
||||
note: "Only hard plastics number 1 or 2",
|
||||
co2e: 30,
|
||||
},
|
||||
{
|
||||
id: "Plastic-Utensil",
|
||||
name: "Plastic Utensil",
|
||||
bin: "Landfill",
|
||||
co2e: 8,
|
||||
},
|
||||
{
|
||||
id: "Styrofoam",
|
||||
name: "Styrofoam",
|
||||
bin: "Landfill",
|
||||
co2e: 45,
|
||||
},
|
||||
];
|
||||
28
Project/lib/firebase.ts
Executable file
28
Project/lib/firebase.ts
Executable file
@@ -0,0 +1,28 @@
|
||||
// lib/firebase.ts
|
||||
|
||||
import { initializeApp } from "firebase/app";
|
||||
import { getAuth } from "firebase/auth";
|
||||
import { initializeFirestore, persistentLocalCache, persistentMultipleTabManager } from "firebase/firestore";
|
||||
|
||||
// Firebase config object
|
||||
const firebaseConfig = {
|
||||
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
|
||||
authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
|
||||
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
|
||||
storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
|
||||
messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
|
||||
appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
|
||||
};
|
||||
|
||||
// Initialize Firebase app
|
||||
const app = initializeApp(firebaseConfig);
|
||||
|
||||
// Initialize Firebase Auth
|
||||
export const auth = getAuth(app);
|
||||
|
||||
// Initialize Firestore with offline persistence
|
||||
export const db = initializeFirestore(app, {
|
||||
localCache: persistentLocalCache({
|
||||
tabManager: persistentMultipleTabManager(),
|
||||
}),
|
||||
});
|
||||
145
Project/lib/useBuildingData.ts
Executable file
145
Project/lib/useBuildingData.ts
Executable file
@@ -0,0 +1,145 @@
|
||||
// lib/useBuildingData.ts
|
||||
"use client";
|
||||
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
export type ElectricityDataPoint = {
|
||||
timestamp: {
|
||||
seconds: number;
|
||||
nanoseconds: number;
|
||||
};
|
||||
kwh: number;
|
||||
emissions: number;
|
||||
};
|
||||
|
||||
export type NaturalGasDataPoint = {
|
||||
timestamp: {
|
||||
seconds: number;
|
||||
nanoseconds: number;
|
||||
};
|
||||
therms: number;
|
||||
emissions: number;
|
||||
};
|
||||
|
||||
export type WasteDataPoint = {
|
||||
timestamp: {
|
||||
seconds: number;
|
||||
nanoseconds: number;
|
||||
};
|
||||
type: string;
|
||||
trashcanID: string;
|
||||
wasteCategory: string;
|
||||
emissions: number;
|
||||
};
|
||||
|
||||
export type Building = {
|
||||
name: string;
|
||||
id: string;
|
||||
address: string;
|
||||
yearBuilt: number;
|
||||
squareFeet: number;
|
||||
imageURL: string;
|
||||
electricityUsage: Array<ElectricityDataPoint>;
|
||||
naturalGasUsage: Array<NaturalGasDataPoint>;
|
||||
wasteGeneration: Array<WasteDataPoint>;
|
||||
}
|
||||
|
||||
const getBuildingsFromAPI = async () => {
|
||||
const response = await fetch('/api/buildings');
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
const updateBuildingInAPI = async (buildingId: string, newData: Partial<Building> & { operation?: string; index?: number }) => {
|
||||
const response = await fetch('/api/buildings', {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ id: buildingId, ...newData }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
|
||||
throw new Error(`Network response was not ok: ${response.status} ${response.statusText}. ${JSON.stringify(errorData)}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export function useBuildingList() {
|
||||
const query = useQuery<Building[], Error>({
|
||||
queryKey: ['buildings'],
|
||||
queryFn: getBuildingsFromAPI,
|
||||
staleTime: 10 * 60 * 1000,
|
||||
gcTime: 15 * 60 * 1000,
|
||||
});
|
||||
|
||||
return {
|
||||
...query,
|
||||
};
|
||||
}
|
||||
|
||||
export function useBuilding(buildingId: string) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const query = useQuery<Building, Error>({
|
||||
queryKey: ['building', buildingId],
|
||||
queryFn: () => getBuildingFromAPI(buildingId),
|
||||
staleTime: 10 * 60 * 1000,
|
||||
gcTime: 15 * 60 * 1000,
|
||||
});
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: (data: Partial<Building> & { operation?: string; index?: number }) => updateBuildingInAPI(buildingId, data),
|
||||
onMutate: async (data) => {
|
||||
await queryClient.cancelQueries({ queryKey: ['building', buildingId] });
|
||||
const previousBuilding = queryClient.getQueryData<Building>(['building', buildingId]);
|
||||
|
||||
queryClient.setQueryData<Building>(['building', buildingId], (oldData) => {
|
||||
if (!oldData) return undefined;
|
||||
|
||||
if (data.operation === 'deleteWasteEntry' && typeof data.index === 'number') {
|
||||
const newWasteGeneration = [...oldData.wasteGeneration];
|
||||
|
||||
newWasteGeneration.splice(data.index, 1);
|
||||
|
||||
return { ...oldData, wasteGeneration: newWasteGeneration };
|
||||
}
|
||||
|
||||
return { ...oldData, ...data };
|
||||
});
|
||||
|
||||
return { previousBuilding };
|
||||
},
|
||||
onError: (err, newData, context) => {
|
||||
console.error("Error updating building data:", err);
|
||||
// Log additional details about the failed update
|
||||
console.error("Failed update data:", newData);
|
||||
queryClient.setQueryData(['building', buildingId], context!.previousBuilding);
|
||||
},
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['building', buildingId] });
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
...query,
|
||||
updateBuilding: mutation.mutate
|
||||
};
|
||||
}
|
||||
|
||||
const getBuildingFromAPI = async (buildingId: string): Promise<Building> => {
|
||||
const response = await fetch(`/api/buildings?id=${buildingId}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
13
Project/next.config.js
Executable file
13
Project/next.config.js
Executable file
@@ -0,0 +1,13 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: '**',
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = nextConfig
|
||||
76
Project/package.json
Executable file
76
Project/package.json
Executable file
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"name": "next-app-template",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbo",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint . --ext .ts,.tsx -c .eslintrc.json --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@azure/ai-form-recognizer": "^5.0.0",
|
||||
"@azure/cosmos": "^4.1.1",
|
||||
"@azure/openai": "^2.0.0-beta.2",
|
||||
"@hyzyla/pdfium": "^2.1.2",
|
||||
"@nextui-org/button": "2.0.38",
|
||||
"@nextui-org/code": "2.0.33",
|
||||
"@nextui-org/input": "2.2.5",
|
||||
"@nextui-org/kbd": "2.0.34",
|
||||
"@nextui-org/link": "2.0.35",
|
||||
"@nextui-org/listbox": "2.1.27",
|
||||
"@nextui-org/navbar": "2.0.37",
|
||||
"@nextui-org/react": "^2.4.8",
|
||||
"@nextui-org/snippet": "2.0.43",
|
||||
"@nextui-org/switch": "2.0.34",
|
||||
"@nextui-org/system": "2.2.6",
|
||||
"@nextui-org/theme": "2.2.11",
|
||||
"@react-aria/ssr": "3.9.4",
|
||||
"@react-aria/visually-hidden": "3.8.12",
|
||||
"@tanstack/react-query": "^5.59.11",
|
||||
"ai": "^3.4.9",
|
||||
"axios": "^1.7.7",
|
||||
"clsx": "2.1.1",
|
||||
"firebase": "^10.14.1",
|
||||
"form-data": "^4.0.1",
|
||||
"formidable": "v3",
|
||||
"framer-motion": "~11.1.9",
|
||||
"inferencejs": "^1.0.13",
|
||||
"intl-messageformat": "^10.6.0",
|
||||
"next": "14.2.4",
|
||||
"next-themes": "^0.2.1",
|
||||
"openai": "^4.67.3",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-draggable": "^4.4.6",
|
||||
"react-pdf": "^9.1.1",
|
||||
"react-webcam": "^7.2.0",
|
||||
"recharts": "^2.13.0",
|
||||
"roboflow-js": "^0.2.32",
|
||||
"screenfull": "^6.0.2",
|
||||
"sharp": "^0.33.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/formidable": "^3.4.5",
|
||||
"@types/node": "20.5.7",
|
||||
"@types/react": "^18.3.11",
|
||||
"@types/react-dom": "18.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "7.2.0",
|
||||
"@typescript-eslint/parser": "7.2.0",
|
||||
"autoprefixer": "10.4.19",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-config-next": "14.2.1",
|
||||
"eslint-config-prettier": "^8.10.0",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.0",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"eslint-plugin-react": "^7.37.1",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"eslint-plugin-unused-imports": "^3.2.0",
|
||||
"postcss": "8.4.38",
|
||||
"tailwind-variants": "0.1.20",
|
||||
"tailwindcss": "3.4.3",
|
||||
"typescript": "5.0.4"
|
||||
}
|
||||
}
|
||||
6
Project/postcss.config.js
Executable file
6
Project/postcss.config.js
Executable file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
BIN
Project/public/carbin.png
Executable file
BIN
Project/public/carbin.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
BIN
Project/public/crumpled-paper.png
Executable file
BIN
Project/public/crumpled-paper.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 2.3 MiB |
BIN
Project/public/demo.png
Executable file
BIN
Project/public/demo.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 10 MiB |
BIN
Project/public/electricity-sample-bill.pdf
Executable file
BIN
Project/public/electricity-sample-bill.pdf
Executable file
Binary file not shown.
BIN
Project/public/favicon.ico
Executable file
BIN
Project/public/favicon.ico
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
BIN
Project/public/homepage-video.mp4
Executable file
BIN
Project/public/homepage-video.mp4
Executable file
Binary file not shown.
23
Project/styles/globals.css
Executable file
23
Project/styles/globals.css
Executable file
@@ -0,0 +1,23 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer utilities {
|
||||
.ripple-effect {
|
||||
position: fixed;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
transform: translate(-50%, -50%) scale(0);
|
||||
animation: ripple-animation 1s ease-out forwards;
|
||||
pointer-events: none;
|
||||
z-index: 5;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes ripple-animation {
|
||||
to {
|
||||
transform: translate(-50%, -50%) scale(300);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
22
Project/tailwind.config.js
Executable file
22
Project/tailwind.config.js
Executable file
@@ -0,0 +1,22 @@
|
||||
import { nextui } from '@nextui-org/theme'
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
'./components/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
'./app/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
'./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}'
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ["var(--font-sans)"],
|
||||
mono: ["var(--font-mono)"],
|
||||
baskerville: ["Libre Baskerville", "serif"],
|
||||
},
|
||||
},
|
||||
},
|
||||
darkMode: "class",
|
||||
darkMode: "class",
|
||||
plugins: [nextui()],
|
||||
}
|
||||
28
Project/tsconfig.json
Executable file
28
Project/tsconfig.json
Executable file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
5
Project/types/index.ts
Executable file
5
Project/types/index.ts
Executable file
@@ -0,0 +1,5 @@
|
||||
import { SVGProps } from "react";
|
||||
|
||||
export type IconSvgProps = SVGProps<SVGSVGElement> & {
|
||||
size?: number;
|
||||
};
|
||||
Reference in New Issue
Block a user