Initial Commit
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
<footer class="bg-primary text-gray-300 mt-16">
|
||||
<div class="max-w-7xl mx-auto px-4 py-12">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8">
|
||||
<!-- Brand -->
|
||||
<div class="md:col-span-1">
|
||||
<div class="flex items-center space-x-2 mb-4">
|
||||
<svg class="w-7 h-7 text-warm" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 3L2 12h3v8h6v-6h2v6h6v-8h3L12 3z"/>
|
||||
</svg>
|
||||
<span class="text-lg font-bold text-white">NexHome</span>
|
||||
</div>
|
||||
<p class="text-sm text-gray-400">Your trusted partner in finding the perfect home. Browse thousands of properties across the United States.</p>
|
||||
</div>
|
||||
|
||||
<!-- Quick Links -->
|
||||
<div>
|
||||
<h3 class="text-white font-semibold mb-4">Quick Links</h3>
|
||||
<ul class="space-y-2 text-sm">
|
||||
<li><a href="/" class="hover:text-warm transition">Home</a></li>
|
||||
<li><a href="/properties" class="hover:text-warm transition">Browse Properties</a></li>
|
||||
<li><a href="/properties/new" class="hover:text-warm transition">List a Property</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Property Types -->
|
||||
<div>
|
||||
<h3 class="text-white font-semibold mb-4">Property Types</h3>
|
||||
<ul class="space-y-2 text-sm">
|
||||
<li><a href="/properties?type=house" class="hover:text-warm transition">Houses</a></li>
|
||||
<li><a href="/properties?type=condo" class="hover:text-warm transition">Condos</a></li>
|
||||
<li><a href="/properties?type=townhouse" class="hover:text-warm transition">Townhouses</a></li>
|
||||
<li><a href="/properties?type=land" class="hover:text-warm transition">Land</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Contact -->
|
||||
<div>
|
||||
<h3 class="text-white font-semibold mb-4">Contact Us</h3>
|
||||
<ul class="space-y-2 text-sm">
|
||||
<li class="flex items-center space-x-2">
|
||||
<svg class="w-4 h-4 text-warm" 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>
|
||||
<span>info@nexhome.com</span>
|
||||
</li>
|
||||
<li class="flex items-center space-x-2">
|
||||
<svg class="w-4 h-4 text-warm" 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>
|
||||
<span>(800) 555-1154</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-gray-600 mt-10 pt-6 text-center text-sm text-gray-500">
|
||||
© 2026 NexHome. All rights reserved. All prices in USD.
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
@@ -0,0 +1,71 @@
|
||||
<nav class="bg-primary shadow-lg">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex items-center justify-between h-16">
|
||||
<!-- Logo -->
|
||||
<a href="/" class="flex items-center space-x-2">
|
||||
<svg class="w-8 h-8 text-warm" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 3L2 12h3v8h6v-6h2v6h6v-8h3L12 3z"/>
|
||||
</svg>
|
||||
<span class="text-xl font-bold text-white tracking-tight">NexHome</span>
|
||||
</a>
|
||||
|
||||
<!-- Desktop nav -->
|
||||
<div class="hidden md:flex items-center space-x-6">
|
||||
<a href="/" class="text-gray-200 hover:text-white transition">Home</a>
|
||||
<a href="/properties" class="text-gray-200 hover:text-white transition">Buy</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>
|
||||
|
||||
{% if user %}
|
||||
<a href="/dashboard" class="text-gray-200 hover:text-white transition">Dashboard</a>
|
||||
<div class="flex items-center space-x-3 ml-2">
|
||||
<span class="text-warm font-medium text-sm">{{ user.username }}</span>
|
||||
<a href="/auth/logout"
|
||||
class="bg-warm hover:bg-warm-dark text-primary font-semibold text-sm px-4 py-2 rounded-lg transition">
|
||||
Logout
|
||||
</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<a href="/auth/login"
|
||||
class="text-gray-200 hover:text-white transition font-medium text-sm">
|
||||
Login
|
||||
</a>
|
||||
<a href="/auth/register"
|
||||
class="bg-warm hover:bg-warm-dark text-primary font-semibold text-sm px-4 py-2 rounded-lg transition">
|
||||
Register
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Mobile menu button -->
|
||||
<button id="mobile-menu-btn" class="md:hidden text-gray-200 hover:text-white">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Mobile menu -->
|
||||
<div id="mobile-menu" class="hidden md:hidden pb-4">
|
||||
<div class="flex flex-col space-y-2">
|
||||
<a href="/" class="text-gray-200 hover:text-white py-2">Home</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/new" class="text-gray-200 hover:text-white py-2">Sell</a>
|
||||
{% if user %}
|
||||
<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>
|
||||
{% else %}
|
||||
<a href="/auth/login" class="text-gray-200 hover:text-white py-2">Login</a>
|
||||
<a href="/auth/register" class="text-warm hover:text-warm-dark py-2 font-medium">Register</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<script>
|
||||
document.getElementById('mobile-menu-btn')?.addEventListener('click', () => {
|
||||
document.getElementById('mobile-menu').classList.toggle('hidden');
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,149 @@
|
||||
{# Phone input component with country code selector, flags, dynamic placeholder, and auto-formatting.
|
||||
Usage: {% include "components/phone_input.html" %}
|
||||
Expects: name, value (optional), id (optional)
|
||||
#}
|
||||
<div class="phone-input-wrapper" id="phoneWrapper">
|
||||
<label for="contact_phone_input" class="block text-sm font-medium text-gray-700 mb-1">Contact Phone</label>
|
||||
<div class="flex rounded-lg border border-gray-300 overflow-hidden focus-within:ring-2 focus-within:ring-accent focus-within:border-transparent transition">
|
||||
<!-- Country selector button -->
|
||||
<button type="button" id="phoneCountryBtn"
|
||||
class="flex items-center gap-1.5 px-3 bg-gray-50 border-r border-gray-300 hover:bg-gray-100 transition text-sm flex-shrink-0"
|
||||
onclick="togglePhoneDropdown()">
|
||||
<span id="phoneFlag" class="text-lg leading-none">🇺🇸</span>
|
||||
<span id="phoneDialCode" class="text-gray-700 font-medium">+1</span>
|
||||
<svg class="w-3 h-3 text-gray-500 ml-0.5" 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>
|
||||
|
||||
<!-- Hidden input for actual phone value (with dial code) -->
|
||||
<input type="hidden" name="contact_phone" id="phoneHidden" value="{{ phone_value|default('') }}">
|
||||
|
||||
<!-- Visible phone display -->
|
||||
<input type="tel" id="phoneDisplay"
|
||||
class="flex-1 px-3 py-2.5 outline-none bg-white text-sm min-w-0"
|
||||
placeholder="(555) 123-4567"
|
||||
autocomplete="tel"
|
||||
oninput="formatPhoneInput(this)"
|
||||
onfocus="formatPhoneInput(this)">
|
||||
</div>
|
||||
|
||||
<!-- Dropdown -->
|
||||
<div id="phoneDropdown"
|
||||
class="hidden absolute z-50 mt-1 w-72 bg-white border border-gray-200 rounded-xl shadow-xl max-h-64 overflow-y-auto">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
const COUNTRIES = [
|
||||
{ code: 'US', flag: '🇺🇸', dial: '+1', name: 'United States', format: '(###) ###-####', digitCount: 10 },
|
||||
{ code: 'CA', flag: '🇨🇦', dial: '+1', name: 'Canada', format: '(###) ###-####', digitCount: 10 },
|
||||
{ code: 'GB', flag: '🇬🇧', dial: '+44', name: 'United Kingdom', format: '#### ######', digitCount: 10 },
|
||||
{ code: 'AU', flag: '🇦🇺', dial: '+61', name: 'Australia', format: '#### ### ###', digitCount: 9 },
|
||||
{ code: 'CN', flag: '🇨🇳', dial: '+86', name: 'China', format: '### #### ####', digitCount: 11 },
|
||||
{ code: 'JP', flag: '🇯🇵', dial: '+81', name: 'Japan', format: '##-####-####', digitCount: 10 },
|
||||
{ code: 'KR', flag: '🇰🇷', dial: '+82', name: 'South Korea', format: '##-####-####', digitCount: 10 },
|
||||
{ code: 'DE', flag: '🇩🇪', dial: '+49', name: 'Germany', format: '### ########', digitCount: 11 },
|
||||
{ code: 'FR', flag: '🇫🇷', dial: '+33', name: 'France', format: '# ## ## ## ##', digitCount: 9 },
|
||||
{ code: 'IN', flag: '🇮🇳', dial: '+91', name: 'India', format: '##### #####', digitCount: 10 },
|
||||
{ code: 'BR', flag: '🇧🇷', dial: '+55', name: 'Brazil', format: '## #####-####', digitCount: 11 },
|
||||
{ code: 'MX', flag: '🇲🇽', dial: '+52', name: 'Mexico', format: '## #### ####', digitCount: 10 },
|
||||
{ code: 'SG', flag: '🇸🇬', dial: '+65', name: 'Singapore', format: '#### ####', digitCount: 8 },
|
||||
{ code: 'HK', flag: '🇭🇰', dial: '+852', name: 'Hong Kong(China)', format: '#### ####', digitCount: 8 },
|
||||
{ code: 'TW', flag: '🇹🇼', dial: '+886', name: 'Taiwan(China)', format: '## ### ####', digitCount: 9 },
|
||||
];
|
||||
|
||||
let selectedCountry = COUNTRIES[0]; // Default: US
|
||||
let rawDigits = '';
|
||||
|
||||
// Parse initial value
|
||||
const hiddenInput = document.getElementById('phoneHidden');
|
||||
const displayInput = document.getElementById('phoneDisplay');
|
||||
if (hiddenInput.value) {
|
||||
for (const c of COUNTRIES) {
|
||||
if (hiddenInput.value.startsWith(c.dial)) {
|
||||
selectedCountry = c;
|
||||
rawDigits = hiddenInput.value.replace(c.dial, '').replace(/\D/g, '');
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!rawDigits && hiddenInput.value) {
|
||||
rawDigits = hiddenInput.value.replace(/\D/g, '');
|
||||
}
|
||||
}
|
||||
|
||||
function updateDisplay() {
|
||||
document.getElementById('phoneFlag').textContent = selectedCountry.flag;
|
||||
document.getElementById('phoneDialCode').textContent = selectedCountry.dial;
|
||||
displayInput.placeholder = formatDigits('', selectedCountry);
|
||||
displayInput.value = formatDigits(rawDigits, selectedCountry);
|
||||
hiddenInput.value = rawDigits.length > 0 ? selectedCountry.dial + rawDigits : '';
|
||||
}
|
||||
|
||||
function formatDigits(digits, country) {
|
||||
if (!digits) return '';
|
||||
let result = '';
|
||||
let di = 0;
|
||||
for (let i = 0; i < country.format.length && di < digits.length; i++) {
|
||||
if (country.format[i] === '#') {
|
||||
result += digits[di++];
|
||||
} else {
|
||||
result += country.format[i];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
window.formatPhoneInput = function(input) {
|
||||
const cursorPos = input.selectionStart;
|
||||
const prevLength = input.value.length;
|
||||
rawDigits = input.value.replace(/\D/g, '').substring(0, selectedCountry.digitCount);
|
||||
updateDisplay();
|
||||
// Restore approximate cursor position
|
||||
const newLength = input.value.length;
|
||||
const newPos = cursorPos + (newLength - prevLength);
|
||||
input.setSelectionRange(newPos, newPos);
|
||||
};
|
||||
|
||||
window.togglePhoneDropdown = function() {
|
||||
const dd = document.getElementById('phoneDropdown');
|
||||
dd.classList.toggle('hidden');
|
||||
if (!dd.classList.contains('hidden')) {
|
||||
renderDropdown();
|
||||
}
|
||||
};
|
||||
|
||||
window.selectCountry = function(code) {
|
||||
selectedCountry = COUNTRIES.find(c => c.code === code);
|
||||
rawDigits = rawDigits.substring(0, selectedCountry.digitCount);
|
||||
updateDisplay();
|
||||
document.getElementById('phoneDropdown').classList.add('hidden');
|
||||
};
|
||||
|
||||
function renderDropdown() {
|
||||
const dd = document.getElementById('phoneDropdown');
|
||||
dd.innerHTML = COUNTRIES.map(c => `
|
||||
<button type="button"
|
||||
onclick="selectCountry('${c.code}')"
|
||||
class="w-full flex items-center gap-3 px-3 py-2.5 hover:bg-accent/5 transition text-left
|
||||
${c.code === selectedCountry.code ? 'bg-accent/10' : ''}">
|
||||
<span class="text-xl">${c.flag}</span>
|
||||
<span class="text-sm font-medium text-gray-700">${c.name}</span>
|
||||
<span class="text-sm text-gray-500 ml-auto">${c.dial}</span>
|
||||
</button>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// Close dropdown on outside click
|
||||
document.addEventListener('click', function(e) {
|
||||
const wrapper = document.getElementById('phoneWrapper');
|
||||
if (!wrapper.contains(e.target)) {
|
||||
document.getElementById('phoneDropdown').classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
// Initial render
|
||||
updateDisplay();
|
||||
})();
|
||||
</script>
|
||||
@@ -0,0 +1,61 @@
|
||||
{% macro property_card(prop) %}
|
||||
<div class="bg-white rounded-xl shadow-md overflow-hidden card-hover">
|
||||
<a href="/properties/{{ prop.id }}" class="block">
|
||||
<div class="relative h-52 bg-gray-200">
|
||||
{% if prop.primary_image %}
|
||||
<img src="/static/{{ prop.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-12 h-12" 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-3 left-3 bg-warm text-primary text-xs font-bold px-2.5 py-1 rounded-full">
|
||||
Featured
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
{% if prop.status == 'sold' %}
|
||||
<span class="absolute top-3 right-3 bg-red-600 text-white text-xs font-bold px-2.5 py-1 rounded-full">
|
||||
Sold
|
||||
</span>
|
||||
{% elif prop.status == 'pending' %}
|
||||
<span class="absolute top-3 right-3 bg-yellow-500 text-white text-xs font-bold px-2.5 py-1 rounded-full">
|
||||
Pending
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<div class="p-5">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="text-2xl font-bold text-primary">${{ "{:,}".format(prop.price) }}</span>
|
||||
</div>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-800 mb-1 truncate">
|
||||
<a href="/properties/{{ prop.id }}" class="hover:text-accent transition">{{ prop.title }}</a>
|
||||
</h3>
|
||||
|
||||
<p class="text-sm text-gray-500 mb-3 flex items-center">
|
||||
<svg class="w-4 h-4 mr-1 flex-shrink-0" 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.city }}, {{ prop.state }}
|
||||
</p>
|
||||
|
||||
<div class="flex items-center justify-between text-sm text-gray-600 border-t pt-3 whitespace-nowrap">
|
||||
<span>{{ prop.bedrooms }} bd</span>
|
||||
<span>{{ prop.bathrooms }} ba</span>
|
||||
<span>{{ "{:,}".format(prop.area_sqft) }} sqft</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
Reference in New Issue
Block a user