Compare commits

..

9 Commits

Author SHA1 Message Date
5dd8b228f4 update readme 2026-03-21 14:35:30 +00:00
ec0da2ab2f update to user feedback 2026-03-21 14:34:05 +00:00
b636009422 Bugfix 2026-03-19 20:26:03 +00:00
2d15819a70 cookies bug fix 2026-03-19 19:00:17 +00:00
644e45434e project cleanup 2026-03-19 17:57:58 +00:00
e5a5911f86 added style sheet 2026-03-19 17:55:48 +00:00
fd5121be80 updated readme 2026-03-19 17:49:06 +00:00
b4add42f2e deployment setup 2026-03-19 17:47:37 +00:00
d15980bb52 time based on periods rather than timestamps 2026-03-19 17:43:00 +00:00
16 changed files with 227 additions and 67 deletions

View File

@@ -1,3 +1,17 @@
# whatsfreeinmyfree # whatsfreeinmyfree
Record of what classrooms are free when I have a free. CS Practice Record of what classrooms are free when I have a free. CS Practice
## Use
To run this, do the following:
- in `frontend` execute `npm run build`
- in `backend` execute `npm run setup`
- in `backend` execute `npm run dev`
If reseting, do the following:
- in `backend` execute `npm run clean`
- in `backend` execute `npm run setup`

3
backend/.gitignore vendored
View File

@@ -24,4 +24,5 @@ dist-ssr
*.sw? *.sw?
adduser.sql adduser.sql
database.db

View File

@@ -16,8 +16,8 @@ CREATE TABLE Rooms (
CREATE TABLE TimeSlots ( CREATE TABLE TimeSlots (
Id INTEGER PRIMARY KEY ASC AUTOINCREMENT, Id INTEGER PRIMARY KEY ASC AUTOINCREMENT,
TimeStart TEXT, Period INTEGER,
TimeEnd TEXT, Day INTEGER,
Room INTEGER, Room INTEGER,
FOREIGN KEY(Room) REFERENCES Rooms(Id) FOREIGN KEY(Room) REFERENCES Rooms(Id)
); );

Binary file not shown.

View File

@@ -4,11 +4,6 @@ import bodyParser from "body-parser";
import bcrypt from "bcrypt"; import bcrypt from "bcrypt";
import Database from "better-sqlite3"; import Database from "better-sqlite3";
Date.prototype.addHours= function(h){
this.setTime(this.getTime() + (h*60*60*1000));
return this;
}
const app = e(); const app = e();
const port = 3000; const port = 3000;
const db = new Database("./database.db"); const db = new Database("./database.db");
@@ -17,34 +12,94 @@ app.use(cors());
app.use(e.json()); app.use(e.json());
app.use(bodyParser.json()); app.use(bodyParser.json());
app.get('/', (req, res) => { Date.prototype.addHours= function(h){
res.send("Nothing Here"); this.setTime(this.getTime() + (h*60*60*1000));
return this;
}
function getPeriod() {
const now = new Date();
let hours = now.getHours();
let minutes = now.getMinutes();
if ((hours == 8) || (hours == 9 && minutes < 45)) {
// P1
return 1;
}
if ((hours == 9 && minutes >= 45) || (hours == 11 && minutes < 5) || (hours == 10)) {
// P2 / Break
return 2;
}
if ((hours == 11 && minutes >= 5) || (hours == 12 && minutes < 5)) {
// P3
return 3;
}
if ((hours == 12 && minutes >= 5) || (hours == 13 && minutes < 5)) {
// P4
return 4;
}
if ((hours == 13 && minutes >= 5) || (hours == 15 && minutes < 15) || (hours == 14)) {
// Lunch to P5
return 5;
} else {
// too early / late
return -1;
}
}
function incrementUserSubmissions(userid) {
const stmt = db.prepare(`UPDATE Users SET Submissions = Submissions + 1 WHERE Id=${parseInt(userid)};`);
stmt.run();
}
app.use('/', e.static('../frontend/dist'));
app.get('/getPeriod', (req, res) => {
// req has nothing
// res has current period
res.status(200).json({period: getPeriod()});
}); });
app.get('/currentRooms', (req, res) => { app.post('/currentRooms', (req, res) => {
// req has no data // req has day
// res has all room at current time // res has all room at current time
console.log("currentRooms"); console.log("currentRooms");
const today = new Date(); const currentPeriod = getPeriod();
let stmt = db.prepare(`SELECT * FROM TimeSlots INNER JOIN Rooms ON Rooms.Id=TimeSlots.Room WHERE TimeEnd BETWEEN '${today.toISOString()}' AND '${today.addHours(1).toISOString()}';`); if (currentPeriod == -1) {
res.status(418).send("Not in school");
return;
}
const today = req.body.day;
let stmt = db.prepare(`SELECT TimeSlots.Id, TimeSlots.Period, TimeSlots.Day, Timeslots.Room, Rooms.RoomName FROM TimeSlots INNER JOIN Rooms ON Rooms.Id=TimeSlots.Room WHERE TimeSlots.Period BETWEEN ${currentPeriod - 1} AND ${currentPeriod + 1} AND Timeslots.Day=${today} ORDER BY Rooms.RoomName ASC;`);
let records = stmt.all(); let records = stmt.all();
res.status(200).json({records: records}); res.status(200).json({records: records});
}); });
app.post('/addTimeSlot', (req, res) => { app.post('/addTimeSlot', (req, res) => {
// req has roomid userid starttime and end time // req has roomid userid period and day
// res has success or faliure // res has success or faliure
console.log("addTimeSlot"); console.log("addTimeSlot");
const body = req.body; const body = req.body;
let roomid = parseInt(body.roomid); let roomid = parseInt(body.roomid);
let userid = parseInt(body.userid); let userid = parseInt(body.userid);
let stmt = db.prepare(`INSERT INTO TimeSlots (TimeStart, TimeEnd, Room) VALUES ('${body.startTime}', '${body.endTime}', ${roomid});`); let stmt = db.prepare(`INSERT INTO TimeSlots (Period, Day, Room) VALUES (${body.period}, ${body.day}, ${roomid});`);
stmt.run();
stmt = db.prepare(`UPDATE Users SET Submissions = Submissions + 1 WHERE Id=${userid};`);
stmt.run(); stmt.run();
incrementUserSubmissions(userid)
res.status(200).send("added timeslot"); res.status(200).send("added timeslot");
}); });
app.post('/removeTimeSlot', (req, res) => {
// req has roomid and userid
// ress has success or faliure
console.log("removeTimeSlot");
const body = req.body;
const slotid = parseInt(body.Id);
const userid = parseInt(body.userId);
let stmt = db.prepare(`DELETE FROM TimeSlots WHERE Id=${slotid};`);
stmt.run();
incrementUserSubmissions(userid);
res.status(200).send("removed timeslot");
})
app.post('/addRoom', (req, res) => { app.post('/addRoom', (req, res) => {
// req has userid and roomname // req has userid and roomname
// res has success or faliure // res has success or faliure
@@ -54,13 +109,12 @@ app.post('/addRoom', (req, res) => {
let stmt = db.prepare(`SELECT * FROM Rooms WHERE RoomName='${name}'`); let stmt = db.prepare(`SELECT * FROM Rooms WHERE RoomName='${name}'`);
let storedRecord = stmt.get(); let storedRecord = stmt.get();
if (storedRecord) { if (storedRecord) {
res.status(400).send("room already exists"); res.status(418).send("room already exists");
return; return;
} }
stmt = db.prepare(`INSERT INTO Rooms (RoomName) VALUES (${name})`); stmt = db.prepare(`INSERT INTO Rooms (RoomName) VALUES ('${name}')`);
stmt.run();
stmt = db.prepare(`UPDATE Users SET Submissions = Submissions + 1 WHERE Id=${parseInt(body.userid)};`);
stmt.run(); stmt.run();
incrementUserSubmissions(parseInt(body.userid));
res.status(200).send("added room"); res.status(200).send("added room");
}); });
@@ -68,7 +122,7 @@ app.get('/getRooms', (req, res) => {
// req has no data // req has no data
// res has success or faliure // res has success or faliure
console.log("getRooms"); console.log("getRooms");
let stmt = db.prepare(`SELECT * FROM Rooms`); let stmt = db.prepare(`SELECT * FROM Rooms ORDER BY RoomName ASC;`);
let records = stmt.all(); let records = stmt.all();
res.status(200).send({records: records}); res.status(200).send({records: records});
}); });
@@ -81,11 +135,11 @@ app.post('/createUser', async (req, res) => {
let stmt = db.prepare(`SELECT * FROM Users WHERE Email='${body.email}';`); let stmt = db.prepare(`SELECT * FROM Users WHERE Email='${body.email}';`);
let storedRecord = stmt.get(); let storedRecord = stmt.get();
if (storedRecord) { if (storedRecord) {
res.status(400).send("account with that email already exists"); res.status(418).send("account with that email already exists");
return; return;
} }
const generatedHash = await bcrypt.hash(body.pass, 10); const generatedHash = await bcrypt.hash(body.pass, 10);
stmt = db.prepare(`INSERT INTO Users (Email, Pass, Username, Submissions) VALUES ('${body.email}', '${generatedHash}', '${body.name}', 0)`); stmt = db.prepare(`INSERT INTO Users (Email, Pass, Username, Submissions) VALUES ('${body.email}', '${generatedHash}', '${body.name}', 0);`);
stmt.run(); stmt.run();
stmt = db.prepare(`SELECT Id FROM Users WHERE Email='${body.email}';`); stmt = db.prepare(`SELECT Id FROM Users WHERE Email='${body.email}';`);
const uid = stmt.get(); const uid = stmt.get();
@@ -100,7 +154,7 @@ app.post('/login', async (req, res) => {
let stmt = db.prepare(`SELECT Pass FROM Users WHERE Email='${body.email}';`); let stmt = db.prepare(`SELECT Pass FROM Users WHERE Email='${body.email}';`);
let storedHash = stmt.get(); let storedHash = stmt.get();
if (!storedHash) { if (!storedHash) {
res.status(400).send("problem with email"); res.status(418).send("problem with email");
return; return;
} }
if (await bcrypt.compare(body.pass, storedHash.Pass)){ if (await bcrypt.compare(body.pass, storedHash.Pass)){
@@ -108,7 +162,7 @@ app.post('/login', async (req, res) => {
const uid = stmt.get(); const uid = stmt.get();
res.status(200).send({uid: uid}); res.status(200).send({uid: uid});
} else { } else {
res.status(400).send("incorrect password"); res.status(418).send("incorrect password");
} }
}); });

View File

@@ -6,7 +6,8 @@
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",
"dev": "node index.js", "dev": "node index.js",
"setup": "rm ./database.db; touch ./database.db; node setup.js" "setup": "touch ./database.db; node setup.js",
"clean": "rm ./database.db"
}, },
"keywords": [], "keywords": [],
"author": "", "author": "",

View File

@@ -19,8 +19,8 @@ CREATE TABLE Rooms (
CREATE TABLE TimeSlots ( CREATE TABLE TimeSlots (
Id INTEGER PRIMARY KEY ASC AUTOINCREMENT, Id INTEGER PRIMARY KEY ASC AUTOINCREMENT,
TimeStart TEXT, Period INTEGER,
TimeEnd TEXT, Day INTEGER,
Room INTEGER, Room INTEGER,
FOREIGN KEY(Room) REFERENCES Rooms(Id) FOREIGN KEY(Room) REFERENCES Rooms(Id)
);`); );`);

View File

@@ -8,6 +8,7 @@
"name": "frontend", "name": "frontend",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"picnic": "^7.1.0",
"vue": "^3.5.30", "vue": "^3.5.30",
"vue-cookies": "^1.8.6" "vue-cookies": "^1.8.6"
}, },
@@ -994,6 +995,18 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/picnic": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/picnic/-/picnic-7.1.0.tgz",
"integrity": "sha512-OFoJClCDSaCnavs7QZPMIIjrodUO8m6V+GBNOWaIN4kk3IjzSAd2kp/RfkpZRwXK2u71q5mJ3UQaQjHJRr3dnw==",
"license": "MIT",
"engines": {
"node": ">=4.0.0"
},
"funding": {
"url": "https://www.paypal.me/franciscopresencia/19"
}
},
"node_modules/picocolors": { "node_modules/picocolors": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",

View File

@@ -9,6 +9,7 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"picnic": "^7.1.0",
"vue": "^3.5.30", "vue": "^3.5.30",
"vue-cookies": "^1.8.6" "vue-cookies": "^1.8.6"
}, },

View File

@@ -17,7 +17,7 @@ onBeforeMount(async () => {
<template> <template>
<Login v-if="page == 0" @next-page="page++"/> <Login v-if="page == 0" @next-page="page++"/>
<HomePage v-if="page == 1" @add-slot="page++"/> <HomePage v-else-if="page == 1" @add-slot="page++"/>
<AddTimeSlot v-if="page == 2" @go-home="page--" @add-room="page++"/> <AddTimeSlot v-else-if="page == 2" @go-home="page--" @add-room="page++"/>
<AddRoom v-if="page == 3" @go-back="page--"/> <AddRoom v-else-if="page == 3" @go-back="page--"/>
</template> </template>

View File

@@ -1,6 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { inject, ref } from 'vue';
import type { VueCookies } from 'vue-cookies';
const $cookies = inject<VueCookies>("$cookies") as VueCookies;
const emit = defineEmits(['goBack']); const emit = defineEmits(['goBack']);
@@ -8,10 +10,11 @@ const roomName = ref<string>();
async function addRoom(e: SubmitEvent) { async function addRoom(e: SubmitEvent) {
e.preventDefault(); e.preventDefault();
let uid = await cookieStore.get("userId"); let uid = await $cookies.get("userId");
const res = await fetch("http://localhost:3000/addRoom", { console.log(uid);
const res = await fetch("/addRoom", {
method: "POST", method: "POST",
body: JSON.stringify({ roomName: roomName.value, userid: uid?.value }), body: JSON.stringify({ roomName: roomName.value, userid: uid }),
headers: new Headers({'content-type': 'application/json'}) headers: new Headers({'content-type': 'application/json'})
}); });
if (res.ok) { if (res.ok) {

View File

@@ -1,26 +1,34 @@
<script setup lang="ts"> <script setup lang="ts">
import { onBeforeMount, ref } from 'vue'; import { inject, onBeforeMount, ref } from 'vue';
import type { VueCookies } from 'vue-cookies';
const $cookies = inject<VueCookies>("$cookies") as VueCookies;
const emit = defineEmits(['goHome', 'addRoom']); const emit = defineEmits(['goHome', 'addRoom']);
const rooms = ref(); const rooms = ref();
const room = ref(); const room = ref();
const startTime = ref(); const weekNumber = ref<number>(1);
const endTime = ref(); const day = ref<number>(1);
const period = ref<number>(1);
onBeforeMount(async () => { onBeforeMount(async () => {
let res = await fetch("http://localhost:3000/getRooms", { let res = await fetch("/getRooms", {
method: "GET" method: "GET"
}); });
rooms.value = await res.json(); rooms.value = await res.json();
const date = new Date();
day.value = date.getDay();
res = await fetch("getPeriod", { method: "GET" });
const json = await res.json();
period.value = json.period;
}); });
async function addTimeSlot(e: SubmitEvent) { async function addTimeSlot(e: SubmitEvent) {
e.preventDefault(); e.preventDefault();
let uid = await cookieStore.get("userId"); let uid = await $cookies.get("userId");
await fetch("http://localhost:3000/addTimeSlot", { await fetch("/addTimeSlot", {
method: "POST", method: "POST",
body: JSON.stringify({ roomid: room.value.Id, userid: uid?.value, startTime: startTime.value, endTime: endTime.value }), body: JSON.stringify({ roomid: room.value.Id, userid: uid, day: (day.value * weekNumber.value), period: period.value }),
headers: new Headers({'content-type': 'application/json'}) headers: new Headers({'content-type': 'application/json'})
}); });
emit('goHome'); emit('goHome');
@@ -36,10 +44,12 @@ async function addTimeSlot(e: SubmitEvent) {
<option v-for="item in rooms.records" :value="item">{{ item.RoomName }}</option> <option v-for="item in rooms.records" :value="item">{{ item.RoomName }}</option>
</select><br> </select><br>
<button @click="$emit('addRoom')">Add room?</button><br> <button @click="$emit('addRoom')">Add room?</button><br>
<label for="start" >Start Time</label><br> <label for="week">Week number</label><br>
<input type="datetime-local" id="start" v-model="startTime" /><br> <input type="number" id="week" v-model="weekNumber" min="1" max="2"/><br>
<label for="end">End Time</label><br> <label for="day">Day</label><br>
<input type="datetime-local" id="end" v-model="endTime" /><br> <input type="number" id="day" v-model="day" min="1" max="5"/><br>
<label for="period">Period</label><br>
<input type="number" id="period" v-model="period" min="1" max="5"/><br>
<button type="submit">Add</button> <button type="submit">Add</button>
</form> </form>
</template> </template>

View File

@@ -1,26 +1,72 @@
<script setup lang="ts"> <script setup lang="ts">
import { onBeforeMount, ref } from 'vue'; import { inject, onBeforeMount, ref } from 'vue';
import type { VueCookies } from 'vue-cookies';
type timeSlot_t = {Id: number, Period: number, Day: number, Room: number, RoomName: string};
const $cookies = inject<VueCookies>("$cookies") as VueCookies;
const rooms = ref(); const rooms = ref();
const showRooms = ref<boolean>(false);
const weekNumber = ref<number>(1);
async function getData() {
showRooms.value = false;
const today = new Date();
// if (today.getDay() == 0 || today.getDay() == 6) {
// return;
// }
const day = today.getDay() * weekNumber.value;
const res = await fetch("/currentRooms", {
method: "POST",
body: JSON.stringify({day: day}),
headers: new Headers({'content-type': 'application/json'})
});
if (res.ok) {
let json = await res.json();
if (json.records.length == 0) {
showRooms.value = false;
} else {
rooms.value = json;
showRooms.value = true;
}
}
}
async function deleteTimeSlot(timeSlot: timeSlot_t) {
const uid = await $cookies.get("userId");
const res = await fetch("/removeTimeSlot", {
method: "POST",
body: JSON.stringify({ Id: timeSlot.Id, userId: uid }),
headers: new Headers({ "content-type": "application/json" })
});
if (res.ok) {
await getData();
} else {
console.log(await res.text());
}
}
defineEmits(['addSlot']); defineEmits(['addSlot']);
onBeforeMount(async () => { onBeforeMount(async () => {
const res = await fetch("http://localhost:3000/currentRooms", { await getData();
method: "GET"
});
rooms.value = await res.json();
}); });
</script> </script>
<template> <template>
<header> <header>
<h1>What's free in my free?</h1> <h1>What's free in my free?</h1>
<button @click="$emit('addSlot')">Add new slot</button> <button @click="$emit('addSlot')">Add new slot</button> <br>
<label for="weekNumber">Week Number</label>
<select v-model="weekNumber" @change="getData" id="weekNumber">
<option :value="1">1</option>
<option :value="2">2</option>
</select>
</header> </header>
<article v-if="rooms"> <article>
<h2>Free Rooms:</h2> <h2>Free Rooms:</h2>
<p v-if="rooms.records.length == 0">No free rooms right now</p> <p v-if="!showRooms">No free rooms right now</p>
<ul v-for="slot in rooms.records"> <ul v-else v-for="slot in rooms.records">
<li>Room: {{ slot.RoomName }}, from {{ slot.TimeStart.toString().split("T")[1] }} til {{ slot.TimeEnd.toString().split("T")[1] }}</li> <li>Room: {{ slot.RoomName }}, in Period {{ slot.Period }}, <button type="button" @click="deleteTimeSlot(slot)">X</button></li>
</ul> </ul>
</article> </article>
</template> </template>

View File

@@ -1,5 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { inject, ref } from 'vue';
import type { VueCookies } from 'vue-cookies';
const $cookies = inject<VueCookies>("$cookies") as VueCookies;
const email = ref<string>(""); const email = ref<string>("");
const pass = ref<string>(""); const pass = ref<string>("");
@@ -10,15 +13,15 @@ const emit = defineEmits(['nextPage']);
async function login(e: SubmitEvent) { async function login(e: SubmitEvent) {
text.value = ""; text.value = "";
e.preventDefault(); e.preventDefault();
const res = await fetch("http://localhost:3000/login", { const res = await fetch("/login", {
method: "POST", method: "POST",
body: JSON.stringify({ email: email.value, pass: pass.value }), body: JSON.stringify({ email: email.value, pass: pass.value }),
headers: new Headers({'content-type': 'application/json'}) headers: new Headers({'content-type': 'application/json'})
}); });
if (res.ok) { if (res.ok) {
cookieStore.set("loggedIn", "true"); $cookies.set("loggedIn", "true");
let uid = await res.json(); let uid = await res.json();
cookieStore.set("userId", uid.uid.Id); $cookies.set("userId", uid.uid.Id);
emit('nextPage'); emit('nextPage');
} else { } else {
text.value = await res.text(); text.value = await res.text();
@@ -28,7 +31,7 @@ async function login(e: SubmitEvent) {
async function signup(e: Event) { async function signup(e: Event) {
text.value = ""; text.value = "";
e.preventDefault(); e.preventDefault();
const res = await fetch("http://localhost:3000/createUser", { const res = await fetch("/createUser", {
method: "POST", method: "POST",
body: JSON.stringify({ email: email.value, pass: pass.value, name: email.value.split("@")[0] }), body: JSON.stringify({ email: email.value, pass: pass.value, name: email.value.split("@")[0] }),
headers: new Headers({'content-type': 'application/json'}) headers: new Headers({'content-type': 'application/json'})

View File

@@ -1,5 +1,5 @@
import { createApp } from 'vue'; import { createApp } from 'vue';
// import './style.css' import './style.css';
import App from './App.vue'; import App from './App.vue';
import VueCookies from 'vue-cookies'; import VueCookies from 'vue-cookies';

View File

@@ -1,4 +1,18 @@
:root { @import "../node_modules/picnic/picnic.min.css";
body {
@media (min-width: 800px) {
margin: 0% 25%;
}
@media (max-width: 800px) {
margin: 0 5%;
}
}
/* :root {
--text: #6b6375; --text: #6b6375;
--text-h: #08060d; --text-h: #08060d;
--bg: #fff; --bg: #fff;
@@ -293,4 +307,4 @@ code {
right: 0; right: 0;
border-right-color: var(--border); border-right-color: var(--border);
} }
} } */