diff --git a/foftickets.js b/foftickets.js index 2f3e46a..c8b7c07 100644 --- a/foftickets.js +++ b/foftickets.js @@ -23,8 +23,10 @@ const QRSalt="!SaltyMagic5392370662"; // + Issue Tickets // + Transfer Tickets Complicated page. (One ticket: static with transfer and QR code. Multi: buttons to transfer or show QR code) // Split Transfer into admin (change owner, unused/used/cancelled pulldown) and public (no Used column) versions -// Claim Tickets // + Use Ticket +// Claim One Claims, displays QRCode, and allows transfer with a single button. +// Claim Many Claims, but then displays tickets line by line with update and QRCode functionality +// Edit Regex based ticket list. Allows changing Owner, Offered, Used (Dropdown). // // ToDo, Later // + Use a templating engine @@ -56,12 +58,13 @@ const users = { "teppy@egenesis.com" : { password: hashPW("x6321872X.1"), "fallsonfire@gmail.com" : { password: hashPW("indiantreeonfire"), superuser:false } }; -const tickets = { "habitat-1" : { owner: "teppy@egenesis.com" , offered: "", paid: 0.00, used: false }, - "habitat-2" : { owner: "teppy@egenesis.com" , offered: "", paid: 0.00 }, - "habitat-3" : { owner: "ginachen94@gmail.com", offered: "teppy@egenesis.com", paid: 0.00 }, - "habitat-4" : { owner: "teppy@egenesis.com" , offered: "noahstern00@gmail.com", paid: 0.00 }, - "habitat-5" : { owner: "teppy@egenesis.com" , offered: "", paid: 0.00, used: true }, - "habitat-6" : { owner: "teppy@egenesis.com" , offered: "", paid: 0.00 }, +// Status can be "i"=issued, "u"=used, "r"=revoked" +const tickets = { "habitat-1" : { owner: "teppyx@egenesis.com" , offered: "", paid: 0.00, status:"i" }, + "habitat-2" : { owner: "teppyx@egenesis.com" , offered: "", paid: 0.00, status:"u" }, + "habitat-3" : { owner: "ginachen94@gmail.com", offered: "teppyx@egenesis.com", paid: 0.00, status:"i" }, + "habitat-4" : { owner: "teppyx@egenesis.com" , offered: "noahstern00@gmail.com", paid: 0.00, status:"i" }, + "habitat-5" : { owner: "teppyx@egenesis.com" , offered: "", paid: 0.00, status:"r" }, + "habitat-6" : { owner: "teppyx@egenesis.com" , offered: "", paid: 0.00, status:"i" }, }; const camps = { "habitat": { leader: "teppy@egenesis.com", lastid:6 } }; @@ -82,6 +85,7 @@ app.use(session({ // Middleware to protect routes function requireLogin(req, res, next) { if (req.session.username) return next(); + req.session.returnTo=req.originalUrl; return res.redirect('/login'); } @@ -139,7 +143,7 @@ function categorizeTickets(username) { let offering=0; let simpleoffered=0; let thet=""; - for (const t in tickets) if (!tickets[t].used) { + for (const t in tickets) if (tickets[t].status=="i") { if (tickets[t].owner==username && tickets[t].offered=="") simpleowner+=1; else if (tickets[t].owner==username && tickets[t].offered!=username) offering+=1; else if (tickets[t].owner!=username && tickets[t].offered==username) simpleoffered+=1; @@ -164,16 +168,30 @@ app.get('/camps',requireSuperUser, (req,res) => { } for (const t in tickets) { const parts=t.split("-"); - const campname=parts[0]; + const campname=parts[0]; const ticketnum=Number(parts[1]); -// camplist[campname]??={ leader:"", issued:0, claimed:0, used:0 }; - camplist[campname].issued+=1; - if (tickets[t].owner!="") camplist[campname].claimed+=1; - if (tickets[t].used) camplist[campname].used+=1; + if (tickets[t].status!="r") { + camplist[campname].issued+=1; + if (tickets[t].owner!="") camplist[campname].claimed+=1; + if (tickets[t].status=="u") camplist[campname].used+=1; + } } - return res.render("camps",{ username:"Teppy", camps:camplist }); + return res.render("camps",{ username:req.session.username, camps:camplist }); }) +app.post("/camps",requireSuperUser,(req,res) => { + console.log("New camp: "); + const campname=req.body.campname; + const leader=req.body.leader; + const qty=Number(req.body.qty); + camps[campname]??={ leader:leader, lastid:0 }; + for (let i=0; i { const camplist={}; @@ -182,26 +200,68 @@ app.get('/issue', requireSuperUser, (req,res) => { const campname=parts[0]; const ticketnum=Number(parts[1]); camplist[campname]??={ issued:0, claimed:0, used:0 }; - camplist[campname].issued+=1; - if (tickets[t].owner!="") camplist[campname].claimed+=1; - if (tickets[t].used) camplist[campname].used+=1; + if (tickets[t].status!="r") { + camplist[campname].issued+=1; + if (tickets[t].owner!="") camplist[campname].claimed+=1; + if (tickets[t].status=="u") camplist[campname].used+=1; + } } - return res.render("issue",{ username:"Teppy", camps:camplist }); + return res.render("issue",{ camps:camplist }); }) -app.post("/camps",(req,res) => { - const campname=req.body.campname; - const email=req.body.email; - const qty=Number(req.body.qty); - console.log("New camp: ",campname); - camps[campname]??={ leader:leader, lastid:0 }; - for (let i=0; i { + let username=req.session.username; + const edit={ tickets: {} }; + for (const t in tickets) if (tickets[t].owner==username) { + edit.tickets[t]={}; + edit.tickets[t].owner=tickets[t].owner; + edit.tickets[t].offered=tickets[t].offered; } - return res.redirect("/camps"); + return res.render("camplead",edit); }) - + + +app.get('/editcamp', requireSuperUser, (req,res) => { + let campname=req.query.campname; + const edit={ username:req.session.username, tickets: {} }; + for (const t in tickets) { + const parts=t.split("-"); + const cname=parts[0]; + const tnum=Number(parts[1]); + console.log("Cname: ",cname," campname ",campname," tnum ",tnum); + if (cname==campname) { + edit.tickets[t]={}; + edit.tickets[t].owner=tickets[t].owner; + edit.tickets[t].offered=tickets[t].offered; + edit.tickets[t].status=tickets[t].status; + } + } + console.log("Edit is ",edit); + return res.render("editcamp",edit); + }) + + +app.get('/mytickets',requireLogin, (req,res)=> { + let username=req.session.username; + let claimed=0; + let owned=0; + let theticket=""; + const edit={ username:req.session.username, tickets: {} }; + for (const t in tickets) { + if (tickets[t].status=="i" && tickets[t].offered==username) { claimed++; tickets[t].owner=username; tickets[t].offered=""; } + if (tickets[t].status=="i" && tickets[t].owner==username) { + owned++; + if (owned==1) theticket=t; else theticket=""; + edit.tickets[t]={}; + edit.tickets[t].owner=tickets[t].owner; + edit.tickets[t].offered=tickets[t].offered; + } + } + if (owned==0) return res.render("message",{ username:username, message:"You have no unused tickets" }); + else if (owned==1) return res.render("oneticket",{ username:username, ticket:theticket, offered:tickets[theticket].offered }); + else return res.render("manytickets",edit); +}); // Big Kahuna // If you have zero tickets, show something saying that @@ -247,11 +307,18 @@ app.post("/toggle", requireSuperUser, (req,res) => { const ticket=req.body.ticket; const isChecked = req.body.checked; tickets[ticket].used=isChecked; - // Perform any server-side logic here res.json({ message: 'Checkbox state received', checked: isChecked }); }) +app.post("/changestatus", requireSuperUser, (req,res) => { + const ticket=req.body.ticket; + console.log("Changestatus ",ticket); + tickets[ticket].status=req.body.status; + res.json({ message: 'Status received', status: req.body.status }); + }) + app.post("/updateoffered", requireLogin, (req,res) => { + console.log("UpdateOffered being rubn"); const ticket=req.body.ticket; const offered=req.body.offered; if (tickets[ticket].owner!=req.session.username) res.status(500).send("Ticket "+ticket+" owned by someone else"); @@ -263,6 +330,16 @@ app.post("/updateoffered", requireLogin, (req,res) => { }) +app.post("/updateticketsu", requireSuperUser, (req,res) => { + const ticket=req.body.ticket; + const owner=req.body.owner; + const offered=req.body.offered; + tickets[ticket].owner=req.body.owner; + tickets[ticket].offered=req.body.offered; + res.json({ message: 'Updated '+ticket }); + }) + + app.get("/useticket",(req,res) => { let ticket=req.t; let hash=req.h; @@ -445,6 +522,13 @@ app.post('/qrcode',requireLogin,async (req,res) => { return res.send({ owner:tickets[ticket].owner, qrcode: URL }); }) +app.post('/qrcodesu',requireSuperUser,async (req,res) => { + const username=req.session.username; + const ticket=req.body.ticket; + const URL=await QRCode.toDataURL('localhost:3000/useticket?t='+ticket+'&h='+hashQR(ticket,username)); + return res.send({ owner:tickets[ticket].owner, qrcode: URL }); + }) + // Protected routes app.get('/products', requireLogin, (req, res) => { res.send(` diff --git a/public/js/nav.js b/public/js/nav.js index 0c893eb..e69de29 100644 --- a/public/js/nav.js +++ b/public/js/nav.js @@ -1,8 +0,0 @@ -document.addEventListener('DOMContentLoaded', function() { - const menuIcon = document.getElementById('menu-icon'); - const navLinks = document.getElementById('nav-links'); - - menuIcon.addEventListener('click', function() { - navLinks.classList.toggle('active'); - }); - }); diff --git a/public/styles.css b/public/styles.css index 50a28c5..10cab71 100644 --- a/public/styles.css +++ b/public/styles.css @@ -37,15 +37,48 @@ cursor: pointer; } -.navbar { - position: relative; - background: #333; - color: #fff; - display: inline-block; - align-items: center; - padding: 0.5rem 1rem; - top:0; - left:0; + +/* General Reset */ +body { + margin: 0; + font-family: Arial, sans-serif; + display: flex; /* Ensure the body uses a flex container */ + height: 100vh; /* Full viewport height */ +} + +/* Sidebar (Nav Links) */ +.nav-links { + width: 250px; /* Set a fixed width for the sidebar */ + background-color: #f4f4f4; /* Light gray background */ + padding: 20px; + box-shadow: 2px 0 5px rgba(0, 0, 0, 0.1); /* Add a slight shadow for separation */ + display: flex; + flex-direction: column; /* Ensure items stack vertically */ + justify-content: flex-start; /* Optional: adjust spacing of items */ + min-height: 100vh; /* Stretch to full viewport height */ +} + +.nav-links nav ul { + list-style: none; /* Remove bullet points */ + padding: 0; +} + +.nav-links nav ul li { + margin: 10px 0; /* Add spacing between links */ +} + +.nav-links nav ul li a { + text-decoration: none; /* Remove underline */ + color: #333; +} + +.nav-links nav ul li a:hover { + color: #007BFF; /* Change link color on hover */ +} + +.content { + flex-grow: 1; /* Allow content to fill the remaining space */ + padding: 20px; } .menu-icon { @@ -62,38 +95,3 @@ transition: 0.4s; } -.nav-links { - list-style: none; - margin: 0; - padding: 0; - display: none; /* Hidden by default; shown when .active */ - position: absolute; - top: 100%; /* Appear below the navbar */ - left: 0; /* Aligned left under the hamburger */ - width: 200px; /* Fixed width for the dropdown panel */ - background: #333; - box-shadow: 0 2px 8px rgba(0,0,0,0.4); /* Optional shadow for a "dropdown" effect */ - border-radius: 4px; /* Slight rounding for a nicer look */ -} - -.nav-links li { - border-bottom: 1px solid #444; -} - -.nav-links li:last-child { - border-bottom: none; /* No border on the last item */ -} - -.nav-links li a { - color: #fff; - text-decoration: none; - display: block; - padding: 0.75rem 1rem; - white-space: nowrap; /* Prevent awkward text wrapping */ -} - -/* Show the nav-links when .active is toggled by JavaScript */ -.nav-links.active { - display: block; -} - diff --git a/views/camplead.ejs b/views/camplead.ejs new file mode 100644 index 0000000..3abda6a --- /dev/null +++ b/views/camplead.ejs @@ -0,0 +1,118 @@ + + + + Your Tickets (Camp Lead) + + + + <%- include('partials/nav') %> + +

Welcome, <%= username %>!

+
+
Server Ready
+ + + + + + + + <% for (const t in tickets) { %> + + + + + + + <% } %> + + +
Ticket#OwnerOffered ToAction
<%=t%><%=tickets[t].owner%>
+
+ + + diff --git a/views/camps.ejs b/views/camps.ejs index 6948d76..a7e3bef 100644 --- a/views/camps.ejs +++ b/views/camps.ejs @@ -6,8 +6,7 @@ <%- include('partials/nav') %> -

Camp Editor (Administrator <%= username %>)

-
+ @@ -18,7 +17,7 @@ <% for (const c in camps) { %> - + @@ -28,16 +27,11 @@
Camp
<%=c%><%=c%> <%=camps[c].leader%> <%=camps[c].issued%> <%=camps[c].claimed%>
-
Create New Group:
+
Create New Camp:
Group Name:
- Lead Email:
- Initial Qty:
+ Lead Email:
+ Initial Qty (Optional):
- - - - diff --git a/views/editcamp.ejs b/views/editcamp.ejs new file mode 100644 index 0000000..842a353 --- /dev/null +++ b/views/editcamp.ejs @@ -0,0 +1,137 @@ + + + + Edit Camp + + + + <%- include('partials/nav') %> + +
+
Server Ready
+ + + + + + + + + <% for (const t in tickets) { %> + + + + + + + + <% } %> + + +
Ticket#OwnerOffered ToStatusAction
<%=t%> + +
+
+ + + diff --git a/views/issue.ejs b/views/issue.ejs index 9484f46..6cd2dc0 100644 --- a/views/issue.ejs +++ b/views/issue.ejs @@ -5,28 +5,34 @@ -

Welcome, <%= username %>!

-
- - - - - - - - <% for (const [c,v] of Object.entries(camps)) { %> + <%- include('partials/nav') %> +
+ +
GroupQuantityClaimedUsed
+ + + + + + + <% for (const [c,v] of Object.entries(camps)) { %> - <% } %> -
GroupQuantityClaimedUsed
<%=c%> <%=v.issued%> <%=v.claimed%> <%=v.used%>
+ <% } %> + Issue new tickets:
Group Name:
Lead Email:
Qty:
- -
- + + diff --git a/views/message.ejs b/views/message.ejs new file mode 100644 index 0000000..978e85b --- /dev/null +++ b/views/message.ejs @@ -0,0 +1,12 @@ + + + + Message + + + + <%- include('partials/nav') %> + <%=message%> + + + diff --git a/views/oneticket.ejs b/views/oneticket.ejs new file mode 100644 index 0000000..c5c1d3f --- /dev/null +++ b/views/oneticket.ejs @@ -0,0 +1,17 @@ + + + + Your Ticket + + + <%- include('partials/nav') %> + +
+ Transfer Ticket: + + +
+
+ Placeholder Image + + diff --git a/views/partials/nav.ejs b/views/partials/nav.ejs index 3f143bf..aad63f1 100644 --- a/views/partials/nav.ejs +++ b/views/partials/nav.ejs @@ -1,17 +1,10 @@ - + diff --git a/views/transfer.ejs b/views/transfer.ejs index a2ba389..0052f99 100644 --- a/views/transfer.ejs +++ b/views/transfer.ejs @@ -38,14 +38,6 @@