Autosave: 20260215-213226
This commit is contained in:
parent
b710d6acf5
commit
516273c8b4
BIN
assets/pasted-20260215-205745-40292c52.png
Normal file
BIN
assets/pasted-20260215-205745-40292c52.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 83 KiB |
318
ballot.php
318
ballot.php
@ -30,121 +30,273 @@ if ($check->fetchColumn() > 0) {
|
|||||||
$positions = $pdo->prepare("SELECT * FROM positions WHERE election_id = ? ORDER BY sort_order ASC");
|
$positions = $pdo->prepare("SELECT * FROM positions WHERE election_id = ? ORDER BY sort_order ASC");
|
||||||
$positions->execute([$id]);
|
$positions->execute([$id]);
|
||||||
$positions = $positions->fetchAll();
|
$positions = $positions->fetchAll();
|
||||||
|
|
||||||
|
$endTime = strtotime($election['end_date_and_time']) * 1000;
|
||||||
?>
|
?>
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<title>Vote: <?= htmlspecialchars($election['title']) ?></title>
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
<title>Ballot: <?= htmlspecialchars($election['title']) ?></title>
|
||||||
<link rel="stylesheet" href="assets/css/dashboard.css?v=<?= time() ?>">
|
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||||
<script src="https://unpkg.com/lucide@latest"></script>
|
<script src="https://unpkg.com/lucide@latest"></script>
|
||||||
<style>
|
<style>
|
||||||
body { background: #f8fafc; color: #1e293b; font-family: 'Inter', sans-serif; }
|
:root {
|
||||||
.ballot-container { max-width: 800px; margin: 40px auto; padding: 0 20px; }
|
--primary: #6366f1;
|
||||||
.ballot-header { text-align: center; margin-bottom: 48px; }
|
--primary-hover: #4f46e5;
|
||||||
.ballot-header h1 { font-size: 2.5rem; font-weight: 800; color: #1e293b; margin-bottom: 12px; letter-spacing: -0.025em; }
|
--bg: #f8fafc;
|
||||||
.ballot-header p { color: #64748b; font-size: 1.125rem; }
|
--text: #0f172a;
|
||||||
|
--text-muted: #64748b;
|
||||||
.position-group { margin-bottom: 40px; background: white; border-radius: 24px; border: 1px solid #e2e8f0; padding: 32px; box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1); }
|
--glass: rgba(255, 255, 255, 0.7);
|
||||||
.position-title { font-size: 1.25rem; font-weight: 700; color: #1e293b; margin-bottom: 24px; display: flex; align-items: center; gap: 12px; }
|
--glass-border: rgba(255, 255, 255, 0.5);
|
||||||
.position-title i { color: #4f46e5; }
|
}
|
||||||
|
|
||||||
.candidates-grid { display: grid; grid-template-columns: 1fr; gap: 12px; }
|
body {
|
||||||
.candidate-label { cursor: pointer; display: block; }
|
background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
|
||||||
|
color: var(--text);
|
||||||
|
font-family: 'Plus Jakarta Sans', sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ballot-container {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 40px 20px 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timer-banner {
|
||||||
|
background: var(--text);
|
||||||
|
color: white;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 100px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
box-shadow: 0 10px 25px -5px rgba(15, 23, 42, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#countdown {
|
||||||
|
color: #fbbf24;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ballot-header {
|
||||||
|
margin-bottom: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ballot-header h1 {
|
||||||
|
font-size: 3rem;
|
||||||
|
font-weight: 800;
|
||||||
|
color: var(--text);
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
letter-spacing: -0.05em;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ballot-header p {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 1.25rem;
|
||||||
|
max-width: 500px;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.position-group {
|
||||||
|
margin-bottom: 48px;
|
||||||
|
background: var(--glass);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
-webkit-backdrop-filter: blur(12px);
|
||||||
|
border-radius: 32px;
|
||||||
|
border: 1px solid var(--glass-border);
|
||||||
|
padding: 40px;
|
||||||
|
box-shadow: 0 20px 40px -15px rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.position-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 800;
|
||||||
|
color: var(--text);
|
||||||
|
margin-bottom: 32px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.position-title i {
|
||||||
|
padding: 10px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 14px;
|
||||||
|
color: var(--primary);
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.candidates-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.candidate-label {
|
||||||
|
cursor: pointer;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
.candidate-card {
|
.candidate-card {
|
||||||
border: 2px solid #e2e8f0;
|
border: 2px solid transparent;
|
||||||
border-radius: 16px;
|
border-radius: 24px;
|
||||||
padding: 20px;
|
padding: 24px;
|
||||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 16px;
|
gap: 20px;
|
||||||
background: white;
|
background: white;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
.candidate-card:hover { border-color: #cbd5e1; background: #f8fafc; }
|
|
||||||
|
.candidate-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 12px 20px -8px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
input[type="radio"]:checked + .candidate-card,
|
input[type="radio"]:checked + .candidate-card,
|
||||||
input[type="checkbox"]:checked + .candidate-card {
|
input[type="checkbox"]:checked + .candidate-card {
|
||||||
border-color: #4f46e5;
|
border-color: var(--primary);
|
||||||
background: #f5f3ff;
|
background: #f5f3ff;
|
||||||
box-shadow: 0 0 0 1px #4f46e5;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="radio"]:checked + .candidate-card .check-icon,
|
input[type="radio"]:checked + .candidate-card .check-icon,
|
||||||
input[type="checkbox"]:checked + .candidate-card .check-icon {
|
input[type="checkbox"]:checked + .candidate-card .check-icon {
|
||||||
background: #4f46e5;
|
background: var(--primary);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="radio"]:checked + .candidate-card .avatar-placeholder {
|
||||||
|
background: var(--primary);
|
||||||
color: white;
|
color: white;
|
||||||
border-color: #4f46e5;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="radio"], input[type="checkbox"] { display: none; }
|
input[type="radio"], input[type="checkbox"] { display: none; }
|
||||||
|
|
||||||
.avatar-placeholder {
|
.avatar-placeholder {
|
||||||
width: 56px;
|
width: 64px;
|
||||||
height: 56px;
|
height: 64px;
|
||||||
background: #f1f5f9;
|
background: #f1f5f9;
|
||||||
border-radius: 14px;
|
border-radius: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-weight: 700;
|
font-weight: 800;
|
||||||
color: #4f46e5;
|
color: var(--primary);
|
||||||
font-size: 1.25rem;
|
font-size: 1.5rem;
|
||||||
border: 1px solid #e2e8f0;
|
transition: all 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.candidate-info h3 { margin: 0; font-size: 1.125rem; font-weight: 700; color: #1e293b; }
|
.candidate-info h3 {
|
||||||
.candidate-info p { margin: 4px 0 0 0; font-size: 0.875rem; color: #64748b; font-weight: 500; }
|
margin: 0;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 800;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.candidate-info p {
|
||||||
|
margin: 4px 0 0 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
.check-icon {
|
.check-icon {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
width: 24px;
|
width: 28px;
|
||||||
height: 24px;
|
height: 28px;
|
||||||
border: 2px solid #e2e8f0;
|
border: 2px solid #e2e8f0;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
color: transparent;
|
color: transparent;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-bar {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 32px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: calc(100% - 40px);
|
||||||
|
max-width: 800px;
|
||||||
|
background: var(--text);
|
||||||
|
padding: 20px 40px;
|
||||||
|
border-radius: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 25px 50px -12px rgba(0,0,0,0.5);
|
||||||
|
z-index: 100;
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-bar p { margin: 0; font-size: 0.875rem; color: #94a3b8; }
|
||||||
|
.submit-bar h4 { margin: 4px 0 0 0; font-size: 1.125rem; font-weight: 700; }
|
||||||
|
|
||||||
|
.btn-submit {
|
||||||
|
background: var(--primary);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 14px 32px;
|
||||||
|
border-radius: 16px;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 800;
|
||||||
|
cursor: pointer;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.submit-section { margin-top: 64px; text-align: center; padding: 48px; background: #1e293b; border-radius: 24px; color: white; }
|
.btn-submit:hover {
|
||||||
.btn-submit {
|
background: var(--primary-hover);
|
||||||
background: #4f46e5;
|
transform: scale(1.05);
|
||||||
color: white;
|
}
|
||||||
border: none;
|
|
||||||
padding: 16px 48px;
|
@media (max-width: 640px) {
|
||||||
border-radius: 12px;
|
.submit-bar {
|
||||||
font-size: 1.125rem;
|
flex-direction: column;
|
||||||
font-weight: 700;
|
gap: 16px;
|
||||||
cursor: pointer;
|
text-align: center;
|
||||||
transition: all 0.2s;
|
padding: 24px;
|
||||||
box-shadow: 0 10px 15px -3px rgba(79, 70, 229, 0.4);
|
}
|
||||||
|
.btn-submit { width: 100%; }
|
||||||
|
.ballot-header h1 { font-size: 2.25rem; }
|
||||||
}
|
}
|
||||||
.btn-submit:hover { transform: translateY(-2px); background: #4338ca; }
|
|
||||||
.btn-submit:active { transform: translateY(0); }
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="ballot-container">
|
<div class="ballot-container">
|
||||||
<div class="ballot-header animate-fade-in">
|
<div style="text-align: center;">
|
||||||
<div style="display: inline-flex; align-items: center; gap: 8px; background: #e0e7ff; color: #4338ca; padding: 6px 16px; border-radius: 100px; font-size: 0.875rem; font-weight: 700; margin-bottom: 16px;">
|
<div class="timer-banner">
|
||||||
<i data-lucide="vote" style="width: 16px;"></i> OFFICIAL BALLOT
|
<i data-lucide="clock" style="width: 18px;"></i>
|
||||||
|
<span>TIME REMAINING:</span>
|
||||||
|
<span id="countdown">00:00:00</span>
|
||||||
</div>
|
</div>
|
||||||
<h1><?= htmlspecialchars($election['title']) ?></h1>
|
|
||||||
<p>Your vote is secure and anonymous. Choose your representatives below.</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form action="api/submit_vote.php" method="POST" onsubmit="return confirm('Are you sure you want to cast your vote? This action cannot be undone.')">
|
<div class="ballot-header">
|
||||||
|
<h1><?= htmlspecialchars($election['title']) ?></h1>
|
||||||
|
<p>Your choice matters. Review the candidates carefully and cast your secure vote below.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="ballotForm" action="api/submit_vote.php" method="POST">
|
||||||
<input type="hidden" name="election_id" value="<?= $id ?>">
|
<input type="hidden" name="election_id" value="<?= $id ?>">
|
||||||
|
|
||||||
<?php foreach ($positions as $index => $pos): ?>
|
<?php foreach ($positions as $index => $pos): ?>
|
||||||
<div class="position-group animate-stagger" style="--order: <?= $index ?>">
|
<div class="position-group">
|
||||||
<div class="position-title">
|
<div class="position-title">
|
||||||
<i data-lucide="award"></i>
|
<i data-lucide="shield-check"></i>
|
||||||
<?= htmlspecialchars($pos['name']) ?>
|
<?= htmlspecialchars($pos['name']) ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -163,8 +315,8 @@ $positions = $positions->fetchAll();
|
|||||||
?>
|
?>
|
||||||
|
|
||||||
<?php if (empty($candidates)): ?>
|
<?php if (empty($candidates)): ?>
|
||||||
<div style="padding: 24px; background: #f8fafc; border-radius: 12px; text-align: center; border: 1px dashed #cbd5e1;">
|
<div style="padding: 32px; background: white; border-radius: 20px; text-align: center; border: 2px dashed #e2e8f0;">
|
||||||
<p style="margin: 0; color: #64748b; font-size: 0.875rem;">No candidates for this position.</p>
|
<p style="margin: 0; color: var(--text-muted); font-weight: 600;">No candidates available for your track.</p>
|
||||||
</div>
|
</div>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<div class="candidates-grid">
|
<div class="candidates-grid">
|
||||||
@ -180,7 +332,7 @@ $positions = $positions->fetchAll();
|
|||||||
<p><?= htmlspecialchars($cand['party_name'] ?: 'Independent') ?></p>
|
<p><?= htmlspecialchars($cand['party_name'] ?: 'Independent') ?></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="check-icon">
|
<div class="check-icon">
|
||||||
<i data-lucide="check" style="width: 14px;"></i>
|
<i data-lucide="check" style="width: 16px;"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
@ -190,21 +342,51 @@ $positions = $positions->fetchAll();
|
|||||||
</div>
|
</div>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
|
|
||||||
<div class="submit-section animate-fade-in">
|
<div class="submit-bar">
|
||||||
<h2 style="margin: 0 0 12px 0; font-size: 1.5rem;">Ready to submit?</h2>
|
<div>
|
||||||
<p style="margin: 0 0 32px 0; color: #94a3b8; font-size: 1rem;">Please review your selections before casting your vote.</p>
|
<p>READY TO SUBMIT?</p>
|
||||||
|
<h4>Review your selections</h4>
|
||||||
|
</div>
|
||||||
<button type="submit" class="btn-submit">
|
<button type="submit" class="btn-submit">
|
||||||
Cast My Vote
|
Cast My Vote
|
||||||
</button>
|
</button>
|
||||||
<div style="margin-top: 24px; display: flex; align-items: center; justify-content: center; gap: 8px; color: #64748b; font-size: 0.875rem;">
|
|
||||||
<i data-lucide="shield-check" style="width: 16px; color: #10b981;"></i> Verified Secure Election
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
lucide.createIcons();
|
lucide.createIcons();
|
||||||
|
|
||||||
|
// Countdown Timer
|
||||||
|
const endTime = <?= $endTime ?>;
|
||||||
|
|
||||||
|
function updateCountdown() {
|
||||||
|
const now = new Date().getTime();
|
||||||
|
const distance = endTime - now;
|
||||||
|
|
||||||
|
if (distance < 0) {
|
||||||
|
document.getElementById("countdown").innerHTML = "EXPIRED";
|
||||||
|
document.getElementById("ballotForm").style.opacity = "0.5";
|
||||||
|
document.getElementById("ballotForm").style.pointerEvents = "none";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hours = Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
|
||||||
|
const minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60));
|
||||||
|
const seconds = Math.floor((distance % (1000 * 60)) / 1000);
|
||||||
|
|
||||||
|
document.getElementById("countdown").innerHTML =
|
||||||
|
(hours < 10 ? "0" : "") + hours + ":" +
|
||||||
|
(minutes < 10 ? "0" : "") + minutes + ":" +
|
||||||
|
(seconds < 10 ? "0" : "") + seconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
setInterval(updateCountdown, 1000);
|
||||||
|
updateCountdown();
|
||||||
|
|
||||||
|
document.getElementById('ballotForm').onsubmit = function() {
|
||||||
|
return confirm('Are you sure you want to cast your vote? This action is permanent.');
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|||||||
@ -6,16 +6,39 @@ define('DB_USER', 'app_38458');
|
|||||||
define('DB_PASS', 'c217529c-a428-4a97-8f31-773c420377a7');
|
define('DB_PASS', 'c217529c-a428-4a97-8f31-773c420377a7');
|
||||||
|
|
||||||
// Supabase Configuration - Provide your project URL and Service Role Key
|
// Supabase Configuration - Provide your project URL and Service Role Key
|
||||||
define('SUPABASE_URL', getenv('SUPABASE_URL') ?: 'https://your-project.supabase.co');
|
define('SUPABASE_URL', getenv('SUPABASE_URL') ?: 'https://siqeqnizegizxemrfgkf.supabase.co');
|
||||||
define('SUPABASE_SERVICE_ROLE_KEY', getenv('SUPABASE_SERVICE_ROLE_KEY') ?: 'your-service-role-key');
|
define('SUPABASE_SERVICE_ROLE_KEY', getenv('SUPABASE_SERVICE_ROLE_KEY') ?: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InNpcWVxbml6ZWdpenhlbXJmZ2tmIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc3MDEyMjgzMywiZXhwIjoyMDg1Njk4ODMzfQ.8K66Y3hSfSq5mRDeU8YT6pH9tqPVngxA9dEwCgQUCl0');
|
||||||
|
define('SUPABASE_DB_PASS', getenv('SUPABASE_DB_PASS') ?: 'gA82h8K80T5QUAwi'); // Set your DB password here for PostgreSQL migration
|
||||||
|
|
||||||
function db() {
|
function db() {
|
||||||
static $pdo;
|
static $pdo;
|
||||||
if (!$pdo) {
|
if (!$pdo) {
|
||||||
$pdo = new PDO('mysql:host='.DB_HOST.';dbname='.DB_NAME.';charset=utf8mb4', DB_USER, DB_PASS, [
|
if (defined('SUPABASE_DB_PASS') && !empty(SUPABASE_DB_PASS)) {
|
||||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
// Use Supabase PostgreSQL
|
||||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
$host = 'aws-1-ap-southeast-1.pooler.supabase.com';
|
||||||
]);
|
$port = '6543';
|
||||||
|
$dbname = 'postgres';
|
||||||
|
$user = 'postgres.siqeqnizegizxemrfgkf';
|
||||||
|
$pass = SUPABASE_DB_PASS;
|
||||||
|
try {
|
||||||
|
$pdo = new PDO("pgsql:host=$host;port=$port;dbname=$dbname", $user, $pass, [
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||||
|
]);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
// Fallback to local MariaDB if PostgreSQL fails
|
||||||
|
$pdo = new PDO('mysql:host='.DB_HOST.';dbname='.DB_NAME.';charset=utf8mb4', DB_USER, DB_PASS, [
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Use local MariaDB
|
||||||
|
$pdo = new PDO('mysql:host='.DB_HOST.';dbname='.DB_NAME.';charset=utf8mb4', DB_USER, DB_PASS, [
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return $pdo;
|
return $pdo;
|
||||||
}
|
}
|
||||||
|
|||||||
29
find_region.php
Normal file
29
find_region.php
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/db/config.php';
|
||||||
|
$regions = [
|
||||||
|
'us-east-1', 'us-east-2', 'us-west-1', 'us-west-2',
|
||||||
|
'ap-southeast-1', 'ap-southeast-2', 'ap-northeast-1', 'ap-northeast-2',
|
||||||
|
'eu-central-1', 'eu-west-1', 'eu-west-2', 'eu-west-3',
|
||||||
|
'sa-east-1', 'ca-central-1', 'me-south-1', 'af-south-1'
|
||||||
|
];
|
||||||
|
|
||||||
|
$pass = 'gA82h8K80T5QUAwi';
|
||||||
|
$projectRef = 'siqeqnizegizxemrfgkf';
|
||||||
|
|
||||||
|
foreach ($regions as $region) {
|
||||||
|
$host = "aws-0-$region.pooler.supabase.com";
|
||||||
|
$user = "postgres.$projectRef";
|
||||||
|
echo "Testing $region ($host)... ";
|
||||||
|
try {
|
||||||
|
$dsn = "pgsql:host=$host;port=6543;dbname=postgres;connect_timeout=5";
|
||||||
|
$pdo = new PDO($dsn, $user, $pass);
|
||||||
|
echo "SUCCESS!\n";
|
||||||
|
exit(0);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
if (strpos($e->getMessage(), 'Tenant or user not found') !== false) {
|
||||||
|
echo "Not here.\n";
|
||||||
|
} else {
|
||||||
|
echo "Error: " . $e->getMessage() . "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
36
index.php
36
index.php
@ -13,7 +13,41 @@ if (in_array($user['role'], ['Admin', 'Adviser', 'Officer'])) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$pdo = db();
|
$pdo = db();
|
||||||
$elections = $pdo->query("SELECT * FROM elections WHERE archived = FALSE ORDER BY created_at DESC")->fetchAll();
|
|
||||||
|
// Voter redirection logic
|
||||||
|
if ($user['role'] === 'Voter') {
|
||||||
|
// Find ongoing elections that this voter is assigned to
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT e.* FROM elections e
|
||||||
|
JOIN election_assignments ea ON e.id = ea.election_id
|
||||||
|
WHERE ea.user_id = ?
|
||||||
|
AND e.status = 'Ongoing'
|
||||||
|
AND e.archived = FALSE
|
||||||
|
AND e.end_date_and_time > CURRENT_TIMESTAMP
|
||||||
|
");
|
||||||
|
$stmt->execute([$user['id']]);
|
||||||
|
$activeElections = $stmt->fetchAll();
|
||||||
|
|
||||||
|
// Filter out elections where the user has already voted
|
||||||
|
$votedElectionsStmt = $pdo->prepare("SELECT election_id FROM votes WHERE voter_id = ?");
|
||||||
|
$votedElectionsStmt->execute([$user['id']]);
|
||||||
|
$votedIds = $votedElectionsStmt->fetchAll(PDO::FETCH_COLUMN);
|
||||||
|
|
||||||
|
$eligibleElections = array_filter($activeElections, function($e) use ($votedIds) {
|
||||||
|
return !in_array($e['id'], $votedIds);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (count($eligibleElections) === 1) {
|
||||||
|
$singleElection = reset($eligibleElections);
|
||||||
|
header("Location: ballot.php?id=" . $singleElection['id']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For voters, only show their assigned elections in the list
|
||||||
|
$elections = $activeElections;
|
||||||
|
} else {
|
||||||
|
$elections = $pdo->query("SELECT * FROM elections WHERE archived = FALSE ORDER BY created_at DESC")->fetchAll();
|
||||||
|
}
|
||||||
|
|
||||||
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Online Election System for Senior High School';
|
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Online Election System for Senior High School';
|
||||||
?>
|
?>
|
||||||
|
|||||||
163
supabase_migration.php
Normal file
163
supabase_migration.php
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/db/config.php';
|
||||||
|
|
||||||
|
// This script migrates the local MariaDB database to Supabase PostgreSQL.
|
||||||
|
// It requires the Supabase Database Password.
|
||||||
|
|
||||||
|
if (php_sapi_name() !== 'cli') {
|
||||||
|
die("This script must be run from the command line.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$dbPassword = $argv[1] ?? '';
|
||||||
|
if (!$dbPassword) {
|
||||||
|
echo "Usage: php supabase_migration.php [SUPABASE_DB_PASSWORD]\n";
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$supabaseHost = "aws-1-ap-southeast-1.pooler.supabase.com";
|
||||||
|
$supabaseUser = "postgres.siqeqnizegizxemrfgkf";
|
||||||
|
$supabaseDb = "postgres";
|
||||||
|
$supabasePort = "6543";
|
||||||
|
|
||||||
|
try {
|
||||||
|
echo "Connecting to local MariaDB...\n";
|
||||||
|
$localPdo = new PDO('mysql:host='.DB_HOST.';dbname='.DB_NAME.';charset=utf8mb4', DB_USER, DB_PASS, [
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||||
|
]);
|
||||||
|
|
||||||
|
echo "Connecting to Supabase PostgreSQL...\n";
|
||||||
|
$dsn = "pgsql:host=$supabaseHost;port=$supabasePort;dbname=$supabaseDb";
|
||||||
|
$supabasePdo = new PDO($dsn, $supabaseUser, $dbPassword, [
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
|
||||||
|
]);
|
||||||
|
|
||||||
|
echo "Converting and creating tables in Supabase...\n";
|
||||||
|
|
||||||
|
// Define tables and their PostgreSQL schemas
|
||||||
|
$schemas = [
|
||||||
|
"users" => "CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id VARCHAR(255) PRIMARY KEY,
|
||||||
|
supabase_uid VARCHAR(255),
|
||||||
|
student_id VARCHAR(10) UNIQUE NOT NULL,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
email VARCHAR(255) UNIQUE NOT NULL,
|
||||||
|
password_hash VARCHAR(255),
|
||||||
|
grade_level INTEGER,
|
||||||
|
track VARCHAR(100),
|
||||||
|
section VARCHAR(100),
|
||||||
|
role VARCHAR(50) DEFAULT 'Voter',
|
||||||
|
access_level INTEGER DEFAULT 0,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
deleted_at TIMESTAMP
|
||||||
|
)",
|
||||||
|
"elections" => "CREATE TABLE IF NOT EXISTS elections (
|
||||||
|
id VARCHAR(255) PRIMARY KEY,
|
||||||
|
title VARCHAR(255) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
status VARCHAR(50) DEFAULT 'Preparing',
|
||||||
|
start_date_and_time TIMESTAMP NOT NULL,
|
||||||
|
end_date_and_time TIMESTAMP NOT NULL,
|
||||||
|
created_by VARCHAR(255) REFERENCES users(id),
|
||||||
|
archived BOOLEAN DEFAULT FALSE,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)",
|
||||||
|
"positions" => "CREATE TABLE IF NOT EXISTS positions (
|
||||||
|
id VARCHAR(255) PRIMARY KEY,
|
||||||
|
election_id VARCHAR(255) REFERENCES elections(id) ON DELETE CASCADE,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
type VARCHAR(50) DEFAULT 'Uniform',
|
||||||
|
max_votes INTEGER DEFAULT 1,
|
||||||
|
sort_order INTEGER DEFAULT 0
|
||||||
|
)",
|
||||||
|
"candidates" => "CREATE TABLE IF NOT EXISTS candidates (
|
||||||
|
id VARCHAR(255) PRIMARY KEY,
|
||||||
|
election_id VARCHAR(255) REFERENCES elections(id) ON DELETE CASCADE,
|
||||||
|
position_id VARCHAR(255) REFERENCES positions(id) ON DELETE CASCADE,
|
||||||
|
user_id VARCHAR(255) REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
party_name VARCHAR(255),
|
||||||
|
manifesto TEXT,
|
||||||
|
approved BOOLEAN DEFAULT FALSE,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)",
|
||||||
|
"votes" => "CREATE TABLE IF NOT EXISTS votes (
|
||||||
|
id VARCHAR(255) PRIMARY KEY,
|
||||||
|
election_id VARCHAR(255) REFERENCES elections(id),
|
||||||
|
position_id VARCHAR(255) REFERENCES positions(id),
|
||||||
|
candidate_id VARCHAR(255) REFERENCES candidates(id),
|
||||||
|
voter_id VARCHAR(255) REFERENCES users(id),
|
||||||
|
casted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
ip_address VARCHAR(45),
|
||||||
|
user_agent TEXT,
|
||||||
|
UNIQUE (election_id, position_id, voter_id)
|
||||||
|
)",
|
||||||
|
"election_assignments" => "CREATE TABLE IF NOT EXISTS election_assignments (
|
||||||
|
id VARCHAR(255) PRIMARY KEY,
|
||||||
|
election_id VARCHAR(255) REFERENCES elections(id) ON DELETE CASCADE,
|
||||||
|
user_id VARCHAR(255) REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
role_in_election VARCHAR(50) DEFAULT 'Voter',
|
||||||
|
assigned_by VARCHAR(255) REFERENCES users(id),
|
||||||
|
assigned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)",
|
||||||
|
"parties" => "CREATE TABLE IF NOT EXISTS parties (
|
||||||
|
id VARCHAR(255) PRIMARY KEY,
|
||||||
|
election_id VARCHAR(255) REFERENCES elections(id) ON DELETE CASCADE,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
logo_url TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)",
|
||||||
|
"audit_logs" => "CREATE TABLE IF NOT EXISTS audit_logs (
|
||||||
|
id VARCHAR(255) PRIMARY KEY,
|
||||||
|
user_id VARCHAR(255) REFERENCES users(id),
|
||||||
|
action VARCHAR(255) NOT NULL,
|
||||||
|
details TEXT,
|
||||||
|
table_name VARCHAR(100),
|
||||||
|
record_id VARCHAR(255),
|
||||||
|
old_values TEXT,
|
||||||
|
new_values TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
election_id VARCHAR(255) REFERENCES elections(id) ON DELETE CASCADE
|
||||||
|
)"
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($schemas as $tableName => $sql) {
|
||||||
|
echo "Creating table: $tableName...\n";
|
||||||
|
$supabasePdo->exec($sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Migrating data...\n";
|
||||||
|
|
||||||
|
$tables = array_keys($schemas);
|
||||||
|
// Order matters for foreign keys: users, elections, positions, candidates, assignments, votes, audit_logs
|
||||||
|
$orderedTables = ["users", "elections", "positions", "election_assignments", "parties", "candidates", "votes", "audit_logs"];
|
||||||
|
|
||||||
|
foreach ($orderedTables as $table) {
|
||||||
|
echo "Migrating data for $table...\n";
|
||||||
|
$stmt = $localPdo->query("SELECT * FROM $table");
|
||||||
|
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (empty($rows)) {
|
||||||
|
echo "No data for $table.\n";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$columns = array_keys($rows[0]);
|
||||||
|
$placeholders = implode(',', array_fill(0, count($columns), '?'));
|
||||||
|
$insertSql = "INSERT INTO $table (" . implode(',', $columns) . ") VALUES ($placeholders) ON CONFLICT (id) DO NOTHING";
|
||||||
|
|
||||||
|
$insertStmt = $supabasePdo->prepare($insertSql);
|
||||||
|
$count = 0;
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$insertStmt->execute(array_values($row));
|
||||||
|
$count++;
|
||||||
|
}
|
||||||
|
echo "Migrated $count rows for $table.\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Migration completed successfully!\n";
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo "Error: " . $e->getMessage() . "\n";
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user