forked from enviPath/enviPy
This PR moves all the collection pages into the new UI in a rough push. I did not put the same amount of care into these as into search, index, and predict. ## Major changes - All modals are now migrated to a state based alpine.js implementation. - jQuery is no longer present in the base layout; ajax is replace by native fetch api - most of the pps.js is now obsolte (as I understand it; the code is not referenced any more @jebus please double check) - in-memory pagination for large result lists (set to 50; we can make that configurable later; performance degrades at around 1k) stukk a bit rough tracked in #235 ## Minor things - Sarch and index also use alpine now - The loading spinner is now CSS animated (not sure if it currently gets correctly called) ## Not done - Ihave not even cheked the admin pages. Not sure If these need migrations - The temporary migration pages still use the old template. Not sure what is supposed to happen with those? @jebus ## What I did to test - opend all pages in browse, and user ; plus all pages reachable from there. - Interacted and tested the functionality of each modal superfically with exception of the API key modal (no functional test). --- This PR is massive sorry for that; just did not want to push half-brokenn state. @jebus @liambrydon I would be glad if you could click around and try to break it :) Finally closes #133 Co-authored-by: Tim Lorsbach <tim@lorsba.ch> Reviewed-on: enviPath/enviPy#236 Co-authored-by: Tobias O <tobias.olenyi@envipath.com> Co-committed-by: Tobias O <tobias.olenyi@envipath.com>
425 lines
17 KiB
HTML
425 lines
17 KiB
HTML
{% extends "framework_modern.html" %}
|
|
{% load static %}
|
|
{% block main_content %}
|
|
<!-- Hero Section with Logo and Search -->
|
|
<section class="hero relative mx-auto h-fit w-full max-w-5xl shadow-none">
|
|
<div
|
|
class="hero from-primary-800 to-primary-600 min-h-[calc(100vh*0.4)] bg-gradient-to-br"
|
|
style="background-image: url('{% static "/images/hero.png" %}'); background-size: cover; background-position: center;"
|
|
>
|
|
<div class="hero-overlay"></div>
|
|
<!-- Predict Pathway text over the image -->
|
|
<div class="absolute bottom-40 left-1/8 -translate-x-8">
|
|
<h2 class="text-base-100 text-left text-3xl text-shadow-lg">
|
|
Predict Your Pathway
|
|
</h2>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<div class="bg-base-200 mx-auto max-w-5xl shadow-md">
|
|
<!-- Predict Pathway Section -->
|
|
<div
|
|
class="relative mx-auto -mt-32 mb-10 w-full flex-col lg:flex-row-reverse"
|
|
>
|
|
<div
|
|
class="card bg-base-100 mx-auto w-3/4 shrink-0 shadow-xl transition-all duration-300 ease-in-out"
|
|
x-data="{
|
|
drawMode: false,
|
|
smiles: '',
|
|
loadExample(smilesStr, linkEl) {
|
|
if (this.drawMode && window.indexKetcher && window.indexKetcher.setMolecule) {
|
|
window.indexKetcher.setMolecule(smilesStr);
|
|
} else {
|
|
this.smiles = smilesStr;
|
|
}
|
|
const original = linkEl.textContent;
|
|
linkEl.textContent = 'loaded!';
|
|
setTimeout(() => linkEl.textContent = original, 1000);
|
|
},
|
|
syncFromKetcher() {
|
|
const ketcher = getKetcherInstance('index-ketcher');
|
|
if (ketcher && ketcher.getSmiles) {
|
|
try {
|
|
const s = ketcher.getSmiles();
|
|
if (s && s.trim()) this.smiles = s;
|
|
} catch (err) {
|
|
console.error('Failed to sync from Ketcher:', err);
|
|
}
|
|
}
|
|
},
|
|
submitForm() {
|
|
let finalSmiles = '';
|
|
if (this.drawMode) {
|
|
const ketcher = getKetcherInstance('index-ketcher');
|
|
if (ketcher && ketcher.getSmiles) {
|
|
try {
|
|
finalSmiles = ketcher.getSmiles().trim();
|
|
} catch (err) {
|
|
console.error('Failed to get SMILES from Ketcher:', err);
|
|
alert('Unable to extract structure. Please try again or switch to SMILES input.');
|
|
return;
|
|
}
|
|
} else {
|
|
alert('The drawing editor is still loading. Please wait and try again.');
|
|
return;
|
|
}
|
|
} else {
|
|
finalSmiles = this.smiles.trim();
|
|
}
|
|
if (!finalSmiles) {
|
|
alert('Please enter a SMILES string or draw a structure.');
|
|
return;
|
|
}
|
|
document.getElementById('index-form-smiles').value = finalSmiles;
|
|
document.getElementById('index-form').submit();
|
|
}
|
|
}"
|
|
x-init="$watch('drawMode', value => { if (!value) syncFromKetcher(); })"
|
|
>
|
|
<div class="card-body">
|
|
<div class="my-4 ml-8 flex h-fit flex-row items-center justify-start">
|
|
<div class="flex items-center gap-1">
|
|
<label class="swap btn btn-ghost btn-sm p-1" title="Input Mode">
|
|
<input type="checkbox" x-model="drawMode" />
|
|
<span class="swap-on flex items-center gap-1">
|
|
<div
|
|
class="bg-neutral/50 text-neutral-content flex items-center justify-center rounded-full p-1"
|
|
>
|
|
<svg
|
|
aria-label="smiles mode"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 20 20"
|
|
class="size-5"
|
|
>
|
|
<g
|
|
stroke-linejoin="round"
|
|
stroke-linecap="round"
|
|
stroke-width="2"
|
|
fill="currentColor"
|
|
stroke="none"
|
|
>
|
|
<path
|
|
fill-rule="evenodd"
|
|
d="M8 2.75A.75.75 0 0 1 8.75 2h7.5a.75.75 0 0 1 0 1.5h-3.215l-4.483 13h2.698a.75.75 0 0 1 0 1.5h-7.5a.75.75 0 0 1 0-1.5h3.215l4.483-13H8.75A.75.75 0 0 1 8 2.75Z"
|
|
clip-rule="evenodd"
|
|
/>
|
|
</g>
|
|
</svg>
|
|
</div>
|
|
<span class="ext-xs">SMILES</span>
|
|
</span>
|
|
<span class="swap-off flex items-center gap-1">
|
|
<div
|
|
class="bg-neutral/50 text-neutral-content flex items-center justify-center rounded-full p-1"
|
|
>
|
|
<svg
|
|
aria-label="draw mode"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 20 20"
|
|
fill="currentColor"
|
|
stroke="none"
|
|
class="size-5"
|
|
>
|
|
<path
|
|
d="m2.695 14.762-1.262 3.155a.5.5 0 0 0 .65.65l3.155-1.262a4 4 0 0 0 1.343-.886L17.5 5.501a2.121 2.121 0 0 0-3-3L3.58 13.419a4 4 0 0 0-.885 1.343Z"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
<span class="text-base/50 text-xs">Draw</span>
|
|
</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<fieldset
|
|
class="fieldset overflow-hidden transition-all duration-300 ease-in-out"
|
|
:class="drawMode ? 'p-4' : 'p-8'"
|
|
>
|
|
<form
|
|
id="index-form"
|
|
action="{{ meta.current_package.url }}/pathway"
|
|
method="POST"
|
|
@submit.prevent="submitForm()"
|
|
>
|
|
{% csrf_token %}
|
|
<div
|
|
id="text-input-container"
|
|
x-show="!drawMode"
|
|
x-transition:enter="transition ease-out duration-300"
|
|
x-transition:enter-start="opacity-0 scale-95"
|
|
x-transition:enter-end="opacity-100 scale-100"
|
|
x-transition:leave="transition ease-in duration-300"
|
|
x-transition:leave-start="opacity-100 scale-100"
|
|
x-transition:leave-end="opacity-0 scale-95"
|
|
>
|
|
<div class="join mx-auto w-full">
|
|
<input
|
|
type="text"
|
|
id="index-form-text-input"
|
|
placeholder="canonical SMILES string"
|
|
class="input input-md join-item grow"
|
|
x-model="smiles"
|
|
/>
|
|
<button class="btn btn-neutral join-item">Predict!</button>
|
|
</div>
|
|
<div class="label relative mt-1 w-full">
|
|
<div class="flex gap-2">
|
|
<a
|
|
href="#"
|
|
class="example-link hover:text-primary cursor-pointer"
|
|
title="load example"
|
|
@click.prevent="loadExample('CN1C=NC2=C1C(=O)N(C(=O)N2C)C', $el)"
|
|
>Caffeine</a
|
|
>
|
|
<a
|
|
href="#"
|
|
class="example-link hover:text-primary cursor-pointer"
|
|
title="load example"
|
|
@click.prevent="loadExample('CC(C)CC1=CC=C(C=C1)C(C)C(=O)O', $el)"
|
|
>Ibuprofen</a
|
|
>
|
|
</div>
|
|
<a
|
|
class="absolute top-0 left-[calc(100%-5.4rem)]"
|
|
href="/predict"
|
|
>Advanced</a
|
|
>
|
|
</div>
|
|
</div>
|
|
<div
|
|
id="ketcher-container"
|
|
x-show="drawMode"
|
|
x-transition:enter="transition ease-out duration-300"
|
|
x-transition:enter-start="opacity-0 scale-95"
|
|
x-transition:enter-end="opacity-100 scale-100"
|
|
x-transition:leave="transition ease-in duration-300"
|
|
x-transition:leave-start="opacity-100 scale-100"
|
|
x-transition:leave-end="opacity-0 scale-95"
|
|
class="w-full"
|
|
>
|
|
<iframe
|
|
id="index-ketcher"
|
|
src="{% static '/js/ketcher2/ketcher.html' %}"
|
|
width="100%"
|
|
height="400"
|
|
class="rounded-lg"
|
|
></iframe>
|
|
<button
|
|
class="btn btn-lg bg-primary-950 text-primary-50 join-item mt-2 w-full"
|
|
>
|
|
Predict!
|
|
</button>
|
|
<div class="mt-1 flex w-full justify-end">
|
|
<a class="label justify-end" href="/predict">Advanced</a>
|
|
</div>
|
|
</div>
|
|
<input
|
|
type="hidden"
|
|
id="index-form-smiles"
|
|
name="smiles"
|
|
value="smiles"
|
|
/>
|
|
<input
|
|
type="hidden"
|
|
id="index-form-predict"
|
|
name="predict"
|
|
value="predict"
|
|
/>
|
|
<input type="hidden" id="current-action" value="predict" />
|
|
</form>
|
|
</fieldset>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Community News Section -->
|
|
<section class="bg-base-200 z-10 mx-8 py-16">
|
|
<div class="mx-auto max-w-7xl px-4">
|
|
<h2 class="h2 mb-8 text-left font-bold">Community Updates</h2>
|
|
|
|
<div id="community-news-container" class="flex justify-center gap-4">
|
|
<!-- News cards will be populated here -->
|
|
<div id="loading" class="flex w-full justify-center">
|
|
<span class="loading loading-spinner loading-lg"></span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-6 text-right">
|
|
<a
|
|
href="https://community.envipath.org/c/announcements/10"
|
|
target="_blank"
|
|
class="btn btn-ghost btn-sm"
|
|
>
|
|
Read More Announcements
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Discourse API integration -->
|
|
<script src="{% static 'js/discourse-api.js' %}"></script>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Mission Statement Section -->
|
|
<section class="from-base-200 to-base-100 bg-gradient-to-b py-16">
|
|
<div class="mx-auto px-8 md:px-12">
|
|
<div class="flex flex-row gap-4">
|
|
<div class="w-1/3">
|
|
<img
|
|
src="{% static "/images/ep-rule-artwork.png" %}"
|
|
alt="rule-based iterative tree greneration"
|
|
class="h-full w-full object-contain"
|
|
/>
|
|
</div>
|
|
<div class="mr-8 w-2/3 space-y-4 text-left">
|
|
<h2 class="h2 mb-8 font-bold">About enviPath</h2>
|
|
<p class="">
|
|
enviPath is a database and prediction system for the microbial
|
|
biotransformation of organic environmental contaminants. The
|
|
database provides the possibility to store and view experimentally
|
|
observed biotransformation pathways.
|
|
</p>
|
|
<p class="">
|
|
The pathway prediction system provides different relative
|
|
reasoning models to predict likely biotransformation pathways and
|
|
products. Explore our tools and contribute to advancing
|
|
environmental biotransformation research.
|
|
</p>
|
|
<div class="float-right flex flex-row gap-4">
|
|
<a href="/about" class="btn btn-ghost-neutral">Read More</a>
|
|
<a href="/about" class="btn btn-neutral">Publications</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Partners Section -->
|
|
<section class="bg-base-100 py-14 sm:py-12">
|
|
<div class="mx-auto px-6 lg:px-8">
|
|
<div class="divider">
|
|
<h2 class="text-center text-lg/8 font-semibold">Backed by Science</h2>
|
|
</div>
|
|
<div
|
|
class="mx-auto mt-10 grid max-w-lg grid-cols-4 items-center gap-x-8 gap-y-10 sm:max-w-xl sm:grid-cols-6 sm:gap-x-10 lg:mx-0 lg:max-w-none lg:grid-cols-3"
|
|
>
|
|
<img
|
|
src="{% static "/images/uoa-logo-small.png" %}"
|
|
alt="The University of Auckland"
|
|
class="max-h-20 w-full object-contain lg:col-span-1"
|
|
/>
|
|
<img
|
|
src="{% static "/images/logo-eawag.svg" %}"
|
|
alt="Eawag"
|
|
class="max-h-12 w-full object-contain lg:col-span-1"
|
|
/>
|
|
<img
|
|
src="{% static "/images/uzh-logo.svg" %}"
|
|
alt="University of Zurich"
|
|
class="2 max-h-16 w-full object-contain lg:col-span-1"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
|
|
<script language="javascript">
|
|
var currentPackage = "{{ meta.current_package.url }}";
|
|
|
|
// Helper function to safely get Ketcher instance from iframe
|
|
function getKetcherInstance(iframeId) {
|
|
const ketcherFrame = document.getElementById(iframeId);
|
|
if (!ketcherFrame) {
|
|
console.error("Ketcher iframe not found:", iframeId);
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
if (
|
|
"contentWindow" in ketcherFrame &&
|
|
ketcherFrame.contentWindow.ketcher
|
|
) {
|
|
return ketcherFrame.contentWindow.ketcher;
|
|
}
|
|
} catch (err) {
|
|
console.error(
|
|
"Cannot access Ketcher iframe - possible CORS issue:",
|
|
err,
|
|
);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// Discourse API integration is now handled by discourse-api.js
|
|
|
|
// Function to render Discourse topics into cards
|
|
function renderDiscourseTopics(topics) {
|
|
const container = document.getElementById("community-news-container");
|
|
if (!container) return;
|
|
|
|
// Clear container
|
|
container.innerHTML = "";
|
|
|
|
// Create cards for each topic
|
|
topics.forEach((topic) => {
|
|
const card = createDiscourseCard(topic);
|
|
container.insertAdjacentHTML("beforeend", card);
|
|
});
|
|
}
|
|
|
|
// Function to create HTML card for a topic
|
|
function createDiscourseCard(topic) {
|
|
const date = new Date(topic.created_at).toLocaleDateString();
|
|
|
|
return `
|
|
<div class="card bg-white shadow-sm hover:shadow-lg transition-shadow duration-300 h-52 w-75 flex-shrink-0">
|
|
<div class="card-body flex flex-col h-full justify-between">
|
|
<h3 class="card-title leading-tight font-normal tracking-tight mb-2 line-clamp-5 overflow-hidden">
|
|
<a href="${topic.url}" target="_blank" class="hover:text-primary">
|
|
${topic.title}
|
|
</a>
|
|
</h3>
|
|
|
|
<div class="flex flex-row items-center justify-between">
|
|
<div class="flex items-center gap-2">
|
|
<div class="avatar tooltip tooltip-right" data-tip="${topic.author}">
|
|
<div class="w-8 rounded-full">
|
|
<img src="${topic.author_avatar}" alt="${topic.author}" />
|
|
</div>
|
|
</div>
|
|
<span class="text-xs text-gray-500">${date}</span>
|
|
</div>
|
|
<a href="${topic.url}" target="_blank" class="btn btn-ghost text-neutral-500 rounded-full p-2">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M12 7v14"/>
|
|
<path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/>
|
|
</svg>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Make render function globally available
|
|
window.renderDiscourseTopics = renderDiscourseTopics;
|
|
|
|
// Ketcher iframe load handler - set up change event to sync SMILES
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
const indexKetcher = document.getElementById("index-ketcher");
|
|
indexKetcher.addEventListener("load", function () {
|
|
const checkKetcherReady = () => {
|
|
const win = this.contentWindow;
|
|
if (win.ketcher && "editor" in win.ketcher) {
|
|
window.indexKetcher = win.ketcher;
|
|
} else {
|
|
setTimeout(checkKetcherReady, 100);
|
|
}
|
|
};
|
|
checkKetcherReady();
|
|
});
|
|
});
|
|
</script>
|
|
{% endblock main_content %}
|