Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,519 changes: 1,462 additions & 57 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@
"deploy": "gh-pages -d dist"
},
"dependencies": {
"firebase": "^12.6.0",
"gh-pages": "^6.3.0",
"react": "^19.2.0",
"react-dom": "^19.2.0"
"react-dom": "^19.2.0",
"recharts": "^3.4.1"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
Expand Down
Binary file added public/geisel.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
259 changes: 230 additions & 29 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,236 @@
import { useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'
// src/App.jsx
import React, { useState } from "react";
import {
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
Legend,
} from "recharts";

// 🔹 Temporary fake data per floor (we'll replace with Firebase data later)
const floorData = [
{ floor: "1", devices: 8 },
{ floor: "2", devices: 12 },
{ floor: "4", devices: 5 },
{ floor: "5", devices: 10 },
{ floor: "6", devices: 15 },
{ floor: "7", devices: 9 },
{ floor: "8", devices: 4 },
];

// Total across all floors
const totalDevices = floorData.reduce((sum, row) => sum + row.devices, 0);

// Data for the bar chart: all floors + total
const vizData = [
...floorData,
{ floor: "Total", devices: totalDevices },
];

function App() {
const [count, setCount] = useState(0)
const [page, setPage] = useState("home");

const renderPage = () => {
if (page === "home") {
return (
<section className="section home-section">
<h1>Geisel Wi-Fi Occupancy Tracker</h1>
<p>
This project estimates how many devices are connected to Wi-Fi near
ESP32 sensors placed throughout Geisel Library. The long-term goal
is to give students a quick sense of which floors are crowded and
which are open.
</p>
</section>
);
}

if (page === "about") {
return (
<section className="section">
<h1>About</h1>
<p>
ESP32 boards, programmed using the Arduino IDE, periodically scan
for nearby Wi-Fi devices. Each board records:
</p>
<p>
<ul>
<li>How many devices were detected</li>
<li>Which floor the ESP32 is on</li>
<li>The time of the scan</li>
</ul>
</p>
<p>
These readings are sent to an online database (Firebase) and are
visualized on this site. What you see now uses example data; later,
it will be driven by real readings from the deployed sensors.
</p>
</section>
);
}

if (page === "visualization") {
return (
<section className="section">
<h1>Visualization</h1>
<p>
Bar chart of <strong>number of devices per floor</strong> in Geisel.
The last bar shows the <strong>total</strong> across all floors.
This currently uses placeholder data but is wired for live data
later.
</p>

<div className="chart-container">
<ResponsiveContainer width="100%" height={320}>
<BarChart
data={vizData}
margin={{ top: 20, right: 30, left: 0, bottom: 10 }}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis
dataKey="floor"
label={{
value: "Floor",
position: "insideBottomRight",
offset: -5,
fill: "#E5E7EB",
}}
tick={{ fill: "#E5E7EB" }}
/>
<YAxis
label={{
value: "Devices",
angle: -90,
position: "insideLeft",
fill: "#E5E7EB",
}}
tick={{ fill: "#E5E7EB" }}
/>
<Tooltip
contentStyle={{
background: "#020617",
border: "1px solid rgba(148,163,184,0.6)",
borderRadius: "0.5rem",
color: "#E5E7EB",
}}
/>
<Legend
wrapperStyle={{ color: "#E5E7EB", fontSize: "0.8rem" }}
/>

{/* 🔵 New blue→blue gradient */}
<defs>
<linearGradient id="barGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#60a5fa" /> {/* light blue */}
<stop offset="100%" stopColor="#2563eb" /> {/* deep blue */}
</linearGradient>
</defs>

<Bar
dataKey="devices"
name="Devices"
fill="url(#barGradient)"
radius={[6, 6, 0, 0]}
/>
</BarChart>
</ResponsiveContainer>
</div>
</section>
);
}

if (page === "input") {
return (
<section className="section">
<h1>Data Input (Demo)</h1>
<p>
In the final version of this project, readings will be sent
automatically by ESP32 boards. This page is for testing what manual
input to the database would look like.
</p>
<form
className="input-form"
onSubmit={(e) => {
e.preventDefault();
alert(
"Demo only: in the real system this will send data to Firebase."
);
}}
>
<label>
Floor:
<select name="floor" required>
<option value="">Select a floor</option>
<option value="1">1st Floor</option>
<option value="2">2nd Floor</option>
<option value="4">4th Floor</option>
<option value="5">5th Floor</option>
<option value="6">6th Floor</option>
<option value="7">7th Floor</option>
<option value="8">8th Floor</option>
</select>
</label>

<label>
Number of devices:
<input type="number" min="0" name="devices" required />
</label>

<button type="submit">Submit (demo)</button>
</form>
</section>
);
}

return null;
};

return (
<>
<div>
<a href="https://vite.dev" target="_blank">
<img src={viteLogo} className="logo" alt="Vite logo" />
</a>
<a href="https://react.dev" target="_blank">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
</div>
<h1>Vite + React</h1>
<div className="card">
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
<p>
Edit <code>src/App.jsx</code> and save to test HMR
</p>
</div>
<p className="read-the-docs">
Click on the Vite and React logos to learn more
</p>
</>
)
<div className="app">
{/* Top navigation bar */}
<nav className="navbar">
<div className="navbar-title">Geisel Occupancy Tracker</div>
<div className="navbar-links">
<button
className={page === "home" ? "nav-link active" : "nav-link"}
onClick={() => setPage("home")}
>
Home
</button>
<button
className={page === "about" ? "nav-link active" : "nav-link"}
onClick={() => setPage("about")}
>
About
</button>
<button
className={page === "visualization" ? "nav-link active" : "nav-link"}
onClick={() => setPage("visualization")}
>
Visualization
</button>
<button
className={page === "input" ? "nav-link active" : "nav-link"}
onClick={() => setPage("input")}
>
Data Input
</button>
</div>
</nav>

{/* Page content */}
<main className="main">{renderPage()}</main>

{/* Footer */}
<footer className="footer">
ESP32 Wi-Fi Occupancy • DS3 Geisel Library Project
</footer>
</div>
);
}

export default App
export default App;
69 changes: 69 additions & 0 deletions src/DataInput.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// src/DataInput.jsx
import { useState } from "react";
import { db } from "./firebase.js";
import { ref, push } from "firebase/database";

function DataInput() {
const [count, setCount] = useState("");
const [status, setStatus] = useState(null); // "success" | "error" | null

const handleSubmit = async (e) => {
e.preventDefault();
setStatus(null);

const parsed = parseInt(count, 10);
if (Number.isNaN(parsed) || parsed < 0) {
setStatus("error");
return;
}

try {
const readingsRef = ref(db, "readings");
await push(readingsRef, {
count: parsed,
timestamp: Date.now(), // current time in ms
});
setStatus("success");
setCount("");
} catch (err) {
console.error("Error writing to Firebase:", err);
setStatus("error");
}
};

return (
<form className="input-form" onSubmit={handleSubmit}>
<label>
Number of devices:
<input
type="number"
min="0"
value={count}
onChange={(e) => setCount(e.target.value)}
required
/>
</label>

<button type="submit">Submit Reading</button>

{status === "success" && (
<p className="status success">
✅ Reading submitted! Check the Visualization page.
</p>
)}
{status === "error" && (
<p className="status error">
❌ There was a problem. Make sure the count is a non-negative number
and Firebase is configured correctly.
</p>
)}

<p className="input-note">
In the real system, your ESP32 boards will send readings automatically.
This form is mainly for testing and manual input.
</p>
</form>
);
}

export default DataInput;
Loading