diff --git a/.gitignore b/.gitignore index c2efc04..e22bad8 100644 --- a/.gitignore +++ b/.gitignore @@ -9,8 +9,11 @@ sites/*/static/external_cache/ # ============================================================= # Intermediate / volatile — never committed anywhere. # ============================================================= -sites/*/scraped_data/ # scrape pipeline intermediate; runtime data lives in instance_seed/*.db -sites/*/instance/ # rebuilt at every container boot from instance_seed/ +# Scrape pipeline intermediate; runtime data lives in instance_seed/*.db. +sites/*/scraped_data/ + +# Rebuilt at every container boot from instance_seed/. +sites/*/instance/ sites/*/venv/ # HF download metadata produced by `hf download`. @@ -35,6 +38,7 @@ __pycache__/ .mypy_cache/ .ruff_cache/ .tox/ +.playwright-cli/ .coverage htmlcov/ dist/ @@ -92,4 +96,4 @@ secrets.json # ============================================================ # Agent demo results # ============================================================= -agent_demo/runs/ \ No newline at end of file +agent_demo/runs/ diff --git a/Dockerfile b/Dockerfile index 991e5ab..1e86b1d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # WebHarbor — slim, self-contained image. -# 15 Flask mirror sites + control plane on :8101. +# 16 Flask mirror sites + control plane on :8101. FROM python:3.12-slim-bookworm @@ -33,6 +33,6 @@ COPY control_server.py /opt/control_server.py COPY site_runner.py /opt/site_runner.py RUN chmod +x /opt/websyn_start.sh -EXPOSE 8101 40000-40014 +EXPOSE 8101 40000-40015 CMD ["/opt/websyn_start.sh"] diff --git a/README.md b/README.md index dce3f93..d3e37f7 100644 --- a/README.md +++ b/README.md @@ -36,17 +36,17 @@ WebHarbor takes a different approach. We leverage coding agent (e.g., Claude Cod - **Deep features unlocked** — carts, checkouts, accounts, all fully testable - **Evolving** — harder tasks drive richer mirrors; the environment grows with agents - **RL-ready** — sub-second database resets between rollouts -- **Community-driven** — 15 sites today, scaling to 100+ together +- **Community-driven** — 16 sites today, scaling to 100+ together ## 🚀 Quickstart One command to run all web environments: ```bash -docker run -p 8101:8101 -p 40000-40014:40000-40014 battalion7244/webharbor:latest +docker run -p 8101:8101 -p 40000-40015:40000-40015 battalion7244/webharbor:latest ``` -Then point your agent at `http://localhost:40000` through `http://localhost:40014` to explore 15 local mirrors of webvoyager sites: `Allrecipes, Amazon, Apple, ArXiv, BBC News, Booking, GitHub, Google Flights, Google Maps, Google Search, Hugging Face, Wolfram Alpha, Cambridge Dictionary, Coursera, and ESPN`. +Then point your agent at `http://localhost:40000` through `http://localhost:40015` to explore 16 local mirrors of webvoyager sites: `Allrecipes, Amazon, Apple, ArXiv, BBC News, Booking, GitHub, Google Flights, Google Maps, Google Search, Hugging Face, Wolfram Alpha, Cambridge Dictionary, Coursera, ESPN, and Craigslist`. For sub-second reset between rollouts, expose the control plane and call `/reset/`: @@ -65,7 +65,7 @@ git clone https://github.com/aiming-lab/WebHarbor && cd WebHarbor ## 🤝 Contribute -We have built 15 high-quality mirrors covering the [WebVoyager](https://github.com/MinorJerry/WebVoyager) benchmark. The next goal is **100+ sites**, covering everything in [Online-Mind2Web](https://huggingface.co/datasets/osunlp/Online-Mind2Web). We are inviting the community to build this together. +We have built 16 high-quality mirrors covering the [WebVoyager](https://github.com/MinorJerry/WebVoyager) benchmark. The next goal is **100+ sites**, covering everything in [Online-Mind2Web](https://huggingface.co/datasets/osunlp/Online-Mind2Web). We are inviting the community to build this together. There are two ways to join the author list: @@ -111,4 +111,4 @@ WebHarbor is initiated by UNC-Chapel Hill and Microsoft, with contributions from url = {https://aiming-lab.github.io/webharbor.github.io}, note = {Project website.} } -``` \ No newline at end of file +``` diff --git a/control_server.py b/control_server.py index c255253..3b7169d 100644 --- a/control_server.py +++ b/control_server.py @@ -26,7 +26,7 @@ 'allrecipes', 'amazon', 'apple', 'arxiv', 'bbc_news', 'booking', 'github', 'google_flights', 'google_map', 'google_search', 'huggingface', 'wolfram_alpha', 'cambridge_dictionary', - 'coursera', 'espn', + 'coursera', 'espn', 'craigslist', ] BASE_PORT = 40000 WEBSYN_DIR = '/opt/WebSyn' diff --git a/sites/craigslist/_health.py b/sites/craigslist/_health.py new file mode 100644 index 0000000..16b07e5 --- /dev/null +++ b/sites/craigslist/_health.py @@ -0,0 +1,3 @@ +"""Per-site health probe (optional, called by control_server).""" +def health(): + return {"ok": True, "site": "craigslist"} diff --git a/sites/craigslist/app.py b/sites/craigslist/app.py new file mode 100644 index 0000000..3e50e1f --- /dev/null +++ b/sites/craigslist/app.py @@ -0,0 +1,676 @@ +"""Craigslist mirror - Flask app.""" +import hashlib +import json +import os +import re +from datetime import datetime + +from flask import Flask, abort, flash, redirect, render_template, request, Response, session, url_for +from flask_login import LoginManager, UserMixin, current_user, login_required, login_user, logout_user +from flask_sqlalchemy import SQLAlchemy +from sqlalchemy import or_ + +from seed_data import CATEGORY_GROUPS, deterministic_password, seed_benchmark_users, seed_database + + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) + +app = Flask(__name__, instance_path=os.path.join(BASE_DIR, "instance")) +app.config["SECRET_KEY"] = "webharbor-craigslist-dev-key" +app.config["SQLALCHEMY_DATABASE_URI"] = f"sqlite:///{os.path.join(BASE_DIR, 'instance', 'craigslist.db')}" +app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False + +os.makedirs(os.path.join(BASE_DIR, "instance"), exist_ok=True) + +db = SQLAlchemy(app) +login_manager = LoginManager(app) +login_manager.login_view = "login" +login_manager.login_message_category = "info" + + +class User(db.Model, UserMixin): + __tablename__ = "users" + + id = db.Column(db.Integer, primary_key=True) + email = db.Column(db.String(140), unique=True, nullable=False, index=True) + username = db.Column(db.String(80), unique=True, nullable=False, index=True) + name = db.Column(db.String(120), nullable=False) + area = db.Column(db.String(80), default="san francisco") + phone = db.Column(db.String(40), default="") + password_hash = db.Column(db.String(120), nullable=False) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + + listings = db.relationship("Listing", backref="owner", lazy=True) + saved_listings = db.relationship("SavedListing", backref="user", cascade="all, delete-orphan") + saved_searches = db.relationship("SavedSearch", backref="user", cascade="all, delete-orphan") + messages = db.relationship("Message", backref="user", cascade="all, delete-orphan") + + def set_password(self, password): + self.password_hash = deterministic_password(self.email, password) + + def check_password(self, password): + return self.password_hash == deterministic_password(self.email, password) + + +class Category(db.Model): + __tablename__ = "categories" + + id = db.Column(db.Integer, primary_key=True) + slug = db.Column(db.String(80), unique=True, nullable=False, index=True) + name = db.Column(db.String(120), nullable=False) + abbrev = db.Column(db.String(20), default="") + group_slug = db.Column(db.String(50), index=True) + group_name = db.Column(db.String(80), default="") + display_order = db.Column(db.Integer, default=0) + + listings = db.relationship("Listing", backref="category", lazy=True) + + +class Listing(db.Model): + __tablename__ = "listings" + + id = db.Column(db.Integer, primary_key=True) + title = db.Column(db.String(220), nullable=False, index=True) + slug = db.Column(db.String(240), unique=True, nullable=False, index=True) + category_id = db.Column(db.Integer, db.ForeignKey("categories.id"), nullable=False) + category_slug = db.Column(db.String(80), index=True) + category_group = db.Column(db.String(50), index=True) + area = db.Column(db.String(80), index=True) + neighborhood = db.Column(db.String(120), default="") + price = db.Column(db.Integer, nullable=True, index=True) + bedrooms = db.Column(db.Integer, nullable=True) + sqft = db.Column(db.Integer, nullable=True) + condition = db.Column(db.String(60), default="") + compensation = db.Column(db.String(120), default="") + company = db.Column(db.String(140), default="") + employment_type = db.Column(db.String(80), default="") + description = db.Column(db.Text, default="") + details_json = db.Column(db.Text, default="{}") + image = db.Column(db.String(260), default="") + seller_name = db.Column(db.String(120), default="craigslist user") + seller_email = db.Column(db.String(160), default="") + reply_phone = db.Column(db.String(40), default="") + owner_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=True) + posted_at = db.Column(db.DateTime, default=datetime.utcnow) + updated_at = db.Column(db.DateTime, default=datetime.utcnow) + status = db.Column(db.String(40), default="active") + view_count = db.Column(db.Integer, default=0) + flag_count = db.Column(db.Integer, default=0) + + saved_by = db.relationship("SavedListing", backref="listing", cascade="all, delete-orphan") + messages = db.relationship("Message", backref="listing", cascade="all, delete-orphan") + + def details(self): + try: + return json.loads(self.details_json or "{}") + except json.JSONDecodeError: + return {} + + @property + def display_price(self): + if self.price is None: + return self.compensation or "" + if self.price == 0: + return "free" + return f"${self.price:,}" + + @property + def age_label(self): + delta = datetime(2026, 5, 12, 15, 0, 0) - self.posted_at + hours = max(1, int(delta.total_seconds() // 3600)) + if hours < 24: + return f"{hours}h ago" + return f"{hours // 24}d ago" + + +class SavedListing(db.Model): + __tablename__ = "saved_listings" + + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False) + listing_id = db.Column(db.Integer, db.ForeignKey("listings.id"), nullable=False) + note = db.Column(db.String(240), default="") + created_at = db.Column(db.DateTime, default=datetime.utcnow) + + +class SavedSearch(db.Model): + __tablename__ = "saved_searches" + + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False) + name = db.Column(db.String(120), nullable=False) + query_text = db.Column(db.String(180), default="") + category_slug = db.Column(db.String(80), default="") + area = db.Column(db.String(80), default="") + min_price = db.Column(db.Integer, nullable=True) + max_price = db.Column(db.Integer, nullable=True) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + + +class HiddenListing(db.Model): + __tablename__ = "hidden_listings" + + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False) + listing_id = db.Column(db.Integer, db.ForeignKey("listings.id"), nullable=False) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + + +class Message(db.Model): + __tablename__ = "messages" + + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=True) + listing_id = db.Column(db.Integer, db.ForeignKey("listings.id"), nullable=False) + sender_name = db.Column(db.String(120), default="") + sender_email = db.Column(db.String(160), default="") + body = db.Column(db.Text, default="") + direction = db.Column(db.String(20), default="outbound") + is_read = db.Column(db.Boolean, default=True) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + + +@login_manager.user_loader +def load_user(user_id): + return db.session.get(User, int(user_id)) + + +def slugify(value): + value = value.lower() + value = re.sub(r"[^a-z0-9]+", "-", value) + return value.strip("-") or "listing" + + +def tokenize(value): + return [t for t in re.split(r"[^a-z0-9]+", (value or "").lower()) if len(t) > 1] + + +def normalize_phrase(value): + return " ".join(t for t in re.split(r"[^a-z0-9]+", (value or "").lower()) if t) + + +def parse_int(value): + if value is None or value == "": + return None + try: + return int(re.sub(r"[^0-9]", "", str(value))) + except ValueError: + return None + + +def listing_score(listing, query): + tokens = tokenize(query) + if not tokens: + return 1 + category = listing.category.name if listing.category else "" + details = " ".join(f"{k} {v}" for k, v in listing.details().items()) + haystacks = { + "title": listing.title.lower(), + "category": category.lower(), + "area": f"{listing.area} {listing.neighborhood}".lower(), + "body": f"{listing.description} {details} {listing.condition} {listing.compensation} {listing.company}".lower(), + } + score = 0 + phrase = normalize_phrase(query) + normalized = {key: normalize_phrase(value) for key, value in haystacks.items()} + if phrase: + if phrase in normalized["title"]: + score += 25 + if phrase in normalized["body"]: + score += 10 + for token in tokens: + if token in haystacks["title"]: + score += 5 + if token in haystacks["category"]: + score += 3 + if token in haystacks["area"]: + score += 2 + if token in haystacks["body"]: + score += 1 + return score + + +def category_groups(): + cats = {c.slug: c for c in Category.query.order_by(Category.display_order).all()} + groups = [] + for group in CATEGORY_GROUPS: + rows = [] + for slug, _name, _abbrev in group["columns"]: + if slug in cats: + rows.append(cats[slug]) + groups.append({"slug": group["slug"], "name": group["name"], "categories": rows}) + return groups + + +def hidden_listing_ids(): + if not current_user.is_authenticated: + return set(session.get("hidden_listing_ids", [])) + return {h.listing_id for h in HiddenListing.query.filter_by(user_id=current_user.id).all()} + + +def is_saved(listing_id): + if not current_user.is_authenticated: + return False + return SavedListing.query.filter_by(user_id=current_user.id, listing_id=listing_id).first() is not None + + +def listing_images(listing): + details = listing.details() + values = details.get("images", []) + if isinstance(values, str): + values = [values] + images = [] + if listing.image: + images.append(listing.image) + for value in values: + if value and value not in images: + images.append(value) + return images + + +def listing_map_point(listing): + details = listing.details() + try: + x = float(details.get("map_x", 50)) + y = float(details.get("map_y", 50)) + lat = float(details.get("map_lat", 37.7749)) + lng = float(details.get("map_lng", -122.4194)) + except (TypeError, ValueError): + digest = hashlib.md5(f"{listing.id}:{listing.neighborhood}".encode()).hexdigest() + x = 18 + (int(digest[:2], 16) % 64) + y = 16 + (int(digest[2:4], 16) % 58) + lat = 37.7749 + lng = -122.4194 + return { + "x": max(6, min(94, x)), + "y": max(8, min(92, y)), + "lat": lat, + "lng": lng, + } + + +def listing_public_details(listing): + hidden = {"images", "map_x", "map_y", "map_lat", "map_lng"} + return [ + (key, value) + for key, value in listing.details().items() + if key not in hidden + ] + + +def base_listing_query(category_slug=None): + query = Listing.query.filter_by(status="active") + if category_slug: + category = Category.query.filter_by(slug=category_slug).first_or_404() + query = query.filter(Listing.category_slug == category.slug) + return query + + +def filter_listings(category_slug=None): + q = request.args.get("q", "").strip() + area = request.args.get("area", "").strip() + min_price = parse_int(request.args.get("min_price")) + max_price = parse_int(request.args.get("max_price")) + has_image = request.args.get("has_image") == "1" + sort = request.args.get("sort", "relevance") + include_hidden = request.args.get("include_hidden") == "1" + + query = base_listing_query(category_slug) + if area: + query = query.filter(or_(Listing.area == area, Listing.neighborhood.ilike(f"%{area}%"))) + if min_price is not None: + query = query.filter(or_(Listing.price == None, Listing.price >= min_price)) # noqa: E711 + if max_price is not None: + query = query.filter(or_(Listing.price == None, Listing.price <= max_price)) # noqa: E711 + if has_image: + query = query.filter(Listing.image != "") + + listings = query.all() + hidden_ids = hidden_listing_ids() + if not include_hidden: + listings = [listing for listing in listings if listing.id not in hidden_ids] + + if q: + scored = [(listing_score(listing, q), listing) for listing in listings] + listings = [listing for score, listing in scored if score > 0] + listings.sort(key=lambda pair: (listing_score(pair, q), pair.posted_at), reverse=True) + elif sort == "price_asc": + listings.sort(key=lambda listing: (listing.price is None, listing.price or 0, -listing.posted_at.timestamp())) + elif sort == "price_desc": + listings.sort(key=lambda listing: (listing.price is None, -(listing.price or 0), -listing.posted_at.timestamp())) + elif sort == "oldest": + listings.sort(key=lambda listing: listing.posted_at) + else: + listings.sort(key=lambda listing: listing.posted_at, reverse=True) + + return listings + + +@app.context_processor +def inject_globals(): + def saved_count(): + if not current_user.is_authenticated: + return 0 + return SavedListing.query.filter_by(user_id=current_user.id).count() + + return { + "category_groups": category_groups, + "is_saved": is_saved, + "saved_count": saved_count, + "listing_images": listing_images, + "listing_map_point": listing_map_point, + "listing_public_details": listing_public_details, + } + + +@app.route("/") +def index(): + featured = Listing.query.filter(Listing.image != "", Listing.status == "active").order_by(Listing.posted_at.desc()).limit(8).all() + recent = Listing.query.filter_by(status="active").order_by(Listing.posted_at.desc()).limit(12).all() + counts = { + category.slug: Listing.query.filter_by(category_slug=category.slug, status="active").count() + for category in Category.query.all() + } + return render_template("index.html", featured=featured, recent=recent, counts=counts) + + +@app.route("/favicon.ico") +def favicon(): + return Response(status=204) + + +@app.route("/search") +def search(): + listings = filter_listings() + return render_template( + "search.html", + listings=listings, + category=None, + query=request.args.get("q", "").strip(), + areas=["san francisco", "east bay", "south bay", "peninsula", "north bay", "santa cruz"], + ) + + +@app.route("/search/") +def category_search(category_slug): + category = Category.query.filter_by(slug=category_slug).first_or_404() + listings = filter_listings(category_slug) + return render_template( + "search.html", + listings=listings, + category=category, + query=request.args.get("q", "").strip(), + areas=["san francisco", "east bay", "south bay", "peninsula", "north bay", "santa cruz"], + ) + + +@app.route("/d//.html") +def listing_detail(slug, listing_id): + listing = Listing.query.get_or_404(listing_id) + if listing.slug != slug: + return redirect(url_for("listing_detail", slug=listing.slug, listing_id=listing.id), code=301) + listing.view_count += 1 + db.session.commit() + nearby = Listing.query.filter( + Listing.id != listing.id, + Listing.category_slug == listing.category_slug, + Listing.status == "active", + ).order_by(Listing.posted_at.desc()).limit(6).all() + return render_template("listing_detail.html", listing=listing, nearby=nearby) + + +@app.route("/login", methods=["GET", "POST"]) +def login(): + if request.method == "POST": + email = request.form.get("email", "").strip().lower() + password = request.form.get("password", "") + user = User.query.filter_by(email=email).first() + if user and user.check_password(password): + login_user(user) + flash("logged in", "success") + return redirect(request.args.get("next") or url_for("account")) + flash("invalid email or password", "error") + return render_template("login.html") + + +@app.route("/register", methods=["GET", "POST"]) +def register(): + if request.method == "POST": + email = request.form.get("email", "").strip().lower() + username = request.form.get("username", "").strip().lower() + name = request.form.get("name", "").strip() or username + password = request.form.get("password", "") + if not email or not username or not password: + flash("email, username, and password are required", "error") + elif User.query.filter(or_(User.email == email, User.username == username)).first(): + flash("that account already exists", "error") + else: + user = User(email=email, username=username, name=name, area=request.form.get("area", "san francisco")) + user.set_password(password) + db.session.add(user) + db.session.commit() + login_user(user) + flash("account created", "success") + return redirect(url_for("account")) + return render_template("register.html") + + +@app.route("/logout") +@login_required +def logout(): + logout_user() + flash("logged out", "info") + return redirect(url_for("index")) + + +@app.route("/account") +@login_required +def account(): + posts = Listing.query.filter_by(owner_id=current_user.id).order_by(Listing.posted_at.desc()).all() + searches = SavedSearch.query.filter_by(user_id=current_user.id).order_by(SavedSearch.created_at.desc()).all() + messages = Message.query.filter_by(user_id=current_user.id).order_by(Message.created_at.desc()).limit(5).all() + return render_template("account.html", posts=posts, searches=searches, messages=messages) + + +@app.route("/account/edit", methods=["GET", "POST"]) +@login_required +def account_edit(): + if request.method == "POST": + current_user.name = request.form.get("name", current_user.name).strip() + current_user.area = request.form.get("area", current_user.area).strip() + current_user.phone = request.form.get("phone", current_user.phone).strip() + db.session.commit() + flash("account updated", "success") + return redirect(url_for("account")) + return render_template("account_edit.html") + + +@app.route("/saved") +@login_required +def saved(): + rows = SavedListing.query.filter_by(user_id=current_user.id).order_by(SavedListing.created_at.desc()).all() + return render_template("saved.html", rows=rows) + + +@app.route("/save-search", methods=["POST"]) +@login_required +def save_search(): + name = request.form.get("name", "").strip() or "saved craigslist search" + saved_search = SavedSearch( + user_id=current_user.id, + name=name, + query_text=request.form.get("q", "").strip(), + category_slug=request.form.get("category_slug", "").strip(), + area=request.form.get("area", "").strip(), + min_price=parse_int(request.form.get("min_price")), + max_price=parse_int(request.form.get("max_price")), + ) + db.session.add(saved_search) + db.session.commit() + flash("search saved", "success") + return redirect(url_for("account")) + + +@app.route("/listing//save", methods=["POST"]) +@login_required +def save_listing(listing_id): + listing = Listing.query.get_or_404(listing_id) + existing = SavedListing.query.filter_by(user_id=current_user.id, listing_id=listing.id).first() + if not existing: + db.session.add(SavedListing( + user_id=current_user.id, + listing_id=listing.id, + note=request.form.get("note", "").strip(), + )) + db.session.commit() + flash("listing saved", "success") + return redirect(request.form.get("next") or url_for("listing_detail", slug=listing.slug, listing_id=listing.id)) + + +@app.route("/listing//unsave", methods=["POST"]) +@login_required +def unsave_listing(listing_id): + saved_row = SavedListing.query.filter_by(user_id=current_user.id, listing_id=listing_id).first() + if saved_row: + db.session.delete(saved_row) + db.session.commit() + flash("listing removed", "info") + return redirect(request.form.get("next") or url_for("saved")) + + +@app.route("/listing//hide", methods=["POST"]) +def hide_listing(listing_id): + listing = Listing.query.get_or_404(listing_id) + if current_user.is_authenticated: + existing = HiddenListing.query.filter_by(user_id=current_user.id, listing_id=listing.id).first() + if not existing: + db.session.add(HiddenListing(user_id=current_user.id, listing_id=listing.id)) + db.session.commit() + else: + ids = set(session.get("hidden_listing_ids", [])) + ids.add(listing.id) + session["hidden_listing_ids"] = sorted(ids) + flash("listing hidden", "info") + return redirect(request.form.get("next") or url_for("search")) + + +@app.route("/listing//flag", methods=["POST"]) +def flag_listing(listing_id): + listing = Listing.query.get_or_404(listing_id) + listing.flag_count += 1 + db.session.commit() + flash("thanks for flagging", "info") + return redirect(url_for("listing_detail", slug=listing.slug, listing_id=listing.id)) + + +@app.route("/reply/", methods=["GET", "POST"]) +def reply(listing_id): + listing = Listing.query.get_or_404(listing_id) + if request.method == "POST": + name = request.form.get("name", "").strip() or (current_user.name if current_user.is_authenticated else "craigslist user") + email = request.form.get("email", "").strip() or (current_user.email if current_user.is_authenticated else "anonymous@example.test") + body = request.form.get("body", "").strip() + if not body: + flash("message body is required", "error") + else: + db.session.add(Message( + user_id=current_user.id if current_user.is_authenticated else None, + listing_id=listing.id, + sender_name=name, + sender_email=email, + body=body, + direction="outbound", + is_read=True, + )) + db.session.commit() + flash("reply sent", "success") + return redirect(url_for("listing_detail", slug=listing.slug, listing_id=listing.id)) + return render_template("reply.html", listing=listing) + + +@app.route("/messages") +@login_required +def messages(): + rows = Message.query.filter_by(user_id=current_user.id).order_by(Message.created_at.desc()).all() + unread = Message.query.filter_by(user_id=current_user.id, is_read=False).all() + for row in unread: + row.is_read = True + db.session.commit() + return render_template("messages.html", rows=rows) + + +@app.route("/post", methods=["GET", "POST"]) +@login_required +def post_listing(): + categories = Category.query.order_by(Category.group_slug, Category.display_order).all() + if request.method == "POST": + category = Category.query.filter_by(slug=request.form.get("category_slug", "")).first() + if not category: + flash("choose a valid category", "error") + return render_template("post.html", categories=categories) + title = request.form.get("title", "").strip() + description = request.form.get("description", "").strip() + if not title or not description: + flash("title and description are required", "error") + return render_template("post.html", categories=categories) + details = { + "posted_by": "owner", + "availability": request.form.get("availability", "available now").strip(), + "contact_preference": request.form.get("contact_preference", "email").strip(), + } + listing = Listing( + title=title, + slug=f"{slugify(title)}-{hashlib.md5((title + current_user.email).encode()).hexdigest()[:6]}", + category_id=category.id, + category_slug=category.slug, + category_group=category.group_slug, + area=request.form.get("area", current_user.area).strip(), + neighborhood=request.form.get("neighborhood", "").strip(), + price=parse_int(request.form.get("price")), + bedrooms=parse_int(request.form.get("bedrooms")), + sqft=parse_int(request.form.get("sqft")), + condition=request.form.get("condition", "").strip(), + compensation=request.form.get("compensation", "").strip(), + company=request.form.get("company", "").strip(), + employment_type=request.form.get("employment_type", "").strip(), + description=description, + details_json=json.dumps(details, sort_keys=True), + seller_name=current_user.name, + seller_email=current_user.email, + reply_phone=current_user.phone, + owner_id=current_user.id, + status="active", + ) + db.session.add(listing) + db.session.commit() + flash("posting published", "success") + return redirect(url_for("listing_detail", slug=listing.slug, listing_id=listing.id)) + return render_template("post.html", categories=categories) + + +@app.route("/posting//delete", methods=["POST"]) +@login_required +def delete_posting(listing_id): + listing = Listing.query.get_or_404(listing_id) + if listing.owner_id != current_user.id: + abort(403) + listing.status = "deleted" + db.session.commit() + flash("posting deleted", "info") + return redirect(url_for("account")) + + +@app.route("/_health") +def health(): + return {"ok": True, "site": "craigslist", "listings": Listing.query.count()} + + +with app.app_context(): + db.create_all() + seed_database(BASE_DIR, db, Category, Listing) + seed_benchmark_users(db, User, Listing, SavedListing, SavedSearch, Message) + + +if __name__ == "__main__": + port = int(os.environ.get("PORT", 5000)) + app.run(host="0.0.0.0", port=port, debug=False) diff --git a/sites/craigslist/requirements.txt b/sites/craigslist/requirements.txt new file mode 100644 index 0000000..8a9b110 --- /dev/null +++ b/sites/craigslist/requirements.txt @@ -0,0 +1,4 @@ +Flask==3.1.0 +Flask-SQLAlchemy==3.1.1 +Flask-Login==0.6.3 +SQLAlchemy==2.0.36 diff --git a/sites/craigslist/seed_data.py b/sites/craigslist/seed_data.py new file mode 100644 index 0000000..89730b1 --- /dev/null +++ b/sites/craigslist/seed_data.py @@ -0,0 +1,907 @@ +"""Deterministic seed data for the Craigslist mirror.""" +from datetime import datetime, timedelta +import hashlib +import json +import os +import re + + +FIXED_NOW = datetime(2026, 5, 12, 9, 30, 0) + + +CATEGORY_GROUPS = [ + { + "slug": "for_sale", + "name": "for sale", + "columns": [ + ("furniture", "furniture", "fua"), + ("electronics", "electronics", "ela"), + ("bikes", "bikes", "bia"), + ("cars_trucks", "cars+trucks", "cta"), + ("appliances", "appliances", "ppa"), + ("free", "free stuff", "zip"), + ("musical", "musical instruments", "msa"), + ("sporting", "sporting goods", "sga"), + ("tools", "tools", "tla"), + ], + }, + { + "slug": "housing", + "name": "housing", + "columns": [ + ("apartments", "apts / housing", "apa"), + ("rooms_shares", "rooms / shared", "roo"), + ("sublets", "sublets / temporary", "sub"), + ("parking", "parking / storage", "prk"), + ("office_commercial", "office / commercial", "off"), + ], + }, + { + "slug": "jobs", + "name": "jobs", + "columns": [ + ("software", "software / qa", "sof"), + ("customer_service", "customer service", "csr"), + ("food_bev_hosp", "food / bev / hosp", "fbh"), + ("general_labor", "general labor", "lab"), + ("healthcare", "healthcare", "hea"), + ("education", "education", "edu"), + ("sales", "sales", "sls"), + ("skilled_trade", "skilled trades", "trd"), + ], + }, + { + "slug": "services", + "name": "services", + "columns": [ + ("automotive_services", "automotive", "aos"), + ("computer_services", "computer", "cps"), + ("creative_services", "creative", "crs"), + ("household_services", "household", "hss"), + ("labor_move", "labor / move", "lbs"), + ("lessons", "lessons", "lss"), + ], + }, + { + "slug": "community", + "name": "community", + "columns": [ + ("events", "events", "eve"), + ("volunteers", "volunteers", "vol"), + ("artists", "artists", "ats"), + ("classes", "classes", "cls"), + ("groups", "groups", "grp"), + ("lost_found", "lost+found", "laf"), + ], + }, +] + + +REGIONS = [ + "san francisco", + "east bay", + "south bay", + "peninsula", + "north bay", + "santa cruz", +] + + +def slugify(value): + value = value.lower() + value = re.sub(r"[^a-z0-9]+", "-", value) + return value.strip("-") or "listing" + + +def deterministic_password(email, password="TestPass123!"): + payload = f"{email}:{password}:webharbor-craigslist".encode("utf-8") + return "sha256$" + hashlib.sha256(payload).hexdigest() + + +def image_pool(base_dir): + img_dir = os.path.join(base_dir, "static", "images") + pools = { + "furniture": [], + "cars_trucks": [], + "apartments": [], + "jobs": [], + } + if not os.path.isdir(img_dir): + return pools + for filename in sorted(os.listdir(img_dir)): + if not filename.lower().endswith((".jpg", ".jpeg", ".png", ".webp")): + continue + rel = f"images/{filename}" + for prefix in pools: + if filename.startswith(prefix + "_"): + pools[prefix].append(rel) + break + return pools + + +def pick_image(pools, category_slug, idx): + if category_slug in pools and pools[category_slug]: + values = pools[category_slug] + return values[idx % len(values)] + if category_slug in {"software", "customer_service", "food_bev_hosp", + "general_labor", "healthcare", "education", + "sales", "skilled_trade"} and pools["jobs"]: + return pools["jobs"][idx % len(pools["jobs"])] + if category_slug in {"rooms_shares", "sublets"} and pools["apartments"]: + return pools["apartments"][idx % len(pools["apartments"])] + return "" + + +def image_gallery(pools, category_slug, idx, primary): + if not primary: + return [] + key = category_slug + if key not in pools or not pools[key]: + if category_slug in {"software", "customer_service", "food_bev_hosp", + "general_labor", "healthcare", "education", + "sales", "skilled_trade"}: + key = "jobs" + elif category_slug in {"rooms_shares", "sublets"}: + key = "apartments" + values = list(pools.get(key, [])) + if not values: + return [primary] + ordered = [primary] + for offset in range(1, min(5, len(values))): + candidate = values[(idx + offset) % len(values)] + if candidate not in ordered: + ordered.append(candidate) + return ordered + + +AREA_MAP_BASES = { + "san francisco": (37.7749, -122.4194, 48, 43), + "east bay": (37.8044, -122.2712, 63, 45), + "south bay": (37.3382, -121.8863, 70, 72), + "peninsula": (37.5630, -122.3255, 50, 62), + "north bay": (38.1074, -122.5697, 36, 24), + "santa cruz": (36.9741, -122.0308, 45, 86), +} + + +def map_details(area, neighborhood, idx): + lat, lng, x, y = AREA_MAP_BASES.get(area, AREA_MAP_BASES["san francisco"]) + digest = int(hashlib.md5(f"{area}:{neighborhood}:{idx}".encode("utf-8")).hexdigest()[:8], 16) + dx = (digest % 1700) / 100 - 8.5 + dy = ((digest // 1700) % 1500) / 100 - 7.5 + return { + "map_lat": round(lat + dy * 0.008, 5), + "map_lng": round(lng + dx * 0.01, 5), + "map_x": round(max(8, min(92, x + dx)), 1), + "map_y": round(max(10, min(90, y + dy)), 1), + } + + +def finalize_record(row, pools, idx): + image = row.get("image", "") + details = dict(row.get("details", {})) + gallery = image_gallery(pools, row["category_slug"], idx, image) + if gallery: + details["images"] = gallery + details.update(map_details(row.get("area", REGIONS[idx % len(REGIONS)]), row.get("neighborhood", "san francisco"), idx)) + row["details"] = details + return row + + +SPECIAL_LISTINGS = [ + { + "category_slug": "furniture", + "title": "Ergonomic task chair", + "area": "east bay", + "neighborhood": "berkeley", + "price": 85, + "condition": "excellent", + "description": "Black mesh home office chair from a smoke-free workspace. Detail table lists the adjustment options and pickup notes.", + "details": {"material": "mesh", "color": "black", "arms": "adjustable", "seat_height": "17-21 in", "delivery": "pickup only"}, + }, + { + "category_slug": "furniture", + "title": "Black office chair", + "area": "east bay", + "neighborhood": "oakland", + "price": 65, + "condition": "good", + "description": "Simple rolling office chair with a firm seat. Works best for a guest desk or short work sessions.", + "details": {"material": "fabric", "color": "black", "arms": "fixed", "delivery": "pickup only"}, + }, + { + "category_slug": "furniture", + "title": "Dining chair set", + "area": "east bay", + "neighborhood": "alameda", + "price": 90, + "condition": "fair", + "description": "Four matching dining chairs. Seats are sturdy, but two cushions have light wear.", + "details": {"material": "wood", "color": "brown", "quantity": "4", "delivery": "buyer pickup"}, + }, + { + "category_slug": "furniture", + "title": "Desk chair for small office", + "area": "east bay", + "neighborhood": "emeryville", + "price": 55, + "condition": "good", + "description": "Compact desk chair with wheels. Fits under a narrow writing desk.", + "details": {"material": "vinyl", "color": "gray", "arms": "none", "delivery": "pickup only"}, + }, + { + "category_slug": "furniture", + "title": "Pair of folding chairs", + "area": "east bay", + "neighborhood": "berkeley", + "price": 25, + "condition": "good", + "description": "Two folding chairs for extra seating. Lightweight and easy to store.", + "details": {"material": "metal", "color": "white", "quantity": "2", "delivery": "porch pickup"}, + }, + { + "category_slug": "furniture", + "title": "Vintage wood chair", + "area": "east bay", + "neighborhood": "oakland", + "price": 75, + "condition": "fair", + "description": "Vintage accent chair with carved back. Needs new felt pads on the legs.", + "details": {"material": "wood", "color": "oak", "style": "accent", "delivery": "pickup only"}, + }, + { + "category_slug": "furniture", + "title": "Walnut writing desk with two drawers", + "area": "san francisco", + "neighborhood": "mission district", + "price": 140, + "condition": "good", + "description": "Compact writing desk that fits a small apartment. Some edge wear on the back left corner.", + "details": {"width": "42 in", "depth": "22 in", "material": "walnut veneer", "delivery": "buyer pickup"}, + }, + { + "category_slug": "furniture", + "title": "Standing desk frame, white", + "area": "peninsula", + "neighborhood": "palo alto", + "price": 210, + "condition": "like new", + "description": "Electric standing desk frame, dual motor, controller works. Desktop not included.", + "details": {"height_range": "25-50 in", "load_rating": "220 lb", "color": "white"}, + }, + { + "category_slug": "electronics", + "title": "Dell 27 inch USB-C monitor", + "area": "south bay", + "neighborhood": "sunnyvale", + "price": 165, + "condition": "excellent", + "description": "Desk display with original power cable. Detail table lists the display specs.", + "details": {"size": "27 in", "resolution": "2560x1440", "ports": "USB-C HDMI DP"}, + }, + { + "category_slug": "electronics", + "title": "Sony noise cancelling headphones", + "area": "san francisco", + "neighborhood": "nob hill", + "price": 120, + "condition": "good", + "description": "Over-ear wireless headphones with case and USB cable. Ear pads replaced last month.", + "details": {"battery": "24 hours", "color": "silver", "included": "case cable"}, + }, + { + "category_slug": "bikes", + "title": "Marin commuter bike", + "area": "east bay", + "neighborhood": "oakland lake merritt", + "price": 460, + "condition": "good", + "description": "Medium frame city bike with fenders, rear rack, and recent tune-up. Braking details are in the table below.", + "details": {"frame": "medium", "brakes": "hydraulic disc", "gears": "1x10", "wheel_size": "700c"}, + }, + { + "category_slug": "bikes", + "title": "Trek hybrid bike, large frame", + "area": "north bay", + "neighborhood": "san rafael", + "price": 520, + "condition": "excellent", + "description": "Large hybrid bike with flat bars, new chain, and puncture-resistant tires.", + "details": {"frame": "large", "brakes": "rim", "gears": "3x8", "wheel_size": "700c"}, + }, + { + "category_slug": "cars_trucks", + "title": "2006 Honda Accord EX sedan", + "area": "south bay", + "neighborhood": "san jose north", + "price": 6200, + "condition": "good", + "description": "Accord sedan with service records, cold AC, and current registration. Title details are listed below.", + "details": {"make": "Honda", "model": "Accord EX", "mileage": "151000", "title_status": "clean", "transmission": "automatic"}, + }, + { + "category_slug": "cars_trucks", + "title": "2005 Honda Civic Hybrid", + "area": "east bay", + "neighborhood": "oakland", + "price": 5400, + "condition": "good", + "description": "Commuter Civic with recent tires and smog certificate. See detail table for title and mileage.", + "details": {"make": "Honda", "model": "Civic Hybrid", "mileage": "178000", "title_status": "rebuilt", "transmission": "automatic"}, + }, + { + "category_slug": "cars_trucks", + "title": "2011 Honda Accord EX-L", + "area": "north bay", + "neighborhood": "santa rosa", + "price": 6900, + "condition": "fair", + "description": "Leather interior and navigation. Needs suspension work soon.", + "details": {"make": "Honda", "model": "Accord EX-L", "mileage": "189000", "title_status": "clean", "transmission": "automatic"}, + }, + { + "category_slug": "cars_trucks", + "title": "2008 Honda Fit manual", + "area": "peninsula", + "neighborhood": "redwood city", + "price": 5900, + "condition": "good", + "description": "Manual hatchback with service history. Cosmetic dents on passenger door.", + "details": {"make": "Honda", "model": "Fit", "mileage": "162000", "title_status": "clean", "transmission": "manual"}, + }, + { + "category_slug": "cars_trucks", + "title": "2014 Honda Odyssey EX-L", + "area": "east bay", + "neighborhood": "pleasanton", + "price": 14950, + "condition": "excellent", + "description": "One-owner minivan with leather seats, backup camera, and recent tires.", + "details": {"make": "Honda", "model": "Odyssey", "mileage": "103000", "title_status": "clean", "seats": "8"}, + }, + { + "category_slug": "cars_trucks", + "title": "Honda Passport project SUV", + "area": "south bay", + "neighborhood": "san jose south", + "price": 1400, + "condition": "fair", + "description": "Mechanic special. Runs, but needs smog work and rear brakes.", + "details": {"make": "Honda", "model": "Passport", "mileage": "204000", "title_status": "salvage"}, + }, + { + "category_slug": "appliances", + "title": "LG washer and gas dryer pair", + "area": "east bay", + "neighborhood": "alameda", + "price": 380, + "condition": "good", + "description": "Front-load washer and gas dryer pair. Both tested before removal.", + "details": {"washer": "front load", "dryer": "gas", "delivery": "curbside available"}, + }, + { + "category_slug": "free", + "title": "Free moving boxes and packing paper", + "area": "east bay", + "neighborhood": "oakland temescal", + "price": 0, + "condition": "used", + "description": "Twenty sturdy moving boxes plus packing paper. Porch pickup after 6pm.", + "details": {"quantity": "20 boxes", "pickup": "porch", "cross_streets": "Telegraph and 45th"}, + }, + { + "category_slug": "apartments", + "title": "Sunny studio near Berkeley BART", + "area": "east bay", + "neighborhood": "berkeley", + "price": 2195, + "bedrooms": 0, + "sqft": 510, + "description": "Top-floor studio with bike room and shared roof deck. The detail table lists laundry, lease, and pet notes.", + "details": {"laundry": "in-unit", "parking": "street", "pet_policy": "cats ok", "lease": "12 months"}, + }, + { + "category_slug": "apartments", + "title": "Berkeley studio near campus", + "area": "east bay", + "neighborhood": "berkeley", + "price": 2050, + "bedrooms": 0, + "sqft": 405, + "description": "Small studio near transit and campus. Amenities are listed in the detail table.", + "details": {"laundry": "shared", "parking": "none", "pet_policy": "no pets", "lease": "12 months"}, + }, + { + "category_slug": "apartments", + "title": "Oakland garden studio", + "area": "east bay", + "neighborhood": "oakland", + "price": 1875, + "bedrooms": 0, + "sqft": 390, + "description": "Garden-level studio with private entrance. Utility and laundry notes are in the detail table.", + "details": {"laundry": "shared", "parking": "street", "pet_policy": "small dogs ok", "lease": "6 months"}, + }, + { + "category_slug": "apartments", + "title": "Alameda studio by the ferry", + "area": "east bay", + "neighborhood": "alameda", + "price": 2310, + "bedrooms": 0, + "sqft": 470, + "description": "Studio close to ferry and shoreline path. Detail table lists building amenities.", + "details": {"laundry": "coin", "parking": "included", "pet_policy": "cats ok", "lease": "12 months"}, + }, + { + "category_slug": "apartments", + "title": "Emeryville studio loft", + "area": "east bay", + "neighborhood": "emeryville", + "price": 2385, + "bedrooms": 0, + "sqft": 525, + "description": "Open loft studio with high ceilings and secure entry. See detail table for laundry policy.", + "details": {"laundry": "shared", "parking": "garage extra", "pet_policy": "cats ok", "lease": "12 months"}, + }, + { + "category_slug": "apartments", + "title": "Mission one bedroom with parking", + "area": "san francisco", + "neighborhood": "mission district", + "price": 2895, + "bedrooms": 1, + "sqft": 650, + "description": "One bedroom apartment with one assigned parking space and shared laundry.", + "details": {"laundry": "shared", "parking": "included", "pet_policy": "no pets", "lease": "12 months"}, + }, + { + "category_slug": "apartments", + "title": "Quiet studio with courtyard view", + "area": "peninsula", + "neighborhood": "san mateo", + "price": 2350, + "bedrooms": 0, + "sqft": 430, + "description": "Courtyard-facing studio, renovated kitchen, no parking, coin laundry.", + "details": {"laundry": "coin", "parking": "none", "pet_policy": "small dogs ok", "lease": "9 months"}, + }, + { + "category_slug": "rooms_shares", + "title": "Room in sunny Oakland craftsman", + "area": "east bay", + "neighborhood": "rockridge", + "price": 1250, + "bedrooms": 1, + "sqft": 140, + "description": "Room in a three-bedroom house with garden, shared kitchen, and storage for one bike.", + "details": {"utilities": "split", "bath": "shared", "move_in": "June 1"}, + }, + { + "category_slug": "software", + "title": "Backend engineer for civic data startup", + "area": "san francisco", + "neighborhood": "soma", + "price": None, + "compensation": "$155k - $180k", + "company": "Harbor Civic Labs", + "employment_type": "full-time", + "description": "Small team building public-record search tools. Python, Postgres, and data pipelines.", + "details": {"remote": "hybrid", "stack": "Python Postgres Flask", "equity": "0.15%"}, + }, + { + "category_slug": "healthcare", + "title": "Speech language pathologist school year role", + "area": "north bay", + "neighborhood": "vallejo", + "price": None, + "compensation": "$62 - $70 per hour", + "company": "Bay Learning Services", + "employment_type": "contract", + "description": "School-year clinician role with onsite team support. Detail table lists setting and license notes.", + "details": {"schedule": "2026-2027 school year", "license": "CA SLP required", "setting": "K-8"}, + }, + { + "category_slug": "food_bev_hosp", + "title": "Line cook for modern Asian bistro", + "area": "san francisco", + "neighborhood": "richmond district", + "price": None, + "compensation": "$26 - $30 per hour plus tips", + "company": "Mika Bistro", + "employment_type": "full-time", + "description": "Dinner service line cook, wok station helpful, two consecutive days off.", + "details": {"shift": "3pm-11pm", "benefits": "meals transit stipend", "experience": "2 years"}, + }, + { + "category_slug": "general_labor", + "title": "Movers needed for weekend apartment turns", + "area": "east bay", + "neighborhood": "emeryville", + "price": None, + "compensation": "$28 per hour cash", + "company": "Bay Move Crew", + "employment_type": "part-time", + "description": "Weekend work loading boxes and furniture. Must be able to lift 60 lb.", + "details": {"schedule": "Saturday and Sunday", "start": "8am", "tools": "gloves provided"}, + }, + { + "category_slug": "lessons", + "title": "Remote algebra and calculus tutoring", + "area": "san francisco", + "neighborhood": "remote", + "price": 55, + "condition": "new", + "description": "Online math lessons for high-school algebra, pre-calculus, and AP calculus.", + "details": {"format": "Zoom", "rate": "$55 per hour", "subjects": "algebra calculus"}, + }, + { + "category_slug": "computer_services", + "title": "MacBook repair and data recovery", + "area": "south bay", + "neighborhood": "santa clara", + "price": 90, + "condition": "new", + "description": "Laptop diagnostics, SSD upgrades, data migration, and screen replacement quotes.", + "details": {"diagnostic": "$90", "turnaround": "same week", "brands": "Apple Dell Lenovo"}, + }, + { + "category_slug": "events", + "title": "Saturday neighborhood plant swap", + "area": "san francisco", + "neighborhood": "mission district", + "price": 0, + "condition": "new", + "description": "Neighborhood plant swap near Dolores Park. Detail table lists the time and what to bring.", + "details": {"date": "Saturday", "time": "10:00 AM", "bring": "labeled plants"}, + }, + { + "category_slug": "volunteers", + "title": "Volunteer bike repair clinic helpers", + "area": "east bay", + "neighborhood": "oakland", + "price": 0, + "condition": "new", + "description": "Help check brakes, patch tubes, and guide neighbors through basic bike fixes.", + "details": {"date": "Saturday", "time": "1:00 PM", "skills": "basic bike repair"}, + }, + { + "category_slug": "artists", + "title": "Seeking photographer for small zine project", + "area": "south bay", + "neighborhood": "santa clara", + "price": 200, + "condition": "new", + "description": "Portrait session for a local zine. Natural light style preferred, two-hour shoot.", + "details": {"budget": "$200", "format": "digital", "deadline": "May 24"}, + }, +] + + +TARGET_NEAR_MISSES = [ + {"category_slug": "bikes", "title": "Commuter bike with rear rack", "area": "east bay", "neighborhood": "oakland", "price": 315, "condition": "good", "description": "Daily commuter bike with fenders and rack. Detail table lists frame and brake setup.", "details": {"frame": "medium", "brakes": "rim", "gears": "3x7", "wheel_size": "700c"}}, + {"category_slug": "bikes", "title": "Lightweight commuter bicycle", "area": "san francisco", "neighborhood": "inner sunset", "price": 440, "condition": "good", "description": "Reliable commuter for city errands. Detail table lists parts and fit.", "details": {"frame": "small", "brakes": "mechanical disc", "gears": "2x8", "wheel_size": "700c"}}, + {"category_slug": "bikes", "title": "Flat bar commuter bike", "area": "south bay", "neighborhood": "campbell", "price": 390, "condition": "fair", "description": "Flat bar commuter with lights and bottle cage. Detail table lists maintenance notes.", "details": {"frame": "large", "brakes": "rim", "gears": "1x8", "wheel_size": "700c"}}, + {"category_slug": "bikes", "title": "Commuter bike, step-through frame", "area": "peninsula", "neighborhood": "redwood city", "price": 285, "condition": "good", "description": "Comfort commuter with upright bars and kickstand.", "details": {"frame": "medium step-through", "brakes": "coaster", "gears": "7 speed", "wheel_size": "26 in"}}, + + {"category_slug": "cars_trucks", "title": "2003 Honda CR-V AWD", "area": "east bay", "neighborhood": "hayward", "price": 5800, "condition": "good", "description": "Older CR-V with roof rack and current smog. Detail table lists title status.", "details": {"make": "Honda", "model": "CR-V", "mileage": "177000", "title_status": "clean", "transmission": "automatic"}}, + {"category_slug": "cars_trucks", "title": "2007 Honda Civic coupe", "area": "south bay", "neighborhood": "milpitas", "price": 4900, "condition": "fair", "description": "Civic coupe with new battery and working AC. Detail table lists paperwork.", "details": {"make": "Honda", "model": "Civic", "mileage": "201000", "title_status": "salvage", "transmission": "automatic"}}, + {"category_slug": "cars_trucks", "title": "2009 Honda Element", "area": "santa cruz", "neighborhood": "aptos", "price": 6600, "condition": "good", "description": "Element with roof bars and camping platform. Detail table lists title status.", "details": {"make": "Honda", "model": "Element", "mileage": "184000", "title_status": "clean", "transmission": "automatic"}}, + + {"category_slug": "events", "title": "Succulent plant exchange", "area": "east bay", "neighborhood": "berkeley", "price": 0, "condition": "new", "description": "Casual plant exchange for cuttings and extra pots.", "details": {"date": "Sunday", "time": "11:00 AM", "bring": "small labeled cuttings"}}, + {"category_slug": "events", "title": "Houseplant swap table", "area": "north bay", "neighborhood": "san rafael", "price": 0, "condition": "new", "description": "Community table for swapping houseplants and garden starts.", "details": {"date": "Friday", "time": "4:00 PM", "bring": "healthy plants only"}}, + {"category_slug": "events", "title": "Seedling swap meetup", "area": "south bay", "neighborhood": "santa clara", "price": 0, "condition": "new", "description": "Swap vegetable seedlings and talk balcony gardening.", "details": {"date": "Saturday", "time": "9:00 AM", "bring": "seedling trays"}}, + {"category_slug": "events", "title": "Plant care workshop", "area": "san francisco", "neighborhood": "richmond district", "price": 0, "condition": "new", "description": "Beginner workshop on repotting and watering indoor plants.", "details": {"date": "Thursday", "time": "6:30 PM", "bring": "one problem plant"}}, + + {"category_slug": "free", "title": "Free wardrobe boxes", "area": "east bay", "neighborhood": "berkeley", "price": 0, "condition": "used", "description": "Tall moving boxes from a recent apartment move.", "details": {"quantity": "6 wardrobe boxes", "pickup": "curb", "cross_streets": "Shattuck and Dwight"}}, + {"category_slug": "free", "title": "Moving boxes and bubble wrap", "area": "east bay", "neighborhood": "alameda", "price": 0, "condition": "used", "description": "Stack of moving boxes, paper, and bubble wrap.", "details": {"quantity": "12 boxes", "pickup": "garage", "cross_streets": "Park and Lincoln"}}, + {"category_slug": "free", "title": "Small boxes for books", "area": "east bay", "neighborhood": "emeryville", "price": 0, "condition": "used", "description": "Free small book boxes from a move.", "details": {"quantity": "15 boxes", "pickup": "lobby", "cross_streets": "40th and Hollis"}}, + {"category_slug": "free", "title": "Flattened moving boxes", "area": "east bay", "neighborhood": "oakland", "price": 0, "condition": "used", "description": "Flattened moving boxes, clean and dry.", "details": {"quantity": "18 boxes", "pickup": "porch", "cross_streets": "Broadway and 51st"}}, + + {"category_slug": "healthcare", "title": "Pediatric speech language assistant", "area": "south bay", "neighborhood": "campbell", "price": None, "compensation": "$38 - $45 per hour", "company": "Bright Steps Therapy", "employment_type": "part-time", "description": "Clinic support role for pediatric speech therapy sessions.", "details": {"schedule": "3 weekdays", "license": "SLPA preferred", "setting": "clinic"}}, + {"category_slug": "healthcare", "title": "Speech therapist telehealth contract", "area": "san francisco", "neighborhood": "remote", "price": None, "compensation": "$58 per hour", "company": "Remote Learning Care", "employment_type": "contract", "description": "Online speech therapy sessions for middle-school students.", "details": {"schedule": "August-May", "license": "CA SLP required", "setting": "telehealth"}}, + {"category_slug": "healthcare", "title": "Language development aide", "area": "east bay", "neighborhood": "oakland", "price": None, "compensation": "$31 per hour", "company": "Oakland Child Services", "employment_type": "full-time", "description": "Support language development programs under clinician supervision.", "details": {"schedule": "school year", "license": "associate permit", "setting": "preschool"}}, + {"category_slug": "healthcare", "title": "School occupational therapist", "area": "peninsula", "neighborhood": "san mateo", "price": None, "compensation": "$65 per hour", "company": "Bay School Staffing", "employment_type": "contract", "description": "School therapist role with elementary caseload.", "details": {"schedule": "2026-2027 school year", "license": "CA OT required", "setting": "K-5"}}, + + {"category_slug": "lessons", "title": "Online math tutoring for algebra", "area": "san francisco", "neighborhood": "remote", "price": 45, "condition": "new", "description": "Remote math tutoring for algebra and geometry.", "details": {"format": "Zoom", "rate": "$45 per hour", "subjects": "algebra geometry"}}, + {"category_slug": "lessons", "title": "Calculus tutoring, weekends", "area": "peninsula", "neighborhood": "palo alto", "price": 70, "condition": "new", "description": "Weekend tutoring for calculus and statistics.", "details": {"format": "library or online", "rate": "$70 per hour", "subjects": "calculus statistics"}}, + {"category_slug": "lessons", "title": "SAT math tutor", "area": "east bay", "neighborhood": "berkeley", "price": 60, "condition": "new", "description": "SAT math prep with practice tests.", "details": {"format": "in person", "rate": "$60 per hour", "subjects": "SAT math"}}, + {"category_slug": "lessons", "title": "Middle school math tutoring", "area": "south bay", "neighborhood": "sunnyvale", "price": 50, "condition": "new", "description": "Patient math tutoring for middle school students.", "details": {"format": "online", "rate": "$50 per hour", "subjects": "pre-algebra"}}, + + {"category_slug": "labor_move", "title": "Moving help for apartments", "area": "east bay", "neighborhood": "oakland", "price": 80, "condition": "new", "description": "Two helpers for local apartment moving jobs.", "details": {"crew": "2 people", "rate": "$80 per hour", "truck": "not included"}}, + {"category_slug": "labor_move", "title": "Small moving crew with blankets", "area": "san francisco", "neighborhood": "mission district", "price": 110, "condition": "new", "description": "Moving crew for furniture, boxes, and studio apartments.", "details": {"crew": "2 people", "rate": "$110 per hour", "truck": "cargo van"}}, + {"category_slug": "labor_move", "title": "Last minute moving labor", "area": "south bay", "neighborhood": "san jose", "price": 75, "condition": "new", "description": "Labor-only moving help for stairs and loading.", "details": {"crew": "1-2 people", "rate": "$75 per hour", "truck": "not included"}}, + {"category_slug": "labor_move", "title": "Weekend moving and hauling", "area": "peninsula", "neighborhood": "san mateo", "price": 125, "condition": "new", "description": "Weekend moving and hauling for small households.", "details": {"crew": "2 people", "rate": "$125 per hour", "truck": "box truck"}}, + + {"category_slug": "electronics", "title": "LG USB-C monitor", "area": "east bay", "neighborhood": "berkeley", "price": 190, "condition": "good", "description": "USB-C display with stand and cable. Detail table lists specs.", "details": {"size": "24 in", "resolution": "1920x1080", "ports": "USB-C HDMI"}}, + {"category_slug": "electronics", "title": "BenQ office monitor", "area": "san francisco", "neighborhood": "nopa", "price": 95, "condition": "good", "description": "Office monitor with stand. Detail table lists specs.", "details": {"size": "27 in", "resolution": "1920x1080", "ports": "HDMI DP"}}, + {"category_slug": "electronics", "title": "Portable USB-C display", "area": "south bay", "neighborhood": "santa clara", "price": 145, "condition": "like new", "description": "Slim portable display with sleeve. Detail table lists specs.", "details": {"size": "15.6 in", "resolution": "1920x1080", "ports": "USB-C mini-HDMI"}}, + {"category_slug": "electronics", "title": "Ultrawide monitor with USB hub", "area": "peninsula", "neighborhood": "san mateo", "price": 260, "condition": "good", "description": "Large monitor with built-in hub. Detail table lists specs.", "details": {"size": "34 in", "resolution": "3440x1440", "ports": "HDMI DP USB-A"}}, + + {"category_slug": "volunteers", "title": "Bike lane cleanup volunteers", "area": "east bay", "neighborhood": "oakland", "price": 0, "condition": "new", "description": "Volunteers needed for weekend bike lane cleanup.", "details": {"date": "Sunday", "time": "9:30 AM", "skills": "comfortable outdoors"}}, + {"category_slug": "volunteers", "title": "Community repair cafe volunteers", "area": "san francisco", "neighborhood": "haight", "price": 0, "condition": "new", "description": "Repair cafe seeks volunteers for small household fixes.", "details": {"date": "Saturday", "time": "2:00 PM", "skills": "basic hand tools"}}, + {"category_slug": "volunteers", "title": "Youth bike rodeo helpers", "area": "south bay", "neighborhood": "santa clara", "price": 0, "condition": "new", "description": "Help kids practice bike safety at a neighborhood event.", "details": {"date": "Saturday", "time": "10:30 AM", "skills": "patience with kids"}}, + {"category_slug": "volunteers", "title": "Tool library repair shift", "area": "east bay", "neighborhood": "berkeley", "price": 0, "condition": "new", "description": "Volunteer shift repairing donated tools and sorting parts.", "details": {"date": "Wednesday", "time": "5:00 PM", "skills": "basic repair"}}, + {"category_slug": "events", "title": "Plant swap picnic table", "area": "peninsula", "neighborhood": "san mateo", "price": 0, "condition": "new", "description": "Small plant swap hosted at a public picnic table.", "details": {"date": "Sunday", "time": "3:00 PM", "bring": "pest-free cuttings"}}, + {"category_slug": "free", "title": "Free boxes for moving day", "area": "east bay", "neighborhood": "richmond", "price": 0, "condition": "used", "description": "Assorted free boxes left from moving day.", "details": {"quantity": "10 boxes", "pickup": "driveway", "cross_streets": "23rd and Barrett"}}, + {"category_slug": "apartments", "title": "West Oakland studio apartment", "area": "east bay", "neighborhood": "west oakland", "price": 2225, "bedrooms": 0, "sqft": 455, "condition": "new", "description": "Compact studio apartment close to BART. Detail table lists laundry and parking notes.", "details": {"laundry": "shared", "parking": "street", "pet_policy": "no pets", "lease": "12 months"}}, + {"category_slug": "healthcare", "title": "Travel speech pathologist opening", "area": "north bay", "neighborhood": "napa", "price": None, "compensation": "$61 per hour", "company": "North Bay Therapy", "employment_type": "contract", "description": "Travel speech clinician opening with district team support.", "details": {"schedule": "fall semester", "license": "CA SLP required", "setting": "high school"}}, + {"category_slug": "healthcare", "title": "Bilingual language pathologist", "area": "east bay", "neighborhood": "fremont", "price": None, "compensation": "$64 per hour", "company": "Fremont Student Services", "employment_type": "full-time", "description": "Bilingual language services role with onsite supervision.", "details": {"schedule": "school calendar", "license": "CA SLP or RPE", "setting": "elementary"}}, + {"category_slug": "lessons", "title": "AP math tutoring online", "area": "north bay", "neighborhood": "remote", "price": 58, "condition": "new", "description": "Online tutoring for AP math courses and exam review.", "details": {"format": "Zoom", "rate": "$58 per hour", "subjects": "AP calculus pre-calculus"}}, + {"category_slug": "labor_move", "title": "Moving help with pickup truck", "area": "north bay", "neighborhood": "san rafael", "price": 90, "condition": "new", "description": "Small moving jobs with one pickup truck and blankets.", "details": {"crew": "2 people", "rate": "$90 per hour", "truck": "pickup"}}, + {"category_slug": "labor_move", "title": "Apartment moving and packing", "area": "east bay", "neighborhood": "berkeley", "price": 100, "condition": "new", "description": "Apartment moving, packing help, and loading support.", "details": {"crew": "2 people", "rate": "$100 per hour", "truck": "van available"}}, + {"category_slug": "volunteers", "title": "Bike repair table volunteers", "area": "east bay", "neighborhood": "emeryville", "price": 0, "condition": "new", "description": "Volunteers needed at a bike repair information table.", "details": {"date": "Sunday", "time": "12:00 PM", "skills": "friendly with cyclists"}}, + {"category_slug": "volunteers", "title": "Community bike repair setup crew", "area": "east bay", "neighborhood": "berkeley", "price": 0, "condition": "new", "description": "Volunteers help set up stands and check in neighbors for a bike repair afternoon.", "details": {"date": "Saturday", "time": "11:00 AM", "skills": "event setup"}}, + {"category_slug": "volunteers", "title": "Bike day repair station helpers", "area": "east bay", "neighborhood": "oakland", "price": 0, "condition": "new", "description": "Repair station seeks helpers for basic intake, tools, and sign-in during bike day.", "details": {"date": "Saturday", "time": "3:30 PM", "skills": "organized with tools"}}, +] + + +EXTRA_BLUEPRINTS = { + "furniture": [ + ("Maple bookcase with adjustable shelves", 95, "good", "solid wood bookcase, light scratches"), + ("Round kitchen table with four chairs", 180, "good", "small dining set for apartment"), + ("Blue loveseat, pet-free home", 160, "excellent", "comfortable loveseat, no stains"), + ("Metal filing cabinet, two drawer", 45, "fair", "office cabinet with working lock"), + ("Queen bed frame with slats", 110, "good", "platform bed frame, no mattress"), + ("Glass coffee table", 70, "good", "thick glass top, chrome legs"), + ], + "electronics": [ + ("iPad Air with keyboard case", 310, "excellent", "tablet, charger, and case"), + ("Nintendo Switch bundle", 240, "good", "console, dock, two controllers"), + ("Eero mesh router three pack", 120, "good", "whole-home wifi kit"), + ("Bose bookshelf speakers", 175, "good", "pair of compact speakers"), + ("Logitech webcam and ring light", 55, "like new", "video call setup"), + ], + "bikes": [ + ("Cannondale road bike, 54cm", 690, "excellent", "aluminum road bike with carbon fork"), + ("Folding bike for Caltrain commute", 375, "good", "compact folding bike with rear rack"), + ("Kids bike, 20 inch wheels", 80, "good", "recent tubes and training stand"), + ("Single speed city bike", 220, "fair", "simple commuter with new tires"), + ], + "apartments": [ + ("Pet friendly one bedroom near Lake Merritt", 2475, "new", "balcony, dishwasher, shared laundry"), + ("South Bay studio with gated parking", 2250, "new", "studio apartment with covered parking"), + ("North Beach two bedroom flat", 3650, "new", "classic flat near restaurants"), + ("Garden level in-law apartment", 1980, "new", "private entrance, utilities included"), + ("Sunny room in shared Mission apartment", 1390, "new", "shared kitchen and roof access"), + ], + "jobs": [ + ("Customer support specialist, hybrid", None, "new", "help members by phone and email"), + ("Part-time barista morning shifts", None, "new", "espresso service and register"), + ("Senior contact center engineer", None, "new", "maintain cloud telephony systems"), + ("Painter apprentice, all levels", None, "new", "residential repaint crew"), + ("Youth tennis coach needed", None, "new", "after-school tennis program"), + ("Registered nurse floater", None, "new", "clinic float role across East Bay"), + ], + "services": [ + ("Two-person moving help with van", 95, "new", "local moves, stairs ok"), + ("Guitar lessons for beginners", 45, "new", "weekly lessons in person or online"), + ("House cleaning, green supplies", 120, "new", "apartments and small homes"), + ("Logo design for small businesses", 250, "new", "brand kit and social avatars"), + ], + "community": [ + ("East Bay board game night", 0, "new", "weekly strategy games and snacks"), + ("Lost gray tabby cat near Panhandle", 0, "new", "microchipped gray tabby"), + ("Figure drawing group seeking models", 0, "new", "weekly session with easels"), + ("Free compost workshop", 0, "new", "learn balcony compost basics"), + ], +} + + +def build_listing_records(base_dir): + pools = image_pool(base_dir) + records = [] + + for i, item in enumerate(SPECIAL_LISTINGS): + row = dict(item) + row["image"] = pick_image(pools, row["category_slug"], i) + records.append(finalize_record(row, pools, i)) + + offset = len(records) + for i, item in enumerate(TARGET_NEAR_MISSES, start=1): + row = dict(item) + row["image"] = pick_image(pools, row["category_slug"], offset + i) + records.append(finalize_record(row, pools, offset + i)) + + category_sequence = [ + ("furniture", "for_sale"), + ("electronics", "for_sale"), + ("bikes", "for_sale"), + ("apartments", "housing"), + ("rooms_shares", "housing"), + ("sublets", "housing"), + ("parking", "housing"), + ("office_commercial", "housing"), + ("software", "jobs"), + ("customer_service", "jobs"), + ("food_bev_hosp", "jobs"), + ("general_labor", "jobs"), + ("healthcare", "jobs"), + ("education", "jobs"), + ("automotive_services", "services"), + ("computer_services", "services"), + ("household_services", "services"), + ("labor_move", "services"), + ("lessons", "services"), + ("events", "community"), + ("volunteers", "community"), + ("artists", "community"), + ("groups", "community"), + ("lost_found", "community"), + ] + area_cycle = REGIONS + neighborhood_cycle = [ + "berkeley", "oakland", "mission district", "palo alto", "san jose", + "san mateo", "santa rosa", "santa cruz", "alameda", "sunnyvale", + ] + + idx = 0 + for category_slug, group in category_sequence: + if category_slug in EXTRA_BLUEPRINTS: + blueprints = EXTRA_BLUEPRINTS[category_slug] + elif group in EXTRA_BLUEPRINTS: + blueprints = EXTRA_BLUEPRINTS[group] + elif group == "jobs": + blueprints = EXTRA_BLUEPRINTS["jobs"] + elif group == "services": + blueprints = EXTRA_BLUEPRINTS["services"] + else: + blueprints = EXTRA_BLUEPRINTS["community"] + for title, price, condition, desc in blueprints[:4]: + idx += 1 + row = { + "category_slug": category_slug, + "title": title, + "area": area_cycle[idx % len(area_cycle)], + "neighborhood": neighborhood_cycle[idx % len(neighborhood_cycle)], + "price": price, + "condition": condition, + "description": desc + ". Contact by email for details.", + "details": { + "posted_by": "owner", + "availability": "available now", + "cross_streets": neighborhood_cycle[(idx + 2) % len(neighborhood_cycle)], + }, + "image": pick_image(pools, category_slug, idx), + } + records.append(finalize_record(row, pools, idx)) + return records + + +def seed_categories(db, Category): + if Category.query.count() > 0: + return + order = 0 + for group in CATEGORY_GROUPS: + for slug, name, abbrev in group["columns"]: + order += 1 + db.session.add(Category( + slug=slug, + name=name, + abbrev=abbrev, + group_slug=group["slug"], + group_name=group["name"], + display_order=order, + )) + db.session.commit() + + +def seed_database(base_dir, db, Category, Listing): + if Listing.query.count() > 0: + return + seed_categories(db, Category) + category_map = {c.slug: c for c in Category.query.all()} + records = build_listing_records(base_dir) + for idx, row in enumerate(records, start=1): + category = category_map[row["category_slug"]] + title = row["title"] + listing = Listing( + title=title, + slug=f"{slugify(title)}-{idx}", + category_id=category.id, + category_slug=category.slug, + category_group=category.group_slug, + area=row.get("area", REGIONS[idx % len(REGIONS)]), + neighborhood=row.get("neighborhood", "san francisco"), + price=row.get("price"), + bedrooms=row.get("bedrooms"), + sqft=row.get("sqft"), + condition=row.get("condition", ""), + compensation=row.get("compensation", ""), + company=row.get("company", ""), + employment_type=row.get("employment_type", ""), + description=row.get("description", ""), + details_json=json.dumps(row.get("details", {}), sort_keys=True), + image=row.get("image", ""), + seller_name=row.get("seller_name", "craigslist user"), + seller_email=row.get("seller_email", f"reply{idx}@example.test"), + reply_phone=row.get("reply_phone", ""), + posted_at=FIXED_NOW - timedelta(hours=idx * 3), + updated_at=FIXED_NOW - timedelta(hours=idx * 2), + status="active", + ) + db.session.add(listing) + db.session.commit() + + +def seed_benchmark_users(db, User, Listing, SavedListing, SavedSearch, Message): + if User.query.filter_by(email="alice.j@test.com").first(): + return + + users = [ + ("alice.j@test.com", "Alice Johnson", "alice", "san francisco"), + ("ben.k@test.com", "Ben Kim", "ben", "east bay"), + ("carla.m@test.com", "Carla Martinez", "carla", "south bay"), + ("david.p@test.com", "David Patel", "david", "peninsula"), + ] + created = [] + for email, name, username, area in users: + user = User( + email=email, + username=username, + name=name, + area=area, + phone="(415) 555-0100", + password_hash=deterministic_password(email), + created_at=FIXED_NOW - timedelta(days=45), + ) + db.session.add(user) + created.append(user) + db.session.flush() + + alice = created[0] + desk = Listing.query.filter(Listing.title.ilike("%task chair%")).first() + accord = Listing.query.filter(Listing.title.ilike("%Accord%")).first() + studio = Listing.query.filter(Listing.title.ilike("%Berkeley BART%")).first() + for listing in [desk, accord, studio]: + if listing: + db.session.add(SavedListing( + user_id=alice.id, + listing_id=listing.id, + note="compare before contacting", + created_at=FIXED_NOW - timedelta(days=2), + )) + + db.session.add(SavedSearch( + user_id=alice.id, + name="East Bay furniture under 100", + query_text="chair desk", + category_slug="furniture", + area="east bay", + max_price=100, + created_at=FIXED_NOW - timedelta(days=3), + )) + db.session.add(SavedSearch( + user_id=alice.id, + name="Studios with laundry", + query_text="studio laundry", + category_slug="apartments", + area="east bay", + max_price=2400, + created_at=FIXED_NOW - timedelta(days=1), + )) + + if studio: + db.session.add(Message( + user_id=alice.id, + listing_id=studio.id, + sender_name="Leasing office", + sender_email="leasing@example.test", + body="The Berkeley studio can be shown Wednesday at 5:30pm or Thursday at noon.", + direction="inbound", + created_at=FIXED_NOW - timedelta(hours=18), + is_read=False, + )) + if accord: + db.session.add(Message( + user_id=alice.id, + listing_id=accord.id, + sender_name="Alice Johnson", + sender_email="alice.j@test.com", + body="Hi, is the Accord still available and can I see service records?", + direction="outbound", + created_at=FIXED_NOW - timedelta(hours=10), + is_read=True, + )) + + db.session.commit() diff --git a/sites/craigslist/static/css/.gitkeep b/sites/craigslist/static/css/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/sites/craigslist/static/css/icomoon.css b/sites/craigslist/static/css/icomoon.css new file mode 100644 index 0000000..b2070f0 --- /dev/null +++ b/sites/craigslist/static/css/icomoon.css @@ -0,0 +1,659 @@ +@font-face{font-family:'icomoon';src:url("data:application/x-font-ttf;charset=utf-8;base64,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AQIBAQEBAQEBGyUBAQEBAQEBAQIBAQEBAQEBAQEBAQECAQEBAQEBAQMBAQMCAQEBAQICBAIBBAIBAgEBAwECBAMJFQwMGQ4NHQ4PHg8DCAQBBAICAwIEBwQDCAMCAwICAwIEBgQDBwMCAwEBAgECAQMGAwICAgEDAgMFAwMFAgEBASYaAQEDBQMBAwIBAwICBAIECgUFCgYDBQMDBgMCAwIBAwIDBwMBAQEBAgEBBAEEBgQDBwQCAwICAwIEBwMEBwQCAwICAwIEBwMPGw4NGQsMFAkJEAcBAwICAQIBAwEBAgEBAQEBAQIBAQEBAQEBAQEBGiRKAgEBAQEBAQECAQEBAQEBAQIBAQMCAgECAgMCAQQCAQEBAQIBAgQCCBMKCxYMDBkNDBoNBAYDAgMCAQMCAwYEAwYDAgMBAgMBAwYDAwUDAgICAQEBAgMFAwEDAQEDAQIFAwIEAwQIBAQGAwEDAQIDAQIDAQMFAgEBFyMDAQECBQMCBgICAgIBAwEDBgIBAgEBAQIDAQMGAwMGAwIDAQIDAQMHAwMGAwIDAQIDAgMGAwwYDAsVCgoRCAgNBQICAQEBAQIBAgEBAgEBAQECAQEBAQEBAQEBAQEeFgEBAQEBAQEBAQECAQEBAQEBAwEBAwEBAQEBAQECAwECBAEBAgEBAgECBAIIEwoLFgwMGQwNGg0DBwMCAwECAwIDBgMEBgMBAwIBAwIDBgMDBQMCAgICAQEBAwUDAQMBAQMBAgUCAwQDBAgEAwcDAQMBAgMBAgMBAgUDAQEXIgQCBQMCBgICAgIBAwEDBgIBAgEBAQIDAQMGAwMGAwIDAQIDAQMHAwMGAwIDAQIDAgMGAwwYDAsVCgoRCAgNBQICAQEBAQIBAgEBAgEBAQECAQEBAQEBAQEBAQEfFgG+AwUDAgICAQMCAgQCBQkFBQoGAwYCAwYDAgMCAQMCAwcDAQEBAQIBAQQBBAcDAwcEAgMCAgMCBAcDBAcEAgMCAgMCBAcEDhwNDRkLDBQKCQ8HAQMCAgECAgIBAQIBAQEBAQECAQEBAQEBAQEBARolAQEBAQEBAQECAQEBAQEBAQEBAQEBAgEBAQEBAQEDAQEDAgEBAQECAgQCAQQCAQIBAQMBAgQDCRUMDBkODR0ODx4PAwgEAQQCAgMCBAcEAwgDAgMCAgMCBAYEAwcDAgMBAQIBAgEDBgMC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") format('truetype');font-weight:normal;font-style:normal;font-display:block}[class^="icom-"],[class*=" icom-"]{font-family:'icomoon' !important;speak:never;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.icom-academia:before{content:"\e901"}.icom-facebook:before{content:"\e902";color:#1877f2}.icom-google:before{content:"\e903";color:#4285f4}.icom-microsoftoutlook:before{content:"\e907";color:#0072c6}.icom-twitter:before{content:"\e906";color:#1da1f2}.icom-yahoo:before{content:"\e900";color:#6001d2}.icom-facebook1:before{content:"\e908";color:#1877f2}.icom-share:before{content:"\e90a"}.icom-share1:before{content:"\e90b"}.icom-share2:before{content:"\e90c"}.icom-share3:before{content:"\e913"}.icom-share4:before{content:"\e915"}.icom-target:before{content:"\e914"}.icom-equalizer:before{content:"\eb5b"}.icom-plus3:before{content:"\ed5d"}.icom-bell2:before{content:"\ea58"}.icom-map5:before{content:"\ea4a"}.icom-list:before{content:"\ec59"}.icom-grid:before{content:"\ec5d"}.icom-star-empty:before{content:"\ece0"}.icom-peace:before{content:"\ed00"}.icom-play4:before{content:"\ed85"}.icom-previous2:before{content:"\ed8c"}.icom-next2:before{content:"\ed8d"}.icom-paragraph-justify3:before{content:"\eec4"}.icom-envelop5:before{content:"\ea35"}.icom-floppy-disk:before{content:"\e904"}.icom-bin:before{content:"\e909"}.icom-plus:before{content:"\e90d"}.icom-minus:before{content:"\e90e"}.icom-close:before{content:"\e90f"}.icom-checkmark:before{content:"\e910"}.icom-radio-checked:before{content:"\e911"}.icom-radio-unchecked:before{content:"\e912"}.icom-warning:before{content:"\ea07"}.icom-notification:before{content:"\ea08"}.icom-question:before{content:"\ea09"}.icom-info:before{content:"\ea0c"}.icom-cancel-circle:before{content:"\ea0d"}.icom-pencil:before{content:"\e905"}.icom-spinner11:before{content:"\e984"}.icom-search:before{content:"\e986"}.icom-cog:before{content:"\e994"}.icom-arrow-up:before{content:"\ea32"}.icom-arrow-right:before{content:"\ea34"}.icom-arrow-down:before{content:"\ea36"}.icom-arrow-left:before{content:"\ea38"}.icom-arrow-up2:before{content:"\ea3a"}.icom-arrow-right2:before{content:"\ea3c"}.icom-arrow-down2:before{content:"\ea3e"}.icom-arrow-left2:before{content:"\ea40"}.icom-sort-alpha-asc:before{content:"\ea48"}.icom-sort-alpha-desc:before{content:"\ea49"}.icom-image2:before{content:"\e93c"}.icom-bubble-dots:before{content:"\eace"}.icom-bubble-dots2:before{content:"\ead1"}.icom-bubble-dots3:before{content:"\ead9"}.icom-bubble-dots4:before{content:"\eadc"}.icom-user:before{content:"\eaf7"}.icom-user2:before{content:"\eb00"}.icom-user3:before{content:"\eb08"}.icom-spinner:before{content:"\eb22"}.icom-spinner2:before{content:"\eb23"}.icom-spinner3:before{content:"\eb24"}.icom-spinner5:before{content:"\eb26"}.icom-spinner6:before{content:"\eb27"}.icom-spinner9:before{content:"\eb2a"}.icom-spinner10:before{content:"\eb2b"}.icom-spinner111:before{content:"\eb2c"}.icom-enlarge6:before{content:"\eb46"}.icom-shrink6:before{content:"\eb47"}.icom-enlarge7:before{content:"\eb48"}.icom-shrink7:before{content:"\eb49"}.icom-lock4:before{content:"\eb53"}.icom-unlocked:before{content:"\eb54"}.icom-cog1:before{content:"\eb5f"}.icom-cog2:before{content:"\eb61"}.icom-cog3:before{content:"\eb62"}.icom-cog4:before{content:"\eb63"}.icom-cog6:before{content:"\eb65"}.icom-bin1:before{content:"\ebfd"}.icom-bin2:before{content:"\ebfe"}.icom-attachment:before{content:"\ecac"}.icom-circle2:before{content:"\ee78"}.icom-database-insert:before{content:"\ea9a"}.icom-database-export:before{content:"\ea9b"}.icom-rotate-cw2:before{content:"\eaaa"}.icom-rotate-cw3:before{content:"\eaac"}.icom-unite:before{content:"\eab5"}.icom-user21:before{content:"\eb01"}.icom-enlarge2:before{content:"\eb3e"}.icom-shrink2:before{content:"\eb3f"}.icom-enlarge5:before{content:"\eb44"}.icom-shrink5:before{content:"\eb45"}.icom-star-full:before{content:"\ece2"}.icom-select2:before{content:"\ed2b"}.icom-warning1:before{content:"\ed4f"}.icom-notification1:before{content:"\ed51"}.icom-cancel-square:before{content:"\ed68"}.icom-checkmark1:before{content:"\ed6f"}.icom-checkmark4:before{content:"\ed72"}.icom-volume-mute5:before{content:"\eda7"}.icom-arrow-up21:before{content:"\edb8"}.icom-arrow-right21:before{content:"\edbc"}.icom-arrow-down21:before{content:"\edc0"}.icom-arrow-left21:before{content:"\edc4"}.icom-key-up:before{content:"\ee5e"}.icom-key-right:before{content:"\ee5f"}.icom-key-down:before{content:"\ee60"}.icom-key-left:before{content:"\ee61"}.icom-checkbox-checked:before{content:"\ee66"}.icom-checkbox-unchecked:before{content:"\ee67"}.icom-toggle-off:before{content:"\ee75"}.icom-compass:before{content:"\e949"}.icom-flag3:before{content:"\eca6"}.icom-flag4:before{content:"\eca7"}.icom-copy3:before{content:"\e9c7"}.icom-phone2:before{content:"\ea1d"}.icom-bubble:before{content:"\eac4"}.icom-pencil7:before{content:"\e916"}.icom-bubbles10:before{content:"\eadf"}.icom-pushpin:before{content:"\ea37"}.icom-user4:before{content:"\eb09"}.icom-search5:before{content:"\eb3a"}.icom-warning2:before{content:"\ed50"}.icom-heart:before{content:"\e9da"}.icom-stack-empty:before{content:"\ed3b"}.icom-stack-check:before{content:"\ed43"}.icom-info1:before{content:"\ed63"}.icom-info2:before{content:"\ed64"}.icom-last:before{content:"\ed8b"}.icom-previous21:before{content:"\ed8e"}.icom-location3:before{content:"\ea3b"}.icom-thumbs-up:before{content:"\ecf4"}.icom-thumbs-down:before{content:"\ecf6"}.icom-stats-bars2:before{content:"\eb8a"}.icom-eye3:before{content:"\ecb4"}.icom-point-up:before{content:"\e917"}.icom-crop:before{content:"\e918"}.icom-menu2:before{content:"\f0c9"}.icom-caret-down:before{content:"\f0d7"}.icom-caret-up:before{content:"\f0d8"}.icom-caret-left:before{content:"\f0d9"}.icom-caret-right:before{content:"\f0da"}.icom-meatballs:before{content:"\f141"}.icom-kabob:before{content:"\f142"}/* required styles */ + +.leaflet-pane, +.leaflet-tile, +.leaflet-marker-icon, +.leaflet-marker-shadow, +.leaflet-tile-container, +.leaflet-pane > svg, +.leaflet-pane > canvas, +.leaflet-zoom-box, +.leaflet-image-layer, +.leaflet-layer { + position: absolute; + left: 0; + top: 0; + } +.leaflet-container { + overflow: hidden; + } +.leaflet-tile, +.leaflet-marker-icon, +.leaflet-marker-shadow { + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + -webkit-user-drag: none; + } +/* Prevents IE11 from highlighting tiles in blue */ +.leaflet-tile::-moz-selection { + background: transparent; +} +.leaflet-tile::selection { + background: transparent; +} +/* Safari renders non-retina tile on retina better with this, but Chrome is worse */ +.leaflet-safari .leaflet-tile { + image-rendering: -webkit-optimize-contrast; + } +/* hack that prevents hw layers "stretching" when loading new tiles */ +.leaflet-safari .leaflet-tile-container { + width: 1600px; + height: 1600px; + -webkit-transform-origin: 0 0; + } +.leaflet-marker-icon, +.leaflet-marker-shadow { + display: block; + } +/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */ +/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */ +.leaflet-container .leaflet-overlay-pane svg { + max-width: none !important; + max-height: none !important; + } +.leaflet-container .leaflet-marker-pane img, +.leaflet-container .leaflet-shadow-pane img, +.leaflet-container .leaflet-tile-pane img, +.leaflet-container img.leaflet-image-layer, +.leaflet-container .leaflet-tile { + max-width: none !important; + max-height: none !important; + width: auto; + padding: 0; + } + +.leaflet-container img.leaflet-tile { + /* See: https://bugs.chromium.org/p/chromium/issues/detail?id=600120 */ + mix-blend-mode: plus-lighter; +} + +.leaflet-container.leaflet-touch-zoom { + touch-action: pan-x pan-y; + } +.leaflet-container.leaflet-touch-drag { + /* Fallback for FF which doesn't support pinch-zoom */ + touch-action: none; + touch-action: pinch-zoom; +} +.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom { + touch-action: none; +} +.leaflet-container { + -webkit-tap-highlight-color: transparent; +} +.leaflet-container a { + -webkit-tap-highlight-color: rgba(51, 181, 229, 0.4); +} +.leaflet-tile { + -webkit-filter: inherit; + filter: inherit; + visibility: hidden; + } +.leaflet-tile-loaded { + visibility: inherit; + } +.leaflet-zoom-box { + width: 0; + height: 0; + -moz-box-sizing: border-box; + box-sizing: border-box; + z-index: 800; + } +/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */ +.leaflet-overlay-pane svg { + -moz-user-select: none; + } + +.leaflet-pane { z-index: 400; } + +.leaflet-tile-pane { z-index: 200; } +.leaflet-overlay-pane { z-index: 400; } +.leaflet-shadow-pane { z-index: 500; } +.leaflet-marker-pane { z-index: 600; } +.leaflet-tooltip-pane { z-index: 650; } +.leaflet-popup-pane { z-index: 700; } + +.leaflet-map-pane canvas { z-index: 100; } +.leaflet-map-pane svg { z-index: 200; } + +.leaflet-vml-shape { + width: 1px; + height: 1px; + } +.lvml { + behavior: url(#default#VML); + display: inline-block; + position: absolute; + } + + +/* control positioning */ + +.leaflet-control { + position: relative; + z-index: 800; + pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */ + pointer-events: auto; + } +.leaflet-top, +.leaflet-bottom { + position: absolute; + z-index: 1000; + pointer-events: none; + } +.leaflet-top { + top: 0; + } +.leaflet-right { + right: 0; + } +.leaflet-bottom { + bottom: 0; + } +.leaflet-left { + left: 0; + } +.leaflet-control { + float: left; + clear: both; + } +.leaflet-right .leaflet-control { + float: right; + } +.leaflet-top .leaflet-control { + margin-top: 10px; + } +.leaflet-bottom .leaflet-control { + margin-bottom: 10px; + } +.leaflet-left .leaflet-control { + margin-left: 10px; + } +.leaflet-right .leaflet-control { + margin-right: 10px; + } + + +/* zoom and fade animations */ + +.leaflet-fade-anim .leaflet-popup { + opacity: 0; + -webkit-transition: opacity 0.2s linear; + transition: opacity 0.2s linear; + } +.leaflet-fade-anim .leaflet-map-pane .leaflet-popup { + opacity: 1; + } +.leaflet-zoom-animated { + -webkit-transform-origin: 0 0; + transform-origin: 0 0; + } +svg.leaflet-zoom-animated { + will-change: transform; +} + +.leaflet-zoom-anim .leaflet-zoom-animated { + -webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1); + transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1); + transition: transform 0.25s cubic-bezier(0,0,0.25,1); + transition: transform 0.25s cubic-bezier(0,0,0.25,1), -webkit-transform 0.25s cubic-bezier(0,0,0.25,1); + } +.leaflet-zoom-anim .leaflet-tile, +.leaflet-pan-anim .leaflet-tile { + -webkit-transition: none; + transition: none; + } + +.leaflet-zoom-anim .leaflet-zoom-hide { + visibility: hidden; + } + + +/* cursors */ + +.leaflet-interactive { + cursor: pointer; + } +.leaflet-grab { + cursor: -webkit-grab; + cursor: grab; + } +.leaflet-crosshair, +.leaflet-crosshair .leaflet-interactive { + cursor: crosshair; + } +.leaflet-popup-pane, +.leaflet-control { + cursor: auto; + } +.leaflet-dragging .leaflet-grab, +.leaflet-dragging .leaflet-grab .leaflet-interactive, +.leaflet-dragging .leaflet-marker-draggable { + cursor: move; + cursor: -webkit-grabbing; + cursor: grabbing; + } + +/* marker & overlays interactivity */ +.leaflet-marker-icon, +.leaflet-marker-shadow, +.leaflet-image-layer, +.leaflet-pane > svg path, +.leaflet-tile-container { + pointer-events: none; + } + +.leaflet-marker-icon.leaflet-interactive, +.leaflet-image-layer.leaflet-interactive, +.leaflet-pane > svg path.leaflet-interactive, +svg.leaflet-image-layer.leaflet-interactive path { + pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */ + pointer-events: auto; + } + +/* visual tweaks */ + +.leaflet-container { + background: #ddd; + outline-offset: 1px; + } +.leaflet-container a { + color: #0078A8; + } +.leaflet-zoom-box { + border: 2px dotted #38f; + background: rgba(255,255,255,0.5); + } + + +/* general typography */ +.leaflet-container { + font-family: "Helvetica Neue", Arial, Helvetica, sans-serif; + font-size: 12px; + font-size: 0.75rem; + line-height: 1.5; + } + + +/* general toolbar styles */ + +.leaflet-bar { + box-shadow: 0 1px 5px rgba(0,0,0,0.65); + border-radius: 4px; + } +.leaflet-bar a { + background-color: #fff; + border-bottom: 1px solid #ccc; + width: 26px; + height: 26px; + line-height: 26px; + display: block; + text-align: center; + text-decoration: none; + color: black; + } +.leaflet-bar a, +.leaflet-control-layers-toggle { + background-position: 50% 50%; + background-repeat: no-repeat; + display: block; + } +.leaflet-bar a:hover, +.leaflet-bar a:focus { + background-color: #f4f4f4; + } +.leaflet-bar a:first-child { + border-top-left-radius: 4px; + border-top-right-radius: 4px; + } +.leaflet-bar a:last-child { + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + border-bottom: none; + } +.leaflet-bar a.leaflet-disabled { + cursor: default; + background-color: #f4f4f4; + color: #bbb; + } + +.leaflet-touch .leaflet-bar a { + width: 30px; + height: 30px; + line-height: 30px; + } +.leaflet-touch .leaflet-bar a:first-child { + border-top-left-radius: 2px; + border-top-right-radius: 2px; + } +.leaflet-touch .leaflet-bar a:last-child { + border-bottom-left-radius: 2px; + border-bottom-right-radius: 2px; + } + +/* zoom control */ + +.leaflet-control-zoom-in, +.leaflet-control-zoom-out { + font: bold 18px 'Lucida Console', Monaco, monospace; + text-indent: 1px; + } + +.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out { + font-size: 22px; + } + + +/* layers control */ + +.leaflet-control-layers { + box-shadow: 0 1px 5px rgba(0,0,0,0.4); + background: #fff; + border-radius: 5px; + } +.leaflet-control-layers-toggle { + background-image: url(images/layers.png); + width: 36px; + height: 36px; + } +.leaflet-retina .leaflet-control-layers-toggle { + background-image: url(images/layers-2x.png); + background-size: 26px 26px; + } +.leaflet-touch .leaflet-control-layers-toggle { + width: 44px; + height: 44px; + } +.leaflet-control-layers .leaflet-control-layers-list, +.leaflet-control-layers-expanded .leaflet-control-layers-toggle { + display: none; + } +.leaflet-control-layers-expanded .leaflet-control-layers-list { + display: block; + position: relative; + } +.leaflet-control-layers-expanded { + padding: 6px 10px 6px 6px; + color: #333; + background: #fff; + } +.leaflet-control-layers-scrollbar { + overflow-y: scroll; + overflow-x: hidden; + padding-right: 5px; + } +.leaflet-control-layers-selector { + margin-top: 2px; + position: relative; + top: 1px; + } +.leaflet-control-layers label { + display: block; + font-size: 13px; + font-size: 1.08333em; + } +.leaflet-control-layers-separator { + height: 0; + border-top: 1px solid #ddd; + margin: 5px -10px 5px -6px; + } + +/* Default icon URLs */ +.leaflet-default-icon-path { /* used only in path-guessing heuristic, see L.Icon.Default */ + background-image: url(images/marker-icon.png); + } + + +/* attribution and scale controls */ + +.leaflet-container .leaflet-control-attribution { + background: #fff; + background: rgba(255, 255, 255, 0.8); + margin: 0; + } +.leaflet-control-attribution, +.leaflet-control-scale-line { + padding: 0 5px; + color: #333; + line-height: 1.4; + } +.leaflet-control-attribution a { + text-decoration: none; + } +.leaflet-control-attribution a:hover, +.leaflet-control-attribution a:focus { + text-decoration: underline; + } +.leaflet-attribution-flag { + display: inline !important; + vertical-align: baseline !important; + width: 1em; + height: 0.6669em; + } +.leaflet-left .leaflet-control-scale { + margin-left: 5px; + } +.leaflet-bottom .leaflet-control-scale { + margin-bottom: 5px; + } +.leaflet-control-scale-line { + border: 2px solid #777; + border-top: none; + line-height: 1.1; + padding: 2px 5px 1px; + white-space: nowrap; + -moz-box-sizing: border-box; + box-sizing: border-box; + background: rgba(255, 255, 255, 0.8); + text-shadow: 1px 1px #fff; + } +.leaflet-control-scale-line:not(:first-child) { + border-top: 2px solid #777; + border-bottom: none; + margin-top: -2px; + } +.leaflet-control-scale-line:not(:first-child):not(:last-child) { + border-bottom: 2px solid #777; + } + +.leaflet-touch .leaflet-control-attribution, +.leaflet-touch .leaflet-control-layers, +.leaflet-touch .leaflet-bar { + box-shadow: none; + } +.leaflet-touch .leaflet-control-layers, +.leaflet-touch .leaflet-bar { + border: 2px solid rgba(0,0,0,0.2); + background-clip: padding-box; + } + + +/* popup */ + +.leaflet-popup { + position: absolute; + text-align: center; + margin-bottom: 20px; + } +.leaflet-popup-content-wrapper { + padding: 1px; + text-align: left; + border-radius: 12px; + } +.leaflet-popup-content { + margin: 13px 24px 13px 20px; + line-height: 1.3; + font-size: 13px; + font-size: 1.08333em; + min-height: 1px; + } +.leaflet-popup-content p { + margin: 17px 0; + margin: 1.3em 0; + } +.leaflet-popup-tip-container { + width: 40px; + height: 20px; + position: absolute; + left: 50%; + margin-top: -1px; + margin-left: -20px; + overflow: hidden; + pointer-events: none; + } +.leaflet-popup-tip { + width: 17px; + height: 17px; + padding: 1px; + + margin: -10px auto 0; + pointer-events: auto; + + -webkit-transform: rotate(45deg); + transform: rotate(45deg); + } +.leaflet-popup-content-wrapper, +.leaflet-popup-tip { + background: white; + color: #333; + box-shadow: 0 3px 14px rgba(0,0,0,0.4); + } +.leaflet-container a.leaflet-popup-close-button { + position: absolute; + top: 0; + right: 0; + border: none; + text-align: center; + width: 24px; + height: 24px; + font: 16px/24px Tahoma, Verdana, sans-serif; + color: #757575; + text-decoration: none; + background: transparent; + } +.leaflet-container a.leaflet-popup-close-button:hover, +.leaflet-container a.leaflet-popup-close-button:focus { + color: #585858; + } +.leaflet-popup-scrolled { + overflow: auto; + } + +.leaflet-oldie .leaflet-popup-content-wrapper { + -ms-zoom: 1; + } +.leaflet-oldie .leaflet-popup-tip { + width: 24px; + margin: 0 auto; + + -ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)"; + filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678); + } + +.leaflet-oldie .leaflet-control-zoom, +.leaflet-oldie .leaflet-control-layers, +.leaflet-oldie .leaflet-popup-content-wrapper, +.leaflet-oldie .leaflet-popup-tip { + border: 1px solid #999; + } + + +/* div icon */ + +.leaflet-div-icon { + background: #fff; + border: 1px solid #666; + } + + +/* Tooltip */ +/* Base styles for the element that has a tooltip */ +.leaflet-tooltip { + position: absolute; + padding: 6px; + background-color: #fff; + border: 1px solid #fff; + border-radius: 3px; + color: #222; + white-space: nowrap; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + pointer-events: none; + box-shadow: 0 1px 3px rgba(0,0,0,0.4); + } +.leaflet-tooltip.leaflet-interactive { + cursor: pointer; + pointer-events: auto; + } +.leaflet-tooltip-top:before, +.leaflet-tooltip-bottom:before, +.leaflet-tooltip-left:before, +.leaflet-tooltip-right:before { + position: absolute; + pointer-events: none; + border: 6px solid transparent; + background: transparent; + content: ""; + } + +/* Directions */ + +.leaflet-tooltip-bottom { + margin-top: 6px; +} +.leaflet-tooltip-top { + margin-top: -6px; +} +.leaflet-tooltip-bottom:before, +.leaflet-tooltip-top:before { + left: 50%; + margin-left: -6px; + } +.leaflet-tooltip-top:before { + bottom: 0; + margin-bottom: -12px; + border-top-color: #fff; + } +.leaflet-tooltip-bottom:before { + top: 0; + margin-top: -12px; + margin-left: -6px; + border-bottom-color: #fff; + } +.leaflet-tooltip-left { + margin-left: -6px; +} +.leaflet-tooltip-right { + margin-left: 6px; +} +.leaflet-tooltip-left:before, +.leaflet-tooltip-right:before { + top: 50%; + margin-top: -6px; + } +.leaflet-tooltip-left:before { + right: 0; + margin-right: -12px; + border-left-color: #fff; + } +.leaflet-tooltip-right:before { + left: 0; + margin-left: -12px; + border-right-color: #fff; + } + +/* Printing */ + +@media print { + /* Prevent printers from removing background-images of controls. */ + .leaflet-control { + -webkit-print-color-adjust: exact; + color-adjust: exact; + print-color-adjust: exact; + } + } +.leaflet-control-zoom{background:none repeat scroll 0 0 rgba(0,0,0,0.25);border-radius:7px 7px 7px 7px;padding:5px}.leaflet-control-zoom a{background-color:rgba(255,255,255,0.75);border-radius:4px 4px 4px 4px}.leaflet-popup-content-wrapper{border-radius:20px}@media print{.leaflet-control-zoom{display:none}}.leaflet-popup-content-wrapper{border-radius:12px}.leaflet-container a.leaflet-popup-hide-button{position:absolute;top:30px;right:10px;cursor:pointer}.leaflet-popup-content{margin:0;padding:1.5em 1em 1em 1em}.title-preview .leaflet-popup-content{padding-top:1em}.cl-marker-icon{background-color:hsla(249,100%,85%,0.6);border:1px solid hsla(249,100%,80%,0.6)}.bd-can-hover .cl-marker-icon:hover,.cl-marker-icon-visited{background-color:hsla(300,69%,83%,0.6);border:1px solid hsla(300,69%,78%,0.6)}.cl-marker-icon.fav,.cl-marker-icon-visited.fav{border:1px solid #888888;font-weight:bolder}.cl-marker-icon,.cl-marker-icon-visited{border-radius:100px;text-align:center}.cl-marker-icon .fav,.cl-marker-icon-visited .fav,.bd-can-hover .cl-marker-icon .fav:hover,.bd-can-hover .cl-marker-icon-visited .fav:hover{background-color:#ffd700;border-color:#daa520}.cl-marker-icon.banished,.cl-marker-icon-visited.banished{display:none}.show-banished .cl-marker-icon.banished,.show-banished .cl-marker-icon-visited.banished,.fav .cl-marker-icon.banished,.fav .cl-marker-icon-visited.banished{display:block;border-color:#a00;color:#888}table{border-collapse:collapse;border-spacing:0}.screen-reader-text{position:absolute;left:-10000px;top:auto;width:1px;height:1px;overflow:hidden;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;user-select:none} diff --git a/sites/craigslist/static/css/main.css b/sites/craigslist/static/css/main.css new file mode 100644 index 0000000..4c8eb06 --- /dev/null +++ b/sites/craigslist/static/css/main.css @@ -0,0 +1,2977 @@ +:root { + --link: #0000ee; + --visited: #551a8b; + --text: #111; + --soft: #f4f4f4; + --panel: #eee; + --border: #ccc; + --light-border: #ddd; +} + +* { + box-sizing: border-box; +} + +html { + background: #fff; +} + +body { + margin: 0; + background: #fff; + color: var(--text); + font-family: Arial, Helvetica, sans-serif; + font-size: 14px; + line-height: 1.25; +} + +a { + color: var(--link); + text-decoration: none; +} + +a:visited { + color: var(--visited); +} + +a:hover { + text-decoration: underline; +} + +button, +input, +select, +textarea, +.buttonish { + font: inherit; +} + +button, +.buttonish { + display: inline-block; + border: 1px solid #999; + border-radius: 3px; + background: #f7f7f7; + color: #000; + padding: 4px 9px; + cursor: pointer; +} + +input, +select, +textarea { + border: 1px solid #aaa; + background: #fff; + padding: 4px 6px; +} + +ul { + margin-top: 0; +} + +.plain-button { + border: 0; + background: transparent; + color: var(--link); + padding: 0; +} + +.global-header { + width: 100%; + min-height: 62px; + display: grid; + grid-template-columns: auto 190px minmax(300px, 1fr) auto; + align-items: center; + gap: 12px; + padding: 8px 12px; + border-bottom: 1px solid var(--border); + background: #eee; +} + +.header-logo { + display: flex; + align-items: center; + gap: 6px; + color: var(--visited); + font-size: 18px; + white-space: nowrap; +} + +.header-logo span:first-child { + display: grid; + width: 34px; + height: 34px; + place-items: center; + border: 2px solid var(--visited); + border-radius: 50%; + font-size: 24px; + line-height: 1; +} + +.header-logo:visited { + color: var(--visited); +} + +.breadcrumbs { + list-style: none; + margin: 0; + padding: 0; + font-size: 20px; + font-weight: bold; +} + +.site-search { + display: grid; + grid-template-columns: minmax(220px, 1fr) 210px auto; + gap: 8px; + align-items: center; +} + +.site-search input, +.site-search select { + width: 100%; + height: 38px; + font-size: 16px; +} + +.site-search button { + height: 38px; + padding: 0 14px; + font-size: 16px; +} + +.userlinks { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 18px; + white-space: nowrap; +} + +.header-action { + display: grid; + min-width: 42px; + justify-items: center; + color: #000; + line-height: 1; + text-align: center; +} + +.header-action small { + margin-top: 2px; + color: #000; + font-size: 12px; +} + +.header-action:visited { + color: #000; +} + +.post-action span, +.account-action span, +.fave-action span { + position: relative; + display: block; + width: 30px; + height: 30px; +} + +.post-action span::before { + content: ""; + position: absolute; + left: 4px; + top: 7px; + width: 18px; + height: 18px; + border: 3px solid #008a00; + border-radius: 2px; +} + +.post-action span::after { + content: ""; + position: absolute; + left: 17px; + top: 1px; + width: 5px; + height: 30px; + background: #008a00; + transform: rotate(45deg); +} + +.account-action span::before { + content: ""; + position: absolute; + left: 10px; + top: 2px; + width: 10px; + height: 10px; + border: 2px solid #111; + border-radius: 50%; +} + +.account-action span::after { + content: ""; + position: absolute; + left: 5px; + top: 17px; + width: 20px; + height: 10px; + border: 2px solid #111; + border-bottom: 0; + border-radius: 10px 10px 0 0; +} + +.fave-action span::before { + content: "\2605"; + position: absolute; + left: 0; + top: -3px; + color: #f0c800; + font-size: 34px; + line-height: 1; +} + +.home-page .site-search { + display: none; +} + +.home-page .global-header { + display: none; +} + +.page-container { + width: min(1260px, 100%); + margin: 0 auto; + padding: 10px 12px 0; +} + +.home-page .page-container { + width: 100%; + max-width: none; + margin: 0; + padding: 0; +} + +.flash-wrap { + width: min(1260px, 100%); + margin: 8px auto 0; + padding: 0 12px; +} + +.flash { + border: 1px solid #ccc; + background: #ffffe5; + padding: 7px 9px; + margin-bottom: 6px; +} + +.flash.error { + background: #ffecec; + border-color: #d99; +} + +.flash.success { + background: #eefbea; + border-color: #9c9; +} + +/* Craigslist home page shell */ +.homepage-content { + display: grid; + width: 960px; + max-width: 100%; + margin: 0 auto; + grid-template-columns: 199px minmax(0, 608px) 113px; + grid-template-rows: 44px auto; + grid-template-areas: + "left topban right" + "left center right"; + column-gap: 20px; + row-gap: 19px; + align-items: start; + font-size: 14px; +} + +#topban { + grid-area: topban; + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 12px; + align-items: center; + min-height: 44px; + margin: 0; + padding: 0 0 0 20px; + background: var(--panel); + border-bottom: 1px solid var(--border); +} + +.regular-area { + display: flex; + align-items: center; + gap: 9px; +} + +.pin-icon { + position: relative; + display: inline-block; + width: 16px; + height: 16px; + flex: 0 0 16px; + margin-left: 1px; + background: var(--link); + border-radius: 50% 50% 50% 0; + transform: rotate(-45deg); +} + +.pin-icon::after { + content: ""; + position: absolute; + left: 5px; + top: 5px; + width: 6px; + height: 6px; + border-radius: 50%; + background: #fff; +} + +.regular-area .area { + margin: 0; + color: var(--link); + font-family: Georgia, "Times New Roman", serif; + font-size: 23px; + font-weight: normal; +} + +.sublinks { + display: inline-flex; + gap: 6px; + list-style: none; + margin: 0; + padding: 0; + font-size: 13px; +} + +.sublinks a { + display: block; + background: #fff; + padding: 4px 6px; +} + +.home-actions { + display: flex; + gap: 17px; + align-items: center; + list-style: none; + margin: 0; + padding: 2px 15px 0 0; + font-size: 10px; + text-align: center; +} + +.home-actions a, +.home-actions a:visited { + display: flex; + flex-direction: column; + align-items: center; + color: #000; +} + +.action-symbol { + position: relative; + display: block; + height: 22px; + width: 22px; + font-size: 22px; + line-height: 1; +} + +.action-symbol.star::before { + content: "\2605"; + color: #ffd900; + font-family: Arial, Helvetica, sans-serif; + font-size: 24px; + line-height: 22px; +} + +.action-symbol.post-icon::before { + content: ""; + position: absolute; + left: 4px; + top: 5px; + width: 12px; + height: 12px; + border: 2px solid #008a00; + border-radius: 2px; +} + +.action-symbol.post-icon::after { + content: ""; + position: absolute; + left: 11px; + top: 2px; + width: 3px; + height: 17px; + background: #008a00; + transform: rotate(45deg); + transform-origin: center; +} + +.action-symbol.post-icon { + color: #008a00; +} + +.action-symbol.acct-icon::before { + content: ""; + position: absolute; + left: 7px; + top: 2px; + width: 9px; + height: 9px; + border: 2px solid #111; + border-radius: 50%; +} + +.action-symbol.acct-icon::after { + content: ""; + position: absolute; + left: 3px; + top: 14px; + width: 16px; + height: 8px; + border: 2px solid #111; + border-bottom: 0; + border-radius: 18px 18px 0 0; +} + +.action-symbol.acct-icon { + color: #111; +} + +.leftbar { + grid-area: left; + padding: 6px 7px 15px; + border-right: 1px solid var(--border); + background: #f7f7f7; +} + +#logo { + display: flex; + align-items: center; + justify-content: center; + gap: 7px; + margin: 0 0 16px; + color: var(--visited); + font-family: Georgia, "Times New Roman", serif; + font-size: 30px; + font-weight: normal; + text-align: center; +} + +.peace-mark { + font-family: Arial, Helvetica, sans-serif; + font-size: 29px; + line-height: 1; +} + +#postlks, +.leftlinks, +.menu, +.col ul { + list-style: none; + padding: 0; +} + +#postlks { + width: 112px; + margin: 0 auto 16px; + padding: 5px 0; + background: #fff; + text-align: center; + font-size: 16px; +} + +#postlks a, +#postlks a:visited { + color: #008a00; +} + +.home-search { + position: relative; + display: grid; + gap: 5px; + width: 186px; + margin: 0 auto 59px; +} + +.home-search::before { + content: ""; + position: absolute; + left: 7px; + top: 5px; + width: 9px; + height: 9px; + border: 2px solid #aaa; + border-radius: 50%; + pointer-events: none; +} + +.home-search::after { + content: ""; + position: absolute; + left: 17px; + top: 16px; + width: 9px; + height: 2px; + background: #aaa; + transform: rotate(45deg); + transform-origin: left center; + pointer-events: none; +} + +.home-search input, +.home-search select, +.home-search button { + width: 100%; +} + +.home-search input { + height: 30px; + padding-left: 25px; + font-size: 15px; +} + +.home-search select, +.home-search button { + display: none; +} + +#calban { + margin: 0; + padding: 0 0 2px; + background: var(--panel); + border: 0; + text-align: center; + font-size: 13px; +} + +.cal { + width: 155px; + margin: 0 auto; + border-collapse: collapse; + background: #fff; + border: 1px solid var(--border); + text-align: center; + font-size: 13px; +} + +.cal th, +.cal td { + width: 22px; + padding: 2px 1px; + border: 1px solid var(--border); +} + +.cal .days th { + color: #555; + font-weight: normal; +} + +.cal .today { + background: #ffffcc; + font-weight: bold; +} + +.leftlinks { + margin: 56px 0 0; + text-align: center; + font-size: 14px; +} + +.leftlinks li { + margin: 9px 0; +} + +.charity-links { + margin-top: 44px; +} + +.charity-icons { + margin-top: 10px !important; + font-size: 12px; + letter-spacing: 1px; +} + +#center { + grid-area: center; + display: grid; + grid-template-columns: minmax(180px, .95fr) minmax(225px, 1.05fr) minmax(170px, .9fr); + gap: 25px; +} + +.center-col { + display: flex; + flex-direction: column; + gap: 17px; +} + +.col { + min-height: 0; +} + +.ban { + margin: 0 0 4px; + padding: 4px 4px; + background: var(--panel); + border: 1px solid var(--border); + color: #000; + font-size: 16px; + line-height: 1.1; + text-align: center; +} + +.ban a, +.ban a:visited { + color: var(--link); + font-weight: bold; +} + +.col ul { + margin: 0; + font-size: 14px; + line-height: 1.15; +} + +.col li { + margin: 0; + padding: 0 0 1px; + border-bottom: 1px solid #ddd; + white-space: normal; +} + +.two-col { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + column-gap: 22px; +} + +.community-stack .two-col { + column-gap: 12px; +} + +.three-col { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + column-gap: 22px; +} + +.jobs-stack { + min-width: 0; +} + +.jobs-stack .two-col { + grid-template-columns: 1fr; +} + +.jobs-stack .gigs-list { + grid-template-columns: repeat(2, minmax(0, 1fr)); + column-gap: 18px; +} + +.resume-col .ban { + margin-bottom: 0; +} + +.extra-cats { + margin-top: 0 !important; +} + +#rightbar { + grid-area: right; + padding: 15px 5px; + border-left: 1px solid var(--border); + background: #f7f7f7; + font-size: 12px; + text-align: center; +} + +#langlinks select { + width: 100%; + margin-bottom: 12px; + font-size: 12px; +} + +.menu { + margin: 0 0 11px; +} + +.menu h5.ban { + font-size: 13px; + text-align: center; +} + +.menu ul { + list-style: none; + margin: 0; + padding: 0 0 0 2px; +} + +.menu li li { + margin: 0; + line-height: 1.18; + padding: 0; +} + +.quick-world { + border-top: 2px solid #ddd; + font-weight: bold; +} + +.quick-world li { + border-bottom: 2px solid #ddd; + padding: 5px 0; +} + +/* Modern Craigslist-style search result pages */ +.search-page .global-header, +.search-page .footer { + display: none; +} + +.search-page .page-container { + width: 100%; + max-width: none; + margin: 0; + padding: 0; +} + +.search-shell { + width: 100%; + min-height: 100vh; + color: #333; + background: #fff; + font-size: 14px; +} + +.cl-search-topbar { + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; + gap: 8px; + align-items: center; + min-height: 45px; + padding: 5px 8px; + border-bottom: 1px solid #ccc; + background: #eee; +} + +.cl-mini-logo { + display: flex; + align-items: center; + gap: 6px; + color: var(--visited); + font-size: 17px; + white-space: nowrap; +} + +.cl-mini-logo span:first-child { + display: grid; + width: 25px; + height: 25px; + place-items: center; + border: 2px solid var(--visited); + border-radius: 50%; + font-size: 18px; + line-height: 1; +} + +.top-selects { + display: grid; + grid-template-columns: 190px 190px 132px minmax(220px, 300px); + gap: 8px; + min-width: 0; +} + +.top-selects select, +.main-search-box input, +.sort-form select, +.modern-filters input, +.modern-filters select { + min-width: 0; + border: 1px solid #bbb; + border-radius: 4px; + background: #fff; +} + +.top-selects select { + height: 34px; + padding: 2px 28px 2px 9px; + font-size: 15px; +} + +.top-actions { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 12px; +} + +.top-icon { + display: grid; + min-width: 32px; + justify-items: center; + color: #000; + line-height: 1; +} + +.top-icon:visited { + color: #000; +} + +.top-icon small { + margin-top: 2px; + font-size: 12px; +} + +.top-icon span { + position: relative; + display: block; + width: 25px; + height: 25px; +} + +.top-post span { + border: 3px solid #008a00; + border-radius: 2px; +} + +.top-post span::after { + content: ""; + position: absolute; + right: -5px; + top: -8px; + width: 5px; + height: 29px; + background: #008a00; + transform: rotate(45deg); +} + +.top-account span::before { + content: ""; + position: absolute; + left: 9px; + top: 2px; + width: 10px; + height: 10px; + border: 2px solid #111; + border-radius: 50%; +} + +.top-account span::after { + content: ""; + position: absolute; + left: 4px; + top: 16px; + width: 20px; + height: 10px; + border: 2px solid #111; + border-bottom: 0; + border-radius: 11px 11px 0 0; +} + +.search-command { + display: grid; + grid-template-columns: 192px minmax(260px, 1fr) auto; + gap: 8px; + align-items: center; + padding: 7px 8px; +} + +.search-back { + overflow: hidden; + color: var(--link); + font-size: 21px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.main-search-box { + position: relative; + display: grid; + grid-template-columns: minmax(0, 1fr) 40px; + gap: 8px; +} + +.main-search-box input { + height: 40px; + padding-left: 37px; + font-size: 18px; +} + +.main-search-box button { + position: relative; + overflow: hidden; + width: 40px; + height: 40px; + border-color: #bbb; + color: transparent; +} + +.main-search-box button::before { + content: ""; + position: absolute; + left: 11px; + top: 10px; + width: 13px; + height: 13px; + border: 3px solid var(--link); + border-radius: 50%; +} + +.main-search-box button::after { + content: ""; + position: absolute; + left: 24px; + top: 24px; + width: 12px; + height: 3px; + background: var(--link); + transform: rotate(45deg); +} + +.search-glass { + position: absolute; + left: 11px; + top: 10px; + width: 14px; + height: 14px; + border: 3px solid #bbb; + border-radius: 50%; + z-index: 1; +} + +.search-glass::after { + content: ""; + position: absolute; + left: 15px; + top: 15px; + width: 13px; + height: 3px; + background: #bbb; + transform: rotate(45deg); +} + +.result-count { + color: #333; + font-size: 14px; + white-space: nowrap; +} + +.search-tools { + display: grid; + grid-template-columns: 192px 118px 118px minmax(300px, 1fr); + gap: 10px; + align-items: center; + padding: 6px 8px 8px; +} + +.owner-tabs, +.view-buttons, +.quick-filter-buttons { + display: flex; + gap: 0; +} + +.owner-tabs button, +.view-buttons a, +.quick-filter-buttons button { + display: grid; + place-items: center; + height: 34px; + border: 1px solid #ccc; + border-radius: 0; + background: #fff; + color: #333; + font-size: 14.4px; + text-decoration: none; +} + +.owner-tabs button { + min-width: 62px; +} + +.view-buttons a { + width: 34px; +} + +.owner-tabs button:first-child, +.view-buttons a:first-child, +.quick-filter-buttons button:first-child { + border-radius: 4px 0 0 4px; +} + +.owner-tabs button:last-child, +.view-buttons a:last-child, +.quick-filter-buttons button:last-child { + border-radius: 0 4px 4px 0; +} + +.owner-tabs .selected, +.view-buttons .selected { + border-color: var(--link); + background: #dbe5ff; + color: var(--link); +} + +.sort-form select { + width: 118px; + height: 34px; + font-size: 14.4px; +} + +.quick-filter-buttons { + gap: 8px; +} + +.quick-filter-buttons button { + border-radius: 4px; + padding: 0 11px; + font-size: 14px; +} + +.modern-search-layout { + display: grid; + grid-template-columns: 192px minmax(0, 1fr); + gap: 0; + align-items: start; + padding: 0 0 24px 8px; +} + +.modern-filters { + position: sticky; + top: 0; + min-height: calc(100vh - 88px); + padding: 0 6px 12px; + border: 0; + background: #fff; + color: #444; +} + +.modern-filters form { + margin: 0; +} + +.modern-filters label { + display: block; + margin: 5px 0; + font-weight: normal; +} + +.modern-filters .check { + font-size: 13px; +} + +.modern-filters input[type="checkbox"] { + width: auto; + margin-right: 4px; +} + +.filter-heading { + margin-top: 16px !important; + color: #444; + font-size: 12px; + font-weight: bold !important; + text-transform: uppercase; +} + +.inline-inputs { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + gap: 4px; + align-items: center; +} + +.price-inputs { + grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); +} + +.inline-inputs input { + width: 100%; + height: 24px; + padding: 2px 5px; +} + +.use-map { + display: block; + margin: 4px 0 12px; +} + +.price-histogram { + display: flex; + align-items: end; + gap: 2px; + height: 52px; + margin: 6px 0 12px 3px; + border-bottom: 2px solid #333; +} + +.price-histogram span { + display: block; + width: 9px; + background: #2374b7; +} + +.update-search { + margin-top: 10px; +} + +.modern-results { + min-width: 0; + padding: 0 8px; +} + +.result-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(320px, 344px)); + gap: 28px; + justify-content: start; + margin: 0; + padding: 0; + border: 0; + list-style: none; + background: transparent; +} + +.result-card { + position: relative; + display: block; + min-width: 0; + overflow: hidden; + border: 1px solid #ccc; + border-radius: 4px; + background: #fff; + box-shadow: 0 1px 5px rgba(0, 0, 0, .18); +} + +.card-photo { + position: relative; + display: block; + width: 100%; + height: 264px; + overflow: hidden; + background: #fff; +} + +.card-image-link { + display: block; + width: 100%; + height: 100%; +} + +.gallery-frame { + display: none; +} + +.gallery-frame.active { + display: block; +} + +.card-photo img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.card-photo .gallery-frame { + display: none; +} + +.card-photo .gallery-frame.active { + display: block; +} + +.card-photo .no-img { + display: grid; + width: 100%; + height: 100%; + place-items: center; + color: #777; + background: #eee; +} + +.card-price { + position: absolute; + left: 0; + top: 0; + z-index: 1; + padding: 2px 5px; + border-radius: 0 0 4px 0; + background: rgba(255, 255, 255, .9); + color: #008000; + font-size: 18px; +} + +.photo-dots { + display: flex; + justify-content: center; + gap: 12px; + height: 19px; + align-items: center; +} + +.photo-dots span, +.photo-dots button { + width: 4px; + height: 4px; + padding: 0; + border: 0; + border-radius: 50%; + background: #ccc; + cursor: pointer; +} + +.photo-dots .active, +.photo-dots button.active { + background: #008000; +} + +.card-gallery-nav { + position: absolute; + top: 50%; + z-index: 2; + display: none; + width: 28px; + height: 44px; + padding: 0; + border: 0; + border-radius: 0; + background: rgba(255, 255, 255, .78); + color: #333; + font-size: 32px; + line-height: 1; + transform: translateY(-50%); +} + +.card-gallery-nav.prev { + left: 0; +} + +.card-gallery-nav.next { + right: 0; +} + +.result-card:hover .card-gallery-nav, +.result-card:focus-within .card-gallery-nav { + display: block; +} + +.card-body { + position: relative; + min-height: 54px; + padding: 0 28px 5px 8px; +} + +.card-body .result-title { + display: block; + overflow: hidden; + margin-bottom: 2px; + font-size: 16px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.card-body .meta { + display: flex; + flex-wrap: wrap; + gap: 4px; + color: #666; + font-size: 12px; +} + +.card-actions { + position: absolute; + left: 7px; + right: 6px; + bottom: 3px; + display: flex; + justify-content: space-between; + pointer-events: none; +} + +.card-actions form { + pointer-events: auto; +} + +.view-list .result-list.result-grid { + display: block; +} + +.view-list .result-row.result-card { + display: grid; + grid-template-columns: 160px minmax(0, 1fr); + gap: 10px; + margin: 0; + border-radius: 0; + box-shadow: none; +} + +.view-list .card-photo { + width: 160px; + height: 118px; +} + +.view-list .photo-dots { + display: none; +} + +.view-list .card-body { + min-height: 118px; + padding: 12px 34px 12px 0; +} + +.view-list .card-body .result-title { + white-space: normal; +} + +.view-grid .result-list.result-grid { + grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); + gap: 18px; +} + +.view-grid .card-photo { + height: 185px; +} + +.view-grid .card-body { + min-height: 76px; +} + +.map-results-panel { + display: grid; + grid-template-columns: minmax(360px, 1fr) 280px; + min-height: 330px; + margin-bottom: 18px; + border: 1px solid #aaa; + background: #f5f5f5; +} + +.bay-map, +.detail-map { + position: relative; + overflow: hidden; + background: + radial-gradient(ellipse at 35% 35%, rgba(118, 178, 219, .58) 0 20%, transparent 21%), + radial-gradient(ellipse at 54% 48%, rgba(118, 178, 219, .65) 0 27%, transparent 28%), + linear-gradient(35deg, transparent 0 38%, rgba(255, 255, 255, .55) 39% 42%, transparent 43%), + linear-gradient(90deg, rgba(0, 0, 0, .06) 1px, transparent 1px), + linear-gradient(rgba(0, 0, 0, .06) 1px, transparent 1px), + #e7eadf; + background-size: auto, auto, auto, 32px 32px, 32px 32px, auto; +} + +.bay-map { + min-height: 330px; +} + +.map-water { + position: absolute; + left: 45%; + top: 38%; + color: rgba(35, 99, 145, .6); + font-size: 28px; + font-style: italic; +} + +.map-label { + position: absolute; + z-index: 1; + padding: 2px 5px; + border: 1px solid rgba(0, 0, 0, .12); + background: rgba(255, 255, 255, .72); + color: #555; + font-size: 12px; +} + +.map-label.sf { + left: 42%; + top: 43%; +} + +.map-label.oakland { + left: 61%; + top: 43%; +} + +.map-label.san-jose { + left: 68%; + top: 73%; +} + +.map-road { + position: absolute; + z-index: 1; + display: block; + height: 4px; + border-top: 1px solid rgba(180, 160, 100, .95); + border-bottom: 1px solid rgba(180, 160, 100, .95); + background: rgba(255, 255, 255, .72); + transform-origin: left center; +} + +.road-1 { + left: 30%; + top: 54%; + width: 48%; + transform: rotate(28deg); +} + +.road-2 { + left: 42%; + top: 36%; + width: 34%; + transform: rotate(-9deg); +} + +.map-pin { + position: absolute; + z-index: 3; + display: grid; + width: 25px; + height: 25px; + place-items: center; + border: 2px solid #fff; + border-radius: 50% 50% 50% 0; + background: #d42027; + color: #fff; + box-shadow: 0 1px 4px rgba(0, 0, 0, .35); + font-size: 11px; + font-weight: bold; + text-decoration: none; + transform: translate(-50%, -100%) rotate(-45deg); +} + +.map-pin span { + transform: rotate(45deg); +} + +.map-pin.single { + pointer-events: none; +} + +.map-result-index { + max-height: 330px; + margin: 0; + padding: 8px 10px 8px 28px; + overflow: auto; + border-left: 1px solid #bbb; + background: #fff; +} + +.map-result-index li { + margin-bottom: 9px; +} + +.map-result-index span { + display: block; + color: #666; + font-size: 12px; +} + +.view-map .result-list.result-grid { + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + gap: 18px; +} + +.view-map .card-photo { + height: 220px; +} + +.card-star, +.card-trash { + border: 0; + background: transparent; + color: #888; + padding: 0; + font-size: 21px; + line-height: 1; +} + +.card-trash { + font-size: 16px; +} + +/* Search pages */ +.search-layout { + display: grid; + grid-template-columns: 240px minmax(0, 1fr); + gap: 12px; + align-items: start; +} + +.filters { + border: 1px solid var(--border); + background: #f7f7f7; + padding: 10px; +} + +.filters h1 { + margin: 0 0 9px; + color: var(--visited); + font-size: 22px; +} + +.filters label, +.form-page label { + display: block; + margin: 8px 0 3px; + font-weight: bold; +} + +.filters input, +.filters select, +.form-page input, +.form-page select, +.form-page textarea { + width: 100%; +} + +.filters button, +.form-page button { + margin-top: 8px; +} + +.check { + font-weight: normal !important; +} + +.check input { + width: auto; +} + +.save-search-form { + margin-top: 16px; + padding-top: 10px; + border-top: 1px solid var(--border); +} + +.mini-cats { + margin-top: 16px; + column-count: 2; + column-gap: 12px; + font-size: 13px; +} + +.mini-cats h3 { + break-after: avoid; + margin: 0 0 3px; + padding: 3px 5px; + background: var(--panel); + border: 1px solid var(--border); + font-size: 13px; +} + +.mini-cats a { + display: block; + line-height: 1.45; +} + +.results { + min-width: 0; +} + +.results-bar { + display: flex; + flex-wrap: wrap; + gap: 8px; + align-items: baseline; + background: var(--panel); + border: 1px solid var(--border); + padding: 6px 9px; + margin-bottom: 0; +} + +.result-list { + list-style: none; + padding: 0; + margin: 0; + border-left: 1px solid var(--light-border); + border-right: 1px solid var(--light-border); + background: #fff; +} + +.result-row { + display: grid; + grid-template-columns: 118px minmax(0, 1fr); + gap: 10px; + border-bottom: 1px solid var(--light-border); + padding: 8px; +} + +.thumb { + display: block; + width: 108px; + height: 82px; + overflow: hidden; + border: 1px solid #ccc; + background: #eee; +} + +.thumb img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.no-img, +.image-placeholder { + display: grid; + width: 100%; + height: 100%; + place-items: center; + color: #777; + font-size: 12px; +} + +.result-title-line { + display: flex; + align-items: baseline; + gap: 8px; +} + +.result-title { + font-size: 16px; +} + +.star { + padding: 1px 6px; + font-size: 12px; +} + +.meta { + display: flex; + flex-wrap: wrap; + gap: 9px; + margin-top: 3px; + color: #666; + font-size: 13px; +} + +.result-main p { + margin: 5px 0; + color: #333; +} + +.empty { + border: 1px solid var(--light-border); + background: #fff; + color: #666; + padding: 20px; + text-align: center; +} + +/* Posting detail */ +.detail-shell, +.narrow { + max-width: 1060px; + margin: 0 auto; +} + +.breadcrumb { + margin: 0 0 8px; + color: #666; + font-size: 13px; +} + +.posting { + border: 1px solid var(--border); + background: #fff; +} + +.posting-head { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 14px; + align-items: center; + padding: 8px 10px; + border-bottom: 1px solid var(--border); + background: var(--panel); +} + +.posting-title-line { + display: flex; + flex-wrap: wrap; + gap: 8px; + align-items: baseline; +} + +.posting-title-line h1 { + margin: 0; + font-size: 22px; + font-weight: normal; +} + +.posting-price { + color: #008000; + font-size: 22px; +} + +.posting-hood { + color: #555; + font-size: 16px; +} + +.posting-meta { + display: flex; + flex-wrap: wrap; + gap: 9px; + margin-top: 3px; + color: #555; + font-size: 12px; +} + +.posting-actions { + display: flex; + gap: 6px; + align-items: center; +} + +.reply-button { + font-weight: bold; +} + +.posting-body { + display: grid; + grid-template-columns: minmax(360px, 700px) minmax(240px, 1fr); + gap: 12px; + padding: 12px 10px; +} + +.posting-image { + min-height: 300px; + border: 1px solid var(--light-border); + background: #f1f1f1; +} + +.posting-photo-stage { + position: relative; + overflow: hidden; + background: #f2f2f2; +} + +.posting-image img { + display: block; + width: 100%; + max-height: 560px; + object-fit: contain; + background: #f2f2f2; +} + +.posting-image .gallery-frame { + display: none; +} + +.posting-image .gallery-frame.active { + display: block; +} + +.gallery-arrow { + position: absolute; + top: 50%; + width: 42px; + height: 64px; + padding: 0; + border: 0; + border-radius: 0; + background: rgba(255, 255, 255, .8); + color: #333; + font-size: 44px; + line-height: 1; + transform: translateY(-50%); +} + +.gallery-arrow.prev { + left: 0; +} + +.gallery-arrow.next { + right: 0; +} + +.photo-strip { + display: flex; + justify-content: center; + gap: 11px; + padding: 8px 0 9px; +} + +.photo-strip span, +.photo-strip button { + width: 5px; + height: 5px; + padding: 0; + border: 0; + border-radius: 50%; + background: #ccc; + cursor: pointer; +} + +.photo-strip .active, +.photo-strip button.active { + background: #008a00; +} + +.facts { + display: grid; + grid-template-columns: 112px minmax(0, 1fr); + margin: 0; + border: 1px solid var(--light-border); + font-size: 13px; +} + +.facts dt, +.facts dd { + margin: 0; + padding: 5px 7px; + border-bottom: 1px solid #eee; +} + +.facts dt { + background: #f5f5f5; + color: #555; +} + +.detail-map { + position: relative; + min-height: 110px; + margin-top: 10px; + border: 1px solid var(--light-border); + color: #555; +} + +.detail-map .map-caption { + position: absolute; + left: 8px; + right: 8px; + bottom: 8px; + z-index: 2; + padding: 5px 7px; + border: 1px solid rgba(0, 0, 0, .15); + background: rgba(255, 255, 255, .82); + text-align: center; +} + +.detail-map .map-caption strong, +.detail-map .map-caption span { + display: block; +} + +.description { + max-width: 720px; + padding: 0 10px 15px; + line-height: 1.5; +} + +.description h2 { + margin: 4px 0 6px; + font-size: 16px; +} + +.nearby-results { + margin-top: 12px; + padding: 10px; + border: 1px solid var(--border); + background: #f7f7f7; +} + +.nearby-results h2 { + margin: 0 0 7px; + font-size: 16px; +} + +/* Forms and account pages */ +.form-page { + width: min(520px, 100%); + margin: 20px auto; + padding: 14px 16px 16px; + border: 1px solid var(--border); + background: #f5f5f5; +} + +.form-page.wide { + width: min(760px, 100%); +} + +.form-page h1 { + margin-top: 0; + margin-bottom: 12px; + color: var(--visited); + font-size: 24px; + font-weight: bold; +} + +.form-page textarea { + resize: vertical; +} + +.form-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px; +} + +.form-back { + margin: 0 0 8px; + font-size: 13px; +} + +.reply-summary { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-bottom: 10px; + color: #555; +} + +.reply-relay { + margin-bottom: 10px; + padding: 8px 10px; + border: 1px solid #d5d5d5; + background: #fff; + color: #444; + font-size: 13px; +} + +.account-shell { + max-width: 1060px; + margin: 0 auto; +} + +.account-tabs { + display: flex; + flex-wrap: wrap; + gap: 0; + margin: 0 0 12px; + border-bottom: 1px solid var(--border); +} + +.account-tabs a { + padding: 7px 12px; + border: 1px solid var(--border); + border-bottom: 0; + background: #eee; +} + +.account-tabs a.active { + background: #fff; + color: #000; + font-weight: bold; +} + +.account-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; +} + +.panel { + padding: 13px; + border: 1px solid var(--border); + background: #fff; +} + +.panel h1, +.panel h2 { + margin-top: 0; + color: var(--visited); + font-size: 20px; +} + +.message-list { + list-style: none; + margin: 0; + padding: 0; +} + +.message-list li { + margin-bottom: 10px; + padding: 11px; + border: 1px solid var(--light-border); + background: #fff; +} + +.message-list li.inbound { + border-left: 4px solid var(--visited); +} + +.message-head { + display: flex; + justify-content: space-between; + gap: 10px; +} + +.saved-shell h1, +.messages-shell h1 { + margin: 0 0 12px; + color: var(--visited); + font-size: 24px; +} + +.compact-list { + list-style: none; + margin: 0; + padding: 0; +} + +.compact-list li { + padding: 5px 0; + border-bottom: 1px dotted #ccc; +} + +.date, +.muted, +.hint { + color: #666; +} + +.date { + display: inline-block; + min-width: 54px; +} + +.footer { + width: 100%; + margin: 25px auto 0; + padding: 12px 12px 9px; + border-top: 1px solid #ddd; + display: block; + color: #666; + background: var(--panel); + text-align: center; +} + +.footer-links { + display: flex; + justify-content: center; + gap: 22px; + margin-bottom: 12px; + font-size: 14px; +} + +.copyright { + color: #555; + font-size: 13px; +} + +.home-page .footer { + margin-top: 28px; +} + +.search-page .modern-filters { + min-height: calc(100vh - 88px); + padding: 0 6px 12px; + border: 0; + background: #fff; +} + +.search-page .result-list.result-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(320px, 344px)); + gap: 28px; + justify-content: start; + border: 0; + background: transparent; +} + +.search-page .result-row.result-card { + display: block; + gap: 0; + padding: 0; + border: 1px solid #ccc; + border-radius: 4px; + box-shadow: 0 1px 5px rgba(0, 0, 0, .18); +} + +/* Search-page fidelity pass: dimensions and icon font from craigslist's current assets. */ +.search-page .icon { + display: inline-block; + font-family: "icomoon" !important; + font-style: normal; + font-weight: normal; + line-height: 1; + text-transform: none; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.search-page .cl-search-topbar { + min-height: 45px; + padding: 3px 8px; +} + +.search-page .cl-mini-logo { + gap: 6px; + font-size: 17px; +} + +.search-page .cl-mini-logo .icon { + display: block; + width: 25px; + height: 25px; + border: 0; + border-radius: 0; + font-size: 25px; +} + +.search-page .cl-mini-logo .icon::before { + content: "\ed00"; +} + +.search-page .top-actions { + gap: 12px; +} + +.search-page .top-icon { + min-width: 32px; + line-height: 1; +} + +.search-page .top-icon .icon { + display: block; + width: 25px; + height: 25px; + border: 0; + border-radius: 0; + font-size: 25px; +} + +.search-page .top-icon .icon::before, +.search-page .top-icon .icon::after { + position: static; + display: inline; + width: auto; + height: auto; + border: 0; + background: transparent; + transform: none; +} + +.search-page .top-post .icon::before { + content: "\e916"; + color: #090; +} + +.search-page .top-account .icon::before { + content: "\eb09"; + color: #000; +} + +.search-page .top-icon small { + margin-top: 1px; + font-size: 12px; +} + +.search-page .top-selects select { + height: 34px; + padding: 2px 28px 2px 9px; + font-size: 15px; +} + +.search-page .search-command { + grid-template-columns: 192px minmax(260px, 1fr) auto; + gap: 8px; + min-height: 54px; + padding: 5px 8px; +} + +.search-title-row { + display: grid; + grid-template-columns: minmax(0, 1fr) 26px; + align-items: center; + min-width: 0; +} + +.search-page .search-back { + font-size: 21px; +} + +.filter-toggle { + display: grid; + width: 26px; + height: 30px; + place-items: center; + border: 0; + background: transparent; + color: var(--link); + font-size: 18px; +} + +.filter-toggle .icon::before { + content: "\edc4"; +} + +.search-page .main-search-box { + grid-template-columns: minmax(0, 1fr) 38px 38px; + gap: 7px; +} + +.search-page .main-search-box input { + height: 34px; + padding: 3px 8px 3px 38px; + font-size: 16px; +} + +.search-page .search-glass { + left: 10px; + top: 6px; + width: auto; + height: auto; + border: 0; + border-radius: 0; + color: #bbb; + font-size: 23px; + z-index: 1; +} + +.search-page .search-glass::before { + content: "\e986"; +} + +.search-page .search-glass::after { + content: none; +} + +.search-page .main-search-box .cl-icon-button { + display: grid; + width: 38px; + height: 34px; + place-items: center; + padding: 0; + border: 1px solid #ccc; + border-radius: 4px; + background: #fff; + color: var(--link); +} + +.search-page .main-search-box .cl-icon-button::before, +.search-page .main-search-box .cl-icon-button::after { + content: none; +} + +.search-page .cl-exec-search .icon::before { + content: "\e986"; + font-size: 24px; +} + +.search-page .cl-save-search .icon::before { + content: "\ea58"; + font-size: 19px; +} + +.search-page .result-count { + justify-self: end; + font-size: 14px; +} + +.search-page .search-tools { + grid-template-columns: 192px 152px 128px minmax(300px, 1fr); + gap: 8px; + min-height: 52px; + padding: 0 8px 8px; +} + +.search-page .owner-tabs button { + min-width: 62px; +} + +.search-page .owner-tabs button, +.search-page .view-buttons a, +.search-page .quick-filter-buttons button, +.search-page .sort-form select { + height: 34px; + border-color: #ccc; + background: #fff; + color: #333; + font-size: 14.4px; +} + +.search-page .view-buttons a { + width: 38px; + font-size: 16px; +} + +.search-page .view-buttons .selected, +.search-page .owner-tabs .selected { + border: 2px solid var(--link); + background: #bacff2; + color: var(--link); +} + +.search-page .cl-search-view-mode-list .icon::before { + content: "\eec4"; +} + +.search-page .cl-search-view-mode-thumb .icon::before { + content: "\ec59"; +} + +.search-page .cl-search-view-mode-gallery .icon::before { + content: "\e93c"; +} + +.search-page .cl-search-view-mode-map .icon::before { + content: "\ea4a"; +} + +.search-page .sort-form { + margin-left: 36px; +} + +.search-page .sort-form select { + width: 128px; +} + +.search-page .quick-filter-buttons { + gap: 8px; + margin-left: 76px; +} + +.search-page .quick-filter-buttons button { + border-radius: 4px; + padding: 0 11px; +} + +.search-page .modern-search-layout { + grid-template-columns: 192px minmax(0, 1fr); + padding: 0 0 24px 8px; +} + +.search-page .modern-filters { + padding: 0 7px 12px 0; + color: #444; +} + +.search-page .modern-filters .check { + margin: 4px 0; + font-size: 14px; +} + +.search-page .modern-filters input[type="checkbox"] { + width: 13px; + height: 13px; + margin: 0 5px 0 0; + vertical-align: -1px; +} + +.search-page .filter-heading { + margin: 15px 0 5px !important; + font-size: 12px; + line-height: 1.1; + text-transform: uppercase; +} + +.search-page .inline-inputs { + gap: 4px; +} + +.search-page .inline-inputs input, +.search-page .modern-filters .full-input { + height: 24px; + padding: 2px 5px; + font-size: 14px; +} + +.search-page .price-inputs { + grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); +} + +.search-page .price-histogram { + width: 178px; + height: 52px; + margin: 6px 0 12px 3px; +} + +.search-page .price-histogram span { + width: 9px; +} + +.filter-disclosure { + display: block; + width: 100%; + margin: 12px 0 0; + padding: 0; + border: 0; + background: transparent; + color: var(--link); + text-align: left; + font-size: 14px; +} + +.filter-disclosure span::before { + content: "\25b6"; + display: inline-block; + margin-right: 6px; + color: var(--link); + font-size: 10px; +} + +.search-page .modern-results { + padding: 0 8px; +} + +.search-page .result-list { + border: 0; + background: transparent; +} + +.search-page .result-list.result-grid { + grid-template-columns: repeat(auto-fill, 352px); + gap: 18px; +} + +.view-gallery .result-list.result-grid { + padding-top: 32px; +} + +.search-page .result-row.result-card { + width: 352px; + min-height: 345px; + overflow: hidden; +} + +.search-page .card-photo { + height: 264px; + background: #eee; +} + +.search-page .card-price { + padding: 2px 5px; + font-size: 16px; +} + +.search-page .photo-dots { + height: 18px; + gap: 13px; +} + +.search-page .photo-dots button { + width: 4px; + height: 4px; +} + +.search-page .card-body { + min-height: 62px; + padding: 0 28px 5px 8px; +} + +.search-page .card-body .result-title { + margin-bottom: 2px; + font-size: 16px; + line-height: 1.5; +} + +.search-page .card-body .meta { + gap: 4px; + margin-top: 0; + color: #666; + font-size: 14px; +} + +.icon-only { + display: inline-grid; + place-items: center; + padding: 0; + border: 0; + background: transparent; + color: #888; +} + +.card-star, +.card-trash, +.cl-favorite-button, +.cl-banish-button.icon-only { + width: 24px; + height: 24px; + border: 0; + background: transparent; + color: #888; + font-size: 16px; +} + +.card-star .icon::before, +.cl-favorite-button .icon::before { + content: "\ece0"; +} + +.card-trash .icon::before, +.cl-banish-button.icon-only .icon::before { + content: "\ebfd"; + color: #bbb; +} + +.view-list .list-results { + display: block; + padding-top: 32px; +} + +.view-list .cl-search-result { + margin: 0 0 16px; +} + +.view-list .result-node { + display: flex; + align-items: flex-start; + min-height: 21px; + overflow: hidden; + white-space: nowrap; +} + +.view-list .cl-favorite-button { + width: 32px; + height: 24px; + margin: -3px 0 0; + padding-right: 8px; +} + +.view-list .posting-title { + max-width: 46vw; + overflow: hidden; + padding-right: 4px; + font-size: 16px; + line-height: 1.15; + text-overflow: ellipsis; + white-space: nowrap; +} + +.view-list .list-meta { + color: #333; + font-size: 14px; + line-height: 1.15; +} + +.search-page .sep { + color: #666; +} + +.search-page .priceinfo { + color: #090; + font-weight: bold; +} + +.search-page .pic { + color: #f90; +} + +.cl-banish-text-button { + width: auto; + height: auto; + padding: 0; + border: 0; + background: transparent; + color: #777; + font-size: 14px; + line-height: 1.15; +} + +.view-thumb .thumb-results { + display: block; + padding-top: 32px; +} + +.view-thumb .cl-search-result { + height: 60px; + margin: 0 0 19px; + overflow: hidden; +} + +.view-thumb .thumb-node { + display: grid; + grid-template-columns: 64px minmax(0, 1fr); + align-items: start; +} + +.view-thumb .thumb-image { + display: flex; + width: 64px; + height: 60px; + padding: 3px; + align-items: flex-start; + justify-content: flex-start; + background: transparent; +} + +.view-thumb .thumb-image img, +.view-thumb .thumb-image .no-img { + width: 50px; + height: 50px; + margin: 4px 8px 0 0; + object-fit: cover; + font-size: 10px; +} + +.view-thumb .thumb-content { + min-width: 0; + padding-left: 14px; +} + +.view-thumb .thumb-location { + margin: 0 0 3px; + color: #333; + font-size: 16px; + line-height: 1.15; +} + +.view-thumb .thumb-title-line { + display: flex; + align-items: center; + height: 18px; + min-width: 0; +} + +.view-thumb .cl-favorite-button { + width: 28px; + height: 18px; + margin-right: 2px; +} + +.view-thumb .posting-title { + overflow: hidden; + font-size: 16px; + line-height: 1.15; + text-overflow: ellipsis; + white-space: nowrap; +} + +.view-thumb .thumb-meta { + display: flex; + align-items: center; + gap: 4px; + color: #333; + font-size: 14px; + line-height: 1; +} + +.view-thumb .thumb-meta form { + display: inline-flex; +} + +.view-thumb .cl-banish-button { + width: 18px; + height: 18px; + margin-left: 3px; +} + +.view-map .result-list.result-grid { + grid-template-columns: repeat(auto-fill, 352px); + gap: 18px; +} + +.view-map .card-photo { + height: 220px; +} + +@media (max-width: 950px) { + .global-header, + .homepage-content, + #topban, + .search-layout, + .posting-head, + .posting-body, + .account-grid { + grid-template-columns: 1fr; + } + + .homepage-content { + display: block; + width: 100%; + min-height: 0; + font-size: 16px; + grid-template-areas: + "topban" + "left" + "center" + "right"; + row-gap: 0; + } + + #topban { + display: flex; + flex-wrap: wrap; + gap: 6px 10px; + min-height: 0; + padding: 8px 10px; + } + + .regular-area { + min-width: 0; + gap: 8px; + } + + .pin-icon { + width: 21px; + height: 21px; + flex-basis: 21px; + } + + .pin-icon::after { + left: 6px; + top: 6px; + width: 9px; + height: 9px; + } + + .regular-area .area { + font-size: 28px; + } + + .sublinks { + gap: 5px; + font-size: 16px; + } + + .sublinks a { + padding: 4px 7px; + } + + .home-actions { + gap: 18px; + padding: 0 0 0 31px; + font-size: 12px; + } + + .action-symbol { + width: 27px; + height: 27px; + } + + .action-symbol.star::before { + font-size: 30px; + line-height: 27px; + } + + .action-symbol.post-icon::before { + left: 5px; + top: 6px; + width: 16px; + height: 16px; + border-width: 3px; + } + + .action-symbol.post-icon::after { + left: 15px; + top: 2px; + width: 4px; + height: 23px; + } + + .action-symbol.acct-icon::before { + left: 8px; + top: 2px; + width: 11px; + height: 11px; + border-width: 2px; + } + + .action-symbol.acct-icon::after { + left: 4px; + top: 18px; + width: 19px; + height: 9px; + border-width: 2px; + border-bottom: 0; + } + + .leftbar { + padding: 8px 10px 12px; + border-right: 0; + border-bottom: 1px solid var(--border); + } + + #logo, + #postlks, + #calban, + .cal, + .leftlinks, + .charity-links { + display: none; + } + + .home-search { + width: 100%; + margin: 0; + } + + .home-search input { + height: 36px; + padding-left: 31px; + font-size: 16px; + } + + .home-search::before { + left: 9px; + top: 8px; + width: 10px; + height: 10px; + border-width: 3px; + } + + .home-search::after { + left: 22px; + top: 22px; + width: 11px; + height: 3px; + } + + #center { + display: block; + padding: 10px; + } + + .center-col { + gap: 13px; + margin-bottom: 13px; + } + + .ban { + padding: 5px 6px; + font-size: 20px; + } + + .col ul { + font-size: 16px; + } + + .two-col, + .three-col, + .community-stack .two-col { + grid-template-columns: 1fr 1fr; + column-gap: 18px; + } + + #rightbar { + display: none; + } + + .site-search, + #center .community, + #center .housing, + #center .jobs, + .two-col, + .form-row { + grid-template-columns: 1fr; + } + + .userlinks, + .breadcrumbs-container { + text-align: left; + } + + .userlinks, + .sublinks { + justify-content: flex-start; + flex-wrap: wrap; + } + + .leftbar, + #rightbar { + min-height: 0; + } + + .mini-cats { + column-count: 1; + } + + .result-row { + grid-template-columns: 1fr; + } + + .search-page .result-list.result-grid { + grid-template-columns: 1fr; + } + + .search-page .card-photo { + height: 210px; + } + + .search-page .cl-search-topbar { + grid-template-columns: 1fr auto; + } + + .search-page .cl-mini-logo { + grid-column: 1 / 2; + } + + .search-page .top-selects { + grid-column: 1 / -1; + grid-template-columns: 1fr 1fr; + } + + .search-page .top-actions { + grid-column: 2 / 3; + grid-row: 1; + gap: 10px; + } + + .search-page .search-command { + grid-template-columns: 1fr; + padding: 7px 8px; + } + + .search-page .search-tools { + grid-template-columns: 1fr; + gap: 7px; + padding: 7px 8px; + } + + .search-page .modern-search-layout { + display: block; + padding: 0 8px 18px; + } + + .search-page .modern-results { + padding: 0; + } + + .search-page .modern-filters { + position: static; + min-height: 0; + margin-bottom: 10px; + padding: 8px; + border: 1px solid var(--border); + background: #f7f7f7; + } + + .search-page .modern-filters form { + display: grid; + grid-template-columns: 1fr 1fr; + column-gap: 8px; + } + + .search-page .modern-filters .filter-heading, + .search-page .modern-filters .inline-inputs, + .search-page .modern-filters .price-histogram, + .search-page .modern-filters .use-map, + .search-page .modern-filters .update-search { + display: none; + } + + .map-results-panel { + grid-template-columns: 1fr; + } + + .map-result-index { + max-height: 160px; + border-top: 1px solid #bbb; + border-left: 0; + } + + .search-layout { + display: flex; + flex-direction: column; + } + + .results { + order: 1; + } + + .filters { + order: 2; + } +} diff --git a/sites/craigslist/static/icons/.gitkeep b/sites/craigslist/static/icons/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/sites/craigslist/static/js/.gitkeep b/sites/craigslist/static/js/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/sites/craigslist/static/js/main.js b/sites/craigslist/static/js/main.js new file mode 100644 index 0000000..887485a --- /dev/null +++ b/sites/craigslist/static/js/main.js @@ -0,0 +1,57 @@ +(function () { + function setupGallery(root) { + var frames = Array.prototype.slice.call(root.querySelectorAll(".gallery-frame")); + if (frames.length <= 1) { + return; + } + var dots = Array.prototype.slice.call(root.querySelectorAll("[data-gallery-index]")); + var index = 0; + + function show(nextIndex) { + index = (nextIndex + frames.length) % frames.length; + frames.forEach(function (frame, frameIndex) { + frame.classList.toggle("active", frameIndex === index); + }); + dots.forEach(function (dot, dotIndex) { + dot.classList.toggle("active", dotIndex === index); + }); + } + + root.querySelectorAll("[data-gallery-prev]").forEach(function (button) { + button.addEventListener("click", function (event) { + event.preventDefault(); + event.stopPropagation(); + show(index - 1); + }); + }); + root.querySelectorAll("[data-gallery-next]").forEach(function (button) { + button.addEventListener("click", function (event) { + event.preventDefault(); + event.stopPropagation(); + show(index + 1); + }); + }); + dots.forEach(function (dot) { + dot.addEventListener("click", function (event) { + event.preventDefault(); + event.stopPropagation(); + show(Number(dot.getAttribute("data-gallery-index")) || 0); + }); + }); + } + + function submitTopSelects(form) { + form.querySelectorAll("select").forEach(function (select) { + select.addEventListener("change", function () { + if (select.name === "area") { + form.submit(); + } + }); + }); + } + + document.addEventListener("DOMContentLoaded", function () { + document.querySelectorAll("[data-gallery]").forEach(setupGallery); + document.querySelectorAll(".top-selects").forEach(submitTopSelects); + }); +}()); diff --git a/sites/craigslist/tasks.jsonl b/sites/craigslist/tasks.jsonl new file mode 100644 index 0000000..e99cc8d --- /dev/null +++ b/sites/craigslist/tasks.jsonl @@ -0,0 +1,20 @@ +{"web_name":"Craigslist","id":"Craigslist--0","ques":"Log in as alice.j@test.com with password TestPass123!, search furniture in the east bay for a chair under $100, open the result whose details say it has adjustable arms, and save it.","web":"http://localhost:40015/","upstream_url":"https://craigslist.org/"} +{"web_name":"Craigslist","id":"Craigslist--1","ques":"Find an east bay apartment or studio under $2400 that has in-unit laundry. Open the listing and report its square footage and pet policy.","web":"http://localhost:40015/","upstream_url":"https://craigslist.org/"} +{"web_name":"Craigslist","id":"Craigslist--2","ques":"Search bikes for a commuter bike under $500. Choose the listing whose detail table says it has hydraulic disc brakes, then send the seller a reply asking whether weekend pickup is possible.","web":"http://localhost:40015/","upstream_url":"https://craigslist.org/"} +{"web_name":"Craigslist","id":"Craigslist--3","ques":"Log in as alice.j@test.com with password TestPass123!, find the 2006 Honda Accord EX sedan listing under $7000 with clean title status, open its detail page, and save it to Alice's account.","web":"http://localhost:40015/","upstream_url":"https://craigslist.org/"} +{"web_name":"Craigslist","id":"Craigslist--4","ques":"Search for studio apartments and compare the Berkeley studio near BART with the quiet San Mateo studio. Which one has the lower rent per square foot?","web":"http://localhost:40015/","upstream_url":"https://craigslist.org/"} +{"web_name":"Craigslist","id":"Craigslist--5","ques":"Log in as alice.j@test.com with password TestPass123!, save a search named \"Gaming chair watch\" for furniture listings matching gaming chair with a maximum price of $200.","web":"http://localhost:40015/","upstream_url":"https://craigslist.org/"} +{"web_name":"Craigslist","id":"Craigslist--6","ques":"Create a new posting in the bikes category for a blue commuter bike in Oakland priced at $325, with a short body mentioning a rear rack and recently replaced brake pads.","web":"http://localhost:40015/","upstream_url":"https://craigslist.org/"} +{"web_name":"Craigslist","id":"Craigslist--7","ques":"Log in as alice.j@test.com with password TestPass123!, open Alice's messages, and identify what showing times the leasing office offered for the Berkeley studio.","web":"http://localhost:40015/","upstream_url":"https://craigslist.org/"} +{"web_name":"Craigslist","id":"Craigslist--8","ques":"From the homepage, browse to community events and find the Saturday plant swap. Open it and report the time and what attendees should bring.","web":"http://localhost:40015/","upstream_url":"https://craigslist.org/"} +{"web_name":"Craigslist","id":"Craigslist--9","ques":"Search free stuff in the east bay, find the listing for moving boxes near Oakland, and reply that you can pick them up after 6pm.","web":"http://localhost:40015/","upstream_url":"https://craigslist.org/"} +{"web_name":"Craigslist","id":"Craigslist--10","ques":"Search jobs for speech language pathologist, open the school-year role, and report the compensation range, setting, and required license.","web":"http://localhost:40015/","upstream_url":"https://craigslist.org/"} +{"web_name":"Craigslist","id":"Craigslist--11","ques":"Find the remote algebra and calculus tutoring service, open the detail page, and send a reply asking for two AP calculus time slots next week.","web":"http://localhost:40015/","upstream_url":"https://craigslist.org/"} +{"web_name":"Craigslist","id":"Craigslist--12","ques":"Log in as alice.j@test.com with password TestPass123! and update the account area to peninsula and the phone number to (650) 555-0144.","web":"http://localhost:40015/","upstream_url":"https://craigslist.org/"} +{"web_name":"Craigslist","id":"Craigslist--13","ques":"Log in as alice.j@test.com with password TestPass123!, open Alice's saved listings, and remove the saved item that is a Honda Accord, leaving the saved furniture and apartment items untouched.","web":"http://localhost:40015/","upstream_url":"https://craigslist.org/"} +{"web_name":"Craigslist","id":"Craigslist--14","ques":"Search electronics for a USB-C monitor, open the matching detail page, and report its resolution and ports.","web":"http://localhost:40015/","upstream_url":"https://craigslist.org/"} +{"web_name":"Craigslist","id":"Craigslist--15","ques":"Search moving services, find a two-person moving help listing, and report the hourly rate before replying with a request for a Saturday morning estimate.","web":"http://localhost:40015/","upstream_url":"https://craigslist.org/"} +{"web_name":"Craigslist","id":"Craigslist--16","ques":"Hide the Honda Passport project SUV listing from search results, then search Honda again and confirm the Accord and Odyssey remain visible.","web":"http://localhost:40015/","upstream_url":"https://craigslist.org/"} +{"web_name":"Craigslist","id":"Craigslist--17","ques":"Find the volunteer bike repair clinic in the east bay. Open its page and report the time and the listed skill requirement.","web":"http://localhost:40015/","upstream_url":"https://craigslist.org/"} +{"web_name":"Craigslist","id":"Craigslist--18","ques":"Search furniture for a walnut writing desk in San Francisco under $150. Open the matching listing and report its width and depth.","web":"http://localhost:40015/","upstream_url":"https://craigslist.org/"} +{"web_name":"Craigslist","id":"Craigslist--19","ques":"Search furniture for a standing desk frame in the peninsula. Open the white frame listing and report the height range and load rating.","web":"http://localhost:40015/","upstream_url":"https://craigslist.org/"} diff --git a/sites/craigslist/templates/.gitkeep b/sites/craigslist/templates/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/sites/craigslist/templates/account.html b/sites/craigslist/templates/account.html new file mode 100644 index 0000000..e44fc6a --- /dev/null +++ b/sites/craigslist/templates/account.html @@ -0,0 +1,69 @@ +{% extends "base.html" %} +{% block title %}account - craigslist{% endblock %} +{% block body_class %}account-page{% endblock %} +{% block content %} + +{% endblock %} diff --git a/sites/craigslist/templates/account_edit.html b/sites/craigslist/templates/account_edit.html new file mode 100644 index 0000000..2dd61f8 --- /dev/null +++ b/sites/craigslist/templates/account_edit.html @@ -0,0 +1,22 @@ +{% extends "base.html" %} +{% block title %}edit account - craigslist{% endblock %} +{% block body_class %}account-page account-edit-page{% endblock %} +{% block content %} +
+

< account

+

edit account

+
+ + + + + + + +
+
+{% endblock %} diff --git a/sites/craigslist/templates/base.html b/sites/craigslist/templates/base.html new file mode 100644 index 0000000..c951c60 --- /dev/null +++ b/sites/craigslist/templates/base.html @@ -0,0 +1,67 @@ + + + + + + {% block title %}craigslist{% endblock %} + + + + + + + + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} +
+ {% for category, message in messages %} +
{{ message }}
+ {% endfor %} +
+ {% endif %} + {% endwith %} + +
+ {% block content %}{% endblock %} +
+ + + + diff --git a/sites/craigslist/templates/index.html b/sites/craigslist/templates/index.html new file mode 100644 index 0000000..709504e --- /dev/null +++ b/sites/craigslist/templates/index.html @@ -0,0 +1,357 @@ +{% extends "base.html" %} +{% block title %}craigslist: SF bay area jobs, apartments, for sale, services, community{% endblock %} +{% block body_class %}home-page{% endblock %} +{% block content %} +{% set ns = namespace(community=None, services=None, housing=None, for_sale=None, jobs=None) %} +{% for group in category_groups() %} + {% if group.slug == "community" %}{% set ns.community = group %}{% endif %} + {% if group.slug == "services" %}{% set ns.services = group %}{% endif %} + {% if group.slug == "housing" %}{% set ns.housing = group %}{% endif %} + {% if group.slug == "for_sale" %}{% set ns.for_sale = group %}{% endif %} + {% if group.slug == "jobs" %}{% set ns.jobs = group %}{% endif %} +{% endfor %} + +{% macro cat_list(group) -%} +
    + {% for cat in group.categories %} +
  • {{ cat.name }}
  • + {% endfor %} +
+{%- endmacro %} + +
+ + + + +
+ + + + + +
+ + +
+{% endblock %} diff --git a/sites/craigslist/templates/listing_detail.html b/sites/craigslist/templates/listing_detail.html new file mode 100644 index 0000000..a3f3b2f --- /dev/null +++ b/sites/craigslist/templates/listing_detail.html @@ -0,0 +1,110 @@ +{% extends "base.html" %} +{% block title %}{{ listing.title }} - craigslist{% endblock %} +{% block body_class %}detail-page{% endblock %} +{% block content %} +
+ + +
+
+
+
+ {% if listing.display_price %}{{ listing.display_price }}{% endif %} +

{{ listing.title }}

+ ({{ listing.neighborhood }}) +
+
+ posted {{ listing.posted_at.strftime("%Y-%m-%d %H:%M") }} + updated {{ listing.updated_at.strftime("%Y-%m-%d") }} + post id: {{ listing.id }} +
+
+
+ reply + {% if current_user.is_authenticated %} +
+ + +
+ {% endif %} +
+
+
+ +
+ {% set photos = listing_images(listing) %} +
+ {% if photos %} +
+ {% for photo in photos %} + + {% endfor %} + {% if photos|length > 1 %} + + + {% endif %} +
+ {% if photos|length > 1 %} +
+ {% for photo in photos %} + + {% endfor %} +
+ {% endif %} + {% else %} +
no image
+ {% endif %} +
+ +
+ +
+

posting body

+

{{ listing.description }}

+ {% if listing.compensation %}

compensation: {{ listing.compensation }}

{% endif %} +

do NOT contact this poster with unsolicited services or offers.

+
+
+ +
+

more from {{ listing.category.name }}

+
    + {% for item in nearby %} +
  • {{ item.title }} {{ item.display_price }} {{ item.neighborhood }}
  • + {% endfor %} +
+
+
+{% endblock %} diff --git a/sites/craigslist/templates/login.html b/sites/craigslist/templates/login.html new file mode 100644 index 0000000..609627f --- /dev/null +++ b/sites/craigslist/templates/login.html @@ -0,0 +1,16 @@ +{% extends "base.html" %} +{% block title %}login - craigslist{% endblock %} +{% block body_class %}login-page{% endblock %} +{% block content %} +
+

log in

+
+ + + + + +
+

create an account

+
+{% endblock %} diff --git a/sites/craigslist/templates/messages.html b/sites/craigslist/templates/messages.html new file mode 100644 index 0000000..21a8c9a --- /dev/null +++ b/sites/craigslist/templates/messages.html @@ -0,0 +1,30 @@ +{% extends "base.html" %} +{% block title %}messages - craigslist{% endblock %} +{% block body_class %}account-page messages-page{% endblock %} +{% block content %} +
+ +

messages

+ {% if rows %} +
    + {% for m in rows %} +
  • +
    + {{ m.listing.title }} + {{ m.direction }} {{ m.created_at.strftime("%Y-%m-%d %H:%M") }} +
    +

    {{ m.body }}

    + {{ m.sender_name }} <{{ m.sender_email }}> +
  • + {% endfor %} +
+ {% else %} +

no messages

+ {% endif %} +
+{% endblock %} diff --git a/sites/craigslist/templates/post.html b/sites/craigslist/templates/post.html new file mode 100644 index 0000000..bed9b91 --- /dev/null +++ b/sites/craigslist/templates/post.html @@ -0,0 +1,77 @@ +{% extends "base.html" %} +{% block title %}create posting - craigslist{% endblock %} +{% block body_class %}post-page account-page{% endblock %} +{% block content %} +
+

< account

+

create a posting

+
+ + + + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ + + + + + + + +
+
+{% endblock %} diff --git a/sites/craigslist/templates/register.html b/sites/craigslist/templates/register.html new file mode 100644 index 0000000..217218a --- /dev/null +++ b/sites/craigslist/templates/register.html @@ -0,0 +1,27 @@ +{% extends "base.html" %} +{% block title %}register - craigslist{% endblock %} +{% block body_class %}login-page register-page{% endblock %} +{% block content %} +
+

create account

+
+ + + + + + + + + + + +
+
+{% endblock %} diff --git a/sites/craigslist/templates/reply.html b/sites/craigslist/templates/reply.html new file mode 100644 index 0000000..e95d529 --- /dev/null +++ b/sites/craigslist/templates/reply.html @@ -0,0 +1,26 @@ +{% extends "base.html" %} +{% block title %}reply - {{ listing.title }}{% endblock %} +{% block body_class %}reply-page{% endblock %} +{% block content %} +
+

< back to posting

+

reply to: {{ listing.title }}

+
+ {{ listing.neighborhood }} + {% if listing.display_price %}{{ listing.display_price }}{% endif %} + {{ listing.category.name }} +
+
+ craigslist mail relay keeps your email address private unless you choose to include it in your message. +
+
+ + + + + + + +
+
+{% endblock %} diff --git a/sites/craigslist/templates/saved.html b/sites/craigslist/templates/saved.html new file mode 100644 index 0000000..7113eb8 --- /dev/null +++ b/sites/craigslist/templates/saved.html @@ -0,0 +1,34 @@ +{% extends "base.html" %} +{% block title %}saved listings - craigslist{% endblock %} +{% block body_class %}account-page saved-page{% endblock %} +{% block content %} +
+ +

saved listings

+ {% if rows %} + + {% else %} +

nothing saved yet

+ {% endif %} +
+{% endblock %} diff --git a/sites/craigslist/templates/search.html b/sites/craigslist/templates/search.html new file mode 100644 index 0000000..5be36c0 --- /dev/null +++ b/sites/craigslist/templates/search.html @@ -0,0 +1,336 @@ +{% extends "base.html" %} +{% block title %}{% if category %}{{ category.name }}{% else %}search{% endif %} - craigslist{% endblock %} +{% block body_class %}search-page{% endblock %} +{% block content %} +{% set requested_view = request.args.get('view', 'gallery') %} +{% set active_view = 'thumb' if requested_view == 'grid' else requested_view %} +{% if active_view not in ['list', 'thumb', 'gallery', 'map'] %}{% set active_view = 'gallery' %}{% endif %} +{% set is_housing = category and category.group_slug == 'housing' %} +{% set is_for_sale = category and category.group_slug == 'for_sale' %} +{% set is_bikes = category and category.slug == 'bikes' %} +{% set purveyor_label = 'dealer' if is_for_sale else 'broker' %} +{% if is_housing %} + {% set quick_filters = ['price', 'beds', 'baths', 'type', 'furnished'] %} +{% elif is_bikes %} + {% set quick_filters = ['price', 'sold by', 'type', 'electric assist', 'condition'] %} +{% elif is_for_sale %} + {% set quick_filters = ['price', 'sold by', 'type', 'condition'] %} +{% else %} + {% set quick_filters = ['price', 'type', 'posted today'] %} +{% endif %} +{% macro view_url(view_mode) -%} + {% if category -%} + {{ url_for('category_search', category_slug=category.slug, q=request.args.get('q', ''), area=request.args.get('area', ''), min_price=request.args.get('min_price', ''), max_price=request.args.get('max_price', ''), sort=request.args.get('sort', 'relevance'), has_image=request.args.get('has_image', ''), view=view_mode) }} + {%- else -%} + {{ url_for('search', q=request.args.get('q', ''), area=request.args.get('area', ''), min_price=request.args.get('min_price', ''), max_price=request.args.get('max_price', ''), sort=request.args.get('sort', 'relevance'), has_image=request.args.get('has_image', ''), view=view_mode) }} + {%- endif %} +{%- endmacro %} +
+
+ +
+ + + + + + + + +
+
+ post + {% if current_user.is_authenticated %} + + {% else %} + + {% endif %} +
+
+ +
+ + + {% if listings %}1 - {{ listings|length }} of {{ listings|length }}{% else %}0 results{% endif %} +
+ +
+
+ + + +
+
+ + + + +
+
+ + + + + + +
+
+ {% for label in quick_filters %} + + {% endfor %} +
+
+ +
+ + +
+ {% if active_view == 'map' %} +
+
+ bay + SF + oakland + san jose + + + {% for listing in listings[:30] %} + {% set point = listing_map_point(listing) %} + + {{ loop.index }} + + {% endfor %} +
+
    + {% for listing in listings[:10] %} +
  1. {{ listing.title }}{{ listing.neighborhood }}
  2. + {% endfor %} +
+
+ {% endif %} + + {% if listings %} + {% if active_view == 'list' %} +
+ {% for listing in listings %} + {% set photos = listing_images(listing) %} +
+
+
+ + +
+ {{ listing.title }} + + + {{ listing.neighborhood }} + + {{ listing.age_label }} + {% if listing.price is not none %}{{ listing.display_price }}{% endif %} + {% if photos %}pic{% endif %} + + +
+ + +
+
+
+ {% endfor %} +
+ {% elif active_view == 'thumb' %} +
+ {% for listing in listings %} + {% set photos = listing_images(listing) %} +
+
+ + {% if photos %} + + {% else %} + no image + {% endif %} + +
+
{{ listing.neighborhood }}
+
+
+ + +
+ {{ listing.title }} +
+
+ {{ listing.age_label }} + {% if listing.price is not none %}{{ listing.display_price }}{% endif %} +
+ + +
+
+
+
+
+ {% endfor %} +
+ {% else %} + + {% endif %} + {% else %} +
no results matched this search
+ {% endif %} +
+
+
+{% endblock %} diff --git a/websyn_start.sh b/websyn_start.sh index 72defad..56a2e90 100644 --- a/websyn_start.sh +++ b/websyn_start.sh @@ -1,12 +1,13 @@ #!/bin/bash -# WebSyn startup: launch all 12 mirror sites, then exec the original CMD. +# WebSyn startup: launch all mirror sites, then exec the original CMD. # This preserves the base image's browser env server (port 8100) as PID 1. set -e SITES=(allrecipes amazon apple arxiv bbc_news booking github google_flights google_map google_search huggingface wolfram_alpha - cambridge_dictionary coursera espn) + cambridge_dictionary coursera espn craigslist) BASE_PORT=40000 +SITE_COUNT=${#SITES[@]} PID_DIR=/tmp/websyn_pids mkdir -p "$PID_DIR" rm -f "$PID_DIR"/*.pid @@ -17,7 +18,7 @@ for d in "${SITES[@]}"; do cp -a "/opt/WebSyn/$d/instance_seed" "/opt/WebSyn/$d/instance" done -echo "[WebSyn] Starting 15 sites on ports ${BASE_PORT}-$((BASE_PORT + 14))..." +echo "[WebSyn] Starting ${SITE_COUNT} sites on ports ${BASE_PORT}-$((BASE_PORT + SITE_COUNT - 1))..." for i in "${!SITES[@]}"; do site="${SITES[$i]}" port=$((BASE_PORT + i)) @@ -51,8 +52,8 @@ except Exception: exit(1) ready=$((ready + 1)) fi done - echo " [${elapsed}/${max_wait}s] ${ready}/15 sites ready" - if [ $ready -eq 15 ]; then + echo " [${elapsed}/${max_wait}s] ${ready}/${SITE_COUNT} sites ready" + if [ $ready -eq $SITE_COUNT ]; then break fi done @@ -78,6 +79,6 @@ done echo "[WebSyn] Starting control server on :8101 (PID 1)..." # Control server becomes PID 1 — receives SIGTERM on `docker stop`, -# keeps the container alive as long as it's running. The 15 site +# keeps the container alive as long as it's running. The site # subprocesses are managed via /tmp/websyn_pids/.pid. exec python3 /opt/control_server.py --port 8101