Initial Commit

This commit is contained in:
2026-06-11 15:05:08 +08:00
commit ea8e41e688
21 changed files with 2317 additions and 0 deletions
+173
View File
@@ -0,0 +1,173 @@
{% extends "base.html" %}
{% block title %}List a Property - NexHome{% endblock %}
{% block content %}
<div class="max-w-3xl mx-auto px-4 py-10">
<h1 class="text-3xl font-bold text-primary mb-2">List a New Property</h1>
<p class="text-gray-500 mb-8">Fill in the details below to create your listing.</p>
{% if error %}
<div class="bg-red-100 border border-red-200 text-red-800 rounded-lg p-4 mb-6 text-sm">
{{ error }}
</div>
{% endif %}
<form method="POST" action="/properties/new" enctype="multipart/form-data" class="bg-white rounded-2xl shadow-lg p-8 space-y-6">
<!-- Title -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Property Title *</label>
<input type="text" name="title" required
value="{{ values.title|default('') }}"
placeholder="e.g. Beautiful Family Home in Austin"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-accent outline-none transition">
</div>
<!-- Description -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Description *</label>
<textarea name="description" required rows="5"
placeholder="Describe your property..."
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-accent outline-none transition">{{ values.description|default('') }}</textarea>
</div>
<!-- Price & Type -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Price (USD) *</label>
<div class="relative">
<span class="absolute left-3 top-3 text-gray-500 font-medium">$</span>
<input type="number" name="price" required min="0"
value="{{ values.price|default('') }}"
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">
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Property Type *</label>
<select name="property_type" required
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-accent outline-none transition">
<option value="">Select type</option>
<option value="house" {% if values.property_type == 'house' %}selected{% endif %}>House</option>
<option value="condo" {% if values.property_type == 'condo' %}selected{% endif %}>Condo</option>
<option value="townhouse" {% if values.property_type == 'townhouse' %}selected{% endif %}>Townhouse</option>
<option value="land" {% if values.property_type == 'land' %}selected{% endif %}>Land</option>
</select>
</div>
</div>
<!-- Beds / Baths / Sqft -->
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Bedrooms *</label>
<input type="number" name="bedrooms" required min="0" max="20"
value="{{ values.bedrooms|default('0') }}"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-accent outline-none transition">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Bathrooms *</label>
<input type="number" name="bathrooms" required min="0" max="20" step="0.5"
value="{{ values.bathrooms|default('0') }}"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-accent outline-none transition">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Area (sqft) *</label>
<input type="number" name="area_sqft" required min="0"
value="{{ values.area_sqft|default('') }}"
placeholder="2000"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-accent outline-none transition">
</div>
</div>
<!-- Address -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Street Address *</label>
<input type="text" name="address" required
value="{{ values.address|default('') }}"
placeholder="123 Main Street"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-accent outline-none transition">
</div>
<!-- City / State / Zip -->
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">City *</label>
<input type="text" name="city" required
value="{{ values.city|default('') }}"
placeholder="Austin"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-accent outline-none transition">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">State *</label>
<input type="text" name="state" required maxlength="2"
value="{{ values.state|default('') }}"
placeholder="TX"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-accent outline-none transition">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Zip Code *</label>
<input type="text" name="zip_code" required maxlength="10"
value="{{ values.zip_code|default('') }}"
placeholder="78701"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-accent outline-none transition">
</div>
</div>
<!-- Year Built -->
<div class="sm:w-1/2">
<label class="block text-sm font-medium text-gray-700 mb-1">Year Built</label>
<input type="number" name="year_built" min="1800" max="2030"
value="{{ values.year_built|default('') }}"
placeholder="e.g. 2005"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-accent outline-none transition">
</div>
<!-- Contact Info -->
<div class="border-t pt-6">
<h3 class="text-lg font-semibold text-primary mb-4">Contact Information</h3>
<p class="text-sm text-gray-500 mb-4">Provide at least one contact method (email or phone).</p>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Contact Email</label>
<input type="email" name="contact_email"
value="{{ values.contact_email|default('') }}"
placeholder="seller@example.com"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-accent outline-none transition">
</div>
<div class="relative">
{% set phone_value = values.contact_phone|default("") %}
{% include "components/phone_input.html" %}
</div>
</div>
</div>
<!-- Image Upload -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Property Image</label>
<div class="border-2 border-dashed border-gray-300 rounded-lg p-6 text-center hover:border-accent transition">
<svg class="w-10 h-10 mx-auto text-gray-400 mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
<p class="text-sm text-gray-500 mb-2">Click or drag to upload an image</p>
<input type="file" name="image" accept="image/*"
class="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold file:bg-accent file:text-white hover:file:bg-blue-700 transition">
<p class="text-xs text-gray-400 mt-2">JPG, PNG or WebP. Max 5MB.</p>
</div>
</div>
<!-- Submit -->
<div class="flex gap-4 pt-4">
<button type="submit"
class="flex-1 bg-accent hover:bg-blue-700 text-white font-semibold py-3.5 rounded-lg transition">
Publish Listing
</button>
<a href="/"
class="px-8 py-3.5 border border-gray-300 text-gray-700 font-medium rounded-lg hover:bg-gray-50 transition">
Cancel
</a>
</div>
</form>
</div>
{% endblock %}
+258
View File
@@ -0,0 +1,258 @@
{% extends "base.html" %}
{% block title %}{{ prop.title }} - NexHome{% endblock %}
{% block content %}
<div class="max-w-7xl mx-auto px-4 py-8">
<!-- Breadcrumb -->
<nav class="text-sm text-gray-500 mb-6">
<a href="/" class="hover:text-accent transition">Home</a>
<span class="mx-2">/</span>
<a href="/properties" class="hover:text-accent transition">Properties</a>
<span class="mx-2">/</span>
<a href="/properties?city={{ prop.city }}" class="hover:text-accent transition">{{ prop.city }}</a>
<span class="mx-2">/</span>
<span class="text-gray-800">{{ prop.title }}</span>
</nav>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<!-- Main Content -->
<div class="lg:col-span-2 space-y-6">
<!-- Image -->
<div class="relative rounded-2xl overflow-hidden bg-gray-200 h-96">
{% if primary_image %}
<img src="/static/{{ primary_image.image_path }}"
alt="{{ prop.title }}"
class="w-full h-full object-cover">
{% else %}
<div class="flex items-center justify-center h-full text-gray-400">
<svg class="w-20 h-20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-4 0a1 1 0 01-1-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 01-1 1h-2z"/>
</svg>
</div>
{% endif %}
{% if prop.is_featured %}
<span class="absolute top-4 left-4 bg-warm text-primary text-sm font-bold px-3 py-1 rounded-full">
Featured
</span>
{% endif %}
{% if prop.status == 'sold' %}
<span class="absolute top-4 right-4 bg-red-600 text-white text-sm font-bold px-3 py-1 rounded-full">
Sold
</span>
{% elif prop.status == 'pending' %}
<span class="absolute top-4 right-4 bg-yellow-500 text-white text-sm font-bold px-3 py-1 rounded-full">
Pending
</span>
{% endif %}
<!-- Floating favorite heart (top-right) -->
{% if user %}
<form method="POST" action="/properties/{{ prop.id }}/favorite" class="absolute top-4 right-4 {% if prop.status == 'sold' or prop.status == 'pending' %}top-14{% endif %}">
<button type="submit"
class="w-12 h-12 rounded-full flex items-center justify-center shadow-lg transition
{% if is_favorited %}
bg-red-500 text-white hover:bg-red-600
{% else %}
bg-white/90 text-gray-600 hover:bg-white hover:text-red-500
{% endif %}">
<svg class="w-6 h-6" fill="{% if is_favorited %}currentColor{% else %}none{% endif %}" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"/>
</svg>
</button>
</form>
{% endif %}
</div>
<!-- Thumbnail gallery -->
{% if images|length > 1 %}
<div class="flex gap-2 overflow-x-auto pb-2">
{% for img in images %}
<img src="/static/{{ img.image_path }}"
alt="Property image {{ loop.index }}"
class="h-20 w-28 object-cover rounded-lg flex-shrink-0 border-2 {% if img.is_primary %}border-accent{% else %}border-transparent{% endif %}">
{% endfor %}
</div>
{% endif %}
<!-- Title & Price -->
<div>
<div class="flex items-center justify-between flex-wrap gap-4">
<h1 class="text-3xl font-bold text-primary">{{ prop.title }}</h1>
<span class="text-3xl font-extrabold text-accent">${{ "{:,}".format(prop.price) }}</span>
</div>
<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">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"/>
</svg>
{{ prop.address }}, {{ prop.city }}, {{ prop.state }} {{ prop.zip_code }}
</p>
</div>
<!-- Key Stats -->
<div class="grid grid-cols-3 gap-4 bg-white rounded-xl p-6 shadow-sm">
<div class="text-center">
<div class="text-2xl font-bold text-primary">{{ prop.bedrooms }}</div>
<div class="text-sm text-gray-500">Bedrooms</div>
</div>
<div class="text-center border-x">
<div class="text-2xl font-bold text-primary">{{ prop.bathrooms }}</div>
<div class="text-sm text-gray-500">Bathrooms</div>
</div>
<div class="text-center">
<div class="text-2xl font-bold text-primary">{{ "{:,}".format(prop.area_sqft) }}</div>
<div class="text-sm text-gray-500">Sq Ft</div>
</div>
</div>
<!-- Description -->
<div class="bg-white rounded-xl p-6 shadow-sm">
<h2 class="text-xl font-bold text-primary mb-4">Description</h2>
<p class="text-gray-600 leading-relaxed whitespace-pre-line">{{ prop.description }}</p>
</div>
<!-- Details -->
<div class="bg-white rounded-xl p-6 shadow-sm">
<h2 class="text-xl font-bold text-primary mb-4">Property Details</h2>
<div class="grid grid-cols-2 gap-4 text-sm">
<div class="flex justify-between py-2 border-b">
<span class="text-gray-500">Property Type</span>
<span class="font-medium text-gray-800 capitalize">{{ prop.property_type }}</span>
</div>
<div class="flex justify-between py-2 border-b">
<span class="text-gray-500">Year Built</span>
<span class="font-medium text-gray-800">{{ prop.year_built or 'N/A' }}</span>
</div>
<div class="flex justify-between py-2 border-b">
<span class="text-gray-500">Status</span>
<span class="font-medium text-gray-800 capitalize">{{ prop.status }}</span>
</div>
<div class="flex justify-between py-2 border-b">
<span class="text-gray-500">Listed</span>
<span class="font-medium text-gray-800">{{ prop.created_at.strftime("%B %d, %Y") }}</span>
</div>
</div>
</div>
</div>
<!-- Sidebar -->
<div class="space-y-6">
<!-- Contact Card -->
<div class="bg-white rounded-xl p-6 shadow-sm border-2 border-accent/20">
<h3 class="font-bold text-primary mb-4 text-lg">Contact Seller</h3>
<!-- Favorite button - large and prominent -->
{% if user %}
<form method="POST" action="/properties/{{ prop.id }}/favorite" class="mb-4">
<button type="submit"
class="w-full py-3.5 rounded-xl font-semibold transition text-base flex items-center justify-center gap-2
{% if is_favorited %}
bg-red-50 text-red-600 border-2 border-red-300 hover:bg-red-100
{% else %}
bg-gray-50 text-gray-700 border-2 border-gray-200 hover:border-red-300 hover:text-red-500 hover:bg-red-50
{% endif %}">
<svg class="w-6 h-6" fill="{% if is_favorited %}currentColor{% else %}none{% endif %}" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"/>
</svg>
{% if is_favorited %}Saved to Favorites{% else %}Add to Favorites{% endif %}
</button>
</form>
{% else %}
<a href="/auth/login"
class="block w-full text-center py-3.5 rounded-xl font-semibold transition text-base bg-accent hover:bg-blue-700 text-white mb-4">
Login to Save
</a>
{% endif %}
<!-- Contact info -->
<div class="space-y-3">
{% if prop.contact_email %}
<a href="mailto:{{ prop.contact_email }}"
class="flex items-center gap-3 p-3 bg-gray-50 rounded-lg hover:bg-accent/5 transition group">
<div class="w-10 h-10 bg-accent/10 rounded-full flex items-center justify-center flex-shrink-0 group-hover:bg-accent/20 transition">
<svg class="w-5 h-5 text-accent" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
</svg>
</div>
<div class="min-w-0">
<div class="text-xs text-gray-500">Email</div>
<div class="text-sm font-medium text-gray-800 truncate">{{ prop.contact_email }}</div>
</div>
</a>
{% endif %}
{% if prop.contact_phone %}
<a href="tel:{{ prop.contact_phone }}"
class="flex items-center gap-3 p-3 bg-gray-50 rounded-lg hover:bg-accent/5 transition group">
<div class="w-10 h-10 bg-green-100 rounded-full flex items-center justify-center flex-shrink-0 group-hover:bg-green-200 transition">
<svg class="w-5 h-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"/>
</svg>
</div>
<div class="min-w-0">
<div class="text-xs text-gray-500">Phone</div>
<div class="text-sm font-medium text-gray-800">{{ prop.contact_phone }}</div>
</div>
</a>
{% endif %}
{% if not prop.contact_email and not prop.contact_phone %}
<!-- Fallback to owner info -->
<div class="flex items-center gap-3 p-3 bg-gray-50 rounded-lg">
<div class="w-12 h-12 bg-primary rounded-full flex items-center justify-center text-white font-bold text-lg flex-shrink-0">
{{ prop.owner.username[0]|upper }}
</div>
<div class="min-w-0">
<div class="font-semibold text-gray-800">{{ prop.owner.username }}</div>
{% if prop.owner.full_name %}
<div class="text-sm text-gray-500">{{ prop.owner.full_name }}</div>
{% endif %}
</div>
</div>
{% endif %}
</div>
{% if user.id == prop.owner_id %}
<div class="mt-4 pt-4 border-t space-y-2">
<a href="/properties/{{ prop.id }}/edit"
class="block w-full text-center py-2.5 bg-accent hover:bg-blue-700 text-white rounded-lg font-medium transition text-sm">
Edit Listing
</a>
<form method="POST" action="/properties/{{ prop.id }}/delete"
onsubmit="return confirm('Are you sure you want to delete this listing?')">
<button type="submit"
class="w-full py-2.5 bg-red-50 hover:bg-red-100 text-red-600 border border-red-200 rounded-lg font-medium transition text-sm">
Delete Listing
</button>
</form>
</div>
{% endif %}
</div>
<!-- Listed By -->
<div class="bg-white rounded-xl p-6 shadow-sm">
<h3 class="font-bold text-primary mb-4">Listed By</h3>
<div class="flex items-center space-x-3">
<div class="w-12 h-12 bg-primary rounded-full flex items-center justify-center text-white font-bold text-lg">
{{ prop.owner.username[0]|upper }}
</div>
<div>
<div class="font-semibold text-gray-800">{{ prop.owner.username }}</div>
{% if prop.owner.full_name %}
<div class="text-sm text-gray-500">{{ prop.owner.full_name }}</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
+176
View File
@@ -0,0 +1,176 @@
{% extends "base.html" %}
{% block title %}Edit Listing - NexHome{% endblock %}
{% block content %}
<div class="max-w-3xl mx-auto px-4 py-10">
<h1 class="text-3xl font-bold text-primary mb-2">Edit Property</h1>
<p class="text-gray-500 mb-8">Update the details of your listing.</p>
{% if error %}
<div class="bg-red-100 border border-red-200 text-red-800 rounded-lg p-4 mb-6 text-sm">
{{ error }}
</div>
{% endif %}
<form method="POST" action="/properties/{{ prop.id }}/edit" enctype="multipart/form-data" class="bg-white rounded-2xl shadow-lg p-8 space-y-6">
<!-- Title -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Property Title *</label>
<input type="text" name="title" required
value="{{ prop.title }}"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-accent outline-none transition">
</div>
<!-- Description -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Description *</label>
<textarea name="description" required rows="5"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-accent outline-none transition">{{ prop.description }}</textarea>
</div>
<!-- Price & Type -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Price (USD) *</label>
<div class="relative">
<span class="absolute left-3 top-3 text-gray-500 font-medium">$</span>
<input type="number" name="price" required min="0"
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">
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Property Type *</label>
<select name="property_type" required
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-accent outline-none transition">
<option value="house" {% if prop.property_type == 'house' %}selected{% endif %}>House</option>
<option value="condo" {% if prop.property_type == 'condo' %}selected{% endif %}>Condo</option>
<option value="townhouse" {% if prop.property_type == 'townhouse' %}selected{% endif %}>Townhouse</option>
<option value="land" {% if prop.property_type == 'land' %}selected{% endif %}>Land</option>
</select>
</div>
</div>
<!-- Beds / Baths / Sqft -->
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Bedrooms *</label>
<input type="number" name="bedrooms" required min="0" max="20"
value="{{ prop.bedrooms }}"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-accent outline-none transition">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Bathrooms *</label>
<input type="number" name="bathrooms" required min="0" max="20" step="0.5"
value="{{ prop.bathrooms }}"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-accent outline-none transition">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Area (sqft) *</label>
<input type="number" name="area_sqft" required min="0"
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">
</div>
</div>
<!-- Address -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Street Address *</label>
<input type="text" name="address" required
value="{{ prop.address }}"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-accent outline-none transition">
</div>
<!-- City / State / Zip -->
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">City *</label>
<input type="text" name="city" required
value="{{ prop.city }}"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-accent outline-none transition">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">State *</label>
<input type="text" name="state" required maxlength="2"
value="{{ prop.state }}"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-accent outline-none transition">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Zip Code *</label>
<input type="text" name="zip_code" required maxlength="10"
value="{{ prop.zip_code }}"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-accent outline-none transition">
</div>
</div>
<!-- Year Built & Status -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Year Built</label>
<input type="number" name="year_built" min="1800" max="2030"
value="{{ prop.year_built or '' }}"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-accent outline-none transition">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Status</label>
<select name="status"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-accent outline-none transition">
<option value="active" {% if prop.status == 'active' %}selected{% endif %}>Active</option>
<option value="pending" {% if prop.status == 'pending' %}selected{% endif %}>Pending</option>
<option value="sold" {% if prop.status == 'sold' %}selected{% endif %}>Sold</option>
</select>
</div>
</div>
<!-- Contact Info -->
<div class="border-t pt-6">
<h3 class="text-lg font-semibold text-primary mb-4">Contact Information</h3>
<p class="text-sm text-gray-500 mb-4">Provide at least one contact method (email or phone).</p>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Contact Email</label>
<input type="email" name="contact_email"
value="{{ prop.contact_email or '' }}"
placeholder="seller@example.com"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-accent outline-none transition">
</div>
<div class="relative">
{% set phone_value = prop.contact_phone or "" %}
{% include "components/phone_input.html" %}
</div>
</div>
</div>
<!-- Current Image -->
{% if primary_image %}
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Current Image</label>
<img src="/static/{{ primary_image.image_path }}" alt="Current"
class="h-40 w-56 object-cover rounded-lg border">
</div>
{% endif %}
<!-- New Image Upload -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Replace Image</label>
<input type="file" name="image" accept="image/*"
class="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold file:bg-accent file:text-white hover:file:bg-blue-700 transition">
<p class="text-xs text-gray-400 mt-1">Leave empty to keep the current image.</p>
</div>
<!-- Submit -->
<div class="flex gap-4 pt-4">
<button type="submit"
class="flex-1 bg-accent hover:bg-blue-700 text-white font-semibold py-3.5 rounded-lg transition">
Save Changes
</button>
<a href="/properties/{{ prop.id }}"
class="px-8 py-3.5 border border-gray-300 text-gray-700 font-medium rounded-lg hover:bg-gray-50 transition">
Cancel
</a>
</div>
</form>
</div>
{% endblock %}
+136
View File
@@ -0,0 +1,136 @@
{% extends "base.html" %}
{% block title %}Browse Properties - NexHome{% endblock %}
{% block content %}
<div class="max-w-7xl mx-auto px-4 py-10">
<h1 class="text-3xl font-bold text-primary mb-8">Browse Properties</h1>
<div class="flex flex-col lg:flex-row gap-8">
<!-- Filter Sidebar -->
<aside class="w-full lg:w-72 flex-shrink-0">
<form action="/properties" method="GET" class="bg-white rounded-2xl shadow-md p-6 space-y-5 sticky top-4">
<h3 class="font-semibold text-primary text-lg border-b pb-3">Filters</h3>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Search</label>
<input type="text" name="search" value="{{ search|default('') }}"
placeholder="City, state, zip..."
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-accent outline-none">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Property Type</label>
<select name="type" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-accent outline-none">
<option value="">All Types</option>
<option value="house" {% if prop_type == 'house' %}selected{% endif %}>House</option>
<option value="condo" {% if prop_type == 'condo' %}selected{% endif %}>Condo</option>
<option value="townhouse" {% if prop_type == 'townhouse' %}selected{% endif %}>Townhouse</option>
<option value="land" {% if prop_type == 'land' %}selected{% endif %}>Land</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Price Range (USD)</label>
<div class="flex gap-2">
<input type="number" name="min_price" value="{{ min_price|default('') }}"
placeholder="Min" min="0"
class="w-1/2 px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-accent outline-none">
<input type="number" name="max_price" value="{{ max_price|default('') }}"
placeholder="Max" min="0"
class="w-1/2 px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-accent outline-none">
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Bedrooms</label>
<select name="bedrooms" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-accent outline-none">
<option value="">Any</option>
<option value="1" {% if bedrooms == '1' %}selected{% endif %}>1+</option>
<option value="2" {% if bedrooms == '2' %}selected{% endif %}>2+</option>
<option value="3" {% if bedrooms == '3' %}selected{% endif %}>3+</option>
<option value="4" {% if bedrooms == '4' %}selected{% endif %}>4+</option>
<option value="5" {% if bedrooms == '5' %}selected{% endif %}>5+</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">City</label>
<input type="text" name="city" value="{{ city_filter|default('') }}"
placeholder="e.g. Austin"
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-accent outline-none">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">State</label>
<input type="text" name="state" value="{{ state_filter|default('') }}"
placeholder="e.g. TX"
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-accent outline-none">
</div>
<button type="submit"
class="w-full bg-accent hover:bg-blue-700 text-white font-semibold py-2.5 rounded-lg transition text-sm">
Apply Filters
</button>
<a href="/properties" class="block text-center text-sm text-gray-500 hover:text-accent transition">
Clear all filters
</a>
</form>
</aside>
<!-- Property Grid -->
<div class="flex-1">
{% if properties %}
<div class="mb-4 text-sm text-gray-500">
Showing {{ properties|length }} propert{{ "y" if properties|length == 1 else "ies" }}
</div>
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
{% from "components/property_card.html" import property_card %}
{% for prop in properties %}
{{ property_card(prop) }}
{% endfor %}
</div>
<!-- Pagination -->
{% if total_pages > 1 %}
<div class="flex justify-center items-center gap-2 mt-10">
{% if page > 1 %}
<a href="?{{ query_string }}&page={{ page - 1 }}"
class="px-4 py-2 bg-white border border-gray-300 rounded-lg text-sm hover:bg-gray-50 transition">
&laquo; Previous
</a>
{% endif %}
{% for p in range(1, total_pages + 1) %}
{% if p == page %}
<span class="px-4 py-2 bg-accent text-white rounded-lg text-sm font-medium">{{ p }}</span>
{% else %}
<a href="?{{ query_string }}&page={{ p }}"
class="px-4 py-2 bg-white border border-gray-300 rounded-lg text-sm hover:bg-gray-50 transition">
{{ p }}
</a>
{% endif %}
{% endfor %}
{% if page < total_pages %}
<a href="?{{ query_string }}&page={{ page + 1 }}"
class="px-4 py-2 bg-white border border-gray-300 rounded-lg text-sm hover:bg-gray-50 transition">
Next &raquo;
</a>
{% endif %}
</div>
{% endif %}
{% else %}
<div class="text-center py-20 text-gray-400">
<svg class="w-16 h-16 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
<p class="text-lg mb-2">No properties found</p>
<p class="text-sm">Try adjusting your filters or <a href="/properties" class="text-accent hover:underline">clear all filters</a>.</p>
</div>
{% endif %}
</div>
</div>
</div>
{% endblock %}