Initial Code Commit
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
images/
|
||||||
|
File Converter/pnpm-lock.yaml
|
||||||
58
AI Training/ai.ipynb
Executable file
58
AI Training/ai.ipynb
Executable file
@@ -0,0 +1,58 @@
|
|||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 3,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"import dask.dataframe as dd"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 5,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "stdout",
|
||||||
|
"output_type": "stream",
|
||||||
|
"text": [
|
||||||
|
" image label\n",
|
||||||
|
"0 {'bytes': b'\\xff\\xd8\\xff\\xe0\\x00\\x10JFIF\\x00\\x... 0\n",
|
||||||
|
"1 {'bytes': b'\\xff\\xd8\\xff\\xe0\\x00\\x10JFIF\\x00\\x... 0\n",
|
||||||
|
"2 {'bytes': b'\\xff\\xd8\\xff\\xe0\\x00\\x10JFIF\\x00\\x... 0\n",
|
||||||
|
"3 {'bytes': b'\\xff\\xd8\\xff\\xe0\\x00\\x10JFIF\\x00\\x... 0\n",
|
||||||
|
"4 {'bytes': b'\\xff\\xd8\\xff\\xe0\\x00\\x10JFIF\\x00\\x... 0\n"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"df = dd.read_parquet(\"hf://datasets/edwinpalegre/trashnet_enhanced/data/train-*.parquet\")\n",
|
||||||
|
"\n",
|
||||||
|
"print(df.show())"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"kernelspec": {
|
||||||
|
"display_name": "Python 3",
|
||||||
|
"language": "python",
|
||||||
|
"name": "python3"
|
||||||
|
},
|
||||||
|
"language_info": {
|
||||||
|
"codemirror_mode": {
|
||||||
|
"name": "ipython",
|
||||||
|
"version": 3
|
||||||
|
},
|
||||||
|
"file_extension": ".py",
|
||||||
|
"mimetype": "text/x-python",
|
||||||
|
"name": "python",
|
||||||
|
"nbconvert_exporter": "python",
|
||||||
|
"pygments_lexer": "ipython3",
|
||||||
|
"version": "3.12.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nbformat": 4,
|
||||||
|
"nbformat_minor": 2
|
||||||
|
}
|
||||||
BIN
AI Training/electricity-sample-bill.pdf
Executable file
BIN
AI Training/electricity-sample-bill.pdf
Executable file
Binary file not shown.
6
AI Training/hf.py
Executable file
6
AI Training/hf.py
Executable file
@@ -0,0 +1,6 @@
|
|||||||
|
# Load model directly
|
||||||
|
from transformers import AutoImageProcessor, AutoModelForImageClassification
|
||||||
|
|
||||||
|
processor = AutoImageProcessor.from_pretrained("edwinpalegre/ee8225-group4-vit-trashnet-enhanced")
|
||||||
|
model = AutoModelForImageClassification.from_pretrained("edwinpalegre/ee8225-group4-vit-trashnet-enhanced")
|
||||||
|
|
||||||
96
AI Training/main.py
Executable file
96
AI Training/main.py
Executable file
@@ -0,0 +1,96 @@
|
|||||||
|
import dask.dataframe as dd
|
||||||
|
|
||||||
|
from azure.cognitiveservices.vision.customvision.training import CustomVisionTrainingClient
|
||||||
|
from azure.cognitiveservices.vision.customvision.prediction import CustomVisionPredictionClient
|
||||||
|
from azure.cognitiveservices.vision.customvision.training.models import ImageFileCreateBatch, ImageFileCreateEntry, Region
|
||||||
|
from msrest.authentication import ApiKeyCredentials
|
||||||
|
import os, time, uuid
|
||||||
|
|
||||||
|
ENDPOINT = "https://trashvision.cognitiveservices.azure.com/"
|
||||||
|
training_key = "611e786a785648e38f346f18e7f7e7ed"
|
||||||
|
prediction_key = "611e786a785648e38f346f18e7f7e7ed"
|
||||||
|
project_id = "a67f7d7b-c980-49bd-b57d-0bd1367b29d0"
|
||||||
|
|
||||||
|
credentials = ApiKeyCredentials(in_headers={"Training-key": training_key})
|
||||||
|
trainer = CustomVisionTrainingClient(ENDPOINT, credentials)
|
||||||
|
prediction_credentials = ApiKeyCredentials(in_headers={"Prediction-key": prediction_key})
|
||||||
|
predictor = CustomVisionPredictionClient(ENDPOINT, prediction_credentials)
|
||||||
|
|
||||||
|
df = dd.read_parquet("hf://datasets/edwinpalegre/trashnet_enhanced/data/train-*.parquet")
|
||||||
|
|
||||||
|
# ## iterate over the the first 5 rows of the dataframe and decode the image bytes to an image and save it to a file
|
||||||
|
|
||||||
|
tags = trainer.get_tags(project_id)
|
||||||
|
|
||||||
|
biodegradable_tag = None
|
||||||
|
cardboard_tag = None
|
||||||
|
glass_tag = None
|
||||||
|
metal_tag = None
|
||||||
|
paper_tag = None
|
||||||
|
plastic_tag = None
|
||||||
|
|
||||||
|
for tag in tags:
|
||||||
|
if tag.name == "biodegradable":
|
||||||
|
biodegradable_tag = tag
|
||||||
|
elif tag.name == "cardboard":
|
||||||
|
cardboard_tag = tag
|
||||||
|
elif tag.name == "glass":
|
||||||
|
glass_tag = tag
|
||||||
|
elif tag.name == "metal":
|
||||||
|
metal_tag = tag
|
||||||
|
elif tag.name == "paper":
|
||||||
|
paper_tag = tag
|
||||||
|
elif tag.name == "plastic":
|
||||||
|
plastic_tag = tag
|
||||||
|
|
||||||
|
print(biodegradable_tag)
|
||||||
|
print(cardboard_tag)
|
||||||
|
print(glass_tag)
|
||||||
|
print(metal_tag)
|
||||||
|
print(paper_tag)
|
||||||
|
print(plastic_tag)
|
||||||
|
|
||||||
|
# get all images from in the current dir and upload them to the custom vision project
|
||||||
|
|
||||||
|
# base_image_location = os.path.join (os.path.dirname(__file__), "images")
|
||||||
|
|
||||||
|
# tagged_images_with_regions = []
|
||||||
|
|
||||||
|
# for image in os.listdir(base_image_location):
|
||||||
|
# print(image)
|
||||||
|
# with open(os.path.join(base_image_location, image), "rb") as image_contents:
|
||||||
|
# trainer.create_images_from_data(project_id, image_contents.read(), [biodegradable_tag.id])
|
||||||
|
# print("Uploaded image: ", image)
|
||||||
|
# time.sleep(5)
|
||||||
|
|
||||||
|
skip = 10031
|
||||||
|
count = 0
|
||||||
|
|
||||||
|
for index, row in df.iterrows():
|
||||||
|
|
||||||
|
if count < skip:
|
||||||
|
count += 1
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
image = row["image"]["bytes"]
|
||||||
|
label = row["label"]
|
||||||
|
|
||||||
|
if label == 0:
|
||||||
|
trainer.create_images_from_data(project_id, image, [biodegradable_tag.id])
|
||||||
|
elif label == 1:
|
||||||
|
trainer.create_images_from_data(project_id, image, [cardboard_tag.id])
|
||||||
|
elif label == 2:
|
||||||
|
trainer.create_images_from_data(project_id, image, [glass_tag.id])
|
||||||
|
elif label == 3:
|
||||||
|
trainer.create_images_from_data(project_id, image, [metal_tag.id])
|
||||||
|
elif label == 4:
|
||||||
|
trainer.create_images_from_data(project_id, image, [paper_tag.id])
|
||||||
|
elif label == 5:
|
||||||
|
trainer.create_images_from_data(project_id, image, [plastic_tag.id])
|
||||||
|
|
||||||
|
print(f"C: {count}, I: {index}, L: {label}, Uploaded image")
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
print("Done uploading images")
|
||||||
39
AI Training/ml.py
Executable file
39
AI Training/ml.py
Executable file
@@ -0,0 +1,39 @@
|
|||||||
|
import dask.dataframe as dd
|
||||||
|
import os
|
||||||
|
|
||||||
|
df = dd.read_parquet("hf://datasets/edwinpalegre/trashnet_enhanced/data/train-*.parquet")
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
|
||||||
|
for index, row in df.iterrows():
|
||||||
|
label = row["label"]
|
||||||
|
image = row["image"]["bytes"]
|
||||||
|
|
||||||
|
if label == 0:
|
||||||
|
with open(os.path.join("images", "biodegradable", f"biodegradable_{count}.jpg"), "wb") as f:
|
||||||
|
f.write(image)
|
||||||
|
elif label == 1:
|
||||||
|
with open(os.path.join("images", "cardboard", f"cardboard_{count}.jpg"), "wb") as f:
|
||||||
|
f.write(image)
|
||||||
|
elif label == 2:
|
||||||
|
with open(os.path.join("images", "glass", f"glass_{count}.jpg"), "wb") as f:
|
||||||
|
f.write(image)
|
||||||
|
elif label == 3:
|
||||||
|
with open(os.path.join("images", "metal", f"metal_{count}.jpg"), "wb") as f:
|
||||||
|
f.write(image)
|
||||||
|
elif label == 4:
|
||||||
|
with open(os.path.join("images", "paper", f"paper_{count}.jpg"), "wb") as f:
|
||||||
|
f.write(image)
|
||||||
|
elif label == 5:
|
||||||
|
with open(os.path.join("images", "plastic", f"plastic_{count}.jpg"), "wb") as f:
|
||||||
|
f.write(image)
|
||||||
|
else:
|
||||||
|
print("Label not found")
|
||||||
|
break
|
||||||
|
|
||||||
|
print(f"Saved image {count}")
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
print("Done!")
|
||||||
|
|
||||||
|
|
||||||
29
AI Training/pdf.py
Executable file
29
AI Training/pdf.py
Executable file
@@ -0,0 +1,29 @@
|
|||||||
|
from inference import get_model
|
||||||
|
import supervision as sv
|
||||||
|
import cv2
|
||||||
|
|
||||||
|
# define the image url to use for inference
|
||||||
|
image_file = "taylor-swift-album-1989.jpeg"
|
||||||
|
image = cv2.imread(image_file)
|
||||||
|
|
||||||
|
# load a pre-trained yolov8n model
|
||||||
|
model = get_model(model_id="taylor-swift-records/3")
|
||||||
|
|
||||||
|
# run inference on our chosen image, image can be a url, a numpy array, a PIL image, etc.
|
||||||
|
results = model.infer(image)[0]
|
||||||
|
|
||||||
|
# load the results into the supervision Detections api
|
||||||
|
detections = sv.Detections.from_inference(results)
|
||||||
|
|
||||||
|
# create supervision annotators
|
||||||
|
bounding_box_annotator = sv.BoundingBoxAnnotator()
|
||||||
|
label_annotator = sv.LabelAnnotator()
|
||||||
|
|
||||||
|
# annotate the image with our inference results
|
||||||
|
annotated_image = bounding_box_annotator.annotate(
|
||||||
|
scene=image, detections=detections)
|
||||||
|
annotated_image = label_annotator.annotate(
|
||||||
|
scene=annotated_image, detections=detections)
|
||||||
|
|
||||||
|
# display the image
|
||||||
|
sv.plot_image(annotated_image)
|
||||||
29
AI Training/train.py
Executable file
29
AI Training/train.py
Executable file
@@ -0,0 +1,29 @@
|
|||||||
|
import dask.dataframe as dd
|
||||||
|
|
||||||
|
from azure.cognitiveservices.vision.customvision.training import CustomVisionTrainingClient
|
||||||
|
from azure.cognitiveservices.vision.customvision.prediction import CustomVisionPredictionClient
|
||||||
|
from azure.cognitiveservices.vision.customvision.training.models import ImageFileCreateBatch, ImageFileCreateEntry, Region
|
||||||
|
from msrest.authentication import ApiKeyCredentials
|
||||||
|
import os, time, uuid
|
||||||
|
|
||||||
|
ENDPOINT = "https://trashvision.cognitiveservices.azure.com/"
|
||||||
|
training_key = "611e786a785648e38f346f18e7f7e7ed"
|
||||||
|
prediction_key = "611e786a785648e38f346f18e7f7e7ed"
|
||||||
|
project_id = "a67f7d7b-c980-49bd-b57d-0bd1367b29d0"
|
||||||
|
|
||||||
|
credentials = ApiKeyCredentials(in_headers={"Training-key": training_key})
|
||||||
|
trainer = CustomVisionTrainingClient(ENDPOINT, credentials)
|
||||||
|
prediction_credentials = ApiKeyCredentials(in_headers={"Prediction-key": prediction_key})
|
||||||
|
predictor = CustomVisionPredictionClient(ENDPOINT, prediction_credentials)
|
||||||
|
|
||||||
|
print ("Training...")
|
||||||
|
iteration = trainer.train_project(project_id)
|
||||||
|
while (iteration.status != "Completed"):
|
||||||
|
iteration = trainer.get_iteration(project_id, iteration.id)
|
||||||
|
print ("Training status: " + iteration.status)
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
# The iteration is now trained. Publish it to the project endpoint
|
||||||
|
trainer.publish_iteration(project_id, iteration.id, "HaxNet1", predictor)
|
||||||
|
print ("Done!")
|
||||||
|
|
||||||
38
File Converter/index.js
Executable file
38
File Converter/index.js
Executable file
@@ -0,0 +1,38 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import bodyParser from 'body-parser';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
import fileUpload from 'express-fileupload';
|
||||||
|
|
||||||
|
import { pdf } from "pdf-to-img";
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
app.use(bodyParser.json());
|
||||||
|
|
||||||
|
app.use(fileUpload());
|
||||||
|
|
||||||
|
app.post('/convert', async(req, res) => {
|
||||||
|
|
||||||
|
console.log(req.files);
|
||||||
|
|
||||||
|
let file = req.files.pdf;
|
||||||
|
|
||||||
|
let images = [];
|
||||||
|
const document = await pdf(file.data, { scale: 3 });
|
||||||
|
|
||||||
|
for await (const image of document) {
|
||||||
|
images.push(image);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.send(images);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/', (req, res) => {
|
||||||
|
res.send('Hello World');
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(5000, () => {
|
||||||
|
console.log('Server is running on http://localhost:3000');
|
||||||
|
});
|
||||||
21
File Converter/package.json
Executable file
21
File Converter/package.json
Executable file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "Opensource",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"type": "module",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"body-parser": "^1.20.3",
|
||||||
|
"express": "^4.21.1",
|
||||||
|
"express-fileupload": "^1.5.1",
|
||||||
|
"fs": "0.0.1-security",
|
||||||
|
"path": "^0.12.7",
|
||||||
|
"pdf-to-img": "^4.1.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
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;
|
||||||
|
};
|
||||||
151
README.md
Normal file
151
README.md
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
# Patriot Hacks 2024
|
||||||
|
|
||||||
|
PatriotHacks 24 - 🏆 Microsoft X Cloudforce & 🏆 Save the World
|
||||||
|
|
||||||
|
## Repository overview
|
||||||
|
|
||||||
|
This repository contains the team's work for PatriotHacks 2024. It is divided into three main folders:
|
||||||
|
|
||||||
|
- `AI Training/` — Python scripts and small dataset utilities used to prepare and train ML models for trash classification.
|
||||||
|
- `File Converter/` — a small Node.js express service that converts PDF documents into images.
|
||||||
|
- `Project/` — a Next.js web application (called "Carbin") for tracking building carbon footprints and providing a trash-detection interface.
|
||||||
|
|
||||||
|
Below are concise descriptions and quick start instructions for each subfolder so you (or another team member) can run or continue development.
|
||||||
|
|
||||||
|
## AI Training/
|
||||||
|
|
||||||
|
Purpose
|
||||||
|
- Utilities and small scripts used to: extract images from a HF parquet dataset, upload images to Azure Custom Vision, and run model inference examples.
|
||||||
|
|
||||||
|
Key files
|
||||||
|
- `hf.py` — shows how to load a Hugging Face image classification model (AutoImageProcessor + AutoModelForImageClassification) for inference.
|
||||||
|
- `train.py` — script that uploads images from a Hugging Face parquet dataset into an Azure Custom Vision project and triggers training. NOTE: the current script contains hard-coded Azure endpoint/API keys and project IDs; replace these with environment variables before sharing or running.
|
||||||
|
- `ml.py` — extracts images from a Hugging Face parquet dataset and writes images into the `AI Training/images/` folders by label (biodegradable, cardboard, glass, metal, paper, plastic).
|
||||||
|
- `main.py` — another example showing uploading images to Custom Vision and iterating the dataset to create images by tag.
|
||||||
|
- `pdf.py` — (small utility) sample code to work with pdf-to-image flows referenced in other parts of the project.
|
||||||
|
|
||||||
|
Quick start (local, Python)
|
||||||
|
|
||||||
|
1. Create and activate a virtual environment (recommended):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Install common dependencies used by the scripts (adjust versions as needed):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install dask[complete] azure-cognitiveservices-vision-customvision msrest transformers opencv-python
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Important: set Azure Custom Vision credentials and the project id as environment variables or edit the scripts to load them securely (do not commit keys):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export AZURE_CUSTOMVISION_ENDPOINT="https://..."
|
||||||
|
export AZURE_CUSTOMVISION_TRAINING_KEY="<your-training-key>"
|
||||||
|
export AZURE_CUSTOMVISION_PREDICTION_KEY="<your-prediction-key>"
|
||||||
|
export CUSTOMVISION_PROJECT_ID="<project-id>"
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Run the scripts as needed. For example, to extract images from the HF parquet source:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python "AI Training/ml.py"
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes and security
|
||||||
|
- Some scripts currently contain hard-coded keys and endpoints. Replace them with environment variables or a secrets store before running in any shared environment.
|
||||||
|
- The code expects a Hugging Face dataset accessible via an `hf://` parquet path. Ensure you have appropriate HF credentials or local parquet files when running.
|
||||||
|
|
||||||
|
## File Converter/
|
||||||
|
|
||||||
|
Purpose
|
||||||
|
- Small Node.js Express service that accepts an uploaded PDF and returns PNG/JPEG frames generated via `pdf-to-img`.
|
||||||
|
|
||||||
|
Key files
|
||||||
|
- `index.js` — Express server exposing `/convert` POST endpoint that accepts a file upload (`express-fileupload`) and converts the PDF into images with `pdf-to-img`.
|
||||||
|
- `package.json` — lists dependencies required by the service.
|
||||||
|
|
||||||
|
Quick start (Node.js)
|
||||||
|
|
||||||
|
1. From the `File Converter` folder, install dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd "File Converter"
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Run the server (the `package.json` in this folder does not define a start script, so run node directly):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node index.js
|
||||||
|
```
|
||||||
|
|
||||||
|
3. The server listens on port 5000 (see `index.js`). POST a `multipart/form-data` request with the `pdf` file field to `http://localhost:5000/convert` to receive converted images in the response.
|
||||||
|
|
||||||
|
Notes
|
||||||
|
- Consider adding a `start` script to `package.json` and proper error handling and file-size limits before production use.
|
||||||
|
|
||||||
|
## Project/ (Carbin)
|
||||||
|
|
||||||
|
Purpose
|
||||||
|
- A Next.js (v14) application named "Carbin" for tracking building carbon footprints. It includes:
|
||||||
|
- Realtime trash detection UI using an inference worker and webcam access
|
||||||
|
- Building list and detail pages
|
||||||
|
- Firebase-backed data persistence for building metrics
|
||||||
|
|
||||||
|
Key files & directories (high level)
|
||||||
|
- `package.json` — lists dependencies and provides `dev`, `build`, and `start` scripts.
|
||||||
|
- `app/` — Next.js app routes and pages including API routes under `app/api/` (e.g. `buildings`, `chat`, `pdf-to-image`).
|
||||||
|
- `components/` — reusable UI components (navbar, trash-detection UI, theme switch, etc.).
|
||||||
|
- `lib/firebase.ts` — Firebase initialization (reads config from `NEXT_PUBLIC_*` env vars).
|
||||||
|
- `lib/useBuildingData.ts` — React Query hooks for fetching and updating building data.
|
||||||
|
- `config/` — app config (site metadata and trash item definitions used for co2 calculations).
|
||||||
|
|
||||||
|
Quick start (Next.js)
|
||||||
|
|
||||||
|
1. From the `Project/` folder, install dependencies and run dev:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd Project
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Required environment variables
|
||||||
|
- The app uses Firebase and expects the following env vars (set these before running):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export NEXT_PUBLIC_FIREBASE_API_KEY="..."
|
||||||
|
export NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN="..."
|
||||||
|
export NEXT_PUBLIC_FIREBASE_PROJECT_ID="..."
|
||||||
|
export NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET="..."
|
||||||
|
export NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID="..."
|
||||||
|
export NEXT_PUBLIC_FIREBASE_APP_ID="..."
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Visit `http://localhost:3000` after running `npm run dev`.
|
||||||
|
|
||||||
|
Notes and next steps
|
||||||
|
- The project contains an inference worker integration (`inferencejs`) used by the `trashDetection` component to run a model in the browser/worker; you will need the correct worker name/id and API key for that service.
|
||||||
|
- The `config/trashItems.ts` file contains per-item CO2e estimates used by the app — adjust values as appropriate.
|
||||||
|
|
||||||
|
## Contribution & development tips
|
||||||
|
|
||||||
|
- Replace any hard-coded secrets (Azure keys, Firebase secrets) with environment variables or a vault before committing.
|
||||||
|
- Add `start`/`dev` scripts where missing (e.g., the `File Converter` folder) for a consistent DX.
|
||||||
|
- Consider adding minimal README files inside each subfolder (AI Training/, File Converter/, Project/) if you plan to expand the team.
|
||||||
|
|
||||||
|
## Contact / Source
|
||||||
|
|
||||||
|
Repository maintained for PatriotHacks 2024. The Next.js app links to the GitHub org in `Project/config/site.ts`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
If you want, I can:
|
||||||
|
- add per-folder README files
|
||||||
|
- create a `requirements.txt` for the Python scripts and a `start` script for the File Converter
|
||||||
|
- remove or externalize hard-coded secrets into env var usage in the Python scripts
|
||||||
|
|
||||||
|
Tell me which of those you'd like next and I'll implement it.
|
||||||
Reference in New Issue
Block a user