Finished user interface. Added mobile-ish support. Added homepage. Added viewpage. Fixed some scrapers

This commit is contained in:
Jurn Wubben 2025-06-25 11:35:13 +02:00
parent 89118c6d1d
commit 42f40ad9a0
8 changed files with 433 additions and 191 deletions

View file

@ -1,125 +1,263 @@
{% set cpath = url_for("edit", id=wishlist.editId) %}
{% extends "base.html" %}
{% block middleNav %}
<a class="btn btn-soft w-40 hidden lg:inline-flex" href="/view/{{ wishlist.viewId }}">View wishlist</a>
{% endblock middleNav %}
{% block middleNavPhone %}
<li><a class="btn btn-soft w-40" href="/view/{{ wishlist.viewId }}">View wishlist</a></li>
{% endblock middleNavPhone %}
<li><a class="btn btn-soft w-40" href="/view/{{ wishlist.viewId }}">View wishlist</a></li>
{% block content %}
<main class="bg-base-100">
<h1>Edit '{{wishlist.title}}'</h1>
<sub>Manage your wishlist details and items</sub>
<br>
<form action="{{ cpath }}" method="POST">
{{ form_wl_editinfo.hidden_tag() }}
{{ form_wl_editinfo.title.label }}
{{ form_wl_editinfo.title(placeholder=wishlist.title) }}
<!-- <br> -->
{{ form_wl_editinfo.description.label }}
{{ form_wl_editinfo.description(placeholder=wishlist.description) }}
<!-- <br> -->
{{ form_wl_editinfo.wl_edit_submit() }}
</form>
<br>
<h1>Urls</h1>
<ul>
<main class="flex flex-col justify-end sm:justify-center items-center h-screen w-full">
<div class="flex flex-col-reverse sm:flex-row rounded-lg border-2 border-base-300 bg-base-100 p-6 shadow-lg w-[34em] max-h-[35em] sm:w-[40em] md:w-[45em] scale-65 sm:scale-90 md:scale-100 ">
<ul class="menu menu-horizontal sm:menu-vertical w-full sm:w-min rounded-box bg-base-200 flex-none gap-1" id="menu">
<li>
View: <a href={{ url_for("view", id=wishlist.viewId) }}>{{ wishlist.viewId }}</a>
<a class="tooltip tooltip-top sm:tooltip-right" data-tip="Info">
<svg xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</a>
</li>
<li>
Edit: <a href={{ url_for("edit", id=wishlist.editId) }}>{{ wishlist.editId }}</a>
<a class="tooltip tooltip-top sm:tooltip-right" data-tip="Urls">
<svg xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
class="w-5 h-5">
<path fill="currentColor" d="M11 17H7q-2.075 0-3.537-1.463T2 12t1.463-3.537T7 7h4v2H7q-1.25 0-2.125.875T4 12t.875 2.125T7 15h4zm-3-4v-2h8v2zm5 4v-2h4q1.25 0 2.125-.875T20 12t-.875-2.125T17 9h-4V7h4q2.075 0 3.538 1.463T22 12t-1.463 3.538T17 17z" />
</svg>
</a>
</li>
</ul>
<form action="{{ cpath }}" method="POST">
{{ form_wl_reseturls.hidden_tag() }}
{{ form_wl_reseturls.wl_reset_submit() }}
</form>
<br>
<h1>New item</h1>
<form action="{{ cpath }}" method="POST">
{{ form_it_new.hidden_tag() }}
<!-- <br> -->
{{ form_it_new.it_new_title.label }}
{{ form_it_new.it_new_title() }}
<!-- <br> -->
{{ form_it_new.description.label }}
{{ form_it_new.description() }}
<!-- <br> -->
{{ form_it_new.price.label }}
{{ form_it_new.price() }}
<!-- <br> -->
{{ form_it_new.url.label }}
{{ form_it_new.url() }}
<!-- <br> -->
{{ form_it_new.image.label }}
{{ form_it_new.image() }}
<!-- <br> -->
{{ form_it_new.it_new_submit() }}
<button id="scrape">Scrape</button>
</form>
<br>
<h1>Delete items</h1>
{% if wishlist.items|length == 0 %}<p>No items yet</p>{% endif %}
<ul>
{% for value in wishlist.items %}
<li>
<form action="{{ cpath }}" method="POST">
{{ form_it_delete.csrf_token }}
{{ form_it_delete.index(value=loop.index) }}
{{ form_it_delete.it_del_submit() }}
</form>
{{ value.title }}
<li>
<a class="tooltip tooltip-top sm:tooltip-right" data-tip="Wishes">
<svg xmlns="http://www.w3.org/2000/svg"
class="w-5 h-5"
viewBox="0 0 24 24">
<path fill="currentColor" d="M4 22V11H2V5h5.2q-.125-.225-.162-.475T7 4q0-1.25.875-2.125T10 1q.575 0 1.075.213T12 1.8q.425-.4.925-.6T14 1q1.25 0 2.125.875T17 4q0 .275-.05.513T16.8 5H22v6h-2v11zM14 3q-.425 0-.712.288T13 4t.288.713T14 5t.713-.288T15 4t-.288-.712T14 3M9 4q0 .425.288.713T10 5t.713-.288T11 4t-.288-.712T10 3t-.712.288T9 4M4 7v2h7V7zm7 13v-9H6v9zm2 0h5v-9h-5zm7-11V7h-7v2z" />
</svg>
</a>
</li>
</ul>
<div class="divider divider-vertical sm:divider-horizontal"></div>
<div class="flex-grow w-full overflow-y-scroll p-2" id="tabs">
<div class="contents">
<!-- INFO -->
<h1 class="text-2xl">Info</h1>
<form action="{{ cpath }}" method="POST">
<!-- {{ form_wl_editinfo.hidden_tag() }} -->
<legend class="fieldset-legend">{{ form_wl_editinfo.title.label.text }}</legend>
{{ form_wl_editinfo.title(placeholder=wishlist.title, class="w-full
input validator mt-1 mb-4") }}
<!-- <br> -->
<legend class="fieldset-legend">{{ form_wl_editinfo.description.label.text }}</legend>
{{ form_wl_editinfo.description(placeholder=wishlist.description,
class="w-full textarea validator mt-1 mb-2") }}
<div class="validator-hint">Please make sure that both inputs are filled.</div>
<!-- <br> -->
<div class="join">
<form action="{{ cpath }}" method="POST" class="contents">
{{ form_wl_delete.hidden_tag() }} {{ form_wl_delete.wl_del_submit(class="btn btn-soft btn-error
join-item w-full") }}
</form>
{{ form_wl_editinfo.wl_edit_submit(class="btn btn-soft btn-success
join-item w-full") }}
</div>
</form>
</div>
<div class="contents">
<!-- URLS -->
<h1 class="text-2xl">Urls</h1>
<ul class="list shadow-sm rounded-md my-4">
<li class="list-row flex items-center">
<svg xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 48 48"
class="opacity-30 size-[1.8em] flex-none">
<defs>
<mask id="ipSPreviewOpen0">
<g fill="none" stroke-linejoin="round" stroke-width="4">
<path fill="#fff" stroke="#fff" d="M24 36c11.046 0 20-12 20-12s-8.954-12-20-12S4 24 4 24s8.954 12 20 12Z" />
<path fill="#000" stroke="#000" d="M24 29a5 5 0 1 0 0-10a5 5 0 0 0 0 10Z" />
</g>
</mask>
</defs>
<path fill="currentColor" d="M0 0h48v48H0z" mask="url(#ipSPreviewOpen0)" />
</svg>
<p class="flex-grow">{{ wishlist.viewId }}</p>
<a class="btn btn-square btn-ghost flex-none"
href="{{ url_for('view', id=wishlist.viewId) }}">
<svg xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
class="opacity-30 size-[1.8em]">
<path fill="currentColor" d="M5 21q-.825 0-1.412-.587T3 19V5q0-.825.588-1.412T5 3h6q.425 0 .713.288T12 4t-.288.713T11 5H5v14h14v-6q0-.425.288-.712T20 12t.713.288T21 13v6q0 .825-.587 1.413T19 21zM19 6.4L10.4 15q-.275.275-.7.275T9 15t-.275-.7t.275-.7L17.6 5H15q-.425 0-.712-.288T14 4t.288-.712T15 3h5q.425 0 .713.288T21 4v5q0 .425-.288.713T20 10t-.712-.288T19 9z" />
</svg>
</a>
</li>
{% endfor %}
</ul>
<br>
<h1>Delete wishlist</h1>
<form action="{{ cpath }}" method="POST">
{{ form_wl_delete.hidden_tag() }}
{{ form_wl_delete.wl_del_submit() }}
</form>
<style>
form {
display:grid;
grid-template-columns: max-content max-content;
grid-gap:5px;
}
form label { text-align:right; }
form label:after { content: ":"; }
</style>
<li class="list-row flex items-center">
<svg xmlns="http://www.w3.org/2000/svg"
width="25"
height="25"
viewBox="0 0 24 24"
class="opacity-30 size-[1.8em] flex-none">
<path fill="currentColor" d="M5 19h1.425L16.2 9.225L14.775 7.8L5 17.575zm-2 2v-4.25L16.2 3.575q.3-.275.663-.425t.762-.15t.775.15t.65.45L20.425 5q.3.275.438.65T21 6.4q0 .4-.137.763t-.438.662L7.25 21zM19 6.4L17.6 5zm-3.525 2.125l-.7-.725L16.2 9.225z" />
</svg>
<p class="flex-grow">{{ wishlist.editId }}</p>
<a class="btn btn-square btn-ghost flex-none"
href='{{ url_for("edit", id=wishlist.editId) }}'>
<svg xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
class="opacity-30 size-[1.8em]">
<path fill="currentColor" d="M5 21q-.825 0-1.412-.587T3 19V5q0-.825.588-1.412T5 3h6q.425 0 .713.288T12 4t-.288.713T11 5H5v14h14v-6q0-.425.288-.712T20 12t.713.288T21 13v6q0 .825-.587 1.413T19 21zM19 6.4L10.4 15q-.275.275-.7.275T9 15t-.275-.7t.275-.7L17.6 5H15q-.425 0-.712-.288T14 4t.288-.712T15 3h5q.425 0 .713.288T21 4v5q0 .425-.288.713T20 10t-.712-.288T19 9z" />
</svg>
</a>
</li>
</ul>
<form action="{{ cpath }}" method="POST">
{{ form_wl_reseturls.hidden_tag() }} {{ form_wl_reseturls.wl_reset_submit(class="btn btn-soft btn-warning")
}}
</form>
</div>
<div class="contents">
<!-- ITEMS -->
<h1 class="text-xl mb-2">Add/Delete items</h1>
<ul class="list bg-base-100 rounded-md shadow-sm">
<li class="opacity-60 list-row">
<span class="flex flex-row items-center font-bold">Create item</span>
<span></span>
<span></span>
<button type="submit"
class="btn btn-square btn-ghost group"
onclick="add_item_modal.showModal()">
<svg class="size-[1.8em] group-hover:fill-white"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24">
<path fill="currentColor" d="M11 13v3q0 .425.288.713T12 17t.713-.288T13 16v-3h3q.425 0 .713-.288T17 12t-.288-.712T16 11h-3V8q0-.425-.288-.712T12 7t-.712.288T11 8v3H8q-.425 0-.712.288T7 12t.288.713T8 13zm-6 8q-.825 0-1.412-.587T3 19V5q0-.825.588-1.412T5 3h14q.825 0 1.413.588T21 5v14q0 .825-.587 1.413T19 21zm0-2h14V5H5zM5 5v14z" />
</svg>
</button>
</li>
{% for value in wishlist.items %}
<li class="list-row bg-base-100">
<div>
<img class="size-10 rounded-box" src="{{ value.image }}" />
</div>
<div class="text-lg">{{ value.title }}</div>
<p class="list-col-wrap text-xs">{{ value.description }}</p>
<form action="{{ cpath }}" method="POST" class="contents">
{{ form_it_delete.csrf_token }}
{{ form_it_delete.index(value=loop.index) }}
<button type="submit" class="btn btn-square btn-ghost btn-error group">
<svg class="size-[1.8em] group-hover:fill-white"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24">
<path d="m9.4 16.5l2.6-2.6l2.6 2.6l1.4-1.4l-2.6-2.6L16 9.9l-1.4-1.4l-2.6 2.6l-2.6-2.6L8 9.9l2.6 2.6L8 15.1zM7 21q-.825 0-1.412-.587T5 19V6H4V4h5V3h6v1h5v2h-1v13q0 .825-.587 1.413T17 21zM17 6H7v13h10zM7 6v13z" />
</svg>
</button>
</form>
</li>
{% endfor %}
</ul>
</div>
</div>
</div>
<span></span>
</main>
<dialog id="add_item_modal" class="modal">
<div class="modal-box">
<form method="dialog">
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" id="close"></button>
</form>
<h1 class="text-xl font-bold">
Item creation
</h3>
<form action="{{ cpath }}" method="POST">
{{ form_it_new.hidden_tag() }}
<legend class="fieldset-legend">{{ form_it_new.it_new_title.label.text }}</legend>
{{ form_it_new.it_new_title(class="w-full input validator mt-1 mb-4") }}
<!-- <br> -->
<legend class="fieldset-legend">{{ form_it_new.description.label.text }}</legend>
{{ form_it_new.description(class="w-full input validator mt-1 mb-2") }}
<legend class="fieldset-legend">{{ form_it_new.price.label.text }}</legend>
{{ form_it_new.price(class="w-full input validator mt-1 mb-2", pattern="(?:€|$)?\d*(?:,|.)\d*") }}
<legend class="fieldset-legend">{{ form_it_new.url.label.text }}</legend>
{{ form_it_new.url(class="w-full input validator mt-1 mb-2") }}
<legend class="fieldset-legend">{{ form_it_new.image.label.text }}</legend>
{{ form_it_new.image(class="w-full input validator mt-1 mb-2") }}
<div class="validator-hint">Please make sure that both inputs are filled and are valid.</div>
<div class="join modal-actions">
<button class="btn btn-soft join-item sm:w-full" id="scrape">Autofill</button>
{{ form_it_new.it_new_submit(class="btn btn-soft btn-success
join-item sm:w-full") }}
</div>
</form>
</div>
</dialog>
<script>
const $q = (...i) => document.querySelector(...i);
const title = $q("#it_new_title")
const price = $q("#price")
const url = $q("#url")
const image = $q("#image")
// const description = $q("#description")
const $all = (...v) => document.querySelectorAll(...v)
const menu = $all("#menu > li > a")
const tabs = $all("#tabs > div")
$q("#scrape").addEventListener("click", async e => {
e.preventDefault()
const tUrl = url.value.trim();
const def = 0
let activeMenu = menu[def]
let activeTab = tabs[def]
if (!tUrl) {
alert("Please provide a valid url.") //TODO: Replace with daisyui modal
return
activeMenu.classList.add("menu-active")
for (let i of Array.from(tabs).filter((_, i) => i != def)) i.classList.add("hidden")
for (let [index, elem] of Object.entries(menu)) {
index = +index
elem.addEventListener("click", e => {
activeMenu.classList.remove("menu-active")
elem.classList.add("menu-active")
activeTab.classList.add("hidden")
tabs[index].classList.remove("hidden")
activeMenu = elem
activeTab = tabs[index]
})
}
const res = await fetch(
"/scrape?" + new URLSearchParams({
url: tUrl
}).toString(),
{
method: "get",
}
)
const $q = (...i) => document.querySelector(...i);
const title = $q("#it_new_title")
const price = $q("#price")
const url = $q("#url")
const image = $q("#image")
// const description = $q("#description")
if (res.status !== 200) {
alert("Failed to scrape site.")
return
}
$q("#scrape").addEventListener("click", async e => {
e.preventDefault()
const tUrl = url.value.trim();
const json = await res.json()
title.value = json.name;
image.value = json.image;
price.value = json.price;
})
if (!tUrl) {
alert("Please provide a valid url.") //TODO: Replace with daisyui modal
return
}
const res = await fetch(
"/scrape?" + new URLSearchParams({
url: tUrl
}).toString(), {
method: "get",
}
)
if (res.status !== 200) {
alert("Failed to scrape site.")
return
}
const json = await res.json()
title.value = json.name;
image.value = json.image;
price.value = json.price;
})
$q("#close").addEventListener("click", e => {
$all("dialog > div > form > input").forEach(v => v.value = "")
})
</script>
{% endblock content %}