Prepare frontend to filter by feed
This commit is contained in:
parent
e48eacf506
commit
31e080f25d
5 changed files with 237 additions and 95 deletions
25
view/app.py
25
view/app.py
|
@ -49,12 +49,24 @@ def websearch_to_fts_query(search: str):
|
|||
)
|
||||
|
||||
|
||||
def get_feeds():
|
||||
return [
|
||||
{
|
||||
"rss_source": str(conf["rss_source"]),
|
||||
"unique_tag": str(conf["unique_tag"]),
|
||||
"feed_name": str(conf["name"]),
|
||||
}
|
||||
for conf in config["feeds"]
|
||||
]
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
db = get_db().cursor()
|
||||
|
||||
search = request.args.get("search", type=str, default="")
|
||||
query = websearch_to_fts_query(search) if search else None
|
||||
selected_feeds = request.args.getlist("feeds[]")
|
||||
|
||||
db.execute(
|
||||
f"SELECT count(*) FROM diffs{'_fts(?)' if query else ''}",
|
||||
|
@ -70,6 +82,7 @@ def index():
|
|||
page=page, total=diff_count, record_name="diffs", css_framework="bootstrap5"
|
||||
)
|
||||
|
||||
# TODO: implement filtering by feeds
|
||||
page_skip = pagination.skip
|
||||
per_page = pagination.per_page
|
||||
if query:
|
||||
|
@ -91,6 +104,8 @@ def index():
|
|||
pagination=pagination,
|
||||
diff_count=diff_count,
|
||||
search=search,
|
||||
feeds=get_feeds(),
|
||||
selected_feeds=selected_feeds,
|
||||
)
|
||||
|
||||
res = make_response(html)
|
||||
|
@ -125,15 +140,7 @@ def about():
|
|||
|
||||
@app.route("/feeds")
|
||||
def feed_list():
|
||||
feeds = []
|
||||
for conf in config["feeds"]:
|
||||
feed = {
|
||||
"rss_source": str(conf["rss_source"]),
|
||||
"unique_tag": str(conf["unique_tag"]),
|
||||
"feed_name": str(conf["name"]),
|
||||
}
|
||||
feeds.append(feed)
|
||||
return render_template("feeds.html", feeds=feeds)
|
||||
return render_template("feeds.html", feeds=get_feeds())
|
||||
|
||||
|
||||
@app.route("/robots.txt")
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
border-color: var(--color-border);
|
||||
}
|
||||
|
||||
* {
|
||||
:not(dialog) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
|
@ -132,18 +132,36 @@
|
|||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.button {
|
||||
.button,
|
||||
.button-outline {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
gap: 0.5em;
|
||||
border-radius: 0.25rem;
|
||||
transition: background-color 120ms;
|
||||
}
|
||||
|
||||
.button {
|
||||
border: 1px solid var(--color-border);
|
||||
background: #f3f4f6;
|
||||
transition: background-color 120ms;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.button:not(:disabled):hover {
|
||||
background: hsl(0 0% 90%);
|
||||
}
|
||||
|
||||
.button-outline {
|
||||
border: 1px solid var(--color-border);
|
||||
background: white;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.button-outline:not(:disabled):hover {
|
||||
background: hsl(0 0% 95%);
|
||||
}
|
||||
|
||||
.button-md {
|
||||
font-size: 0.9em;
|
||||
line-height: 1.5rem;
|
||||
|
@ -151,14 +169,10 @@
|
|||
padding: 0.375rem 0.75rem;
|
||||
}
|
||||
|
||||
.button:not(:disabled) {
|
||||
button:not(:disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.button:not(:disabled):hover {
|
||||
background: hsl(0 0% 90%);
|
||||
}
|
||||
|
||||
/* Pagination */
|
||||
|
||||
.pagination {
|
||||
|
@ -234,19 +248,37 @@
|
|||
border-bottom-width: 1px;
|
||||
}
|
||||
|
||||
[hx-swap-oob] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid var(--color-border);
|
||||
box-shadow: var(--un-shadow-inset) 0 1px 3px 0 rgba(0,0,0,0.1),var(--un-shadow-inset) 0 1px 2px -1px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.dialog::backdrop {
|
||||
background-color: hsl(0 0% 90% / 80%);
|
||||
}
|
||||
|
||||
/* layer: shortcuts */
|
||||
.container{padding-left:1rem;padding-right:1rem;margin-left:auto;margin-right:auto;max-width:1200px;}
|
||||
.action-link{font-size:0.875rem;line-height:1.25rem;font-weight:500;--un-text-opacity:1;color:rgba(107,114,128,var(--un-text-opacity));display:inline-flex;gap:0.375rem;align-items:center;}
|
||||
.text-caption{font-size:0.875rem;line-height:1.25rem;font-weight:500;--un-text-opacity:1;color:rgba(107,114,128,var(--un-text-opacity));}
|
||||
.text-muted{--un-text-opacity:1;color:rgba(107,114,128,var(--un-text-opacity));}
|
||||
.action-link:hover{--un-text-opacity:1;color:rgba(29,78,216,var(--un-text-opacity));}
|
||||
/* layer: default */
|
||||
.p-0{padding:0;}
|
||||
.px-2{padding-left:0.5rem;padding-right:0.5rem;}
|
||||
.px-4{padding-left:1rem;padding-right:1rem;}
|
||||
.px-5{padding-left:1.25rem;padding-right:1.25rem;}
|
||||
.py-1\.5{padding-top:0.375rem;padding-bottom:0.375rem;}
|
||||
.py-3{padding-top:0.75rem;padding-bottom:0.75rem;}
|
||||
.py-4{padding-top:1rem;padding-bottom:1rem;}
|
||||
.py-6{padding-top:1.5rem;padding-bottom:1.5rem;}
|
||||
.pr-2{padding-right:0.5rem;}
|
||||
.mx-2{margin-left:0.5rem;margin-right:0.5rem;}
|
||||
.my-6{margin-top:1.5rem;margin-bottom:1.5rem;}
|
||||
.mb-2{margin-bottom:0.5rem;}
|
||||
.mb-3{margin-bottom:0.75rem;}
|
||||
|
@ -254,18 +286,24 @@
|
|||
.mb-6{margin-bottom:1.5rem;}
|
||||
.mb-8{margin-bottom:2rem;}
|
||||
.mb-auto{margin-bottom:auto;}
|
||||
.ml-2{margin-left:0.5rem;}
|
||||
.mr-2{margin-right:0.5rem;}
|
||||
.mt-12{margin-top:3rem;}
|
||||
.mt-3{margin-top:0.75rem;}
|
||||
.block{display:block;}
|
||||
.display-none,
|
||||
.hidden{display:none;}
|
||||
.bg-white{--un-bg-opacity:1;background-color:rgba(255,255,255,var(--un-bg-opacity));}
|
||||
.hover\:bg-gray-100:hover{--un-bg-opacity:1;background-color:rgba(243,244,246,var(--un-bg-opacity));}
|
||||
.fill-current{fill:currentColor;}
|
||||
.border-b{border-bottom-width:1px;}
|
||||
.rounded{border-radius:0.25rem;}
|
||||
.text-2xl{font-size:1.5rem;line-height:2rem;}
|
||||
.text-4xl{font-size:2.25rem;line-height:2.5rem;}
|
||||
.text-lg{font-size:1.125rem;line-height:1.75rem;}
|
||||
.text-sm{font-size:0.875rem;line-height:1.25rem;}
|
||||
.text-xl{font-size:1.25rem;line-height:1.75rem;}
|
||||
.text-xs{font-size:0.75rem;line-height:1rem;}
|
||||
.font-bold{font-weight:700;}
|
||||
.font-medium{font-weight:500;}
|
||||
.text-black{--un-text-opacity:1;color:rgba(0,0,0,var(--un-text-opacity));}
|
||||
|
@ -278,22 +316,25 @@
|
|||
.shrink-0{flex-shrink:0;}
|
||||
.flex-wrap{flex-wrap:wrap;}
|
||||
.gap-2{gap:0.5rem;}
|
||||
.gap-3{gap:0.75rem;}
|
||||
.gap-x-2{column-gap:0.5rem;}
|
||||
.gap-x-4{column-gap:1rem;}
|
||||
.gap-x-6{column-gap:1.5rem;}
|
||||
.gap-x-8{column-gap:2rem;}
|
||||
.gap-y-2{row-gap:0.5rem;}
|
||||
.sticky{position:sticky;}
|
||||
.static{position:static;}
|
||||
.max-w-20rem{max-width:20rem;}
|
||||
.max-w-800px{max-width:800px;}
|
||||
.w-3{width:0.75rem;}
|
||||
.w-4{width:1rem;}
|
||||
.w-full{width:100%;}
|
||||
.overflow-x-auto{overflow-x:auto;}
|
||||
.justify-between{justify-content:space-between;}
|
||||
.items-center{align-items:center;}
|
||||
.top-0{top:0;}
|
||||
.z-10{z-index:10;}
|
||||
.transition-color{transition-property:color;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:150ms;}
|
||||
.columns-2{columns:2;}
|
||||
@media (min-width: 768px){
|
||||
.md\:hidden{display:none;}
|
||||
.md\:display-table{display:table;}
|
||||
|
|
|
@ -41,3 +41,4 @@
|
|||
{% include "parts/footer.html" %}
|
||||
</body>
|
||||
</html>
|
||||
{% block top %}{% endblock %}
|
||||
|
|
|
@ -4,8 +4,16 @@
|
|||
|
||||
{% block body %}
|
||||
<div class="bg-white sticky top-0 py-3 border-b z-10 mb-6">
|
||||
<section class="container flex items-center flex-wrap shrink-0 gap-x-6 gap-y-2">
|
||||
<form method="get" class="flex gap-x-2">
|
||||
<section class="container">
|
||||
<div class="flex items-center flex-wrap shrink-0 gap-x-2 gap-y-2">
|
||||
<form
|
||||
method="get"
|
||||
class="flex gap-x-2"
|
||||
hx-trigger="change"
|
||||
hx-get="/"
|
||||
hx-target="#diff-list"
|
||||
hx-select="#diff-list"
|
||||
>
|
||||
<input
|
||||
class="text-input w-full max-w-20rem"
|
||||
type="text"
|
||||
|
@ -13,42 +21,100 @@
|
|||
name="search"
|
||||
value="{{ search|e }}"
|
||||
/>
|
||||
<button
|
||||
class="button button-md"
|
||||
type="submit"
|
||||
value="Hledat"
|
||||
>
|
||||
Hledat
|
||||
<button class="button button-md" type="submit" value="Hledat">
|
||||
Search
|
||||
<svg class="w-4 fill-current" viewBox="0 0 24 24">
|
||||
<path d="M18.031 16.6168L22.3137 20.8995L20.8995 22.3137L16.6168 18.031C15.0769 19.263 13.124 20 11 20C6.032 20 2 15.968 2 11C2 6.032 6.032 2 11 2C15.968 2 20 6.032 20 11C20 13.124 19.263 15.0769 18.031 16.6168ZM16.0247 15.8748C17.2475 14.6146 18 12.8956 18 11C18 7.1325 14.8675 4 11 4C7.1325 4 4 7.1325 4 11C4 14.8675 7.1325 18 11 18C12.8956 18 14.6146 17.2475 15.8748 16.0247L16.0247 15.8748Z"></path>
|
||||
<path
|
||||
d="M18.031 16.6168L22.3137 20.8995L20.8995 22.3137L16.6168 18.031C15.0769 19.263 13.124 20 11 20C6.032 20 2 15.968 2 11C2 6.032 6.032 2 11 2C15.968 2 20 6.032 20 11C20 13.124 19.263 15.0769 18.031 16.6168ZM16.0247 15.8748C17.2475 14.6146 18 12.8956 18 11C18 7.1325 14.8675 4 11 4C7.1325 4 4 7.1325 4 11C4 14.8675 7.1325 18 11 18C12.8956 18 14.6146 17.2475 15.8748 16.0247L16.0247 15.8748Z"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" x-model="expandDiffs" x-bind:checked="expandDiffs" />
|
||||
<button class="button-outline button-md" @click="document.querySelector('#feeds-dialog').showModal()">
|
||||
<svg class="w-4 fill-current" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M10 14L4 5V3H20V5L14 14V20L10 22V14Z"></path></svg>
|
||||
Feeds
|
||||
</button>
|
||||
|
||||
<dialog id="feeds-dialog" hx-boost="false" class="dialog">
|
||||
<header class="flex justify-between mb-6 items-center">
|
||||
<p>Filter by feed</p>
|
||||
<form method="dialog">
|
||||
<button formmethod="dialog" class="button-outline button-md">
|
||||
✕
|
||||
</button>
|
||||
</form>
|
||||
</header>
|
||||
|
||||
<form action="/" method="GET" id="feeds-filter">
|
||||
<div class="columns-2 mb-6">
|
||||
{% for feed in feeds %}
|
||||
<label
|
||||
class="flex items-center gap-3 px-2 py-1.5 hover:bg-gray-100 rounded"
|
||||
>
|
||||
<input type="checkbox" name="feeds[]" value="{{ feed['feed_name'] }}"
|
||||
{{ 'checked' if feed['feed_name'] in selected_feeds }} > {{
|
||||
feed['feed_name'] }}
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<button type="submit" class="button button-md">
|
||||
Filter diffs
|
||||
</button>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<label class="checkbox mx-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
x-model="expandDiffs"
|
||||
x-bind:checked="expandDiffs"
|
||||
/>
|
||||
<p>Expand diffs</p>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{% if selected_feeds|length > 0 %}
|
||||
<div class="text-xs font-medium text-muted flex items-center mt-3">
|
||||
<svg class="w-3 fill-current mr-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M10 14L4 5V3H20V5L14 14V20L10 22V14Z"></path></svg>
|
||||
<div>
|
||||
Feeds: <span>{{ ", ".join(selected_feeds) }}</span>
|
||||
<a href="/" class="ml-2">✕ Clear</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="container mb-8">
|
||||
<div id="diff-list">
|
||||
<section class="container mb-8">
|
||||
{% for diff in diffs %}
|
||||
<article class="card px-5 py-4 mb-4">
|
||||
<p class="flex gap-x-4 gap-y-2 mb-3 flex-wrap shrink-0">
|
||||
<span class="text-sm font-medium">{{ diff.feed_name }}</span>
|
||||
<time class="text-sm font-medium">{{ diff.diff_time }}</time>
|
||||
<a class="action-link" href="{{ diff.article_url }}">
|
||||
<svg class="w-4 fill-current" viewBox="0 0 24 24"><path d="M10 6V8H5V19H16V14H18V20C18 20.5523 17.5523 21 17 21H4C3.44772 21 3 20.5523 3 20V7C3 6.44772 3.44772 6 4 6H10ZM21 3V11H19L18.9999 6.413L11.2071 14.2071L9.79289 12.7929L17.5849 5H13V3H21Z"></path></svg>
|
||||
<svg class="w-4 fill-current" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M10 6V8H5V19H16V14H18V20C18 20.5523 17.5523 21 17 21H4C3.44772 21 3 20.5523 3 20V7C3 6.44772 3.44772 6 4 6H10ZM21 3V11H19L18.9999 6.413L11.2071 14.2071L9.79289 12.7929L17.5849 5H13V3H21Z"
|
||||
></path>
|
||||
</svg>
|
||||
Display current article
|
||||
</a>
|
||||
<a class="action-link" href="/article/{{ diff.article_id }}">
|
||||
<svg class="w-4 fill-current" viewBox="0 0 24 24"><path d="M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20ZM13 12H17V14H11V7H13V12Z"></path></svg>
|
||||
<svg class="w-4 fill-current" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20ZM13 12H17V14H11V7H13V12Z"
|
||||
></path>
|
||||
</svg>
|
||||
Show change history
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<div class="text-lg" x-bind:class="{ hidden: expandDiffs }">{{ diff.diff_html|safe }}</div>
|
||||
<div class="text-lg" x-bind:class="{ hidden: expandDiffs }">
|
||||
{{ diff.diff_html|safe }}
|
||||
</div>
|
||||
<div x-bind:class="{ hidden: !expandDiffs }">
|
||||
<div class="md:hidden">
|
||||
<div class="mb-2">
|
||||
|
@ -74,14 +140,12 @@
|
|||
</div>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<div class="container mb-3">
|
||||
{{ pagination.links }}
|
||||
<div class="overflow-x-auto">
|
||||
<div class="container mb-3">{{ pagination.links }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container text-sm text-gray-500">
|
||||
{{ pagination.info }}
|
||||
|
||||
<div class="container text-sm text-gray-500">{{ pagination.info }}</div>
|
||||
</div>
|
||||
{% endblock body %}
|
||||
|
|
|
@ -31,7 +31,7 @@ const globalCss = (theme: Required<Theme>) => css`
|
|||
border-color: var(--color-border);
|
||||
}
|
||||
|
||||
* {
|
||||
:not(dialog) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
|
@ -133,18 +133,36 @@ const globalCss = (theme: Required<Theme>) => css`
|
|||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.button {
|
||||
.button,
|
||||
.button-outline {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
gap: 0.5em;
|
||||
border-radius: ${theme.borderRadius.sm};
|
||||
transition: background-color 120ms;
|
||||
}
|
||||
|
||||
.button {
|
||||
border: 1px solid var(--color-border);
|
||||
background: ${theme.colors.gray[100]};
|
||||
transition: background-color 120ms;
|
||||
color: ${theme.colors.gray[600]};
|
||||
}
|
||||
|
||||
.button:not(:disabled):hover {
|
||||
background: hsl(0 0% 90%);
|
||||
}
|
||||
|
||||
.button-outline {
|
||||
border: 1px solid var(--color-border);
|
||||
background: white;
|
||||
color: ${theme.colors.gray[600]};
|
||||
}
|
||||
|
||||
.button-outline:not(:disabled):hover {
|
||||
background: hsl(0 0% 95%);
|
||||
}
|
||||
|
||||
.button-md {
|
||||
font-size: 0.9em;
|
||||
line-height: 1.5rem;
|
||||
|
@ -152,14 +170,10 @@ const globalCss = (theme: Required<Theme>) => css`
|
|||
padding: 0.375rem 0.75rem;
|
||||
}
|
||||
|
||||
.button:not(:disabled) {
|
||||
button:not(:disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.button:not(:disabled):hover {
|
||||
background: hsl(0 0% 90%);
|
||||
}
|
||||
|
||||
/* Pagination */
|
||||
|
||||
.pagination {
|
||||
|
@ -234,6 +248,20 @@ const globalCss = (theme: Required<Theme>) => css`
|
|||
.table-styled thead tr {
|
||||
border-bottom-width: 1px;
|
||||
}
|
||||
|
||||
[hx-swap-oob] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
border-radius: ${theme.borderRadius["md"]};
|
||||
border: 1px solid var(--color-border);
|
||||
box-shadow: ${theme.boxShadow.DEFAULT};
|
||||
}
|
||||
|
||||
.dialog::backdrop {
|
||||
background-color: hsl(0 0% 90% / 80%);
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
|
@ -261,6 +289,7 @@ export default defineConfig({
|
|||
md: "0.5rem",
|
||||
},
|
||||
},
|
||||
rules: [[/^columns-(\d+)$/, ([_, num]) => ({ columns: num })]],
|
||||
shortcuts: {
|
||||
container: "max-w-1200px px-4 mx-auto",
|
||||
"action-link":
|
||||
|
|
Loading…
Reference in a new issue