Repository: pretalx/pretalx

Issue:
Search box for talks
### Problem and impact

In pentabarf, we had a searchbox in the navigation menu which would search over all presenters (name/biography) and talks which was reachable from anywhere in the organiser interface.

It would also already start doing autosuggest while typing. 
In pretalx, searching a talk will require much more clicks: Proposal > proposal > Search box > search

![image](https://github.com/pretalx/pretalx/assets/868434/4a79493c-2f59-488b-874a-0179fc95aefc)


### Proposed solution / feature

Proposal would be a similar quicksearch box in the interface. 

### Context or examples

see https://github.com/pretalx/pretalx/discussions/1637 for more information on our general setup.

Discussion:

Patch:
diff --git a/doc/changelog.rst b/doc/changelog.rst
index 1c7608bcc..9c3bae9f9 100644
--- a/doc/changelog.rst
+++ b/doc/changelog.rst
@@ -3,6 +3,7 @@
 Release Notes
 =============
 
+- :feature:`orga,1672` The organiser area now has a search box located in the side bar, opened after clicking the event name. It searches all events the current user has full access to (that is, not a limited reviewer account) for events, sessions and speakers.
 - :feature:`orga:review,1633` Resources uploaded or linked by speakers are now shown in the review view (unless anonymous reviews are used, as resources can currently not be anonymised).
 - :feature:`orga:review,1675` In order to make large events more manageable, the review dashboard is now by default paginated to 100 sessions per page. The page size can be adjusted up to showing all sessions as before (and the page size is remembered on a per-user basis).
 - :feature:`orga` The default page size for paginated pages has been increased to 50.
diff --git a/src/pretalx/orga/templates/orga/base.html b/src/pretalx/orga/templates/orga/base.html
index 646fcff6b..188c62f20 100644
--- a/src/pretalx/orga/templates/orga/base.html
+++ b/src/pretalx/orga/templates/orga/base.html
@@ -56,61 +56,27 @@
             <a class="navbar-brand d-none d-md-block" href="/orga/">
                 <img loading="lazy" src="{% static "common/img/icons/icon.svg" %}" alt="{% translate "The pretalx logo" %}"> pretalx
             </a>
-            <div class="navbar-collapse event-dropdown" id="navbartoggle">
+            <div class="navbar-collapse" id="navbartoggle">
                 <ul class="navbar-nav mr-auto flip">
-                    <li class="nav-item dropdown active" id="event-navigation">
-                        {% if request.orga_events|length > 1 %}
-                            <a class="nav-link dropdown-toggle" id="eventDropdown" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
-                                <i class="fa fa-calendar"></i>
-                                <div class="event-text">
-                                    {% if request.event %}
-                                        <span class="event-name">{{ request.event.name }}</span>
-                                        <span class="event-date">{{ request.event.get_date_range_display }}</span>
-                                    {% else %}
-                                        {% translate "Events" %}
-                                    {% endif %}
-                                </div>
-                            </a>
-                            <ul class="dropdown-menu event-dropdown" role="menu" data-event-typeahead aria-labelledby="eventDropdown" data-source="{% url "orga:event.typeahead" %}">
-                                <li class="query-holder">
-                                    <div class="form-box">
-                                        <input type="text" class="form-control" id="event-search"
-                                               placeholder="{% translate "Search for events" %}"
-                                               data-typeahead-query>
-                                    </div>
-                                </li>
-                            </ul>
-                        {% elif request.event %}
-                            <a id="eventDropdown" class="nav-link">
-                                <i class="fa fa-calendar"></i>
-                                <div class="event-text">
-                                    {% if request.event %}
-                                        <span class="event-name">{{ request.event.name }}</span>
-                                        <span class="event-date">{{ request.event.get_date_range_display }}</span>
-                                    {% else %}
-                                        {% translate "Events" %}
-                                    {% endif %}
-                                </div>
-                            </a>
-                        {% endif %}
-                    </li>
                     {% if request.event %}
                         <li class="nav-item">
                             {% if request.event.custom_domain and not request.event.is_public %}
                                 <form action="{{ request.event.urls.auth.full }}" method="post"
-                                      target="_blank" class="mobile-navbar-view-form visible-xs-block">
+                                      target="_blank" class="mobile-navbar-view-form visible-xs-block d-flex align-items-center">
                                     <input type="hidden" value="{{ new_session }}" name="session">
                                     <input type="hidden" value="{{ go_to_target }}" name="target">
                                     <button type="submit" class="btn btn-link navbar-toggle">
                                         <i class="fa fa-eye"></i>
+                                        {% translate "View event" %}
                                     </button>
                                 </form>
                             {% else %}
                                 <a
-                                    class="nav-link" target="_blank" rel="noopener"
+                                    class="nav-link d-flex align-items-center" target="_blank" rel="noopener"
                                     href="{% if go_to_target == 'schedule' %}{{ request.event.urls.schedule }}{% else %}{{ request.event.cfp.urls.public }}{% endif %}"
                                 >
-                                    <i class="fa fa-eye"></i>
+                                    <i class="fa fa-eye mr-2 mt-0"></i>
+                                    {% translate "View event" %}
                                 </a>
                             {% endif %}
                         </li>
@@ -166,6 +132,34 @@
         {% endif %}
         <div id="page-wrapper">
             <aside class="nav flex-column sidebar">
+                <a class="dropdown nav-link p-0" id="nav-search-wrapper">
+                    <div id="nav-search" class="dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+                        <span id="search-context-icon" class="fa-stack fa-lg">
+                            <i class="fa fa-circle fa-stack-2x"></i>
+                            <i class="fa fa-{% if request.event %}calendar{% elif request.organiser %}group{% else %}user{% endif %} fa-stack-1x fa-inverse"></i>
+                        </span>
+                        <div id="search-context-text">
+                            {% if request.event %}
+                                <span class="context-name">{{ request.event.name }}</span>
+                                <span class="context-meta">{{ request.event.get_date_range_display }}</span>
+                            {% elif request.organiser %}
+                                <span class="context-name">{{ request.organiser.name }}</span>
+                                <span class="context-meta">{% translate "Organiser account" %}</span>
+                            {% else %}
+                                <span class="context-name">{{ request.user.get_display_name }}</span>
+                            {% endif %}
+                        </div>
+                        <div id="nav-search-dropdown" class="dropdown-menu dropdown-menu-left" aria-labelledby="nav-search" data-source="{% url "orga:nav.typeahead" %}" data-event-typeahead{% if request.event %} data-organiser="{{ request.event.organiser.pk }}"{% elif request.organiser %} data-organiser="{{ request.organiser.pk }}"{% endif %}>
+                            <div class="query-holder">
+                                <div class="form-box">
+                                    <input type="text" class="form-control" placeholder="{% translate "Search" %}" data-typeahead-query>
+                                </div>
+                            </div>
+                            <ul id="search-results">
+                            </ul>
+                        </div>
+                    </div>
+                </a>
                 {% if request.event %}
                     {% has_perm 'orga.view_orga_area' request.user request.event as can_see_orga_area %}
                     {% has_perm 'orga.change_submissions' request.user request.event as can_see_orga_exclusive %}
diff --git a/src/pretalx/orga/urls.py b/src/pretalx/orga/urls.py
index 998374a54..e225a74f8 100644
--- a/src/pretalx/orga/urls.py
+++ b/src/pretalx/orga/urls.py
@@ -17,6 +17,7 @@
     schedule,
     speaker,
     submission,
+    typeahead,
 )
 
 app_name = "orga"
@@ -102,8 +103,8 @@
             ]
         ),
     ),
+    path("nav/typeahead/", typeahead.nav_typeahead, name="nav.typeahead"),
     path("event/new/", event.EventWizard.as_view(), name="event.create"),
-    path("event/typeahead/", event.event_list, name="event.typeahead"),
     path("event/", dashboard.DashboardEventListView.as_view(), name="event.list"),
     path(
         "event/<slug:event>/",
diff --git a/src/pretalx/orga/views/event.py b/src/pretalx/orga/views/event.py
index 566796077..476fea4bf 100644
--- a/src/pretalx/orga/views/event.py
+++ b/src/pretalx/orga/views/event.py
@@ -1,5 +1,3 @@
-import json
-from contextlib import suppress
 from pathlib import Path
 
 from csp.decorators import csp_update
@@ -9,9 +7,7 @@
 from django.core.exceptions import ValidationError
 from django.core.files.storage import FileSystemStorage
 from django.db import transaction
-from django.db.models import Q
 from django.forms.models import inlineformset_factory
-from django.http import JsonResponse
 from django.shortcuts import get_object_or_404, redirect
 from django.urls import reverse
 from django.utils.decorators import method_decorator
@@ -719,43 +715,6 @@ def form_valid(self, request, *args, **kwargs):
         return redirect("/orga/")
 
 
-def event_list(request):
-    query = json.dumps(str(request.GET.get("query", "")))[1:-1]
-    page = 1
-    with suppress(ValueError):
-        page = int(request.GET.get("page", "1"))
-    qs = (
-        request.user.get_events_with_any_permission()
-        .filter(
-            Q(name__icontains=query)
-            | Q(slug__icontains=query)
-            | Q(organiser__name__icontains=query)
-            | Q(organiser__slug__icontains=query)
-        )
-        .order_by("-date_from")
-    )
-
-    total = qs.count()
-    pagesize = 20
-    offset = (page - 1) * pagesize
-    doc = {
-        "results": [
-            {
-                "id": event.pk,
-                "slug": event.slug,
-                "organiser": str(event.organiser.name),
-                "name": str(event.name),
-                "text": str(event.name),
-                "date_range": event.get_date_range_display(),
-                "url": event.orga_urls.base,
-            }
-            for event in qs.select_related("organiser")[offset : offset + pagesize]
-        ],
-        "pagination": {"more": total >= (offset + pagesize)},
-    }
-    return JsonResponse(doc)
-
-
 @method_decorator(csp_update(SCRIPT_SRC="'self' 'unsafe-eval'"), name="dispatch")
 class WidgetSettings(EventPermissionRequired, FormView):
     form_class = WidgetSettingsForm
diff --git a/src/pretalx/orga/views/typeahead.py b/src/pretalx/orga/views/typeahead.py
new file mode 100644
index 000000000..7f35a6849
--- /dev/null
+++ b/src/pretalx/orga/views/typeahead.py
@@ -0,0 +1,173 @@
+import json
+from contextlib import suppress
+
+from django.db.models import Exists, OuterRef, Q
+from django.http import JsonResponse
+from django.utils.translation import gettext as _
+from django_scopes import scopes_disabled
+
+from pretalx.event.models import Organiser
+from pretalx.person.models import SpeakerProfile
+from pretalx.submission.models import Submission
+
+
+def serialize_user(user):
+    return {
+        "type": "user",
+        "name": str(user),
+        "url": "/orga/me",
+    }
+
+
+def serialize_orga(orga):
+    return {
+        "type": "organiser",
+        "name": str(orga.name),
+        "url": orga.orga_urls.base,
+    }
+
+
+def serialize_event(event):
+    return {
+        "type": "event",
+        "name": str(event.name),
+        "url": event.orga_urls.base,
+        "organiser": str(event.organiser.name),
+        "date_range": event.get_date_range_display(),
+    }
+
+
+def serialize_submission(submission):
+    return {
+        "type": "submission",
+        "name": _("Session") + f" {submission.title}",
+        "url": submission.orga_urls.base,
+        "event": str(submission.event.name),
+    }
+
+
+def serialize_speaker(speaker):
+    return {
+        "type": "speaker",
+        "name": _("Speaker") + f" {speaker.user.name}",
+        "url": speaker.orga_urls.base,
+        "event": str(speaker.event.name),
+    }
+
+
+@scopes_disabled()
+def nav_typeahead(request):
+    organiser = request.GET.get("organiser")
+    query = json.dumps(str(request.GET.get("query", "")))[1:-1]
+    page = 1
+    with suppress(ValueError):
+        page = int(request.GET.get("page", "1"))
+
+    qs_events = (
+        request.user.get_events_with_any_permission()
+        .filter(
+            Q(name__icontains=query)
+            | Q(slug__icontains=query)
+            | Q(organiser__name__icontains=query)
+            | Q(organiser__slug__icontains=query)
+        )
+        .order_by("-date_from")
+    )
+
+    show_user = (
+        not query
+        or (
+            query and request.user.email and query.lower() in request.user.email.lower()
+        )
+        or (query and request.user.name and query.lower() in request.user.name.lower())
+    )
+
+    qs_orga = Organiser.objects.filter(
+        pk__in=request.user.teams.values_list("organiser", flat=True)
+    )
+    if query:
+        if organiser and show_user:
+            qs_orga = qs_orga.filter(
+                Q(name__icontains=query) | Q(slug__icontains=query) | Q(pk=organiser)
+            )
+        else:
+            qs_orga = qs_orga.filter(
+                Q(name__icontains=query) | Q(slug__icontains=query)
+            )
+
+    if organiser:
+        organiser = qs_orga.filter(pk=organiser).first()
+
+    qs_submissions = Submission.objects.none()
+    qs_speakers = SpeakerProfile.objects.none()
+    if query and len(query) >= 3:
+        full_events = request.user.get_events_for_permission(
+            can_change_submissions=True
+        )
+        # review_events = request.user.get_events_for_permission(
+        #     is_reviewer=True, can_change_submissions=False
+        # )
+        # review_events = set(review_events) - set(full_submission_permissions)
+        # We'll exclude review events entirely for now, as they have extra challenges:
+        # users may be restricted from seeing speaker names by review settings, or
+        # limited to seeing submissions in specific tracks.
+        if full_events:
+            qs_submissions = Submission.objects.filter(
+                Q(title__icontains=query) | Q(code__istartswith=query),
+                event__in=full_events,
+            ).order_by()
+
+            qs_speakers = (
+                SpeakerProfile.objects.filter(
+                    Q(user__name__icontains=query)
+                    | Q(user__email__iexact=query)
+                    | Q(user__code__istartswith=query),
+                    event__in=full_events,
+                )
+                .annotate(
+                    # We need this subquery to filter out profiles without submissions.
+                    has_submission=Exists(
+                        Submission.objects.filter(
+                            event=OuterRef("event"), speakers__in=OuterRef("user")
+                        )
+                    )
+                )
+                .filter(
+                    has_submission=True,
+                )
+                .order_by()
+            )
+
+    total = qs_events.count() + qs_orga.count()
+    pagesize = 20
+    offset = (page - 1) * pagesize
+    results = (
+        ([serialize_user(request.user)] if show_user else [])
+        + [
+            serialize_orga(e)
+            for e in qs_orga[offset : offset + (pagesize if query else 5)]
+        ]
+        + [
+            serialize_event(e)
+            for e in qs_events.select_related("organiser")[
+                offset : offset + (pagesize if query else 5)
+            ]
+        ]
+        + [
+            serialize_submission(e)
+            for e in qs_submissions[offset : offset + (pagesize if query else 5)]
+        ]
+        + [
+            serialize_speaker(e)
+            for e in qs_speakers[offset : offset + (pagesize if query else 5)]
+        ]
+    )
+
+    if show_user and organiser:
+        current_organiser = serialize_orga(organiser)
+        if current_organiser in results:
+            results.remove(current_organiser)
+        results.insert(1, current_organiser)
+
+    doc = {"results": results, "pagination": {"more": total >= (offset + pagesize)}}
+    return JsonResponse(doc)
diff --git a/src/pretalx/static/common/scss/_dashboard.scss b/src/pretalx/static/common/scss/_dashboard.scss
index 6f4e7f7c7..290fd03a0 100644
--- a/src/pretalx/static/common/scss/_dashboard.scss
+++ b/src/pretalx/static/common/scss/_dashboard.scss
@@ -283,12 +283,4 @@
       }
     }
   }
-
-  .event-dropdown {
-    flex-basis: inherit !important;
-  }
-
-  .dropdown-menu.show {
-    position: absolute !important;
-  }
 }
diff --git a/src/pretalx/static/orga/js/typeahead.js b/src/pretalx/static/orga/js/typeahead.js
index 29de1d156..143a7731d 100644
--- a/src/pretalx/static/orga/js/typeahead.js
+++ b/src/pretalx/static/orga/js/typeahead.js
@@ -1,89 +1,92 @@
-/*global $,u2f */
-$(function() {
-  $("ul.navbar-nav .dropdown, .navbar-events-collapse").on(
+$(() => {
+  $("#nav-search-wrapper").on(
     "shown.bs.collapse shown.bs.dropdown",
-    function() {
-      $(this)
-        .parent()
-        .find("input")
-        .val("")
-        .change()
-        .focus()
-    }
+    () => $("#nav-search-dropdown input").val("").change().focus()
   )
-  $(".dropdown-menu input").click(function(e) {
+  $("#nav-search-dropdown input").click(function(e) {
     e.stopPropagation()
   })
 
   $("[data-event-typeahead]").each(function() {
+        console.log("got here")
     var $container = $(this)
     var $query = $(this).find("[data-typeahead-query]").length
       ? $(this).find("[data-typeahead-query]")
       : $($(this).attr("data-typeahead-field"))
-    $container.find("li:not(.query-holder)").remove()
+    var lastQuery = null;
+    var loadIndicatorTimeout = null;
+
+    function showLoadIndicator() {
+        $container.find("li").remove();
+        $container.append("<li class='loading'><span class='fa fa-4x fa-cog fa-spin'></span></li>");
+        $container.toggleClass('focused', $query.is(":focus") && $container.children().length > 0);
+    }
 
     $query.on("change", function() {
-      if ($container.attr("data-typeahead-field") && $query.val() === "") {
-        $container.removeClass("focused")
-        $container.find("li:not(.query-holder)").remove()
+      if ($query.val() === lastQuery) {
         return
       }
+
+      var thisQuery = $query.val()
+      lastQuery = thisQuery
+      window.clearTimeout(loadIndicatorTimeout)
+      loadIndicatorTimeout = window.setTimeout(showLoadIndicator, 80)
+
       $.getJSON(
-        $container.attr("data-source") +
-          "?query=" +
-          encodeURIComponent($query.val()),
+        $container.attr("data-source") + "?query=" + encodeURIComponent($query.val()) + (typeof $container.attr("data-organiser") !== "undefined" ? "&organiser=" + $container.attr("data-organiser") : ""),
         function(data) {
-          $container.find("li:not(.query-holder)").remove()
+          if (thisQuery !== lastQuery) {
+            // Ignore this response, it's for an old query
+            return
+          }
+          window.clearTimeout(loadIndicatorTimeout);
+
+          $container.find("li").remove()
           $.each(data.results, function(i, res) {
-            $container.append(
-              $("<li>").append(
-                $("<a>")
-                  .attr("href", res.url)
-                  .addClass("event-text")
-                  .append(
-                    $("<span>")
-                      .addClass("event-name")
-                      .append(
-                        $("<div>")
-                          .text(res.name)
-                          .html()
-                      )
-                  )
-                  .append(
-                    $("<span>")
-                      .addClass("event-organiser")
-                      .append($("<span>").addClass("fa fa-users fa-fw"))
-                      .append(" ")
-                      .append(
-                        $("<div>")
-                          .text(res.organiser)
-                          .html()
-                      )
-                  )
-                  .append(
-                    $("<span>")
-                      .addClass("event-date")
-                      .append($("<span>").addClass("fa fa-calendar fa-fw"))
-                      .append(" ")
-                      .append(res.date_range)
-                  )
-                  .on("mousedown", function(event) {
-                    if ($(this).length) {
-                      location.href = $(this).attr("href")
-                    }
-                    $(this)
-                      .parent()
-                      .addClass("active")
-                    event.preventDefault()
-                    event.stopPropagation()
-                  })
+            let $content = $("<div>")
+            if (res.type === "organiser" || res.type === "user") {
+                const icon = res.type === "organiser" ? "fa-users" : "fa-user";
+                $content.append(
+                    $("<span>").addClass("search-title").append(
+                        $("<i>").addClass("fa fa-fw " + icon)
+                    ).append(" ").append($("<span>").text(res.name).html())
+                )
+            } else if (res.type === "submission" || res.type === "speaker") {
+                $content.append(
+                    $("<span>").addClass("search-title").append($("<div>").text(res.name).html())
+                ).append(
+                    $("<span>").addClass("search-detail").append(
+                        $("<span>").addClass("fa fa-calendar fa-fw")
+                    ).append(" ").append($("<div>").text(res.event).html())
+                )
+            } else if (res.type === "event") {
+                $content.append(
+                    $("<span>").addClass("search-title").append($("<div>").text(res.name).html())
+                ).append(
+                    $("<span>").addClass("search-detail").append(
+                        $("<span>").addClass("fa fa-users fa-fw")
+                    ).append(" ").append($("<div>").text(res.organiser).html())
+                ).append(
+                    $("<span>").addClass("search-detail").append(
+                        $("<span>").addClass("fa fa-calendar fa-fw")
+                    ).append(" ").append(res.date_range)
+                )
+            }
+
+            $container.find("ul").append($("<li>")
+                .append($("<a>").attr("href", res.url).append($content)
+                    .on("mousedown", function(event) {
+                        if ($(this).length) {
+                          location.href = $(this).attr("href")
+                        }
+                        $(this).parent().addClass("active")
+                        event.preventDefault()
+                        event.stopPropagation()
+                      })
+                )
               )
-            )
           })
-          $container.toggleClass(
-            "focused",
-            $query.is(":focus") && $container.children().length > 0
-          )
+          $container.toggleClass("focused", $query.is(":focus") && $container.children().length > 0)
         }
       )
     })
diff --git a/src/pretalx/static/orga/scss/_layout.scss b/src/pretalx/static/orga/scss/_layout.scss
index 723b0041e..8ee409fa3 100644
--- a/src/pretalx/static/orga/scss/_layout.scss
+++ b/src/pretalx/static/orga/scss/_layout.scss
@@ -38,6 +38,7 @@ body {
 
 aside.sidebar {
   background-color: $gray-lightest;
+  border-right: 1px solid $gray-lighter;
   width: 45px;
   height: 100vh;
   font-size: 14px;
@@ -57,6 +58,15 @@ aside.sidebar {
     .nav-fold div > a {
       display: block;
     }
+    #nav-search {
+      #search-context-icon .fa-stack-1x,
+      #search-context-icon .fa-stack-2x {
+        margin-left: 0;
+      }
+      #search-context-text {
+        display: flex;
+      }
+    }
   }
 
   a {
@@ -74,13 +84,14 @@ aside.sidebar {
 
     .arrow {
       flex-grow: 0;
-      width: 50px;
+      width: 40px;
       border: none;
       display: flex;
       justify-content: center;
 
       i {
         padding-top: 4px;
+        margin-right: 0;
       }
     }
   }
@@ -131,90 +142,132 @@ aside.sidebar {
   .nav-link.nav-link-second-level:hover {
     background-color: $gray-lighter;
   }
-}
+  #nav-search {
+    display: flex;
+    flex-direction: column;
+    padding: 8px 4px;
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    padding-bottom: 8px;
+    cursor: pointer;
+    color: $brand-primary;
+    height: 56px;
 
-.navbar {
-  height: 50px;
-  color: $brand-primary;
-  background-color: $offwhite;
-  border-bottom: 4px $brand-primary solid;
-  padding: 0.25rem;
-  box-shadow:
-    1px 1px 3px rgba(0, 0, 0, 0.12),
-    1px 1px 2px rgba(0, 0, 0, 0.24);
-  z-index: 200;
+    &:after {
+      /* dropdown indicator */
+      margin-right: 12px;
+    }
 
-  .twitter-typeahead {
-    width: inherit;
-    margin-right: 8px;
-  }
+    #search-context-icon {
+      min-width: 38px;
+    }
 
-  .fa-eye {
-    margin-top: 15px;
-    margin-left: 5px;
+    #search-context-text {
+      display: none;
+      flex-direction: column;
+      flex-grow: 1;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      margin-left: 6px;
+      .context-name,
+      .context-meta {
+        text-decoration: none;
+        text-overflow: ellipsis;
+        white-space: nowrap;
+        overflow: hidden;
+      }
+      .context-name {
+        font-family: "Titillium Web";
+        font-weight: 600;
+        font-size: 15px;
+      }
+      .context-meta {
+        font-size: 11px;
+        color: $body-color-light;
+        font-weight: normal;
+      }
+    }
   }
 
-  .dropdown-menu {
-    position: absolute;
-    top: 49px;
-    padding: 8px;
-    box-shadow:
-      0 1px 3px rgba(0, 0, 0, 0.12),
-      0 1px 2px rgba(0, 0, 0, 0.24);
+  #nav-search-dropdown {
+    width: 250px;
+    transform: unset !important;
+    top: 53px !important;
+    max-height: calc(100vh - 53px - 50px);
+    margin: 0;
+    padding: 0;
+
+    box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
     border-radius: 0;
-    width: 300px;
-    z-index: 0;
     background: $offwhite;
-    max-height: 500px;
     overflow-y: auto;
-    li {
-      border-top: $gray-light 1px solid;
-      &:hover,
-      &.active {
-        background: $gray-lightest;
-      }
-    }
-  }
 
-  #eventDropdown {
-    display: flex;
-    align-items: center;
-    padding: 7px;
-
-    .event-text {
-      width: 200px;
-    }
-    &[aria-expanded="true"] {
-      background: $gray-lighter;
+    .query-holder {
+      padding: 8px;
+      position: sticky;
+      top: 0;
+      background: $offwhite;
     }
-  }
-  #eventDropdown .event-text,
-  .dropdown-menu .event-text {
-    padding: 8px;
-    font-size: 14px;
-    line-height: 15px;
-    display: flex;
-    flex-direction: column;
-    text-decoration: none;
 
-    span {
-      display: inline-block;
-      text-overflow: ellipsis;
-      white-space: nowrap;
-      overflow: hidden;
-    }
+    ul#search-results {
+      padding: 0;
+      margin: 0;
+      list-style: none;
 
-    .event-name {
-      font-family: "Titillium Web";
-      font-weight: 600;
-    }
+      li.loading {
+        text-align: center;
+        padding: 8px;
+      }
 
-    .event-date,
-    .event-organiser {
-      font-size: 11px;
-      color: $body-color-light;
+      li > a {
+        text-decoration: none;
+        & > div {
+          display: flex;
+          flex-direction: column;
+          margin: 0;
+          border-top: 1px solid $gray-light;
+          padding: 8px;
+          font-size: 14px;
+          line-height: 15px;
+          &:hover,
+          &.active {
+            background: $gray-lightest;
+          }
+        }
+        .search-title,
+        .search-detail {
+          display: inline-block;
+          text-overflow: ellipsis;
+          white-space: nowrap;
+          overflow: hidden;
+        }
+        .search-title {
+          font-family: "Titillium Web";
+          padding-bottom: 4px;
+          display: flex;
+          align-items: center;
+        }
+        .search-detail {
+          font-size: 11px;
+          color: $body-color-light;
+          font-weight: normal;
+        }
+      }
     }
   }
+}
+
+.navbar {
+  height: 50px;
+  color: $brand-primary;
+  background-color: $offwhite;
+  border-bottom: 4px $brand-primary solid;
+  padding: 0.25rem;
+  box-shadow:
+    1px 1px 3px rgba(0, 0, 0, 0.12),
+    1px 1px 2px rgba(0, 0, 0, 0.24);
+  z-index: 200;
   a {
     color: $brand-primary;
   }
@@ -503,9 +556,6 @@ footer {
   nav .twiter-typeahead {
     display: none !important;
   }
-  nav #event-navigation #eventDropdown {
-    display: none;
-  }
   .container-fluid {
     padding: 0;
     width: 100%;


Test patch:
diff --git a/src/tests/orga/views/test_orga_views_event.py b/src/tests/orga/views/test_orga_views_event.py
index a0d452bed..676d8d12e 100644
--- a/src/tests/orga/views/test_orga_views_event.py
+++ b/src/tests/orga/views/test_orga_views_event.py
@@ -1091,19 +1091,24 @@ def test_edit_review_settings_reviewer_cannot_move_review_phase(review_client, e
 
 @pytest.mark.django_db
 def test_organiser_can_see_event_suggestions(orga_client, event):
-    response = orga_client.get("/orga/event/typeahead/")
+    response = orga_client.get("/orga/nav/typeahead/")
     assert response.status_code == 200
     content = json.loads(response.content.decode())["results"]
-    assert len(content) == 1
-    assert content[0]["id"] == event.id
+    assert len(content) == 3
+    assert content[0]["type"] == "user"
+    assert content[1]["type"] == "organiser"
+    assert content[1]["name"] == str(event.organiser)
+    assert content[2]["type"] == "event"
+    assert content[2]["name"] == str(event.name)
 
 
 @pytest.mark.django_db
 def test_speaker_cannot_see_event_suggestions(speaker_client, event):
-    response = speaker_client.get("/orga/event/typeahead/")
+    response = speaker_client.get("/orga/nav/typeahead/")
     assert response.status_code == 200
     content = json.loads(response.content.decode())["results"]
-    assert len(content) == 0
+    assert len(content) == 1
+    assert content[0]["type"] == "user"
 
 
 @pytest.mark.django_db