This commit is contained in:
louiscklaw
2025-02-01 01:58:47 +08:00
parent b3da7aaef5
commit 04dbefcbaf
1259 changed files with 280657 additions and 0 deletions

115
_tecky/party-planner/.gitignore vendored Normal file
View File

@@ -0,0 +1,115 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Uploads filder
public/uploads
# Json files
users.json
# Others
.DS_Store
.idea

View File

@@ -0,0 +1,3 @@
build
coverage
node_modules

View File

@@ -0,0 +1,6 @@
DB_NAME=
DB_USERNAME=
DB_PASSWORD=
SESSION_SECRET=
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=

View File

@@ -0,0 +1,76 @@
import express from "express";
import expressSession from "express-session";
import path from "path";
import pg from "pg";
import dontenv from "dotenv";
import grant from "grant";
import { loginRoutes } from "./routes/loginRoutes";
import { registerRoutes } from "./routes/registerRoutes";
import { eventsRoutes } from "./routes/eventsRoutes";
import { isLoggedIn, isLoggedInAPI } from "./util/guard";
import { personalInfoRoutes } from "./routes/personalInfoRoutes";
import { itemsRoutes } from "./routes/itemsRoutes";
import { scheduleRoutes } from "./routes/scheduleRoutes";
import { commentRoutes } from "./routes/commentRoutes";
dontenv.config();
export const client = new pg.Client({
host: "postgres",
database: process.env.DB_NAME || "postgres",
user: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
});
client.connect();
const app = express();
const sessionMiddleware = expressSession({
secret: process.env.SESSION_SECRET || "helloworld_secret",
resave: true,
saveUninitialized: true,
cookie: { secure: false, httpOnly: true },
});
declare module "express-session" {
interface SessionData {
user?: number;
}
}
const grantExpress = grant.express({
defaults: {
origin: "https://partyplanner.duncantang.dev",
transport: "session",
state: true,
},
google: {
key: process.env.GOOGLE_CLIENT_ID || "",
secret: process.env.GOOGLE_CLIENT_SECRET || "",
scope: ["profile", "email"],
callback: "/login/google", //3
},
});
app.use(express.json(), sessionMiddleware, express.static("public"), grantExpress as express.RequestHandler);
app.use("/login", loginRoutes);
app.use("/register", registerRoutes);
app.use("/events", eventsRoutes, scheduleRoutes);
app.use("/personalPage", isLoggedInAPI, personalInfoRoutes);
app.use("/items", itemsRoutes);
app.use("/eventSchedule", scheduleRoutes);
app.use("/comment", commentRoutes);
app.use(isLoggedIn, express.static("private"));
app.use((req, res) => {
res.status(404).sendFile(path.resolve("./public/404.html"));
});
const PORT = 8080;
app.listen(PORT, () => {
console.log(`Listening at http://localhost:${PORT}/`);
});

View File

@@ -0,0 +1,22 @@
#!/usr/bin/env bash
set -ex
export DB_NAME=postgres
export DB_USERNAME=postgres
export DB_PASSWORD=postgres
export DB_HOST=postgres
export DB_PORT=5432
# export |grep -i DB
# npm i -d
# sleep 1
npm run lazy
# npm run initDB
# npm run clearDB
# npm run truncateDB
# npm run regUsers
# npm run regUsers

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,58 @@
{
"name": "party",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"docker_rebuild": "docker compose up -d --build",
"start": "ts-node-dev app.ts",
"format": "prettier --config ./prettierrc.json --write .",
"lazy": "ts-node ./util/database/seedingFunctions/lazy.ts",
"initDB": "ts-node ./util/database/seedingFunctions/initDB.ts",
"clearDB": "ts-node ./util/database/seedingFunctions/clearDB.ts",
"truncateDB": "ts-node ./util/database/seedingFunctions/truncateDB.ts",
"regUsers": "ts-node ./util/database/seedingFunctions/regUsers.ts",
"createEvents": "ts-node ./util/database/seedingFunctions/createEvents.ts",
"joinEvents": "ts-node ./util/database/seedingFunctions/joinEvents.ts",
"addParticipants": "ts-node ./util/database/seedingFunctions/addParticipants.ts",
"addItems": "ts-node ./util/database/seedingFunctions/addItems.ts",
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/pofungt/party.git"
},
"author": "",
"license": "ISC",
"bugs": {
"url": "https://github.com/pofungt/party/issues"
},
"homepage": "https://github.com/pofungt/party#readme",
"dependencies": {
"@types/bcryptjs": "^2.4.2",
"@types/dotenv": "^8.2.0",
"@types/express-session": "^1.17.5",
"@types/jsonfile": "^6.1.0",
"@types/pg": "^8.6.5",
"@types/socket.io": "^3.0.2",
"@types/winston": "^2.4.4",
"bcryptjs": "^2.4.3",
"cross-fetch": "^3.1.5",
"date-fns": "^2.29.3",
"dotenv": "^16.0.3",
"express": "^4.18.1",
"express-session": "^1.17.3",
"grant": "^5.4.21",
"jsonfile": "^6.1.0",
"pg": "^8.8.0",
"prettier": "^2.7.1",
"socket.io": "^4.5.2",
"ts-node": "^10.9.1",
"typescript": "^4.8.3",
"winston": "^3.8.2"
},
"devDependencies": {
"@types/express": "^4.17.14",
"ts-node-dev": "^2.0.0"
}
}

View File

@@ -0,0 +1,162 @@
body {
padding-left: 50px;
padding-right: 50px;
font-family: 'Kalam';
}
#my-events-header {
font-size: 2vw;
}
#participated-events-header {
font-size: 2vw;
}
#page-header {
font-size: 40px;
padding-top: 10px;
}
.header {
text-align: center;
}
#comment-page-container {
text-align: center;
}
header {
text-align: center;
}
#my-events {
background-color: #efefd0;
font-size: 1vw;
min-height: 600px;
max-height: 600px;
width: 100%;
border-radius: 30px;
padding: 20px;
overflow: auto;
}
#participated-events {
font-size: 1vw;
max-height: 600px;
min-height: 600px;
width: 100%;
background-color: #efefd0;
border-radius: 30px;
padding: 20px;
overflow: auto;
}
#my-events-list {
min-width: 100%;
min-height: 100%;
background-color: #f29659;
border-radius: 30px;
}
#participated-events-list {
min-width: 100%;
min-height: 100%;
background-color: #f29659;
border-radius: 30px;
}
.background-frame {
min-height: 100%;
}
#input-list-my-events {
max-width: 100%;
text-align: center;
}
#input-list-participated-events {
max-width: 100%;
text-align: center;
}
#btn-container {
display: flex;
justify-content: center;
text-align: center;
}
/* CSS */
.button-53 {
background-color: #f29659;
border: 0 solid #e5e7eb;
box-sizing: border-box;
color: #000000;
display: flex;
font-family: ui-sans-serif, system-ui, -apple-system, system-ui, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
font-size: 1rem;
font-weight: 700;
justify-content: center;
line-height: 1.75rem;
padding: 0.75rem 1.65rem;
position: relative;
text-align: center;
text-decoration: none #000000 solid;
text-decoration-thickness: auto;
width: 100%;
max-width: 460px;
position: relative;
cursor: pointer;
transform: rotate(-2deg);
user-select: none;
-webkit-user-select: none;
touch-action: manipulation;
}
.button-53:focus {
outline: 0;
}
.button-53:after {
content: '';
position: absolute;
border: 1px solid #000000;
bottom: 4px;
left: 4px;
width: calc(100% - 1px);
height: calc(100% - 1px);
}
.button-53:hover:after {
bottom: 2px;
left: 2px;
}
@media (min-width: 768px) {
.button-53 {
padding: 0.75rem 3rem;
font-size: 1.25rem;
}
}
/* Scroll bar */
::-webkit-scrollbar {
width: 15px;
}
::-webkit-scrollbar-track {
box-shadow: inset 0 0 5px grey;
border-radius: 5px;
height: 200px;
}
::-webkit-scrollbar-thumb {
background: #f29659;
border-radius: 5px;
min-height: 250px;
}
::-webkit-scrollbar-thumb:hover {
background: white;
}

View File

@@ -0,0 +1,158 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.1/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-iYQeCzEYFbKjA/T2uDLTpkwGzCiq6soy8tYaI1GyVh/UjpbCx/TYkiZhlZB6+fzT"
crossorigin="anonymous"
/>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.9.1/font/bootstrap-icons.css" />
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.0/css/all.min.css"
integrity="sha512-xh6O/CkQoPOWDdYTDqeRdPCVd1SpvCA9XXcUnZS2FmJNp1coAFzvtCN9BmamE+4aHK8yyUHUSCcJHgXloTyT2A=="
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>
<link href="https://fonts.googleapis.com/css?family=Kalam" rel="stylesheet" />
<link rel="stylesheet" href="./comment.css" />
<title>Comment Page</title>
</head>
<body style="display: none">
<div class="navbar-container container">
<!-- NAVBAR: to be loaded with js -->
</div>
<header id="page-header">COMMENT</header>
<div id="btn-container">
<div class="btn button-53" data-bs-toggle="modal" data-bs-target="#comment-modal">Submit Comment</div>
</div>
<div id="comment-page" class="row">
<div class="col-md-6">
<div class="eventname frame line1">
<div class="background-frame">
<div class="header" id="my-events-header">My Events</div>
<div id="my-events">
<div id="my-events-list">
<table class="table" id="input-list-my-events">
<thead>
<tr>
<th scope="col">Sender</th>
<th scope="col">Message</th>
<th scope="col">Event</th>
<th scope="col">Received on</th>
<th scope="col">Mark as read</th>
</tr>
</thead>
</table>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="date-time frame line1">
<div class="background-frame">
<div class="header" id="participated-events-header">Participated Events</div>
<div id="participated-events">
<div id="participated-events-list">
<table class="table" id="input-list-participated-events">
<thead>
<tr>
<th scope="col">Sender</th>
<th scope="col">Message</th>
<th scope="col">Event</th>
<th scope="col">Received on</th>
<th scope="col">Read by Creator</th>
</tr>
</thead>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Comment Modal -->
<div
class="modal fade"
id="comment-modal"
data-bs-backdrop="static"
data-bs-keyboard="false"
tabindex="-1"
aria-labelledby="staticBackdropLabel"
aria-hidden="true"
>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<div></div>
<h5 class="modal-title">SUBMIT COMMENT</h5>
<div class="exit-modal">
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
</div>
<div class="modal-body">
<div class="input-group">
<form id="comment-form" class="register-form">
<div class="input-panel mb-3">
<div class="form-header">RECEIVER</div>
<select name="receiver" id="receiver">
<option value="null">Selector Receiver</option>
<!-- to be input by JS -->
</select>
</div>
<div class="input-panel mb-3">
<div class="form-header">COMMENT</div>
<textarea id="comment" class="form-control" name="comment" rows="5" cols="50">
</textarea>
</div>
<div class="input-panel mb-3">
<div class="form-header">Regarding...</div>
<select name="category" id="category">
<option value="items">Event Items</option>
<option value="venue">Location</option>
<option value="date-time">Event Date/Time</option>
<option value="budget">Budget</option>
<option value="participant">Participant</option>
<option value="activity">Event Activity</option>
<option value="others">others</option>
</select>
</div>
<div class="input-panel mb-3"></div>
<input type="checkbox" id="anonymous" name="anonymous" unchecked />
<label for="anonymous">Send anonymously?</label><br />
<div class="modal-footer">
<button id="submit-comment" type="submit" class="btn btn-primary button-53">
Submit
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<script src="https://thibaultjanbeyer.github.io/DragSelect/ds.min.js"></script>
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.1/dist/js/bootstrap.bundle.min.js"
integrity="sha384-u1OknCvxWvY5kfmNBILK2hRnQC3Pr17a+RTT6rIHI7NnikvbZlHgTPOOmMi466C8"
crossorigin="anonymous"
></script>
<script type="module" src="./comment.js"></script>
</body>
</html>

View File

@@ -0,0 +1,202 @@
import { addNavbar } from '/functions/addNavbar.js';
import { loadName } from '/functions/loadName.js';
window.addEventListener('load', async () => {
addNavbar();
await loadName();
await getComment();
await checkedComment();
document.body.style.display = 'block';
});
async function getComment() {
const res = await fetch(`/comment/`);
if (res.status !== 200) {
const data = await res.json();
alert(data.msg);
return;
}
const result = await res.json();
// to Creator List
const creatorCommentArr = result.cComment;
const creatorCommentContainer = document.querySelector('#input-list-my-events');
creatorCommentArr.forEach((comment) => {
let name = comment.first_name + ' ' + comment.last_name;
if (comment.anonymous) {
name = 'Anonymous';
}
let checked = 'unchecked';
const eventId = comment.event_id;
const content = comment.content;
const receivingTime = new Date(comment.created_at).toLocaleDateString('en-US');
const commentId = comment.id;
const eventName = comment.name;
if (comment.read) {
checked = 'checked';
creatorCommentContainer.innerHTML += `
<tr id="comment-box" class="comment-box">
<th class="table-secondary">${name}</th>
<th class="table-secondary">${content}</th>
<th class="table-secondary">${eventName}</th>
<th class="table-secondary">${receivingTime}</th>
<th class="table-secondary"><input type="checkbox" event="${eventId}" value="${commentId}" id="creator-read" name="creator-read" ${checked}></th>
</tr>
`;
} else {
creatorCommentContainer.innerHTML += `
<tr id="comment-box" class="comment-box">
<th class="table-primary">${name}</th>
<th class="table-primary">${content}</th>
<th class="table-primary">${eventName}</th>
<th class="table-primary">${receivingTime}</th>
<th class="table-primary"><input type="checkbox" event="${eventId}" value="${commentId}" id="creator-read" name="creator-read" ${checked}></th>
</tr>
`;
}
});
// to Participant List
const participantsCommentArr = result.pComment;
const participantsCommentContainer = document.querySelector('#input-list-participated-events');
participantsCommentArr.forEach(async (comment) => {
let name = comment.first_name + ' ' + comment.last_name;
if (comment.anonymous) {
name = 'Anonymous';
}
let checked = 'unchecked';
const content = comment.content;
const receivingTime = new Date(comment.created_at).toLocaleDateString('en-US');
const commentId = comment.id;
const eventName = comment.name;
if (comment.read) {
checked = 'checked';
participantsCommentContainer.innerHTML += `
<tr id="comment-box" class="comment-box">
<th class="table-secondary">${name}</th>
<th class="table-secondary">${content}</th>
<th class="table-secondary">${eventName}</th>
<th class="table-secondary">${receivingTime}</th>
<th class="table-secondary"><input type="checkbox" value="${commentId}" id="read" name="read" disabled readonly ${checked}></th>
</tr>
`;
} else {
participantsCommentContainer.innerHTML += `
<tr id="comment-box" class="comment-box">
<th class="table-primary">${name}</th>
<th class="table-primary">${content}</th>
<th class="table-primary">${eventName}</th>
<th class="table-primary">${receivingTime}</th>
<th class="table-primary"><input type="checkbox" value="${commentId}" id="read" name="read" disabled readonly ${checked}></th>
</tr>
`;
}
});
postComment(result);
}
async function postComment(result) {
const eventArr = result.events;
eventArr.forEach((event) => {
document.querySelector('#receiver').innerHTML += `
<option value="${event.event_id}">${event.name}</option>
`;
});
document.querySelector('#comment-form').addEventListener('submit', async (e) => {
e.preventDefault();
const form = e.target;
const receiver = form['receiver'].value;
const comment = form['comment'].value;
const category = form['category'].value;
const anonymous = form['anonymous'].checked;
let dataPass = true;
if (!comment || onlySpaces(comment)) {
dataPass = false;
alert('Comment field seems to be empty or only space');
return;
}
if (receiver === 'null') {
dataPass = false;
alert('Please select a receiving event!');
return;
}
if (dataPass) {
const formObj = {
receiver: receiver,
comment: comment,
category: category,
anonymous: anonymous
};
const res = await fetch(`/comment/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formObj)
});
if (res.status !== 200) {
const data = await res.json();
alert(data.msg);
return;
}
const result = await res.json();
console.log(result);
if (result.status === true) {
alert('Comment successfully sent!');
location.reload();
}
}
});
}
async function checkedComment() {
document.querySelectorAll('#creator-read').forEach((checkbox) => {
checkbox.addEventListener('change', async (e) => {
e.preventDefault;
const commentId = e.target.value;
const check = e.target.checked;
const eventId = e.target.getAttribute('event');
const obj = {
commentId: commentId,
check: check,
eventId: eventId
};
const res = await fetch(`/comment/`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(obj)
});
const result = await res.json();
if (result.status) {
console.log(result.msg);
} else {
alert('something when wrong when marking message as read');
}
});
});
}
function onlySpaces(str) {
return str.trim().length === 0;
}

View File

@@ -0,0 +1,427 @@
body {
position: relative;
margin-left: 2px;
margin-right: 20px;
font-family: 'Kalam';
font-size: 16px;
}
.modal-body {
height: fit-content;
}
.input-group {
display: flex;
justify-content: center;
}
.input-panel {
margin: 0px;
}
.item-list-box {
min-height: 150px;
min-width: 100px;
}
#item-form-text {
text-align: center;
}
header {
display: flex;
justify-content: center;
}
#item-form-header {
text-align: center;
}
.item-list-box {
border-radius: 10px;
background-color: #e8e892;
height: 100%;
padding-top: 10px;
padding-bottom: 10px;
overflow: auto;
}
.modal-content {
align-items: center;
width: fit-content;
}
#date-selector-container {
display: flex;
justify-content: right;
padding-bottom: 10px;
padding-right: 10px;
}
textarea {
border-radius: 5px;
background: none;
border-color: #3f3f3f;
max-height: 100%;
overflow: auto;
}
header {
text-align: center;
}
#time-block-memo-container {
position: relative;
max-height: 100%;
}
#close-memo {
position: absolute;
top: 0px;
font-size: 10px;
right: 10px;
}
#time-block-page-container {
max-height: 800px;
height: 550px;
padding-top: 0px;
padding-bottom: 30px;
}
#event-time-container {
text-align: center;
}
.event-schedule {
background-color: #efefd0 !important;
}
.event-schedule:hover {
background-color: #e8e892 !important;
}
@media (max-width: 1000px) {
#line {
display: none;
}
}
@media (max-width: 576px) {
#line {
display: none;
}
#memo {
font-size: large;
max-height: 100%;
max-width: 100%;
overflow: auto;
}
#time-block-memo-container {
max-height: 300px;
height: 300px;
overflow: auto;
}
#rundown-container {
max-height: 300px;
}
}
#memo {
background-color: #efefd0;
height: 90%;
border-radius: 10px;
overflow: auto;
}
#page {
display: flex;
justify-content: center;
height: 90%;
width: 100%;
}
/* #line {
position: absolute;
} */
textarea {
width: 100%;
}
.create-button {
position: fixed;
left: 30px;
bottom: 30px;
}
#rundown {
max-height: 80%;
max-width: 100%;
}
#rundown-container {
max-height: 100%;
position: relative;
}
.time-stamp-container {
background-color: none;
position: relative;
display: flex;
justify-content: center;
align-items: center;
}
.individual-time-block {
height: 100%;
padding: 2px;
max-width: 100%;
overflow: visible;
}
.time-block {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
background-color: rgba(181, 180, 180, 0.625);
padding: 10px;
border-radius: 5px;
padding: 1px;
font-size: 1.5vw;
text-align: center;
transition: background-color 50ms ease-out 50ms;
}
.save-time-block {
height: 100%;
color: white;
opacity: 0.7;
padding: 10px;
border-radius: 5px;
text-align: center;
transition: background-color 50ms ease-out 50ms;
}
.save-time-block:hover {
opacity: 0.8;
transform: scale(1.01);
}
.time-stamp {
position: absolute;
top: -5px;
right: 10px;
font-size: 10px;
}
.last-time-stamp {
position: absolute;
bottom: -5px;
right: 10px;
font-size: 10px;
}
.memo-item-container {
position: relative;
background-color: rgba(255, 255, 255, 0.522);
border-radius: 10px;
width: 100%;
height: 100%;
margin-bottom: 10px;
padding: 10px;
font-size: 15px;
}
#memo-item-cluster {
display: flex;
flex-direction: column;
justify-content: space-between;
height: 100%;
padding: 10px;
}
.memo-item-label {
font-weight: 800;
font-size: 15px;
padding: 5px;
}
/* Scroll bar */
::-webkit-scrollbar {
width: 15px;
}
::-webkit-scrollbar-track {
box-shadow: inset 0 0 5px grey;
border-radius: 5px;
height: 200px;
}
::-webkit-scrollbar-thumb {
background: #efefd0;
border-radius: 5px;
min-height: 250px;
}
::-webkit-scrollbar-thumb:hover {
background: white;
}
#memo,
#memo-tag {
animation-duration: 0.5s;
animation-name: animate-fade;
animation-delay: 0.1s;
animation-fill-mode: backwards;
}
@keyframes animate-fade {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
#edit-activities {
border-radius: 100px;
background-color: #f2965985;
color: white;
text-align: center;
min-width: 22px !important;
min-height: 22px !important;
padding: 3px;
font-size: 10px;
}
#edit-remarks {
border-radius: 100px;
background-color: #f2965985;
color: white;
text-align: center;
min-width: 22px !important;
min-height: 22px !important;
padding: 3px;
font-size: 10px;
}
#edit-show-item {
border-radius: 100px;
background-color: #f2965985;
color: white;
text-align: center;
min-width: 22px !important;
min-height: 22px !important;
padding: 3px;
font-size: 10px;
}
.edit-button {
position: absolute;
top: 5px;
right: 5px;
}
.modal-dialog {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
#event-name {
font-size: 2vw;
padding-top: 10px;
}
.fa-trash {
z-index: 1000;
right: 20px;
top: 20px;
}
.creator-function {
z-index: 1000;
}
.modal-body {
align-items: center;
text-align: center;
}
/* CSS */
.button-53 {
background-color: #f29659;
border: 0 solid #e5e7eb;
box-sizing: border-box;
color: #000000;
display: flex;
font-family: ui-sans-serif, system-ui, -apple-system, system-ui, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
font-size: 1rem;
font-weight: 700;
justify-content: center;
line-height: 1.75rem;
padding: 0.75rem 1.65rem;
position: relative;
text-align: center;
text-decoration: none #000000 solid;
text-decoration-thickness: auto;
width: 100%;
max-width: 460px;
position: relative;
cursor: pointer;
transform: rotate(-2deg);
user-select: none;
-webkit-user-select: none;
touch-action: manipulation;
}
.button-53:focus {
outline: 0;
}
.button-53:after {
content: '';
position: absolute;
border: 1px solid #000000;
bottom: 4px;
left: 4px;
width: calc(100% - 1px);
height: calc(100% - 1px);
}
.button-53:hover:after {
bottom: 2px;
left: 2px;
}
@media (min-width: 768px) {
.button-53 {
padding: 0.75rem 3rem;
font-size: 1.25rem;
}
}
#back-page {
position: absolute;
left: 50px;
top: 110px;
text-decoration: none;
}
.bi-chevron-left {
font-size: 30px;
top: 90px;
left: 40px;
color: #444a58;
cursor: pointer;
}

View File

@@ -0,0 +1,353 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.1/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-iYQeCzEYFbKjA/T2uDLTpkwGzCiq6soy8tYaI1GyVh/UjpbCx/TYkiZhlZB6+fzT"
crossorigin="anonymous"
/>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.9.1/font/bootstrap-icons.css" />
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.0/css/all.min.css"
integrity="sha512-xh6O/CkQoPOWDdYTDqeRdPCVd1SpvCA9XXcUnZS2FmJNp1coAFzvtCN9BmamE+4aHK8yyUHUSCcJHgXloTyT2A=="
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>
<link href="https://fonts.googleapis.com/css?family=Kalam" rel="stylesheet" />
<link rel="stylesheet" href="./eventSchedule.css" />
<title>Event Schedule</title>
</head>
<body>
<div class="navbar-container container">
<!-- NAVBAR: to be loaded with js -->
</div>
<header id="event-name"></header>
<div id="event-time-container">
<!-- time and date input here -->
</div>
<div class="container container-fluid" id="page">
<div id="time-block-page-container" class="container row">
<div id="date-selector-container" class="col-12">
<!-- time selector here -->
</div>
<div id="time-block-memo-container" class="col-sm-5 overflow-auto"></div>
<div id="rundown-container" class="col-sm-7 overflow-auto">
<div id="rundown" class="rundown" data-current="0">
<!-- Add Time Block and Time-stamp on window load -->
</div>
</div>
</div>
<!-- Back Button -->
<a id="back-page">
<i class="bi bi-chevron-left"></i>
</a>
</div>
<!-- create time block modal -->
<div
class="modal fade"
id="create-time-block-modal"
data-bs-backdrop="static"
data-bs-keyboard="false"
tabindex="-1"
aria-labelledby="staticBackdropLabel"
aria-hidden="true"
>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<div></div>
<h5 class="modal-title">CREATE ACTIVITY</h5>
<div class="exit-modal">
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
</div>
<div class="modal-body">
<div class="input-group">
<form id="activity-form" class="register-form">
<div class="input-panel mb-3">
<div class="form-header">Activity Name *</div>
<input
type="text"
class="form-control"
name="activity-name"
aria-label="first-name"
aria-describedby="basic-addon1"
/>
</div>
<div class="input-panel mb-3">
<div class="form-header">Description</div>
<textarea
id="description"
class="form-control"
name="description"
rows="5"
cols="50"
></textarea>
</div>
<div id="time-container">
<div class="input-panel mb-3">
<div class="form-header">Start Time *</div>
<input
type="time"
name="start"
class="form-control"
id="start-time"
name="start-time"
min="00:00"
max="24:00"
step="900"
required
/>
</div>
<div class="input-panel mb-3">
<div class="form-header">End Time *</div>
<input
type="time"
name="end"
class="form-control"
id="end-time"
name="end-time"
min="00:00"
max="24:00"
step="900"
required
/>
</div>
</div>
<div class="input-panel mb-3">
<div class="form-header">Remarks</div>
<textarea
id="remark"
class="form-control"
name="remark"
rows="5"
cols="50"
></textarea>
</div>
<div class="input-panel mb-3">
<div class="form-header">Display Color</div>
<input value="#f29659" id="color" class="form-control" name="color" type="color" />
</div>
<div class="modal-footer">
<button id="submit-new-activity" type="submit" class="btn btn-primary button-53">
Submit
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<!-- edit Time block modal (name and time only) -->
<div
class="modal fade"
id="edit-time-name-modal"
data-bs-backdrop="static"
data-bs-keyboard="false"
tabindex="-1"
aria-labelledby="staticBackdropLabel"
aria-hidden="true"
>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<div></div>
<h5 class="modal-title">EDIT NAME AND TIME</h5>
<div class="exit-modal">
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
</div>
<div class="modal-body">
<div class="input-group">
<form id="edit-time-name-form" class="register-form">
<div class="input-panel mb-3">
<div class="form-header">Activity Name *</div>
<input
type="text"
class="form-control"
name="edit-activity-name"
id="edit-activity-name"
aria-describedby="basic-addon1"
/>
</div>
<div id="time-container">
<div class="input-panel mb-3">
<div class="form-header">Start Time *</div>
<input
type="time"
name="start"
class="form-control"
id="edit-start-time"
name="edit-start-time"
min="00:00"
max="24:00"
step="900"
required
/>
</div>
<div class="input-panel mb-3">
<div class="form-header">End Time *</div>
<input
type="time"
name="end"
class="form-control"
id="edit-end-time"
name="edit-end-time"
min="00:00"
max="24:00"
step="900"
required
/>
</div>
</div>
<div class="input-panel mb-3">
<div class="form-header">Display Color</div>
<input
value="#f29659"
id="edit-color"
class="form-control"
name="color"
type="color"
/>
</div>
<div class="modal-footer">
<button id="submit-edit-time-name" type="submit" class="btn btn-primary button-53">
Submit
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<!-- Edit Item Modal -->
<div
class="modal fade"
id="edit-item-modal"
data-bs-backdrop="static"
data-bs-keyboard="false"
tabindex="-1"
aria-labelledby="staticBackdropLabel"
aria-hidden="true"
>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<div></div>
<h5 class="modal-title">ITEM LIST</h5>
<div class="exit-modal">
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
</div>
<div class="modal-body">
<div id="item-form-text" class="form-text">
Select items you wish to add to this activity. Blanketed indicates total quantity of each
items assigned to the event.
</div>
<div class="input-group">
<form id="edit-item-form" class="edit-item-form row">
<div class="input-panel mb-3 col-6">
<div class="form-header" id="item-form-header">Food</div>
<div class="list-items-container">
<ul
id="food-list"
class="item-list-box"
role="listbox"
tabindex="0"
aria-label="emails list"
>
<!-- to be added by JS -->
</ul>
</div>
</div>
<div class="input-panel mb-3 col-6">
<div class="form-header" id="item-form-header">Drinks</div>
<div class="list-items-container">
<ul
id="drink-list"
class="item-list-box"
role="listbox"
tabindex="0"
aria-label="emails list"
>
<!-- to be added by JS -->
</ul>
</div>
</div>
<div class="input-panel mb-3 col-6">
<div class="form-header" id="item-form-header">Decoration</div>
<div class="list-items-container">
<ul
id="decoration-list"
class="item-list-box"
role="listbox"
tabindex="0"
aria-label="emails list"
>
<!-- to be added by JS -->
</ul>
</div>
</div>
<div class="input-panel mb-3 col-6">
<div class="form-header" id="item-form-header">Others</div>
<div class="list-items-container">
<ul
id="other-list"
class="item-list-box"
role="listbox"
tabindex="0"
aria-label="emails list"
>
<!-- to be added by JS -->
</ul>
</div>
</div>
<div class="modal-footer">
<button id="submit-edit-item" type="submit" class="btn btn-primary button-53">
Submit
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<script src="https://thibaultjanbeyer.github.io/DragSelect/ds.min.js"></script>
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.1/dist/js/bootstrap.bundle.min.js"
integrity="sha384-u1OknCvxWvY5kfmNBILK2hRnQC3Pr17a+RTT6rIHI7NnikvbZlHgTPOOmMi466C8"
crossorigin="anonymous"
></script>
<script type="module" src="./eventSchedule.js"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,972 @@
/* @import */
body {
font-family: 'Calibri';
background-color: #ffffff;
font-size: 16px;
font-family: 'Lato', sans-serif;
}
textarea {
border-radius: 10px;
background: none;
border-color: #3f3f3f;
}
a {
color: inherit;
text-decoration: none;
}
.content {
height: calc(100vh - 115px);
margin-top: 10px;
}
.vertical-flex {
flex-direction: column;
justify-content: flex-start !important;
align-items: flex-start !important;
}
.background-frame:not(.eventname .background-frame) {
background-color: #f9f9da;
border-radius: 10px;
}
.background-frame {
position: relative;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
padding: 10px 20px;
}
.line1 {
height: 15vh;
}
.line2 {
height: 65vh;
display: flex;
flex-direction: column;
}
.participant {
height: 65%;
}
.venue {
height: 35%;
}
.frame {
padding: 10px 0 10px 0;
}
.frame-title {
font-size: 20px;
font-weight: bold;
text-align: left;
}
.frame-title-container {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
}
.frame-content-container {
width: 100%;
display: flex;
padding: 10px 0;
height: 100%;
overflow-y: auto;
}
.eventname {
font-size: 20px;
font-weight: bold;
text-align: center;
line-height: 100%;
}
.eventname .emoji {
font-size: 30px;
}
.date-time .background-frame > div {
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.date-time .background-frame > div:first-child {
width: 30%;
}
.date-time .background-frame > div:nth-child(2) {
width: 70%;
}
.date-time .background-frame {
display: flex;
justify-content: space-between;
align-items: center;
text-align: center;
}
.date-time .frame-content,
.date-time .frame-content-label {
display: flex;
flex-direction: column;
align-items: flex-start;
}
.date-time .frame-content-label {
width: 20%;
}
.date-time .frame-content {
width: 80%;
}
.participant .frame-content-container {
width: 100%;
max-height: 70%;
overflow-y: auto;
}
.venue .frame-title-container,
.schedule .frame-title-container {
justify-content: space-between;
align-items: center;
}
#number-of-participants {
margin: 0 10px;
display: flex;
justify-content: center;
align-items: center;
background-color: #f2d492;
color: #ee6c4d;
width: 30px;
height: 30px;
border-radius: 5px;
}
.participant .left {
display: flex;
align-items: center;
}
.datetime-form {
width: 100%;
}
.red_creator {
color: red;
font-weight: bold;
}
.input-group-text {
cursor: pointer;
}
#invitation-link {
display: flex;
justify-content: center;
align-items: center;
}
.copied {
background-color: #059862;
color: white;
font-weight: bold;
border: 4px solid #07c982;
}
.name-block {
display: flex;
margin: 10px 15px 10px 10px;
justify-content: center;
align-items: center;
}
.delete_event-button-container {
position: absolute;
right: 10px;
top: 50%;
transform: translate(0, -50%);
}
.warning-sign {
font-size: 70px;
width: 90px;
height: 90px;
border-radius: 200px;
border: solid 4px black;
display: flex;
justify-content: center;
align-items: center;
}
.warning {
font-size: 30px;
font-weight: bold;
}
#delete-event-modal .modal-body,
#overwrite-venue-poll-modal .modal-body,
#overwrite-datetime-poll-modal .modal-body {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.hide {
display: none;
}
div[class^='venue_poll_'] {
display: flex;
justify-content: flex-start;
align-items: center;
width: 100%;
margin: 5px 0;
}
div[class^='datetime_poll_'] {
display: flex;
justify-content: flex-start;
align-items: center;
width: 100%;
margin: 5px 0;
}
.datetime_title {
display: flex;
justify-content: flex-start;
align-items: center;
width: 100%;
margin: 5px 0;
}
div[class^='venue_poll_'] label {
width: 80px;
}
div[class^='datetime_poll_'] label {
width: 80px;
}
.datetime_title > div:first-child {
width: 80px;
}
.datetime_title > div:nth-child(2) {
display: flex;
width: 100%;
}
.datetime_title > div:nth-child(2) div {
width: 50%;
display: flex;
justify-content: center;
align-items: center;
font-weight: bold;
}
.venue-poll-options-container {
margin: 10px 20px !important;
}
/* ↓↓ Scroll bar ↓↓ */
.frame-content-container::-webkit-scrollbar {
display: none;
}
/* ↑↑ Scroll bar ↑↑ */
/* ↓↓ Buttons ↓↓ */
.venue-buttons-container {
display: flex;
}
.edit-button,
.poll-button {
border-radius: 100px;
background-color: #f29559;
color: white;
width: 22px;
height: 22px;
font-size: 10px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
}
.poll-button {
margin: 0 10px;
}
.invite-button {
border-radius: 100px;
background-color: #f29559;
color: white;
width: 22px;
height: 22px;
font-size: 13px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
margin-right: 10px;
}
.info-button {
border-radius: 100px;
background-color: #f29559;
color: white;
width: 22px;
height: 22px;
font-size: 10px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
}
.delete-button {
border-radius: 100px;
background-color: #c74b38;
color: white;
width: 22px;
height: 22px;
font-size: 10px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
}
#delete-event-submit,
#overwrite-venue-poll-submit,
#overwrite-datetime-poll-submit {
background-color: #d8684e;
color: white;
font-weight: bold;
border: #c7492c 3px solid;
}
#delete-event-modal .modal-submit-button,
#overwrite-venue-poll-modal .modal-submit-button,
#overwrite-datetime-poll-modal .modal-submit-button {
box-shadow: none;
}
.switch-buttons-container {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 15px;
}
.btn-outline-success,
.btn-outline-danger {
--bs-btn-color: #6c757d;
--bs-btn-border-color: #6c757d;
--bs-btn-active-bg: #6c757d;
--bs-btn-active-border-color: #6c757d;
font-size: 15px;
}
.btn-outline-success {
--bs-btn-border-radius: 0.375rem 0 0 0.375rem;
}
.btn-outline-danger {
--bs-btn-border-radius: 0 0.375rem 0.375rem 0;
}
.option-button-container {
display: flex;
align-items: center;
justify-content: center;
}
#venue-add-option,
#venue-remove-option {
margin: 10px;
box-shadow: none;
}
/* ↑↑ Button ↑↑ */
/* Modal */
.modal-submit-button {
font-size: 14px;
color: #293241;
font-family: 'Lato', sans-serif;
background-color: #f2d492;
padding: 10px 20px 10px 20px;
border-radius: 20px;
margin: 18px;
min-width: 110px;
border: none;
box-shadow: 0px 1px 12px #f2d492;
}
.modal-footer {
display: flex;
justify-content: center;
align-content: center;
}
.modal-login {
font-size: 14px;
color: #293241;
font-family: 'Lato', sans-serif;
background-color: #f2d492;
padding: 10px 20px 10px 20px;
border-radius: 20px;
margin: 18px;
width: 110px;
border: none;
box-shadow: 0px 1px 12px #f2d492;
}
.modal-header {
display: flex;
justify-content: space-between;
}
.modal-title {
font-size: 25px;
font-weight: bold;
color: #293241;
font-family: 'Lato', sans-serif;
transform: translateX(10px);
}
.form-label {
color: #293241;
font-family: 'Lato', sans-serif;
}
.input-group-container {
width: 100%;
display: flex;
flex-direction: column;
justify-content: center;
margin: 0 0 10px 0;
}
.input-title,
.reminder-text {
text-align: center;
}
.reminder-text {
color: grey;
width: 70%;
}
.form-control {
border-width: 2px;
border-radius: 10px;
}
#datetime-submit,
#venue-submit,
#venue-poll-submit {
width: 50%;
}
.form-header {
margin: 0 0 5px 0;
}
.form,
.input-panel {
margin: 20px 20px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.invite-button-container {
position: absolute;
bottom: -25px;
left: 50%;
transform: translate(-50%, -50%);
width: 80%;
height: 20%;
display: flex;
justify-content: center;
align-items: center;
}
#participants-list-container {
display: flex;
justify-content: center;
align-items: center;
}
.participants-list {
height: 35vh;
padding: 10px;
margin-top: 18px;
border-radius: 10px;
border: solid grey 2px;
position: relative;
}
.participants-list-title {
position: absolute;
top: -3px;
left: 50%;
transform: translate(-50%, -50%);
background-color: white;
font-size: 20px;
font-weight: bold;
}
#current-participants-list .participants-list-title {
color: #2fd0c1;
}
#deleted-participants-list .participants-list-title {
color: #d02f3e;
}
.user-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
}
#participants-modal .frame-content-container > div {
width: 100%;
}
.datetime-buttons-container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-end;
}
.date-time .poll-button {
margin: 0 0 5px 0;
}
/* From eventPageSchedule.css */
.item-list-box {
min-height: 150px;
min-width: 100px;
}
#item-form-text {
text-align: center;
}
#item-form-header {
text-align: center;
}
.item-list-box {
border-radius: 10px;
background-color: #e8e892;
height: 100%;
padding-top: 10px;
padding-bottom: 10px;
overflow: auto;
}
#date-selector-container {
display: flex;
justify-content: right;
padding-bottom: 10px;
padding-right: 10px;
}
#time-block-memo-container {
position: relative;
max-height: 100%;
}
#close-memo {
position: absolute;
top: 0px;
font-size: 10px;
right: 10px;
}
#time-block-page-container {
max-height: 800px;
height: 550px;
padding-top: 0px;
padding-bottom: 30px;
}
#event-time-container {
text-align: center;
}
.event-schedule {
background-color: #efefd0 !important;
}
.event-schedule:hover {
background-color: #e8e892 !important;
}
@media (max-width: 1000px) {
#line {
display: none;
}
}
@media (max-width: 576px) {
@media (max-width: 1000px) {
#line {
display: none;
}
#memo {
font-size: large;
max-height: 100%;
max-width: 100%;
overflow: auto;
}
#time-block-memo-container {
max-height: 300px;
height: 300px;
overflow: auto;
}
#rundown-container {
max-height: 300px;
}
}
}
#memo {
background-color: #efefd0;
height: 90%;
border-radius: 10px;
overflow: auto;
}
#page {
display: flex;
justify-content: center;
height: 90%;
width: 100%;
}
/* #line {
position: absolute;
} */
#rundown {
max-height: 80%;
max-width: 100%;
}
#rundown-container {
max-height: 100%;
position: relative;
background-color: white;
padding-top: 30px;
padding-bottom: 20px;
}
.time-stamp-container {
background-color: none;
position: relative;
display: flex;
justify-content: center;
align-items: center;
}
.individual-time-block {
height: 100%;
padding: 2px;
max-width: 100%;
overflow: visible;
}
.time-block {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
background-color: rgba(181, 180, 180, 0.625);
padding: 10px;
border-radius: 5px;
padding: 1px;
font-size: 1.5vw;
text-align: center;
transition: background-color 50ms ease-out 50ms;
}
.save-time-block {
height: 100%;
color: white;
opacity: 0.7;
padding: 10px;
border-radius: 5px;
text-align: center;
transition: background-color 50ms ease-out 50ms;
}
.save-time-block:hover {
opacity: 0.8;
transform: scale(1.01);
}
.time-stamp {
position: absolute;
top: -5px;
right: 10px;
font-size: 10px;
}
.last-time-stamp {
position: absolute;
bottom: -5px;
right: 10px;
font-size: 10px;
}
.memo-item-container {
position: relative;
background-color: rgba(255, 255, 255, 0.522);
border-radius: 10px;
width: 100%;
height: 100%;
margin-bottom: 10px;
padding: 10px;
font-size: 15px;
}
#memo-item-cluster {
display: flex;
flex-direction: column;
justify-content: space-between;
height: 100%;
padding: 10px;
}
.memo-item-label {
font-weight: 800;
font-size: 15px;
padding: 5px;
}
/* Scroll bar */
::-webkit-scrollbar {
width: 15px;
}
::-webkit-scrollbar-track {
box-shadow: inset 0 0 5px grey;
border-radius: 5px;
height: 200px;
}
::-webkit-scrollbar-thumb {
background: #efefd0;
border-radius: 5px;
min-height: 250px;
}
::-webkit-scrollbar-thumb:hover {
background: white;
}
#memo,
#memo-tag {
animation-duration: 0.5s;
animation-name: animate-fade;
animation-delay: 0.1s;
animation-fill-mode: backwards;
}
@keyframes animate-fade {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
#edit-activities {
border-radius: 100px;
background-color: #f2965985;
color: white;
text-align: center;
min-width: 22px !important;
min-height: 22px !important;
padding: 3px;
font-size: 10px;
}
#edit-remarks {
border-radius: 100px;
background-color: #f2965985;
color: white;
text-align: center;
min-width: 22px !important;
min-height: 22px !important;
padding: 3px;
font-size: 10px;
}
#edit-show-item {
border-radius: 100px;
background-color: #f2965985;
color: white;
text-align: center;
min-width: 22px !important;
min-height: 22px !important;
padding: 3px;
font-size: 10px;
}
.modal-dialog {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
#event-name {
font-size: 2vw;
padding-top: 10px;
}
.fa-trash {
z-index: 1000;
right: 20px;
top: 20px;
}
.creator-function {
z-index: 1000;
}
/* CSS */
.button-53 {
background-color: #f29659;
border: 0 solid #e5e7eb;
box-sizing: border-box;
color: #000000;
display: flex;
font-family: ui-sans-serif, system-ui, -apple-system, system-ui, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
font-size: 1rem;
font-weight: 700;
justify-content: center;
line-height: 1.75rem;
padding: 0.75rem 1.65rem;
position: relative;
text-align: center;
text-decoration: none #000000 solid;
text-decoration-thickness: auto;
width: 100%;
max-width: 460px;
position: relative;
cursor: pointer;
transform: rotate(-2deg);
user-select: none;
-webkit-user-select: none;
touch-action: manipulation;
}
.button-53:focus {
outline: 0;
}
.button-53:after {
content: '';
position: absolute;
border: 1px solid #000000;
bottom: 4px;
left: 4px;
width: calc(100% - 1px);
height: calc(100% - 1px);
}
.button-53:hover:after {
bottom: 2px;
left: 2px;
}
@media (min-width: 768px) {
.button-53 {
padding: 0.75rem 3rem;
font-size: 1.25rem;
}
}
/* From itemSummary */
.shopping-list {
height: 90%;
border-radius: 20px;
margin: 10px 2px 5px 5px;
width: 100%;
}
.shopping-list .item-list {
position: relative;
height: 100%;
width: 100%;
background-color: #fff;
}
.border {
border: 10px solid #9bafd0;
border-radius: 20px;
padding: 10px;
max-height: 500px;
overflow-y: scroll;
}
.pending-item-header,
.pending-item {
display: flex;
justify-content: space-between;
}
.bi-filter-circle {
color: #495871;
width: 20px;
height: 20px;
}
.shorting-btn {
border: none;
border-radius: 20px;
background-color: #fff;
}
.check-btn {
border: none;
border-radius: 20px;
background-color: #fff;
}
.dropdown-item {
cursor: pointer;
}

View File

@@ -0,0 +1,579 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.1/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-iYQeCzEYFbKjA/T2uDLTpkwGzCiq6soy8tYaI1GyVh/UjpbCx/TYkiZhlZB6+fzT"
crossorigin="anonymous"
/>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.9.1/font/bootstrap-icons.css" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.0/css/all.min.css"
integrity="sha512-xh6O/CkQoPOWDdYTDqeRdPCVd1SpvCA9XXcUnZS2FmJNp1coAFzvtCN9BmamE+4aHK8yyUHUSCcJHgXloTyT2A=="
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>
<link rel="stylesheet" href="./event.css" />
<title>Event Summary</title>
</head>
<body style="display: none">
<div class="navbar-container container">
<!-- NAVBAR: to be loaded with js -->
</div>
<div class="content">
<div class="container">
<div class="row">
<div class="col-md-4">
<div class="eventname frame line1">
<div class="background-frame">
<!-- Event Name: to be added by JS -->
</div>
</div>
</div>
<div class="col-md-8">
<div class="date-time frame line1">
<div class="background-frame">
<!-- Event Name: to be added by JS -->
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-4">
<div class="participant-venue line2">
<div class="participant frame">
<div class="background-frame vertical-flex">
<!-- Participants: to be added by JS -->
</div>
</div>
<div class="venue frame">
<div class="background-frame vertical-flex">
<!-- Venue: to be added by JS -->
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="schedule frame line2">
<div class="background-frame vertical-flex">
<!-- Schedule: to be added by JS -->
</div>
</div>
</div>
<div class="col-md-4">
<div class="item frame line2">
<div class="background-frame vertical-flex">
<!-- Item: to be added by JS -->
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Modal Parts -->
<!-- Date/Time Modal -->
<div
class="modal fade"
id="datetime-modal"
data-bs-backdrop="static"
data-bs-keyboard="false"
tabindex="-1"
aria-labelledby="staticBackdropLabel"
aria-hidden="true"
>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<div></div>
<h5 class="modal-title">Date & Time</h5>
<div class="exit-modal">
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
</div>
<div class="modal-body">
<form>
<div class="switch-buttons-container">
<input
type="radio"
class="btn-check"
name="options-outlined"
id="edit-datetime-switch"
autocomplete="off"
checked
/>
<label class="btn btn-outline-success" for="edit-datetime-switch">Editing</label>
<input
type="radio"
class="btn-check"
name="options-outlined"
id="poll-datetime-switch"
autocomplete="off"
/>
<label class="btn btn-outline-danger" for="poll-datetime-switch">Polling</label>
</div>
</form>
<!-- Edit datetime part -->
<div class="input-group edit-input">
<div class="input-group-container">
<div class="input-title">Please edit your date & time below.</div>
<form id="datetime-form">
<div class="datetime-start input-panel mb-3">
<div class="form-label">Start date and time</div>
<input
class="clock"
type="datetime-local"
id="datetime-start"
name="datetime_start"
min="2021-06-07T00:00"
max="2035-12-30T00:00"
step="900"
/>
</div>
<div class="datetime-end input-panel mb-3">
<div class="form-label">End date and time</div>
<input
class="clock"
type="datetime-local"
id="datetime-end"
name="datetime_end"
min="2021-06-07T00:00"
max="2035-12-30T00:00"
step="900"
/>
</div>
<div class="input-panel mb-3">
<div class="reminder-text">
Please fill in both start and end date/time. <br />
Enter the time in 15 mins interval, e.g. 15:00 or 15:15 or 15:30 or 15:45.
</div>
</div>
<div class="modal-footer">
<button id="datetime-submit" type="submit" class="modal-submit-button">
Edit
</button>
</div>
</form>
</div>
</div>
<!-- Poll Datetime Form -->
<div class="input-group poll-input hide">
<div class="input-group-container">
<div class="input-title">Please enter your polling options below:</div>
<form id="datetime-poll-form">
<div class="datetime-poll-options-container input-panel mb-3">
<div class="datetime_title">
<div></div>
<div>
<div>Start</div>
<div>End</div>
</div>
</div>
<div class="datetime_poll_1">
<label for="datetime_poll">Option 1: </label>
<input
class="clock"
type="datetime-local"
id="datetime_poll_start"
name="datetime_poll_start"
min="2021-06-07T00:00"
max="2035-12-30T00:00"
step="900"
/>
<input
class="clock"
type="datetime-local"
id="datetime_poll_end"
name="datetime_poll_end"
min="2021-06-07T00:00"
max="2035-12-30T00:00"
step="900"
/>
</div>
<div class="datetime_poll_2">
<label for="datetime_poll">Option 2: </label>
<input
class="clock"
type="datetime-local"
id="datetime_poll_start"
name="datetime_poll_start"
min="2021-06-07T00:00"
max="2035-12-30T00:00"
step="900"
/>
<input
class="clock"
type="datetime-local"
id="datetime_poll_end"
name="datetime_poll_end"
min="2021-06-07T00:00"
max="2035-12-30T00:00"
step="900"
/>
</div>
<!-- More options added by JS -->
</div>
<div class="input-panel mb-3">
<div class="reminder-text">
Please fill in both start and end date/time. <br />
Enter the time in 15 mins interval, e.g. 15:00 or 15:15 or 15:30 or 15:45.
</div>
</div>
<div class="option-button-container">
<button id="datetime-add-option" class="modal-submit-button">
<i class="fa-solid fa-plus"></i>&nbsp Option
</button>
<button id="datetime-remove-option" class="modal-submit-button">
<i class="fa-solid fa-minus"></i>&nbsp Option
</button>
</div>
<div class="modal-footer">
<button id="datetime-poll-submit" type="submit" class="modal-submit-button">
Start Polling
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Overwrite datetime Poll Confirmation Modal -->
<div
class="modal fade"
id="overwrite-datetime-poll-modal"
data-bs-backdrop="static"
data-bs-keyboard="false"
tabindex="-1"
aria-labelledby="staticBackdropLabel"
aria-hidden="true"
>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<div></div>
<h5 class="modal-title">Overwrite Poll</h5>
<div class="exit-modal">
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
</div>
<div class="modal-body">
<div class="warning-sign">
<i class="fa-solid fa-exclamation"></i>
</div>
<div class="warning">Are you sure?</div>
<div class="reminder-text">Overwritten poll cannot be restored!</div>
<div class="modal-footer">
<button id="overwrite-datetime-poll-submit" type="submit" class="modal-submit-button">
OVERWRITE
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Venue Modal -->
<div
class="modal fade"
id="venue-modal"
data-bs-backdrop="static"
data-bs-keyboard="false"
tabindex="-1"
aria-labelledby="staticBackdropLabel"
aria-hidden="true"
>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<div></div>
<h5 class="modal-title">Venue</h5>
<div class="exit-modal">
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
</div>
<div class="modal-body">
<form>
<div class="switch-buttons-container">
<input
type="radio"
class="btn-check"
name="options-outlined"
id="edit-venue-switch"
autocomplete="off"
checked
/>
<label class="btn btn-outline-success" for="edit-venue-switch">Editing</label>
<input
type="radio"
class="btn-check"
name="options-outlined"
id="poll-venue-switch"
autocomplete="off"
/>
<label class="btn btn-outline-danger" for="poll-venue-switch">Polling</label>
</div>
</form>
<!-- Edit Venue Form -->
<div class="input-group edit-input">
<div class="input-group-container">
<div class="input-title">Please enter your venue below:</div>
<form id="venue-form">
<div class="venue-start input-panel mb-3">
<input
type="text"
class="form-control"
name="venue"
aria-label="venue"
aria-describedby="basic-addon1"
/>
</div>
<div class="modal-footer">
<button id="venue-submit" type="submit" class="modal-submit-button">
Edit
</button>
</div>
</form>
</div>
</div>
<!-- Poll Venue Form -->
<div class="input-group poll-input hide">
<div class="input-group-container">
<div class="input-title">Please enter your polling options below:</div>
<form id="venue-poll-form">
<div class="venue-poll-options-container input-panel mb-3">
<div class="venue_poll_1">
<label for="venue_poll">Option 1: </label>
<input
type="text"
class="form-control"
name="venue_poll"
aria-label="venue_poll"
aria-describedby="basic-addon1"
/>
</div>
<div class="venue_poll_2">
<label for="venue_poll">Option 2: </label>
<input
type="text"
class="form-control"
name="venue_poll"
aria-label="venue_poll"
aria-describedby="basic-addon1"
/>
</div>
<!-- More options added by JS -->
</div>
<div class="option-button-container">
<button id="venue-add-option" class="modal-submit-button">
<i class="fa-solid fa-plus"></i>&nbsp Option
</button>
<button id="venue-remove-option" class="modal-submit-button">
<i class="fa-solid fa-minus"></i>&nbsp Option
</button>
</div>
<div class="modal-footer">
<button id="venue-poll-submit" type="submit" class="modal-submit-button">
Start Polling
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Overwrite Venue Poll Confirmation Modal -->
<div
class="modal fade"
id="overwrite-venue-poll-modal"
data-bs-backdrop="static"
data-bs-keyboard="false"
tabindex="-1"
aria-labelledby="staticBackdropLabel"
aria-hidden="true"
>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<div></div>
<h5 class="modal-title">Overwrite Poll</h5>
<div class="exit-modal">
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
</div>
<div class="modal-body">
<div class="warning-sign">
<i class="fa-solid fa-exclamation"></i>
</div>
<div class="warning">Are you sure?</div>
<div class="reminder-text">Overwritten poll cannot be restored!</div>
<div class="modal-footer">
<button id="overwrite-venue-poll-submit" type="submit" class="modal-submit-button">
OVERWRITE
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Delete Participants Modal -->
<div
class="modal fade"
id="participants-modal"
data-bs-backdrop="static"
data-bs-keyboard="false"
tabindex="-1"
aria-labelledby="staticBackdropLabel"
aria-hidden="true"
>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<div></div>
<h5 class="modal-title">Participants</h5>
<div class="exit-modal">
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
</div>
<div class="modal-body">
<div class="input-group">
<div class="input-group-container">
<div class="input-title">Please edit your participants below:</div>
<div id="participants-list-container">
<div class="container">
<div class="row justify-content-center">
<div class="col-sm-6">
<div class="participants-list" id="current-participants-list">
<div class="participants-list-title">Current</div>
<!-- Current Participants Here -->
</div>
</div>
<div class="col-sm-6">
<div class="participants-list" id="deleted-participants-list">
<div class="participants-list-title">Deleted</div>
<!-- Participants To Be Removed Here -->
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button id="participants-submit" type="submit" class="modal-submit-button">Submit</button>
<button id="participants-reset" type="reset" class="modal-submit-button">Reset</button>
</div>
</div>
</div>
</div>
<!-- Invitation Link Modal -->
<div
class="modal fade"
id="invitation-modal"
data-bs-backdrop="static"
data-bs-keyboard="false"
tabindex="-1"
aria-labelledby="staticBackdropLabel"
aria-hidden="true"
>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<div></div>
<h5 class="modal-title">Invitation</h5>
<div class="exit-modal">
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
</div>
<div class="modal-body">
<div class="input-title">Please send your friend the invitation link below:</div>
<div class="input-group mb-3">
<input
type="text"
class="form-control"
aria-label="invitation"
name="invitation"
aria-describedby="invitation-link"
readonly
/>
<a class="input-group-text" id="invitation-link">Copy Link</a>
</div>
<form id="invitation-form">
<div class="modal-footer">
<div class="reminder-text">
Note: Previous link will be invalid <br />
when new link is created.
</div>
<button id="invitation-submit" type="submit" class="modal-submit-button">
Create New Link
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Delete Event Confirmation Modal -->
<div
class="modal fade"
id="delete-event-modal"
data-bs-backdrop="static"
data-bs-keyboard="false"
tabindex="-1"
aria-labelledby="staticBackdropLabel"
aria-hidden="true"
>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<div></div>
<h5 class="modal-title">Delete Event</h5>
<div class="exit-modal">
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
</div>
<div class="modal-body">
<div class="warning-sign">
<i class="fa-solid fa-exclamation"></i>
</div>
<div class="warning">Are you sure?</div>
<div class="reminder-text">You will not be able to revert this action!</div>
<div class="modal-footer">
<button id="delete-event-submit" type="submit" class="modal-submit-button">
DELETE EVENT
</button>
</div>
</div>
</div>
</div>
</div>
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.1/dist/js/bootstrap.bundle.min.js"
integrity="sha384-u1OknCvxWvY5kfmNBILK2hRnQC3Pr17a+RTT6rIHI7NnikvbZlHgTPOOmMi466C8"
crossorigin="anonymous"
></script>
<script type="module" src="./event.js"></script>
</body>
</html>

View File

@@ -0,0 +1,477 @@
import { addNavbar } from '/functions/addNavbar.js';
import { loadName } from '/functions/loadName.js';
import { loadEventDetails, pasteInvitationLink } from '../loadEvent.js';
import { deletedParticipantsList } from '../listenButtons.js';
import { getEventSchedule } from './eventPageSchedule/eventPageSchedule.js';
import { fetchPendingItems } from './itemSummary.js';
window.addEventListener('load', async () => {
addNavbar();
await loadName();
await loadEventDetails();
getEventSchedule();
await fetchPendingItems('food');
document.body.style.display = 'block';
});
// Submit datetime form
document.querySelector('#datetime-form').addEventListener('submit', async function (e) {
e.preventDefault();
const form = e.target;
const startTime = form.datetime_start.value ? new Date(form.datetime_start.value).toISOString() : null;
const endTime = form.datetime_end.value ? new Date(form.datetime_end.value).toISOString() : null;
const nowTimeValue = new Date().getTime();
const startTimeValue = new Date(startTime).getTime();
const endTimeValue = new Date(endTime).getTime();
let dataPass = true;
if (startTimeValue && endTimeValue) {
if (startTimeValue <= nowTimeValue) {
dataPass = false;
alert('Start time must be later than time now!');
} else if (startTimeValue >= endTimeValue) {
dataPass = false;
alert('Start time cannot equals or later than end time!');
}
} else if (!!startTimeValue + !!endTimeValue) {
dataPass = false;
alert('You cannot only leave 1 time blank!');
}
if (dataPass) {
const formObj = {
startTime,
endTime
};
const params = new URLSearchParams(window.location.search);
const eventId = params.get('event-id');
const res = await fetch(`/events/detail/datetime/${eventId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formObj)
});
const eventsResult = await res.json();
if (eventsResult.status) {
alert('Date & Time successfully updated!');
const myModal = bootstrap.Modal.getInstance(document.getElementById('datetime-modal'));
myModal.hide();
loadEventDetails();
} else {
alert('Unable to update.');
}
}
});
// Datetime edit-poll toggle
document.querySelector('#edit-datetime-switch').addEventListener('change', () => {
document.querySelector('#datetime-modal .edit-input').classList.toggle('hide');
document.querySelector('#datetime-modal .poll-input').classList.toggle('hide');
});
document.querySelector('#poll-datetime-switch').addEventListener('change', () => {
document.querySelector('#datetime-modal .edit-input').classList.toggle('hide');
document.querySelector('#datetime-modal .poll-input').classList.toggle('hide');
});
// Datetime polling add option button
document.querySelector('#datetime-add-option').addEventListener('click', (e) => {
e.preventDefault();
const numberOfOptions = document.querySelectorAll('div[class^="datetime_poll_"]').length;
let newDiv = document.createElement('div');
newDiv.classList = `datetime_poll_${numberOfOptions + 1}`;
newDiv.innerHTML = `
<label for="datetime_poll">Option ${numberOfOptions + 1}: </label>
<input class="clock" type="datetime-local" id="datetime_poll_start" name="datetime_poll_start"
min="2021-06-07T00:00" max="2035-12-30T00:00" step="900">
<input class="clock" type="datetime-local" id="datetime_poll_end" name="datetime_poll_end"
min="2021-06-07T00:00" max="2035-12-30T00:00" step="900">
`;
document.querySelector('.datetime-poll-options-container').appendChild(newDiv);
});
// Datetime polling remove option button
document.querySelector('#datetime-remove-option').addEventListener('click', (e) => {
e.preventDefault();
const venuePollOptionsDivList = document.querySelectorAll('div[class^="datetime_poll_"]');
const numberOfOptions = venuePollOptionsDivList.length;
if (numberOfOptions > 2) {
venuePollOptionsDivList[numberOfOptions - 1].remove();
}
});
// Submit datetime polling
document.querySelector('#datetime-poll-form').addEventListener('submit', async (e) => {
e.preventDefault();
const params = new URLSearchParams(window.location.search);
const eventId = params.get('event-id');
let dataPass = true;
let formList = [];
const form = e.target;
const startList = form.datetime_poll_start;
const endList = form.datetime_poll_end;
for (let i = 0; i < startList.length; i++) {
if (!startList[i].value || !endList[i].value) {
dataPass = false;
alert('Please fill in all options!');
break;
} else if (new Date(startList[i].value).getTime() <= new Date().getTime()) {
dataPass = false;
alert('Start time must be later than today!');
break;
} else if (new Date(startList[i].value).getTime() >= new Date(endList[i].value).getTime()) {
dataPass = false;
alert('Start time must be before end time!');
break;
} else {
formList.push({
start: new Date(startList[i].value),
end: new Date(endList[i].value)
});
}
}
if (dataPass) {
const res = await fetch(`/events/poll/datetime/${eventId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formList)
});
const result = await res.json();
if (result.status) {
alert('Successfully created a datetime poll!');
window.location.href = `/poll/datetimePoll.html?${params}&is-creator=1`;
} else {
if (result.created) {
// Modal not yet added
alert('Poll has been created before!');
const datetimePollModal = bootstrap.Modal.getInstance(document.getElementById('datetime-modal'));
datetimePollModal.hide();
const datetimePollOverwriteModal = new bootstrap.Modal(
document.getElementById('overwrite-datetime-poll-modal')
);
datetimePollOverwriteModal.show();
} else {
alert('Unable to create poll.');
}
}
}
});
// Overwrite datetime poll confirmed
document.querySelector('#overwrite-datetime-poll-submit').addEventListener('click', async (e) => {
e.preventDefault();
const params = new URLSearchParams(window.location.search);
const eventId = params.get('event-id');
let dataPass = true;
let formList = [];
const form = document.querySelector('#datetime-poll-form');
const startList = form.datetime_poll_start;
const endList = form.datetime_poll_end;
for (let i = 0; i < startList.length; i++) {
if (!startList[i].value || !endList[i].value) {
dataPass = false;
alert('Please fill in all options!');
break;
} else if (new Date(startList[i].value).getTime() <= new Date().getTime()) {
dataPass = false;
alert('Start time must be later than today!');
break;
} else if (new Date(startList[i].value).getTime() >= new Date(endList[i].value).getTime()) {
dataPass = false;
alert('Start time must be before end time!');
break;
} else {
formList.push({
start: new Date(startList[i].value),
end: new Date(endList[i].value)
});
}
}
if (dataPass) {
const res = await fetch(`/events/poll/datetime/replacement/${eventId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formList)
});
const result = await res.json();
if (result.status) {
alert('Successfully created a datetime poll!');
window.location.href = `/poll/datetimePoll.html?${params}&is-creator=1`;
} else {
alert('Unable to create poll.');
}
}
});
// Submit venue form
document.querySelector('#venue-form').addEventListener('submit', async function (e) {
e.preventDefault();
const form = e.target;
const venue = form.venue.value;
let dataPass = true;
if (!venue) {
dataPass = false;
alert('Please enter new venue to update!');
}
if (dataPass) {
const formObj = {
venue
};
const params = new URLSearchParams(window.location.search);
const eventId = params.get('event-id');
const res = await fetch(`/events/detail/venue/${eventId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formObj)
});
const eventsResult = await res.json();
if (eventsResult.status) {
alert('Venue successfully updated!');
const myModal = bootstrap.Modal.getInstance(document.getElementById('venue-modal'));
myModal.hide();
loadEventDetails();
} else {
alert('Unable to update.');
}
}
});
// Venue edit-poll toggle
document.querySelector('#edit-venue-switch').addEventListener('change', () => {
document.querySelector('#venue-modal .edit-input').classList.toggle('hide');
document.querySelector('#venue-modal .poll-input').classList.toggle('hide');
});
document.querySelector('#poll-venue-switch').addEventListener('change', () => {
document.querySelector('#venue-modal .edit-input').classList.toggle('hide');
document.querySelector('#venue-modal .poll-input').classList.toggle('hide');
});
// Venue polling add option button
document.querySelector('#venue-add-option').addEventListener('click', (e) => {
e.preventDefault();
const numberOfOptions = document.querySelectorAll('div[class^="venue_poll_"]').length;
let newDiv = document.createElement('div');
newDiv.classList = `venue_poll_${numberOfOptions + 1}`;
newDiv.innerHTML = `
<label for="venue_poll">Option ${numberOfOptions + 1}: </label>
<input type="text" class="form-control" name="venue_poll" aria-label="venue_poll"
aria-describedby="basic-addon1" />
`;
document.querySelector('.venue-poll-options-container').appendChild(newDiv);
});
// Venue polling remove option button
document.querySelector('#venue-remove-option').addEventListener('click', (e) => {
e.preventDefault();
const venuePollOptionsDivList = document.querySelectorAll('div[class^="venue_poll_"]');
const numberOfOptions = venuePollOptionsDivList.length;
if (numberOfOptions > 2) {
venuePollOptionsDivList[numberOfOptions - 1].remove();
}
});
// Submit venue polling
document.querySelector('#venue-poll-form').addEventListener('submit', async (e) => {
e.preventDefault();
const params = new URLSearchParams(window.location.search);
const eventId = params.get('event-id');
let dataPass = true;
let formList = [];
const form = e.target;
const formInputNodeList = form.venue_poll;
formInputNodeList.forEach((each) => {
if (!!each.value) {
formList.push(each.value);
}
});
if (formList.length < 2) {
dataPass = false;
alert('Please enter at least 2 options!');
}
if (dataPass) {
const res = await fetch(`/events/poll/venue/${eventId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formList)
});
const result = await res.json();
if (result.status) {
alert('Successfully created a venue poll!');
window.location.href = `/poll/venuePoll.html?event-id=${eventId}&is-creator=1`;
} else {
if (result.created) {
alert('Poll has been created before!');
const venuePollModal = bootstrap.Modal.getInstance(document.getElementById('venue-modal'));
venuePollModal.hide();
const venuePollOverwriteModal = new bootstrap.Modal(
document.getElementById('overwrite-venue-poll-modal')
);
venuePollOverwriteModal.show();
} else {
alert('Unable to create poll.');
}
}
}
});
// Overwrite venue poll confirmed
document.querySelector('#overwrite-venue-poll-submit').addEventListener('click', async (e) => {
e.preventDefault();
const params = new URLSearchParams(window.location.search);
const eventId = params.get('event-id');
let dataPass = true;
let formList = [];
const form = document.querySelector('#venue-poll-form');
const formInputNodeList = form.venue_poll;
formInputNodeList.forEach((each) => {
if (!!each.value) {
formList.push(each.value);
}
});
if (formList.length < 2) {
formList = false;
alert('Please enter at least 2 options!');
}
if (dataPass) {
const res = await fetch(`/events/poll/venue/replacement/${eventId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formList)
});
const result = await res.json();
if (result.status) {
alert('Successfully created a venue poll!');
window.location.href = `/poll/venuePoll.html?event-id=${eventId}&is-creator=1`;
} else {
alert('Unable to create poll.');
}
}
});
// Submit participants form
document.querySelector('#participants-submit').addEventListener('click', async () => {
const params = new URLSearchParams(window.location.search);
const eventId = parseInt(params.get('event-id'));
const res = await fetch(`/events/participants/${eventId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(deletedParticipantsList)
});
if (res.status !== 200) {
const data = await res.json();
alert(data.msg);
return;
}
const result = await res.json();
if (result.status) {
if (result.notDeletable.length) {
let warnText = 'Unable to remove following participant(s):';
for (let each of result.notDeletable) {
warnText += `
# ${each.deletedParticipant.id} ${each.deletedParticipant.first_name} ${each.deletedParticipant.last_name}
Unsettled Item(s):`;
for (let i = 0; i < each.itemInCharge.length; i++) {
warnText += `
[${each.itemInCharge[i].type_name}] ${each.itemInCharge[i].name}`;
}
warnText += `
`;
}
alert(warnText);
deletedParticipantsList.splice(0, deletedParticipantsList.length);
loadEventDetails();
//Warn
} else {
deletedParticipantsList.splice(0, deletedParticipantsList.length);
loadEventDetails();
alert('Successfully deleted all selected participants!');
}
} else {
alert('Unable to delete selected participants!');
}
});
// Reset participants form
document.querySelector('#participants-reset').addEventListener('click', async () => {
deletedParticipantsList.splice(0, deletedParticipantsList.length);
loadEventDetails();
});
// Submit Invitation Copy Link Button
document.querySelector('#invitation-form').addEventListener('submit', async function (e) {
e.preventDefault();
const params = new URLSearchParams(window.location.search);
const eventId = params.get('event-id');
const res = await fetch(`/events/detail/invitation/${eventId}`);
const invitationResult = await res.json();
if (invitationResult.status) {
pasteInvitationLink(eventId, invitationResult.invitation_token);
alert('Link renewed!');
} else {
alert('Unable to create link.');
}
});
// Copy Invitation Link Button
document.querySelector('#invitation-link').addEventListener('click', (e) => {
const linkTextDiv = document.querySelector('#invitation-modal .form-control');
// Select the text field
linkTextDiv.select();
linkTextDiv.setSelectionRange(0, 99999); // For mobile devices
// Copy the text inside the text field
navigator.clipboard.writeText(linkTextDiv.value);
// Change button to copied
e.target.classList.add('copied');
const currentWidth = e.target.offsetWidth;
e.target.style.width = `${currentWidth}px`;
e.target.innerHTML = 'Copied!';
// Change back the button to normal
setTimeout(() => {
e.target.classList.remove('copied');
e.target.innerHTML = 'Copy Link';
}, 5000);
});
// Delete Event Button
document.querySelector('#delete-event-submit').addEventListener('click', async () => {
const params = new URLSearchParams(window.location.search);
const eventId = params.get('event-id');
const res = await fetch(`/events/${eventId}`, {
method: 'DELETE'
});
const result = await res.json();
if (result.status) {
alert('Event deleted!');
window.location.href = '/index.html';
} else {
alert('Unable to delete event!');
}
});

View File

@@ -0,0 +1,307 @@
export async function getEventSchedule() {
const params = new URLSearchParams(window.location.search);
const eventId = params.get('event-id');
const isCreator = params.get('is-creator');
const res = await fetch(`/events/?event-id=${eventId}&is-creator=${isCreator}`);
if (res.status !== 200) {
const data = await res.json();
alert(data.msg);
return;
}
const result = await res.json();
if (result.status) {
const option = {
hour12: false,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
};
const startDateTime = new Date(result.detail.start_datetime)
.toLocaleString('en-US', option)
.replace(', ', ' ')
.slice(0, -3);
const endDateTime = new Date(result.detail.end_datetime)
.toLocaleString('en-US', option)
.replace(', ', ' ')
.slice(0, -3);
const activitiesArr = result.activities;
const startTime = startDateTime.slice(-5);
const endTime = endDateTime.slice(-5);
const startDate = startDateTime.slice(0, 10);
const endDate = endDateTime.slice(0, 10);
const startYear = startDateTime.slice(6, 10);
const endYear = endDateTime.slice(6, 10);
const startMonth = startDateTime.slice(0, 2);
const endMonth = endDateTime.slice(0, 2);
const startDay = startDateTime.slice(3, 5);
const endDay = endDateTime.slice(3, 5);
const date = `${startYear}${startMonth}${startDay}`;
let startTimeInMin = toMin(startTime);
let endTimeInMin = toMin(endTime);
const dayDifference = (new Date(endDate).getTime() - new Date(startDate).getTime()) / 1000 / 60 / 60 / 24;
if (
dayDifference > 0 &&
date !== `${startYear}${startMonth}${startDay}` &&
date !== `${endYear}${endMonth}${endDay}`
) {
startTimeInMin = 0;
endTimeInMin = 1440;
}
if (dayDifference > 0 && date === `${startYear}${startMonth}${startDay}`) {
endTimeInMin = 1440;
}
if (dayDifference > 0 && date === `${endYear}${endMonth}${endDay}`) {
startTimeInMin = 0;
}
await getPresetTimeBlock(startTimeInMin);
await getSavedTimeBlocks(activitiesArr);
await correctDiv(startTimeInMin, endTimeInMin);
setGlobalHeight(2);
}
}
async function getPresetTimeBlock(startTime) {
let rundown = document.querySelector('#rundown');
//generate time block for 24 hours
for (let i = 0; i < 96; i++) {
let start = i * 15;
let end = (i + 1) * 15;
const timeString = minToTimeString(start);
const height = end - start;
rundown.innerHTML += `
<div id="time-block-container-${start}" start="${start}" end="${end}" class="individual-time-block row">
<span id="time-stamp-box" class="time-stamp-container col-sm-2">
<div id="stamp-${start}" class="time-stamp">${timeString}</div>
</span>
<span id="time-block-${start}" start="${start}" end="${end}" class="time-block col-sm-10"></span>
</div>
`;
document.querySelector(`#time-block-${start}`).style.height = `${height}px`;
}
//set scroll bar top
document.querySelector(`#time-block-${startTime}`).innerHTML = 'Event Start Time';
const scrollBarDiv = document.querySelector('#rundown-container');
scrollBarDiv.scrollTop = document.querySelector(`#time-block-${startTime}`).offsetTop;
}
async function getSavedTimeBlocks(activitiesArr) {
activitiesArr.forEach(async (activity) => {
const start = activity.start_time;
const title = activity.title;
const startTimeInMin = toMin(activity.start_time);
const endTimeInMin = toMin(activity.end_time);
const divHeight = endTimeInMin - startTimeInMin;
const id = activity.id;
const presetColor = '#f29659';
let color = activity.color;
if (activity.color === null || undefined) {
color = presetColor;
}
document.querySelector(`#time-block-container-${startTimeInMin}`).innerHTML = `
<span id="time-stamp-box" class="time-stamp-container col-2">
<div id="stamp-${startTimeInMin}" class="time-stamp">${start}</div>
</span>
<span value="${id}" type="button" id="time-block-${startTimeInMin}" start="${startTimeInMin}" end="${endTimeInMin}" class="time-block save-time-block col-10">
</span>
`;
document.querySelector(`#time-block-${startTimeInMin}`).innerHTML = title;
document.querySelector(`#time-block-${startTimeInMin}`).style.height = `${divHeight}px`;
document.querySelector(`#time-block-${startTimeInMin}`).style.backgroundColor = `${color}`;
});
}
async function fixDivHeight(x) {
if (x > 0) {
const divCluster = document.querySelectorAll('.time-block');
divCluster.forEach((div) => {
let nextDiv;
if (div.parentElement.nextElementSibling?.childNodes) {
nextDiv = div.parentElement.nextElementSibling.childNodes[3];
}
const height = parseInt(div.getAttribute('end')) - parseInt(div.getAttribute('start'));
if (!!div.parentElement.nextElementSibling?.childNodes) {
if (nextDiv) {
if (div.classList === nextDiv.classList && !div.classList.contains('save-time-block')) {
div.style.height = newHeight;
const newHeight = parseInt(nextDiv.getAttribute('end')) - parseInt(div.getAttribute('start'));
div.setAttribute(`start`, `${nextDiv.getAttribute('end')}`);
nextDiv.parentElement.innerHTML = '';
} else if (height > 60 && !div.classList.contains('save-time-block')) {
div.style.height = '60px';
const redundantHeight = height - 60;
nextDiv.setAttribute(`start`, `${parseInt(nextDiv.getAttribute('start')) + redundantHeight}`);
} else {
div.style.height = `${height}px`;
}
}
}
});
fixDivHeight(x - 1);
} else {
return;
}
}
function toMin(timeInput) {
const hourInMin = parseInt(timeInput.slice(0, 2)) * 60;
const min = parseInt(timeInput.slice(3, 5));
return hourInMin + min;
}
async function setGlobalHeight(input) {
const allBlocks = document.querySelectorAll('.time-block');
allBlocks.forEach((block) => {
const originalHeight = parseInt(block.style.height.slice(0, -2));
block.style.height = `${originalHeight * input}px`;
});
}
async function fixTimeStamp() {
const timeStampDiv = document.querySelectorAll('.time-stamp');
timeStampDiv.forEach((stamp) => {
let nextTimeBlock;
let placeholder = stamp.parentElement.nextElementSibling;
while (placeholder) {
if (placeholder.classList.contains('time-block')) {
nextTimeBlock = placeholder;
break;
}
placeholder = placeholder.nextElementSibling;
}
const time = minToTimeString(parseInt(nextTimeBlock.getAttribute('start')));
stamp.innerHTML = time;
});
}
async function deleteRedundantDiv(x) {
const divCluster = document.querySelectorAll(`.time-block`);
if (x > 0) {
for (let i = 0; i < divCluster.length; i++) {
if (!!divCluster[i + 1]) {
const endTime = parseInt(divCluster[i].getAttribute('end'));
const nextStartTime = parseInt(divCluster[i + 1].getAttribute('start'));
if (endTime > nextStartTime) {
divCluster[i + 1].parentElement.remove();
} else if (endTime < nextStartTime) {
divCluster[i + 1].setAttribute(`start`, `${endTime}`);
}
}
}
deleteRedundantDiv(x - 1);
}
return;
}
async function correctDiv(eventStartTimeInMin, eventEndTimeInMin) {
const divCluster = document.querySelectorAll(`.time-block`);
const params = new URLSearchParams(window.location.search);
const isCreator = params.get('is-creator');
for (let i = 0; i < divCluster.length; i++) {
const startTime = parseInt(divCluster[i].getAttribute('start'));
const endTime = parseInt(divCluster[i].getAttribute('end'));
const height = endTime - startTime;
const timeString = minToTimeString(startTime);
if (!!divCluster[i + 1]) {
divCluster[i].style.height = `${height}px`;
const nextStartTime = parseInt(divCluster[i + 1].getAttribute('start'));
const nextEndTime = parseInt(divCluster[i + 1].getAttribute('end'));
const newDivHeight = nextStartTime - endTime;
const nextStartTimeFormat = minToTimeString(nextStartTime);
if (endTime < nextStartTime && startTime >= eventStartTimeInMin && startTime < eventEndTimeInMin) {
divCluster[i].parentNode.insertAdjacentHTML(
'afterend',
`
<div id="time-block-container-${endTime}" class="individual-time-block row">
<span id="time-stamp-box" class="time-stamp-container col-2">
<div id="stamp-${endTime}" class="time-stamp">${nextStartTimeFormat}</div>
</span>
<span type="button" id="time-block-${endTime}" start="${endTime}" end="${nextStartTime}" class="time-block event-schedule col-10" data-bs-toggle="modal" data-bs-target="#create-time-block-modal"></span>
</div>
`
);
document.querySelector(`#time-block-${endTime}`).style.height = `${newDivHeight}px`;
} else if (endTime < nextStartTime) {
divCluster[i].parentNode.insertAdjacentHTML(
'afterend',
`
<div id="time-block-container-${endTime}" class="individual-time-block row">
<span id="time-stamp-box" class="time-stamp-container col-2">
<div id="stamp-${endTime}" class="time-stamp">${nextStartTimeFormat}</div>
</span>
<span id="time-block-${endTime}" start="${endTime}" end="${nextStartTime}" class="time-block col-10"></span>
</div>
`
);
document.querySelector(`#time-block-${endTime}`).style.height = `${newDivHeight}px`;
}
document.querySelector(`#stamp-${startTime}`).innerHTML = timeString;
divCluster[i].style.height = `${height}`;
}
if (
startTime >= eventStartTimeInMin &&
startTime < eventEndTimeInMin &&
!divCluster[i].classList.contains('save-time-block')
) {
divCluster[i].classList.add('event-schedule');
if (isCreator === '1') {
divCluster[i].setAttribute(`data-bs-target`, `#create-time-block-modal`);
divCluster[i].setAttribute(`type`, 'button');
divCluster[i].setAttribute(`data-bs-toggle`, `modal`);
}
}
}
deleteRedundantDiv(100);
fixTimeStamp();
fixDivHeight(10);
}
function minToTimeString(timeInMin) {
if (timeInMin < 10) {
return `00:0${timeInMin}`;
} else if (timeInMin < 60) {
return `00:${timeInMin}`;
} else if (Math.floor(timeInMin / 60) < 10 && timeInMin % 60 < 10) {
const hour = Math.floor(timeInMin / 60);
const min = timeInMin % 60;
return `0${hour}:0${min}`;
} else if (Math.floor(timeInMin / 60) >= 10 && timeInMin % 60 < 10) {
const hour = Math.floor(timeInMin / 60);
const min = timeInMin % 60;
return `${hour}:0${min}`;
} else if (Math.floor(timeInMin / 60) >= 10 && timeInMin % 60 >= 10) {
const hour = Math.floor(timeInMin / 60);
const min = timeInMin % 60;
return `${hour}:${min}`;
} else if (Math.floor(timeInMin / 60) < 10 && timeInMin % 60 >= 10) {
const hour = Math.floor(timeInMin / 60);
const min = timeInMin % 60;
return `0${hour}:${min}`;
}
}

View File

@@ -0,0 +1,410 @@
body {
position: relative;
margin-left: 2px;
margin-right: 20px;
font-family: 'Kalam';
font-size: 16px;
}
.modal-body {
height: fit-content;
}
.input-group {
display: flex;
justify-content: center;
}
.input-panel {
margin: 0px;
}
.item-list-box {
min-height: 150px;
min-width: 100px;
}
#item-form-text {
text-align: center;
}
header {
display: flex;
justify-content: center;
}
#item-form-header {
text-align: center;
}
.item-list-box {
border-radius: 10px;
background-color: #e8e892;
height: 100%;
padding-top: 10px;
padding-bottom: 10px;
overflow: auto;
}
.modal-content {
align-items: center;
width: fit-content;
}
#date-selector-container {
display: flex;
justify-content: right;
padding-bottom: 10px;
padding-right: 10px;
}
textarea {
border-radius: 5px;
background: none;
border-color: #3f3f3f;
max-height: 100%;
overflow: auto;
}
header {
text-align: center;
}
#time-block-memo-container {
position: relative;
max-height: 100%;
}
#close-memo {
position: absolute;
top: 0px;
font-size: 10px;
right: 10px;
}
#time-block-page-container {
max-height: 800px;
height: 550px;
padding-top: 0px;
padding-bottom: 30px;
}
#event-time-container {
text-align: center;
}
.event-schedule {
background-color: #efefd0 !important;
}
.event-schedule:hover {
background-color: #e8e892 !important;
}
@media (max-width: 1000px) {
#line {
display: none;
}
}
@media (max-width: 576px) {
@media (max-width: 1000px) {
#line {
display: none;
}
#memo {
font-size: large;
max-height: 100%;
max-width: 100%;
overflow: auto;
}
#time-block-memo-container {
max-height: 300px;
height: 300px;
overflow: auto;
}
#rundown-container {
max-height: 300px;
}
}
}
#memo {
background-color: #efefd0;
height: 90%;
border-radius: 10px;
overflow: auto;
}
#page {
display: flex;
justify-content: center;
height: 90%;
width: 100%;
}
/* #line {
position: absolute;
} */
textarea {
width: 100%;
}
.create-button {
position: fixed;
left: 30px;
bottom: 30px;
}
#rundown {
max-height: 80%;
max-width: 100%;
}
#rundown-container {
max-height: 100%;
position: relative;
background-color: white;
padding-top: 30px;
padding-bottom: 20px;
}
.time-stamp-container {
background-color: none;
position: relative;
display: flex;
justify-content: center;
align-items: center;
}
.individual-time-block {
height: 100%;
padding: 2px;
max-width: 100%;
overflow: visible;
}
.time-block {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
background-color: rgba(181, 180, 180, 0.625);
padding: 10px;
border-radius: 5px;
padding: 1px;
font-size: 1.5vw;
text-align: center;
transition: background-color 50ms ease-out 50ms;
}
.save-time-block {
height: 100%;
color: white;
opacity: 0.7;
padding: 10px;
border-radius: 5px;
text-align: center;
transition: background-color 50ms ease-out 50ms;
}
.save-time-block:hover {
opacity: 0.8;
transform: scale(1.01);
}
.time-stamp {
position: absolute;
top: -5px;
right: 10px;
font-size: 10px;
}
.last-time-stamp {
position: absolute;
bottom: -5px;
right: 10px;
font-size: 10px;
}
.memo-item-container {
position: relative;
background-color: rgba(255, 255, 255, 0.522);
border-radius: 10px;
width: 100%;
height: 100%;
margin-bottom: 10px;
padding: 10px;
font-size: 15px;
}
#memo-item-cluster {
display: flex;
flex-direction: column;
justify-content: space-between;
height: 100%;
padding: 10px;
}
.memo-item-label {
font-weight: 800;
font-size: 15px;
padding: 5px;
}
/* Scroll bar */
::-webkit-scrollbar {
width: 15px;
}
::-webkit-scrollbar-track {
box-shadow: inset 0 0 5px grey;
border-radius: 5px;
height: 200px;
}
::-webkit-scrollbar-thumb {
background: #efefd0;
border-radius: 5px;
min-height: 250px;
}
::-webkit-scrollbar-thumb:hover {
background: white;
}
#memo,
#memo-tag {
animation-duration: 0.5s;
animation-name: animate-fade;
animation-delay: 0.1s;
animation-fill-mode: backwards;
}
@keyframes animate-fade {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
#edit-activities {
border-radius: 100px;
background-color: #f2965985;
color: white;
text-align: center;
min-width: 22px !important;
min-height: 22px !important;
padding: 3px;
font-size: 10px;
}
#edit-remarks {
border-radius: 100px;
background-color: #f2965985;
color: white;
text-align: center;
min-width: 22px !important;
min-height: 22px !important;
padding: 3px;
font-size: 10px;
}
#edit-show-item {
border-radius: 100px;
background-color: #f2965985;
color: white;
text-align: center;
min-width: 22px !important;
min-height: 22px !important;
padding: 3px;
font-size: 10px;
}
.edit-button {
position: absolute;
top: 5px;
right: 5px;
}
.modal-dialog {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
#event-name {
font-size: 2vw;
padding-top: 10px;
}
.fa-trash {
z-index: 1000;
right: 20px;
top: 20px;
}
.creator-function {
z-index: 1000;
}
/* CSS */
.button-53 {
background-color: #f29659;
border: 0 solid #e5e7eb;
box-sizing: border-box;
color: #000000;
display: flex;
font-family: ui-sans-serif, system-ui, -apple-system, system-ui, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
font-size: 1rem;
font-weight: 700;
justify-content: center;
line-height: 1.75rem;
padding: 0.75rem 1.65rem;
position: relative;
text-align: center;
text-decoration: none #000000 solid;
text-decoration-thickness: auto;
width: 100%;
max-width: 460px;
position: relative;
cursor: pointer;
transform: rotate(-2deg);
user-select: none;
-webkit-user-select: none;
touch-action: manipulation;
}
.button-53:focus {
outline: 0;
}
.button-53:after {
content: '';
position: absolute;
border: 1px solid #000000;
bottom: 4px;
left: 4px;
width: calc(100% - 1px);
height: calc(100% - 1px);
}
.button-53:hover:after {
bottom: 2px;
left: 2px;
}
@media (min-width: 768px) {
.button-53 {
padding: 0.75rem 3rem;
font-size: 1.25rem;
}
}

View File

@@ -0,0 +1,47 @@
export async function fetchPendingItems(selectType) {
const params = new URLSearchParams(window.location.search);
const eventId = params.get('event-id');
const resShopList = await (await fetch(`/items/pendingItems?eventID=${eventId}`)).json();
if (resShopList.status) {
let listItems = '';
for (const items of resShopList.itemObj[selectType]) {
listItems += `
<tr id="list-item-${items.id}">
<td>
<div class="pending-item">
${items.name}
<button id="checking-${items.id}" class="check-btn">
<i class="bi bi-check-circle"></i>
</button>
</div>
</td>
</tr>
`;
}
document.querySelector(`#shopping-list-update`).innerHTML = listItems;
checkShoppingListItem();
document.querySelectorAll(`.dropdown-item`).forEach((dropdown) => {
dropdown.addEventListener('click', function (e) {
const selectType = e.currentTarget.innerHTML.toLowerCase();
fetchPendingItems(selectType);
});
});
}
}
function checkShoppingListItem() {
document.querySelectorAll(`.check-btn`).forEach((button) => {
button.addEventListener('click', async function (e) {
const itemID = e.currentTarget.id.slice(9);
const res = await fetch(`/items/pendingItems/${itemID}`, {
method: 'PUT'
});
if ((await res.json()).status === true) {
const updateOnTheList = document.querySelector('#list-item-' + itemID);
updateOnTheList.remove();
}
});
});
}

View File

@@ -0,0 +1,188 @@
body {
font-family: 'Lato', sans-serif;
background-color: #ffffff;
}
.initial-hide {
display: none;
}
.table-header {
background-color: white;
}
#add-new-party-button {
width: 500px;
background-color: #f2d492;
border-radius: 10px;
font-size: 20px;
font-weight: bold;
border: none;
display: flex;
justify-content: space-between;
align-items: center;
padding: 5px 15px;
}
.round {
border-radius: 50%;
background-color: #f1f1f1;
color: grey;
}
#add-new-party-button {
width: 500px;
background-color: #f2d492;
}
#add-new-party-button .text {
color: black;
border-radius: 10px;
text-align: left;
}
#add-new-party-button .plus {
display: flex;
justify-content: center;
align-items: center;
}
#add-new-party-button .plus div {
background-color: #f29559;
border-radius: 100px;
width: 20px;
height: 20px;
display: flex;
justify-content: center;
align-items: center;
font-size: 15px;
font-weight: unset;
}
th {
text-align: center;
height: 30px;
}
th > div {
height: 100%;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.container {
padding: 0 0 20px 0;
}
.table-content-row {
font-size: 1vw;
border-bottom: grey 1px solid;
}
.turn-page-button-container {
display: flex;
justify-content: flex-end;
align-items: center;
}
.previous-round,
.next-round {
margin: 0;
padding: 10px 15px;
width: 40px;
height: 25px;
font-size: 10px;
border: rgb(197, 197, 197) 1px solid;
display: flex;
justify-content: center;
align-items: center;
}
.previous-round {
border-radius: 10px 0 0 10px;
}
.next-round {
border-radius: 0 10px 10px 0;
}
.page-number {
margin: 10px;
font-size: 8px;
color: grey;
}
.parties-list-container header {
margin: 10px 0 0 0;
font-size: 20px;
font-weight: bold;
}
.parties-list-container .table-container {
background-color: #fdfdf4;
padding: 20px;
border-radius: 20px;
}
.parties-list-container .table-header {
background-color: white;
border: rgba(191, 191, 191, 0.473) solid 1px;
font-size: 1vw;
}
.deletedStatus {
background-color: #979797;
color: white;
}
.completedStatus {
background-color: #caefe2;
color: #00b69b;
}
.progressStatus {
background-color: #ded2f3;
color: #6226ef;
}
.deletedStatus,
.completedStatus,
.progressStatus {
display: flex;
justify-content: center;
align-items: center;
padding: 0 15px;
height: 25px;
border-radius: 5px;
}
.edit-button {
border-radius: 100px;
background-color: #f29559;
color: white;
width: 22px;
height: 22px;
font-size: 10px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
}
@media only screen and (max-width: 600px) {
#add-new-party-button {
width: 90%;
}
.hidable-1 {
display: none;
}
}
@media only screen and (max-width: 420px) {
.hidable-2 {
display: none;
}
}

View File

@@ -0,0 +1,99 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.1/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-iYQeCzEYFbKjA/T2uDLTpkwGzCiq6soy8tYaI1GyVh/UjpbCx/TYkiZhlZB6+fzT"
crossorigin="anonymous"
/>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.9.1/font/bootstrap-icons.css" />
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.0/css/all.min.css"
integrity="sha512-xh6O/CkQoPOWDdYTDqeRdPCVd1SpvCA9XXcUnZS2FmJNp1coAFzvtCN9BmamE+4aHK8yyUHUSCcJHgXloTyT2A=="
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>
<link rel="stylesheet" href="./index.css" />
<title>Dashboard</title>
</head>
<body style="display: none">
<div class="navbar-container container">
<!-- NAVBAR: to be loaded with js -->
</div>
<div class="container">
<a href="/newEvent/newEvent.html" class="btn btn-primary" id="add-new-party-button">
<div class="text">Add New Party</div>
<div class="plus"><div>+</div></div>
</a>
<div class="parties-list-container">
<header>Created:</header>
<div class="create table-container">
<table class="table table-body table-hover">
<thead>
<tr class="table-header">
<th scope="col" class="hidable-2">ID</th>
<th scope="col">PARTY NAME</th>
<th scope="col">ADDRESS</th>
<th scope="col">START TIME</th>
<th scope="col" class="hidable-1">END TIME</th>
<th scope="col">STATUS</th>
<th scope="col"></th>
</tr>
</thead>
<tbody class="events-container">
<!-- to be added by loadEvent.js -->
</tbody>
</table>
<span class="turn-page-button-container">
<button type="button" class="previous-round btn btn-light">
<i class="fa-sharp fa-solid fa-less-than"></i>
</button>
<button type="button" class="next-round btn btn-light">
<i class="fa-sharp fa-solid fa-greater-than"></i>
</button>
</span>
</div>
<header>Participated:</header>
<div class="participate table-container">
<table class="table table-body table-hover">
<thead>
<tr class="table-header">
<th scope="col">ID</th>
<th scope="col">PARTY NAME</th>
<th scope="col">ADDRESS</th>
<th scope="col">START TIME</th>
<th scope="col">END TIME</th>
<th scope="col">STATUS</th>
<th scope="col"></th>
</tr>
</thead>
<tbody class="events-container">
<!-- to be added by loadEvent.js -->
</tbody>
</table>
<span class="turn-page-button-container">
<button type="button" class="previous-round btn btn-light">
<i class="fa-sharp fa-solid fa-less-than"></i>
</button>
<button type="button" class="next-round btn btn-light">
<i class="fa-sharp fa-solid fa-greater-than"></i>
</button>
</span>
</div>
</div>
</div>
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.1/dist/js/bootstrap.bundle.min.js"
integrity="sha384-u1OknCvxWvY5kfmNBILK2hRnQC3Pr17a+RTT6rIHI7NnikvbZlHgTPOOmMi466C8"
crossorigin="anonymous"
></script>
<script type="module" src="./index.js"></script>
</body>
</html>

View File

@@ -0,0 +1,47 @@
import { loadCreateEvents, loadParticipateEvents } from './loadEvent.js';
import { addNavbar } from '/functions/addNavbar.js';
import { loadName } from '/functions/loadName.js';
function onlyNumbers(str) {
return /^[0-9]+$/.test(str);
}
window.addEventListener('load', async () => {
addNavbar();
loadName();
const params = new URLSearchParams(window.location.search);
let createPage = '1';
let participatePage = '1';
if (!params.has('create-page')) {
loadCreateEvents(createPage);
} else {
if (onlyNumbers(params.get('create-page'))) {
if (parseInt(params.get('create-page')) >= 1) {
createPage = params.get('create-page');
createPage = await loadCreateEvents(createPage);
} else {
loadCreateEvents(createPage);
}
} else {
loadCreateEvents(createPage);
}
}
if (!params.has('participate-page')) {
loadParticipateEvents(participatePage);
} else {
if (onlyNumbers(params.get('participate-page'))) {
if (parseInt(params.get('participate-page')) >= 1) {
participatePage = params.get('participate-page');
participatePage = await loadParticipateEvents(participatePage);
} else {
loadParticipateEvents(participatePage);
}
} else {
loadParticipateEvents(participatePage);
}
}
document.body.style.display = 'block';
});

View File

@@ -0,0 +1,163 @@
.bi-chevron-left {
font-size: 30px;
position: fixed;
top: 90px;
left: 40px;
color: #444a58;
cursor: pointer;
}
.category-text {
font-size: 20px;
font-family: 'Lato', sans-serif;
margin-bottom: 10px;
}
/* shopping list */
.shopping-list {
background-color: #fefde2;
height: 700px;
border-radius: 20px;
margin: 10px 2px 5px 5px;
width: 100%;
padding: 20px;
}
.shopping-list .item-list {
height: 700px;
width: 100%;
background-color: #fff;
}
.pending-item-header {
display: flex;
justify-content: space-between;
}
.bi-filter-circle {
color: #495871;
width: 20px;
height: 20px;
}
.pending-item {
display: flex;
justify-content: space-between;
}
.check-btn {
border: none;
border-radius: 20px;
background-color: #fff;
}
.bi-check-circle {
color: #495871;
width: 20px;
height: 20px;
}
.shorting-btn {
border: none;
border-radius: 20px;
background-color: #fff;
}
/* item category */
.category-text-items {
font-size: 20px;
font-family: 'Lato', sans-serif;
display: flex;
flex-direction: row;
justify-content: space-between;
width: 100%;
margin-bottom: 10px;
}
.category-edit {
border-radius: 20px;
border: none;
background-color: #f29559;
color: #fff;
}
.bi-pencil-square:hover {
color: #495871;
}
.item-category {
background-color: #fefde2;
height: 338px;
border-radius: 20px;
margin: 15px;
width: 100%;
padding: 20px;
}
.item-category .item-list {
height: 70%;
width: 100%;
background-color: #fff;
}
/* Modal UI */
#editModalTittle {
font-size: 30px;
font-family: 'Lato', sans-serif;
}
.modal-header {
display: flex;
justify-content: space-between;
}
/* modal - table */
.border {
border: 10px solid #9bafd0;
border-radius: 20px;
padding: 10px;
max-height: 500px;
overflow-y: scroll;
}
tr {
font-family: 'Lato', sans-serif;
color: #293241;
}
.bi-trash {
color: #ee6c4d;
}
.delete-btn {
border-radius: 20px;
border: none;
background-color: #fff;
}
/* modal - footer */
.format {
justify-content: space-between;
font-family: 'Lato', sans-serif;
color: #293241;
}
.save-submit {
font-size: 18px;
color: #293241;
font-family: 'Lato', sans-serif;
background-color: #f2d492;
padding: 4px 14px 4px 14px;
border-radius: 20px;
margin: 8px;
width: 100px;
box-sizing: border-box;
border: none;
outline: solid 3px #f2d492;
transition: outline 0.3s linear;
margin: 0.5em;
}
.save-submit:hover {
outline-width: 5px;
}

View File

@@ -0,0 +1,259 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.1/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-iYQeCzEYFbKjA/T2uDLTpkwGzCiq6soy8tYaI1GyVh/UjpbCx/TYkiZhlZB6+fzT"
crossorigin="anonymous"
/>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.9.1/font/bootstrap-icons.css" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Lato:wght@300;400&family=Pacifico&display=swap"
rel="stylesheet"
/>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.0/css/all.min.css"
integrity="sha512-xh6O/CkQoPOWDdYTDqeRdPCVd1SpvCA9XXcUnZS2FmJNp1coAFzvtCN9BmamE+4aHK8yyUHUSCcJHgXloTyT2A=="
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>
<link rel="stylesheet" href="./itemPost.css" />
<title>Post Event Items</title>
</head>
<body style="display: none">
<div class="navbar-container container">
<!-- NAVBAR: to be loaded with js -->
</div>
<div class="container">
<a id="back-page"><i class="bi bi-chevron-left"></i></a>
<div class="row">
<div class="col-xl-4">
<div class="shopping-list">
<div class="category-text">Shopping list:</div>
<div class="item-list border">
<table class="table">
<thead>
<tr>
<th scope="col" class="pending-item-header">
Pending Items:
<div>
<button
id="shopping-list-shorting"
class="shorting-btn"
type="button"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<i class="bi bi-filter-circle"></i>
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item">Food</a></li>
<li><a class="dropdown-item">Drink</a></li>
<li><a class="dropdown-item">Decoration</a></li>
<li><a class="dropdown-item">Other</a></li>
</ul>
</div>
</th>
</tr>
</thead>
<tbody id="shipping-list-update">
<!--loaded with itemPost.js "shopping list JS"-->
</tbody>
</table>
</div>
</div>
</div>
<!-- item category -->
<div class="col-xl-4">
<div class="item-category">
<div class="item-category-title">
<div class="category-text-items">
Food:
<button
type="button"
class="category-edit"
data-bs-toggle="modal"
data-bs-target="#editModal"
itemtype="food"
>
<i class="bi bi-pencil-square"></i>
</button>
</div>
</div>
<div class="item-list border">
<table id="food-table" class="table">
<tbody>
<!-- loaded with itemPost.js "category items JS"-->
</tbody>
</table>
</div>
</div>
<div class="item-category">
<div class="category-text-items">
Decoration:
<button
type="button"
class="category-edit"
data-bs-toggle="modal"
data-bs-target="#editModal"
itemtype="decoration"
>
<i class="bi bi-pencil-square"></i>
</button>
</div>
<div class="item-list border">
<table id="decoration-table" class="table">
<tbody>
<tr>
<!-- loaded with itemPost.js "category items JS" -->
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="col-xl-4">
<div class="item-category">
<div class="category-text-items">
Drink:
<button
type="button"
class="category-edit"
data-bs-toggle="modal"
data-bs-target="#editModal"
itemtype="drink"
>
<i class="bi bi-pencil-square"></i>
</button>
</div>
<div class="item-list border">
<table id="drink-table" class="table">
<tbody>
<tr>
<!-- loaded with itemPost.js "category items JS"-->
</tr>
</tbody>
</table>
</div>
</div>
<div class="item-category">
<div class="category-text-items">
Others:
<button
type="button"
class="category-edit"
data-bs-toggle="modal"
data-bs-target="#editModal"
itemtype="others"
>
<i class="bi bi-pencil-square"></i>
</button>
</div>
<div class="item-list border">
<table id="other-table" class="table">
<tbody>
<tr>
<!-- loaded with itemPost.js "category items JS"-->
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- modal UI -->
<div class="modal fade" id="editModal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<div></div>
<h1 class="modal-title fs-5" id="editModalTittle">Edit Item</h1>
<div class="exit-modal">
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
</div>
<div class="modal-body">
<div class="border">
<table class="table">
<thead class="table-light">
<tr>
<th scope="col">Item description</th>
<th scope="col">Quantity</th>
<th scope="col">Price $</th>
<th scope="col"><i class="bi bi-person-fill"></i></th>
<th scope="col"></th>
</tr>
</thead>
<tbody id="edit-item-list">
<!-- loaded with itemPost.js "category model list JS" -->
</tbody>
</table>
</div>
</div>
<!-- footer modal -->
<div class="modal-footer">
<div class="container-fluid">
<form id="from-container">
<div class="row format">
<div class="col-md-5">
<label for="add-item" class="form-label">Item name</label>
<input type="text" class="form-control" id="add-item" name="item_name" />
</div>
<div class="col-md-1">
<label for="add-quantity" class="form-label">Quantity</label>
<input
type="number"
class="form-control"
id="add-quantity"
name="item_quantity"
min="1"
/>
</div>
<div class="col-md-1">
<label for="add-price" class="form-label">Price</label>
<input
type="number"
class="form-control"
id="add-price"
name="item_price"
min="0"
/>
</div>
<div class="col-md-2">
<label for="add-user" class="form-label">
<i class="bi bi-person-fill"></i>
PIC
</label>
<select id="select-participant" class="form-select" name="item_user">
<option selected value="">Select</option>
<!-- loaded with itemPost.js "modal participants select"-->
</select>
</div>
<button class="save-submit">Add</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.1/dist/js/bootstrap.bundle.min.js"
integrity="sha384-u1OknCvxWvY5kfmNBILK2hRnQC3Pr17a+RTT6rIHI7NnikvbZlHgTPOOmMi466C8"
crossorigin="anonymous"
></script>
<script type="module" src="./itemPost.js"></script>
</body>
</html>

View File

@@ -0,0 +1,202 @@
import { addNavbar } from '/functions/addNavbar.js';
import { loadName } from '/functions/loadName.js';
let editingType = null;
let itemData = null;
let eventID = null;
window.addEventListener('load', async () => {
const query = new URLSearchParams(window.location.search);
eventID = query.get('event-id');
itemData = await (await fetch(`/items?eventID=${eventID}`)).json();
addNavbar();
await loadName();
fetchItem();
fetchParticipant(eventID);
fetchPendingItems('food');
document.body.style.display = 'block';
});
document.querySelectorAll('.category-edit').forEach((button) => {
button.addEventListener('click', function (e) {
editingType = button.attributes.itemType.value;
fetchEditItem(itemData);
});
});
document.querySelector('#from-container').addEventListener('submit', async function (e) {
const query = new URLSearchParams(window.location.search);
const eventID = query.get('event-id');
e.preventDefault();
const form = e.target;
const typeName = editingType;
const itemName = form.item_name.value;
const itemQuantity = form.item_quantity.value;
const itemPrice = form.item_price.value || null;
const user_id = form.item_user.value || null;
let formObj = {
typeName,
itemName,
itemQuantity,
itemPrice,
user_id
};
let dataPass = true;
if (!user_id) {
alert('Please select PIC');
return;
}
if (dataPass) {
const res = await fetch(`/items/eventId/${eventID}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formObj)
});
const eventsResult = await res.json();
if (eventsResult.status === true) {
const itemData = await (await fetch(`/items?eventID=${eventID}`)).json();
fetchEditItem(itemData);
}
}
form.reset();
fetchItem();
fetchPendingItems(typeName);
});
// category items JS
async function fetchItem() {
const res = await (await fetch(`/items?eventID=${eventID}`)).json();
if (res.status === true) {
const typeName = ['food', 'drink', 'decoration', 'other'];
for (const tableName of typeName) {
let itemsList = '';
for (const items of res.itemObj[tableName]) {
itemsList += `
<tr>
<td>${items.name}</td>
</tr>
`;
}
document.querySelector(`#${tableName}-table`).innerHTML = itemsList;
}
}
}
// category model list JS
async function fetchEditItem() {
const resEditItem = await (await fetch(`/items?eventID=${eventID}`)).json();
if (resEditItem.status === true) {
let items = '';
for (const itemsData of resEditItem.itemObj[editingType]) {
items += `
<tr id="item-row-${itemsData.id}">
<td>${itemsData.name}</td>
<td>${itemsData.quantity}</td>
<td>${itemsData.price}</td>
<td>${itemsData.first_name} ${itemsData.last_name}</td>
<td>
<button id="item-${itemsData.id}" data-type-name="${itemsData.type_name}"
itemDelete="button" class="delete-btn">
<i class="bi bi-trash"></i>
</button>
</td>
</tr>
`;
}
document.querySelector(`#edit-item-list`).innerHTML = items;
addDeleteEventListener();
}
}
function addDeleteEventListener() {
document.querySelectorAll('.delete-btn').forEach((button) => {
button.addEventListener('click', async function (e) {
const itemID = e.currentTarget.id.slice(5);
const typeName = e.currentTarget.getAttribute('data-type-name');
const res = await fetch(`/items/${itemID}`, {
method: 'DELETE'
});
if ((await res.json()).status === true) {
const deleteResult = document.querySelector('#item-row-' + itemID);
deleteResult.remove();
fetchItem();
fetchPendingItems(typeName);
}
});
});
}
// modal participants select
async function fetchParticipant(eventID) {
const resParticipant = await (await fetch(`/items/participated?eventID=${eventID}`)).json();
if (resParticipant.status === true) {
for (const participantData of resParticipant.user) {
document.querySelector(`#select-participant`).innerHTML += `
<option value="${participantData.id}">${participantData.first_name} ${participantData.last_name}
</option>
`;
}
}
}
// shopping list JS
async function fetchPendingItems(selectType) {
const resShopList = await (await fetch(`/items/pendingItems?eventID=${eventID}`)).json();
if (resShopList.status === true) {
let listItems = '';
for (const items of resShopList.itemObj[selectType]) {
listItems += `
<tr id="list-item-${items.id}">
<td>
<div class="pending-item">
${items.name}
<button id="checking-${items.id}" class="check-btn">
<i class="bi bi-check-circle"></i>
</button>
</div>
</td>
</tr>
`;
}
document.querySelector(`#shipping-list-update`).innerHTML = listItems;
checkShoppingListItem();
fetchItem();
}
}
function checkShoppingListItem() {
document.querySelectorAll(`.check-btn`).forEach((button) => {
button.addEventListener('click', async function (e) {
const itemID = e.currentTarget.id.slice(9);
const res = await fetch(`/items/pendingItems/${itemID}`, {
method: 'PUT'
});
if ((await res.json()).status === true) {
const updateOnTheList = document.querySelector('#list-item-' + itemID);
updateOnTheList.remove();
}
});
});
}
document.querySelector(`#back-page`).addEventListener('click', function () {
const params = new URLSearchParams(window.location.search);
const eventId = params.get('event-id');
const isCreator = params.get('is-creator');
window.location = `/eventSummary/event.html?event-id=${eventId}&is-creator=${isCreator}`;
});
document.querySelectorAll(`.dropdown-item`).forEach((dropdown) => {
dropdown.addEventListener('click', function (e) {
const selectType = e.currentTarget.innerHTML.toLowerCase();
fetchPendingItems(selectType);
});
});

View File

@@ -0,0 +1,222 @@
import {
loadCreateEvents,
loadParticipateEvents,
currentParticipantsList,
loadParticipantsModal
} from './loadEvent.js';
function onlyNumbers(str) {
return /^[0-9]+$/.test(str);
}
export function listenCreateButtons() {
document.querySelector('.create .next-round').addEventListener('click', async (e) => {
e.stopImmediatePropagation();
const params = new URLSearchParams(window.location.search);
let page = '2';
if (!params.has('create-page')) {
page = await loadCreateEvents(page);
} else {
if (onlyNumbers(params.get('create-page'))) {
if (parseInt(params.get('create-page')) >= 1) {
page = (parseInt(params.get('create-page')) + 1).toString();
page = await loadCreateEvents(page);
} else {
page = '1';
page = await loadCreateEvents(page);
}
} else {
page = '1';
page = await loadCreateEvents(page);
}
}
if (!params.has('participate-page')) {
history.pushState({}, 'Dashboard', `http://localhost:8080/index.html?create-page=${page}`);
} else {
const participatePage = params.get('participate-page');
history.pushState(
{},
'Dashboard',
`http://localhost:8080/index.html?create-page=${page}&participate-page=${participatePage}`
);
}
listenCreateButtons();
});
document.querySelector('.create .previous-round').addEventListener('click', async (e) => {
e.stopImmediatePropagation();
const params = new URLSearchParams(window.location.search);
let page = '1';
if (!params.has('create-page')) {
page = await loadCreateEvents(page);
} else {
if (onlyNumbers(params.get('create-page'))) {
if (parseInt(params.get('create-page')) >= 2) {
page = (parseInt(params.get('create-page')) - 1).toString();
page = await loadCreateEvents(page);
} else {
page = '1';
page = await loadCreateEvents(page);
}
} else {
page = '1';
page = await loadCreateEvents(page);
}
}
if (!params.has('participate-page')) {
history.pushState({}, 'Dashboard', `http://localhost:8080/index.html?create-page=${page}`);
} else {
const participatePage = params.get('participate-page');
history.pushState(
{},
'Dashboard',
`http://localhost:8080/index.html?create-page=${page}&participate-page=${participatePage}`
);
}
listenCreateButtons();
});
}
export function listenParticipateButtons() {
document.querySelector('.participate .next-round').addEventListener('click', async (e) => {
e.stopImmediatePropagation();
const params = new URLSearchParams(window.location.search);
let page = '2';
if (!params.has('participate-page')) {
page = await loadParticipateEvents(page);
} else {
if (onlyNumbers(params.get('participate-page'))) {
if (parseInt(params.get('participate-page')) >= 1) {
page = (parseInt(params.get('participate-page')) + 1).toString();
page = await loadParticipateEvents(page);
} else {
page = '1';
page = await loadParticipateEvents(page);
}
} else {
page = '1';
page = await loadParticipateEvents(page);
}
}
if (!params.has('create-page')) {
history.pushState({}, 'Dashboard', `http://localhost:8080/index.html?participate-page=${page}`);
} else {
const createPage = params.get('create-page');
history.pushState(
{},
'Dashboard',
`http://localhost:8080/index.html?create-page=${createPage}&participate-page=${page}`
);
}
listenParticipateButtons();
});
document.querySelector('.participate .previous-round').addEventListener('click', async (e) => {
e.stopImmediatePropagation();
const params = new URLSearchParams(window.location.search);
let page = '1';
if (!params.has('participate-page')) {
page = await loadParticipateEvents(page);
} else {
if (onlyNumbers(params.get('participate-page'))) {
if (parseInt(params.get('participate-page')) >= 2) {
page = (parseInt(params.get('participate-page')) - 1).toString();
page = await loadParticipateEvents(page);
} else {
page = '1';
page = await loadParticipateEvents(page);
}
} else {
page = '1';
page = await loadParticipateEvents(page);
}
}
if (!params.has('create-page')) {
history.pushState({}, 'Dashboard', `http://localhost:8080/index.html?participate-page=${page}`);
} else {
const createPage = params.get('create-page');
history.pushState(
{},
'Dashboard',
`http://localhost:8080/index.html?create-page=${createPage}&participate-page=${page}`
);
}
listenParticipateButtons();
});
}
export function listenEditButtons() {
const editCreatedButtons = document.querySelectorAll("[class^='created_detail_'] a");
for (let editButton of editCreatedButtons) {
editButton.addEventListener('click', async () => {
const className = editButton.parentNode.parentNode.className;
const eventId = className.replace('created_detail_', '');
window.location.href = `/eventSummary/event.html?event-id=${eventId}&is-creator=1`;
});
}
const editParticipatedButtons = document.querySelectorAll("[class^='participated_detail_'] a");
for (let editButton of editParticipatedButtons) {
editButton.addEventListener('click', async () => {
const className = editButton.parentNode.parentNode.className;
const eventId = className.replace('participated_detail_', '');
window.location.href = `/eventSummary/event.html?event-id=${eventId}&is-creator=0`;
});
}
}
export function listenToSchedulePage(datetime) {
const date = datetime ? `${datetime.slice(0, 4)}${datetime.slice(5, 7)}${datetime.slice(8, 10)}` : '';
const toScheduleDiv = document.querySelector('.schedule .info-button');
const params = new URLSearchParams(window.location.search);
const eventId = params.get('event-id');
const isCreator = params.get('is-creator');
if (toScheduleDiv) {
toScheduleDiv.addEventListener('click', () => {
window.location.href = `/eventSchedule/eventSchedule.html?event-id=${eventId}&is-creator=${isCreator}&date=${date}`;
});
}
}
export function listenToItemPage() {
const toScheduleDiv = document.querySelector('.item .info-button');
const params = new URLSearchParams(window.location.search);
const eventId = params.get('event-id');
const isCreator = params.get('is-creator');
if (toScheduleDiv) {
toScheduleDiv.addEventListener('click', () => {
window.location.href = `/itemPostPage/itemPost.html?event-id=${eventId}&is-creator=${isCreator}`;
});
}
}
export let deletedParticipantsList = [];
export function listenToDeleteParticipants() {
const deleteButtonDivList = document.querySelectorAll('#participants-modal .delete-button');
for (let deleteButtonDiv of deleteButtonDivList) {
deleteButtonDiv.addEventListener('click', (e) => {
e.stopImmediatePropagation();
const userId = parseInt(e.path[1].id.replace('delete_button_user_', ''));
for (let i = 0; i < currentParticipantsList.length; i++) {
if (currentParticipantsList[i].id === userId) {
const [deletedParticipant] = currentParticipantsList.splice(i, 1);
deletedParticipantsList.push(deletedParticipant);
loadParticipantsModal(currentParticipantsList, deletedParticipantsList);
}
}
listenToDeleteParticipants();
});
}
}

View File

@@ -0,0 +1,571 @@
import {
listenCreateButtons,
listenParticipateButtons,
listenEditButtons,
listenToSchedulePage,
listenToItemPage,
listenToDeleteParticipants
} from './listenButtons.js';
export let currentParticipantsList = [];
let deletedParticipantsList = [];
export async function loadCreateEvents(page) {
const res = await fetch(`/events/created?page=${page}`);
if (res.status !== 200) {
const data = await res.json();
alert(data.msg);
return;
}
const result = await res.json();
const events = result.object;
const currentPage = result.currentPage;
const totalPage = result.page;
const eventsCreateContainer = document.querySelector('.create .events-container');
const pageCreateContainer = document.querySelector('.create .turn-page-button-container');
let eventsCreateHTML = '';
for (let event of events) {
let status = '';
let statusClass = '';
const today = new Date().getTime();
const eventStartDate = new Date(event.start_datetime).getTime();
if (event.deleted) {
status = 'Deleted';
statusClass = 'deletedStatus';
} else {
if (today > eventStartDate && eventStartDate) {
status = 'Completed';
statusClass = 'completedStatus';
} else {
status = 'Processing';
statusClass = 'progressStatus';
}
}
eventsCreateHTML += `
<tr class="table-content-row">
<th scope="col" class="ID_${event.id} hidable-2">
<div>${event.id}</div>
</th>
<th scope="col" class="name_${event.id}">
<div>${event.name}</div>
</th>
<th scope="col" class="address_${event.id}">
<div>${!event.venue ? 'TBD' : event.venue}</div>
</th>
<th scope="col" class="start_datetime_${event.id}">
<div>
${
!event.start_datetime
? 'TBD'
: new Date(event.start_datetime)
.toLocaleString('en-US', { hour12: false })
.replace(', ', ' ')
.slice(0, -3)
}
</div>
</th>
<th scope="col" class="end_datetime_${event.id} hidable-1">
<div>
${
!event.end_datetime
? 'TBD'
: new Date(event.end_datetime)
.toLocaleString('en-US', { hour12: false })
.replace(', ', ' ')
.slice(0, -3)
}
</div>
</th>
<th scope="col" class="event_status_${event.id}">
<div><div class="${statusClass}">${status}</div></div>
</th>
<th scope="col" class="created_detail_${event.id}">
<div>
<a class="edit-button">
<i class="fa-regular fa-pen-to-square"></i>
</a>
</div>
</th>
</tr>
`;
}
const pageHTML = !totalPage ? '' : `Showing ${currentPage} of ${totalPage}`;
eventsCreateContainer.innerHTML = eventsCreateHTML;
pageCreateContainer.innerHTML = `
<div class="page-number">${pageHTML}</div>
<button type="button" class="previous-round btn btn-light">
<i class="fa-sharp fa-solid fa-less-than"></i>
</button>
<button type="button" class="next-round btn btn-light">
<i class="fa-sharp fa-solid fa-greater-than"></i>
</button>
`;
listenCreateButtons();
listenEditButtons();
return currentPage;
}
export async function loadParticipateEvents(page) {
const res = await fetch(`/events/participated?page=${page}`);
if (res.status !== 200) {
const data = await res.json();
alert(data.msg);
return;
}
const result = await res.json();
const events = result.object;
const currentPage = result.currentPage;
const totalPage = result.page;
const eventsParticipateContainer = document.querySelector('.participate .events-container');
const pageParticipateContainer = document.querySelector('.participate .turn-page-button-container');
let eventsParticipateHTML = '';
for (let event of events) {
let status = '';
let statusClass = '';
const today = new Date().getTime();
const eventStartDate = new Date(event.start_datetime).getTime();
if (event.deleted) {
status = 'Deleted';
statusClass = 'deletedStatus';
} else {
if (today > eventStartDate && eventStartDate) {
status = 'Completed';
statusClass = 'completedStatus';
} else {
status = 'Processing';
statusClass = 'progressStatus';
}
}
eventsParticipateHTML += `
<tr class="table-content-row">
<th scope="col" class="ID_${event.id} hidable-2">
<div>${event.id}</div>
</th>
<th scope="col" class="name_${event.id}">
<div>${event.name}</div>
</th>
<th scope="col" class="address_${event.id}">
<div>${!event.venue ? 'TBD' : event.venue}</div>
</th>
<th scope="col" class="start_datetime_${event.id}">
<div>
${
!event.start_datetime
? 'TBD'
: new Date(event.start_datetime)
.toLocaleString('en-US', { hour12: false })
.replace(', ', ' ')
.slice(0, -3)
}
</div>
</th>
<th scope="col" class="end_datetime_${event.id} hidable-1">
<div>
${
!event.end_datetime
? 'TBD'
: new Date(event.end_datetime)
.toLocaleString('en-US', { hour12: false })
.replace(', ', ' ')
.slice(0, -3)
}
</div>
</th>
<th scope="col" class="event_status_${event.id}">
<div><div class="${statusClass}">${status}</div></div>
</th>
<th scope="col" class="participated_detail_${event.id}">
<div>
<a class="edit-button">
<i class="fa-regular fa-pen-to-square"></i>
</a>
</div>
</th>
</tr>
`;
}
const pageHTML = !totalPage ? '' : `Showing ${currentPage} of ${totalPage}`;
eventsParticipateContainer.innerHTML = eventsParticipateHTML;
pageParticipateContainer.innerHTML = `
<div class="page-number">${pageHTML}</div>
<button type="button" class="previous-round btn btn-light">
<i class="fa-sharp fa-solid fa-less-than"></i>
</button>
<button type="button" class="next-round btn btn-light">
<i class="fa-sharp fa-solid fa-greater-than"></i>
</button>
`;
listenParticipateButtons();
listenEditButtons();
return currentPage;
}
export async function loadEventDetails() {
const params = new URLSearchParams(window.location.search);
const isCreator = parseInt(params.get('is-creator'));
const eventId = params.get('event-id');
const res = await fetch(`/events/detail/${isCreator ? 'created' : 'participated'}/${eventId}`);
if (res.status !== 200) {
const data = await res.json();
alert(data.msg);
return;
}
const result = await res.json();
if (result.status) {
// Check if the event is processing
const today = new Date().getTime();
const eventStartDate = new Date(result.detail.start_datetime).getTime();
const processing = today <= eventStartDate || !eventStartDate;
const deleted = result.detail.deleted;
// Load Event Name into Page
let deleteEventButton = '';
if (isCreator && processing && !deleted) {
deleteEventButton = `
<div class="delete_event-button-container">
<a class="delete-button" id="delete_event-button" data-bs-toggle="modal" data-bs-target="#delete-event-modal">
<i class="fa-solid fa-trash-can"></i>
</a>
</div>
`;
}
const eventName = document.querySelector('.eventname .background-frame');
eventName.innerHTML = `
<div class="name-block">
<div class="emoji">
🎉
</div>
<div>
${result.detail.name}
</div>
</div>
${deleteEventButton}
`;
// Load Date Time into Page
let dateTimeLabel = '';
let startDateTimeString = '';
let endDateTimeString = '';
if (result.detail.start_datetime && result.detail.end_datetime) {
startDateTimeString = new Date(result.detail.start_datetime)
.toLocaleString('en-US', { hour12: false })
.replace(', ', ' ')
.slice(0, -3);
endDateTimeString = new Date(result.detail.end_datetime)
.toLocaleString('en-US', { hour12: false })
.replace(', ', ' ')
.slice(0, -3);
dateTimeLabel = `
<div>Start:</div>
<div>End:</div>
`;
} else {
startDateTimeString = 'To Be Determined';
}
let editTimeButton = '';
if (isCreator && processing && !deleted) {
editTimeButton = `
<a class="edit-button" data-bs-toggle="modal" data-bs-target="#datetime-modal">
<i class="fa-regular fa-pen-to-square"></i>
</a>
`;
}
let datetimePollButton = '';
if (result.detail.date_poll_created) {
datetimePollButton = `
<a class="poll-button" href="/poll/datetimePoll.html?${params}">
<i class="fa-solid fa-check"></i>
</a>
`;
}
const dateTime = document.querySelector('.date-time .background-frame');
dateTime.innerHTML = `
<div class="frame-title">
Date & Time
</div>
<div>
<div class="frame-content-label">
${dateTimeLabel}
</div>
<div class="frame-content">
<div>${startDateTimeString}</div>
<div>${endDateTimeString}</div>
</div>
<div class="datetime-buttons-container">
${datetimePollButton}
${editTimeButton}
</div>
</div>
`;
// Load Participants into Page
let participantListHTML = '';
participantListHTML += '<div>';
participantListHTML += `
<div class="red_creator creator_${result.creator.id}">
<i class="fa-solid fa-user"></i>
&nbsp; &nbsp;
${result.creator.first_name} ${result.creator.last_name}
</div>
`;
if (result.participants.length) {
const userList = result.participants;
for (let user of userList) {
participantListHTML += `
<div class="user_${user.id}">
<i class="fa-solid fa-user"></i>
&nbsp; &nbsp;
${user.first_name} ${user.last_name}
</div>
`;
}
}
participantListHTML += '</div>';
let editParticipantsButton = '';
if (isCreator && processing && !deleted) {
editParticipantsButton = `
<a class="edit-button" data-bs-toggle="modal" data-bs-target="#participants-modal">
<i class="fa-regular fa-pen-to-square"></i>
</a>
`;
}
let inviteButton = '';
if (isCreator && processing && !deleted) {
inviteButton = `
<div class="invite-button-container">
<a class="invite-button" data-bs-toggle="modal" data-bs-target="#invitation-modal">
+
</a>
<div>
Invite more friends
</div>
</div>
`;
}
const participant = document.querySelector('.participant .background-frame');
participant.innerHTML = `
<div class="frame-title-container">
<div class="left">
<div class="frame-title">
Participants
</div>
<div id="number-of-participants">
${result.participants.length + 1}
</div>
</div>
${editParticipantsButton}
</div>
<div class="frame-content-container">
${participantListHTML}
</div>
${inviteButton}
`;
// Load Participants Modal
currentParticipantsList = structuredClone(result.participants);
loadParticipantsModal(currentParticipantsList, deletedParticipantsList);
// Load Invitation Link
pasteInvitationLink(result.detail.id, result.detail.invitation_token);
// Load Venue into Page
let venueString = '';
if (result.detail.venue) {
venueString = `
<a href="https://www.google.com/maps/search/${result.detail.venue.replaceAll(' ', '+')}/" target="_blank">
${result.detail.venue || ''}
</a>
`;
} else {
venueString = 'To Be Determined';
}
let editVenueButton = '';
if (isCreator && processing && !deleted) {
editVenueButton = `
<a class="edit-button" data-bs-toggle="modal" data-bs-target="#venue-modal">
<i class="fa-regular fa-pen-to-square"></i>
</a>
`;
}
let venuePollButton = '';
if (result.detail.venue_poll_created) {
venuePollButton = `
<a class="poll-button" href="/poll/venuePoll.html?${params}">
<i class="fa-solid fa-check"></i>
</a>
`;
}
const venue = document.querySelector('.venue .background-frame');
venue.innerHTML = `
<div class="frame-title-container">
<div class="frame-title">
Venue
</div>
<div class="venue-buttons-container">
${venuePollButton}
${editVenueButton}
</div>
</div>
<div class="frame-content-container">
<i class="fa-solid fa-location-dot"></i>
&nbsp; &nbsp;
${venueString}
</div>
`;
// Load schedule into Page
let infoButtonHTML = '';
if (result.detail.start_datetime && result.detail.end_datetime) {
infoButtonHTML = `
<a class="info-button">
<i class="fa-solid fa-info"></i>
</a>
`;
}
const schedule = document.querySelector('.schedule .background-frame');
schedule.innerHTML = `
<div class="frame-title-container">
<div id="frame-content-container" class="frame-title">
Schedule
</div>
${infoButtonHTML}
</div>
<div class="frame-content-container">
<div id="rundown-container" class="overflow-auto" data-current="0">
<div id="rundown" class="row">
</div>
</div>
</div>
`;
// Load item into Page
const item = document.querySelector('.item .background-frame');
item.innerHTML = `
<div class="frame-title-container">
<div class="frame-title">
Item
</div>
<a class="info-button">
<i class="fa-solid fa-info"></i>
</a>
</div>
<div class="frame-content-container">
<div class="shopping-list">
<div class="item-list border">
<table class="table">
<thead>
<tr>
<th scope="col" class="pending-item-header">
Pending Items:
<div>
<button id="shopping-list-shorting" class="shorting-btn" type="button"
data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-filter-circle"></i></button>
<ul class="dropdown-menu">
<li><a class="dropdown-item">Food</a></li>
<li><a class="dropdown-item">Drink</a></li>
<li><a class="dropdown-item">Decoration</a></li>
<li><a class="dropdown-item">Other</a></li>
</ul>
</div>
</th>
</tr>
</thead>
<tbody id="shopping-list-update">
<!--loaded with itemPost.js "shopping list JS"-->
</tbody>
</table>
</div>
</div>
</div>
`;
listenToSchedulePage(result.detail.start_datetime);
listenToItemPage();
listenToDeleteParticipants();
} else {
const roleName = isCreator ? 'creator' : 'participant';
alert(`You are not ${roleName} of the event!`);
}
}
export function loadParticipantsModal(currentList, deletedList) {
let currentParticipantListModalHTML = '';
if (currentList.length) {
currentParticipantListModalHTML += '<div>';
for (let user of currentList) {
currentParticipantListModalHTML += `
<div class="user-wrapper current" id="wrapper_user_${user.id}">
<div class="user_${user.id}">
<i class="fa-solid fa-user"></i>
&nbsp; &nbsp;
${user.first_name} ${user.last_name}
</div>
<a class="delete-button" id="delete_button_user_${user.id}">
<i class="fa-solid fa-trash-can"></i>
</a>
</div>
`;
}
currentParticipantListModalHTML += '</div>';
}
const currentParticipantModal = document.querySelector('#participants-modal #current-participants-list');
currentParticipantModal.innerHTML = `
<div class="participants-list-title">
Current
</div>
<div class="frame-content-container">
${currentParticipantListModalHTML}
</div>
`;
let deletedParticipantListModalHTML = '';
if (deletedList.length) {
deletedParticipantListModalHTML += '<div>';
for (let user of deletedList) {
deletedParticipantListModalHTML += `
<div class="user-wrapper current" id="wrapper_user_${user.id}">
<div class="user_${user.id}">
<i class="fa-solid fa-user"></i>
&nbsp; &nbsp;
${user.first_name} ${user.last_name}
</div>
</div>
`;
}
deletedParticipantListModalHTML += '</div>';
}
const deletedParticipantModal = document.querySelector('#participants-modal #deleted-participants-list');
deletedParticipantModal.innerHTML = `
<div class="participants-list-title">
Deleted
</div>
<div class="frame-content-container">
${deletedParticipantListModalHTML}
</div>
`;
}
export function pasteInvitationLink(eventId, invitation_token) {
document.querySelector(
'#invitation-modal .form-control'
).value = `http://${window.location.host}/invitationPage/invitation.html?event-id=${eventId}&token=${invitation_token}`;
}

View File

@@ -0,0 +1,110 @@
.bi-chevron-left {
font-size: 30px;
position: fixed;
top: 90px;
left: 50px;
color: #444a58;
cursor: pointer;
}
/* event body CSS */
#from-container {
display: flex;
justify-content: center;
margin: 30px 10px;
}
.box {
margin: 10px 10px;
}
.form-header {
font-size: 20px;
font-family: 'Lato', sans-serif;
padding: 3px;
}
.venue-check {
display: flex;
font-size: 16px;
font-family: 'Lato', sans-serif;
}
.form-check {
margin: 10px;
font-size: 16 px;
font-family: 'Lato', sans-serif;
}
/* Date part */
.eventDate-box {
display: flex;
flex-direction: row;
}
.event-time-label {
font-size: 19px;
font-family: 'Lato', sans-serif;
margin: 4px 10px 4px 0px;
}
.event-time-label-box {
display: flex;
flex-direction: column;
}
.event-time-label-box > input {
color: #495871;
}
.clock {
margin: 4px 10px 4px 0px;
}
/* parking */
.parking-box {
display: flex;
margin: 15px 0px 5px 0px;
color: #495871;
}
.parking-number-input {
margin-left: 10px;
width: 70px;
}
/* budget */
.budget-box {
color: #495871;
font-family: 'Lato', sans-serif;
margin: 5px 0px;
}
.budget-box span {
margin-left: 5px;
}
/* submit btn */
.submit-btn {
display: flex;
justify-content: center;
max-height: 50px;
}
#event-submit {
border: none;
margin-top: 10px;
background-color: white;
}
.event-date-reminder {
color: grey;
font-size: 12px;
}
/* Remark Css */
.form-control {
border-width: 2px;
border-radius: 10px;
width: 90%;
}

View File

@@ -0,0 +1,95 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.1/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-iYQeCzEYFbKjA/T2uDLTpkwGzCiq6soy8tYaI1GyVh/UjpbCx/TYkiZhlZB6+fzT"
crossorigin="anonymous"
/>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.9.1/font/bootstrap-icons.css" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.0/css/all.min.css"
integrity="sha512-xh6O/CkQoPOWDdYTDqeRdPCVd1SpvCA9XXcUnZS2FmJNp1coAFzvtCN9BmamE+4aHK8yyUHUSCcJHgXloTyT2A=="
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>
<link rel="stylesheet" href="./newEvent.css" />
<title>Add New Party</title>
</head>
<body style="display: none">
<div class="navbar-container container">
<!-- NAVBAR: to be loaded with js -->
</div>
<div class="container">
<a href="/index.html"><i class="bi bi-chevron-left"></i></a>
<div class="row">
<form id="from-container">
<div class="outer-container">
<div class="event-name-box">
<div class="form-header">Name of the Event *</div>
<input id="event-name-input" type="text" class="form-control" name="event_name" />
</div>
<div class="Venue-box">
<div class="form-header">Venue of the party</div>
<input type="text" class="form-control" name="event_venue" />
</div>
<div class="event-date">
<div class="eventDate-box">
<div class="event-time-label-box">
<label for="meeting-time-start" class="event-time-label">Start date & time:</label>
<label for="meeting-time-end" class="event-time-label">End date & time:</label>
</div>
<div class="event-time-label-box">
<input
class="clock"
type="datetime-local"
id="meeting-time-start"
name="event_date_start"
min="2021-06-07T00:00"
max="2035-12-30T00:00"
step="900"
/>
<input
class="clock"
type="datetime-local"
id="meeting-time-end"
name="event_date_end"
min="2021-06-07T00:00"
max="2035-12-30T00:00"
step="900"
/>
</div>
</div>
</div>
<div class="event-date-reminder">
Enter the time in 15 mins interval, e.g. 15:00 or 15:15 or 15:30 or 15:45.
</div>
<!-- submit btn -->
<div class="submit-btn">
<button id="event-submit">
<img src="/asset/submit_button.svg" alt="Submit Button" />
</button>
</div>
</div>
</form>
</div>
</div>
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.1/dist/js/bootstrap.bundle.min.js"
integrity="sha384-u1OknCvxWvY5kfmNBILK2hRnQC3Pr17a+RTT6rIHI7NnikvbZlHgTPOOmMi466C8"
crossorigin="anonymous"
></script>
<script type="module" src="./newEvent.js"></script>
</body>
</html>

View File

@@ -0,0 +1,69 @@
import { addNavbar } from '/functions/addNavbar.js';
import { loadName } from '/functions/loadName.js';
function onlyNumbers(str) {
return /^[0-9]+$/.test(str);
}
window.addEventListener('load', () => {
addNavbar();
loadName();
document.body.style.display = 'block';
});
document.querySelector('#from-container').addEventListener('submit', async function (e) {
e.preventDefault();
const form = e.target;
const eventName = form.event_name.value;
const eventVenue = form.event_venue.value || null;
const startTime = form.event_date_start.value ? new Date(form.event_date_start.value).toISOString() : null;
const endTime = form.event_date_end.value ? new Date(form.event_date_end.value).toISOString() : null;
let dataPass = true;
if (!eventName) {
dataPass = false;
alert('Please fill in the event name!');
}
const nowTimeValue = new Date().getTime();
const startTimeValue = new Date(startTime).getTime();
const endTimeValue = new Date(endTime).getTime();
// check time validity
if (startTimeValue && endTimeValue) {
if (startTimeValue <= nowTimeValue) {
dataPass = false;
alert('Start time must be later than time now!');
} else if (startTimeValue >= endTimeValue) {
dataPass = false;
alert('Start time cannot equals or later than end time!');
}
} else if (!!startTimeValue + !!endTimeValue) {
dataPass = false;
alert('You cannot only leave 1 time blank!');
}
if (dataPass) {
let formObj = {
eventName,
eventVenue,
startTime,
endTime
};
const res = await fetch('/events', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formObj)
});
const eventsResult = await res.json();
if (eventsResult.msg === 'Posted to DB') {
window.location.href = '/index.html'; //
}
}
});

View File

@@ -0,0 +1,61 @@
#account-info {
text-align: center;
}
#personal-page-form {
max-width: 400px;
}
/* @media (min-width: 1200px) {
.form-container {
padding: 0px 400px !important;
}
} */
.form-container {
padding: 0px 50px;
display: flex;
justify-content: center;
}
.name_container {
display: flex;
}
.first_name_container {
padding: 0px 20px 0px 0px;
}
.update {
border-radius: 20px;
color: black;
background-color: #f29559;
border: none;
}
.cancel {
border-radius: 20px;
color: black;
background-color: #9bafd0;
border: none;
}
.btn {
min-width: 120px;
font-size: 15px;
}
.mb-3 {
max-width: 400px;
}
.button-container {
padding: 30px;
display: flex;
justify-content: space-between;
}
#account-info {
padding: 50px 0px;
}

View File

@@ -0,0 +1,109 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.1/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-iYQeCzEYFbKjA/T2uDLTpkwGzCiq6soy8tYaI1GyVh/UjpbCx/TYkiZhlZB6+fzT"
crossorigin="anonymous"
/>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.9.1/font/bootstrap-icons.css" />
<link rel="stylesheet" href="./personalPage.css" />
<title>Personal Page</title>
</head>
<body style="display: none">
<div class="navbar-container container">
<!-- NAVBAR: to be loaded with js -->
</div>
<header id="account-info">Account Info</header>
<div class="form-container">
<form action="/personalPage" method="PUT" id="personal-page-form">
<div class="mb-3 name_container">
<span class="first_name_container">
<label for="first_name" class="form-label">First Name</label>
<input
type="text"
class="form-control"
name="first_name"
id="first_name"
placeholder="Input Your First Name"
value=""
required
/>
</span>
<span>
<label for="last_name" class="form-label">Last Name</label>
<input
type="text"
class="form-control"
name="last_name"
id="last_name"
placeholder="Input Your Last Name"
required
/>
</span>
</div>
<div class="mb-3">
<label for="email" class="form-label">Email</label>
<input type="email" class="form-control" name="email" id="email" readonly />
</div>
<div class="mb-3">
<label for="phone" class="form-label">Phone</label>
<input
type="text"
class="form-control"
name="phone"
id="phone"
placeholder="Input Phone"
pattern="[0-9]{3}-[0-9]{3}-[0-9]{4}"
required
/>
<div class="form-text">Phone number must be in the following format: e.g. 647-111-1111</div>
</div>
<div class="mb-3 google-user">
<label for="current_password" class="form-label">Current Password</label>
<input type="password" class="form-control" name="current_password" id="current_password" />
<div class="form-text">Enter your current password if you wish to update a new password</div>
</div>
<div class="mb-3 google-user">
<label for="new_password" class="form-label">New Password</label>
<input
type="password"
class="form-control"
name="new_password"
id="new_password"
placeholder="Input your new password here"
minlength="8"
/>
<div class="form-text">Password must be 8 characters long</div>
</div>
<div class="mb-3 google-user">
<label for="new_confirmed_password" class="form-label">Confirm Password</label>
<input
type="password"
class="form-control"
name="new_confirmed_password"
id="new_confirmed_password"
placeholder="Type the new password again"
minlength="8"
/>
</div>
<div class="button-container mb-3">
<button type="submit" value="submit" class="btn btn-primary update">Update</button>
<button type="reset" value="cancel" class="btn btn-primary cancel">Cancel</button>
</div>
</form>
</div>
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.1/dist/js/bootstrap.bundle.min.js"
integrity="sha384-u1OknCvxWvY5kfmNBILK2hRnQC3Pr17a+RTT6rIHI7NnikvbZlHgTPOOmMi466C8"
crossorigin="anonymous"
></script>
<script type="module" src="./personalPage.js"></script>
</body>
</html>

View File

@@ -0,0 +1,117 @@
import { addNavbar } from '/functions/addNavbar.js';
import { loadName } from '/functions/loadName.js';
window.addEventListener('load', async () => {
await loadInfo();
await hideInfo();
addNavbar();
loadName();
document.body.style.display = 'block';
});
async function isGoogleUser(password) {
if (password.substring(0, 11) === 'google_user') {
return true;
} else {
return false;
}
}
async function hideInfo() {
const res = await fetch(`/personalPage`);
const result = await res.json();
const divCluster = document.querySelectorAll('.google-user');
if (await isGoogleUser(result.password)) {
divCluster.forEach((div) => {
div.style.display = 'none';
});
} else {
divCluster.forEach((div) => {
div.style.display = 'block';
});
}
}
async function loadInfo() {
const res = await fetch(`/personalPage`);
const result = await res.json();
const firstName = document.querySelector('#first_name');
const lastName = document.querySelector('#last_name');
const email = document.querySelector('#email');
const phone = document.querySelector('#phone');
const currentPassword = document.querySelector('#current_password');
const newPassword = document.querySelector('#new_password');
const newConfirmedPassword = document.querySelector('#new_confirmed_password');
firstName.value = result.first_name;
lastName.value = result.last_name;
email.value = result.email;
phone.value = result.phone;
currentPassword.value = '';
newPassword.value = '';
newConfirmedPassword.value = '';
}
document.querySelector('#personal-page-form').addEventListener('submit', async function updateInfo(event) {
event.preventDefault();
const form = event.target;
const lastName = form.last_name.value;
const firstName = form.first_name.value;
const phone = form.phone.value;
const email = form.email.value;
const currentPassword = form.current_password.value;
const newPassword = form.new_password.value;
const newConfirmedPassword = form.new_confirmed_password.value;
let dataPass = true;
if (newPassword || newConfirmedPassword) {
if (!(newPassword === newConfirmedPassword)) {
dataPass = false;
alert('Password and confirm password do not match!');
} else if (newPassword === currentPassword) {
dataPass = false;
alert('Your current password and the new password are the same!');
} else if (!currentPassword) {
dataPass = false;
alert('Please input your current password if you wish to update your password');
}
}
if (dataPass) {
const formObject = {};
formObject['first_name'] = firstName;
formObject['last_name'] = lastName;
formObject['email'] = email;
formObject['phone'] = phone;
formObject['current_password'] = currentPassword;
formObject['new_password'] = newPassword;
if (newPassword) {
formObject['password'] = newPassword;
} else {
formObject['password'] = currentPassword;
}
const res = await fetch(`/personalPage`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formObject)
});
const result = await res.json();
console.log(result);
if (res.status === 400) {
alert('Something wrong, please check if you have the correct password');
} else {
alert('Update successful!');
location.reload();
}
}
});

View File

@@ -0,0 +1,149 @@
body {
font-family: 'Calibri';
position: relative;
}
.poll-container {
position: relative;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.poll-frame {
display: flex;
justify-content: center;
align-items: flex-start;
margin: 20px 0;
flex-wrap: wrap;
}
.option-container {
width: 200px;
height: 200px;
background-color: #f9f9da;
border-radius: 20px;
margin: 20px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
flex-wrap: wrap;
cursor: pointer;
}
.option-container:hover {
opacity: 0.7;
}
.title {
font-size: 30px;
font-weight: bold;
}
.address {
font-size: 20px;
}
#poll-submit-button,
#poll-terminate-button,
#poll-terminate-confirm-button {
font-size: 20px;
font-family: 'Lato', sans-serif;
padding: 10px 20px 10px 20px;
border-radius: 20px;
margin: 18px;
min-width: 110px;
}
#poll-submit-button {
box-shadow: 0px 1px 12px #f2d492;
background-color: #f2d492;
border: none;
color: #293241;
}
#poll-terminate-button,
#poll-terminate-confirm-button {
box-shadow: none;
background-color: #d8684e;
border: #c7492c 3px solid;
color: white;
font-weight: bold;
}
.poll-title {
font-size: 20px;
margin-top: 20px;
font-weight: bold;
}
.selected {
border: solid black 5px;
}
.vote {
font-size: 15px;
}
.warning-sign {
font-size: 70px;
width: 90px;
height: 90px;
border-radius: 200px;
border: solid 4px black;
display: flex;
justify-content: center;
align-items: center;
}
.warning {
font-size: 30px;
font-weight: bold;
}
.reminder-text {
text-align: center;
}
.modal-title {
font-size: 25px;
font-weight: bold;
color: #293241;
font-family: 'Lato', sans-serif;
transform: translateX(10px);
}
.modal-header {
display: flex;
justify-content: space-between;
}
.modal-body {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.modal-footer {
display: flex;
justify-content: center;
align-content: center;
}
#back-page {
position: absolute;
left: 50px;
top: 15px;
text-decoration: none;
}
.bi-chevron-left {
font-size: 30px;
top: 90px;
left: 40px;
color: #444a58;
cursor: pointer;
}

View File

@@ -0,0 +1,86 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.1/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-iYQeCzEYFbKjA/T2uDLTpkwGzCiq6soy8tYaI1GyVh/UjpbCx/TYkiZhlZB6+fzT"
crossorigin="anonymous"
/>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.9.1/font/bootstrap-icons.css" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.0/css/all.min.css"
integrity="sha512-xh6O/CkQoPOWDdYTDqeRdPCVd1SpvCA9XXcUnZS2FmJNp1coAFzvtCN9BmamE+4aHK8yyUHUSCcJHgXloTyT2A=="
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>
<link rel="stylesheet" href="./datetimePoll.css" />
<title>Venue Poll</title>
</head>
<body style="display: none">
<div class="navbar-container container">
<!-- NAVBAR: to be loaded with js -->
</div>
<div class="poll-container">
<div class="poll-title"></div>
<div class="poll-frame"></div>
<div class="button-container"></div>
<!-- Back Button -->
<a id="back-page">
<i class="bi bi-chevron-left"></i>
</a>
</div>
<!-- Modal -->
<!-- Terminate Poll Confirmation Modal -->
<div
class="modal fade"
id="delete-poll-modal"
data-bs-backdrop="static"
data-bs-keyboard="false"
tabindex="-1"
aria-labelledby="staticBackdropLabel"
aria-hidden="true"
>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<div></div>
<h5 class="modal-title">Terminate Poll</h5>
<div class="exit-modal">
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
</div>
<div class="modal-body">
<div class="warning-sign">
<i class="fa-solid fa-exclamation"></i>
</div>
<div class="warning">Are you sure?</div>
<div class="reminder-text">You will not be able to revert this action!</div>
<div class="modal-footer">
<button id="poll-terminate-confirm-button" type="submit" class="modal-submit-button">
TERMINATE
</button>
</div>
</div>
</div>
</div>
</div>
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.1/dist/js/bootstrap.bundle.min.js"
integrity="sha384-u1OknCvxWvY5kfmNBILK2hRnQC3Pr17a+RTT6rIHI7NnikvbZlHgTPOOmMi466C8"
crossorigin="anonymous"
></script>
<script type="module" src="./datetimePoll.js"></script>
</body>
</html>

View File

@@ -0,0 +1,154 @@
import { addNavbar } from '/functions/addNavbar.js';
import { loadName } from '/functions/loadName.js';
window.addEventListener('load', async () => {
addNavbar();
await loadName();
await loadOptions();
document.body.style.display = 'block';
});
async function loadOptions() {
const params = new URLSearchParams(window.location.search);
const eventId = params.get('event-id');
const res = await fetch(`/events/poll/datetime/${eventId}`);
const result = await res.json();
if (result.status) {
let pollTitle = '';
let pollFrameHTML = '';
let buttonContainerHTML = '';
// Poll title HTML
if (result.pollTerminated) {
pollTitle = 'Poll Terminated';
} else if (result.eventDeleted) {
pollTitle = 'Deleted Event';
} else if (!result.creator) {
if (result.choice) {
pollTitle = `Your choice was: <br>
start: ${new Date(result.choice.start)
.toLocaleString('en-US', { hour12: false })
.replace(',', '')
.slice(0, -3)}<br>
end: ${new Date(result.choice.end)
.toLocaleString('en-US', { hour12: false })
.replace(',', '')
.slice(0, -3)}
`;
} else {
pollTitle = 'Please click on the venue option to vote:';
}
} else {
pollTitle = 'You may click button below to terminate poll.';
}
// Poll Options HTML
const optionsList = result.pollOptions;
optionsList.forEach((each, index) => {
const voteCount = result.voteCounts[each.id].count;
pollFrameHTML += `
<div class="option-container" id="option_${each.id}">
<div class="title">
Datetime ${index + 1}
</div>
<div class="start">
Start: ${new Date(each.start_datetime)
.toLocaleString('en-US', { hour12: false })
.replace(',', '')
.slice(0, -3)}
</div>
<div class="end">
End: ${new Date(each.end_datetime)
.toLocaleString('en-US', { hour12: false })
.replace(',', '')
.slice(0, -3)}
</div>
<div class="vote">
${voteCount === '1' ? `${voteCount} Vote` : `${voteCount} Votes`}
</div>
</div>
`;
});
// Button HTML
if (!result.pollTerminated && !result.eventDeleted) {
if (result.creator) {
buttonContainerHTML = `<button id="poll-terminate-button">Terminate Poll</button>`;
} else {
if (!result.choice) {
buttonContainerHTML = `<button id="poll-submit-button">Submit Choice</button>`;
}
}
}
// Add HTML to the page
document.querySelector('.poll-title').innerHTML = pollTitle;
document.querySelector('.poll-frame').innerHTML = pollFrameHTML;
document.querySelector('.button-container').innerHTML = buttonContainerHTML;
// Check if participant that has not yet voted
if (!result.pollTerminated && !result.eventDeleted) {
if (!result.creator && !result.choice) {
// Listen option choice
let optionId;
const optionsDiv = document.querySelectorAll('.option-container');
optionsDiv.forEach((each) => {
each.addEventListener('click', (e) => {
if (e.target.classList.contains('option-container')) {
e.target.classList.add('selected');
optionId = e.target.id;
const otherDiv = document.querySelectorAll(`.option-container:not([id*="${optionId}"])`);
otherDiv.forEach((each) => {
each.classList.remove('selected');
});
}
});
});
// Listen submit button for voting
document.querySelector('#poll-submit-button').addEventListener('click', async () => {
const optionId = document.querySelector('.selected').id.replace('option_', '');
const res = await fetch(`/events/poll/datetime/vote/${eventId}/${optionId}`, {
method: 'POST'
});
const result = await res.json();
if (result.status) {
alert('Successfully voted!');
await loadOptions();
} else {
alert('Unable to submit vote!');
}
});
} else if (result.creator) {
// Listen to terminate button
document.querySelector('#poll-terminate-button').addEventListener('click', () => {
const dateTimeTerminatePoll = new bootstrap.Modal(document.getElementById('delete-poll-modal'));
dateTimeTerminatePoll.show();
document.querySelector('#poll-terminate-confirm-button').addEventListener('click', async () => {
const params = new URLSearchParams(window.location.search);
const eventId = params.get('event-id');
const res = await fetch(`/events/poll/datetime/${eventId}`, {
method: 'DELETE'
});
const result = await res.json();
if (result.status) {
alert('Successfully terminated poll!');
dateTimeTerminatePoll.hide();
loadOptions();
} else {
alert('Unable to terminate poll!');
}
});
});
}
}
// Add backward button
document.querySelector('#back-page').href = `/eventSummary/event.html?${params}`;
} else {
alert('Unable to load datetime poll page!');
window.location.href = '/index.html';
}
}

View File

@@ -0,0 +1,149 @@
body {
font-family: 'Calibri';
position: relative;
}
.poll-container {
position: relative;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.poll-frame {
display: flex;
justify-content: center;
align-items: flex-start;
margin: 20px 0;
flex-wrap: wrap;
}
.option-container {
width: 200px;
height: 200px;
background-color: #f9f9da;
border-radius: 20px;
margin: 20px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
flex-wrap: wrap;
cursor: pointer;
}
.option-container:hover {
opacity: 0.7;
}
.title {
font-size: 30px;
font-weight: bold;
}
.address {
font-size: 20px;
}
#poll-submit-button,
#poll-terminate-button,
#poll-terminate-confirm-button {
font-size: 20px;
font-family: 'Lato', sans-serif;
padding: 10px 20px 10px 20px;
border-radius: 20px;
margin: 18px;
min-width: 110px;
}
#poll-submit-button {
box-shadow: 0px 1px 12px #f2d492;
background-color: #f2d492;
border: none;
color: #293241;
}
#poll-terminate-button,
#poll-terminate-confirm-button {
box-shadow: none;
background-color: #d8684e;
border: #c7492c 3px solid;
color: white;
font-weight: bold;
}
.poll-title {
font-size: 20px;
margin-top: 20px;
font-weight: bold;
}
.selected {
border: solid black 5px;
}
.vote {
font-size: 15px;
}
.warning-sign {
font-size: 70px;
width: 90px;
height: 90px;
border-radius: 200px;
border: solid 4px black;
display: flex;
justify-content: center;
align-items: center;
}
.warning {
font-size: 30px;
font-weight: bold;
}
.reminder-text {
text-align: center;
}
.modal-title {
font-size: 25px;
font-weight: bold;
color: #293241;
font-family: 'Lato', sans-serif;
transform: translateX(10px);
}
.modal-header {
display: flex;
justify-content: space-between;
}
.modal-body {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.modal-footer {
display: flex;
justify-content: center;
align-content: center;
}
#back-page {
position: absolute;
left: 50px;
top: 15px;
text-decoration: none;
}
.bi-chevron-left {
font-size: 30px;
top: 90px;
left: 40px;
color: #444a58;
cursor: pointer;
}

View File

@@ -0,0 +1,86 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.1/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-iYQeCzEYFbKjA/T2uDLTpkwGzCiq6soy8tYaI1GyVh/UjpbCx/TYkiZhlZB6+fzT"
crossorigin="anonymous"
/>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.9.1/font/bootstrap-icons.css" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.0/css/all.min.css"
integrity="sha512-xh6O/CkQoPOWDdYTDqeRdPCVd1SpvCA9XXcUnZS2FmJNp1coAFzvtCN9BmamE+4aHK8yyUHUSCcJHgXloTyT2A=="
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>
<link rel="stylesheet" href="./venuePoll.css" />
<title>Venue Poll</title>
</head>
<body style="display: none">
<div class="navbar-container container">
<!-- NAVBAR: to be loaded with js -->
</div>
<div class="poll-container">
<div class="poll-title"></div>
<div class="poll-frame"></div>
<div class="button-container"></div>
<!-- Back Button -->
<a id="back-page">
<i class="bi bi-chevron-left"></i>
</a>
</div>
<!-- Modal -->
<!-- Terminate Poll Confirmation Modal -->
<div
class="modal fade"
id="delete-poll-modal"
data-bs-backdrop="static"
data-bs-keyboard="false"
tabindex="-1"
aria-labelledby="staticBackdropLabel"
aria-hidden="true"
>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<div></div>
<h5 class="modal-title">Terminate Poll</h5>
<div class="exit-modal">
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
</div>
<div class="modal-body">
<div class="warning-sign">
<i class="fa-solid fa-exclamation"></i>
</div>
<div class="warning">Are you sure?</div>
<div class="reminder-text">You will not be able to revert this action!</div>
<div class="modal-footer">
<button id="poll-terminate-confirm-button" type="submit" class="modal-submit-button">
TERMINATE
</button>
</div>
</div>
</div>
</div>
</div>
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.1/dist/js/bootstrap.bundle.min.js"
integrity="sha384-u1OknCvxWvY5kfmNBILK2hRnQC3Pr17a+RTT6rIHI7NnikvbZlHgTPOOmMi466C8"
crossorigin="anonymous"
></script>
<script type="module" src="./venuePoll.js"></script>
</body>
</html>

View File

@@ -0,0 +1,134 @@
import { addNavbar } from '/functions/addNavbar.js';
import { loadName } from '/functions/loadName.js';
window.addEventListener('load', async () => {
addNavbar();
await loadName();
await loadOptions();
document.body.style.display = 'block';
});
async function loadOptions() {
const params = new URLSearchParams(window.location.search);
const eventId = params.get('event-id');
const res = await fetch(`/events/poll/venue/${eventId}`);
const result = await res.json();
if (result.status) {
let pollTitle = '';
let pollFrameHTML = '';
let buttonContainerHTML = '';
// Poll title HTML
if (result.pollTerminated) {
pollTitle = 'Poll Terminated';
} else if (result.eventDeleted) {
pollTitle = 'Deleted Event';
} else if (!result.creator) {
if (result.choice) {
pollTitle = `Your choice was: ${result.choice.address}`;
} else {
pollTitle = 'Please click on the venue option to vote:';
}
} else {
pollTitle = 'You may click button below to terminate poll.';
}
// Poll Options HTML
const optionsList = result.pollOptions;
optionsList.forEach((each, index) => {
const voteCount = result.voteCounts[each.id].count;
pollFrameHTML += `
<div class="option-container" id="option_${each.id}">
<div class="title">
Venue ${index + 1}
</div>
<div class="address">
${each.address}
</div>
<div class="vote">
${voteCount === '1' ? `${voteCount} Vote` : `${voteCount} Votes`}
</div>
</div>
`;
});
// Button HTML
if (!result.pollTerminated && !result.eventDeleted) {
if (result.creator) {
buttonContainerHTML = `<button id="poll-terminate-button">Terminate Poll</button>`;
} else {
if (!result.choice) {
buttonContainerHTML = `<button id="poll-submit-button">Submit Choice</button>`;
}
}
}
// Add HTML to the page
document.querySelector('.poll-title').innerHTML = pollTitle;
document.querySelector('.poll-frame').innerHTML = pollFrameHTML;
document.querySelector('.button-container').innerHTML = buttonContainerHTML;
// Check if participant that has not yet voted
if (!result.pollTerminated && !result.eventDeleted) {
if (!result.creator && !result.choice) {
// Listen option choice
let optionId;
const optionsDiv = document.querySelectorAll('.option-container');
optionsDiv.forEach((each) => {
each.addEventListener('click', (e) => {
e.currentTarget.classList.add('selected');
optionId = e.currentTarget.id;
const otherDiv = document.querySelectorAll(`.option-container:not([id*="${optionId}"])`);
otherDiv.forEach((each) => {
each.classList.remove('selected');
});
});
});
// Listen submit button for voting
document.querySelector('#poll-submit-button').addEventListener('click', async () => {
const optionId = document.querySelector('.selected').id.replace('option_', '');
const res = await fetch(`/events/poll/venue/vote/${eventId}/${optionId}`, {
method: 'POST'
});
const result = await res.json();
if (result.status) {
alert('Successfully voted!');
await loadOptions();
} else {
alert('Unable to submit vote!');
}
});
} else if (result.creator) {
// Listen to terminate button
document.querySelector('#poll-terminate-button').addEventListener('click', () => {
const venueTerminatePoll = new bootstrap.Modal(document.getElementById('delete-poll-modal'));
venueTerminatePoll.show();
document.querySelector('#poll-terminate-confirm-button').addEventListener('click', async () => {
const params = new URLSearchParams(window.location.search);
const eventId = params.get('event-id');
const res = await fetch(`/events/poll/venue/${eventId}`, {
method: 'DELETE'
});
const result = await res.json();
if (result.status) {
alert('Successfully terminated poll!');
venueTerminatePoll.hide();
loadOptions();
} else {
alert('Unable to terminate poll!');
}
});
});
}
}
// Add backward button
document.querySelector('#back-page').href = `/eventSummary/event.html?${params}`;
} else {
alert('Unable to load venue poll page!');
window.location.href = '/index.html';
}
}

View File

@@ -0,0 +1,18 @@
body {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
img {
width: 300px;
height: auto;
}
.word {
font-family: 'Courier New', Courier, monospace;
font-weight: bold;
font-size: 50px;
margin: 30px;
}

View File

@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="/404.css" />
<title>404 Not Found</title>
</head>
<body>
<div class="word">404 Not Found</div>
<img src="/asset/PngItem_6655441.png" />
</body>
</html>

View File

@@ -0,0 +1,59 @@
/* login nav css */
.header-nav {
display: flex;
justify-content: space-between;
align-items: center;
min-height: 92px;
background-color: #efefd0;
}
.navbar-brand {
display: flex;
justify-content: flex-start;
align-items: center;
}
.navbar-brand > img {
border-radius: 10px;
}
.navbar > .navbar-brand {
font-size: 30px;
font-family: 'Pacifico', cursive;
margin: 5px;
}
.navbar > .btn {
font-size: 20px;
color: #293241;
}
.login-part {
height: 50%;
}
.landing-Page-login-btn {
font-size: 20px;
color: #495871;
font-family: 'Lato', sans-serif;
}
.landing-Page-login-btn > .bi {
margin: 0px 10px 0px 2px;
}
.login-part .dropdown-menu {
color: #495871;
font-family: 'Lato', sans-serif;
}
.logout {
cursor: pointer;
}
@media only screen and (max-width: 450px) {
.navbar-logo-text {
display: none;
}
}

Binary file not shown.

Binary file not shown.

BIN
_tecky/party-planner/backend/public/asset/party_icon.jpg (Stored with Git LFS) Normal file

Binary file not shown.

View File

@@ -0,0 +1,41 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="161"
height="72"
fill="none"
viewBox="0 0 161 72"
>
<g filter="url(#a)" opacity=".482">
<rect
width="56.047"
height="7.606"
x="52.31"
y="47.698"
fill="#F29559"
rx="2"
/>
</g>
<rect width="160.667" height="51.718" y=".543" fill="#F29559" rx="6" />
<path
fill="#fff"
d="M33.651 32.18c-1.32 0-2.454-.264-3.402-.792a5.468 5.468 0 0 1-2.16-2.268c-.504-.984-.756-2.142-.756-3.474 0-1.332.252-2.484.756-3.456a5.354 5.354 0 0 1 2.16-2.25c.948-.54 2.082-.81 3.402-.81.852 0 1.656.132 2.412.396.768.264 1.398.642 1.89 1.134l-.756 1.836c-.54-.456-1.092-.786-1.656-.99a5.04 5.04 0 0 0-1.818-.324c-1.284 0-2.262.39-2.934 1.17-.672.768-1.008 1.866-1.008 3.294s.336 2.532 1.008 3.312c.672.78 1.65 1.17 2.934 1.17.648 0 1.254-.102 1.818-.306.564-.216 1.116-.552 1.656-1.008l.756 1.836c-.492.48-1.122.858-1.89 1.134a7.264 7.264 0 0 1-2.412.396Zm6.097-.18v-8.802h2.196v1.548c.432-1.044 1.356-1.62 2.772-1.728l.684-.054.144 1.908-1.296.126c-1.476.144-2.214.9-2.214 2.268V32h-2.286Zm11.246.18c-.996 0-1.854-.186-2.574-.558a4.024 4.024 0 0 1-1.674-1.584c-.384-.684-.576-1.494-.576-2.43 0-.912.186-1.71.558-2.394a4.186 4.186 0 0 1 1.566-1.602c.672-.396 1.434-.594 2.286-.594 1.248 0 2.232.396 2.952 1.188.732.792 1.098 1.872 1.098 3.24v.666h-6.3c.168 1.572 1.068 2.358 2.7 2.358.492 0 .984-.072 1.476-.216a4.19 4.19 0 0 0 1.35-.72l.63 1.512c-.42.348-.948.624-1.584.828a6.216 6.216 0 0 1-1.908.306Zm-.324-7.632c-.66 0-1.194.204-1.602.612-.408.408-.654.96-.738 1.656h4.428c-.048-.732-.252-1.29-.612-1.674-.348-.396-.84-.594-1.476-.594Zm8.64 7.632c-.636 0-1.206-.12-1.71-.36a3.1 3.1 0 0 1-1.17-1.008 2.532 2.532 0 0 1-.414-1.422c0-.648.168-1.158.504-1.53.336-.384.882-.66 1.638-.828.756-.168 1.77-.252 3.042-.252h.63v-.378c0-.6-.132-1.032-.396-1.296s-.708-.396-1.332-.396a5.21 5.21 0 0 0-1.512.234 6.547 6.547 0 0 0-1.566.684l-.648-1.53a4.948 4.948 0 0 1 1.098-.558 7.248 7.248 0 0 1 1.35-.378c.48-.096.93-.144 1.35-.144 1.284 0 2.238.3 2.862.9.624.588.936 1.506.936 2.754V32h-2.106v-1.404a2.508 2.508 0 0 1-.972 1.17c-.444.276-.972.414-1.584.414Zm.468-1.548c.588 0 1.074-.204 1.458-.612.396-.408.594-.924.594-1.548v-.396h-.612c-1.128 0-1.914.09-2.358.27-.432.168-.648.48-.648.936 0 .396.138.72.414.972.276.252.66.378 1.152.378Zm10.51 1.548c-2.328 0-3.492-1.152-3.492-3.456V24.89h-1.692v-1.692h1.692V20.57h2.25v2.628h2.664v1.692h-2.664v3.708c0 .576.126 1.008.378 1.296.252.288.66.432 1.224.432.168 0 .342-.018.522-.054.18-.048.366-.096.558-.144l.342 1.656c-.216.12-.492.216-.828.288a4.39 4.39 0 0 1-.954.108Zm7.126 0c-.996 0-1.854-.186-2.574-.558a4.024 4.024 0 0 1-1.674-1.584c-.384-.684-.576-1.494-.576-2.43 0-.912.186-1.71.558-2.394a4.186 4.186 0 0 1 1.566-1.602c.672-.396 1.434-.594 2.286-.594 1.248 0 2.232.396 2.952 1.188.732.792 1.098 1.872 1.098 3.24v.666h-6.3c.168 1.572 1.068 2.358 2.7 2.358.492 0 .984-.072 1.476-.216a4.19 4.19 0 0 0 1.35-.72l.63 1.512c-.42.348-.948.624-1.584.828a6.216 6.216 0 0 1-1.908.306Zm-.324-7.632c-.66 0-1.194.204-1.602.612-.408.408-.654.96-.738 1.656h4.428c-.048-.732-.252-1.29-.612-1.674-.348-.396-.84-.594-1.476-.594Zm14.932 7.632c-.996 0-1.854-.186-2.574-.558a4.024 4.024 0 0 1-1.674-1.584c-.384-.684-.576-1.494-.576-2.43 0-.912.186-1.71.558-2.394a4.186 4.186 0 0 1 1.566-1.602c.672-.396 1.434-.594 2.286-.594 1.248 0 2.232.396 2.952 1.188.732.792 1.098 1.872 1.098 3.24v.666h-6.3c.168 1.572 1.068 2.358 2.7 2.358.492 0 .984-.072 1.476-.216a4.19 4.19 0 0 0 1.35-.72l.63 1.512c-.42.348-.948.624-1.584.828a6.216 6.216 0 0 1-1.908.306Zm-.324-7.632c-.66 0-1.194.204-1.602.612-.408.408-.654.96-.738 1.656h4.428c-.048-.732-.252-1.29-.612-1.674-.348-.396-.84-.594-1.476-.594ZM99.89 32l-3.816-8.802h2.394l2.448 6.156 2.538-6.156h2.25L101.835 32h-1.944Zm11.062.18c-.996 0-1.854-.186-2.574-.558a4.024 4.024 0 0 1-1.674-1.584c-.384-.684-.576-1.494-.576-2.43 0-.912.186-1.71.558-2.394a4.186 4.186 0 0 1 1.566-1.602c.672-.396 1.434-.594 2.286-.594 1.248 0 2.232.396 2.952 1.188.732.792 1.098 1.872 1.098 3.24v.666h-6.3c.168 1.572 1.068 2.358 2.7 2.358.492 0 .984-.072 1.476-.216a4.19 4.19 0 0 0 1.35-.72l.63 1.512c-.42.348-.948.624-1.584.828a6.216 6.216 0 0 1-1.908.306Zm-.324-7.632c-.66 0-1.194.204-1.602.612-.408.408-.654.96-.738 1.656h4.428c-.048-.732-.252-1.29-.612-1.674-.348-.396-.84-.594-1.476-.594ZM116.371 32v-8.802h2.196v1.368c.3-.504.702-.888 1.206-1.152a3.738 3.738 0 0 1 1.728-.396c2.076 0 3.114 1.206 3.114 3.618V32h-2.25v-5.256c0-.684-.132-1.182-.396-1.494-.252-.312-.648-.468-1.188-.468-.66 0-1.188.21-1.584.63-.384.408-.576.954-.576 1.638V32h-2.25Zm14.561.18c-2.328 0-3.492-1.152-3.492-3.456V24.89h-1.692v-1.692h1.692V20.57h2.25v2.628h2.664v1.692h-2.664v3.708c0 .576.126 1.008.378 1.296.252.288.66.432 1.224.432.168 0 .342-.018.522-.054.18-.048.366-.096.558-.144l.342 1.656c-.216.12-.492.216-.828.288a4.39 4.39 0 0 1-.954.108Z"
/>
<defs>
<filter
id="a"
width="88.666"
height="40.225"
x="36.001"
y="31.388"
color-interpolation-filters="sRGB"
filterUnits="userSpaceOnUse"
>
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape" />
<feGaussianBlur
result="effect1_foregroundBlur_38_26948"
stdDeviation="8.155"
/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@@ -0,0 +1,28 @@
export function addLoginNavbar() {
document.querySelector('head').innerHTML += `
<link href="https://fonts.googleapis.com/css2?family=Lato:wght@300;400&family=Pacifico&display=swap"
rel="stylesheet" />
<link rel="stylesheet" href="/addNavbar.css" />
`;
document.querySelector('.navbar-container').innerHTML = `
<div class="row">
<div class="col-12 header-nav">
<nav class="navbar">
<a class="navbar-brand" href="/index.html">
<img src="/asset/party_icon.jpg" width="54" height="54" class="d-inline-block align-top">
Party Planner
</a>
</nav>
<div class="login-part">
<div class="user-login">
<button type="button" class="btn landing-Page-login-btn" data-bs-toggle="modal" data-bs-target="#login-modal">
<i class="bi bi-person-circle"></i>
Login
</button>
</div>
</div>
</div>
</div>
`;
}

View File

@@ -0,0 +1,56 @@
export function addNavbar() {
// innerHTML only works for link and img and text, not Script tag
document.querySelector('head').innerHTML += `
<link href="https://fonts.googleapis.com/css2?family=Lato:wght@300;400&family=Pacifico&display=swap"
rel="stylesheet" />
<link rel="stylesheet" href="/addNavbar.css" />
`;
document.querySelector('.navbar-container').innerHTML = `
<div class="row">
<div class="col-12 header-nav">
<nav class="navbar">
<a class="navbar-brand" href="/index.html">
<img src="/asset/party_icon.jpg" width="54" height="54" class="d-inline-block align-top">
<div class="navbar-logo-text">
Party Planner
</div>
</a>
</nav>
<div class="login-part">
<div class="user-login dropdown">
<button type="button" class="btn landing-Page-login-btn dropdown-toggle"
data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-person-circle"></i>
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="/personalPage/personalPage.html">Edit Profile</a></li>
<li><a class="dropdown-item" href="/comment/comment.html">Comments</a></li>
<li><a class="dropdown-item logout">Log out</a></li>
</ul>
</div>
</div>
</div>
</div>
`;
document.querySelector('.logout').addEventListener('click', async () => {
const res = await fetch('/login/logout', {
method: 'POST'
});
if (res.status !== 200) {
const data = await res.json();
alert(data.msg);
return;
}
const result = await res.json();
if (result.status) {
window.location.href = '/';
} else {
alert('Unable to log out!');
}
});
}

View File

@@ -0,0 +1,13 @@
export async function loadName() {
const res = await fetch(`/login/name`);
if (res.status !== 200) {
const data = await res.json();
alert(data.msg);
return;
}
const result = await res.json();
if (result.status) {
const nameHTML = document.querySelector('.user-login button');
nameHTML.innerHTML = `<i class="bi bi-person-circle"></i>${result.user}`;
}
}

View File

@@ -0,0 +1,76 @@
.invitation-container {
height: 85vh;
width: 100vw;
display: flex;
justify-content: center;
align-items: center;
}
.invitation-frame {
width: 80%;
height: 90%;
background-color: #f9f9da;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border-radius: 20px;
}
.event-name-container,
.datetime-container,
.venue-container,
.join-button-container {
font-size: 20px;
font-family: 'Calibri';
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.event-name-container,
.join-button-container {
margin: 20px 0;
}
.venue-container,
.datetime-container {
margin: 20px 40px;
}
.event-name-container {
font-size: 50px;
font-weight: bold;
}
.title {
font-size: 30px;
font-weight: bold;
}
.subtitle {
font-weight: bold;
}
#join-event-button {
font-size: 20px;
color: #293241;
font-family: 'Lato', sans-serif;
background-color: #f2d492;
padding: 10px 20px 10px 20px;
border-radius: 20px;
margin: 18px;
min-width: 110px;
border: none;
box-shadow: 0px 1px 12px #f2d492;
}
.content-container {
height: 50%;
width: 90%;
display: flex;
justify-content: center;
align-items: center;
}

View File

@@ -0,0 +1,52 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.1/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-iYQeCzEYFbKjA/T2uDLTpkwGzCiq6soy8tYaI1GyVh/UjpbCx/TYkiZhlZB6+fzT"
crossorigin="anonymous"
/>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.9.1/font/bootstrap-icons.css" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.0/css/all.min.css"
integrity="sha512-xh6O/CkQoPOWDdYTDqeRdPCVd1SpvCA9XXcUnZS2FmJNp1coAFzvtCN9BmamE+4aHK8yyUHUSCcJHgXloTyT2A=="
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>
<link rel="stylesheet" href="./invitation.css" />
<title>Invitation</title>
</head>
<body style="display: none">
<div class="navbar-container container">
<!-- NAVBAR: to be loaded with js -->
</div>
<div class="invitation-container">
<div class="invitation-frame">
<div class="event-name-container"></div>
<div class="content-container">
<div class="datetime-container"></div>
<div class="venue-container"></div>
</div>
<div class="join-button-container">
<button id="join-event-button" type="submit">Join Event Now</button>
</div>
</div>
</div>
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.1/dist/js/bootstrap.bundle.min.js"
integrity="sha384-u1OknCvxWvY5kfmNBILK2hRnQC3Pr17a+RTT6rIHI7NnikvbZlHgTPOOmMi466C8"
crossorigin="anonymous"
></script>
<script type="module" src="./invitation.js"></script>
</body>
</html>

View File

@@ -0,0 +1,107 @@
import { addNavbar } from '/functions/addNavbar.js';
import { loadName } from '/functions/loadName.js';
window.addEventListener('load', async () => {
await checkInvitationValidity();
});
async function checkInvitationValidity() {
const params = new URLSearchParams(window.location.search);
const eventId = params.get('event-id');
const token = params.get('token');
const res = await fetch(`/events/invitation/validation/${eventId}/${token}`, {
method: 'POST'
});
const result = await res.json();
if (result.status) {
addNavbar();
await loadName();
// Load invitation page content
document.querySelector('.event-name-container').innerHTML = `
<div>
🎉 ${result.eventDetail.name}
</div>
`;
let dateString = '';
if (!result.eventDetail.start_datetime) {
dateString += 'To Be Confirmed';
} else {
dateString += `
<div class="subtitle">
Start
</div>
<div>
${new Date(result.eventDetail.start_datetime)
.toLocaleString('en-US', { hour12: false })
.replace(', ', ' ')
.slice(0, -3)}
</div>
<div class="subtitle">
End
</div>
<div>
${new Date(result.eventDetail.end_datetime).toLocaleString('en-US', { hour12: false }).replace(', ', ' ').slice(0, -3)}
</div>
`;
}
document.querySelector('.datetime-container').innerHTML = `
<div class="title">
Date & Time
</div>
<div>
${dateString}
</div>
`;
document.querySelector('.venue-container').innerHTML = `
<div class="title">
Venue
</div>
<div>
${result.eventDetail.venue ? result.eventDetail.venue : 'To Be Confirmed'}
</div>
`;
document.body.style.display = 'block';
} else {
if (result.login) {
alert('Invitation link is invalid or expired!');
window.location.href = '/index.html';
} else {
alert('Please log in or register to join event!');
window.location.href = `/?event-id=${eventId}&token=${token}`;
}
}
}
document.querySelector('#join-event-button').addEventListener('click', async () => {
const params = new URLSearchParams(window.location.search);
const eventId = params.get('event-id');
const token = params.get('token');
const res = await fetch(`/events/invitation/participation/${eventId}/${token}`, {
method: 'POST'
});
const result = await res.json();
if (result.status) {
alert('You have successfully joined the event!');
window.location.href = `/eventSummary/event.html?event-id=${eventId}&is-creator=0`;
} else {
if (result.login) {
if (result.isCreator) {
alert('You are already a creator of the event, no need to join again!');
window.location.href = `/eventSummary/event.html?event-id=${eventId}&is-creator=1`;
} else if (result.joined) {
alert('You have already joined the event!');
window.location.href = `/eventSummary/event.html?event-id=${eventId}&is-creator=0`;
} else {
alert('Invitation link is invalid or expired!');
window.location.href = '/index.html';
}
} else {
alert('Please log in or register to join event!');
window.location.href = `/?event-id=${eventId}&token=${token}`;
}
}
});

View File

@@ -0,0 +1,302 @@
/* font family: */
/* font-family: 'Lato', sans-serif;
font-family: 'Pacifico', cursive; */
.header {
display: flex;
flex-direction: row;
justify-content: space-between;
background-color: #efefd0;
}
.navbar-brand > img {
border-radius: 10px;
}
.navbar > .navbar-brand {
font-size: 30px;
font-family: 'Pacifico', cursive;
margin: 5px;
}
.navbar > .btn {
font-size: 20px;
font-family: 'Lato', sans-serif;
color: #293241;
}
#logo-name {
font-size: 60px;
color: #293241;
font-family: 'Pacifico', cursive;
display: flex;
justify-content: center;
padding: 30px;
}
.login-part {
display: flex;
justify-items: center;
}
.user-login {
display: flex;
align-content: center;
}
.landing-Page-login-btn {
font-size: 20px;
color: #495871;
}
.landing-Page-login-btn > .bi {
margin: 0px 10px 0px 2px;
}
.introduction-box {
background-color: #efefd0;
border-radius: 20px;
display: flex;
flex-direction: column;
align-items: center;
height: 500px;
margin-top: 20px;
}
.description-box {
background-color: #a6b7f4;
width: 400px;
border-radius: 20px;
height: 220px;
display: flex;
flex-direction: column;
align-items: center;
}
.description {
font-size: 30px;
font-family: 'Lato', sans-serif;
display: flex;
flex-direction: column;
align-items: center;
padding: 30px;
}
.description-details {
font-size: 18px;
font-family: 'Lato', sans-serif;
padding: 10px;
width: 320px;
}
.buttons {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin-top: 20px;
}
.buttons > button {
font-size: 20px;
color: #293241;
font-family: 'Lato', sans-serif;
background-color: #f2d492;
padding: 10px 20px 10px 20px;
border-radius: 20px;
margin: 18px;
width: 200px;
box-sizing: border-box;
border: none;
box-shadow: 0px 1px 14px #f2d492;
touch-action: manipulation;
}
#logo-name {
font-size: 60px;
font-family: 'Pacifico', cursive;
display: flex;
justify-content: center;
padding: 30px;
}
.introduction-box {
background-color: #efefd0;
border-radius: 20px;
display: flex;
flex-direction: column;
align-items: center;
height: 500px;
margin-top: 20px;
}
.description-box {
background-color: #a6b7f4;
width: 90%;
border-radius: 20px;
height: 220px;
display: flex;
flex-direction: column;
align-items: center;
}
.description {
font-size: 30px;
font-family: 'Lato', sans-serif;
display: flex;
flex-direction: column;
align-items: center;
padding: 30px;
}
.description-details {
font-size: 18px;
font-family: 'Lato', sans-serif;
padding: 10px;
width: 320px;
}
.buttons {
display: flex;
flex-direction: column;
justify-content: center;
margin-top: 20px;
}
.buttons > button {
font-size: 20px;
color: #293241;
font-family: 'Lato', sans-serif;
background-color: #f2d492;
padding: 10px 20px 10px 20px;
border-radius: 20px;
margin: 18px;
width: 200px;
box-sizing: border-box;
border: none;
box-shadow: 0px 1px 14px #f2d492;
touch-action: manipulation;
}
/* Modal */
.modal-login {
font-size: 14px;
color: #293241;
font-family: 'Lato', sans-serif;
background-color: #f2d492;
padding: 10px 20px 10px 20px;
border-radius: 20px;
margin: 18px;
width: 110px;
border: none;
box-shadow: 0px 1px 12px #f2d492;
}
.modal-footer {
display: flex;
flex-direction: column;
justify-content: center;
align-content: center;
}
.modal-login, .modal-login:hover {
font-size: 14px;
color: #293241;
font-family: 'Lato', sans-serif;
background-color: #f2d492;
padding: 10px 20px 10px 20px;
border-radius: 20px;
margin: 18px;
width: 110px;
border: none;
box-shadow: 0px 1px 12px #f2d492;
text-decoration: none;
}
.modal-login:hover {
text-decoration: none;
}
.modal-header {
display: flex;
justify-content: space-between;
}
.modal-title {
font-size: 25px;
font-weight: bold;
color: #293241;
font-family: 'Lato', sans-serif;
}
.form-label {
color: #293241;
font-family: 'Lato', sans-serif;
}
.input-group {
display: flex;
flex-direction: column;
justify-content: center;
}
.input-title {
text-align: center;
}
.form-control {
border-width: 2px;
border-radius: 10px;
}
#user-email {
border-color: #495871;
}
#user-password {
border-color: #ee6c4d;
}
#login-form-submit,
#register-submit {
width: 100%;
}
.form-label {
position: absolute;
top: -12px;
left: 20px;
background-color: white;
}
.form-header {
margin: 0 0 5px 0;
}
#login-modal .input-panel {
margin: 20px 20px;
position: relative;
}
#register-modal .input-panel {
margin: 0 20px;
}
.user-email .form-label {
color: #495871;
}
.user-password .form-label {
color: #ee6c4d;
}
#or-separator {
position: absolute;
top: -18px;
background-color: white;
}
#google-separator {
position: relative;
}

View File

@@ -0,0 +1,163 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.1/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-iYQeCzEYFbKjA/T2uDLTpkwGzCiq6soy8tYaI1GyVh/UjpbCx/TYkiZhlZB6+fzT" crossorigin="anonymous" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.9.1/font/bootstrap-icons.css" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.0/css/all.min.css"
integrity="sha512-xh6O/CkQoPOWDdYTDqeRdPCVd1SpvCA9XXcUnZS2FmJNp1coAFzvtCN9BmamE+4aHK8yyUHUSCcJHgXloTyT2A=="
crossorigin="anonymous" referrerpolicy="no-referrer" />
<link rel="stylesheet" href="./landingPage.css" />
<title>Party Planner</title>
</head>
<body style="display: none">
<div class="navbar-container container">
<!-- NAVBAR:to be added by js -->
</div>
<div class="container">
<div class="row">
<div class="col-md-8">
<div class="introduction-box">
<header id="logo-name">Party Planner</header>
<div class="description-box">
<div class="description">Easy to use!</div>
<div class="description-details">
Try using the Party Planner to manage your party agenda with friends!
</div>
</div>
</div>
</div>
<div class="col-md-4 buttons">
<button type="button" class="login-button" data-bs-toggle="modal" data-bs-target="#login-modal">
Log in
</button>
<button type="button" class="signup-button" data-bs-toggle="modal" data-bs-target="#register-modal">
Sign up
</button>
</div>
</div>
</div>
<div class="modal fade" id="login-modal" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1"
aria-labelledby="staticBackdropLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<div></div>
<h5 class="modal-title">LOG IN</h5>
<div class="exit-modal">
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
</div>
<div class="modal-body">
<div class="input-group">
<div class="input-title">Welcome back! Please enter your details.</div>
<form>
<div class="user-email input-panel mb-3">
<div class="form-label">Email address</div>
<input type="email" class="form-control" id="user-email" aria-describedby="emailHelp"
required />
<div id="emailHelp" class="form-text">
We'll never share your email with anyone else.
</div>
</div>
<div class="user-password input-panel mb-3">
<div class="form-label">Password</div>
<input type="password" class="form-control" id="user-password" required />
</div>
</form>
</div>
<div class="modal-footer">
<button id="login-form-submit" type="button" class="modal-login">Log in</button>
</div>
<div class="modal-footer" id="google-separator">
<span id="or-separator">or</span>
<a href="/connect/google" type="button" class="modal-login">
<i class="fa-brands fa-google"></i>
Google
</a>
</div>
</div>
</div>
</div>
</div>
<div class="modal fade" id="register-modal" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1"
aria-labelledby="staticBackdropLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<div></div>
<h5 class="modal-title">CREATE ACCOUNT</h5>
<div class="exit-modal">
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
</div>
<div class="modal-body">
<div class="input-group">
<form class="register-form">
<div class="input-panel mb-3">
<div class="form-header">First Name *</div>
<input type="text" class="form-control" name="first_name" aria-label="first_name"
aria-describedby="basic-addon1" required />
</div>
<div class="input-panel mb-3">
<div class="form-header">Last Name *</div>
<input type="text" class="form-control" name="last_name" aria-label="last_name"
aria-describedby="basic-addon1" required />
</div>
<div class="input-panel mb-3">
<div class="form-header">Email Address *</div>
<input type="email" class="form-control" name="email" aria-label="email"
aria-describedby="basic-addon1" placeholder="name@example.com" required />
</div>
<div class="input-panel mb-3">
<div class="form-header">Phone Number *</div>
<input type="text" class="form-control" name="phone" aria-label="phone"
aria-describedby="basic-addon1" pattern="[0-9]{3}-[0-9]{3}-[0-9]{4}" required />
<div class="form-text">
Phone number must be in the following format: e.g. 647-111-1111
</div>
</div>
<div class="input-panel mb-3">
<div class="form-header">Create Password *</div>
<input type="password" class="form-control" name="password" aria-label="password"
aria-describedby="basic-addon1" minlength="8" required />
<div class="form-text">Password must be 8 characters long</div>
</div>
<div class="input-panel mb-3">
<div class="form-header">Confirm Password *</div>
<input type="password" class="form-control" name="confirm_password"
aria-label="confirm_password" aria-describedby="basic-addon1" minlength="8"
required />
</div>
<div class="modal-footer">
<button id="register-submit" type="submit" class="btn btn-primary">Register</button>
</div>
</form>
<div class="modal-footer" id="google-separator">
<span id="or-separator">or</span>
<a href="/connect/google" type="button" class="modal-login">
<i class="fa-brands fa-google"></i>
Google
</a>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.1/dist/js/bootstrap.bundle.min.js"
integrity="sha384-u1OknCvxWvY5kfmNBILK2hRnQC3Pr17a+RTT6rIHI7NnikvbZlHgTPOOmMi466C8"
crossorigin="anonymous"></script>
<script type="module" src="./landingPage.js"></script>
</body>
</html>

View File

@@ -0,0 +1,116 @@
import { addNavbar } from "/functions/addNavbar.js";
import { loadName } from "/functions/loadName.js";
import { addLoginNavbar } from "/functions/addLoginNavbar.js";
window.addEventListener("load", async () => {
const res = await fetch("/login/name");
if (res.status === 200) {
addNavbar();
loadName();
} else {
addLoginNavbar();
}
document.body.style.display = "block";
});
async function loginFormSubmission() {
const userEmail = document.querySelector("#user-email").value;
const userPassword = document.querySelector("#user-password").value;
if (userEmail !== "" && userPassword !== "") {
const formObj = {
email: userEmail,
password: userPassword,
};
const res = await fetch("/login", {
method: "POST",
headers: {
"content-Type": "application/json",
},
body: JSON.stringify(formObj),
});
const loginResult = await res.json();
if (loginResult.status) {
const params = new URLSearchParams(window.location.search);
if (params.has("event-id") && params.has("token")) {
const eventId = params.get("event-id");
const token = params.get("token");
window.location.href = `/invitationPage/invitation.html?event-id=${eventId}&token=${token}`;
} else {
window.location.href = "/index.html";
}
} else {
alert("Unable to login!");
}
}
}
document.querySelector("#login-form-submit").addEventListener("click", () => {
loginFormSubmission();
});
document.querySelector("#user-password").addEventListener("keypress", (event) => {
if (event.key === "Enter") {
event.preventDefault();
loginFormSubmission();
}
});
document.querySelector(".register-form").addEventListener("submit", async function (e) {
e.preventDefault();
const form = e.target;
const first_name = form.first_name.value;
const last_name = form.last_name.value;
const email = form.email.value;
const phone = form.phone.value;
const password = form.password.value;
const confirm_password = form.confirm_password.value;
let formObj = {
first_name: first_name,
last_name: last_name,
email: email,
phone: phone,
password: password,
};
let dataPass = true;
// Checking data validity
if (!(password === confirm_password)) {
dataPass = false;
alert("Password and confirm password do not match!");
}
if (dataPass) {
const res = await fetch("/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(formObj),
});
const registerResult = await res.json();
form.reset();
if (registerResult.duplicate) {
alert("Registered already!");
} else if (registerResult.passwordNotMatch) {
alert("Password not match!");
} else if (!registerResult.status) {
alert("Unable to Register, please try again!");
} else {
const params = new URLSearchParams(window.location.search);
if (params.has("event-id") && params.has("token")) {
alert("Successfully registered! Please login now to join the event.");
const myModal = bootstrap.Modal.getInstance(document.getElementById("register-modal"));
myModal.hide();
} else {
alert("Successfully registered!");
const myModal = bootstrap.Modal.getInstance(document.getElementById("register-modal"));
myModal.hide();
}
}
}
});

View File

@@ -0,0 +1,154 @@
import express, { Request, Response } from 'express';
import { client } from '../app';
import { isLoggedInAPI } from '../util/guard';
import { logger } from '../util/logger';
export const commentRoutes = express.Router();
commentRoutes.get('/', isLoggedInAPI, getComment);
commentRoutes.post('/', isLoggedInAPI, postComment);
commentRoutes.put('/', isLoggedInAPI, checkedComment);
async function getComment(req: Request, res: Response) {
try {
logger.debug('Before reading DB');
const userId = req.session.user;
const events = (
await client.query(
`
SELECT * from events
JOIN participants ON participants.event_id = events.id
WHERE participants.user_id = $1
`,
[userId]
)
).rows;
const participantComment = (
await client.query(
`
SELECT comments.id, comments.event_id, comments.content, comments.created_at, events.name, users.first_name, users.last_name, comments.read, comments.anonymous FROM participants
JOIN events ON participants.event_id = events.id
JOIN comments ON events.id = comments.event_id
JOIN users on participants.user_id = users.id
WHERE participants.user_id = $1
ORDER BY comments.created_at Desc,
comments.read Asc
`,
[userId]
)
).rows;
const creatorComment = (
await client.query(
`
SELECT comments.read, comments.anonymous, comments.id, comments.event_id, comments.content, comments.created_at, events.name, users.first_name, users.last_name FROM comments
JOIN events ON events.id = comments.event_id
JOIN users ON comments.user_id =users.id
WHERE events.creator_id = $1
ORDER BY comments.created_at Desc,
comments.read Asc
`,
[userId]
)
).rows;
res.json({
status: true,
events: events,
pComment: participantComment,
cComment: creatorComment
});
// 唔好用簡寫
} catch (e) {
logger.error(e);
res.status(500).json({
msg: '[CMT001]: Failed to get Comment'
});
}
}
async function postComment(req: Request, res: Response) {
try {
logger.debug('Before reading DB');
const userId = req.session.user;
const eventId = parseInt(req.body.receiver);
const category = req.body.category;
const comment = req.body.comment;
const anonymous = req.body.anonymous;
await client.query(
`
INSERT INTO comments
(user_id, event_id, category, content, anonymous, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, NOW(), NOW())
`,
[userId, eventId, category, comment, anonymous]
);
res.json({
status: true,
msg: 'comment sent successfully'
});
} catch (e) {
logger.error(e);
res.status(500).json({
msg: '[MSG001]: Failed to send Comment'
});
}
}
async function checkedComment(req: Request, res: Response) {
try {
logger.debug('Before reading DB');
const userId = req.session.user;
const commentId = parseInt(req.body.commentId);
const eventId = req.body.eventId;
const read = req.body.check;
let isCreator = true;
const creatorEvent = (
await client.query(
`
SELECT creator_id FROM events
WHERE id = $1
`,
[eventId]
)
).rows[0];
if (creatorEvent.creator_id !== userId) {
isCreator = false;
}
if (isCreator) {
await client.query(
`
UPDATE comments
SET read = $1,
updated_at = $2
WHERE id = $3
`,
[read, 'now()', commentId]
);
res.json({
status: true,
msg: 'Checked/Unchecked'
});
} else {
res.status(400).json({
status: false,
msg: 'Unauthorized Request'
});
}
} catch (e) {
logger.error(e);
res.status(500).json({
msg: '[MSG002]: Failed to edit Comment'
});
}
}

View File

@@ -0,0 +1,385 @@
import express, { Request, Response } from 'express';
import { client } from '../app';
import { logger } from '../util/logger';
export const datetimePollRoutes = express.Router();
datetimePollRoutes.get('/:id', getPollOptions);
datetimePollRoutes.post('/:id', createPoll);
datetimePollRoutes.delete('/:id', deletePoll);
datetimePollRoutes.post('/replacement/:id', overwriteTerminatedPoll);
datetimePollRoutes.post('/vote/:event_id/:vote_id', submitVoteChoice);
async function getPollOptions(req: Request, res: Response) {
try {
logger.debug('Before reading DB');
const eventId = parseInt(req.params.id);
const userId = req.session.user;
const [eventDetail] = (
await client.query(
`
SELECT * FROM events WHERE id = $1 AND creator_id = $2;
`,
[eventId, userId]
)
).rows;
if (eventDetail) {
if (eventDetail.date_poll_created) {
const pollOptions = (
await client.query(
`
SELECT * FROM event_date_time WHERE event_id = $1;
`,
[eventId]
)
).rows;
let voteCounts = {};
for (let pollOption of pollOptions) {
const [voteCount] = (
await client.query(
`
SELECT COUNT(*) FROM event_date_time_votes
WHERE event_date_time_id = $1;
`,
[pollOption.id]
)
).rows;
voteCounts[pollOption.id] = voteCount;
}
res.json({
status: true,
creator: true,
pollTerminated: eventDetail.date_poll_terminated,
eventDeleted: eventDetail.deleted,
pollOptions,
voteCounts
});
} else {
res.json({ status: false });
}
} else {
const [participant] = (
await client.query(
`
SELECT * FROM participants
INNER JOIN events ON events.id = participants.event_id
WHERE events.id = $1 AND participants.user_id = $2;
`,
[eventId, userId]
)
).rows;
if (participant) {
const [eventDetailParticipant] = (
await client.query(
`
SELECT * FROM events WHERE id = $1;
`,
[eventId]
)
).rows;
if (eventDetailParticipant.date_poll_created) {
const pollOptions = (
await client.query(
`
SELECT * FROM event_date_time WHERE event_id = $1;
`,
[eventId]
)
).rows;
let voteCounts = {};
for (let pollOption of pollOptions) {
const [voteCount] = (
await client.query(
`
SELECT COUNT(*) FROM event_date_time_votes
WHERE event_date_time_id = $1;
`,
[pollOption.id]
)
).rows;
voteCounts[pollOption.id] = voteCount;
}
const [choiceMade] = (
await client.query(
`
SELECT * FROM event_date_time_votes
WHERE event_date_time_id IN (SELECT id FROM event_date_time
WHERE event_id = $1)
AND user_id = $2;
`,
[eventId, userId]
)
).rows;
let chosenDateTime;
if (choiceMade) {
[chosenDateTime] = (
await client.query(
`
SELECT * FROM event_date_time
WHERE id = $1;
`,
[choiceMade.event_date_time_id]
)
).rows;
}
res.json({
status: true,
creator: false,
pollTerminated: eventDetailParticipant.date_poll_terminated,
eventDeleted: eventDetailParticipant.deleted,
choice: choiceMade
? {
id: `option_${choiceMade.event_date_time_id}`,
start: `${chosenDateTime.start_datetime}`,
end: `${chosenDateTime.end_datetime}`
}
: '',
pollOptions,
voteCounts
});
} else {
res.json({ status: false });
}
} else {
res.json({ status: false });
}
}
} catch (e) {
logger.error(e);
res.status(500).json({
msg: '[DTP001]: Failed to get datetime poll options'
});
}
}
async function createPoll(req: Request, res: Response) {
try {
logger.debug('Before reading DB');
const eventId = parseInt(req.params.id);
const [eventDetail] = (
await client.query(
`
SELECT * FROM events
WHERE id = $1 AND creator_id = $2;
`,
[eventId, req.session.user]
)
).rows;
if (eventDetail) {
if (!eventDetail.date_poll_created) {
const inputList = req.body;
for (let input of inputList) {
await client.query(
`
INSERT INTO event_date_time (start_datetime,end_datetime, event_id, created_at, updated_at)
VALUES ($1,$2,$3,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP);
`,
[input.start, input.end, eventId]
);
// date_poll_Created is not necessary
await client.query(
`
UPDATE events
SET date_poll_created = TRUE
WHERE id = $1;
`,
[eventId]
);
}
res.json({ status: true });
} else {
res.json({
status: false,
created: true
});
}
} else {
res.json({
status: false
});
}
} catch (e) {
logger.error(e);
res.status(500).json({
msg: '[DTP002]: Failed to create datetime poll'
});
}
}
async function deletePoll(req: Request, res: Response) {
try {
logger.debug('Before reading DB');
const eventId = parseInt(req.params.id);
const [eventDetail] = (
await client.query(
`
SELECT * FROM events
WHERE id = $1 AND creator_id = $2;
`,
[eventId, req.session.user]
)
).rows;
if (eventDetail) {
if (eventDetail.date_poll_created) {
if (!eventDetail.date_poll_terminated) {
await client.query(
`
UPDATE events SET date_poll_terminated = TRUE
WHERE id = $1;
`,
[eventId]
);
res.json({ status: true });
} else {
res.json({
status: false,
terminated: true
});
}
} else {
res.json({
status: false,
noPoll: true
});
}
} else {
res.json({ status: false });
}
} catch (e) {
logger.error(e);
res.status(500).json({
msg: '[DTP003]: Failed to delete datetime poll'
});
}
}
async function overwriteTerminatedPoll(req: Request, res: Response) {
try {
logger.debug('Before reading DB');
const eventId = parseInt(req.params.id);
const [eventDetail] = (
await client.query(
`
SELECT * FROM events
WHERE id = $1 AND creator_id = $2;
`,
[eventId, req.session.user]
)
).rows;
if (eventDetail) {
// Initialize the polling data
await client.query(
`
DELETE FROM event_date_time_votes
WHERE event_date_time_id IN (SELECT id FROM event_date_time
WHERE event_id = $1);
`,
[eventId]
);
await client.query(
`
DELETE FROM event_date_time WHERE event_id = $1;
`,
[eventId]
);
await client.query(
`
UPDATE events
SET date_poll_created = FALSE,
date_poll_terminated = FALSE
WHERE id = $1;
`,
[eventId]
);
const inputList = req.body;
for (let input of inputList) {
await client.query(
`
INSERT INTO event_date_time (start_datetime,end_datetime, event_id, created_at, updated_at)
VALUES ($1,$2,$3,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP);
`,
[input.start, input.end, eventId]
);
await client.query(
`
UPDATE events
SET date_poll_created = TRUE
WHERE id = $1;
`,
[eventId]
);
}
res.json({ status: true });
} else {
res.json({
status: false
});
}
} catch (e) {
logger.error(e);
res.status(500).json({
msg: '[DTP004]: Failed to overwrite datetime poll'
});
}
}
async function submitVoteChoice(req: Request, res: Response) {
try {
logger.debug('Before reading DB');
const eventId = parseInt(req.params.event_id);
const userId = req.session.user;
const [participant] = (
await client.query(
`
SELECT * FROM participants
INNER JOIN events ON events.id = participants.event_id
WHERE participants.user_id = $1
AND events.id = $2;
`,
[userId, eventId]
)
).rows;
if (participant) {
const [choiceMade] = (
await client.query(
`
SELECT * FROM event_date_time_votes
WHERE event_date_time_id IN (SELECT id FROM event_date_time
WHERE event_id = $1);
`,
[eventId]
)
).rows;
if (!choiceMade) {
await client.query(
`
INSERT INTO event_date_time_votes
(event_date_time_id,user_id,created_at,updated_at)
VALUES ($1,$2,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP);
`,
[parseInt(req.params.vote_id), userId]
);
res.json({ status: true });
} else {
res.json({
status: false,
duplicate: true
});
}
} else {
res.json({ status: false });
}
} catch (e) {
logger.error(e);
res.status(500).json({
msg: '[DTP005]: Failed to submit datetime vote'
});
}
}

View File

@@ -0,0 +1,251 @@
import express, { Request, Response } from 'express';
import { client } from '../app';
import { logger } from '../util/logger';
import crypto from 'crypto';
export const eventDetailsRoutes = express.Router();
eventDetailsRoutes.get('/created/:id', getCreatedEventDetails);
eventDetailsRoutes.get('/participated/:id', getParticipatedEventDetails);
eventDetailsRoutes.get('/invitation/:id', getInvitationLink);
eventDetailsRoutes.put('/datetime/:id', updateDateTime);
eventDetailsRoutes.put('/venue/:id', updateVenue);
async function getCreatedEventDetails(req: Request, res: Response) {
try {
logger.debug('Before reading DB');
const eventId = req.params.id;
const [event] = (
await client.query(
`
SELECT * FROM events
WHERE id = $1
AND creator_id = $2;
`,
[parseInt(eventId), req.session.user]
)
).rows;
if (event) {
const [creatorDetail] = (
await client.query(
`
SELECT * FROM users
WHERE id = $1;
`,
[req.session.user]
)
).rows;
const participantList = (
await client.query(
`
SELECT users.id, users.first_name, users.last_name FROM users
INNER JOIN participants ON participants.user_id = users.id
INNER JOIN events ON participants.event_id = events.id
WHERE events.id = $1;
`,
[parseInt(eventId)]
)
).rows;
res.json({
status: true,
creator: {
id: creatorDetail.id,
first_name: creatorDetail.first_name,
last_name: creatorDetail.last_name
},
detail: event,
participants: participantList
});
} else {
res.json({
status: false
});
}
} catch (e) {
logger.error(e);
res.status(500).json({
msg: '[ETD001]: Failed to get Created Event Details'
});
}
}
async function getParticipatedEventDetails(req: Request, res: Response) {
try {
logger.debug('Before reading DB');
const eventId = req.params.id;
/*
events
INNER join users as creator on events.creator_id = users.id
INNER JOIN participants on ON participants.event_id = events.id
INNER JOIN users ON participants.user_id = users.id
*/
const [event] = (
await client.query(
`
SELECT events.* FROM events
INNER JOIN participants ON participants.event_id = events.id
INNER JOIN users ON participants.user_id = users.id
WHERE events.id = $1 AND users.id = $2;
`,
[parseInt(eventId), req.session.user]
)
).rows;
if (event) {
const [creatorDetail] = (
await client.query(
`
SELECT * FROM users
INNER JOIN events ON events.creator_id = users.id
WHERE events.id = $1;
`,
[parseInt(eventId)]
)
).rows;
const participantList = (
await client.query(
`
SELECT users.id, users.first_name, users.last_name FROM users
INNER JOIN participants ON participants.user_id = users.id
INNER JOIN events ON participants.event_id = events.id
WHERE events.id = $1;
`,
[parseInt(eventId)]
)
).rows;
res.json({
status: true,
creator: {
id: creatorDetail.id,
first_name: creatorDetail.first_name,
last_name: creatorDetail.last_name
},
detail: event,
participants: participantList
});
} else {
res.json({
status: false
});
}
} catch (e) {
logger.error(e);
res.status(500).json({
msg: '[ETD002]: Failed to get Participated Event Details'
});
}
}
async function getInvitationLink(req: Request, res: Response) {
try {
logger.debug('Before reading DB');
const eventId = req.params.id;
const [event] = (
await client.query(
`
SELECT * FROM events
WHERE id = $1
AND creator_id = $2;
`,
[parseInt(eventId), req.session.user]
)
).rows;
if (event) {
const invitation_token = crypto.randomBytes(64).toString('hex');
await client.query(
`
UPDATE events SET invitation_token = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2;
`,
[invitation_token, parseInt(eventId)]
);
res.json({
status: true,
invitation_token
});
} else {
res.json({ status: false });
}
} catch (e) {
logger.error(e);
res.status(500).json({
msg: '[ETD005]: Failed to copy invitation link'
});
}
}
async function updateDateTime(req: Request, res: Response) {
try {
logger.debug('Before reading DB');
const eventId = req.params.id;
// CORRECT
const [event] = (
await client.query(
`
SELECT * FROM events
WHERE id = $1
AND creator_id = $2;
`,
[parseInt(eventId), req.session.user]
)
).rows;
if (event) {
await client.query(
`
UPDATE events
SET start_datetime = $1, end_datetime = $2, updated_at = CURRENT_TIMESTAMP
WHERE id = $3;
`,
[req.body.startTime, req.body.endTime, parseInt(eventId)]
);
res.json({ status: true });
} else {
res.json({ status: false });
}
} catch (e) {
logger.error(e);
res.status(500).json({
msg: '[ETD003]: Failed to update date/time'
});
}
}
async function updateVenue(req: Request, res: Response) {
try {
logger.debug('Before reading DB');
const eventId = req.params.id;
const [event] = (
await client.query(
`
SELECT * FROM events
WHERE id = $1
AND creator_id = $2;
`,
[parseInt(eventId), req.session.user]
)
).rows;
if (event) {
await client.query(
`
UPDATE events
SET venue = $1, updated_at = CURRENT_TIMESTAMP
WHERE id = $2;
`,
[req.body.venue, parseInt(eventId)]
);
res.json({ status: true });
} else {
res.json({ status: false });
}
} catch (e) {
logger.error(e);
res.status(500).json({
msg: '[ETD004]: Failed to update venue'
});
}
}

View File

@@ -0,0 +1,103 @@
import express, { Request, Response } from 'express';
import { client } from '../app';
import { logger } from '../util/logger';
export const eventInvitationRoutes = express.Router();
eventInvitationRoutes.post('/validation/:eventId/:token', validateInvitationToken);
eventInvitationRoutes.post('/participation/:eventId/:token', joinEvent);
async function validateInvitationToken(req: Request, res: Response) {
try {
const [eventDetail] = (
await client.query(
`
SELECT * FROM events
WHERE id = $1 AND invitation_token = $2;
`,
[req.params.eventId, req.params.token]
)
).rows;
if (eventDetail) {
res.json({
status: true,
eventDetail
});
} else {
res.json({
status: false,
login: true // 唔洗問client side 的因為server 本身知
});
}
} catch (e) {
logger.error(e);
res.status(500).json({
msg: '[ETD005]: Failed to validate invitation link'
});
}
}
async function joinEvent(req: Request, res: Response) {
try {
const [eventDetail] = (
await client.query(
`
SELECT * FROM events
WHERE id = $1 AND invitation_token = $2;
`,
[req.params.eventId, req.params.token]
)
).rows;
if (eventDetail) {
if (eventDetail.creator_id === req.session.user) {
res.json({
status: false,
login: true,
isCreator: true
});
} else {
const [participant] =
// Insert On Conflict
// Select -> exists -> update
// |-> not exists -> insert
(
await client.query(
`
SELECT * FROM participants
WHERE event_id = $1 AND user_id = $2;
`,
[req.params.eventId, req.session.user]
)
).rows;
if (participant) {
res.json({
status: false,
login: true,
joined: true
});
} else {
await client.query(
`
INSERT INTO participants (event_id, user_id, created_at, updated_at)
VALUES ($1, $2, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);
`,
[req.params.eventId, req.session.user]
);
res.json({ status: true });
}
}
} else {
res.json({
status: false,
login: true
});
}
} catch (e) {
logger.error(e);
res.status(500).json({
msg: '[ETD006]: Failed to join event'
});
}
}

View File

@@ -0,0 +1,246 @@
import express, { Request, Response } from 'express';
import { client } from '../app';
import { Events } from '../util/models';
import { onlyNumbers } from '../util/functions/onlyNumbers';
import { logger } from '../util/logger';
import { isLoggedInAPI, isLoggedInInvitation } from '../util/guard';
import { eventDetailsRoutes } from './eventDetailsRoutes';
import { venuePollRoutes } from './venuePollRoutes';
import { datetimePollRoutes } from './datetimePollRoutes';
import crypto from 'crypto';
import { eventInvitationRoutes } from './eventInvitationRoutes';
export const eventsRoutes = express.Router();
eventsRoutes.get('/created', isLoggedInAPI, getCreateEventList);
eventsRoutes.get('/participated', isLoggedInAPI, getParticipateEventList);
eventsRoutes.post('/', isLoggedInAPI, postEvent);
eventsRoutes.delete('/:eventId', isLoggedInAPI, deleteEvent);
eventsRoutes.delete('/participants/:eventId', isLoggedInAPI, deleteParticipants);
eventsRoutes.use('/detail', isLoggedInAPI, eventDetailsRoutes);
eventsRoutes.use('/invitation', isLoggedInInvitation, eventInvitationRoutes);
eventsRoutes.use('/poll/venue', isLoggedInAPI, venuePollRoutes);
eventsRoutes.use('/poll/datetime', isLoggedInAPI, datetimePollRoutes);
// getCreatedEvents
async function getCreateEventList(req: Request, res: Response) {
try {
logger.debug('Before reading DB');
const [columnCountObject] = (
await client.query(`SELECT COUNT(*) FROM events WHERE creator_id = $1 `, [req.session.user || 0])
).rows;
const columnCount = parseInt(columnCountObject.count);
let currentPage = req.query.page + '';
let offset: number = onlyNumbers(currentPage) ? (parseInt(currentPage) - 1) * 10 : 0;
if (!columnCount) {
currentPage = '1';
offset = 0;
} else if (Math.ceil(columnCount / 10) < parseInt(currentPage)) {
currentPage = Math.ceil(columnCount / 10).toString();
offset = (Math.ceil(columnCount / 10) - 1) * 10;
}
const result = await client.query(
`
SELECT * FROM events
WHERE creator_id = $1
ORDER BY start_datetime DESC, id DESC
LIMIT 10 OFFSET $2;
`,
[req.session.user || 0, offset]
);
const eventList: Events[] = result.rows;
res.json({
object: eventList, // should be called eventList or events
currentPage: currentPage,
page: columnCount ? Math.ceil(columnCount / 10) : 1
});
} catch (e) {
logger.error(e);
res.status(500).json({
msg: '[EVT001]: Failed to get Created Event List'
});
}
}
// getParticipatedEvents
async function getParticipateEventList(req: Request, res: Response) {
try {
logger.debug('Before reading DB');
const [columnCountObject] = (
await client.query(
`SELECT COUNT(events.*) FROM events
INNER JOIN participants ON participants.event_id = events.id
INNER JOIN users ON participants.user_id = users.id
WHERE users.id = $1;`,
[req.session.user || 0]
)
).rows;
const columnCount = parseInt(columnCountObject.count);
let currentPage = req.query.page as string;
let offset: number = onlyNumbers(currentPage) ? (parseInt(currentPage) - 1) * 10 : 0;
if (!columnCount) {
currentPage = '1';
offset = 0;
} else if (Math.ceil(columnCount / 10) < parseInt(currentPage)) {
currentPage = Math.ceil(columnCount / 10).toString();
offset = (Math.ceil(columnCount / 10) - 1) * 10;
}
const result = await client.query(
`
SELECT events.* FROM events
INNER JOIN participants ON participants.event_id = events.id
INNER JOIN users ON participants.user_id = users.id
WHERE users.id = $1
ORDER BY events.start_datetime DESC, events.id DESC
LIMIT 10 OFFSET $2;
`,
[req.session.user || 0, offset]
);
const eventList: Events[] = result.rows;
res.json({
object: eventList,
currentPage: currentPage,
page: columnCount ? Math.ceil(columnCount / 10) : 1
});
} catch (e) {
logger.error(e);
res.status(500).json({
msg: '[EVT002]: Failed to get Participated Event List'
});
}
}
async function postEvent(req: Request, res: Response) {
try {
logger.debug('Before reading DB');
const invitation_token = crypto.randomBytes(64).toString('hex');
await client.query(
`INSERT INTO events
(name, venue, start_datetime, end_datetime,
creator_id, invitation_token, deleted,
date_poll_created,
date_poll_terminated,
venue_poll_created,
venue_poll_terminated,
created_at, updated_at)
VALUES ($1,$2,$3,$4,$5,$6,FALSE,FALSE,FALSE,FALSE,FALSE,
CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)`,
[
req.body.eventName,
req.body.eventVenue,
req.body.startTime,
req.body.endTime,
req.session.user,
invitation_token
]
);
res.json({ msg: 'Posted to DB' });
} catch (e) {
logger.error(e);
res.status(500).json({ msg: '[EVT003]: Failed to post Event' });
}
}
async function deleteParticipants(req: Request, res: Response) {
try {
logger.debug('Before reading DB');
const eventId = req.params.eventId ? parseInt(req.params.eventId) : 0;
const [eventDetail] = (
await client.query(
`
SELECT * FROM events
WHERE creator_id = $1
AND id = $2;
`,
[req.session.user, eventId]
)
).rows;
const deletedParticipants: { id: number }[] = req.body.deletedParticipants;
if (eventDetail) {
const participantsWithItemAssigned = await client.query(
`
SELECT user_id FROM items
WHERE user_id = ANY($1::int[])
`,
[deletedParticipants.map((p) => p.id)]
);
console.log(participantsWithItemAssigned);
// Removed all of the ids from participantsWithItemAssigned
// Then run delete
let notDeletable = [];
for (let deletedParticipant of req.body) {
// n + 1 problem
const itemInCharge = (
await client.query(
`
SELECT * FROM items
WHERE user_id = $1 AND event_id = $2 AND purchased = FALSE;
`,
[deletedParticipant.id, eventId]
)
).rows;
if (itemInCharge.length) {
notDeletable.push({
deletedParticipant,
itemInCharge
});
} else {
await client.query(
`
DELETE FROM participants WHERE user_id = $1 and event_id = $2;
`,
[deletedParticipant.id, eventId]
);
}
}
res.json({
status: true,
notDeletable
});
} else {
res.status(500).json({ status: false });
}
} catch (e) {
logger.error(e);
res.status(500).json({
msg: '[EVT004]: Failed to delete Participants'
});
}
}
// Archived
async function deleteEvent(req: Request, res: Response) {
try {
logger.debug('Before reading DB');
const eventId = req.params.eventId ? parseInt(req.params.eventId) : 0;
const [eventDetail] = (
await client.query(
`
SELECT * FROM events
WHERE creator_id = $1
AND id = $2;
`,
[req.session.user, eventId]
)
).rows;
if (eventDetail) {
// Marked Delete
await client.query(
`
UPDATE events SET deleted = TRUE
WHERE id = $1;
`,
[eventId]
);
res.json({ status: true });
} else {
res.json({ status: false });
}
} catch (e) {
logger.error(e);
res.status(500).json({
msg: '[EVT005]: Failed to delete Event'
});
}
}

View File

@@ -0,0 +1,175 @@
import express, { Request, Response } from 'express';
import { logger } from '../util/logger';
import { client } from '../app';
export const itemsRoutes = express.Router();
itemsRoutes.get('/participated', getParticipateEventList);
itemsRoutes.get('/', getItem);
itemsRoutes.post('/eventId/:id', postItem);
itemsRoutes.delete('/:id', deleteItem);
itemsRoutes.get('/pendingItems', getPendingItem);
itemsRoutes.put('/pendingItems/:id', updateItemStatus);
enum TypeName {
Food = 'food',
Drink = 'drink',
Decoration = 'decoration',
Other = 'other'
}
export type ItemType = 'food' | 'drink' | 'decoration' | 'other';
async function getItem(req: Request, res: Response) {
try {
logger.debug('Before reading DB');
// Can use group by
const itemResult = await client.query(
`
SELECT items.type_name, items.name, items.quantity, items.price, items.id, users.first_name, users.last_name
FROM items
INNER JOIN users ON users.id = items.user_id
WHERE event_id = $1
`,
[req.query.eventID]
);
const itemObj = {
[TypeName.Food]: [],
[TypeName.Drink]: [],
[TypeName.Decoration]: [],
[TypeName.Other]: []
};
for (const items of itemResult.rows) {
itemObj[items.type_name].push(items);
}
res.json({ itemObj, status: true, msg: 'get item from DB' });
} catch (e) {
logger.error(e);
res.status(500).json({ msg: '[ITM001]: Failed to post Item' });
}
}
async function getParticipateEventList(req: Request, res: Response) {
try {
logger.debug('Before reading DB');
const participateResult = await client.query(
`
SELECT users.first_name, users.last_name, users.id
FROM participants
INNER JOIN users ON users.id = participants.user_id
WHERE event_id =$1
`,
[req.query.eventID]
);
res.json({
user: participateResult.rows,
status: true,
msg: 'get participant from DB'
});
} catch (e) {
logger.error(e);
res.status(500).json({ msg: '[ITM002]: Failed to post Item' });
}
}
async function postItem(req: Request, res: Response) {
try {
logger.debug('Before reading DB');
const result = await client.query(
`INSERT INTO items
(type_name, name, quantity, price, user_id, event_id, purchased,
created_at, updated_at )
VALUES ($1,$2,$3,$4,$5,$6,FALSE, NOW(), NOW())
RETURNING *
`,
[
req.body.typeName,
req.body.itemName,
req.body.itemQuantity,
req.body.itemPrice,
req.body.user_id,
req.params.id
]
);
res.json({ result: result.rows, status: true, msg: 'Posted to DB' });
} catch (e) {
logger.error(e);
res.status(500).json({ msg: '[ITM005]: Failed to post Item' });
}
}
async function deleteItem(req: Request, res: Response) {
try {
logger.debug('Before reading DB');
await client.query(
`
DELETE FROM items where items.id = $1
`,
[req.params.id]
);
res.json({ status: true, msg: 'successfully delete' });
} catch (e) {
logger.error(e);
res.status(500).json({ msg: '[ITM006]: Failed to post Item' });
}
}
async function getPendingItem(req: Request, res: Response) {
try {
logger.debug('Before reading DB');
const result = await client.query(
`
SELECT items.name, items.id, items.type_name FROM items
WHERE purchased = false AND event_id = $1
`,
[req.query.eventID]
);
const itemObj = {
[TypeName.Food]: [],
[TypeName.Drink]: [],
[TypeName.Decoration]: [],
[TypeName.Other]: []
};
for (const items of result.rows) {
itemObj[items.type_name].push(items);
}
res.json({ itemObj, status: true, msg: 'get pending items from DB' });
} catch (e) {
logger.error(e);
res.status(500).json({ msg: '[ITM007]: Failed to post Pending Items' });
}
}
async function updateItemStatus(req: Request, res: Response) {
try {
logger.debug('Before reading DB');
const result = await client.query(
`
UPDATE items SET purchased = true
WHERE items.id = $1
`,
[req.params.id]
);
res.json({
updateItem: result.rows,
status: true,
msg: 'update pending items from DB'
});
} catch (e) {
logger.error(e);
res.status(500).json({
msg: '[ITM008]: Failed to update Pending Items'
});
}
}

View File

@@ -0,0 +1,122 @@
import express, { Request, Response } from 'express';
import { logger } from '../util/logger';
import { client } from '../app';
import { checkPassword } from '../util/functions/hash';
import fetch from 'cross-fetch';
import crypto from 'crypto';
export const loginRoutes = express.Router();
loginRoutes.get('/', checkSessionLogin); // not necessary
loginRoutes.post('/', login);
loginRoutes.get('/name', getName);
loginRoutes.post('/logout', logout);
loginRoutes.get('/google', loginGoogle);
//not necessary
async function checkSessionLogin(req: Request, res: Response) {
try {
logger.debug('Before reading DB');
if (req.session.user) {
const loginUser = (await client.query(`SELECT * FROM users WHERE id = $1`, [req.session.user])).rows[0];
if (loginUser) {
res.json({ status: true });
} else {
res.status(401).json({ status: false });
}
} else {
res.status(401).json({ status: false });
}
} catch (e) {
logger.error(e);
res.status(500).json({ msg: '[LOG001]: Failed to check Login' });
}
}
async function login(req: Request, res: Response) {
try {
logger.debug('Before reading DB');
const loginUser = (await client.query(`SELECT * FROM users WHERE email = $1`, [req.body.email])).rows[0];
if (loginUser) {
const match = await checkPassword(req.body.password, loginUser.password);
if (match) {
req.session.user = loginUser.id;
res.json({
status: true,
user: loginUser.email
});
} else {
res.status(401).json({ status: false });
}
} else {
res.status(401).json({ status: false });
}
} catch (e) {
logger.error(e);
res.status(500).json({ msg: '[LOG002]: Failed to check Login' });
}
}
async function getName(req: Request, res: Response) {
try {
logger.debug('Before reading DB');
const userName = (await client.query(`SELECT * FROM users WHERE id = $1`, [req.session.user])).rows[0];
if (userName) {
res.json({
status: true,
user: userName.first_name
});
} else {
res.status(401).json({ status: false });
}
} catch (e) {
logger.error(e);
res.status(500).json({ msg: '[LOG003]: Failed to get Name' });
}
}
async function logout(req: Request, res: Response) {
try {
logger.debug('Before logging out');
delete req.session.user;
res.json({ status: true });
} catch (e) {
logger.error(e);
res.status(500).json({ msg: '[LOG004]: Failed to Logout' });
}
}
async function loginGoogle(req: express.Request, res: express.Response) {
const accessToken = req.session?.['grant'].response.access_token;
const fetchRes = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
method: 'get',
headers: {
Authorization: `Bearer ${accessToken}`
}
});
const result = await fetchRes.json();
const password = `google_user_` + crypto.randomBytes(20).toString('hex');
const users = (await client.query(`SELECT * FROM users WHERE email = $1`, [result.email])).rows;
let user = users[0];
if (!user) {
user = (
await client.query(
`INSERT INTO users (first_name, last_name, password, phone, email, created_at, updated_at)
VALUES ($1,$2,$3,$4,$5,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP) RETURNING *`,
[result.given_name, result.family_name, password, '', result.email]
)
).rows[0];
console.log(`User with id ${user.id} is created`);
}
if (req.session) {
req.session.user = user.id;
}
res.redirect('/index.html');
}

View File

@@ -0,0 +1,76 @@
import express, { Request, Response } from 'express';
import { client } from '../app';
import { checkPassword, hashPassword } from '../util/functions/hash';
import { logger } from '../util/logger';
import { Users } from '../util/models';
import { isLoggedInAPI } from '../util/guard';
export const personalInfoRoutes = express.Router();
personalInfoRoutes.get('/', isLoggedInAPI, getPersonalInfo);
personalInfoRoutes.put('/', isLoggedInAPI, updatePersonalInfo);
async function getPersonalInfo(req: Request, res: Response) {
try {
logger.debug('Before reading DB');
const result = await client.query(
`SELECT * FROM users
WHERE id = $1
`,
[req.session.user]
);
const user: Users = result.rows[0];
res.json(user);
} catch (e) {
logger.error(e);
res.status(500).json({
msg: '[ERR001]: Failed to get information'
});
}
}
async function updatePersonalInfo(req: Request, res: Response) {
try {
logger.debug('Before reading DB');
await client.query(
`UPDATE users
SET first_name = $1, last_name = $2, phone = $3, updated_at = CURRENT_TIMESTAMP
WHERE id = $4`,
[req.body.first_name, req.body.last_name, req.body.phone, req.session.user]
);
if (req.body.current_password) {
//check if input password is correct
const hashedPassword = await client.query(
`SELECT password FROM users
WHERE id = $1`,
[req.session.user]
);
if (!(await checkPassword(req.body.current_password, hashedPassword.rows[0].password))) {
res.status(400);
throw new Error(`Failed login attempt from user ${req.session.user}`);
}
// update DB with new password
const password = await hashPassword(req.body.password);
await client.query(
`UPDATE users
SET password = $1, updated_at = CURRENT_TIMESTAMP
WHERE id = $2`,
[password, req.session.user]
);
}
res.json({ status: true });
} catch (e) {
logger.error(e);
res.status(400).json({
msg: '[UPD001]: Failed to update information at Database'
});
}
}

View File

@@ -0,0 +1,37 @@
import express, { Request, Response } from 'express';
import { logger } from '../util/logger';
import { client } from '../app';
import { hashPassword } from '../util/functions/hash';
export const registerRoutes = express.Router();
registerRoutes.post('/', registerUser);
async function registerUser(req: Request, res: Response) {
try {
logger.debug('Before reading DB');
// Needs to send email SMS verification if you really want to enforce them to be unique.
const loginUser = (
await client.query(`SELECT * FROM users WHERE email = $1 OR phone = $2`, [
req.body.email,
!!req.body.phone ? req.body.phone : '0'
])
).rows[0];
if (!loginUser) {
const password = await hashPassword(req.body.password);
await client.query(
`INSERT INTO users (first_name, last_name, email, phone, password, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);`,
[req.body.first_name, req.body.last_name, req.body.email, req.body.phone, password]
);
res.json({ status: true });
} else {
res.status(401).json({ status: false });
}
} catch (e) {
logger.error(e);
res.status(500).json({ msg: '[REG001]: Failed to Register' });
}
}

View File

@@ -0,0 +1,627 @@
import express, { Request, Response } from 'express';
import { client } from '../app';
import { isLoggedInAPI } from '../util/guard';
import { logger } from '../util/logger';
export const scheduleRoutes = express.Router();
scheduleRoutes.get('/', isLoggedInAPI, getEventSchedule);
scheduleRoutes.post('/activity', isLoggedInAPI, postEventSchedule);
scheduleRoutes.put('/description/edit', isLoggedInAPI, editDescription);
scheduleRoutes.put('/remark/edit', isLoggedInAPI, editRemark);
scheduleRoutes.put('/timeName/edit', isLoggedInAPI, editTimeName);
scheduleRoutes.post('/item', isLoggedInAPI, postItem);
scheduleRoutes.delete('/timeBlock/', isLoggedInAPI, deleteTimeBlock);
// select * from time_blocks where start_time
// between '2022-10-12T10:00:00' and '2022-10-12T12:00:00'
// or end_time between '2022-10-12T10:00:00' and '2022-10-12T12:00:00';
// Date <- Birthday , no need to do comparison , Time is not useful
// Time <- only if it is not a moment, but a periodic time. "Every day 3pm"
// Datetime <- everything else
async function postItem(req: Request, res: Response) {
try {
logger.debug('Before reading DB');
const creator = req.query['is-creator'];
const timeBlockId = req.query['id'];
const itemList = req.body;
const eventId = req.query['event-id'];
const event = (
await client.query(
`
SELECT start_datetime, end_datetime, deleted FROM events
WHERE id = $1
`,
[eventId]
)
).rows[0];
const isDeleted = event.deleted;
const eventStartTimeInMin = event.start_datetime.getTime();
const eventEndTimeInMin = event.end_datetime.getTime();
const now = new Date().getTime();
let isProcessing = true;
if (eventStartTimeInMin < now && eventEndTimeInMin < now) {
isProcessing = false;
//event is finished
}
if (isDeleted) {
isProcessing = false;
//event was deleted by creator
}
if (creator === '1' && isProcessing) {
// delete existing list
await client.query(
`
DELETE FROM time_block_item
WHERE time_block_item.time_block_id = $1
`,
[timeBlockId]
);
itemList.forEach(async (item: any) => {
await client.query(
`
INSERT INTO time_block_item (time_block_id, item_id, created_at, updated_at)
VALUES ($1, $2, $3, $4)
`,
[timeBlockId, `${item}`, 'now()', 'now()']
);
});
res.json({
status: true,
msg: 'Items Added'
});
} else {
res.status(400).json({
msg: '[EER001]: Unauthorized Request'
});
}
} catch (e) {
logger.error(e);
res.status(500).json({
msg: '[ITM003]: Failed to Add Show Item'
});
}
}
async function editTimeName(req: Request, res: Response) {
try {
logger.debug('Before reading DB');
const eventId = req.query['event-id'];
const creator = req.query['is-creator'];
const timeBlockId = parseInt(req.query['id'] as string);
const date = req.query.date;
const title = req.body.title;
const startTime = req.body.editStartTime;
const endTime = req.body.editEndTime;
const color = req.body.editColor;
const event = (
await client.query(
`
SELECT start_datetime, end_datetime, deleted FROM events
WHERE id = $1
`,
[eventId]
)
).rows[0];
const isDeleted = event.deleted;
const eventStartTimeInMin = event.start_datetime.getTime();
const eventEndTimeInMin = event.end_datetime.getTime();
const now = new Date().getTime();
let isProcessing = true;
if (eventStartTimeInMin < now && eventEndTimeInMin < now) {
isProcessing = false;
//event is finished
}
if (isDeleted) {
isProcessing = false;
//event was deleted by creator
}
if (creator === '1' && isProcessing) {
//check time collision with existing time-blocks
//bug: correct end time = 00:00 problem
const existingActivities = (
await client.query(
`
SELECT start_time, end_time, id FROM time_blocks
WHERE event_id = $1
AND date = $2
AND id != $3
ORDER BY start_time ASC;
`,
[eventId, date, timeBlockId]
)
).rows;
let reject = false;
const newStartTimeInMin = toMin(req.body.editStartTime);
const newEndTimeInMin = toMin(req.body.editEndTime);
existingActivities.forEach((activity) => {
const startTimeInMin = toMin(activity.start_time);
const endTimeInMin = toMin(activity.end_time);
if (newStartTimeInMin > startTimeInMin && newStartTimeInMin < endTimeInMin) {
reject = true;
console.log('1');
}
if (newEndTimeInMin > startTimeInMin && newEndTimeInMin < endTimeInMin) {
reject = true;
console.log('2');
}
if (newStartTimeInMin <= startTimeInMin && newEndTimeInMin >= endTimeInMin) {
reject = true;
console.log('3');
}
});
//writing update to DB
if (reject) {
res.status(400).json({
msg: '[EER002]: Activity Start Time or End Time Overlapped with Existing Activity'
});
} else {
await client.query(
`
UPDATE time_blocks
SET title = $1,
start_time = $2,
end_time = $3,
color = $4,
updated_at = $5
WHERE event_id = $6
AND id = $7
AND date = $8
`,
[title, startTime, endTime, color, 'now()', eventId, timeBlockId, date]
);
res.json({
status: true,
msg: 'Edit success'
});
}
} else {
res.json({
status: false,
msg: '[EER001]: Unauthorized Request'
});
}
} catch (e) {
logger.error(e);
res.status(500).json({
msg: '[TBE002]: Failed to Edit Time & Name'
});
}
}
async function editRemark(req: Request, res: Response) {
try {
logger.debug('Before reading DB');
const eventId = req.query['event-id'];
const creator = req.query['is-creator'];
const timeBlockId = req.query['id'];
const date = req.query.date;
const remark = req.body.remark;
const event = (
await client.query(
`
SELECT start_datetime, end_datetime, deleted FROM events
WHERE id = $1
`,
[eventId]
)
).rows[0];
const isDeleted = event.deleted;
const eventStartTimeInMin = event.start_datetime.getTime();
const eventEndTimeInMin = event.end_datetime.getTime();
const now = new Date().getTime();
let isProcessing = true;
if (eventStartTimeInMin < now && eventEndTimeInMin < now) {
isProcessing = false;
//event is finished
}
if (isDeleted) {
isProcessing = false;
//event was deleted by creator
}
if (creator === '1' && isProcessing) {
await client.query(
`
UPDATE time_blocks
SET remark = $1,
updated_at = $2
WHERE event_id = $3
AND id = $4
AND date = $5
`,
[remark, 'now()', eventId, timeBlockId, date]
);
res.json({
status: true,
msg: 'Edit success'
});
} else {
res.json({
status: false,
msg: '[EER001]: Unauthorized Request'
});
}
} catch (e) {
logger.error(e);
res.status(500).json({
msg: '[TBE001]: Failed to Edit Remark'
});
}
}
async function editDescription(req: Request, res: Response) {
try {
logger.debug('Before reading DB');
const eventId = req.query['event-id'];
const creator = req.query['is-creator'];
const timeBlockId = req.query['id'];
const date = req.query.date;
const description = req.body.description;
const event = (
await client.query(
`
SELECT start_datetime, end_datetime, deleted FROM events
WHERE id = $1
`,
[eventId]
)
).rows[0];
const isDeleted = event.deleted;
const eventStartTimeInMin = event.start_datetime.getTime();
const eventEndTimeInMin = event.end_datetime.getTime();
const now = new Date().getTime();
let isProcessing = true;
if (eventStartTimeInMin < now && eventEndTimeInMin < now) {
isProcessing = false;
//event is finished
}
if (isDeleted) {
isProcessing = false;
//event was deleted by creator
}
if (creator === '1' && isProcessing) {
await client.query(
`
UPDATE time_blocks
SET description = $1,
updated_at = $2
WHERE event_id = $3
AND id = $4
AND date = $5
`,
[description, 'now()', eventId, timeBlockId, date]
);
res.json({
status: true,
msg: 'Edit success'
});
} else {
res.json({
status: false,
msg: '[EER001]: Unauthorized Request'
});
}
} catch (e) {
logger.error(e);
res.status(500).json({
msg: '[TBE003]: Failed to Edit Description'
});
}
}
async function deleteTimeBlock(req: Request, res: Response) {
try {
logger.debug('Before reading DB');
const eventId = req.query['event-id'];
const creator = req.query['is-creator'];
const timeBlockId = req.query['id'];
const date = req.query.date;
const event = (
await client.query(
`
SELECT start_datetime, end_datetime, deleted FROM events
WHERE id = $1
`,
[eventId]
)
).rows[0];
const isDeleted = event.deleted;
const eventStartTimeInMin = event.start_datetime.getTime();
const eventEndTimeInMin = event.end_datetime.getTime();
const now = new Date().getTime();
let isProcessing = true;
if (eventStartTimeInMin < now && eventEndTimeInMin < now) {
isProcessing = false;
//event is finished
}
if (isDeleted) {
isProcessing = false;
//event was deleted by creator
}
if (creator === '1' && isProcessing) {
await client.query(
`
DELETE FROM time_block_item
WHERE time_block_id = $1
`,
[timeBlockId]
);
await client.query(
`
DELETE FROM time_blocks
WHERE id = $1
AND event_id = $2
AND date = $3
`,
[timeBlockId, eventId, date]
);
res.json({
status: true,
msg: 'Delete success'
});
} else {
res.status(400).json({
status: false,
msg: '[EER001]: Unauthorized Request'
});
}
} catch (e) {
logger.error(e);
res.status(500).json({
msg: '[TBD001]: Failed to Delete Time Block'
});
}
}
async function getEventSchedule(req: Request, res: Response) {
try {
logger.debug('Before reading DB');
const eventId = req.query['event-id'];
const creator = req.query['is-creator'];
let date = req.query.date;
let event;
if (creator === '1') {
event = (
await client.query(
`
SELECT * FROM events
WHERE events.id = $1
AND events.creator_id = $2
`,
[eventId, req.session.user]
)
).rows[0];
} else {
event = (
await client.query(
`
SELECT * FROM events
INNER JOIN participants ON participants.event_id = events.id
WHERE events.id = $1
AND participants.user_id = $2;
`,
[eventId, req.session.user]
)
).rows[0];
}
if (event.start_datetime) {
if (date === 'null' || 'undefined') {
const option = {
hour12: false,
year: 'numeric',
month: '2-digit',
day: '2-digit'
};
let placeholder = event.start_datetime.toLocaleString('en-GB', option).split('/');
date = `${placeholder[2]}${placeholder[1]}${placeholder[0]}`;
}
const activitiesArr = (
await client.query(
`
SELECT * FROM time_blocks
WHERE event_id = $1
AND date = $2
`,
[eventId, date]
)
).rows;
const itemList = (
await client.query(
`
SELECT * FROM items
WHERE items.event_id = $1
`,
[eventId]
)
).rows;
const savedItemList = (
await client.query(
`
SELECT * FROM items
JOIN time_block_item ON items.id = time_block_item.item_id
JOIN time_blocks ON time_block_item.time_block_id = time_blocks.id
WHERE time_blocks.event_id = $1
AND time_blocks.date = $2
`,
[eventId, date]
)
).rows;
res.json({
status: true,
detail: event,
activities: activitiesArr,
items: itemList,
savedItems: savedItemList
});
} else {
res.json({ status: false });
}
} catch (e) {
logger.error(e);
res.status(500).json({
msg: '[ETS001]: Failed to get Event Schedule'
});
}
}
function toMin(timeInput: String) {
const hourInMin = parseInt(timeInput.slice(0, 2)) * 60;
const min = parseInt(timeInput.slice(3, 5));
return hourInMin + min;
}
async function postEventSchedule(req: Request, res: Response) {
try {
logger.debug('Before reading DB');
const eventId = req.query['event-id'];
const creator = req.query['is-creator'];
const date = req.query.date;
const event = (
await client.query(
`
SELECT start_datetime, end_datetime, deleted FROM events
WHERE id = $1
`,
[eventId]
)
).rows[0];
const isDeleted = event.deleted;
const eventStartTimeInMin = event.start_datetime.getTime();
const eventEndTimeInMin = event.end_datetime.getTime();
const now = new Date().getTime();
let isProcessing = true;
if (eventStartTimeInMin < now && eventEndTimeInMin < now) {
isProcessing = false;
//event is finished
}
if (isDeleted) {
isProcessing = false;
//event was deleted by creator
}
if (creator === '1' && isProcessing) {
//check if start time and end time collided with existing activities
const existingActivities = (
await client.query(
`
SELECT start_time, end_time FROM time_blocks
WHERE event_id = $1
AND date = $2
ORDER BY start_time ASC;
`,
[eventId, date]
)
).rows;
let reject = false;
existingActivities.forEach((activity) => {
const startTimeInMin = toMin(activity.start_time);
const endTimeInMin = toMin(activity.end_time);
const newStartTimeInMin = toMin(req.body.startTime);
const newEndTimeInMin = toMin(req.body.endTime);
if (newStartTimeInMin > startTimeInMin && newStartTimeInMin < endTimeInMin) {
reject = true;
} else if (newEndTimeInMin > startTimeInMin && newEndTimeInMin < endTimeInMin) {
reject = true;
}
});
//writing request to DB
if (reject) {
res.status(400).json({
msg: '[EER002]: Activity Start Time or End Time Overlapped with Existing Activity'
});
} else {
await client.query(
`
INSERT INTO time_blocks
(title, description, event_id, user_id, start_time,
end_time, remark, date, color, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`,
[
req.body.title,
req.body.description,
eventId,
req.session.user,
req.body.startTime,
req.body.endTime,
req.body.remark,
date,
req.body.color,
'now()',
'now()'
]
);
res.json({
status: true,
msg: 'save success'
});
}
} else {
res.status(400).json({
msg: '[EER001]: Unauthorized Request'
});
}
} catch (e) {
logger.error(e);
res.status(500).json({
msg: '[ETS002]: Failed to Post Event Schedule'
});
}
}

View File

@@ -0,0 +1,394 @@
import express, { Request, Response } from 'express';
import { client } from '../app';
import { logger } from '../util/logger';
export const venuePollRoutes = express.Router();
venuePollRoutes.get('/:id', getPollOptions);
venuePollRoutes.post('/:id', createPoll);
venuePollRoutes.delete('/:id', deletePoll);
venuePollRoutes.post('/replacement/:id', replaceTerminatedPoll);
venuePollRoutes.post('/vote/:event_id/:vote_id', submitVoteChoice);
async function getPollOptions(req: Request, res: Response) {
try {
logger.debug('Before reading DB');
const eventId = parseInt(req.params.id);
const userId = req.session.user;
const [eventDetail] = (
await client.query(
`
SELECT * FROM events WHERE id = $1 AND creator_id = $2;
`,
[eventId, userId]
)
).rows;
if (eventDetail) {
if (eventDetail.venue_poll_created) {
const pollNew = (await client.query(`
SELECT event_venues.id as option_id,
event_venues.address as address,
event_venues_votes.id as votes_id
FROM event_venues
LEFT JOIN event_venues_votes ON event_venues_votes.event_venues_id = event_venues.id
WHERE event_venues.event_id = $1;
`,
[eventId]
)).rows;
let pollOptions: {
id: number,
address: string
}[] = [];
let voteCounts:{
[keys in number]: {
count: number
}
} = {};
for (let eachVote of pollNew) {
if (!pollOptions.find((obj)=>obj.id === eachVote.option_id)) {
pollOptions.push({
id: eachVote.option_id,
address: eachVote.address
});
voteCounts[eachVote.option_id] = {count: 0};
}
if (eachVote.votes_id) {
voteCounts[eachVote.option_id].count ++;
}
}
res.json({
status: true,
creator: true,
pollTerminated: eventDetail.venue_poll_terminated,
eventDeleted: eventDetail.deleted,
pollOptions,
voteCounts
});
} else {
res.json({ status: false });
}
} else {
// Should be participants join events join event_venues join event_venue_votes
const [participant] = (
await client.query(
`
SELECT * FROM participants
INNER JOIN events ON events.id = participants.event_id
WHERE events.id = $1 AND participants.user_id = $2;
`,
[eventId, userId]
)
).rows;
if (participant) {
const [eventDetailParticipant] = (
await client.query(
`
SELECT * FROM events WHERE id = $1;
`,
[eventId]
)
).rows;
if (eventDetailParticipant.venue_poll_created) {
const pollOptions = (
await client.query(
`
SELECT * FROM event_venues WHERE event_id = $1;
`,
[eventId]
)
).rows;
let voteCounts = {};
for (let pollOption of pollOptions) {
const [voteCount] = (
await client.query(
`
SELECT COUNT(*) FROM event_venues_votes
WHERE event_venues_id = $1;
`,
[pollOption.id]
)
).rows;
voteCounts[pollOption.id] = voteCount;
}
const [choiceMade] = (
await client.query(
`
SELECT * FROM event_venues_votes
WHERE event_venues_id IN (SELECT id FROM event_venues
WHERE event_id = $1)
AND user_id = $2;
`,
[eventId, userId]
)
).rows;
let chosenAddress;
if (choiceMade) {
[chosenAddress] = (
await client.query(
`
SELECT * FROM event_venues
WHERE id = $1;
`,
[choiceMade.event_venues_id]
)
).rows;
}
res.json({
status: true,
creator: false,
pollTerminated: eventDetailParticipant.venue_poll_terminated,
eventDeleted: eventDetailParticipant.deleted,
choice: choiceMade
? {
id: `option_${choiceMade.event_venues_id}`,
address: `${chosenAddress.address}`
}
: '',
pollOptions,
voteCounts
});
} else {
res.json({ status: false });
}
} else {
res.json({ status: false });
}
}
} catch (e) {
logger.error(e);
res.status(500).json({
msg: '[VNP001]: Failed to get venue poll options'
});
}
}
async function createPoll(req: Request, res: Response) {
try {
logger.debug('Before reading DB');
const eventId = parseInt(req.params.id);
const [eventDetail] = (
await client.query(
`
SELECT * FROM events
WHERE id = $1 AND creator_id = $2;
`,
[eventId, req.session.user]
)
).rows;
if (eventDetail) {
if (!eventDetail.venue_poll_created) {
const inputList = req.body;
for (let input of inputList) {
await client.query(
`
INSERT INTO event_venues (address, event_id, created_at, updated_at)
VALUES ($1,$2,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP);
`,
[input, eventId]
);
await client.query(
`
UPDATE events
SET venue_poll_created = TRUE
WHERE id = $1;
`,
[eventId]
);
}
res.json({ status: true });
} else {
res.json({
status: false,
created: true
});
}
} else {
res.json({
status: false
});
}
} catch (e) {
logger.error(e);
res.status(500).json({
msg: '[VNP002]: Failed to create venue poll'
});
}
}
async function deletePoll(req: Request, res: Response) {
try {
logger.debug('Before reading DB');
const eventId = parseInt(req.params.id);
const [eventDetail] = (
await client.query(
`
SELECT * FROM events
WHERE id = $1 AND creator_id = $2;
`,
[eventId, req.session.user]
)
).rows;
if (eventDetail) {
if (eventDetail.venue_poll_created) {
if (!eventDetail.venue_poll_terminated) {
await client.query(
`
UPDATE events SET venue_poll_terminated = TRUE
WHERE id = $1;
`,
[eventId]
);
res.json({ status: true });
} else {
res.json({
status: false,
terminated: true
});
}
} else {
res.json({
status: false,
noPoll: true
});
}
} else {
res.json({ status: false });
}
} catch (e) {
logger.error(e);
res.status(500).json({
msg: '[VNP003]: Failed to delete venue poll'
});
}
}
// Should not have poll_created as the column , but poll_terminated can retain
async function replaceTerminatedPoll(req: Request, res: Response) {
try {
logger.debug('Before reading DB');
const eventId = parseInt(req.params.id);
const [eventDetail] = (
await client.query(
`
SELECT * FROM events
WHERE id = $1 AND creator_id = $2;
`,
[eventId, req.session.user]
)
).rows;
if (eventDetail) {
// Initialize the polling data
await client.query(
`
DELETE FROM event_venues_votes
WHERE event_venues_id IN (SELECT id FROM event_venues
WHERE event_id = $1);
`,
[eventId]
);
await client.query(
`
DELETE FROM event_venues WHERE event_id = $1;
`,
[eventId]
);
await client.query(
`
UPDATE events
SET venue_poll_created = FALSE,
venue_poll_terminated = FALSE
WHERE id = $1;
`,
[eventId]
);
const inputList = req.body;
for (let input of inputList) {
await client.query(
`
INSERT INTO event_venues (address, event_id, created_at, updated_at)
VALUES ($1,$2,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP);
`,
[input, eventId]
);
await client.query(
`
UPDATE events
SET venue_poll_created = TRUE
WHERE id = $1;
`,
[eventId]
);
}
res.json({ status: true });
} else {
res.json({
status: false
});
}
} catch (e) {
logger.error(e);
res.status(500).json({
msg: '[VNP004]: Failed to overwrite venue poll'
});
}
}
async function submitVoteChoice(req: Request, res: Response) {
try {
logger.debug('Before reading DB');
const eventId = parseInt(req.params.event_id);
const userId = req.session.user;
const [participant] = (
await client.query(
`
SELECT * FROM participants
INNER JOIN events ON events.id = participants.event_id
WHERE participants.user_id = $1
AND events.id = $2;
`,
[userId, eventId]
)
).rows;
if (participant) {
const [choiceMade] = (
await client.query(
`
SELECT * FROM event_venues_votes
WHERE event_venues_id IN (SELECT id FROM event_venues
WHERE event_id = $1);
`,
[eventId]
)
).rows;
if (!choiceMade) {
await client.query(
`
INSERT INTO event_venues_votes
(event_venues_id,user_id,created_at,updated_at)
VALUES ($1,$2,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP);
`,
[parseInt(req.params.vote_id), userId]
);
res.json({ status: true });
} else {
res.json({
status: false,
duplicate: true
});
}
} else {
res.json({ status: false });
}
} catch (e) {
logger.error(e);
res.status(500).json({
msg: '[VNP005]: Failed to submit vote choice'
});
}
}

View File

@@ -0,0 +1,18 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"lib": ["es6", "dom"],
"sourceMap": true,
"jsx": "react",
"esModuleInterop": true,
"moduleResolution": "node",
"noImplicitReturns": true,
"noImplicitThis": true,
"noImplicitAny": true,
"strictNullChecks": true,
"suppressImplicitAnyIndexErrors": true,
"noUnusedLocals": true
},
"exclude": ["node_modules", "build", "scripts", "index.js"]
}

View File

@@ -0,0 +1,893 @@
{
"firstName": [
"John",
"William",
"James",
"Charles",
"George",
"Frank",
"Joseph",
"Thomas",
"Henry",
"Robert",
"Edward",
"Harry",
"Walter",
"Arthur",
"Fred",
"Albert",
"Samuel",
"David",
"Louis",
"Joe",
"Charlie",
"Clarence",
"Richard",
"Andrew",
"Daniel",
"Ernest",
"Will",
"Jesse",
"Oscar",
"Lewis",
"Peter",
"Benjamin",
"Frederick",
"Willie",
"Alfred",
"Sam",
"Roy",
"Herbert",
"Jacob",
"Tom",
"Elmer",
"Carl",
"Lee",
"Howard",
"Martin",
"Michael",
"Bert",
"Herman",
"Jim",
"Francis",
"Harvey",
"Earl",
"Eugene",
"Ralph",
"Ed",
"Claude",
"Edwin",
"Ben",
"Charley",
"Paul",
"Edgar",
"Isaac",
"Otto",
"Luther",
"Lawrence",
"Ira",
"Patrick",
"Guy",
"Oliver",
"Theodore",
"Hugh",
"Clyde",
"Alexander",
"August",
"Floyd",
"Homer",
"Jack",
"Leonard",
"Horace",
"Marion",
"Philip",
"Allen",
"Archie",
"Stephen",
"Chester",
"Willis",
"Raymond",
"Mary",
"Anna",
"Emma",
"Elizabeth",
"Minnie",
"Margaret",
"Ida",
"Alice",
"Bertha",
"Sarah",
"Annie",
"Clara",
"Ella",
"Florence",
"Cora",
"Martha",
"Laura",
"Nellie",
"Grace",
"Carrie",
"Maude",
"Mabel",
"Bessie",
"Jennie",
"Gertrude",
"Julia",
"Hattie",
"Edith",
"Mattie",
"Rose",
"Catherine",
"Lillian",
"Ada",
"Lillie",
"Helen",
"Jessie",
"Louise",
"Ethel",
"Lula",
"Myrtle",
"Eva",
"Frances",
"Lena",
"Lucy",
"Edna",
"Maggie",
"Pearl",
"Daisy",
"Fannie",
"Josephine",
"Dora",
"Rosa",
"Katherine",
"Agnes",
"Marie",
"Nora",
"May",
"Mamie",
"Blanche",
"Stella",
"Ellen",
"Nancy",
"Effie",
"Sallie",
"Nettie",
"Della",
"Lizzie",
"Flora",
"Susie",
"Maud",
"Mae",
"Etta",
"Harriet",
"Sadie",
"Caroline",
"Katie",
"Lydia",
"Elsie",
"Kate",
"Susan",
"Mollie",
"Alma",
"Addie",
"Georgia",
"Eliza",
"Lulu",
"Nannie",
"Lottie",
"Amanda",
"Belle",
"Charlotte"
],
"lastName": [
"Smith",
"Johnson",
"Williams",
"Brown",
"Jones",
"Miller",
"Davis",
"Garcia",
"Rodriguez",
"Wilson",
"Martinez",
"Anderson",
"Taylor",
"Thomas",
"Hernandez",
"Moore",
"Martin",
"Jackson",
"Thompson",
"White",
"Lopez",
"Lee",
"Gonzalez",
"Harris",
"Clark",
"Lewis",
"Robinson",
"Walker",
"Perez",
"Hall",
"Young",
"Allen",
"Sanchez",
"Wright",
"King",
"Scott",
"Green",
"Baker",
"Adams",
"Nelson",
"Hill",
"Ramirez",
"Campbell",
"Mitchell",
"Roberts",
"Carter",
"Phillips",
"Evans",
"Turner",
"Torres",
"Parker",
"Collins",
"Edwards",
"Stewart",
"Flores",
"Morris",
"Nguyen",
"Murphy",
"Rivera",
"Cook",
"Rogers",
"Morgan",
"Peterson",
"Cooper",
"Reed",
"Bailey",
"Bell",
"Gomez",
"Kelly",
"Howard",
"Ward",
"Cox",
"Diaz",
"Richardson",
"Wood",
"Watson",
"Brooks",
"Bennett",
"Gray",
"James",
"Reyes",
"Cruz",
"Hughes",
"Price",
"Myers",
"Long",
"Foster",
"Sanders",
"Ross",
"Morales",
"Powell",
"Sullivan",
"Russell",
"Ortiz",
"Jenkins",
"Gutierrez",
"Perry",
"Butler",
"Barnes",
"Fisher",
"Henderson",
"Coleman",
"Simmons",
"Patterson",
"Jordan",
"Reynolds",
"Hamilton",
"Graham",
"Kim",
"Gonzales",
"Alexander",
"Ramos",
"Wallace",
"Griffin",
"West",
"Cole",
"Hayes",
"Chavez",
"Gibson",
"Bryant",
"Ellis",
"Stevens",
"Murray",
"Ford",
"Marshall",
"Owens",
"Mcdonald",
"Harrison",
"Ruiz",
"Kennedy",
"Wells",
"Alvarez",
"Woods",
"Mendoza",
"Castillo",
"Olson",
"Webb",
"Washington",
"Tucker",
"Freeman",
"Burns",
"Henry",
"Vasquez",
"Snyder",
"Simpson",
"Crawford",
"Jimenez",
"Porter",
"Mason",
"Shaw",
"Gordon",
"Wagner",
"Hunter",
"Romero",
"Hicks",
"Dixon",
"Hunt",
"Palmer",
"Robertson",
"Black",
"Holmes",
"Stone",
"Meyer",
"Boyd",
"Mills",
"Warren",
"Fox",
"Rose",
"Rice",
"Moreno",
"Schmidt",
"Patel",
"Ferguson",
"Nichols",
"Herrera",
"Medina",
"Ryan",
"Fernandez",
"Weaver",
"Daniels",
"Stephens",
"Gardner"
],
"emailHost": ["hotmail.com", "gmail.com", "yahoo.com"],
"phoneAreaCode": ["905", "807", "705", "647", "613", "519", "416", "343", "289", "226"],
"partyReason": [
"Birthday",
"Surprise",
"Garden",
"Cocktail",
"Tea",
"Dance",
"Costume",
"Pool",
"Singles",
"Fundraising",
"Graduation",
"Dating",
"Housewarming",
"Welcome",
"Farewell",
"Drinking",
"Poker"
],
"streetName": [
"Lane S College W Gladstone",
"Lane E Glenside S Walpole",
"Corona Street",
"Durant Avenue",
"Dana Avenue",
"Drumheller Road",
"Distin Avenue",
"Dixington Crescent",
"Corylus Court",
"Dickens Street",
"Courton Drive",
"Denny Court",
"Delburn Drive",
"Delaware Avenue North",
"Connie Street",
"Diesel Drive",
"Delverton Place",
"Court Square",
"Courage Avenue",
"Donald Avenue",
"Dolan Gate",
"Delisle Avenue",
"Coltman Crescent",
"Dinnick Crescent",
"Dewhurst Boulevard North",
"Cosburn Avenue",
"Delemere Avenue",
"Commons Drive",
"Conroy Avenue",
"Constellation Court",
"Cranborne Avenue",
"Dobbin Road",
"Crestview Road",
"Doctor O Lane",
"Cooper Street",
"Coyote Crescent",
"Crete Court",
"Dekoven Mews",
"Mcglashan Court",
"Delroy Drive",
"Owen Boulevard",
"Dittmer Crescent",
"Lane S Dundas E Hamilton Street",
"Doncrest Road",
"Nagel Road",
"Donna Shaw Lane",
"Neston Avenue",
"Denison Square",
"Cherrydale Court",
"Coules Court",
"Leader Lane",
"Ridgevale Drive",
"Royalavon Crescent",
"Christine Crescent",
"Conference Boulevard",
"Donnybrook Lane",
"Redbird Gate",
"Coronation Drive",
"Dewbourne Avenue",
"Redcar Avenue",
"Cawthra Square",
"Devondale Avenue",
"Tompkins Mews",
"Elgin Avenue",
"Carey Road",
"Rosita Crescent",
"St Columba Place",
"Castlemere Crescent",
"Redland Crescent East",
"Dixon Avenue",
"Dumbarton Road",
"Rosedale Valley Road",
"Redstone Path",
"Dunboyne Court",
"Charlotte Street",
"Peelar Mews",
"Dorine Crescent",
"Duncannon Drive",
"Rosemarie Drive",
"Endicott Avenue",
"Skyridge Road",
"Sackville Street",
"Emmott Avenue",
"Dellbrook Crescent",
"Redfern Avenue",
"Carus Avenue",
"St Helens Avenue",
"Rathnelly Avenue",
"Chestnut Street",
"Duggan Avenue",
"Casebridge Court",
"Dusay Place",
"Rooksnest Trail",
"Sheila Court",
"Peregrine Way",
"Royal Doulton Drive",
"Reindeer Drive",
"Conamore Crescent",
"Caroline Avenue",
"Dunsany Crescent",
"Springmount Avenue",
"Edith Drive",
"Dufflaw Road",
"Relmar Road",
"Pepper Vineway",
"Rock Fernway",
"Elmview Drive",
"Rintella Court",
"Condor Avenue",
"Dundas Square",
"Cascaden Street",
"Chimes Lane",
"Carmichael Avenue",
"Dunvegan Road",
"Rooksgrove Place",
"Craighurst Avenue",
"Emmeline Crescent",
"Paulvale Crescent",
"Dunfield Avenue",
"Emcarr Drive",
"Delahaye Street",
"Raymond Avenue",
"Silversted Drive",
"Cora Crescent",
"Rutland Street",
"Cornwall Street",
"Chipping Road",
"Chetta Place",
"Shawford Crescent",
"Charles H Hiscott Bridge",
"Delaware Avenue",
"Squirewood Road",
"Dorval Road",
"Silverton Avenue",
"Donwoods Lane",
"Cotillion Court",
"Pastrano Court",
"Sherway Gardens Road",
"Chevron Crescent",
"Delsing Drive",
"Peard Road",
"Saddle Ridge Drive",
"Dunkirk Road",
"Raybould Street",
"Charnwood Road",
"Lane N Chapman E Royal York",
"Sawmill Road",
"Peach Willoway",
"Falcon Lane",
"Pinoak Street",
"Photography Drive",
"St Andrews Boulevard",
"Rideau Avenue",
"Lane E Nairn N Rogers",
"Spring Forest Square",
"Ronan Avenue",
"Connolly Street",
"Cheritan Avenue",
"Rossdean Drive",
"Shockley Drive",
"Chipper Crescent",
"Sapling Court",
"Thistle Down Terrace",
"Lane E Ossington S Acores",
"Deep Dene Drive",
"Saunders Avenue",
"Gunns Road",
"Lane S Morningside W Lavinia",
"Hemford Crescent",
"Chestnut Hills Parkway",
"Rawlinson Avenue",
"Madonna Gardens",
"Sheppard Street",
"Lesmill Road",
"Lane W Laing N Sears",
"Lane S Carlton E Sackville Street",
"Civic Road",
"Tillbrook Court",
"Lane W York S Richmond",
"Lane W Montrose S Cinder",
"Hillborn Avenue",
"Lane W Alhambra S Indian Trail",
"Lane S Eglinton W Rostrevor",
"Niantic Crescent",
"Lane E Lount Street S Torbrick",
"Grenview Boulevard South",
"Pine Ridge Drive",
"Donmore Avenue",
"Van Allan Road",
"Lane S Davenport E Salem Avenue",
"Glencrest Boulevard",
"Lane E South Kingsway N Ormskirk Court",
"Thornbury Crescent",
"Lane S College E Beatrice",
"Lane W Yonge N Woburn",
"Lane E Runnymede S Maher",
"Pintail Crescent",
"Vanauley Court",
"Feldbar Court",
"Denison Road West",
"Cather Crescent",
"Mystic Avenue",
"St Clair Avenue East",
"Gretna Avenue",
"Casemore Road",
"Lane W Macdonell N Garden",
"Newbridge Road",
"Grenadier Road",
"Thurodale Avenue",
"Lane S Davenport E Bathurst",
"Ketchum Place",
"Valerie Road",
"Leading Road",
"Saranac Boulevard",
"Nesbitt Drive",
"Lane E Melita Crescent N Wycrest",
"Cranston Manor Court",
"Chestnut Lane",
"Duern Street",
"Horfield Avenue",
"Kentish Crescent",
"Lane E Shaw N Yarmouth",
"Leander Court",
"Lane W Carroll S Thompson",
"Markham Street",
"Hocken Avenue",
"Leroy Avenue",
"Lane E Sunnyside N Galley",
"Crestridge Heights Road",
"Lane S Carlton W Dermott Place",
"Claremont Place",
"Geary Avenue",
"Sherman Court",
"Connaught Avenue",
"Peyton Lane",
"Lane E Sibley S Dentonia Park",
"St Andrews Road",
"Langbourne Place",
"Delaney Crescent",
"Coltbridge Court",
"Hillcrest Drive",
"Vanellan Court",
"Rayward Court",
"Paulander Avenue",
"Newmill Gate",
"Lane 1 W Sorauren N Fern",
"Lane W Wineva S Violet",
"Lackman Court",
"Gennela Square",
"Marathon Crescent",
"Ebonywood Gate",
"Eastdale Avenue",
"Nicholas Avenue",
"Grey Road",
"Tromley Drive",
"Gilroy Drive",
"Lane S Queen W Glen Manor",
"Lane E River N Labatt",
"Manor Road East",
"Hullen Crescent",
"Lambton Avenue",
"Hodge Lane",
"Thistlewaite Crescent",
"Nightingale Place",
"Lane 1 S Carlton W Parliament",
"Farmhill Court",
"Coolhurst Drive",
"Markburn Court",
"Maple Bush Avenue",
"Elba Avenue",
"Ringwood Crescent",
"Emmett Avenue",
"Lethbridge Avenue",
"Nearctic Drive",
"Ravenhill Road",
"Kylemore Crescent",
"Lane S Cook W Haynes",
"Don Roadway",
"Satterly Road",
"Felicity Drive",
"Phenix Drive",
"Duval Drive",
"Vanley Crescent",
"Lunness Road",
"Lane 1 N Foxley W Grove",
"Trinity Street",
"Gerigs Street",
"Latimer Avenue",
"Thornhill Avenue",
"Church Street",
"Thorold Gate",
"Paynter Drive",
"Dewey Drive",
"Reddick Court",
"Lane S Queen W Pape",
"Littles Road",
"Lane S Kingston W Glen Manor Road",
"Thursfield Crescent",
"Cleadon Road",
"Twin Circle Court",
"Munster Avenue",
"Community Circle",
"Thorndale Crescent",
"Bruton Road",
"Safari Street",
"Nettlecreek Crescent",
"Pine Hill Road",
"Marianfeld Avenue",
"Rollins Place",
"Ladysmith Avenue",
"Gypsy Roseway",
"Taylor Road",
"Upper Humber Drive",
"Archgate Lane",
"Ferrand Drive",
"Ameer Avenue",
"Holita Road",
"Lane S Davenport W Walmer",
"Lane 1 N Bloor E Bathurst",
"Leswyn Road",
"Braddock Road",
"Centrepark Drive",
"Lane E Spadina N Harbord",
"Lane W Avon S Avon",
"Evans Lane",
"Lane S The Queensway W Dayton",
"Leslie Street",
"Lane S Gerrard W Parliament",
"Leavenworth Crescent",
"St Albans Road",
"Anola Place",
"Bartlett Avenue",
"Hallmark Avenue",
"Lane W Pape S Cosburn",
"Furnival Road",
"Reno Drive",
"Lane S Ascot W Nairn",
"Lane W Indian Road S Annette",
"Lane 1 E Yonge S Charles",
"Lyall Avenue",
"Simcoe Street",
"Larabee Crescent",
"Deakin Avenue",
"Lane E Brock N Brockton",
"Dora Avenue",
"Harvest Moon Drive",
"Homeview Avenue",
"Lane E Melita Crescent S Lambertlodge",
"Harnworth Drive",
"Muir Drive",
"Rensburg Drive",
"Lane E Ohara N Maple Grove",
"Muir Avenue",
"Hasbrooke Drive",
"Ranstone Gardens",
"Eglinton E St Dennis Ramp",
"Eglinton W 27 N Ramp",
"Avalon Avenue",
"Sunburst Square",
"Guildpark Pathway",
"Fermanagh Avenue",
"Bexhill Court",
"Benlark Court",
"Penny Lane",
"Lane E Pape N Harcourt",
"Atlee Avenue",
"Berkindale Crescent",
"Triton Road",
"Anvil Millway",
"Lane S Lake Shore E 10th",
"Lane S Rogers W Nairn",
"Pepper Tree Drive",
"Parsonage Drive",
"Bevdale Road",
"Lane S Davenport W Osler",
"Hardwick Court",
"Hillwood Court",
"Lane E Spadina N Lonsdale",
"Marsh Grassway",
"Marrakesh Drive",
"Comrie Terrace",
"Thornbush Crescent",
"Lane S Yorkminster W Fairmeadow",
"Maresfield Drive",
"Springbank Avenue",
"Mainshep Road",
"Lane 3 N Bloor W Bartlett",
"Purpledusk Trail",
"Lane E Grace N Plymouth",
"Genessee Avenue",
"Benedict Road",
"Prue Avenue",
"Lane W Jarvis S Shuter",
"Lane S Hepbourne W Ossington",
"Evans W West Mall Ramp",
"Netheravon Road",
"Lindylou Road",
"Lane E Bernice S Eileen",
"Charlesworth Lane",
"Haddon Avenue",
"Lane W Arlington S St Clair",
"Tilden Crescent",
"Garrybrook Drive",
"Silbury Drive",
"Benbow Road",
"Signal Hill Avenue",
"Lormar Drive",
"Cherryhill Avenue",
"Mulvey Avenue",
"Dynevor Road",
"Lane 1 W Airley S 4 Oaks",
"Lane E Earlscourt N Rogers",
"Pumfrey Crescent",
"Lakeshore W Browns Line Ramp",
"Lane W Greensides S Benson",
"Lane S Argyle E Northcote",
"Lane W Symington N Kingsley",
"Lane E Hazelton S Scollard",
"Garrison Road",
"Lane S Yarmouth W Miles Place",
"Needham Drive",
"Lane E Broadview N Dundas",
"Macgregor Avenue",
"Lane W Shaw N Harbord",
"Pettibone Square",
"Lane S Helena E Wychwood",
"Nina Street",
"Lane S Bloor E Grace",
"Lane W Evans N Ardagh",
"Gillard Avenue",
"Dunsdale Square",
"Lane E Ossington N Acores",
"Regency Square",
"Pear Tree Mews",
"Stonegate Road",
"Ladner Drive",
"Thorny Vineway",
"Farley Crescent",
"Gatesview Avenue",
"Lane E Burrard N Hadrian",
"Lane 1 N Gerrard W Hastings",
"Lane S Eglinton E Northcliffe",
"Guildwood Parkway",
"Pont Lane",
"Amarillo Drive",
"Lane S College W Beatrice",
"Atomic Avenue",
"Sunshine Street",
"Munson Crescent",
"Hillock Place",
"Lane 1 N Newport W Byng",
"Mansewood Gardens",
"Newbold Avenue",
"Salem Avenue North",
"Lane E Mcroberts S Rogers",
"Lane E Broadview N Gerrard",
"Ellendale Drive",
"Cliff Street",
"Lane S Dundas W St Clarens",
"Lane S Howard Park W Lynd",
"Thyme Court",
"Southlawn Drive",
"Leafy Woodway",
"Donna Court",
"Council Crescent",
"Trio Avenue",
"Denewood Crescent",
"St Emilion Way",
"Don Mount Court",
"Lane S Liberty E Jefferson",
"York Avenue",
"Bernard Avenue",
"Court Street",
"Lane E Carling S Irene Avenue",
"Dunedin Drive",
"Chestnut Park",
"Lane S Dundas E Cordova",
"Munford Crescent",
"Niven Street",
"Benlight Crescent",
"Landry Avenue",
"Oakley Boulevard",
"Hopedale Avenue",
"Lane 2 S Eastern E Logan",
"Denton Avenue",
"Grenadine Court",
"Vanderhoof Avenue",
"Suraty Avenue",
"Penrose Road",
"Degrey Court",
"Hopecrest Crescent",
"Lane S Eastern W Knox",
"Major Street",
"Elgin Road",
"Lane W Yonge N Wellesley",
"Severn Road",
"Beta Street",
"Murmouth Road",
"Lapsley Road",
"Lane S Bloor W Lothian"
],
"food": ["apples", "oranges", "bananas", "pineapples", "strawberry"],
"drink": ["coke", "milk", "coffee", "tea", "juice"],
"decoration": ["flowers", "stickers", "balloons", "pumpkins", "tapes"],
"other": ["pens", "table cloth", "straws", "cups", "chopsticks"]
}

View File

@@ -0,0 +1,560 @@
import pg from "pg";
import dotenv from "dotenv";
import jsonfile from "jsonfile";
import path from "path";
import crypto from "crypto";
import { hashPassword } from "../functions/hash";
import { DataParts, Users } from "../models";
import { format } from "date-fns";
import { logger } from "../logger";
function randomDate(start: Date, days: number): Date {
const startTime = start.getTime();
const minusTime = startTime - days * 86_400_000;
const plusTime = startTime + days * 86_400_000;
return new Date(minusTime + Math.random() * (plusTime - minusTime));
}
function randomIntFromInterval(min: number, max: number): number {
return Math.floor(Math.random() * (max - min + 1) + min);
}
export async function clearDB() {
dotenv.config();
const client = new pg.Client({
host: process.env.DB_HOST,
database: process.env.DB_NAME,
user: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
});
await client.connect();
await client.query(`
DROP TABLE event_date_time_votes;
DROP TABLE event_date_time;
DROP TABLE event_venues_votes;
DROP TABLE event_venues;
DROP TABLE comments;
DROP TABLE time_block_item;
DROP TABLE time_blocks;
DROP TABLE items;
DROP TABLE participants;
DROP TABLE events;
DROP TABLE users;
`);
await client.end();
}
export async function initDB() {
dotenv.config();
const client = new pg.Client({
host: process.env.DB_HOST,
database: process.env.DB_NAME,
user: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
});
await client.connect();
await client.query(`CREATE TABLE users (
id SERIAL primary key,
first_name varchar not NULL,
last_name varchar not NULL,
email varchar not NULL,
phone varchar not NULL,
password varchar not NULL,
created_at timestamp not NULL,
updated_at timestamp not NULL
);
CREATE TABLE events (
id SERIAL primary key,
name varchar not NULL,
venue varchar,
start_datetime timestamptz,
end_datetime timestamptz,
creator_id int not NULL,
invitation_token varchar not NULL,
deleted boolean not NULL,
date_poll_created boolean not NULL,
date_poll_terminated boolean not NULL,
venue_poll_created boolean not NULL,
venue_poll_terminated boolean not NULL,
created_at timestamp not NULL,
updated_at timestamp not NULL,
FOREIGN KEY (creator_id) REFERENCES users(id)
);
CREATE TABLE participants (
id SERIAL primary key,
event_id int not NULL,
user_id int not NULL,
created_at timestamp not NULL,
updated_at timestamp not NULL,
FOREIGN KEY (event_id) REFERENCES events(id),
FOREIGN KEY (user_id) REFERENCES users(id)
);
CREATE TABLE items (
id SERIAL primary key,
name varchar not NULL,
purchased boolean not NULL,
type_name varchar not NULL,
event_id int not NULL,
user_id int not NULL,
quantity int,
price int,
created_at timestamp not NULL,
updated_at timestamp not NULL,
FOREIGN KEY (event_id) REFERENCES events(id),
FOREIGN KEY (user_id) REFERENCES users(id)
);
CREATE TABLE time_blocks (
id SERIAL primary key,
title varchar not NULL,
description varchar,
event_id int not NULL,
user_id int not NULL,
date varchar,
start_time time not NULL,
end_time time not NULL,
color varchar,
remark varchar,
remark_2 varchar,
remark_3 varchar,
created_at timestamp not NULL,
updated_at timestamp not NULL,
FOREIGN KEY (event_id) REFERENCES events(id),
FOREIGN KEY (user_id) REFERENCES users(id)
);
CREATE TABLE time_block_item (
id SERIAL primary key,
item_id int not NULL,
time_block_id int not NULL,
quantity int,
created_at timestamp not NULL,
updated_at timestamp not NULL,
FOREIGN KEY (item_id) REFERENCES items(id),
FOREIGN KEY (time_block_id) REFERENCES time_blocks(id)
);
CREATE TABLE comments (
id SERIAL primary key,
user_id int not NULL,
event_id int not NULL,
category varchar not NULL,
content varchar not NULL,
anonymous boolean not NULL,
read boolean not NULL DEFAULT 'false',
created_at timestamp not NULL,
updated_at timestamp not NULL,
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (event_id) REFERENCES events(id)
);
CREATE TABLE event_venues (
id SERIAL primary key,
address varchar not NULL,
event_id int not NULL,
created_at timestamp not NULL,
updated_at timestamp not NULL,
FOREIGN KEY (event_id) REFERENCES events(id)
);
CREATE TABLE event_venues_votes (
id SERIAL primary key,
event_venues_id int not NULL,
user_id int not NULL,
created_at timestamp not NULL,
updated_at timestamp not NULL,
FOREIGN KEY (event_venues_id) REFERENCES event_venues(id),
FOREIGN KEY (user_id) REFERENCES users(id)
);
CREATE TABLE event_date_time (
id SERIAL primary key,
start_datetime timestamptz not NULL,
end_datetime timestamptz not NULL,
event_id int not NULL,
created_at timestamp not NULL,
updated_at timestamp not NULL,
FOREIGN KEY (event_id) REFERENCES events(id)
);
CREATE TABLE event_date_time_votes (
id SERIAL primary key,
event_date_time_id int not NULL,
user_id int not NULL,
created_at timestamp not NULL,
updated_at timestamp not NULL,
FOREIGN KEY (event_date_time_id) REFERENCES event_date_time(id),
FOREIGN KEY (user_id) REFERENCES users(id)
);`);
client.end();
}
export async function regUsers(newUsersAmount: number) {
dotenv.config();
const client = new pg.Client({
host: process.env.DB_HOST,
database: process.env.DB_NAME,
user: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
});
await client.connect();
// Insert test user when DB is empty
const [usersDB] = (await client.query(`SELECT * FROM users WHERE id = -1;`)).rows;
if (!usersDB) {
const first_name = "Gordon";
const last_name = "Lau";
const email = "gordonlau@tecky.io";
const phone = "647-111-1111";
const testPassword = await hashPassword("test");
await client.query(
`INSERT INTO users
(id,first_name,last_name,email,phone,password,created_at,updated_at)
VALUES (-1,$1,$2,$3,$4,$5,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP);`,
[first_name, last_name, email, phone, testPassword]
);
}
// Read random data parts for data assembling
let parts: DataParts = await jsonfile.readFile(path.join(__dirname, "/data/dataParts.json"));
let counter = 0;
while (counter < newUsersAmount) {
// Names
const first_name: string = parts["firstName"][Math.floor(Math.random() * parts["firstName"].length)];
const last_name: string = parts["lastName"][Math.floor(Math.random() * parts["lastName"].length)];
// Email
const emailHost: string = parts["emailHost"][Math.floor(Math.random() * parts["emailHost"].length)];
const email: string = `${first_name.toLowerCase()}${last_name.toLowerCase()}@${emailHost}`;
// Phone
const phoneAreaCode: string = parts["phoneAreaCode"][Math.floor(Math.random() * parts["phoneAreaCode"].length)];
const phone: string = `${phoneAreaCode}-${Math.random()
.toString()
.concat("0".repeat(3))
.substring(2, 3)}-${Math.random().toString().concat("0".repeat(3)).substring(2, 4)}`;
// Password
const password: string = "test";
const hashedPassword = await hashPassword(password);
const [checkUsers] = (await client.query(`SELECT * FROM users WHERE email = $1 OR phone = $2;`, [email, phone]))
.rows;
if (!checkUsers) {
await client.query(
`INSERT INTO users
(first_name,last_name,email,phone,password,created_at,updated_at)
VALUES ($1,$2,$3,$4,$5,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP);`,
[first_name, last_name, email, phone, hashedPassword]
);
counter++;
}
}
client.end();
}
export async function createEvents(eventNumbers: number) {
dotenv.config();
const client = new pg.Client({
host: process.env.DB_HOST,
database: process.env.DB_NAME,
user: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
});
await client.connect();
// Read random data parts for data assembling
let parts: DataParts = await jsonfile.readFile(path.join(__dirname, "/data/dataParts.json"));
// Obtain users info for event creation for each user
let users: Users[] = (await client.query(`SELECT * FROM users;`)).rows;
for (let i = 0; i < eventNumbers; i++) {
for (const user of users) {
// Party name
const partyReason: string = parts["partyReason"][Math.floor(Math.random() * parts["partyReason"].length)];
const name: string = `${user.first_name}'s ${partyReason} Party`;
// Party venue
const venue: string = `${Math.floor(Math.random() * 999) + 1} ${
parts["streetName"][Math.floor(Math.random() * parts["streetName"].length)]
}`;
// Date
const date: string = format(randomDate(new Date(), 100), "yyyy/MM/dd");
const userDetail = (await client.query(`SELECT * FROM users WHERE email = $1;`, [user.email])).rows[0];
// Time
const start_time: string = `${randomIntFromInterval(12, 17)}:${Math.random() > 0.5 ? "00" : "30"}`;
const end_time: string = `${randomIntFromInterval(18, 23)}:${Math.random() > 0.5 ? "00" : "30"}`;
// DateTime
const start_datetime: string = new Date(`${date} ${start_time}`).toISOString();
const end_datetime: string = new Date(`${date} ${end_time}`).toISOString();
// Creator id
const creator_id: number = userDetail.id;
// Invitation Token
const invitation_token = crypto.randomBytes(64).toString("hex");
await client.query(
`INSERT INTO events
(name,venue,start_datetime,end_datetime,
creator_id,invitation_token,deleted,
date_poll_created,
date_poll_terminated,
venue_poll_created,
venue_poll_terminated,
created_at,updated_at)
VALUES ($1,$2,$3,$4,$5,$6,FALSE,FALSE,FALSE,FALSE,FALSE,
CURRENT_TIMESTAMP,CURRENT_TIMESTAMP);`,
[name, venue, start_datetime, end_datetime, creator_id, invitation_token]
);
}
}
client.end();
}
export async function joinEvents(eventsJoinedPerUser: number) {
dotenv.config();
const client = new pg.Client({
host: process.env.DB_HOST,
database: process.env.DB_NAME,
user: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
});
await client.connect();
const eventsParticipantsRelations = (
await client.query(
`SELECT DISTINCT events.id as event_id,
events.creator_id,
participants.user_id as participants_id
FROM events
LEFT JOIN participants ON events.id = participants.event_id
ORDER BY events.id, participants.user_id;
`
)
).rows;
let usersOfEventsList:
| {
(keys: number): {
creator_id: number;
participants_id: number[] | null[];
};
}
| {} = {};
for (let relation of eventsParticipantsRelations) {
if (!(relation.event_id in usersOfEventsList)) {
usersOfEventsList[relation.event_id] = {
creator_id: relation.creator_id,
participants_id: relation.participants_id ? [relation.participants_id] : [],
};
} else {
usersOfEventsList[relation.event_id]["participants_id"].push(relation.participants_id);
}
}
let usersIdList = (await client.query(`SELECT id FROM users;`)).rows;
for (let userId of usersIdList) {
let eventsJoined = 0;
for (const eventId in usersOfEventsList) {
const usersInfoInEvent = usersOfEventsList[eventId];
if (usersInfoInEvent.creator_id !== userId.id && !usersInfoInEvent.participants_id.includes(userId.id)) {
await client.query(
`
INSERT INTO participants (event_id, user_id, created_at, updated_at)
VALUES ($1, $2, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);
`,
[eventId, userId.id]
);
eventsJoined++;
}
if (eventsJoined === eventsJoinedPerUser) {
break;
}
}
}
client.end();
}
export async function addParticipants(eventId: number, participantsAmount: number) {
dotenv.config();
const client = new pg.Client({
host: process.env.DB_HOST,
database: process.env.DB_NAME,
user: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
});
await client.connect();
try {
// Get creator ID of the event (need to exclude)
const [creatorUserObj] = (
await client.query(
`
SELECT creator_id FROM events WHERE id = $1;
`,
[eventId]
)
).rows;
if (!creatorUserObj) {
throw new Error(`No such event (event id: ${eventId})!`);
}
const creatorUser: number = creatorUserObj.creator_id;
// Get participant ID of the event (need to exclude)
const participantsObj: { [key: string]: number }[] = (
await client.query(
`
SELECT user_id FROM participants
WHERE event_id = $1;
`,
[eventId]
)
).rows;
const participants = participantsObj.map((each) => {
return each.user_id;
});
// Obtain users info for event creation for each user (excluding creator)
const userIdListRawObj: { [key: string]: number }[] = (
await client.query(
`
SELECT id FROM users
WHERE id != $1;
`,
[creatorUser]
)
).rows;
const userIdListRaw: number[] = userIdListRawObj.map((each) => {
return each.id;
});
const participantsSet = new Set(participants);
const userIdList = userIdListRaw.filter((userId) => {
return !participantsSet.has(userId);
});
const loopTimes: number = Math.min(userIdList.length, participantsAmount);
for (let i = 0; i < loopTimes; i++) {
const usersIndex: number = Math.floor(Math.random() * userIdList.length);
const [userId] = userIdList.splice(usersIndex, 1);
await client.query(
`INSERT INTO participants
(event_id,user_id,created_at,updated_at)
VALUES ($1,$2,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP);`,
[eventId, userId]
);
}
} catch (e) {
logger.error(e);
}
client.end();
}
export async function addItems(eventId: number) {
dotenv.config();
const client = new pg.Client({
host: process.env.DB_HOST,
database: process.env.DB_NAME,
user: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
});
await client.connect();
try {
// Read random data parts for data assembling
let parts: DataParts = await jsonfile.readFile(path.join(__dirname, "/data/dataParts.json"));
// Get participant ID of the event (need to exclude)
const participantsObj: { [key: string]: number }[] = (
await client.query(
`
SELECT user_id FROM participants
WHERE event_id = $1;
`,
[eventId]
)
).rows;
const participants = participantsObj.map((each) => {
return each.user_id;
});
// addItems
let types = ["food", "drink", "decoration", "other"];
for (let type of types) {
for (let i = 0; i < parts[type].length; i++) {
await client.query(
`
INSERT INTO items
(name, purchased, type_name, event_id, user_id, quantity, price, created_at, updated_at)
VALUES ($1, $2 ,$3, $4, $5, $6, $7, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);
`,
[
parts[type][i],
Math.random() > 0.5,
type,
eventId,
participants.splice(0, 1)[0],
Math.floor(Math.random() * 20),
Math.floor(Math.random() * 1000),
]
);
}
}
} catch (e) {
logger.error(e);
}
client.end();
}
export async function truncateDB() {
dotenv.config();
const client = new pg.Client({
host: process.env.DB_HOST,
database: process.env.DB_NAME,
user: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
});
await client.connect();
await client.query(`
DELETE FROM event_date_time_votes;
DELETE FROM event_date_time;
DELETE FROM event_venues_votes;
DELETE FROM event_venues;
DELETE FROM comments;
DELETE FROM time_block_item;
DELETE FROM time_blocks;
DELETE FROM items;
DELETE FROM participants;
DELETE FROM events;
DELETE FROM users;
`);
client.end();
jsonfile.writeFile(path.join(__dirname, "/data/users.json"), []);
}

View File

@@ -0,0 +1,4 @@
import { addItems } from '../dbSetupFunctions';
const eventId = 1;
addItems(eventId);

View File

@@ -0,0 +1,5 @@
import { addParticipants } from '../dbSetupFunctions';
const eventId = 1;
const participantsAmount = 100;
addParticipants(eventId, participantsAmount);

View File

@@ -0,0 +1,3 @@
import { clearDB } from '../dbSetupFunctions';
clearDB();

View File

@@ -0,0 +1,4 @@
import { createEvents } from '../dbSetupFunctions';
const eventNumbers = 50;
createEvents(eventNumbers);

View File

@@ -0,0 +1,3 @@
import { initDB } from "../dbSetupFunctions";
initDB();

View File

@@ -0,0 +1,4 @@
import { joinEvents } from '../dbSetupFunctions';
const eventsJoinedPerUser = 50;
joinEvents(eventsJoinedPerUser);

View File

@@ -0,0 +1,46 @@
import pg from "pg";
import dotenv from "dotenv";
import { addItems, addParticipants, clearDB, createEvents, initDB, regUsers } from "../dbSetupFunctions";
const newUsersNumber: number = 100;
const createEventsAmountPerUser: number = 1;
const eventId: number = 1;
const participantAmount: number = 100;
lazy();
async function lazy() {
dotenv.config();
const client = new pg.Client({
host: process.env.DB_HOST,
database: process.env.DB_NAME,
user: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
});
await client.connect();
const [tableCount] = (
await client.query(`
select count(*)
from information_schema.tables
where table_schema = 'public';
`)
).rows;
console.log(tableCount);
await client.end();
if (parseInt(tableCount.count)) {
await clearDB();
}
await initDB();
await regUsers(newUsersNumber);
await createEvents(createEventsAmountPerUser);
await addParticipants(eventId, participantAmount);
await addItems(eventId);
}

View File

@@ -0,0 +1,4 @@
import { regUsers } from '../dbSetupFunctions';
const newUsersAmount = 10;
regUsers(newUsersAmount);

View File

@@ -0,0 +1,3 @@
import { truncateDB } from '../dbSetupFunctions';
truncateDB();

View File

@@ -0,0 +1,13 @@
import * as bcrypt from 'bcryptjs';
const SALT_ROUNDS = 10;
export async function hashPassword(plainPassword: string) {
const hash = await bcrypt.hash(plainPassword, SALT_ROUNDS);
return hash;
}
export async function checkPassword(plainPassword: string, hashPassword: string) {
const match = await bcrypt.compare(plainPassword, hashPassword);
return match;
}

View File

@@ -0,0 +1,3 @@
export function onlyNumbers(str: string): boolean {
return /^[0-9]+$/.test(str);
}

View File

@@ -0,0 +1,29 @@
import express from 'express';
export const isLoggedIn = (req: express.Request, res: express.Response, next: express.NextFunction) => {
if (req.session?.user) {
next();
} else {
res.status(404).redirect('/landingPage.html');
}
};
export const isLoggedInAPI = (req: express.Request, res: express.Response, next: express.NextFunction) => {
if (req.session?.user) {
next();
} else {
// redirect to 404 page
res.status(400).json({ error: "You don't have the permission" });
}
};
export const isLoggedInInvitation = (req: express.Request, res: express.Response, next: express.NextFunction) => {
if (req.session?.user) {
next();
} else {
res.json({
status: false,
login: false
});
}
};

View File

@@ -0,0 +1,12 @@
import winston from 'winston';
const logFormat = winston.format.printf(function (info) {
let date = new Date().toISOString();
return `${date}[${info.level}]: ${info.message}\n`;
});
export const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(winston.format.colorize(), logFormat),
transports: [new winston.transports.Console()]
});

View File

@@ -0,0 +1,60 @@
export interface Events {
id: number;
name: string;
venue: string | null;
start_datetime: string | null;
end_datetime: string | null;
creator_id: number;
invitation_token: string;
deleted: boolean;
created_at: string;
updated_at: string;
status?: string;
}
export interface Users {
id: number;
first_name: string;
last_name: string;
email: string;
phone: string | null;
password: string;
created_at: string;
updated_at: string;
}
export type UsersInput = Omit<Users, 'id' | 'created_at' | 'updated_at'>;
export type UserPickedInput = Pick<Users, 'id' | 'created_at' | 'updated_at'>;
export interface Participants {
id: number;
event_id: number;
user_id: number;
created_at: string;
updated_at: string;
}
export interface DataParts {
firstName: string[];
lastName: string[];
emailHost: string[];
phoneAreaCode: string[];
partyReason: string[];
streetName: string[];
food: string[];
drink: string[];
decoration: string[];
other: string[];
}
export interface Items {
event_id: number;
user_id: number;
name: string;
type_name: string;
quantity: number;
price: number | null;
created_at: string;
updated_at: string;
}

View File

@@ -0,0 +1,16 @@
#!/usr/bin/env bash
docker compose kill
docker compose down
set -ex
docker compose up -d --build
# sleep 3
# docker compose kill
# docker compose down
# docker compose exec -it backend bash

View File

@@ -0,0 +1,46 @@
services:
postgres:
image: postgres
shm_size: "256m"
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
PGDATA: /data/postgres
POSTGRES_DB: test
volumes:
- postgres:/data/postgres
ports:
- "5432:5432"
restart: always
pgweb:
# container_name: pgweb
image: sosedoff/pgweb
ports:
- 8081:8081
environment:
- DATABASE_URL=postgres://postgres:postgres@postgres:5432/test?sslmode=disable
depends_on:
- postgres
restart: always
backend:
image: node:18-buster
command: sleep infinity
ports:
- 8080:8080
volumes:
- $PWD/backend:/usr/src/app
working_dir: /usr/src/app
restart: always
environment:
- POSTGRES_HOST=postgres
- DB_NAME=postgres
- DB_USERNAME=postgres
- DB_PASSWORD=postgres
volumes:
backend_node_modules:
cms_node_modules:
postgres:

View File

@@ -0,0 +1,86 @@
---
tags: [tecky, typescript, javascript]
---
# Party Planner
A planner app that helps party organizers to manage party details and members.
Users can create event, start polls on date/venue, invite participants, manage event timetable, organize item list and assign participants to prepare the items.
# Database Guide
All database seeding script files are stored in '/util/database/seedingFunctions'
Parameters can be altered in the files
### Set up DB with data with just one script
Run the following
```bash
npm run lazy
```
### Initiate tables to an empty DB
Run the following
```bash
npm run initDB
```
### Remove all DB tables
Run the following
```bash
npm run clearDB
```
### Clear all DB data
Run the following
```bash
npm run truncateDB
```
### Register users
Run the following
```bash
npm run regUsers
```
### Create events for every users
Run the following
```bash
npm run createEvents
```
### Join events for every users
Run the following
```bash
npm run joinEvents
```
### Add participants to a specific event
Run the following
```bash
npm run addParticipants
```
### Add items to a specific event
Run the following
```bash
npm run addItems
```

3428
_tecky/party-planner/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,59 @@
{
"name": "party",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"docker_rebuild": "docker compose up -d --build",
"into_backend": "docker compose exec -it backend bash",
"start": "ts-node-dev app.ts",
"format": "prettier --config ./prettierrc.json --write .",
"lazy": "ts-node ./util/database/seedingFunctions/lazy.ts",
"initDB": "ts-node ./util/database/seedingFunctions/initDB.ts",
"clearDB": "ts-node ./util/database/seedingFunctions/clearDB.ts",
"truncateDB": "ts-node ./util/database/seedingFunctions/truncateDB.ts",
"regUsers": "ts-node ./util/database/seedingFunctions/regUsers.ts",
"createEvents": "ts-node ./util/database/seedingFunctions/createEvents.ts",
"joinEvents": "ts-node ./util/database/seedingFunctions/joinEvents.ts",
"addParticipants": "ts-node ./util/database/seedingFunctions/addParticipants.ts",
"addItems": "ts-node ./util/database/seedingFunctions/addItems.ts",
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/pofungt/party.git"
},
"author": "",
"license": "ISC",
"bugs": {
"url": "https://github.com/pofungt/party/issues"
},
"homepage": "https://github.com/pofungt/party#readme",
"dependencies": {
"@types/bcryptjs": "^2.4.2",
"@types/dotenv": "^8.2.0",
"@types/express-session": "^1.17.5",
"@types/jsonfile": "^6.1.0",
"@types/pg": "^8.6.5",
"@types/socket.io": "^3.0.2",
"@types/winston": "^2.4.4",
"bcryptjs": "^2.4.3",
"cross-fetch": "^3.1.5",
"date-fns": "^2.29.3",
"dotenv": "^16.0.3",
"express": "^4.18.1",
"express-session": "^1.17.3",
"grant": "^5.4.21",
"jsonfile": "^6.1.0",
"pg": "^8.8.0",
"prettier": "^2.7.1",
"socket.io": "^4.5.2",
"ts-node": "^10.9.1",
"typescript": "^4.8.3",
"winston": "^3.8.2"
},
"devDependencies": {
"@types/express": "^4.17.14",
"ts-node-dev": "^2.0.0"
}
}

View File

@@ -0,0 +1,13 @@
{
"singleQuote": true,
"trailingComma": "none",
"tabWidth": 4,
"useTabs": true,
"semi": true,
"quoteProps": "as-needed",
"jsxSingleQuote": true,
"bracketSpacing": true,
"jsxBracketSameLine": true,
"arrowParens": "always",
"printWidth": 120
}