06/18 Phase 7
This commit is contained in:
@@ -4,6 +4,16 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{% block title %}Classifieds{% endblock %}</title>
|
||||
{% block meta_description %}<meta name="description" content="Classifieds — buy, sell, find jobs and services in your local Vietnamese and Hispanic community across the US.">{% endblock %}
|
||||
{# Open Graph #}
|
||||
<meta property="og:site_name" content="Classifieds">
|
||||
<meta property="og:title" content="{% block og_title %}Classifieds{% endblock %}">
|
||||
<meta property="og:description" content="{% block og_description %}Buy, sell, find jobs and services in your local community.{% endblock %}">
|
||||
<meta property="og:type" content="{% block og_type %}website{% endblock %}">
|
||||
{% block og_image %}{% endblock %}
|
||||
{% block og_url %}<meta property="og:url" content="{{ request.url }}">{% endblock %}
|
||||
{# Canonical #}
|
||||
<link rel="canonical" href="{{ request.base_url }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
@@ -12,6 +22,7 @@
|
||||
{% set slot = "header" %}{% include "ads/_slot.html" %}
|
||||
<div class="wrap">
|
||||
<a class="brand" href="{{ url_for('main.index') }}">Classifieds</a>
|
||||
<button class="nav-toggle" aria-label="{{ _('Menu') }}" onclick="document.querySelector('.nav').classList.toggle('open')">☰</button>
|
||||
<nav class="nav">
|
||||
<a href="{{ url_for('listings.browse') }}">{{ _('Browse') }}</a>
|
||||
<a href="{{ url_for('payments.pricing') }}">{{ _('Pricing') }}</a>
|
||||
@@ -60,5 +71,19 @@
|
||||
{% set slot = "footer" %}{% include "ads/_slot.html" %}
|
||||
<div class="wrap">© {{ 2025 }} Classifieds · <a href="{{ url_for('ads.sponsor_directory') }}">{{ _('Sponsors') }}</a></div>
|
||||
</footer>
|
||||
{% block scripts %}{% endblock %}
|
||||
<script>
|
||||
// Auto-dismiss flash toasts after 5 s
|
||||
document.querySelectorAll('.flash').forEach(function(el) {
|
||||
setTimeout(function(){ el.style.opacity='0'; setTimeout(function(){ el.remove(); }, 400); }, 5000);
|
||||
el.style.transition = 'opacity .4s';
|
||||
});
|
||||
// Close mobile nav on link click
|
||||
document.querySelectorAll('.nav a').forEach(function(a) {
|
||||
a.addEventListener('click', function() {
|
||||
document.querySelector('.nav').classList.remove('open');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ _('Classifieds — Home') }}{% endblock %}
|
||||
{% block title %}{{ _('Classifieds — Buy, Sell & Find Services') }}{% endblock %}
|
||||
{% block meta_description %}<meta name="description" content="{{ _('Free classifieds for Vietnamese and Hispanic communities across the US. Buy, sell, find jobs, services, and more.') }}">{% endblock %}
|
||||
{% block content %}
|
||||
<section class="hero">
|
||||
<h1>{{ _('Find what you need. Post what you offer.') }}</h1>
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ _('Browse listings') }}{% endblock %}
|
||||
{% block title %}
|
||||
{% if filters.get('q') %}{{ _('Results for "%(q)s"', q=filters.get('q')) }} — Classifieds
|
||||
{% elif filters.get('category') %}{{ _('Browse listings') }} — Classifieds
|
||||
{% else %}{{ _('Browse listings') }} — Classifieds
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% block meta_description %}<meta name="description" content="{{ _('Browse local classifieds — for sale, jobs, services, and more.') }}">{% endblock %}
|
||||
{% block content %}
|
||||
<div class="browse">
|
||||
<aside class="filters card">
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ listing.title }}{% endblock %}
|
||||
{% block title %}{{ listing.title }} — Classifieds{% endblock %}
|
||||
{% block meta_description %}<meta name="description" content="{{ (listing.body[:155] | replace('"', '"') | replace('\n', ' ')) }}...">{% endblock %}
|
||||
{% block og_title %}{{ listing.title }}{% endblock %}
|
||||
{% block og_description %}{{ listing.body[:200] | replace('\n', ' ') }}{% endblock %}
|
||||
{% block og_type %}product{% endblock %}
|
||||
{% block og_image %}
|
||||
{% if listing.images %}
|
||||
<meta property="og:image" content="{{ request.host_url }}{{ url_for('listings.media', rel=listing.images[0].path)[1:] }}">
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% block head %}
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org/",
|
||||
"@type": "Product",
|
||||
"name": {{ listing.title | tojson }},
|
||||
"description": {{ (listing.body[:500] | replace('\n', ' ')) | tojson }},
|
||||
{% if listing.images %}"image": "{{ request.host_url }}{{ url_for('listings.media', rel=listing.images[0].path)[1:] }}",{% endif %}
|
||||
{% if listing.price_cents %}"offers": {"@type": "Offer", "price": "{{ '%.2f'|format(listing.price_cents/100) }}", "priceCurrency": "USD"},{% endif %}
|
||||
"seller": {"@type": "Person", "name": {{ listing.user.display_name | tojson }}}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<article class="detail">
|
||||
<div class="detail-main card">
|
||||
@@ -17,7 +39,7 @@
|
||||
{% if listing.images %}
|
||||
<div class="gallery">
|
||||
{% for img in listing.images %}
|
||||
<img src="{{ url_for('listings.media', rel=img.path) }}" alt="">
|
||||
<img src="{{ url_for('listings.media', rel=img.path) }}" alt="{{ listing.title }}" loading="{{ 'eager' if loop.first else 'lazy' }}">
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -39,6 +61,12 @@
|
||||
<div class="seller">
|
||||
<strong>{{ listing.user.display_name }}</strong>
|
||||
{% if listing.user.verified %}<span class="badge ok">{{ _('Verified') }}</span>{% endif %}
|
||||
{% if listing_rating and listing_rating.avg %}
|
||||
<div class="review-stars" style="font-size:14px">
|
||||
{{ '★' * (listing_rating.avg | round | int) }}{{ '☆' * (5 - (listing_rating.avg | round | int)) }}
|
||||
<span class="muted" style="font-size:12px">({{ listing_rating.count }})</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if is_owner %}
|
||||
<a class="btn" href="{{ url_for('listings.edit', listing_id=listing.id) }}">{{ _('Edit') }}</a>
|
||||
@@ -78,7 +106,35 @@
|
||||
{{ _('Sign in to contact') }}
|
||||
</a>
|
||||
{% endif %}
|
||||
{# Leave-review button for sold listings when viewer had a conversation #}
|
||||
{% if current_user.is_authenticated and listing.status.value == 'sold'
|
||||
and not is_owner and can_review %}
|
||||
<a class="btn ghost" href="{{ url_for('listings.leave_review', listing_id=listing.id) }}">
|
||||
{{ _('Leave a review') }}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</aside>
|
||||
</article>
|
||||
|
||||
{# Reviews section #}
|
||||
{% if listing.reviews.count() %}
|
||||
<section class="card" style="margin-top:16px">
|
||||
<h3>{{ _('Reviews') }} ({{ listing.reviews.count() }})</h3>
|
||||
{% set rating_info = listing_rating %}
|
||||
{% if rating_info and rating_info.avg %}
|
||||
<div class="rating-summary">
|
||||
<span class="review-stars">{{ '★' * (rating_info.avg | round | int) }}{{ '☆' * (5 - (rating_info.avg | round | int)) }}</span>
|
||||
{{ '%.1f'|format(rating_info.avg) }} / 5 <span class="muted small">({{ rating_info.count }})</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% for review in listing.reviews.order_by(none).all() %}
|
||||
<div class="review-block">
|
||||
<div class="review-stars">{{ '★' * review.rating }}{{ '☆' * (5 - review.rating) }}</div>
|
||||
<div class="muted small">{{ review.author.display_name }} · {{ review.created_at.strftime('%Y-%m-%d') }}</div>
|
||||
{% if review.body %}<p>{{ review.body }}</p>{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ _('Leave a review') }} — {{ listing.title }}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="card narrow">
|
||||
<h2>{{ _('Leave a review for "%(t)s"', t=listing.title) }}</h2>
|
||||
<p class="muted">{{ _('Sold by %(name)s', name=listing.user.display_name) }}</p>
|
||||
|
||||
<form method="post">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
|
||||
<div class="field">
|
||||
<label>{{ _('Rating') }}</label>
|
||||
<div class="rating-picker">
|
||||
{% for v in [5, 4, 3, 2, 1] %}
|
||||
<label>
|
||||
<input type="radio" name="rating" value="{{ v }}" required>
|
||||
{{ '★' * v }}{{ '☆' * (5 - v) }}
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>{{ _('Comment (optional)') }}</label>
|
||||
<textarea class="input" name="body" rows="4" maxlength="1000"></textarea>
|
||||
</div>
|
||||
|
||||
<button class="btn" type="submit">{{ _('Submit review') }}</button>
|
||||
<a class="btn ghost" href="{{ url_for('listings.detail', listing_id=listing.id) }}">{{ _('Cancel') }}</a>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,43 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ cat.name }} {{ _('classifieds') }} — Classifieds{% endblock %}
|
||||
{% block meta_description %}<meta name="description" content="{{ _('Browse %(cat)s listings near you — buy, sell, and connect with your local community.', cat=cat.name) }}">{% endblock %}
|
||||
{% block og_title %}{{ cat.name }} {{ _('classifieds') }}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
<h1>{{ cat.name }} {{ _('listings') }}</h1>
|
||||
<p class="muted">
|
||||
{{ _('%(n)s active listings', n=listings|length) }} ·
|
||||
<a href="{{ url_for('listings.browse', category=cat.id) }}">{{ _('See all with filters') }}</a>
|
||||
</p>
|
||||
{% if not listings %}
|
||||
<p class="muted">{{ _('No listings yet in this category.') }}</p>
|
||||
{% else %}
|
||||
<div class="listing-grid">
|
||||
{% for l in listings %}
|
||||
<a class="listing-card" href="{{ url_for('listings.detail', listing_id=l.id) }}">
|
||||
{% if l.images %}
|
||||
<img src="{{ url_for('listings.media', rel=l.images[0].thumb_path) }}"
|
||||
alt="{{ l.title }}" loading="lazy">
|
||||
{% endif %}
|
||||
<div class="listing-info">
|
||||
<div class="listing-title">{{ l.title }}</div>
|
||||
{% if l.price_display %}<div class="price">{{ l.price_display }}</div>{% endif %}
|
||||
<div class="muted small">{{ l.city or '' }}{% if l.city and l.state %}, {% endif %}{{ l.state or '' }}</div>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if cat.children %}
|
||||
<h2>{{ _('Subcategories') }}</h2>
|
||||
<ul>
|
||||
{% for sub in cat.children %}
|
||||
{% if sub.is_active %}
|
||||
<li><a href="{{ url_for('listings.browse', category=sub.id) }}">{{ sub.name }}</a></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,32 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ state }} {{ _('classifieds') }} — Classifieds{% endblock %}
|
||||
{% block meta_description %}<meta name="description" content="{{ _('Browse local classifieds in %(state)s — buy, sell, find jobs and services.', state=state) }}">{% endblock %}
|
||||
{% block og_title %}{{ state }} {{ _('classifieds') }}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
<h1>{{ state }} {{ _('listings') }}</h1>
|
||||
<p class="muted">
|
||||
{{ _('%(n)s active listings', n=listings|length) }} ·
|
||||
<a href="{{ url_for('listings.browse', state=state) }}">{{ _('See all with filters') }}</a>
|
||||
</p>
|
||||
{% if not listings %}
|
||||
<p class="muted">{{ _('No listings in %(state)s yet.', state=state) }}</p>
|
||||
{% else %}
|
||||
<div class="listing-grid">
|
||||
{% for l in listings %}
|
||||
<a class="listing-card" href="{{ url_for('listings.detail', listing_id=l.id) }}">
|
||||
{% if l.images %}
|
||||
<img src="{{ url_for('listings.media', rel=l.images[0].thumb_path) }}"
|
||||
alt="{{ l.title }}" loading="lazy">
|
||||
{% endif %}
|
||||
<div class="listing-info">
|
||||
<div class="listing-title">{{ l.title }}</div>
|
||||
{% if l.price_display %}<div class="price">{{ l.price_display }}</div>{% endif %}
|
||||
<div class="muted small">{{ l.city or '' }}{% if l.city and l.state %}, {% endif %}{{ l.state or '' }}</div>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
{% for u in urls %}
|
||||
<url>
|
||||
<loc>{{ u.loc }}</loc>
|
||||
{% if u.get('lastmod') %}<lastmod>{{ u.lastmod }}</lastmod>{% endif %}
|
||||
<changefreq>{{ u.changefreq }}</changefreq>
|
||||
<priority>{{ u.priority }}</priority>
|
||||
</url>
|
||||
{% endfor %}
|
||||
</urlset>
|
||||
Reference in New Issue
Block a user