Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 10 additions & 1 deletion src/components/MortgageCalculator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,20 @@ export const MortgageCalculator: React.FC<MortgageCalculatorProps> = ({
years: yearsLabel,
});

let currentUrl = '';
try {
if (typeof window !== 'undefined') {
currentUrl = new URL(window.location.href).href;
}
} catch {
currentUrl = '';
}

if (navigator.share) {
navigator.share({
title: t('mortgageCalculator.shareTitle'),
text,
url: window.location.href,
url: currentUrl,
}).catch((err) => logger.error('Mortgage calculation error:', err));
} else {
navigator.clipboard.writeText(text);
Expand Down
27 changes: 14 additions & 13 deletions src/components/PropertyCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,27 +36,23 @@ export const PropertyCard: React.FC<PropertyCardProps> = ({
const { addFavorite, removeFavorite, isFavorite } = useFavoritesStore();

const handleAddToCart = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
addItem(property, 1);
};

const handleComparisonToggle = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
toggleProperty(property);
};

const handleCompareToggle = (e: React.MouseEvent<HTMLInputElement>) => {
e.preventDefault();
e.stopPropagation();
if (!compareLimitReached) {
togglePropertyId(property.id);
}
};

const handleToggleFavorite = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
if (isFavorite(property.id)) {
removeFavorite(property.id);
Expand All @@ -66,12 +62,10 @@ export const PropertyCard: React.FC<PropertyCardProps> = ({
};

return (
<Link
href={`/properties/${property.id}`}
className={`group bg-white dark:bg-gray-800 rounded-xl shadow-md hover:shadow-xl transition-all duration-300 overflow-hidden ${
<article
className={`bg-white dark:bg-gray-800 rounded-xl shadow-md hover:shadow-xl transition-all duration-300 overflow-hidden ${
isListView ? 'flex flex-row' : 'flex flex-col'
}`}
aria-label={`View details for ${property.name}`}
>
{/* Image */}
<div className={`relative overflow-hidden ${isListView ? 'w-64 flex-shrink-0' : 'w-full h-56'}`}>
Expand Down Expand Up @@ -188,9 +182,12 @@ export const PropertyCard: React.FC<PropertyCardProps> = ({
</div>

{/* Title */}
<h3 className="text-base sm:text-lg font-bold text-gray-900 dark:text-white mb-1 sm:mb-2 group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors line-clamp-2">
<Link
href={`/properties/${property.id}`}
className="text-base sm:text-lg font-bold text-gray-900 dark:text-white mb-1 sm:mb-2 hover:text-blue-600 dark:hover:text-blue-400 transition-colors line-clamp-2"
>
{property.name}
</h3>
</Link>

{/* Location */}
<div className="flex items-start gap-1 text-xs sm:text-sm text-gray-600 dark:text-gray-400 mb-2 sm:mb-3">
Expand Down Expand Up @@ -280,12 +277,16 @@ export const PropertyCard: React.FC<PropertyCardProps> = ({
<Plus className="w-2 h-2 sm:w-3 sm:h-3" aria-hidden="true" />
<span className="hidden sm:inline">Add to Cart</span>
</button>
<button className="px-2 sm:px-4 py-1.5 sm:py-2 bg-blue-600 hover:bg-blue-700 text-white text-xs sm:text-sm font-medium rounded-lg transition-colors flex-shrink-0 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2" aria-label={`View details for ${property.name}`}>
<Link
href={`/properties/${property.id}`}
className="px-2 sm:px-4 py-1.5 sm:py-2 bg-blue-600 hover:bg-blue-700 text-white text-xs sm:text-sm font-medium rounded-lg transition-colors flex-shrink-0 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 inline-flex items-center justify-center"
aria-label={`View details for ${property.name}`}
>
View
</button>
</Link>
</div>
</div>
</div>
</Link>
</article>
);
};
6 changes: 4 additions & 2 deletions src/components/ViewToggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,16 +95,18 @@ export function ViewToggle({ mode, onChange }: ViewToggleProps) {
onClick={() => safeChange("grid")}
className={`px-3 py-1.5 flex items-center gap-1 ${mode === "grid" ? "bg-indigo-600 text-white" : "text-gray-600 hover:bg-gray-100"}`}
aria-pressed={mode === "grid"}
aria-label="Grid view"
>
<GridIcon /> Grid
<GridIcon /> <span>Grid</span>
</button>
<button
type="button"
onClick={() => safeChange("list")}
className={`px-3 py-1.5 flex items-center gap-1 ${mode === "list" ? "bg-indigo-600 text-white" : "text-gray-600 hover:bg-gray-100"}`}
aria-pressed={mode === "list"}
aria-label="List view"
>
<ListIcon /> List
<ListIcon /> <span>List</span>
</button>
</div>
);
Expand Down
8 changes: 5 additions & 3 deletions src/components/__tests__/PropertyCard.a11y.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,12 @@ describe('PropertyCard Accessibility', () => {
expect(results).toHaveNoViolations();
});

it('should have accessible property link with proper aria-label', () => {
it('should have accessible property links with proper aria-labels', () => {
render(<PropertyCard property={mockProperty} />);
const link = screen.getByRole('link');
expect(link).toHaveAttribute('aria-label', 'View details for Sunset Villa');
const links = screen.getAllByRole('link');
expect(links.length).toBeGreaterThanOrEqual(2);
const viewLink = screen.getByLabelText('View details for Sunset Villa');
expect(viewLink).toBeInTheDocument();
});

it('should have accessible image with descriptive alt text', () => {
Expand Down
22 changes: 13 additions & 9 deletions src/components/property/ShareButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ import {
DialogTrigger,
} from '@/components/ui/dialog';
import { toast } from 'sonner';
import {
buildPropertyShareUrl,
buildShareText,
buildTwitterShareUrl,
buildLinkedInShareUrl,
buildEmailShareUrl,
} from '@/utils/security/shareUrl';

interface ShareButtonProps {
property: {
Expand Down Expand Up @@ -51,13 +58,11 @@ export const ShareButton: React.FC<ShareButtonProps> = ({
const [copied, setCopied] = useState(false);
const [isDialogOpen, setIsDialogOpen] = useState(false);

const propertyUrl = `${typeof window !== 'undefined' ? window.location.origin : ''}/properties/${property.id}`;

const shareText = `Check out this property: ${property.name} in ${property.location.city}, ${property.location.state}. ${property.metrics.roi}% ROI - ${property.price.total} ETH total value.`;

const twitterUrl = `https://twitter.com/intent/tweet?text=${encodeURIComponent(shareText)}&url=${encodeURIComponent(propertyUrl)}`;
const propertyUrl = buildPropertyShareUrl(property);
const shareText = buildShareText(property);

const linkedinUrl = `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(propertyUrl)}`;
const twitterUrl = propertyUrl ? buildTwitterShareUrl(propertyUrl, shareText) : '';
const linkedinUrl = propertyUrl ? buildLinkedInShareUrl(propertyUrl) : '';

const handleNativeShare = async () => {
if (navigator.share) {
Expand Down Expand Up @@ -106,9 +111,8 @@ export const ShareButton: React.FC<ShareButtonProps> = ({
};

const handleEmailShare = () => {
const subject = encodeURIComponent(`Check out this property: ${property.name}`);
const body = encodeURIComponent(`I found this interesting property and thought you might like it:\n\n${shareText}\n\nView it here: ${propertyUrl}`);
window.open(`mailto:?subject=${subject}&body=${body}`);
const mailtoUrl = buildEmailShareUrl(property.name, shareText, propertyUrl);
window.open(mailtoUrl);
toast.success('Opening email client...');
};

Expand Down
34 changes: 34 additions & 0 deletions src/hooks/usePerformanceMonitoring.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
'use client';
import { useEffect, useRef } from 'react';
import { setupPerformanceMonitoring, type PerformanceMetrics } from '@/lib/mobile-optimizer';

let globalCleanup: (() => void) | null = null;

export function usePerformanceMonitoring(callback: (metrics: PerformanceMetrics) => void) {
const callbackRef = useRef(callback);
callbackRef.current = callback;

useEffect(() => {
if (globalCleanup) {
globalCleanup();
}

globalCleanup = setupPerformanceMonitoring((metrics) => {
callbackRef.current(metrics);
});

return () => {
if (globalCleanup) {
globalCleanup();
globalCleanup = null;
}
};
}, []);
}

export function resetPerformanceMonitoring() {
if (globalCleanup) {
globalCleanup();
globalCleanup = null;
}
}
31 changes: 28 additions & 3 deletions src/utils/earlyErrorSuppression.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
const stringifyArgs = (args: readonly unknown[]): string =>
args.map((arg) => (typeof arg === 'string' ? arg : String(arg))).join(' ');

const earlySuppressionCleanups: Array<() => void> = [];

// This file should be imported as early as possible in the application
// It immediately starts suppressing extension errors

Expand Down Expand Up @@ -31,21 +33,34 @@ if (typeof window !== 'undefined') {
if (shouldSuppress(...args)) return;
originalConsoleWarn.apply(console, args);
};

earlySuppressionCleanups.push(() => {
console.error = originalConsoleError;
console.warn = originalConsoleWarn;
});

// Suppress global errors
window.addEventListener('error', (event) => {
const handleError = (event: ErrorEvent) => {
if (shouldSuppress(event.error, event.filename, event.message)) {
event.preventDefault();
event.stopPropagation();
}
}, true);
};
window.addEventListener('error', handleError, true);
earlySuppressionCleanups.push(() => {
window.removeEventListener('error', handleError, true);
});

// Suppress unhandled promise rejections
window.addEventListener('unhandledrejection', (event) => {
const handleRejection = (event: PromiseRejectionEvent) => {
if (shouldSuppress(event.reason)) {
event.preventDefault();
event.stopPropagation();
}
};
window.addEventListener('unhandledrejection', handleRejection);
earlySuppressionCleanups.push(() => {
window.removeEventListener('unhandledrejection', handleRejection);
});

// Override window.onerror
Expand All @@ -59,9 +74,19 @@ if (typeof window !== 'undefined') {
}
return false;
};
earlySuppressionCleanups.push(() => {
window.onerror = originalOnError;
});
}

export const initializeEarlyErrorSuppression = () => {
// This function can be called to ensure suppression is active
// Note: Early error suppression is already active from module load
};

export const cleanupEarlyErrorSuppression = () => {
let fn: (() => void) | undefined;
while ((fn = earlySuppressionCleanups.pop()) !== undefined) {
fn();
}
};
30 changes: 27 additions & 3 deletions src/utils/extensionDetection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,12 @@ export const sanitizeExtensionError = (error: unknown): string => {
return 'Wallet extension error. Please check your extension settings and try again.';
};

const extensionErrorListeners: Array<() => void> = [];

export const setupExtensionErrorHandling = () => {
if (typeof window === 'undefined') return;

cleanupExtensionErrorHandling();

// Override console.error to filter out extension errors
const originalConsoleError = console.error;
Expand All @@ -92,26 +96,46 @@ export const setupExtensionErrorHandling = () => {

// Filter out known extension errors that don't affect functionality
if (errorString.includes('chrome-extension://') ||
errorString.includes('evmask.js') ||
errorString.includes('evmask.js')) {
return; // Silently ignore these errors
}

originalConsoleError.apply(console, args);
};

extensionErrorListeners.push(() => {
console.error = originalConsoleError;
});

// Add global error handler for unhandled extension errors
window.addEventListener('error', (event) => {
const handleError = (event: ErrorEvent) => {
if (isExtensionError(event.error)) {
event.preventDefault();
logger.warn('Extension error filtered:', sanitizeExtensionError(event.error));
}
};

window.addEventListener('error', handleError);
extensionErrorListeners.push(() => {
window.removeEventListener('error', handleError);
});

window.addEventListener('unhandledrejection', (event) => {
const handleRejection = (event: PromiseRejectionEvent) => {
if (isExtensionError(event.reason)) {
event.preventDefault();
logger.warn('Extension promise rejection filtered:', sanitizeExtensionError(event.reason));
}
};

window.addEventListener('unhandledrejection', handleRejection);
extensionErrorListeners.push(() => {
window.removeEventListener('unhandledrejection', handleRejection);
});
};

export const cleanupExtensionErrorHandling = () => {
let fn: (() => void) | undefined;
while ((fn = extensionErrorListeners.pop()) !== undefined) {
fn();
}
};
Loading