New approach to extracting publication data and filtering/searching it #17
7 changed files with 253 additions and 77 deletions
|
|
@ -16,19 +16,11 @@ jobs:
|
|||
uses: actions/checkout@v4
|
||||
- name: Prepare environment
|
||||
uses: ./.forgejo/actions/prep-metadata-query
|
||||
- name: Install jq
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y jq
|
||||
- name: Update pages
|
||||
- name: Update publication pages
|
||||
run: |
|
||||
dtc get-records ${{ env.DUMPTHINGS_APIURL }} public -C XYZPublication \
|
||||
| qri inline-records -p about -p attributed_to -c public \
|
||||
| jq -sc '{pid: "xyzri:this-is-not-important", publications: .}' \
|
||||
| qri render-record page_templates/publications.json.j2 'data/publications.json'
|
||||
| qri render-record page_templates/publication.md.j2 \
|
||||
'content/{__pid_curie_reference}/_index.md'
|
||||
- name: Deposit changes
|
||||
run: |
|
||||
git add data
|
||||
git diff --quiet --cached \
|
||||
&& echo "Already up-to-date" \
|
||||
|| ( git commit -m "chore: auto-generate content from metadata" && git push origin )
|
||||
uses: ./.forgejo/actions/deposit-changes
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
---
|
||||
title: Publications
|
||||
---
|
||||
67
layouts/_partials/publication-item.html
Normal file
67
layouts/_partials/publication-item.html
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
<article
|
||||
class="pub mb-6 border border-neutral-200 dark:border-neutral-700"
|
||||
data-kind="{{ .Params.kind }}"
|
||||
data-year="{{ if .Params.date }}{{ substr .Params.date 0 4 }}{{ else }}unknown{{ end }}"
|
||||
data-topics='{{ .Params.topic | jsonify }}'
|
||||
data-authors='{{ .Params.author | jsonify }}'
|
||||
data-title="{{ .Title | lower }}"
|
||||
style="border-radius: 5px; padding: 1em;"
|
||||
>
|
||||
|
||||
<!-- Title, including year and DOI and publication kind -->
|
||||
<h3 class="text-xl font-semibold text-neutral-900 dark:text-neutral-100" style="margin-top: 0;">
|
||||
<a href="{{ .RelPermalink }}" style="text-decoration: unset;">
|
||||
{{ .Title }}
|
||||
</a>
|
||||
{{ if .Params.date }}
|
||||
{{ $year := substr .Params.date 0 4 }}
|
||||
<span class="text-neutral-500 text-base font-normal">
|
||||
({{ $year }})
|
||||
</span>
|
||||
{{ end }}
|
||||
{{ with .Params.doi }}
|
||||
<span class="mt-3 text-sm" style="border-left: 1px solid grey; padding-left: 0.5em;">
|
||||
<a
|
||||
href="https://doi.org/{{ . }}"
|
||||
target="_blank"
|
||||
class="text-primary-600"
|
||||
style="text-decoration: unset;"
|
||||
>
|
||||
DOI: {{ . }}
|
||||
</a>
|
||||
</span>
|
||||
{{ end }}
|
||||
{{ with .Params.kind }}
|
||||
<span class="mt-3 text-xs" style="font-style: italic; border-left: 1px solid grey; padding-left: 0.5em; margin-left: 0.5em;">
|
||||
{{ . | replaceRE "^.*:" "" }}
|
||||
</span>
|
||||
{{ end }}
|
||||
</h3>
|
||||
|
||||
<!-- Authors -->
|
||||
{{ with .Params.author }}
|
||||
<div class="mt-2 flex flex-wrap gap-2">
|
||||
{{ range $i, $a := . }}
|
||||
<span class="text-sm px-2 py-1 rounded-full border border-neutral-200 dark:border-neutral-700 whitespace-nowrap">
|
||||
{{ $a.given_name }} {{ $a.family_name }}
|
||||
</span>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
<!-- Topics -->
|
||||
{{ with .Params.topic }}
|
||||
<div class="flex flex-wrap gap-2" style="margin-top: 1em;">
|
||||
{{ range . }}
|
||||
<span
|
||||
class="topic-chip text-xs bg-neutral-200 dark:bg-neutral-700 text-neutral-800 dark:text-neutral-200 cursor-pointer transition-colors duration-150 hover:bg-primary-100 dark:hover:bg-primary-900 hover:text-primary-700 dark:hover:text-primary-300"
|
||||
style="border-radius: 4px; padding: 0.5em"
|
||||
data-topic="{{ .display_label }}"
|
||||
>
|
||||
{{ .display_label }}
|
||||
</span>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
</article>
|
||||
|
|
@ -9,6 +9,9 @@
|
|||
<div class="pub-layout">
|
||||
<!-- Sidebar -->
|
||||
<aside class="filters-panel">
|
||||
<button onclick="clearAllFilters()" class="clear-button mt-2 text-sm text-primary-600 hover:underline">
|
||||
Clear all
|
||||
</button>
|
||||
<div class="filter-block">
|
||||
<strong>Kind</strong>
|
||||
<div id="filter-kind" class="filter-group"></div>
|
||||
|
|
@ -26,14 +29,17 @@
|
|||
</aside>
|
||||
<!-- Main rendered content -->
|
||||
<div class="results-panel">
|
||||
<input type="text" id="search" placeholder="Search publications..." class="search-bar" />
|
||||
<div id="results"></div>
|
||||
<input type="text" id="search" placeholder="Search publications..." class="search-bar" /> <span id="pub-count"></span>
|
||||
<!-- Data -->
|
||||
{{ $pubPages := where .Site.Pages "Section" "publications" }}
|
||||
{{ $pubPagesTerms := where $pubPages "Kind" "term" }}
|
||||
<div id="results">
|
||||
{{ range $i, $p := $pubPagesTerms }}
|
||||
{{ partial "publication-item.html" $p }}
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Data -->
|
||||
<script id="pub-data" type="application/json">
|
||||
{{ site.Data.publications }}
|
||||
</script>
|
||||
<!-- JS + CSS -->
|
||||
<script src="/publications.js"></script>
|
||||
<link rel="stylesheet" href="/publications.css">
|
||||
59
page_templates/publication.md.j2
Normal file
59
page_templates/publication.md.j2
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
{% macro taxonomy_terms(taxonomy, terms_list) -%}
|
||||
{% if terms_list -%}
|
||||
{{ taxonomy }}:
|
||||
{% for el in terms_list -%}
|
||||
{% if el.pid -%}
|
||||
- {{ el.pid.split('/')[-1] }}
|
||||
{% endif -%}
|
||||
{% endfor -%}
|
||||
{% endif -%}
|
||||
{%- endmacro -%}
|
||||
|
||||
{%- set doi = (
|
||||
__rec.identifiers
|
||||
| selectattr('schema_type', 'equalto', 'dlthings:DOI')
|
||||
| list
|
||||
| first
|
||||
) if __rec.identifiers is defined and __rec.identifiers is sequence else none -%}
|
||||
|
||||
{%- set generation = (
|
||||
__rec.generated_by
|
||||
| selectattr('object', 'equalto', 'obo:IAO_0000444')
|
||||
| list
|
||||
| first
|
||||
) if __rec.generated_by is defined and __rec.generated_by is sequence else none -%}
|
||||
|
||||
{%- set authors = [] -%}
|
||||
{%- for auth in __rec.attributed_to -%}
|
||||
{%- set _ = authors.append({
|
||||
"pid": auth.object.pid | default(none),
|
||||
"given_name": auth.object.given_name | default(none),
|
||||
"family_name": auth.object.family_name | default(none)
|
||||
}) -%}
|
||||
{%- endfor -%}
|
||||
|
||||
{%- set topics = [] -%}
|
||||
{%- for top in __rec.about -%}
|
||||
{%- set _ = topics.append({
|
||||
"pid": top.pid | default(none),
|
||||
"display_label": top.display_label | default(none)
|
||||
}) -%}
|
||||
{%- endfor -%}
|
||||
|
||||
{%- set publication = {
|
||||
"pid": __rec.pid | default(none),
|
||||
"doi": doi.notation | default(none) if doi else none,
|
||||
"date": generation.at_time | default(none) if generation else none,
|
||||
"title": __rec.title | default(none),
|
||||
"kind": __rec.kind | default(none),
|
||||
"author": authors | default(none),
|
||||
"topic": topics | default(none)
|
||||
} -%}
|
||||
---
|
||||
title: {{ publication.title | toyaml | replace('...\n','') | trim }}
|
||||
{{ taxonomy_terms('persons', authors) -}}
|
||||
{{ taxonomy_terms('topics', topics) -}}
|
||||
params:
|
||||
graphRootNodePID: {{ pid }}
|
||||
{{ publication | toyaml | indent(2) | trim }}
|
||||
---
|
||||
|
|
@ -18,6 +18,12 @@
|
|||
max-height: calc(100vh - 4rem);
|
||||
overflow-y: auto;
|
||||
scroll-behavior: smooth;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.clear-button {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.results-panel {
|
||||
|
|
@ -49,4 +55,11 @@
|
|||
color: #292524;
|
||||
padding-left: 0.5em;
|
||||
border-radius: 3px;
|
||||
margin-bottom: 1em;
|
||||
border: 1px solid grey;
|
||||
}
|
||||
|
||||
.topic-chip.active {
|
||||
background-color: rgb(59 130 246 / 0.15);
|
||||
color: rgb(59 130 246);
|
||||
}
|
||||
|
|
@ -1,13 +1,5 @@
|
|||
// Load data
|
||||
const rawData = JSON.parse(document.getElementById("pub-data").textContent);
|
||||
|
||||
// Normalize data
|
||||
const publications = rawData.map(p => ({
|
||||
...p,
|
||||
year: p.date ? new Date(p.date).getFullYear() : 'unknown',
|
||||
topics: (p.topic || []).map(t => t.display_label),
|
||||
authors: (p.author || []).map(a => `${a.given_name} ${a.family_name}`)
|
||||
}));
|
||||
// Load data from all pre-rendered publication item divs
|
||||
const publications = Array.from(document.querySelectorAll(".pub"));
|
||||
|
||||
// State
|
||||
let state = {
|
||||
|
|
@ -20,15 +12,13 @@ let state = {
|
|||
// Build filter options dynamically
|
||||
function getUniqueValues(field) {
|
||||
const values = new Set();
|
||||
|
||||
publications.forEach(p => {
|
||||
if (Array.isArray(p[field])) {
|
||||
p[field].forEach(v => values.add(v));
|
||||
} else if (p[field]) {
|
||||
values.add(p[field]);
|
||||
publications.forEach(el => {
|
||||
if (field === "topic") {
|
||||
getTopics(el).forEach(v => values.add(v));
|
||||
} else {
|
||||
values.add(el.dataset[field]);
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(values).sort();
|
||||
}
|
||||
|
||||
|
|
@ -42,7 +32,6 @@ function formatValue(field, value) {
|
|||
return value
|
||||
}
|
||||
|
||||
// Render checkboxes for filters
|
||||
function renderFilter(containerId, field, values) {
|
||||
const container = document.getElementById(containerId);
|
||||
values.forEach(value => {
|
||||
|
|
@ -52,6 +41,8 @@ function renderFilter(containerId, field, values) {
|
|||
const checkbox = document.createElement("input");
|
||||
checkbox.type = "checkbox";
|
||||
checkbox.value = value;
|
||||
checkbox.dataset.field = field;
|
||||
checkbox.id = id;
|
||||
checkbox.addEventListener("change", () => {
|
||||
if (checkbox.checked) {
|
||||
state[field].add(value);
|
||||
|
|
@ -67,67 +58,118 @@ function renderFilter(containerId, field, values) {
|
|||
}
|
||||
|
||||
// Filtering logic
|
||||
function matchesFilters(p) {
|
||||
// Kind
|
||||
if (state.kind.size && !state.kind.has(p.kind)) return false;
|
||||
// Year
|
||||
if (state.year.size && !state.year.has(String(p.year))) return false;
|
||||
// Topic (multi-value)
|
||||
function matchesFilters(el) {
|
||||
const kind = el.dataset.kind;
|
||||
const year = el.dataset.year;
|
||||
const topics = getTopics(el);
|
||||
|
||||
// kind filter
|
||||
if (state.kind.size && !state.kind.has(kind)) {
|
||||
return false;
|
||||
}
|
||||
// year filter
|
||||
if (state.year.size && !state.year.has(year)) {
|
||||
return false;
|
||||
}
|
||||
// topic filter (multi-match)
|
||||
if (state.topic.size) {
|
||||
const match = p.topics.some(t => state.topic.has(t));
|
||||
const match = topics.some(t => state.topic.has(t));
|
||||
if (!match) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Search
|
||||
function matchesSearch(p) {
|
||||
// Helper to get topics from an element
|
||||
function getTopics(el) {
|
||||
try {
|
||||
return JSON.parse(el.dataset.topics || "[]").map(t => t.display_label);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Search logic
|
||||
function matchesSearch(el) {
|
||||
if (!state.search) return true;
|
||||
const text = (
|
||||
p.title +
|
||||
" " +
|
||||
p.kind +
|
||||
" " +
|
||||
p.topics.join(" ") +
|
||||
" " +
|
||||
p.authors.join(" ")
|
||||
el.dataset.title + " " +
|
||||
el.dataset.kind + " " +
|
||||
el.dataset.year + " " +
|
||||
el.dataset.topics + " " +
|
||||
el.dataset.authors
|
||||
).toLowerCase();
|
||||
|
||||
return text.includes(state.search);
|
||||
}
|
||||
|
||||
// Render results
|
||||
// Change display of divs based on filtering/searching
|
||||
function render() {
|
||||
const container = document.getElementById("results");
|
||||
container.innerHTML = "";
|
||||
const filtered = publications.filter(p =>
|
||||
matchesFilters(p) && matchesSearch(p)
|
||||
);
|
||||
if (filtered.length === 0) {
|
||||
container.innerHTML = "<p>No results</p>";
|
||||
return;
|
||||
}
|
||||
filtered.forEach(p => {
|
||||
const div = document.createElement("div");
|
||||
div.className = "pub";
|
||||
div.innerHTML = `
|
||||
<h3><em>${p.title} ${p.year ? ' ('+p.year+')' : ''}</em></h3>
|
||||
<p><strong>Authors:</strong> ${p.authors.join(", ")}</p>
|
||||
<p><strong>Kind:</strong> ${formatValue("kind", p.kind)}</p>
|
||||
<p><strong>Topics:</strong> ${p.topics.join(", ")}</p>
|
||||
`;
|
||||
container.appendChild(div);
|
||||
let count = 0;
|
||||
publications.forEach(el => {
|
||||
const visible =
|
||||
matchesFilters(el) &&
|
||||
matchesSearch(el);
|
||||
if (visible) count+=1;
|
||||
renderCount(count)
|
||||
el.style.display = visible ? "" : "none";
|
||||
});
|
||||
}
|
||||
|
||||
// Count of searched+filtered publications
|
||||
function renderCount(count) {
|
||||
const countEl = document.getElementById('pub-count');
|
||||
countEl.innerHTML = `${count}` ;
|
||||
}
|
||||
|
||||
// Set checkbox if user clicks on topic pill
|
||||
function selectFilter(field, value) {
|
||||
const checkbox = document.querySelector(
|
||||
`input[type="checkbox"][data-field="${field}"][value="${CSS.escape(value)}"]`
|
||||
);
|
||||
if (!checkbox) return;
|
||||
if (!checkbox.checked) {
|
||||
checkbox.checked = true;
|
||||
state[field].add(value);
|
||||
render();
|
||||
}
|
||||
}
|
||||
|
||||
// Clear all
|
||||
function clearAllFilters() {
|
||||
// Reset state
|
||||
state.kind.clear();
|
||||
state.topic.clear();
|
||||
state.year.clear();
|
||||
state.search = "";
|
||||
// Uncheck all checkboxes
|
||||
document.querySelectorAll('input[type="checkbox"]').forEach(cb => {
|
||||
cb.checked = false;
|
||||
});
|
||||
// Clear search input
|
||||
const searchInput = document.getElementById("search");
|
||||
if (searchInput) {
|
||||
searchInput.value = "";
|
||||
}
|
||||
// Re-render results
|
||||
render();
|
||||
}
|
||||
|
||||
// On startup:
|
||||
// Build filters
|
||||
// 1) Build filters
|
||||
renderFilter("filter-kind", "kind", getUniqueValues("kind"));
|
||||
renderFilter("filter-topic", "topic", getUniqueValues("topics"));
|
||||
renderFilter("filter-topic", "topic", getUniqueValues("topic"));
|
||||
renderFilter("filter-year", "year", getUniqueValues("year").map(String));
|
||||
// Search input
|
||||
// 2) Register search input
|
||||
document.getElementById("search").addEventListener("input", e => {
|
||||
state.search = e.target.value.toLowerCase();
|
||||
render();
|
||||
});
|
||||
// Initial render
|
||||
// 3) Add topic click handlers
|
||||
document.querySelectorAll(".topic-chip").forEach(el => {
|
||||
el.addEventListener("click", () => {
|
||||
const topic = el.dataset.topic;
|
||||
selectFilter("topic", topic);
|
||||
});
|
||||
});
|
||||
// 4) Initial render
|
||||
render();
|
||||
Loading…
Add table
Add a link
Reference in a new issue