diff --git a/README.md b/README.md index 82aec58..43b8797 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,6 @@ Docker configuration files are provided to guarantee a uniform development envir - Jack Turner - Luke Pring - Alec Thompson -- Sujal Shah - Victor Tepeniuc --- diff --git a/app/app.js b/app/app.js index 97e6de1..8805579 100644 --- a/app/app.js +++ b/app/app.js @@ -41,7 +41,7 @@ const apiRoutes = require("./models/api"); function requireLogin(req, res, next) { if (!req.session.user_id) { if (req.xhr || (req.headers.accept && req.headers.accept.indexOf('json') > -1)) { - return res.status(401).json({ error: "You must be logged in" }); + return res.status(401).json({ error: "You must be logged in" }); } return res.redirect(`/?login=true&continue=${encodeURIComponent(req.originalUrl)}`); } @@ -64,10 +64,12 @@ function testDBConnection(retries = 30) { if (retries > 0) { setTimeout(() => testDBConnection(retries - 1), 2000); } else { - console.error("DB connection failed"); + console.error(" ❌ DB connection failed"); + process.exit(1); } } else { - console.log("DB ready"); + console.log(" ✅ DB is ready"); + console.log(" 🍺 TapThat is running!"); } }); } diff --git a/app/models/api.js b/app/models/api.js index b120dcb..a9b8e13 100644 --- a/app/models/api.js +++ b/app/models/api.js @@ -3,7 +3,8 @@ const router = express.Router(); const db = require("../services/db"); router.get("/pubs", (req, res) => { - const sql = ` + const search = req.query.search; + let sql = ` SELECT pubs.id, pubs.name, @@ -25,7 +26,14 @@ router.get("/pubs", (req, res) => { ) `; - db.query(sql, (err, results) => { + const params = []; + if (search) { + sql += ` WHERE pubs.name LIKE ? OR pubs.address LIKE ?`; + const searchPattern = `%${search}%`; + params.push(searchPattern, searchPattern); + } + + db.query(sql, params, (err, results) => { if (err) { return res.status(500).json({ error: err }); } diff --git a/app/models/index.js b/app/models/index.js index 27b7668..9c5d20a 100644 --- a/app/models/index.js +++ b/app/models/index.js @@ -4,9 +4,30 @@ const db = require("../services/db"); const bcrypt = require("bcrypt"); router.get("/", (req, res) => { - res.render("index", { - user_email: req.session.user_email, - first_name: req.session.first_name + const sql = ` + SELECT pubs.id, pubs.name, pubs.address, COALESCE(AVG(reviews.rating), 0) AS average_rating + FROM pubs + LEFT JOIN reviews ON pubs.id = reviews.pub_id + GROUP BY pubs.id + ORDER BY average_rating DESC, pubs.name ASC + LIMIT 3 + `; + + db.query(sql, (err, results) => { + if (err) { + console.error(err); + return res.render("index", { + user_email: req.session.user_email, + first_name: req.session.first_name, + featuredPubs: [] + }); + } + + res.render("index", { + user_email: req.session.user_email, + first_name: req.session.first_name, + featuredPubs: results + }); }); }); diff --git a/app/models/users.js b/app/models/users.js index 84b8d80..e25504a 100644 --- a/app/models/users.js +++ b/app/models/users.js @@ -41,6 +41,7 @@ router.get("/:id", (req, res) => { JOIN pubs ON reviews.pub_id = pubs.id JOIN beers ON reviews.beer_id = beers.id WHERE reviews.user_id = ? + ORDER BY reviews.created_at DESC `; db.query(userQuery, [userId], (err, userResults) => { diff --git a/app/views/index.pug b/app/views/index.pug index 2138521..6b551c1 100644 --- a/app/views/index.pug +++ b/app/views/index.pug @@ -2,22 +2,54 @@ extends layout block content - main - section.hero - if first_name - h1 Ready for your next pint, #{first_name}? - else - h1 Ready for your next pint? - p Discover pubs, rate pints, and see what others recommend. + section.hero + if first_name + h1 Ready for your next pint, #{first_name}? + .search-container + form.search-form(method="GET" action="/map") + input.search-input(type="text" name="search" placeholder="Search for a pub near you..." value=search autocomplete="off") + button.search-button(type="submit") #[i.fa-solid.fa-magnifying-glass] + else + h1(style="margin-bottom: 30px;") Discover pubs, rate pints, and see what others recommend. + a(href=`/login`).btn Log in to get started - section - h2 Quick Actions - ul - li - a(href="/pubs") Find a pub near you - li - a(href="/users") Browse users and reviews + main.container(style="margin-top: 40px; margin-bottom: 60px;") + section.remove-styling + h2(style="text-align: left; margin-bottom: 30px; font-size: 2.5em;") Featured Pubs + if featuredPubs && featuredPubs.length > 0 + .section-row + each pub in featuredPubs + section.w-third.remove-styling + div.beer-item(style="text-align: left; padding: 0; display: flex; flex-direction: column; align-items: flex-start; justify-content: flex-start; height: 100%; box-sizing: border-box; overflow: hidden;") + - var lightness1 = 35 + (pub.id * 17) % 25 + - var lightness2 = 20 + (pub.id * 13) % 20 + - var bgStyle = `background: linear-gradient(135deg, hsl(29, 60%, ${lightness1}%), hsl(29, 70%, ${lightness2}%));` + div(style=`${bgStyle} width: 100%; height: 180px; display: flex; align-items: center; justify-content: center;`) + i.fa-solid.fa-beer-mug-empty(style="font-size: 5em; color: rgba(255, 255, 255, 0.5);") + div(style="padding: 20px; display: flex; flex-direction: column; align-items: flex-start; flex-grow: 1; width: 100%; box-sizing: border-box;") + strong(style="display: block; font-size: 1.3em; margin-bottom: 10px;") #{pub.name} + p(style="margin: 5px 0 10px 0; color: #555;") #[i.fa-solid.fa-location-dot(style="margin-right: 6px;")] #{pub.address} + - var avgRating = parseFloat(pub.average_rating) || 0 + p(style="margin: 5px 0 20px 0; color: #555; font-size: 1.1em;") #{avgRating.toFixed(1)} #[i.fa-solid.fa-star(style="color: #FFCC00;")] + a.view-profile-btn(href=`/pubs/${pub.id}`, style="margin-top: auto;") View Pub + else + p(style="text-align: left;") No featured pubs available. - section - h2 About - p TapThat helps you discover pubs, review pints, and see what others recommend. Use the map to find nearby pubs and check ratings before you go. \ No newline at end of file + section.remove-styling(style="margin-top: 60px;") + h2(style="text-align: left; margin-bottom: 30px; font-size: 2.5em;") Quick Actions + .section-row + section.w-third.remove-styling + div.beer-item(style="text-align: left; padding: 30px 20px;") + i.fa-solid.fa-map-location-dot(style="font-size: 2.5em; color: #B87333; margin-bottom: 15px;") + strong(style="display: block; font-size: 1.2em; margin-bottom: 15px;") Interactive Map + a.view-profile-btn(href="/map") Explore Map + section.w-third.remove-styling + div.beer-item(style="text-align: left; padding: 30px 20px;") + i.fa-solid.fa-beer-mug-empty(style="font-size: 2.5em; color: #B87333; margin-bottom: 15px;") + strong(style="display: block; font-size: 1.2em; margin-bottom: 15px;") Browse Pubs + a.view-profile-btn(href="/pubs") View Directory + section.w-third.remove-styling + div.beer-item(style="text-align: left; padding: 30px 20px;") + i.fa-solid.fa-users(style="font-size: 2.5em; color: #B87333; margin-bottom: 15px;") + strong(style="display: block; font-size: 1.2em; margin-bottom: 15px;") Community + a.view-profile-btn(href="/users") View Users \ No newline at end of file diff --git a/app/views/layout.pug b/app/views/layout.pug index ffa109d..76c24a7 100644 --- a/app/views/layout.pug +++ b/app/views/layout.pug @@ -41,8 +41,9 @@ html block content - footer - p © 2026 TapThat + block footer + footer + p © 2026 TapThat - Created by Luke Pring, Jack Turner, Alec Thompson, and Victor Tepeniuc - University of Roehampton London - #[a(href="https://github.com/jackoturner/SoftwareEngineeringModuleCoursework" target="_blank") View Project on GitHub] script(src="https://cdn.jsdelivr.net/npm/particles.js@2.0.0/particles.min.js") script(src="/static/bubbles.js") @@ -101,6 +102,7 @@ html script. function showLoginModal() { document.getElementById('loginModal').classList.add('show'); + setTimeout(() => window.dispatchEvent(new Event('resize')), 50); } document.addEventListener('DOMContentLoaded', () => { diff --git a/app/views/map.pug b/app/views/map.pug index 84841d7..6b06f5d 100644 --- a/app/views/map.pug +++ b/app/views/map.pug @@ -1,15 +1,93 @@ extends layout block content - main.container + style. + body { + display: flex; + flex-direction: column; + height: 100vh; + margin: 0; + overflow: hidden; + } + main { + flex: 1; + margin: 0 !important; + padding: 0 !important; + max-width: 100% !important; + } + .map-wrapper { + height: 100% !important; + width: 100%; + } + .map-search-overlay { + position: absolute; + top: 20px; + left: 50%; + transform: translateX(-50%); + background: white; + padding: 10px 20px; + border-radius: 20px; + box-shadow: 0 4px 15px rgba(0,0,0,0.2); + z-index: 1000; + display: flex; + align-items: center; + gap: 10px; + font-weight: 600; + color: #333; + } + .clear-search-btn { + color: #dc3545; + text-decoration: none; + font-weight: bold; + font-size: 1.1em; + } + .map-search-form { + position: absolute; + top: 20px; + left: 20px; + background: white; + padding: 5px 15px; + border-radius: 20px; + box-shadow: 0 4px 15px rgba(0,0,0,0.2); + z-index: 1000; + display: flex; + align-items: center; + gap: 5px; + } + .map-search-form input { + border: none; + outline: none; + font-size: 16px; + background: transparent; + width: 200px; + padding: 5px 0; + } + .map-search-form button { + background: none; + border: none; + cursor: pointer; + font-size: 18px; + } + main .map-wrapper #map + + form(action="/map" method="GET").map-search-form + input#mapSearchInput(type="text" name="search" placeholder="Search...") + button(type="submit") #[i.fa-solid.fa-magnifying-glass] - button#addReviewBtn.map-btn.add-btn ✚ - button#directionsBtn.map-btn.dir-btn ➹ + #searchOverlay.map-search-overlay(style="display: none;") + span#searchOverlayText + a(href="/map" class="clear-search-btn") ✕ + + button#addReviewBtn.map-btn.add-btn #[i.fa-solid.fa-plus] + button#directionsBtn.map-btn.dir-btn #[i.fa-solid.fa-location-arrow] link(rel="stylesheet", href="https://unpkg.com/leaflet/dist/leaflet.css") script(src="https://unpkg.com/leaflet/dist/leaflet.js") script(src="https://unpkg.com/leaflet-routing-machine/dist/leaflet-routing-machine.js") link(rel="stylesheet", href="https://unpkg.com/leaflet-routing-machine/dist/leaflet-routing-machine.css") - script(src="/static/maps.js") \ No newline at end of file + script(src="/static/maps.js") + +block footer + // Footer omitted on this page \ No newline at end of file diff --git a/app/views/pub.pug b/app/views/pub.pug index a309a12..ed6e227 100644 --- a/app/views/pub.pug +++ b/app/views/pub.pug @@ -6,7 +6,7 @@ block content p(style="margin-bottom: 30px;") img(src="/static/images/pin.png", alt="Location", width="24", height="24", style="vertical-align: middle; margin-right: 10px;") | #{pub.address} - a(href="/map").btn Get Directions + a(href=`/map?pub_id=${pub.id}`).btn Get Directions main.container .section-row diff --git a/app/views/user.pug b/app/views/user.pug index ef41d5f..7bb3c71 100644 --- a/app/views/user.pug +++ b/app/views/user.pug @@ -1,38 +1,52 @@ extends layout block content - .container - .profile-card - .profile-avatar - .avatar-large #{user.first_name.charAt(0)}#{user.last_name.charAt(0)} - .profile-details - h1 #{user.first_name} #{user.last_name} - p.email #{user.email} - - var joinDate = user.created_at ? new Date(user.created_at).toDateString() : 'Unknown' - p.joined Joined: #{joinDate} - p.stats Total reviews: #{reviews.length} + section.hero.pub-hero + .container(style="display: flex; align-items: center; justify-content: flex-start; gap: 20px;") + .avatar-large(style="box-shadow: 0 4px 10px rgba(0,0,0,0.3);") #{user.first_name.charAt(0)}#{user.last_name.charAt(0)} + div(style="text-align: left;") + h1(style="margin: 0; text-shadow: none;") #{user.first_name} #{user.last_name} + p(style="margin: 10px 0 0 0;") + - var joinDate = user.created_at ? new Date(user.created_at).toDateString() : 'Unknown' + if user.id == user_id + | #[i.fa-solid.fa-envelope(style="margin-right: 8px;")] #{user.email} - h2.reviews-title Reviews by #{user.first_name} - .reviews-grid - each review in reviews - .review-card - .review-header - span.pub-name 📍 #{review.pub_name} - span.beer-name 🍺 #{review.beer_name} - .review-rating - - var rating = parseInt(review.rating) || 0 - - for (var i = 1; i <= 5; i++) - if i <= rating - span.star-filled ★ - else - span.star-empty ☆ - span.rating-value (#{rating}/5) - if review.ai_pour_score - - var aiScore = parseFloat(review.ai_pour_score) || 0 - .ai-score AI Pour Score: #{aiScore.toFixed(1)}/5 - .review-comment "#{review.comment}" - - var reviewDate = review.created_at ? new Date(review.created_at).toLocaleDateString() : 'Unknown' - .review-date #{reviewDate} + main.container + .section-row + section.w-two-thirds.remove-styling + h2(style="margin-top: 0;") Reviews by #{user.first_name} + .section-row + each review in reviews + section.w-third + .review-header(style="margin-bottom: 8px;") + p(style="margin: 0; font-weight: bold;") #[i.fa-solid.fa-location-dot(style="margin-right: 6px;")] #{review.pub_name} + .review-rating(style="margin-bottom: 8px;") + - var rating = parseInt(review.rating) || 0 + - for (var i = 1; i <= 5; i++) + if i <= rating + span.star-filled(style="color: #FFCC00;") #[i.fa-solid.fa-star] + else + span.star-empty(style="color: #ccc;") #[i.fa-regular.fa-star] + span.rating-value(style="margin-left: 5px; font-size: 0.9em;") (#{rating}/5) + p(style="margin: 0 0 8px 0; color: #555;") #[i.fa-solid.fa-beer-mug-empty(style="margin-right: 4px;")] #{review.beer_name} + if review.ai_pour_score + - var aiScore = parseFloat(review.ai_pour_score) || 0 + p(style="margin: 0 0 8px 0; font-size: 0.9em; color: #777;") AI Pour Score: #{aiScore.toFixed(1)}/5 + p(style="margin: 0 0 10px 0; font-style: italic;") "#{review.comment}" + - var reviewDate = review.created_at ? new Date(review.created_at).toLocaleDateString() : 'Unknown' + p(style="margin: 0; font-size: 0.8em; color: #999; text-align: right;") #{reviewDate} - if reviews.length === 0 - p.no-reviews No reviews yet. \ No newline at end of file + if reviews.length === 0 + p.no-reviews(style="width: 100%;") No reviews yet. + + section.w-third.remove-styling(style="padding-left: 30px;") + h2(style="margin-top: 0;") User Stats + div.beer-item(style="margin-bottom: 15px; text-align: center;") + strong Total Reviews + p(style="margin: 5px 0 0 0; font-size: 1.5em; color: #B87333;") #{reviews.length} + div.beer-item(style="margin-bottom: 15px; text-align: center;") + strong Average rating + p(style="margin: 5px 0 0 0; font-size: 1.5em; color: #B87333;") #{reviews.length > 0 ? (reviews.reduce((acc, review) => acc + review.rating, 0) / reviews.length).toFixed(1) : 0} + div.beer-item(style="margin-bottom: 15px; text-align: center;") + strong Date joined + p(style="margin: 5px 0 0 0; font-size: 1.5em; color: #B87333;") #{joinDate} \ No newline at end of file diff --git a/app/views/users.pug b/app/views/users.pug index 6a83f39..1f273ef 100644 --- a/app/views/users.pug +++ b/app/views/users.pug @@ -1,62 +1,22 @@ extends layout block content - .container - .search-container + main.container + .search-container(style="margin-bottom: 40px;") form.search-form(method="GET" action="/users") - input.search-input(type="text" name="search" placeholder="Search by name or email" value=search autocomplete="off") - button.search-button(type="submit") 🔍 + input.search-input(type="text" name="search" placeholder="Search by name..." value=search autocomplete="off") + button.search-button(type="submit") #[i.fa-solid.fa-magnifying-glass] - .users-grid + .section-row each user in users - .user-card - .user-avatar - .avatar-placeholder #{user.first_name.charAt(0)}#{user.last_name.charAt(0)} - .user-info - h3.user-name #{user.first_name} #{user.last_name} - p.user-email #{user.email} - - var joinDate = user.created_at ? new Date(user.created_at).toDateString() : 'Unknown' - p.user-joined Joined: #{joinDate} + section.w-third.remove-styling + div.beer-item(style="text-align: center; padding: 30px 20px; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; box-sizing: border-box;") + .user-avatar(style="margin-bottom: 15px;") + .avatar-placeholder(style="margin: 0 auto;") #{user.first_name.charAt(0)}#{user.last_name.charAt(0)} + strong(style="display: block; font-size: 1.2em; margin-bottom: 10px;") #{user.first_name} #{user.last_name} - var avgRating = parseFloat(user.average_rating) || 0 - p.user-rating Avg rating: #{avgRating.toFixed(1)} ★ - .user-action - a.view-profile-btn(href=`/users/${user.id}`) View Profile + p(style="margin: 5px 0 20px 0; color: #555; font-size: 0.9em;") Avg rating: #{avgRating.toFixed(1)} #[i.fa-solid.fa-star(style="color: #FFCC00;")] + a.view-profile-btn(href=`/users/${user.id}`, style="margin-top: auto;") View Profile if users.length === 0 - p.no-results No users found. - - script. - const searchInput = document.getElementById('userSearch'); - const clearBtn = document.getElementById('clearSearch'); - const cards = document.querySelectorAll('.user-card'); - - function filterUsers() { - const term = searchInput.value.toLowerCase().trim(); - cards.forEach(card => { - const name = card.getAttribute('data-name'); - const email = card.getAttribute('data-email'); - const id = card.getAttribute('data-id'); - const rating = parseFloat(card.getAttribute('data-rating')) || 0; - - let matchesText = name.includes(term) || email.includes(term) || id.includes(term); - let matchesRating = false; - if (!isNaN(parseFloat(term)) && isFinite(term)) { - const ratingValue = parseFloat(term); - matchesRating = rating >= ratingValue; - } - - if (matchesText || matchesRating) { - card.style.display = ''; - } else { - card.style.display = 'none'; - } - }); - } - - searchInput.addEventListener('input', filterUsers); - - clearBtn.addEventListener('click', function() { - searchInput.value = ''; - filterUsers(); - searchInput.focus(); - }); \ No newline at end of file + p.no-results(style="width: 100%; text-align: center;") No users found. \ No newline at end of file diff --git a/schema.sql b/schema.sql index 7f8f4b3..c594aa1 100644 --- a/schema.sql +++ b/schema.sql @@ -74,7 +74,6 @@ CREATE TABLE reviews ( -- USERS DATA INSERT INTO users VALUES -(1,'Sujal','Shah','sujal@example.com','$2b$10$8qUOIqqAO8hHNj2aUvqEHO7pCSg23W6JYQ1V6ve/7zitt3pM7O/86','2005-04-12',NULL,NULL,NOW()), (2,'Luke','Pring','luke@example.com','$2b$10$8qUOIqqAO8hHNj2aUvqEHO7pCSg23W6JYQ1V6ve/7zitt3pM7O/86','2003-09-21',NULL,NULL,NOW()), (3,'Jack','Turner','jack@example.com','$2b$10$8qUOIqqAO8hHNj2aUvqEHO7pCSg23W6JYQ1V6ve/7zitt3pM7O/86','2001-06-10',NULL,NULL,NOW()), (4,'Victor','Tepeniuc','victor@example.com','$2b$10$8qUOIqqAO8hHNj2aUvqEHO7pCSg23W6JYQ1V6ve/7zitt3pM7O/86','1999-06-11',NULL,NULL,NOW()), @@ -136,7 +135,8 @@ INSERT INTO pubs VALUES (47,'The Seven Stars','53-54 Carey Street','WC2A 2JB',51.51620000,-0.11320000,'Tiny historic legal quarter pub',NOW()), (48,'The Ship & Shovell','1-3 Craven Passage','WC2N 5PH',51.50790000,-0.12450000,'Unique split pub near Charing Cross',NOW()), (49,'The White Cross','1 Water Lane','TW9 1TH',51.46080000,-0.30720000,'Riverside Richmond pub',NOW()), -(50,'The Trafalgar Tavern','Park Row','SE10 9NW',51.48230000,-0.00680000,'Grand Greenwich riverside pub',NOW()); +(50,'The Trafalgar Tavern','Park Row','SE10 9NW',51.48230000,-0.00680000,'Grand Greenwich riverside pub',NOW()), +(51,'The George Inn','High Street, Robertsbridge','TN32 5AW',50.98595400,0.48202100,'Historic inn in Robertsbridge, East Sussex',NOW()); -- BEERS DATA INSERT INTO beers VALUES @@ -207,11 +207,11 @@ INSERT INTO pub_beers (pub_id, beer_id, is_available) VALUES (47,13,1),(47,3,1),(47,11,1),(47,5,1),(47,14,1),(47,8,1),(47,9,1), (48,12,1),(48,7,1),(48,9,1),(48,1,1),(48,2,1),(48,11,1), (49,15,1),(49,12,1),(49,3,1),(49,9,1),(49,1,1),(49,6,1), -(50,10,1),(50,9,1),(50,3,1),(50,7,1),(50,13,1),(50,1,1),(50,5,1),(50,6,1),(50,12,1),(50,15,1); +(50,10,1),(50,9,1),(50,3,1),(50,7,1),(50,13,1),(50,1,1),(50,5,1),(50,6,1),(50,12,1),(50,15,1), +(51,1,1),(51,2,1),(51,5,1),(51,12,1),(51,13,1); -- REVIEWS DATA INSERT INTO reviews (user_id,pub_id,beer_id,rating,ai_pour_score,comment) VALUES -(1,1,1,5,4.8,'Perfect pint'), (2,1,2,4,4.2,'Nice lager'), (3,2,3,5,4.9,'Top IPA'), (4,2,6,4,4.1,'Smooth'), @@ -221,7 +221,6 @@ INSERT INTO reviews (user_id,pub_id,beer_id,rating,ai_pour_score,comment) VALUES (8,4,6,4,4.0,'Decent'), (9,5,1,5,4.7,'Excellent'), (10,5,5,4,4.3,'Good stout'), -(1,6,7,5,4.9,'Historic atmosphere, perfect ale'), (2,6,5,4,4.5,'Great Guinness in a hidden gem'), (3,7,5,5,4.8,'Best Guinness in Soho'), (4,7,10,4,4.2,'Excellent session IPA'), @@ -231,7 +230,6 @@ INSERT INTO reviews (user_id,pub_id,beer_id,rating,ai_pour_score,comment) VALUES (8,9,7,4,4.4,'Lovely old inn'), (9,10,2,4,4.1,'Riverside views with cold lager'), (10,10,6,5,4.8,'Perfect by the Thames'), -(1,11,3,5,4.9,'Classic Covent Garden IPA'), (2,11,13,4,4.3,'Landlord on great form'), (3,12,1,5,4.7,'Floral charm and excellent ale'), (4,12,7,4,4.5,'Iconic pub'), @@ -241,7 +239,6 @@ INSERT INTO reviews (user_id,pub_id,beer_id,rating,ai_pour_score,comment) VALUES (8,14,5,5,4.9,'Victorian masterpiece'), (9,15,3,5,4.8,'Bohemian vibes'), (10,15,10,4,4.3,'Half pints only but perfect'), -(1,16,5,5,4.7,'Secluded and cosy'), (2,16,7,4,4.4,'Great military history feel'), (3,17,1,5,4.9,'Dickens would approve'), (4,17,12,4,4.2,'Historic and hearty'), @@ -251,7 +248,6 @@ INSERT INTO reviews (user_id,pub_id,beer_id,rating,ai_pour_score,comment) VALUES (8,19,14,5,4.7,'Warm and inviting'), (9,20,3,5,4.8,'Elegant Mayfair pour'), (10,20,10,4,4.1,'Refined experience'), -(1,21,7,5,4.9,'Best pies and ales'), (2,21,5,4,4.4,'Unpretentious gem'), (3,22,1,4,4.2,'Historic City tavern'), (4,22,12,5,4.8,'Wine and ale mix perfect'), @@ -261,7 +257,6 @@ INSERT INTO reviews (user_id,pub_id,beer_id,rating,ai_pour_score,comment) VALUES (8,24,9,4,4.5,'South London favourite'), (9,25,10,4,4.2,'Jazz and great beer'), (10,25,14,5,4.8,'Atmospheric East End'), -(1,26,3,5,4.7,'Soho classic'), (2,26,7,4,4.4,'Pavement tables great'), (3,27,5,5,4.9,'Fun and offbeat'), (4,27,1,4,4.3,'Queer-friendly boozer'), @@ -271,7 +266,6 @@ INSERT INTO reviews (user_id,pub_id,beer_id,rating,ai_pour_score,comment) VALUES (8,29,9,4,4.5,'Bloomsbury hidden spot'), (9,30,14,4,4.2,'Real East End feel'), (10,30,10,5,4.9,'Piano and pints'), -(1,31,5,5,4.8,'Tiny and brilliant'), (2,31,3,4,4.4,'Fitzrovia favourite'), (3,32,7,5,4.9,'Pub of the year vibes'), (4,32,1,4,4.3,'Newington Green joy'), @@ -281,7 +275,6 @@ INSERT INTO reviews (user_id,pub_id,beer_id,rating,ai_pour_score,comment) VALUES (8,34,14,4,4.5,'Bohemian charm'), (9,35,10,4,4.1,'Central but calm'), (10,35,5,5,4.9,'New Cavendish delight'), -(1,36,3,5,4.7,'Brixton community hub'), (2,36,7,4,4.4,'Lyham Road local'), (3,37,1,4,4.3,'Nunhead classic'), (4,37,12,5,4.8,'Community spirit'), @@ -291,7 +284,6 @@ INSERT INTO reviews (user_id,pub_id,beer_id,rating,ai_pour_score,comment) VALUES (8,39,10,5,4.7,'Flask Walk perfect'), (9,40,5,5,4.8,'Holly Bush cosy'), (10,40,3,4,4.4,'Hampstead legend'), -(1,41,7,5,4.9,'Lamb''s Conduit classic'), (2,41,1,4,4.3,'Bloomsbury beauty'), (3,42,12,4,4.2,'Museum Tavern great'), (4,42,13,5,4.8,'Near British Museum'), @@ -301,7 +293,6 @@ INSERT INTO reviews (user_id,pub_id,beer_id,rating,ai_pour_score,comment) VALUES (8,44,5,5,4.9,'Kentish Town vibe'), (9,45,3,5,4.8,'Assembly House landmark'), (10,45,7,4,4.4,'Victorian splendour'), -(1,46,1,4,4.3,'Fleet Street Irish'), (2,46,12,5,4.7,'Tipperary classic'), (3,47,13,5,4.9,'Seven Stars tiny treasure'), (4,47,9,4,4.5,'Carey Street gem'), @@ -310,4 +301,5 @@ INSERT INTO reviews (user_id,pub_id,beer_id,rating,ai_pour_score,comment) VALUES (7,49,5,5,4.7,'Riverside Richmond'), (8,49,3,4,4.4,'White Cross views'), (9,50,7,5,4.9,'Trafalgar Tavern grand'), -(10,50,1,4,4.3,'Greenwich riverside perfect'); +(10,50,1,4,4.3,'Greenwich riverside perfect'), +(2,51,5,5,5.00,'Wonderful service from Ethan Pring'); diff --git a/static/bubbles.js b/static/bubbles.js index 68c469e..11dc6fb 100644 --- a/static/bubbles.js +++ b/static/bubbles.js @@ -1,15 +1,17 @@ -document.addEventListener("DOMContentLoaded", function() { +document.addEventListener("DOMContentLoaded", function () { if (typeof particlesJS !== "function") return; - const heroes = document.querySelectorAll('section.beer-hero, .beer-hero'); + const heroes = document.querySelectorAll('section.beer-hero, .beer-hero, #loginModal'); heroes.forEach((hero, index) => { const id = 'particles-hero-' + index; const div = document.createElement('div'); div.id = id; div.className = 'particles-bg'; - + // Setup hero for absolute positioning of the canvas - hero.style.position = 'relative'; - + if (window.getComputedStyle(hero).position === 'static') { + hero.style.position = 'relative'; + } + // zIndex 1 for children so they sit on top of the canvas Array.from(hero.children).forEach(child => { child.style.position = 'relative'; diff --git a/static/images/hero-bg.png b/static/images/hero-bg.png new file mode 100644 index 0000000..67268c7 Binary files /dev/null and b/static/images/hero-bg.png differ diff --git a/static/images/pub-placeholder.png b/static/images/pub-placeholder.png new file mode 100644 index 0000000..5d4aeb8 Binary files /dev/null and b/static/images/pub-placeholder.png differ diff --git a/static/maps.js b/static/maps.js index c2f8999..acd3753 100644 --- a/static/maps.js +++ b/static/maps.js @@ -18,15 +18,44 @@ async function fetchJson(url, options) { return res.json(); } -fetch("/api/pubs") +const urlParams = new URLSearchParams(window.location.search); +const searchQuery = urlParams.get('search'); +let apiUrl = "/api/pubs"; +if (searchQuery) { + apiUrl += `?search=${encodeURIComponent(searchQuery)}`; +} + +fetch(apiUrl) .then(res => res.json()) .then(data => { pubs = data; initMap(pubs); + + const urlParams = new URLSearchParams(window.location.search); + const targetPubId = urlParams.get('pub_id'); + if (targetPubId) { + const targetPub = pubs.find(p => p.id == targetPubId); + if (targetPub) { + if (!navigator.geolocation) { + alert("Geolocation not supported"); + return; + } + navigator.geolocation.getCurrentPosition( + position => { + const userLat = position.coords.latitude; + const userLng = position.coords.longitude; + showRoute(userLat, userLng, targetPub); + }, + () => { + alert("Unable to get your location"); + } + ); + } + } }); function initMap(pubs) { - map = L.map("map").setView([51.505, -0.09], 13); + map = L.map("map", { zoomControl: false }).setView([51.505, -0.09], 13); L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { attribution: "© OpenStreetMap contributors" @@ -67,9 +96,9 @@ function initMap(pubs) { const beerIcon = L.icon({ iconUrl: 'data:image/svg+xml;base64,' + btoa(beerSVG), - iconSize: [26, 32], - iconAnchor: [13, 32], - popupAnchor: [0, -32] + iconSize: [26, 32], + iconAnchor: [13, 32], + popupAnchor: [0, -32] }); // Beer-themed popup CSS @@ -95,22 +124,24 @@ function initMap(pubs) { padding: 12px 16px; font-size: 18px; font-weight: bold; - border-bottom: 4px solid #f4c95d; + border-bottom: 4px solid #b87334; text-align: center; + font-family: 'Instrument Serif', serif; } /* Golden beer body */ .beer-popup .popup-body { - background: #FFCC00; - color: #2c3e50; + background: #b87334; + color: #fff; padding: 14px 16px; line-height: 1.6; + font-family: 'Inter', sans-serif; } .beer-popup .popup-body a { - color: #2c3e50; + color: #fff; font-weight: bold; - text-decoration: underline; + text-decoration: none; } .beer-popup .popup-body a:hover { @@ -118,16 +149,18 @@ function initMap(pubs) { } .beer-popup .leaflet-popup-tip { - background: #FFCC00; + background: #b87334; } + `; - document.head.appendChild(style); + document.head.appendChild(style); + const markers = []; pubs.forEach(pub => { let reviewHTML = `No reviews yet`; if (pub.comment) { - reviewHTML = ` + reviewHTML = ` Latest Review:
Rating: ${pub.rating}/5
${pub.ai_pour_score ? `AI Score: ${pub.ai_pour_score}
` : ""} @@ -141,25 +174,53 @@ function initMap(pubs) { ${pub.name} `; - L.marker([pub.latitude, pub.longitude], { - icon: beerIcon + const marker = L.marker([pub.latitude, pub.longitude], { + icon: beerIcon }) .addTo(map) .bindPopup(popupHTML, { - className: 'beer-popup', + className: 'beer-popup', closeButton: true, maxWidth: 280 }); + + markers.push(marker); }); + + const urlParams = new URLSearchParams(window.location.search); + const searchQuery = urlParams.get('search'); + + if (searchQuery) { + const searchInput = document.getElementById('mapSearchInput'); + if (searchInput) { + searchInput.value = searchQuery; + } + + const overlay = document.getElementById('searchOverlay'); + const overlayText = document.getElementById('searchOverlayText'); + if (overlay && overlayText) { + overlayText.textContent = `Showing ${pubs.length} result${pubs.length === 1 ? '' : 's'} for "${searchQuery}"`; + overlay.style.display = 'flex'; + } + + if (markers.length > 0) { + const group = L.featureGroup(markers); + map.fitBounds(group.getBounds(), { padding: [50, 50], maxZoom: 16 }); + } + } } /* Button and logic for nearest pub */ @@ -236,6 +297,15 @@ function showRoute(userLat, userLng, pub) { L.latLng(userLat, userLng), L.latLng(pub.latitude, pub.longitude) ], - routeWhileDragging: false + routeWhileDragging: false, + fitSelectedRoutes: false // Disable the default fit so we can do it manually with padding }).addTo(map); + + routingControl.on('routesfound', function (e) { + const routes = e.routes; + if (routes && routes.length > 0) { + const bounds = L.latLngBounds(routes[0].coordinates); + map.fitBounds(bounds, { padding: [50, 50] }); + } + }); } \ No newline at end of file diff --git a/static/styles.css b/static/styles.css index de2d78b..49e145a 100644 --- a/static/styles.css +++ b/static/styles.css @@ -47,8 +47,13 @@ header { .hero { background-color: #afafaf; + background-image: linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5)), url('/static/images/hero-bg.png'); + background-size: cover; + background-position: center; + background-repeat: no-repeat; color: white; - padding: 40px 20px; + padding: 60px 20px; + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.6); text-align: center; margin: 0; border-radius: 0; @@ -65,6 +70,7 @@ header { flex-direction: column; justify-content: center; align-items: center; + text-shadow: none; animation: heroFadeIn 1s ease-out forwards; } @@ -612,7 +618,7 @@ section.remove-styling:hover { } .beer-item { - background-color: #ffffff; + background-color: #ffffff; border: 1px solid #f0f0f0; border-radius: 6px; padding: 12px 15px; @@ -641,24 +647,24 @@ section.remove-styling:hover { height: 55px; border-radius: 50%; border: none; - font-size: 48px; + font-size: 24px; color: white; cursor: pointer; display: flex; align-items: center; justify-content: center; - box-shadow: 0 4px 10px rgba(0,0,0,0.3); + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3); z-index: 1000; } .add-btn { left: 20px; - background-color: #28a745; + background-color: #B87333; } .dir-btn { right: 20px; - background-color: #dc3545; + background-color: #B87333; } .map-btn:hover { @@ -670,7 +676,7 @@ section.remove-styling:hover { position: fixed; z-index: 9999; inset: 0; - background: rgba(0,0,0,0.5); + background: rgba(0, 0, 0, 0.5); backdrop-filter: blur(4px); } @@ -686,7 +692,7 @@ section.remove-styling:hover { background: #ffffff; border-radius: 12px; overflow: hidden; - box-shadow: 0 10px 30px rgba(0,0,0,0.25); + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25); font-family: 'Inter', sans-serif; } @@ -794,6 +800,7 @@ textarea { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); @@ -824,17 +831,26 @@ body.login-page { z-index: 2; background: #ffffff; padding: 40px; - border-radius: 20px; - box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15); + border-radius: 12px; + box-shadow: 0 15px 35px rgba(184, 115, 51, 0.15); width: 100%; max-width: 400px; - border: 1px solid rgba(0, 0, 0, 0.05); + border: 1px solid rgba(184, 115, 51, 0.1); } +.login-container .close-btn { + color: #888 !important; + z-index: 10; +} +.login-container .close-btn:hover { + color: #333 !important; +} -.login-title { - color: #2c2c2c; +.modal-content h2.login-title { + color: #B87333; + background: transparent; + padding: 0; text-align: center; margin-bottom: 30px; font-size: 2.8rem; @@ -843,22 +859,24 @@ body.login-page { font-style: italic; } - -.login-form { - width: 100%; - margin: 0 auto; +.login-form { + width: 100%; + margin: 0 auto; display: flex; flex-direction: column; align-items: center; } -.form-group { margin-bottom: 24px; } +.form-group { + margin-bottom: 24px; + width: 100%; +} .form-label { display: block; - color: #16183a; + color: #2c2c2c; margin-bottom: 8px; - font-weight: 500; + font-weight: 600; text-align: left; } @@ -867,17 +885,18 @@ body.login-page { padding: 12px 16px; border: 1px solid #ddd; border-radius: 8px; - background: white; + background: #fdfdfd; font-size: 1rem; color: #333; transition: all 0.2s ease; + box-sizing: border-box; } - .form-input:focus { outline: none; background: #fff; - box-shadow: 0 0 0 4px rgba(212, 175, 55, 0.5); + border-color: #B87333; + box-shadow: 0 0 0 4px rgba(184, 115, 51, 0.2); } .login-button { @@ -895,7 +914,7 @@ body.login-page { } .login-button:hover { - background: #ff9800; + background: #a0632b; transform: translateY(-2px); box-shadow: 0 4px 12px rgba(184, 115, 51, 0.3); } @@ -914,16 +933,16 @@ body.login-page { } .error-popup { - position: fixed; + position: fixed; top: 40px; left: 50%; - transform: translateX(-50%); + transform: translateX(-50%); background-color: #d32f2f; color: white; padding: 14px 28px; border-radius: 12px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4); - z-index: 9999; + z-index: 9999; font-weight: 600; font-size: 1rem; letter-spacing: 0.5px; @@ -932,13 +951,14 @@ body.login-page { } @keyframes slideDownFade { - from { - opacity: 0; - transform: translate(-50%, -30px); + from { + opacity: 0; + transform: translate(-50%, -30px); } - to { - opacity: 1; - transform: translate(-50%, 0); + + to { + opacity: 1; + transform: translate(-50%, 0); } } @@ -960,7 +980,7 @@ body.login-page { } .profile-btn i { - font-size: 16px; + font-size: 16px; color: #333; } @@ -974,12 +994,12 @@ body.login-page { top: 52px; background: white; border-radius: 12px; - box-shadow: 0 10px 25px rgba(0,0,0,0.15); + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15); display: none; flex-direction: column; min-width: 180px; overflow: hidden; - border: 1px solid rgba(0,0,0,0.05); + border: 1px solid rgba(0, 0, 0, 0.05); } .profile-dropdown a { @@ -1020,11 +1040,13 @@ body.login-page { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } } + /* ========== SEARCH BAR (Google style, centered) ========== */ .search-container { display: flex; @@ -1051,7 +1073,7 @@ body.login-page { .search-input:focus { border-color: #B87333; - box-shadow: 0 1px 6px rgba(0,0,0,0.1); + box-shadow: 0 1px 6px rgba(0, 0, 0, 0.1); } .search-button { @@ -1063,7 +1085,7 @@ body.login-page { border: none; font-size: 20px; cursor: pointer; - color: #777; + color: #B87333; } /* ========== USERS GRID (cards) ========== */ @@ -1078,7 +1100,7 @@ body.login-page { background: white; border-radius: 16px; padding: 20px; - box-shadow: 0 2px 8px rgba(0,0,0,0.08); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); transition: transform 0.2s, box-shadow 0.2s; display: flex; flex-direction: column; @@ -1088,7 +1110,7 @@ body.login-page { .user-card:hover { transform: translateY(-4px); - box-shadow: 0 12px 20px rgba(0,0,0,0.1); + box-shadow: 0 12px 20px rgba(0, 0, 0, 0.1); } .avatar-placeholder { @@ -1115,7 +1137,8 @@ body.login-page { font-size: 0.9rem; } -.user-joined, .user-rating { +.user-joined, +.user-rating { font-size: 0.85rem; color: #555; margin: 6px 0; @@ -1147,7 +1170,7 @@ body.login-page { align-items: center; gap: 30px; flex-wrap: wrap; - box-shadow: 0 4px 12px rgba(0,0,0,0.05); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); } .avatar-large { @@ -1189,12 +1212,12 @@ body.login-page { background: white; border-radius: 16px; padding: 20px; - box-shadow: 0 2px 8px rgba(0,0,0,0.08); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); transition: 0.2s; } .review-card:hover { - box-shadow: 0 8px 20px rgba(0,0,0,0.1); + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1); } .review-header { @@ -1206,7 +1229,8 @@ body.login-page { padding-bottom: 8px; } -.pub-name, .beer-name { +.pub-name, +.beer-name { font-size: 1rem; } @@ -1244,8 +1268,18 @@ body.login-page { text-align: right; } -.no-results, .no-reviews { +.no-results, +.no-reviews { text-align: center; padding: 40px; color: #777; +} + +.leaflet-routing-container.leaflet-bar.leaflet-control { + border-radius: 30px; + padding: 0 10px; +} + +.leaflet-routing-container h2 { + font-size: 24px; } \ No newline at end of file