Compare commits

..

No commits in common. "0baf51646ed361eed03b6e4893ad5600e2dbcb43" and "4f141b01c30a4e8e35de6484879d347fac28618c" have entirely different histories.

7 changed files with 60 additions and 112 deletions

View file

@ -43,41 +43,3 @@ test('sidebar is sorted alphabetically', async ({ page }) => {
await expect(page.getByRole('navigation')).toHaveText(/Alfa\s*Golf\s*Zulu/); await expect(page.getByRole('navigation')).toHaveText(/Alfa\s*Golf\s*Zulu/);
}); });
test('upcoming and recent show at least one birthday a week away', async ({ page }) => {
const monthday = d => d.toISOString().split("T")[0].replace(/^\d{4}-/, '');
const today = monthday(new Date());
const yesterday = monthday((() => {
let date = new Date();
date.setDate(date.getDate() - 1);
return date;
})());
const tomorrow = monthday((() => {
let date = new Date();
date.setDate(date.getDate() + 1);
return date;
})());
const aMonthAgo = monthday((() => {
let date = new Date();
date.setDate(date.getDate() - 28);
return date;
})());
const inAMonth = monthday((() => {
let date = new Date();
date.setDate(date.getDate() + 28);
return date;
})());
await verifyCreateUser(page, { names: ['Alfa'], birthday: today });
await verifyCreateUser(page, { names: ['Beta'], birthday: yesterday });
await verifyCreateUser(page, { names: ['Echo'], birthday: today });
await verifyCreateUser(page, { names: ['Golf'], birthday: yesterday });
await verifyCreateUser(page, { names: ['Lima'], birthday: tomorrow });
await verifyCreateUser(page, { names: ['Mike'], birthday: yesterday });
await verifyCreateUser(page, { names: ['Xray'], birthday: inAMonth });
await verifyCreateUser(page, { names: ['Zulu'], birthday: aMonthAgo });
await page.goto('/');
await expect(page.locator('#upcoming').getByRole('link')).toHaveCount(4);
await expect(page.locator('#recent').getByRole('link')).toHaveCount(4);
});

View file

@ -1,5 +1,5 @@
insert into contacts(id, birthday, manually_freshened_at) values insert into contacts(id, birthday, manually_freshened_at) values
(0, '04-15', '2000-01-01T12:00:00'); (0, '--0415', '2000-01-01T12:00:00');
insert into names(contact_id, sort, name) values insert into names(contact_id, sort, name) values
(0, 0, 'Alex Aaronson'), (0, 0, 'Alex Aaronson'),
(0, 1, 'Alexi'), (0, 1, 'Alexi'),
@ -16,7 +16,7 @@ insert into groups(contact_id, name, slug) values
(1, 'ABC', 'abc'); (1, 'ABC', 'abc');
insert into contacts(id, birthday) values insert into contacts(id, birthday) values
(2, '1995-10-18'); (2, '19951018');
insert into names(contact_id, sort, name) values insert into names(contact_id, sort, name) values
(2, 0, 'Charlie Certaindate'); (2, 0, 'Charlie Certaindate');
insert into groups(contact_id, name, slug) values insert into groups(contact_id, name, slug) values

View file

@ -1,7 +0,0 @@
update contacts
set birthday = substr(birthday, 3, 2) || '-' || substr(birthday, 5, 2)
where birthday GLOB '--[01][0-9][0-3][0-9]';
update contacts
set birthday = substr(birthday, 1, 4) || '-' || substr(birthday, 5, 2) || '-' || substr(birthday, 7, 2)
where birthday GLOB '[0-9][0-9][0-9][0-9][01][0-9][0-3][0-9]';

View file

@ -20,10 +20,11 @@ pub enum Birthday {
impl Display for Birthday { impl Display for Birthday {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self { let str = match self {
Birthday::Date(date) => write!(f, "{}", date), Birthday::Date(date) => date.to_string(),
Birthday::Text(t) => write!(f, "{}", t.value), Birthday::Text(t) => t.value.clone(),
} };
write!(f, "{}", str)
} }
} }
@ -55,6 +56,13 @@ impl Birthday {
} }
} }
} }
pub fn serialize(&self) -> String {
match &self {
Birthday::Text(text) => text.value.clone(),
Birthday::Date(date) => date.serialize(),
}
}
} }
impl FromStr for Birthday { impl FromStr for Birthday {

View file

@ -43,6 +43,15 @@ impl YearOptionalDate {
None None
} }
} }
pub fn serialize(&self) -> String {
format!(
"{}{:0>2}{:0>2}",
self.year.map_or("--".to_string(), |y| format!("{:0>4}", y)),
self.month,
self.day
)
}
} }
impl Display for YearOptionalDate { impl Display for YearOptionalDate {
@ -57,18 +66,21 @@ impl Display for YearOptionalDate {
impl FromStr for YearOptionalDate { impl FromStr for YearOptionalDate {
type Err = anyhow::Error; type Err = anyhow::Error;
fn from_str(str: &str) -> Result<Self, Self::Err> { fn from_str(str: &str) -> Result<Self, Self::Err> {
let date_re = Regex::new(r"^(?:([0-9]{4})-)?([0-9]{2})-([0-9]{2})$").unwrap(); let date_re = Regex::new(r"^([0-9]{4}|--)([0-9]{2})([0-9]{2})$").unwrap();
if let Some(caps) = date_re.captures(str) { if let Some(caps) = date_re.captures(str) {
let year = caps let year_str = &caps[1];
.get(1)
.map(|yyyy| i16::from_str(yyyy.as_str()).unwrap());
let month = i8::from_str(&caps[2]).unwrap(); let month = i8::from_str(&caps[2]).unwrap();
let day = i8::from_str(&caps[3]).unwrap(); let day = i8::from_str(&caps[3]).unwrap();
let year = if year_str == "--" {
None
} else {
Some(i16::from_str(year_str).unwrap())
};
return Ok(Self { year, month, day }); return Ok(Self { year, month, day });
} }
Err(anyhow::Error::msg(format!( Err(anyhow::Error::msg(format!(
"parsing failure in YearOptionalDate: '{}' does not match regex /([0-9]{{4}}-)?[0-9]{{2}}-[0-9]{{2}}/", "parsing failure in YearOptionalDate: '{}' does not match regex /([0-9]{{4}}|--)[0-9]{{4}}/",
str str
))) )))
} }
@ -98,6 +110,6 @@ where
&self, &self,
buf: &mut <Sqlite as Database>::ArgumentBuffer<'r>, buf: &mut <Sqlite as Database>::ArgumentBuffer<'r>,
) -> Result<IsNull, Box<dyn std::error::Error + Sync + Send>> { ) -> Result<IsNull, Box<dyn std::error::Error + Sync + Send>> {
<String as Encode<'r, Sqlite>>::encode(self.to_string(), buf) <String as Encode<'r, Sqlite>>::encode(self.serialize(), buf)
} }
} }

View file

@ -278,10 +278,10 @@ mod get {
input name="periodicity" id="periodicity" value=(format!("{:#}", contact.periodicity)); input name="periodicity" id="periodicity" value=(format!("{:#}", contact.periodicity));
span .hint { code { "[0-9]+ (yr|mo|wk|day|h|m|s)" } "(" a href="https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing" { "details" } ")" } span .hint { code { "[0-9]+ (yr|mo|wk|day|h|m|s)" } "(" a href="https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing" { "details" } ")" }
} }
label for="birthday" { "birthday" } label { "birthday" }
div { div {
input name="birthday" id="birthday" value=(contact.birthday.clone().map_or("".to_string(), |b| format!("{b}"))); input name="birthday" value=(contact.birthday.clone().map_or("".to_string(), |b| b.serialize()));
span .hint { code { "(yyyy-)?mm-dd" } " or free text" } span .hint { code { "(yyyy|--)mmdd" } " or free text" }
} }
label for="manually_freshened_on" { "freshened" } label for="manually_freshened_on" { "freshened" }
div x-data=(json!({ "date": mfresh_on_str, "stamp": mfresh_at_str })) x-init="today = () => (new Date().toISOString().split('T')[0])" { div x-data=(json!({ "date": mfresh_on_str, "stamp": mfresh_at_str })) x-init="today = () => (new Date().toISOString().split('T')[0])" {

View file

@ -1,7 +1,7 @@
use axum::extract::State; use axum::extract::State;
use axum::response::IntoResponse; use axum::response::IntoResponse;
use cache_bust::asset; use cache_bust::asset;
use jiff::{Timestamp, ToSpan, Unit, Zoned, civil, tz::TimeZone}; use jiff::{Timestamp, Unit, Zoned, civil, tz::TimeZone};
use maud::{Markup, html}; use maud::{Markup, html};
use sqlx::sqlite::SqlitePool; use sqlx::sqlite::SqlitePool;
@ -53,40 +53,13 @@ fn birthdays_section(
prev_birthdays: &Vec<KnownBirthdayContact>, prev_birthdays: &Vec<KnownBirthdayContact>,
upcoming_birthdays: &Vec<KnownBirthdayContact>, upcoming_birthdays: &Vec<KnownBirthdayContact>,
) -> Result<Markup, AppError> { ) -> Result<Markup, AppError> {
let now = Timestamp::now().to_zoned(TimeZone::UTC);
let in_a_week = upcoming_birthdays
.iter()
.position(|b| {
now.until(&b.next_birthday.to_zoned(TimeZone::UTC).unwrap())
.unwrap()
.compare((&1_i32.week(), &now))
.unwrap()
!= std::cmp::Ordering::Less
})
.unwrap_or(upcoming_birthdays.len());
let upcoming = &upcoming_birthdays
[0..std::cmp::min(std::cmp::max(3, in_a_week + 1), upcoming_birthdays.len())];
let a_week_ago = prev_birthdays
.iter()
.position(|b| {
now.since(&b.prev_birthday.to_zoned(TimeZone::UTC).unwrap())
.unwrap()
.compare((&1_i32.week(), &now))
.unwrap()
!= std::cmp::Ordering::Less
})
.unwrap_or(upcoming_birthdays.len());
let recent =
&prev_birthdays[0..std::cmp::min(std::cmp::max(3, a_week_ago + 1), prev_birthdays.len())];
Ok(html! { Ok(html! {
div id="birthdays" { div id="birthdays" {
h2 { "Birthdays" } h2 { "Birthdays" }
#birthday-sections { #birthday-sections {
.datelist #upcoming { .datelist {
h3 { "upcoming" } h3 { "upcoming" }
@for contact in upcoming { @for contact in &upcoming_birthdays[0..std::cmp::min(3, upcoming_birthdays.len())] {
a href=(format!("/contact/{}", contact.contact_id)) { a href=(format!("/contact/{}", contact.contact_id)) {
(contact.display) (contact.display)
} }
@ -95,9 +68,9 @@ fn birthdays_section(
} }
} }
} }
.datelist #recent { .datelist {
h3 { "recent" } h3 { "recent" }
@for contact in recent { @for contact in &prev_birthdays[0..std::cmp::min(3, prev_birthdays.len())] {
a href=(format!("/contact/{}", contact.contact_id)) { a href=(format!("/contact/{}", contact.contact_id)) {
(contact.display) (contact.display)
} }