Files
garden-plan/static/index.html
2026-01-27 10:05:53 +01:00

1405 lines
46 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Garden Planner 2026-2027</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
::-webkit-scrollbar {
width: 10px;
height: 10px;
}
::-webkit-scrollbar-track {
background: #262e3d;
}
::-webkit-scrollbar-thumb {
background: #444c5b;
border-radius: 2px;
}
::-webkit-scrollbar-thumb:hover {
background: #4e5665;
}
body {
font-family: 'JetBrains Mono', 'Fira Code', 'SF Mono', 'Cascadia Code', 'Consolas', 'Monaco', monospace;
background: #19212e;
min-height: 100vh;
padding: 0;
color: #ABB7C1;
overflow-x: hidden;
}
.container {
max-width: 100%;
margin: 12px;
background: #1c2433;
border-radius: 4px;
padding: 0;
box-shadow: none;
border: 1px solid #303847;
overflow: hidden;
}
@media (min-width: 1400px) {
.container {
max-width: calc(100% - 24px);
margin: 12px auto;
}
}
h1 {
color: #69C3FF;
margin: 0;
padding: 16px 20px 8px 20px;
font-size: 1.5em;
font-weight: 600;
letter-spacing: -0.5px;
}
.subtitle {
color: #ABB7C1;
margin: 0;
padding: 0 20px 16px 20px;
font-size: 0.9em;
border-bottom: 1px solid #303847;
}
.view-toggle {
display: flex;
gap: 0;
margin: 0;
padding: 12px 20px;
background: #232b3a;
border-bottom: 1px solid #303847;
}
.view-toggle button {
padding: 6px 16px;
border: 1px solid #444c5b;
background: #262e3d;
color: #ABB7C1;
border-radius: 0;
cursor: pointer;
font-size: 13px;
font-weight: 500;
font-family: inherit;
transition: all 0.15s;
}
.view-toggle button:first-child {
border-radius: 3px 0 0 3px;
}
.view-toggle button:last-child {
border-radius: 0 3px 3px 0;
border-left: none;
}
.view-toggle button.active {
background: #3a4251;
color: #69C3FF;
border-color: #69C3FF;
}
.view-toggle button:hover:not(.active) {
background: #303847;
}
/* Timeline View */
.timeline-view {
display: none;
}
.timeline-view.active {
display: block;
}
/* Coming Up Section */
.coming-up-section {
background: #232b3a;
border-bottom: 1px solid #303847;
padding: 16px 20px;
}
.coming-up-header {
display: flex;
justify-content: space-between;
align-items: center;
color: #69C3FF;
font-size: 1em;
font-weight: 600;
margin-bottom: 12px;
}
.coming-up-controls {
display: flex;
gap: 8px;
align-items: center;
}
.expand-btn, .nav-btn {
padding: 4px 12px;
border: 1px solid #444c5b;
background: #262e3d;
color: #ABB7C1;
border-radius: 3px;
cursor: pointer;
font-size: 0.85em;
font-weight: 500;
font-family: inherit;
transition: all 0.15s;
}
.expand-btn:hover, .nav-btn:hover:not(:disabled) {
background: #303847;
border-color: #69C3FF;
color: #69C3FF;
}
.nav-btn {
padding: 4px 10px;
font-size: 0.8em;
}
.nav-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.coming-up-tasks {
display: flex;
flex-direction: column;
gap: 8px;
}
.coming-up-task {
display: grid;
grid-template-columns: 80px 1fr 100px;
gap: 12px;
align-items: center;
background: #1c2433;
padding: 10px 12px;
border-radius: 3px;
border: 1px solid #303847;
transition: all 0.15s;
}
.coming-up-task:hover {
border-color: #444c5b;
background: #262e3d;
}
.task-date {
font-size: 0.85em;
font-weight: 600;
text-align: center;
padding: 4px 8px;
border-radius: 2px;
border: 1px solid;
}
.task-date.urgent {
background: rgba(255, 115, 138, 0.2);
color: #FF738A;
border-color: #FF738A;
}
.task-date.soon {
background: rgba(234, 205, 97, 0.2);
color: #EACD61;
border-color: #EACD61;
}
.task-date.later {
background: rgba(105, 195, 255, 0.2);
color: #69C3FF;
border-color: #69C3FF;
}
.task-details {
display: flex;
flex-direction: column;
gap: 4px;
}
.task-main {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.9em;
}
.task-meta {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.8em;
color: #626a79;
}
.task-notes {
color: #ABB7C1;
}
.task-countdown {
text-align: right;
font-size: 0.85em;
color: #69C3FF;
font-weight: 500;
}
.bed-section {
margin-bottom: 0;
border-bottom: 1px solid #303847;
}
.bed-header {
display: flex;
align-items: center;
gap: 12px;
margin: 0;
padding: 10px 20px;
background: #232b3a;
border-top: 1px solid #303847;
border-bottom: 1px solid #444c5b;
color: #ABB7C1;
}
.bed-name {
font-size: 1em;
font-weight: 600;
color: #3CEC85;
}
.bed-size {
font-size: 0.85em;
color: #626a79;
}
.timeline {
position: relative;
padding: 12px 0 0 0;
background: #1c2433;
min-height: 200px;
}
.timeline-months {
display: grid;
grid-template-columns: repeat(12, 1fr);
margin: 0 20px 8px 20px;
border-bottom: 1px solid #444c5b;
padding-bottom: 4px;
}
.month-label {
text-align: center;
font-size: 0.75em;
color: #626a79;
font-weight: 500;
text-transform: uppercase;
}
.crop-row {
position: relative;
min-height: 32px;
border-bottom: 1px solid #262e3d;
padding: 4px 20px;
}
.crop-bar {
position: absolute;
height: 24px;
border-radius: 2px;
padding: 0 8px;
font-size: 0.75em;
color: #1c2433;
font-weight: 500;
display: flex;
align-items: center;
justify-content: flex-start;
text-align: left;
box-shadow: none;
cursor: pointer;
transition: all 0.1s ease;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
border: 1px solid rgba(0,0,0,0.2);
}
.crop-bar:hover {
transform: none;
box-shadow: none;
filter: brightness(1.15);
border-color: #ABB7C1;
}
.crop-bar.expanded {
height: auto;
min-height: 80px;
z-index: 100;
white-space: normal;
align-items: flex-start;
padding: 8px;
border-color: #ABB7C1;
}
.crop-bar-content {
width: 100%;
}
.crop-bar-title {
font-weight: 600;
font-size: 1em;
margin-bottom: 6px;
}
.crop-bar-details {
display: none;
font-size: 0.85em;
line-height: 1.5;
margin-top: 6px;
font-weight: 400;
color: #1c2433;
}
.crop-bar.expanded .crop-bar-details {
display: block;
}
/* Garden Overview */
.garden-view {
display: none;
padding: 20px;
background: #1c2433;
}
.garden-view.active {
display: block;
}
.timeline-controls {
margin-bottom: 20px;
background: #232b3a;
padding: 20px;
border-radius: 4px;
border: 1px solid #303847;
}
.timeline-slider-container {
margin-top: 12px;
}
.timeline-slider {
width: 100%;
height: 8px;
background: #262e3d;
border-radius: 4px;
position: relative;
cursor: pointer;
border: 1px solid #444c5b;
}
.timeline-slider-track {
height: 100%;
background: #69C3FF;
border-radius: 4px;
width: 0%;
transition: width 0.1s;
}
.timeline-slider-thumb {
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
width: 20px;
height: 20px;
background: #69C3FF;
border-radius: 50%;
border: 2px solid #1c2433;
cursor: pointer;
left: 0%;
}
.timeline-date-display {
display: flex;
justify-content: space-between;
margin-top: 8px;
font-size: 0.85em;
color: #ABB7C1;
}
.current-date {
font-weight: 600;
color: #69C3FF;
}
.garden-container {
position: relative;
background: #232b3a;
padding: 40px;
border-radius: 4px;
border: 1px solid #303847;
min-height: 600px;
}
.garden-layout {
position: relative;
margin: 0 auto;
width: 800px;
height: 550px;
}
.bed {
position: absolute;
background: #2d3648;
border: 2px solid #444c5b;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.bed:hover {
border-color: #69C3FF;
background: #343d50;
}
.bed-label {
position: absolute;
top: 8px;
left: 8px;
font-size: 0.75em;
font-weight: 600;
color: #626a79;
background: rgba(28, 36, 51, 0.9);
padding: 4px 8px;
border-radius: 2px;
z-index: 10;
}
.bed-size {
position: absolute;
bottom: 8px;
right: 8px;
font-size: 0.65em;
color: #626a79;
background: rgba(28, 36, 51, 0.9);
padding: 2px 6px;
border-radius: 2px;
}
/* North bed - spans full width at top */
.bed-north {
top: 0;
left: 0;
width: 800px;
height: 80px;
}
/* Pathway */
.pathway-horizontal {
position: absolute;
height: 40px;
background: #1a2130;
border: 1px dashed #303847;
}
/* Middle bed - left side */
.bed-middle {
top: 120px;
left: 0;
width: 240px;
height: 200px;
}
/* Plot North - right of middle */
.bed-plot-north {
top: 120px;
right: 0;
width: 240px;
height: 200px;
}
/* South bed - bottom left */
.bed-south {
bottom: 0;
left: 0;
width: 240px;
height: 200px;
}
/* Plot South - right of south */
.bed-plot-south {
bottom: 0;
right: 0;
width: 240px;
height: 200px;
}
/* Vertical pathway between beds and plots */
.pathway-vertical {
position: absolute;
width: 80px;
background: #1a2130;
border: 1px dashed #303847;
top: 120px;
left: 360px;
height: 430px;
}
/* Horizontal pathway between middle/plot-north and south/plot-south */
.pathway-middle {
position: absolute;
height: 30px;
width: 560px;
background: #1a2130;
border: 1px dashed #303847;
top: 320px;
left: 0;
}
.pathway-middle-right {
position: absolute;
height: 30px;
width: 240px;
background: #1a2130;
border: 1px dashed #303847;
top: 320px;
right: 0;
}
/* Crop circles */
.crop-circle {
position: absolute;
border-radius: 50%;
border: 1.5px solid rgba(0, 0, 0, 0.3);
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.65em;
color: #1c2433;
font-weight: 700;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.crop-circle:hover {
transform: scale(1.4);
z-index: 100;
border-color: #ABB7C1;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.4);
}
.crop-tooltip {
position: absolute;
background: #232b3a;
border: 1px solid #69C3FF;
padding: 12px;
border-radius: 4px;
font-size: 0.85em;
color: #ABB7C1;
z-index: 1000;
pointer-events: none;
white-space: nowrap;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
display: none;
}
.crop-tooltip.visible {
display: block;
}
.crop-tooltip strong {
color: #69C3FF;
}
.bed-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 2px;
font-size: 0.85em;
font-weight: 500;
border: 1px solid;
}
.bed-badge.south {
background: rgba(255, 115, 138, 0.2);
color: #FF738A;
border-color: #FF738A;
}
.bed-badge.middle {
background: rgba(234, 205, 97, 0.2);
color: #EACD61;
border-color: #EACD61;
}
.bed-badge.north {
background: rgba(60, 236, 133, 0.2);
color: #3CEC85;
border-color: #3CEC85;
}
.bed-badge.plot.south,
.bed-badge.plot-south {
background: rgba(199, 132, 75, 0.2);
color: #c7844b;
border-color: #c7844b;
}
.bed-badge.plot.north,
.bed-badge.plot-north {
background: rgba(246, 224, 94, 0.2);
color: #f6e05e;
border-color: #f6e05e;
}
.activity-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 2px;
font-size: 0.8em;
font-weight: 500;
border: 1px solid;
}
.activity-badge.greenhouse {
background: rgba(105, 195, 255, 0.2);
color: #69C3FF;
border-color: #69C3FF;
}
.activity-badge.direct {
background: rgba(60, 236, 133, 0.2);
color: #3CEC85;
border-color: #3CEC85;
}
.activity-badge.transplant {
background: rgba(189, 147, 255, 0.2);
color: #bd93ff;
border-color: #bd93ff;
}
.legend {
display: flex;
gap: 12px;
margin: 0;
padding: 12px 20px;
background: #232b3a;
border-top: 1px solid #303847;
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.85em;
color: #626a79;
}
.legend-color {
width: 24px;
height: 16px;
border-radius: 2px;
border: 1px solid #444c5b;
}
</style>
</head>
<body>
<div class="container">
<h1>Garden Planner 2026-2027</h1>
<p class="subtitle">Western Zealand, Denmark // Three Bed System</p>
<div class="view-toggle">
<button class="active" onclick="switchView('timeline')">Timeline View</button>
<button onclick="switchView('garden')">Garden Overview</button>
</div>
<div class="timeline-view active" id="timelineView">
<div id="timelineContainer"></div>
<div class="legend">
<div class="legend-item">
<strong></strong> Click any crop box to see growing details
</div>
</div>
</div>
<div class="garden-view" id="gardenView">
<div class="timeline-controls">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
<div>
<strong style="color: #69C3FF;">Timeline</strong>
<span id="plantCount" style="margin-left: 12px; color: #626a79; font-size: 0.85em;"></span>
</div>
<button onclick="playTimeline()" id="playButton" style="padding: 4px 12px; background: #262e3d; border: 1px solid #444c5b; color: #ABB7C1; border-radius: 3px; cursor: pointer; font-family: inherit; font-size: 0.85em;">▶ Play</button>
</div>
<div class="timeline-slider-container">
<div class="timeline-slider" id="timelineSlider" onmousedown="startDrag(event)">
<div class="timeline-slider-track" id="sliderTrack"></div>
<div class="timeline-slider-thumb" id="sliderThumb"></div>
</div>
<div class="timeline-date-display">
<span>March 2026</span>
<span class="current-date" id="currentDate">March 1, 2026</span>
<span>February 2027</span>
</div>
</div>
</div>
<div class="garden-container">
<div class="garden-layout" id="gardenLayout">
<!-- North Bed -->
<div class="bed bed-north">
<div class="bed-label">▲ North Bed</div>
<div class="bed-size">300×50cm</div>
</div>
<!-- Horizontal pathway -->
<div class="pathway-horizontal" style="top: 80px; left: 0; width: 800px;"></div>
<!-- Middle Bed -->
<div class="bed bed-middle">
<div class="bed-label">■ Middle Bed</div>
<div class="bed-size">150×100cm</div>
</div>
<!-- Plot North -->
<div class="bed bed-plot-north">
<div class="bed-label">◇ Plot North</div>
<div class="bed-size">150×100cm</div>
</div>
<!-- Vertical pathway -->
<div class="pathway-vertical"></div>
<!-- Horizontal pathway between rows -->
<div class="pathway-middle"></div>
<div class="pathway-middle-right"></div>
<!-- South Bed -->
<div class="bed bed-south">
<div class="bed-label">▼ South Bed</div>
<div class="bed-size">150×100cm</div>
</div>
<!-- Plot South -->
<div class="bed bed-plot-south">
<div class="bed-label">◆ Plot South</div>
<div class="bed-size">150×100cm</div>
</div>
</div>
</div>
<div class="crop-tooltip" id="cropTooltip"></div>
</div>
</div>
<script>
let timelineData = [];
let eventsData = [];
let cropColors = {};
async function loadData() {
try {
const [timelineRes, eventsRes, colorsRes] = await Promise.all([
fetch('/api/timeline'),
fetch('/api/events'),
fetch('/api/colors')
]);
timelineData = await timelineRes.json();
eventsData = await eventsRes.json();
cropColors = await colorsRes.json();
console.log('Loaded', eventsData.length, 'events');
console.log('Loaded', timelineData.length, 'timeline entries');
console.log('Loaded', Object.keys(cropColors).length, 'crop colors');
renderTimeline();
} catch (error) {
console.error('Error loading data:', error);
}
}
function switchView(view) {
document.querySelectorAll('.view-toggle button').forEach(btn => btn.classList.remove('active'));
document.querySelectorAll('.timeline-view, .garden-view').forEach(v => v.classList.remove('active'));
if (view === 'timeline') {
document.getElementById('timelineView').classList.add('active');
event.target.classList.add('active');
} else if (view === 'garden') {
document.getElementById('gardenView').classList.add('active');
event.target.classList.add('active');
renderGardenView();
}
}
function renderTimeline() {
const container = document.getElementById('timelineContainer');
const beds = ['South', 'Middle', 'North', 'Plot South', 'Plot North'];
const bedSizes = {
'South': '150×100cm',
'Middle': '150×100cm',
'North': '300×50cm',
'Plot South': '150×100cm',
'Plot North': '150×100cm'
};
const months = ['Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec', 'Jan', 'Feb'];
let html = '';
// Add "Coming Up" section with next 3 tasks
html += renderComingUp();
beds.forEach(bed => {
const bedEvents = timelineData.filter(e => e.bed === bed);
const bedSymbol = bed === 'South' ? '▼' :
bed === 'Middle' ? '■' :
bed === 'North' ? '▲' :
bed === 'Plot South' ? '◆' :
bed === 'Plot North' ? '◇' : '●';
html += `
<div class="bed-section">
<div class="bed-header">
<div class="bed-name">${bedSymbol} ${bed} Bed</div>
<div class="bed-size">${bedSizes[bed]} // ${bedEvents.length} plantings</div>
</div>
<div class="timeline">
<div class="timeline-months">
${months.map(m => `<div class="month-label">${m}</div>`).join('')}
</div>
`;
bedEvents.forEach((event, idx) => {
const color = getCropColor(event.crop);
html += `<div class="crop-row">`;
// Render each phase as a separate box
const cropGroupId = `${bed}-${idx}`;
event.phases.forEach((phase, phaseIdx) => {
const startPos = dateToPosition(phase.start_date);
const endPos = phase.end_date ? dateToPosition(phase.end_date) : startPos + 8;
const width = endPos - startPos;
// Different styling for harvest phase
const isHarvestPhase = phase.activity === 'Harvest';
let phaseColor, phaseLabel;
if (isHarvestPhase) {
phaseColor = adjustColor(color, 1.2); // Lighter for harvest
phaseLabel = '⚘';
} else {
phaseColor = phaseIdx === 0 ? adjustColor(color, 0.7) : color;
phaseLabel = phase.activity.includes('Greenhouse') ? '◆' :
phase.activity.includes('Transplant') ? '▸' : '●';
}
const isLastPhase = phaseIdx === event.phases.length - 1;
// Build details for this specific phase
let phaseDetails = `
${phaseIdx + 1}. ${phase.activity} - ${phase.start_date}${phase.end_date ? ` to ${phase.end_date}` : ''}<br>
${phase.notes ? `&nbsp;&nbsp;&nbsp;&nbsp;${phase.notes}<br>` : ''}
`;
// Add quantity info only to the last non-harvest phase
if (isLastPhase && !isHarvestPhase) {
phaseDetails += `
<strong>Quantity:</strong> ${eventsData.find(e => e.crop === event.crop && e.variety === event.variety)?.quantity || 'N/A'}
`;
}
html += `
<div class="crop-bar" id="crop-${bed}-${idx}-${phaseIdx}"
data-crop-group="${cropGroupId}"
style="left: ${startPos}%; width: ${width}%; background: ${phaseColor};"
onclick="toggleCropDetails('${cropGroupId}')">
<div class="crop-bar-content">
<div class="crop-bar-title">${phaseLabel} ${event.crop} - ${event.variety}</div>
<div class="crop-bar-details" id="details-${bed}-${idx}-${phaseIdx}">
${phaseDetails}
</div>
</div>
</div>
`;
});
html += `</div>`;
});
html += `
</div>
</div>
`;
});
container.innerHTML = html;
}
let comingUpExpanded = false;
let comingUpPage = 0;
let allUpcomingTasks = [];
function renderComingUp() {
// Get current date
const today = new Date();
// Collect all upcoming phases
allUpcomingTasks = [];
timelineData.forEach(entry => {
entry.phases.forEach(phase => {
const startDate = new Date(phase.start_date);
if (startDate >= today) {
allUpcomingTasks.push({
date: startDate,
dateStr: phase.start_date,
activity: phase.activity,
crop: entry.crop,
variety: entry.variety,
bed: entry.bed,
notes: phase.notes
});
}
});
});
// Sort by date
allUpcomingTasks.sort((a, b) => a.date - b.date);
if (allUpcomingTasks.length === 0) {
return '';
}
// Determine how many tasks to show
const tasksToShow = comingUpExpanded ? 10 : 3;
const startIdx = comingUpExpanded ? comingUpPage * 10 : 0;
const endIdx = startIdx + tasksToShow;
const tasksSlice = allUpcomingTasks.slice(startIdx, endIdx);
const totalPages = Math.ceil(allUpcomingTasks.length / 10);
let html = `
<div class="coming-up-section">
<div class="coming-up-header">
<div>
<strong>⏰ Coming Up</strong>
<span style="color: #626a79; font-size: 0.9em;">
${comingUpExpanded ? `Page ${comingUpPage + 1} of ${totalPages}` : `Next ${tasksSlice.length} tasks`}
</span>
</div>
<div class="coming-up-controls">
${comingUpExpanded ? `
<button onclick="navigateComingUp(-1)" ${comingUpPage === 0 ? 'disabled' : ''} class="nav-btn">◀</button>
<button onclick="navigateComingUp(1)" ${endIdx >= allUpcomingTasks.length ? 'disabled' : ''} class="nav-btn">▶</button>
<button onclick="toggleComingUp()" class="expand-btn">Show Less</button>
` : `
<button onclick="toggleComingUp()" class="expand-btn">Show More</button>
`}
</div>
</div>
<div class="coming-up-tasks">
`;
tasksSlice.forEach(task => {
const daysUntil = Math.ceil((task.date - today) / (1000 * 60 * 60 * 24));
const urgency = daysUntil <= 7 ? 'urgent' : daysUntil <= 14 ? 'soon' : 'later';
const bedClass = task.bed.toLowerCase().replace(' ', '-');
html += `
<div class="coming-up-task">
<div class="task-date ${urgency}">${formatTaskDate(task.date)}</div>
<div class="task-details">
<div class="task-main">
<span class="activity-badge ${getActivityClass(task.activity)}">${task.activity}</span>
<strong>${task.crop}</strong> - ${task.variety}
</div>
<div class="task-meta">
<span class="bed-badge ${bedClass}">${task.bed}</span>
${task.notes ? `<span class="task-notes">${task.notes}</span>` : ''}
</div>
</div>
<div class="task-countdown">${daysUntil === 0 ? 'Today!' : daysUntil === 1 ? 'Tomorrow' : `${daysUntil} days`}</div>
</div>
`;
});
html += `
</div>
</div>
`;
return html;
}
function toggleComingUp() {
comingUpExpanded = !comingUpExpanded;
if (!comingUpExpanded) {
comingUpPage = 0; // Reset to first page when collapsing
}
renderTimeline();
}
function navigateComingUp(direction) {
const totalPages = Math.ceil(allUpcomingTasks.length / 10);
comingUpPage = Math.max(0, Math.min(comingUpPage + direction, totalPages - 1));
renderTimeline();
}
function formatTaskDate(date) {
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
return `${months[date.getMonth()]} ${date.getDate()}`;
}
function getActivityClass(activity) {
if (activity.includes('Greenhouse')) return 'greenhouse';
if (activity.includes('Transplant')) return 'transplant';
if (activity.includes('Direct')) return 'direct';
return '';
}
function dateToPosition(dateStr) {
const date = new Date(dateStr);
const startDate = new Date('2026-03-01');
const endDate = new Date('2027-03-01');
const totalDays = (endDate - startDate) / (1000 * 60 * 60 * 24);
const daysSinceStart = (date - startDate) / (1000 * 60 * 60 * 24);
return (daysSinceStart / totalDays) * 100;
}
function getCropColor(crop) {
return cropColors[crop] || '#4299e1';
}
function adjustColor(hex, factor) {
// Adjust color brightness: factor < 1 darkens, factor > 1 lightens
const rgb = hex.match(/\w\w/g).map(x => parseInt(x, 16));
const adjusted = rgb.map(c => {
if (factor < 1) {
return Math.round(c * factor);
} else {
// Lighten by moving towards 255
return Math.round(c + (255 - c) * (factor - 1));
}
});
return '#' + adjusted.map(x => Math.min(255, x).toString(16).padStart(2, '0')).join('');
}
function toggleCropDetails(cropGroupId) {
// Get all phases of this crop using data attribute
const allPhases = document.querySelectorAll(`[data-crop-group="${cropGroupId}"]`);
const wasExpanded = allPhases[0]?.classList.contains('expanded');
// Collapse all other expanded crops
document.querySelectorAll('.crop-bar.expanded').forEach(bar => {
bar.classList.remove('expanded');
});
// Toggle all phases of this crop
if (!wasExpanded) {
allPhases.forEach(phase => phase.classList.add('expanded'));
}
}
// Garden View Variables
let currentTimelineDate = new Date('2026-03-01');
let isDragging = false;
let playInterval = null;
// Timeline slider interaction
function startDrag(event) {
isDragging = true;
updateSliderPosition(event);
document.addEventListener('mousemove', updateSliderPosition);
document.addEventListener('mouseup', stopDrag);
}
function stopDrag() {
isDragging = false;
document.removeEventListener('mousemove', updateSliderPosition);
document.removeEventListener('mouseup', stopDrag);
}
function updateSliderPosition(event) {
if (!isDragging && event.type === 'mousemove') return;
const slider = document.getElementById('timelineSlider');
const rect = slider.getBoundingClientRect();
const x = Math.max(0, Math.min(event.clientX - rect.left, rect.width));
const percentage = (x / rect.width) * 100;
// Calculate date from percentage
const startDate = new Date('2026-03-01');
const endDate = new Date('2027-03-01');
const totalDays = (endDate - startDate) / (1000 * 60 * 60 * 24);
const daysFromStart = (percentage / 100) * totalDays;
currentTimelineDate = new Date(startDate.getTime() + daysFromStart * 24 * 60 * 60 * 1000);
// Update UI
document.getElementById('sliderThumb').style.left = percentage + '%';
document.getElementById('sliderTrack').style.width = percentage + '%';
document.getElementById('currentDate').textContent = formatDate(currentTimelineDate);
// Update garden view
renderGardenView();
}
function playTimeline() {
const button = document.getElementById('playButton');
if (playInterval) {
// Stop playing
clearInterval(playInterval);
playInterval = null;
button.textContent = '▶ Play';
} else {
// Start playing
button.textContent = '⏸ Pause';
playInterval = setInterval(() => {
const startDate = new Date('2026-03-01');
const endDate = new Date('2027-03-01');
// Advance by 3 days
currentTimelineDate = new Date(currentTimelineDate.getTime() + 3 * 24 * 60 * 60 * 1000);
// Loop back to start if we reach the end
if (currentTimelineDate > endDate) {
currentTimelineDate = new Date(startDate);
}
// Calculate percentage
const totalDays = (endDate - startDate) / (1000 * 60 * 60 * 24);
const daysFromStart = (currentTimelineDate - startDate) / (1000 * 60 * 60 * 24);
const percentage = (daysFromStart / totalDays) * 100;
// Update UI
document.getElementById('sliderThumb').style.left = percentage + '%';
document.getElementById('sliderTrack').style.width = percentage + '%';
document.getElementById('currentDate').textContent = formatDate(currentTimelineDate);
// Update garden view
renderGardenView();
}, 200);
}
}
function formatDate(date) {
const months = ['January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December'];
return `${months[date.getMonth()]} ${date.getDate()}, ${date.getFullYear()}`;
}
function renderGardenView() {
const container = document.getElementById('gardenLayout');
// Remove existing crop circles
document.querySelectorAll('.crop-circle').forEach(el => el.remove());
// Get active crops for current date
const activeCrops = getActiveCrops(currentTimelineDate);
// Calculate total plants
const totalPlants = activeCrops.reduce((sum, crop) => sum + crop.quantity, 0);
const uniqueCrops = new Set(activeCrops.map(c => c.crop)).size;
document.getElementById('plantCount').textContent = `${totalPlants} plants // ${uniqueCrops} crops`;
// Group crops by bed and expand by quantity
const cropsByBed = {
'South': [],
'Middle': [],
'North': [],
'Plot South': [],
'Plot North': []
};
activeCrops.forEach(crop => {
if (cropsByBed[crop.bed]) {
// Create individual circle entries based on quantity
for (let i = 0; i < crop.quantity; i++) {
cropsByBed[crop.bed].push({
...crop,
plantIndex: i
});
}
}
});
// Render crops in each bed
Object.entries(cropsByBed).forEach(([bedName, crops]) => {
const bedClass = bedName === 'South' ? 'bed-south' :
bedName === 'Middle' ? 'bed-middle' :
bedName === 'North' ? 'bed-north' :
bedName === 'Plot South' ? 'bed-plot-south' :
'bed-plot-north';
const bedElement = container.querySelector(`.${bedClass}`);
const bedRect = bedElement.getBoundingClientRect();
const containerRect = container.getBoundingClientRect();
// Position crops in a grid within the bed
crops.forEach((crop, idx) => {
const circle = document.createElement('div');
circle.className = 'crop-circle';
// Determine circle size and position based on bed dimensions
const circleSize = bedName === 'North' ? 30 : 35;
circle.style.width = circleSize + 'px';
circle.style.height = circleSize + 'px';
circle.style.background = getCropColor(crop.crop);
// Position in grid
const position = getCirclePosition(bedName, idx, crops.length, bedElement);
circle.style.left = position.x + 'px';
circle.style.top = position.y + 'px';
// Add crop initial
const initial = crop.crop.substring(0, 1);
circle.textContent = initial;
// Add hover tooltip
circle.addEventListener('mouseenter', (e) => showTooltip(e, crop));
circle.addEventListener('mouseleave', hideTooltip);
container.appendChild(circle);
});
});
}
function getCirclePosition(bedName, index, total, bedElement) {
const rect = bedElement.getBoundingClientRect();
const container = document.getElementById('gardenLayout').getBoundingClientRect();
// Get bed dimensions
const width = rect.width;
const height = rect.height;
const left = rect.left - container.left;
const top = rect.top - container.top;
// Calculate grid layout based on bed shape and total count
let cols, rows;
if (bedName === 'North') {
// North bed is wide and short (300×50cm = 6:1 ratio)
cols = Math.min(total, 12);
rows = Math.ceil(total / cols);
} else if (bedName === 'Plot South' || bedName === 'Plot North') {
// Plots - assume similar to regular beds for now
cols = Math.ceil(Math.sqrt(total * 1.5));
rows = Math.ceil(total / cols);
} else {
// Middle and South beds are 150×100cm (3:2 ratio)
cols = Math.ceil(Math.sqrt(total * 1.5));
rows = Math.ceil(total / cols);
}
const row = Math.floor(index / cols);
const col = index % cols;
const cellWidth = width / (cols + 1);
const cellHeight = height / (rows + 1);
const circleSize = bedName === 'North' ? 15 : 17.5;
return {
x: left + cellWidth * (col + 1) - circleSize,
y: top + cellHeight * (row + 1) - circleSize
};
}
function parseQuantity(quantityStr) {
if (!quantityStr) return 1;
// Extract number from various formats
const match = quantityStr.match(/(\d+)/);
if (match) {
return parseInt(match[1]);
}
// Handle special cases
if (quantityStr.toLowerCase().includes('double')) return 2;
if (quantityStr.toLowerCase().includes('heavy')) return 1;
return 1;
}
function getActiveCrops(date) {
const activeCrops = [];
const seenCrops = new Set();
timelineData.forEach(entry => {
entry.phases.forEach(phase => {
const startDate = new Date(phase.start_date);
const endDate = phase.end_date ? new Date(phase.end_date) : new Date('2027-03-01');
if (date >= startDate && date <= endDate) {
// Create unique key to avoid duplicates from multiple phases
const cropKey = `${entry.crop}-${entry.variety}-${entry.bed}`;
if (!seenCrops.has(cropKey)) {
seenCrops.add(cropKey);
const quantity = parseQuantity(entry.quantity);
activeCrops.push({
crop: entry.crop,
variety: entry.variety,
bed: entry.bed,
phase: phase.activity,
harvest_period: entry.harvest_period,
notes: phase.notes,
quantity: quantity
});
}
}
});
});
return activeCrops;
}
function showTooltip(event, crop) {
const tooltip = document.getElementById('cropTooltip');
tooltip.innerHTML = `
<strong>${crop.crop}</strong> - ${crop.variety}<br>
Quantity: ${crop.quantity} plants<br>
Phase: ${crop.phase}<br>
Harvest: ${crop.harvest_period}<br>
${crop.notes ? `Notes: ${crop.notes}` : ''}
`;
tooltip.classList.add('visible');
// Position tooltip near cursor
tooltip.style.left = (event.pageX + 15) + 'px';
tooltip.style.top = (event.pageY - 10) + 'px';
}
function hideTooltip() {
const tooltip = document.getElementById('cropTooltip');
tooltip.classList.remove('visible');
}
loadData();
</script>
</body>
</html>