Skip to content

Commit bca4993

Browse files
committed
squashed commit of general improvements
commit f833519 Author: Danny White <3104761+dnywh@users.noreply.github.com> Date: Fri Mar 13 14:27:09 2026 +1100 profile client leaf commit 7471b7a Author: Danny White <3104761+dnywh@users.noreply.github.com> Date: Fri Mar 13 14:15:17 2026 +1100 version bump commit c669444 Author: Danny White <3104761+dnywh@users.noreply.github.com> Date: Fri Mar 13 14:13:38 2026 +1100 more deduplicate route fetching commit fcf5f12 Author: Danny White <3104761+dnywh@users.noreply.github.com> Date: Fri Mar 13 14:08:41 2026 +1100 deduplicate route data fetching for listing and map pages commit 4bba868 Author: Danny White <3104761+dnywh@users.noreply.github.com> Date: Fri Mar 13 14:03:34 2026 +1100 fix chat text alignment
1 parent 4c3cf74 commit bca4993

9 files changed

Lines changed: 83 additions & 76 deletions

File tree

package-lock.json

Lines changed: 16 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
"@types/react": "19.2.2",
5353
"@types/react-dom": "19.2.2",
5454
"autoprefixer": "^10.4.20",
55-
"baseline-browser-mapping": "^2.9.14",
55+
"baseline-browser-mapping": "^2.10.7",
5656
"postcss": "^8.4.49",
5757
"tailwind-merge": "^2.5.2",
5858
"tailwindcss-animate": "^1.0.7",

src/app/(core)/(interact)/(centered)/listings/[slug]/page.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { notFound } from "next/navigation";
33
import { generateListingMetadata } from "@/utils/listingUtils";
44
import ListingRead from "@/components/ListingRead";
55
import { styled } from "@pigment-css/react";
6+
import { cache } from "react";
67

78
const StyledMain = styled("main")(({ theme }) => ({
89
flex: 1, // Should be shared with layout used by Profile and Listings pages
@@ -29,7 +30,7 @@ const StyledMain = styled("main")(({ theme }) => ({
2930
},
3031
}));
3132

32-
async function getListingData(slug) {
33+
const getListingData = cache(async (slug) => {
3334
const supabase = await createClient();
3435
const {
3536
data: { user },
@@ -40,7 +41,7 @@ async function getListingData(slug) {
4041
.match({ slug })
4142
.single();
4243
return { user, listing };
43-
}
44+
});
4445

4546
export async function generateMetadata({ params }) {
4647
const { slug } = await params;

src/app/(core)/(interact)/(centered)/profile/page.js

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,39 +5,37 @@ import ProfileListings from "@/components/ProfileListings";
55
import ProfileActions from "@/components/ProfileActions";
66
import LegalFooter from "@/components/LegalFooter";
77
import { styled } from "@pigment-css/react";
8+
import { Suspense } from "react";
9+
import Toast from "@/components/Toast";
810

911
export const metadata = {
1012
title: "Profile",
1113
};
1214

13-
// This page could be static instead of dynamic by handling searchParams in a client component instead and using Suspense here. See the homepage and Toast component as an example.
14-
export default async function ProfilePage({ searchParams }) {
15-
const message = (await searchParams)?.message;
16-
const error = (await searchParams)?.error;
15+
// Keep URL-based feedback in a client leaf so server rendering is driven by auth/data only.
16+
export default async function ProfilePage() {
1717
const supabase = await createClient();
18-
// Get user data and listings in one query
18+
// Get the authenticated user first, then fetch profile data in parallel.
1919
const {
2020
data: { user },
2121
} = await supabase.auth.getUser();
2222

23-
const { data: listings } = await supabase
24-
// We can access the "listings" table here directly as we have a policy set allowing authenticated owners access to their full listings
25-
// TODO: but should we? Can we get everything we need from the private_data view?
26-
.from("listings")
27-
.select()
28-
.eq("owner_id", user.id)
29-
.order("created_at", { ascending: true });
30-
31-
const { data: profile } = await supabase
32-
.from("profiles")
33-
.select()
34-
.eq("id", user.id)
35-
.single();
23+
const [{ data: listings }, { data: profile }] = await Promise.all([
24+
supabase
25+
// We can access the "listings" table here directly as we have a policy set allowing authenticated owners access to their full listings
26+
// TODO: but should we? Can we get everything we need from the private_data view?
27+
.from("listings")
28+
.select()
29+
.eq("owner_id", user.id)
30+
.order("created_at", { ascending: true }),
31+
supabase.from("profiles").select().eq("id", user.id).single(),
32+
]);
3633

3734
return (
3835
<>
39-
{message && <p>Message: {message}</p>}
40-
{error && <p>Error: {error}</p>}
36+
<Suspense>
37+
<Toast />
38+
</Suspense>
4139

4240
<NakedSection>
4341
<ProfileHeader profile={profile} user={user} />

src/app/(core)/(interact)/(stretched)/map/page.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ import { createClient } from "@/utils/supabase/server";
22
import { siteConfig } from "@/config/site";
33
import { generateListingMetadata } from "@/utils/listingUtils";
44
import MapPageClient from "@/components/MapPageClient";
5+
import { cache } from "react";
56

67
// Fetch data only once and use across metadata and page
7-
async function getInitialData(listingSlug) {
8+
const getInitialData = cache(async (listingSlug) => {
89
const supabase = await createClient();
910

1011
// Get user first
@@ -25,11 +26,10 @@ async function getInitialData(listingSlug) {
2526
user,
2627
listing: listingResponse?.data,
2728
};
28-
}
29+
});
2930

3031
export async function generateMetadata({ searchParams }) {
3132
const listingSlug = (await searchParams)?.listing;
32-
const { user, listing } = await getInitialData(listingSlug);
3333

3434
if (!listingSlug) {
3535
return {
@@ -40,6 +40,8 @@ export async function generateMetadata({ searchParams }) {
4040
};
4141
}
4242

43+
const { user, listing } = await getInitialData(listingSlug);
44+
4345
// Use shared utility to generate metadata
4446
return generateListingMetadata(listing, user);
4547
}

src/components/ChatMessage/ChatMessage.jsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ const ChatBubble = styled("p")(({ theme }) => ({
3434
marginLeft: "30%",
3535
backgroundColor: theme.colors.message.sent.background,
3636
color: theme.colors.message.sent.text,
37-
textAlign: "right",
3837
textShadow: `0 0.35px 1px rgba(0, 0, 0, .1)`, // To increase contrast of text
3938
},
4039
},
@@ -45,7 +44,6 @@ const ChatBubble = styled("p")(({ theme }) => ({
4544
marginRight: "30%",
4645
backgroundColor: theme.colors.message.received.background,
4746
color: theme.colors.message.received.text,
48-
textAlign: "left",
4947
},
5048
},
5149
],

src/components/Toast/Toast.jsx

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
"use client";
22

3-
import { useState, useEffect } from "react";
4-
import Button from "@/components/Button";
53
import { styled } from "@pigment-css/react";
64
import { useSearchParams } from "next/navigation";
75

@@ -55,26 +53,28 @@ const StyledToast = styled("div")(({ theme }) => ({
5553
}));
5654

5755
function Toast({ variant: propVariant, children: propChildren }) {
58-
const [isOpen, setIsOpen] = useState(true);
5956
const searchParams = useSearchParams();
6057

6158
// Handle URL-based toasts
6259
const error = searchParams?.get("error");
60+
const message = searchParams?.get("message");
6361
const success = searchParams?.get("success");
64-
const shouldShowEmailUpdatedToast = success === "email_change";
62+
const successMessage =
63+
message ||
64+
(success === "email_change"
65+
? "Your email has been successfully updated"
66+
: success && success !== "true"
67+
? success
68+
: null);
6569

6670
const variant = propVariant || (error ? "error" : "success");
67-
const children =
68-
propChildren ||
69-
(error
70-
? "Something's not right. Mind trying again?"
71-
: "Your email has been successfully updated");
71+
const children = propChildren || error || successMessage;
7272

7373
// Only show if we have props OR relevant URL params
74-
if (!propChildren && !error && !shouldShowEmailUpdatedToast) return null;
74+
if (!children) return null;
7575

7676
return (
77-
<StyledToast variant={variant} isOpen={isOpen}>
77+
<StyledToast variant={variant}>
7878
<p>{children}</p>
7979
{/* <Button
8080
variant="secondary"

src/lib/content/handlers/legal.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { notFound } from "next/navigation";
2+
import { cache } from "react";
23
import type { LegalPageData } from "../types";
34
import {
45
formatContentData,
56
validateBaseCustomMetadata,
67
validateBaseMetadata,
78
} from "../utils";
89

9-
export async function getLegalPageMetadata(
10+
export const getLegalPageMetadata = cache(async function getLegalPageMetadata(
1011
slug: string
1112
): Promise<LegalPageData> {
1213
try {
@@ -36,4 +37,4 @@ export async function getLegalPageMetadata(
3637
console.error(error?.message);
3738
return notFound();
3839
}
39-
}
40+
});

src/lib/content/handlers/newsletter.ts

Lines changed: 26 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { notFound } from "next/navigation";
2+
import { cache } from "react";
23
import type { NewsletterIssueData } from "../types";
34
import {
45
formatContentData,
@@ -7,33 +8,35 @@ import {
78
validateNewsletterMetadata,
89
} from "../utils";
910

10-
export async function getNewsletterIssueMetadata(
11-
slug: string
12-
): Promise<NewsletterIssueData> {
13-
try {
14-
const file = await import(`@/content/newsletter/${slug}.mdx`);
11+
export const getNewsletterIssueMetadata = cache(
12+
async function getNewsletterIssueMetadata(
13+
slug: string
14+
): Promise<NewsletterIssueData> {
15+
try {
16+
const file = await import(`@/content/newsletter/${slug}.mdx`);
1517

16-
if (file?.metadata && file?.customMetadata) {
17-
validateNewsletterMetadata(file.metadata, slug);
18-
validateNewsletterCustomMetadata(file.customMetadata, slug);
18+
if (file?.metadata && file?.customMetadata) {
19+
validateNewsletterMetadata(file.metadata, slug);
20+
validateNewsletterCustomMetadata(file.customMetadata, slug);
1921

20-
return formatContentData(
21-
{
22-
slug,
23-
metadata: file.metadata,
24-
customMetadata: file.customMetadata,
25-
},
26-
"publishDate",
27-
true // publishDate is required for legal pages
28-
);
29-
}
22+
return formatContentData(
23+
{
24+
slug,
25+
metadata: file.metadata,
26+
customMetadata: file.customMetadata,
27+
},
28+
"publishDate",
29+
true // publishDate is required for legal pages
30+
);
31+
}
3032

31-
throw new Error(`Unable to find metadata for ${slug}.mdx`);
32-
} catch (error: any) {
33-
console.error(error?.message);
34-
return notFound();
33+
throw new Error(`Unable to find metadata for ${slug}.mdx`);
34+
} catch (error: any) {
35+
console.error(error?.message);
36+
return notFound();
37+
}
3538
}
36-
}
39+
);
3740

3841
export async function getAllNewsletterIssues(): Promise<NewsletterIssueData[]> {
3942
const slugs = await getAllContentSlugs("newsletter");

0 commit comments

Comments
 (0)