From e57ef0c68222ef05af27ce61fb3df376255fe57d Mon Sep 17 00:00:00 2001 From: Elisha Suleiman <112385548+lishmanTech@users.noreply.github.com> Date: Tue, 30 Jun 2026 14:55:03 +0000 Subject: [PATCH 1/4] feat(ux): add branded referral sharing component and email templates --- package-lock.json | 1 + src/components/referral/EmailTemplate.tsx | 35 +++++++++++++++++++++++ src/components/referral/ReferralShare.tsx | 29 +++++++++++++++++++ src/components/referral/ShareButtons.tsx | 29 +++++++++++++++++++ src/components/referral/index.ts | 4 +++ src/hooks/useShare.ts | 22 ++++++++++++++ src/lib/share.ts | 6 ++++ tests/ReferralShare.test.tsx | 9 ++++++ 8 files changed, 135 insertions(+) create mode 100644 src/components/referral/EmailTemplate.tsx create mode 100644 src/components/referral/ReferralShare.tsx create mode 100644 src/components/referral/ShareButtons.tsx create mode 100644 src/components/referral/index.ts create mode 100644 src/hooks/useShare.ts create mode 100644 src/lib/share.ts create mode 100644 tests/ReferralShare.test.tsx diff --git a/package-lock.json b/package-lock.json index b146856c..6208e36e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18534,6 +18534,7 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, diff --git a/src/components/referral/EmailTemplate.tsx b/src/components/referral/EmailTemplate.tsx new file mode 100644 index 00000000..f40d2e04 --- /dev/null +++ b/src/components/referral/EmailTemplate.tsx @@ -0,0 +1,35 @@ +interface Props { + referralLink: string; +} + +export default function EmailTemplate({ + referralLink, +}: Props) { + return ( +
+
+

Join PropChain

+ +

+ I invited you to join PropChain. +

+ + + Accept Invitation + +
+
+ ); +} \ No newline at end of file diff --git a/src/components/referral/ReferralShare.tsx b/src/components/referral/ReferralShare.tsx new file mode 100644 index 00000000..320d4a44 --- /dev/null +++ b/src/components/referral/ReferralShare.tsx @@ -0,0 +1,29 @@ +import ShareButtons from "./ShareButtons"; +import CopyButton from "./CopyButton"; + +interface Props { + referralLink: string; + referralCode: string; +} + +export default function ReferralShare({ + referralLink, +}: Props) { + return ( +
+

Invite Friends

+ +

+ Share your referral link and earn rewards. +

+ + + + +
+ ); +} \ No newline at end of file diff --git a/src/components/referral/ShareButtons.tsx b/src/components/referral/ShareButtons.tsx new file mode 100644 index 00000000..4318946a --- /dev/null +++ b/src/components/referral/ShareButtons.tsx @@ -0,0 +1,29 @@ +import { useShare } from "@/hooks/useShare"; +import { + SHARE_TEXT, + SHARE_TITLE, +} from "@/lib/share"; + +interface Props { + referralLink: string; +} + +export default function ShareButtons({ + referralLink, +}: Props) { + const { share } = useShare(); + + return ( + + ); +} \ No newline at end of file diff --git a/src/components/referral/index.ts b/src/components/referral/index.ts new file mode 100644 index 00000000..47192cb6 --- /dev/null +++ b/src/components/referral/index.ts @@ -0,0 +1,4 @@ +export { default as ReferralShare } from "./ReferralShare"; +export { default as ShareButtons } from "./ShareButtons"; +export { default as CopyButton } from "./CopyButton"; +export { default as EmailTemplate } from "./EmailTemplate"; \ No newline at end of file diff --git a/src/hooks/useShare.ts b/src/hooks/useShare.ts new file mode 100644 index 00000000..5102bea9 --- /dev/null +++ b/src/hooks/useShare.ts @@ -0,0 +1,22 @@ +export interface ShareData { + title: string; + text: string; + url: string; +} + +export function useShare() { + const share = async ({ title, text, url }: ShareData) => { + if (navigator.share) { + await navigator.share({ + title, + text, + url, + }); + return; + } + + await navigator.clipboard.writeText(url); + }; + + return { share }; +} \ No newline at end of file diff --git a/src/lib/share.ts b/src/lib/share.ts new file mode 100644 index 00000000..ee61f8e0 --- /dev/null +++ b/src/lib/share.ts @@ -0,0 +1,6 @@ +export const SHARE_TITLE = "Join PropChain"; + +export const SHARE_TEXT = + "Join me on PropChain and start investing using my referral link."; + +export const SHARE_BUTTON_TEXT = "Share"; \ No newline at end of file diff --git a/tests/ReferralShare.test.tsx b/tests/ReferralShare.test.tsx new file mode 100644 index 00000000..18d7d79b --- /dev/null +++ b/tests/ReferralShare.test.tsx @@ -0,0 +1,9 @@ +describe("ReferralShare", () => { + it("renders share button", () => {}); + + it("renders copy button", () => {}); + + it("calls navigator.share when supported", () => {}); + + it("copies the link when Web Share API isn't available", () => {}); +}); \ No newline at end of file From 139ca8b676e0697a8be91419f2ec396e2767107b Mon Sep 17 00:00:00 2001 From: Elisha Suleiman <112385548+lishmanTech@users.noreply.github.com> Date: Tue, 30 Jun 2026 15:00:11 +0000 Subject: [PATCH 2/4] feat(ux): sync pagination state with shareable URL query parameters --- src/hooks/usePaginationUrl.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 src/hooks/usePaginationUrl.ts diff --git a/src/hooks/usePaginationUrl.ts b/src/hooks/usePaginationUrl.ts new file mode 100644 index 00000000..bf5a301d --- /dev/null +++ b/src/hooks/usePaginationUrl.ts @@ -0,0 +1,18 @@ +import { useSearchParams } from "react-router-dom"; + +export function usePaginationUrl() { + const [searchParams, setSearchParams] = useSearchParams(); + + const page = + Number(searchParams.get("page")) || 1; + + const setPage = (newPage: number) => { + searchParams.set("page", newPage.toString()); + setSearchParams(searchParams); + }; + + return { + page, + setPage, + }; +} \ No newline at end of file From c4f99187e31eed2883d465e7bd3e7be521f883ca Mon Sep 17 00:00:00 2001 From: Elisha Suleiman <112385548+lishmanTech@users.noreply.github.com> Date: Tue, 30 Jun 2026 15:04:57 +0000 Subject: [PATCH 3/4] feat(filters): group sidebar filters into collapsible sections --- src/components/filters/AdvancedFilters.tsx | 33 ++++++++++++++++++++++ src/components/filters/FilterSection.tsx | 33 ++++++++++++++++++++++ src/components/filters/FilterSidebar.tsx | 33 ++++++++++++++++++++++ src/components/filters/index.ts | 3 ++ 4 files changed, 102 insertions(+) create mode 100644 src/components/filters/AdvancedFilters.tsx create mode 100644 src/components/filters/FilterSection.tsx create mode 100644 src/components/filters/FilterSidebar.tsx create mode 100644 src/components/filters/index.ts diff --git a/src/components/filters/AdvancedFilters.tsx b/src/components/filters/AdvancedFilters.tsx new file mode 100644 index 00000000..f8b4fad2 --- /dev/null +++ b/src/components/filters/AdvancedFilters.tsx @@ -0,0 +1,33 @@ +interface Props { + values: any; + onChange: (name: string, value: any) => void; +} + +export default function AdvancedFilters({ + values, + onChange, +}: Props) { + return ( + <> + + + + + + + + + ); +} \ No newline at end of file diff --git a/src/components/filters/FilterSection.tsx b/src/components/filters/FilterSection.tsx new file mode 100644 index 00000000..9cd2b5d3 --- /dev/null +++ b/src/components/filters/FilterSection.tsx @@ -0,0 +1,33 @@ +import { useState } from "react"; + +interface FilterSectionProps { + title: string; + children: React.ReactNode; + defaultOpen?: boolean; +} + +export default function FilterSection({ + title, + children, + defaultOpen = true, +}: FilterSectionProps) { + const [isOpen, setIsOpen] = useState(defaultOpen); + + return ( +
+ + + {isOpen && ( +
+ {children} +
+ )} +
+ ); +} \ No newline at end of file diff --git a/src/components/filters/FilterSidebar.tsx b/src/components/filters/FilterSidebar.tsx new file mode 100644 index 00000000..1e8483e7 --- /dev/null +++ b/src/components/filters/FilterSidebar.tsx @@ -0,0 +1,33 @@ +import FilterSection from "./FilterSection"; +import AdvancedFilters from "./AdvancedFilters"; + +export default function FilterSidebar(props) { + return ( + <> + + + + + + + + + + + + + + + + + + + + + + + ); +} \ No newline at end of file diff --git a/src/components/filters/index.ts b/src/components/filters/index.ts new file mode 100644 index 00000000..513f2de1 --- /dev/null +++ b/src/components/filters/index.ts @@ -0,0 +1,3 @@ +export { default as FilterSidebar } from "./FilterSidebar"; +export { default as FilterSection } from "./FilterSection"; +export { default as AdvancedFilters } from "./AdvancedFilters"; \ No newline at end of file From 3b6c2435f07b5c3890d9035e88e6256de525a4a9 Mon Sep 17 00:00:00 2001 From: Elisha Suleiman <112385548+lishmanTech@users.noreply.github.com> Date: Tue, 30 Jun 2026 15:09:35 +0000 Subject: [PATCH 4/4] feat(ux): add actionable error toasts with retry and learn more options --- src/components/toast/ErrorToast.tsx | 35 +++++++++++++++++++++++++++++ src/lib/toast.ts | 24 ++++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 src/components/toast/ErrorToast.tsx create mode 100644 src/lib/toast.ts diff --git a/src/components/toast/ErrorToast.tsx b/src/components/toast/ErrorToast.tsx new file mode 100644 index 00000000..6455ef31 --- /dev/null +++ b/src/components/toast/ErrorToast.tsx @@ -0,0 +1,35 @@ +interface ErrorToastProps { + message: string; + onRetry?: () => void; + docsUrl?: string; +} + +export default function ErrorToast({ + message, + onRetry, + docsUrl, +}: ErrorToastProps) { + return ( +
+

{message}

+ +
+ {onRetry && ( + + )} + + {docsUrl && ( + + Learn More + + )} +
+
+ ); +} \ No newline at end of file diff --git a/src/lib/toast.ts b/src/lib/toast.ts new file mode 100644 index 00000000..bab7cdf7 --- /dev/null +++ b/src/lib/toast.ts @@ -0,0 +1,24 @@ +import { toast } from "sonner"; +// or react-hot-toast depending on the project + +import ErrorToast from "@/components/toast/ErrorToast"; + +interface ErrorToastOptions { + message: string; + onRetry?: () => void; + docsUrl?: string; +} + +export function showErrorToast({ + message, + onRetry, + docsUrl, +}: ErrorToastOptions) { + toast.custom(() => ( + + )); +} \ No newline at end of file