1405 lines
46 KiB
HTML
1405 lines
46 KiB
HTML
<!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 ? ` ${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>
|