Compare commits
11 Commits
ab9abdb53e
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 5d70a6416b | |||
| 984fb97499 | |||
| ebb3a6ad08 | |||
| a149559a74 | |||
| 76b498ae14 | |||
| b17fc2d6e0 | |||
| c8c8f028fa | |||
| 03ee2b3645 | |||
| 8ded70b743 | |||
| fe07a23a2f | |||
| b455bae614 |
@@ -1,4 +1,6 @@
|
||||
[
|
||||
{
|
||||
"eventDates": "FEBRUARY 27 – MARCH 8, 2025",
|
||||
"restaurants": [
|
||||
{
|
||||
"name": "1898 Public House",
|
||||
"slug": "1898",
|
||||
@@ -2519,13 +2521,46 @@
|
||||
"phone": "(509) 863-9235",
|
||||
"courses": {
|
||||
"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": [
|
||||
|
||||
{
|
||||
"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": [
|
||||
|
||||
{
|
||||
"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",
|
||||
"courses": {
|
||||
"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": [
|
||||
|
||||
{
|
||||
"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": [
|
||||
|
||||
{
|
||||
"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",
|
||||
"courses": {
|
||||
"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": [
|
||||
|
||||
{
|
||||
"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 fixin’s 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": [
|
||||
|
||||
{
|
||||
"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",
|
||||
"courses": {
|
||||
"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": [
|
||||
|
||||
{
|
||||
"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": [
|
||||
|
||||
{
|
||||
"name": "Blueberry Crumble",
|
||||
"desc": "Sweet-tasting blueberry filling, delicious shortbread crust and topped with an old-fashioned coconut and oat crumble V "
|
||||
},
|
||||
{
|
||||
"name": "Reese’s Tart",
|
||||
"desc": "Vegan tart shell filled with Reese’s 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",
|
||||
"courses": {
|
||||
"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": [
|
||||
|
||||
{
|
||||
"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": [
|
||||
|
||||
{
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"name": "Tavolàta",
|
||||
"name": "Salad",
|
||||
"desc": "Little gem lettuce, chicory, pistachio, red onion, Calabrian vinaigrette, pecorino GF V"
|
||||
},
|
||||
{
|
||||
@@ -5825,12 +5992,23 @@
|
||||
"desc": "Black pepper, butter, pecorino romano GFA"
|
||||
},
|
||||
{
|
||||
"name": "Roasted",
|
||||
"name": "Pork Loin",
|
||||
"desc": "Fig mostarda, radicchio, hazelnut bread crumb GFA"
|
||||
}
|
||||
],
|
||||
"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 @@
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
[
|
||||
{
|
||||
"eventDates": "FEBRUARY 26 – MARCH 7, 2026",
|
||||
"restaurants": [
|
||||
{
|
||||
"name": "315",
|
||||
"slug": "315cuisine",
|
||||
@@ -5254,44 +5256,44 @@
|
||||
"courses": {
|
||||
"First Course": [
|
||||
{
|
||||
"name": "V Nut Free",
|
||||
"desc": "Cauliflower WingsBuffalo or Thai ginger style • carrot sticks • dipping sauce df – Make it full size: $7"
|
||||
"name": "Cauliflower Wings",
|
||||
"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",
|
||||
"desc": "Blistered Shishitosmiso-tahini sauce • lemon salt • togarashi – Make it full size: $7"
|
||||
"name": "Blistered Shishitos",
|
||||
"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",
|
||||
"desc": "Creamy Tomato Soupbreadcrumbs • parsley • parmesan – Make it a bowl $4"
|
||||
"name": "Creamy Tomato Soup",
|
||||
"desc": "breadcrumbs • parsley • parmesan GFA V+ DF Nut Free Soy Free – Make it a bowl $4"
|
||||
}
|
||||
],
|
||||
"Second Course": [
|
||||
{
|
||||
"name": "GFA V+ DF Nut Free",
|
||||
"desc": "Bbq Jackfruit Sandwichrainbow slaw • crispy onion • Carolina style BBQ sauce • pretzel bun • choice of side"
|
||||
"name": "Bbq Jackfruit Sandwich",
|
||||
"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",
|
||||
"desc": "Lemongrass Yellow Currytofu • jasmine rice • roasted radish • snap pea • red bell pepper • carrot slaw • crispy shallot"
|
||||
"name": "Lemongrass Yellow Curry",
|
||||
"desc": "tofu • jasmine rice • roasted radish • snap pea • red bell pepper • carrot slaw • crispy shallot GFA V+ DF Nut Free"
|
||||
},
|
||||
{
|
||||
"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": [
|
||||
{
|
||||
"name": "GF V+",
|
||||
"desc": "Tea Timeearl grey cheesecake • lemon coulis • lemon-ginger sugar cookie"
|
||||
"name": "Tea Time",
|
||||
"desc": "earl grey cheesecake • lemon coulis • lemon-ginger sugar cookie GF V+ DF"
|
||||
},
|
||||
{
|
||||
"name": "GF V DF Nut Free",
|
||||
"desc": "Cookies \u0026 Creamchocolate chunk cookie • vanilla ice cream • raspberry coulis"
|
||||
"name": "Cookies & Cream",
|
||||
"desc": "chocolate chunk cookie • vanilla ice cream • raspberry coulis GF V DF Nut Free"
|
||||
},
|
||||
{
|
||||
"name": "GF V+",
|
||||
"desc": "Turtle Torteflourless chocolate torte • miso caramel • candied walnuts"
|
||||
"name": "Turtle Torte",
|
||||
"desc": "flourless chocolate torte • miso caramel • candied walnuts GF V+ DF"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -6417,12 +6419,12 @@
|
||||
"courses": {
|
||||
"First Course": [
|
||||
{
|
||||
"name": "V DF",
|
||||
"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"
|
||||
"name": "Crispy Spring Rolls",
|
||||
"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+",
|
||||
"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"
|
||||
"name": "Golden Samosas",
|
||||
"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",
|
||||
@@ -6449,8 +6451,8 @@
|
||||
"desc": "Your choice of two scoops: vanilla, coconut, huckleberry or Thai tea GF"
|
||||
},
|
||||
{
|
||||
"name": "GF V V+",
|
||||
"desc": "Mango Sticky RiceWarm, sweet white sticky rice topped with mango and sweetened coconut cream"
|
||||
"name": "Mango Sticky Rice",
|
||||
"desc": "Warm, sweet white sticky rice topped with mango and sweetened coconut cream GF V V+ DF"
|
||||
},
|
||||
{
|
||||
"name": "Coconut Cheesecake",
|
||||
@@ -7160,3 +7162,4 @@
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
38
README.md
38
README.md
@@ -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
|
||||
- **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
|
||||
- **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
|
||||
- **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
|
||||
@@ -23,7 +24,7 @@ Each couple gets their own set of interest checkboxes ("His Picks" / "Her Picks"
|
||||
|
||||
## 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.
|
||||
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.
|
||||
@@ -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
|
||||
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
|
||||
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.
|
||||
|
||||
### 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
|
||||
|
||||
```json
|
||||
@@ -179,7 +207,9 @@ Run the following Python script against the JSON file to identify any restaurant
|
||||
import json
|
||||
|
||||
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:
|
||||
name = r.get('name', 'Unknown')
|
||||
|
||||
123
fix-tavolata.ps1
123
fix-tavolata.ps1
@@ -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 '&','&' -replace ''',"'" -replace '"','"' -replace '<','<' -replace '>','>' -replace ' ',' ' -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"
|
||||
}
|
||||
@@ -35,15 +35,14 @@ Each entry in `YEAR-restaurants.json`:
|
||||
```
|
||||
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)
|
||||
- **Wayback snapshot used**: `20250306132630` (primary), `20250401000000` (backup for some)
|
||||
- **Complete (3/3/3+)**: 111 restaurants
|
||||
- **Wayback snapshot used**: `20250306132630`
|
||||
- **Complete (3/3/3+)**: 112 restaurants
|
||||
- **gardenparty**: 3/3/4 — correct, it genuinely offers 4 dessert choices
|
||||
- **tavolata**: 3/3/0 — needs fix-tavolata.ps1 run when rate limit resets
|
||||
- **tavolata**: 3/3/3 — FIXED (recovered Third Course from snapshot `20250306132630`)
|
||||
- **0/0/0 (JS-only, unrecoverable)**: heritage, kismet, littlenoodle, macdaddys, purgatory, redtail, republickitchen, republicpi, vicinopizza
|
||||
|
||||
## Scripts in Project Directory
|
||||
- `fix-tavolata.ps1` — run after rate limit resets to recover tavolata Third Course
|
||||
- Copy to local temp and run: `cp ...\fix-tavolata.ps1 C:\Users\derekc.CHNSLocal\AppData\Local\Temp\`
|
||||
- Then: `powershell.exe -ExecutionPolicy Bypass -File C:\Users\derekc.CHNSLocal\AppData\Local\Temp\fix-tavolata.ps1`
|
||||
- `fix-tavolata.ps1` — already run, tavolata is complete; kept for reference
|
||||
- `check-2025.ps1` — validates all restaurant course counts
|
||||
|
||||
@@ -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}}
|
||||
.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}
|
||||
.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: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}
|
||||
@@ -97,8 +98,9 @@ header p{font-size:0.85rem;color:#f0cca0;margin-top:3px}
|
||||
.toolbar select{flex:1;min-width:0}
|
||||
.spacer{display:none}
|
||||
.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}
|
||||
.course-browse-bar select{margin-left:0;width:100%}
|
||||
.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)}
|
||||
.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>
|
||||
<p>Browse menus · Mark favorites · Let fate decide</p>
|
||||
</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>
|
||||
<div class="toolbar">
|
||||
<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('Second Course')">🍖 Second 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 class="main">
|
||||
<div class="list-panel" id="listPanel"></div>
|
||||
@@ -172,8 +178,8 @@ header p{font-size:0.85rem;color:#f0cca0;margin-top:3px}
|
||||
</div>
|
||||
<script>
|
||||
let RESTAURANTS = [];
|
||||
|
||||
let interests = JSON.parse(localStorage.getItem('irw_interests')||'{}');
|
||||
let currentYear = '2026';
|
||||
let interests = JSON.parse(localStorage.getItem('irw_interests_2026')||'{}');
|
||||
let filteredList = [];
|
||||
let activeSlug = null;
|
||||
let currentWinner = null;
|
||||
@@ -240,7 +246,7 @@ function toggleInterest(slug,who,checked){
|
||||
if(!interests[slug]) interests[slug]={};
|
||||
interests[slug][who]=checked;
|
||||
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();
|
||||
updateCounts();
|
||||
if(activeSlug===slug) showDetail(slug);
|
||||
@@ -249,7 +255,7 @@ function toggleInterest(slug,who,checked){
|
||||
function clearAll(){
|
||||
if(!confirm('Clear all your selections?')) return;
|
||||
interests={};
|
||||
localStorage.setItem('irw_interests',JSON.stringify(interests));
|
||||
localStorage.setItem('irw_interests_'+currentYear,JSON.stringify(interests));
|
||||
renderList(); updateCounts();
|
||||
}
|
||||
function resetFilters(){
|
||||
@@ -354,15 +360,37 @@ function openCourseBrowse(course){
|
||||
function closeCourseBrowse(){ document.getElementById('courseBrowseOverlay').style.display = 'none'; }
|
||||
function selectCourseDish(slug){ closeCourseBrowse(); showDetail(slug); }
|
||||
|
||||
// Init
|
||||
fetch('2026-restaurants.json')
|
||||
function loadYear(year){
|
||||
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(data){
|
||||
RESTAURANTS = data;
|
||||
RESTAURANTS = Array.isArray(data) ? data : data.restaurants;
|
||||
if(data.eventDates) document.getElementById('eventDates').textContent = data.eventDates;
|
||||
initDropdowns();
|
||||
applyFilters();
|
||||
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>
|
||||
<div class="course-modal-overlay" id="courseBrowseOverlay" style="display:none" onclick="if(event.target===this)closeCourseBrowse()">
|
||||
<div class="course-modal">
|
||||
|
||||
Reference in New Issue
Block a user