Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
e156973
feat(storybook): snapshot share images and their button placements
tomeredlich Sep 6, 2026
8bfbc09
feat(snapshot): grow text share images and mark the selection in place
tomeredlich Sep 6, 2026
4a84573
feat(snapshot): make the post card the post page, and give it the room
tomeredlich Sep 6, 2026
5232c3d
feat(snapshot): spend the post card's spacing on its copy
tomeredlich Sep 7, 2026
0832f16
feat(snapshot): lead the post card with the TLDR
tomeredlich Sep 7, 2026
b17223d
style(snapshot): set the post TLDR at normal weight
tomeredlich Sep 7, 2026
9e8dc2e
style(snapshot): grow the wide card further over its gradient
tomeredlich Sep 7, 2026
6d9c227
feat(snapshot): let a growing card shrink, and set its copy as body text
tomeredlich Sep 7, 2026
1e7b647
feat(snapshot): strip the post card to the TLDR and its credit
tomeredlich Sep 7, 2026
7e9502f
feat(snapshot): put the headline back on the post card
tomeredlich Sep 7, 2026
b1c910e
style(snapshot): rule the post card between copy and credit
tomeredlich Sep 7, 2026
6f571d2
feat(snapshot): reduce the post card to its TLDR
tomeredlich Sep 7, 2026
416480c
feat(snapshot): credit the post card again
tomeredlich Sep 7, 2026
b389564
feat(snapshot): set the highlight card like the post card
tomeredlich Sep 7, 2026
1234817
feat(snapshot): one treatment for every text share image
tomeredlich Sep 7, 2026
0d6744d
feat(snapshot): label the surface on the happening now card
tomeredlich Sep 7, 2026
6aafb79
feat(snapshot): move every surface label onto the logo row
tomeredlich Sep 7, 2026
7b8656e
style(snapshot): centre the hot take, unrule the invite
tomeredlich Sep 7, 2026
0455265
Merge branch 'main' into snapshot-share-images
tomeredlich Sep 9, 2026
9d3903b
feat(snapshot): keep the paragraph around a highlighted selection
tomeredlich Sep 10, 2026
1880f96
Merge branch 'main' into snapshot-share-images
idoshamun Sep 10, 2026
fb74064
fix(snapshot): reconcile the share cards with the shipped post page
idoshamun Sep 10, 2026
64bd142
chore(storybook): bring the snapshot stories in line with main
idoshamun Sep 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 130 additions & 0 deletions packages/shared/src/features/snapshot/AchievementSnapshotCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import type { ReactElement } from 'react';
import React, { forwardRef } from 'react';
import { AchievementRarityTier } from '../profile/components/achievements/achievementRarity';
import { SnapshotFrame } from './SnapshotFrame';

const CARD_WIDTH = 620;
/** Trading-card proportions (2.5:3.5) rather than a square slab. */
const CARD_HEIGHT = Math.round(CARD_WIDTH * 1.4);
const CARD_RADIUS = 36;

/**
* The Game Center slab treatment: artwork full bleed, a scrim carrying the
* copy, and gold reserved for the sub-1% band so it means something.
*/
const SCRIM =
'linear-gradient(to top, rgba(6,8,11,0.94) 0%, rgba(6,8,11,0.72) 34%, rgba(6,8,11,0.12) 66%, rgba(6,8,11,0) 100%)';

const PILL = {
gold: { background: '#efab27', color: '#08110c' },
plain: { background: 'rgba(8,10,13,0.72)', color: '#FFFFFF' },
};

export interface AchievementSnapshotCardProps {
name: string;
description: string;
image?: string;
rarity: number | null;
tier: AchievementRarityTier | null;
completedAt: string;
seed?: string;
}

function AchievementSnapshotCardComponent(
{
name,
description,
image,
rarity,
tier,
completedAt,
seed,
}: AchievementSnapshotCardProps,
ref: React.Ref<HTMLDivElement>,
): ReactElement {
const isEmerald = tier === AchievementRarityTier.Emerald;
const pill = isEmerald ? PILL.gold : PILL.plain;
const rarityLabel = isEmerald ? '<1%' : `${Math.round(rarity ?? 0)}%`;

return (
<SnapshotFrame bare logoPlacement="top-right" ref={ref} seed={seed ?? name}>
<div
className="relative flex flex-col justify-end overflow-hidden"
style={{
width: CARD_WIDTH,
height: CARD_HEIGHT,
borderRadius: CARD_RADIUS,
background: '#12151C',
boxShadow: '0 48px 96px rgba(4, 2, 9, 0.7)',
}}
>
{image && (
<img
src={image}
alt=""
crossOrigin="anonymous"
className="absolute inset-0 block size-full object-cover"
/>
)}

<span
aria-hidden
className="absolute inset-0"
style={{ background: SCRIM }}
/>

{tier && (
<span
className="absolute font-bold"
style={{
left: 26,
top: 26,
padding: '9px 20px',
borderRadius: 999,
fontSize: 26,
lineHeight: 1,
...pill,
}}
>
{rarityLabel} rare
</span>
)}

<div
className="relative flex flex-col"
style={{ padding: '0 34px 34px' }}
>
<span
className="snapshot-copy font-bold text-white"
style={{ fontSize: 46, lineHeight: 1.2, letterSpacing: '-0.01em' }}
>
{name}
</span>
<span
style={{
marginTop: 8,
color: 'rgba(255,255,255,0.78)',
fontSize: 28,
lineHeight: 1.32,
}}
>
{description}
</span>
<span
style={{
marginTop: 14,
color: 'rgba(255,255,255,0.7)',
fontSize: 26,
}}
>
Completed {completedAt}
</span>
</div>
</div>
</SnapshotFrame>
);
}

export const AchievementSnapshotCard = forwardRef(
AchievementSnapshotCardComponent,
);
99 changes: 99 additions & 0 deletions packages/shared/src/features/snapshot/AchievementsSnapshotCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import type { ReactElement } from 'react';
import React, { forwardRef } from 'react';
import colors from '../../styles/colors';
import { largeNumberFormat } from '../../lib/numberFormat';
import { SnapshotEyebrow } from './SnapshotEyebrow';
import { SnapshotFrame } from './SnapshotFrame';
import type { SnapshotIdentityProps } from './SnapshotIdentity';
import { SnapshotIdentity } from './SnapshotIdentity';
import { SnapshotTile } from './SnapshotStats';

const MUTED = colors.salt['90'];
const DIVIDER = colors.pepper['10'];

const TILE_SIZE = 104;

export interface UnlockedAchievement {
name: string;
image?: string;
emoji?: string;
}

export interface AchievementsSnapshotCardProps {
user: SnapshotIdentityProps;
unlocked: number;
total: number;
points: number;
achievements: UnlockedAchievement[];
seed?: string;
}

function AchievementsSnapshotCardComponent(
{
user,
unlocked,
total,
points,
achievements,
seed,
}: AchievementsSnapshotCardProps,
ref: React.Ref<HTMLDivElement>,
): ReactElement {
return (
<SnapshotFrame
logoAside={<SnapshotEyebrow label="Achievements" />}
ref={ref}
seed={seed ?? 'achievements'}
>
<div className="flex flex-1 flex-col gap-6">
<SnapshotIdentity {...user} />

<div className="flex gap-4">
<SnapshotTile
label={`of ${total} unlocked`}
value={String(unlocked)}
/>
<SnapshotTile
label="Achievement points"
value={largeNumberFormat(points) ?? String(points)}
/>
</div>

<div className="mt-auto flex flex-col gap-3">
<span style={{ color: MUTED, fontSize: 26 }}>Rarest unlocked</span>
<div className="grid grid-cols-5 gap-4">
{achievements.slice(0, 10).map((achievement) => (
<span
key={achievement.name}
className="flex items-center justify-center overflow-hidden rounded-20"
style={{
width: TILE_SIZE,
height: TILE_SIZE,
border: `1px solid ${DIVIDER}`,
background: 'rgba(255, 255, 255, 0.04)',
}}
>
{achievement.image ? (
<img
src={achievement.image}
alt=""
crossOrigin="anonymous"
className="block size-full object-cover"
/>
) : (
<span style={{ fontSize: 54, lineHeight: 1 }}>
{achievement.emoji}
</span>
)}
</span>
))}
</div>
</div>
</div>
</SnapshotFrame>
);
}

export const AchievementsSnapshotCard = forwardRef(
AchievementsSnapshotCardComponent,
);
104 changes: 104 additions & 0 deletions packages/shared/src/features/snapshot/AwardSnapshotCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import type { ReactElement } from 'react';
import React, { forwardRef } from 'react';
import colors from '../../styles/colors';
import { SnapshotEyebrow } from './SnapshotEyebrow';
import { SnapshotFrame } from './SnapshotFrame';
import type { SnapshotIdentityProps } from './SnapshotIdentity';
import { SnapshotIdentity } from './SnapshotIdentity';

const MUTED = colors.salt['90'];
const DIVIDER = colors.pepper['10'];

export interface AwardSnapshotCardProps {
/** The recipient — the card is theirs to share. */
user: SnapshotIdentityProps;
/** Who sent it. Named, because that is the whole point of this moment. */
from: string;
award: string;
emoji?: string;
image?: string;
/** What the award was given for, e.g. a post or comment title. */
reason?: string;
/** How many of this award the recipient now holds. */
total?: number;
seed?: string;
}

/**
* Being awarded: the one status moment that comes from someone
* else rather than from your own activity. The sender is on the card because a
* gift with no giver reads as a self-congratulation.
*/
function AwardSnapshotCardComponent(
{
user,
from,
award,
emoji,
image,
reason,
total,
seed,
}: AwardSnapshotCardProps,
ref: React.Ref<HTMLDivElement>,
): ReactElement {
return (
<SnapshotFrame
logoAside={<SnapshotEyebrow label="Awarded" />}
ref={ref}
seed={seed ?? `award-${award}`}
watermark={emoji}
>
<div className="flex flex-1 flex-col">
<SnapshotIdentity {...user} />

<div className="flex flex-1 flex-col justify-center">
{image ? (
<img
src={image}
alt=""
crossOrigin="anonymous"
className="block object-contain"
style={{ width: 200, height: 200 }}
/>
) : (
emoji && (
<span style={{ fontSize: 168, lineHeight: 1 }}>{emoji}</span>
)
)}

<span
className="mt-6 font-bold text-white"
style={{ fontSize: 66, lineHeight: 1.05, letterSpacing: '-0.02em' }}
>
{award}
</span>
<span
className="mt-3"
style={{ color: colors.cabbage['10'], fontSize: 30 }}
>
from {from}
</span>
{reason && (
<span
className="snapshot-copy mt-6"
style={{ color: MUTED, fontSize: 28, lineHeight: 1.35 }}
>
{reason}
</span>
)}
</div>

{total !== undefined && (
<div style={{ paddingTop: 26, borderTop: `1px solid ${DIVIDER}` }}>
<span style={{ color: MUTED, fontSize: 26 }}>
{total} awards received
</span>
</div>
)}
</div>
</SnapshotFrame>
);
}

export const AwardSnapshotCard = forwardRef(AwardSnapshotCardComponent);
Loading
Loading