Skip to content

Commit 05e7561

Browse files
committed
feat: start of upcoming races
1 parent 778c1c5 commit 05e7561

7 files changed

Lines changed: 127 additions & 35 deletions

File tree

src/application/race_service.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use crate::{
22
DbConnection,
3-
domain::race::{NewRaceResult, RaceView, SubmitTownSearchParams},
3+
domain::race::{NewRaceResult, RaceSearchParams, RaceView, SubmitTownSearchParams},
44
infrastructure::db::RaceRepository,
55
util::pagination::PaginatedResponse,
66
};
@@ -24,6 +24,13 @@ impl RaceService {
2424
}
2525

2626
pub async fn submit_result(&self, result: NewRaceResult) -> Result<(), String> {
27-
self.race_repository.save(result).await
27+
self.race_repository.save_result(result).await
28+
}
29+
30+
pub async fn search_for_upcoming(
31+
&self,
32+
params: RaceSearchParams,
33+
) -> PaginatedResponse<RaceView> {
34+
self.race_repository.search_for_upcoming(params).await
2835
}
2936
}

src/application/town_service.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ impl TownService {
3838

3939
let race_result = NewRaceResult::new(user.id, &race, form.notes);
4040
let _ = self.town_repository.mark_completed(user.id, town_id).await;
41-
let _ = self.race_repository.save(race_result).await;
41+
let _ = self.race_repository.save_result(race_result).await;
4242

4343
Ok(())
4444
}

src/domain/race.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use crate::{
66
distance::{DistanceUnit, Kilometers, Miles},
77
town::SubmitTown,
88
},
9-
util::pagination::Paginatable,
9+
util::pagination::{Paginatable, Pagination},
1010
};
1111

1212
#[derive(Debug, Deserialize, Serialize, FromRow)]
@@ -49,6 +49,23 @@ pub struct SubmitTownSearchParams {
4949
pub town_id: i64,
5050
}
5151

52+
#[derive(Deserialize)]
53+
pub struct RaceSearchParams {
54+
pub race_name: Option<String>,
55+
pub town_id: Option<i64>,
56+
pub page: Option<i64>,
57+
pub page_size: Option<i64>,
58+
}
59+
60+
impl From<RaceSearchParams> for Pagination {
61+
fn from(params: RaceSearchParams) -> Self {
62+
Self {
63+
page: params.page,
64+
page_size: params.page_size,
65+
}
66+
}
67+
}
68+
5269
pub struct NewRace {
5370
pub name: String,
5471
pub town_id: i64,

src/infrastructure/db/race_repository.rs

Lines changed: 34 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use sqlx::{query, query_as};
22

33
use crate::{
44
DbConnection,
5-
domain::race::{NewRace, NewRaceResult, RaceView, SubmitTownSearchParams},
5+
domain::race::{NewRace, NewRaceResult, RaceSearchParams, RaceView, SubmitTownSearchParams},
66
util::pagination::{Paginatable, PaginatedResponse, Pagination},
77
};
88

@@ -25,29 +25,6 @@ impl RaceRepository {
2525
Ok(race)
2626
}
2727

28-
pub async fn submit_town_search(
29-
&self,
30-
params: &SubmitTownSearchParams,
31-
) -> PaginatedResponse<RaceView> {
32-
let pattern = &format!("%{}%", params.race_name.to_lowercase());
33-
34-
RaceView::paginate_filter(
35-
&self.db,
36-
&Pagination::default(),
37-
Some(
38-
r#"
39-
LOWER(name) LIKE ? AND town_id = ? AND
40-
start_date >= DATE('now', '-6 months') AND
41-
start_date <= DATE('now')
42-
ORDER BY start_date DESC
43-
"#,
44-
),
45-
vec![pattern, &params.town_id.to_string()],
46-
)
47-
.await
48-
.unwrap()
49-
}
50-
5128
pub async fn get_or_create(&self, race: NewRace) -> Result<RaceView, String> {
5229
let race_id: i64 = sqlx::query_scalar(
5330
r#"
@@ -71,7 +48,7 @@ impl RaceRepository {
7148
self.find_by_id(race_id).await
7249
}
7350

74-
pub async fn save(&self, result: NewRaceResult) -> Result<(), String> {
51+
pub async fn save_result(&self, result: NewRaceResult) -> Result<(), String> {
7552
query(
7653
r#"
7754
INSERT INTO race_results (user_id, race_id, notes)
@@ -87,4 +64,36 @@ impl RaceRepository {
8764

8865
Ok(())
8966
}
67+
68+
pub async fn search_for_upcoming(
69+
&self,
70+
params: RaceSearchParams,
71+
) -> PaginatedResponse<RaceView> {
72+
RaceView::paginate_filter(&self.db, &Pagination::from(params), None, vec![])
73+
.await
74+
.unwrap()
75+
}
76+
77+
pub async fn submit_town_search(
78+
&self,
79+
params: &SubmitTownSearchParams,
80+
) -> PaginatedResponse<RaceView> {
81+
let pattern = &format!("%{}%", params.race_name.to_lowercase());
82+
83+
RaceView::paginate_filter(
84+
&self.db,
85+
&Pagination::default(),
86+
Some(
87+
r#"
88+
LOWER(name) LIKE ? AND town_id = ? AND
89+
start_date >= DATE('now', '-6 months') AND
90+
start_date <= DATE('now')
91+
ORDER BY start_date DESC
92+
"#,
93+
),
94+
vec![pattern, &params.town_id.to_string()],
95+
)
96+
.await
97+
.unwrap()
98+
}
9099
}

src/routes/races.rs

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,39 @@
1-
use axum::{Router, extract::State, routing::get};
1+
use crate::{
2+
domain::{
3+
race::{RaceSearchParams, RaceView},
4+
rbac::Role,
5+
},
6+
util::pagination::PaginatedResponse,
7+
};
8+
use askama::Template;
9+
use askama_web::WebTemplate;
10+
use axum::{
11+
Router,
12+
extract::{Query, State},
13+
response::IntoResponse,
14+
routing::get,
15+
};
216

3-
use crate::SharedState;
17+
use crate::{SharedState, extract::MaybeCurrentUser, routes::SharedContext};
418

519
pub fn routes() -> Router<SharedState> {
6-
Router::new().route("/races", get(races))
20+
Router::new().route("/upcoming-races", get(upcoming_races))
721
}
822

9-
async fn races(State(_): State<SharedState>) {
10-
todo!()
23+
#[derive(Template, WebTemplate)]
24+
#[template(path = "races/upcoming.html")]
25+
pub struct UpcomingRacesTemplate {
26+
shared: SharedContext,
27+
races: PaginatedResponse<RaceView>,
28+
}
29+
30+
async fn upcoming_races(
31+
State(state): State<SharedState>,
32+
MaybeCurrentUser(user): MaybeCurrentUser,
33+
Query(params): Query<RaceSearchParams>,
34+
) -> impl IntoResponse {
35+
UpcomingRacesTemplate {
36+
shared: SharedContext::new(&state.app_info, user.as_deref().cloned()),
37+
races: state.race_service.search_for_upcoming(params).await,
38+
}
1139
}

templates/_partials/navbar.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ <h1 class="brand desktop-only">{{ shared.app_info.name }}</h1>
5151
{% endif %}
5252
<ul>
5353
<li class="desktop-only"><a href="/members">Members</a></li>
54-
<li class="desktop-only"><a href="/">Races</a></li>
54+
<li class="desktop-only"><a href="/upcoming-races">Upcoming Races</a></li>
5555
{% if let Some(user) = shared.current_user %} {% if let Some(runner_id) = user.runner_id
5656
%}
5757
<li>

templates/races/upcoming.html

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<title>{{ shared.app_info.name }} | Races</title>
5+
<meta name="description" content="Search for races uploaded by other members." />
6+
<link rel="canonical" href="{{ shared.app_info.website_url }}" />
7+
{% include "_partials/meta.html" %} {% include "_partials/scripts.html" %}
8+
</head>
9+
<body>
10+
{% include "_partials/navbar.html" %}
11+
<main>
12+
<div class="container">
13+
<section class="card flex-col">
14+
<h1>Upcoming Races</h1>
15+
{% if races.total > 0 %}
16+
<ul>
17+
{% for race in races.items %}
18+
<li>
19+
{{ race.name | title }} ({{ race.miles }} miles) - {{ race.start_date }}
20+
</li>
21+
{% endfor %}
22+
</ul>
23+
{% else %}
24+
<p>No upcoming races found.</p>
25+
{% endif %}
26+
</section>
27+
</div>
28+
</main>
29+
{% include "_partials/footer.html" %}
30+
</body>
31+
</html>

0 commit comments

Comments
 (0)