06/15 Phase 1 + 2 codes
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
{% macro field(f) %}
|
||||
<div class="field">
|
||||
{{ f.label }}
|
||||
{{ f(class="input") }}
|
||||
{% if f.errors %}
|
||||
<ul class="errors">
|
||||
{% for e in f.errors %}<li>{{ e }}</li>{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro turnstile_widget(enabled) %}
|
||||
{% if enabled and TURNSTILE_SITE_KEY %}
|
||||
<div class="cf-turnstile" data-sitekey="{{ TURNSTILE_SITE_KEY }}"></div>
|
||||
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
@@ -0,0 +1,19 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "auth/_macros.html" import field %}
|
||||
{% block title %}{{ _('Sign in') }}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="card narrow">
|
||||
<h2>{{ _('Sign in') }}</h2>
|
||||
<form method="post" novalidate>
|
||||
{{ form.hidden_tag() }}
|
||||
{{ field(form.email) }}
|
||||
{{ field(form.password) }}
|
||||
<label class="check">{{ form.remember() }} {{ form.remember.label.text }}</label>
|
||||
{{ form.submit(class="btn") }}
|
||||
</form>
|
||||
<p class="muted">
|
||||
<a href="{{ url_for('auth.reset_request') }}">{{ _('Forgot password?') }}</a> ·
|
||||
<a href="{{ url_for('auth.register') }}">{{ _('Create account') }}</a>
|
||||
</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,18 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "auth/_macros.html" import field, turnstile_widget %}
|
||||
{% block title %}{{ _('Register') }}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="card narrow">
|
||||
<h2>{{ _('Create account') }}</h2>
|
||||
<form method="post" novalidate>
|
||||
{{ form.hidden_tag() }}
|
||||
{{ field(form.display_name) }}
|
||||
{{ field(form.email) }}
|
||||
{{ field(form.password) }}
|
||||
{{ field(form.confirm) }}
|
||||
{{ turnstile_widget(turnstile) }}
|
||||
{{ form.submit(class="btn") }}
|
||||
</form>
|
||||
<p class="muted"><a href="{{ url_for('auth.login') }}">{{ _('Already have an account? Sign in') }}</a></p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,14 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "auth/_macros.html" import field %}
|
||||
{% block title %}{{ _('Set new password') }}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="card narrow">
|
||||
<h2>{{ _('Set new password') }}</h2>
|
||||
<form method="post" novalidate>
|
||||
{{ form.hidden_tag() }}
|
||||
{{ field(form.password) }}
|
||||
{{ field(form.confirm) }}
|
||||
{{ form.submit(class="btn") }}
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,13 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "auth/_macros.html" import field %}
|
||||
{% block title %}{{ _('Reset password') }}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="card narrow">
|
||||
<h2>{{ _('Reset password') }}</h2>
|
||||
<form method="post" novalidate>
|
||||
{{ form.hidden_tag() }}
|
||||
{{ field(form.email) }}
|
||||
{{ form.submit(class="btn") }}
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,52 @@
|
||||
<!doctype html>
|
||||
<html lang="{{ get_locale() }}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{% block title %}Classifieds{% endblock %}</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
<div class="wrap">
|
||||
<a class="brand" href="{{ url_for('main.index') }}">Classifieds</a>
|
||||
<nav class="nav">
|
||||
<a href="{{ url_for('listings.browse') }}">{{ _('Browse') }}</a>
|
||||
{% if current_user.is_authenticated %}
|
||||
<a href="{{ url_for('listings.create') }}">{{ _('Post') }}</a>
|
||||
<a href="{{ url_for('listings.mine') }}">{{ _('My listings') }}</a>
|
||||
<span class="hi">{{ current_user.display_name }}</span>
|
||||
<a href="{{ url_for('auth.logout') }}">{{ _('Sign out') }}</a>
|
||||
{% else %}
|
||||
<a href="{{ url_for('auth.login') }}">{{ _('Sign in') }}</a>
|
||||
<a class="btn" href="{{ url_for('auth.register') }}">{{ _('Register') }}</a>
|
||||
{% endif %}
|
||||
<span class="langs">
|
||||
{% for code in SUPPORTED_LOCALES %}
|
||||
<a href="{{ url_for('i18n.set_lang', code=code) }}"
|
||||
class="{{ 'on' if get_locale()|string == code else '' }}">{{ code|upper }}</a>
|
||||
{% endfor %}
|
||||
</span>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="wrap">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
<div class="flashes">
|
||||
{% for category, msg in messages %}
|
||||
<div class="flash flash-{{ category }}">{{ msg }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="wrap">© Classifieds — Phase 1 scaffold</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,3 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}403{% endblock %}
|
||||
{% block content %}<div class="card narrow"><h2>403</h2><p>{{ _('Forbidden — you do not have access.') }}</p><p><a href="{{ url_for('main.index') }}">{{ _('Back home') }}</a></p></div>{% endblock %}
|
||||
@@ -0,0 +1,3 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}404{% endblock %}
|
||||
{% block content %}<div class="card narrow"><h2>404</h2><p>{{ _('Not found.') }}</p><p><a href="{{ url_for('main.index') }}">{{ _('Back home') }}</a></p></div>{% endblock %}
|
||||
@@ -0,0 +1,3 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}500{% endblock %}
|
||||
{% block content %}<div class="card narrow"><h2>500</h2><p>{{ _('Something went wrong.') }}</p><p><a href="{{ url_for('main.index') }}">{{ _('Back home') }}</a></p></div>{% endblock %}
|
||||
@@ -0,0 +1,12 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ _('Classifieds — Home') }}{% endblock %}
|
||||
{% block content %}
|
||||
<section class="hero">
|
||||
<h1>{{ _('Find what you need. Post what you offer.') }}</h1>
|
||||
<p>{{ _('Buy, sell, request, hire, and connect across your community.') }}</p>
|
||||
<a class="btn btn-lg" href="{{ url_for('listings.browse') }}">{{ _('Browse listings') }}</a>
|
||||
{% if not current_user.is_authenticated %}
|
||||
<a class="btn btn-lg ghost" href="{{ url_for('auth.register') }}">{{ _('Get started') }}</a>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,68 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ _('Browse listings') }}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="browse">
|
||||
<aside class="filters card">
|
||||
<h3>{{ _('Filter') }}</h3>
|
||||
<form method="get" action="{{ url_for('listings.browse') }}">
|
||||
<div class="field">
|
||||
<label>{{ _('Keyword') }}</label>
|
||||
<input class="input" name="q" value="{{ filters.get('q','') }}">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ _('Category') }}</label>
|
||||
<select class="input" name="category">
|
||||
<option value="">{{ _('All') }}</option>
|
||||
{% for c in categories %}
|
||||
<option value="{{ c.id }}" {{ 'selected' if filters.get('category')|string == c.id|string }}>{{ c.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="field"><label>{{ _('State') }}</label>
|
||||
<input class="input" name="state" maxlength="2" value="{{ filters.get('state','') }}"></div>
|
||||
<div class="row2">
|
||||
<div class="field"><label>{{ _('Min $') }}</label>
|
||||
<input class="input" name="min_price" type="number" value="{{ filters.get('min_price','') }}"></div>
|
||||
<div class="field"><label>{{ _('Max $') }}</label>
|
||||
<input class="input" name="max_price" type="number" value="{{ filters.get('max_price','') }}"></div>
|
||||
</div>
|
||||
<div class="row2">
|
||||
<div class="field"><label>{{ _('Near ZIP') }}</label>
|
||||
<input class="input" name="zip" value="{{ filters.get('zip','') }}"></div>
|
||||
<div class="field"><label>{{ _('Radius (mi)') }}</label>
|
||||
<input class="input" name="radius" type="number" value="{{ filters.get('radius','') }}"></div>
|
||||
</div>
|
||||
<button class="btn" type="submit">{{ _('Apply') }}</button>
|
||||
</form>
|
||||
</aside>
|
||||
|
||||
<section class="results">
|
||||
{% if near %}<p class="muted">{{ _('%(n)s results within %(r)s mi of %(z)s', n=near.total, r=near.radius, z=near.zip) }}</p>{% endif %}
|
||||
{% if not results %}<p class="muted">{{ _('No listings found.') }}</p>{% endif %}
|
||||
<div class="grid">
|
||||
{% for l, dist in results %}
|
||||
<a class="tile card" href="{{ url_for('listings.detail', listing_id=l.id) }}">
|
||||
{% if l.cover %}<img class="thumb" src="{{ url_for('listings.media', rel=l.cover.thumb_path) }}" alt="">
|
||||
{% else %}<div class="thumb noimg">{{ _('No photo') }}</div>{% endif %}
|
||||
<div class="tile-body">
|
||||
<div class="tile-title">{{ l.title }}</div>
|
||||
<div class="tile-meta">
|
||||
{% if l.price_display %}<span class="price">{{ l.price_display }}</span>{% endif %}
|
||||
{% if l.is_featured %}<span class="badge">{{ _('Featured') }}</span>{% endif %}
|
||||
</div>
|
||||
<div class="muted small">
|
||||
{% if l.city %}{{ l.city }}, {{ l.state }}{% endif %}
|
||||
{% if dist is not none %} · {{ dist }} mi{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="pager">
|
||||
{% if page > 1 %}<a href="{{ url_for('listings.browse', **merge_query(page=page-1)) }}">← {{ _('Prev') }}</a>{% endif %}
|
||||
{% if has_next %}<a href="{{ url_for('listings.browse', **merge_query(page=page+1)) }}">{{ _('Next') }} →</a>{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,59 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ listing.title }}{% endblock %}
|
||||
{% block content %}
|
||||
<article class="detail">
|
||||
<div class="detail-main card">
|
||||
<h1>{{ listing.title }}</h1>
|
||||
<div class="detail-meta">
|
||||
{% if listing.price_display %}<span class="price big">{{ listing.price_display }}</span>{% endif %}
|
||||
<span class="badge cat">{{ listing.category.name }}</span>
|
||||
{% if not listing.is_live %}<span class="badge warn">{{ listing.status.value }}</span>{% endif %}
|
||||
</div>
|
||||
<p class="muted small">
|
||||
{% if listing.city %}{{ listing.city }}, {{ listing.state }} {{ listing.zip }}{% endif %}
|
||||
· {{ _('%(n)s views', n=listing.view_count) }}
|
||||
</p>
|
||||
|
||||
{% if listing.images %}
|
||||
<div class="gallery">
|
||||
{% for img in listing.images %}
|
||||
<img src="{{ url_for('listings.media', rel=img.path) }}" alt="">
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="body">{{ listing.body | replace('\n','<br>') | safe }}</div>
|
||||
|
||||
{% if listing.attributes %}
|
||||
<table class="attrs">
|
||||
{% for f in listing.category.fields() %}
|
||||
{% if listing.attributes.get(f.name) is not none %}
|
||||
<tr><th>{{ f.label }}</th><td>{{ listing.attributes.get(f.name) }}</td></tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<aside class="detail-side card">
|
||||
<div class="seller">
|
||||
<strong>{{ listing.user.display_name }}</strong>
|
||||
{% if listing.user.verified %}<span class="badge ok">{{ _('Verified') }}</span>{% endif %}
|
||||
</div>
|
||||
{% if is_owner %}
|
||||
<a class="btn" href="{{ url_for('listings.edit', listing_id=listing.id) }}">{{ _('Edit') }}</a>
|
||||
{% if listing.status.value == 'active' %}
|
||||
<form method="post" action="{{ url_for('listings.mark_sold', listing_id=listing.id) }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"><button class="btn ghost" type="submit">{{ _('Mark sold') }}</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
<form method="post" action="{{ url_for('listings.delete', listing_id=listing.id) }}"
|
||||
onsubmit="return confirm('{{ _('Delete this listing?') }}')">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"><button class="btn danger" type="submit">{{ _('Delete') }}</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<p class="muted small">{{ _('Messaging arrives in Phase 3.') }}</p>
|
||||
{% endif %}
|
||||
</aside>
|
||||
</article>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,75 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "auth/_macros.html" import field %}
|
||||
{% block title %}{{ _('Edit listing') if listing else _('New listing') }}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="card form-wide">
|
||||
<h2>{{ _('Edit listing') if listing else _('New listing') }}</h2>
|
||||
<form method="post" enctype="multipart/form-data" novalidate>
|
||||
{{ form.hidden_tag() }}
|
||||
{{ field(form.category_id) }}
|
||||
{{ field(form.title) }}
|
||||
{{ field(form.body) }}
|
||||
<div class="row2">{{ field(form.lang) }}{{ field(form.price) }}</div>
|
||||
{{ field(form.zip) }}
|
||||
|
||||
{# category-specific fields: one group per category, toggled by JS #}
|
||||
<div id="attr-groups">
|
||||
{% for c in categories %}
|
||||
<div class="attr-group" data-cat="{{ c.id }}" style="display:none">
|
||||
{% for f in c.fields() %}
|
||||
<div class="field">
|
||||
<label>{{ f.label }}{% if f.required %} *{% endif %}</label>
|
||||
{% set val = (listing.attributes.get(f.name) if listing and listing.attributes else '') %}
|
||||
{% if f.type == 'select' %}
|
||||
<select class="input" name="attr_{{ f.name }}">
|
||||
<option value="">—</option>
|
||||
{% for opt in f.options %}<option value="{{ opt }}" {{ 'selected' if val==opt }}>{{ opt }}</option>{% endfor %}
|
||||
</select>
|
||||
{% elif f.type == 'number' %}
|
||||
<input class="input" type="number" name="attr_{{ f.name }}" value="{{ val }}">
|
||||
{% elif f.type == 'bool' %}
|
||||
<input type="checkbox" name="attr_{{ f.name }}" value="true" {{ 'checked' if val }}>
|
||||
{% else %}
|
||||
<input class="input" name="attr_{{ f.name }}" value="{{ val }}">
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{{ field(form.images) }}
|
||||
{{ form.submit(class="btn") }}
|
||||
</form>
|
||||
|
||||
{% if listing and listing.images %}
|
||||
<div class="existing-images">
|
||||
<h4>{{ _('Current photos') }}</h4>
|
||||
<div class="thumbs">
|
||||
{% for img in listing.images %}
|
||||
<div class="thumb-wrap">
|
||||
<img src="{{ url_for('listings.media', rel=img.thumb_path) }}" alt="">
|
||||
<form method="post" action="{{ url_for('listings.delete_image', listing_id=listing.id, image_id=img.id) }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button class="btn danger tiny" type="submit">×</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
var sel = document.querySelector('[name=category_id]');
|
||||
var groups = document.querySelectorAll('.attr-group');
|
||||
function sync(){
|
||||
groups.forEach(function(g){
|
||||
g.style.display = (g.dataset.cat === sel.value) ? 'block' : 'none';
|
||||
});
|
||||
}
|
||||
if (sel){ sel.addEventListener('change', sync); sync(); }
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,24 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ _('My listings') }}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
<div class="mine-head">
|
||||
<h2>{{ _('My listings') }}</h2>
|
||||
<span class="muted">{{ _('Active: %(a)s', a=active) }}{% if cap is not none %} / {{ cap }}{% else %} / ∞{% endif %}</span>
|
||||
<a class="btn" href="{{ url_for('listings.create') }}">{{ _('Post new') }}</a>
|
||||
</div>
|
||||
{% if not items %}<p class="muted">{{ _('No listings yet.') }}</p>{% endif %}
|
||||
<table class="list">
|
||||
{% for l in items %}
|
||||
<tr>
|
||||
<td><a href="{{ url_for('listings.detail', listing_id=l.id) }}">{{ l.title }}</a></td>
|
||||
<td>{{ l.category.name }}</td>
|
||||
<td>{{ l.price_display or '—' }}</td>
|
||||
<td><span class="badge {{ 'ok' if l.is_live else 'warn' }}">{{ l.status.value }}</span></td>
|
||||
<td>{{ l.view_count }} {{ _('views') }}</td>
|
||||
<td><a href="{{ url_for('listings.edit', listing_id=l.id) }}">{{ _('Edit') }}</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user