diff --git a/index.html b/index.html index d6da5fa..dbe62c9 100644 --- a/index.html +++ b/index.html @@ -227,13 +227,18 @@ 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 { + 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.decided { + background: rgba(0, 255, 0, 0.1); + border-color: rgba(0, 255, 0, 0.4); + } .idea-card h3 { margin-bottom: 20px; /* pushes the next form‐group down */ @@ -810,13 +815,15 @@ } // Render cards view - function renderCardsView() { - const container = document.getElementById('cardsView'); - container.innerHTML = ''; + function renderCardsView() { + const container = document.getElementById('cardsView'); + container.innerHTML = ''; + const decidedIdea = currentTour.ideas.find(i => i.decided); - currentTour.ideas.forEach(idea => { - const card = document.createElement('div'); - card.className = 'idea-card'; + currentTour.ideas.forEach(idea => { + const card = document.createElement('div'); + card.className = 'idea-card'; + if (idea.decided) card.classList.add('decided'); let timeBadge = ""; if (idea.start_time && idea.end_time) { @@ -863,23 +870,30 @@

${idea.name}

${timeBadge}

${linkify(idea.description)}

-
- πŸ‘ ${idea.voters.length} votes - -
+
+ πŸ‘ ${idea.voters.length} votes + ${decidedIdea ? '' : ``} + ${decidedIdea ? '' : ``} +
${idea.voters.map(voter => `${voter}`).join('')}
`; - container.appendChild(card); - }); - } + container.appendChild(card); + }); + + document.querySelectorAll('#addIdeaForm input, #addIdeaForm textarea, #addIdeaForm button').forEach(el => { + el.disabled = !!decidedIdea; + }); + } // Render matrix view - function renderMatrixView() { - const container = document.getElementById('matrixView'); + function renderMatrixView() { + const container = document.getElementById('matrixView'); - const allVoters = [...new Set(currentTour.ideas.flatMap(idea => idea.voters))]; + const decidedIdea = currentTour.ideas.find(i => i.decided); + + const allVoters = [...new Set(currentTour.ideas.flatMap(idea => idea.voters))]; let html = '' + ''; @@ -888,9 +902,9 @@ }); html += ''; - currentTour.ideas.forEach(idea => { - // Format time only (HH:mm) in Norwegian: - let timeCell = ""; + 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 @@ -913,18 +927,23 @@ timeCell = ``; } - html += `` + let rowStyle = idea.decided ? ' style="background: rgba(0,255,0,0.1)"' : ''; + html += `` + `` + timeCell; allVoters.forEach(voter => { html += ``; }); html += ``; - }); + }); - html += '
IdeaTime RangeTotal
β€”
${idea.name}
${linkify(idea.description)}
${idea.voters.includes(voter) ? 'βœ“' : ''}${idea.voters.length}
'; - container.innerHTML = html; - } + html += ''; + container.innerHTML = html; + + document.querySelectorAll('#addIdeaForm input, #addIdeaForm textarea, #addIdeaForm button').forEach(el => { + el.disabled = !!decidedIdea; + }); + } // Switch view function showView(view) { @@ -942,7 +961,11 @@ // Utility functions function sortIdeasByVotes() { if (!currentTour) return; - currentTour.ideas.sort((a, b) => b.voters.length - a.voters.length); + currentTour.ideas.sort((a, b) => { + if (a.decided) return -1; + if (b.decided) return 1; + return b.voters.length - a.voters.length; + }); } function generateUUID() { @@ -967,13 +990,30 @@ const voteSubmitBtn = document.getElementById('voteSubmitBtn'); let _pendingVoteIdeaId = null; - function voteForIdea(ideaId) { - _pendingVoteIdeaId = ideaId; - voterNameInput.value = ''; - voteSubmitBtn.disabled = true; - voteModal.classList.remove('hidden'); - setTimeout(() => voterNameInput.focus(), 100); - } + function voteForIdea(ideaId) { + _pendingVoteIdeaId = ideaId; + voterNameInput.value = ''; + voteSubmitBtn.disabled = true; + voteModal.classList.remove('hidden'); + setTimeout(() => voterNameInput.focus(), 100); + } + + async function decideIdea(ideaId) { + if (!confirm('Mark this idea as decided?')) return; + showLoader(); + try { + const resp = await fetch(`${API_URL}/tours/${currentTour.id}/ideas/${ideaId}/decide`, { method: 'POST' }); + if (!resp.ok) throw new Error('Failed'); + const decidedIdea = await resp.json(); + currentTour.ideas = currentTour.ideas.map(i => i.id === decidedIdea.id ? decidedIdea : i); + renderIdeas(); + } catch (err) { + console.error('Decide error', err); + alert('Failed to decide idea'); + } finally { + hideLoader(); + } + } voterNameInput.addEventListener('input', () => { voteSubmitBtn.disabled = voterNameInput.value.trim().length === 0; diff --git a/server.py b/server.py index 6ef2181..b079267 100644 --- a/server.py +++ b/server.py @@ -20,6 +20,7 @@ class Idea(BaseModel): end_time: Optional[datetime] = None description: str voters: list[str] = Field(default_factory=list) + decided: bool = False class Tour(BaseModel): id: str @@ -127,6 +128,9 @@ def add_idea(tour_id: str, idea: IdeaCreate): with open(filepath) as f: data = json.load(f) + if any(i.get("decided") for i in data["ideas"]): + raise HTTPException(status_code=403, detail="Idea already decided") + idea_id = str(uuid.uuid4()) new_idea = Idea( id=idea_id, @@ -135,6 +139,7 @@ def add_idea(tour_id: str, idea: IdeaCreate): voters=[], start_time=idea.start_time, end_time=idea.end_time, + decided=False, ) data["ideas"].append(new_idea.model_dump()) @@ -159,6 +164,8 @@ def vote_idea(tour_id: str, idea_id: str, vote: Vote): with open(filepath) as f: data = json.load(f) + if any(i.get("decided") for i in data["ideas"]): + raise HTTPException(status_code=403, detail="Idea already decided") for idea in data["ideas"]: if idea["id"] == idea_id: if vote.voterName not in idea["voters"]: @@ -170,6 +177,32 @@ def vote_idea(tour_id: str, idea_id: str, vote: Vote): raise HTTPException(status_code=404, detail="Idea not found") +# ─── ENDPOINT: DECIDE IDEA ──────────────────────────────────────────────────── + +@app.post("/tour/v1/tours/{tour_id}/ideas/{idea_id}/decide", response_model=Idea) +def decide_idea(tour_id: str, idea_id: str): + """Mark an idea as decided. Only one idea can be decided per tour.""" + filepath = get_tour_filepath(tour_id) + if not os.path.exists(filepath): + raise HTTPException(status_code=404, detail="Tour not found") + + lock = FileLock(filepath + ".lock") + with lock: + with open(filepath) as f: + data = json.load(f) + + if any(i.get("decided") for i in data["ideas"]): + raise HTTPException(status_code=403, detail="Idea already decided") + + for idea in data["ideas"]: + if idea["id"] == idea_id: + idea["decided"] = True + with open(filepath, "w") as f: + json.dump(data, f, default=str) + return Idea(**idea) + + raise HTTPException(status_code=404, detail="Idea not found") + # ─── MAIN: RUN UVIDORN WITH COMMAND-LINE DATA DIR ────────────────────────────── if __name__ == "__main__":