neiam
/fieri-forever
the flavortown lunch engine
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
Log in to mark this repository.
"To try" rank
6a8dcc6 · Gabriel Morell · 2026-10-28 21:56
Message
{commit_body(@commit)}
Files changed
modified
README.md
+5
−3
| 3 | 3 | Where are we going for lunch? Not there again. |
| 4 | 4 | |
| 5 | 5 | People pin restaurants on an OpenStreetMap map and rank each one **Obsessed** |
| 6 | −(since a date), **Always**, **Positive**, **Neutral** or **Never**. When a group goes out, the app suggests | |
| 6 | +(since a date), **Always**, **Positive**, **To try**, **Neutral** or **Never**. When a group goes out, the app suggests | |
| 7 | 7 | somewhere from what everyone going likes and where the team hasn't been |
| 8 | 8 | lately, and the group logs where it actually went so the next pick knows. |
| 9 | 9 |
| 122 | 122 | All in `src/suggest.rs`, pure functions with unit tests. For the people going: |
| 123 | 123 | |
| 124 | 124 | - **Veto.** If anyone going ranked a place *Never*, it's out. |
| 125 | −- **Liked.** The group's average weight: Obsessed 6, Always 4, Positive 2, | |
| 126 | − Neutral 1. Obsessed records when it started; picking it again keeps that | |
| 125 | +- **Liked.** The group's average weight: Obsessed 6, Always 4, To try 3, | |
| 126 | + Positive 2, Neutral 1. A "to try" counts as Neutral once that person has | |
| 127 | + been on a logged lunch there (on or after the day they marked it), and the | |
| 128 | + rank buttons ask how it was. Obsessed records when it started; picking it again keeps that | |
| 127 | 129 | date, and it can be backdated from the rank widget. |
| 128 | 130 | Unranked counts as Neutral, so nobody has to rank everything. |
| 129 | 131 | - **Open.** If OSM says a place is closed at the meal time (noon by |
modified
assets/f/css/style.css
+1
−0
| 25 | 25 | .rank-pick-obsessed:has(:checked) { @apply btn-warning; } |
| 26 | 26 | .rank-pick-always:has(:checked) { @apply btn-success; } |
| 27 | 27 | .rank-pick-positive:has(:checked) { @apply btn-accent; } |
| 28 | +.rank-pick-to_try:has(:checked) { @apply btn-info; } | |
| 28 | 29 | .rank-pick-neutral:has(:checked) { @apply btn-neutral; } |
| 29 | 30 | .rank-pick-never:has(:checked) { @apply btn-error; } |
| 30 | 31 | /* daisyUI's colour classes only set --btn-color; .btn turns that into a |
modified
assets/f/js/map.js
+5
−3
| 22 | 22 | sky: "blueprint", forest: "night", clays: "night", |
| 23 | 23 | }; |
| 24 | 24 | |
| 25 | −// Rank colours, as in the badges: flame / success / lime / neutral / error. | |
| 25 | +// Rank colours, as in the badges: flame / success / lime / info / neutral / | |
| 26 | +// error. Unranked is white, so blue can mean "to try". | |
| 26 | 27 | const RANK_COLORS = { |
| 27 | − obsessed: "#ea580c", always: "#16a34a", positive: "#84cc16", neutral: "#a8a29e", never: "#dc2626", | |
| 28 | + obsessed: "#ea580c", always: "#16a34a", positive: "#84cc16", to_try: "#3b82f6", | |
| 29 | + neutral: "#a8a29e", never: "#dc2626", | |
| 28 | 30 | }; |
| 29 | −const UNRANKED = "#3b82f6"; | |
| 31 | +const UNRANKED = "#ffffff"; | |
| 30 | 32 | const CANDIDATE = "#f59e0b"; |
| 31 | 33 | |
| 32 | 34 | const maps = new Set(); |
modified
src/entities/rankings.rs
+17
−2
| 13 | 13 | Always, |
| 14 | 14 | #[sea_orm(string_value = "positive")] |
| 15 | 15 | Positive, |
| 16 | + /// Not been yet, want to go. | |
| 17 | + #[sea_orm(string_value = "to_try")] | |
| 18 | + ToTry, | |
| 16 | 19 | #[sea_orm(string_value = "neutral")] |
| 17 | 20 | Neutral, |
| 18 | 21 | #[sea_orm(string_value = "never")] |
| 21 | 24 | |
| 22 | 25 | impl RankKind { |
| 23 | 26 | /// Strongest first — the order the rank buttons appear in. |
| 24 | − pub const ALL: [RankKind; 5] = | |
| 25 | − [RankKind::Obsessed, RankKind::Always, RankKind::Positive, RankKind::Neutral, RankKind::Never]; | |
| 27 | + pub const ALL: [RankKind; 6] = [ | |
| 28 | + RankKind::Obsessed, | |
| 29 | + RankKind::Always, | |
| 30 | + RankKind::Positive, | |
| 31 | + RankKind::ToTry, | |
| 32 | + RankKind::Neutral, | |
| 33 | + RankKind::Never, | |
| 34 | + ]; | |
| 26 | 35 | |
| 27 | 36 | pub fn as_str(self) -> &'static str { |
| 28 | 37 | match self { |
| 29 | 38 | RankKind::Obsessed => "obsessed", |
| 30 | 39 | RankKind::Always => "always", |
| 31 | 40 | RankKind::Positive => "positive", |
| 41 | + RankKind::ToTry => "to_try", | |
| 32 | 42 | RankKind::Neutral => "neutral", |
| 33 | 43 | RankKind::Never => "never", |
| 34 | 44 | } |
| 48 | 58 | Obsessed { since: Date }, |
| 49 | 59 | Always, |
| 50 | 60 | Positive, |
| 61 | + /// Not been yet, want to go. Once you have been (see `crate::suggest`), | |
| 62 | + /// it counts as Neutral until you rank it properly. | |
| 63 | + ToTry, | |
| 51 | 64 | Neutral, |
| 52 | 65 | Never, |
| 53 | 66 | } |
| 58 | 71 | Rank::Obsessed { .. } => RankKind::Obsessed, |
| 59 | 72 | Rank::Always => RankKind::Always, |
| 60 | 73 | Rank::Positive => RankKind::Positive, |
| 74 | + Rank::ToTry => RankKind::ToTry, | |
| 61 | 75 | Rank::Neutral => RankKind::Neutral, |
| 62 | 76 | Rank::Never => RankKind::Never, |
| 63 | 77 | } |
| 81 | 95 | RankKind::Obsessed => Rank::Obsessed { since: since.unwrap_or(fallback) }, |
| 82 | 96 | RankKind::Always => Rank::Always, |
| 83 | 97 | RankKind::Positive => Rank::Positive, |
| 98 | + RankKind::ToTry => Rank::ToTry, | |
| 84 | 99 | RankKind::Neutral => Rank::Neutral, |
| 85 | 100 | RankKind::Never => Rank::Never, |
| 86 | 101 | } |
modified
src/handlers/areas.rs
+7
−5
| 134 | 134 | restaurant: restaurants::Model, |
| 135 | 135 | my_rank: Option<Rank>, |
| 136 | 136 | my_since: Option<NaiveDate>, |
| 137 | + /// Your "to try" here has been tried; time to rank it properly. | |
| 138 | + tried: bool, | |
| 137 | 139 | cuisines: Vec<String>, |
| 138 | 140 | added_by: Option<String>, |
| 139 | 141 | visits: usize, |
| 168 | 170 | .map_err(internal("list restaurants failed"))?; |
| 169 | 171 | let ids: Vec<Uuid> = list.iter().map(|r| r.id).collect(); |
| 170 | 172 | |
| 171 | − let my_ranks: HashMap<Uuid, Rank> = rankings::Entity::find() | |
| 173 | + let my_rows = rankings::Entity::find() | |
| 172 | 174 | .filter(rankings::Column::UserId.eq(user.user.id)) |
| 173 | 175 | .filter(rankings::Column::RestaurantId.is_in(ids.clone())) |
| 174 | 176 | .all(db) |
| 175 | 177 | .await |
| 176 | − .map_err(internal("load rankings failed"))? | |
| 177 | − .into_iter() | |
| 178 | − .map(|r| (r.restaurant_id, r.rank())) | |
| 179 | − .collect(); | |
| 178 | + .map_err(internal("load rankings failed"))?; | |
| 179 | + let tried = super::lunch::tried(db, &my_rows).await?; | |
| 180 | + let my_ranks: HashMap<Uuid, Rank> = my_rows.into_iter().map(|r| (r.restaurant_id, r.rank())).collect(); | |
| 180 | 181 | |
| 181 | 182 | let mut history: HashMap<Uuid, Vec<NaiveDate>> = HashMap::new(); |
| 182 | 183 | for l in lunches::Entity::find() |
| 195 | 196 | RestaurantRow { |
| 196 | 197 | my_rank: my_ranks.get(&r.id).copied(), |
| 197 | 198 | my_since: my_ranks.get(&r.id).and_then(|k| k.since()), |
| 199 | + tried: tried.contains(&(user.user.id, r.id)), | |
| 198 | 200 | cuisines: r.cuisine_tags(), |
| 199 | 201 | added_by: r.added_by.and_then(|u| names.get(&u)).map(|n| (*n).to_owned()), |
| 200 | 202 | visits: visits.map_or(0, Vec::len), |
modified
src/handlers/lunch.rs
+42
−5
Click to load diff…
modified
src/handlers/restaurants.rs
+11
−6
Click to load diff…
modified
src/lib.rs
+1
−1
Click to load diff…
modified
src/suggest.rs
+15
−1
Click to load diff…
modified
templates/_macros.html
+10
−6
Click to load diff…
modified
templates/areas/show.html
+1
−1
Click to load diff…
modified
templates/lunch/_plan.html
+1
−1
Click to load diff…
modified
templates/restaurants/_rank.html
+6
−3
Click to load diff…
modified
templates/restaurants/new.html
+2
−2
Click to load diff…
modified
templates/restaurants/show.html
+1
−1
Click to load diff…
modified
tests/templates.rs
+34
−0
Click to load diff…
Parents: 00e6765