First commit
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
{% extends "admin/change_form.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block object-tools-items %}
|
||||
{% if original %}
|
||||
<li>
|
||||
<a href="{% url 'admin:properties_property_upload_photos' original.pk %}"
|
||||
class="historylink">
|
||||
📷 Ajout groupé de photos
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{{ block.super }}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,240 @@
|
||||
{% extends "admin/base_site.html" %}
|
||||
{% load i18n static %}
|
||||
|
||||
{% block title %}📷 Ajout de photos — {{ property.name_fr }}{% endblock %}
|
||||
|
||||
{% block extrahead %}
|
||||
{{ block.super }}
|
||||
<style>
|
||||
.upload-card {
|
||||
background: #fff;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 6px;
|
||||
padding: 24px;
|
||||
max-width: 680px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.upload-card h2 { margin-top: 0; font-size: 16px; color: #417690; }
|
||||
.upload-card .form-row { margin-bottom: 16px; }
|
||||
.upload-card label {
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
margin-bottom: 4px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.upload-card select,
|
||||
.upload-card input[type=text],
|
||||
.upload-card input[type=file] {
|
||||
width: 100%;
|
||||
padding: 6px 8px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.upload-card .help { font-size: 11px; color: #888; margin-top: 4px; }
|
||||
.upload-card .separator {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
margin: 4px 0;
|
||||
position: relative;
|
||||
}
|
||||
.drop-zone {
|
||||
border: 2px dashed #417690;
|
||||
border-radius: 6px;
|
||||
padding: 32px 20px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: background .2s;
|
||||
background: #f9fbfc;
|
||||
}
|
||||
.drop-zone.dragover { background: #e8f3fa; border-color: #2c6a8e; }
|
||||
.drop-zone p { margin: 0; color: #417690; font-size: 14px; }
|
||||
.drop-zone small { color: #888; font-size: 12px; }
|
||||
#file-preview { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
|
||||
.preview-item {
|
||||
position: relative;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
.preview-item img { width: 100%; height: 100%; object-fit: cover; }
|
||||
.preview-item span {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: rgba(0,0,0,.55);
|
||||
color: #fff;
|
||||
font-size: 9px;
|
||||
padding: 2px 3px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.submit-row { display: flex; gap: 10px; margin-top: 20px; }
|
||||
.btn-upload {
|
||||
background: #417690;
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 10px 22px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn-upload:hover { background: #2c6a8e; }
|
||||
.btn-again {
|
||||
background: #f0f0f0;
|
||||
color: #333;
|
||||
border: 1px solid #ccc;
|
||||
padding: 10px 18px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
#file-count { font-size: 12px; color: #417690; margin-top: 6px; }
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<div class="breadcrumbs">
|
||||
<a href="{% url 'admin:index' %}">Accueil</a> ›
|
||||
<a href="{% url 'admin:app_list' app_label='properties' %}">Properties</a> ›
|
||||
<a href="{% url 'admin:properties_property_changelist' %}">Biens immobiliers</a> ›
|
||||
<a href="{% url 'admin:properties_property_change' property.pk %}">{{ property.name_fr }}</a> ›
|
||||
Ajout de photos
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>📷 Ajout groupé de photos — <em>{{ property.name_fr }}</em></h1>
|
||||
|
||||
{% if messages %}
|
||||
{% for msg in messages %}
|
||||
<p class="success">{{ msg }}</p>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<form method="post" enctype="multipart/form-data" id="upload-form">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="upload-card">
|
||||
<h2>1. Sélectionner ou créer une pièce</h2>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="id_room">Pièce existante :</label>
|
||||
<select name="room" id="id_room">
|
||||
<option value="">— Photos générales (sans pièce) —</option>
|
||||
{% for room in rooms %}
|
||||
<option value="{{ room.pk }}">{{ room.name_fr }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="separator">— ou créer une nouvelle pièce —</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="id_new_room_fr">Nom français de la nouvelle pièce :</label>
|
||||
<input type="text" name="new_room_fr" id="id_new_room_fr"
|
||||
placeholder="ex : Cuisine, Salon, Chambre 1…"
|
||||
autocomplete="off">
|
||||
<p class="help">Si renseigné, cette pièce est créée et les photos lui sont attribuées (prioritaire sur la sélection ci-dessus).</p>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="id_new_room_en">Nom anglais (optionnel) :</label>
|
||||
<input type="text" name="new_room_en" id="id_new_room_en"
|
||||
placeholder="ex : Kitchen, Living room, Bedroom 1…"
|
||||
autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="upload-card">
|
||||
<h2>2. Choisir les photos</h2>
|
||||
|
||||
<div class="drop-zone" id="drop-zone">
|
||||
<p>📂 Glisser-déposer les photos ici</p>
|
||||
<small>ou cliquer pour sélectionner (sélection multiple possible)</small>
|
||||
<input type="file" name="images" id="id_images"
|
||||
multiple accept="image/*"
|
||||
style="position:absolute;opacity:0;width:0;height:0">
|
||||
</div>
|
||||
<div id="file-count"></div>
|
||||
<div id="file-preview"></div>
|
||||
</div>
|
||||
|
||||
<div class="submit-row">
|
||||
<button type="submit" name="next" value="change" class="btn-upload">
|
||||
✓ Télécharger et revenir à la fiche
|
||||
</button>
|
||||
<button type="submit" name="next" value="again" class="btn-again">
|
||||
✓ Télécharger et ajouter une autre pièce
|
||||
</button>
|
||||
<a href="{% url 'admin:properties_property_change' property.pk %}"
|
||||
style="padding:10px 16px;font-size:14px;color:#666">
|
||||
Annuler
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const input = document.getElementById('id_images');
|
||||
const preview = document.getElementById('file-preview');
|
||||
const fileCount = document.getElementById('file-count');
|
||||
let dt = new DataTransfer();
|
||||
|
||||
// Ouvrir le sélecteur au clic sur la zone
|
||||
dropZone.addEventListener('click', () => input.click());
|
||||
|
||||
// Drag-and-drop
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('dragover');
|
||||
});
|
||||
dropZone.addEventListener('dragleave', () => dropZone.classList.remove('dragover'));
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('dragover');
|
||||
addFiles(e.dataTransfer.files);
|
||||
});
|
||||
|
||||
input.addEventListener('change', () => {
|
||||
addFiles(input.files);
|
||||
input.value = '';
|
||||
});
|
||||
|
||||
function addFiles(files) {
|
||||
Array.from(files).forEach(f => {
|
||||
if (!f.type.startsWith('image/')) return;
|
||||
dt.items.add(f);
|
||||
showThumb(f);
|
||||
});
|
||||
syncInput();
|
||||
}
|
||||
|
||||
function showThumb(file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'preview-item';
|
||||
div.innerHTML = `<img src="${e.target.result}" alt="${file.name}">
|
||||
<span>${file.name}</span>`;
|
||||
preview.appendChild(div);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
|
||||
function syncInput() {
|
||||
input.files = dt.files;
|
||||
const n = dt.files.length;
|
||||
fileCount.textContent = n > 0 ? `${n} photo(s) sélectionnée(s)` : '';
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,96 @@
|
||||
{% load i18n static property_tags %}<!DOCTYPE html>
|
||||
<html lang="{{ LANGUAGE_CODE }}" dir="ltr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="{% block meta_description %}{% trans "Biens immobiliers à vendre et à louer — LOGIS" %}{% endblock %}">
|
||||
{% block meta_keywords_tag %}{% endblock %}
|
||||
<title>{% block title %}LOGIS{% endblock %} — {% trans "Immobilier" %}</title>
|
||||
|
||||
<link rel="canonical" href="{% block canonical %}{{ request.build_absolute_uri }}{% endblock %}">
|
||||
|
||||
<meta property="og:site_name" content="LOGIS PERSONNEL">
|
||||
<meta property="og:type" content="{% block og_type %}website{% endblock %}">
|
||||
<meta property="og:locale" content="{{ LANGUAGE_CODE }}">
|
||||
<meta property="og:title" content="{% block og_title %}LOGIS — {% trans 'Immobilier' %}{% endblock %}">
|
||||
<meta property="og:description" content="{% block og_description %}{% trans "Biens immobiliers à vendre et à louer — LOGIS" %}{% endblock %}">
|
||||
<meta property="og:url" content="{{ request.build_absolute_uri }}">
|
||||
{% block og_image_tag %}{% endblock %}
|
||||
{% block extra_head %}{% endblock %}
|
||||
|
||||
{# Alternate links hreflang pour le SEO bilingue — URLs réelles traduites #}
|
||||
{% get_available_languages as LANGUAGES %}
|
||||
{% for lang_code, lang_name in LANGUAGES %}
|
||||
{% lang_switch_url lang_code as hreflang_url %}
|
||||
<link rel="alternate" hreflang="{{ lang_code }}" href="{{ request.scheme }}://{{ request.get_host }}{{ hreflang_url }}">
|
||||
{% endfor %}
|
||||
|
||||
<link rel="icon" type="image/svg+xml" href="{% static 'images/favicon.svg' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/main.css' %}">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
{# ── Navigation ─────────────────────────────────────────────────────────── #}
|
||||
<header class="site-header">
|
||||
<nav class="navbar container">
|
||||
<a href="{% url 'properties:list' %}" class="navbar-brand" aria-label="{% trans 'Accueil LOGIS' %}">
|
||||
<span class="brand-icon">🏠</span>
|
||||
<span class="brand-name">LOGIS PERSONNEL</span>
|
||||
</a>
|
||||
|
||||
<div class="navbar-actions">
|
||||
{# Sélecteur de langue — un formulaire par langue avec next pré-traduit #}
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
{% get_available_languages as LANGUAGES %}
|
||||
<div class="lang-switcher" role="group" aria-label="{% trans 'Choisir la langue' %}">
|
||||
{% for lang_code, lang_name in LANGUAGES %}
|
||||
{% lang_switch_url lang_code as target_url %}
|
||||
<form action="{% url 'set_language' %}" method="post" style="display:inline">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="next" value="{{ target_url }}">
|
||||
<button type="submit" name="language" value="{{ lang_code }}"
|
||||
class="lang-btn {% if lang_code == LANGUAGE_CODE %}active{% endif %}"
|
||||
aria-pressed="{% if lang_code == LANGUAGE_CODE %}true{% else %}false{% endif %}">
|
||||
{{ lang_code|upper }}
|
||||
</button>
|
||||
</form>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
{# ── Contenu principal ───────────────────────────────────────────────────── #}
|
||||
<main id="main-content">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
{# ── Pied de page ────────────────────────────────────────────────────────── #}
|
||||
<footer class="site-footer">
|
||||
<div class="container footer-inner">
|
||||
<p class="footer-brand">LOGIS PERSONNEL — {% trans "Transactions immobilières" %}</p>
|
||||
<p class="footer-copy">© {% now "Y" %} LOGIS PERSONNEL. {% trans "Tous droits réservés." %}</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
{# ── Modal de la galerie (instanciée par Gallery.js) ────────────────────── #}
|
||||
<div id="gallery-modal" class="gallery-modal" role="dialog" aria-modal="true" aria-label="{% trans 'Galerie photos' %}" hidden>
|
||||
<div class="gallery-modal-overlay" id="gallery-overlay"></div>
|
||||
<div class="gallery-modal-content">
|
||||
<button class="gallery-modal-close" id="gallery-close" aria-label="{% trans 'Fermer' %}">×</button>
|
||||
<button class="gallery-modal-nav prev" id="gallery-prev" aria-label="{% trans 'Photo précédente' %}">‹</button>
|
||||
<figure class="gallery-modal-figure">
|
||||
<img id="gallery-modal-img" src="" alt="">
|
||||
<figcaption id="gallery-modal-caption" class="gallery-modal-caption"></figcaption>
|
||||
</figure>
|
||||
<button class="gallery-modal-nav next" id="gallery-next" aria-label="{% trans 'Photo suivante' %}">›</button>
|
||||
<div id="gallery-counter" class="gallery-modal-counter"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="{% static 'js/Gallery.js' %}"></script>
|
||||
<script src="{% static 'js/ContactForm.js' %}"></script>
|
||||
<script src="{% static 'js/main.js' %}"></script>
|
||||
{% block extra_js %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,52 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Demande de contact LOGIS</title>
|
||||
</head>
|
||||
<body style="font-family:Arial,sans-serif;background:#f4f4f4;padding:20px;">
|
||||
<div style="max-width:600px;margin:0 auto;background:#fff;border-radius:8px;overflow:hidden;">
|
||||
<div style="background:#1a1a2e;color:#fff;padding:20px 30px;">
|
||||
<h1 style="margin:0;font-size:22px;">LOGIS — Nouvelle demande de contact</h1>
|
||||
</div>
|
||||
<div style="padding:30px;">
|
||||
<table style="width:100%;border-collapse:collapse;">
|
||||
<tr>
|
||||
<td style="padding:8px 0;color:#666;width:120px;">Date</td>
|
||||
<td style="padding:8px 0;font-weight:bold;">{{ contact.created_at|date:"d/m/Y à H:i" }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:8px 0;color:#666;">Nom</td>
|
||||
<td style="padding:8px 0;font-weight:bold;">{{ contact.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:8px 0;color:#666;">Email</td>
|
||||
<td style="padding:8px 0;">
|
||||
<a href="mailto:{{ contact.email }}" style="color:#e94560;">{{ contact.email }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% if contact.phone %}
|
||||
<tr>
|
||||
<td style="padding:8px 0;color:#666;">Téléphone</td>
|
||||
<td style="padding:8px 0;">{{ contact.phone }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if property %}
|
||||
<tr>
|
||||
<td style="padding:8px 0;color:#666;">Bien</td>
|
||||
<td style="padding:8px 0;">{{ property.name_fr }} — {{ property.city }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
|
||||
<div style="margin-top:20px;background:#f9f9f9;border-left:4px solid #e94560;padding:15px 20px;border-radius:0 4px 4px 0;">
|
||||
<p style="margin:0 0 8px;color:#666;font-size:13px;text-transform:uppercase;letter-spacing:.5px;">Message</p>
|
||||
<p style="margin:0;white-space:pre-wrap;">{{ contact.message }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div style="background:#f0f0f0;padding:15px 30px;text-align:center;color:#999;font-size:12px;">
|
||||
LOGIS{% if SITE_DOMAIN %} — {{ SITE_DOMAIN }}{% endif %} • IP : {{ contact.ip_address|default:"—" }}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,14 @@
|
||||
LOGIS — Nouvelle demande de contact
|
||||
====================================
|
||||
|
||||
Date : {{ contact.created_at|date:"d/m/Y H:i" }}
|
||||
Nom : {{ contact.name }}
|
||||
Email : {{ contact.email }}
|
||||
Téléphone : {{ contact.phone|default:"—" }}
|
||||
{% if property %}Bien : {{ property.name_fr }} ({{ property.city }})
|
||||
{% endif %}
|
||||
Adresse IP : {{ contact.ip_address|default:"—" }}
|
||||
|
||||
Message :
|
||||
---------
|
||||
{{ contact.message }}
|
||||
@@ -0,0 +1 @@
|
||||
{% load i18n %}[LOGIS] Demande de renseignements de {{ contact.name }}{% if property %} — {{ property.name_fr }}{% endif %}
|
||||
@@ -0,0 +1,124 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n static property_tags %}
|
||||
|
||||
{% block title %}{% trans "Biens immobiliers" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{# ── Hero ────────────────────────────────────────────────────────────────── #}
|
||||
{% if properties.count == 1 %}
|
||||
{% with prop=properties.first %}
|
||||
<section class="hero"
|
||||
{% if prop.thumbnail %}
|
||||
style="--hero-bg:url('{{ prop.thumbnail.url }}')"
|
||||
{% elif prop.cover_photo %}
|
||||
style="--hero-bg:url('{{ prop.cover_photo.image.url }}')"
|
||||
{% endif %}>
|
||||
<div class="hero-overlay"></div>
|
||||
<div class="hero-content container">
|
||||
<span class="hero-label">
|
||||
{% if prop.status == 'sale' %}{% trans "À vendre" %}{% else %}{% trans "À louer" %}{% endif %}
|
||||
</span>
|
||||
<h1 class="hero-title">{{ prop.name }}</h1>
|
||||
<p class="hero-meta">
|
||||
{{ prop.city }} • {{ prop.living_area }} m² • {{ prop.rooms_count }} {% trans "pièces" %}
|
||||
</p>
|
||||
{% if not prop.price_on_request %}
|
||||
<p class="hero-price">{{ prop.price|price_fr }}</p>
|
||||
{% else %}
|
||||
<p class="hero-price">{% trans "Prix sur demande" %}</p>
|
||||
{% endif %}
|
||||
<a href="{{ prop.get_absolute_url }}" class="btn btn-primary btn-hero">
|
||||
{% trans "Découvrir ce bien" %} →
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
<section class="hero hero-generic">
|
||||
<div class="hero-overlay"></div>
|
||||
<div class="hero-content container">
|
||||
<h1 class="hero-title">{% trans "Nos biens immobiliers" %}</h1>
|
||||
<p class="hero-subtitle">{% trans "Trouvez votre futur chez-vous" %}</p>
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
{# ── Grille des biens ────────────────────────────────────────────────────── #}
|
||||
<section class="properties-section">
|
||||
<div class="container">
|
||||
|
||||
{% if not properties %}
|
||||
<div class="empty-state">
|
||||
<p>{% trans "Aucun bien immobilier disponible pour le moment." %}</p>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="properties-grid">
|
||||
{% for prop in properties %}
|
||||
<article class="property-card" data-status="{{ prop.status }}">
|
||||
<a href="{{ prop.get_absolute_url }}" class="property-card-link">
|
||||
|
||||
<div class="property-card-img-wrapper">
|
||||
{% if prop.thumbnail %}
|
||||
<img src="{{ prop.thumbnail.url }}"
|
||||
alt="{{ prop.name }}"
|
||||
class="property-card-img"
|
||||
loading="lazy">
|
||||
{% elif prop.cover_photo %}
|
||||
<img src="{{ prop.cover_photo.image.url }}"
|
||||
alt="{{ prop.name }}"
|
||||
class="property-card-img"
|
||||
loading="lazy">
|
||||
{% else %}
|
||||
<div class="property-card-img-placeholder">🏠</div>
|
||||
{% endif %}
|
||||
<span class="property-card-status
|
||||
{% if prop.status == 'sale' %}status-sale{% else %}status-rent{% endif %}">
|
||||
{% if prop.status == 'sale' %}{% trans "À vendre" %}{% else %}{% trans "À louer" %}{% endif %}
|
||||
</span>
|
||||
{% if prop.energy_class %}
|
||||
<span class="property-card-dpe" style="background:{{ prop.energy_class|dpe_color }}">
|
||||
{{ prop.energy_class }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="property-card-body">
|
||||
<h2 class="property-card-title">{{ prop.name }}</h2>
|
||||
<p class="property-card-location">📍 {{ prop.city }} ({{ prop.postal_code }})</p>
|
||||
|
||||
<ul class="property-card-specs">
|
||||
<li title="{% trans 'Surface habitable' %}">
|
||||
<span class="spec-icon">⬜</span> {{ prop.living_area }} m²
|
||||
</li>
|
||||
<li title="{% trans 'Pièces' %}">
|
||||
<span class="spec-icon">🚪</span> {{ prop.rooms_count }} {% trans "p." %}
|
||||
</li>
|
||||
<li title="{% trans 'Chambres' %}">
|
||||
<span class="spec-icon">🛏</span> {{ prop.bedrooms_count }}
|
||||
</li>
|
||||
<li title="{% trans 'Salles d\'eau' %}">
|
||||
<span class="spec-icon">🚿</span> {{ prop.bathrooms_count }}
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p class="property-card-price">
|
||||
{% if prop.price_on_request %}
|
||||
{% trans "Prix sur demande" %}
|
||||
{% else %}
|
||||
{{ prop.price|price_fr }}
|
||||
{% if prop.status == 'rent' %}<small>{% trans "/ mois" %}</small>{% endif %}
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</a>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,352 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n static property_tags %}
|
||||
|
||||
{% block title %}{{ property.seo_title }}{% endblock %}
|
||||
{% block meta_description %}{{ property.seo_description }}{% endblock %}
|
||||
{% block meta_keywords_tag %}<meta name="keywords" content="{{ property.seo_keywords }}">{% endblock %}
|
||||
{% block canonical %}{{ request.build_absolute_uri }}{% endblock %}
|
||||
{% block og_title %}{{ property.seo_title }}{% endblock %}
|
||||
{% block og_description %}{{ property.seo_description }}{% endblock %}
|
||||
{% block og_type %}website{% endblock %}
|
||||
{% block og_image_tag %}{% if property.thumbnail %}<meta property="og:image" content="{{ request.scheme }}://{{ request.get_host }}{{ property.thumbnail.url }}">{% elif property.cover_photo %}<meta property="og:image" content="{{ request.scheme }}://{{ request.get_host }}{{ property.cover_photo.image.url }}">{% endif %}{% endblock %}
|
||||
{% block extra_head %}
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "RealEstateListing",
|
||||
"name": "{{ property.name|escapejs }}",
|
||||
"description": "{{ property.seo_description|escapejs }}",
|
||||
"url": "{{ request.build_absolute_uri }}",
|
||||
{% if property.thumbnail %}"image": "{{ request.scheme }}://{{ request.get_host }}{{ property.thumbnail.url }}",{% elif property.cover_photo %}"image": "{{ request.scheme }}://{{ request.get_host }}{{ property.cover_photo.image.url }}",{% endif %}
|
||||
"address": {
|
||||
"@type": "PostalAddress",
|
||||
"streetAddress": "{{ property.address|escapejs }}",
|
||||
"addressLocality": "{{ property.city|escapejs }}",
|
||||
"postalCode": "{{ property.postal_code|escapejs }}",
|
||||
"addressCountry": "FR"
|
||||
}{% if not property.price_on_request %},
|
||||
"offers": {
|
||||
"@type": "Offer",
|
||||
"price": "{{ property.price }}",
|
||||
"priceCurrency": "EUR"
|
||||
}{% endif %}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="detail-page">
|
||||
|
||||
{# ── En-tête du bien ───────────────────────────────────────────────────── #}
|
||||
<section class="detail-header container">
|
||||
<div class="detail-header-meta">
|
||||
<a href="{% url 'properties:list' %}" class="back-link">
|
||||
← {% trans "Retour aux annonces" %}
|
||||
</a>
|
||||
<span class="detail-status
|
||||
{% if property.status == 'sale' %}status-sale{% else %}status-rent{% endif %}">
|
||||
{% if property.status == 'sale' %}{% trans "À vendre" %}{% else %}{% trans "À louer" %}{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
<h1 class="detail-title">{{ property.name }}</h1>
|
||||
<p class="detail-location">
|
||||
📍 {{ property.address }}, {{ property.city }} ({{ property.postal_code }})
|
||||
</p>
|
||||
<p class="detail-price">
|
||||
{% if property.price_on_request %}
|
||||
{% trans "Prix sur demande" %}
|
||||
{% else %}
|
||||
{{ property.price|price_fr }}
|
||||
{% if property.status == 'rent' %}<small>{% trans "/ mois" %}</small>{% endif %}
|
||||
{% endif %}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{# ── Galerie photos ────────────────────────────────────────────────────── #}
|
||||
<section class="detail-gallery container" id="gallery-section">
|
||||
|
||||
{# Onglets par pièce #}
|
||||
{% if rooms_with_photos or general_photos %}
|
||||
<div class="gallery-tabs" role="tablist" aria-label="{% trans 'Photos par pièce' %}">
|
||||
{% if general_photos %}
|
||||
<button class="gallery-tab active" role="tab" aria-selected="true"
|
||||
data-room="general" aria-controls="room-general">
|
||||
{% trans "Toutes" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% for room in rooms_with_photos %}
|
||||
{% if room.photos.all %}
|
||||
<button class="gallery-tab {% if not general_photos and forloop.first %}active{% endif %}"
|
||||
role="tab" aria-selected="{% if not general_photos and forloop.first %}true{% else %}false{% endif %}"
|
||||
data-room="{{ room.pk }}" aria-controls="room-{{ room.pk }}">
|
||||
{{ room.name }}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{# Grille photos — général #}
|
||||
{% if general_photos %}
|
||||
<div class="gallery-grid active" id="room-general" role="tabpanel">
|
||||
{% for photo in general_photos %}
|
||||
<button class="gallery-item" data-gallery="main" data-index="{{ forloop.counter0 }}"
|
||||
aria-label="{{ photo.caption|default:property.name }}">
|
||||
<img src="{{ photo.image.url }}" alt="{{ photo.caption|default:property.name }}"
|
||||
loading="lazy" class="gallery-thumb">
|
||||
{% if photo.caption %}<span class="gallery-item-caption">{{ photo.caption }}</span>{% endif %}
|
||||
</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Grille photos — par pièce #}
|
||||
{% for room in rooms_with_photos %}
|
||||
{% if room.photos.all %}
|
||||
<div class="gallery-grid {% if not general_photos and forloop.first %}active{% endif %}"
|
||||
id="room-{{ room.pk }}" role="tabpanel">
|
||||
{% for photo in room.photos.all %}
|
||||
<button class="gallery-item" data-gallery="room-{{ room.pk }}" data-index="{{ forloop.counter0 }}"
|
||||
aria-label="{{ photo.caption|default:room.name }}">
|
||||
<img src="{{ photo.image.url }}" alt="{{ photo.caption|default:room.name }}"
|
||||
loading="lazy" class="gallery-thumb">
|
||||
{% if photo.caption %}<span class="gallery-item-caption">{{ photo.caption }}</span>{% endif %}
|
||||
</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p class="no-photos">{% trans "Aucune photo disponible." %}</p>
|
||||
{% endif %}
|
||||
|
||||
</section>
|
||||
|
||||
{# ── Corps — infos + description ───────────────────────────────────────── #}
|
||||
<div class="detail-body container">
|
||||
<div class="detail-main">
|
||||
|
||||
{# Description #}
|
||||
<section class="detail-section">
|
||||
<h2 class="section-title">{% trans "Description" %}</h2>
|
||||
<div class="detail-description">
|
||||
{{ property.description|linebreaks }}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{# Environnement #}
|
||||
{% if property.environment %}
|
||||
<section class="detail-section">
|
||||
<h2 class="section-title">{% trans "Situation géographique" %}</h2>
|
||||
|
||||
<p class="env-type">
|
||||
{% with ltype=property.environment.location_type %}
|
||||
{% if ltype == 'city' %}🏙 {% trans "Environnement urbain" %}
|
||||
{% elif ltype == 'village' %}🏘 {% trans "Village" %}
|
||||
{% else %}🌿 {% trans "Campagne" %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</p>
|
||||
|
||||
{% if property.environment.description %}
|
||||
<p class="env-description">{{ property.environment.description }}</p>
|
||||
{% endif %}
|
||||
|
||||
<div class="env-services">
|
||||
<div class="env-group">
|
||||
<h3 class="env-group-title">🛒 {% trans "Commerces de proximité" %}</h3>
|
||||
<ul class="env-list">
|
||||
{% for icon, label, available in property.environment.get_nearby_shops %}
|
||||
<li class="env-item {% if available %}env-available{% else %}env-unavailable{% endif %}">
|
||||
<span class="env-icon">{{ icon }}</span>
|
||||
{{ label }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="env-group">
|
||||
<h3 class="env-group-title">🏛️ {% trans "Services publics" %}</h3>
|
||||
<ul class="env-list">
|
||||
{% for icon, label, available in property.environment.get_public_services %}
|
||||
<li class="env-item {% if available %}env-available{% else %}env-unavailable{% endif %}">
|
||||
<span class="env-icon">{{ icon }}</span>
|
||||
{{ label }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="env-group">
|
||||
<h3 class="env-group-title">🩺 {% trans "Santé" %}</h3>
|
||||
<ul class="env-list">
|
||||
{% for icon, label, available in property.environment.get_health_services %}
|
||||
<li class="env-item {% if available %}env-available{% else %}env-unavailable{% endif %}">
|
||||
<span class="env-icon">{{ icon }}</span>
|
||||
{{ label }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
</div>{# /detail-main #}
|
||||
|
||||
{# ── Fiche récapitulative (sidebar) ──────────────────────────────────── #}
|
||||
<aside class="detail-sidebar">
|
||||
|
||||
<div class="sidebar-card">
|
||||
<h2 class="sidebar-title">{% trans "Informations clés" %}</h2>
|
||||
<dl class="key-info">
|
||||
<dt>{% trans "Type de bien" %}</dt>
|
||||
<dd>{{ property.get_property_type_display }}</dd>
|
||||
|
||||
<dt>{% trans "Surface habitable" %}</dt>
|
||||
<dd>{{ property.living_area }} m²</dd>
|
||||
|
||||
<dt>{% trans "Surface totale" %}</dt>
|
||||
<dd>{{ property.total_area }} m²</dd>
|
||||
|
||||
{% if property.total_land_area %}
|
||||
<dt>{% trans "Surface terrain" %}</dt>
|
||||
<dd>{{ property.total_land_area }} m²</dd>
|
||||
{% endif %}
|
||||
|
||||
<dt>{% trans "Nombre de pièces" %}</dt>
|
||||
<dd>{{ property.rooms_count }}</dd>
|
||||
|
||||
<dt>{% trans "Chambres" %}</dt>
|
||||
<dd>{{ property.bedrooms_count }}</dd>
|
||||
|
||||
<dt>{% trans "Salles d'eau" %}</dt>
|
||||
<dd>{{ property.bathrooms_count }}</dd>
|
||||
|
||||
{% if property.exposure %}
|
||||
<dt>{% trans "Exposition" %}</dt>
|
||||
<dd>{{ property.get_exposure_display }}</dd>
|
||||
{% endif %}
|
||||
|
||||
{% if property.construction_year %}
|
||||
<dt>{% trans "Année de construction" %}</dt>
|
||||
<dd>{{ property.construction_year }}</dd>
|
||||
{% endif %}
|
||||
|
||||
<dt>{% trans "État du bien" %}</dt>
|
||||
<dd>{{ property.get_condition_display }}</dd>
|
||||
|
||||
<dt>{% trans "Ascenseur" %}</dt>
|
||||
<dd>{% if property.has_elevator %}{% trans "Oui" %}{% else %}{% trans "Non" %}{% endif %}</dd>
|
||||
</dl>
|
||||
|
||||
{# Extérieurs #}
|
||||
{% if property.has_garden or property.has_pool or property.has_dependencies %}
|
||||
<div class="exterior-badges">
|
||||
{% if property.has_garden %}
|
||||
<span class="badge badge-exterior">🌳 {% trans "Jardin" %}</span>
|
||||
{% endif %}
|
||||
{% if property.has_pool %}
|
||||
<span class="badge badge-exterior">🏊 {% trans "Piscine" %}</span>
|
||||
{% endif %}
|
||||
{% if property.has_dependencies %}
|
||||
<span class="badge badge-exterior">🏚 {% trans "Dépendances" %}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Caractéristiques libres #}
|
||||
{% if property.features.all %}
|
||||
<div class="features-list">
|
||||
<h3 class="features-title">{% trans "Caractéristiques" %}</h3>
|
||||
<ul>
|
||||
{% for feat in property.features.all %}
|
||||
<li>
|
||||
{% if feat.icon %}<span>{{ feat.icon }}</span> {% endif %}
|
||||
{{ feat.name }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# Diagnostics DPE / GES #}
|
||||
{% if property.energy_class or property.ges_class %}
|
||||
<div class="sidebar-card diagnostics-card">
|
||||
<h2 class="sidebar-title">{% trans "Diagnostics" %}</h2>
|
||||
|
||||
{% if property.energy_class %}
|
||||
<div class="diag-row">
|
||||
<span class="diag-label">{% trans "Classe énergie (DPE)" %}</span>
|
||||
{% dpe_badge property.energy_class property.energy_value "kWh/m²/an" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if property.ges_class %}
|
||||
<div class="diag-row">
|
||||
<span class="diag-label">{% trans "Émissions GES" %}</span>
|
||||
{% dpe_badge property.ges_class property.ges_value "kgCO₂/m²/an" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="dpe-scale-wrapper">
|
||||
{% dpe_scale property.energy_class property.ges_class %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Bouton de contact #}
|
||||
<div class="sidebar-card contact-card">
|
||||
<h2 class="sidebar-title">{% trans "Renseignements" %}</h2>
|
||||
<p class="contact-intro">
|
||||
{% trans "Vous souhaitez visiter ce bien ou obtenir plus d'informations ?" %}
|
||||
</p>
|
||||
<button class="btn btn-primary btn-full" id="open-contact-modal"
|
||||
data-property="{{ property.pk }}">
|
||||
✉ {% trans "Envoyer un message" %}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
</div>{# /detail-body #}
|
||||
|
||||
</div>{# /detail-page #}
|
||||
|
||||
{# ── Modal de contact ─────────────────────────────────────────────────────── #}
|
||||
{% include "properties/partials/contact_modal.html" %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
// Données des galeries transmises au JS
|
||||
const GALLERY_DATA = {
|
||||
{% if general_photos %}
|
||||
'main': [
|
||||
{% for photo in general_photos %}
|
||||
{ src: '{{ photo.image.url }}', caption: '{{ photo.caption|escapejs }}' }{% if not forloop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
],
|
||||
{% endif %}
|
||||
{% for room in rooms_with_photos %}
|
||||
{% if room.photos.all %}
|
||||
'room-{{ room.pk }}': [
|
||||
{% for photo in room.photos.all %}
|
||||
{ src: '{{ photo.image.url }}', caption: '{{ photo.caption|escapejs }}' }{% if not forloop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
]{% if not forloop.last %},{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
};
|
||||
const CONTACT_URL = '{% url "properties:contact" %}';
|
||||
const CSRF_TOKEN = '{{ csrf_token }}';
|
||||
const I18N = {
|
||||
of: '{% trans "sur" %}',
|
||||
sending: '{% trans "Envoi en cours…" %}',
|
||||
send: '{% trans "Envoyer" %}',
|
||||
successTitle: '{% trans "Message envoyé !" %}',
|
||||
successMsg: '{% trans "Nous vous répondrons dans les plus brefs délais." %}',
|
||||
};
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,73 @@
|
||||
{% load i18n %}
|
||||
{# Modale de contact — injectée dans la page detail #}
|
||||
<div id="contact-modal" class="modal" role="dialog" aria-modal="true"
|
||||
aria-labelledby="contact-modal-title" hidden>
|
||||
<div class="modal-overlay" id="contact-overlay"></div>
|
||||
<div class="modal-box">
|
||||
<button class="modal-close" id="contact-modal-close" aria-label="{% trans 'Fermer' %}">×</button>
|
||||
|
||||
<h2 id="contact-modal-title" class="modal-title">
|
||||
✉ {% trans "Demande de renseignements" %}
|
||||
</h2>
|
||||
|
||||
{# Zone de retour message succès #}
|
||||
<div id="contact-success" class="contact-success" hidden>
|
||||
<p class="success-icon">✓</p>
|
||||
<p class="success-title">{% trans "Message envoyé !" %}</p>
|
||||
<p class="success-body">{% trans "Nous vous répondrons dans les plus brefs délais." %}</p>
|
||||
</div>
|
||||
|
||||
{# Formulaire #}
|
||||
<form id="contact-form" class="contact-form" novalidate>
|
||||
{% csrf_token %}
|
||||
|
||||
{# Honeypot invisible #}
|
||||
<div class="hp-field" aria-hidden="true">
|
||||
<input type="text" name="website" id="id_website" tabindex="-1" autocomplete="off">
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="property_id" id="id_property_id">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="id_name">{% trans "Nom complet" %} <span class="required">*</span></label>
|
||||
<input type="text" id="id_name" name="name" required
|
||||
autocomplete="name" maxlength="200"
|
||||
placeholder="{% trans 'Votre nom et prénom' %}">
|
||||
<span class="field-error" data-field="name"></span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="id_email">{% trans "Adresse email" %} <span class="required">*</span></label>
|
||||
<input type="email" id="id_email" name="email" required
|
||||
autocomplete="email"
|
||||
placeholder="{% trans 'votre@email.com' %}">
|
||||
<span class="field-error" data-field="email"></span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="id_phone">{% trans "Téléphone" %} <span class="optional">({% trans "facultatif" %})</span></label>
|
||||
<input type="tel" id="id_phone" name="phone"
|
||||
autocomplete="tel" maxlength="30"
|
||||
placeholder="+33 6 XX XX XX XX">
|
||||
<span class="field-error" data-field="phone"></span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="id_message">{% trans "Message" %} <span class="required">*</span></label>
|
||||
<textarea id="id_message" name="message" required rows="5"
|
||||
placeholder="{% trans 'Votre message, vos questions…' %}"></textarea>
|
||||
<span class="field-error" data-field="message"></span>
|
||||
</div>
|
||||
|
||||
<p class="form-notice">
|
||||
{% trans "* champs obligatoires — vos coordonnées ne seront utilisées que pour répondre à votre demande." %}
|
||||
</p>
|
||||
|
||||
<div class="form-global-error" id="contact-global-error" hidden></div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-full" id="contact-submit">
|
||||
{% trans "Envoyer" %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,14 @@
|
||||
{% load i18n property_tags %}
|
||||
{# Échelle visuelle DPE / GES style barreaux #}
|
||||
<div class="dpe-scale">
|
||||
<p class="dpe-scale-label">{% trans "Échelle de performance" %}</p>
|
||||
{% for cls in classes %}
|
||||
<div class="dpe-bar {% if cls == energy_class %}dpe-active{% endif %}"
|
||||
style="background:{{ dpe_colors|get_item:cls }};width:calc(30px + {{ forloop.counter }}*22px)">
|
||||
<span>{{ cls }}</span>
|
||||
{% if cls == energy_class %}
|
||||
<span class="dpe-arrow">◀ {% trans "DPE" %}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
@@ -0,0 +1,6 @@
|
||||
User-agent: *
|
||||
Disallow: /admin/
|
||||
Disallow: /i18n/
|
||||
Allow: /
|
||||
|
||||
Sitemap: {{ request.scheme }}://{{ request.get_host }}/sitemap.xml
|
||||
Reference in New Issue
Block a user