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 (
+
+ );
+}
\ 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