Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
7c91a34
Verify installs and create folder structure
Nick040791 Jun 12, 2026
6ae3eeb
Routes added
Nick040791 Jun 12, 2026
09c6738
created context components
Nick040791 Jun 12, 2026
1ba6083
imported search context into useSearch
Nick040791 Jun 12, 2026
9b7d38d
update main.jsx and App.jsx
Nick040791 Jun 12, 2026
89cfae3
updated main and stats
Nick040791 Jun 12, 2026
1d9f025
asd
Nick040791 Jun 13, 2026
d01d53d
Search menu logic
Nick040791 Jun 14, 2026
325af25
Fixed SearchMenu typo and syntax
Nick040791 Jun 14, 2026
bb32d3b
1. Fixed Nav elements in NavBar.jsx
Nick040791 Jun 14, 2026
4d8c159
Updated search menu and completed metric cards
Nick040791 Jun 21, 2026
7a5b8c9
Update Homepage to use Home component, fixed flaky imports and syntax…
Nick040791 Jun 27, 2026
83d9d5c
Finish search results table
Nick040791 Jun 27, 2026
e9b0d52
Fixed SearchMenu showing two success messages
Nick040791 Jun 28, 2026
54a5ae4
Added search results table component to search page
Nick040791 Jun 28, 2026
ba1e37a
fixed lower_keyword storing non lowercase values in routes.js
Nick040791 Jul 1, 2026
2c56b8e
Fix routes and no initial state error rendering in useSearch
Nick040791 Jul 4, 2026
0a5afd7
stuff
Nick040791 Jul 4, 2026
576e03e
Update the server to handle empty initial filtertype value
Nick040791 Jul 5, 2026
bce70c0
realized I didn't add metic cards to the SearchPage :/ and added boot…
Nick040791 Jul 5, 2026
00ba733
change route path to catch all invalid paths
Nick040791 Jul 5, 2026
a9efa98
Spice up 404 and hompage with bootstrap
Nick040791 Jul 5, 2026
ef2c10b
fixed metric field key
Nick040791 Jul 6, 2026
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
416 changes: 267 additions & 149 deletions api/package-lock.json

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions api/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@ router.get("/data/search", (req, res, next) => {
- behaviorClass
*/

const filterType = req.query.filterType || null;
const filterType = req.query.filterType;
const keyword = req.query.keyword || null;
let searchType = "unfiltered"; // default the search type

let searchType;
if (filterType) {
if (filterType && filterType !=="") { // if filter type is truthy and not empty
const lower_case = filterType.toLowerCase();
searchType =
lower_case === "model"

searchType = lower_case==="model"
? "m"
: lower_case === "gender"
? "g"
Expand Down Expand Up @@ -53,7 +53,7 @@ router.get("/data/search", (req, res, next) => {
0;
break;
case "bc":
include = record["User Behavior Class"] === lower_keyword;
include = record["User Behavior Class"].toLowerCase() === lower_keyword;
break;
default:
return false;
Expand Down
1,473 changes: 768 additions & 705 deletions assignment-app/package-lock.json

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions assignment-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@
"preview": "vite preview"
},
"dependencies": {
"bootstrap": "^5.3.8",
"react": "^18.3.1",
"react-bootstrap": "^2.10.10",
"react-dom": "^18.3.1",
"react-router-dom": "^6.28.0"
"react-router-dom": "^6.30.4"
},
"devDependencies": {
"@eslint/js": "^9.13.0",
Expand All @@ -24,6 +26,6 @@
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.14",
"globals": "^15.11.0",
"vite": "^5.4.10"
"vite": "^4.5.14"
}
}
21 changes: 14 additions & 7 deletions assignment-app/src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
import React from "react";
import { Routes, Route, } from "react-router-dom";
import HomePage from './pages/HomePage';
import SearchPage from './pages/SearchPage';
import NotFound from './components/NotFound';
import Navbar from './components/Navbar';

function App() {
return (
<div>
<nav className="navbar navbar-expand-lg navbar-dark bg-dark sticky-top">
{/* <p> tag is a placeholder, You'll need to change the tag/component type later*/}
<p className="navbar-brand ms-4 nav-link">User Behavior Data</p>
</nav>
<>
<Navbar />
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/Search" element={<SearchPage />} />
<Route path="/404" element={<NotFound />} />
<Route path="*" element={<NotFound />} />
</Routes>
Comment thread
Nick040791 marked this conversation as resolved.
<hr />
</div>
</>
);
}

Expand Down
35 changes: 35 additions & 0 deletions assignment-app/src/components/Home.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
const Home = () => {
return <section className="container my-5">
<div className="card shadow-sm border-0">
<div className="card-body p-4 p-md-5">
<h1 className="display-5 fw-bold text-primary mb-3">User Behavior Dataset</h1>
<p className="lead-text-muted">This dataset provides a comprehensive analysis
of mobile device usage patterns and user behavior classification. It contains 700 samples
of user data, including metrics such as app usage time, screen-on time, battery drain, and data
consumption. Each entry is categorized into one of five user behavior classes, ranging from light to
extreme usage, allowing for insightful analysis and modeling.</p>

<p className="h-4 fw-semibold mt-4 mb-3">Key Features: </p>

<ul className="list-group list-group-flush">
<li className="list-group-item px-0"><strong>User ID:</strong> Unique identifier for each user.</li>
<li className="list-group-item px-0"><strong>Device Model:</strong> Model of the user{`'s`} smartphone.</li>
<li className="list-group-item px-0"><strong>Operating System:</strong> The OS of the device (iOS or Android).</li>
<li className="list-group-item px-0"><strong>App Usage Time:</strong> Daily time spent on mobile applications, measured in minutes.</li>
<li className="list-group-item px-0"><strong>Screen On Time:</strong> Average hours per day the screen is active.</li>
<li className="list-group-item px-0"><strong>Battery Drain:</strong> Daily battery consumption in mAh.</li>
<li className="list-group-item px-0"><strong>Number of Apps Installed:</strong> Total apps available on the device.</li>
<li className="list-group-item px-0"><strong>Data Usage:</strong> Daily mobile data consumption in megabytes.</li>
<li className="list-group-item px-0"><strong>Age:</strong> Age of the user.</li>
<li className="list-group-item px-0"><strong>Gender:</strong> Gender of the user (Male or Female).</li>
<li className="list-group-item px-0"><strong>User Behavior Class:</strong> Classification of user behavior based on usage patterns (1 to 5).</li>
</ul>
</div>
</div>
<div>
<a href="https://www.kaggle.com/datasets/valakhorasani/mobile-device-usage-and-user-behavior-dataset?resource=download" >Sourced from this Kaggle Dataset</a>
</div>
</section>
};

export default Home;
64 changes: 64 additions & 0 deletions assignment-app/src/components/MetricCards.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { useMemo } from 'react';
import { useSearch } from '../hooks/useSearch';
import { METRIC_FIELDS } from '../utils/constants';
import { average, formatNumber, median } from '../utils/stats';

function MetricCards() {
const { results, status } = useSearch();
const metricData = useMemo(() => {
return METRIC_FIELDS.map((field) => {
const values = results
.map((record) => Number(record[field.key]))
.filter((value) => !Number.isNaN(value));

return {
title: field.title,
unit: field.unit,
averageValue: average(values),
medianValue: median(values),
};
});
},[results])

if (results.length === 0 && status != 'loading') {
return (
<section className="container py-4">
<div className="card text-center text-muted">
<div className="card-body">
Run a search to see metrics.
</div>
</div>
</section>
);
}

return (
<section className="container py-4">
<div className="row row-cols-1 row-col-me-4 g-3">
{
metricData.map((metric) => (
<div className="col" key={metric.title}>
<div className="card h-100 shadow-sm">
<div className="card-body">
<h2 className="h5 card-title">{metric.title}</h2>

<p className="card-text mb-1">
Average: {formatNumber(metric.averageValue)} {metric.unit}
</p>

<p className="card-text mb-0">
Median: {formatNumber(metric.medianValue)} {metric.unit}
</p>
</div>
</div>
</div>
)
)
}

</div>
</section>
);
};

export default MetricCards;
13 changes: 13 additions & 0 deletions assignment-app/src/components/Navbar.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { NavLink } from "react-router-dom";

const Navbar = () => {
return (
<nav className="navbar navbar-expand-lg navbar-dark bg-dark sticky-top">
<NavLink to="/" className="navbar-brand ms-4 nav-link">User Behavior Data</NavLink>
<NavLink to="/Search" className="navbar-brand ms-4 nav-link">Search</NavLink>
</nav>
Comment thread
Nick040791 marked this conversation as resolved.
);
};

export default Navbar;

25 changes: 25 additions & 0 deletions assignment-app/src/components/NotFound.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Link } from "react-router-dom";


// The not found component for when stuff is...

const NotFound = () => {
return (
<section className="container py-5 text-center border border-4 bg-light shadow-sm">
<div className="row justify-content-center">
<div className="col-md-6">
<h1 className="display-1 fw-bold text-danger mb-4">404</h1>
<h2 className="h-4 mb-3 text-primary">Not Found</h2>
<p className="text-muted mb-5">This page either does not exist or I broke it somehow :/</p>
<div className="d-grip gap-2 col-md-4 mx-auto">
<p className="text-muted">You don&apos;t have to go</p>
<Link to="/">HOME</Link>
<p className="text-muted">But you can&apos;t stay here</p>
</div>
</div>
</div>
</section>
);
};

export default NotFound;
73 changes: 73 additions & 0 deletions assignment-app/src/components/SearchMenu.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { useState } from "react";
import { useSearch } from '../hooks/useSearch'
import { FILTER_TYPE_OPTIONS } from "../utils/constants";

/*
Okay, so the SearchMenu component handles the search menu on the search page.
I'm trying to grab the search state and helper functions from the search context.
This is the main search state that the rest of the app will use.
The 'local state' below the function is just for what the user sees before they hit submit and stuff.
*/


function SearchMenu(){

const { filterType, setFilterType, keyword, setKeyword, results, status, errorMsg, runSearch, } = useSearch();
const[localFilterType, setLocalFilterType] = useState(filterType);
const[localKeyword, setLocalKeyword] = useState(keyword);

function handleSubmit(event){ // you're a nested function
event.preventDefault(); // when we hit the search button, don't do what you normally do
setFilterType(localFilterType); // instead set the SearchMenu filter type using the local one we just got
setKeyword(localKeyword); // also do it again but keyword (͡° ͜ʖ ͡°)

runSearch({
filterType: localFilterType,
keyword: localKeyword,
});
}



return (
<section className="container py-4 text-start">
<div className="p-4 bg-light rounded-3 shadow-sm border">
<form className="row g-3 align-items-end" onSubmit={handleSubmit}>

<div className="col-md-3">
<label className="form-label" htmlFor="filterType">Filter Type</label>
<select id="filterType" value={localFilterType} className="form-select" onChange={(event) => setLocalFilterType(event.target.value)}>
{FILTER_TYPE_OPTIONS.map((option) => (<option value ={option.value} key={option.value || 'all'}>
{option.label}
</option>) )}
</select>
Comment thread
Nick040791 marked this conversation as resolved.
</div>

<div className="col-md-6">
<label className="form-label" htmlFor="keyword">
Keyword
</label>
<input id="keyword" className="form-control" type="search" value={localKeyword} onChange={(event) => setLocalKeyword(event.target.value)} placeholder="Enter search keyword" />
</div>

<div className="col-md-3">
<button className="btn btn-primary w-100" type="submit" disabled={status === 'loading'}>
{status === 'loading' ? 'Searching...' : 'Search'}
</button>
</div>

</form>
</div>

<div className="mt-3">
{status === 'loading' && (<p className="text-muted mb-0">Loading...</p>)}
{status === 'error' && (<p className="text-danger mb-0">Error: {errorMsg}</p>)}
{status === 'success' && results.length > 0 && (
<p className="text-muted mb-0">Displaying {results.length} Records</p>
)}
</div>
</section>
);
};

export default SearchMenu;
66 changes: 66 additions & 0 deletions assignment-app/src/components/SearchResultsTable.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// 🡫 Grab the useSearch hook! 🡫
import { useSearch } from "../hooks/useSearch";

function SearchResultsTable(){

//Put the {fries and stuff} in the bag;
const { results = [] , status = "loading", errorMsg = "" } = useSearch();


// 🡫 Show a loading artifact while fetching and rendering results and stuff 🡫
if (status ==="loading"){
return (
<section className="container py-3">
<p className="text-muted">Loading Records...</p>
</section>
);
};

if (results.length === 0) {
return (
<section className="container py-4 text-start">
{status === "error" && (
<div className="text-danger mb-3">Error: {errorMsg}</div>
)}

<p className="text-muted">No Records To Display</p>
</section>
);
}

const headers = Object.keys(results[0]);

return (
<section className="container py-4 text-start">
{status === "error" && (
<div className="text-danger mb-3">Error: {errorMsg}</div>
)}

<div className="table-responsive">
<table className="table table-striped table-hover text-start">
<thead>
<tr>
{headers.map((header) => (
<th className="text-start" key={header} scope="col">
{header}
</th>
))}
</tr>
</thead>

<tbody>
{results.map((record, index) => (
<tr key={record["User ID"] || index}>
{headers.map((header) => (
<td key={header}>{record[header]}</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</section>
);
}

export default SearchResultsTable;
Loading