defter 0.3.9

Self-hosted Zotero sync and file storage server, with metadata in SQLite and attachments in S3.
{% extends "base.html" %}

{% block title %}{{ person.name }}{% endblock %}

{#- Unlike the other pages, this one is meant to be found. -#}
{% block meta %}
  {%- if !person.summary.is_empty() %}
  <meta name="description" content="{{ person.summary }}">
  {%- endif %}
{% endblock %}

{% block header_actions %}
<form method="get" action="/{{ orcid }}" class="flex items-center gap-1">
  {%- include "filter-fields.html" %}
  {%- if !q.is_empty() %}
  <input type="hidden" name="q" value="{{ q }}">
  {%- endif %}
  <select name="lang" data-language aria-label="{{ t.language }}" class="rounded-md border bg-background px-2 py-1 text-sm">
    {%- for option in languages %}
    <option value="{{ option.code() }}"{% if option.code() == lang.code() %} selected{% endif %}>{{ option.name() }}</option>
    {%- endfor %}
  </select>
  <noscript><button type="submit" class="rounded-md border px-2 py-1 text-sm hover:bg-muted">{{ t.apply }}</button></noscript>
</form>
{% endblock %}

{#- Laid out as a person's page on commonmeta.org (dragoman's App.svelte and
    PersonCard.svelte): the person, their affiliations, then the works with
    their facets beside them. A facet value is a link and a facet a <details>,
    so all of it works without JavaScript; with it, htmx replaces only the list
    of works. Buttons that need JavaScript show only with it (`.js`). -#}
{% block content %}
<div class="space-y-2">
  <div class="border border-border rounded-md overflow-hidden">
    <div class="px-5 py-3 border-b border-border bg-muted/50">
      <h2 class="text-lg font-semibold text-primary">{{ t.person }}</h2>
    </div>
    <div class="flex items-start gap-3 px-5 py-4 bg-background">
      <div class="flex-1 min-w-0 space-y-0.5">
        <h1 class="text-lg font-semibold leading-snug">{{ person.name }}</h1>
        <dl class="mt-2 space-y-3 text-sm">
          {%- if !person.given_name.is_empty() %}
          <div>
            <dt class="font-semibold text-foreground">{{ t.given_name }}</dt>
            <dd class="text-muted-foreground">{{ person.given_name }}</dd>
          </div>
          {%- endif %}
          {%- if !person.family_name.is_empty() %}
          <div>
            <dt class="font-semibold text-foreground">{{ t.family_name }}</dt>
            <dd class="text-muted-foreground">{{ person.family_name }}</dd>
          </div>
          {%- endif %}
          {%- if !person.additional_names.is_empty() %}
          <div>
            <dt class="font-semibold text-foreground">{{ t.additional_names }}</dt>
            <dd class="text-muted-foreground">{{ person.additional_names }}</dd>
          </div>
          {%- endif %}
          {%- if !person.description.is_empty() %}
          <div>
            <dd class="text-muted-foreground line-clamp-6">{{ person.description }}</dd>
          </div>
          {%- endif %}
          {%- if !person.urls.is_empty() %}
          <div>
            <dt class="font-semibold text-foreground">{{ t.websites }}</dt>
            <dd class="text-muted-foreground">
              {%- for link in person.urls %}
              {%- if !loop.first %} · {% endif -%}
              <a href="{{ link.url }}" target="_blank" rel="noreferrer" class="hover:underline">{{ link.name }}</a>
              {%- endfor -%}
            </dd>
          </div>
          {%- endif %}
          {%- if !person.country.is_empty() %}
          <div>
            <dt class="font-semibold text-foreground">{{ t.country }}</dt>
            <dd class="text-muted-foreground">{{ person.country }}</dd>
          </div>
          {%- endif %}
        </dl>
        <p class="text-sm text-muted-foreground/60 mt-2">{{ works_count }}</p>
        <a href="{{ person.orcid_url }}" target="_blank" rel="noreferrer" class="text-sm text-primary hover:underline inline-flex items-center gap-1.5 pt-0.5 wrap-anywhere max-w-full">
          <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="14" height="14" role="img" aria-label="ORCID" class="shrink-0"><path fill="#A6CE39" d="M256 128c0 70.7-57.3 128-128 128C57.3 256 0 198.7 0 128 0 57.3 57.3 0 128 0c70.7 0 128 57.3 128 128z"/><path fill="#fff" d="M86.3 186.2H70.9V79.1h15.4v48.4zM108.9 79.1h41.6c39.6 0 57 28.3 57 53.6 0 27.5-21.5 53.6-56.8 53.6h-41.8zm15.4 93.3h24.5c34.9 0 42.9-26.5 42.9-39.7 0-21.5-13.7-39.7-43.7-39.7h-23.7zM88.7 56.8c0 5.5-4.5 10.1-10.1 10.1s-10.1-4.6-10.1-10.1c0-5.6 4.5-10.1 10.1-10.1s10.1 4.6 10.1 10.1z"/></svg>
          {{- person.orcid_url -}}
        </a>
      </div>
      <div class="flex flex-col gap-0.5 shrink-0">
        <button type="button" data-copy="{{ person.json }}" aria-label="{{ t.copy_citation }}" title="{{ t.copy_citation }}" class="hidden [.js_&]:inline-flex items-center justify-center h-7 w-7 rounded-md text-muted-foreground hover:bg-muted hover:text-foreground">
          {% include "copy-icons.html" %}
        </button>
      </div>
    </div>
  </div>

  {%- if !person.affiliations.is_empty() %}
  <div class="border border-border rounded-md overflow-hidden">
    <div class="px-5 py-3 border-b border-border bg-muted/50">
      <h2 class="text-base font-semibold">{{ t.affiliations }}</h2>
    </div>
    <div class="bg-background">
      {%- for affiliation in person.affiliations %}
      <div class="px-5 py-4 space-y-0.5">
        <p class="text-sm font-medium inline-flex items-center gap-0.5">
          {%- if let Some(ror) = affiliation.ror %}
          <a href="{{ ror }}" target="_blank" rel="noreferrer" class="hover:underline">{{ affiliation.name }}</a>{% include "ror-icon.html" %}
          {%- else %}
          {{ affiliation.name }}
          {%- endif %}
        </p>
        {%- if !affiliation.role.is_empty() %}
        <p class="text-sm text-muted-foreground">{{ affiliation.role }}</p>
        {%- endif %}
        {%- if !affiliation.dates.is_empty() %}
        <div class="text-sm text-muted-foreground">{{ affiliation.dates }}</div>
        {%- endif %}
      </div>
      {%- endfor %}
    </div>
  </div>
  {%- endif %}

  {#- Rendered alone for htmx (see ui::profile_page), which swaps it in place of
      itself. It stays where it is on the page, as the list does on
      commonmeta.org: `scrollIntoViewOnBoost` is off in base.html. -#}
  {% block works %}
  <section id="works" hx-boost="true" hx-target="#works" hx-swap="outerHTML" hx-indicator="#works"
           class="pt-4 transition-opacity [&.htmx-request]:opacity-50 {% if !facets.is_empty() %}lg:grid lg:grid-cols-[13rem_1fr] lg:gap-x-8 lg:items-start{% endif %}">
    {%- if !chips.is_empty() && !facets.is_empty() %}
    <div class="hidden lg:block" aria-hidden="true"></div>
    {%- endif %}
    {%- if !chips.is_empty() %}
    {#- What the list is narrowed by, at every width: the sidebar is gone below
        lg, and this is how a filter is seen and cleared on a phone. -#}
    <div class="flex flex-wrap items-center gap-1.5 mb-3">
      <span class="text-xs font-semibold uppercase tracking-wide text-muted-foreground">{{ t.filters }}</span>
      {%- for chip in chips %}
      <a href="{{ chip.href }}" aria-label="{{ chip.label }} — {{ t.clear_filters }}" class="inline-flex items-center gap-1.5 rounded-full border border-primary/30 bg-primary/10 px-2.5 py-1 text-xs font-medium text-foreground transition-colors hover:border-primary/60">
        {%- if !chip.heading.is_empty() %}
        <span class="text-muted-foreground">{{ chip.heading }}</span>
        {%- endif %}
        {{ chip.label }}
        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" class="w-3 h-3 opacity-60"><path d="M18 6 6 18M6 6l12 12"/></svg>
      </a>
      {%- endfor %}
      <a href="{{ clear_href }}" class="ml-1 text-xs text-primary hover:underline">{{ t.clear_filters }}</a>
    </div>
    {%- endif %}

    {%- if !facets.is_empty() %}
    <aside class="hidden lg:block sticky top-20 self-start" aria-label="{{ t.filters }}">
      {%- if let Some(value) = featured %}
      <a href="{{ value.href }}"{% if value.picked %} aria-current="true"{% endif %}
         class="flex items-start gap-2 rounded px-1.5 py-1 -mx-1.5 mb-3 text-sm transition-colors {% if value.picked %}bg-muted font-medium text-foreground{% else %}text-muted-foreground hover:bg-muted hover:text-foreground{% endif %}">
        <span aria-hidden="true" class="mt-[3px] shrink-0 w-3.5 h-3.5 rounded-sm border flex items-center justify-center {% if value.picked %}border-primary bg-primary text-primary-foreground{% else %}border-muted-foreground/40{% endif %}">
          {%- if value.picked %}
          <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" class="w-2.5 h-2.5"><path d="M20 6 9 17l-5-5"/></svg>
          {%- endif %}
        </span>
        <span class="flex-1 min-w-0 break-words">{{ value.label }}</span>
        <span class="shrink-0 text-xs tabular-nums opacity-70 mt-px">{{ value.count }}</span>
      </a>
      {%- endif %}
      {%- if facets.len() > 1 %}
      <div class="hidden [.js_&]:flex justify-end mb-3">
        <button type="button" data-facets-toggle data-open-label="{{ t.open_all }}" data-close-label="{{ t.close_all }}" class="text-xs text-muted-foreground hover:text-foreground transition-colors">{{ t.open_all }}</button>
      </div>
      {%- endif %}
      {%- for facet in facets %}
      <details data-facet="{{ facet.key }}" class="group mb-5"{% if facet.picked %} open{% endif %}>
        <summary class="w-full flex items-center gap-1 text-xs font-semibold uppercase tracking-wide text-muted-foreground mb-2 hover:text-foreground transition-colors cursor-pointer list-none [&::-webkit-details-marker]:hidden">
          <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true" class="w-3 h-3 shrink-0 transition-transform -rotate-90 group-open:rotate-0"><path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5"/></svg>
          <span class="flex-1 text-left">{{ facet.heading }}</span>
        </summary>
        <ul class="space-y-0.5 text-sm">
          {%- for value in facet.values %}
          <li>
            <a href="{{ value.href }}"{% if value.picked %} aria-current="true"{% endif %}
               class="flex items-start gap-2 rounded px-1.5 py-1 -mx-1.5 transition-colors {% if value.picked %}bg-muted font-medium text-foreground{% else %}text-muted-foreground hover:bg-muted hover:text-foreground{% endif %}">
              <span aria-hidden="true" class="mt-[3px] shrink-0 w-3.5 h-3.5 rounded-sm border flex items-center justify-center {% if value.picked %}border-primary bg-primary text-primary-foreground{% else %}border-muted-foreground/40{% endif %}">
                {%- if value.picked %}
                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" class="w-2.5 h-2.5"><path d="M20 6 9 17l-5-5"/></svg>
                {%- endif %}
              </span>
              <span class="flex-1 min-w-0 break-words">{{ value.label }}</span>
              <span class="shrink-0 text-xs tabular-nums opacity-70 mt-px">{{ value.count }}</span>
            </a>
          </li>
          {%- endfor %}
        </ul>
      </details>
      {%- endfor %}
    </aside>
    {%- endif %}

    <div class="min-w-0">
      <form id="work-search" method="get" action="/{{ orcid }}" role="search" class="mb-3">
        {%- include "filter-fields.html" %}
        {%- if let Some(code) = lang_param %}
        <input type="hidden" name="lang" value="{{ code }}">
        {%- endif %}
        <div class="flex items-center border rounded-md bg-background overflow-hidden transition-colors focus-within:border-primary">
          <input type="search" name="q" value="{{ q }}" placeholder="{{ t.search_placeholder }}" aria-label="{{ t.search_placeholder }}"
                 autocomplete="off" spellcheck="false"
                 class="flex-1 h-9 px-3 text-sm bg-transparent outline-none min-w-0 [&::-webkit-search-cancel-button]:hidden">
          {%- if !q.is_empty() %}
          <a href="{{ clear_search_href }}" aria-label="{{ t.clear }}" class="h-9 w-8 flex items-center justify-center text-muted-foreground hover:text-foreground transition-colors shrink-0">
            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-3.5 h-3.5"><path d="M18 6 6 18M6 6l12 12"/></svg>
          </a>
          {%- endif %}
          <button type="submit" aria-label="{{ t.search }}" class="h-9 px-3 bg-primary text-primary-foreground hover:opacity-90 shrink-0 flex items-center justify-center transition-colors">
            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class="w-4 h-4"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
          </button>
        </div>
      </form>
      <div class="border border-border rounded-md overflow-hidden">
        <div class="px-5 py-3 border-b border-border bg-muted/50 flex items-baseline gap-3 flex-wrap">
          <h2 class="text-base font-semibold">{{ total }}</h2>
          {%- if let Some(page_of) = page_of %}
          <span class="ml-auto text-sm text-muted-foreground">{{ page_of }}</span>
          {%- endif %}
        </div>
        {%- if works.is_empty() %}
        <p class="px-5 py-4 bg-background text-sm text-muted-foreground">
          {%- if chips.is_empty() %}{{ t.no_works }}{% else %}{{ t.no_results }}{% endif -%}
        </p>
        {%- endif %}
        <ol class="bg-background">
          {%- for work in works %}
          <li class="flex items-start gap-3 px-5 py-4 border-b border-border last:border-b-0">
            <span class="text-xs text-muted-foreground min-w-5 shrink-0 pt-0.5 text-right tabular-nums">{{ lang.count(start + loop.index) }}.</span>
            <div class="flex-1 min-w-0 space-y-0.5">
              <p class="text-base font-semibold leading-snug">
                {{- work.title -}}
              </p>
              <p class="text-sm text-muted-foreground">
                {{- work.kind }}
                {%- if !work.date.is_empty() %} {{ t.published }} {{ work.date }}{% endif %}
                {%- if !work.container.is_empty() %} {{ t.in_container }} <em>{{ work.container }}</em>{% endif %}
                {%- if !work.language.is_empty() %} ({{ work.language }}){% endif -%}
              </p>
              {%- if !work.authors.is_empty() %}
              <p class="text-sm text-muted-foreground leading-relaxed">{{ work.authors }}</p>
              {%- endif %}
              {%- if !work.description.is_empty() %}
              <p class="text-sm text-muted-foreground line-clamp-3 mt-3 mb-3">{{ work.description }}</p>
              {%- endif %}
              {%- if !work.license.is_empty() %}
              <p class="text-sm text-muted-foreground/60">{% if work.is_license %}{{ t.license }} {% endif %}{% if let Some(url) = work.license_url %}<a href="{{ url }}" target="_blank" rel="noreferrer" class="hover:underline">{{ work.license }}</a>{% else %}{{ work.license }}{% endif %}.
                {%- if !work.license_icons.is_empty() %}&nbsp;
                {%- if let Some(url) = work.license_url %}
                <a href="{{ url }}" target="_blank" rel="noreferrer" aria-label="{{ work.license }}" class="inline-flex items-center gap-0.5 align-text-bottom hover:opacity-70 transition-opacity">
                  {%- for icon in work.license_icons %}{% include "cc-icon.html" %}{% endfor -%}
                </a>
                {%- else %}
                <span class="inline-flex items-center gap-0.5 align-text-bottom">
                  {%- for icon in work.license_icons %}{% include "cc-icon.html" %}{% endfor -%}
                </span>
                {%- endif %}
                {%- endif -%}
              </p>
              {%- endif %}
              {%- if let Some(pdf) = work.pdf %}
              <div class="text-sm mt-3 mb-2">
                <p class="font-semibold text-foreground">{{ t.content }}</p>
                <div class="text-muted-foreground leading-5"><a href="{{ pdf }}" target="_blank" rel="noreferrer" class="text-primary hover:underline inline-flex items-center gap-1">{% include "pdf-icon.html" %}PDF</a></div>
              </div>
              {%- endif %}
              {%- if let Some(link) = work.link %}
              <a href="{{ link }}" target="_blank" rel="noreferrer" class="text-sm text-primary hover:underline flex items-center gap-1.5 pt-0.5 wrap-anywhere max-w-full">
                {%- if work.has_doi %}{% include "doi-icon.html" %}{% endif %}{{ link -}}
              </a>
              {%- endif %}
            </div>
            <div class="flex flex-col gap-0.5 shrink-0">
              <button type="button" data-copy="{{ work.citation }}" aria-label="{{ t.copy_citation }}" title="{{ t.copy_citation }}" class="hidden [.js_&]:inline-flex items-center justify-center h-7 w-7 rounded-md text-muted-foreground hover:bg-muted hover:text-foreground">
                {% include "copy-icons.html" %}
              </button>
            </div>
          </li>
          {%- endfor %}
        </ol>
        {%- if pager.len() > 1 %}
        <div class="px-5 pb-4">
          <nav aria-label="{{ t.pagination }}" class="mt-4 overflow-x-auto">
            <div class="w-max mx-auto flex items-center border border-border rounded-lg bg-background overflow-hidden divide-x divide-border shadow-sm">
              {%- if let Some(href) = previous_href %}
              <a href="{{ href }}" aria-label="{{ t.previous_page }}" class="inline-flex items-center justify-center h-9 min-w-10 px-2 text-xs transition-colors hover:bg-muted"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="w-4 h-4"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5 8.25 12l7.5-7.5"/></svg></a>
              {%- endif %}
              {%- for cell in pager %}
              {%- if let Some(href) = cell.href %}
              <a href="{{ href }}"{% if cell.current %} aria-current="page"{% endif %} class="inline-flex items-center justify-center h-9 min-w-10 px-2 text-xs transition-colors tabular-nums {% if cell.current %}bg-muted font-medium{% else %}hover:bg-muted{% endif %}">{{ cell.label }}</a>
              {%- else %}
              <span class="inline-flex items-center justify-center h-9 min-w-10 px-2 text-xs text-muted-foreground select-none">{{ cell.label }}</span>
              {%- endif %}
              {%- endfor %}
              {%- if let Some(href) = next_href %}
              <a href="{{ href }}" aria-label="{{ t.next_page }}" class="inline-flex items-center justify-center h-9 min-w-10 px-2 text-xs transition-colors hover:bg-muted"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="w-4 h-4"><path stroke-linecap="round" stroke-linejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5"/></svg></a>
              {%- endif %}
            </div>
          </nav>
        </div>
        {%- endif %}
        {#- A plain form, not htmx's: it downloads a file. -#}
        <form method="get" action="/{{ orcid }}/export" hx-boost="false" data-export class="border-t border-border px-4 py-3 bg-muted flex items-center gap-2 flex-wrap">
          {%- include "filter-fields.html" %}
          {%- if !q.is_empty() %}
          <input type="hidden" name="q" value="{{ q }}">
          {%- endif %}
          <input type="hidden" name="lang" value="{{ lang.code() }}">
          <select name="format" data-export-format aria-label="{{ t.export_format }}" class="w-40 h-8 rounded-md border bg-background px-2 text-xs">
            {%- for format in formats %}
            <option value="{{ format.value }}"{% if format.value == default_format %} selected{% endif %}>
              {%- if let Some(label) = format.label %}{{ label }}{% else %}{{ t.formatted_citation }}{% endif -%}
            </option>
            {%- endfor %}
          </select>
          <select name="style" data-export-style aria-label="{{ t.citation_style }}" class="flex-1 min-w-36 h-8 rounded-md border bg-background px-2 text-xs">
            {%- for style in styles %}
            <option value="{{ style.0 }}">{{ style.1 }}</option>
            {%- endfor %}
          </select>
          <button type="submit" class="inline-flex items-center h-8 gap-1.5 rounded-md bg-primary px-3 text-xs font-medium text-primary-foreground hover:opacity-90">
            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true" class="w-3.5 h-3.5 shrink-0"><path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3"/></svg>
            {{- t.export -}}
          </button>
          <button type="button" data-export-copy data-copied-label="{{ t.copied }}" class="hidden [.js_&]:inline-flex items-center h-8 gap-1.5 rounded-md border bg-background px-3 text-xs font-medium hover:bg-muted">
            {% include "copy-icons.html" %}<span>{{ t.copy }}</span>
          </button>
        </form>
      </div>
    </div>
  </section>
  {% endblock %}
</div>
{% endblock %}