Compare commits

...

11 Commits

6 changed files with 310 additions and 194 deletions

View File

@@ -1,4 +1,6 @@
[ {
"eventDates": "FEBRUARY 27 MARCH 8, 2025",
"restaurants": [
{ {
"name": "1898 Public House", "name": "1898 Public House",
"slug": "1898", "slug": "1898",
@@ -2519,13 +2521,46 @@
"phone": "(509) 863-9235", "phone": "(509) 863-9235",
"courses": { "courses": {
"First Course": [ "First Course": [
{
"name": "Fried Pickles",
"desc": "Hand-breaded fried pickle spears served with a housemade honey mustard dressing V"
},
{
"name": "Panzanella Salad",
"desc": "A quick-pickled cucumber, red onion and tomato salad topped with our fresh-made seasoned croutons V V+ Add fresh mozzarella $2"
},
{
"name": "Pizza Bagels!",
"desc": "Mini bagels with house red sauce, fresh mozzarella and prosciutto. Baked to perfection and topped with fresh basil"
}
], ],
"Second Course": [ "Second Course": [
{
"name": "Impossible Meatball Sub",
"desc": "Impossible Italian meatballs smothered in a family recipe marinara. Topped with vegan mozzarella cheese and fresh basil on a sub roll “Anything is possible!” Kevin Garnett V V+"
},
{
"name": "Cajun Cheesesteak",
"desc": "Thin-sliced beef, house-blended Cajun seasoning, creole mayo, onions and peppers topped with melty cheese on a sub roll"
},
{
"name": "Muffins and Sauce",
"desc": "A King family favorite. Fresh-made mini biscuits in a slow roasted pork gravy"
}
], ],
"Third Course": [ "Third Course": [
{
"name": "Tap Beer",
"desc": "Choose from one of our eight local beers on tap V V+"
},
{
"name": "Deep Fried Banana",
"desc": "Battered and deep fried banana bites tossed in cinnamon and sugar. Served with a caramel dip V"
},
{
"name": "Heritage Ice Cream Cake",
"desc": "Locally made ice cream layered on a cookie crust V"
}
] ]
} }
} }
@@ -3125,13 +3160,46 @@
"phone": "(509) 309-2944", "phone": "(509) 309-2944",
"courses": { "courses": {
"First Course": [ "First Course": [
{
"name": "Grilled Queso",
"desc": "Grilled Chihuahua cheese, tostada, verde, fermented aioli, pico de gallo GF V "
},
{
"name": "Tacos Whettos",
"desc": "Beef cheek barbacoa, manchego cheese, fermented hot sauce, salsa, corn tortillas GF "
},
{
"name": "Panzanella Salad",
"desc": "Roasted squash, marinated tomatoes, smoked tomato vinaigrette, mixed greens, manchego cheese, toasted focaccia V "
}
], ],
"Second Course": [ "Second Course": [
{
"name": "Tamales",
"desc": "Choice of pork or vegetable tamale, pan fried, molé, bean ragout, machaca salsa GF V V+ "
},
{
"name": "Smoked Cheese Black Bean Empanada",
"desc": "Smoked black beans, house labneh, pickled chilies, verde, crema V "
},
{
"name": "Bocalo",
"desc": "Pan-seared white fish, harissa sauce, black garlic aioli, pickled vegetables, Spanish rice GF "
}
], ],
"Third Course": [ "Third Course": [
{
"name": "Tres Leches Cake",
"desc": "Milk-soaked cake, topped with housemade whipped cream and seasonal fruit V"
},
{
"name": "Mexican Chocolate Brownie",
"desc": "Mexican chocolate brownie, cardamom and coriander gelato, seasonal fruit V"
},
{
"name": "Sorbet",
"desc": "Seasonal fruit flavors GF V V+ "
}
] ]
} }
} }
@@ -3440,13 +3508,46 @@
"phone": "(509) 994-5157", "phone": "(509) 994-5157",
"courses": { "courses": {
"First Course": [ "First Course": [
{
"name": "Fresh Spring Rolls",
"desc": "Fresh veggies, rice noodles and fresh local basil wrapped in a rice paper, served with our house-made peanut sauce. Choice of shrimp or tofu GF GFA V V+"
},
{
"name": "Edamame",
"desc": "Whole soy bean pods seasoned with choice of sea salt, sriracha garlic or black garlic truffle Parmesan V V+"
},
{
"name": "Sake Clams",
"desc": "Clams sautéed in a lemongrass-basil compound butter and sake. Topped with local microgreens, fried garlic and a side of toasted crostini GFA"
}
], ],
"Second Course": [ "Second Course": [
{
"name": "Pho",
"desc": "Our full-bodied broth served with rice noodles, a cabbage-carrot blend, lime, jalapeno, onion and locally grown herbs. Choice of protein. Vegan options available GF GFA V V+ "
},
{
"name": "Pho-rrito",
"desc": "All our pho fixins and your choice of protein wrapped in a garlic herb tortilla and served with a side of our broth for dipping. Vegan options available GFA V V+ "
},
{
"name": "Spicy Curry Noodles",
"desc": "Thin rice noodles in Thai red curry broth with pickled veggies and our marinated onsen egg. Garnished with local microgreens and choice of protein GF GFA V V+ Add kimchi $3.50"
}
], ],
"Third Course": [ "Third Course": [
{
"name": "Matcha Panna Cotta",
"desc": "A creamy green tea panna cotta topped with raspberry sugar GF V"
},
{
"name": "Pineapple Sago",
"desc": "A sweet pineapple tapioca pudding, topped with a spicy pineapple chutney GF V"
},
{
"name": "Honey Paloma Green Tea Sorbet",
"desc": "Locally-made tea from Boba Proper that we turn into a refreshing sorbet GF V V+ Add a shot of sake $4"
}
] ]
} }
} }
@@ -3640,13 +3741,46 @@
"phone": "(509) 474-1336", "phone": "(509) 474-1336",
"courses": { "courses": {
"First Course": [ "First Course": [
{
"name": "Deep Fried Pickleback Pickles",
"desc": "Dill spears marinated in Jameson Whiskey, coated in beer batter and deep fried to a golden brown V "
},
{
"name": "Buffalo Chicken Mac Balls",
"desc": "Buffalo chicken mac and cheese, breaded and deep fried to a golden crunch"
},
{
"name": "Blue Cheese Wedge",
"desc": "Iceberg wedge salad topped with blue cheese dressing, blue cheese crumbles, cherry tomatoes, bacon crumbles and fresh chives GFA"
}
], ],
"Second Course": [ "Second Course": [
{
"name": "Cajun Mac and Cheese",
"desc": "Mac and cheese grilled with Andouille sausage, Cajun-seasoned chicken, blended with cheddar and pepper jack cheeses GFA Gluten-free noodles $3"
},
{
"name": "Beer Cheese Mac and Cheese",
"desc": "Mac and cheese grilled with German bratwurst, caramelized onions, blended with a beer cheese sauce and topped with crunchy pretzel bites GFA Gluten-free noodles $3"
},
{
"name": "MacDaddy Smash Burger",
"desc": "Two smashed beef patties topped with cheddar jack cheese, grilled red onion, jalapeño, and Cougar Gold mac and cheese, grilled to perfection. Served with fries GFA"
}
], ],
"Third Course": [ "Third Course": [
{
"name": "Blueberry Crumble",
"desc": "Sweet-tasting blueberry filling, delicious shortbread crust and topped with an old-fashioned coconut and oat crumble V "
},
{
"name": "Reeses Tart",
"desc": "Vegan tart shell filled with Reeses Peanut Butter and topped with a dark chocolate glaze V"
},
{
"name": "Huckleberry Ice Cream",
"desc": "House-made by our friends at the Milk Bottle V "
}
] ]
} }
} }
@@ -4539,13 +4673,46 @@
"phone": "(509) 290-6518", "phone": "(509) 290-6518",
"courses": { "courses": {
"First Course": [ "First Course": [
{
"name": "Roasted Goat Cheese with Blueberries and Honey",
"desc": "Roasted goat cheese with blueberries and honey, served with toasted baguette"
},
{
"name": "Pretzel Sticks",
"desc": "Pretzel sticks served with beer cheese or beer mustard. Add both beer cheese and beer mustard for $2"
},
{
"name": "Chicken Pot Pie Croquettes",
"desc": "Savory filling of chicken, onions, carrots and peas together with a rich bechamel sauce and encrusted in seasoned bread crumbs"
}
], ],
"Second Course": [ "Second Course": [
{
"name": "Grilled Chicken Sandwich",
"desc": "Marinated grilled chicken breast, jalapeños, lettuce, tomato, onion, sriracha aioli, jack cheese served on ciabatta"
},
{
"name": "Pork Belly Tacos",
"desc": "Flour tortillas, cabbage, green onion, pickled red onion, house Peruvian green sauce, sour cream and cilantro"
},
{
"name": "Whiskey Crusted Baby Back Ribs",
"desc": "Slow smoked baby back ribs, hand rubbed with our house seasoning, served with our house barbecue sauce on the side "
}
], ],
"Third Course": [ "Third Course": [
{
"name": "Bourbon Caramel Sauce Cheesecake",
"desc": "House-made bourbon caramel sauce served over New York-style cheesecake with toasted pecans"
},
{
"name": "Southern Whiskey Bread Pudding",
"desc": "Southern-style bread pudding with a whiskey caramel sauce and maple candied bacon topping with toasted pecans"
},
{
"name": "Lemon Blueberry Layered Cake",
"desc": "Sweet lemon blueberry layered cake with a cream cheese frosting"
}
] ]
} }
} }
@@ -5807,7 +5974,7 @@
"desc": "Lady Rose Apple, pink peppercorn, thyme, crostini V Add house-made focaccia $5" "desc": "Lady Rose Apple, pink peppercorn, thyme, crostini V Add house-made focaccia $5"
}, },
{ {
"name": "Tavolàta", "name": "Salad",
"desc": "Little gem lettuce, chicory, pistachio, red onion, Calabrian vinaigrette, pecorino GF V" "desc": "Little gem lettuce, chicory, pistachio, red onion, Calabrian vinaigrette, pecorino GF V"
}, },
{ {
@@ -5825,12 +5992,23 @@
"desc": "Black pepper, butter, pecorino romano GFA" "desc": "Black pepper, butter, pecorino romano GFA"
}, },
{ {
"name": "Roasted", "name": "Pork Loin",
"desc": "Fig mostarda, radicchio, hazelnut bread crumb GFA" "desc": "Fig mostarda, radicchio, hazelnut bread crumb GFA"
} }
], ],
"Third Course": [ "Third Course": [
{
"name": "Zeppole",
"desc": "Lemon doughnuts, dark chocolate sauce V"
},
{
"name": "Tiramasu",
"desc": "Mascarpone, amaretto, espresso, lady finger V"
},
{
"name": "Sorbetto or Gelato",
"desc": "Salted shortbread cookie GFA V"
}
] ]
} }
} }
@@ -6738,3 +6916,4 @@
} }
} }
] ]
}

View File

@@ -1,4 +1,6 @@
[ {
"eventDates": "FEBRUARY 26 MARCH 7, 2026",
"restaurants": [
{ {
"name": "315", "name": "315",
"slug": "315cuisine", "slug": "315cuisine",
@@ -5254,44 +5256,44 @@
"courses": { "courses": {
"First Course": [ "First Course": [
{ {
"name": "V  Nut Free", "name": "Cauliflower Wings",
"desc": "Cauliflower WingsBuffalo or Thai ginger style • carrot sticks • dipping sauce df Make it full size: $7" "desc": "Buffalo or Thai ginger style • carrot sticks • dipping sauce df V Nut Free Make it full size: $7"
}, },
{ {
"name": "GF  V+  DF  Nut Free", "name": "Blistered Shishitos",
"desc": "Blistered Shishitosmiso-tahini sauce • lemon salt • togarashi Make it full size: $7" "desc": "miso-tahini sauce • lemon salt • togarashi GF V+ DF Nut Free Make it full size: $7"
}, },
{ {
"name": "GFA  V+  DF  Nut Free  Soy Free", "name": "Creamy Tomato Soup",
"desc": "Creamy Tomato Soupbreadcrumbs • parsley • parmesan Make it a bowl $4" "desc": "breadcrumbs • parsley • parmesan GFA V+ DF Nut Free Soy Free Make it a bowl $4"
} }
], ],
"Second Course": [ "Second Course": [
{ {
"name": "GFA  V+  DF  Nut Free", "name": "Bbq Jackfruit Sandwich",
"desc": "Bbq Jackfruit Sandwichrainbow slaw • crispy onion • Carolina style BBQ sauce • pretzel bun • choice of side" "desc": "rainbow slaw • crispy onion • Carolina style BBQ sauce • pretzel bun • choice of side GFA V+ DF Nut Free"
}, },
{ {
"name": "GFA  V+  DF  Nut Free", "name": "Lemongrass Yellow Curry",
"desc": "Lemongrass Yellow Currytofu • jasmine rice • roasted radish • snap pea • red bell pepper • carrot slaw • crispy shallot" "desc": "tofu • jasmine rice • roasted radish • snap pea • red bell pepper • carrot slaw • crispy shallot GFA V+ DF Nut Free"
}, },
{ {
"name": "Philly Sandwich", "name": "Philly Sandwich",
"desc": "Braised mushroom medley • onion \u0026 peppers • smokey cashew provolone • long bread • choice of side V+" "desc": "braised mushroom medley • onion & peppers • smokey cashew provolone • long bread • choice of side V+ DF"
} }
], ],
"Third Course": [ "Third Course": [
{ {
"name": "GF  V+", "name": "Tea Time",
"desc": "Tea Timeearl grey cheesecake • lemon coulis • lemon-ginger sugar cookie" "desc": "earl grey cheesecake • lemon coulis • lemon-ginger sugar cookie GF V+ DF"
}, },
{ {
"name": "GF  V  DF  Nut Free", "name": "Cookies & Cream",
"desc": "Cookies \u0026 Creamchocolate chunk cookie • vanilla ice cream • raspberry coulis" "desc": "chocolate chunk cookie • vanilla ice cream • raspberry coulis GF V DF Nut Free"
}, },
{ {
"name": "GF  V+", "name": "Turtle Torte",
"desc": "Turtle Torteflourless chocolate torte • miso caramel • candied walnuts" "desc": "flourless chocolate torte • miso caramel • candied walnuts GF V+ DF"
} }
] ]
} }
@@ -6417,12 +6419,12 @@
"courses": { "courses": {
"First Course": [ "First Course": [
{ {
"name": "V DF", "name": "Crispy Spring Rolls",
"desc": "Crispy Spring RollsTasty combination of vegetables and Thai spices hand-rolled in a thin wrapper, deep fried to a golden brown and served with plum sauce" "desc": "Tasty combination of vegetables and Thai spices hand-rolled in a thin wrapper, deep fried to a golden brown and served with plum sauce V DF V+"
}, },
{ {
"name": "V V+", "name": "Golden Samosas",
"desc": "Golden SamosasA blend of sweet potatoes, carrots, peas, potatoes, cashews and Indian spices in a wonton wrapper, fried and served with a sweet chili dipping sauce" "desc": "A blend of sweet potatoes, carrots, peas, potatoes, cashews and Indian spices in a wonton wrapper, fried and served with a sweet chili dipping sauce V V+ DF"
}, },
{ {
"name": "Chicken Satay", "name": "Chicken Satay",
@@ -6449,8 +6451,8 @@
"desc": "Your choice of two scoops: vanilla, coconut, huckleberry or Thai tea GF" "desc": "Your choice of two scoops: vanilla, coconut, huckleberry or Thai tea GF"
}, },
{ {
"name": "GF V V+", "name": "Mango Sticky Rice",
"desc": "Mango Sticky RiceWarm, sweet white sticky rice topped with mango and sweetened coconut cream" "desc": "Warm, sweet white sticky rice topped with mango and sweetened coconut cream GF V V+ DF"
}, },
{ {
"name": "Coconut Cheesecake", "name": "Coconut Cheesecake",
@@ -7160,3 +7162,4 @@
} }
} }
] ]
}

View File

@@ -16,6 +16,7 @@ Each couple gets their own set of interest checkboxes ("His Picks" / "Her Picks"
- **Interest filter** — narrow the list to His Picks, Her Picks, Both Picked, or Either Picked - **Interest filter** — narrow the list to His Picks, Her Picks, Both Picked, or Either Picked
- **Random picker** — "Pick for Us!" draws a random winner from the currently filtered pool, with an option to re-roll - **Random picker** — "Pick for Us!" draws a random winner from the currently filtered pool, with an option to re-roll
- **Course browse** — dedicated First Course, Second Course, and Third Course buttons open a modal listing every dish for that course across all restaurants; click any dish to jump straight to that restaurant's detail view - **Course browse** — dedicated First Course, Second Course, and Third Course buttons open a modal listing every dish for that course across all restaurants; click any dish to jump straight to that restaurant's detail view
- **Year selector** — dropdown on the course browse bar switches between available event years (2025, 2026); restaurant data, filters, and interest picks are all scoped to the selected year
- **Clear Filters** — one-click button to reset all search and filter fields back to their defaults - **Clear Filters** — one-click button to reset all search and filter fields back to their defaults
- **Reset Selected** — clears all saved picks (with a confirmation prompt) so you can start fresh - **Reset Selected** — clears all saved picks (with a confirmation prompt) so you can start fresh
- **Mobile-friendly** — responsive single-panel layout on small screens; the list and detail views swap in place, with a Back button to return to the list - **Mobile-friendly** — responsive single-panel layout on small screens; the list and detail views swap in place, with a Back button to return to the list
@@ -23,7 +24,7 @@ Each couple gets their own set of interest checkboxes ("His Picks" / "Her Picks"
## Usage ## Usage
1. Serve the folder from a web server and open `restaurant-picker.html` in a browser. (The app fetches `2026-restaurants.json` at runtime, so it cannot be opened directly as a local `file://` URL — a server is required. A simple option is the VS Code Live Server extension, or `npx serve .` in the project folder.) 1. Serve the folder from a web server and open `restaurant-picker.html` in a browser. (The app fetches a `YYYY-restaurants.json` file at runtime, so it cannot be opened directly as a local `file://` URL — a server is required. A simple option is the VS Code Live Server extension, or `npx serve .` in the project folder.)
2. Browse the restaurant list or use the toolbar filters to narrow choices. 2. Browse the restaurant list or use the toolbar filters to narrow choices.
3. Use the **First Course**, **Second Course**, or **Third Course** buttons to browse all dishes for a given course and jump to any restaurant that interests you. 3. Use the **First Course**, **Second Course**, or **Third Course** buttons to browse all dishes for a given course and jump to any restaurant that interests you.
4. Check the **You** and **Wife** boxes on any restaurant you're each interested in. 4. Check the **You** and **Wife** boxes on any restaurant you're each interested in.
@@ -45,12 +46,25 @@ Each couple gets their own set of interest checkboxes ("His Picks" / "Her Picks"
``` ```
restaurant-picker.html # App shell — HTML, CSS, and JS logic restaurant-picker.html # App shell — HTML, CSS, and JS logic
2026-restaurants.json # Restaurant data for the 2026 event (loaded at runtime via fetch) 2026-restaurants.json # Restaurant + event date data for the 2026 event
2025-restaurants.json # Restaurant + event date data for the 2025 event
LICENSE LICENSE
README.md README.md
``` ```
Each year, create a new `YYYY-restaurants.json` and update the filename in the `fetch()` call near the bottom of `restaurant-picker.html`. ### Adding a New Year
1. Create a new `YYYY-restaurants.json` file following the schema described below (see [Data Entry Guide](#data-entry-guide)).
2. In `restaurant-picker.html`, add a new `<option>` to the year selector dropdown:
```html
<select id="yearSelect" onchange="loadYear(this.value)">
<option value="2027">2027</option> <!-- add new year at top -->
<option value="2026">2026</option>
<option value="2025">2025</option>
</select>
```
3. Update the hardcoded default in the `loadYear('2026')` init call at the bottom of the `<script>` block to the new year.
4. Update the static fallback date in the `<span id="eventDates">` header element — it will be overwritten at runtime, but the static value is shown briefly on first load.
--- ---
@@ -58,6 +72,20 @@ Each year, create a new `YYYY-restaurants.json` and update the filename in the `
Each year's restaurant data is stored as a JSON file (`YYYY-restaurants.json`) loaded by `restaurant-picker.html` at runtime. The source data comes from the [IRW restaurant listing pages](https://inlanderrestaurantweek.com/restaurants/). This section covers the data schema and common pitfalls to avoid when entering or updating restaurant records. Each year's restaurant data is stored as a JSON file (`YYYY-restaurants.json`) loaded by `restaurant-picker.html` at runtime. The source data comes from the [IRW restaurant listing pages](https://inlanderrestaurantweek.com/restaurants/). This section covers the data schema and common pitfalls to avoid when entering or updating restaurant records.
### File-Level Schema
Each `YYYY-restaurants.json` is an **object** (not a bare array) with two top-level keys:
```json
{
"eventDates": "FEBRUARY 26 MARCH 7, 2026",
"restaurants": [ ... ]
}
```
- `eventDates` — the display string shown in the header when that year is selected; use an em dash (``) between the start and end dates
- `restaurants` — array of restaurant objects (see schema below)
### Restaurant Object Schema ### Restaurant Object Schema
```json ```json
@@ -179,7 +207,9 @@ Run the following Python script against the JSON file to identify any restaurant
import json import json
with open('2026-restaurants.json', encoding='utf-8') as f: with open('2026-restaurants.json', encoding='utf-8') as f:
restaurants = json.load(f) data = json.load(f)
restaurants = data['restaurants']
for r in restaurants: for r in restaurants:
name = r.get('name', 'Unknown') name = r.get('name', 'Unknown')

View File

@@ -1,123 +0,0 @@
# fix-tavolata.ps1
# Run this after the Wayback Machine rate limit resets (wait ~30 minutes after last run)
# Recovers tavolata's Third Course using the same-block parser strategy
$projectDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
$jsonPath = Join-Path $projectDir '2025-restaurants.json'
$data = Get-Content $jsonPath -Raw -Encoding UTF8 | ConvertFrom-Json
function Decode-Html($str) {
if (-not $str) { return $str }
($str -replace '&amp;','&' -replace '&#039;',"'" -replace '&quot;','"' -replace '&lt;','<' -replace '&gt;','>' -replace '&nbsp;',' ' -replace '\s+',' ').Trim()
}
function Get-CleanText($rawHtml) { Decode-Html ($rawHtml -replace '<[^>]+>', ' ') }
function Test-DietaryTag($str) { $str -match '^(GF|GFA|V\+?|DF|DFA|V:|2025|Drink|V\+A)$' }
function Get-Dish($pContent) {
$opts = [System.Text.RegularExpressions.RegexOptions]::Singleline
$bWithBrM = [regex]::Match($pContent, '(?s)<b>(.*?)<br\s*/?>', $opts)
if ($bWithBrM.Success) {
$name = Get-CleanText $bWithBrM.Groups[1].Value
if ($name.Length -ge 3 -and $name.Length -le 80 -and -not (Test-DietaryTag $name) -and $name -notmatch '^[A-Z]{1,3}:') {
return [PSCustomObject]@{ name = $name; desc = Get-CleanText ($pContent.Substring($bWithBrM.Index + $bWithBrM.Length)) }
}
}
$bM = [regex]::Match($pContent, '(?s)<b>(.*?)</b>', $opts)
if ($bM.Success) {
$namePart = Get-CleanText $bM.Groups[1].Value
if ($namePart.Length -ge 3 -and -not (Test-DietaryTag $namePart)) {
$afterB = $pContent.Substring($bM.Index + $bM.Length)
$sM2 = [regex]::Match($afterB, '(?s)^[^<]*<strong>(.*?)</strong>(.*)', $opts)
if ($sM2.Success) {
$p2 = Get-CleanText $sM2.Groups[1].Value
if (-not (Test-DietaryTag $p2) -and $p2.Length -ge 2) {
return [PSCustomObject]@{ name = "$namePart $p2".Trim(); desc = Get-CleanText $sM2.Groups[2].Value }
}
}
return [PSCustomObject]@{ name = $namePart; desc = Get-CleanText $afterB }
}
}
$sM = [regex]::Match($pContent, '(?s)<strong>(.*?)</strong>', $opts)
if ($sM.Success) {
$name = Get-CleanText $sM.Groups[1].Value
if ($name.Length -lt 3 -or $name.Length -gt 80 -or (Test-DietaryTag $name) -or $name -match '^[A-Z]{1,3}:') { return $null }
$afterBr = ''
if ($pContent -match '(?s)<br\s*/?>(.*?)$') { $afterBr = $matches[1] }
else { $am = [regex]::Match($pContent, '(?s)</strong>(.*?)$', $opts); if ($am.Success) { $afterBr = $am.Groups[1].Value } }
return [PSCustomObject]@{ name = $name; desc = Get-CleanText $afterBr }
}
return $null
}
function Get-Dishes($courseHtml) {
$dishes = [System.Collections.ArrayList]@()
$opts = [System.Text.RegularExpressions.RegexOptions]::Singleline
foreach ($pm in [regex]::Matches($courseHtml, '(?s)<p[^>]*>(.*?)</p>', $opts)) {
$pc = $pm.Groups[1].Value
if ($pc -notmatch '<b>|<strong>') { continue }
$d = Get-Dish $pc
if ($d -and $d.name) { $null = $dishes.Add($d) }
}
return ,$dishes
}
function Get-CourseBlock($html, $label, $nextLabel) {
$opts = [System.Text.RegularExpressions.RegexOptions]::Singleline
if ($nextLabel) {
$m = [regex]::Match($html, ([regex]::Escape($label) + '(.+?)(?=' + [regex]::Escape($nextLabel) + ')'), $opts)
if ($m.Success) { return $m.Groups[1].Value }
}
$idx = $html.IndexOf($label)
if ($idx -ge 0) {
$sub = $html.Substring($idx, [Math]::Min(8000, $html.Length - $idx))
$sameDivM = [regex]::Match($sub, '(?s)</h[123]>\s*(<p.+?)(?=</div>)', $opts)
if ($sameDivM.Success -and $sameDivM.Groups[1].Value -match '<p') { return $sameDivM.Groups[1].Value }
$im = [regex]::Match($sub, '(?s)et_pb_text_inner">(?!<h[123])(.+?)(?=et_pb_text_inner"><h|</div>\s*</div>\s*</div>\s*</div>\s*<div)', $opts)
if ($im.Success) { return $im.Groups[1].Value }
}
return ''
}
$r = $data | Where-Object { $_.slug -eq 'tavolata' }
Write-Host "tavolata currently: $($r.menu.courses.'First Course'.Count)/$($r.menu.courses.'Second Course'.Count)/$($r.menu.courses.'Third Course'.Count)"
$timestamps = @('20250306132630','20250401000000','20250415000000','20250501000000')
$success = $false
foreach ($ts in $timestamps) {
if ($success) { break }
Write-Host "Trying timestamp $ts..." -NoNewline
try {
$url = "https://web.archive.org/web/$ts/https://inlanderrestaurantweek.com/project/tavolata/"
$resp = Invoke-WebRequest -Uri $url -UseBasicParsing -TimeoutSec 60 -ErrorAction Stop
$html = $resp.Content
if ($html -match '429 Too Many') { throw "Rate limited" }
$first = Get-Dishes (Get-CourseBlock $html 'First Course' 'Second Course')
$second = Get-Dishes (Get-CourseBlock $html 'Second Course' 'Third Course')
$third = Get-Dishes (Get-CourseBlock $html 'Third Course' $null)
Write-Host " -> $($first.Count)/$($second.Count)/$($third.Count)"
if ($third.Count -gt 0) {
if ($first.Count -gt 0) { $r.menu.courses.'First Course' = @($first) }
if ($second.Count -gt 0) { $r.menu.courses.'Second Course' = @($second) }
$r.menu.courses.'Third Course' = @($third)
Write-Host "SUCCESS! tavolata Third Course recovered." -ForegroundColor Green
$success = $true
} else {
Write-Host " Third Course still empty, trying next timestamp..."
}
} catch {
Write-Host " ERROR: $_" -ForegroundColor Red
}
Start-Sleep -Seconds 10
}
if (-not $success) {
Write-Host "Could not recover tavolata Third Course. Try again later." -ForegroundColor Yellow
} else {
$json = $data | ConvertTo-Json -Depth 10
[System.IO.File]::WriteAllText($jsonPath, $json, [System.Text.Encoding]::UTF8)
Write-Host "Saved to $jsonPath"
}

View File

@@ -35,15 +35,14 @@ Each entry in `YEAR-restaurants.json`:
``` ```
Price is always 25, 35, or 45. gardenparty genuinely has 4 Third Course options. Price is always 25, 35, or 45. gardenparty genuinely has 4 Third Course options.
## 2025 Data Status ## 2025 Data Status — COMPLETE
- **File**: `2025-restaurants.json` (121 restaurants) - **File**: `2025-restaurants.json` (121 restaurants)
- **Wayback snapshot used**: `20250306132630` (primary), `20250401000000` (backup for some) - **Wayback snapshot used**: `20250306132630`
- **Complete (3/3/3+)**: 111 restaurants - **Complete (3/3/3+)**: 112 restaurants
- **gardenparty**: 3/3/4 — correct, it genuinely offers 4 dessert choices - **gardenparty**: 3/3/4 — correct, it genuinely offers 4 dessert choices
- **tavolata**: 3/3/0needs fix-tavolata.ps1 run when rate limit resets - **tavolata**: 3/3/3FIXED (recovered Third Course from snapshot `20250306132630`)
- **0/0/0 (JS-only, unrecoverable)**: heritage, kismet, littlenoodle, macdaddys, purgatory, redtail, republickitchen, republicpi, vicinopizza - **0/0/0 (JS-only, unrecoverable)**: heritage, kismet, littlenoodle, macdaddys, purgatory, redtail, republickitchen, republicpi, vicinopizza
## Scripts in Project Directory ## Scripts in Project Directory
- `fix-tavolata.ps1`run after rate limit resets to recover tavolata Third Course - `fix-tavolata.ps1`already run, tavolata is complete; kept for reference
- Copy to local temp and run: `cp ...\fix-tavolata.ps1 C:\Users\derekc.CHNSLocal\AppData\Local\Temp\` - `check-2025.ps1` — validates all restaurant course counts
- Then: `powershell.exe -ExecutionPolicy Bypass -File C:\Users\derekc.CHNSLocal\AppData\Local\Temp\fix-tavolata.ps1`

View File

@@ -74,7 +74,8 @@ header p{font-size:0.85rem;color:#f0cca0;margin-top:3px}
@keyframes pop{from{transform:scale(0.85);opacity:0}to{transform:scale(1);opacity:1}} @keyframes pop{from{transform:scale(0.85);opacity:0}to{transform:scale(1);opacity:1}}
.no-results{padding:30px;text-align:center;color:#5a3a20} .no-results{padding:30px;text-align:center;color:#5a3a20}
::-webkit-scrollbar{width:5px}::-webkit-scrollbar-track{background:#1a0a00}::-webkit-scrollbar-thumb{background:#5a2a00;border-radius:3px} ::-webkit-scrollbar{width:5px}::-webkit-scrollbar-track{background:#1a0a00}::-webkit-scrollbar-thumb{background:#5a2a00;border-radius:3px}
.course-browse-bar{background:#241000;padding:8px 24px;display:flex;gap:10px;border-bottom:1px solid #5a2a00} .course-browse-bar{background:#241000;padding:8px 24px;display:flex;gap:10px;align-items:center;border-bottom:1px solid #5a2a00}
.course-browse-bar select{background:#3a1a00;border:1px solid #7a4a00;color:#f0e6d3;padding:7px 10px;border-radius:6px;font-size:0.85rem;cursor:pointer;margin-left:auto;flex-shrink:0}
.btn-course{background:#3a1a00;border:1px solid #7a4a00;color:#f5c842;padding:7px 20px;border-radius:20px;cursor:pointer;font-size:0.85rem;font-weight:600;transition:all 0.2s} .btn-course{background:#3a1a00;border:1px solid #7a4a00;color:#f5c842;padding:7px 20px;border-radius:20px;cursor:pointer;font-size:0.85rem;font-weight:600;transition:all 0.2s}
.btn-course:hover{background:#5a2a00;border-color:#f5c842} .btn-course:hover{background:#5a2a00;border-color:#f5c842}
.course-modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,0.78);z-index:100;display:flex;align-items:center;justify-content:center} .course-modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,0.78);z-index:100;display:flex;align-items:center;justify-content:center}
@@ -97,8 +98,9 @@ header p{font-size:0.85rem;color:#f0cca0;margin-top:3px}
.toolbar select{flex:1;min-width:0} .toolbar select{flex:1;min-width:0}
.spacer{display:none} .spacer{display:none}
.tag-counts{width:100%;order:10;text-align:center} .tag-counts{width:100%;order:10;text-align:center}
.course-browse-bar{padding:8px 12px} .course-browse-bar{padding:8px 12px;flex-wrap:wrap}
.btn-course{flex:1;padding:7px 6px;text-align:center} .btn-course{flex:1;padding:7px 6px;text-align:center}
.course-browse-bar select{margin-left:0;width:100%}
.main{height:auto;min-height:calc(100svh - 98px);flex-direction:column} .main{height:auto;min-height:calc(100svh - 98px);flex-direction:column}
.list-panel{width:100%;min-width:0;border-right:none;min-height:calc(100svh - 98px)} .list-panel{width:100%;min-width:0;border-right:none;min-height:calc(100svh - 98px)}
.detail-panel{display:none;width:100%;padding:16px 14px} .detail-panel{display:none;width:100%;padding:16px 14px}
@@ -120,7 +122,7 @@ header p{font-size:0.85rem;color:#f0cca0;margin-top:3px}
<h1>🍽️ Inlander Restaurant Week Picker</h1> <h1>🍽️ Inlander Restaurant Week Picker</h1>
<p>Browse menus · Mark favorites · Let fate decide</p> <p>Browse menus · Mark favorites · Let fate decide</p>
</div> </div>
<span style="margin-left:auto;font-size:1.5rem;font-weight:700;color:#f5c842;white-space:nowrap">FEBRUARY 26 MARCH 7, 2026</span> <span id="eventDates" style="margin-left:auto;font-size:1.5rem;font-weight:700;color:#f5c842;white-space:nowrap">FEBRUARY 26 MARCH 7, 2026</span>
</header> </header>
<div class="toolbar"> <div class="toolbar">
<input type="text" id="search" placeholder="Search restaurants..." oninput="applyFilters()"/> <input type="text" id="search" placeholder="Search restaurants..." oninput="applyFilters()"/>
@@ -147,6 +149,10 @@ header p{font-size:0.85rem;color:#f0cca0;margin-top:3px}
<button class="btn btn-course" onclick="openCourseBrowse('First Course')">🥗 First Course</button> <button class="btn btn-course" onclick="openCourseBrowse('First Course')">🥗 First Course</button>
<button class="btn btn-course" onclick="openCourseBrowse('Second Course')">🍖 Second Course</button> <button class="btn btn-course" onclick="openCourseBrowse('Second Course')">🍖 Second Course</button>
<button class="btn btn-course" onclick="openCourseBrowse('Third Course')">🍮 Third Course</button> <button class="btn btn-course" onclick="openCourseBrowse('Third Course')">🍮 Third Course</button>
<select id="yearSelect" onchange="loadYear(this.value)">
<option value="2026">2026</option>
<option value="2025">2025</option>
</select>
</div> </div>
<div class="main"> <div class="main">
<div class="list-panel" id="listPanel"></div> <div class="list-panel" id="listPanel"></div>
@@ -172,8 +178,8 @@ header p{font-size:0.85rem;color:#f0cca0;margin-top:3px}
</div> </div>
<script> <script>
let RESTAURANTS = []; let RESTAURANTS = [];
let currentYear = '2026';
let interests = JSON.parse(localStorage.getItem('irw_interests')||'{}'); let interests = JSON.parse(localStorage.getItem('irw_interests_2026')||'{}');
let filteredList = []; let filteredList = [];
let activeSlug = null; let activeSlug = null;
let currentWinner = null; let currentWinner = null;
@@ -240,7 +246,7 @@ function toggleInterest(slug,who,checked){
if(!interests[slug]) interests[slug]={}; if(!interests[slug]) interests[slug]={};
interests[slug][who]=checked; interests[slug][who]=checked;
if(!interests[slug].you && !interests[slug].wife) delete interests[slug]; if(!interests[slug].you && !interests[slug].wife) delete interests[slug];
localStorage.setItem('irw_interests',JSON.stringify(interests)); localStorage.setItem('irw_interests_'+currentYear,JSON.stringify(interests));
renderList(); renderList();
updateCounts(); updateCounts();
if(activeSlug===slug) showDetail(slug); if(activeSlug===slug) showDetail(slug);
@@ -249,7 +255,7 @@ function toggleInterest(slug,who,checked){
function clearAll(){ function clearAll(){
if(!confirm('Clear all your selections?')) return; if(!confirm('Clear all your selections?')) return;
interests={}; interests={};
localStorage.setItem('irw_interests',JSON.stringify(interests)); localStorage.setItem('irw_interests_'+currentYear,JSON.stringify(interests));
renderList(); updateCounts(); renderList(); updateCounts();
} }
function resetFilters(){ function resetFilters(){
@@ -354,15 +360,37 @@ function openCourseBrowse(course){
function closeCourseBrowse(){ document.getElementById('courseBrowseOverlay').style.display = 'none'; } function closeCourseBrowse(){ document.getElementById('courseBrowseOverlay').style.display = 'none'; }
function selectCourseDish(slug){ closeCourseBrowse(); showDetail(slug); } function selectCourseDish(slug){ closeCourseBrowse(); showDetail(slug); }
// Init function loadYear(year){
fetch('2026-restaurants.json') currentYear = year;
interests = JSON.parse(localStorage.getItem('irw_interests_'+year)||'{}');
activeSlug = null;
currentWinner = null;
document.querySelector('.main').classList.remove('show-detail');
document.getElementById('detailPanel').innerHTML = `
<div class="detail-empty">
<div class="icon">🍴</div>
<h3 style="color:#8b4a20">Select a restaurant to see its menu</h3>
<p>Check boxes to mark ones you're interested in, then hit "Pick for Us!"</p>
</div>`;
document.getElementById('filterArea').innerHTML = '<option value="">All Areas</option>';
document.getElementById('filterCuisine').innerHTML = '<option value="">All Cuisines</option>';
resetFilters();
fetch(year+'-restaurants.json', {cache:'no-store'})
.then(function(r){ return r.json(); }) .then(function(r){ return r.json(); })
.then(function(data){ .then(function(data){
RESTAURANTS = data; RESTAURANTS = Array.isArray(data) ? data : data.restaurants;
if(data.eventDates) document.getElementById('eventDates').textContent = data.eventDates;
initDropdowns(); initDropdowns();
applyFilters(); applyFilters();
updateCounts(); updateCounts();
})
.catch(function(err){
document.getElementById('listPanel').innerHTML = '<div class="no-results">⚠️ Failed to load restaurant data.<br><small>'+err+'</small><br><small>Try serving this file from a local web server instead of opening it directly.</small></div>';
}); });
}
// Init
loadYear('2026');
</script> </script>
<div class="course-modal-overlay" id="courseBrowseOverlay" style="display:none" onclick="if(event.target===this)closeCourseBrowse()"> <div class="course-modal-overlay" id="courseBrowseOverlay" style="display:none" onclick="if(event.target===this)closeCourseBrowse()">
<div class="course-modal"> <div class="course-modal">