···11+-- Advent challenges table
22+CREATE TABLE IF NOT EXISTS challenges (
33+ id BIGSERIAL PRIMARY KEY,
44+ user_did TEXT NOT NULL,
55+ day INT NOT NULL,
66+ time_started TIMESTAMPTZ NOT NULL DEFAULT NOW(),
77+ time_challenge_one_completed TIMESTAMPTZ NULL,
88+ time_challenge_two_completed TIMESTAMPTZ NULL,
99+ verification_code_one TEXT NULL,
1010+ verification_code_two TEXT NULL,
1111+ CONSTRAINT challenges_user_day_unique UNIQUE(user_did, day),
1212+ CONSTRAINT challenges_day_range CHECK (day >= 1 AND day <= 25)
1313+);
1414+1515+-- Indexes to speed up common lookups
1616+CREATE INDEX IF NOT EXISTS idx_challenges_user_did ON challenges(user_did);
1717+CREATE INDEX IF NOT EXISTS idx_challenges_day ON challenges(day);
···11+Hey! Welcome to at://advent! A 25 day challenge to learn atproto with a new set of challenges every day.
22+33+(Pretend this is going into more details explaining everything)
44+55+Starting out simple, create a record at the collection `codes.advent.challenge.day`
66+with the record key `1` and put this as the record.
77+88+```json
99+{
1010+ "$type": "codes.advent.challenge.day",
1111+ "partOne": "{{code}}"
1212+}
1313+```
1414+1515+[//]: # (<input type="file" id="part_one_input" placeholder="Enter your code here" />)
+7
shared/challenges_markdown/one/part_two.md
···11+Great job beating Part 1! Now onto Part 2.
22+33+Keeping it simple proof of concept, blah, blah will have a real one here another time. Add a new field `partTwo` to the
44+record with the value `{{code}}`
55+66+77+[//]: # (<input type="file" id="part_one_input" placeholder="Enter your code here" />)
···11+pub mod challenges;
22+pub mod day;
33+44+use crate::advent::day::Day;
55+use crate::assets::ChallengesMarkdown;
66+use crate::models::db_models::ChallengeProgress;
77+use async_trait::async_trait;
88+use handlebars::{Handlebars, RenderError};
99+use markdown::{CompileOptions, Options};
1010+use rand::distr::{Alphanumeric, SampleString};
1111+use rust_embed::EmbeddedFile;
1212+use serde_json::json;
1313+use sqlx::PgPool;
1414+use std::str::Utf8Error;
1515+use thiserror::Error;
1616+1717+#[derive(Debug, Error)]
1818+pub enum AdventError {
1919+ #[error("Database error: {0}")]
2020+ Database(#[from] sqlx::Error),
2121+ #[error("Io error: {0}")]
2222+ Io(#[from] std::io::Error),
2323+ #[error("Invalid day: {0}. Day must be between 1 and 25")]
2424+ InvalidDay(i32),
2525+ #[error("This challenge only has a single challenge")]
2626+ NoPartTwo,
2727+ #[error("UTF-8 error: {0}")]
2828+ Utf8Error(#[from] Utf8Error),
2929+ #[error("Render error: {0}")]
3030+ RenderError(#[from] RenderError),
3131+ #[error("This was not designed to happen: {0}")]
3232+ ShouldNotHappen(String),
3333+}
3434+3535+pub enum AdventAction {
3636+ //If Part one is done it shows both, if not it only shows part one
3737+ // the Option<String> here is the did of the user. If none only show part one no matter what
3838+ ViewChallenge(Option<String>),
3939+4040+ //The strings here are the users did's
4141+ StartPartOne(String),
4242+ StartPartTwo(String),
4343+4444+ //TODO should I have like SubmitPartOne(String) and it's the code?
4545+ //These actions will be locked behind logged in
4646+ SubmitPartOne,
4747+ SubmitPartTwo,
4848+}
4949+5050+pub enum AdventPart {
5151+ One,
5252+ Two,
5353+}
5454+5555+pub enum CompletionStatus {
5656+ ///None of the day's challenges have been completed
5757+ None,
5858+ ///PartOne of the day's challenges has been completed
5959+ PartOne,
6060+ ///PartTwo of the day's challenges has been completed
6161+ //i dont think this was needed
6262+ // PartTwo,
6363+ ///Both of the day's challenges have been completed
6464+ Both,
6565+}
6666+6767+pub enum ChallengeCheckResponse {
6868+ Correct,
6969+ ///Error message on why it was incorrect
7070+ Incorrect(String),
7171+}
7272+7373+pub enum AdventActionResult {
7474+ ShowPartOne,
7575+ // If partone is completed, this will be shown
7676+ ShowPartTwo,
7777+7878+ CorrectSubmission(AdventPart),
7979+ IncorrectSubmission,
8080+8181+ Completed,
8282+8383+ Error(String),
8484+}
8585+8686+#[async_trait]
8787+pub trait AdventChallenge {
8888+ /// The db pool in case the challenge needs extra access
8989+ fn pool(&self) -> &PgPool;
9090+9191+ /// The day of the challenge 1-25
9292+ fn day(&self) -> Day;
9393+9494+ fn get_day_markdown_file(&self, part: AdventPart) -> Result<Option<EmbeddedFile>, AdventError> {
9595+ let day = self.day().to_string().to_ascii_lowercase();
9696+ let path = match part {
9797+ AdventPart::One => format!("{day}/part_one.md"),
9898+ AdventPart::Two => format!("{day}/part_two.md"),
9999+ };
100100+ match ChallengesMarkdown::get(path.as_str()) {
101101+ None => {
102102+ log::error!("Missing the part one challenge file for day: {}", day);
103103+ Ok(None)
104104+ }
105105+ Some(day_one_file) => Ok(Some(day_one_file)),
106106+ }
107107+ }
108108+109109+ /// Does the day have a part two challenge?
110110+ fn has_part_two(&self) -> bool;
111111+ //Commenting this out and just going leave it to who makes the impl. This is less code to write,
112112+ // but really it's a faster response to just have the author put true or false
113113+ //
114114+ // {
115115+ // match self.get_day_markdown_file(AdventPart::Two) {
116116+ // Ok(_) => true,
117117+ // Err(_) => false,
118118+ // }
119119+ // }
120120+121121+ /// The text Markdown for challenge 1
122122+ fn markdown_text_part_one(
123123+ &self,
124124+ verification_code: Option<String>,
125125+ ) -> Result<String, AdventError> {
126126+ let day = self.day();
127127+128128+ //May acutally leave this unwrap or put a panic in case it doesn't exist since it is needed to have a part one
129129+ match self.get_day_markdown_file(AdventPart::One)? {
130130+ None => {
131131+ log::error!("Missing the part one challenge file for day: {}", day);
132132+ Ok("Someone let the admins know this page is missing. No, this is not an Easter egg, it's an actual bug".to_string())
133133+ }
134134+ Some(day_one_file) => {
135135+ //TODO probably should be a shared variable, but prototyping
136136+ let reg = Handlebars::new();
137137+138138+ let day_one_text = std::str::from_utf8(day_one_file.data.as_ref())?;
139139+ let code = verification_code.unwrap_or_else(|| "Login to get a code".to_string());
140140+ let handlebar_rendered =
141141+ reg.render_template(day_one_text, &json!({"code": code}))?;
142142+143143+ Ok(
144144+ markdown::to_html_with_options(&handlebar_rendered, &get_markdown_options())
145145+ .unwrap(),
146146+ )
147147+ }
148148+ }
149149+ }
150150+151151+ /// The text Markdown for challenge 2, could be None
152152+ fn markdown_text_part_two(
153153+ &self,
154154+ verification_code: Option<String>,
155155+ ) -> Result<Option<String>, AdventError> {
156156+ match self.get_day_markdown_file(AdventPart::Two)? {
157157+ None => Ok(None),
158158+ Some(day_two_file) => {
159159+ //TODO probably should be a shared variable, but prototyping
160160+ let reg = Handlebars::new();
161161+162162+ let day_two_text = std::str::from_utf8(day_two_file.data.as_ref())?;
163163+ let code = verification_code.unwrap_or_else(|| "Login to get a code".to_string());
164164+ let handlebar_rendered =
165165+ reg.render_template(day_two_text, &json!({"code": code}))?;
166166+167167+ Ok(Some(
168168+ markdown::to_html_with_options(&handlebar_rendered, &get_markdown_options())
169169+ .unwrap(),
170170+ ))
171171+ }
172172+ }
173173+ }
174174+175175+ /// Checks to see if the day's challenge had been started for the user
176176+ async fn day_started(&self, did: &str) -> Result<bool, AdventError> {
177177+ let exists = sqlx::query_scalar::<_, i64>(
178178+ "SELECT id FROM challenges WHERE user_did = $1 AND day = $2 LIMIT 1",
179179+ )
180180+ .bind(did)
181181+ .bind(self.day() as i16)
182182+ .fetch_optional(self.pool())
183183+ .await?;
184184+ Ok(exists.is_some())
185185+ }
186186+187187+ async fn get_days_challenge(
188188+ &self,
189189+ did: String,
190190+ ) -> Result<Option<ChallengeProgress>, AdventError> {
191191+ let day = self.day();
192192+ Ok(sqlx::query_as::<_, ChallengeProgress>(
193193+ "SELECT * FROM challenges WHERE user_did = $1 AND day = $2",
194194+ )
195195+ .bind(did)
196196+ .bind(day as i16)
197197+ .fetch_optional(self.pool())
198198+ .await?)
199199+ }
200200+201201+ async fn start_challenge(&self, did: String, part: AdventPart) -> Result<String, AdventError> {
202202+ let code = get_random_token();
203203+ match part {
204204+ AdventPart::One => sqlx::query(
205205+ "INSERT INTO challenges (user_did, day, time_started, verification_code_one)
206206+ VALUES ($1, $2, NOW(), $3)
207207+ ON CONFLICT (user_did, day)
208208+ DO UPDATE SET verification_code_one = $3
209209+ WHERE challenges.user_did = $1 AND challenges.day = $2",
210210+ ),
211211+ //TODO just going leave these as an update. It should never ideally be an insert
212212+ AdventPart::Two => sqlx::query(
213213+ "UPDATE challenges
214214+ SET verification_code_two = $3
215215+ WHERE challenges.user_did = $1 AND challenges.day = $2",
216216+ ),
217217+ }
218218+ .bind(did)
219219+ .bind(self.day() as i16)
220220+ .bind(code.clone())
221221+ .execute(self.pool())
222222+ .await?;
223223+ Ok(code)
224224+ }
225225+226226+ /// Marks the challenge as completed.
227227+ async fn complete_part_one(&self, did: String) -> Result<(), AdventError> {
228228+ sqlx::query(
229229+ "UPDATE challenges
230230+ SET time_challenge_one_completed = COALESCE(time_challenge_one_completed, NOW())
231231+ WHERE user_did = $1 AND day = $2",
232232+ )
233233+ .bind(did)
234234+ .bind(self.day() as i16)
235235+ .execute(self.pool())
236236+ .await?;
237237+ Ok(())
238238+ }
239239+240240+ /// Marks the challenge as completed.
241241+ async fn complete_part_two(&self, did: String) -> Result<(), AdventError> {
242242+ sqlx::query(
243243+ "UPDATE challenges
244244+ SET time_challenge_two_completed = COALESCE(time_challenge_two_completed, NOW())
245245+ WHERE user_did = $1 AND day = $2",
246246+ )
247247+ .bind(did)
248248+ .bind(self.day() as i16)
249249+ .execute(self.pool())
250250+ .await?;
251251+ Ok(())
252252+ }
253253+254254+ async fn get_completed_status(
255255+ &self,
256256+ did: Option<String>,
257257+ ) -> Result<CompletionStatus, AdventError> {
258258+ match did {
259259+ None => Ok(CompletionStatus::None),
260260+ Some(did) => {
261261+ let day = self.day() as i32;
262262+ let result = sqlx::query!(
263263+ "SELECT time_challenge_one_completed, time_challenge_two_completed
264264+ FROM challenges
265265+ WHERE user_did = $1 AND day = $2",
266266+ did,
267267+ day
268268+ )
269269+ .fetch_optional(self.pool())
270270+ .await?;
271271+272272+ Ok(match result {
273273+ None => CompletionStatus::None,
274274+ Some(row) => match (
275275+ row.time_challenge_one_completed,
276276+ row.time_challenge_two_completed,
277277+ ) {
278278+ (None, None) => CompletionStatus::None,
279279+ (Some(_), None) => CompletionStatus::PartOne,
280280+ (Some(_), Some(_)) => CompletionStatus::Both,
281281+ _ => panic!(
282282+ "This should never happen as in part one shouldn't be not done but 2 is"
283283+ ),
284284+ },
285285+ })
286286+ }
287287+ }
288288+ }
289289+290290+ ///This is where the magic happens, aka logic to check if the user got it. Verification code is optional cause sometiems you may need to find it somewhere, sometimes maybe the backend does
291291+ async fn check_part_one(
292292+ &self,
293293+ did: String,
294294+ verification_code: Option<String>,
295295+ ) -> Result<ChallengeCheckResponse, AdventError>;
296296+297297+ ///This is where the magic happens, aka logic to check if the user got it. Verification code is optional cause sometiems you may need to find it somewhere, sometimes maybe the backend does
298298+ /// part two does have a hard error if its called and there is not a part 2
299299+ async fn check_part_two(
300300+ &self,
301301+ _did: String,
302302+ _verification_code: Option<String>,
303303+ ) -> Result<ChallengeCheckResponse, AdventError> {
304304+ unimplemented!("Second day challenges are optional")
305305+ }
306306+}
307307+308308+pub fn get_random_token() -> String {
309309+ let mut rng = rand::rng();
310310+311311+ let full_code = Alphanumeric.sample_string(&mut rng, 10);
312312+313313+ let slice_one = &full_code[0..5].to_ascii_uppercase();
314314+ let slice_two = &full_code[5..10].to_ascii_uppercase();
315315+ format!("{slice_one}-{slice_two}")
316316+}
317317+318318+fn get_markdown_options() -> Options {
319319+ Options {
320320+ parse: Default::default(),
321321+ compile: CompileOptions {
322322+ //Setting this to allow HTML in the markdown. So pleas be careful what you put in there
323323+ allow_dangerous_html: true,
324324+ ..Default::default()
325325+ },
326326+ }
327327+}
···11+use atrium_api::types::Unknown;
22+use serde::de;
33+14pub mod dns_resolver;
25pub mod stores;
66+77+/// Safely parses an unknown record into a type. If it fails, it logs the error and returns an error.
88+pub fn safe_check_unknown_record_parse<T>(unknown: Unknown) -> serde_json::Result<T>
99+where
1010+ T: de::DeserializeOwned,
1111+{
1212+ let json = serde_json::to_vec(&unknown).map_err(|err| {
1313+ log::error!("Error getting the bytes of a record: {}", err);
1414+ err
1515+ })?;
1616+ serde_json::from_slice::<T>(&json)
1717+}
+7-4
shared/src/atrium/stores.rs
···11/// Storage impls to persis OAuth sessions if you are not using the memory stores
22/// https://github.com/bluesky-social/statusphere-example-app/blob/main/src/auth/storage.ts
33-use crate::cache::{ATRIUM_SESSION_STORE_PREFIX, ATRIUM_STATE_STORE_KEY, create_prefixed_key};
33+use crate::cache::{
44+ ATRIUM_SESSION_STORE_PREFIX, ATRIUM_STATE_STORE_KEY, Cache, create_prefixed_key,
55+};
46use atrium_api::types::string::Did;
57use atrium_common::store::Store;
68use atrium_oauth::store::session::SessionStore;
···118120119121 async fn set(&self, key: K, value: V) -> Result<(), Self::Error> {
120122 let cache_key = create_prefixed_key(ATRIUM_STATE_STORE_KEY, key.as_ref());
121121- let json_value = serde_json::to_string(&value)?;
122122- let mut cache = self.cache_pool.get().await?;
123123- let _: () = cache.set(cache_key, json_value).await?;
123123+ let mut cache = Cache::new(self.cache_pool.get().await?);
124124+ let _ = cache
125125+ .write_to_cache_with_seconds(&cache_key, value, 3_6000)
126126+ .await?;
124127 Ok(())
125128 }
126129
···11+// @generated - This file is generated by esquema-codegen (forked from atrium-codegen). DO NOT EDIT.
22+//!Definitions for the `codes` namespace.
33+pub mod advent;
+3
shared/src/lexicons/codes/advent.rs
···11+// @generated - This file is generated by esquema-codegen (forked from atrium-codegen). DO NOT EDIT.
22+//!Definitions for the `codes.advent` namespace.
33+pub mod challenge;
+9
shared/src/lexicons/codes/advent/challenge.rs
···11+// @generated - This file is generated by esquema-codegen (forked from atrium-codegen). DO NOT EDIT.
22+//!Definitions for the `codes.advent.challenge` namespace.
33+pub mod day;
44+#[derive(Debug)]
55+pub struct Day;
66+impl atrium_api::types::Collection for Day {
77+ const NSID: &'static str = "codes.advent.challenge.day";
88+ type Record = day::Record;
99+}
+18
shared/src/lexicons/codes/advent/challenge/day.rs
···11+// @generated - This file is generated by esquema-codegen (forked from atrium-codegen). DO NOT EDIT.
22+//!Definitions for the `codes.advent.challenge.day` namespace.
33+use atrium_api::types::TryFromUnknown;
44+#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
55+#[serde(rename_all = "camelCase")]
66+pub struct RecordData {
77+ #[serde(skip_serializing_if = "core::option::Option::is_none")]
88+ pub created_at: core::option::Option<atrium_api::types::string::Datetime>,
99+ pub part_one: String,
1010+ #[serde(skip_serializing_if = "core::option::Option::is_none")]
1111+ pub part_two: core::option::Option<String>,
1212+}
1313+pub type Record = atrium_api::types::Object<RecordData>;
1414+impl From<atrium_api::types::Unknown> for RecordData {
1515+ fn from(value: atrium_api::types::Unknown) -> Self {
1616+ Self::try_from_unknown(value).unwrap()
1717+ }
1818+}
+3
shared/src/lexicons/mod.rs
···11+// @generated - This file is generated by esquema-codegen (forked from atrium-codegen). DO NOT EDIT.
22+pub mod record;
33+pub mod codes;
+27
shared/src/lexicons/record.rs
···11+// @generated - This file is generated by esquema-codegen (forked from atrium-codegen). DO NOT EDIT.
22+//!A collection of known record types.
33+#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
44+#[serde(tag = "$type")]
55+pub enum KnownRecord {
66+ #[serde(rename = "codes.advent.challenge.day")]
77+ LexiconsCodesAdventChallengeDay(
88+ Box<crate::lexicons::codes::advent::challenge::day::Record>,
99+ ),
1010+}
1111+impl From<crate::lexicons::codes::advent::challenge::day::Record> for KnownRecord {
1212+ fn from(record: crate::lexicons::codes::advent::challenge::day::Record) -> Self {
1313+ KnownRecord::LexiconsCodesAdventChallengeDay(Box::new(record))
1414+ }
1515+}
1616+impl From<crate::lexicons::codes::advent::challenge::day::RecordData> for KnownRecord {
1717+ fn from(
1818+ record_data: crate::lexicons::codes::advent::challenge::day::RecordData,
1919+ ) -> Self {
2020+ KnownRecord::LexiconsCodesAdventChallengeDay(Box::new(record_data.into()))
2121+ }
2222+}
2323+impl Into<atrium_api::types::Unknown> for KnownRecord {
2424+ fn into(self) -> atrium_api::types::Unknown {
2525+ atrium_api::types::TryIntoUnknown::try_into_unknown(&self).unwrap()
2626+ }
2727+}
+37
shared/src/lib.rs
···11+extern crate core;
22+33+use crate::atrium::dns_resolver::HickoryDnsTxtResolver;
44+use crate::atrium::stores::{AtriumSessionStore, AtriumStateStore};
55+use atrium_api::agent::Agent;
66+use atrium_identity::did::CommonDidResolver;
77+use atrium_identity::handle::AtprotoHandleResolver;
88+use atrium_oauth::{DefaultHttpClient, OAuthClient};
99+use std::sync::Arc;
1010+1111+pub mod advent;
1212+pub mod assets;
113pub mod atrium;
214pub mod cache;
315pub mod db;
416pub mod models;
517pub mod web_helpers;
1818+1919+pub mod lexicons;
2020+2121+/// OAuthClientType to make it easier to access the OAuthClient in web requests
2222+pub type OAuthClientType = Arc<
2323+ OAuthClient<
2424+ AtriumStateStore,
2525+ AtriumSessionStore,
2626+ CommonDidResolver<DefaultHttpClient>,
2727+ AtprotoHandleResolver<HickoryDnsTxtResolver, DefaultHttpClient>,
2828+ >,
2929+>;
3030+3131+/// HandleResolver type to make it easier to access the resolver in web requests
3232+pub type HandleResolver = Arc<CommonDidResolver<DefaultHttpClient>>;
3333+3434+/// The agent(what makes atproto calls)
3535+pub type OAuthAgentType = Agent<
3636+ atrium_oauth::OAuthSession<
3737+ DefaultHttpClient,
3838+ CommonDidResolver<DefaultHttpClient>,
3939+ AtprotoHandleResolver<HickoryDnsTxtResolver, DefaultHttpClient>,
4040+ AtriumSessionStore,
4141+ >,
4242+>;
···11+use askama::Template;
22+use axum::http::StatusCode;
33+use axum::response::{Html, IntoResponse, Response};
44+55+pub mod day;
66+pub mod error;
77+pub mod login;
88+pub mod home;
99+1010+pub struct HtmlTemplate<T>(pub T);
1111+1212+/// Allows us to convert Askama HTML templates into valid HTML
1313+/// for axum to serve in the response.
1414+impl<T> IntoResponse for HtmlTemplate<T>
1515+where
1616+ T: Template,
1717+{
1818+ fn into_response(self) -> Response {
1919+ // Attempt to render the template with askama
2020+ match self.0.render() {
2121+ // If we're able to successfully parse and aggregate the template, serve it
2222+ Ok(html) => Html(html).into_response(),
2323+ // If we're not, return an error or some bit of fallback HTML
2424+ Err(err) => {
2525+ log::error!("Failed to render template: {}", err);
2626+ IntoResponse::into_response((
2727+ StatusCode::INTERNAL_SERVER_ERROR,
2828+ "Failed to render the HTML Template",
2929+ ))
3030+ }
3131+ }
3232+ }
3333+}
+54-25
web/src/unlock.rs
···11-use axum::extract::{Path, Request, State};
11+use axum::extract::{Path, Request};
22use axum::http;
33use axum::{
44 middleware,
55 response::{self, IntoResponse},
66};
77-use std::time;
88-99-#[derive(Clone)]
1010-pub struct Unlock {
1111- start: time::Instant,
1212- interval: time::Duration,
1313-}
1414-1515-impl Unlock {
1616- pub fn new(start: time::Instant, interval: time::Duration) -> Self {
1717- Self { start, interval }
1818- }
1919-}
77+use chrono::Datelike;
208219pub async fn unlock(
2222- Path(day): Path<u32>,
2323- State(unlocker): State<Unlock>,
1010+ Path(mut day): Path<u8>,
2411 request: Request,
2512 next: middleware::Next,
2613) -> response::Response {
2727- let deadline = unlocker.start + unlocker.interval * day;
2828- let now = time::Instant::now();
2929- if now >= deadline {
1414+ if day == 0 {
1515+ day = 1;
1616+ }
1717+1818+ if day == 69 {
1919+ return (http::StatusCode::FORBIDDEN, "Really?").into_response();
2020+ }
2121+2222+ if day == 42 {
2323+ return (
2424+ http::StatusCode::FORBIDDEN,
2525+ "Oh, you have all the answers, huh?",
2626+ )
2727+ .into_response();
2828+ }
2929+3030+ if day > 25 || day < 1 {
3131+ return (
3232+ http::StatusCode::FORBIDDEN,
3333+ "This isn't even a day in the advent calendar????",
3434+ )
3535+ .into_response();
3636+ }
3737+3838+ let now = chrono::Utc::now();
3939+ let current_day = now.day();
4040+ let month = now.month();
4141+4242+ if month != 12 {
4343+ return (
4444+ http::StatusCode::FORBIDDEN,
4545+ "It's not December yet! NO PEAKING",
4646+ )
4747+ .into_response();
4848+ }
4949+5050+ //Show any day previous to the current day and current day
5151+ if day as u32 <= current_day {
3052 return next.run(request).await;
3153 }
3232- let time_remaining = deadline.saturating_duration_since(now);
3333- let error_response = axum::Json(serde_json::json!({
3434- "error": "Route Locked",
3535- "time_remaining_seconds": time_remaining.as_secs(),
3636- }));
37543838- (http::StatusCode::FORBIDDEN, error_response).into_response()
5555+ (
5656+ http::StatusCode::FORBIDDEN,
5757+ "Now just hold on a minute. It ain't time yet.",
5858+ )
5959+ .into_response()
6060+6161+ // Just commenting out for now if we do want a json endpoint and i forgot easiest way to return it
6262+ // let error_response = axum::Json(serde_json::json!({
6363+ // "error": "Route Locked",
6464+ // "time_remaining_seconds": time_remaining.as_secs(),
6565+ // }));
6666+6767+ // (http::StatusCode::FORBIDDEN, error_response).into_response()
3968}
+55
web/templates/day.askama.html
···11+{% extends "layout.askama.html" %}
22+33+{% block content %}
44+ <h2 class="text-xl">Day {{ day }}</h2>
55+ <p>Part 1:</p>
66+ <article class="prose">{{ challenge_one_text | safe }}</article>
77+ <br/>
88+ {% if let Some(msg) = part_one_submit_message %}
99+ {% match msg %}
1010+ {% when FlashMessage::Success with (success) %}
1111+ <span class="text-success">{{success}}</span>
1212+ {% when FlashMessage::Error with (error) %}
1313+ <div class="alert alert-error mb-2">{{error}}</div>
1414+ {% endmatch %}
1515+ {% endif %}
1616+1717+ {% if !challenge_one_completed %}
1818+ <form method="post" action="/day/{{ day }}">
1919+ <!-- TODO will be optional prob load from a markdown variable? -->
2020+ <!-- <input type="text" name="verification_code_one" placeholder="Enter Part 1 code" class="input input-bordered mr-2"/>-->
2121+ <button class="btn" type="submit">Check answer</button>
2222+ </form>
2323+ {% else %}
2424+ <span class="text-success">Great work, you've completed Part 1</span>
2525+ {% endif %}
2626+2727+2828+ {% if let Some(challenge_two_text) = challenge_two_text %}
2929+ <hr class="my-4"/>
3030+ <p>Part 2:</p>
3131+ <article class="prose">{{ challenge_two_text | safe }}</article>
3232+ {% if let Some(msg) = part_two_submit_message %}
3333+ {% match msg %}
3434+ {% when FlashMessage::Success with (success) %}
3535+ <span class="text-success">{{success}}</span>
3636+ {% when FlashMessage::Error with (error) %}
3737+ <div class="alert alert-error mb-2">{{error}}</div>
3838+ {% endmatch %}
3939+ {% endif %}
4040+ {% if !challenge_two_completed %}
4141+ <form method="post" action="/day/{{ day }}">
4242+ <!-- TODO will be optional prob load from a markdown variable? -->
4343+ <!-- <input type="text" name="verification_code_two" placeholder="Enter Part 2 code" class="input input-bordered mr-2"/>-->
4444+ <button class="btn" type="submit">Check answer</button>
4545+ </form>
4646+ {% endif %}
4747+4848+ {% endif %}
4949+5050+ {% if challenge_one_completed && challenge_two_completed %}
5151+ <br>
5252+ <span class="text-success">Great work, you've completed all the challenges for today! Come back tomorrow for more at 00:00 UTC</span>
5353+ {% endif %}
5454+5555+{% endblock %}