Skip to content

Commit 59f9bfa

Browse files
authored
Merge pull request #50 from Waybox-AI/codex/sync-booking-previews
chore: sync Chicago static preview
2 parents 4c9937f + 2789a11 commit 59f9bfa

1 file changed

Lines changed: 309 additions & 17 deletions

File tree

assets/preview-chicago.html

Lines changed: 309 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@
114114
font-size: .72rem; font-weight: 700; white-space: nowrap; }
115115
.booking-price.unknown { color: var(--muted); font-weight: 600; }
116116
.booking-copy p { margin: 0; color: var(--muted); font-size: .82rem; }
117+
.booking-copy .booking-item-meta { color: var(--ink); font-weight: 600; }
117118
.booking-copy p + p { margin-top: 3px; }
118119
.booking-empty { margin: 18px 0 8px; color: var(--muted); font-size: .88rem; }
119120

@@ -978,9 +979,226 @@ <h2>📅 Day by day</h2>
978979
return "attraction";
979980
}
980981

981-
function bookingDateParts(value) {
982+
function bookingMatchTokens(value) {
983+
var stop = ["and", "area", "book", "booking", "cabin", "camp", "campsite", "canyon",
984+
"cafe", "city", "dining", "direct", "grand", "guided", "hotel", "hostel", "inn",
985+
"lodge", "lodging", "meal", "motel", "national", "park", "point", "reservation",
986+
"reserve", "resort", "restaurant", "room", "the", "tour"];
987+
return (String(value || "").toLowerCase().match(/[a-z0-9\u3400-\u9fff]{3,}/g) || [])
988+
.filter(function (token) { return stop.indexOf(token) < 0; });
989+
}
990+
991+
function sharedBookingTokens(left, right) {
992+
var rightTokens = bookingMatchTokens(right);
993+
return bookingMatchTokens(left).filter(function (token, index, tokens) {
994+
return tokens.indexOf(token) === index && rightTokens.indexOf(token) >= 0;
995+
}).length;
996+
}
997+
998+
function bookingDeadlineScore(item, deadline) {
999+
var deadlineText = [deadline.item, deadline.where, deadline.note].join(" ").toLowerCase();
1000+
var name = String(item.item || item.name || "").toLowerCase();
1001+
var context = String(item.matchContext || item.area || "").toLowerCase();
1002+
var score = sharedBookingTokens(name, deadlineText) * 3 +
1003+
sharedBookingTokens(context, deadlineText);
1004+
if (name.length >= 4 && deadlineText.indexOf(name) >= 0) score += 10;
1005+
if (context.length >= 4 && deadlineText.indexOf(context) >= 0) score += 8;
1006+
return score;
1007+
}
1008+
1009+
function bookingDeadlineAssignments(items, deadlines) {
1010+
var matches = {};
1011+
var usedDeadlines = {};
1012+
deadlines.forEach(function (deadline, deadlineIndex) {
1013+
var bestItem = -1;
1014+
var bestScore = 1;
1015+
items.forEach(function (item, itemIndex) {
1016+
if (matches[itemIndex]) return;
1017+
var score = bookingDeadlineScore(item, deadline);
1018+
if (score > bestScore) { bestScore = score; bestItem = itemIndex; }
1019+
});
1020+
if (bestItem >= 0) {
1021+
matches[bestItem] = deadline;
1022+
usedDeadlines[deadlineIndex] = true;
1023+
}
1024+
});
1025+
return { matches: matches, usedDeadlines: usedDeadlines };
1026+
}
1027+
1028+
function bookingItemKey(value) {
1029+
return String(value || "").toLowerCase().replace(/[^a-z0-9\u3400-\u9fff]+/g, " ").trim();
1030+
}
1031+
1032+
function appendVisitDate(item, date) {
1033+
if (date && item.visitDates.indexOf(date) < 0) item.visitDates.push(date);
1034+
}
1035+
1036+
function hotelBookingItems(list) {
1037+
var deadlines = list.filter(function (item) { return bookingCategory(item) === "hotel"; });
1038+
var hotels = Array.isArray(T.lodging) ? T.lodging.filter(function (hotel) {
1039+
return hotel && (hotel.name || hotel.area);
1040+
}) : [];
1041+
if (!hotels.length) return deadlines;
1042+
1043+
var assignments = bookingDeadlineAssignments(hotels, deadlines);
1044+
1045+
var merged = hotels.map(function (hotel, hotelIndex) {
1046+
var deadline = assignments.matches[hotelIndex] || {};
1047+
var price = deadline.price;
1048+
if (typeof hotel.pricePerNight === "number" && isFinite(hotel.pricePerNight) &&
1049+
hotel.pricePerNight >= 0) {
1050+
price = {
1051+
amount: hotel.pricePerNight,
1052+
currency: price && price.currency ? price.currency : cur,
1053+
unit: "night",
1054+
reliability: price && price.reliability ? price.reliability : "reference"
1055+
};
1056+
}
1057+
return {
1058+
item: hotel.name || deadline.item || hotel.area,
1059+
bookBy: deadline.bookBy || "",
1060+
where: deadline.where || "",
1061+
category: "hotel",
1062+
price: price,
1063+
priority: deadline.priority,
1064+
note: deadline.note,
1065+
stayArea: hotel.area || "",
1066+
nights: hotel.nights,
1067+
bookingRequired: true,
1068+
booked: hotel.booked === true
1069+
};
1070+
});
1071+
deadlines.forEach(function (deadline, deadlineIndex) {
1072+
if (!assignments.usedDeadlines[deadlineIndex]) merged.push(deadline);
1073+
});
1074+
return merged;
1075+
}
1076+
1077+
function restaurantBookingItems(list) {
1078+
var deadlines = list.filter(function (item) { return bookingCategory(item) === "restaurant"; });
1079+
var meals = [];
1080+
var mealsByName = {};
1081+
(T.days || []).forEach(function (day) {
1082+
var meal = day.meal;
1083+
if (!meal || !meal.name) return;
1084+
var key = bookingItemKey(meal.name);
1085+
var item = mealsByName[key];
1086+
if (!item) {
1087+
item = {
1088+
item: meal.name,
1089+
category: "restaurant",
1090+
visitDates: [],
1091+
matchContext: day.title || ""
1092+
};
1093+
if (typeof meal.perPerson === "number" && isFinite(meal.perPerson) &&
1094+
meal.perPerson >= 0) {
1095+
item.price = {
1096+
amount: meal.perPerson, currency: cur, unit: "person", reliability: "estimate"
1097+
};
1098+
}
1099+
mealsByName[key] = item;
1100+
meals.push(item);
1101+
}
1102+
appendVisitDate(item, day.date);
1103+
});
1104+
if (!meals.length) return deadlines;
1105+
1106+
var assignments = bookingDeadlineAssignments(meals, deadlines);
1107+
var merged = meals.map(function (meal, mealIndex) {
1108+
var deadline = assignments.matches[mealIndex] || {};
1109+
return {
1110+
item: meal.item,
1111+
bookBy: deadline.bookBy || "",
1112+
where: deadline.where || "",
1113+
category: "restaurant",
1114+
price: deadline.price || meal.price,
1115+
priority: deadline.priority,
1116+
note: deadline.note,
1117+
visitDates: meal.visitDates,
1118+
bookingRequired: Boolean(deadline.bookBy)
1119+
};
1120+
});
1121+
deadlines.forEach(function (deadline, deadlineIndex) {
1122+
if (!assignments.usedDeadlines[deadlineIndex]) merged.push(deadline);
1123+
});
1124+
return merged;
1125+
}
1126+
1127+
function attractionBookingItems(list) {
1128+
var deadlines = list.filter(function (item) { return bookingCategory(item) === "attraction"; });
1129+
var excludedTypes = ["charge", "charging", "city", "food", "fuel", "gas", "hotel",
1130+
"lodging", "restaurant"];
1131+
var attractions = [];
1132+
var attractionsByName = {};
1133+
(T.days || []).forEach(function (day) {
1134+
(day.stops || []).forEach(function (stop) {
1135+
if (!stop || !stop.name || excludedTypes.indexOf(String(stop.type || "").toLowerCase()) >= 0) {
1136+
return;
1137+
}
1138+
var key = bookingItemKey(stop.name);
1139+
var item = attractionsByName[key];
1140+
if (!item) {
1141+
var admission = stop.admission && typeof stop.admission === "object"
1142+
? stop.admission : {};
1143+
var admissionStatus = String(admission.status || "").toLowerCase();
1144+
if (["free", "included", "paid", "unknown"].indexOf(admissionStatus) < 0) {
1145+
var legacyTicket = String(stop.ticket || "").toLowerCase();
1146+
admissionStatus = legacyTicket.indexOf("free") >= 0 ? "free" :
1147+
(legacyTicket.indexOf("pass") >= 0 ? "included" :
1148+
(/ticket|fee|guided tour/.test(legacyTicket) ? "paid" : "unknown"));
1149+
}
1150+
item = {
1151+
item: stop.name,
1152+
category: "attraction",
1153+
visitDates: [],
1154+
matchContext: [day.title, stop.type, stop.ticket, admission.label].join(" "),
1155+
admissionStatus: admissionStatus,
1156+
admissionLabel: admission.label || "",
1157+
price: admission.price
1158+
};
1159+
attractionsByName[key] = item;
1160+
attractions.push(item);
1161+
}
1162+
appendVisitDate(item, day.date);
1163+
});
1164+
});
1165+
if (!attractions.length) return deadlines;
1166+
1167+
var assignments = bookingDeadlineAssignments(attractions, deadlines);
1168+
var merged = attractions.map(function (attraction, attractionIndex) {
1169+
var deadline = assignments.matches[attractionIndex] || {};
1170+
return {
1171+
item: attraction.item,
1172+
bookBy: deadline.bookBy || "",
1173+
where: deadline.where || "",
1174+
category: "attraction",
1175+
price: deadline.price || attraction.price,
1176+
admissionStatus: attraction.admissionStatus,
1177+
admissionLabel: attraction.admissionLabel,
1178+
priority: deadline.priority,
1179+
note: deadline.note,
1180+
visitDates: attraction.visitDates,
1181+
bookingRequired: Boolean(deadline.bookBy)
1182+
};
1183+
});
1184+
deadlines.forEach(function (deadline, deadlineIndex) {
1185+
if (!assignments.usedDeadlines[deadlineIndex]) merged.push(deadline);
1186+
});
1187+
return merged;
1188+
}
1189+
1190+
function bookingDateParts(value, bookingRequired) {
9821191
var match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(value || ""));
983-
if (!match) return { short: esc(value || "—"), year: "", iso: "" };
1192+
if (!match) {
1193+
var noAdvanceBooking = bookingRequired === false;
1194+
return {
1195+
short: esc(value || (noAdvanceBooking
1196+
? (isZh() ? "无需提前预约" : "No advance booking")
1197+
: (isZh() ? "暂时不知道截止日期" : "Deadline unknown"))),
1198+
year: "",
1199+
iso: ""
1200+
};
1201+
}
9841202
var months = isZh()
9851203
? ["1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月"]
9861204
: ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"];
@@ -996,8 +1214,21 @@ <h2>📅 Day by day</h2>
9961214
if (typeof price === "number") price = { amount: price };
9971215
if (!price || typeof price !== "object" || typeof price.amount !== "number" ||
9981216
!isFinite(price.amount) || price.amount < 0) {
1217+
if (item.category === "attraction") {
1218+
if (item.admissionStatus === "free") {
1219+
return '<span class="booking-price">' + (isZh() ? "免费" : "Free") + "</span>";
1220+
}
1221+
if (item.admissionStatus === "included") {
1222+
return '<span class="booking-price">' +
1223+
(isZh() ? "公园通票已包含" : "Included in park pass") + "</span>";
1224+
}
1225+
if (item.admissionStatus === "paid") {
1226+
return '<span class="booking-price unknown">' +
1227+
(isZh() ? "收费 · 价格暂不可用" : "Paid · price unavailable") + "</span>";
1228+
}
1229+
}
9991230
return '<span class="booking-price unknown">' +
1000-
(isZh() ? "价格待确认" : "Check price") + "</span>";
1231+
(isZh() ? "价格暂不可用" : "Price unavailable") + "</span>";
10011232
}
10021233
if (price.amount === 0) {
10031234
return '<span class="booking-price">' + (isZh() ? "免费" : "Free") + "</span>";
@@ -1016,48 +1247,109 @@ <h2>📅 Day by day</h2>
10161247
Number(price.amount).toLocaleString("en-US") + esc(units[unit] || ("/" + unit)) + "</span>";
10171248
}
10181249

1250+
function tripDateLabel(value) {
1251+
var slash = /^(\d{1,2})\/(\d{1,2})$/.exec(String(value || ""));
1252+
if (slash) {
1253+
if (isZh()) return Number(slash[1]) + "月" + Number(slash[2]) + "日";
1254+
var months = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN",
1255+
"JUL", "AUG", "SEP", "OCT", "NOV", "DEC"];
1256+
return months[Number(slash[1]) - 1] + " " + Number(slash[2]);
1257+
}
1258+
var iso = bookingDateParts(value);
1259+
return iso.iso ? iso.short : String(value || "");
1260+
}
1261+
1262+
function bookingItemMetaMarkup(item) {
1263+
var bits = [];
1264+
if (item.visitDates && item.visitDates.length) {
1265+
bits.push((isZh() ? "行程日期 " : "Trip date ") +
1266+
item.visitDates.map(tripDateLabel).join(", "));
1267+
}
1268+
if (item.stayArea) bits.push(esc(item.stayArea));
1269+
if (typeof item.nights === "number" && item.nights > 0) {
1270+
bits.push(isZh() ? item.nights + " 晚" :
1271+
item.nights + " night" + (item.nights === 1 ? "" : "s"));
1272+
}
1273+
return bits.length ? '<p class="booking-item-meta">' + bits.join(" · ") + "</p>" : "";
1274+
}
1275+
1276+
function bookingPanelTitle(category, items) {
1277+
var count = items.length;
1278+
if (category.id === "hotel") {
1279+
var totalNights = items.reduce(function (sum, item) {
1280+
return sum + (typeof item.nights === "number" && item.nights > 0 ? item.nights : 0);
1281+
}, 0);
1282+
if (totalNights) {
1283+
return isZh()
1284+
? count + " 个住宿 · " + totalNights + " 晚"
1285+
: count + " stay" + (count === 1 ? "" : "s") + " · " + totalNights +
1286+
" night" + (totalNights === 1 ? "" : "s");
1287+
}
1288+
}
1289+
if (isZh()) {
1290+
if (category.id === "restaurant") return count + " 个用餐安排与预订截止日期";
1291+
if (category.id === "hotel") return count + " 个住宿安排与预订截止日期";
1292+
return count + " 个景点与预订截止日期";
1293+
}
1294+
var noun = category.id === "restaurant" ? "dining plan" :
1295+
(category.id === "hotel" ? "stay" : "attraction");
1296+
return count + " " + noun + (count === 1 ? "" : "s") + " and booking deadlines";
1297+
}
1298+
10191299
function bookingPanelMarkup(category, items, active) {
10201300
var label = isZh() ? category.zh : category.en;
1021-
var first = items.length ? bookingDateParts(items[0].bookBy) : null;
1301+
var first = null;
1302+
items.some(function (item) {
1303+
var date = bookingDateParts(item.bookBy, item.bookingRequired);
1304+
if (!date.iso) return false;
1305+
first = date;
1306+
return true;
1307+
});
10221308
var earliest = first
10231309
? (isZh() ? "最早截止 · " : "Earliest deadline · ") + first.short
10241310
: (isZh() ? "暂无截止日期" : "No deadlines yet");
1025-
var title = isZh()
1026-
? items.length + " 个" + label + "预订需要安排"
1027-
: items.length + " " + (items.length === 1 ? category.singular : label.toLowerCase()) +
1028-
" booking" + (items.length === 1 ? "" : "s") +
1029-
" to schedule";
1311+
var title = bookingPanelTitle(category, items);
10301312
var rows = items.map(function (item) {
1031-
var date = bookingDateParts(item.bookBy);
1313+
var date = bookingDateParts(item.bookBy, item.bookingRequired);
10321314
var source = item.where
10331315
? "<p>" + (isZh() ? "预订渠道:" : "via ") + esc(item.where) + "</p>" : "";
1316+
var meta = bookingItemMetaMarkup(item);
10341317
var note = item.note ? "<p>" + esc(item.note) + "</p>" : "";
10351318
var price = bookingPriceMarkup(item);
10361319
var priority = item.priority === "high" ? " pri-high" : "";
1320+
var deadlineLabel = date.iso
1321+
? (isZh() ? "预订截止 " : "Book by ") + item.bookBy
1322+
: (item.bookingRequired === false
1323+
? (isZh() ? "无需提前预约" : "No advance booking required")
1324+
: (isZh() ? "暂时不知道截止日期" : "Booking deadline unknown"));
10371325
return '<article class="booking-timeline-row">' +
10381326
'<time class="booking-timeline-date" datetime="' + esc(date.iso) + '" aria-label="' +
1039-
(isZh() ? "预订截止 " : "Book by ") + esc(item.bookBy) + '"><strong>' +
1327+
esc(deadlineLabel) + '"><strong>' +
10401328
date.short + "</strong><span>" + date.year + "</span></time>" +
10411329
'<span class="booking-timeline-dot' + priority + '" aria-hidden="true"></span>' +
10421330
'<div class="booking-copy"><div class="booking-title-row"><h3>' + esc(item.item) +
1043-
"</h3>" + price + "</div>" + source + note + "</div></article>";
1331+
"</h3>" + price + "</div>" + meta + source + note + "</div></article>";
10441332
}).join("");
10451333
if (!rows) rows = '<p class="booking-empty">' +
1046-
(isZh() ? "这个分类暂时没有需要提前预订的项目。" :
1047-
"Nothing in this category needs advance booking yet.") + "</p>";
1334+
(isZh() ? "这个分类暂时没有项目。" : "Nothing in this category yet.") + "</p>";
10481335
return '<section class="booking-panel" id="booking-panel-' + category.id +
10491336
'" role="tabpanel" tabindex="0" aria-labelledby="booking-tab-' + category.id + '"' +
10501337
(active ? "" : " hidden") + '><div class="booking-panel-head"><div>' +
1051-
'<p class="booking-kicker">' + label + (isZh() ? "预订" : " reservations") + "</p>" +
1338+
'<p class="booking-kicker">' + label + "</p>" +
10521339
"<h2>" + title + '</h2></div><span class="booking-earliest">' + earliest + "</span></div>" +
10531340
'<div class="booking-timeline">' + rows + "</div></section>";
10541341
}
10551342

10561343
function renderBookingTabs() {
10571344
var list = T.bookingCountdown || [];
1058-
if (!list.length) { el("bookingTabs").innerHTML = ""; return; }
10591345
var groups = { attraction: [], restaurant: [], hotel: [] };
1060-
list.forEach(function (item) { groups[bookingCategory(item)].push(item); });
1346+
groups.attraction = attractionBookingItems(list);
1347+
groups.restaurant = restaurantBookingItems(list);
1348+
groups.hotel = hotelBookingItems(list);
1349+
if (!groups.attraction.length && !groups.restaurant.length && !groups.hotel.length) {
1350+
el("bookingTabs").innerHTML = "";
1351+
return;
1352+
}
10611353
BOOKING_CATEGORIES.forEach(function (category) {
10621354
groups[category.id].sort(function (a, b) {
10631355
return String(a.bookBy || "9999").localeCompare(String(b.bookBy || "9999"));

0 commit comments

Comments
 (0)