Webbserverprogrammering 1

Show sourcecode

The following files exists in this folder. Click to view.

webbserverprogrammering/submissions/projekt-matkort-handler/public/js/

map.js
roulette.js

map.js

279 lines UTF-8 Windows (CRLF)
function initMap() {
  console.log("initMap started!");
  console.log(
    "Restaurants array length:",
    restaurants ? restaurants.length : "undefined",
    restaurants,
  );

  const centerMap = { lat: 59.2984738, lng: 18.0753175 };
  const mapElement = document.getElementById("google-map");

  if (!mapElement) {
    console.error("Critical: #google-map element not found in DOM!");
    return;
  }
  console.log("Map element found, initializing Google Map...");

  const mapOptions = {
    center: centerMap,
    zoom: 13,
    disableDefaultUI: true,
    gestureHandling: "greedy",
    styles: [
      { elementType: "geometry", stylers: [{ color: "#fce8d5" }] },
      { elementType: "labels.text.stroke", stylers: [{ color: "#fce8d5" }] },
      { elementType: "labels.text.fill", stylers: [{ color: "#e67d4f" }] },
      {
        featureType: "administrative.locality",
        elementType: "labels.text.fill",
        stylers: [{ color: "#c96236" }],
      },
      {
        featureType: "poi",
        elementType: "labels.text.fill",
        stylers: [{ color: "#c96236" }],
      },
      {
        featureType: "poi.park",
        elementType: "geometry",
        stylers: [{ color: "#f5d3b3" }],
      },
      {
        featureType: "poi.park",
        elementType: "labels.text.fill",
        stylers: [{ color: "#c96236" }],
      },
      {
        featureType: "road",
        elementType: "geometry",
        stylers: [{ color: "#ffffff" }],
      },
      {
        featureType: "road",
        elementType: "geometry.stroke",
        stylers: [{ color: "#fce8d5" }],
      },
      {
        featureType: "road",
        elementType: "labels.text.fill",
        stylers: [{ color: "#c96236" }],
      },
      {
        featureType: "road.highway",
        elementType: "geometry",
        stylers: [{ color: "#ffd0a6" }],
      },
      {
        featureType: "road.highway",
        elementType: "geometry.stroke",
        stylers: [{ color: "#fce8d5" }],
      },
      {
        featureType: "road.highway",
        elementType: "labels.text.fill",
        stylers: [{ color: "#c96236" }],
      },
      {
        featureType: "transit",
        elementType: "geometry",
        stylers: [{ color: "#fce8d5" }],
      },
      {
        featureType: "transit.station",
        elementType: "labels.text.fill",
        stylers: [{ color: "#e67d4f" }],
      },
      {
        featureType: "water",
        elementType: "geometry",
        stylers: [{ color: "#e67d4f" }],
      },
      {
        featureType: "water",
        elementType: "labels.text.fill",
        stylers: [{ color: "#ffffff" }],
      },
      {
        featureType: "water",
        elementType: "labels.text.stroke",
        stylers: [{ color: "#e67d4f" }],
      },
    ],
  };

  const map = new google.maps.Map(mapElement, mapOptions);

  const infoWindow = new google.maps.InfoWindow({
    minWidth: 280,
    maxWidth: 320,
  });

  // Close infoWindow automatically when clicking outside it on the map
  map.addListener("click", function () {
    infoWindow.close();
  });

  const bounds = new google.maps.LatLngBounds();

  // Custom SVG Marker Icon
  const mapPinSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 24 24" fill="#e53935" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-map-pin"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3"></circle></svg>`;
  const customIcon = {
    url: "data:image/svg+xml;charset=UTF-8," + encodeURIComponent(mapPinSvg),
    scaledSize: new google.maps.Size(36, 36),
    anchor: new google.maps.Point(18, 36),
  };

  if (restaurants && restaurants.length > 0) {
    for (let i = 0; i < restaurants.length; i++) {
      try {
        const lat = parseFloat(restaurants[i]["lat"]);
        const lng = parseFloat(restaurants[i]["lng"]);

        if (isNaN(lat) || isNaN(lng)) {
          console.warn("Invalid lat/lng for restaurant:", restaurants[i]);
          continue;
        }

        const restaurant = new google.maps.Marker({
          position: { lat: lat, lng: lng },
          map: map,
          title: restaurants[i]["name"],
          icon: customIcon,
        });

        google.maps.event.addListener(restaurant, "click", function () {
          // Show loading state first
          infoWindow.setContent(
            `<div class="info-window"><p>Laddar statistik...</p></div>`,
          );
          infoWindow.open(map, restaurant);

          // Fetch stats for this restaurant
          $.get(
            `api/restaurant_ajax.php?action=get_stats&restaurant_id=${restaurants[i]["id"]}`,
          ).done(function (data) {
            let statsHtml = "";
            if (data.avg_health !== null && data.avg_happy !== null) {
              const healthScore = parseFloat(data.avg_health || 0);
              let healthStars = "";
              for (let s = 1; s <= 5; s++) {
                if (healthScore >= s - 0.5) {
                  healthStars += `<i data-feather="star" style="color:#facc15; fill:#facc15; width:16px; height:16px;"></i>`;
                } else {
                  healthStars += `<i data-feather="star" style="color:#d1d5db; width:16px; height:16px;"></i>`;
                }
              }

              const happyScore = parseFloat(data.avg_happy || 0);
              let faceColor = "#22c55e"; // green default
              let faceType = "smile";

              if (happyScore < 1.5) {
                faceColor = "#ef4444"; // red
                faceType = "frown";
              } else if (happyScore < 2.5) {
                faceColor = "#facc15"; // yellow
                faceType = "meh";
              }

              let happyIcons = "";
              for (let s = 1; s <= 3; s++) {
                if (happyScore >= s - 0.5) {
                  happyIcons += `<i data-feather="${faceType}" style="color:${faceColor}; width:16px; height:16px;"></i>`;
                } else {
                  happyIcons += `<i data-feather="${faceType}" style="color:#d1d5db; width:16px; height:16px;"></i>`;
                }
              }

              statsHtml = `
            <div style="display:flex; align-items:center; gap:6px;">
              <span style="font-size:0.9em; color:#444;">Health score:</span>
              <div style="display:flex; gap:2px;">${healthStars}</div>
              <strong style="margin-left:4px; font-size:1.0em;">${data.avg_health}/5</strong>
            </div>
            <div style="display:flex; align-items:center; gap:6px; margin-top:6px;">
              <span style="font-size:0.9em; color:#444;">Happiness score:</span>
              <div style="display:flex; gap:2px;">${happyIcons}</div>
              <strong style="margin-left:4px; font-size:1.0em;">${data.avg_happy}/3</strong>
            </div>
          `;
            } else {
              statsHtml = `<p style="margin:0;"><em>Inga betyg ännu.</em></p>`;
            }

            let bookmarkIcon = "";
            if (isLoggedIn) {
              const fillClass = data.is_favorite ? "booked" : "unbooked";
              bookmarkIcon = `<span style="cursor:pointer; display:inline-block;" onclick="toggleBookmark(${restaurants[i]["id"]}, this)"><i data-feather="bookmark" class="bookmark-icon ${fillClass}"></i></span>`;
            }

            const infoWindowContent = `
          <div class="info-window" style="padding-top:4px;">
            <div class="info-header" style="display: flex; gap: 8px; align-items: center; padding-right: 16px;">
                <h3 style="margin: 0; font-size: 1.15em; line-height: 1.2;">${restaurants[i]["name"]}</h3>
                <div style="display: flex; align-items: center;">${bookmarkIcon}</div>
            </div>
            <address style="margin: 4px 0 12px; color: #555; font-size: 0.85em; font-style: normal;">
              ${restaurants[i]["address"]}
            </address>
            <div class="info-stats" style="background:#f4f4f4; padding:8px 12px; border-radius:4px;">
               ${statsHtml}
            </div>
          </div>
        `;

            infoWindow.setContent(infoWindowContent);
            // We have to recall feather.replace() inside the window to render the newly injected SVG
            setTimeout(() => {
              feather.replace();
            }, 10);
          });
        });

        infoWindow.addListener("closeclick", function () {
          map.fitBounds(bounds);
        });

        bounds.extend(new google.maps.LatLng(lat, lng));
      } catch (error) {
        console.error(`Error processing restaurant index ${i}:`, error);
      }
    }
    map.fitBounds(bounds);
  } else {
    console.warn("No restaurants data found or array is empty!");
  }
}

// Global function to handle bookmarking
window.toggleBookmark = function (restaurantId, element) {
  $.ajax({
    url: "api/restaurant_ajax.php?action=toggle_favorite",
    type: "POST",
    contentType: "application/json",
    data: JSON.stringify({ restaurant_id: restaurantId }),
    success: function (response) {
      if (response.error) {
        alert(response.error);
        return;
      }

      // Element is our <span> wrapper. We target the <svg> inside it.
      const icon = element.querySelector("svg") || element;

      // Toggle SVG icon class visual appearance
      if (response.status === "added") {
        icon.classList.remove("unbooked");
        icon.classList.add("booked");
      } else if (response.status === "removed") {
        icon.classList.remove("booked");
        icon.classList.add("unbooked");
      }
    },
    error: function () {
      alert("Något gick fel med att spara favoriten.");
    },
  });
};