From 9f3a7995a1a9777fbe982cf346e3813f2e387a8d Mon Sep 17 00:00:00 2001
From: Edward Betts <edward@4angle.com>
Date: Thu, 21 Sep 2023 04:59:17 +0100
Subject: [PATCH] Improvements

---
 confarchive/model.py             |  69 +++++++++++-
 main.py                          | 100 ++++++++++++++++--
 templates/conference.html        | 141 +++++++++++++++++++++----
 templates/event.html             |   4 +-
 templates/index.html             |  24 ++++-
 templates/merge_people.html      |   5 +-
 templates/person.html            | 176 ++++++++++++++++++++-----------
 templates/search_everything.html |  97 +++++++++++++++++
 templates/top_speakers.html      |  26 +++--
 9 files changed, 532 insertions(+), 110 deletions(-)
 create mode 100644 templates/search_everything.html

diff --git a/confarchive/model.py b/confarchive/model.py
index b05531c..2795491 100644
--- a/confarchive/model.py
+++ b/confarchive/model.py
@@ -9,7 +9,7 @@ from sqlalchemy.ext.declarative import declarative_base
 from sqlalchemy.ext.orderinglist import ordering_list
 from sqlalchemy.orm import relationship
 from sqlalchemy.schema import Column, ForeignKey
-from sqlalchemy.types import Date, DateTime, Integer, String
+from sqlalchemy.types import Boolean, Date, DateTime, Integer, String
 
 from .database import session
 
@@ -25,6 +25,18 @@ class TimeStampedModel(Base):
     modified = Column(DateTime, default=func.now(), onupdate=func.now())
 
 
+class Series(TimeStampedModel):
+    """Conference series."""
+
+    __tablename__ = "series"
+    id = Column(Integer, primary_key=True)
+    name = Column(String, nullable=False)
+    slug = Column(String, unique=True)
+    wikidata_qid = Column(String, unique=True)
+
+    conferences = relationship("Conference", back_populates="series")
+
+
 class Conference(TimeStampedModel):
     """Conference."""
 
@@ -42,6 +54,9 @@ class Conference(TimeStampedModel):
     schedule_xml_url = Column(String)
     short_name = Column(String, unique=True)
     venue_id = Column(Integer, ForeignKey("venue.id"))
+    online = Column(Boolean)
+    wikidata_qid = Column(String, unique=True)
+    series_id = Column(Integer, ForeignKey("series.id"))
 
     people_detail = relationship(
         "ConferencePerson", lazy="dynamic", back_populates="conference"
@@ -56,6 +71,7 @@ class Conference(TimeStampedModel):
     )
 
     venue = relationship("Venue", back_populates="conferences")
+    series = relationship("Series", back_populates="conferences")
 
 
 class City(TimeStampedModel):
@@ -95,6 +111,17 @@ class Country(TimeStampedModel):
 
     cities = relationship("City", back_populates="country")
 
+    @property
+    def flag(self) -> str:
+        a = ord("A")
+        flag_a = 0x1F1E6
+        char1, char2 = (
+            flag_a + ord(self.alpha2[0]) - a,
+            flag_a + ord(self.alpha2[1]) - a,
+        )
+
+        return chr(char1) + chr(char2)
+
 
 class ConferencePerson(Base):
     __tablename__ = "conference_person"
@@ -108,6 +135,16 @@ class ConferencePerson(Base):
     person = relationship("Person", back_populates="conferences_association")
     conference = relationship("Conference", back_populates="people_detail")
 
+    @property
+    def events(self):
+        return (
+            Event.query.join(EventPerson)
+            .filter(
+                Event.conference == self.conference, EventPerson.person == self.person
+            )
+            .order_by(Event.event_date.desc())
+        )
+
 
 class Event(TimeStampedModel):
     """Event."""
@@ -140,7 +177,7 @@ class Event(TimeStampedModel):
     people = association_proxy(
         "people_detail",
         "person",
-        creator=lambda i: EventPerson(person=i[0], named_as=i[1]),
+        creator=lambda i: EventPerson(person=i),
     )
 
 
@@ -162,7 +199,23 @@ class Person(TimeStampedModel):
     events = association_proxy("events_association", "event")
 
     conferences_association = relationship("ConferencePerson", back_populates="person")
-    conferences = association_proxy("conference_association", "conference")
+    conferences = association_proxy("conferences_association", "conference")
+
+    @property
+    def conference_count(self):
+        return ConferencePerson.query.filter_by(person_id=self.id).count()
+
+    @property
+    def event_count(self):
+        return EventPerson.query.filter_by(person_id=self.id).count()
+
+    def active_years(self):
+        q = (
+            session.query(func.min(Event.event_date), func.max(Event.event_date))
+            .join(EventPerson)
+            .filter_by(person_id=self.id)
+        )
+        return q.one()
 
     # photos = relationship("PersonPhoto", back_populates="person")
 
@@ -176,6 +229,16 @@ class Person(TimeStampedModel):
 
         return q
 
+    def conference_by_time(self):
+        q = (
+            session.query(ConferencePerson)
+            .join(Conference)
+            .filter(ConferencePerson.person == self)
+            .order_by(Conference.start.desc())
+        )
+
+        return q
+
 
 # class PersonPhoto(TimeStampedModel):
 #     """Person photo."""
diff --git a/main.py b/main.py
index 27ce2f3..7037f7c 100755
--- a/main.py
+++ b/main.py
@@ -10,7 +10,7 @@ from typing import cast
 import flask
 import requests
 import sqlalchemy
-from sqlalchemy import func, update
+from sqlalchemy import func, or_, update
 from werkzeug.wrappers import Response
 
 from confarchive import database, model
@@ -50,7 +50,7 @@ def wikidata_search(q: str) -> list[dict[str, typing.Any]]:
         data = r.json()
         time.sleep(1)
 
-    return cast(dict[str, typing.Any], data["query"]["search"])
+    return cast(list[dict[str, typing.Any]], data["query"]["search"])
 
 
 def wikidata_get_item(qid: str) -> typing.Any:
@@ -58,6 +58,7 @@ def wikidata_get_item(qid: str) -> typing.Any:
     if os.path.exists(cache_filename):
         item = json.load(open(cache_filename))
     else:
+        print(qid)
         params: dict[str, str | int] = {
             "action": "wbgetentities",
             "ids": qid,
@@ -78,7 +79,7 @@ def top_speakers() -> sqlalchemy.orm.query.Query:
         .join(model.ConferencePerson)
         .group_by(model.Person)
         .order_by(func.count().desc())
-        .having(func.count() > 5)
+        .having(func.count() > 3)
     )
     return q
 
@@ -118,7 +119,9 @@ def person(person_id: int) -> str | Response:
             flask.url_for(flask.request.endpoint, person_id=person_id)
         )
 
-    return flask.render_template("person.html", item=item, Event=model.Event)
+    return flask.render_template(
+        "person.html", item=item, Event=model.Event, plural=plural
+    )
 
 
 @app.route("/event/<int:event_id>")
@@ -132,7 +135,9 @@ def conference_page(short_name: str) -> str:
     item = model.Conference.query.filter_by(short_name=short_name).one_or_none()
     if item is None:
         flask.abort(404)
-    return flask.render_template("conference.html", item=item)
+    return flask.render_template(
+        "conference.html", item=item, person_image_filename=person_image_filename
+    )
 
 
 @app.route("/people")
@@ -157,9 +162,16 @@ def merge() -> str | Response:
         merge_to_id = min(item_ids)
         other_ids = [i for i in item_ids if i != merge_to_id]
 
+        name_from_person_id = flask.request.form["name"]
+
         print(other_ids, "->", merge_to_id)
 
         with database.session.begin():
+            if merge_to_id != name_from_person_id:
+                merge_to = model.Person.query.get(merge_to_id)
+                name_from_person = model.Person.query.get(name_from_person_id)
+                merge_to.name = name_from_person.name
+
             print("update ConferencePerson")
             database.session.execute(
                 update(model.ConferencePerson)
@@ -224,15 +236,37 @@ def index() -> str:
         "conference": model.Conference.query.count(),
         "event": model.Event.query.count(),
         "person": model.Person.query.count(),
+        "country": model.Country.query.count(),
+        "venue": model.Venue.query.count(),
     }
 
     return flask.render_template("index.html", items=q, count=count)
 
 
+def plural(num: int, label: str) -> str:
+    return f'{num:,d} {label}{"s" if num != 1 else ""}'
+
+
+def speaker_counts():
+    sql = """
+select num, count(*)
+from (select person_id, count(*) as num from conference_person group by person_id) a
+group by num
+order by num
+"""
+
+    return database.session.execute(sql)
+
+
 @app.route("/speakers")
 def top_speakers_page() -> str:
     """Top speakers page."""
-    return flask.render_template("top_speakers.html", top_speakers=top_speakers())
+    return flask.render_template(
+        "top_speakers.html",
+        top_speakers=top_speakers(),
+        speaker_counts=speaker_counts(),
+        plural=plural,
+    )
 
 
 @app.route("/country")
@@ -264,11 +298,11 @@ def link_to_wikidata() -> str:
     for person, num in top_speakers2():
         if person.wikidata_qid:
             continue
-        search_hits = wikidata_search(f'"{person.name}"')
+        search_hits = wikidata_search(person.name)
         if not search_hits:
             continue
 
-        if len(search_hits) > 10:
+        if len(search_hits) > 14:
             continue
 
         hits = []
@@ -299,5 +333,55 @@ def link_to_wikidata() -> str:
     return flask.render_template("wikidata.html", items=items)
 
 
+@app.route("/search")
+def search_everything() -> str:
+    search_for = flask.request.args["q"]
+    if not search_for:
+        return flask.render_template("search_everything.html")
+
+    search_for = search_for.strip()
+    like = f"%{search_for}%"
+
+    people = model.Person.query.filter(model.Person.name.ilike(like)).order_by(
+        model.Person.name
+    )
+
+    events = model.Event.query.filter(
+        or_(model.Event.abstract.ilike(like), model.Event.description.ilike(like))
+    ).order_by(model.Event.event_date)
+
+    return flask.render_template(
+        "search_everything.html", people=people, events=events, search_for=search_for
+    )
+
+
+@app.route("/person/<int:person_id>/delete", methods=["POST"])
+def delete_person(person_id: int) -> str | Response:
+    item = model.Person.query.get(person_id)
+
+    for cp in item.conferences_association:
+        database.session.delete(cp)
+
+    for ep in item.events_association:
+        database.session.delete(ep)
+    database.session.delete(item)
+
+    database.session.commit()
+
+    return flask.redirect(flask.url_for("index"))
+
+
+def person_image_filename(person_id):
+    person = model.Person.query.get(person_id)
+    return os.path.join("wikidata_photo", "thumb", person.wikidata_photo[0])
+    for filename in person.wikidata_photo:
+        face_crop = "face_1_" + filename
+        full = os.path.join("static", "wikidata_photo", "face_cropped", face_crop)
+        if os.path.exists(full):
+            return os.path.join("wikidata_photo", "face_cropped", face_crop)
+
+    return os.path.join("wikidata_photo", "thumb", person.wikidata_photo[0])
+
+
 if __name__ == "__main__":
     app.run(host="0.0.0.0", port=5002)
diff --git a/templates/conference.html b/templates/conference.html
index 8b355fc..fd5064c 100644
--- a/templates/conference.html
+++ b/templates/conference.html
@@ -1,5 +1,28 @@
 {% extends "base.html" %}
 
+{% block style %}
+<style>
+.image-container {
+   width: 200px; /* Adjust this to your desired square size */
+   height: 240px; /* Same as width for a square */
+   display: inline-flex; /* Use inline-flex to display containers horizontally */
+   margin-right: 10px; /* Add some spacing between images (adjust as needed) */
+   justify-content: center; /* Horizontally center the content */
+   align-items: center; /* Vertically center the content */
+   overflow: hidden; /* Hide overflowing image parts */
+}
+
+.image-container img {
+    max-width: 100%;
+    max-height: 100%;
+    object-fit: cover; /* Crop and scale the image to fit the container */
+     object-position: center; /* Center the cropping horizontally */
+}
+</style>
+{% endblock %}
+
+{% set show_images = True %}
+
 {% block title %}{{ item.title }}{% endblock %}
 
   {% block content %}
@@ -8,36 +31,84 @@
   <h1>{{ item.title }}</h1>
   <p><a href="{{ url_for("index") }}">home</a></p>
 
-  <ul>
-    <li>start: {{ item.start }}</li>
-    <li>end: {{ item.end }}</li>
+  <div>
+    <div>series: {{ item.series.name }}
+      {% if item.series.wikidata_qid %}
+      <a href="https://www.wikidata.org/wiki/{{ item.series.wikidata_qid }}">Wikidata</a>
+      {% endif %}
+    </div>
+    <div>start: {{ item.start }}</div>
+    <div>end: {{ item.end }}</div>
     {% if days %}
-      <li>days: {{ item.days }}</li>
+      <div>days: {{ item.days }}</div>
     {% endif %}
-    <li>short name: {{ item.short_name }}</li>
-    <li>country: {{ item.country or "n/a" }}</li>
-  </ul>
+    {# <div>short name: {{ item.short_name }}</div> #}
+    {% if item.venue %}
+    {% set country = item.venue.city.country %}
+    <div>
+      venue: {{ item.venue.name }}
+      {% if item.venue.wikidata_qid %}
+      <a href="https://www.wikidata.org/wiki/{{ item.venue.wikidata_qid }}">Wikidata</a>
+      {% endif %}
+    </div>
+    <div>
+      city: {{ item.venue.city.name }}
+      {% if item.venue.city.wikidata_qid %}
+      <a href="https://www.wikidata.org/wiki/{{ item.venue.city.wikidata_qid }}">Wikidata</a>
+      {% endif %}
+    </div>
+    <div>country: {{ country.name }} {{ country.flag }}</div>
+    {% endif %}
+    {% if item.wikidata_qid %}
+    <div>wikidata: <a href="https://www.wikidata.org/wiki/{{ item.wikidata_qid }}">{{ item.wikidata_qid }}</a></div>
+    {% endif %}
+  </div>
+
+  {% if show_images %}
+  <div>
+  {% for person in item.people %}
+  {% if person.wikidata_photo %}
+  <span class="image-container">
+     <a href="{{ url_for("person", person_id=person.id) }}">
+      <img src="{{ url_for("static", filename=person_image_filename(person.id)) }}" alt="{{ person.name}}" title="{{ person.name}}">
+    </a>
+  </span>
+  {% endif %}
+  {% endfor %}
+  </div>
+  {% endif %}
 
   <h3>Talks</h3>
 
   <p>{{ item.events.count() }} talks</p>
   {%  for event in item.events %}
 
-  <div class="card my-2">
-  <div class="card-body">
-    <h5 class="card-title">
-      <a href="{{ url_for("event_page", event_id=event.id) }}">{{ event.title }}</a>
-    </h5>
-    <h6 class="card-subtitle mb-2 text-body-secondary">
+  <div>
+    <div>
+      <p>
+        🎤
+        <a href="{{ url_for("event_page", event_id=event.id) }}">{{ event.title }}</a><br>
+
+      Speakers:
+      {% for p in event.people %}
+        👤
+        <a href="{{ url_for("person", person_id=p.id) }}">{{ p.name }}</a>
+      {% endfor %}<br>
+
       {% if event.event_date %}
-        {{ event.event_date.strftime("%d %b %Y at %H:%M") }}
+        📅 {{ event.event_date.strftime("%a, %d %b %Y at %H:%M") }}
       {% else %}
         event date missing
       {% endif %}
-    </h6>
-    <p class="card-text">
+
+      <a class="event-detail-toggle" href="#">show details</a><br>
+
+
+
+    </p>
+    <div class="event-detail" id="event_{{event.id }}" style="display:none">
       {% if event.url %}
-      <a href="{{ event.url }}">talk on conference website</a>
+      <p><a href="{{ event.url }}">talk on conference website</a></p>
       {% endif %}
 
     {% if event.abstract %}
@@ -60,12 +131,7 @@
     </p>
     {% endif %}
 
-    <p class="card-text">
-      Speakers:
-      {% for p in event.people %}
-        <a href="{{ url_for("person", person_id=p.id) }}">{{ p.name }}</a>
-      {% endfor %}
-    </p>
+    </div>
   </div>
 </div>
 
@@ -74,3 +140,32 @@
   </div>
   </div>
 {% endblock %}
+
+{% block script %}
+<script>
+  // Get all elements with the class "event-detail-toggle"
+  var toggleLinks = document.querySelectorAll(".event-detail-toggle");
+
+  // Loop through each toggle link and attach a click event handler
+  toggleLinks.forEach(function(link) {
+    link.addEventListener("click", function(e) {
+      e.preventDefault(); // Prevent the default link behavior
+
+      // Find the parent div of the clicked link
+      var parentDiv = this.closest("div");
+
+      // Find the element with class "event-detail" inside the parent div
+      var detailElement = parentDiv.querySelector(".event-detail");
+
+      // Toggle the display of the detail element
+      if (detailElement.style.display === "none" || detailElement.style.display === "") {
+        detailElement.style.display = "block";
+        this.textContent = "hide detail"; // Change the link text
+      } else {
+        detailElement.style.display = "none";
+        this.textContent = "show detail"; // Change the link text
+      }
+    });
+  });
+</script>
+{% endblock %}
diff --git a/templates/event.html b/templates/event.html
index 1aa828e..7c3cbda 100644
--- a/templates/event.html
+++ b/templates/event.html
@@ -33,13 +33,15 @@
     {% endif %}
 
 
+    {% if item.description %}
     <p class="card-text">
-      {% if "<" in item.description %}
+      {% if  "<" in item.description %}
       {{ item.description | safe }}
       {% else %}
       {{ item.description }}
       {% endif %}
     </p>
+    {% endif %}
     <p class="card-text">
       Speakers:
       {% for p in item.people %}
diff --git a/templates/index.html b/templates/index.html
index d0f2051..8e063c1 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -15,27 +15,49 @@
       <button type="submit" class="btn btn-primary">Search</button>
     </form>
 
-
     <div style="margin-bottom:1rem">
+      👥
       {{ "{:,d}".format(count.conference) }} conferences<br/>
+      🌍
+      {{ "{:,d}".format(count.country) }} countries<br/>
+      📍
+      {{ "{:,d}".format(count.venue) }} venues<br/>
+      🎤
       {{ "{:,d}".format(count.event) }} talks -
       <a href="{{ url_for("events_page") }}">most common titles</a><br/>
+      👤
       {{ "{:,d}".format(count.person) }} speakers
       <a href="{{ url_for("top_speakers_page") }}">top speakers</a><br/>
     </div>
 
+    <h2>Conferences</h2>
+
     {% for item in items %}
+    {% if loop.first or item.start.year != loop.previtem.start.year %}
+    <h3>{{ item.start.year }}</h3>
+    {% endif %}
+
     <div style="margin-bottom:1.5rem">
+      👥
       <a href="{{ url_for("conference_page", short_name=item.short_name) }}">{{ item.title }}</a>
+      📅
       {{ item.start.strftime("%d %b %Y") }}
       <br/>
       {% if item.venue %}
+        📍
         {{ item.venue.name }}
         &ndash;
         {{ item.venue.city.name }},
         {{ item.venue.city.country.name }}
+        {{ item.venue.city.country.flag }}
       <br/>
       {% endif %}
+      {#
+      {% if item.series %}
+      📃 Series: {{ item.series.name }}
+      <br/>
+      {% endif %}
+      #}
 
       {{ (item.end - item.start).days + 1 }} days,
       {{ item.events.count() }} talks,
diff --git a/templates/merge_people.html b/templates/merge_people.html
index 54e2685..2f04c89 100644
--- a/templates/merge_people.html
+++ b/templates/merge_people.html
@@ -25,7 +25,6 @@
     <div class="form-check">
       <input class="form-check-input" type="checkbox" name="person_id" value="{{ item.id }}" id="person{{ item.id }}">
       <label class="form-check-label" for="person{{ item.id }}">
-        {{ item.id }}
         <a href="{{ url_for("person", person_id=item.id) }}">{{ item.name }}</a>
 
         {% if item.wikidata_qid %}
@@ -33,6 +32,10 @@
         <a href="https://www.wikidata.org/wiki/{{ item.wikidata_qid }}">{{ item.wikidata_qid }} on Wikidata</a>
         {% endif %}
       </label>
+      <input class="form-check-input" type="radio" name="name" value="{{ item.id }}" id="name{{ item.id }}">
+      <label class="form-check-label" for="name{{ item.id }}">use this name</label><br>
+
+      {% for conf in item.conferences %} 👥{{ conf.title }}{% endfor %}
     </div>
 
     {% endfor %}
diff --git a/templates/person.html b/templates/person.html
index 50c6452..b23897e 100644
--- a/templates/person.html
+++ b/templates/person.html
@@ -6,25 +6,24 @@
   <div class="container">
     <div class="row">
   <h1>{{ item.name }}</h1>
-  <p><a href="{{ url_for("index") }}">home</a></p>
+
+  <p>
+  👥 {{ plural(item.conference_count,  "conference") }}<br/>
+  🎤 {{ plural(item.event_count, "talk") }}<br/>
+  {% set start, end = item.active_years() %}
+  📅 Years active: {{ start.year }} to {{end.year }}
+  {% if item.wikidata_qid %}
+  <br/>
+  📊 Wikidata: <a href="https://www.wikidata.org/wiki/{{ item.wikidata_qid }}">{{ item.wikidata_qid }}</a>
+  {% endif %}
+  </p>
 
 
-  <h3>Conferences</h3>
-  {% for apperance in item.conferences_association %}
-    {% set conf = apperance.conference %}
+  {% if item.wikidata_photo %}
+    <img src="{{ url_for("static", filename="wikidata_photo/thumb/" + item.wikidata_photo.0) }}">
+  {% endif %}
 
-    <div class="card my-2">
-    <div class="card-body">
-      <h5 class="card-title">{{ conf.id }}: {{ conf.title }}</h5>
-      <p class="card-text">
-        {% if apperance.bio %}{{ apperance.bio | safe }}{% else %}No speaker biography.{% endif %}
-      </p>
-    </div>
-    </div>
-  {% endfor %}
-
-
-  {% set search_for = '"' + item.name + '" ' + " haswbstatement:P31=Q5" %}
+  {% set search_for = item.name + ' ' + " haswbstatement:P31=Q5" %}
   <p><a href="https://www.wikidata.org/w/index.php?search={{ search_for | urlencode }}&title=Special%3ASearch&ns0=1&ns120=1">Search for {{ item.name }} on Wikidata</a></p>
 
   <form method="POST">
@@ -39,60 +38,111 @@
     <button type="submit" class="btn btn-primary">Submit</button>
   </form>
 
-  <h3>Talks</h3>
-  <p>Has {{ item.events_association.count() }} events</p>
-  {%  for event in item.events_by_time() %}
-  <div class="card my-2">
-  <div class="card-body">
-    <h5 class="card-title">
-      <a href="{{ url_for("event_page", event_id=event.id) }}">{{ event.title }}</a>
-    </h5>
-    <h6 class="card-subtitle mb-2 text-body-secondary">
-      {{ event.conference.title }}
-      &mdash;
-      {% if event.event_date %}
-        {{ event.event_date.strftime("%d %b %Y") }}
-      {% else %}
-        event date missing
-      {% endif %}
+  <form method="POST" action="{{ url_for("delete_person", person_id=item.id) }}">
+    <button type="submit" class="btn btn-primary">delete</button>
+  </form>
 
-    </h6>
-    <p class="card-text">
-      {% if event.url %}
-      <a href="{{ event.url }}">{{ event.title }} on conference website</a>
-      {% endif %}
 
-    {% if event.abstract %}
-    <p class="card-text">
-      {% if "<" in event.abstract %}
-      {{ event.abstract | safe }}
-      {% else %}
-      {{ event.abstract }}
-      {% endif %}
-    </p>
-    {% endif %}
 
-    {% if event.description %}
-    <p class="card-text">
-      {% if "<" in event.description %}
-      {{ event.description | safe }}
-      {% else %}
-      {{ event.description }}
-      {% endif %}
-    </p>
-    {% endif %}
-    <p class="card-text">
-      {% for p in event.people %}
-        {% if p.id != item.id %}
-        <a href="{{ url_for(request.endpoint, person_id=p.id) }}">{{ p.name }}</a>
+  {% for apperance in item.conference_by_time() %}
+    {% set conf = apperance.conference %}
+
+    <div>
+    <h3>👥 {{ conf.title }}
+      <small>📅 {{ conf.start.strftime("%d %b %Y") }}</small>
+
+    </h3>
+    {% if apperance.bio %}<p>Biography: {{ apperance.bio | safe }}</p>{% endif %}
+    </div>
+
+    {%  for event in apperance.events %}
+    <div>
+      <h4>
+        🎤
+        <a href="{{ url_for("event_page", event_id=event.id) }}">{{ event.title }}</a>
+        <small>
+        {% if event.event_date %}
+          {{ event.event_date.strftime("%d %b %Y") }}
+        {% else %}
+          event date missing
         {% endif %}
-      {% endfor %}
-    </p>
-  </div>
-</div>
+
+        <a class="event-detail-toggle" href="#">show details</a>
+        </small>
+      </h4>
+      <div class="event-detail" id="event_{{event.id }}" style="display:none">
+
+      <p>
+        {% if event.url %}
+        <a href="{{ event.url }}">talk on conference website</a>
+        {% endif %}
+      <p>
+
+      {% if event.abstract %}
+      <div>
+        {% if "<" in event.abstract %}
+        {{ event.abstract | safe }}
+        {% else %}
+        {{ event.abstract }}
+        {% endif %}
+      </div>
+      {% endif %}
+
+      {% if event.description %}
+      <div>
+        {% if "<" in event.description %}
+        {{ event.description | safe }}
+        {% else %}
+        {{ event.description }}
+        {% endif %}
+      </div>
+      {% endif %}
+      <div>
+        {% for p in event.people %}
+          {% if p.id != item.id %}
+          <a href="{{ url_for(request.endpoint, person_id=p.id) }}">{{ p.name }}</a>
+          {% endif %}
+        {% endfor %}
+      </div>
+      </div>
+    </div>
+
+    {% endfor %}
+
+
 
   {% endfor %}
 
+
   </div>
   </div>
 {% endblock %}
+
+{% block script %}
+<script>
+  // Get all elements with the class "event-detail-toggle"
+  var toggleLinks = document.querySelectorAll(".event-detail-toggle");
+
+  // Loop through each toggle link and attach a click event handler
+  toggleLinks.forEach(function(link) {
+    link.addEventListener("click", function(e) {
+      e.preventDefault(); // Prevent the default link behavior
+
+      // Find the parent div of the clicked link
+      var parentDiv = this.closest("div");
+
+      // Find the element with class "event-detail" inside the parent div
+      var detailElement = parentDiv.querySelector(".event-detail");
+
+      // Toggle the display of the detail element
+      if (detailElement.style.display === "none" || detailElement.style.display === "") {
+        detailElement.style.display = "block";
+        this.textContent = "hide detail"; // Change the link text
+      } else {
+        detailElement.style.display = "none";
+        this.textContent = "show detail"; // Change the link text
+      }
+    });
+  });
+</script>
+{% endblock %}
diff --git a/templates/search_everything.html b/templates/search_everything.html
new file mode 100644
index 0000000..4ba27ba
--- /dev/null
+++ b/templates/search_everything.html
@@ -0,0 +1,97 @@
+{% extends "base.html" %}
+
+{% block title %}Conference archive{% endblock %}
+
+  {% block content %}
+  <div class="container">
+    <div class="row">
+    <h1>Conference archive</h1>
+    <p><a href="{{ url_for("index") }}">home</a></p>
+
+    <form action="{{ url_for("search_everything") }}">
+      <div class="mb-3">
+        <input type="text" class="form-control" name="q" id="q" value="{{ search_for }}">
+      </div>
+      <button type="submit" class="btn btn-primary">Search</button>
+    </form>
+
+    {% if search_for %}
+
+    <h3>Talks</h3>
+
+    <p>Found {{ events.count() }} events matching '{{ search_for }}'</p>
+
+    {% for event in events %}
+    <div>
+      <p>
+        🎤
+        <a href="{{ url_for("event_page", event_id=event.id) }}">{{ event.title }}</a><br>
+      Speakers:
+      {% for p in event.people %}
+        👤
+        <a href="{{ url_for("person", person_id=p.id) }}">{{ p.name }}</a>
+      {% endfor %}<br>
+
+      👥 <a href="{{ url_for("conference_page", short_name=event.conference.short_name) }}">{{ event.conference.title }}</a><br>
+
+      {% if event.event_date %}
+        📅 {{ event.event_date.strftime("%a, %d %b %Y at %H:%M") }}
+      {% else %}
+        event date missing
+      {% endif %}
+    </p>
+
+    {% if event.abstract %}
+    <p class="card-text">
+      {% if "<" in event.abstract %}
+      {{ event.abstract | safe }}
+      {% else %}
+      {% for line in event.abstract.splitlines() %}
+      {{ line }}<br>
+      {% endfor %}
+      {% endif %}
+    </p>
+    {% endif %}
+
+    {% if event.description and event.description != event.abstract %}
+    <p class="card-text">
+      {% if "<" in event.description %}
+      {{ event.description | safe }}
+      {% else %}
+      {% for line in event.description.splitlines() %}
+      {{ line }}<br>
+      {% endfor %}
+      {% endif %}
+    </p>
+    {% endif %}
+    </div>
+
+    {% endfor %}
+
+
+    <h3>People</h3>
+    <p>
+    Found {{ people.count() }} people matching '{{ search_for }}'
+
+    </p>
+
+    <ul>
+
+    {% for item in people %}
+    <li>
+      <a href="{{ url_for("person", person_id=item.id) }}">{{ item.name }}</a>
+      {% if item.wikidata_qid %}
+      &mdash;
+      <a href="https://www.wikidata.org/wiki/{{ item.wikidata_qid }}">{{ item.wikidata_qid }} on Wikidata</a>
+      {% endif %}
+    </li>
+    {% endfor %}
+    </ul>
+
+    {% endif %}
+
+  </div>
+  </div>
+{% endblock %}
+
+
diff --git a/templates/top_speakers.html b/templates/top_speakers.html
index 8b5a71b..8b7beb1 100644
--- a/templates/top_speakers.html
+++ b/templates/top_speakers.html
@@ -3,8 +3,6 @@
 {% block title %}Conference archive{% endblock %}
 
   {% block content %}
-  <div class="container">
-    <div class="row">
     <h1>Conference archive</h1>
 
     <form action="{{ url_for("search_people") }}">
@@ -15,23 +13,31 @@
       <button type="submit" class="btn btn-primary">Search</button>
     </form>
 
+    <h3>Speaker/conference frequency distribution</h3>
+
+    <p>Distribution of speakers by conference count.</p>
+    {% for conf_count, speaker_count in speaker_counts %}
+    <div>
+      {{ plural(conf_count, "conference") }}:
+      {{ plural(speaker_count, "speaker") }}
+    </div>
+    {% endfor %}
+
     <h3>Top speakers</h3>
     <ul>
 
     {% for person, count in top_speakers %}
-    <li>
+    <div>
+      👤
       <a href="{{ url_for("person", person_id=person.id) }}">{{ person.name }}</a>
-      ({{ count }})
+      ({{ count }} conferences, {{ person.event_count }} talks)
+      {% if person.wikidata_photo %}📷{% endif %}
       {% if person.wikidata_qid %}
-      &mdash;
-      <a href="https://www.wikidata.org/wiki/{{ person.wikidata_qid }}">{{ person.wikidata_qid }} on Wikidata</a>
+      <a href="https://www.wikidata.org/wiki/{{ person.wikidata_qid }}">Wikidata</a>
       {% endif %}
-    </li>
+    </div>
     {% endfor %}
-    </ul>
 
-  </div>
-  </div>
 {% endblock %}