895 lines
30 KiB
HTML
895 lines
30 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>TourVote - Futuristic Tour Planning</title>
|
||
<style>
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
:root {
|
||
--bg-primary: #0a0a0f;
|
||
--bg-secondary: #1a1a2e;
|
||
--bg-card: rgba(255, 255, 255, 0.03);
|
||
--text-primary: #ffffff;
|
||
--text-secondary: #a0a0b8;
|
||
--accent-primary: #00d4ff;
|
||
--accent-secondary: #7b2ff7;
|
||
--accent-gradient: linear-gradient(135deg, #00d4ff, #7b2ff7);
|
||
--border-color: rgba(255, 255, 255, 0.1);
|
||
--glow: 0 0 20px rgba(0, 212, 255, 0.5);
|
||
}
|
||
|
||
body {
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||
background: var(--bg-primary);
|
||
color: var(--text-primary);
|
||
min-height: 100vh;
|
||
overflow-x: hidden;
|
||
position: relative;
|
||
}
|
||
|
||
/* Animated background */
|
||
body::before {
|
||
content: '';
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background:
|
||
radial-gradient(circle at 20% 50%, rgba(0, 212, 255, 0.1) 0%, transparent 50%),
|
||
radial-gradient(circle at 80% 80%, rgba(123, 47, 247, 0.1) 0%, transparent 50%),
|
||
radial-gradient(circle at 40% 20%, rgba(0, 212, 255, 0.05) 0%, transparent 50%);
|
||
pointer-events: none;
|
||
z-index: -1;
|
||
}
|
||
a {
|
||
color: var(--accent-primary);
|
||
text-decoration: underline;
|
||
}
|
||
|
||
.idea-description a,
|
||
.tour-info a {
|
||
/* ensure they stand out against the dark bg */
|
||
color: var(--accent-primary);
|
||
}
|
||
.container {
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
padding: 20px 20px 40px;
|
||
}
|
||
|
||
/* Header */
|
||
.header {
|
||
text-align: center;
|
||
padding: 60px 0 20px;
|
||
position: relative;
|
||
}
|
||
|
||
.logo {
|
||
font-size: 3rem;
|
||
font-weight: 900;
|
||
background: var(--accent-gradient);
|
||
-webkit-background-clip: text;
|
||
-webkit-text-fill-color: transparent;
|
||
background-clip: text;
|
||
text-transform: uppercase;
|
||
letter-spacing: 3px;
|
||
margin-bottom: 10px;
|
||
text-shadow: var(--glow);
|
||
}
|
||
|
||
.subtitle {
|
||
color: var(--text-secondary);
|
||
font-size: 1.1rem;
|
||
letter-spacing: 1px;
|
||
}
|
||
|
||
/* Glassmorphism cards */
|
||
.glass-card {
|
||
background: var(--bg-card);
|
||
backdrop-filter: blur(10px);
|
||
-webkit-backdrop-filter: blur(10px);
|
||
border: 1px solid var(--border-color);
|
||
border-radius: 20px;
|
||
padding: 30px;
|
||
max-width: 700px;
|
||
margin: 0 auto 30px;
|
||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.glass-card h2 {
|
||
margin-bottom: 20px; /* pushes the next form‐group down */
|
||
}
|
||
|
||
.glass-card h3 {
|
||
margin-bottom: 16px; /* pushes the next form‐group down */
|
||
}
|
||
|
||
.glass-card:hover {
|
||
transform: translateY(-5px);
|
||
box-shadow: 0 12px 40px rgba(0, 212, 255, 0.2);
|
||
border-color: rgba(0, 212, 255, 0.3);
|
||
}
|
||
|
||
.date-row {
|
||
display: flex;
|
||
gap: 24px;
|
||
}
|
||
|
||
.date-row .form-group {
|
||
width: calc((100% - 42px) / 2);
|
||
min-width: 0;
|
||
}
|
||
|
||
@media (max-width: 600px) {
|
||
.date-row {
|
||
flex-direction: column;
|
||
gap: 20px;
|
||
}
|
||
|
||
.date-row .form-group {
|
||
flex: 1;
|
||
}
|
||
}
|
||
|
||
/* Forms */
|
||
.form-group {
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
label {
|
||
display: block;
|
||
margin-bottom: 8px;
|
||
color: var(--text-secondary);
|
||
font-size: 0.9rem;
|
||
text-transform: uppercase;
|
||
letter-spacing: 1px;
|
||
}
|
||
|
||
input, textarea {
|
||
width: 100%;
|
||
padding: 15px;
|
||
background: rgba(255, 255, 255, 0.05);
|
||
border: 1px solid var(--border-color);
|
||
border-radius: 10px;
|
||
color: var(--text-primary);
|
||
font-size: 1rem;
|
||
transition: all 0.3s ease;
|
||
box-sizing: border-box; /* ← ensure padding is included in width */
|
||
}
|
||
|
||
input:focus, textarea:focus {
|
||
outline: none;
|
||
border-color: var(--accent-primary);
|
||
box-shadow: 0 0 0 3px rgba(0, 212, 255, 0.1);
|
||
background: rgba(255, 255, 255, 0.08);
|
||
}
|
||
|
||
textarea {
|
||
resize: vertical;
|
||
min-height: 100px;
|
||
}
|
||
|
||
/* Buttons */
|
||
.btn {
|
||
background: var(--accent-gradient);
|
||
color: white;
|
||
border: none;
|
||
padding: 15px 30px;
|
||
border-radius: 10px;
|
||
font-size: 1rem;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
text-transform: uppercase;
|
||
letter-spacing: 1px;
|
||
display: inline-block;
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.btn:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: var(--glow);
|
||
}
|
||
|
||
.btn:active {
|
||
transform: translateY(0);
|
||
}
|
||
|
||
.btn-secondary {
|
||
background: transparent;
|
||
border: 2px solid var(--accent-primary);
|
||
color: var(--accent-primary);
|
||
}
|
||
|
||
.btn-secondary:hover {
|
||
background: var(--accent-primary);
|
||
color: var(--bg-primary);
|
||
}
|
||
|
||
.btn-small {
|
||
padding: 8px 16px;
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.ideas-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||
gap: 20px;
|
||
margin-top: 30px;
|
||
}
|
||
|
||
.idea-card {
|
||
background: rgba(255, 255, 255, 0.02);
|
||
border: 1px solid var(--border-color);
|
||
border-radius: 15px;
|
||
padding: 20px;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.idea-card h3 {
|
||
margin-bottom: 20px; /* pushes the next form‐group down */
|
||
}
|
||
|
||
.idea-card:hover {
|
||
background: rgba(255, 255, 255, 0.05);
|
||
border-color: rgba(0, 212, 255, 0.3);
|
||
transform: translateY(-3px);
|
||
}
|
||
|
||
.idea-title {
|
||
font-size: 1.2rem;
|
||
font-weight: 700;
|
||
margin-bottom: 10px;
|
||
color: var(--accent-primary);
|
||
}
|
||
|
||
.idea-description {
|
||
color: var(--text-secondary);
|
||
margin-bottom: 15px;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.voters-list {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
margin-top: 10px;
|
||
}
|
||
|
||
.voter-chip {
|
||
background: rgba(0, 212, 255, 0.1);
|
||
border: 1px solid rgba(0, 212, 255, 0.3);
|
||
padding: 4px 12px;
|
||
border-radius: 20px;
|
||
font-size: 0.85rem;
|
||
color: var(--accent-primary);
|
||
}
|
||
|
||
.vote-section {
|
||
display: flex;
|
||
gap: 10px;
|
||
align-items: center;
|
||
margin-top: 15px;
|
||
}
|
||
|
||
.vote-count {
|
||
font-size: 1.1rem;
|
||
font-weight: 700;
|
||
color: var(--accent-primary);
|
||
}
|
||
|
||
/* Tour info */
|
||
.tour-info {
|
||
background: rgba(123, 47, 247, 0.1);
|
||
border: 1px solid rgba(123, 47, 247, 0.3);
|
||
border-radius: 15px;
|
||
padding: 20px;
|
||
margin-bottom: 30px;
|
||
}
|
||
|
||
.tour-title {
|
||
font-size: 1.8rem;
|
||
font-weight: 700;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.tour-date {
|
||
color: var(--accent-primary);
|
||
font-size: 1.1rem;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
/* Hidden class */
|
||
.hidden {
|
||
display: none !important;
|
||
}
|
||
|
||
/* Loading spinner */
|
||
.loader {
|
||
width: 50px;
|
||
height: 50px;
|
||
border: 3px solid var(--border-color);
|
||
border-top: 3px solid var(--accent-primary);
|
||
border-radius: 50%;
|
||
animation: spin 1s linear infinite;
|
||
margin: 50px auto;
|
||
}
|
||
|
||
@keyframes spin {
|
||
0% { transform: rotate(0deg); }
|
||
100% { transform: rotate(360deg); }
|
||
}
|
||
|
||
/* Responsive */
|
||
@media (max-width: 768px) {
|
||
.logo {
|
||
font-size: 2rem;
|
||
}
|
||
|
||
.container {
|
||
padding: 15px;
|
||
}
|
||
|
||
.glass-card {
|
||
padding: 20px;
|
||
}
|
||
|
||
.ideas-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
|
||
/* Matrix view */
|
||
.matrix-view {
|
||
overflow-x: auto;
|
||
margin-top: 30px;
|
||
}
|
||
|
||
.matrix-table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
min-width: 600px;
|
||
}
|
||
|
||
.matrix-table th, .matrix-table td {
|
||
padding: 12px;
|
||
text-align: left;
|
||
border: 1px solid var(--border-color);
|
||
}
|
||
|
||
.matrix-table th {
|
||
background: rgba(0, 212, 255, 0.1);
|
||
font-weight: 600;
|
||
text-transform: uppercase;
|
||
letter-spacing: 1px;
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.matrix-table td {
|
||
background: var(--bg-card);
|
||
}
|
||
|
||
.vote-cell {
|
||
text-align: center;
|
||
font-size: 1.2rem;
|
||
color: var(--accent-primary);
|
||
}
|
||
|
||
.toggle-view {
|
||
display: flex;
|
||
gap: 10px;
|
||
justify-content: center;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.date-badge {
|
||
background: var(--accent-gradient);
|
||
padding: 6px 12px;
|
||
border-radius: 12px;
|
||
display: inline-block;
|
||
font-size: 0.9rem;
|
||
font-weight: 600;
|
||
color: var(--bg-primary);
|
||
margin-right: 8px;
|
||
text-shadow: var(--glow);
|
||
letter-spacing: 0.5px;
|
||
}
|
||
.date-label {
|
||
font-size: 0.85rem;
|
||
color: var(--text-secondary);
|
||
margin-bottom: 4px;
|
||
display: block;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<header class="header">
|
||
<h1 class="logo"><a href=".">TourVote</a></h1>
|
||
<p class="subtitle">Collaborative Tour Planning Platform</p>
|
||
</header>
|
||
|
||
<div id="createTourSection" class="glass-card">
|
||
<h2>Create a New Tour</h2>
|
||
<form id="createTourForm">
|
||
<div class="form-group">
|
||
<label for="tourName">Tour Name</label>
|
||
<input type="text" id="tourName" required placeholder="Enter tour name">
|
||
</div>
|
||
<div class="date-row">
|
||
<div class="form-group">
|
||
<label class="date-label" for="tourStartDate">Start Date</label>
|
||
<input type="date" id="tourStartDate">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="date-label" for="tourEndDate">End Date</label>
|
||
<input type="date" id="tourEndDate">
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="tourDescription">Description</label>
|
||
<textarea id="tourDescription" required placeholder="Describe your tour"></textarea>
|
||
</div>
|
||
<button type="submit" class="btn">Create Tour</button>
|
||
</form>
|
||
</div>
|
||
|
||
<!-- Tour View Section -->
|
||
<div id="tourViewSection" class="hidden">
|
||
<!-- Tour Info -->
|
||
<div id="tourInfo" class="tour-info"></div>
|
||
|
||
<!-- Add Idea Section -->
|
||
<div class="glass-card">
|
||
<h3>Add New Idea</h3>
|
||
<form id="addIdeaForm">
|
||
<div class="form-group">
|
||
<label for="ideaName">Idea Name</label>
|
||
<input type="text" id="ideaName" required placeholder="Enter idea name">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="ideaDescription">Description</label>
|
||
<textarea id="ideaDescription" required placeholder="Describe your idea"></textarea>
|
||
</div>
|
||
<div class="date-row">
|
||
<div class="form-group">
|
||
<label class="date-label" for="ideaStartTime">Start Time</label>
|
||
<input type="datetime-local" id="ideaStartTime">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="date-label" for="ideaEndTime">End Time</label>
|
||
<input type="datetime-local" id="ideaEndTime">
|
||
</div>
|
||
</div>
|
||
<button type="submit" class="btn">Add Idea</button>
|
||
</form>
|
||
</div>
|
||
|
||
<!-- View Toggle -->
|
||
<div class="toggle-view">
|
||
<button class="btn btn-secondary btn-small" onclick="showView('cards')">Card View</button>
|
||
<button class="btn btn-secondary btn-small" onclick="showView('matrix')">Matrix View</button>
|
||
</div>
|
||
|
||
<!-- Ideas Display -->
|
||
<div id="ideasContainer">
|
||
<div id="cardsView" class="ideas-grid"></div>
|
||
<div id="matrixView" class="matrix-view hidden"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Loading -->
|
||
<div id="loader" class="loader hidden"></div>
|
||
</div>
|
||
|
||
<script>
|
||
// Mock API URL - Replace with your actual API endpoint
|
||
const API_URL = 'https://kaizenkodo.no/tour/v1';
|
||
|
||
// State management
|
||
let currentTour = null;
|
||
let currentView = 'cards';
|
||
|
||
// Initialize app
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
checkForTourId();
|
||
setupEventListeners();
|
||
});
|
||
|
||
// Check if tour ID is in URL
|
||
function checkForTourId() {
|
||
const tourId = window.location.hash.slice(1);
|
||
if (tourId) {
|
||
loadTour(tourId);
|
||
}
|
||
}
|
||
|
||
// Setup event listeners
|
||
function setupEventListeners() {
|
||
document.getElementById('createTourForm').addEventListener('submit', createTour);
|
||
document.getElementById('addIdeaForm').addEventListener('submit', addIdea);
|
||
|
||
// Listen for hash changes
|
||
window.addEventListener('hashchange', checkForTourId);
|
||
}
|
||
|
||
function linkify(text) {
|
||
if (!text) return '';
|
||
return text.replace(
|
||
/(\[([^\]]+)\]\((https?:\/\/[^\s)]+)\))|(https?:\/\/[^\s]+)/g,
|
||
(match, mdFull, mdLabel, mdUrl, plainUrl) => {
|
||
if (mdUrl) {
|
||
// matched [label](url)
|
||
return `<a href="${mdUrl}" target="_blank" rel="noopener">${mdLabel}</a>`;
|
||
}
|
||
// matched a bare URL
|
||
return `<a href="${plainUrl}" target="_blank" rel="noopener">${plainUrl}</a>`;
|
||
}
|
||
);
|
||
}
|
||
|
||
// Create new tour
|
||
async function createTour(e) {
|
||
e.preventDefault();
|
||
|
||
const startDate = document.getElementById('tourStartDate').value;
|
||
const endDate = document.getElementById('tourEndDate').value;
|
||
const payload = {
|
||
name: document.getElementById('tourName').value,
|
||
description: document.getElementById('tourDescription').value,
|
||
...(startDate ? { start_date: startDate } : {}),
|
||
...(endDate ? { end_date: endDate } : {})
|
||
};
|
||
|
||
showLoader();
|
||
try {
|
||
const response = await fetch(`${API_URL}/tours`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(payload)
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`Server responded with ${response.status}`);
|
||
}
|
||
|
||
// FastAPI returns the full Tour JSON (with id, createdAt, ideas=[])
|
||
const tourData = await response.json();
|
||
currentTour = tourData;
|
||
|
||
// Push the new tour’s ID into the URL hash so loadTour() can pick it up
|
||
window.location.hash = tourData.id;
|
||
|
||
showTourView();
|
||
} catch (error) {
|
||
console.error('Error creating tour:', error);
|
||
alert('Failed to create tour. Please try again.');
|
||
} finally {
|
||
hideLoader();
|
||
}
|
||
}
|
||
|
||
// Load existing tour
|
||
async function loadTour(tourId) {
|
||
showLoader();
|
||
try {
|
||
const response = await fetch(`${API_URL}/tours/${tourId}`);
|
||
if (response.status === 404) {
|
||
throw new Error('Not Found');
|
||
}
|
||
if (!response.ok) {
|
||
throw new Error(`Server responded with ${response.status}`);
|
||
}
|
||
const tourData = await response.json();
|
||
currentTour = tourData;
|
||
showTourView();
|
||
} catch (error) {
|
||
console.error('Error loading tour:', error);
|
||
alert('Tour not found or access denied.');
|
||
window.location.hash = '';
|
||
} finally {
|
||
hideLoader();
|
||
}
|
||
}
|
||
|
||
// Add new idea
|
||
async function addIdea(e) {
|
||
e.preventDefault();
|
||
|
||
// Only send what FastAPI’s IdeaCreate expects:
|
||
const name = document.getElementById('ideaName').value;
|
||
const desc = document.getElementById('ideaDescription').value;
|
||
const startTime = document.getElementById('ideaStartTime').value;
|
||
const endTime = document.getElementById('ideaEndTime').value;
|
||
|
||
const payload = {
|
||
name,
|
||
description: desc,
|
||
...(startTime ? { start_time: startTime } : {}),
|
||
...(endTime ? { end_time: endTime } : {})
|
||
};
|
||
|
||
showLoader();
|
||
try {
|
||
const response = await fetch(
|
||
`${API_URL}/tours/${currentTour.id}/ideas`,
|
||
{
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(payload)
|
||
}
|
||
);
|
||
if (response.status === 404) {
|
||
throw new Error('Tour not found');
|
||
}
|
||
if (!response.ok) {
|
||
throw new Error(`Server responded with ${response.status}`);
|
||
}
|
||
|
||
// FastAPI sends back the full Idea object, including id and empty voters
|
||
const newIdea = await response.json();
|
||
currentTour.ideas.push(newIdea);
|
||
renderIdeas();
|
||
document.getElementById('addIdeaForm').reset();
|
||
} catch (error) {
|
||
console.error('Error adding idea:', error);
|
||
alert('Failed to add idea. Please try again.');
|
||
} finally {
|
||
hideLoader();
|
||
}
|
||
}
|
||
|
||
// Vote for idea
|
||
async function voteForIdea(ideaId) {
|
||
const voterName = prompt('Enter your name to vote:');
|
||
if (!voterName) return;
|
||
|
||
showLoader();
|
||
try {
|
||
const response = await fetch(
|
||
`${API_URL}/tours/${currentTour.id}/ideas/${ideaId}/vote`,
|
||
{
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ voterName })
|
||
}
|
||
);
|
||
if (response.status === 404) {
|
||
throw new Error('Tour or Idea not found');
|
||
}
|
||
if (!response.ok) {
|
||
throw new Error(`Server responded with ${response.status}`);
|
||
}
|
||
|
||
// FastAPI returns the Idea object (with updated voters list)
|
||
const updatedIdea = await response.json();
|
||
// Replace the old idea in currentTour.ideas with the updated one
|
||
currentTour.ideas = currentTour.ideas.map(i =>
|
||
i.id === updatedIdea.id ? updatedIdea : i
|
||
);
|
||
renderIdeas();
|
||
} catch (error) {
|
||
console.error('Error voting:', error);
|
||
alert('Failed to vote. Please try again.');
|
||
} finally {
|
||
hideLoader();
|
||
}
|
||
}
|
||
|
||
// Show tour view
|
||
function showTourView() {
|
||
document.getElementById('createTourSection').classList.add('hidden');
|
||
document.getElementById('tourViewSection').classList.remove('hidden');
|
||
|
||
// Display tour info
|
||
const tourInfo = document.getElementById('tourInfo');
|
||
|
||
// Build a friendly date‐range display:
|
||
let dateHTML = "";
|
||
if (currentTour.start_date && currentTour.end_date) {
|
||
const s = new Date(currentTour.start_date).toLocaleDateString('nb-NO', {
|
||
day: '2-digit',
|
||
month: '2-digit',
|
||
year: 'numeric'
|
||
});
|
||
const e = new Date(currentTour.end_date).toLocaleDateString('nb-NO', {
|
||
day: '2-digit',
|
||
month: '2-digit',
|
||
year: 'numeric'
|
||
});
|
||
dateHTML = `<span class="date-badge">📅 ${s} – ${e}</span>`;
|
||
} else if (currentTour.start_date) {
|
||
const s = new Date(currentTour.start_date).toLocaleDateString('nb-NO', {
|
||
day: '2-digit',
|
||
month: '2-digit',
|
||
year: 'numeric'
|
||
});
|
||
dateHTML = `<span class="date-badge">📅 Starts ${s}</span>`;
|
||
} else if (currentTour.end_date) {
|
||
const e = new Date(currentTour.end_date).toLocaleDateString('nb-NO', {
|
||
day: '2-digit',
|
||
month: '2-digit',
|
||
year: 'numeric'
|
||
});
|
||
dateHTML = `<span class="date-badge">📅 Ends ${e}</span>`;
|
||
} else {
|
||
dateHTML = `<span class="date-badge">📅 TBD</span>`;
|
||
}
|
||
|
||
tourInfo.innerHTML = `
|
||
<h2 class="tour-title">${currentTour.name}</h2>
|
||
${dateHTML}
|
||
<p style="margin-top: 12px;">${linkify(currentTour.description)}</p>
|
||
`;
|
||
|
||
renderIdeas();
|
||
}
|
||
|
||
// Render ideas
|
||
function renderIdeas() {
|
||
if (currentView === 'cards') {
|
||
renderCardsView();
|
||
} else {
|
||
renderMatrixView();
|
||
}
|
||
}
|
||
|
||
// Render cards view
|
||
function renderCardsView() {
|
||
const container = document.getElementById('cardsView');
|
||
container.innerHTML = '';
|
||
|
||
currentTour.ideas.forEach(idea => {
|
||
const card = document.createElement('div');
|
||
card.className = 'idea-card';
|
||
|
||
let timeBadge = "";
|
||
if (idea.start_time && idea.end_time) {
|
||
const s = new Date(idea.start_time).toLocaleDateString('nb-NO', {
|
||
day: '2-digit',
|
||
month: '2-digit',
|
||
year: '2-digit',
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
hour12: false
|
||
});
|
||
const e = new Date(idea.end_time).toLocaleDateString('nb-NO', {
|
||
day: '2-digit',
|
||
month: '2-digit',
|
||
year: '2-digit',
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
hour12: false
|
||
});
|
||
timeBadge = `<span class="date-badge">${s} → ${e}</span>`;
|
||
} else if (idea.start_time) {
|
||
const s = new Date(idea.start_time).toLocaleDateString('nb-NO', {
|
||
day: '2-digit',
|
||
month: '2-digit',
|
||
year: '2-digit',
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
hour12: false
|
||
});
|
||
timeBadge = `<span class="date-badge">🕒 Starts ${s}</span>`;
|
||
} else if (idea.end_time) {
|
||
const e = new Date(idea.end_time).toLocaleDateString('nb-NO', {
|
||
day: '2-digit',
|
||
month: '2-digit',
|
||
year: '2-digit',
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
hour12: false
|
||
});
|
||
timeBadge = `<span class="date-badge">🕒 Ends ${e}</span>`;
|
||
}
|
||
|
||
card.innerHTML = `
|
||
<h3 class="idea-title">${idea.name}</h3>
|
||
${timeBadge}
|
||
<p class="idea-description" style="margin-top: 8px;">${linkify(idea.description)}</p>
|
||
<div class="vote-section">
|
||
<span class="vote-count">👍 ${idea.voters.length} votes</span>
|
||
<button class="btn btn-secondary btn-small" onclick="voteForIdea('${idea.id}')">Vote</button>
|
||
</div>
|
||
<div class="voters-list">
|
||
${idea.voters.map(voter => `<span class="voter-chip">${voter}</span>`).join('')}
|
||
</div>
|
||
`;
|
||
container.appendChild(card);
|
||
});
|
||
}
|
||
|
||
// Render matrix view
|
||
function renderMatrixView() {
|
||
const container = document.getElementById('matrixView');
|
||
|
||
const allVoters = [...new Set(currentTour.ideas.flatMap(idea => idea.voters))];
|
||
|
||
let html = '<table class="matrix-table"><thead><tr>'
|
||
+ '<th>Idea</th><th>Time Range</th>';
|
||
allVoters.forEach(voter => {
|
||
html += `<th>${voter}</th>`;
|
||
});
|
||
html += '<th>Total</th></tr></thead><tbody>';
|
||
|
||
currentTour.ideas.forEach(idea => {
|
||
// Format time only (HH:mm) in Norwegian:
|
||
let timeCell = "";
|
||
if (idea.start_time && idea.end_time) {
|
||
const s = new Date(idea.start_time).toLocaleDateString('nb-NO', {
|
||
hour: '2-digit', minute: '2-digit', hour12: false
|
||
}).replace(/ /g, ' ');
|
||
const e = new Date(idea.end_time).toLocaleDateString('nb-NO', {
|
||
hour: '2-digit', minute: '2-digit', hour12: false
|
||
}).replace(/ /g, ' ');
|
||
timeCell = `<td>${s}<br/>→ ${e}</td>`;
|
||
} else if (idea.start_time) {
|
||
const s = new Date(idea.start_time).toLocaleDateString('nb-NO', {
|
||
hour: '2-digit', minute: '2-digit', hour12: false
|
||
});
|
||
timeCell = `<td>Starts ${s}</td>`;
|
||
} else if (idea.end_time) {
|
||
const e = new Date(idea.end_time).toLocaleDateString('nb-NO', {
|
||
hour: '2-digit', minute: '2-digit', hour12: false
|
||
});
|
||
timeCell = `<td>Ends ${e}</td>`;
|
||
} else {
|
||
timeCell = `<td style="color: var(--text-secondary);">—</td>`;
|
||
}
|
||
|
||
html += `<tr>`
|
||
+ `<td><strong>${idea.name}</strong><br><small>${linkify(idea.description)}</small></td>`
|
||
+ timeCell;
|
||
allVoters.forEach(voter => {
|
||
html += `<td class="vote-cell">${idea.voters.includes(voter) ? '✓' : ''}</td>`;
|
||
});
|
||
html += `<td class="vote-cell">${idea.voters.length}</td></tr>`;
|
||
});
|
||
|
||
html += '</tbody></table>';
|
||
container.innerHTML = html;
|
||
}
|
||
|
||
// Switch view
|
||
function showView(view) {
|
||
currentView = view;
|
||
if (view === 'cards') {
|
||
document.getElementById('cardsView').classList.remove('hidden');
|
||
document.getElementById('matrixView').classList.add('hidden');
|
||
} else {
|
||
document.getElementById('cardsView').classList.add('hidden');
|
||
document.getElementById('matrixView').classList.remove('hidden');
|
||
}
|
||
renderIdeas();
|
||
}
|
||
|
||
// Utility functions
|
||
function generateUUID() {
|
||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||
const r = Math.random() * 16 | 0;
|
||
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
||
return v.toString(16);
|
||
});
|
||
}
|
||
|
||
function showLoader() {
|
||
document.getElementById('loader').classList.remove('hidden');
|
||
}
|
||
|
||
function hideLoader() {
|
||
document.getElementById('loader').classList.add('hidden');
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|