+
{children}
diff --git a/src/app/login/landlord/page.tsx b/src/app/login/landlord/page.tsx
index 0e9e626..325a0d1 100644
--- a/src/app/login/landlord/page.tsx
+++ b/src/app/login/landlord/page.tsx
@@ -5,6 +5,7 @@ import { useRouter } from 'next/navigation';
import Link from 'next/link';
import { createClient } from '@/utils/supabase/client';
import toast from 'react-hot-toast';
+import { track } from '@/utils/analytics';
export default function LoginLandlord() {
const [email, setEmail] = useState('');
@@ -36,7 +37,7 @@ export default function LoginLandlord() {
const loadingToast = toast.loading('Authenticating...');
try {
- const { data, error } = await supabase.auth.signInWithPassword({
+ const { error } = await supabase.auth.signInWithPassword({
email,
password,
});
@@ -53,6 +54,7 @@ export default function LoginLandlord() {
router.refresh();
+ track('login', { role: 'landlord' });
toast.success('Welcome back.', { id: loadingToast });
router.push('/dashboard');
diff --git a/src/app/login/tenant/page.tsx b/src/app/login/tenant/page.tsx
index 629a23f..bc884a5 100644
--- a/src/app/login/tenant/page.tsx
+++ b/src/app/login/tenant/page.tsx
@@ -5,6 +5,7 @@ import { useRouter } from 'next/navigation';
import Link from 'next/link';
import { createClient } from '@/utils/supabase/client';
import toast from 'react-hot-toast';
+import { track } from '@/utils/analytics';
export default function LoginTenant() {
const [email, setEmail] = useState('');
@@ -36,7 +37,7 @@ export default function LoginTenant() {
const loadingToast = toast.loading('Authenticating...');
try {
- const { data, error } = await supabase.auth.signInWithPassword({
+ const { error } = await supabase.auth.signInWithPassword({
email,
password,
});
@@ -53,6 +54,7 @@ export default function LoginTenant() {
router.refresh();
+ track('login', { role: 'tenant' });
toast.success('Welcome back.', { id: loadingToast });
router.push('/properties');
diff --git a/src/app/messages/page.tsx b/src/app/messages/page.tsx
index 571dcc8..bcf7991 100644
--- a/src/app/messages/page.tsx
+++ b/src/app/messages/page.tsx
@@ -93,11 +93,15 @@ function MessagesContent() {
}, [supabase, userId, activeContact]);
useEffect(() => {
- if (userId) fetchContacts();
+ if (!userId) return;
+ // Defer to a microtask so state updates happen asynchronously
+ // (avoids cascading renders from sync setState in effects)
+ void Promise.resolve().then(fetchContacts);
}, [userId, fetchContacts]);
useEffect(() => {
- if (userId && activeContact) fetchMessages();
+ if (!userId || !activeContact) return;
+ void Promise.resolve().then(fetchMessages);
}, [userId, activeContact, fetchMessages]);
// Real-time subscription
diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx
new file mode 100644
index 0000000..80ac96d
--- /dev/null
+++ b/src/app/not-found.tsx
@@ -0,0 +1,23 @@
+import Link from 'next/link';
+
+export default function NotFound() {
+ return (
+
+
+ Error 404
+
+
+ This page does not exist.
+
+
+ The listing may have been removed, or the address was mistyped.
+
+
+ Browse Properties
+
+
+ );
+}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index e802565..fe48a2c 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -22,7 +22,7 @@ export default function Home() {
Stop guessing.
- KNOW what it's worth.
+ KNOW what it's worth.
RentWise uses billions of data points to evaluate properties in Bangalore. Find underpriced luxury and verified inventory instantly.
diff --git a/src/app/register/landlord/page.tsx b/src/app/register/landlord/page.tsx
index 85a2892..1fddb9f 100644
--- a/src/app/register/landlord/page.tsx
+++ b/src/app/register/landlord/page.tsx
@@ -1,10 +1,11 @@
'use client'
-import React, { useState, useEffect } from 'react';
+import React, { useState } from 'react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import { createClient } from '@/utils/supabase/client';
import toast from 'react-hot-toast';
+import { track } from '@/utils/analytics';
export default function RegisterLandlord() {
const [email, setEmail] = useState('');
@@ -43,6 +44,7 @@ export default function RegisterLandlord() {
if (data?.user?.identities?.length === 0) {
toast.error('An account with this email already exists.', { id: loadingToast });
} else {
+ track('signup', { role: 'landlord' });
toast.success('Registration successful! Check your email to verify.', { id: loadingToast });
router.push('/login/landlord');
}
diff --git a/src/app/register/tenant/page.tsx b/src/app/register/tenant/page.tsx
index 0d48b47..005585b 100644
--- a/src/app/register/tenant/page.tsx
+++ b/src/app/register/tenant/page.tsx
@@ -1,10 +1,11 @@
'use client'
-import React, { useState, useEffect } from 'react';
+import React, { useState } from 'react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import { createClient } from '@/utils/supabase/client';
import toast from 'react-hot-toast';
+import { track } from '@/utils/analytics';
export default function RegisterTenant() {
const [email, setEmail] = useState('');
@@ -44,6 +45,7 @@ export default function RegisterTenant() {
if (data?.user?.identities?.length === 0) {
toast.error('An account with this email already exists.', { id: loadingToast });
} else {
+ track('signup', { role: 'tenant' });
toast.success('Registration successful! Check your email to verify.', { id: loadingToast });
router.push('/login/tenant');
}
diff --git a/src/app/robots.ts b/src/app/robots.ts
new file mode 100644
index 0000000..59bc756
--- /dev/null
+++ b/src/app/robots.ts
@@ -0,0 +1,14 @@
+import type { MetadataRoute } from 'next';
+
+const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'http://157.245.110.163:3009';
+
+export default function robots(): MetadataRoute.Robots {
+ return {
+ rules: {
+ userAgent: '*',
+ allow: '/',
+ disallow: ['/dashboard', '/messages', '/api/'],
+ },
+ sitemap: `${BASE_URL}/sitemap.xml`,
+ };
+}
diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts
new file mode 100644
index 0000000..188f7db
--- /dev/null
+++ b/src/app/sitemap.ts
@@ -0,0 +1,41 @@
+import type { MetadataRoute } from 'next';
+import { createClient } from '@supabase/supabase-js';
+
+const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'http://157.245.110.163:3009';
+
+export const revalidate = 3600;
+
+export default async function sitemap(): Promise {
+ const staticRoutes: MetadataRoute.Sitemap = [
+ { url: BASE_URL, changeFrequency: 'daily', priority: 1 },
+ { url: `${BASE_URL}/properties`, changeFrequency: 'hourly', priority: 0.9 },
+ { url: `${BASE_URL}/login`, changeFrequency: 'monthly', priority: 0.3 },
+ { url: `${BASE_URL}/register`, changeFrequency: 'monthly', priority: 0.3 },
+ ];
+
+ const url = process.env.NEXT_PUBLIC_SUPABASE_URL;
+ const key = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
+ if (!url || !key) return staticRoutes;
+
+ try {
+ // Cookie-less client: sitemap only needs public listing data and may
+ // run outside a request scope (e.g. at build time).
+ const supabase = createClient(url, key);
+ const { data } = await supabase
+ .from('properties')
+ .select('property_id, scraped_at')
+ .order('property_id', { ascending: false })
+ .limit(1000);
+
+ const propertyRoutes: MetadataRoute.Sitemap = (data || []).map((p) => ({
+ url: `${BASE_URL}/properties/${p.property_id}`,
+ lastModified: p.scraped_at ? new Date(p.scraped_at) : undefined,
+ changeFrequency: 'daily',
+ priority: 0.7,
+ }));
+
+ return [...staticRoutes, ...propertyRoutes];
+ } catch {
+ return staticRoutes;
+ }
+}
diff --git a/src/components/FilterBar.tsx b/src/components/FilterBar.tsx
index 76535cf..a8d6d34 100644
--- a/src/components/FilterBar.tsx
+++ b/src/components/FilterBar.tsx
@@ -52,7 +52,7 @@ export default function FilterBar() {
[name]: newValue
};
- setFilters(newFilters as any);
+ setFilters(newFilters as typeof filters);
// Update URL
const current = new URLSearchParams(Array.from(searchParams.entries()));
diff --git a/src/components/dashboard/LandlordApplications.tsx b/src/components/dashboard/LandlordApplications.tsx
index c141c57..5cd9372 100644
--- a/src/components/dashboard/LandlordApplications.tsx
+++ b/src/components/dashboard/LandlordApplications.tsx
@@ -1,6 +1,6 @@
'use client'
-import { useState, useEffect, useCallback } from 'react';
+import { useState, useEffect } from 'react';
import { createClient } from '@/utils/supabase/client';
import toast from 'react-hot-toast';
import { generateLeaseDocument } from '@/utils/generateLease';
@@ -25,26 +25,26 @@ export default function LandlordApplications({ landlord_id }: { landlord_id: str
const [applications, setApplications] = useState([]);
const [isLoading, setIsLoading] = useState(true);
- const fetchApplications = useCallback(async () => {
- setIsLoading(true);
+ useEffect(() => {
+ if (!landlord_id) return;
+ let cancelled = false;
// Supabase RLS policies already restrict Landlords to viewing only applications for their properties
- const { data, error } = await supabase
+ supabase
.from('applications')
.select(`
*,
properties ( address, rent )
`)
- .order('created_at', { ascending: false });
-
- if (!error && data) {
- setApplications(data as Application[]);
- }
- setIsLoading(false);
- }, [supabase]);
-
- useEffect(() => {
- if (landlord_id) fetchApplications();
- }, [landlord_id, fetchApplications]);
+ .order('created_at', { ascending: false })
+ .then(({ data, error }) => {
+ if (cancelled) return;
+ if (!error && data) {
+ setApplications(data as unknown as Application[]);
+ }
+ setIsLoading(false);
+ });
+ return () => { cancelled = true; };
+ }, [landlord_id, supabase]);
const handleUpdateStatus = async (appId: number, status: 'approved' | 'rejected') => {
const loadingToast = toast.loading('Executing status update...');
@@ -97,7 +97,7 @@ export default function LandlordApplications({ landlord_id }: { landlord_id: str
{app.properties?.address}
- "{app.intent_description}"
+ “{app.intent_description}”
Move In: {new Date(app.move_in_date).toLocaleDateString()}
diff --git a/src/components/dashboard/TenantApplications.tsx b/src/components/dashboard/TenantApplications.tsx
index 17153c6..bcf3913 100644
--- a/src/components/dashboard/TenantApplications.tsx
+++ b/src/components/dashboard/TenantApplications.tsx
@@ -1,6 +1,6 @@
'use client'
-import { useState, useEffect, useCallback } from 'react';
+import { useState, useEffect } from 'react';
import { createClient } from '@/utils/supabase/client';
import { generateLeaseDocument } from '@/utils/generateLease';
import PaymentModal from '@/components/property/PaymentModal';
@@ -25,27 +25,27 @@ export default function TenantApplications({ tenant_id }: { tenant_id: string })
const [isLoading, setIsLoading] = useState(true);
const [paymentModalApp, setPaymentModalApp] = useState
(null);
- const fetchApplications = useCallback(async () => {
- setIsLoading(true);
- const { data, error } = await supabase
+ useEffect(() => {
+ if (!tenant_id) return;
+ let cancelled = false;
+ supabase
.from('applications')
.select(`
*,
properties ( address, rent, landlord_id )
`)
.eq('tenant_id', tenant_id)
- .order('created_at', { ascending: false });
-
- if (!error && data) {
- setApplications(data as Application[]);
- }
- setIsLoading(false);
+ .order('created_at', { ascending: false })
+ .then(({ data, error }) => {
+ if (cancelled) return;
+ if (!error && data) {
+ setApplications(data as unknown as Application[]);
+ }
+ setIsLoading(false);
+ });
+ return () => { cancelled = true; };
}, [supabase, tenant_id]);
- useEffect(() => {
- if (tenant_id) fetchApplications();
- }, [tenant_id, fetchApplications]);
-
if (isLoading) {
return ;
}
diff --git a/src/components/property/MapboxCluster.tsx b/src/components/property/MapboxCluster.tsx
index 1536534..ce28bff 100644
--- a/src/components/property/MapboxCluster.tsx
+++ b/src/components/property/MapboxCluster.tsx
@@ -29,11 +29,13 @@ export default function MapboxCluster({ properties }: Props) {
// Map properties to scatter them slightly if they are in the same area
const mapData = useMemo(() => {
- return properties.map(p => {
+ return properties.map((p, idx) => {
const baseCoords = areaCoordinates[p.area_name] || [77.5946, 12.9716]; // Default to Bangalore center
- // Add a tiny random offset to prevent perfect stacking
- const offsetLon = (Math.random() - 0.5) * 0.01;
- const offsetLat = (Math.random() - 0.5) * 0.01;
+ // Deterministic per-property offset to prevent perfect stacking
+ // (stable across renders, unlike Math.random)
+ const seed = (p.property_id ?? idx) * 2654435761 % 1000;
+ const offsetLon = ((seed % 100) / 100 - 0.5) * 0.01;
+ const offsetLat = ((Math.floor(seed / 100) % 100) / 100 - 0.5) * 0.01;
return {
...p,
@@ -79,7 +81,7 @@ export default function MapboxCluster({ properties }: Props) {
longitude={p.longitude}
latitude={p.latitude}
anchor="bottom"
- onClick={(e: any) => {
+ onClick={(e: { originalEvent: MouseEvent }) => {
e.originalEvent.stopPropagation();
setSelectedProperty(p);
}}
diff --git a/src/components/property/PropertyReviews.tsx b/src/components/property/PropertyReviews.tsx
index 5f196d3..ed62369 100644
--- a/src/components/property/PropertyReviews.tsx
+++ b/src/components/property/PropertyReviews.tsx
@@ -76,7 +76,7 @@ export default function PropertyReviews({ propertyId }: Props) {
{review.date}
- "{review.text}"
+ “{review.text}”
{[...Array(5)].map((_, i) => (
diff --git a/src/components/property/TourScheduler.tsx b/src/components/property/TourScheduler.tsx
index 60a83a7..9e6f49c 100644
--- a/src/components/property/TourScheduler.tsx
+++ b/src/components/property/TourScheduler.tsx
@@ -42,7 +42,7 @@ export default function TourScheduler({ propertyId }: Props) {
Tour Confirmed
- {new Date(selectedDate).toLocaleDateString()} at {selectedTime}
+ REF RW-{propertyId} · {new Date(selectedDate).toLocaleDateString()} at {selectedTime}