diff --git a/website/public/demo/archival-flyer.webp b/website/public/demo/archival-flyer.webp new file mode 100644 index 0000000..c49092a Binary files /dev/null and b/website/public/demo/archival-flyer.webp differ diff --git a/website/public/demo/cat-keyboard-reel.webp b/website/public/demo/cat-keyboard-reel.webp new file mode 100644 index 0000000..a7e4b13 Binary files /dev/null and b/website/public/demo/cat-keyboard-reel.webp differ diff --git a/website/public/demo/clara-z.webp b/website/public/demo/clara-z.webp new file mode 100644 index 0000000..ec68df3 Binary files /dev/null and b/website/public/demo/clara-z.webp differ diff --git a/website/public/demo/composer-photo.webp b/website/public/demo/composer-photo.webp new file mode 100644 index 0000000..239f717 Binary files /dev/null and b/website/public/demo/composer-photo.webp differ diff --git a/website/public/demo/composer-reel.webp b/website/public/demo/composer-reel.webp new file mode 100644 index 0000000..d1e56a1 Binary files /dev/null and b/website/public/demo/composer-reel.webp differ diff --git a/website/public/demo/finished-poster.webp b/website/public/demo/finished-poster.webp new file mode 100644 index 0000000..521a3e1 Binary files /dev/null and b/website/public/demo/finished-poster.webp differ diff --git a/website/public/demo/poster-mockup.webp b/website/public/demo/poster-mockup.webp new file mode 100644 index 0000000..92e4164 Binary files /dev/null and b/website/public/demo/poster-mockup.webp differ diff --git a/website/public/demo/printed-posters.webp b/website/public/demo/printed-posters.webp new file mode 100644 index 0000000..a226ca8 Binary files /dev/null and b/website/public/demo/printed-posters.webp differ diff --git a/website/public/demo/swiss-posters-reel.webp b/website/public/demo/swiss-posters-reel.webp new file mode 100644 index 0000000..ce9a3c3 Binary files /dev/null and b/website/public/demo/swiss-posters-reel.webp differ diff --git a/website/src/components/HeroDemo.astro b/website/src/components/HeroDemo.astro new file mode 100644 index 0000000..f5f2b19 --- /dev/null +++ b/website/src/components/HeroDemo.astro @@ -0,0 +1,1305 @@ +--- +import HeroDemoMessage from './HeroDemoMessage.astro'; +import { + HERO_COMPOSER_MEDIA, + HERO_MESSAGES, + HERO_PROFILE, + type HeroDemoMessage as HeroMessage, +} from '../data/hero-conversation'; + +const templateBase = { + key: 'template', + sender: 'you', + timestamp: { time: '10:42 AM', date: 'Wednesday' }, + showDate: false, + showTime: false, +} as const; +const textTemplate: HeroMessage = { + ...templateBase, + content: { type: 'text', text: '' }, +}; +const photoTemplate: HeroMessage = { + ...templateBase, + content: { + type: 'media', + label: 'photo', + image: HERO_COMPOSER_MEDIA.photo, + alt: 'Zine proofs on a worktable', + }, +}; +const reelTemplate: HeroMessage = { + ...templateBase, + content: { + type: 'shared-post', + source: 'print.room', + caption: 'late night riso run', + image: HERO_COMPOSER_MEDIA.reel, + alt: 'Risograph printing Reel', + }, +}; +--- + +
+
+ + DM2Text +
+ +
+
+
+
+ + + {HERO_PROFILE.username} + {HERO_PROFILE.name} + +
+ + +
+ +
+ { + HERO_MESSAGES.map((message, index) => ( + + )) + } +
+ + + + + +
+ + + + + +
+ + + + +
+ +
+
+ pasteboard.txtplain text +
+ + + + +
+
+
+ + + + diff --git a/website/src/components/HeroDemoMessage.astro b/website/src/components/HeroDemoMessage.astro new file mode 100644 index 0000000..16ecb44 --- /dev/null +++ b/website/src/components/HeroDemoMessage.astro @@ -0,0 +1,142 @@ +--- +import { HERO_PROFILE, type HeroDemoMessage } from '../data/hero-conversation'; + +interface Props { + message: HeroDemoMessage; + index?: number; +} + +const { message, index = 0 } = Astro.props; +const incoming = message.sender === 'clara'; +const sender = incoming ? HERO_PROFILE.name : 'You'; +const { content } = message; +--- + +{message.showDate &&
{message.timestamp.date}
} +{message.showTime &&
{message.timestamp.time}
} + +
+ { + incoming && ( + + ) + } + + + + { + content.type === 'text' && ( +
+ {message.reply && ( + + {message.reply.sender} + {message.reply.preview} + + )} + {content.text} +
+ ) + } + + { + content.type === 'media' && ( +
+ {content.alt} +
+ ) + } + + { + content.type === 'shared-post' && ( +
+ {content.alt} + + Reel + + {content.source} + {content.caption} + +
+ ) + } + + +
diff --git a/website/src/data/hero-conversation.ts b/website/src/data/hero-conversation.ts new file mode 100644 index 0000000..a2d2e9e --- /dev/null +++ b/website/src/data/hero-conversation.ts @@ -0,0 +1,320 @@ +export const HERO_PROFILE = { + name: 'Clara Z.', + username: 'clara.zk', + avatar: '/demo/clara-z.webp', +} as const; + +export const HERO_COMPOSER_MEDIA = { + photo: '/demo/composer-photo.webp', + reel: '/demo/composer-reel.webp', +} as const; + +export type HeroSender = 'clara' | 'you'; + +export type HeroContent = + | { type: 'text'; text: string } + | { type: 'media'; label: 'photo'; image: string; alt: string } + | { + type: 'shared-post'; + source: string; + caption: string; + image: string; + alt: string; + }; + +export interface HeroReply { + sender: string; + preview: string; +} + +export interface HeroDemoMessage { + key: string; + sender: HeroSender; + timestamp: { time: string; date: 'Tuesday' | 'Wednesday' }; + showDate: boolean; + showTime: boolean; + content: HeroContent; + reply?: HeroReply; +} + +type RawMessage = + | readonly [time: string, sender: HeroSender, text: string, reply?: HeroReply] + | readonly [ + time: string, + sender: HeroSender, + content: Exclude, + ]; + +const photo = ( + image: string, + alt: string, +): Exclude => ({ + type: 'media', + label: 'photo', + image, + alt, +}); + +const reel = ( + source: string, + caption: string, + image: string, + alt: string, +): Extract => ({ + type: 'shared-post', + source, + caption, + image, + alt, +}); + +const tuesday: readonly RawMessage[] = [ + ['9:18 AM', 'clara', 'did you export the new version'], + ['9:19 AM', 'you', 'nope'], + ['9:19 AM', 'you', 'i fell asleep 😭'], + ['9:20 AM', 'clara', 'incredible'], + ['9:20 AM', 'clara', "professional operation we're running here"], + ['9:22 AM', 'you', 'give me like 20 mins'], + ['9:22 AM', 'clara', 'take your time'], + ['9:22 AM', 'clara', "i'm getting coffee anyway"], + ['9:37 AM', 'you', 'wait'], + ['9:37 AM', 'you', 'which one did you like more'], + ['9:37 AM', 'you', 'the white one or the one with the huge title'], + ['9:40 AM', 'clara', 'huge title'], + ['9:40 AM', 'clara', 'the white one looks too clean'], + ['9:41 AM', 'you', "that's what i thought"], + ['9:41 AM', 'you', 'looks like a bank made a zine'], + ['9:41 AM', 'clara', 'exactly lol'], + [ + '9:43 AM', + 'you', + photo('/demo/poster-mockup.webp', 'Poster mockup on a laptop and desk'), + ], + ['9:44 AM', 'you', 'this one?'], + ['9:45 AM', 'clara', 'YES'], + ['9:45 AM', 'clara', 'move the date down a little though'], + ['9:45 AM', 'clara', "it's fighting the photo"], + ['9:46 AM', 'you', 'true'], + ['9:47 AM', 'you', 'what about the red'], + ['9:48 AM', 'clara', 'keep it'], + ['9:48 AM', 'clara', 'otherwise everything is beige and depressing'], + ['9:48 AM', 'you', 'beige and depressing is contemporary design'], + ['9:49 AM', 'clara', 'unfortunately'], + ['10:06 AM', 'clara', 'btw'], + ['10:06 AM', 'clara', 'look at this'], + [ + '10:07 AM', + 'clara', + reel( + 'design.archive', + 'Swiss posters from the 1970s', + '/demo/swiss-posters-reel.webp', + 'Swiss poster archive Reel', + ), + ], + ['10:08 AM', 'you', 'oh this is hard'], + ['10:08 AM', 'you', 'second one especially'], + ['10:09 AM', 'clara', "that's what i was thinking for the back"], + ['10:10 AM', 'you', 'the tiny type?'], + ['10:10 AM', 'clara', 'yeah'], + ['10:10 AM', 'clara', 'just names / date / place'], + ['10:11 AM', 'you', 'bet'], + ['11:34 AM', 'you', 'exported'], + ['11:34 AM', 'you', 'finally'], + ['11:35 AM', 'clara', 'send'], + [ + '11:36 AM', + 'you', + photo( + '/demo/finished-poster.webp', + 'Finished red, black, and cream poster', + ), + ], + ['11:37 AM', 'clara', 'wait this is actually good'], + ['11:37 AM', 'you', '“actually” 😭'], + ['11:38 AM', 'clara', 'you know what i mean'], + ['11:38 AM', 'clara', "don't start"], + ['11:39 AM', 'you', "nah i'm quitting"], + ['11:39 AM', 'clara', 'after doing all the work?'], + ['11:39 AM', 'you', 'especially after doing all the work'], + ['11:40 AM', 'clara', 'very efficient labor strategy'], + ['12:12 PM', 'clara', 'are you going to campus later'], + ['12:14 PM', 'you', 'yeah probably around 2'], + ['12:14 PM', 'clara', 'can you bring the prints'], + ['12:15 PM', 'you', "if the printer doesn't decide to kill itself again"], + ['12:15 PM', 'clara', 'fair'], + ['12:16 PM', 'clara', 'third floor printer'], + ['12:16 PM', 'clara', 'NOT the library one'], + ['12:17 PM', 'you', 'why'], + ['12:17 PM', 'clara', 'library one made everything green yesterday'], + ['12:17 PM', 'you', 'lmao'], + ['1:03 PM', 'you', 'printer works'], + ['1:03 PM', 'you', 'society survives another day'], + ['1:05 PM', 'clara', 'historic victory'], + [ + '1:05 PM', + 'you', + photo('/demo/printed-posters.webp', 'Stack of freshly printed posters'), + ], + ['1:05 PM', 'you', '20 enough?'], + ['1:06 PM', 'clara', 'yeah'], + ['1:06 PM', 'clara', 'maybe 25?'], + ['1:07 PM', 'you', 'mf'], + ['1:07 PM', 'clara', 'sorry 😭'], + ['2:21 PM', 'clara', 'where are you'], + ['2:23 PM', 'you', 'union'], + ['2:23 PM', 'clara', 'upstairs?'], + ['2:23 PM', 'you', 'yep'], + ['2:24 PM', 'clara', 'coming'], + ['3:48 PM', 'clara', 'i forgot my charger there'], + ['3:49 PM', 'you', 'black usb c?'], + ['3:49 PM', 'clara', 'yes'], + ['3:50 PM', 'you', 'i have it'], + ['3:50 PM', 'clara', "you're a hero"], + ['3:50 PM', 'you', '$14.99 to get it back'], + ['3:51 PM', 'clara', 'exploitation'], + ['3:51 PM', 'you', 'market price'], + ['5:14 PM', 'clara', 'okay serious question'], + ['5:14 PM', 'clara', 'should we put the little paragraph on the poster'], + ['5:15 PM', 'you', 'absolutely not'], + ['5:15 PM', 'clara', 'why'], + [ + '5:16 PM', + 'you', + 'because nobody standing in a hallway wants to read a manifesto', + ], + ['5:16 PM', 'clara', 'coward'], + ['5:16 PM', 'you', 'put it on the site'], + ['5:17 PM', 'clara', 'fine'], + [ + '5:52 PM', + 'you', + 'already did', + { sender: 'Clara Z.', preview: 'put it on the site' }, + ], + ['5:53 PM', 'clara', 'send link'], + ['5:54 PM', 'you', 'still deploying'], + ['5:54 PM', 'clara', 'of course'], + ['5:56 PM', 'you', 'okay now'], + ['5:57 PM', 'clara', 'mobile is broken'], + ['5:57 PM', 'you', 'WHAT'], + ['5:57 PM', 'clara', '💀'], + ['5:58 PM', 'you', 'hold on'], + ['6:06 PM', 'you', 'fixed'], + ['6:07 PM', 'clara', 'yeah'], + ['6:07 PM', 'clara', 'much better'], + ['6:08 PM', 'clara', 'the image is still huge though'], + ['6:08 PM', 'you', 'intentional'], + ['6:09 PM', 'clara', 'sure'], + [ + '6:30 PM', + 'clara', + reel( + 'catworkers', + 'cat repeatedly falling asleep on a keyboard', + '/demo/cat-keyboard-reel.webp', + 'Cat asleep on a keyboard Reel', + ), + ], + ['6:31 PM', 'clara', 'you coding tonight'], + ['6:31 PM', 'you', 'basically'], + ['6:32 PM', 'you', "that's literally me"], + ['6:32 PM', 'clara', 'thought so'], + ['8:46 PM', 'you', 'did you eat btw'], + ['8:49 PM', 'clara', 'yeah'], + ['8:49 PM', 'clara', 'you?'], + ['8:50 PM', 'you', 'not yet'], + ['8:50 PM', 'clara', 'go eat'], + ['8:50 PM', 'you', 'later'], + ['8:51 PM', 'clara', 'no'], + ['8:51 PM', 'clara', 'now'], + ['8:51 PM', 'you', 'okay mom'], + ['10:14 PM', 'clara', 'i changed two lines in the description'], + ['10:14 PM', 'clara', "don't be mad"], + ['10:15 PM', 'you', 'what did you do'], + ['10:15 PM', 'clara', 'nothing crazy'], + ['10:16 PM', 'clara', 'refresh'], + ['10:17 PM', 'you', 'oh'], + ['10:17 PM', 'you', "yeah that's better actually"], + ['10:18 PM', 'clara', 'screenshotting this'], + ['10:18 PM', 'you', 'shut up'], +]; + +const wednesday: readonly RawMessage[] = [ + ['8:03 AM', 'clara', 'awake?'], + ['8:27 AM', 'you', 'unfortunately'], + ['8:28 AM', 'clara', 'meeting moved to 11'], + ['8:28 AM', 'you', 'god bless'], + ['8:29 AM', 'clara', "knew you'd appreciate that"], + ['9:11 AM', 'you', 'do we have the room confirmed'], + ['9:13 AM', 'clara', '204'], + ['9:13 AM', 'clara', 'i think'], + ['9:14 AM', 'you', 'reassuring'], + ['9:15 AM', 'clara', 'checking'], + ['9:17 AM', 'clara', 'yes 204'], + ['9:42 AM', 'clara', 'found this btw'], + [ + '9:42 AM', + 'clara', + photo('/demo/archival-flyer.webp', 'Old photocopied archival flyer'), + ], + ['9:43 AM', 'you', 'where tf did you find that'], + ['9:43 AM', 'clara', 'basement archive'], + ['9:44 AM', 'clara', 'look at the type at the bottom'], + ['9:45 AM', 'you', 'ohhh'], + ['9:45 AM', 'you', "wait that's perfect"], + ['9:46 AM', 'clara', 'exactly'], + ['10:02 AM', 'you', 'stealing it'], + ['10:02 AM', 'clara', '*learning from history'], + ['10:03 AM', 'you', 'sure clara'], + ['10:03 AM', 'clara', '😇'], + ['10:38 AM', 'you', 'headed over'], + ['10:39 AM', 'clara', "i'm already here"], + ['10:39 AM', 'you', 'nerd'], + [ + '10:40 AM', + 'clara', + 'says the person bringing a laptop to a 20 minute meeting', + ], + ['10:40 AM', 'you', 'you never know'], + ['10:41 AM', 'clara', 'you absolutely know'], +]; + +function toMinutes(time: string): number { + const match = /^(\d{1,2}):(\d{2}) (AM|PM)$/u.exec(time); + if (!match) return 0; + const [, hourText = '0', minuteText = '0', period = 'AM'] = match; + const hour = (Number(hourText) % 12) + (period === 'PM' ? 12 : 0); + return hour * 60 + Number(minuteText); +} + +function buildDay( + date: HeroDemoMessage['timestamp']['date'], + raw: readonly RawMessage[], + offset: number, +): HeroDemoMessage[] { + return raw.map(([time, sender, value, reply], index) => { + const content: HeroContent = + typeof value === 'string' ? { type: 'text', text: value } : value; + const previous = raw[index - 1]; + const showTime = + index === 0 || + (previous !== undefined && + toMinutes(time) - toMinutes(previous[0]) >= 15); + + return { + key: `seed-${offset + index + 1}`, + sender, + timestamp: { time, date }, + showDate: index === 0, + showTime, + content, + ...(reply ? { reply } : {}), + }; + }); +} + +export const HERO_MESSAGES: readonly HeroDemoMessage[] = [ + ...buildDay('Tuesday', tuesday, 0), + ...buildDay('Wednesday', wednesday, tuesday.length), +]; diff --git a/website/src/pages/index.astro b/website/src/pages/index.astro index 5fbaf28..afa5c27 100644 --- a/website/src/pages/index.astro +++ b/website/src/pages/index.astro @@ -1,5 +1,6 @@ --- import ChromeCta from '../components/ChromeCta.astro'; +import HeroDemo from '../components/HeroDemo.astro'; import { GITHUB_URL, HOME_DESCRIPTION, HOME_TITLE } from '../config'; import BaseLayout from '../layouts/BaseLayout.astro'; --- @@ -8,44 +9,7 @@ import BaseLayout from '../layouts/BaseLayout.astro';
-
-
- DM2Text -
-
-
- An anonymized Instagram Direct conversation with DM2Text's Copy context action
Copy context
-
-
-
- clipboard.txtplain text -
[10:41 AM, Tuesday] Alex: did you see the draft
-
-You: yep
-
-Alex: are you free now?
-
-You (replying to Alex:
-are you free now?):
-i'm at the office
-
-Alex: sounds good
-
-[10:44 AM, Tuesday] You:
-sorry!
-
-
-
+

DM2Text copies part of an Instagram Direct conversation diff --git a/website/tests/site-output.test.mjs b/website/tests/site-output.test.mjs index f41a133..2be2c24 100644 --- a/website/tests/site-output.test.mjs +++ b/website/tests/site-output.test.mjs @@ -53,14 +53,36 @@ test('home matches the reference product presentation', async () => { const visible = text(html); assert.match(html, /href="\/privacy\/"/); assert.match(html, /https:\/\/github\.com\/postigodev\/dm2text/); - assert.match(html, /src="\/marketing\/dm-example\.png"/); + assert.match(html, /data-dm2text-demo/); + assert.match(html, /data-message-count="156"/); + assert.match(html, /data-copy-context/); + assert.match(html, /data-unsend/); + assert.match(html, /data-composer/); + assert.match(html, /data-add-photo/); + assert.match(html, /data-add-reel/); + assert.match(html, /data-message-template="text"/); + assert.match(html, /data-message-template="media"/); + assert.match(html, /data-message-template="shared-post"/); + assert.match(html, /data-count-input/); + assert.match(html, /data-paste-zone/); + assert.match(html, /navigator\.clipboard\?\.writeText/); + assert.doesNotMatch(html, /src="\/marketing\/dm-example\.png"/); + assert.doesNotMatch(html, /images\.unsplash\.com/); assert.match(html, /src="\/brand\/google-chrome\.svg"/); assert.match( html, /href="https:\/\/chromewebstore\.google\.com\/detail\/dm2text\/gpedpddbcooaomkehnmpcjjghnbknpbd"/, ); + assert.match(visible, /clara\.zk Clara Z\./); + assert.match(visible, /Tuesday 9:18 AM .*did you export the new version/); + assert.match(html, /data-text="professional operation we're running here"/); + assert.match(visible, /Swiss posters from the 1970s/); + assert.match(visible, /Wednesday 8:03 AM .*awake\?/); + assert.match(visible, /you absolutely know/); assert.match(visible, /Copy context/); - assert.match(visible, /clipboard\.txt plain text/); + assert.match(visible, /Messages to include/); + assert.match(visible, /pasteboard\.txt plain text/); + assert.match(visible, /Nothing is inserted here automatically\./); assert.match( visible, /copies part of an Instagram Direct conversation as plain text/, @@ -116,7 +138,7 @@ test('pages preserve canonical metadata and avoid hydration', async () => { ); }); -test('build includes local brand and marketing assets', async () => { +test('build includes local brand and unique demo media assets', async () => { for (const path of [ 'brand/dm2text-mark.svg', 'brand/google-chrome.svg', @@ -124,6 +146,33 @@ test('build includes local brand and marketing assets', async () => { 'brand/icon-96.png', 'brand/icon-128.png', 'marketing/dm-example.png', + 'demo/clara-z.webp', + 'demo/poster-mockup.webp', + 'demo/finished-poster.webp', + 'demo/printed-posters.webp', + 'demo/swiss-posters-reel.webp', + 'demo/archival-flyer.webp', + 'demo/cat-keyboard-reel.webp', + 'demo/composer-photo.webp', + 'demo/composer-reel.webp', ]) await access(pageUrl(path)); }); + +test('canonical seeded media uses one distinct local asset per event', async () => { + const html = await readFile(pageUrl('index.html'), 'utf8'); + const seededAssets = [ + 'poster-mockup.webp', + 'finished-poster.webp', + 'printed-posters.webp', + 'swiss-posters-reel.webp', + 'archival-flyer.webp', + 'cat-keyboard-reel.webp', + ]; + + for (const asset of seededAssets) { + assert.equal(html.match(new RegExp(`/demo/${asset}`, 'g'))?.length, 1); + } + assert.match(html, /data-source="design\.archive"/); + assert.match(html, /data-source="catworkers"/); +});