Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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
283 changes: 69 additions & 214 deletions apps/web/src/app/invitations/page.tsx

Large diffs are not rendered by default.

23 changes: 16 additions & 7 deletions apps/web/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import './globals.css';
import type { Metadata } from 'next';
import { Geist } from 'next/font/google';
import './globals.css';

import { Toaster } from 'react-hot-toast';
import { Providers } from '~/components/shared/layout/providers';
import { Navbar } from '../components/layout/Navbar';
import { RightSidebar } from '../components/layout/RightSidebar';

const geist = Geist({ subsets: ['latin'] });
const geist = Geist({
subsets: ['latin'],
});

export const metadata: Metadata = {
title: 'StellaRent',
description: 'Plataforma de alquiler de propiedades',
title: 'Stellar Rent',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

variables in english

description: 'USDC rentals on the Stellar Network',
};

export default function RootLayout({
Expand All @@ -17,7 +22,7 @@ export default function RootLayout({
children: React.ReactNode;
}) {
return (
<html lang="es" suppressHydrationWarning>
<html lang="en" suppressHydrationWarning>
<head>
<meta name="color-scheme" content="light dark" />
<meta
Expand All @@ -26,10 +31,14 @@ export default function RootLayout({
/>
<script src="https://accounts.google.com/gsi/client" async defer />
</head>
<body className={`${geist.className} min-h-screen bg-[#0B1320] text-white antialiased`}>
<body className={`${geist.className} bg-[#0B1221] min-h-screen antialiased text-white`}>
<div id="theme-portal-root" />
<Providers>
<main className="flex-1 flex flex-col">{children}</main>
<Navbar />
<div className="flex flex-1 overflow-hidden pt-14">
<main className="flex-1 flex flex-col min-w-0">{children}</main>
<RightSidebar />
</div>
<Toaster position="top-right" />
</Providers>
</body>
Expand Down
38 changes: 38 additions & 0 deletions apps/web/src/app/messages/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
'use client';

import { Search } from 'lucide-react';
import { useState } from 'react';

export default function MessagesPage() {
const [searchQuery, setSearchQuery] = useState('');

return (
<div className="flex h-[calc(100vh-56px)] w-full bg-[#0B1221] text-white overflow-hidden font-sans">
<aside className="w-80 border-r border-gray-800 flex flex-col bg-[#0F172A]">
<div className="p-4 border-b border-gray-800">
<div className="relative">
<Search className="absolute left-4 top-2.5 h-4 w-4 text-gray-500" aria-hidden="true" />
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search chat"
aria-label="Search chats"
className="w-full bg-[#161F2F] border-none rounded-full py-2 pl-12 pr-4 text-sm focus:ring-1 focus:ring-blue-500 outline-none placeholder:text-gray-500"
/>
</div>
</div>

<div className="flex-1 flex flex-col items-center pt-8">
<span className="text-gray-500 text-sm font-light italic">No chats available</span>
</div>
</aside>

<main className="flex-1 flex items-center justify-center bg-[#0B1221]">
<span className="text-gray-500 text-sm font-light tracking-[0.2em] uppercase opacity-60">
Select a chat
</span>
</main>
</div>
);
}
50 changes: 25 additions & 25 deletions apps/web/src/app/search/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ const PropertyMap = dynamic(() => import('@/components/search/Map'), {

export default function SearchPage() {
const [page, setPage] = useState(1);
const pageSize = 3;
const [sort, setSort] = useState('price_asc');
const PAGE_SIZE = 3;
const [sortOrder, setSortOrder] = useState('price_asc');
const [isLoading, setIsLoading] = useState(false);
const [filters, setFilters] = useState({
price: 0,
Expand All @@ -26,25 +26,24 @@ export default function SearchPage() {
});
const searchParams = useSearchParams();

const center: LatLngTuple = [-34.61, -58.39];
const markers: { position: LatLngTuple; title: string }[] = [
const mapCenter: LatLngTuple = [-34.61, -58.39];
const mapMarkers: { position: LatLngTuple; title: string }[] = [
{ position: [-34.61, -58.39], title: 'Modern Apartment with Kitchen' },
{ position: [-34.6, -58.37], title: 'Cozy Studio Apartment' },
];

// Filter & sort properties with memoization
const filteredSortedProperties = useMemo(() => {
let result = [...MOCK_PROPERTIES];

const location = searchParams.get('location')?.toLowerCase() || '';
if (location) {
result = result.filter((p) => p.location.toLowerCase().includes(location));
const locationQuery = searchParams.get('location')?.toLowerCase() || '';
if (locationQuery) {
result = result.filter((p) => p.location.toLowerCase().includes(locationQuery));
}

result = result.filter((p) => p.price >= filters.price);

const selectedAmenities = Object.entries(filters.amenities)
.filter(([, checked]) => checked)
.filter(([, isChecked]) => isChecked)
.map(([key]) => key.toLowerCase());

if (selectedAmenities.length > 0) {
Expand All @@ -57,34 +56,35 @@ export default function SearchPage() {
result = result.filter((p) => p.rating >= filters.rating);
}

if (sort === 'price_asc') result.sort((a, b) => a.price - b.price);
if (sort === 'price_desc') result.sort((a, b) => b.price - a.price);
if (sort === 'rating') result.sort((a, b) => b.rating - a.rating);
if (sort === 'distance') {
if (sortOrder === 'price_asc') result.sort((a, b) => a.price - b.price);
if (sortOrder === 'price_desc') result.sort((a, b) => b.price - a.price);
if (sortOrder === 'rating') result.sort((a, b) => b.rating - a.rating);
if (sortOrder === 'distance') {
result.sort((a, b) => {
const aDist = Number.parseFloat(a.distance);
const bDist = Number.parseFloat(b.distance);
return aDist - bDist;
const distA = Number.parseFloat(a.distance);
const distB = Number.parseFloat(b.distance);
return distA - distB;
});
}

return result;
}, [filters, sort, searchParams]);
}, [filters, sortOrder, searchParams]);

const visibleProperties = useMemo(() => {
return filteredSortedProperties.slice(0, page * pageSize);
return filteredSortedProperties.slice(0, page * PAGE_SIZE);
}, [filteredSortedProperties, page]);

const loadNextPage = useCallback(() => {
if (isLoading) return;
setIsLoading(true);
// Simulate async loading
setTimeout(() => {
setPage((prev) => prev + 1);
setIsLoading(false);
}, 200); // simulate load
}, 200);
}, [isLoading]);

const minMax = useMemo(() => {
const priceRange = useMemo(() => {
const sorted = [...MOCK_PROPERTIES].sort((a, b) => a.price - b.price);
return [sorted[0]?.price || 0, sorted.at(-1)?.price || 0] as [number, number];
}, []);
Expand All @@ -95,17 +95,17 @@ export default function SearchPage() {
<div className="w-full lg:w-72">
<FilterSidebar
filters={filters}
minAndMaxPrice={minMax}
minAndMaxPrice={priceRange}
onFiltersChange={setFilters}
center={center}
markers={markers}
center={mapCenter}
markers={mapMarkers}
/>
</div>

<div className="flex-1">
<div className="flex flex-col md:flex-row justify-between gap-4 items-center border md:mt-5 p-1 md:pr-4">
<SearchBar />
<SortOptions onSortChange={setSort} />
<SortOptions onSortChange={setSortOrder} />
</div>

<div className="flex flex-col lg:flex-row">
Expand All @@ -115,7 +115,7 @@ export default function SearchPage() {
</div>

<div className="w-full lg:w-[40%] h-[300px] lg:h-[70vh] mt-4 lg:mt-12 rounded-2xl border m-0 lg:m-6 block">
<PropertyMap center={center} markers={markers} />
<PropertyMap center={mapCenter} markers={mapMarkers} />
</div>
</div>
</div>
Expand Down
58 changes: 19 additions & 39 deletions apps/web/src/app/search/property/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
'use client';

import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
import { Calendar, Home, MapPin, Star, Users, Wallet } from 'lucide-react';
Expand All @@ -13,7 +14,7 @@ export default function PropertyDetailPage() {
const propertyId = searchParams.get('propertyId');
const [property, setProperty] = useState<FullPropertyProps | null>(null);

const [imageError, _setImageError] = useState(false);
const [imageError, setImageError] = useState(false);
const [bookingData, setBookingData] = useState({
checkIn: '',
checkOut: '',
Expand All @@ -26,18 +27,17 @@ export default function PropertyDetailPage() {
const checkInDate = new Date(checkIn);
const checkOutDate = new Date(checkOut);

// Validate dates
if (checkInDate >= checkOutDate) return 0;

const timeDiff = checkOutDate.getTime() - checkInDate.getTime();
return Math.ceil(timeDiff / (1000 * 60 * 60 * 24));
};

const nights = calculateNights(bookingData.checkIn, bookingData.checkOut);
const subtotal = property && property.price * nights;
const subtotal = property ? property.price * nights : 0;
const cleaningFee = 150;
const serviceFee = 100;
const total = subtotal && subtotal + cleaningFee + serviceFee;
const total = subtotal + cleaningFee + serviceFee;

useEffect(() => {
if (propertyId) {
Expand All @@ -59,13 +59,12 @@ export default function PropertyDetailPage() {
<button
type="button"
onClick={() => window.history.back()}
className=" text-base underline text-blue-800 dark:text-blue-400 "
className="text-base underline text-blue-800 dark:text-blue-400"
>
← Back to Search
</button>

<div className="grid grid-cols-1 lg:grid-cols-3 gap-8 mt-4">
{/* Property Images */}
<div className="lg:col-span-2">
<div className="relative h-[400px] w-full rounded-lg overflow-hidden">
{!imageError ? (
Expand All @@ -83,6 +82,7 @@ export default function PropertyDetailPage() {
src={img}
alt={`${property.title} ${index + 1}`}
className="w-full h-full object-cover"
onError={() => setImageError(true)}
/>
</div>
))}
Expand Down Expand Up @@ -124,15 +124,12 @@ export default function PropertyDetailPage() {
<div className="mb-8">
<h2 className="text-xl font-semibold mb-4">About this property</h2>
<p className="text-muted-foreground mb-4">
This beautiful property offers a perfect blend of comfort and luxury. Located in the
heart of {property.location}, it provides easy access to local attractions,
restaurants, and transportation.
This property is located in {property.location}, providing access to local
attractions.
</p>
<p className="text-muted-foreground">
The property features modern amenities, including high-speed WiFi, a fully equipped
kitchen, and comfortable sleeping arrangements. Perfect for both short and long-term
stays, this rental property accepts cryptocurrency payments for a seamless booking
experience.
Modern amenities include high-speed WiFi and fully equipped kitchen for both short and
long-term stays.
</p>
</div>

Expand All @@ -149,7 +146,6 @@ export default function PropertyDetailPage() {
</div>
</div>

{/* Booking Card */}
<div className="lg:col-span-1">
<Card className="p-6 sticky top-6">
<div className="flex items-center justify-between mb-6">
Expand All @@ -163,19 +159,13 @@ export default function PropertyDetailPage() {
Check-in
</label>
<div className="flex items-center border rounded-md p-2 bg-background">
<Calendar className="h-5 w-5 text-muted-foreground " />
<Calendar className="h-5 w-5 text-muted-foreground" />
<input
id="check-in"
type="date"
className="border-0 p-0 focus:outline-none w-full bg-transparent"
placeholder="Add date"
value={bookingData.checkIn}
onChange={(e) =>
setBookingData({
...bookingData,
checkIn: e.target.value,
})
}
onChange={(e) => setBookingData({ ...bookingData, checkIn: e.target.value })}
/>
</div>
</div>
Expand All @@ -190,15 +180,9 @@ export default function PropertyDetailPage() {
id="check-out"
type="date"
className="border-0 p-0 focus:outline-none w-full bg-transparent"
placeholder="Add date"
value={bookingData.checkOut}
onChange={(e) =>
setBookingData({
...bookingData,
checkOut: e.target.value,
})
}
min={bookingData.checkIn} // Prevent selecting checkout before checkin
onChange={(e) => setBookingData({ ...bookingData, checkOut: e.target.value })}
min={bookingData.checkIn}
/>
</div>
</div>
Expand All @@ -215,10 +199,7 @@ export default function PropertyDetailPage() {
className="border-0 p-0 focus:outline-none w-full bg-transparent"
value={bookingData.guests}
onChange={(e) =>
setBookingData({
...bookingData,
guests: Number(e.target.value),
})
setBookingData({ ...bookingData, guests: Number(e.target.value) })
}
>
{[...Array(property.maxGuests)].map((_, i) => (
Expand All @@ -233,9 +214,9 @@ export default function PropertyDetailPage() {
<div className="mb-6">
<div className="flex justify-between mb-2">
<span>
${property.price} Γ— {nights || 0} nights
${property.price} Γ— {nights} nights
</span>
<span>${subtotal || 0}</span>
<span>${subtotal}</span>
</div>
<div className="flex justify-between mb-2">
<span>Cleaning fee</span>
Expand All @@ -247,7 +228,7 @@ export default function PropertyDetailPage() {
</div>
<div className="border-t pt-2 mt-2 flex justify-between font-bold">
<span>Total (USDC)</span>
<span>${total || cleaningFee + serviceFee}</span>
<span>${total}</span>
</div>
</div>

Expand All @@ -256,8 +237,7 @@ export default function PropertyDetailPage() {
</Button>

<p className="text-xs text-center text-muted-foreground mt-4">
You won&apos;t be charged yet. Payment will be processed through our secure crypto
payment gateway.
Payment will be processed through our secure payment gateway.
</p>
</Card>
</div>
Expand Down
Loading