diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..bdea30c85 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,33 @@ +name: CI + +on: + push: + branches-ignore: + - master + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.3' + bundler-cache: true + + - name: Build site + run: bundle exec jekyll build --config _config.yml + env: + BUNDLE_PATH: vendor/bundle + + - name: Check HTML + run: | + bundle exec htmlproofer ./_site \ + --disable-external \ + --ignore-urls "/localhost/" \ + --allow-hash-href + continue-on-error: true diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 000000000..b9b3b0de0 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +3.3.11 diff --git a/Gemfile b/Gemfile index 1498e641c..d37e11d5e 100644 --- a/Gemfile +++ b/Gemfile @@ -1,4 +1,7 @@ source "https://rubygems.org" gem "github-pages", group: :jekyll_plugins +gem "jekyll-seo-tag" +gem "jekyll-sitemap" +gem "jekyll-feed" gem 'wdm', '>= 0.1.0' if Gem.win_platform? \ No newline at end of file diff --git a/_config.yml b/_config.yml index 1d96b5394..92d107004 100644 --- a/_config.yml +++ b/_config.yml @@ -12,6 +12,9 @@ open_new_tab : true # Opens external URLs in ### Plugins ### plugins: - jemoji + - jekyll-seo-tag + - jekyll-sitemap + - jekyll-feed ### Navbar Settings ### @@ -61,6 +64,9 @@ collections: research: output: true permalink: /research/:name + trips: + output: true + permalink: /trips/:name elements: # For Documentation Only output: true # For Documentation Only @@ -99,6 +105,11 @@ defaults: type: "research" values: layout: "page" + - scope: + path: "" + type: "trips" + values: + layout: "trip" - scope: # For Documentation Only path: "" # For Documentation Only type: "elements" # For Documentation Only @@ -117,3 +128,5 @@ exclude: - CONTRIBUTING.md - LICENSE - "*.log" + - build/ + - vendor/ diff --git a/_data/social-media.yml b/_data/social-media.yml index 28934e475..8131bee9e 100644 --- a/_data/social-media.yml +++ b/_data/social-media.yml @@ -22,22 +22,22 @@ email: # color : 3b5998 github: - url : https://www.github.com/ + url : https://www.github.com/vishalgattani icon : fab fa-github color : 333333 gitlab: - url : https://gitlab.com/ + url : https://gitlab.com/vishalgattani icon : fab fa-gitlab color : fca326 instagram: - url : https://www.instagram.com/ + url : https://www.instagram.com/vishalgattani icon : fab fa-instagram color : 405de6 linkedin: - url : https://www.linkedin.com/in/ + url : https://www.linkedin.com/in/vishalgattani icon : fab fa-linkedin-in color : 007bb5 diff --git a/_includes/blog/tags.html b/_includes/blog/tags.html index 23e66082e..d584dfd0d 100644 --- a/_includes/blog/tags.html +++ b/_includes/blog/tags.html @@ -1,17 +1,41 @@ - -{%- assign tags = blank -%} + +{%- assign all_tags = "" | split: "" -%} {%- for post in site.posts -%} - {%- assign post_tags = post.tags | join:'|' | append:'|' -%} - {%- if post_tags != '|' -%} - {%- assign tags = tags | append:post_tags -%} - {%- endif -%} + {%- for tag in post.tags -%} + {%- assign all_tags = all_tags | push: tag -%} + {%- endfor -%} {%- endfor -%} -{%- assign tags = tags | split:'|' | uniq | sort -%} +{%- assign unique_tags = all_tags | uniq | sort -%} + + +
+ {%- for tag in unique_tags -%} + {%- assign count = 0 -%} + {%- for t in all_tags -%} + {%- if t == tag -%}{%- assign count = count | plus: 1 -%}{%- endif -%} + {%- endfor -%} + + + {{ tag }} {{ count }} + + + {%- endfor -%} +
+ +
+ + +{% for tag in unique_tags %} + {%- assign count = 0 -%} + {%- for t in all_tags -%} + {%- if t == tag -%}{%- assign count = count | plus: 1 -%}{%- endif -%} + {%- endfor -%} - -{% for tag in tags %}
-

{{ tag }}

+

+ {{ tag }} + {{ count }} +

    {% for post in site.posts %} {%- if post.tags contains tag -%} @@ -21,11 +45,11 @@

    {{ tag }}

    {%- assign url = post.url | relative_url -%} {%- endif -%}
  1. {{ post.title }}
  2. - - {{ post.date | date_to_long_string }} + โ€” {{ post.date | date_to_long_string }}
    {%- endif -%} {% endfor %}

-{% endfor %} \ No newline at end of file +{% endfor %} diff --git a/_includes/landing.html b/_includes/landing.html index 7c559c6e7..97d2fce7a 100644 --- a/_includes/landing.html +++ b/_includes/landing.html @@ -11,5 +11,12 @@

{{ site.description }}

+
+ Projects + Blog + Trips + Research +
+ \ No newline at end of file diff --git a/_includes/trips/index.html b/_includes/trips/index.html new file mode 100644 index 000000000..505822363 --- /dev/null +++ b/_includes/trips/index.html @@ -0,0 +1,9 @@ + + + + +
+ {% for trip in site.trips reversed %} + {% include trips/trip-card.html %} + {% endfor %} +
diff --git a/_includes/trips/map-section.html b/_includes/trips/map-section.html new file mode 100644 index 000000000..1e4d1836b --- /dev/null +++ b/_includes/trips/map-section.html @@ -0,0 +1,18 @@ +{% comment %} + Usage in trip markdown: + {% include trips/map-section.html id="day2" stops=page.day2_stops type="road" %} + + `id` โ€” unique string, used as the HTML element ID (required) + `stops` โ€” array from frontmatter (required) + `type` โ€” road | flight | place (default: road) +{% endcomment %} + +
+ + diff --git a/_includes/trips/trip-card.html b/_includes/trips/trip-card.html new file mode 100644 index 000000000..2fab28988 --- /dev/null +++ b/_includes/trips/trip-card.html @@ -0,0 +1,37 @@ +{% assign card_id = trip.slug %} + +
+ +
+
+
{{ trip.title }}
+ {% if trip.description %} +

{{ trip.description }}

+ {% endif %} +

+ {% if trip.trip_type == "road" %} + ๐Ÿš— Road Trip + {% elsif trip.trip_type == "flight" %} + โœˆ๏ธ Flight + {% else %} + ๐Ÿ“ Place + {% endif %} + {% if trip.date %} + {{ trip.date | date: "%b %Y" }} + {% endif %} + {% for tag in trip.tags %} + {{ tag }} + {% endfor %} +

+
+
+
+ + diff --git a/_layouts/trip.html b/_layouts/trip.html new file mode 100644 index 000000000..38ec4602a --- /dev/null +++ b/_layouts/trip.html @@ -0,0 +1,39 @@ +--- +layout: default +--- + + + + + +
+ +

{{ page.title }}

+ +
+ {% if page.trip_type == "road" %} + ๐Ÿš— Road Trip + {% elsif page.trip_type == "flight" %} + โœˆ๏ธ Flight + {% else %} + ๐Ÿ“ Place + {% endif %} + {% if page.date %} + {{ page.date | date: "%B %Y" }} + {% endif %} + {% for tag in page.tags %} + {{ tag }} + {% endfor %} +
+ +
+ +
+ {{ content }} +
+ +
+ + diff --git a/_posts/2024-08-15-how-to-add-a-trip.md b/_posts/2024-08-15-how-to-add-a-trip.md new file mode 100644 index 000000000..242fc3b91 --- /dev/null +++ b/_posts/2024-08-15-how-to-add-a-trip.md @@ -0,0 +1,275 @@ +--- +title: How to Add a Trip +tags: [Site, Documentation, Maps] +style: fill +color: secondary +description: A complete reference for writing trip markdown files โ€” frontmatter fields, all marker types, section maps, and naming conventions. +--- + +Every trip lives as a single markdown file in `_trips/`. The filename becomes the URL: `_trips/2024-09-my-road-trip.md` โ†’ `/trips/2024-09-my-road-trip`. + +--- + +## File naming + +``` +_trips/YYYY-MM-description.md +``` + +Examples: +``` +_trips/2024-07-colorado.md +_trips/2024-09-college-park-to-waitsfield.md +_trips/2025-03-tokyo.md +``` + +--- + +## Frontmatter โ€” top-level fields + +```yaml +--- +title: "College Park, MD โ†’ Waitsfield, VT" +date: 2024-09-14 +trip_type: road # road | flight | place +description: "One-liner shown on the card." +tags: [Road Trip, East Coast, Fall Foliage] +--- +``` + +| Field | Required | Values | +|-------|----------|--------| +| `title` | yes | Free text. Use `โ†’` for A-to-B trips. | +| `date` | yes | `YYYY-MM-DD` | +| `trip_type` | yes | `road` ยท `flight` ยท `place` | +| `description` | recommended | Shown on the card and in SEO tags | +| `tags` | optional | Array of strings | + +--- + +## The `stops` array โ€” overview map + +`stops` drives the **top-of-page map**. For `road` trips it also provides the waypoints for the OSRM route line. + +```yaml +stops: + - label: "College Park, MD" + lat: 38.9807 + lng: -76.9369 + + - label: "Waitsfield, VT" + lat: 44.1906 + lng: -72.8317 +``` + +Every stop has `lat`, `lng`, and an optional `type`. When `type` is omitted it defaults to `pin`. + +### Stop types + +#### `pin` โ€” dropped marker (default) + +```yaml +- label: "Denver, CO" + lat: 39.7392 + lng: -104.9903 +``` + +#### `numbered` โ€” pin with a number badge + +```yaml +- label: "Boulder" + lat: 40.0150 + lng: -105.2705 + type: numbered + number: 1 + note: "Lunch on Pearl Street" +``` + +#### `emoji` โ€” any emoji as the icon + +```yaml +- label: "Estes Park" + lat: 40.3775 + lng: -105.5224 + type: emoji + icon: "๐ŸฆŒ" + note: "Elk everywhere" +``` + +#### `circle` โ€” flat filled dot + +Good for photo spots and minor waypoints that don't need a full pin. + +```yaml +- label: "Bear Lake Trailhead" + lat: 40.3120 + lng: -105.6456 + type: circle + color: "#28a745" + note: "Arrive before 7am" +``` + +#### `label` โ€” floating text, no pin + +Use for region names or area annotations. + +```yaml +- label: "Rocky Mountain National Park" + lat: 40.4000 + lng: -105.6800 + type: label +``` + +#### `polygon` โ€” shaded region + +```yaml +- label: "RMNP Boundary" + lat: 40.35 + lng: -105.65 + type: polygon + color: "#6f42c1" + coords: + - [40.27, -105.75] + - [40.27, -105.55] + - [40.50, -105.55] + - [40.50, -105.75] +``` + +`lat`/`lng` on a polygon stop are ignored for routing โ€” they're only used to set the popup anchor if you click the shape. + +#### `circle_area` โ€” radius bubble + +```yaml +- label: "Moraine Park Campground" + lat: 40.3760 + lng: -105.6200 + type: circle_area + radius: 500 # metres + color: "#fd7e14" + note: "Site 14B" +``` + +#### `freeform` โ€” arbitrary polyline + +For hikes, bike routes, ferries, ski runs โ€” anything that isn't a road route. + +```yaml +- label: "Emerald Lake Trail" + lat: 40.3120 + lng: -105.6456 + type: freeform + color: "#dc3545" + dashed: false # true for dotted line + coords: + - [40.3120, -105.6456] + - [40.3082, -105.6521] + - [40.3055, -105.6592] +``` + +### Shared optional fields + +Any stop type accepts: + +| Field | Effect | +|-------|--------| +| `note` | Second line of text in the popup when the marker is clicked | +| `color` | Hex color โ€” overrides the default blue `#007bff` | + +--- + +## Section maps inside the markdown body + +The top-of-page `stops` map is the **overview**. To show a focused map for a specific day or leg, define a named stops array in frontmatter and drop an include in the body. + +### Naming convention + +Name the array `_stops`. Common patterns: + +```yaml +day1_stops: [...] +day2_stops: [...] +morning_stops: [...] +hike_stops: [...] +flight_stops: [...] +``` + +There is no special meaning to `day1` vs `day2` โ€” the names are just HTML element IDs. The only rules are: **end in `_stops`** (by convention) and **be unique within the file**. + +### Frontmatter + +```yaml +day1_stops: + - label: "Denver" + lat: 39.7392 + lng: -104.9903 + type: numbered + number: 1 + - label: "Boulder" + lat: 40.0150 + lng: -105.2705 + type: numbered + number: 2 + +day2_stops: + - label: "Trail Ridge Road" + lat: 40.3960 + lng: -105.6070 + type: emoji + icon: "๐Ÿ”๏ธ" + - label: "Milner Pass" + lat: 40.4203 + lng: -105.8125 + type: numbered + number: 2 +``` + +### Include in markdown body + +```liquid +{% raw %}{% include trips/map-section.html id="day1" stops=page.day1_stops type="road" %}{% endraw %} +``` + +| Parameter | Required | Notes | +|-----------|----------|-------| +| `id` | yes | Must match the prefix of your `_stops` key โ€” used as the HTML element ID. Must be unique on the page. | +| `stops` | yes | `page._stops` | +| `type` | optional | `road` (default) ยท `flight` ยท `place` | + +--- + +## Full minimal example + +```yaml +--- +title: "San Francisco โ†’ Portland" +date: 2025-03-15 +trip_type: road +description: "PCH the whole way up." +tags: [Road Trip, West Coast] + +stops: + - label: "San Francisco, CA" + lat: 37.7749 + lng: -122.4194 + - label: "Portland, OR" + lat: 45.5051 + lng: -122.6750 + +day1_stops: + - { label: "San Francisco", lat: 37.7749, lng: -122.4194, type: numbered, number: 1 } + - { label: "Santa Cruz", lat: 36.9741, lng: -122.0308, type: emoji, icon: "๐Ÿ„" } + - { label: "Monterey", lat: 36.6002, lng: -121.8947, type: numbered, number: 3 } +--- + +## Day 1 โ€” SF to Monterey + +{% raw %}{% include trips/map-section.html id="day1" stops=page.day1_stops type="road" %}{% endraw %} + +Took PCH the whole way. 17-Mile Drive is worth the $12 toll. +``` + +--- + +## Live demo + +The trip [Colorado Rockies โ€” Map Features Demo](/trips/2024-08-rockies-demo) exercises every marker type and section map in one file. Read the source at [`_trips/2024-08-rockies-demo.md`](https://github.com/vishalgattani/vishalgattani.github.io/blob/feat/site-improvements/_trips/2024-08-rockies-demo.md). diff --git a/_trips/2024-07-colorado.md b/_trips/2024-07-colorado.md new file mode 100644 index 000000000..d5401ac2e --- /dev/null +++ b/_trips/2024-07-colorado.md @@ -0,0 +1,23 @@ +--- +title: "Colorado" +date: 2024-07-04 +trip_type: place +description: "A week in the Rockies โ€” hiking, altitude, and too much coffee." +tags: [Colorado, Hiking, Mountains] +stops: + - label: "Denver, CO" + lat: 39.7392 + lng: -104.9903 +--- + +## Overview + +Spent a week exploring Colorado โ€” Denver as a base, day trips into Rocky Mountain National Park, and a couple of nights up in Estes Park. + +## Rocky Mountain National Park + +Trail Ridge Road tops out above 12,000 ft. Worth it for the views even if your lungs disagree. + +## Estes Park + +Classic mountain town. Crowded in July but the Stanley Hotel is worth the detour if you're a Shining fan. diff --git a/_trips/2024-08-rockies-demo.md b/_trips/2024-08-rockies-demo.md new file mode 100644 index 000000000..473f503a9 --- /dev/null +++ b/_trips/2024-08-rockies-demo.md @@ -0,0 +1,147 @@ +--- +title: "Colorado Rockies โ€” Map Features Demo" +date: 2024-08-10 +trip_type: road +description: "A demo trip showing every marker type available โ€” pins, emojis, numbered stops, labels, polygons, areas, and freeform routes." +tags: [Demo, Colorado, Hiking] + +# `stops` is the overview map at the top of the page. +# It also drives the OSRM route line โ€” only pin/circle/numbered/emoji/label +# types are used as routing waypoints. Everything else (polygon, circle_area, +# freeform) is drawn as an overlay on top. +stops: + - label: "Denver, CO" + lat: 39.7392 + lng: -104.9903 + + - label: "Boulder, CO" + lat: 40.0150 + lng: -105.2705 + type: numbered + number: 1 + note: "Pearl Street Mall, Chautauqua Park" + + - label: "Estes Park" + lat: 40.3775 + lng: -105.5224 + type: emoji + icon: "๐ŸฆŒ" + note: "Elk everywhere. Seriously." + + - label: "Bear Lake Trailhead" + lat: 40.3120 + lng: -105.6456 + type: circle + color: "#28a745" + note: "Arrive before 7am or the parking lot is full" + + - label: "Rocky Mountain National Park" + lat: 40.4000 + lng: -105.6800 + type: label + + - label: "RMNP Boundary (approximate)" + lat: 40.3500 + lng: -105.6500 + type: polygon + color: "#6f42c1" + coords: + - [40.27, -105.75] + - [40.27, -105.55] + - [40.50, -105.55] + - [40.50, -105.75] + + - label: "Campsite (~500m radius)" + lat: 40.3760 + lng: -105.6200 + type: circle_area + radius: 500 + color: "#fd7e14" + note: "Moraine Park Campground" + + - label: "Emerald Lake Trail" + lat: 40.3120 + lng: -105.6456 + type: freeform + color: "#dc3545" + coords: + - [40.3120, -105.6456] + - [40.3098, -105.6489] + - [40.3082, -105.6521] + - [40.3065, -105.6554] + - [40.3055, -105.6592] + +# Section maps: name them anything + _stops. +# day1_stops, day2_stops, morning_stops, hike_stops โ€” all work the same way. +# Use {% include trips/map-section.html id="day1" stops=page.day1_stops type="road" %} +# The `id` must be unique within the page (it becomes the HTML element ID). +day1_stops: + - label: "Denver โ€” start" + lat: 39.7392 + lng: -104.9903 + type: numbered + number: 1 + - label: "Boulder" + lat: 40.0150 + lng: -105.2705 + type: numbered + number: 2 + note: "Lunch at Chautauqua" + - label: "Estes Park" + lat: 40.3775 + lng: -105.5224 + type: numbered + number: 3 + note: "Check in, elk walk at dusk" + +day2_stops: + - label: "Trail Ridge Road start" + lat: 40.3960 + lng: -105.6070 + type: numbered + number: 1 + - label: "Alpine Visitor Center (11,796 ft)" + lat: 40.4575 + lng: -105.7131 + type: emoji + icon: "๐Ÿ”๏ธ" + note: "Highest paved road in the US" + - label: "Milner Pass โ€” Continental Divide" + lat: 40.4203 + lng: -105.8125 + type: numbered + number: 3 +--- + +## Overview + +This is a **demo trip** โ€” every marker type and section map convention is shown here. See the blog post [How to Add a Trip](/blog/how-to-add-a-trip) for the full reference. + +--- + +## Marker types at a glance + +| Type | When to use | +|------|-------------| +| `pin` (default) | Main stops, towns | +| `numbered` | Ordered itinerary steps | +| `emoji` | Restaurants ๐Ÿ”, summits ๐Ÿ”๏ธ, wildlife ๐ŸฆŒ | +| `circle` | Photo spots, minor waypoints | +| `label` | Region names, floating text โ€” no pin | +| `polygon` | Park boundaries, neighborhoods | +| `circle_area` | Campsite, "stayed near here" with radius | +| `freeform` | Hikes, bike routes, ferries | + +--- + +## Day 1 โ€” Denver โ†’ Boulder โ†’ Estes Park + +{% include trips/map-section.html id="day1" stops=page.day1_stops type="road" %} + +Drive up US-36 from Denver to Boulder, grab lunch on Pearl Street, then follow Canyon Boulevard into the mountains. Estes Park is about 45 minutes from Boulder. + +## Day 2 โ€” Trail Ridge Road + +{% include trips/map-section.html id="day2" stops=page.day2_stops type="road" %} + +The road closes roughly October to late May. Check [nps.gov/romo](https://www.nps.gov/romo) before going. diff --git a/_trips/2024-09-college-park-to-waitsfield.md b/_trips/2024-09-college-park-to-waitsfield.md new file mode 100644 index 000000000..62d486983 --- /dev/null +++ b/_trips/2024-09-college-park-to-waitsfield.md @@ -0,0 +1,39 @@ +--- +title: "College Park, MD โ†’ Waitsfield, VT" +date: 2024-09-14 +trip_type: road +description: "Fall foliage road trip up the East Coast โ€” 10 hours, one tank of gas, zero regrets." +tags: [Road Trip, East Coast, Fall Foliage] +stops: + - label: "College Park, MD" + lat: 38.9807 + lng: -76.9369 + - label: "Waitsfield, VT" + lat: 44.1906 + lng: -72.8317 + +day2_stops: + - label: "Burlington, VT" + lat: 44.4759 + lng: -73.2121 + - label: "Stowe, VT" + lat: 44.4654 + lng: -72.6874 + - label: "Waitsfield, VT" + lat: 44.1906 + lng: -72.8317 +--- + +## Day 1 โ€” College Park to Burlington + +Straight shot up I-95 then cut across to Vermont. The moment you cross into Vermont the highway disappears and the trees take over. + +## Day 2 โ€” Burlington to Waitsfield + +{% include trips/map-section.html id="day2" stops=page.day2_stops type="road" %} + +Burlington farmers market in the morning, then wound south through Stowe and down into the Mad River Valley. Peak foliage was about a week out but the colors were already going. + +## Mad River Glen + +Famously co-op owned, no snowboards allowed. In September it's just a quiet gondola ride up to an empty summit. diff --git a/assets/js/trip-map.js b/assets/js/trip-map.js new file mode 100644 index 000000000..33c54c199 --- /dev/null +++ b/assets/js/trip-map.js @@ -0,0 +1,174 @@ +/* trip-map.js โ€” renders a Leaflet map from a stops array + trip_type. + * + * Stop types: + * pin โ€” default dropped marker (default when type is omitted) + * circle โ€” flat filled dot, good for photo spots / waypoints + * numbered โ€” pin with a number badge (requires `number` field) + * emoji โ€” custom emoji icon (requires `icon` field, e.g. "๐Ÿ”") + * polygon โ€” shaded area (requires `coords` array of [lat,lng] pairs) + * circle_area โ€” radius around a point (requires `radius` in metres) + * label โ€” permanent text label with no marker (requires `label`) + * freeform โ€” polyline drawn through `coords` array (hike, bike, ferry) + * + * Any stop with a `note` field gets a richer popup body. + * Any stop with a `color` field overrides the default #007bff blue. + */ + +window.TripMap = { + + render: function (mapId, stops, tripType, opts) { + opts = opts || {}; + var interactive = opts.interactive !== false; + var height = opts.height || '400px'; + + var el = document.getElementById(mapId); + if (!el) return; + el.style.height = height; + + var mapOpts = interactive ? {} : { + dragging: false, zoomControl: false, scrollWheelZoom: false, + doubleClickZoom: false, keyboard: false, tap: false, attributionControl: false + }; + var map = L.map(mapId, mapOpts); + + L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + attribution: interactive + ? 'ยฉ OpenStreetMap contributors' + : '', + maxZoom: 18 + }).addTo(map); + + if (!stops || stops.length === 0) return; + + var pinStops = stops.filter(function (s) { + return !s.type || s.type === 'pin' || s.type === 'circle' || + s.type === 'numbered' || s.type === 'emoji' || s.type === 'label'; + }); + var latlngs = pinStops.map(function (s) { return [s.lat, s.lng]; }); + if (latlngs.length === 0) latlngs = [[stops[0].lat, stops[0].lng]]; + + // --- draw each stop --- + stops.forEach(function (s, i) { + var color = s.color || '#007bff'; + var type = s.type || 'pin'; + var popup = TripMap._popupHtml(s); + + if (type === 'pin') { + L.marker([s.lat, s.lng]).addTo(map).bindPopup(popup); + + } else if (type === 'circle') { + L.circleMarker([s.lat, s.lng], { + radius: 8, color: '#fff', fillColor: color, fillOpacity: 0.9, weight: 2 + }).addTo(map).bindPopup(popup); + + } else if (type === 'numbered') { + var num = s.number || (i + 1); + var icon = L.divIcon({ + className: '', + html: '
' + num + '
', + iconSize: [28, 28], iconAnchor: [14, 14], popupAnchor: [0, -16] + }); + L.marker([s.lat, s.lng], { icon: icon }).addTo(map).bindPopup(popup); + + } else if (type === 'emoji') { + var icon = L.divIcon({ + className: '', + html: '
' + + (s.icon || '๐Ÿ“') + '
', + iconSize: [28, 28], iconAnchor: [14, 14], popupAnchor: [0, -16] + }); + L.marker([s.lat, s.lng], { icon: icon }).addTo(map).bindPopup(popup); + + } else if (type === 'label') { + var icon = L.divIcon({ + className: '', + html: '
' + (s.label || '') + '
', + iconAnchor: [0, 10], popupAnchor: [0, -10] + }); + L.marker([s.lat, s.lng], { icon: icon }).addTo(map); + + } else if (type === 'polygon') { + if (s.coords && s.coords.length) { + L.polygon(s.coords, { + color: color, fillColor: color, fillOpacity: 0.15, weight: 2 + }).addTo(map).bindPopup(popup); + } + + } else if (type === 'circle_area') { + L.circle([s.lat, s.lng], { + radius: s.radius || 1000, + color: color, fillColor: color, fillOpacity: 0.15, weight: 2 + }).addTo(map).bindPopup(popup); + + } else if (type === 'freeform') { + if (s.coords && s.coords.length) { + L.polyline(s.coords, { + color: s.color || '#28a745', weight: 3, opacity: 0.85, + dashArray: s.dashed ? '6 4' : null + }).addTo(map).bindPopup(popup); + } + } + }); + + // --- draw route / fit bounds --- + if (latlngs.length === 1) { + map.setView(latlngs[0], 10); + + } else if (tripType === 'road') { + map.fitBounds(L.latLngBounds(latlngs).pad(0.2)); + var coords = latlngs.map(function (ll) { return ll[1] + ',' + ll[0]; }).join(';'); + fetch('https://router.project-osrm.org/route/v1/driving/' + coords + '?overview=full&geometries=geojson') + .then(function (r) { return r.json(); }) + .then(function (data) { + var route = (data.routes && data.routes[0]) + ? data.routes[0].geometry.coordinates.map(function (c) { return [c[1], c[0]]; }) + : latlngs; + L.polyline(route, { color: '#007bff', weight: 4, opacity: 0.85 }).addTo(map); + map.fitBounds(L.latLngBounds(route).pad(0.1)); + }) + .catch(function () { + L.polyline(latlngs, { color: '#007bff', weight: 4, opacity: 0.85 }).addTo(map); + }); + + } else if (tripType === 'flight') { + var arc = TripMap._greatCircleArc(latlngs[0], latlngs[latlngs.length - 1]); + L.polyline(arc, { color: '#007bff', weight: 3, opacity: 0.85, dashArray: '8 8' }).addTo(map); + map.fitBounds(L.latLngBounds(latlngs).pad(0.3)); + + } else { + map.fitBounds(L.latLngBounds(latlngs).pad(0.2)); + } + }, + + _popupHtml: function (s) { + var html = s.label ? '' + s.label + '' : ''; + if (s.note) html += (html ? '
' : '') + '' + s.note + ''; + return html || ' '; + }, + + _greatCircleArc: function (from, to) { + var R2D = 180 / Math.PI, D2R = Math.PI / 180; + var p1 = { lat: from[0] * D2R, lng: from[1] * D2R }; + var p2 = { lat: to[0] * D2R, lng: to[1] * D2R }; + var d = 2 * Math.asin(Math.sqrt( + Math.pow(Math.sin((p2.lat - p1.lat) / 2), 2) + + Math.cos(p1.lat) * Math.cos(p2.lat) * Math.pow(Math.sin((p2.lng - p1.lng) / 2), 2) + )); + var pts = []; + for (var i = 0; i <= 80; i++) { + var f = i / 80; + if (d === 0) { pts.push(from); continue; } + var A = Math.sin((1 - f) * d) / Math.sin(d), B = Math.sin(f * d) / Math.sin(d); + var x = A * Math.cos(p1.lat) * Math.cos(p1.lng) + B * Math.cos(p2.lat) * Math.cos(p2.lng); + var y = A * Math.cos(p1.lat) * Math.sin(p1.lng) + B * Math.cos(p2.lat) * Math.sin(p2.lng); + var z = A * Math.sin(p1.lat) + B * Math.sin(p2.lat); + pts.push([Math.atan2(z, Math.sqrt(x * x + y * y)) * R2D, Math.atan2(y, x) * R2D]); + } + return pts; + } +}; diff --git a/build/BUILD_INSTRUCTIONS.md b/build/BUILD_INSTRUCTIONS.md new file mode 100644 index 000000000..002a6befc --- /dev/null +++ b/build/BUILD_INSTRUCTIONS.md @@ -0,0 +1,73 @@ +# Build Instructions + +Local setup for [vishalgattani.github.io](https://vishalgattani.github.io) โ€” a Jekyll static site. + +--- + +## Prerequisites + +### 1. Homebrew + +```bash +/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" +``` + +### 2. rbenv (Ruby version manager) + +```bash +brew install rbenv ruby-build +echo 'eval "$(rbenv init - zsh)"' >> ~/.zshrc +source ~/.zshrc +``` + +### 3. Ruby 3.3.11 + +The project pins Ruby to `3.3.11` via `.ruby-version`. + +```bash +rbenv install 3.3.11 +rbenv local 3.3.11 +ruby --version # should print ruby 3.3.11 +``` + +### 4. Bundler + +```bash +gem install bundler +``` + +--- + +## Install Dependencies + +From the repo root: + +```bash +bundle install +``` + +This installs Jekyll and all plugins declared in `Gemfile`. + +--- + +## Run Locally + +```bash +bash build/serve.sh +``` + +Opens `http://localhost:4000` automatically with live-reload enabled. +Changes to any source file rebuild the site in place โ€” no restart needed. + +To stop the server: `Ctrl+C` + +--- + +## Troubleshooting + +| Problem | Fix | +|---------|-----| +| `ruby: command not found` | Run `rbenv install 3.1.0 && rbenv local 3.1.0` | +| `bundle: command not found` | Run `gem install bundler` | +| Port 4000 already in use | `lsof -ti:4000 \| xargs kill` then retry | +| `cannot load such file -- webrick` | `bundle add webrick` (Ruby 3+ removed it from stdlib) | diff --git a/build/serve.sh b/build/serve.sh new file mode 100755 index 000000000..192af63b9 --- /dev/null +++ b/build/serve.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -e + +# Init rbenv so the pinned Ruby version is used instead of the system Ruby +export PATH="$HOME/.rbenv/bin:$PATH" +eval "$(rbenv init - bash)" + +cd "$(dirname "$0")/.." +bundle exec jekyll serve --livereload --open-url diff --git a/build/setup.sh b/build/setup.sh new file mode 100755 index 000000000..a71fc9bac --- /dev/null +++ b/build/setup.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +set -e + +export PATH="$HOME/.rbenv/bin:$PATH" + +RUBY_VERSION="3.3.11" + +info() { echo "[info] $*"; } +success() { echo "[ok] $*"; } +warn() { echo "[warn] $*"; } + +# --------------------------------------------------------------------------- +# Homebrew +# --------------------------------------------------------------------------- +if command -v brew &>/dev/null; then + success "Homebrew already installed" +else + info "Installing Homebrew..." + /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" + # Apple Silicon: add brew to PATH for the rest of this script + [[ -f /opt/homebrew/bin/brew ]] && eval "$(/opt/homebrew/bin/brew shellenv)" + success "Homebrew installed" +fi + +# --------------------------------------------------------------------------- +# rbenv +# --------------------------------------------------------------------------- +if command -v rbenv &>/dev/null; then + success "rbenv already installed" +else + info "Installing rbenv..." + brew install rbenv ruby-build + eval "$(rbenv init - bash)" + + SHELL_RC="$HOME/.zshrc" + [[ "$SHELL" == */bash ]] && SHELL_RC="$HOME/.bashrc" + + if ! grep -q 'rbenv init' "$SHELL_RC" 2>/dev/null; then + echo 'eval "$(rbenv init - zsh)"' >> "$SHELL_RC" + warn "Added rbenv init to $SHELL_RC โ€” open a new terminal after setup completes" + fi + success "rbenv installed" +fi + +# --------------------------------------------------------------------------- +# Ruby +# --------------------------------------------------------------------------- +eval "$(rbenv init - bash)" 2>/dev/null || true + +if rbenv versions --bare | grep -qx "$RUBY_VERSION"; then + success "Ruby $RUBY_VERSION already installed" +else + info "Installing Ruby $RUBY_VERSION (this takes a few minutes)..." + rbenv install "$RUBY_VERSION" + success "Ruby $RUBY_VERSION installed" +fi + +rbenv local "$RUBY_VERSION" +info "Ruby version set to $(ruby --version)" + +# --------------------------------------------------------------------------- +# Bundler +# --------------------------------------------------------------------------- +if gem list bundler -i &>/dev/null; then + success "Bundler already installed" +else + info "Installing Bundler..." + gem install bundler + rbenv rehash + success "Bundler installed" +fi + +# --------------------------------------------------------------------------- +# Project dependencies +# --------------------------------------------------------------------------- +cd "$(dirname "$0")/.." +info "Running bundle install..." +bundle install +success "Dependencies installed" + +echo "" +echo "Setup complete. Run the site locally with:" +echo " bash build/serve.sh" diff --git a/pages/trips.html b/pages/trips.html new file mode 100644 index 000000000..3102ca3bb --- /dev/null +++ b/pages/trips.html @@ -0,0 +1,8 @@ +--- +layout: default +title: Trips +permalink: /trips/ +weight: 5 +--- + +{% include trips/index.html %}