diff --git a/e2e/pages/home.spec.ts b/e2e/pages/home.spec.ts index 8ec39dd..13b9fa1 100644 --- a/e2e/pages/home.spec.ts +++ b/e2e/pages/home.spec.ts @@ -2,82 +2,44 @@ import { test, expect } from '@playwright/test'; import { login, verifyCreateUser, todate } from './util'; test.beforeEach(async ({ page }) => { - await login(page); + await login(page); }); test('can log out', async ({ page }) => { - await page.getByText("Logout").click(); - await expect(page.getByLabel("Username")).toBeVisible(); + await page.getByText("Logout").click(); + await expect(page.getByLabel("Username")).toBeVisible(); }); test('has no contacts', async ({ page }) => { - await expect(page.getByRole("navigation").getByRole("link")).toHaveCount(0); + await expect(page.getByRole("navigation").getByRole("link")).toHaveCount(0); }); test('can add contacts', async ({ page }) => { - await verifyCreateUser(page, { names: ['John Contact'] }); - await verifyCreateUser(page, { names: ['Jack Contact'] }); - await expect(page.getByRole("navigation").getByRole("link")).toHaveCount(2); + await verifyCreateUser(page, { names: ['John Contact'] }); + await verifyCreateUser(page, { names: ['Jack Contact'] }); + await expect(page.getByRole("navigation").getByRole("link")).toHaveCount(2); }); test('shows "never" for unfreshened contacts', async ({ page }) => { - await verifyCreateUser(page, { names: ['John Contact'] }); - await page.getByRole('link', { name: 'Mascarpone' }).click(); + await verifyCreateUser(page, { names: ['John Contact'] }); + await page.getByRole('link', { name: 'Mascarpone' }).click(); - await expect(page.locator('#freshness')).toContainText('John Contactnever'); + await expect(page.locator('#freshness')).toContainText('John Contactnever'); }); test('shows the date for fresh contacts', async ({ page }) => { - await verifyCreateUser(page, { names: ['John Contact'] }); - await page.getByRole('link', { name: /edit/i }).click(); - await page.getByRole('button', { name: /fresh/i }).click(); - await page.getByRole('button', { name: /save/i }).click(); - await page.getByRole('link', { name: 'Mascarpone' }).click(); - await expect(page.locator('#freshness')).toContainText(`John Contact${todate()}`); + await verifyCreateUser(page, { names: ['John Contact'] }); + await page.getByRole('link', { name: /edit/i }).click(); + await page.getByRole('button', { name: /fresh/i }).click(); + await page.getByRole('button', { name: /save/i }).click(); + await page.getByRole('link', { name: 'Mascarpone' }).click(); + await expect(page.locator('#freshness')).toContainText(`John Contact${todate()}`); }); test('sidebar is sorted alphabetically', async ({ page }) => { - await verifyCreateUser(page, { names: ['Zulu'] }); - await verifyCreateUser(page, { names: ['Alfa'] }); - await verifyCreateUser(page, { names: ['Golf'] }); + await verifyCreateUser(page, { names: ['Zulu'] }); + await verifyCreateUser(page, { names: ['Alfa'] }); + await verifyCreateUser(page, { names: ['Golf'] }); - 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); + await expect(page.getByRole('navigation')).toHaveText(/Alfa\s*Golf\s*Zulu/); }); diff --git a/migrations/demo.sql b/migrations/demo.sql index 73b2d4b..8eb0d76 100644 --- a/migrations/demo.sql +++ b/migrations/demo.sql @@ -1,5 +1,5 @@ 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 (0, 0, 'Alex Aaronson'), (0, 1, 'Alexi'), @@ -16,7 +16,7 @@ insert into groups(contact_id, name, slug) values (1, 'ABC', 'abc'); insert into contacts(id, birthday) values - (2, '1995-10-18'); + (2, '19951018'); insert into names(contact_id, sort, name) values (2, 0, 'Charlie Certaindate'); insert into groups(contact_id, name, slug) values diff --git a/migrations/each_user/0014_birthday-format.sql b/migrations/each_user/0014_birthday-format.sql deleted file mode 100644 index 80b69cf..0000000 --- a/migrations/each_user/0014_birthday-format.sql +++ /dev/null @@ -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]'; diff --git a/src/models/birthday.rs b/src/models/birthday.rs index 2db0296..f7c0256 100644 --- a/src/models/birthday.rs +++ b/src/models/birthday.rs @@ -20,10 +20,11 @@ pub enum Birthday { impl Display for Birthday { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - match self { - Birthday::Date(date) => write!(f, "{}", date), - Birthday::Text(t) => write!(f, "{}", t.value), - } + let str = match self { + Birthday::Date(date) => date.to_string(), + 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 { diff --git a/src/models/year_optional_date.rs b/src/models/year_optional_date.rs index a4cd381..fa1fc1f 100644 --- a/src/models/year_optional_date.rs +++ b/src/models/year_optional_date.rs @@ -43,6 +43,15 @@ impl YearOptionalDate { 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 { @@ -57,18 +66,21 @@ impl Display for YearOptionalDate { impl FromStr for YearOptionalDate { type Err = anyhow::Error; fn from_str(str: &str) -> Result { - 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) { - let year = caps - .get(1) - .map(|yyyy| i16::from_str(yyyy.as_str()).unwrap()); + let year_str = &caps[1]; let month = i8::from_str(&caps[2]).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 }); } 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 ))) } @@ -98,6 +110,6 @@ where &self, buf: &mut ::ArgumentBuffer<'r>, ) -> Result> { - >::encode(self.to_string(), buf) + >::encode(self.serialize(), buf) } } diff --git a/src/web/contact/mod.rs b/src/web/contact/mod.rs index b99abdb..7dc487b 100644 --- a/src/web/contact/mod.rs +++ b/src/web/contact/mod.rs @@ -278,10 +278,10 @@ mod get { 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" } ")" } } - label for="birthday" { "birthday" } + label { "birthday" } div { - input name="birthday" id="birthday" value=(contact.birthday.clone().map_or("".to_string(), |b| format!("{b}"))); - span .hint { code { "(yyyy-)?mm-dd" } " or free text" } + input name="birthday" value=(contact.birthday.clone().map_or("".to_string(), |b| b.serialize())); + span .hint { code { "(yyyy|--)mmdd" } " or free text" } } 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])" { diff --git a/src/web/home.rs b/src/web/home.rs index 49fbb4a..231ee6a 100644 --- a/src/web/home.rs +++ b/src/web/home.rs @@ -1,7 +1,7 @@ use axum::extract::State; use axum::response::IntoResponse; 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 sqlx::sqlite::SqlitePool; @@ -53,40 +53,13 @@ fn birthdays_section( prev_birthdays: &Vec, upcoming_birthdays: &Vec, ) -> Result { - 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! { div id="birthdays" { h2 { "Birthdays" } #birthday-sections { - .datelist #upcoming { + .datelist { 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)) { (contact.display) } @@ -95,9 +68,9 @@ fn birthdays_section( } } } - .datelist #recent { + .datelist { 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)) { (contact.display) }