Compare commits
1 Commits
6-poll-det
...
b9245240c2
| Author | SHA1 | Date | |
|---|---|---|---|
| b9245240c2 |
32
.gitignore
vendored
@@ -22,34 +22,6 @@
|
|||||||
go.work
|
go.work
|
||||||
go.work.sum
|
go.work.sum
|
||||||
|
|
||||||
# ---> React & npm
|
# env file
|
||||||
node_modules/
|
.env
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
pnpm-debug.log*
|
|
||||||
|
|
||||||
# ---> NPM cache
|
|
||||||
.npmrc
|
|
||||||
|
|
||||||
# ---> Webpack
|
|
||||||
dist/
|
|
||||||
build/
|
|
||||||
|
|
||||||
# ---> Babel
|
|
||||||
.babelrc
|
|
||||||
babel.config.js
|
|
||||||
|
|
||||||
# ---> ESLint
|
|
||||||
.eslintrc
|
|
||||||
.eslintignore
|
|
||||||
eslintcache.js
|
|
||||||
|
|
||||||
# ---> Prettier
|
|
||||||
.prettierrc
|
|
||||||
prettierignore
|
|
||||||
|
|
||||||
# ---> Jest
|
|
||||||
coverage/
|
|
||||||
jest.config.js
|
|
||||||
|
|
||||||
|
|||||||
23
client/.gitignore
vendored
@@ -1,23 +0,0 @@
|
|||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
|
||||||
|
|
||||||
# dependencies
|
|
||||||
/node_modules
|
|
||||||
/.pnp
|
|
||||||
.pnp.js
|
|
||||||
|
|
||||||
# testing
|
|
||||||
/coverage
|
|
||||||
|
|
||||||
# production
|
|
||||||
/build
|
|
||||||
|
|
||||||
# misc
|
|
||||||
.DS_Store
|
|
||||||
.env.local
|
|
||||||
.env.development.local
|
|
||||||
.env.test.local
|
|
||||||
.env.production.local
|
|
||||||
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
16720
client/package-lock.json
generated
@@ -1,42 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "client",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"private": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@emotion/react": "^11.14.0",
|
|
||||||
"@emotion/styled": "^11.14.1",
|
|
||||||
"@mui/icons-material": "^7.3.7",
|
|
||||||
"@mui/material": "^7.3.7",
|
|
||||||
"axios": "^1.13.2",
|
|
||||||
"cra-template-pwa": "2.0.0",
|
|
||||||
"react": "^19.2.3",
|
|
||||||
"react-dom": "^19.2.3",
|
|
||||||
"react-router": "7.12.0",
|
|
||||||
"react-scripts": "5.0.1",
|
|
||||||
"recharts": "^3.7.0"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"start": "react-scripts start",
|
|
||||||
"build": "react-scripts build",
|
|
||||||
"test": "react-scripts test",
|
|
||||||
"eject": "react-scripts eject"
|
|
||||||
},
|
|
||||||
"eslintConfig": {
|
|
||||||
"extends": [
|
|
||||||
"react-app",
|
|
||||||
"react-app/jest"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"browserslist": {
|
|
||||||
"production": [
|
|
||||||
">0.2%",
|
|
||||||
"not dead",
|
|
||||||
"not op_mini all"
|
|
||||||
],
|
|
||||||
"development": [
|
|
||||||
"last 1 chrome version",
|
|
||||||
"last 1 firefox version",
|
|
||||||
"last 1 safari version"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 3.8 KiB |
@@ -1,40 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<meta name="theme-color" content="#000000" />
|
|
||||||
<meta name="description" content="Web site created using create-react-app" />
|
|
||||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
|
||||||
<!--
|
|
||||||
manifest.json provides metadata used when your web app is installed on a
|
|
||||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
|
||||||
-->
|
|
||||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
|
||||||
<!--
|
|
||||||
Notice the use of %PUBLIC_URL% in the tags above.
|
|
||||||
It will be replaced with the URL of the `public` folder during the build.
|
|
||||||
Only files inside the `public` folder can be referenced from the HTML.
|
|
||||||
|
|
||||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
|
||||||
work correctly both with client-side routing and a non-root public URL.
|
|
||||||
Learn how to configure a non-root public URL by running `npm run build`.
|
|
||||||
-->
|
|
||||||
<title>React App</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
|
||||||
<div id="root"></div>
|
|
||||||
<!--
|
|
||||||
This HTML file is a template.
|
|
||||||
If you open it directly in the browser, you will see an empty page.
|
|
||||||
|
|
||||||
You can add webfonts, meta tags, or analytics to this file.
|
|
||||||
The build step will place the bundled scripts into the <body> tag.
|
|
||||||
|
|
||||||
To begin the development, run `npm start` or `yarn start`.
|
|
||||||
To create a production bundle, use `npm run build` or `yarn build`.
|
|
||||||
-->
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 9.4 KiB |
@@ -1,25 +0,0 @@
|
|||||||
{
|
|
||||||
"short_name": "React App",
|
|
||||||
"name": "Create React App Sample",
|
|
||||||
"icons": [
|
|
||||||
{
|
|
||||||
"src": "favicon.ico",
|
|
||||||
"sizes": "64x64 32x32 24x24 16x16",
|
|
||||||
"type": "image/x-icon"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "logo192.png",
|
|
||||||
"type": "image/png",
|
|
||||||
"sizes": "192x192"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "logo512.png",
|
|
||||||
"type": "image/png",
|
|
||||||
"sizes": "512x512"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"start_url": ".",
|
|
||||||
"display": "standalone",
|
|
||||||
"theme_color": "#000000",
|
|
||||||
"background_color": "#ffffff"
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
# https://www.robotstxt.org/robotstxt.html
|
|
||||||
User-agent: *
|
|
||||||
Disallow:
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
.App {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.App-logo {
|
|
||||||
height: 40vmin;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: no-preference) {
|
|
||||||
.App-logo {
|
|
||||||
animation: App-logo-spin infinite 20s linear;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.App-header {
|
|
||||||
background-color: #282c34;
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: calc(10px + 2vmin);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.App-link {
|
|
||||||
color: #61dafb;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes App-logo-spin {
|
|
||||||
from {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropbtn {
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-content {
|
|
||||||
display: none; /* Hidden by default */
|
|
||||||
position: absolute;
|
|
||||||
background-color: #f9f9f9;
|
|
||||||
min-width: 160px;
|
|
||||||
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-content a {
|
|
||||||
color: black;
|
|
||||||
padding: 12px 16px;
|
|
||||||
text-decoration: none;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Show the dropdown menu on hover */
|
|
||||||
.dropdown:hover .dropdown-content {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Add zebra striping to tables */
|
|
||||||
table tr:nth-child(odd) {
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { BrowserRouter, Routes, Route, Link } from "react-router";
|
|
||||||
import Home from "./pages/Home";
|
|
||||||
import AdminLogin from "./pages/AdminLogin";
|
|
||||||
import AdminMembers from "./pages/AdminMembers";
|
|
||||||
import AdminMembersView from "./pages/AdminMembersView";
|
|
||||||
import AdminCreateVote from "./pages/AdminCreateVote";
|
|
||||||
import PollList from "./pages/PollList";
|
|
||||||
import PollDetails from "./pages/PollDetails";
|
|
||||||
import './App.css';
|
|
||||||
|
|
||||||
export default function App() {
|
|
||||||
return (
|
|
||||||
<BrowserRouter>
|
|
||||||
<nav style={{ padding: "1rem", background: "#f5f5f5" }}>
|
|
||||||
<Link to="/">Home</Link> |
|
|
||||||
<Link to="/admin-login">Admin Login</Link> |
|
|
||||||
{/* Member dropdown */}
|
|
||||||
<div className="dropdown">
|
|
||||||
<button className="dropbtn">Member ▼</button>
|
|
||||||
<div className="dropdown-content">
|
|
||||||
<a href="/admin-members">Upload Members</a>
|
|
||||||
<a href="/admin-members-view">View Members</a>
|
|
||||||
</div>
|
|
||||||
</div> |
|
|
||||||
<div className="dropdown">
|
|
||||||
<button className="dropbtn">Vote ▼</button>
|
|
||||||
<div className="dropdown-content">
|
|
||||||
<a href="/create-vote">Create Vote</a>
|
|
||||||
<a href="/polls">Poll List</a>
|
|
||||||
<a href="/poll-details/:id">View Poll Details</a> {/* Add this line */}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<Routes>
|
|
||||||
<Route path="/" element={<Home />} />
|
|
||||||
<Route path="/admin-login" element={<AdminLogin />} />
|
|
||||||
{/* Member routes */}
|
|
||||||
<Route path="/admin-members" element={<AdminMembers />} />
|
|
||||||
<Route path="/admin-members-view" element={<AdminMembersView />} />
|
|
||||||
{/* Vote routes */}
|
|
||||||
<Route path="/create-vote" element={<AdminCreateVote />} />
|
|
||||||
<Route path="/polls" element={<PollList />} />
|
|
||||||
<Route path="/poll-details/:id" element={<PollDetails />} /> {/* Add this route */}
|
|
||||||
</Routes>
|
|
||||||
</BrowserRouter>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { render, screen } from '@testing-library/react';
|
|
||||||
import App from './App';
|
|
||||||
|
|
||||||
test('renders learn react link', () => {
|
|
||||||
render(<App />);
|
|
||||||
const linkElement = screen.getByText(/learn react/i);
|
|
||||||
expect(linkElement).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
|
||||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
|
||||||
sans-serif;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
||||||
|
|
||||||
code {
|
|
||||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
|
||||||
monospace;
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { createRoot } from "react-dom/client";
|
|
||||||
import App from "./App";
|
|
||||||
import "./index.css"; // optional – keep CRA default styling
|
|
||||||
|
|
||||||
const container = document.getElementById("root");
|
|
||||||
createRoot(container).render(<App />);
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3">
|
|
||||||
<g fill="#61DAFB">
|
|
||||||
<path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/>
|
|
||||||
<circle cx="420.9" cy="296.5" r="45.7"/>
|
|
||||||
<path d="M520.5 78.1z"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.6 KiB |
@@ -1,67 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
|
|
||||||
function AdminCreateVote() {
|
|
||||||
const [question, setQuestion] = useState('');
|
|
||||||
const [expiresInHours, setExpiresInHours] = useState('');
|
|
||||||
const [status, setStatus] = useState("");
|
|
||||||
|
|
||||||
|
|
||||||
const handleSubmit = async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("question", question);
|
|
||||||
formData.append("expiresInHours", expiresInHours);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const resp = await fetch("/api/admin/new-vote", {
|
|
||||||
method: "POST",
|
|
||||||
body: formData,
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await resp.json();
|
|
||||||
|
|
||||||
if (data.success) {
|
|
||||||
setStatus(`✅ Vote created with ID`);
|
|
||||||
} else {
|
|
||||||
setStatus(`❌ Server error: ${data.error}`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
setStatus("❌ Failed to create vote. Please try again: " + (error.response?.data?.error || error.message));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1>Create New Vote</h1>
|
|
||||||
<form onSubmit={handleSubmit}>
|
|
||||||
<div>
|
|
||||||
<label>Question:</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={question}
|
|
||||||
onChange={(e) => setQuestion(e.target.value)}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label>Expires In (hours):</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
value={expiresInHours}
|
|
||||||
onChange={(e) => setExpiresInHours(e.target.value)}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit">Create Vote</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{status && <p style={{ marginTop: "1rem" }}>{status}</p>}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AdminCreateVote;
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
import React, { useState } from "react";
|
|
||||||
import { useNavigate } from "react-router";
|
|
||||||
|
|
||||||
export default function AdminLogin() {
|
|
||||||
const [username, setUsername] = useState("");
|
|
||||||
const [password, setPassword] = useState("");
|
|
||||||
const [error, setError] = useState("");
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const handleSubmit = async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setError("");
|
|
||||||
setIsLoading(true);
|
|
||||||
|
|
||||||
if (!username || !password) {
|
|
||||||
setError("⚠️ Please enter both username and password.");
|
|
||||||
setIsLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const resp = await fetch("/api/admin/login", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
username,
|
|
||||||
password,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await resp.json();
|
|
||||||
|
|
||||||
if (resp.ok && data.success) {
|
|
||||||
// Store the auth token in localStorage
|
|
||||||
localStorage.setItem("adminToken", data.token);
|
|
||||||
setError("");
|
|
||||||
navigate("/admin-members");
|
|
||||||
} else {
|
|
||||||
setError(`❌ ${data.error || "Login failed"}`);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
setError(`❌ Network error: ${err.message}`);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
padding: "2rem",
|
|
||||||
maxWidth: "400px",
|
|
||||||
margin: "5rem auto",
|
|
||||||
border: "1px solid #ddd",
|
|
||||||
borderRadius: "8px",
|
|
||||||
boxShadow: "0 2px 4px rgba(0,0,0,0.1)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<h2 style={{ textAlign: "center", marginBottom: "2rem" }}>Admin Login</h2>
|
|
||||||
<form onSubmit={handleSubmit}>
|
|
||||||
<div style={{ marginBottom: "1rem" }}>
|
|
||||||
<label htmlFor="username">Username:</label>
|
|
||||||
<input
|
|
||||||
id="username"
|
|
||||||
type="text"
|
|
||||||
value={username}
|
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
|
||||||
style={{
|
|
||||||
width: "100%",
|
|
||||||
padding: "0.5rem",
|
|
||||||
marginTop: "0.5rem",
|
|
||||||
border: "1px solid #ccc",
|
|
||||||
borderRadius: "4px",
|
|
||||||
boxSizing: "border-box",
|
|
||||||
}}
|
|
||||||
placeholder="Enter your username"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ marginBottom: "1.5rem" }}>
|
|
||||||
<label htmlFor="password">Password:</label>
|
|
||||||
<input
|
|
||||||
id="password"
|
|
||||||
type="password"
|
|
||||||
value={password}
|
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
|
||||||
style={{
|
|
||||||
width: "100%",
|
|
||||||
padding: "0.5rem",
|
|
||||||
marginTop: "0.5rem",
|
|
||||||
border: "1px solid #ccc",
|
|
||||||
borderRadius: "4px",
|
|
||||||
boxSizing: "border-box",
|
|
||||||
}}
|
|
||||||
placeholder="Enter your password"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
padding: "1rem",
|
|
||||||
marginBottom: "1rem",
|
|
||||||
backgroundColor: "#ffe0e0",
|
|
||||||
border: "1px solid #ff6b6b",
|
|
||||||
borderRadius: "4px",
|
|
||||||
color: "#c92a2a",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={isLoading}
|
|
||||||
style={{
|
|
||||||
width: "100%",
|
|
||||||
padding: "0.75rem",
|
|
||||||
backgroundColor: isLoading ? "#ccc" : "#007bff",
|
|
||||||
color: "white",
|
|
||||||
border: "none",
|
|
||||||
borderRadius: "4px",
|
|
||||||
cursor: isLoading ? "not-allowed" : "pointer",
|
|
||||||
fontSize: "1rem",
|
|
||||||
fontWeight: "bold",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{isLoading ? "Logging in..." : "Login"}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
|
||||||
import { useNavigate } from 'react-router';
|
|
||||||
|
|
||||||
export default function AdminMembers() {
|
|
||||||
const [year, setYear] = useState("");
|
|
||||||
const [file, setFile] = useState(null);
|
|
||||||
const [status, setStatus] = useState("");
|
|
||||||
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const isAdmin = () => {
|
|
||||||
return localStorage.getItem('adminToken') !== null;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!isAdmin()) {
|
|
||||||
navigate('/admin-login');
|
|
||||||
return <div>Redirecting...</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSubmit = async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
if (!year) {
|
|
||||||
setStatus("⚠️ Please enter a year.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!file) {
|
|
||||||
setStatus("⚠️ Please select a CSV file.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("year", year);
|
|
||||||
formData.append("members.csv", file); // name must match the Go handler
|
|
||||||
|
|
||||||
try {
|
|
||||||
const resp = await fetch("/api/admin/members", {
|
|
||||||
method: "POST",
|
|
||||||
body: formData,
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await resp.json();
|
|
||||||
|
|
||||||
if (data.success) {
|
|
||||||
setStatus(`✅ Uploaded!`);
|
|
||||||
} else {
|
|
||||||
setStatus(`❌ Server error: ${data.error}`);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
setStatus(`❌ Network error: ${err.message}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ padding: "2rem" }}>
|
|
||||||
<h2>Upload Members CSV</h2>
|
|
||||||
<form onSubmit={handleSubmit}>
|
|
||||||
<div style={{ marginBottom: "1rem" }}>
|
|
||||||
<label htmlFor="year">Year:</label>
|
|
||||||
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
id="year"
|
|
||||||
name="year"
|
|
||||||
value={year}
|
|
||||||
onChange={(e) => setYear(e.target.value)}
|
|
||||||
required
|
|
||||||
min="1900"
|
|
||||||
max="2100"
|
|
||||||
style={{ width: "150px", padding: "0.3rem" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ marginBottom: "1rem" }}>
|
|
||||||
<label htmlFor="members.csv">CSV File:</label>
|
|
||||||
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
id="members.csv"
|
|
||||||
name="members.csv"
|
|
||||||
accept=".csv"
|
|
||||||
onChange={(e) => setFile(e.target.files[0])}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit" style={{ padding: "0.5rem 1rem" }}>
|
|
||||||
Upload
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{status && <p style={{ marginTop: "1rem" }}>{status}</p>}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
import React, { useState } from "react";
|
|
||||||
import { useNavigate } from 'react-router';
|
|
||||||
import Table from '@mui/material/Table';
|
|
||||||
|
|
||||||
export default function AdminMembersView() {
|
|
||||||
const [year, setYear] = useState("");
|
|
||||||
const [members, setMembers] = useState([]);
|
|
||||||
const [status, setStatus] = useState("");
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const isAdmin = () => {
|
|
||||||
return localStorage.getItem('adminToken') !== null;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!isAdmin()) {
|
|
||||||
navigate('/admin-login');
|
|
||||||
return <div>Redirecting...</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSubmit = async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const resp = await fetch(`/api/admin/members/view?year=${year}`);
|
|
||||||
const data = await resp.json();
|
|
||||||
|
|
||||||
if (data.success) {
|
|
||||||
setMembers(data.members);
|
|
||||||
} else {
|
|
||||||
setStatus(`❌ Server error: ${data.error}`);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
setStatus(`❌ Network error: ${err.message}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ padding: "2rem" }}>
|
|
||||||
<h2>View Members</h2>
|
|
||||||
|
|
||||||
<form onSubmit={handleSubmit}>
|
|
||||||
<div style={{ marginBottom: "1rem" }}>
|
|
||||||
<label htmlFor="year">Year:</label>
|
|
||||||
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
id="year"
|
|
||||||
name="year"
|
|
||||||
value={year}
|
|
||||||
onChange={(e) => setYear(e.target.value)}
|
|
||||||
required
|
|
||||||
min="1900"
|
|
||||||
max="2100"
|
|
||||||
style={{ width: "150px", padding: "0.3rem" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit" style={{ padding: "0.5rem 1rem" }}>
|
|
||||||
View Members
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{members.length > 0 && (
|
|
||||||
<Table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Email</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{members.map((member, index) => (
|
|
||||||
<tr key={index}>
|
|
||||||
<td>{member.Name}</td>
|
|
||||||
<td>{member.Email}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</Table>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{status && <p style={{ marginTop: "1rem" }}>{status}</p>}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
export default function Home() {
|
|
||||||
return (
|
|
||||||
<div style={{ padding: "2rem" }}>
|
|
||||||
<h1>Welcome!</h1>
|
|
||||||
<p>This is the landing page. Use the navigation bar to go to the admin page.</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import axios from 'axios';
|
|
||||||
import {
|
|
||||||
PieChart,
|
|
||||||
Pie,
|
|
||||||
Cell,
|
|
||||||
Tooltip,
|
|
||||||
Legend,
|
|
||||||
} from 'recharts';
|
|
||||||
|
|
||||||
const COLORS = ['#0088FE', '#FEB43C'];
|
|
||||||
|
|
||||||
export default function PollDetail({ question }) {
|
|
||||||
const [poll, setPoll] = useState(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchPoll();
|
|
||||||
}, [question]);
|
|
||||||
|
|
||||||
const fetchPoll = async () => {
|
|
||||||
try {
|
|
||||||
const response = await axios.post('/api/admin/view-votes', { question });
|
|
||||||
setPoll(response.data[0]);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching poll:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!poll) return <div>Loading...</div>;
|
|
||||||
|
|
||||||
const memberData = [
|
|
||||||
{ name: 'Yes', value: poll.member_yes },
|
|
||||||
{ name: 'No', value: poll.member_no },
|
|
||||||
];
|
|
||||||
|
|
||||||
const nonMemberData = [
|
|
||||||
{ name: 'Yes', value: poll.non_member_yes },
|
|
||||||
{ name: 'No', value: poll.non_member_no },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1>{poll.question}</h1>
|
|
||||||
<p>Created At: {new Date(poll.created_at).toLocaleString()}</p>
|
|
||||||
|
|
||||||
<div style={{ width: '40%', margin: '20px' }}>
|
|
||||||
<PieChart width={400} height={300}>
|
|
||||||
<Pie
|
|
||||||
data={memberData}
|
|
||||||
cx="50%"
|
|
||||||
cy="50%"
|
|
||||||
labelLine={false}
|
|
||||||
outerRadius={80}
|
|
||||||
fill="#8884d8"
|
|
||||||
dataKey="value"
|
|
||||||
>
|
|
||||||
{memberData.map((entry, index) => (
|
|
||||||
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
|
|
||||||
))}
|
|
||||||
</Pie>
|
|
||||||
<Tooltip />
|
|
||||||
<Legend />
|
|
||||||
</PieChart>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ width: '40%', margin: '20px' }}>
|
|
||||||
<PieChart width={400} height={300}>
|
|
||||||
<Pie
|
|
||||||
data={nonMemberData}
|
|
||||||
cx="50%"
|
|
||||||
cy="50%"
|
|
||||||
labelLine={false}
|
|
||||||
outerRadius={80}
|
|
||||||
fill="#8884d8"
|
|
||||||
dataKey="value"
|
|
||||||
>
|
|
||||||
{nonMemberData.map((entry, index) => (
|
|
||||||
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
|
|
||||||
))}
|
|
||||||
</Pie>
|
|
||||||
<Tooltip />
|
|
||||||
<Legend />
|
|
||||||
</PieChart>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p>Member Yes Votes: {poll.member_yes}</p>
|
|
||||||
<p>Member No Votes: {poll.member_no}</p>
|
|
||||||
<p>Non-Member Yes Votes: {poll.non_member_yes}</p>
|
|
||||||
<p>Non-Member No Votes: {poll.non_member_no}</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
|
||||||
import axios from 'axios';
|
|
||||||
import {
|
|
||||||
PieChart,
|
|
||||||
Pie,
|
|
||||||
Tooltip,
|
|
||||||
Legend,
|
|
||||||
Cell,
|
|
||||||
} from 'recharts';
|
|
||||||
|
|
||||||
const COLORS = ['#0088FE', '#FEB43C'];
|
|
||||||
|
|
||||||
export default function PollDetails() {
|
|
||||||
const [poll, setPoll] = useState(null);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
|
|
||||||
// Get poll ID from URL parameters
|
|
||||||
const match = window.location.pathname.match(/\/poll-details\/(\d+)/);
|
|
||||||
const pollId = match ? parseInt(match[1], 10) : null;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (pollId) {
|
|
||||||
fetchPollDetails(pollId);
|
|
||||||
}
|
|
||||||
}, [pollId]);
|
|
||||||
|
|
||||||
const fetchPollDetails = async (id) => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
const response = await axios.post(`/api/poll/${id}`);
|
|
||||||
setPoll(response.data);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching poll details:', error);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (loading || !poll) return <div>Loading...</div>;
|
|
||||||
|
|
||||||
// Define data for pie charts
|
|
||||||
const memberData = [
|
|
||||||
{ name: 'Yes', value: poll.member_yes },
|
|
||||||
{ name: 'No', value: poll.member_no }
|
|
||||||
];
|
|
||||||
|
|
||||||
const nonMemberData = [
|
|
||||||
{ name: 'Yes', value: poll.non_member_yes },
|
|
||||||
{ name: 'No', value: poll.non_member_no }
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="poll-details">
|
|
||||||
<h1>{poll.question}</h1>
|
|
||||||
<p>Created At: {new Date(poll.created_at).toLocaleString()}</p>
|
|
||||||
|
|
||||||
<div style={{ width: '40%', margin: '20px' }}>
|
|
||||||
<PieChart width={400} height={300}>
|
|
||||||
<Pie
|
|
||||||
data={memberData}
|
|
||||||
cx="50%"
|
|
||||||
cy="50%"
|
|
||||||
labelLine={false}
|
|
||||||
outerRadius={80}
|
|
||||||
fill="#8884d8"
|
|
||||||
dataKey="value"
|
|
||||||
>
|
|
||||||
{memberData.map((entry, index) => (
|
|
||||||
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
|
|
||||||
))}
|
|
||||||
</Pie>
|
|
||||||
<Tooltip />
|
|
||||||
<Legend />
|
|
||||||
</PieChart>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ width: '40%', margin: '20px' }}>
|
|
||||||
<PieChart width={400} height={300}>
|
|
||||||
<Pie
|
|
||||||
data={nonMemberData}
|
|
||||||
cx="50%"
|
|
||||||
cy="50%"
|
|
||||||
labelLine={false}
|
|
||||||
outerRadius={80}
|
|
||||||
fill="#8884d8"
|
|
||||||
dataKey="value"
|
|
||||||
>
|
|
||||||
{nonMemberData.map((entry, index) => (
|
|
||||||
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
|
|
||||||
))}
|
|
||||||
</Pie>
|
|
||||||
<Tooltip />
|
|
||||||
<Legend />
|
|
||||||
</PieChart>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p>Member Yes Votes: {poll.member_yes}</p>
|
|
||||||
<p>Member No Votes: {poll.member_no}</p>
|
|
||||||
<p>Non-Member Yes Votes: {poll.non_member_yes}</p>
|
|
||||||
<p>Non-Member No Votes: {poll.non_member_no}</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import axios from 'axios';
|
|
||||||
import { Link } from 'react-router';
|
|
||||||
|
|
||||||
export default function PollList() {
|
|
||||||
const [polls, setPolls] = useState([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchPolls();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const fetchPolls = async () => {
|
|
||||||
try {
|
|
||||||
const response = await axios.post('/api/admin/view-votes');
|
|
||||||
setPolls(response.data);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching polls:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="poll-list">
|
|
||||||
<h1>Poll List</h1>
|
|
||||||
{polls.map((poll) => (
|
|
||||||
<div key={poll.id} className="poll-item">
|
|
||||||
<Link to={`/poll-details/${poll.id}`}>
|
|
||||||
<h2>{poll.question}</h2>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
const reportWebVitals = (onPerfEntry) => {
|
|
||||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
|
||||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
|
||||||
getCLS(onPerfEntry);
|
|
||||||
getFID(onPerfEntry);
|
|
||||||
getFCP(onPerfEntry);
|
|
||||||
getLCP(onPerfEntry);
|
|
||||||
getTTFB(onPerfEntry);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default reportWebVitals;
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
/* eslint-disable no-restricted-globals */
|
|
||||||
|
|
||||||
// This service worker can be customized!
|
|
||||||
// See https://developers.google.com/web/tools/workbox/modules
|
|
||||||
// for the list of available Workbox modules, or add any other
|
|
||||||
// code you'd like.
|
|
||||||
// You can also remove this file if you'd prefer not to use a
|
|
||||||
// service worker, and the Workbox build step will be skipped.
|
|
||||||
|
|
||||||
import { clientsClaim } from 'workbox-core';
|
|
||||||
import { ExpirationPlugin } from 'workbox-expiration';
|
|
||||||
import { precacheAndRoute, createHandlerBoundToURL } from 'workbox-precaching';
|
|
||||||
import { registerRoute } from 'workbox-routing';
|
|
||||||
import { StaleWhileRevalidate } from 'workbox-strategies';
|
|
||||||
|
|
||||||
clientsClaim();
|
|
||||||
|
|
||||||
// Precache all of the assets generated by your build process.
|
|
||||||
// Their URLs are injected into the manifest variable below.
|
|
||||||
// This variable must be present somewhere in your service worker file,
|
|
||||||
// even if you decide not to use precaching. See https://cra.link/PWA
|
|
||||||
precacheAndRoute(self.__WB_MANIFEST);
|
|
||||||
|
|
||||||
// Set up App Shell-style routing, so that all navigation requests
|
|
||||||
// are fulfilled with your index.html shell. Learn more at
|
|
||||||
// https://developers.google.com/web/fundamentals/architecture/app-shell
|
|
||||||
const fileExtensionRegexp = new RegExp('/[^/?]+\\.[^/]+$');
|
|
||||||
registerRoute(
|
|
||||||
// Return false to exempt requests from being fulfilled by index.html.
|
|
||||||
({ request, url }) => {
|
|
||||||
// If this isn't a navigation, skip.
|
|
||||||
if (request.mode !== 'navigate') {
|
|
||||||
return false;
|
|
||||||
} // If this is a URL that starts with /_, skip.
|
|
||||||
|
|
||||||
if (url.pathname.startsWith('/_')) {
|
|
||||||
return false;
|
|
||||||
} // If this looks like a URL for a resource, because it contains // a file extension, skip.
|
|
||||||
|
|
||||||
if (url.pathname.match(fileExtensionRegexp)) {
|
|
||||||
return false;
|
|
||||||
} // Return true to signal that we want to use the handler.
|
|
||||||
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html')
|
|
||||||
);
|
|
||||||
|
|
||||||
// An example runtime caching route for requests that aren't handled by the
|
|
||||||
// precache, in this case same-origin .png requests like those from in public/
|
|
||||||
registerRoute(
|
|
||||||
// Add in any other file extensions or routing criteria as needed.
|
|
||||||
({ url }) => url.origin === self.location.origin && url.pathname.endsWith('.png'), // Customize this strategy as needed, e.g., by changing to CacheFirst.
|
|
||||||
new StaleWhileRevalidate({
|
|
||||||
cacheName: 'images',
|
|
||||||
plugins: [
|
|
||||||
// Ensure that once this runtime cache reaches a maximum size the
|
|
||||||
// least-recently used images are removed.
|
|
||||||
new ExpirationPlugin({ maxEntries: 50 }),
|
|
||||||
],
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// This allows the web app to trigger skipWaiting via
|
|
||||||
// registration.waiting.postMessage({type: 'SKIP_WAITING'})
|
|
||||||
self.addEventListener('message', (event) => {
|
|
||||||
if (event.data && event.data.type === 'SKIP_WAITING') {
|
|
||||||
self.skipWaiting();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Any other custom service worker logic can go here.
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
// This optional code is used to register a service worker.
|
|
||||||
// register() is not called by default.
|
|
||||||
|
|
||||||
// This lets the app load faster on subsequent visits in production, and gives
|
|
||||||
// it offline capabilities. However, it also means that developers (and users)
|
|
||||||
// will only see deployed updates on subsequent visits to a page, after all the
|
|
||||||
// existing tabs open on the page have been closed, since previously cached
|
|
||||||
// resources are updated in the background.
|
|
||||||
|
|
||||||
// To learn more about the benefits of this model and instructions on how to
|
|
||||||
// opt-in, read https://cra.link/PWA
|
|
||||||
|
|
||||||
const isLocalhost = Boolean(
|
|
||||||
window.location.hostname === 'localhost' ||
|
|
||||||
// [::1] is the IPv6 localhost address.
|
|
||||||
window.location.hostname === '[::1]' ||
|
|
||||||
// 127.0.0.0/8 are considered localhost for IPv4.
|
|
||||||
window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/)
|
|
||||||
);
|
|
||||||
|
|
||||||
export function register(config) {
|
|
||||||
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
|
||||||
// The URL constructor is available in all browsers that support SW.
|
|
||||||
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
|
|
||||||
if (publicUrl.origin !== window.location.origin) {
|
|
||||||
// Our service worker won't work if PUBLIC_URL is on a different origin
|
|
||||||
// from what our page is served on. This might happen if a CDN is used to
|
|
||||||
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('load', () => {
|
|
||||||
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
|
|
||||||
|
|
||||||
if (isLocalhost) {
|
|
||||||
// This is running on localhost. Let's check if a service worker still exists or not.
|
|
||||||
checkValidServiceWorker(swUrl, config);
|
|
||||||
|
|
||||||
// Add some additional logging to localhost, pointing developers to the
|
|
||||||
// service worker/PWA documentation.
|
|
||||||
navigator.serviceWorker.ready.then(() => {
|
|
||||||
console.log(
|
|
||||||
'This web app is being served cache-first by a service ' +
|
|
||||||
'worker. To learn more, visit https://cra.link/PWA'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Is not localhost. Just register service worker
|
|
||||||
registerValidSW(swUrl, config);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function registerValidSW(swUrl, config) {
|
|
||||||
navigator.serviceWorker
|
|
||||||
.register(swUrl)
|
|
||||||
.then((registration) => {
|
|
||||||
registration.onupdatefound = () => {
|
|
||||||
const installingWorker = registration.installing;
|
|
||||||
if (installingWorker == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
installingWorker.onstatechange = () => {
|
|
||||||
if (installingWorker.state === 'installed') {
|
|
||||||
if (navigator.serviceWorker.controller) {
|
|
||||||
// At this point, the updated precached content has been fetched,
|
|
||||||
// but the previous service worker will still serve the older
|
|
||||||
// content until all client tabs are closed.
|
|
||||||
console.log(
|
|
||||||
'New content is available and will be used when all ' +
|
|
||||||
'tabs for this page are closed. See https://cra.link/PWA.'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Execute callback
|
|
||||||
if (config && config.onUpdate) {
|
|
||||||
config.onUpdate(registration);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// At this point, everything has been precached.
|
|
||||||
// It's the perfect time to display a
|
|
||||||
// "Content is cached for offline use." message.
|
|
||||||
console.log('Content is cached for offline use.');
|
|
||||||
|
|
||||||
// Execute callback
|
|
||||||
if (config && config.onSuccess) {
|
|
||||||
config.onSuccess(registration);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('Error during service worker registration:', error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkValidServiceWorker(swUrl, config) {
|
|
||||||
// Check if the service worker can be found. If it can't reload the page.
|
|
||||||
fetch(swUrl, {
|
|
||||||
headers: { 'Service-Worker': 'script' },
|
|
||||||
})
|
|
||||||
.then((response) => {
|
|
||||||
// Ensure service worker exists, and that we really are getting a JS file.
|
|
||||||
const contentType = response.headers.get('content-type');
|
|
||||||
if (
|
|
||||||
response.status === 404 ||
|
|
||||||
(contentType != null && contentType.indexOf('javascript') === -1)
|
|
||||||
) {
|
|
||||||
// No service worker found. Probably a different app. Reload the page.
|
|
||||||
navigator.serviceWorker.ready.then((registration) => {
|
|
||||||
registration.unregister().then(() => {
|
|
||||||
window.location.reload();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Service worker found. Proceed as normal.
|
|
||||||
registerValidSW(swUrl, config);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
console.log('No internet connection found. App is running in offline mode.');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function unregister() {
|
|
||||||
if ('serviceWorker' in navigator) {
|
|
||||||
navigator.serviceWorker.ready
|
|
||||||
.then((registration) => {
|
|
||||||
registration.unregister();
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error(error.message);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
|
||||||
// allows you to do things like:
|
|
||||||
// expect(element).toHaveTextContent(/react/i)
|
|
||||||
// learn more: https://github.com/testing-library/jest-dom
|
|
||||||
import '@testing-library/jest-dom';
|
|
||||||
@@ -1,330 +0,0 @@
|
|||||||
"Joined At","First Name","Last Name","Email","Mobile","Member Type","Check Number","Type","Year","Price Paid","Status","student_1_grade","student_1_full_name","Would you like to be added to a no-obligation volunteer email list?","Student 1 Grade","Student 1 Full Name","Would you like to be added to a no-obligation volunteer list?","Would you like a car magnet as a thank you for your generous donation?","student_2_grade","student_2_full_name","student_4_full_name","student_3_full_name","Student 2 Grade","Student 2 Full Name","student_3_grade","student_1_grade_level","student_2_grade_level","Member_2_-_Email","Member_2_-_Last name","Member_2_-_First name"
|
|
||||||
"02/07/2023 at 4:53 pm","Alexandria","Capecci ","alexandria_capecci@hcpss.org","","Faculty/Staff","","Store Purchase","2022 - 2023","$10.00","","","","","","","","","","","","","",""
|
|
||||||
"01/25/2023 at 3:48 pm","Pamela","Buzzerd","pamelabuzzerd@gmail.com","3014664634","Grandparent","","Store Purchase","2022 - 2023","$10.00","","1","Allison Buzzerd","Yes","","","","","","","","","",""
|
|
||||||
"12/29/2022 at 5:04 pm","Anthony","Kattukaran","anthonykattukaran@yahoo.com","6086283259","Parent/Guardian","","Store Purchase","2022 - 2023","$75.00","","","","","4","George Kattukaran","No","Yes","","","","","",""
|
|
||||||
"12/14/2022 at 9:06 pm","Phaedra","Larner","Plarner@hcpss.org","4109487215","Faculty/Staff","","Store Purchase","2022 - 2023","$12.00","","","","Yes","1","Phaedra Larner","","","","","","","",""
|
|
||||||
"11/21/2022 at 1:17 pm","Jennifer","Lowney","jennifer_lowney@hcpss.org","4104043012","Faculty/Staff","","Store Purchase","2022 - 2023","$10.00","","","","","","","","","","","","","",""
|
|
||||||
"11/16/2022 at 8:05 pm","Cith","Nadarajah","Cithparan@gmail.com","4109257631","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","3","Abira Nadarajah","Yes","","","","","2","Aashna Nadarajah","","","",""
|
|
||||||
"11/16/2022 at 8:05 pm","Sudha","Nadarajah","ss849319@gmail.com","443-841-9319","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","3","Abira Nadarajah","Yes","","","","","2","Aashna Nadarajah","","","",""
|
|
||||||
"11/09/2022 at 8:59 am","Jane","Derro","Jderro@hcpss.org","443 535 3179","Faculty/Staff","","Store Purchase","2022 - 2023","$10.00","","","","","","","","","","","","","",""
|
|
||||||
"11/03/2022 at 4:13 pm","Phaedra","Larner","Plarner@hotmail.com","410-948-7215","Faculty/Staff","","Store Purchase","2022 - 2023","$10.00","","","","","","","","","","","","","",""
|
|
||||||
"10/26/2022 at 8:13 am","Lina","Chauhan-Klein","lina_chauhan-klein1@hcpss.org","2406547886","Faculty/Staff","","Store Purchase","2022 - 2023","$10.00","","","","","","","","","","","","","",""
|
|
||||||
"10/21/2022 at 9:38 pm","Caroline","Chisholm","carolinewchisholm@gmail.com","","Parent/Guardian","","Cash","2022 - 2023","$30.00","","","","Yes","1","Connor Chisholm","","","","","","","",""
|
|
||||||
"10/21/2022 at 9:33 pm","Kathleen","Newberry","kathleen_newberry@hcpss.org","","Faculty/Staff","","Cash","2022 - 2023","$10.00","","","","","","","","","","","","","",""
|
|
||||||
"10/21/2022 at 9:32 pm","Jen","Alexander","jennifer_alexander1@hcpss.org","","Faculty/Staff","","Cash","2022 - 2023","$10.00","","","","","","","","","","","","","",""
|
|
||||||
"10/21/2022 at 11:33 am","Elizabeth","Shoff","E.ULBRECHT@GMAIL.COM","8147777444","Faculty/Staff","","Store Purchase","2022 - 2023","$10.00","","","","","","","","","","","","","",""
|
|
||||||
"10/20/2022 at 9:03 am","Carl","Frantz","carl_frantz@hcpss.org","2405060888","Faculty/Staff","","Store Purchase","2022 - 2023","$10.00","","","","","","","","","","","","","",""
|
|
||||||
"10/20/2022 at 9:01 am","Elizabeth","Carty","Elizabeth_carty@hcpss.org","2402103520","Faculty/Staff","","Store Purchase","2022 - 2023","$10.00","","","","","","","","","","","","","",""
|
|
||||||
"10/19/2022 at 2:58 pm","Rebecca","Whitaker","Rebecca_Whitaker@hcpss.org","","Faculty/Staff","","Store Purchase","2022 - 2023","$10.00","","","","","","","","","","","","","",""
|
|
||||||
"10/18/2022 at 4:09 pm","Nancy","Carter","nancy_carter@hcpss.org","","Faculty/Staff","","Store Purchase","2022 - 2023","$10.00","","","","","","","","","","","","","",""
|
|
||||||
"10/12/2022 at 4:32 pm","Katherine","Etheridge","i8that2@gmail.com","4107502168","Parent/Guardian","","Store Purchase","2022 - 2023","$75.00","","","","","4","Arianna Delaney","Yes","No","","","","","",""
|
|
||||||
"10/12/2022 at 1:13 pm","Jimmy","Kwak","Kwak.james@gmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","1","Henry Kwak","No","","","","No","","","","","",""
|
|
||||||
"10/12/2022 at 1:13 pm","Jennifer","Kwak","jennifer.y.kwak@gmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","1","Henry Kwak","Yes","","","","No","","","","","",""
|
|
||||||
"10/12/2022 at 8:58 am","Scott ","Moore ","Roastedsmoores@gmail.com","3013468777","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","2","Filomena Moore ","No","","","","No","","","","","",""
|
|
||||||
"10/12/2022 at 8:58 am","Lara","Moore","lara.paolini@gmail.com","3013468777","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","2","Filomena Moore ","No","","","","No","5","Desmond Moore ","","","",""
|
|
||||||
"10/08/2022 at 3:21 pm","Peggy","Fredriksson","Margaret_fredriksson@hcpss.org","","Faculty/Staff","","Store Purchase","2022 - 2023","$10.00","","","","","","","","","","","","","",""
|
|
||||||
"10/05/2022 at 12:39 pm","Alexis","Kalivretenos","alexis_kalivretenos@hcpss.org","443-765-0907","Faculty/Staff","","Store Purchase","2022 - 2023","$10.00","","","","","","","","","","","","","",""
|
|
||||||
"10/04/2022 at 9:57 pm","Jean","Austin","jean_austin@hcpss.org","","Faculty/Staff","","Cash","2022 - 2023","$10.00","","","","","","","","","","","","","",""
|
|
||||||
"10/04/2022 at 3:03 pm","Chelsea","Berube","chelsea_berube@hcpss.org","","Faculty/Staff","","Store Purchase","2022 - 2023","$10.00","","","","","","","","","","","","","",""
|
|
||||||
"10/02/2022 at 9:30 pm","Chunling","Zhao","Chunlingzhao@hotmail.com","2406201699","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","1","Nathan Lu","Yes","","","","Yes","5","Andy Lu","","","",""
|
|
||||||
"10/02/2022 at 9:30 pm","Qijin","Lu","qijinlu@yahoo.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","1","Nathan Lu","Yes","","","","Yes","","Andy Lu","Chunking Zhao","","",""
|
|
||||||
"10/01/2022 at 3:34 pm","Derek ","Easter","deaste1@hotmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","3","Mabel Easter","No","","","","No","1","James Easter","","","",""
|
|
||||||
"10/01/2022 at 3:34 pm","Carissa","Easter","carissaeaster@gmail.com","4439043176","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","3","Mabel Easter","Yes","","","","No","1","James Easter","","","",""
|
|
||||||
"09/30/2022 at 8:53 am","Jason","Drenner","drennerjason@gmail.com","3018754226","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","1","Rhyze Drenner","Yes","","","","No","3","Joules Drenner","","","",""
|
|
||||||
"09/30/2022 at 8:53 am","Amber ","Drenner","amber.drenner@uwcm.org","4107037019","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","1","Amber M Drenner","Yes","","","","No","3","Rhyze Anjuli Drenner","Jason Drenner","Joules Epifania Drenner","",""
|
|
||||||
"09/27/2022 at 9:49 am","Andrew","Dain","Andyd1@gmail.com","4102411005","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","3","Eli Dain","Yes","","","","No","","","","","",""
|
|
||||||
"09/27/2022 at 9:49 am","Renee ","Dain ","renee.dain@gmail.com","4102078276","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","3","Eli Dain","Yes","","","","Yes","","","","","",""
|
|
||||||
"09/26/2022 at 10:22 pm","Kim","Cofsky","kimberlycofsky@gmail.com","","Parent/Guardian","","Cash","2022 - 2023","$30.00","","","","Yes","1","Nora Cofsky","","","","","","","",""
|
|
||||||
"09/26/2022 at 10:20 pm","Asra","Khatoon ","asramajid1710@gmail.com","","Parent/Guardian","","Cash","2022 - 2023","$12.00","","","","Yes","2","Yousuf Majid","","","","","","","1","Zoya Majid"
|
|
||||||
"09/23/2022 at 3:57 pm","Robert","Holsopple","","4108492914","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","4","Henry Holsopple","Yes","","","","Yes","","","","","",""
|
|
||||||
"09/23/2022 at 3:57 pm","Carrie","Baran","baran.holsopple@gmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","4","Henry Holsopple","Yes","","","","Yes","","","","","",""
|
|
||||||
"09/22/2022 at 12:32 pm","Shannon","Kennedy","Shannon_kennedy@hcpss.org","4107074913","Faculty/Staff","","Store Purchase","2022 - 2023","$10.00","","","","","","","","","","","","","",""
|
|
||||||
"09/21/2022 at 2:37 pm","Jack","Morton","jmorton96@gmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","3","Anna Morton ","No","","","","","","","","","",""
|
|
||||||
"09/21/2022 at 2:37 pm","Tiffany","Preddy","tgpreddy@yahoo.com","5122936323","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","3","Anna Morton","Yes","","","","","","","","","",""
|
|
||||||
"09/21/2022 at 11:20 am","Elizabeth","Stern","lizandscottwed2013@gmail.com","6174135512","Parent/Guardian","","Store Purchase","2022 - 2023","$12.00","","","","Yes","2","Andrew Stern","","","","","","","2","Gabriel Stern"
|
|
||||||
"09/20/2022 at 8:41 pm","Swetha","Enaganti","swethae@gmail.com","","Parent/Guardian","","Cash","2022 - 2023","$12.00","","","","Yes","5","Adhvaita Chandar","","","","","","","1","Eshwar Chandar"
|
|
||||||
"09/19/2022 at 1:47 pm","Diana","Zhuravel","dina.zhuravel@gmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$12.00","","","","Yes","3","Joshua Zhuravel ","","","","","","","",""
|
|
||||||
"09/18/2022 at 8:22 pm","Ken","Ng","kennethngdo@gmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","4","Nora Ng","No","","","","No","","","","","",""
|
|
||||||
"09/18/2022 at 8:22 pm","Elizabeth","Taylor","elizabeth.taylor16@gmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","4","Nora Ng","Yes","","","","No","","","","","",""
|
|
||||||
"09/17/2022 at 9:07 pm","Michael","Mayfield","mmfield@gmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","4","Nolan Mayfield","No","","","","No","1","Blythe Mayfield","","","",""
|
|
||||||
"09/17/2022 at 9:07 pm","Catherine","Mayfield","cate.mayfield@gmail.com","4153062125","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","4","Nolan Mayfield","No","","","","No","1","Blythe Mayfield","","","",""
|
|
||||||
"09/17/2022 at 4:37 pm","Michelle ","Stein","michelle.l.stein@gmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$12.00","","","","Yes","4","Farrah Stein","","","","","","","",""
|
|
||||||
"09/16/2022 at 4:35 pm","Daniel","Becker","danielvarebecker@yahoo.com","301-455-9656","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","4","Andrew Becker","No","","","","","1","Bryce Becker","","","",""
|
|
||||||
"09/16/2022 at 4:35 pm","Natasha","Becker","nrbecker08@gmail.com","4104048436","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","4","Andrew Becker","Yes","","","","","1","Bryce Becker","","","",""
|
|
||||||
"09/16/2022 at 8:53 am","Tim","Lane","lane.tim2@gmail.com","4104094617","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","3","Patrick Lane","Yes","","","","","1","Jack Lane","","","",""
|
|
||||||
"09/16/2022 at 8:53 am","Michelle","Lane","michellelane713@gmail.com","6095757873","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","3","Patrick Lane","Yes","","","","","1","Jack Lane","","","",""
|
|
||||||
"09/16/2022 at 7:27 am","Mike","DeSimone","Mike.DeSimone@gmail.com","","Parent/Guardian","","Cash","2022 - 2023","$50.00","","5","Nathan DeSimone","Yes","","","","No","","","","","",""
|
|
||||||
"09/16/2022 at 7:27 am","Veronica","DeSimone","VLDeSimone@gmail.com","","Parent/Guardian","487","Check","2022 - 2023","$50.00","","5","Nathan DeSimone","Yes","","","","No","","","","","",""
|
|
||||||
"09/15/2022 at 9:57 pm","Cindy","Mei-Yip","cindy.x.mei@gmail.com","","Parent/Guardian","128","Check","2022 - 2023","$75.00","","","","","2","Sara Mei Yip","No","Yes","","","","","5","Summer Mei Yip"
|
|
||||||
"09/15/2022 at 9:55 pm","Wajeeha","Qayyumi","wajeehas198@gmail.com","","Parent/Guardian","","Cash","2022 - 2023","$15.00","","3","Zakariya Shahid","No","","","","","1","Moeez Shahid","","","",""
|
|
||||||
"09/15/2022 at 9:55 pm","Shahid ","Khursheed","lovebird4u_usa@yahoo.com","","Parent/Guardian","","Cash","2022 - 2023","$15.00","","3","Zakariya Shahid","No","","","","","1","Moeez Shahid","","","",""
|
|
||||||
"09/15/2022 at 7:49 pm","Joseph","Danquah","jbdan72@hotmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","5","Rhema Danquah","Yes","","","","","","","","","",""
|
|
||||||
"09/15/2022 at 7:49 pm","Mame","Danquah","mame700@yahoo.com","4104023206","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","5","Rhema Danquah","Yes","","","","","","","","","",""
|
|
||||||
"09/15/2022 at 6:22 pm","Didier","Monestine","dmonestine@gmail.com","7139270628","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","3","Zoey Monestine","No","","","","","","","","","",""
|
|
||||||
"09/15/2022 at 6:22 pm","Melondy","Monestine","no.1bizneswoman@gmail.com","4438393761","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","3","Zoey Monestine","Yes","","","","","","","","","",""
|
|
||||||
"09/15/2022 at 1:43 pm","Ryan","Hikel","hikelrw@gmail.com","4432770566","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","3","Avery Hikel","No","","","","","","","","","",""
|
|
||||||
"09/15/2022 at 1:43 pm","Stephanie","Hikel","smhikel@gmail.com","4102361226","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","3","Avery Hikel","Yes","","","","","","","","","",""
|
|
||||||
"09/15/2022 at 10:36 am","James","Stepanek","jjstepanek@hotmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","4","Ryan Stepanek","Yes","","","","","","","","","",""
|
|
||||||
"09/15/2022 at 10:36 am","Laura ","Stepanek","lmagurk@hotmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","4","Ryan Stepanek","Yes","","","","","","","","","",""
|
|
||||||
"09/15/2022 at 8:33 am","Katie","Coffey","katherine_coffey@hcpss.org","4103002846","Faculty/Staff","","Store Purchase","2022 - 2023","$10.00","","","","","","","","","","","","","",""
|
|
||||||
"09/15/2022 at 7:36 am","Chasity","Thompson","cnmthomp@icloud.com","","Parent/Guardian","1052","Check","2022 - 2023","$75.00","","","","","4","Noa Thompson","No","Yes","","","","","","Nicholas Thompson"
|
|
||||||
"09/15/2022 at 7:33 am","Bonyoung","Lee","kellygu0121@gmail.com","","Parent/Guardian","","Cash","2022 - 2023","$37.50","","K","Aiden Raon Lee","Yes","","","","Yes","","","","","",""
|
|
||||||
"09/15/2022 at 7:33 am","Scott","Lee","may4am@gmail.com","","Parent/Guardian","1912","Check","2022 - 2023","$37.50","","K","Aiden Raon Lee","Yes","","","","Yes","","","","","",""
|
|
||||||
"09/15/2022 at 7:30 am","Allyson","Jackson","ja_jackson@verizon.net","","Parent/Guardian","2940","Check","2022 - 2023","$30.00","","","","Yes","5","Kyle Jackson","","","","","","","",""
|
|
||||||
"09/15/2022 at 7:26 am","Chris","Bechis","chris.bechis@gmail.com","","Parent/Guardian","","Cash","2022 - 2023","$15.00","","3","June Bechis","Yes","","","","","","","","","",""
|
|
||||||
"09/15/2022 at 7:26 am","Leslie","Bechis","leslie.bechis@gmail.com","","Parent/Guardian","663","Check","2022 - 2023","$15.00","","3","June Bechis","Yes","","","","","","","","","",""
|
|
||||||
"09/15/2022 at 7:23 am","Joseph","Greenseid","jgreenseid@gmail.com","","Parent/Guardian","","Cash","2022 - 2023","$15.00","","2","Lily Greenseid","Yes","","","","","","","","","",""
|
|
||||||
"09/15/2022 at 7:23 am","Jessica","Greenseid","jessica.greenseid@gmail.com","","Parent/Guardian","564","Check","2022 - 2023","$15.00","","2","Lily Greenseid","Yes","","","","","","","","","",""
|
|
||||||
"09/15/2022 at 7:19 am","Adrian","Loftus","acewx333@hotmail.com","","Parent/Guardian","","Cash","2022 - 2023","$15.00","","4","Diego Loftus","No","","","","","","","","","",""
|
|
||||||
"09/15/2022 at 7:19 am","Andrea","Loftus","andruloftus@hotmail.com","","Parent/Guardian","","Cash","2022 - 2023","$15.00","","4","Diego Loftus","Yes","","","","","","","","","",""
|
|
||||||
"09/15/2022 at 7:09 am","Tara","Persaud","tara.persaud31@gmail.com","4434547247","Parent/Guardian","","Store Purchase","2022 - 2023","$12.00","","","","Yes","3","Avery Persaud","","","","","","","",""
|
|
||||||
"09/14/2022 at 9:47 pm","Jon","Zawodny","jonsawodny@gmail.com","","Parent/Guardian","","Cash","2022 - 2023","$37.50","","2","Olivia Zawodny","Yes","","","","Yes","","","","","",""
|
|
||||||
"09/14/2022 at 9:47 pm","Vanessa","Zawodny","vanessa.zawodny@gmail.com","","Parent/Guardian","230","Check","2022 - 2023","$37.50","","2","Olivia Zawodny","Yes","","","","Yes","","","","","",""
|
|
||||||
"09/14/2022 at 7:09 pm","Jeff","Guy","Jeffguy@gmail.com","3018441239","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","4","Kara Guy","Yes","","","","","2","Nadia Guy","","","",""
|
|
||||||
"09/14/2022 at 7:09 pm","Rebecca ","Guy","rebstill@gmail.com","8048394625","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","4","Kara Guy","Yes","","","","","2","Nadia Guy","","","",""
|
|
||||||
"09/14/2022 at 6:53 pm","Mitchell ","Gutshall","Mguts001@verizon.net","2402775384","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","4","Davey Gutshall","No","","","","No","1","Ellie Gutshall","","","",""
|
|
||||||
"09/14/2022 at 6:53 pm","Marianne ","Gutshall","mgutshall@gmail.com","4434733740","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","4","Davey Gutshall","Yes","","","","Yes","1","Ellie Gutshall","","","",""
|
|
||||||
"09/14/2022 at 4:26 pm","Talia","Skyles","talia_skyles@hcpss.org","3019221380","Faculty/Staff","","Store Purchase","2022 - 2023","$10.00","","","","","","","","","","","","","",""
|
|
||||||
"09/14/2022 at 11:50 am","Jennifer ","Tinnirella","Jagoodemote@gmail.com","4439040473","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","1","Shaylee Tinnirella","No","","","","","","","","","",""
|
|
||||||
"09/14/2022 at 11:50 am","Anthony ","Tinnirella","ajtinnirella@gmail.com","4102411388","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","1","Shaylee Tinnirella","No","","","","","","","","","",""
|
|
||||||
"09/13/2022 at 6:48 pm","Jason","Mabee","Jason.mabee1@gmail.com","443-255-4135","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","1","Grant Mabee","No","","","","","","","","","",""
|
|
||||||
"09/13/2022 at 6:48 pm","Adrienne","Mabee ","adrienne.mabee@gmail.com","4434528612","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","1","Grant Mabee","Yes","","","","","5","Bryn Mabee","","","",""
|
|
||||||
"09/13/2022 at 6:45 pm","Nicole","Gable","cmgable37@gmail.com","4436764204","Parent/Guardian","","Store Purchase","2022 - 2023","$75.00","","","","","2","Linzey Gable","No","No","","","","","",""
|
|
||||||
"09/13/2022 at 6:02 pm","Jennifer","Fitzpatrick","jenfitz1283@gmail.com","4438785344","Parent/Guardian","","Store Purchase","2022 - 2023","$12.00","","","","No","1","Devyn Fitzpatrick ","","","","","","","3","Brooks Fitzpatrick "
|
|
||||||
"09/13/2022 at 6:01 pm","Abhishek","Rege","abhishekrege@gmail.com","4104998349","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","1","Oorja Rege","Yes","","","","","","","","","",""
|
|
||||||
"09/13/2022 at 6:01 pm","Avanti","Shetye","avshetye@gmail.com","2405158469","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","1","Oorja Rege","Yes","","","","","","","","","",""
|
|
||||||
"09/13/2022 at 5:36 pm","Daniel","Ji","Danielaramji@gmail.com","4438126426","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","K","Eli Ji","Yes","","","","No","","","","","",""
|
|
||||||
"09/13/2022 at 5:36 pm","Deborah","Yi","Deborahmyi@gmail.com","9178818945","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","K","Eli Ji","Yes","","","","Yes","","","","","",""
|
|
||||||
"09/13/2022 at 12:53 pm","Laura","Rose","laura.eva.rose@gmail.com","3307048863","Parent/Guardian","","Store Purchase","2022 - 2023","$75.00","","","","","K","Maddox Howe","Yes","Yes","","","","","",""
|
|
||||||
"09/12/2022 at 10:01 pm","Jeanne","DeBoy","jdeboy@hcpss.org","","Faculty/Staff","","Store Purchase","2022 - 2023","$10.00","","","","","","","","","","","","","",""
|
|
||||||
"09/12/2022 at 8:24 pm","Amy","Bailey","amy_bailey@hcpss.org","","Faculty/Staff","","Store Purchase","2022 - 2023","$10.00","","","","","","","","","","","","","",""
|
|
||||||
"09/12/2022 at 8:15 pm","Andrea","Pendergast","andrea_pendergast@hcpss.org","5514863820","Faculty/Staff","","Store Purchase","2022 - 2023","$10.00","","","","","","","","","","","","","",""
|
|
||||||
"09/12/2022 at 8:15 pm","Maddie","McErlean","madeline_mcerlean@hcpss.org","4103369883","Faculty/Staff","","Store Purchase","2022 - 2023","$10.00","","","","","","","","","","","","","","",""
|
|
||||||
"09/12/2022 at 6:11 pm","Bill ","Rodney","Brodney@optonline.net","","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","3","Samantha Rodney ","No","","","","","","","","","","",""
|
|
||||||
"09/12/2022 at 6:11 pm","Donna","Rodney","gelchiondm@gmail.com","6318480162","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","3","Samantha Rodney ","Yes","","","","","","","","","","",""
|
|
||||||
"09/12/2022 at 4:07 pm","Esther","Jean-Louis","esther_jean-louis@hcpss.org","4438327187","Faculty/Staff","","Store Purchase","2022 - 2023","$10.00","","","","","","","","","","","","","","",""
|
|
||||||
"09/12/2022 at 4:00 pm","Kyle","Robson","Kar0880@gmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","1","Paige Robson","Yes","","","","","","","","","","",""
|
|
||||||
"09/12/2022 at 4:00 pm","Kristina","Robson","kristinanrobson@gmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","1","Paige Robson","Yes","","","","","","","","","","",""
|
|
||||||
"09/12/2022 at 1:19 pm","Robert ","Schmidt ","Rob.a.schmidt@gmail.com","301-875-2785","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","2","Ella Schmidt ","Yes","","","","No","","","","","","",""
|
|
||||||
"09/12/2022 at 1:19 pm","Ashley","Schmidt","ancook85@gmail.com","4105300024","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","2","Ella Schmidt ","Yes","","","","No","","","","","","",""
|
|
||||||
"09/12/2022 at 1:04 pm","Vinoth kumar ","Mohan kumar","Vinoth.mohank@gmail.com","8607134762","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","2","Aditi Vinod","Yes","","","","","","","","","","",""
|
|
||||||
"09/12/2022 at 1:04 pm","Parvadha suganya","Manimude","parvadha.suganya@gmail.com","8604715173","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","2","Aditi Vinod","Yes","","","","","","","","","","",""
|
|
||||||
"09/12/2022 at 11:52 am","Kimberly ","Young","yyoung@bcps.org","","Grandparent","","Store Purchase","2022 - 2023","$15.00","","4","Sky Burris","Yes","","","","","2","Sage Burris","","","","",""
|
|
||||||
"09/12/2022 at 11:52 am","Yardley","Young","Yardleyyoung@ymail.com","4435400049","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","4","Sky Burris","Yes","","","","","2","Sage Burris","","","","",""
|
|
||||||
"09/12/2022 at 10:16 am","Joe","Murray","joseph.c.murray@gmail.com","510-484-1153","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","1","Ethan Murray","No","","","","No","","","","","","",""
|
|
||||||
"09/12/2022 at 10:16 am","Carrie","Murray","carrie.chao.murray@gmail.com","415-846-9941","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","1","Ethan Murray","Yes","","","","Yes","","","","","","",""
|
|
||||||
"09/11/2022 at 4:02 pm","Santosh ","Venkatesha","santosh123@gmail.com","443-621-4424","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","4","Akash Venkatesha","Yes","","","","No","","","","","","",""
|
|
||||||
"09/11/2022 at 4:02 pm","Mona","Gahunia","mkg112@gmail.com","4435406214","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","4","Akash Venkatesha","Yes","","","","Yes","","","","","","",""
|
|
||||||
"09/10/2022 at 8:45 pm","Brian","Johnson","bjohnson31@gmail.com","4109359136","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","3","Grant Johnson","Yes","","","","","","","","","","",""
|
|
||||||
"09/10/2022 at 8:45 pm","Elizabeth ","Johnson","eajohnson621@gmail.com","2404223408","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","3","Grant Johnson","Yes","","","","","","","","","","",""
|
|
||||||
"09/10/2022 at 7:27 pm","Alexander","Livieratos","Aleclivi@gmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","4","Paxton livieratos","No","","","","Yes","2","Thea livieratos","","","","",""
|
|
||||||
"09/10/2022 at 7:27 pm","Kelly","Livieratos","kellykm@gmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","4","Paxton Livieratos","Yes","","","","Yes","2","Thea Livieratos","","","","",""
|
|
||||||
"09/10/2022 at 4:42 pm","Laurie ","Daman","ljdaman@gmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","4","Elena Daman","Yes","","","","","","","","","","",""
|
|
||||||
"09/10/2022 at 4:42 pm","Kurt","Daman","kdaman77@gmail.com","4436990849","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","4","Elena Daman","Yes","","","","","","","","","","",""
|
|
||||||
"09/10/2022 at 2:19 pm","Thomas","Ruo","Rrmedoff@yahoo.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","4","Robert Ruo","No","","","","No","2","Jason Ruo","","","","",""
|
|
||||||
"09/10/2022 at 2:19 pm","Rachel","Ruo","rrmedoff@gmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","4","Robert Ruo","No","","","","No","2","Jason Ruo","","","","",""
|
|
||||||
"09/10/2022 at 1:49 pm","Beverly","Weber","bevy.lynn@gmail.com","","Parent/Guardian","","Cash","2022 - 2023","$30.00","","","","Yes","3","Isaac Weber","","","","","","","5","Anderson Weber",""
|
|
||||||
"09/10/2022 at 1:47 pm","Akdas","Mumtaz","akdasmumtaz@gmail.com","","Parent/Guardian","","Cash","2022 - 2023","$30.00","","","","Yes","K","Hiba Mumtaz","","","","","","","","",""
|
|
||||||
"09/10/2022 at 1:29 pm","Tabassum","Sarfraz","Muminah2011@gmail.com","","Parent/Guardian","Square (BTSP)","Check","2022 - 2023","$30.00","","","","Yes","4","Nuraniya Tahir","","","","","","","K","Tasbiha Tahir",""
|
|
||||||
"09/10/2022 at 1:26 pm","Joohee","Chae","chaejoo84@gmail.com","","Parent/Guardian","Square (BTSP)","Check","2022 - 2023","$12.00","","","","No","4","Subeen Oh","","","","","","","3","Seoyeon Oh",""
|
|
||||||
"09/10/2022 at 1:23 pm","Jaye","Van Acht","jayevanacht@gmail.com","","Parent/Guardian","Square (BTSP)","Check","2022 - 2023","$75.00","","","","","5","Grey Van Acht","No","Yes","","","","","","Saul Van Acht",""
|
|
||||||
"09/09/2022 at 6:02 pm","Erin","Coleman","2030erin@gmail.com","4103702508","Faculty/Staff","","Store Purchase","2022 - 2023","$10.00","","","","","","","","","","","","","","",""
|
|
||||||
"09/09/2022 at 4:16 pm","Jeff","Plank","jplank3020@gmail.com","4102274952","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","5","Whitney Plank","No","","","","Yes","3","Everett Plank","","","","",""
|
|
||||||
"09/09/2022 at 4:16 pm","Corinne","Plank","cplank2225@gmail.com","4106529305","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","5","Whitney Plank","Yes","","","","Yes","3","Everett Plank","","","","",""
|
|
||||||
"09/09/2022 at 4:16 pm","Corinne","Plank","cplank1225@gmail.com","4106529305","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","5","Whitney Plank","Yes","","","","Yes","3","Everett Plank","","","","",""
|
|
||||||
"09/09/2022 at 4:13 pm","Brad","Piergrossi","Bradpiergrossi@yahoo.com","4109387396","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","4","Fiona Raftery Piergrossi","Yes","","","","No","4","Aleena Raftery Piergrossi","","","","",""
|
|
||||||
"09/09/2022 at 4:13 pm","Martina","Raftery","tinaraftery@yahoo.com","4438441908","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","4","Fiona Raftery Piergrossi","Yes","","","","No","4","Aleena Raftery Piergrossi","Brad Piergrossi","","","",""
|
|
||||||
"09/09/2022 at 2:59 pm","Patricia","Lough Buzzerd","Pat.A.Lough@GMAIL.COM","3014520150","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","1","Allison Buzzerd","No","","","","","","","","","","",""
|
|
||||||
"09/09/2022 at 2:59 pm","Christian","Buzzerd","LOUGHBUZZERD@GMAIL.COM","4432543400","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","1","Allison Buzzerd ","Yes","","","","","","","","","","",""
|
|
||||||
"09/09/2022 at 8:19 am","kevin","knott","kevin.knott2@gmail.com","","Parent/Guardian","","Cash","2022 - 2023","$5.00","","","","No","4","bryan knott","","","","","","","2","chloe knott",""
|
|
||||||
"09/09/2022 at 7:38 am","Christina","Harold","christina_harold@hcpss.org","","Faculty/Staff","","Store Purchase","2022 - 2023","$10.00","","","","","","","","","","","","","","",""
|
|
||||||
"09/08/2022 at 10:35 pm","Stephen","Senerchia","steve@senerchia.com","","Parent/Guardian","","Cash","2022 - 2023","$37.50","","4","Sofee Senerchia","No","","","","Yes","1","Mia Senerchia","","","","",""
|
|
||||||
"09/08/2022 at 10:35 pm","Sara","Senerchia","sara@senerchia.com","","Parent/Guardian","","Cash","2022 - 2023","$37.50","","4","Sofee Senerchia","Yes","","","","Yes","1","Mia Senerchia","","","","",""
|
|
||||||
"09/08/2022 at 4:06 pm","Xin","Li","Lixintt.chn@gmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","1","Hailey Tao","Yes","","","","Yes","4","Bryan Tao","","","","",""
|
|
||||||
"09/08/2022 at 4:06 pm","Tao","Tao ","Taotao.chn@gmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","1","Hailey Tao","Yes","","","","Yes","4","Bryan Tao","","","","",""
|
|
||||||
"09/08/2022 at 2:17 pm","Eun-Chung","Cintron","eun-chung_cintron@hcpss.org","","Faculty/Staff","","Store Purchase","2022 - 2023","$10.00","","","","","","","","","","","","","","",""
|
|
||||||
"09/08/2022 at 10:31 am","Josh","Ferguson","joshuatferguson@gmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","5","Hailey Ferguson","No","","","","","3","Evan Ferguson","","Alex Ferguson","","","K"
|
|
||||||
"09/08/2022 at 10:31 am","Linda","Ferguso","linda.f.ferguson@gmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","5","Hailey Ferguson","Yes","","","","","3","Evan Ferguson","","Alex Ferguson","","","K"
|
|
||||||
"09/08/2022 at 8:18 am","Kevin ","Sharpe","Ksharpe45@gmail.com","4436236643","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","4","Kylie Sharpe","No","","","","","","","","","","",""
|
|
||||||
"09/08/2022 at 8:18 am","Colleen","Cavanaugh ","Csquared27@gmail.com","3152547584","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","4","Kylie Sharpe","Yes","","","","","","","","","","",""
|
|
||||||
"09/08/2022 at 6:55 am","Chidimma","Agbakwuru","chidimmaa@gmail.com","","Parent/Guardian","","Cash","2022 - 2023","$37.50","","K","Elaine Agbakwuru","No","","","","Yes","","","","","","",""
|
|
||||||
"09/08/2022 at 6:55 am","Chinedu","Agbakwuru","edwuru@gmail.com","","Parent/Guardian","","Cash","2022 - 2023","$37.50","","K","Elaine Agbakwuru","No","","","","Yes","","","","","","",""
|
|
||||||
"09/07/2022 at 10:47 pm","Lily","Park","insuzzang81@hotmail.com","4437149711","Parent/Guardian","","Store Purchase","2022 - 2023","$12.00","","","","Yes","4","Nathaniel Park","","","","","","","","",""
|
|
||||||
"09/07/2022 at 10:07 pm","Calvin","Ball","cball@howardcountymd.gov","","Community","","Cash","2022 - 2023","$12.00","","","","No","K","N/A","","","","","","","","",""
|
|
||||||
"09/07/2022 at 9:49 pm","Maryam","Diaz","maryamgdiaz@gmail.com","","Parent/Guardian","","Cash","2022 - 2023","$37.50","","K","Samantha Diaz","Yes","","","","Yes","","","","","","",""
|
|
||||||
"09/07/2022 at 9:49 pm","Alvin","Diaz","alvin1782@yahoo.com","","Parent/Guardian","","Cash","2022 - 2023","$37.50","","K","Samantha Diaz","Yes","","","","Yes","","","","","","",""
|
|
||||||
"09/07/2022 at 9:32 pm","Craig","Newcomb","cknewcomb@gmail.com","443-896-4412","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","4","Annelise Newcomb","No","","","","No","2","Lorelei Newcomb","","","","",""
|
|
||||||
"09/07/2022 at 9:32 pm","Jennifer","Newcomb","jflanigan@gmail.com","717-495-4596","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","4","Annelise Newcomb","Yes","","","","Yes","2","Lorelei Newcomb","","","","",""
|
|
||||||
"09/07/2022 at 7:41 pm","Frances","Felder","franwfelder@gmail.com","4103400988","Parent/Guardian","","Store Purchase","2022 - 2023","$75.00","","","","","5","Delayni Felder","Yes","Yes","","","","","","",""
|
|
||||||
"09/07/2022 at 7:11 pm","Nina","Philipsen","nm.philipsen@gmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$12.00","","","","Yes","5","Nora Hetzner","","","","","","","","",""
|
|
||||||
"09/07/2022 at 6:47 pm","David","Goggin","Dgoggin77@gmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","1","Emma Goggin","No","","","","","","","","","","",""
|
|
||||||
"09/07/2022 at 6:47 pm","Sara","Goggin","sgoggin22@gmail.com","6177557387","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","1","Emma Goggin","Yes","","","","","","","","","","",""
|
|
||||||
"09/07/2022 at 6:47 pm","Brian ","Day","Briday50@gmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","1","Ellie Day","No","","","","No","","","","","","",""
|
|
||||||
"09/07/2022 at 6:47 pm","Sarah ","Day","sarahday52@gmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","1","Ellie day","Yes","","","","No","","","","","","",""
|
|
||||||
"09/07/2022 at 6:41 pm","Dan","Notari","dnotari44@gmail.com","4103132813","Faculty/Staff","","Store Purchase","2022 - 2023","$10.00","","","","","","","","","","","","","","",""
|
|
||||||
"09/07/2022 at 6:04 pm","Michael","Brewer","michaeldbrewer1974@gmail.com","4107030386","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","4","Evelyn Brewer","No","","","","","","","","","","",""
|
|
||||||
"09/07/2022 at 6:04 pm","Katherine","Brewer","katie.cardoni.brewer@gmail.com","7037856121","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","4","Evelyn Brewer","Yes","","","","","","","","","","",""
|
|
||||||
"09/07/2022 at 5:54 pm","Seth","Groman","seth.groman@gmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","3","Max Groman","No","","","","No","","","","","","",""
|
|
||||||
"09/07/2022 at 5:54 pm","Jenna","Groman","Jenna.groman@gmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","3","Max Groman","Yes","","","","Yes","","","","","","",""
|
|
||||||
"09/07/2022 at 5:05 pm","Bob","Bergin","bobbybergin@hotmail.com","","Parent/Guardian","","Cash","2022 - 2023","$37.50","","4","Ruby Bergin","Yes","","","","Yes","3","Grant Bergin","","","","",""
|
|
||||||
"09/07/2022 at 5:05 pm","Mandy","Bergin","mandybergin@hotmail.com","","Parent/Guardian","230","Check","2022 - 2023","$37.50","","3","Grant Bergin","Yes","","","","Yes","4","Ruby Bergin","","","","",""
|
|
||||||
"09/07/2022 at 4:58 pm","Kevin","Cooke","kcooke2442@gmail.com","","Parent/Guardian","","Cash","2022 - 2023","$37.50","","K","John Cooke","Yes","","","","Yes","","","","","","",""
|
|
||||||
"09/07/2022 at 4:58 pm","Sarah ","Cooke","sarahcooke2015@gmail.com","","Parent/Guardian","255","Check","2022 - 2023","$37.50","","K","John Cooke","Yes","","","","Yes","","","","","","",""
|
|
||||||
"09/07/2022 at 4:42 pm","Kyung","Oh","klee6256@gmail.com","4436904290","Parent/Guardian","","Cash","2022 - 2023","$15.00","","1","Jeremy Oh","No","","","","","","","","","","",""
|
|
||||||
"09/07/2022 at 4:42 pm","Thomas","Oh","yoojaeha@hotmail.com","","Parent/Guardian","308","Check","2022 - 2023","$15.00","","1","Jeremy Oh","No","","","","","","","","","","",""
|
|
||||||
"09/07/2022 at 1:45 pm","Lucas","Grace","Lukeandlinds@comcast.net","4437456636","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","1","Kylie Grace","Yes","","","","Yes","K","Mackenzie Grace","","","","",""
|
|
||||||
"09/07/2022 at 1:45 pm","Lindsay","Grace","lindsaypickett24@gmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","1","Kylie Grace","Yes","","","","Yes","K","Mackenzie Grace","","","","",""
|
|
||||||
"09/07/2022 at 11:28 am","ANISH","SHAIKH","anish786@gmail.com","4109055706","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","4","MohammadAamir Shaikh","Yes","","","","","","","","","","",""
|
|
||||||
"09/07/2022 at 11:28 am","HINA ","SHAIKH","hina7860@gmail.com","4439850708","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","4","MohammadAamir Shaikh","Yes","","","","","","","ANISH SHAIKH","","","",""
|
|
||||||
"09/07/2022 at 9:28 am","Melissa","Orgera","melissa_orgera@hcpss.org","","Faculty/Staff","","Store Purchase","2022 - 2023","$10.00","","","","No","","","","","","","","","","",""
|
|
||||||
"09/06/2022 at 9:14 pm","Michele","Barron","mlbarron10@gmail.com","4109250146","Faculty/Staff","","Store Purchase","2022 - 2023","$10.00","","","","","","","","","","","","","","",""
|
|
||||||
"09/06/2022 at 8:14 pm","Tim","Kelley","Tkelley9175@gmail.com","410-925-2283","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","4","Jack Kelley","No","","","","","","","","","","",""
|
|
||||||
"09/06/2022 at 8:14 pm","Carrie","Kelley","Ckelley917@yahoo.com","410-206-9490","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","4","Jack Kelley","No","","","","","","","","","","",""
|
|
||||||
"09/06/2022 at 5:36 pm","Justin","Ford","22jayferd@gmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","2","Kathryn Ford","No","","","","","K","Owen Ford","","","","",""
|
|
||||||
"09/06/2022 at 5:36 pm","Jennifer","Ford","jmpiechocki@gmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","2","Kathryn Ford","Yes","","","","","K","Owen Ford","","","","",""
|
|
||||||
"09/06/2022 at 4:18 pm","Jimmy","Brown ","tarheel931@gmail.com","706-267-8547","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","3","Sullivan Brown ","No","","","","","","","","","","",""
|
|
||||||
"09/06/2022 at 4:18 pm","Mary","Brown ","marbear610@yahoo.com","706-267-9457","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","3","Sullivan Brown ","Yes","","","","","","","","","","",""
|
|
||||||
"09/06/2022 at 3:46 pm","Sarah","Fauver ","sarah_fauver@hcpss.org","4438124299","Faculty/Staff","","Store Purchase","2022 - 2023","$10.00","","","","","","","","","","","","","","",""
|
|
||||||
"09/06/2022 at 1:59 pm","Amy","Leisner","Amy_Leisner@hcpss.org","443-603-4343","Faculty/Staff","","Store Purchase","2022 - 2023","$10.00","","","","","","","","","","","","","","",""
|
|
||||||
"09/06/2022 at 1:57 pm","Rob","Davis","robraydavis@gmail.com","2672535568","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","3","Drew Davis","No","","","","","","","","","","",""
|
|
||||||
"09/06/2022 at 1:57 pm","Amala","Davis","amaladavis75@gmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","3","Drew Davis","Yes","","","","","","Drew Davis","","","","",""
|
|
||||||
"09/06/2022 at 1:40 pm","Max ","Fiallos","maxfiallos@gmail.com","4434694191","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","4","Sebastian Fiallos","No","","","","No","3","Natalia Fiallos","","","","",""
|
|
||||||
"09/06/2022 at 1:40 pm","Telma","Batres","telmabatres@gmail.com","4434227784","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","4","Sebastian Fiallos","Yes","","","","Yes","3","Natalia Fiallos","","","","",""
|
|
||||||
"09/06/2022 at 12:25 pm","Drew","Petrella","vapetrella@gmail.com","2405053266","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","3","Vance Petrella","No","","","","Yes","","","","","","",""
|
|
||||||
"09/06/2022 at 12:25 pm","Liz","Petrella","petrella.liz@gmail.com","3015128186","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","3","Vance Petrella","Yes","","","","Yes","","","","","","",""
|
|
||||||
"09/06/2022 at 12:17 pm","Jessica Roy-Harrison","Roy-Harrison","jroyharrison@gmail.com","4348254611","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","3","Maximus Bridges","Yes","","","","","3","Alora Bridges","","","","",""
|
|
||||||
"09/06/2022 at 12:17 pm","David","Bridges","dobridges@me.com","4349890736","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","3","David Bridges","Yes","","","","","3","Maximus Bridges","","Alora Bridges","","",""
|
|
||||||
"09/05/2022 at 9:44 pm","Shelly","Post","shellylpost@gmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$12.00","","","","Yes","4","Naomi Post","","","","","","","2","Aila Post",""
|
|
||||||
"09/05/2022 at 8:17 pm","Kee","Jang","kwjang28@gmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","3","Nathan Jang","No","","","","","","","","","","",""
|
|
||||||
"09/05/2022 at 8:17 pm","Kee","Jang","anajang65@gmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","3","Nathan Jang","Yes","","","","","","","","","","",""
|
|
||||||
"09/05/2022 at 7:38 am","Preston","Schoenly","lisacass@gmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","3","Nathan Schoenly","No","","","","No","","Mason Schoenly","","","","",""
|
|
||||||
"09/05/2022 at 7:38 am","Lisa","Schoenly","lisaschoenly@gmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","3","Nathan Schoenly","Yes","","","","Yes","1","Mason Schoenly","","","","",""
|
|
||||||
"09/04/2022 at 8:10 pm","Kevin","Drummond","","4437948245","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","5","Dylan Drummond","No","","","","No","","","","","","",""
|
|
||||||
"09/04/2022 at 8:10 pm","Kasey","Drummond","kaseydrummond@yahoo.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","5","Dylan Drummond","No","","","","No","","","","","","","","",""
|
|
||||||
"09/04/2022 at 12:56 pm","Erica","Voss","ericavoss@yahoo.com","410 844 2496","Parent/Guardian","","Store Purchase","2022 - 2023","$12.00","","","Elizabeth Cherry ","Yes","","","","","","","","","","","","K",""
|
|
||||||
"09/04/2022 at 12:03 pm","Scott","Hays ","Sahays12@gmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","4","Hailey ","No","","","","","","","","","","","","",""
|
|
||||||
"09/04/2022 at 12:03 pm","Miriam","Hays","mmhays9@gmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","4","Hailey Hays ","No","","","","","2","Payton Hays","","","","","","",""
|
|
||||||
"09/04/2022 at 11:36 am","Chris","Rosas","Christopher_rosas@hcpss.org","","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","5","Nicki Rosas","No","","","","","","","","","","","","",""
|
|
||||||
"09/04/2022 at 11:36 am","Beth","Rosas","Beth_Rosas@hcpss.org","3017689171","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","5","Nicki Rosas","Yes","","","","","","","","","","","","",""
|
|
||||||
"09/03/2022 at 9:46 pm","Christa ","Marsico","christa_marsico@hcpss.org","4432859415","Faculty/Staff","","Store Purchase","2022 - 2023","$10.00","","","","","","","","","","","","","","","","",""
|
|
||||||
"09/03/2022 at 9:20 pm","Kevin ","Rodkey","Kevin.rodkey@gmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","1","Grace Rodkey ","No","","","","","","","","","","","","",""
|
|
||||||
"09/03/2022 at 9:20 pm","Erika","Rodkey","erika.rodkey@gmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","1","Grace Rodkey ","Yes","","","","","","","","","","","","",""
|
|
||||||
"09/03/2022 at 12:17 pm","Bhavik ","Hukmani","b2hukmani@yahoo.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","4","Damian Hukmani","No","","","","","","","","","","","","",""
|
|
||||||
"09/03/2022 at 12:17 pm","Kelly","Hukmani","kehukmani@yahoo.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","4","Damian Hukmani","No","","","","","","","","","","","","",""
|
|
||||||
"09/03/2022 at 12:10 pm","Gary","Smith","gsmith072608@gmail.com","4104598056","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","2","Oscar Smith","No","","","","Yes","K","Laura Smith","","","","","","",""
|
|
||||||
"09/03/2022 at 12:10 pm","Sadie","Smith","smatarazzosmith@gmail.com","4109168430","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","2","Oscar Smith","Yes","","","","Yes","K","Laura Smith","","","","","","",""
|
|
||||||
"09/03/2022 at 11:38 am","Michael ","Moskowitz ","mjmoskowitz@gmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","4","Anya Moskowitz ","No","","","","","","","","","","","","",""
|
|
||||||
"09/03/2022 at 11:38 am","Sayli","Moskowitz","sayliw@gmail.com","2024863103","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","4","Anya Moskowitz","Yes","","","","","","","","","","","","",""
|
|
||||||
"09/03/2022 at 9:19 am","Jennifer","Olchowski","jenniferparandian@yahoo.com","4435403138","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","3","Charlotte Olchowski","Yes","","","","","","","","","","","","",""
|
|
||||||
"09/03/2022 at 9:19 am","Adam","Olchowski ","olchowskia@yahoo.com","4103823364","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","3","Charlotte Olchowski","No","","","","","","","Jennifer Olchowski","","","","","",""
|
|
||||||
"09/03/2022 at 9:12 am","Roxi","Da Silva","Roxitoo@gmail.com","443-799-9551","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","K","Giovanni Da Silva","Yes","","","","","","","","","","","","",""
|
|
||||||
"09/03/2022 at 9:12 am","Diego","Da Silva","Sumoamor@gmail.com","410-204-2880","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","K","Giovanni Da Silva","Yes","","","","","","","","","","","","",""
|
|
||||||
"09/03/2022 at 8:56 am","Michael ","McCormick","mmccor12@gmail.com","4109677949","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","1","Liana McCormick ","No","","","","Yes","","","","","","","","",""
|
|
||||||
"09/03/2022 at 8:56 am","Heather","McCormick","hurdheather87@gmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","1","Liana McCormick","Yes","","","","Yes","","","Mike McCormick","","","","","",""
|
|
||||||
"09/02/2022 at 10:07 pm","Richard ","Cresswell","rcresswell@gmail.com","443-621-3307","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","5","Logan Cresswell","No","","","","No","3","Kalina Cresswell","","","","","","",""
|
|
||||||
"09/02/2022 at 10:07 pm","Kasandra","Cresswell","kasandracresswell@gmail.com","301-502-7037","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","5","Logan Cresswell","No","","","","Yes","3","Kalina Cresswell","","","","","","",""
|
|
||||||
"09/02/2022 at 4:20 pm","Glenn","Wolfe","zlonewolfe@gmail.com","2069204947","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","3","Ethan Wolfe","No","","","","No","K","Jackson Wolfe","","","","","","",""
|
|
||||||
"09/02/2022 at 4:20 pm","Jamie","Wolfe","rainyjamie8@gmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","3","Ethan Wolfe","Yes","","","","Yes","K","Jackson Wolfe","Jamie Wolfe","","","","","",""
|
|
||||||
"09/02/2022 at 3:14 pm","Maryiam","Cutlerywala","busybeeqk786@yahoo.com","443-670-6555","Faculty/Staff","","Cash","2022 - 2023","$10.00","","","","","","","","","","","","","","","","",""
|
|
||||||
"09/02/2022 at 10:50 am","Alan","Kramer","ackramer@hotmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","5","Calista Kramer","No","","","","","","","","","","","","",""
|
|
||||||
"09/02/2022 at 10:50 am","Angie","Kramer","angela.l.kramer@gmail.com","4108186622","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","5","Calista Kramer","Yes","","","","","","","","","","","","",""
|
|
||||||
"09/02/2022 at 7:43 am","Mark","Den Herder","Mdherder000@gmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","5","Emmma Den Herder","No","","","","","","","","","","","","",""
|
|
||||||
"09/02/2022 at 7:43 am","Christine","Den Herder","Cdenherder@gmail.com","613-967-3159","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","5","Emma Den Herder ","Yes","","","","","","","","","","","","",""
|
|
||||||
"09/01/2022 at 9:28 pm","Adam ","Fisher","adam.fisher.personal@gmail.com","4436778847","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","5","Ruby Fisher","No","","","","No","3","Gary Fisher","","","","","","",""
|
|
||||||
"09/01/2022 at 9:28 pm","Jane","Fisher","jane.gribble@gmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","5","Ruby Fisher","No","","","","No","3","Gary Fisher","Jane R Fisher","","","","","",""
|
|
||||||
"09/01/2022 at 9:22 pm","Christian","Vainieri","Vainiericm@gmail.com","9179231531","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","5","Milo Vainieri","Yes","","","","No","5","Emmett Vainierj","","","","","","",""
|
|
||||||
"09/01/2022 at 9:22 pm","EMILY","VAINIERI","thepiendak@gmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","5","Milo Vainieri","Yes","","","","Yes","5","Emmett Vainieri","","","","","","",""
|
|
||||||
"09/01/2022 at 9:22 pm","EMILY","VAINIERI","emily.vainieri1@maryland.gov","","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","5","Milo Vainieri","Yes","","","","Yes","5","Emmett Vainieri","","","","","","",""
|
|
||||||
"09/01/2022 at 7:53 pm","Harris","Gofstein","Hascogo@gmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","4","Juliet Gofstein ","No","","","","No","","","","","","","","",""
|
|
||||||
"09/01/2022 at 7:53 pm","Danielle","Gofstein","danielle.steiner@gmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","4","Juliet Gofstein","Yes","","","","Yes","","","","","","","","",""
|
|
||||||
"09/01/2022 at 6:31 pm","Rachel ","Haak","ocjetskimom@gmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$12.00","","","Rose Haak ","Yes","","","","","","","","","","","","5",""
|
|
||||||
"09/01/2022 at 4:02 pm","Brittany","Gutierrez","brittanyegutierrez@gmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$12.00","","","Andres Gutierrez ","Yes","","","","","","Marcelo Gutierrez ","","","","","","3","1"
|
|
||||||
"09/01/2022 at 4:02 pm","Eric","Nolan","nolanericj@gmail.com","2543835516","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","1","Jacob Nolan ","No","","","","","","","","","","","","",""
|
|
||||||
"09/01/2022 at 4:02 pm","Dana","Nolan","Dana.s.nolan@gmail.com","9089024102","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","1","Jacob Nolan","Yes","","","","","","","","","","","","",""
|
|
||||||
"09/01/2022 at 2:44 pm","Jackie","DeBella ","jacquelyn_debella@hcpss.org","4103135000","Faculty/Staff","","Store Purchase","2022 - 2023","$10.00","","","","","","","","","","","","","","","","",""
|
|
||||||
"09/01/2022 at 2:41 pm","Amanda","Diaz","Amanda_diaz@hcpss.org","4107074576","Faculty/Staff","","Store Purchase","2022 - 2023","$10.00","","","","","","","","","","","","","","","","",""
|
|
||||||
"08/31/2022 at 8:21 pm","Chad","Van Patten","cvanpatten@sumologic.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","K","Eli Van Patten","No","","","","No","3","Isaac Van Patten","","","","","","",""
|
|
||||||
"08/31/2022 at 8:21 pm","Elizabeth","Van Patten","elizabethvanpatten@gmail.com","4436522861","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","K","Eli Van Patten","Yes","","","","Yes","3","Isaac Van Patten","Chad Van Patten","","","","","",""
|
|
||||||
"08/31/2022 at 4:43 pm","Patrick","Devine","m9adevip@yahoo.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","4","Parker","No","","","","No","K","Savannah","","","","","","",""
|
|
||||||
"08/31/2022 at 4:43 pm","Jennifer","Devine","jlmgls@gmail.com","4437451851","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","4","Parker Devine","Yes","","","","Yes","K","Savannah Devine","Patrick d Devine","","","","","",""
|
|
||||||
"08/31/2022 at 2:13 pm","JeeMin","Lee","jeemin_lee@hcpss.org","","Faculty/Staff","","Store Purchase","2022 - 2023","$10.00","","","","","","","","","","","","","","","","",""
|
|
||||||
"08/31/2022 at 1:13 pm","Sharon ","Nath","sbn5160@gmail.com","4432543389","Parent/Guardian","","Store Purchase","2022 - 2023","$12.00","","","Brayden Nath","Yes","","","","","","","","","","","","4",""
|
|
||||||
"08/31/2022 at 12:42 pm","Thomas ","Noble","thomas.m.noble@gmail.com","410-205-9170","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","3","Joseph Noble","No","","","","","1","Tucker Noble","","","","","","",""
|
|
||||||
"08/31/2022 at 12:42 pm","Leela","Noble","lcollins802@gmail.com","4434650837","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","3","Joseph Noble","Yes","","","","","1","Tucker Noble","Leela Noble","","","","","",""
|
|
||||||
"08/31/2022 at 11:49 am","Reddy N","Apoori","anr747@yahoo.com","5712750943","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","4","Aditya Apoori","No","","","","","","","","","","","","","","","",""
|
|
||||||
"08/31/2022 at 11:49 am","Latha","Ramachandran","latha2310@gmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","4","Aditya Apoori","No","","","","","","","","","","","","","","","",""
|
|
||||||
"08/31/2022 at 11:14 am","Erin","Fowler","Erin_n_fowler@hcpss.org","","Faculty/Staff","","Store Purchase","2022 - 2023","$10.00","","","","","","","","","","","","","","","","","","","",""
|
|
||||||
"08/31/2022 at 10:55 am","Matthew ","Maschal","mmdrummerjpc@yahoo.com","4435276276","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","2","Chelsea Harumi Maschal","Yes","","","","Yes","","","","","","","","","","","",""
|
|
||||||
"08/31/2022 at 10:55 am","Sayuri ","Kamimura ","sayurikamimura@hotmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","2","Chelsea Harumi Maschal","Yes","","","","Yes","","","matthew r maschal","","","","","","","","",""
|
|
||||||
"08/31/2022 at 10:42 am","David","Kane","Dkane417@gmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","2","Mollie Kane","Yes","","","","Yes","K","Isla Kane","","","","","","","","","",""
|
|
||||||
"08/31/2022 at 10:42 am","Christina","Kane","Christinam.kane@gmail.com","4436671518","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","2","Mollie Kane","Yes","","","","Yes","K","Isla Kane","","","","","","","","","",""
|
|
||||||
"08/31/2022 at 9:31 am","Michael","Eitelman","eitelmanm@gmail.com","9105839045","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","3","Aaron Mason","No","","","","","3","Gabriel Eitelman","","Sean Eitelman","","","5","","","","",""
|
|
||||||
"08/31/2022 at 9:31 am","Megan","Mason","meganmason1@gmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","3","Aaron Mason","Yes","","","","","3","Gabriel Eitelman","","Sean Eitelman","","","5","","","","",""
|
|
||||||
"08/31/2022 at 9:23 am","Becky","Poirier","rebecca_poirier@hcpss.org","","Faculty/Staff","","Store Purchase","2022 - 2023","$10.00","","","","","","","","","","","","","","","","","","","",""
|
|
||||||
"08/31/2022 at 7:33 am","Kelli","Byle","kbyle@hcpss.org","","Faculty/Staff","","Store Purchase","2022 - 2023","$10.00","","","","","","","","","","","","","","","","","","","",""
|
|
||||||
"08/30/2022 at 9:31 pm","Natalie","Blake","Natalie_Blake@hcpss.org","2402155990","Faculty/Staff","","Store Purchase","2022 - 2023","$10.00","","","","","","","","","","","","","","","","","","","",""
|
|
||||||
"08/30/2022 at 6:32 pm","Keegan","Tozaki","keegantozaki@me.com","3016765330","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","4","Vera Nieto","Yes","","","","No","","","","","","","","","","","",""
|
|
||||||
"08/30/2022 at 6:32 pm","Jaclyn ","Tozaki ","avmomrn@gmail.com","2026791758","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","4","Vera Nieto","Yes","","","","No","","","","","","","","","","","",""
|
|
||||||
"08/30/2022 at 4:37 pm","Vanessa","Lichliter","vanessa_lichliter@hcpss.org","4438647995","Faculty/Staff","","Store Purchase","2022 - 2023","$10.00","","","","","","","","","","","","","","","","","","","",""
|
|
||||||
"08/30/2022 at 3:37 pm","Kelly","Pavlic","Kelly_pavlic@hcpss.org","7246009240","Faculty/Staff","","Store Purchase","2022 - 2023","$10.00","","","","","","","","","","","","","","","","","","","",""
|
|
||||||
"08/30/2022 at 3:17 pm","Kelly","Rippeon","kelly_rippeon@hcpss.org","","Faculty/Staff","","Store Purchase","2022 - 2023","$10.00","","","","","","","","","","","","","","","","","","","",""
|
|
||||||
"08/30/2022 at 2:41 pm","Elizabeth","Taylor","elizabeth_taylor@hcpss.org","","Faculty/Staff","","Store Purchase","2022 - 2023","$10.00","","","","","","","","","","","","","","","","","","","",""
|
|
||||||
"08/30/2022 at 1:14 pm","Pieter","Baker","pieter.baker@gmail.com","7606705130","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","K","Juliette Baker","No","","","","","2","Max Baker","","","","","","","","","",""
|
|
||||||
"08/30/2022 at 1:14 pm","Monica","Baker","monicarosebaker@gmail.com","7605226514","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","K","Juliette Baker","Yes","","","","","2","Max Baker","Pieter Baker","","","","","","","","",""
|
|
||||||
"08/28/2022 at 7:13 pm","Paul","Halvorsen","paul@halvo.me","","Parent/Guardian","","Cash","2022 - 2023","$37.50","","1","David Halvorsen","Yes","","","","No","3","Lily Halvorsen","","","","","","","","","",""
|
|
||||||
"08/28/2022 at 7:13 pm","Meg","Halvorsen","meg.halvorsen@gmail.com","","Parent/Guardian","","Cash","2022 - 2023","$37.50","","1","David Halvorsen","Yes","","","","No","3","Lily Halvorsen","","","","","","","","","",""
|
|
||||||
"08/28/2022 at 2:55 pm","Katrina ","Vala","campingcelebration2014@gmail.com","3522624032","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","K","Amelia Vala","Yes","","","","Yes","","","","","","","","","","","",""
|
|
||||||
"08/28/2022 at 2:55 pm","Katrina","Vala","katrina.hein@gmail.com","717 315 8296","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","K","Amelia Vala","Yes","","","","Yes","","","","","","","","","","","",""
|
|
||||||
"08/28/2022 at 2:10 pm","Alan","Mejibovsky ","Mejick24@gmail.com","585-313-1847","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","K","Keegan Mejibovsky ","Yes","","","","","","","","","","","","","","","",""
|
|
||||||
"08/28/2022 at 2:10 pm","Rachel ","Mejibovsky ","Stamanr1@gmail.com","319-521-6890","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","K","Keegan Mejibovsky ","Yes","","","","","","","","","","","","","","","",""
|
|
||||||
"08/28/2022 at 9:47 am","Scott","Lambert","slambert815@gmail.com","518-524-0007","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","1","Ella Rachel Lambert","No","","","","","","","","","","","","","","","",""
|
|
||||||
"08/28/2022 at 9:47 am","Alyssa","Lambert","alyssa.litman@gmail.com","215-833-8085","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","1","Ella Rachel Lambert","Yes","","","","","","","","","","","","","","","",""
|
|
||||||
"08/26/2022 at 9:40 pm","Brandon","Lawton","lawton.brandon@gmail.com","4436830446","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","3","Mae Lawton","Yes","","","","","","","","","","","","","","","",""
|
|
||||||
"08/26/2022 at 9:40 pm","Pey Lian","Lim","p3y1i4n@gmail.com","4436830577","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","3","Mae Lawton","Yes","","","","","","","","","","","","","","","",""
|
|
||||||
"08/26/2022 at 9:25 am","Rebecca","Care","rebecca_care@hcpss.org","4109788063","Faculty/Staff","","Store Purchase","2022 - 2023","$10.00","","","","","","","","","","","","","","","","","","","",""
|
|
||||||
"08/26/2022 at 7:10 am","Christina","Harkness","christina_harkness@hcpss.org","4439203138","Faculty/Staff","","Store Purchase","2022 - 2023","$10.00","","","","","","","","","","","","","","","","","","","",""
|
|
||||||
"08/25/2022 at 10:19 pm","John","Ballman","xjohnballx@yahoo.com","7142251447","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","3","John Ballman","Yes","","","","No","","","","","","","","","","","",""
|
|
||||||
"08/25/2022 at 10:19 pm","Megan","Ballman","meg.ballman@gmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","3","John Ballman","Yes","","","","No","","","","","","","","","","","",""
|
|
||||||
"08/25/2022 at 9:54 pm","Kevin","Foster","kevin.h.foster@gmail.com","8609174683","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","K","Ashton Foster","Yes","","","","No","","","","","","","","","","","",""
|
|
||||||
"08/25/2022 at 9:54 pm","Rebecca","Foster","becca.l.foster@gmail.com","6033054221","Faculty/Staff","","Store Purchase","2022 - 2023","$37.50","","K","Ashton Foster","Yes","","","","Yes","","","","","","","","","","","",""
|
|
||||||
"08/25/2022 at 9:32 pm","Brian","Scully","bjscully4@gmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","5","Donovan Scully","Yes","","","","Yes","2","Mairead Scully","","","","","","","","","",""
|
|
||||||
"08/25/2022 at 9:32 pm","Tara","Scully","taramscully@gmail.com","3012214171","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","5","Donovan Scully","Yes","","","","Yes","2","Mairead Scully","","","","","","","","","",""
|
|
||||||
"08/25/2022 at 9:32 pm","Glenn","Van Scyoc","gevanscyoc@yahoo.com","4104932399","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","K","Sophia Braunstein","Yes","","","","Yes","","","","","","","","","","","",""
|
|
||||||
"08/25/2022 at 9:32 pm","Kerstin","Braunstein","kerstin.braunstein@gmail.com","4439098503","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","K","Sophia Braunstein","Yes","","","","Yes","","","","","","","","","","","",""
|
|
||||||
"08/25/2022 at 7:27 pm","Josh","Burns","burnsie42@gmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","3","Taylor Burns","No","","","","No","1","Jackson Burns","","","","","","","","","Burns","Taylor"
|
|
||||||
"08/25/2022 at 7:27 pm","Tiffany","Burns","tmb926@yahoo.com","2406456887","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","3","Taylor Burns","No","","","","Yes","1","Jackson Burns","","","","","","","","","Burns","Taylor"
|
|
||||||
"08/25/2022 at 12:42 pm","Katelyn","Niu","katelyn.y.niu@gmail.com","4109164158","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","K","Alexander Yang","Yes","","","","Yes","","","","","","","","","","","",""
|
|
||||||
"08/25/2022 at 12:42 pm","Kevin","Yang","kevin.r.yang@gmail.com","4107363596","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","K","Alexander Yang","Yes","","","","Yes","","","","","","","","","","","",""
|
|
||||||
"08/25/2022 at 9:45 am","Chad","Morris","Morris1175@gmail.com","4108046385","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","K","Nora Morris","Yes","","","","","","","","","","","","","","Beckykaymorris@gmail.com","Morris","Becky"
|
|
||||||
"08/25/2022 at 9:45 am","Rebecca","Morris","beckykaymorris@gmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","K","Nora Morris","Yes","","","","","","","","","","","","","","Morris1175@gmail.com","Morris","Chad"
|
|
||||||
"08/25/2022 at 9:44 am","Mike","Elwell","mrmikeelwell@gmail.com","4109523585","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","K","Waverly Elwell","Yes","","","","Yes","","","","","","","","","","","",""
|
|
||||||
"08/25/2022 at 9:44 am","Sarah","Elwell","sarahkelwell0927@gmail.com","4102068615","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","K","Waverly Elwell","Yes","","","","Yes","","","","","","","","","","","",""
|
|
||||||
"08/24/2022 at 6:42 pm","Shira","Levy","ryanmlevy@gmail.com","4104935320","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","2","Maxwell Levy","Yes","","","","","K","Miles Levy","","","","","","","","","",""
|
|
||||||
"08/24/2022 at 6:42 pm","Shira","Levy","shirarlevy@gmail.com","4437425566","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","2","Maxwell Levy","Yes","","","","","K","Miles Levy","","","","","","","","ryanmlevy@gmail.com","Levy","Ryan"
|
|
||||||
"08/24/2022 at 6:41 pm","Michael","Bennett","Bennettmichael3@gmail.com","4438120216","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","K","Reese Bennett","Yes","","","","","3","Chase Bennett","","","","","","","","bennettmichael3@gmail.com","Bennett","Michael"
|
|
||||||
"08/24/2022 at 6:41 pm","Meghan","Bennett","barrmn@yahoo.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","K","Reese Bennett","Yes","","","","","3","Chase Bennett","","","","","","","","barrmn@yahoo.com","Bennett","Meghan"
|
|
||||||
"08/24/2022 at 2:46 pm","Eric","Wood","clemsoncrab@gmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","K","Spencer Wood","No","","","","Yes","","","","","","","","","","","",""
|
|
||||||
"08/24/2022 at 2:46 pm","Kim","Wood","kimberlymariewood@gmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","K","Spencer Wood","Yes","","","","Yes","","","","","","","","","","","",""
|
|
||||||
"08/24/2022 at 6:43 am","Tolly","Peddicord","tolly_peddicord@hcpss.org","410-591-9507","Faculty/Staff","","Store Purchase","2022 - 2023","$10.00","","","","","","","","","","","","","","","","","","","",""
|
|
||||||
"08/24/2022 at 5:42 am","Kathleen","Griffith","Kathleen_griffith@hcpss.org","4102074776","Faculty/Staff","","Store Purchase","2022 - 2023","$10.00","","","","","","","","","","","","","","","","","","","",""
|
|
||||||
"08/22/2022 at 12:17 pm","Cindy","Chen","qixin_chen@hcpss.org","4438519277","Faculty/Staff","","Store Purchase","2022 - 2023","$10.00","","","","","","","","","","","","","","","","","","","",""
|
|
||||||
"08/21/2022 at 2:12 pm","John","Hopkins","Jfhopkins01@hotmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","3","Benjamin Hopkins ","No","","","","Yes","","","","","","","","","","","",""
|
|
||||||
"08/21/2022 at 2:12 pm","Jennifer","Hopkins","Piccolag8r@hotmail.com","3212460421","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","3","Benjamin Hopkins","Yes","","","","Yes","","","","","","","","","","Jfhopkins01@hotmail.com","Hopkins","John"
|
|
||||||
"08/18/2022 at 1:24 am","Chris","Miles","cmiles4@aol.com","","Parent/Guardian","","Cash","2022 - 2023","$5.00","","","","No","","","","","","","","","","","","","","","",""
|
|
||||||
"08/18/2022 at 1:22 am","Justin","Callaway","justin.c.callaway@gmail.com","","Parent/Guardian","","Cash","2022 - 2023","$5.00","","","","No","","","","","","","","","","","","","","","",""
|
|
||||||
"08/18/2022 at 1:21 am","Doug","Cherneski","doug.cherneski@gmail.com","","Parent/Guardian","","Cash","2022 - 2023","$5.00","","","","No","","","","","","","","","","","","","","","",""
|
|
||||||
"08/17/2022 at 7:58 pm","Margaret","Fischer","margaret_fischer@hcpss.org","410-313-2813","Faculty/Staff","","Store Purchase","2022 - 2023","$10.00","","","","","","","","","","","","","","","","","","","",""
|
|
||||||
"08/17/2022 at 4:48 pm","Michael","O'Neill","mponeill84@gmail.com","4103024802","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","K","Maxwell O'Neill","Yes","","","","Yes","","","","","","","","","","","",""
|
|
||||||
"08/17/2022 at 4:48 pm","Katherine","O'Neill","karaoneill523@gmail.com","4102180624","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","K","Maxwell O'Neill","Yes","","","","Yes","","","","","","","","","","","",""
|
|
||||||
"08/17/2022 at 2:55 pm","Harish","Krishnaswamy","harish.swamy@gmail.com","6092731776","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","K","Arvin Krishnaswamy","Yes","","","","Yes","","","","","","","","","","","",""
|
|
||||||
"08/17/2022 at 2:55 pm","Ashwini","Anjanappa","ashwini.a@gmail.com","4102076273","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","K","Arvin Krishnaswamy","Yes","","","","Yes","","","","","","","","","","","",""
|
|
||||||
"08/17/2022 at 10:18 am","Jordan ","Wilson","JordanWilson14@gmail.com","240-460-8531","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","2","Ava Wilson","No","","","","","","","","","","","","","","","",""
|
|
||||||
"08/17/2022 at 10:18 am","Tara","Wilson","TaraWilson0212@gmail.com","410-245-7398","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","2","Ava Wilson","Yes","","","","","","","","","","","","","","JordanWilson14@gmail.com","Wilson","Jordan "
|
|
||||||
"08/15/2022 at 10:04 am","Elton","Edinborough","eltonseba2014@gmail.com","301-873-8022","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","K","Mena Kurian Edinborough","Yes","","","","Yes","","","","","","","","","","","",""
|
|
||||||
"08/15/2022 at 10:04 am","Seba","Kurian","seba.kurian@gmail.com","832-603-0301","Parent/Guardian","","Store Purchase","2022 - 2023","$37.50","","K","Mena Kurian Edinborough","Yes","","","","Yes","","","","","","","","","","","",""
|
|
||||||
"08/14/2022 at 6:44 pm","Steve","Sansone","Sansone50@gmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","1","Samantha Sansone","No","","","","","","","","","","","","","","","",""
|
|
||||||
"08/14/2022 at 6:44 pm","Lauren","Sansone","Lauren.c.sansone@gmail.com","4433264535","Parent/Guardian","","Store Purchase","2022 - 2023","$15.00","","1","Samantha Sansone ","Yes","","","","","","","","","","","","","","Sansone50@gmail.com","Sansone","Steve "
|
|
||||||
"08/10/2022 at 9:07 am","Holly","Miles","hollymiles8@gmail.com","","Faculty/Staff","","Cash","2022 - 2023","$75.00","","1","Kinslie Miles","","","","","","","","","","","","","","","cmiles4@aol.com","Miles","Chris"
|
|
||||||
"08/09/2022 at 11:55 am","Ashley","Callaway","ashley.e.simmons@gmail.com","410-707-8177","Parent/Guardian","","Store Purchase","2022 - 2023","$30.00","","1","Colin Callaway","Yes","","","","","","","","","","","","","","justin.c.callaway@gmail.com","Callaway","Justin"
|
|
||||||
"08/08/2022 at 1:46 pm","Candace","Knott","candace.knott@gmail.com","","Parent/Guardian","","Store Purchase","2022 - 2023","$30.00","","4","Bryan Knott","Yes","","","","","2","Chloe Knott","","","","","","","","kevin.knott2@gmail.com","Knott","Kevin"
|
|
||||||
"07/21/2022 at 9:33 am","Remya","Arul","remyaarul1@gmail.com","7576604855","Parent/Guardian","","Store Purchase","2022 - 2023","$30.00","","K","Kala Cherneski","Yes","","","","","","","","","","","","","","doug.cherneski@gmail.com","Cherneski","Doug"
|
|
||||||
|
11
go.mod
@@ -2,23 +2,14 @@ module go-sjles-pta-vote
|
|||||||
|
|
||||||
go 1.24.4
|
go 1.24.4
|
||||||
|
|
||||||
require (
|
require github.com/glebarez/go-sqlite v1.22.0
|
||||||
github.com/glebarez/go-sqlite v1.22.0
|
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.0
|
|
||||||
github.com/gorilla/mux v1.8.1
|
|
||||||
github.com/pkg/errors v0.9.1
|
|
||||||
github.com/stretchr/testify v1.11.1
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/google/uuid v1.5.0 // indirect
|
github.com/google/uuid v1.5.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
golang.org/x/sys v0.15.0 // indirect
|
golang.org/x/sys v0.15.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
|
||||||
modernc.org/libc v1.37.6 // indirect
|
modernc.org/libc v1.37.6 // indirect
|
||||||
modernc.org/mathutil v1.6.0 // indirect
|
modernc.org/mathutil v1.6.0 // indirect
|
||||||
modernc.org/memory v1.7.2 // indirect
|
modernc.org/memory v1.7.2 // indirect
|
||||||
|
|||||||
16
go.sum
@@ -1,34 +1,18 @@
|
|||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
|
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
|
||||||
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
|
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
|
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
|
||||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||||
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
|
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
|
||||||
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
|
||||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
modernc.org/libc v1.37.6 h1:orZH3c5wmhIQFTXF+Nt+eeauyd+ZIt2BX6ARe+kD+aw=
|
modernc.org/libc v1.37.6 h1:orZH3c5wmhIQFTXF+Nt+eeauyd+ZIt2BX6ARe+kD+aw=
|
||||||
modernc.org/libc v1.37.6/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE=
|
modernc.org/libc v1.37.6/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE=
|
||||||
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
package common
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
DATE_FORMAT = "2006-01-02 15:04:05"
|
|
||||||
SUCCESS = "success"
|
|
||||||
)
|
|
||||||
|
|
||||||
func SendError(w http.ResponseWriter, errStr string, statusCode int) {
|
|
||||||
w.WriteHeader(statusCode)
|
|
||||||
json.NewEncoder(w).Encode(map[string]string{"error": errStr})
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
@@ -17,8 +17,6 @@ var conf *Config
|
|||||||
var conf_path string = ".env"
|
var conf_path string = ".env"
|
||||||
|
|
||||||
func GetConfig() *Config {
|
func GetConfig() *Config {
|
||||||
_ = GenerateEnvFileIfNotExists("./sjles-pta-vote.db")
|
|
||||||
|
|
||||||
if conf != nil {
|
if conf != nil {
|
||||||
return conf
|
return conf
|
||||||
}
|
}
|
||||||
@@ -27,8 +25,9 @@ func GetConfig() *Config {
|
|||||||
|
|
||||||
// TODO: Make this into a ini or toml file
|
// TODO: Make this into a ini or toml file
|
||||||
configContent, err := os.ReadFile(conf_path)
|
configContent, err := os.ReadFile(conf_path)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error reading .env file: %v", err)
|
fmt.Println("Error reading .env file: ", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,6 +42,11 @@ func GetConfig() *Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal([]byte(configContent), conf); err != nil {
|
||||||
|
fmt.Println("Error unmarshalling config file: ", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Better mapping of key to json values
|
// TODO: Better mapping of key to json values
|
||||||
// TODO: Better error checking if values are missing
|
// TODO: Better error checking if values are missing
|
||||||
// TODO: Default values
|
// TODO: Default values
|
||||||
@@ -54,7 +58,7 @@ func GetConfig() *Config {
|
|||||||
} else if strings.Contains(key, "redis_password") {
|
} else if strings.Contains(key, "redis_password") {
|
||||||
conf.RedisPassword = value
|
conf.RedisPassword = value
|
||||||
} else {
|
} else {
|
||||||
log.Printf("Error, Unknown key value pair: %s = %s", key, value)
|
fmt.Println("Error, Unknown key value pair: ", key, " = ", value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,12 +68,3 @@ func GetConfig() *Config {
|
|||||||
func SetConfig(init_conf *Config) {
|
func SetConfig(init_conf *Config) {
|
||||||
conf = init_conf
|
conf = init_conf
|
||||||
}
|
}
|
||||||
|
|
||||||
func GenerateEnvFileIfNotExists(dbPath string) error {
|
|
||||||
_, err := os.Stat(".env")
|
|
||||||
if err == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
envContent := fmt.Sprintf("db_path=\"%s\"\n", dbPath)
|
|
||||||
return os.WriteFile(".env", []byte(envContent), 0644)
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,6 @@ package db
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"log"
|
|
||||||
|
|
||||||
"go-sjles-pta-vote/server/config"
|
"go-sjles-pta-vote/server/config"
|
||||||
|
|
||||||
@@ -32,8 +31,7 @@ CREATE TABLE IF NOT EXISTS voters (
|
|||||||
CREATE TABLE IF NOT EXISTS members (
|
CREATE TABLE IF NOT EXISTS members (
|
||||||
email TEXT NOT NULL,
|
email TEXT NOT NULL,
|
||||||
member_name TEXT,
|
member_name TEXT,
|
||||||
school_year UNSIGNED INT NOT NULL,
|
PRIMARY KEY (email)
|
||||||
PRIMARY KEY (email, school_year)
|
|
||||||
);
|
);
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -43,26 +41,18 @@ func Connect() (*sql.DB, error) {
|
|||||||
db_config := config.GetConfig()
|
db_config := config.GetConfig()
|
||||||
|
|
||||||
db, err := sql.Open("sqlite", db_config.DBPath)
|
db, err := sql.Open("sqlite", db_config.DBPath)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error opening database: %v", err)
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = db.Exec(build_db_query)
|
_, err = db.Exec(build_db_query)
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error updating schema: %v", err)
|
|
||||||
_ = db.Close()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return db, nil
|
return db, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func Close() {
|
func Close() {
|
||||||
if db != nil {
|
if db != nil {
|
||||||
err := db.Close()
|
_ = db.Close()
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error closing database: %v", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 125 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 845 B |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 15 KiB |
@@ -1 +0,0 @@
|
|||||||
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
|
||||||
226
server/main.go
@@ -1,226 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"math/rand"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
|
|
||||||
"go-sjles-pta-vote/server/common"
|
|
||||||
"go-sjles-pta-vote/server/models"
|
|
||||||
"go-sjles-pta-vote/server/services"
|
|
||||||
)
|
|
||||||
|
|
||||||
func voteHandler(resWriter http.ResponseWriter, request *http.Request) {
|
|
||||||
var vote models.Vote
|
|
||||||
if err := json.NewDecoder(request.Body).Decode(&vote); err != nil {
|
|
||||||
common.SendError(resWriter, "Invalid JSON", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := services.SetVote(&vote); err != nil {
|
|
||||||
common.SendError(resWriter, "Failed to set vote", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resWriter.WriteHeader(http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
func voteIDHandler(resWriter http.ResponseWriter, request *http.Request) {
|
|
||||||
vars := mux.Vars(request)
|
|
||||||
idStr := vars["id"]
|
|
||||||
id, err := strconv.ParseInt(idStr, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
common.SendError(resWriter, "Invalid poll ID", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
vote := models.Vote{
|
|
||||||
PollId: id,
|
|
||||||
Email: "example@example.com", // Replace with actual email retrieval logic
|
|
||||||
Vote: true, // Replace with actual vote retrieval logic
|
|
||||||
}
|
|
||||||
|
|
||||||
err = services.SetVote(&vote)
|
|
||||||
if err != nil {
|
|
||||||
common.SendError(resWriter, "Failed to set vote", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resWriter.WriteHeader(http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
func statsHandler(resWriter http.ResponseWriter, request *http.Request) {
|
|
||||||
if request.Method == http.MethodGet {
|
|
||||||
filePath := "./server/templates/stats.html"
|
|
||||||
http.ServeFile(resWriter, request, filePath)
|
|
||||||
} else if request.Method == http.MethodPost {
|
|
||||||
vars := mux.Vars(request)
|
|
||||||
id := vars["id"]
|
|
||||||
|
|
||||||
poll, err := services.GetPollByQuestion(id)
|
|
||||||
if err != nil {
|
|
||||||
common.SendError(resWriter, "Failed to get poll", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
json.NewEncoder(resWriter).Encode(poll)
|
|
||||||
} else {
|
|
||||||
resWriter.WriteHeader(http.StatusMethodNotAllowed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func pollsIDHandler(resWriter http.ResponseWriter, request *http.Request) {
|
|
||||||
vars := mux.Vars(request)
|
|
||||||
id, err := strconv.ParseInt(vars["id"], 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
common.SendError(resWriter, "Invalid poll ID", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
poll, err := services.GetPollById(id)
|
|
||||||
if err == services.ErrPollNotFound {
|
|
||||||
common.SendError(resWriter, "Poll not found", http.StatusNotFound)
|
|
||||||
return
|
|
||||||
} else if err != nil {
|
|
||||||
common.SendError(resWriter, "Failed to get poll", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
json.NewEncoder(resWriter).Encode(poll)
|
|
||||||
}
|
|
||||||
|
|
||||||
func adminLoginHandler(resWriter http.ResponseWriter, request *http.Request) {
|
|
||||||
if request.Method != http.MethodPost {
|
|
||||||
common.SendError(resWriter, "Method not allowed", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var loginReq services.LoginRequest
|
|
||||||
if err := json.NewDecoder(request.Body).Decode(&loginReq); err != nil {
|
|
||||||
common.SendError(resWriter, "Invalid JSON", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate admin credentials
|
|
||||||
isValid, err := services.ValidateAdminLogin(loginReq.Username, loginReq.Password)
|
|
||||||
if err != nil {
|
|
||||||
common.SendError(resWriter, "Invalid username or password", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !isValid {
|
|
||||||
common.SendError(resWriter, "Invalid username or password", http.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate JWT token
|
|
||||||
token, err := services.GenerateAuthToken(loginReq.Username)
|
|
||||||
if err != nil {
|
|
||||||
common.SendError(resWriter, "Failed to generate auth token", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resWriter.WriteHeader(http.StatusOK)
|
|
||||||
json.NewEncoder(resWriter).Encode(services.LoginResponse{
|
|
||||||
Success: true,
|
|
||||||
Token: token,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func initDatabase() error {
|
|
||||||
// Seed random generator for reproducible results in tests
|
|
||||||
rand.Seed(42)
|
|
||||||
|
|
||||||
polls := []models.Poll{
|
|
||||||
{
|
|
||||||
ID: 1,
|
|
||||||
Question: "Should we increase the budget?",
|
|
||||||
MemberYes: rand.Int63n(50),
|
|
||||||
MemberNo: rand.Int63n(50),
|
|
||||||
NonMemberYes: rand.Int63n(20),
|
|
||||||
NonMemberNo: rand.Int63n(20),
|
|
||||||
TotalVotes: int(rand.Int63n(100)),
|
|
||||||
WhoVoted: []string{"email1@example.com", "email2@example.com", "email3@example.com", "email4@example.com"},
|
|
||||||
CreatedAt: time.Now().Format(time.RFC3339),
|
|
||||||
UpdatedAt: time.Now().Format(time.RFC3339),
|
|
||||||
ExpiresAt: time.Now().Add(24 * time.Hour).Format(time.RFC3339),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: 2,
|
|
||||||
Question: "Should we hire more staff?",
|
|
||||||
MemberYes: rand.Int63n(50),
|
|
||||||
MemberNo: rand.Int63n(50),
|
|
||||||
NonMemberYes: rand.Int63n(20),
|
|
||||||
NonMemberNo: rand.Int63n(20),
|
|
||||||
TotalVotes: int(rand.Int63n(100)),
|
|
||||||
WhoVoted: []string{"email1@example.com", "email2@example.com", "email3@example.com", "email4@example.com"},
|
|
||||||
CreatedAt: time.Now().Format(time.RFC3339),
|
|
||||||
UpdatedAt: time.Now().Format(time.RFC3339),
|
|
||||||
ExpiresAt: time.Now().Add(24 * time.Hour).Format(time.RFC3339),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: 3,
|
|
||||||
Question: "Should we renovate the building?",
|
|
||||||
MemberYes: rand.Int63n(50),
|
|
||||||
MemberNo: rand.Int63n(50),
|
|
||||||
NonMemberYes: rand.Int63n(20),
|
|
||||||
NonMemberNo: rand.Int63n(20),
|
|
||||||
TotalVotes: int(rand.Int63n(100)),
|
|
||||||
WhoVoted: []string{"email1@example.com", "email2@example.com", "email3@example.com", "email4@example.com"},
|
|
||||||
CreatedAt: time.Now().Format(time.RFC3339),
|
|
||||||
UpdatedAt: time.Now().Format(time.RFC3339),
|
|
||||||
ExpiresAt: time.Now().Add(24 * time.Hour).Format(time.RFC3339),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, poll := range polls {
|
|
||||||
if err := services.CreatePollIgnore(&poll); err != nil {
|
|
||||||
return fmt.Errorf("failed to create poll %d: %v", poll.ID, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
log.SetOutput(os.Stdout)
|
|
||||||
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
|
||||||
|
|
||||||
// Initialize database with sample data
|
|
||||||
if err := initDatabase(); err != nil {
|
|
||||||
log.Fatalf("Failed to initialize database: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
http.HandleFunc("/api/vote", voteHandler)
|
|
||||||
http.HandleFunc("/api/vote/{id}", voteIDHandler)
|
|
||||||
http.HandleFunc("/api/stats", statsHandler)
|
|
||||||
http.HandleFunc("/api/polls/{id}", pollsIDHandler)
|
|
||||||
http.HandleFunc("/api/admin/new-vote", services.AdminNewVoteHandler)
|
|
||||||
http.HandleFunc("/api/admin/view-votes", services.AdminViewVoteHandler)
|
|
||||||
http.HandleFunc("/api/admin/login", adminLoginHandler)
|
|
||||||
http.HandleFunc("/api/admin/members", services.AdminMembersHandler)
|
|
||||||
http.HandleFunc("/api/admin/members/view", services.AdminMembersView)
|
|
||||||
|
|
||||||
buildPath := filepath.Join(".", "client", "build")
|
|
||||||
fs := http.FileServer(http.Dir(buildPath))
|
|
||||||
|
|
||||||
http.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// If the file exists on disk, let the file server handle it.
|
|
||||||
if _, err := os.Stat(filepath.Join(buildPath, r.URL.Path)); err == nil {
|
|
||||||
fs.ServeHTTP(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Otherwise serve index.html (so React Router can handle the route)
|
|
||||||
http.ServeFile(w, r, filepath.Join(buildPath, "index.html"))
|
|
||||||
}))
|
|
||||||
|
|
||||||
log.Printf("Starting server on :8080")
|
|
||||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
|
||||||
}
|
|
||||||
@@ -3,5 +3,4 @@ package models
|
|||||||
type Members struct {
|
type Members struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
SchoolYear int `json:"school_year"`
|
|
||||||
}
|
}
|
||||||
@@ -3,10 +3,6 @@ package models
|
|||||||
type Poll struct {
|
type Poll struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
Question string `json:"question"`
|
Question string `json:"question"`
|
||||||
MemberYes int64 `json:"member_yes"`
|
|
||||||
MemberNo int64 `json:"member_no"`
|
|
||||||
NonMemberYes int64 `json:"non_member_yes"`
|
|
||||||
NonMemberNo int64 `json:"non_member_no"`
|
|
||||||
TotalVotes int `json:"total_votes"`
|
TotalVotes int `json:"total_votes"`
|
||||||
WhoVoted []string `json:"who_voted"`
|
WhoVoted []string `json:"who_voted"`
|
||||||
CreatedAt string `json:"created_at"`
|
CreatedAt string `json:"created_at"`
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
type Vote struct {
|
type Vote struct {
|
||||||
PollId int64 `json:"poll_id"`
|
PollID int `json:"poll_id"`
|
||||||
Vote bool `json:"vote"`
|
OptionIndex int `json:"option_index"`
|
||||||
Email string `json:"email"`
|
IsMember bool `json:"is_member"`
|
||||||
}
|
}
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
package models
|
|
||||||
|
|
||||||
import (
|
|
||||||
"go-sjles-pta-vote/server/db"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Voter struct {
|
|
||||||
Email string `json:"email"`
|
|
||||||
IsMember bool `json:"is_member"`
|
|
||||||
YesVote bool `json:"yes_vote"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetVoters(pollId int64) ([]Voter, error) {
|
|
||||||
db_conn, err := db.Connect()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
rows, err := db_conn.Query(`
|
|
||||||
SELECT v.voter_email,
|
|
||||||
CASE
|
|
||||||
WHEN m.email IS NOT NULL THEN 1
|
|
||||||
ELSE 0
|
|
||||||
END AS is_member,
|
|
||||||
CASE
|
|
||||||
WHEN p.member_yes_votes + p.non_member_yes_votes > p.member_no_votes + p.non_member_no_votes THEN 1
|
|
||||||
ELSE 0
|
|
||||||
END AS yes_vote
|
|
||||||
FROM voters v
|
|
||||||
LEFT JOIN members m ON v.voter_email = m.email
|
|
||||||
LEFT JOIN polls p ON v.poll_id = p.id
|
|
||||||
WHERE v.poll_id = $1
|
|
||||||
`, pollId)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
var voters []Voter
|
|
||||||
for rows.Next() {
|
|
||||||
var voter Voter
|
|
||||||
var isMember int
|
|
||||||
var yesVote int
|
|
||||||
if err := rows.Scan(&voter.Email, &isMember, &yesVote); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
voter.IsMember = isMember == 1
|
|
||||||
voter.YesVote = yesVote == 1
|
|
||||||
voters = append(voters, voter)
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return voters, nil
|
|
||||||
}
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
package services
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/hex"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
type LoginRequest struct {
|
|
||||||
Username string `json:"username"`
|
|
||||||
Password string `json:"password"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LoginResponse struct {
|
|
||||||
Success bool `json:"success"`
|
|
||||||
Token string `json:"token,omitempty"`
|
|
||||||
Error string `json:"error,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var jwtSecret string
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
jwtSecret = os.Getenv("JWT_SECRET")
|
|
||||||
if jwtSecret == "" {
|
|
||||||
jwtSecret = "your-secret-key-change-in-production"
|
|
||||||
log.Println("WARNING: JWT_SECRET not set, using default value. Change this in production!")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAdminCredentials retrieves admin credentials from environment variables
|
|
||||||
// Format: ADMIN_USERS=username:password|username2:password2
|
|
||||||
func getAdminCredentials() map[string]string {
|
|
||||||
adminUsers := os.Getenv("ADMIN_USERS")
|
|
||||||
if adminUsers == "" {
|
|
||||||
// Default admin user (change in production)
|
|
||||||
adminUsers = "admin:admin"
|
|
||||||
log.Println("WARNING: ADMIN_USERS not set, using default admin:admin")
|
|
||||||
}
|
|
||||||
|
|
||||||
credentials := make(map[string]string)
|
|
||||||
for _, userPass := range strings.Split(adminUsers, "|") {
|
|
||||||
parts := strings.Split(strings.TrimSpace(userPass), ":")
|
|
||||||
if len(parts) == 2 {
|
|
||||||
credentials[parts[0]] = parts[1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return credentials
|
|
||||||
}
|
|
||||||
|
|
||||||
// hashPassword hashes a password using SHA256
|
|
||||||
func hashPassword(password string) string {
|
|
||||||
hash := sha256.Sum256([]byte(password))
|
|
||||||
return hex.EncodeToString(hash[:])
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValidateAdminLogin checks if the provided username and password are valid
|
|
||||||
func ValidateAdminLogin(username, password string) (bool, error) {
|
|
||||||
if username == "" || password == "" {
|
|
||||||
return false, errors.New("username and password are required")
|
|
||||||
}
|
|
||||||
|
|
||||||
credentials := getAdminCredentials()
|
|
||||||
storedPassword, exists := credentials[username]
|
|
||||||
|
|
||||||
if !exists {
|
|
||||||
// Return false but not an error for security reasons (don't reveal if user exists)
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compare passwords (you could enhance this with bcrypt in production)
|
|
||||||
if storedPassword != password {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GenerateAuthToken generates a JWT token for an authenticated admin user
|
|
||||||
func GenerateAuthToken(username string) (string, error) {
|
|
||||||
claims := jwt.MapClaims{
|
|
||||||
"username": username,
|
|
||||||
"exp": time.Now().Add(time.Hour * 24).Unix(), // Token expires in 24 hours
|
|
||||||
"iat": time.Now().Unix(),
|
|
||||||
}
|
|
||||||
|
|
||||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
|
||||||
tokenString, err := token.SignedString([]byte(jwtSecret))
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return "", errors.Wrap(err, "failed to generate token")
|
|
||||||
}
|
|
||||||
|
|
||||||
return tokenString, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// VerifyAuthToken verifies a JWT token and returns the username if valid
|
|
||||||
func VerifyAuthToken(tokenString string) (string, error) {
|
|
||||||
token, err := jwt.ParseWithClaims(tokenString, &jwt.MapClaims{}, func(token *jwt.Token) (interface{}, error) {
|
|
||||||
return []byte(jwtSecret), nil
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return "", errors.Wrap(err, "failed to parse token")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !token.Valid {
|
|
||||||
return "", errors.New("invalid token")
|
|
||||||
}
|
|
||||||
|
|
||||||
claims, ok := token.Claims.(*jwt.MapClaims)
|
|
||||||
if !ok {
|
|
||||||
return "", errors.New("invalid token claims")
|
|
||||||
}
|
|
||||||
|
|
||||||
username, ok := (*claims)["username"].(string)
|
|
||||||
if !ok {
|
|
||||||
return "", errors.New("username not found in token")
|
|
||||||
}
|
|
||||||
|
|
||||||
return username, nil
|
|
||||||
}
|
|
||||||
@@ -1,237 +0,0 @@
|
|||||||
package services
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/csv"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"strings"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"io/ioutil"
|
|
||||||
"encoding/json"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
|
|
||||||
"go-sjles-pta-vote/server/common"
|
|
||||||
"go-sjles-pta-vote/server/db"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Member struct {
|
|
||||||
Name string
|
|
||||||
Email string
|
|
||||||
}
|
|
||||||
|
|
||||||
const BATCH_SIZE = 100
|
|
||||||
const CVS_FILE_FIELD = "members.csv"
|
|
||||||
|
|
||||||
func AdminMembersHandler(resWriter http.ResponseWriter, request *http.Request) {
|
|
||||||
if request.Method != http.MethodPost {
|
|
||||||
resWriter.WriteHeader(http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var year int
|
|
||||||
var err error
|
|
||||||
|
|
||||||
if err = request.ParseMultipartForm(10 << 20); err != nil {
|
|
||||||
common.SendError(resWriter, "Failed to parse multipart form", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
year_from_form := request.FormValue("year")
|
|
||||||
if year_from_form == "" {
|
|
||||||
common.SendError(resWriter, "Year is required", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
year, err = strconv.Atoi(year_from_form)
|
|
||||||
if err != nil {
|
|
||||||
common.SendError(resWriter, "Invalid year", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
file, _, err := request.FormFile(CVS_FILE_FIELD)
|
|
||||||
if err != nil {
|
|
||||||
common.SendError(resWriter, "Failed to read " + CVS_FILE_FIELD + " file", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
fileBytes, err := ioutil.ReadAll(file)
|
|
||||||
if err != nil {
|
|
||||||
common.SendError(resWriter, "Failed to read " + CVS_FILE_FIELD + " file", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = ParseMembersFromBytes(year, fileBytes); err != nil {
|
|
||||||
common.SendError(resWriter, "Failed to parse members from CSV", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resWriter.WriteHeader(http.StatusOK)
|
|
||||||
json.NewEncoder(resWriter).Encode(map[string]bool{common.SUCCESS: true})
|
|
||||||
}
|
|
||||||
|
|
||||||
func AdminMembersView(resWriter http.ResponseWriter, request *http.Request) {
|
|
||||||
yearStr := request.URL.Query().Get("year")
|
|
||||||
if yearStr == "" {
|
|
||||||
common.SendError(resWriter, "Year is required", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
year, err := strconv.Atoi(yearStr)
|
|
||||||
if err != nil {
|
|
||||||
common.SendError(resWriter, "Invalid year", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
members, err := GetMembersByYear(year)
|
|
||||||
if err != nil {
|
|
||||||
common.SendError(resWriter, "Failed to get members", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resWriter.WriteHeader(http.StatusOK)
|
|
||||||
json.NewEncoder(resWriter).Encode(map[string]interface{}{
|
|
||||||
common.SUCCESS: true,
|
|
||||||
"members": members,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseMembersFromBytes(year int, fileBytes []byte) error {
|
|
||||||
reader := csv.NewReader(strings.NewReader(string(fileBytes)))
|
|
||||||
reader.FieldsPerRecord = -1 // Allow variable number of fields per record
|
|
||||||
records, err := reader.ReadAll()
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "failed to read CSV from bytes")
|
|
||||||
}
|
|
||||||
|
|
||||||
var members []Member
|
|
||||||
|
|
||||||
for i, record := range records {
|
|
||||||
if i == 0 {
|
|
||||||
continue // Skip the first line (column headers)
|
|
||||||
}
|
|
||||||
if len(record) < 4 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
firstName := strings.TrimSpace(record[1])
|
|
||||||
lastName := strings.TrimSpace(record[2])
|
|
||||||
email := strings.TrimSpace(record[3])
|
|
||||||
|
|
||||||
members = append(members, Member{
|
|
||||||
Name: fmt.Sprintf("%s %s", firstName, lastName),
|
|
||||||
Email: email,
|
|
||||||
})
|
|
||||||
|
|
||||||
if len(record) < 30 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
email2 := strings.TrimSpace(record[27])
|
|
||||||
if email2 != "" {
|
|
||||||
firstName2 := strings.TrimSpace(record[29])
|
|
||||||
lastName2 := strings.TrimSpace(record[28])
|
|
||||||
|
|
||||||
members = append(members, Member{
|
|
||||||
Name: fmt.Sprintf("%s %s", firstName2, lastName2),
|
|
||||||
Email: email2,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return saveMember(year, members)
|
|
||||||
}
|
|
||||||
|
|
||||||
func saveMember(year int, members []Member) error {
|
|
||||||
insertMembersQuery := `
|
|
||||||
INSERT OR REPLACE INTO members (email, member_name, school_year)
|
|
||||||
VALUES ($1, $2, $3)
|
|
||||||
`
|
|
||||||
log.Printf("Starting to save %d members for year %d", len(members), year)
|
|
||||||
|
|
||||||
db_conn, err := db.Connect()
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "failed to connect to database")
|
|
||||||
}
|
|
||||||
defer db_conn.Close()
|
|
||||||
|
|
||||||
tx, err := db_conn.Begin()
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "failed to begin transaction")
|
|
||||||
}
|
|
||||||
|
|
||||||
stmt, err := tx.Prepare(insertMembersQuery)
|
|
||||||
if err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
return errors.Wrap(err, "failed to prepare statement")
|
|
||||||
}
|
|
||||||
defer stmt.Close()
|
|
||||||
|
|
||||||
for index, member := range members {
|
|
||||||
_, err = stmt.Exec(member.Email, member.Name, year)
|
|
||||||
if err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
return errors.Wrap(err, "failed to execute insert")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (index+1) % BATCH_SIZE == 0 {
|
|
||||||
err = tx.Commit()
|
|
||||||
if err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
return errors.Wrap(err, "failed to commit transaction")
|
|
||||||
}
|
|
||||||
|
|
||||||
tx, err = db_conn.Begin()
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "failed to begin new transaction")
|
|
||||||
}
|
|
||||||
|
|
||||||
stmt, err = tx.Prepare(insertMembersQuery)
|
|
||||||
if err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
return errors.Wrap(err, "failed to prepare new statement")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return tx.Commit()
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetMembersByYear(year int) ([]Member, error) {
|
|
||||||
query := `
|
|
||||||
SELECT member_name, email
|
|
||||||
FROM members
|
|
||||||
WHERE school_year = $1
|
|
||||||
ORDER BY member_name ASC
|
|
||||||
`
|
|
||||||
|
|
||||||
db_conn, err := db.Connect()
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "failed to connect to database")
|
|
||||||
}
|
|
||||||
defer db_conn.Close()
|
|
||||||
|
|
||||||
rows, err := db_conn.Query(query, year)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "failed to execute query")
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
var members []Member
|
|
||||||
|
|
||||||
for rows.Next() {
|
|
||||||
var member Member
|
|
||||||
if err := rows.Scan(&member.Name, &member.Email); err != nil {
|
|
||||||
return nil, errors.Wrap(err, "failed to scan row")
|
|
||||||
}
|
|
||||||
members = append(members, member)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, errors.Wrap(err, "row iteration error")
|
|
||||||
}
|
|
||||||
|
|
||||||
return members, nil
|
|
||||||
}
|
|
||||||
@@ -2,73 +2,18 @@ package services
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"go-sjles-pta-vote/server/common"
|
|
||||||
"go-sjles-pta-vote/server/db"
|
"go-sjles-pta-vote/server/db"
|
||||||
"go-sjles-pta-vote/server/models"
|
"go-sjles-pta-vote/server/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
func CreatePoll(poll *models.Poll) (*models.Poll, error) {
|
||||||
ErrQuestionAlreadyExists = errors.New("Question already exists")
|
new_poll := models.Poll{}
|
||||||
ErrQuestionDoesntExist = errors.New("Question does not exist yet")
|
|
||||||
ErrVoterAlreadyVoted = errors.New("Voter already voted")
|
|
||||||
ErrPollNotFound = errors.New("Poll not found")
|
|
||||||
ErrFailedToUpdateVote = errors.New("Failed to update vote")
|
|
||||||
ErrFailedToDeletePoll = errors.New("Failed to delete poll")
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
DEFAULT_POLL_DURATION_HOURS = 24
|
|
||||||
)
|
|
||||||
|
|
||||||
func AdminNewVoteHandler(resWriter http.ResponseWriter, request *http.Request) {
|
|
||||||
if request.Method != http.MethodPost {
|
|
||||||
resWriter.WriteHeader(http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
question := request.FormValue("question")
|
|
||||||
if question == "" {
|
|
||||||
common.SendError(resWriter, "Question is required", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
durationHours := DEFAULT_POLL_DURATION_HOURS
|
|
||||||
if durationStr := request.FormValue("duration"); durationStr != "" {
|
|
||||||
var err error
|
|
||||||
durationHours, err = strconv.Atoi(durationStr)
|
|
||||||
if err != nil {
|
|
||||||
common.SendError(resWriter, "Invalid duration", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
poll := models.Poll{
|
|
||||||
Question: question,
|
|
||||||
ExpiresAt: time.Now().Add(time.Duration(durationHours) * time.Hour).Format(common.DATE_FORMAT),
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := CreatePoll(&poll)
|
|
||||||
if err != nil {
|
|
||||||
common.SendError(resWriter, "Failed to create poll", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resWriter.WriteHeader(http.StatusOK)
|
|
||||||
json.NewEncoder(resWriter).Encode(map[string]bool{common.SUCCESS: true})
|
|
||||||
}
|
|
||||||
|
|
||||||
func CreatePoll(poll *models.Poll) (int64, error) {
|
|
||||||
db_conn, err := db.Connect()
|
db_conn, err := db.Connect()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to connect to database: %s", err.Error())
|
return nil, err
|
||||||
return -1, err
|
|
||||||
}
|
}
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
|
|
||||||
@@ -78,19 +23,17 @@ func CreatePoll(poll *models.Poll) (int64, error) {
|
|||||||
WHERE question == $1
|
WHERE question == $1
|
||||||
`)
|
`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("%s", err.Error())
|
return nil, err
|
||||||
return -1, err
|
|
||||||
}
|
}
|
||||||
defer get_stmt.Close()
|
defer get_stmt.Close()
|
||||||
|
|
||||||
var id int
|
var id int
|
||||||
err = get_stmt.QueryRow(poll.Question).Scan(&id)
|
err = get_stmt.QueryRow(poll.Question).Scan(&id)
|
||||||
if err != nil {
|
|
||||||
if err != sql.ErrNoRows {
|
if err != sql.ErrNoRows {
|
||||||
log.Printf("%s", err.Error())
|
if err != nil {
|
||||||
return -1, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return -1, ErrQuestionAlreadyExists
|
return nil, errors.New("Already exists")
|
||||||
}
|
}
|
||||||
|
|
||||||
stmt, err := db_conn.Prepare(`
|
stmt, err := db_conn.Prepare(`
|
||||||
@@ -104,435 +47,17 @@ func CreatePoll(poll *models.Poll) (int64, error) {
|
|||||||
`)
|
`)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("%s", err.Error())
|
return nil, err
|
||||||
return -1, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
defer stmt.Close()
|
defer stmt.Close()
|
||||||
|
|
||||||
res, err := stmt.Exec(poll.Question, poll.ExpiresAt)
|
res, err := stmt.Exec(poll.Question, poll.ExpiresAt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("%s", err.Error())
|
|
||||||
return -1, err
|
|
||||||
}
|
|
||||||
|
|
||||||
new_poll_id, err := res.LastInsertId()
|
|
||||||
return new_poll_id, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func AdminViewVoteHandler(resWriter http.ResponseWriter, request *http.Request) {
|
|
||||||
if request.Method != http.MethodPost {
|
|
||||||
resWriter.WriteHeader(http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var polls []models.Poll
|
|
||||||
var err error
|
|
||||||
question := request.FormValue("question")
|
|
||||||
if question == "" {
|
|
||||||
polls, err = GetAllPolls()
|
|
||||||
if err != nil {
|
|
||||||
common.SendError(resWriter, "Failed to get polls", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
poll, err := GetPollByQuestion(question)
|
|
||||||
if err != nil {
|
|
||||||
common.SendError(resWriter, "Failed to get poll question "+question, http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
polls = append(polls, *poll)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = json.NewEncoder(resWriter).Encode(polls)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error encoding response: %v", err)
|
|
||||||
common.SendError(resWriter, "Failed to encode polls", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetAllPolls() ([]models.Poll, error) {
|
|
||||||
db_conn, err := db.Connect()
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("%s", err.Error())
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
get_polls_stmt, err := db_conn.Prepare(`
|
|
||||||
SELECT
|
|
||||||
id, question,
|
|
||||||
member_yes_votes, member_no_votes,
|
|
||||||
non_member_yes_votes, non_member_no_votes,
|
|
||||||
created_at, updated_at,
|
|
||||||
expires_at
|
|
||||||
FROM polls
|
|
||||||
`)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("%s", err.Error())
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer get_polls_stmt.Close()
|
|
||||||
|
|
||||||
rows, err := get_polls_stmt.Query()
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("%s", err.Error())
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
var polls []models.Poll
|
|
||||||
for rows.Next() {
|
|
||||||
new_poll := models.Poll{}
|
|
||||||
err = rows.Scan(
|
|
||||||
&new_poll.ID, &new_poll.Question,
|
|
||||||
&new_poll.MemberYes, &new_poll.MemberNo,
|
|
||||||
&new_poll.NonMemberYes, &new_poll.NonMemberNo,
|
|
||||||
&new_poll.CreatedAt, &new_poll.UpdatedAt,
|
|
||||||
&new_poll.ExpiresAt,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("%s", err.Error())
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
polls = append(polls, new_poll)
|
new_poll.ID, err = res.LastInsertId()
|
||||||
}
|
|
||||||
|
return &new_poll, err
|
||||||
return polls, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetPollByQuestion(question string) (*models.Poll, error) {
|
|
||||||
db_conn, err := db.Connect()
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("%s", err.Error())
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
get_poll_stmt, err := db_conn.Prepare(`
|
|
||||||
SELECT
|
|
||||||
id, question,
|
|
||||||
member_yes_votes, member_no_votes,
|
|
||||||
non_member_yes_votes, non_member_no_votes,
|
|
||||||
created_at, updated_at,
|
|
||||||
expires_at
|
|
||||||
FROM polls
|
|
||||||
WHERE question == $1
|
|
||||||
`)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("%s", err.Error())
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer get_poll_stmt.Close()
|
|
||||||
|
|
||||||
new_poll := models.Poll{}
|
|
||||||
err = get_poll_stmt.QueryRow(question).Scan(
|
|
||||||
&new_poll.ID, &new_poll.Question,
|
|
||||||
&new_poll.MemberYes, &new_poll.MemberNo,
|
|
||||||
&new_poll.NonMemberYes, &new_poll.NonMemberNo,
|
|
||||||
&new_poll.CreatedAt, &new_poll.UpdatedAt,
|
|
||||||
&new_poll.ExpiresAt,
|
|
||||||
)
|
|
||||||
|
|
||||||
if err == sql.ErrNoRows {
|
|
||||||
return nil, ErrPollNotFound
|
|
||||||
} else if err != nil {
|
|
||||||
log.Printf("%s", err.Error())
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
get_voters_stmt, err := db_conn.Prepare(`
|
|
||||||
SELECT voter_email
|
|
||||||
FROM voters
|
|
||||||
WHERE poll_id == $1
|
|
||||||
`)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer get_voters_stmt.Close()
|
|
||||||
|
|
||||||
rows, err := get_voters_stmt.Query(new_poll.ID)
|
|
||||||
for rows.Next() {
|
|
||||||
var voter_email string
|
|
||||||
err = rows.Scan(&voter_email)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("%s", err.Error())
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
new_poll.WhoVoted = append(new_poll.WhoVoted, voter_email)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &new_poll, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetPollById(id int64) (*models.Poll, error) {
|
|
||||||
db_conn, err := db.Connect()
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("%s", err.Error())
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
get_poll_stmt, err := db_conn.Prepare(`
|
|
||||||
SELECT
|
|
||||||
id, question,
|
|
||||||
member_yes_votes, member_no_votes,
|
|
||||||
non_member_yes_votes, non_member_no_votes,
|
|
||||||
created_at, updated_at,
|
|
||||||
expires_at
|
|
||||||
FROM polls
|
|
||||||
WHERE id == $1
|
|
||||||
`)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("%s", err.Error())
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer get_poll_stmt.Close()
|
|
||||||
|
|
||||||
new_poll := models.Poll{}
|
|
||||||
err = get_poll_stmt.QueryRow(id).Scan(
|
|
||||||
&new_poll.ID, &new_poll.Question,
|
|
||||||
&new_poll.MemberYes, &new_poll.MemberNo,
|
|
||||||
&new_poll.NonMemberYes, &new_poll.NonMemberNo,
|
|
||||||
&new_poll.CreatedAt, &new_poll.UpdatedAt,
|
|
||||||
&new_poll.ExpiresAt,
|
|
||||||
)
|
|
||||||
|
|
||||||
if err == sql.ErrNoRows {
|
|
||||||
return nil, ErrPollNotFound
|
|
||||||
} else if err != nil {
|
|
||||||
log.Printf("%s", err.Error())
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &new_poll, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetAndCreatePollByQuestion(question string) (*models.Poll, error) {
|
|
||||||
new_poll, err := GetPollByQuestion(question)
|
|
||||||
|
|
||||||
if err == ErrPollNotFound {
|
|
||||||
create_poll := &models.Poll{
|
|
||||||
Question: question,
|
|
||||||
ExpiresAt: time.Now().Add(time.Hour * 10).Format(common.DATE_FORMAT),
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err = CreatePoll(create_poll); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return GetPollByQuestion(question)
|
|
||||||
} else if err != nil {
|
|
||||||
log.Printf("%s", err.Error())
|
|
||||||
return nil, err
|
|
||||||
} else {
|
|
||||||
return new_poll, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func SetVote(vote *models.Vote) error {
|
|
||||||
db_conn, err := db.Connect()
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("%s", err.Error())
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
set_voter_stmt, err := db_conn.Prepare(`
|
|
||||||
INSERT OR IGNORE INTO voters
|
|
||||||
(poll_id, voter_email)
|
|
||||||
VALUES ($1, $2)
|
|
||||||
`)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("%s", err.Error())
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer set_voter_stmt.Close()
|
|
||||||
|
|
||||||
res, err := set_voter_stmt.Exec(vote.PollId, vote.Email)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("%s", err.Error())
|
|
||||||
return err
|
|
||||||
} else {
|
|
||||||
rows_changed, err := res.RowsAffected()
|
|
||||||
if rows_changed != 1 {
|
|
||||||
return ErrVoterAlreadyVoted
|
|
||||||
} else if err != nil {
|
|
||||||
log.Printf("%s", err.Error())
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is_voter_member_stmt, err := db_conn.Prepare(`
|
|
||||||
SELECT 1
|
|
||||||
FROM members
|
|
||||||
WHERE email == $1
|
|
||||||
`)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("%s", err.Error())
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer is_voter_member_stmt.Close()
|
|
||||||
|
|
||||||
var member_check int64
|
|
||||||
is_member := true
|
|
||||||
err = is_voter_member_stmt.QueryRow(vote.Email).Scan(&member_check)
|
|
||||||
if err == sql.ErrNoRows {
|
|
||||||
is_member = false
|
|
||||||
} else if err != nil {
|
|
||||||
log.Printf("%s", err.Error())
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Member column name is not dependant on user input
|
|
||||||
// So it's ok to put it directly in the query
|
|
||||||
member_column_name := "member_"
|
|
||||||
if !is_member {
|
|
||||||
member_column_name = "non_" + member_column_name
|
|
||||||
}
|
|
||||||
|
|
||||||
if vote.Vote {
|
|
||||||
member_column_name += "yes_votes"
|
|
||||||
} else {
|
|
||||||
member_column_name += "no_votes"
|
|
||||||
}
|
|
||||||
|
|
||||||
add_vote_stmt, err := db_conn.Prepare(`
|
|
||||||
UPDATE polls
|
|
||||||
SET ` + member_column_name + ` = ` + member_column_name + ` + 1
|
|
||||||
WHERE id == $1
|
|
||||||
`)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("%s", err.Error())
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer add_vote_stmt.Close()
|
|
||||||
|
|
||||||
res, err = add_vote_stmt.Exec(vote.PollId)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("%s", err.Error())
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if num, err := res.RowsAffected(); num != 1 {
|
|
||||||
return ErrFailedToUpdateVote
|
|
||||||
} else if err != nil {
|
|
||||||
log.Printf("%s", err.Error())
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete a poll by name
|
|
||||||
func DeletePollByQuestion(question string) error {
|
|
||||||
db_conn, err := db.Connect()
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("%s", err.Error())
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
delete_votes_stmt, err := db_conn.Prepare(`
|
|
||||||
DELETE FROM voters
|
|
||||||
WHERE poll_id IN (
|
|
||||||
SELECT id
|
|
||||||
FROM polls
|
|
||||||
WHERE question == $1
|
|
||||||
)
|
|
||||||
`)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("%s", err.Error())
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer delete_votes_stmt.Close()
|
|
||||||
|
|
||||||
_, err = delete_votes_stmt.Exec(question)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("%s", err.Error())
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
delete_poll_stmt, err := db_conn.Prepare(`
|
|
||||||
DELETE FROM polls
|
|
||||||
WHERE question == $1
|
|
||||||
`)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("%s", err.Error())
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer delete_poll_stmt.Close()
|
|
||||||
|
|
||||||
res, err := delete_poll_stmt.Exec(question)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("%s", err.Error())
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if num, err := res.RowsAffected(); num != 1 {
|
|
||||||
return ErrFailedToDeletePoll
|
|
||||||
} else if err != nil {
|
|
||||||
log.Printf("%s", err.Error())
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func CreatePollIgnore(poll *models.Poll) error {
|
|
||||||
db_conn, err := db.Connect()
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("%s", err.Error())
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
stmt, err := db_conn.Prepare(`
|
|
||||||
INSERT OR IGNORE INTO polls (
|
|
||||||
question,
|
|
||||||
expires_at,
|
|
||||||
member_yes_votes,
|
|
||||||
member_no_votes,
|
|
||||||
non_member_yes_votes,
|
|
||||||
non_member_no_votes,
|
|
||||||
created_at,
|
|
||||||
updated_at
|
|
||||||
) VALUES (
|
|
||||||
$1,
|
|
||||||
$2,
|
|
||||||
$3,
|
|
||||||
$4,
|
|
||||||
$5,
|
|
||||||
$6,
|
|
||||||
$7,
|
|
||||||
$8
|
|
||||||
)
|
|
||||||
`)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("%s", err.Error())
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer stmt.Close()
|
|
||||||
|
|
||||||
_, err = stmt.Exec(
|
|
||||||
poll.Question,
|
|
||||||
poll.ExpiresAt,
|
|
||||||
poll.MemberYes,
|
|
||||||
poll.MemberNo,
|
|
||||||
poll.NonMemberYes,
|
|
||||||
poll.NonMemberNo,
|
|
||||||
poll.CreatedAt,
|
|
||||||
poll.UpdatedAt,
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("%s", err.Error())
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"math/rand"
|
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -11,190 +10,7 @@ import (
|
|||||||
"go-sjles-pta-vote/server/models"
|
"go-sjles-pta-vote/server/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-=`~!@#$%^&*()_+[]\\;',./{}|:\"<>?"
|
|
||||||
|
|
||||||
var new_members = []struct {
|
|
||||||
email string
|
|
||||||
member_name string
|
|
||||||
}{
|
|
||||||
{"test1@mail.me", "test1"},
|
|
||||||
{"test2@mail.me", "test2"},
|
|
||||||
{"test3@mail.me", "test3"},
|
|
||||||
{"test4@mail.me", "test4"},
|
|
||||||
{"test5@mail.me", "test5"},
|
|
||||||
{"test6@mail.me", "test6"},
|
|
||||||
{"test7@mail.me", "test7"},
|
|
||||||
{"test100@mail.me", "test100"},
|
|
||||||
{"test101@mail.me", "test101"},
|
|
||||||
{"test102@mail.me", "test102"},
|
|
||||||
{"test103@mail.me", "test103"},
|
|
||||||
{"test104@mail.me", "test104"},
|
|
||||||
{"test105@mail.me", "test105"},
|
|
||||||
}
|
|
||||||
|
|
||||||
var new_polls = []struct {
|
|
||||||
question string
|
|
||||||
member_yes_votes int64
|
|
||||||
member_no_votes int64
|
|
||||||
non_member_yes_votes int64
|
|
||||||
non_member_no_votes int64
|
|
||||||
}{
|
|
||||||
{"ques1", 1, 2, 3, 4},
|
|
||||||
{"ques2", 3, 2, 4, 5},
|
|
||||||
{"ques3", 4, 3, 6, 5},
|
|
||||||
}
|
|
||||||
|
|
||||||
var new_voters = []struct {
|
|
||||||
poll_id int64
|
|
||||||
voter_email string
|
|
||||||
}{
|
|
||||||
{1, "test1@mail.me"},
|
|
||||||
{1, "test2@mail.me"},
|
|
||||||
{1, "test3@mail.me"},
|
|
||||||
{1, "test10@mail.me"},
|
|
||||||
{1, "test11@mail.me"},
|
|
||||||
{1, "test12@mail.me"},
|
|
||||||
{1, "test13@mail.me"},
|
|
||||||
{1, "test14@mail.me"},
|
|
||||||
{1, "test15@mail.me"},
|
|
||||||
{1, "test16@mail.me"},
|
|
||||||
{2, "test1@mail.me"},
|
|
||||||
{2, "test2@mail.me"},
|
|
||||||
{2, "test3@mail.me"},
|
|
||||||
{2, "test4@mail.me"},
|
|
||||||
{2, "test5@mail.me"},
|
|
||||||
{2, "test10@mail.me"},
|
|
||||||
{2, "test11@mail.me"},
|
|
||||||
{2, "test12@mail.me"},
|
|
||||||
{2, "test13@mail.me"},
|
|
||||||
{2, "test14@mail.me"},
|
|
||||||
{2, "test15@mail.me"},
|
|
||||||
{2, "test16@mail.me"},
|
|
||||||
{2, "test17@mail.me"},
|
|
||||||
{2, "test18@mail.me"},
|
|
||||||
{3, "test1@mail.me"},
|
|
||||||
{3, "test2@mail.me"},
|
|
||||||
{3, "test3@mail.me"},
|
|
||||||
{3, "test4@mail.me"},
|
|
||||||
{3, "test5@mail.me"},
|
|
||||||
{3, "test6@mail.me"},
|
|
||||||
{3, "test7@mail.me"},
|
|
||||||
{3, "test10@mail.me"},
|
|
||||||
{3, "test11@mail.me"},
|
|
||||||
{3, "test12@mail.me"},
|
|
||||||
{3, "test13@mail.me"},
|
|
||||||
{3, "test14@mail.me"},
|
|
||||||
{3, "test15@mail.me"},
|
|
||||||
{3, "test16@mail.me"},
|
|
||||||
{3, "test17@mail.me"},
|
|
||||||
{3, "test18@mail.me"},
|
|
||||||
{3, "test19@mail.me"},
|
|
||||||
{3, "test20@mail.me"},
|
|
||||||
}
|
|
||||||
|
|
||||||
func RandString(length int) string {
|
|
||||||
rand_bytes := make([]byte, length)
|
|
||||||
for rand_index := range length {
|
|
||||||
rand_bytes[rand_index] = charset[rand.Intn(len(charset))]
|
|
||||||
}
|
|
||||||
return string(rand_bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
func PreLoadDB() error {
|
|
||||||
db_conn, err := db.Connect()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
// Insert members
|
|
||||||
for i := range new_members {
|
|
||||||
_, err := db_conn.Exec(`INSERT INTO members (email, member_name, school_year) VALUES (?, ?, ?)`, new_members[i].email, new_members[i].member_name, 2023)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert polls
|
|
||||||
for i := range new_polls {
|
|
||||||
result, err := db_conn.Exec(`INSERT INTO polls (question, member_yes_votes, member_no_votes, non_member_yes_votes, non_member_no_votes, expires_at) VALUES (?, ?, ?, ?, ?, ?)`, new_polls[i].question, new_polls[i].member_yes_votes, new_polls[i].member_no_votes, new_polls[i].non_member_yes_votes, new_polls[i].non_member_no_votes, time.Now().Add(time.Hour*10).Format("2006-01-02 15:04:05"))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err = result.LastInsertId()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert voters
|
|
||||||
for i := range new_voters {
|
|
||||||
_, err := db_conn.Exec(`INSERT INTO voters (poll_id, voter_email) VALUES (?, ?)`, new_voters[i].poll_id, new_voters[i].voter_email)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCreatePoll(t *testing.T) {
|
func TestCreatePoll(t *testing.T) {
|
||||||
parameters := []struct {
|
|
||||||
question string
|
|
||||||
table_index int64
|
|
||||||
}{
|
|
||||||
{RandString(10) + "1", 1},
|
|
||||||
{RandString(10) + "2", 2},
|
|
||||||
{RandString(10) + "3", 3},
|
|
||||||
{"\"" + RandString(10) + "4", 4},
|
|
||||||
{"\\\"" + RandString(10) + "5", 5},
|
|
||||||
{"'" + RandString(10) + "6", 6},
|
|
||||||
{";" + RandString(10) + "7", 7},
|
|
||||||
{"\\" + RandString(10) + "8", 8},
|
|
||||||
}
|
|
||||||
|
|
||||||
tmp_db, err := os.CreateTemp("", "vote_test.*.db")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf(`Failed to create temporary db file: %v`, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
init_conf := &config.Config{
|
|
||||||
DBPath: string(tmp_db.Name()),
|
|
||||||
}
|
|
||||||
config.SetConfig(init_conf)
|
|
||||||
|
|
||||||
defer os.Remove(tmp_db.Name())
|
|
||||||
tmp_db.Close()
|
|
||||||
|
|
||||||
if _, err := db.Connect(); err != nil {
|
|
||||||
t.Errorf(`Failed to create the database: %v`, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := range parameters {
|
|
||||||
create_poll := &models.Poll{
|
|
||||||
Question: parameters[i].question,
|
|
||||||
ExpiresAt: time.Now().Add(time.Hour * 10).Format("2006-01-02 15:04:05"),
|
|
||||||
}
|
|
||||||
|
|
||||||
new_poll_id, err := CreatePoll(create_poll)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf(`Failed to create new poll %s: %v`, parameters[i].question, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if new_poll_id == -1 {
|
|
||||||
t.Errorf(`Failed to insert %s into table`, parameters[i].question)
|
|
||||||
}
|
|
||||||
|
|
||||||
if new_poll_id != parameters[i].table_index {
|
|
||||||
t.Errorf(`Incorrect increment in index for %s: expected %d != %d`, parameters[i].question, parameters[i].table_index, new_poll_id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAlreadyExists(t *testing.T) {
|
|
||||||
question := "TestQuestion"
|
|
||||||
|
|
||||||
tmp_db, err := os.CreateTemp("", "vote_test.*.db")
|
tmp_db, err := os.CreateTemp("", "vote_test.*.db")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf(`Failed to create temporary db file: %v`, err)
|
t.Errorf(`Failed to create temporary db file: %v`, err)
|
||||||
@@ -213,285 +29,17 @@ func TestAlreadyExists(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
create_poll := &models.Poll{
|
create_poll := &models.Poll{
|
||||||
Question: question,
|
Question: "TestQuestion",
|
||||||
ExpiresAt: time.Now().Add(time.Hour * 10).Format("2006-01-02 15:04:05"),
|
ExpiresAt: time.Now().Add(time.Hour * 10).Format("2006-01-02 15:04:05"),
|
||||||
}
|
}
|
||||||
|
|
||||||
new_poll, err := CreatePoll(create_poll)
|
new_poll, err := CreatePoll(create_poll)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf(`Failed to create new poll %s: %v`, question, err)
|
t.Errorf(`Failed to insert into table: %v`, err)
|
||||||
}
|
|
||||||
|
|
||||||
if new_poll == -1 {
|
|
||||||
t.Errorf(`Failed to insert %s into table`, question)
|
|
||||||
}
|
|
||||||
|
|
||||||
new_poll, err = CreatePoll(create_poll)
|
|
||||||
|
|
||||||
if err != ErrQuestionAlreadyExists {
|
|
||||||
t.Errorf(`Should have failed adding %s as it already exists`, question)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetPollByQuestion(t *testing.T) {
|
|
||||||
question := "TestQuestion"
|
|
||||||
|
|
||||||
tmp_db, err := os.CreateTemp("", "vote_test.*.db")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf(`Failed to create temporary db file: %v`, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
init_conf := &config.Config{
|
|
||||||
DBPath: string(tmp_db.Name()),
|
|
||||||
}
|
|
||||||
config.SetConfig(init_conf)
|
|
||||||
|
|
||||||
defer os.Remove(tmp_db.Name())
|
|
||||||
tmp_db.Close()
|
|
||||||
|
|
||||||
if _, err := db.Connect(); err != nil {
|
|
||||||
t.Errorf(`Failed to create the database: %v`, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
create_poll := &models.Poll{
|
|
||||||
Question: question,
|
|
||||||
ExpiresAt: time.Now().Add(time.Hour * 10).Format("2006-01-02 15:04:05"),
|
|
||||||
}
|
|
||||||
|
|
||||||
new_poll, err := CreatePoll(create_poll)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf(`Failed to create new poll %s: %v`, question, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if new_poll == -1 {
|
|
||||||
t.Errorf(`Failed to insert %s into table`, question)
|
|
||||||
}
|
|
||||||
|
|
||||||
get_poll, err := GetPollByQuestion(question)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf(`Failed to get the poll %s: %v`, question, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if get_poll.Question != question {
|
|
||||||
t.Errorf(`Questions don't match: expected %s: recieved %s`, question, get_poll.Question)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetCreatePollByQuestion(t *testing.T) {
|
|
||||||
parameters := []struct {
|
|
||||||
question string
|
|
||||||
table_index int64
|
|
||||||
}{
|
|
||||||
{RandString(10) + "1", 1},
|
|
||||||
{RandString(10) + "2", 2},
|
|
||||||
{RandString(10) + "3", 3},
|
|
||||||
{"\"" + RandString(10) + "4", 4},
|
|
||||||
{"'" + RandString(10) + "5", 5},
|
|
||||||
{";" + RandString(10) + "6", 6},
|
|
||||||
}
|
|
||||||
|
|
||||||
tmp_db, err := os.CreateTemp("", "vote_test.*.db")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf(`Failed to create temporary db file: %v`, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
init_conf := &config.Config{
|
|
||||||
DBPath: string(tmp_db.Name()),
|
|
||||||
}
|
|
||||||
config.SetConfig(init_conf)
|
|
||||||
|
|
||||||
defer os.Remove(tmp_db.Name())
|
|
||||||
tmp_db.Close()
|
|
||||||
|
|
||||||
if _, err := db.Connect(); err != nil {
|
|
||||||
t.Errorf(`Failed to create the database: %v`, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := range parameters {
|
|
||||||
new_poll, err := GetAndCreatePollByQuestion(parameters[i].question)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf(`Failed to create new poll %s: %v`, parameters[i].question, err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if new_poll == nil {
|
if new_poll == nil {
|
||||||
t.Errorf(`Failed to insert %s into table`, parameters[i].question)
|
t.Errorf(`Failed to insert into table`)
|
||||||
}
|
|
||||||
|
|
||||||
if new_poll.ID != parameters[i].table_index {
|
|
||||||
t.Errorf(`Incorrect increment in index for %s: expected %d != %d`, parameters[i].question, parameters[i].table_index, new_poll.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
if new_poll.Question != parameters[i].question {
|
|
||||||
t.Errorf(`Incorrect question returned: Expected %s != %s`, parameters[i].question, new_poll.Question)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSetVote(t *testing.T) {
|
|
||||||
// Preload the database with members, polls, and voters
|
|
||||||
tmp_db, err := os.CreateTemp("", "vote_test.*.db")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Failed to create temporary database: %v", err)
|
|
||||||
}
|
|
||||||
defer os.Remove(tmp_db.Name())
|
|
||||||
|
|
||||||
init_conf := &config.Config{
|
|
||||||
DBPath: string(tmp_db.Name()),
|
|
||||||
}
|
|
||||||
config.SetConfig(init_conf)
|
|
||||||
|
|
||||||
err = PreLoadDB()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Failed to preload database: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a non-member vote
|
|
||||||
random_email := RandString(10) + "@mail.me"
|
|
||||||
vote := &models.Vote{
|
|
||||||
PollId: 1,
|
|
||||||
Email: random_email,
|
|
||||||
Vote: true,
|
|
||||||
}
|
|
||||||
err = SetVote(vote)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Failed to set non-member vote: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a member vote
|
|
||||||
member_email := "test100@mail.me"
|
|
||||||
vote = &models.Vote{
|
|
||||||
PollId: 1,
|
|
||||||
Email: member_email,
|
|
||||||
Vote: true,
|
|
||||||
}
|
|
||||||
err = SetVote(vote)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Failed to set member vote: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify the votes were added correctly
|
|
||||||
voters, err := models.GetVoters(1) // Use GetVoters from models
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Failed to get voters: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
expected_non_member_votes := 4 + 1 // Original non-member votes + new non-member vote
|
|
||||||
expected_member_votes := 3 + 1 // Original member votes + new member vote
|
|
||||||
|
|
||||||
for _, voter := range voters {
|
|
||||||
if voter.Email == random_email && voter.YesVote {
|
|
||||||
expected_non_member_votes--
|
|
||||||
} else if voter.Email == member_email && voter.YesVote {
|
|
||||||
expected_member_votes--
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if expected_non_member_votes != 5 || expected_member_votes != 4 {
|
|
||||||
t.Errorf("Expected %d non-member votes and %d member votes, but got %d non-member votes and %d member votes", 4+1, 3+1, expected_non_member_votes, expected_member_votes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestVoterAlreadyVoted(t *testing.T) {
|
|
||||||
// Preload the database with members, polls, and voters
|
|
||||||
tmp_db, err := os.CreateTemp("", "vote_test.*.db")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Failed to create temporary database: %v", err)
|
|
||||||
}
|
|
||||||
defer os.Remove(tmp_db.Name())
|
|
||||||
|
|
||||||
init_conf := &config.Config{
|
|
||||||
DBPath: string(tmp_db.Name()),
|
|
||||||
}
|
|
||||||
config.SetConfig(init_conf)
|
|
||||||
|
|
||||||
err = PreLoadDB()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Failed to preload database: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a non-member vote
|
|
||||||
random_email := RandString(10) + "@mail.me"
|
|
||||||
vote := &models.Vote{
|
|
||||||
PollId: 1,
|
|
||||||
Email: random_email,
|
|
||||||
Vote: true,
|
|
||||||
}
|
|
||||||
err = SetVote(vote)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Failed to set non-member vote: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a member vote
|
|
||||||
member_email := "test100@mail.me"
|
|
||||||
vote = &models.Vote{
|
|
||||||
PollId: 1,
|
|
||||||
Email: member_email,
|
|
||||||
Vote: true,
|
|
||||||
}
|
|
||||||
err = SetVote(vote)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Failed to set member vote: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attempt to add another non-member vote
|
|
||||||
vote = &models.Vote{
|
|
||||||
PollId: 1,
|
|
||||||
Email: random_email,
|
|
||||||
Vote: true,
|
|
||||||
}
|
|
||||||
err = SetVote(vote)
|
|
||||||
if err != ErrVoterAlreadyVoted {
|
|
||||||
t.Errorf("Expected ErrVoterAlreadyVoted, but got %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attempt to add another member vote
|
|
||||||
vote = &models.Vote{
|
|
||||||
PollId: 1,
|
|
||||||
Email: member_email,
|
|
||||||
Vote: true,
|
|
||||||
}
|
|
||||||
err = SetVote(vote)
|
|
||||||
if err != ErrVoterAlreadyVoted {
|
|
||||||
t.Errorf("Expected ErrVoterAlreadyVoted, but got %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDeletePollByQuestion(t *testing.T) {
|
|
||||||
// Preload the database with members, polls, and voters
|
|
||||||
tmp_db, err := os.CreateTemp("", "vote_test.*.db")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Failed to create temporary database: %v", err)
|
|
||||||
}
|
|
||||||
defer os.Remove(tmp_db.Name())
|
|
||||||
|
|
||||||
init_conf := &config.Config{
|
|
||||||
DBPath: string(tmp_db.Name()),
|
|
||||||
}
|
|
||||||
config.SetConfig(init_conf)
|
|
||||||
|
|
||||||
err = PreLoadDB()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Failed to preload database: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get a question from the new_polls array
|
|
||||||
testQuestion := new_polls[0].question
|
|
||||||
|
|
||||||
// Delete the poll by question
|
|
||||||
err = DeletePollByQuestion(testQuestion)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Failed to delete poll by question: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify that the poll was deleted
|
|
||||||
_, err = GetPollByQuestion(testQuestion)
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("Expected error when getting deleted poll, but got none")
|
|
||||||
} else if err != ErrPollNotFound {
|
|
||||||
t.Errorf("Expected ErrPollNotFound, but got %v", err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||