feat: Introduce pyproject.toml for build configuration

Migrated project dependencies and metadata into a newly added `pyproject.toml`, aligning with modern Python packaging practices. Removed `requirements.txt` as dependencies are now specified in the unified project configuration. Restructured the project by moving Python code, static assets, and templates into a `src/structables` directory to encapsulate project components better and facilitate packaging. Moreover, refactor `main.py` to read environment variables earlier, streamline argument parsing, and ensure configurations are correctly applied before the Flask app initialization. This reorganization supports better project scalability, simplifies the build process, and enhances maintainability by consolidating project configurations and dependencies.

Note: The usage of `pyproject.toml` requires tools that support PEP 518 and might necessitate updates to CI/CD pipelines or developer workflows.
This commit is contained in:
Kumi 2024-05-23 08:06:36 +02:00
parent 25a07d797b
commit 85d5c88294
Signed by: kumi
GPG key ID: ECBCC9082395383F
34 changed files with 108 additions and 60 deletions

View file

1282
src/structables/main.py Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,323 @@
:root {
--main-font: "DejaVu Sans Mono", monospace;
--main-color: #bbc2cf;
--link-color: #ff6c6b;
--heading-color: #51afef;
--code-bg: #20232a;
--code-color: #969ba6;
--border-color: lightgrey;
}
body {
font-family: var(--main-font);
margin: 20px auto;
line-height: 1.5;
font-size: 1.1em;
max-width: 100vw;
color: var(--main-color);
padding: 0 10px;
hyphens: auto;
}
a {
color: var(--link-color);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
h1,
h2,
h3 {
line-height: 1.2;
color: var(--heading-color);
}
h1 {
font-size: 1.5em;
}
h2 {
font-size: 1.3em;
}
h3 {
font-size: 1.2em;
}
pre,
code {
background: var(--code-bg);
color: var(--code-color);
border: 1px solid var(--border-color);
padding: 5px;
tab-size: 4;
overflow-x: auto;
}
blockquote {
border-left: 10px solid var(--code-color);
padding-left: 10px;
margin-right: 10px;
}
.navbar-logo {
height: 64px;
}
.card {
background-color: #fff;
border: 1px solid #ddd;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
overflow: hidden;
display: flex;
flex-direction: column;
}
.card-body {
flex: 1;
padding: 1rem;
}
.card-footer {
padding: 1rem;
background-color: #f8f9fa;
border-top: 1px solid #ddd;
margin-top: auto;
}
.card-img-top {
max-width: 100%;
height: auto;
object-fit: cover;
}
.navbar-collapse {
display: flex;
justify-content: space-between;
}
.navbar-nav {
flex-direction: row;
}
.nav-item {
margin-left: 1rem;
}
@media (max-width: 768px) {
.navbar-nav {
flex-direction: column;
align-items: center;
}
.nav-item {
margin-left: 0;
margin-bottom: 0.5rem;
}
.navbar-collapse {
flex-direction: column;
align-items: center;
}
.form-control {
width: 100%;
margin-bottom: 0.5rem;
}
}
.ibles {
display: inline-block;
vertical-align: top;
}
.ible-small {
font-size: 0.7em;
font-weight: thin;
line-height: 1em;
}
.step-section {
background-color: var(--primary-bg);
border: 1px solid var(--border-color);
border-radius: 8px;
box-shadow: 0 2px 4px var(--shadow-color);
padding: 1.5rem;
}
.step-header h2 {
font-size: 2em;
color: var(--heading-color);
}
.step-images img,
.step-videos video,
.step-iframes iframe {
border-radius: 8px;
box-shadow: 0 2px 4px var(--shadow-color);
}
.step-text {
font-size: 1.1em;
}
.reply-button,
.replies {
display: none;
}
.reply-button+label {
position: relative;
display: block;
cursor: pointer;
}
input.reply-button:checked+label+.replies {
display: flex;
flex-direction: column;
gap: 1rem;
margin-top: 1rem;
}
.member-list {
display: inline-block;
max-width: 200px;
vertical-align: top;
}
.ible-list-item {
display: inline-block;
max-width: 350px;
vertical-align: top;
margin-bottom: 2rem;
}
.contest-list-item {
display: inline-block;
max-width: 500px;
vertical-align: top;
margin-bottom: 2rem;
}
.archive-month-wrapper {
display: inline-block;
width: 30vw;
vertical-align: top;
}
.archive-month {
display: flex;
flex-direction: column;
gap: -10px;
margin-bottom: 1rem;
justify-content: space-between;
}
.archive {
margin-bottom: -20px;
}
ul.pagination {
display: flex;
justify-content: space-around;
padding: 0 33vw;
list-style-type: none;
align-items: center;
}
ul.pagination li.active a,
ul.pagination li.disabled a,
ul.pagination li.active a:hover,
ul.pagination li.disabled a:hover {
color: #bbc2cf;
text-decoration: none;
}
.closed-contest-contest {
object-fit: cover;
width: 33vw;
height: 15vw;
display: inline-block;
vertical-align: top;
padding: 0 10px;
}
.closed-contest-winner,
.closed-contest-winner-img {
width: 15vw;
display: inline-block;
vertical-align: top;
padding: 0 10px;
font-size: 0.8em;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.sitemap-group {
margin-top: 2em;
display: inline-block;
width: 30vw;
text-align: left;
vertical-align: top;
}
.sitemap-group h2 {
text-align: center;
}
.container {
margin-top: 20px !important;
}
header {
margin-bottom: 50px;
}
.go_here_link {
background-color: #4caf50;
/* Green */
border: none;
color: white;
padding: 15px 32px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
}
.go_here_link:hover {
color: black;
}
.wrap {
word-wrap: break-word;
overflow-wrap: break-word;
}
.navbar-logo {
height: 64px;
}
.img-fluid {
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
video {
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
iframe {
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.text-muted {
color: #6c757d !important;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 891 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 512 512"><!--! Font Awesome Free 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352a144 144 0 1 0 0-288 144 144 0 1 0 0 288z"/></svg>

After

Width:  |  Height:  |  Size: 494 B

View file

@ -0,0 +1,8 @@
{% extends "base.html" %}
{% block content %}
<center>
<h1>400</h1>
<p>The request was malformed</p>
</center>
{% endblock %}

View file

@ -0,0 +1,13 @@
{% extends "base.html" %}
{% block content %}
<center>
<h1>404</h1>
<p>This resource cannot be found</p>
<br>
<p>Why don't you try one of these instead?</p>
<a href="/" class="go_here_link">Featured</a>
<a href="/contest" class="go_here_link">Contests</a>
<br><br>
</center>
{% endblock %}

View file

@ -0,0 +1,9 @@
{% extends "base.html" %}
{% block content %}
{% include "header.html" %}
<center>
<h1>429</h1>
<p>This instance is being rate-limited</p>
</center>
{% endblock %}

View file

@ -0,0 +1,8 @@
{% extends "base.html" %}
{% block content %}
<center>
<h1>500</h1>
<p>An internal server error has occurred. Please try again later, or notify the administrator if this keeps happening.</p>
</center>
{% endblock %}

View file

@ -0,0 +1,31 @@
{% extends "base.html" %}
{% block content %}
<center>
<h1>Past Contests</h1>
<p>{{ contest_count }}</p>
<p><a href="/contest/">See running contests</a></p>
</center>
{% for year in contest_list %}
<div class="archive-year">
<hr>
<h2>{{ year[0] }}</h2>
<hr>
{% for month in year[1] %}
<div class="archive-month-wrapper">
<div class="archive-month">
<h3>{{ month[0] }}</h3>
{% for contest in month[1] %}
<div class="archive">
<p>{{ contest[0] }}<a href="{{ contest[1] }}">{{ contest[2] }}</a></p>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
<br>
</div>
{% endfor %}
<hr>
{{ pagination|safe }}
{% endblock %}

View file

@ -0,0 +1,7 @@
{% extends "base.html" %}
{% block content %}
<center>
<h1>This content is being reviewed</h1>
<p>This Instructable was just published and is still pending review.</p>
</center>
{% endblock %}

View file

@ -0,0 +1,87 @@
{% extends "base.html" %} {% block content %}
<div class="container my-5">
<div class="header-section text-center mb-5">
<h1 class="display-4">{{ title }}</h1>
<p class="text-muted">
by
<a href="{{ author_link }}" class="text-decoration-none">{{ author }}</a>
in
<a href="{{ category_link }}" class="text-decoration-none"
>{{ category }}</a
>
&gt;
<a href="{{ channel_link }}" class="text-decoration-none"
>{{ channel }}</a
>
</p>
<p class="text-muted">
{{ views }} Views, {{ favorites }} Favorites, {{ comment_count }} Comments
</p>
</div>
{% for step in steps %}
<section class="step-section mb-5 p-4 rounded shadow-sm">
<div class="step-header mb-4">
<h2>{{ step.title }}</h2>
</div>
{% if step.imgs %}
<div class="step-images row mb-4">
{% for step_img in step.imgs %}
<div class="col-md-4 mb-3">
<img
src="{{ step_img.src }}"
alt="{{ step_img.alt }}"
class="img-fluid rounded shadow-sm"
/>
</div>
{% endfor %}
</div>
{% endif %} {% if step.videos %}
<div class="step-videos row mb-4">
{% for step_video in step.videos %}
<div class="col-md-6 mb-3">
<video
src="{{ step_video }}"
controls
class="w-100 rounded shadow-sm"
></video>
</div>
{% endfor %}
</div>
{% endif %} {% if step.iframes %}
<div class="step-iframes row mb-4">
{% for step_iframe in step.iframes %}
<div class="col-md-8 mb-3">
<iframe
src="{{ step_iframe.src }}"
width="100%"
height="{{ step_iframe.height }}"
frameborder="0"
class="rounded shadow-sm"
></iframe>
</div>
{% endfor %}
</div>
{% endif %}
<div class="step-text mb-3">{{ step.text|safe }}</div>
{% if step.downloads %}
<div class="step-downloads row mb-4">
<div class="col-12">
<h3>Downloads</h3>
</div>
{% for step_download in step.downloads %}
<div class="col-md-2 mb-3">
<a href="{{ step_download.src }}" class="btn btn-primary w-100"
>{{ step_download.name }}</a
>
</div>
{% endfor %}
</div>
{% endif %}
</section>
{% endfor %}
</div>
{% endblock %}

View file

@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{% if title %}{{ title }} - {% endif %}Structables</title>
<link
rel="stylesheet"
href="{{ url_for('static', filename='css/style.css') }}"
/>
<link
rel="stylesheet"
href="{{ url_for('static', filename='dist/css/bootstrap.min.css') }}"
/>
<link
rel="icon"
type="image/png"
href="{{ url_for('static', filename='img/logo.png') }}"
/>
</head>
<body>
{% include "header.html" %}
<main>{% block content %} {% endblock %}</main>
{% include "footer.html" %}
</body>
</html>

View file

@ -0,0 +1,68 @@
{% extends "base.html" %} {% block content %}
<div class="container">
<h1 class="text-center">{{ title }}</h1>
<div class="row">
{% for channel in channels %}
<div class="col-6 col-md-4 col-lg-3 text-center mb-3">
<a
href="/{{ channel }}"
class="btn btn-primary d-block position-relative"
>
{{ channel }}
</a>
</div>
{% endfor %}
</div>
<h2 class="text-center">
<a href="{{ path }}projects/">Featured Projects</a>
</h2>
<div class="row">
{% for ible in ibles %}
<div class="col-xs-12 col-sm-6 col-lg-4 mb-4">
<div class="card h-100 d-flex flex-column">
<a href="{{ ible.link }}">
<img
class="card-img-top"
src="{{ ible.img }}"
alt="{{ ible.alt }}"
style="max-width: 100%"
/>
<div class="card-body">
<h5 class="card-title">{{ ible.title }}</h5>
</div>
</a>
<div class="card-footer mt-auto">
<p class="card-text">
by <a href="{{ ible.author_link }}">{{ ible.author }}</a>
</p>
<p class="card-text">
in <a href="{{ ible.channel_link }}">{{ ible.channel }}</a>
</p>
<p class="card-text">
{{ ible.favorites }} Favorites, {{ ible.views }} Views
</p>
</div>
</div>
</div>
{% endfor %}
</div>
<!-- Doesn't seem to be current
<h2 class="text-center"><a href="/contest/">Contests</a></h2>
<div class="row">
<div class="col">
{% for contest in contests %}
<a href="{{ contest.link }}">
<img
src="{{ contest.img }}"
alt="{{ contest.title }}"
class="img-fluid"
/>
</a>
{% endfor %}
</div>
</div>
-->
</div>
{% endblock %}

View file

@ -0,0 +1,34 @@
{% extends "base.html" %} {% block content %}
<div class="container text-center">
<h1>{{ title }}</h1>
<p>
by <a href="{{ author_link }}">{{ author }}</a> in
<a href="{{ category_link }}">{{ category }}</a> &gt;
<a href="{{ channel_link }}">{{ channel }}</a>
</p>
<p>{{ views }} Views, {{ favorites }} Favorites</p>
<div class="row justify-content-center">
{% for thumbnail in thumbnails %}
<div class="col-md-4 mb-3">
<div class="ible-list-item">
<a href="{{ thumbnail.link }}" style="color: #bbc2cf">
<img
class="img-fluid"
src="{{ thumbnail.img }}"
alt="{{ thumbnail.title }}"
style="max-width: 350px"
/>
<p>{{ thumbnail.author }}</p>
</a>
<p>
by <a href="{{ thumbnail.author_link }}">{{ thumbnail.author }}</a> in
<a href="{{ thumbnail.channel_link }}">{{ thumbnail.channel }}</a>
</p>
</div>
</div>
{% endfor %}
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,23 @@
{% extends "base.html" %}
{% block content %}
<center>
<img src="{{ img }}" alt="{{ title }}" style="max-width:98vw;">
<p>{{ entry_count }} Entries, {{ prizes }} Prizes</p>
<br>
{{ info|safe }}
<div class="ible-list">
{% for ible in entries %}
<div class="ible-list-item">
<a href="{{ ible.link }}" style="color:#bbc2cf;">
<img style="max-width:350px;" src="{{ ible.entry_img }}" alt="{{ ible.entry_title }}">
<p>{{ ible.entry_title }}</p>
</a>
<p>by <a href="{{ ible.author_link }}">{{ ible.author }}</a> in <a href="{{ ible.channel_link }}">{{ ible.channel }}</a></p>
<p>{{ ible.views }} Views</p>
</div>
{% endfor %}
</div>
</center>
{% endblock %}

View file

@ -0,0 +1,40 @@
{% extends "base.html" %}
{% block content %}
<center>
<h1>{{ title }}</h1>
{{ contest_count|safe }}
<br>
<div class="contest-list">
{% for contest in contests %}
<div class="contest-list-item">
<a href="{{ contest.link }}">
<img src="{{ contest.img }}" alt="{{ contest.alt }}" style="max-width:500px;">
</a>
<p>Closes {{ contest.deadline }}</p>
<p class="ible-small">{{ contest.prizes }} Prizes, {{ contest.entries }} Entries</p>
</div>
{% endfor %}
</div>
<div class="closed-contests" id="contest-winners">
<h2>Winner's Circle</h2>
{% for closed in closed %}
<div class="closed-contest">
<a href="{{ closed.link }}"><img class="closed-contest-contest" src="{{ closed.img }}"
alt="{{ closed.alt }}"></a>
{% for featured_items in closed.featured_items %}
<div class="closed-contest-winner">
<a href="{{ featured_items.link }}">
<img class="closed-contest-winner-img" src="{{ featured_items.img }}"
alt="{{ featured_items.title}}">
<br>
<b>{{ featured_items.title }}</b>
</a>
<p>by <a href="{{ featured_items.author_link }}">{{ featured_items.author }}</a></p>
</div>
{% endfor %}
</div>
{% endfor %}
</div>
</center>
{% endblock %}

View file

@ -0,0 +1,11 @@
<footer class="text-center">
<hr />
<p>
<a href="https://git.private.coffee/PrivateCoffee/structables"
>Structables Code (AGPLv3)</a
>
</p>
<p>
<a href="/privacypolicy">View privacy policy.</a>
</p>
</footer>

View file

@ -0,0 +1,39 @@
<header>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container-fluid">
<a class="navbar-brand" href="/">
<img
class="navbar-logo"
src="{{ url_for('static', filename='img/logo.png') }}"
alt="Structables Logo"
/>
</a>
<div class="navbar-collapse">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link" href="/projects/">Projects</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/contest/">Contests</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/teachers/">Teachers</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/sitemap/">Sitemap</a>
</li>
</ul>
<form class="d-flex" action="/search" method="post">
<input
class="form-control me-2"
type="search"
placeholder="Search"
name="q"
aria-label="Search"
/>
<button class="btn btn-outline-success" type="submit">Search</button>
</form>
</div>
</div>
</nav>
</header>

View file

@ -0,0 +1,12 @@
<html>
<head>
<title>iframe content</title>
</head>
<body>
<h1>Blocked iframe</h1>
<p>This page contains content from outside Instructables.com. This was blocked for your safety.</p>
<p>It tries to load the following URL:</p>
<p><a href="{{ url | safe }}" target="_self">{{ url | safe }}</a></p>
<p>Click <a href="{{ url | safe }}" target="_self">here</a> to load the content.</p>
</body>
</html>

View file

@ -0,0 +1,43 @@
{% extends "base.html" %} {% block content %}
<div class="container text-center">
<h1>{{ title }}</h1>
{% for section in sections %}
<section>
<div class="row">
<h3><a href="{{ section[1] }}">{{ section[0] }}</a></h3>
</div>
<div class="row justify-content-center">
{% for ible in section[2] %}
<div class="col-xs-12 col-sm-6 col-lg-4 mb-4">
<div class="card h-100">
<a href="{{ ible.link }}">
<img
class="card-img-top"
src="{{ ible.img }}"
alt="{{ ible.alt }}"
style="max-width: 100%"
/>
<div class="card-body">
<h5 class="card-title">{{ ible.title }}</h5>
</div>
</a>
<div class="card-footer">
<p class="card-text">
by <a href="{{ ible.author_link }}">{{ ible.author }}</a>
</p>
<p class="card-text">
in <a href="{{ ible.channel_link }}">{{ ible.channel }}</a>
</p>
<p class="card-text">
{{ ible.favorites }} Favorites, {{ ible.views }} Views
</p>
</div>
</div>
</div>
{% endfor %}
</div>
<hr />
</section>
{% endfor %}
</div>
{% endblock %}

View file

@ -0,0 +1,32 @@
{% extends "base.html" %}
{% block content %}
<center>
<img width=150px height=150px style="display:inline-block;" src="{{ header_content.avatar }}" alt="{{ header_content.title }}">
<h1>{{ header_content.title }}</h1>
<span>{{ header_content.location }}&nbsp;&nbsp;</span>
<span>{{ header_content.signup }}</span>
<br>
<span>{{ header_content.instructables }} Instructables&nbsp;&nbsp;</span>
<span>{{ header_content.views }} Views&nbsp;&nbsp;</span>
<span>{{ header_content.comments }} Comments&nbsp;&nbsp;</span>
<span>{{ header_content.followers }} Followers</span>
<br>
<p>{{ header_content.bio }}</p>
<hr>
<h2>Instructables</h2>
<div style="max-width:90%;">
{% for ible in ibles %}
<div class="ible-list-item">
<a href="{{ ible.link }}" style="color:#bbc2cf;">
<img style="max-width:350px;" src="{{ ible.img }}" alt="{{ ible.title }}">
<p>{{ ible.title }}</p>
<p>{{ ible.views }} Views, {{ ible.favorites }} Favorites</p>
</a>
</div>
{% endfor %}
</div>
</center>
{% endblock %}

View file

@ -0,0 +1,39 @@
{% extends "base.html" %}
{% block content %}
<center>
<img width=150px height=150px style="display:inline-block;" src="{{ header_content.avatar }}" alt="{{ header_content.title }}">
<h1>{{ header_content.title }}</h1>
<span>{{ header_content.location }}&nbsp;&nbsp;</span>
<span>{{ header_content.signup }}</span>
<br>
<span>{{ header_content.instructables }} Instructables&nbsp;&nbsp;</span>
<span>{{ header_content.views }} Views&nbsp;&nbsp;</span>
<span>{{ header_content.comments }} Comments&nbsp;&nbsp;</span>
<span>{{ header_content.followers }} Followers</span>
<br>
<p>{{ header_content.bio }}</p>
{% if ible_list_title != "" %}
<hr>
{% endif %}
<h2>{{ ible_list_title }}</h2>
{% for ible in ibles %}
<div class="member-list">
<a href="{{ ible.link }}" style="color:#bbc2cf;">
<img style="max-width:200px;" src="{{ ible.img }}" alt="{{ ible.title }}">
<p>{{ ible.title }}</p>
</a>
</div>
{% endfor %}
<p><a href="/member/{{ header_content.title }}/instructables/">View all Instructables</a></p>
<br>
<h2>{{ ach_list_title }}</h2>
{% for ach in achs %}
<div class="member-list">
<p><b>{{ ach[0] }}</b></p>
<p>{{ ach[1] }}</p>
</div>
{% endfor %}
</center>
{% endblock %}

View file

@ -0,0 +1,10 @@
{% extends 'base.html' %}
{% block content %}
<center>
<br>
<h1 style="font-size:2em;line-height:0em;">Privacy Policy</h1>
<br>
<p>{{ content }}</p>
</center>
{% endblock %}

View file

@ -0,0 +1,56 @@
{% extends "base.html" %} {% block content %}
<center>
<h1>{{ title }}</h1>
<span><a href="{{ path }}/">Featured</a></span>
<span><a href="{{ path }}/recent/">Recent</a></span>
<span><a href="{{ path }}/popular/">Popular</a></span>
<span><a href="{{ path }}/views/">Views</a></span>
<span><a href="{{ path }}/winners/">Winners</a></span>
<br />
<div class="container">
<div class="row">
{% for ible in ibles %}
<div class="col-xs-12 col-sm-6 col-lg-4 mb-4">
<div class="card h-100 d-flex flex-column">
<a href="{{ ible.link }}">
<img
class="card-img-top"
src="{{ ible.img }}"
alt="{{ ible.alt }}"
style="max-width: 100%"
/>
<div class="card-body">
<h5 class="card-title">{{ ible.title }}</h5>
</div>
</a>
<div class="card-footer mt-auto">
<p class="card-text">
by <a href="{{ ible.author_link }}">{{ ible.author }}</a>
</p>
<p class="card-text">
in <a href="{{ ible.channel_link }}">{{ ible.channel }}</a>
</p>
<p class="card-text">
{{ ible.favorites }} Favorites, {{ ible.views }} Views
</p>
</div>
</div>
</div>
{% endfor %}
<ul class="pagination">
{% for page in pagination %}
<li class="page-item">
<a
class="page-link {% if page.active %}active{% endif %} {% if page.disabled %}disabled{% endif %}"
href="{{ page.link }}"
>{{ page.text }}</a
>
</li>
{% endfor %}
</ul>
</div>
</div>
</center>
{% endblock %}

View file

@ -0,0 +1,23 @@
{% extends "base.html" %}
{% block content %}
<center>
<h1>Sitemap</h1>
<div class="container">
<div class="row sitemap-groups"></div>
{% for group in groups %}
<div class="sitemap-group col-xs-12 col-sm-6 col-lg-4">
<a href="{{ group[1] }}">
<h2>{{ group[0] }}</h2>
</a>
<ul>
{% for channel in group[2] %}
<li><a href="{{ channel[1] }}">{{ channel[0] }}</a></li>
{% endfor %}
</ul>
</div>
{% endfor %}
</div>
</div>
</center>
{% endblock %}

View file

@ -0,0 +1,2 @@
<!-- TODO: Get rid of this -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">