Skip to content

Commit 5586b6f

Browse files
committed
smooth hero cta swap
1 parent 6dc0577 commit 5586b6f

5 files changed

Lines changed: 45 additions & 56 deletions

File tree

docs/auth-session-architecture.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ This means the first paint can briefly look signed-out on public pages. That is
3232

3333
`UnreadMessagesProvider` belongs near the UI that displays or consumes unread state, such as tab-bar and chat layouts. It should not wrap the root layout.
3434

35-
The unread check should stay deferred so it does not delay public HTML. If the unread dot disappears entirely, check the provider scope, the idle auth check, the `chat_threads` query, and the `unread_chat_thread_ids` RPC before moving the provider back to the root.
35+
The unread check should stay deferred so it does not delay public HTML. If the unread dot disappears entirely, check the provider scope, the idle auth check, and the unread `chat_messages` count before moving the provider back to the root.
3636

3737
## Homepage First Paint
3838

e2e/home.spec.ts

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -146,9 +146,14 @@ test("homepage account button stays hidden while signed-in profile state loads",
146146
}
147147

148148
const profileAccountButton = page.getByTestId("account-button-profile");
149+
const heroSecondaryAction = page.getByTestId("hero-secondary-action");
149150

150151
await expect(profileAccountButton).toHaveAttribute("href", "/profile");
151152
await expect(profileAccountButton).toHaveCSS("opacity", "1");
153+
await expect(heroSecondaryAction).toHaveAttribute(
154+
"href",
155+
"/profile/listings/new"
156+
);
152157
await expect(page.getByTestId("locale-picker-select")).toHaveCount(0);
153158
});
154159

@@ -160,36 +165,34 @@ test("homepage account button links guests to sign in", async ({ page }) => {
160165
"/sign-in"
161166
);
162167
await expect(page.getByTestId("account-button-profile")).toHaveCount(0);
168+
await expect(page.getByTestId("hero-secondary-action")).toHaveAttribute(
169+
"href",
170+
"/sign-up"
171+
);
163172
await expect(page.getByTestId("locale-picker-select")).toBeVisible();
164173
});
165174

166175
test("homepage unread chat dot appears after scoped unread check", async ({
167176
page,
168177
}) => {
169-
await page.route(/\/rest\/v1\/chat_threads(?:\?|$)/, async (route) => {
170-
if (route.request().method() !== "GET") {
178+
await page.route(/\/rest\/v1\/chat_messages(?:\?|$)/, async (route) => {
179+
const method = route.request().method();
180+
181+
if (method !== "GET" && method !== "HEAD") {
171182
await route.continue();
172183
return;
173184
}
174185

175186
await route.fulfill({
176187
status: 200,
177188
contentType: "application/json",
178-
body: JSON.stringify([{ id: "33333333-3333-4333-8333-333333333333" }]),
189+
headers: {
190+
"content-range": "0-0/1",
191+
"access-control-expose-headers": "content-range",
192+
},
193+
body: method === "HEAD" ? "" : JSON.stringify([]),
179194
});
180195
});
181-
await page.route(
182-
/\/rest\/v1\/rpc\/unread_chat_thread_ids(?:\?|$)/,
183-
async (route) => {
184-
await route.fulfill({
185-
status: 200,
186-
contentType: "application/json",
187-
body: JSON.stringify([
188-
{ thread_id: "33333333-3333-4333-8333-333333333333" },
189-
]),
190-
});
191-
}
192-
);
193196

194197
await signIn(page, { email: DONOR_EMAIL, redirectTo: "/" });
195198

src/components/HeroButtons/HeroButtons.tsx

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,23 @@ export default function HeroButtons() {
1515
{t("browseMap")}
1616
</HeroActionButton>
1717
{user ? (
18-
<HeroActionButton
18+
<SecondaryHeroActionButton
1919
href="/profile/listings/new"
2020
variant="secondary"
2121
size="massive"
22+
data-testid="hero-secondary-action"
2223
>
2324
{t("addListing")}
24-
</HeroActionButton>
25+
</SecondaryHeroActionButton>
2526
) : (
26-
<HeroActionButton href="/sign-up" variant="secondary" size="massive">
27+
<SecondaryHeroActionButton
28+
href="/sign-up"
29+
variant="secondary"
30+
size="massive"
31+
data-testid="hero-secondary-action"
32+
>
2733
{t("signUp")}
28-
</HeroActionButton>
34+
</SecondaryHeroActionButton>
2935
)}
3036
</ButtonContainer>
3137
);
@@ -41,6 +47,12 @@ const HeroActionButton = styled(Button)`
4147
}
4248
`;
4349

50+
const SecondaryHeroActionButton = styled(HeroActionButton)`
51+
@media (min-width: 768px) {
52+
min-width: 13.5rem;
53+
}
54+
`;
55+
4456
const ButtonContainer = styled.div`
4557
margin-top: 1rem;
4658
width: 100%;

src/contexts/UnreadMessagesContext.tsx

Lines changed: 8 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,6 @@ type ChatMessagePayload = {
2121
thread_id: string | null;
2222
};
2323

24-
type ChatThreadRow = {
25-
id: string;
26-
};
27-
28-
type UnreadThreadRow = {
29-
thread_id: string | null;
30-
};
31-
3224
type UnreadMessagesContextValue = {
3325
unreadCount: number;
3426
setUnreadCount: Dispatch<SetStateAction<number>>;
@@ -133,39 +125,19 @@ export function UnreadMessagesProvider({ children }: PropsWithChildren) {
133125
return;
134126
}
135127

136-
const { data: threads, error: threadsError } = await supabase
137-
.from("chat_threads")
138-
.select("id")
139-
.or(`initiator_id.eq.${userId},owner_id.eq.${userId}`);
128+
const { count, error } = await supabase
129+
.from("chat_messages")
130+
.select("id", { count: "exact", head: true })
131+
.neq("sender_id", userId)
132+
.is("read_at", null);
140133

141-
if (threadsError) {
142-
console.error("Error checking chat threads:", threadsError);
134+
if (error) {
135+
console.error("Error checking unread messages:", error);
143136
return;
144137
}
145138

146-
const threadIds = ((threads as ChatThreadRow[] | null) ?? []).map(
147-
(thread) => thread.id
148-
);
149-
const { data: unreadThreads, error: unreadThreadsError } =
150-
threadIds.length > 0
151-
? await supabase.rpc("unread_chat_thread_ids", {
152-
thread_ids: threadIds,
153-
})
154-
: { data: [], error: null };
155-
156-
if (unreadThreadsError) {
157-
console.error("Error checking unread messages:", unreadThreadsError);
158-
return;
159-
}
160-
161-
const unreadThreadIds = new Set(
162-
((unreadThreads as UnreadThreadRow[] | null) ?? [])
163-
.map((thread) => thread.thread_id)
164-
.filter((threadId): threadId is string => Boolean(threadId))
165-
);
166-
167139
if (!isActive) return;
168-
setUnreadCount(unreadThreadIds.size);
140+
setUnreadCount(count ?? 0);
169141
} catch (error) {
170142
console.error("Error in checkUnreadMessages:", error);
171143
}

src/proxy.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ function shouldUpdateSession(request: NextRequest) {
4545
}
4646

4747
export async function proxy(request: NextRequest) {
48+
// Public routes deliberately skip Supabase auth refresh for first-paint
49+
// performance. Auth-aware client slots converge after hydration.
4850
const response = shouldUpdateSession(request)
4951
? await updateSession(request)
5052
: createSignedOutResponse(request);

0 commit comments

Comments
 (0)