♑🛠️
- add show price preference(store in cookies)
This commit is contained in:
@@ -204,7 +204,9 @@ def _clear_auth_cookies(response: RedirectResponse) -> RedirectResponse:
|
|||||||
|
|
||||||
@router.get("/login", response_class=HTMLResponse)
|
@router.get("/login", response_class=HTMLResponse)
|
||||||
async def login_page(request: Request):
|
async def login_page(request: Request):
|
||||||
return templates.TemplateResponse(request, "auth/login.html", {})
|
return templates.TemplateResponse(request, "auth/login.html", {
|
||||||
|
"price_pref": request.state.price_pref,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
@router.post("/login")
|
@router.post("/login")
|
||||||
@@ -216,6 +218,7 @@ async def login_submit(request: Request, db: db_dependency):
|
|||||||
if not user:
|
if not user:
|
||||||
return templates.TemplateResponse(request, "auth/login.html", {
|
return templates.TemplateResponse(request, "auth/login.html", {
|
||||||
"error": "Invalid username or password",
|
"error": "Invalid username or password",
|
||||||
|
"price_pref": request.state.price_pref,
|
||||||
})
|
})
|
||||||
access = create_access_token(user.username, user.id)
|
access = create_access_token(user.username, user.id)
|
||||||
refresh = create_refresh_token(user.username, user.id, db)
|
refresh = create_refresh_token(user.username, user.id, db)
|
||||||
@@ -226,7 +229,9 @@ async def login_submit(request: Request, db: db_dependency):
|
|||||||
|
|
||||||
@router.get("/register", response_class=HTMLResponse)
|
@router.get("/register", response_class=HTMLResponse)
|
||||||
async def register_page(request: Request):
|
async def register_page(request: Request):
|
||||||
return templates.TemplateResponse(request, "auth/register.html", {})
|
return templates.TemplateResponse(request, "auth/register.html", {
|
||||||
|
"price_pref": request.state.price_pref,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
@router.post("/register")
|
@router.post("/register")
|
||||||
@@ -258,6 +263,7 @@ async def register_submit(request: Request, db: db_dependency):
|
|||||||
"username": username,
|
"username": username,
|
||||||
"email": email,
|
"email": email,
|
||||||
"full_name": full_name,
|
"full_name": full_name,
|
||||||
|
"price_pref": request.state.price_pref,
|
||||||
})
|
})
|
||||||
|
|
||||||
db.add(User(
|
db.add(User(
|
||||||
|
|||||||
@@ -14,6 +14,16 @@ _loader = jinja2.FileSystemLoader("templates")
|
|||||||
_env = jinja2.Environment(loader=_loader, autoescape=jinja2.select_autoescape(), cache_size=0)
|
_env = jinja2.Environment(loader=_loader, autoescape=jinja2.select_autoescape(), cache_size=0)
|
||||||
templates = Jinja2Templates(env=_env)
|
templates = Jinja2Templates(env=_env)
|
||||||
|
|
||||||
|
# Custom Jinja2 filter: {{ prop.price|price_fmt(price_pref, prop.area_sqft) }}
|
||||||
|
from price_pref import format_price as _format_price
|
||||||
|
|
||||||
|
|
||||||
|
def _price_fmt(value, price_pref="total", area_sqft=0):
|
||||||
|
return _format_price(value, price_pref, area_sqft)
|
||||||
|
|
||||||
|
|
||||||
|
_env.filters["price_fmt"] = _price_fmt
|
||||||
|
|
||||||
bcrypt_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
bcrypt_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||||
|
|
||||||
oauth2_bearer = OAuth2PasswordBearer(tokenUrl="auth/token", auto_error=False)
|
oauth2_bearer = OAuth2PasswordBearer(tokenUrl="auth/token", auto_error=False)
|
||||||
|
|||||||
@@ -35,6 +35,15 @@ class RefreshTokenMiddleware(BaseHTTPMiddleware):
|
|||||||
app.add_middleware(RefreshTokenMiddleware)
|
app.add_middleware(RefreshTokenMiddleware)
|
||||||
|
|
||||||
|
|
||||||
|
class PricePrefMiddleware(BaseHTTPMiddleware):
|
||||||
|
async def dispatch(self, request: Request, call_next):
|
||||||
|
from price_pref import get_price_pref
|
||||||
|
request.state.price_pref = get_price_pref(request)
|
||||||
|
return await call_next(request)
|
||||||
|
|
||||||
|
app.add_middleware(PricePrefMiddleware)
|
||||||
|
|
||||||
|
|
||||||
db_dependency = Annotated[Session, Depends(get_db)]
|
db_dependency = Annotated[Session, Depends(get_db)]
|
||||||
user_dependency = Annotated[Optional[dict], Depends(get_current_user)]
|
user_dependency = Annotated[Optional[dict], Depends(get_current_user)]
|
||||||
|
|
||||||
@@ -64,4 +73,5 @@ async def homepage(request: Request, db: db_dependency, user: user_dependency):
|
|||||||
return templates.TemplateResponse(request, "index.html", {
|
return templates.TemplateResponse(request, "index.html", {
|
||||||
"user": user,
|
"user": user,
|
||||||
"properties": featured,
|
"properties": featured,
|
||||||
|
"price_pref": request.state.price_pref,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
VALID_PRICE_PREFS = ("total", "sqft", "both")
|
||||||
|
|
||||||
|
|
||||||
|
def get_price_pref(request) -> str:
|
||||||
|
pref = request.cookies.get("price_pref", "total")
|
||||||
|
if pref not in VALID_PRICE_PREFS:
|
||||||
|
pref = "total"
|
||||||
|
return pref
|
||||||
|
|
||||||
|
|
||||||
|
def format_price(value: int, price_pref: str = "total", area_sqft: int = 0) -> str:
|
||||||
|
|
||||||
|
if area_sqft > 0:
|
||||||
|
unit_price = round(value / area_sqft)
|
||||||
|
if price_pref == "sqft":
|
||||||
|
return f"${unit_price:,}/sqft"
|
||||||
|
if price_pref == "both":
|
||||||
|
return f"${value:,} (${unit_price:,}/sqft)"
|
||||||
|
return f"${value:,}"
|
||||||
+16
-2
@@ -52,9 +52,17 @@ async def property_list(request: Request, db: Session = Depends(get_db)):
|
|||||||
if prop_type:
|
if prop_type:
|
||||||
query = query.filter(Property.property_type == prop_type)
|
query = query.filter(Property.property_type == prop_type)
|
||||||
if min_price:
|
if min_price:
|
||||||
query = query.filter(Property.price >= int(min_price))
|
min_val = int(min_price)
|
||||||
|
if request.state.price_pref == "sqft":
|
||||||
|
query = query.filter(Property.price >= min_val * Property.area_sqft)
|
||||||
|
else:
|
||||||
|
query = query.filter(Property.price >= min_val)
|
||||||
if max_price:
|
if max_price:
|
||||||
query = query.filter(Property.price <= int(max_price))
|
max_val = int(max_price)
|
||||||
|
if request.state.price_pref == "sqft":
|
||||||
|
query = query.filter(Property.price <= max_val * Property.area_sqft)
|
||||||
|
else:
|
||||||
|
query = query.filter(Property.price <= max_val)
|
||||||
if bedrooms:
|
if bedrooms:
|
||||||
query = query.filter(Property.bedrooms >= int(bedrooms))
|
query = query.filter(Property.bedrooms >= int(bedrooms))
|
||||||
if city:
|
if city:
|
||||||
@@ -91,6 +99,7 @@ async def property_list(request: Request, db: Session = Depends(get_db)):
|
|||||||
"bedrooms": bedrooms,
|
"bedrooms": bedrooms,
|
||||||
"city_filter": city,
|
"city_filter": city,
|
||||||
"state_filter": state,
|
"state_filter": state,
|
||||||
|
"price_pref": request.state.price_pref,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -102,6 +111,7 @@ async def property_create_form(request: Request, db: Session = Depends(get_db)):
|
|||||||
return templates.TemplateResponse(request, "properties/create.html", {
|
return templates.TemplateResponse(request, "properties/create.html", {
|
||||||
"user": user,
|
"user": user,
|
||||||
"values": {},
|
"values": {},
|
||||||
|
"price_pref": request.state.price_pref,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -154,6 +164,7 @@ async def property_create_submit(
|
|||||||
"user": user,
|
"user": user,
|
||||||
"values": {k: v for k, v in request._form.items()} if hasattr(request, '_form') else {},
|
"values": {k: v for k, v in request._form.items()} if hasattr(request, '_form') else {},
|
||||||
"error": "Please provide at least one contact method (email or phone).",
|
"error": "Please provide at least one contact method (email or phone).",
|
||||||
|
"price_pref": request.state.price_pref,
|
||||||
})
|
})
|
||||||
|
|
||||||
prop = Property(
|
prop = Property(
|
||||||
@@ -211,6 +222,7 @@ async def property_detail(prop_id: int, request: Request, db: Session = Depends(
|
|||||||
"primary_image": primary_image,
|
"primary_image": primary_image,
|
||||||
"images": images,
|
"images": images,
|
||||||
"is_favorited": is_favorited,
|
"is_favorited": is_favorited,
|
||||||
|
"price_pref": request.state.price_pref,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -231,6 +243,7 @@ async def property_edit_form(prop_id: int, request: Request, db: Session = Depen
|
|||||||
"user": user,
|
"user": user,
|
||||||
"prop": prop,
|
"prop": prop,
|
||||||
"primary_image": primary_image,
|
"primary_image": primary_image,
|
||||||
|
"price_pref": request.state.price_pref,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -365,4 +378,5 @@ async def dashboard(request: Request, db: Session = Depends(get_db)):
|
|||||||
"user": user,
|
"user": user,
|
||||||
"my_properties": my_properties,
|
"my_properties": my_properties,
|
||||||
"favorites": favorites,
|
"favorites": favorites,
|
||||||
|
"price_pref": request.state.price_pref,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -16,6 +16,38 @@
|
|||||||
<a href="/properties?type=rent" class="text-gray-200 hover:text-white transition">Rent</a>
|
<a href="/properties?type=rent" class="text-gray-200 hover:text-white transition">Rent</a>
|
||||||
<a href="/properties/new" class="text-gray-200 hover:text-white transition">Sell</a>
|
<a href="/properties/new" class="text-gray-200 hover:text-white transition">Sell</a>
|
||||||
|
|
||||||
|
<!-- Price Preference Toggle -->
|
||||||
|
<div class="relative" id="pricePrefWrapper">
|
||||||
|
<button type="button" onclick="togglePricePref()"
|
||||||
|
class="flex items-center gap-1.5 text-gray-200 hover:text-white transition text-sm">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||||
|
</svg>
|
||||||
|
<span id="pricePrefLabel">{{ {"total": "Total", "sqft": "/Sqft", "both": "Both"}[price_pref] }}</span>
|
||||||
|
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div id="pricePrefDropdown"
|
||||||
|
class="hidden absolute right-0 mt-2 w-48 bg-white rounded-lg shadow-xl z-50 py-1 border border-gray-200">
|
||||||
|
<button onclick="setPricePref('total')"
|
||||||
|
class="w-full text-left px-4 py-2 text-sm hover:bg-gray-50 transition
|
||||||
|
{% if price_pref == 'total' %}text-accent font-semibold{% else %}text-gray-700{% endif %}">
|
||||||
|
Total Price
|
||||||
|
</button>
|
||||||
|
<button onclick="setPricePref('sqft')"
|
||||||
|
class="w-full text-left px-4 py-2 text-sm hover:bg-gray-50 transition
|
||||||
|
{% if price_pref == 'sqft' %}text-accent font-semibold{% else %}text-gray-700{% endif %}">
|
||||||
|
Price per Sq Ft
|
||||||
|
</button>
|
||||||
|
<button onclick="setPricePref('both')"
|
||||||
|
class="w-full text-left px-4 py-2 text-sm hover:bg-gray-50 transition
|
||||||
|
{% if price_pref == 'both' %}text-accent font-semibold{% else %}text-gray-700{% endif %}">
|
||||||
|
Both
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% if user %}
|
{% if user %}
|
||||||
<a href="/dashboard" class="text-gray-200 hover:text-white transition">Dashboard</a>
|
<a href="/dashboard" class="text-gray-200 hover:text-white transition">Dashboard</a>
|
||||||
<div class="flex items-center space-x-3 ml-2">
|
<div class="flex items-center space-x-3 ml-2">
|
||||||
@@ -52,6 +84,27 @@
|
|||||||
<a href="/properties" class="text-gray-200 hover:text-white py-2">Buy</a>
|
<a href="/properties" class="text-gray-200 hover:text-white py-2">Buy</a>
|
||||||
<a href="/properties?type=rent" class="text-gray-200 hover:text-white py-2">Rent</a>
|
<a href="/properties?type=rent" class="text-gray-200 hover:text-white py-2">Rent</a>
|
||||||
<a href="/properties/new" class="text-gray-200 hover:text-white py-2">Sell</a>
|
<a href="/properties/new" class="text-gray-200 hover:text-white py-2">Sell</a>
|
||||||
|
<!-- Mobile Price Preference -->
|
||||||
|
<div class="border-t border-white/20 pt-2 mt-1">
|
||||||
|
<span class="text-xs text-gray-400 uppercase tracking-wide">Price Display</span>
|
||||||
|
<div class="flex gap-2 mt-1">
|
||||||
|
<button onclick="setPricePref('total')"
|
||||||
|
class="px-3 py-1 text-xs rounded-full transition
|
||||||
|
{% if price_pref == 'total' %}bg-warm text-primary font-semibold{% else %}bg-white/10 text-gray-300{% endif %}">
|
||||||
|
Total
|
||||||
|
</button>
|
||||||
|
<button onclick="setPricePref('sqft')"
|
||||||
|
class="px-3 py-1 text-xs rounded-full transition
|
||||||
|
{% if price_pref == 'sqft' %}bg-warm text-primary font-semibold{% else %}bg-white/10 text-gray-300{% endif %}">
|
||||||
|
/Sqft
|
||||||
|
</button>
|
||||||
|
<button onclick="setPricePref('both')"
|
||||||
|
class="px-3 py-1 text-xs rounded-full transition
|
||||||
|
{% if price_pref == 'both' %}bg-warm text-primary font-semibold{% else %}bg-white/10 text-gray-300{% endif %}">
|
||||||
|
Both
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% if user %}
|
{% if user %}
|
||||||
<a href="/dashboard" class="text-gray-200 hover:text-white py-2">Dashboard</a>
|
<a href="/dashboard" class="text-gray-200 hover:text-white py-2">Dashboard</a>
|
||||||
<a href="/auth/logout" class="text-warm hover:text-warm-dark py-2 font-medium">Logout ({{ user.username }})</a>
|
<a href="/auth/logout" class="text-warm hover:text-warm-dark py-2 font-medium">Logout ({{ user.username }})</a>
|
||||||
@@ -68,4 +121,20 @@
|
|||||||
document.getElementById('mobile-menu-btn')?.addEventListener('click', () => {
|
document.getElementById('mobile-menu-btn')?.addEventListener('click', () => {
|
||||||
document.getElementById('mobile-menu').classList.toggle('hidden');
|
document.getElementById('mobile-menu').classList.toggle('hidden');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function togglePricePref() {
|
||||||
|
document.getElementById('pricePrefDropdown').classList.toggle('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPricePref(pref) {
|
||||||
|
document.cookie = 'price_pref=' + pref + ';path=/;max-age=31536000;samesite=lax';
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('click', function(e) {
|
||||||
|
var wrapper = document.getElementById('pricePrefWrapper');
|
||||||
|
if (wrapper && !wrapper.contains(e.target)) {
|
||||||
|
document.getElementById('pricePrefDropdown').classList.add('hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{% macro property_card(prop) %}
|
{% macro property_card(prop, price_pref="total") %}
|
||||||
<div class="bg-white rounded-xl shadow-md overflow-hidden card-hover">
|
<div class="bg-white rounded-xl shadow-md overflow-hidden card-hover">
|
||||||
<a href="/properties/{{ prop.id }}" class="block">
|
<a href="/properties/{{ prop.id }}" class="block">
|
||||||
<div class="relative h-52 bg-gray-200">
|
<div class="relative h-52 bg-gray-200">
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
|
|
||||||
<div class="p-5">
|
<div class="p-5">
|
||||||
<div class="flex items-center justify-between mb-2">
|
<div class="flex items-center justify-between mb-2">
|
||||||
<span class="text-2xl font-bold text-primary">${{ "{:,}".format(prop.price) }}</span>
|
<span class="text-2xl font-bold text-primary">{{ prop.price|price_fmt(price_pref, prop.area_sqft) }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 class="text-lg font-semibold text-gray-800 mb-1 truncate">
|
<h3 class="text-lg font-semibold text-gray-800 mb-1 truncate">
|
||||||
|
|||||||
@@ -64,7 +64,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 font-semibold text-primary">${{ "{:,}".format(prop.price) }}</td>
|
<td class="px-6 py-4 font-semibold text-primary">{{ prop.price|price_fmt(price_pref, prop.area_sqft) }}</td>
|
||||||
<td class="px-6 py-4">
|
<td class="px-6 py-4">
|
||||||
<span class="px-2.5 py-1 rounded-full text-xs font-medium
|
<span class="px-2.5 py-1 rounded-full text-xs font-medium
|
||||||
{% if prop.status == 'active' %}bg-green-100 text-green-800
|
{% if prop.status == 'active' %}bg-green-100 text-green-800
|
||||||
@@ -106,7 +106,7 @@
|
|||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
{% from "components/property_card.html" import property_card %}
|
{% from "components/property_card.html" import property_card %}
|
||||||
{% for fav_prop in favorites %}
|
{% for fav_prop in favorites %}
|
||||||
{{ property_card(fav_prop) }}
|
{{ property_card(fav_prop, price_pref) }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
{% from "components/property_card.html" import property_card %}
|
{% from "components/property_card.html" import property_card %}
|
||||||
{% for prop in properties %}
|
{% for prop in properties %}
|
||||||
{{ property_card(prop) }}
|
{{ property_card(prop, price_pref) }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|||||||
@@ -34,13 +34,37 @@
|
|||||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-1">Price (USD) *</label>
|
<label class="block text-sm font-medium text-gray-700 mb-1">Price (USD) *</label>
|
||||||
<div class="relative">
|
<!-- Input mode toggle -->
|
||||||
|
<div class="flex gap-1 mb-2 bg-gray-100 rounded-lg p-0.5 text-xs">
|
||||||
|
<button type="button" id="priceModeTotal" onclick="setPriceMode('total')"
|
||||||
|
class="flex-1 py-1.5 rounded-md font-medium transition bg-white shadow text-primary">
|
||||||
|
Total Price
|
||||||
|
</button>
|
||||||
|
<button type="button" id="priceModeUnit" onclick="setPriceMode('unit')"
|
||||||
|
class="flex-1 py-1.5 rounded-md font-medium transition text-gray-500">
|
||||||
|
Per Sqft
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<!-- Total price input (default visible) -->
|
||||||
|
<div class="relative" id="totalPriceGroup">
|
||||||
<span class="absolute left-3 top-3 text-gray-500 font-medium">$</span>
|
<span class="absolute left-3 top-3 text-gray-500 font-medium">$</span>
|
||||||
<input type="number" name="price" required min="0"
|
<input type="number" id="totalPriceInput" name="price" required min="0"
|
||||||
value="{{ values.price|default('') }}"
|
value="{{ values.price|default('') }}"
|
||||||
placeholder="450,000"
|
placeholder="450,000"
|
||||||
class="w-full pl-8 pr-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-accent outline-none transition">
|
class="w-full pl-8 pr-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-accent outline-none transition">
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Unit price input (hidden by default) -->
|
||||||
|
<div class="relative hidden" id="unitPriceGroup">
|
||||||
|
<span class="absolute left-3 top-3 text-gray-500 font-medium">$</span>
|
||||||
|
<input type="number" id="unitPriceInput" min="0" step="0.01"
|
||||||
|
placeholder="225"
|
||||||
|
class="w-full pl-8 pr-12 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-accent outline-none transition">
|
||||||
|
<span class="absolute right-3 top-3 text-gray-400 text-sm">/sqft</span>
|
||||||
|
</div>
|
||||||
|
<!-- Reference total when in unit mode -->
|
||||||
|
<p class="text-xs text-gray-400 mt-1 hidden" id="priceRef">
|
||||||
|
Total: <span id="priceRefValue">$0</span>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-1">Property Type *</label>
|
<label class="block text-sm font-medium text-gray-700 mb-1">Property Type *</label>
|
||||||
@@ -71,7 +95,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-1">Area (sqft) *</label>
|
<label class="block text-sm font-medium text-gray-700 mb-1">Area (sqft) *</label>
|
||||||
<input type="number" name="area_sqft" required min="0"
|
<input type="number" id="areaSqftInput" name="area_sqft" required min="0"
|
||||||
value="{{ values.area_sqft|default('') }}"
|
value="{{ values.area_sqft|default('') }}"
|
||||||
placeholder="2000"
|
placeholder="2000"
|
||||||
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-accent outline-none transition">
|
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-accent outline-none transition">
|
||||||
@@ -170,4 +194,61 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
var priceMode = 'total';
|
||||||
|
|
||||||
|
window.setPriceMode = function(mode) {
|
||||||
|
priceMode = mode;
|
||||||
|
var totalGroup = document.getElementById('totalPriceGroup');
|
||||||
|
var unitGroup = document.getElementById('unitPriceGroup');
|
||||||
|
var totalBtn = document.getElementById('priceModeTotal');
|
||||||
|
var unitBtn = document.getElementById('priceModeUnit');
|
||||||
|
var ref = document.getElementById('priceRef');
|
||||||
|
var priceInput = document.getElementById('totalPriceInput');
|
||||||
|
|
||||||
|
if (mode === 'total') {
|
||||||
|
totalGroup.classList.remove('hidden');
|
||||||
|
unitGroup.classList.add('hidden');
|
||||||
|
ref.classList.add('hidden');
|
||||||
|
totalBtn.classList.add('bg-white', 'shadow', 'text-primary');
|
||||||
|
totalBtn.classList.remove('text-gray-500');
|
||||||
|
unitBtn.classList.remove('bg-white', 'shadow', 'text-primary');
|
||||||
|
unitBtn.classList.add('text-gray-500');
|
||||||
|
} else {
|
||||||
|
totalGroup.classList.add('hidden');
|
||||||
|
unitGroup.classList.remove('hidden');
|
||||||
|
ref.classList.remove('hidden');
|
||||||
|
unitBtn.classList.add('bg-white', 'shadow', 'text-primary');
|
||||||
|
unitBtn.classList.remove('text-gray-500');
|
||||||
|
totalBtn.classList.remove('bg-white', 'shadow', 'text-primary');
|
||||||
|
totalBtn.classList.add('text-gray-500');
|
||||||
|
updateTotalFromUnit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function getArea() {
|
||||||
|
return parseInt(document.getElementById('areaSqftInput').value) || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTotalFromUnit() {
|
||||||
|
var unitVal = parseFloat(document.getElementById('unitPriceInput').value) || 0;
|
||||||
|
var area = getArea();
|
||||||
|
var total = Math.round(unitVal * area);
|
||||||
|
document.getElementById('totalPriceInput').value = total || '';
|
||||||
|
document.getElementById('priceRefValue').textContent = '$' + total.toLocaleString();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('unitPriceInput').addEventListener('input', updateTotalFromUnit);
|
||||||
|
document.getElementById('areaSqftInput').addEventListener('input', function() {
|
||||||
|
if (priceMode === 'unit') updateTotalFromUnit();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ensure total price is synced before form submission
|
||||||
|
document.querySelector('form').addEventListener('submit', function() {
|
||||||
|
if (priceMode === 'unit') updateTotalFromUnit();
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -82,7 +82,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="flex items-center justify-between flex-wrap gap-4">
|
<div class="flex items-center justify-between flex-wrap gap-4">
|
||||||
<h1 class="text-3xl font-bold text-primary">{{ prop.title }}</h1>
|
<h1 class="text-3xl font-bold text-primary">{{ prop.title }}</h1>
|
||||||
<span class="text-3xl font-extrabold text-accent">${{ "{:,}".format(prop.price) }}</span>
|
<span class="text-3xl font-extrabold text-accent">{{ prop.price|price_fmt(price_pref, prop.area_sqft) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="flex items-center text-gray-500 mt-2">
|
<p class="flex items-center text-gray-500 mt-2">
|
||||||
<svg class="w-5 h-5 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-5 h-5 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
|||||||
@@ -32,12 +32,36 @@
|
|||||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-1">Price (USD) *</label>
|
<label class="block text-sm font-medium text-gray-700 mb-1">Price (USD) *</label>
|
||||||
<div class="relative">
|
<!-- Input mode toggle -->
|
||||||
|
<div class="flex gap-1 mb-2 bg-gray-100 rounded-lg p-0.5 text-xs">
|
||||||
|
<button type="button" id="priceModeTotal" onclick="setPriceMode('total')"
|
||||||
|
class="flex-1 py-1.5 rounded-md font-medium transition bg-white shadow text-primary">
|
||||||
|
Total Price
|
||||||
|
</button>
|
||||||
|
<button type="button" id="priceModeUnit" onclick="setPriceMode('unit')"
|
||||||
|
class="flex-1 py-1.5 rounded-md font-medium transition text-gray-500">
|
||||||
|
Per Sqft
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<!-- Total price input (default visible) -->
|
||||||
|
<div class="relative" id="totalPriceGroup">
|
||||||
<span class="absolute left-3 top-3 text-gray-500 font-medium">$</span>
|
<span class="absolute left-3 top-3 text-gray-500 font-medium">$</span>
|
||||||
<input type="number" name="price" required min="0"
|
<input type="number" id="totalPriceInput" name="price" required min="0"
|
||||||
value="{{ prop.price }}"
|
value="{{ prop.price }}"
|
||||||
class="w-full pl-8 pr-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-accent outline-none transition">
|
class="w-full pl-8 pr-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-accent outline-none transition">
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Unit price input (hidden by default) -->
|
||||||
|
<div class="relative hidden" id="unitPriceGroup">
|
||||||
|
<span class="absolute left-3 top-3 text-gray-500 font-medium">$</span>
|
||||||
|
<input type="number" id="unitPriceInput" min="0" step="0.01"
|
||||||
|
placeholder="225"
|
||||||
|
class="w-full pl-8 pr-12 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-accent outline-none transition">
|
||||||
|
<span class="absolute right-3 top-3 text-gray-400 text-sm">/sqft</span>
|
||||||
|
</div>
|
||||||
|
<!-- Reference total when in unit mode -->
|
||||||
|
<p class="text-xs text-gray-400 mt-1 hidden" id="priceRef">
|
||||||
|
Total: <span id="priceRefValue">$0</span>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-1">Property Type *</label>
|
<label class="block text-sm font-medium text-gray-700 mb-1">Property Type *</label>
|
||||||
@@ -67,7 +91,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-1">Area (sqft) *</label>
|
<label class="block text-sm font-medium text-gray-700 mb-1">Area (sqft) *</label>
|
||||||
<input type="number" name="area_sqft" required min="0"
|
<input type="number" id="areaSqftInput" name="area_sqft" required min="0"
|
||||||
value="{{ prop.area_sqft }}"
|
value="{{ prop.area_sqft }}"
|
||||||
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-accent outline-none transition">
|
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-accent outline-none transition">
|
||||||
</div>
|
</div>
|
||||||
@@ -173,4 +197,68 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
var priceMode = 'total';
|
||||||
|
|
||||||
|
window.setPriceMode = function(mode) {
|
||||||
|
priceMode = mode;
|
||||||
|
var totalGroup = document.getElementById('totalPriceGroup');
|
||||||
|
var unitGroup = document.getElementById('unitPriceGroup');
|
||||||
|
var totalBtn = document.getElementById('priceModeTotal');
|
||||||
|
var unitBtn = document.getElementById('priceModeUnit');
|
||||||
|
var ref = document.getElementById('priceRef');
|
||||||
|
|
||||||
|
if (mode === 'total') {
|
||||||
|
totalGroup.classList.remove('hidden');
|
||||||
|
unitGroup.classList.add('hidden');
|
||||||
|
ref.classList.add('hidden');
|
||||||
|
totalBtn.classList.add('bg-white', 'shadow', 'text-primary');
|
||||||
|
totalBtn.classList.remove('text-gray-500');
|
||||||
|
unitBtn.classList.remove('bg-white', 'shadow', 'text-primary');
|
||||||
|
unitBtn.classList.add('text-gray-500');
|
||||||
|
} else {
|
||||||
|
totalGroup.classList.add('hidden');
|
||||||
|
unitGroup.classList.remove('hidden');
|
||||||
|
ref.classList.remove('hidden');
|
||||||
|
unitBtn.classList.add('bg-white', 'shadow', 'text-primary');
|
||||||
|
unitBtn.classList.remove('text-gray-500');
|
||||||
|
totalBtn.classList.remove('bg-white', 'shadow', 'text-primary');
|
||||||
|
totalBtn.classList.add('text-gray-500');
|
||||||
|
updateTotalFromUnit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function getArea() {
|
||||||
|
return parseInt(document.getElementById('areaSqftInput').value) || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTotalFromUnit() {
|
||||||
|
var unitVal = parseFloat(document.getElementById('unitPriceInput').value) || 0;
|
||||||
|
var area = getArea();
|
||||||
|
var total = Math.round(unitVal * area);
|
||||||
|
document.getElementById('totalPriceInput').value = total || '';
|
||||||
|
document.getElementById('priceRefValue').textContent = '$' + total.toLocaleString();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('unitPriceInput').addEventListener('input', updateTotalFromUnit);
|
||||||
|
document.getElementById('areaSqftInput').addEventListener('input', function() {
|
||||||
|
if (priceMode === 'unit') updateTotalFromUnit();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize unit price from existing values
|
||||||
|
var priceInput = document.getElementById('totalPriceInput');
|
||||||
|
var areaInput = document.getElementById('areaSqftInput');
|
||||||
|
if (priceInput.value && areaInput.value && parseInt(areaInput.value) > 0) {
|
||||||
|
var unitPrice = parseInt(priceInput.value) / parseInt(areaInput.value);
|
||||||
|
document.getElementById('unitPriceInput').value = Math.round(unitPrice * 100) / 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure total price is synced before form submission
|
||||||
|
document.querySelector('form').addEventListener('submit', function() {
|
||||||
|
if (priceMode === 'unit') updateTotalFromUnit();
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -30,7 +30,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-1">Price Range (USD)</label>
|
<label class="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Price Range
|
||||||
|
{% if price_pref == 'sqft' %}
|
||||||
|
<span class="text-xs text-gray-400 font-normal">(per sqft)</span>
|
||||||
|
{% endif %}
|
||||||
|
</label>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<input type="number" name="min_price" value="{{ min_price|default('') }}"
|
<input type="number" name="min_price" value="{{ min_price|default('') }}"
|
||||||
placeholder="Min" min="0"
|
placeholder="Min" min="0"
|
||||||
@@ -86,7 +91,7 @@
|
|||||||
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
|
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||||
{% from "components/property_card.html" import property_card %}
|
{% from "components/property_card.html" import property_card %}
|
||||||
{% for prop in properties %}
|
{% for prop in properties %}
|
||||||
{{ property_card(prop) }}
|
{{ property_card(prop, price_pref) }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user