Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
195 changes: 190 additions & 5 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nova Scotia Walk Rater</title>
<title>Kate and Jack's Walks</title>
<style>
:root {
--ocean: #2b6777;
Expand All @@ -18,6 +18,7 @@
--text: #2d3436;
--text-light: #636e72;
--shadow: rgba(43, 103, 119, 0.15);
--pink: #e8a0bf;
}

* { margin: 0; padding: 0; box-sizing: border-box; }
Expand Down Expand Up @@ -50,10 +51,39 @@
}

header h1 {
font-size: 2rem;
font-size: 2.2rem;
font-weight: 700;
letter-spacing: -0.5px;
margin-bottom: 0.25rem;
position: relative;
z-index: 1;
}

.title-hearts { color: var(--pink); font-size: 0.85em; }

.floating-hearts {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
pointer-events: none; overflow: hidden;
}
.floating-hearts span {
position: absolute; font-size: 1.2rem; opacity: 0.15;
animation: floatHeart 12s infinite ease-in-out;
}
.floating-hearts span:nth-child(1) { left: 5%; animation-delay: 0s; animation-duration: 10s; }
.floating-hearts span:nth-child(2) { left: 15%; animation-delay: 2s; animation-duration: 14s; font-size: 0.9rem; }
.floating-hearts span:nth-child(3) { left: 30%; animation-delay: 4s; animation-duration: 11s; }
.floating-hearts span:nth-child(4) { left: 50%; animation-delay: 1s; animation-duration: 13s; font-size: 1.5rem; }
.floating-hearts span:nth-child(5) { left: 65%; animation-delay: 3s; animation-duration: 10s; font-size: 0.8rem; }
.floating-hearts span:nth-child(6) { left: 80%; animation-delay: 5s; animation-duration: 12s; }
.floating-hearts span:nth-child(7) { left: 92%; animation-delay: 2.5s; animation-duration: 15s; }

@keyframes floatHeart {
0% { transform: translateY(100px) rotate(0deg) scale(1); opacity: 0; }
10% { opacity: 0.15; }
50% { opacity: 0.2; }
90% { opacity: 0.1; }
100% { transform: translateY(-200px) rotate(25deg) scale(0.6); opacity: 0; }
}

header p {
Expand Down Expand Up @@ -174,6 +204,7 @@
}

.form-group textarea { resize: vertical; min-height: 70px; }
.form-group .hint { font-size: 0.78rem; color: var(--text-light); margin-top: 0.25rem; }

.rating-input {
display: flex;
Expand Down Expand Up @@ -370,6 +401,59 @@
font-style: italic;
}

.photo-upload-btn {
display: inline-flex; align-items: center; gap: 0.35rem; cursor: pointer;
padding: 0.5rem 1rem; background: var(--ocean); color: var(--white);
border-radius: 8px; font-size: 0.9rem; font-weight: 600;
transition: background 0.2s;
}
.photo-upload-btn:hover { background: var(--ocean-light); }

.photo-preview {
display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 0.5rem;
}
.photo-preview-item {
position: relative; width: 70px; height: 70px; border-radius: 8px; overflow: hidden;
}
.photo-preview-item img {
width: 100%; height: 100%; object-fit: cover;
}
.photo-preview-item .remove-photo {
position: absolute; top: 2px; right: 2px; width: 20px; height: 20px;
background: rgba(0,0,0,0.6); color: white; border: none; border-radius: 50%;
font-size: 0.7rem; cursor: pointer; display: flex; align-items: center;
justify-content: center; line-height: 1;
}

.walk-photos {
display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 0.5rem;
}
.walk-photo {
width: 80px; height: 80px; object-fit: cover; border-radius: 8px;
cursor: pointer; transition: transform 0.15s, box-shadow 0.15s;
}
.walk-photo:hover { transform: scale(1.05); box-shadow: 0 3px 12px var(--shadow); }

.photo-lightbox {
display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.85);
z-index: 200; align-items: center; justify-content: center; padding: 1rem;
}
.photo-lightbox.active { display: flex; }
.photo-lightbox img { max-width: 95%; max-height: 90vh; border-radius: 8px; }
.photo-lightbox-close {
position: absolute; top: 1rem; right: 1rem; background: rgba(255,255,255,0.2);
color: white; border: none; font-size: 1.5rem; width: 40px; height: 40px;
border-radius: 50%; cursor: pointer; display: flex; align-items: center; justify-content: center;
}

.walk-map-link {
display: inline-flex; align-items: center; gap: 0.35rem; margin-top: 0.5rem;
padding: 0.3rem 0.7rem; background: linear-gradient(135deg, #5b63d3, #7c5bbf);
color: var(--white); border-radius: 20px; font-size: 0.78rem; font-weight: 600;
text-decoration: none; transition: transform 0.15s, box-shadow 0.15s;
}
.walk-map-link:hover { transform: translateY(-1px); box-shadow: 0 3px 10px rgba(91,99,211,0.3); }

.walk-tags {
display: flex;
gap: 0.4rem;
Expand Down Expand Up @@ -439,6 +523,9 @@
gap: 1rem;
}

footer { text-align: center; padding: 1.5rem 1rem 2rem; color: var(--text-light); font-size: 0.8rem; }
.footer-hearts { color: var(--pink); font-size: 1rem; letter-spacing: 3px; margin-bottom: 0.3rem; }

@media (max-width: 500px) {
header h1 { font-size: 1.5rem; }
.raters, .names-row { grid-template-columns: 1fr; }
Expand All @@ -449,7 +536,11 @@
</head>
<body>
<header>
<h1>Nova Scotia Walk Rater</h1>
<div class="floating-hearts">
<span>&#10084;</span><span>&#10084;</span><span>&#10084;</span>
<span>&#10084;</span><span>&#10084;</span><span>&#10084;</span><span>&#10084;</span>
</div>
<h1><span class="title-hearts">&#10084;</span> Kate and Jack's Walks <span class="title-hearts">&#10084;</span></h1>
<p>Our trail adventures, rated together</p>
<div class="stats-bar">
<div class="stat">
Expand All @@ -475,11 +566,11 @@ <h3>Welcome! Who's walking?</h3>
<div class="names-row">
<div class="form-group">
<label for="name1Input">Person 1</label>
<input type="text" id="name1Input" placeholder="e.g. Alex">
<input type="text" id="name1Input" placeholder="e.g. Kate">
</div>
<div class="form-group">
<label for="name2Input">Person 2</label>
<input type="text" id="name2Input" placeholder="e.g. Jordan">
<input type="text" id="name2Input" placeholder="e.g. Jack">
</div>
</div>
<div class="form-actions">
Expand All @@ -503,6 +594,11 @@ <h3>Welcome! Who's walking?</h3>
<div class="walks-list" id="walksList"></div>
</main>

<footer>
<div class="footer-hearts">&#10084; &#10084; &#10084;</div>
<p>Made with love in Nova Scotia</p>
</footer>

<!-- Add/Edit Modal -->
<div class="modal-overlay" id="modalOverlay" onclick="closeModalOutside(event)">
<div class="modal">
Expand Down Expand Up @@ -564,6 +660,20 @@ <h4 id="rater2Label">Person 2</h4>
</div>

<div class="form-group" style="margin-top:1rem;">
<label for="walkRunkeeper">RunKeeper Map Link</label>
<input type="url" id="walkRunkeeper" placeholder="e.g. https://runkeeper.com/user/...">
<div class="hint">Paste your RunKeeper activity URL to attach the route map</div>
</div>

<div class="form-group">
<label>Photos</label>
<label class="photo-upload-btn" for="walkPhotos">&#128247; Add Photos</label>
<input type="file" id="walkPhotos" accept="image/*" multiple style="display:none" onchange="handlePhotoUpload(event)">
<div class="hint">Tap to add photos from your walk (stored locally)</div>
<div class="photo-preview" id="photoPreview"></div>
</div>

<div class="form-group">
<label for="walkNotes">Notes</label>
<textarea id="walkNotes" placeholder="How was the walk? Any highlights?"></textarea>
</div>
Expand All @@ -580,6 +690,12 @@ <h4 id="rater2Label">Person 2</h4>
</div>
</div>

<!-- Photo Lightbox -->
<div class="photo-lightbox" id="photoLightbox" onclick="closeLightbox()">
<button class="photo-lightbox-close" onclick="closeLightbox()">&times;</button>
<img id="lightboxImg" src="" alt="Walk photo">
</div>

<script>
// ---- Data layer (localStorage) ----
function getData() {
Expand Down Expand Up @@ -632,19 +748,25 @@ <h4 id="rater2Label">Person 2</h4>
document.getElementById('walkDistance').value = w.distance || '';
document.getElementById('walkDifficulty').value = w.difficulty || '';
document.getElementById('walkNotes').value = w.notes || '';
document.getElementById('walkRunkeeper').value = w.runkeeper || '';
document.getElementById('walkTags').value = (w.tags || []).join(', ');
setRating('rating1', w.rating1);
setRating('rating2', w.rating2);
currentPhotos = w.photos ? [...w.photos] : [];
renderPhotoPreview();
} else {
document.getElementById('walkName').value = '';
document.getElementById('walkLocation').value = '';
document.getElementById('walkDate').value = new Date().toISOString().split('T')[0];
document.getElementById('walkDistance').value = '';
document.getElementById('walkDifficulty').value = '';
document.getElementById('walkNotes').value = '';
document.getElementById('walkRunkeeper').value = '';
document.getElementById('walkTags').value = '';
clearRating('rating1');
clearRating('rating2');
currentPhotos = [];
renderPhotoPreview();
}

document.getElementById('modalOverlay').classList.add('active');
Expand Down Expand Up @@ -691,6 +813,8 @@ <h4 id="rater2Label">Person 2</h4>
rating1: r1,
rating2: r2,
notes: document.getElementById('walkNotes').value.trim(),
runkeeper: document.getElementById('walkRunkeeper').value.trim(),
photos: currentPhotos,
tags: document.getElementById('walkTags').value.split(',').map(t => t.trim()).filter(Boolean),
};

Expand Down Expand Up @@ -802,6 +926,8 @@ <h3>No walks yet!</h3>
${a ? `<div class="walk-avg">&#9733; ${a.toFixed(1)}</div>` : ''}
</div>
${w.notes ? `<div class="walk-notes">"${esc(w.notes)}"</div>` : ''}
${w.runkeeper ? `<a class="walk-map-link" href="${esc(w.runkeeper)}" target="_blank" rel="noopener">&#128506; View Route Map</a>` : ''}
${w.photos && w.photos.length ? `<div class="walk-photos">${w.photos.map((p,i) => `<img src="${p}" alt="Walk photo" class="walk-photo" onclick="viewPhoto('${w.id}',${i})">`).join('')}</div>` : ''}
${w.tags && w.tags.length ? `<div class="walk-tags">${w.tags.map(t => `<span class="tag">${esc(t)}</span>`).join('')}</div>` : ''}
<div class="walk-card-actions">
<button class="btn-edit" onclick="openModal('${w.id}')">Edit</button>
Expand Down Expand Up @@ -844,6 +970,65 @@ <h3>No walks yet!</h3>
return d.innerHTML;
}

// ---- Photos ----
let currentPhotos = [];

function handlePhotoUpload(event) {
const files = Array.from(event.target.files);
files.forEach(file => {
if (!file.type.startsWith('image/')) return;
const reader = new FileReader();
reader.onload = (e) => {
// Resize to save localStorage space
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
const maxSize = 600;
let w = img.width, h = img.height;
if (w > maxSize || h > maxSize) {
if (w > h) { h = Math.round(h * maxSize / w); w = maxSize; }
else { w = Math.round(w * maxSize / h); h = maxSize; }
}
canvas.width = w; canvas.height = h;
canvas.getContext('2d').drawImage(img, 0, 0, w, h);
currentPhotos.push(canvas.toDataURL('image/jpeg', 0.7));
renderPhotoPreview();
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
});
event.target.value = '';
}

function renderPhotoPreview() {
const container = document.getElementById('photoPreview');
if (!container) return;
container.innerHTML = currentPhotos.map((p, i) =>
`<div class="photo-preview-item">
<img src="${p}" alt="Photo ${i+1}">
<button class="remove-photo" onclick="removePhoto(${i})">&times;</button>
</div>`
).join('');
}

function removePhoto(index) {
currentPhotos.splice(index, 1);
renderPhotoPreview();
}

function viewPhoto(walkId, photoIndex) {
const data = getData();
const walk = data.walks.find(w => w.id === walkId);
if (!walk || !walk.photos || !walk.photos[photoIndex]) return;
document.getElementById('lightboxImg').src = walk.photos[photoIndex];
document.getElementById('photoLightbox').classList.add('active');
}

function closeLightbox() {
document.getElementById('photoLightbox').classList.remove('active');
}

// ---- Keyboard ----
document.addEventListener('keydown', e => {
if (e.key === 'Escape') closeModal();
Expand Down
Loading