mirror of
https://github.com/FAUSheppy/flask-json-dream-website
synced 2025-12-06 08:11:35 +01:00
Initial
This commit is contained in:
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
js/
|
||||
css/
|
||||
*.swp
|
||||
*.jpg
|
||||
*.png
|
||||
cache.json
|
||||
auth.txt
|
||||
106
README.md
Normal file
106
README.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# Requirements
|
||||
This Softwares runs python3-flask with markdown, json and caldav.
|
||||
|
||||
python3 -m pip install flask, json, caldav, markdown2
|
||||
|
||||
This Software requires bootstrap > 4.13 which can be downloaded [here](https://getbootstrap.com/docs/4.3/getting-started/download/). It must be unpacked into the *static/*-directory into *js* and *css* respectively.
|
||||
|
||||
Additionally bootstrap depends on [jquery](https://code.jquery.com) which you have to download into a file called *jquery.min.js* in *static/js/*.
|
||||
|
||||
# Usage
|
||||
|
||||
./server.py -h
|
||||
usage: server.py [-h] [-i INTERFACE] [-p PORT] --cal-info CAL_INFO
|
||||
[--no-update-on-start]
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
-i INTERFACE Interface to listen on (default: 0.0.0.0)
|
||||
-p PORT, --port PORT Port to listen on (default: 5000)
|
||||
--cal-info CAL_INFO File Containing a public calendar link (default: None)
|
||||
--no-update-on-start Don't update the calendar on start (default: False)
|
||||
|
||||
|
||||
# Configuration
|
||||
The page and most of it's content is configured via json. To use the CalDav-events section, you need to add a comma seperated file with the following format format/information:
|
||||
|
||||
URL,USER,PASSWORD
|
||||
|
||||
## Main Config
|
||||
The main Config ``config.json`` which must be placed in the project-root must contain the following values:
|
||||
|
||||
{
|
||||
"siteTitle":"the default site title",
|
||||
}
|
||||
|
||||
Additionally it may contain the following information:
|
||||
|
||||
"teamspeak-server" : "TS_SERVER",
|
||||
"discord-server" : "DISCORD_LINK",
|
||||
"facebook" : "FACEBOOK_LINK",
|
||||
"instagram" : "INSTAGRAM_LINK",
|
||||
"twitter" : "TWITTER_LINK"
|
||||
|
||||
## Startpage Sections
|
||||
### Events
|
||||
The events section from the start-page is imported from the calendar and will show events a given time in the future or past. It will by default show three events. If there are more than three events, a *'More'*-button will be displayed. If there are no events the section will not be shown at all.
|
||||
|
||||
### News/Announcements
|
||||
This Section will read and display JSON configuration from the *news/*-direcotry. A news-configuration must contain these information:
|
||||
|
||||
{
|
||||
"title" : "title of the announcement",
|
||||
"uid" : a_unique_integer_number,
|
||||
"markdown-file" : "path to markdown file containing the actual announcement",
|
||||
"active" : boolean_if_it_should_be_displayed,
|
||||
"description" : "a short description",
|
||||
"date" : date_posted_as_unix_timestamp,
|
||||
}
|
||||
|
||||
Additionally it must contain either a 'fixed-timeout' or 'relative-timeout-weeks' after which it will no longer be displayed. If both a specified the fixed timeout takes precedence.
|
||||
|
||||
"timeout-relative-weeks" : 12,
|
||||
"timeout-fixed" : a_date_as_unix_timestamp
|
||||
|
||||
Finally it MAY contain a text to display on the button leading to the article (otherwise it will use a default).
|
||||
|
||||
"link-text" : "maximum 25 characters"
|
||||
|
||||
Obviously the markdown file referenced in the configuration must also be created.
|
||||
|
||||
### Other Sections
|
||||
All following sections are read and created from the *vereinSection* directory. Json configuration for these sections much contain these information. The pictures for all the sections should have a similar aspect ratio.
|
||||
|
||||
{
|
||||
"picture" : "path to a picture for this section",
|
||||
"title" : "A title for this card",
|
||||
"text" : "A potentially very long text of multiple lines that will be displayed next to the picture...",
|
||||
}
|
||||
|
||||
The configuration may contain the following information, which add a button-like link to the section.
|
||||
|
||||
"moreInfoButtonText" : "less than 25 charaters",
|
||||
"moreInfoButtonHref" : "href to go to"
|
||||
|
||||
The alpha-numeric order of the filenames specifies the order in which the sections will be displayed on the website, so the files should be prefixed with a number, for example *10_section_hello.json* and *90_section_ending.json*.
|
||||
|
||||
## People
|
||||
To display a person on the people-subpage create a JSON-file in the *people/*-directory containing the following information:
|
||||
|
||||
{
|
||||
"title" : "Name of the Person",
|
||||
"subtitle" : "Function of the Person",
|
||||
"image" : "path to image",
|
||||
"text" : "Potentially long text describing the person and their functions."
|
||||
}
|
||||
|
||||
The order is again specified by the alpha-numeric order of the files.
|
||||
|
||||
# Adding new Subpages
|
||||
New subpages must be added as a new location in the *server.py* like this:
|
||||
|
||||
@app.route("/subpage")
|
||||
def subpage():
|
||||
return flask.render_template("subpage.html", conf=mainConfig)
|
||||
|
||||
See the example subpage-templates in *templates/*.
|
||||
10
config.json
Normal file
10
config.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"siteTitle":"Site Title",
|
||||
"teamspeak-server" : "teamspeak.com",
|
||||
"discord-server" : "https://discord.gg/",
|
||||
"facebook" : "https://www.facebook.com/",
|
||||
"instagram" : "https://www.instagram.com/",
|
||||
"twitter" : "https://twitter.com/its_a_sheppy",
|
||||
"twitch-channel" : "esports-erlangen",
|
||||
"twitch-placeholder-img" : "placeholder.png"
|
||||
}
|
||||
54
markdown/example_markdown.md
Normal file
54
markdown/example_markdown.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# Utero est
|
||||
|
||||
## Plurima duro adeo viros spargere fletumque feras
|
||||
|
||||
Lorem markdownum Eurytion Troiana *nescioquam* fuit, tenuesque manebant illic
|
||||
admittitur vultu. Praereptaque potitus vento.
|
||||
|
||||
1. Nec ostia sinus tellure
|
||||
2. Esse focus labori hastas quippe
|
||||
3. Vivos misit claustraque ille
|
||||
4. Nusquam simulacra daedalus inde servor accessit obliquis
|
||||
5. Restabam aestusque videri puerum dum videri nomen
|
||||
6. Et radice cladis natus surgit ante
|
||||
|
||||
## Indicio hoc
|
||||
|
||||
Te spoliare lux sonuere genas: et medi Iuppiter litoris fama Piraeaque Phrygia
|
||||
amorem obsessa animas? Nostros sustulit cauda cautus videntur consiste si
|
||||
videtur *copia sed* inmemor ope nimia, proles. Sumpta Calydonida avido ipsorum
|
||||
Phoebus tum fortuna rupit res: haud illa inpulsu et referre duabus. Ossaque
|
||||
[albescere](http://contra-servavique.net/) nulla facta foedumque thalamos
|
||||
deposuit accipit imago iactu percutiens **hostes**.
|
||||
|
||||
**Utinam versasse** sorores mirantes: rigidum cognoscite vicit ulterius famulis,
|
||||
suis. Iam navita ferarum cacumina sincera tuli, iusserat pharetrae quod carne
|
||||
at? Sororum videbitur aethera amor, mera lugubre [aetas artes
|
||||
captantur](http://levibus.net/) conantem suae thalamos Minervaetransformabantur,
|
||||
querenda capro ab media *quoque*. Cum sibila ait illa velo, et ab vestes iubent
|
||||
crevit.
|
||||
|
||||
## Primusque Phaethon ramis
|
||||
|
||||
[Felici maduisse](http://latonaetransitus.io/non). Sub bimembres decipienda est
|
||||
texta stipes. Ense ordine poscit rescindere **vidit** fulmina cubile leones!
|
||||
Virgo per quatit ducem intabescere locus et superi ab quotiens amantes nos
|
||||
dissimiles si pateres hastas. Magna imago, imperat discederet tanto loquebatur
|
||||
turbatus, aether esse missus, in.
|
||||
|
||||
## Socialis Amor delubraque non et anseribus bracchia
|
||||
|
||||
Addicere graia qualis passu orantemque honorem Achaia coruscant Talia in ripae
|
||||
perstat refert mediusve nemorum parentis. Cupit specie patuisse ad laniata et
|
||||
patuit capillis pulsatus; primo est bis! Minervae accensus unda, tactosque
|
||||
corpus, dea Iuppiter, vota herba, per deam inmitem **negata** visa inmania
|
||||
extimuit? *Forent atque fulvis* vultu fidibusque causa, maestis purus per.
|
||||
|
||||
> In meritorum Lycaona in est velum ambo: si dixit et nubibus, illa quid Icelon.
|
||||
> Sententia possis dumque; relinquam mandatam, ad leti terrebat gramen aenum,
|
||||
> templis Issen atrorum.
|
||||
|
||||
Et tegebat eodem minimus curae, miseri illo huic *viscera ulli*, atque toros non
|
||||
ego certaminis facibus. Sinus nec [Althaea](http://www.fortiatelum.io/), cum
|
||||
Laelapa arbore, [contentus quam](http://www.vincere.io/nostras-qui): non, *et*.
|
||||
Manebit dentes dedisses licet Vix Ilion quam putares gemino.
|
||||
10
news/example_news.json
Normal file
10
news/example_news.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"title" : "Title",
|
||||
"uid" : 15,
|
||||
"markdown-file" : "markdown/example_markdown.md",
|
||||
"active" : true,
|
||||
"description" : "Lorem markdownum Eurytion Troiana *nescioquam* fuit, tenuesque manebant illic admittitur vultu. Praereptaque potitus vento.",
|
||||
"date" : 1594282463,
|
||||
"timeout-relative-weeks" : 60,
|
||||
"timeout-fixed" : 2094282463
|
||||
}
|
||||
6
people/10_john_doe.json
Normal file
6
people/10_john_doe.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"title" : "John Doe",
|
||||
"subtitle" : "CEO & Founder",
|
||||
"image" : "placeholder.png",
|
||||
"text" : "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English."
|
||||
}
|
||||
6
people/20_jane_doe.json
Normal file
6
people/20_jane_doe.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"title" : "Jane Doe",
|
||||
"subtitle" : "COO & Founder",
|
||||
"image" : "placeholder.png",
|
||||
"text" : "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English."
|
||||
}
|
||||
7
sections/00-example-section.json
Normal file
7
sections/00-example-section.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"picture" : "/static/pictures/placeholder.png",
|
||||
"title" : "Section Title",
|
||||
"text" : "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
|
||||
"moreInfoButtonText" : "Mehr..",
|
||||
"moreInfoButtonHref" : "/impressum"
|
||||
}
|
||||
5
sections/10-example-section.json
Normal file
5
sections/10-example-section.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"picture" : "/static/pictures/placeholder.png",
|
||||
"title" : "Section Title",
|
||||
"text" : "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
|
||||
}
|
||||
7
sections/20-example-section.json
Normal file
7
sections/20-example-section.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"picture" : "/static/pictures/placeholder.png",
|
||||
"title" : "Section Title",
|
||||
"text" : "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
|
||||
"moreInfoButtonText" : "Mehr..",
|
||||
"moreInfoButtonHref" : "/impressum"
|
||||
}
|
||||
197
server.py
Executable file
197
server.py
Executable file
@@ -0,0 +1,197 @@
|
||||
#!/usr/bin/python3
|
||||
import json
|
||||
import os
|
||||
import flask
|
||||
import argparse
|
||||
|
||||
import caldav
|
||||
import datetime as dt
|
||||
import markdown2
|
||||
|
||||
VEREIN_SECTIONS_DIR = "sections/"
|
||||
MAIN_LINKS_DIR = "main-links/"
|
||||
NEWS_DIR = "news/"
|
||||
|
||||
app = flask.Flask("athq-landing-page")
|
||||
mainConfig = dict()
|
||||
with open("config.json") as f:
|
||||
mainConfig = json.load(f)
|
||||
|
||||
caldavUrl = None
|
||||
caldavPassword = None
|
||||
caldavUsername = None
|
||||
|
||||
def updateEventsFromCalDav():
|
||||
|
||||
|
||||
if app.config["USE_CALENDAR"]:
|
||||
client = caldav.DAVClient(url=caldavUrl, username=caldavUsername, password=caldavPassword)
|
||||
authenticatedClient = client.principal()
|
||||
defaultCal = authenticatedClient.calendars()[0]
|
||||
|
||||
start = dt.datetime.now()
|
||||
start = start - dt.timedelta(seconds=start.timestamp() % dt.timedelta(days=1).total_seconds())
|
||||
end = start + dt.timedelta(days=90)
|
||||
|
||||
# TODO remove this
|
||||
# start = start - dt.timedelta(days=90)
|
||||
|
||||
events = sorted(defaultCal.date_search(start, end),
|
||||
key=lambda e: e.vobject_instance.vevent.dtstart.value)
|
||||
eventsDictList = []
|
||||
for e in events:
|
||||
date = e.vobject_instance.vevent.dtstart.value
|
||||
date += dt.timedelta(hours=2)
|
||||
newEventDict = { "description" : e.vobject_instance.vevent.summary.value,
|
||||
"time" : date.strftime("%H:%M"),
|
||||
"day" : date.strftime("%d"),
|
||||
"month" : date.strftime("%b"),
|
||||
"year" : date.strftime("%Y") }
|
||||
try:
|
||||
newEventDict.update({ "location" : e.vobject_instance.vevent.location.value })
|
||||
except AttributeError:
|
||||
pass
|
||||
eventsDictList += [newEventDict]
|
||||
else:
|
||||
eventsDictList = []
|
||||
|
||||
with open("cache.json", "w") as f:
|
||||
json.dump(eventsDictList, f)
|
||||
|
||||
|
||||
def getEventsCache():
|
||||
with open("cache.json", "r") as f:
|
||||
return json.load(f)
|
||||
|
||||
def readJsonDir(basedir):
|
||||
|
||||
# load json files from projects/ dir #
|
||||
jsonDictList =[]
|
||||
for root, dirs, files in os.walk(basedir):
|
||||
for filename in sorted(files):
|
||||
if filename.endswith(".json"):
|
||||
with open(os.path.join(basedir, filename)) as f:
|
||||
jsonDictList += [json.load(f)]
|
||||
|
||||
return jsonDictList
|
||||
|
||||
def parseNewsDirWithTimeout():
|
||||
|
||||
TIMEOUT_RELATIVE = "timeout-relative-weeks"
|
||||
TIMEOUT_FIXED = "timeout-fixed"
|
||||
PARSED_TIME = "parsed-time"
|
||||
ACTIVE = "active"
|
||||
DATE = "date"
|
||||
|
||||
news = readJsonDir(NEWS_DIR)
|
||||
now = dt.datetime.now()
|
||||
for n in news:
|
||||
n.update( { PARSED_TIME : dt.datetime.fromtimestamp(n[DATE]) } )
|
||||
if n.get(ACTIVE):
|
||||
continue
|
||||
if n.get(TIMEOUT_FIXED):
|
||||
if dt.datetime.fromtimestamp(n[TIMEOUT_FIXED]) < now:
|
||||
n[ACTIVE] = False
|
||||
elif n.get(TIMEOUT_RELATIVE):
|
||||
if n[PARSED_TIME] + dt.timedelta(weeks=n[TIMEOUT_RELATIVE]) < now:
|
||||
n[ACTIVE] = False
|
||||
else:
|
||||
raise ValueError("No timeout for news {} specified!", n)
|
||||
|
||||
return sorted(news, key=lambda n: n[PARSED_TIME], reverse=True)
|
||||
|
||||
|
||||
@app.route("/invalidate")
|
||||
def invalidateEventCache():
|
||||
updateEventsFromCalDav();
|
||||
return ("", 204)
|
||||
|
||||
@app.route("/")
|
||||
def root():
|
||||
announcements = parseNewsDirWithTimeout()
|
||||
return flask.render_template("index.html", mainLinks=readJsonDir(MAIN_LINKS_DIR),
|
||||
siteTitle=mainConfig["siteTitle"],
|
||||
conf=mainConfig,
|
||||
events=getEventsCache(),
|
||||
moreEvents=len(getEventsCache())>3,
|
||||
vereinSections=readJsonDir(VEREIN_SECTIONS_DIR),
|
||||
announcements=announcements)
|
||||
|
||||
@app.route("/impressum")
|
||||
def impressum():
|
||||
return flask.render_template("impressum.html", conf=mainConfig)
|
||||
|
||||
@app.route("/verein")
|
||||
def verein():
|
||||
return flask.render_template("verein.html", conf=mainConfig)
|
||||
|
||||
@app.route("/stammtisch")
|
||||
def stammtisch():
|
||||
return flask.render_template("stammtisch.html", conf=mainConfig)
|
||||
|
||||
@app.route("/people")
|
||||
def people():
|
||||
return flask.render_template("people.html", conf=mainConfig,
|
||||
people=readJsonDir("people/"))
|
||||
|
||||
@app.route("/news")
|
||||
def news():
|
||||
|
||||
uid = int(flask.request.args.get("uid"))
|
||||
|
||||
news = parseNewsDirWithTimeout()
|
||||
newsDict = dict()
|
||||
for n in news:
|
||||
newsDict.update( { n["uid"] : n } )
|
||||
|
||||
if not uid or not newsDict[uid]:
|
||||
return ("", 404)
|
||||
|
||||
article = newsDict[uid]
|
||||
try:
|
||||
with open(article["markdown-file"]) as f:
|
||||
article.update( { "markdown-content" : markdown2.markdown(f.read()) } )
|
||||
except FileNotFoundError as e:
|
||||
return ("File not found Error ({})".format(e), 404)
|
||||
|
||||
return flask.render_template("news.html", conf=mainConfig, article=article)
|
||||
|
||||
@app.route("/static/<path:path>")
|
||||
def sendStatic(path):
|
||||
if "pictures" in path:
|
||||
cache_timeout = 2592000
|
||||
else:
|
||||
cache_timeout = None
|
||||
return flask.send_from_directory('static', path, cache_timeout=cache_timeout)
|
||||
|
||||
@app.route('/defaultFavicon.ico')
|
||||
def icon():
|
||||
return app.send_static_file('defaultFavicon.ico')
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
parser = argparse.ArgumentParser(description='Projects Showcase',
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||
|
||||
# general parameters #
|
||||
parser.add_argument("-i", "--interface", default="0.0.0.0", help="Interface to listen on")
|
||||
parser.add_argument("-p", "--port", default="5000", help="Port to listen on")
|
||||
parser.add_argument("--cal-info", help="File Containing a public calendar link")
|
||||
parser.add_argument("--no-update-on-start", action="store_const", const=True, default=False,
|
||||
help="Don't update the calendar on start")
|
||||
|
||||
# startup #
|
||||
app.config['TEMPLATES_AUTO_RELOAD'] = True
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.cal_info:
|
||||
app.config["USE_CALENDAR"] = True
|
||||
with open(args.cal_info) as f:
|
||||
caldavUrl, caldavUsername, caldavPassword = f.read().strip().split(",")
|
||||
else:
|
||||
app.config["USE_CALENDAR"] = False
|
||||
|
||||
if not args.no_update_on_start:
|
||||
updateEventsFromCalDav()
|
||||
|
||||
app.run(host=args.interface, port=args.port)
|
||||
BIN
static/defaultFavicon.ico
Normal file
BIN
static/defaultFavicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
static/pictures/placeholder.png
Normal file
BIN
static/pictures/placeholder.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
1
static/pictures/wallpaper.jpg
Symbolic link
1
static/pictures/wallpaper.jpg
Symbolic link
@@ -0,0 +1 @@
|
||||
placeholder.png
|
||||
70
static/site.css
Normal file
70
static/site.css
Normal file
@@ -0,0 +1,70 @@
|
||||
html {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.masthead {
|
||||
height: 50vh;
|
||||
min-height: 250px;
|
||||
background-image: url('/static/pictures/wallpaper.jpg');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.bg-special{
|
||||
background-color: #eae9e9;
|
||||
}
|
||||
|
||||
.text-color-special{
|
||||
color: #eae9e9;
|
||||
}
|
||||
|
||||
.stretched-link::after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
pointer-events: auto;
|
||||
content: "";
|
||||
background-color: rgba(0,0,0,0);
|
||||
}
|
||||
.hover-to-75:hover *{
|
||||
opacity: 0.75;
|
||||
display: unset;
|
||||
}
|
||||
|
||||
.position-relative{
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.centered-text-in-image{
|
||||
position: absolute;
|
||||
color: black !important;
|
||||
font-weight: bold;
|
||||
font-size: x-large;
|
||||
top: 50%;
|
||||
text-decoration: none !important;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.centered-text-in-image:hover{
|
||||
display: none;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.image-min-dimensions{
|
||||
min-width: 330px;
|
||||
/*min-height: 220px; */
|
||||
}
|
||||
|
||||
.text-min-dimensions{
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.image-max-dimensions{
|
||||
max-width: 400px;
|
||||
}
|
||||
63
static/toggle_button.css
Normal file
63
static/toggle_button.css
Normal file
@@ -0,0 +1,63 @@
|
||||
.switch {
|
||||
position: relative;
|
||||
float: left;
|
||||
width: 60px;
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
.switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #ccc;
|
||||
-webkit-transition: .4s;
|
||||
transition: .4s;
|
||||
}
|
||||
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 26px;
|
||||
width: 26px;
|
||||
left: 4px;
|
||||
bottom: 4px;
|
||||
background-color: white;
|
||||
-webkit-transition: .4s;
|
||||
transition: .4s;
|
||||
}
|
||||
|
||||
input:checked + .slider {
|
||||
background-color: #2196F3;
|
||||
}
|
||||
|
||||
input:focus + .slider {
|
||||
box-shadow: 0 0 1px #2196F3;
|
||||
}
|
||||
|
||||
input:checked + .slider:before {
|
||||
-webkit-transform: translateX(26px);
|
||||
-ms-transform: translateX(26px);
|
||||
transform: translateX(26px);
|
||||
}
|
||||
|
||||
/* Rounded sliders */
|
||||
.slider.round {
|
||||
border-radius: 34px;
|
||||
}
|
||||
|
||||
.slider.round:before {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.switch-caption {
|
||||
float: left;
|
||||
}
|
||||
18
templates/announcements.html
Normal file
18
templates/announcements.html
Normal file
@@ -0,0 +1,18 @@
|
||||
<div class="row">
|
||||
{% for a in announcements if a.get("active") %}
|
||||
<div class="col-md-4 text-dark">
|
||||
<div class="card mb-4 box-shadow bg-special">
|
||||
<div class="card-body">
|
||||
<h4>{{ a["parsed-time"].strftime("%d.%m.%y") }}</h4>
|
||||
<h2>{{ a["title"] }}</h2>
|
||||
<p>{{ a["description"] }}</p>
|
||||
<p>
|
||||
<a class="mt-3 btn btn-secondary float-right" href="/news?uid={{ a['uid'] }}" role="button">
|
||||
{% if a["link-title"] %} {{ a["link-title"] }} {% else %} Mehr.. {% endif %}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
43
templates/events.html
Normal file
43
templates/events.html
Normal file
@@ -0,0 +1,43 @@
|
||||
{% if events %}
|
||||
<div class="col grid-margin-md stretch-card d-flex_" style="font-size: x-large;">
|
||||
<div class="card bg-transparent border-0">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between"></div>
|
||||
{% for event in events %}
|
||||
<div class="moreDates" {% if loop.index > 3 %} style="display: none;" {% endif %}>
|
||||
<div class="row">
|
||||
<div class="pt-1 border-bottom {% if loop.index == 1 %} border-top {% endif %} col">
|
||||
<div class="d-flex justify-content-center align-items-center">
|
||||
<h1 class="mr-5 text-primary font-weight-bold text-info">{{ event["day"] }}</h1>
|
||||
<div>
|
||||
<p class="font-weight-bold mb-0 text-dark">{{ event["month"] }}</p>
|
||||
<p class="mb-2">{{ event["year"] }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pt-1 border-bottom {% if loop.index == 1 %} border-top {% endif %} col pl-3">
|
||||
<p class="text-dark font-weight-bold mb-0">
|
||||
{{ event["description"] }}
|
||||
{% if event.get("location") %} @{{ event["location"] }} {% endif %}
|
||||
</p>
|
||||
<p class="mb-0">{{ event["time"] }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if moreEvents %}
|
||||
<button id="moreDatesButton" class="float-right mt-3 btn btn-light" onClick=showAdditionalDates()>Mehr..</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- mehr Termine Button -->
|
||||
<script>
|
||||
function showAdditionalDates(){
|
||||
Array.from(document.getElementsByClassName("moreDates")).forEach(element => {
|
||||
element.style.display = "";
|
||||
});
|
||||
document.getElementById("moreDatesButton").style.display = "none"
|
||||
}
|
||||
</script>
|
||||
{% endif %}
|
||||
3
templates/footer.html
Normal file
3
templates/footer.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<div class="footer-copyright text-center py-3 bg-dark" style="position: relative; bottom: 0;">
|
||||
<a style="color: rgba(255,255,255,.5);" href="/impressum">Impressum/Kontakt</a>
|
||||
</div>
|
||||
14
templates/head.html
Normal file
14
templates/head.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<meta name="description" content="">
|
||||
<meta name="author" content="">
|
||||
<link rel="shortcut icon" href="/defaultFavicon.ico">
|
||||
|
||||
<!-- Bootstrap core CSS -->
|
||||
<link href="/static/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="/static/site.css" rel="stylesheet">
|
||||
<link href="/static/toggle_button.css" rel="stylesheet">
|
||||
|
||||
<!-- Bootstrap core JS -->
|
||||
<script src="/static/js/jquery.min.js"></script>
|
||||
<script src="/static/js/bootstrap.min.js"></script>
|
||||
51
templates/impressum.html
Normal file
51
templates/impressum.html
Normal file
@@ -0,0 +1,51 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
|
||||
{% include 'head.html' %}
|
||||
|
||||
<title>{{ conf["siteTitle"] }}</title>
|
||||
|
||||
</head>
|
||||
<body style="background-color: #eae9e9">
|
||||
|
||||
{% include 'navbar.html' %}
|
||||
<div class="navbar navbar-default"></div>
|
||||
<div class="container h-100 mt-5">
|
||||
<h1>Impressum</h1>
|
||||
<div class="row impressum mt-5 pl-3">
|
||||
<div class="col-sm">
|
||||
<h4>Hello World</h4>
|
||||
<p>Organisation<br>
|
||||
Hello World: Hello World<br>
|
||||
Hello World: Hello World<br>
|
||||
Mail: <a href="mailto:noreply@example.com">
|
||||
noreply@example.com</a></p>
|
||||
<h4>Person</h4>
|
||||
<p>John Doe<br>
|
||||
Location<br> Postal City<br>
|
||||
Phone: 12345 6789 </p>
|
||||
</div>
|
||||
<div class="col-sm pr-5">
|
||||
<h4>Hello World</h4>
|
||||
<p>Jane Doe<br>
|
||||
Location<br> Postal City<br>
|
||||
<p>
|
||||
</p>
|
||||
<h4>Admin C/Hello World</h4>
|
||||
<p>
|
||||
Free Text
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="row impressum mt-5"></div>
|
||||
<div class="col-lg-12">
|
||||
{% include 'impressum_text.html' %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include 'footer.html' %}
|
||||
</body>
|
||||
</html>
|
||||
21
templates/impressum_text.html
Normal file
21
templates/impressum_text.html
Normal file
@@ -0,0 +1,21 @@
|
||||
<h1>HTML Ipsum</h1>
|
||||
|
||||
<p><strong>Pellentesque habitant morbi tristique</strong> senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. <em>Aenean ultricies mi vitae est.</em> Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, <code>commodo vitae</code>, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. <a href="#">Donec non enim</a> in turpis pulvinar facilisis. Ut felis.</p>
|
||||
|
||||
<h2>Header Level 2</h2>
|
||||
|
||||
<ol>
|
||||
<li>Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</li>
|
||||
<li>Aliquam tincidunt mauris eu risus.</li>
|
||||
</ol>
|
||||
|
||||
<blockquote><p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus magna. Cras in mi at felis aliquet congue. Ut a est eget ligula molestie gravida. Curabitur massa. Donec eleifend, libero at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. Vivamus pretium ornare est.</p></blockquote>
|
||||
|
||||
<h3>Header Level 3</h3>
|
||||
|
||||
<ul>
|
||||
<li>Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</li>
|
||||
<li>Aliquam tincidunt mauris eu risus.</li>
|
||||
</ul>
|
||||
|
||||
<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus</p>
|
||||
126
templates/index.html
Normal file
126
templates/index.html
Normal file
@@ -0,0 +1,126 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
|
||||
{% include 'head.html' %}
|
||||
|
||||
<title>{{ conf["siteTitle"] }}</title>
|
||||
|
||||
<!-- Load the Twitch embed script -->
|
||||
<!-- <script src="https://embed.twitch.tv/embed/v1.js"></script> -->
|
||||
<script src="https://sslrelay.atlantishq.de/twitch"></script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<!-- NAVBAR -->
|
||||
{% include 'navbar.html' %}
|
||||
|
||||
<!-- Site wellcome header -->
|
||||
<header class="masthead">
|
||||
<div class="container h-50">
|
||||
<div class="row h-100 align-items-center">
|
||||
<div class="col-12 text-center">
|
||||
<div style="opacity: 0;"></div>
|
||||
<div class="mt-5" style="opacity: 0;"></div>
|
||||
<!--
|
||||
<h1 class="font-weight-light">{{ conf["siteWellcomeMsg"] }}</h1>
|
||||
<p class="lead">{{ conf["siteWellcomeMsgSub"] }}</p>
|
||||
-->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- main links -->
|
||||
<div class="bg-secondary">
|
||||
<div class="container pb-2 pt-2">
|
||||
{% include 'events.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- announcements -->
|
||||
{% if announcements %}
|
||||
<div class="bg-secondary">
|
||||
<div class="container pb-2 pt-2">
|
||||
{% include 'announcements.html' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- twitch -->
|
||||
{% if conf["twitch-channel"] %}
|
||||
<div class="bg-dark pb-4">
|
||||
<div class="container pt-5 text-color-special">
|
||||
|
||||
<div id="twitch-consent-placeholder" class="card bg-dark text-white">
|
||||
<img style="min-width: 80%; min-height: 200px;"
|
||||
class="card-img" src="/static/pictures/{{ conf['twitch-placeholder-img'] }}" >
|
||||
<div class="card-img-overlay">
|
||||
<label class="switch mt-3">
|
||||
<input id="toogle-twitch" class="custom-control-input"
|
||||
type="checkbox" onchange="handleToggle(this.id)">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
<div class="switch-caption ml-3 mt-3" style="font-size: x-large;">
|
||||
Laden externe Inhalte von Twitch.tv zulassen
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Add a placeholder for the Twitch embed -->
|
||||
<div id="twitch-embed"></div>
|
||||
<script>
|
||||
document.getElementById("toogle-twitch").checked = false
|
||||
function handleToggle(id){
|
||||
if(document.getElementById(id).checked){
|
||||
createTwitchFrame()
|
||||
document.getElementById("twitch-consent-placeholder").style.display = "none";
|
||||
}else{
|
||||
document.getElementById("twitch-consent-placeholder").style.display = "";
|
||||
destroyTwitchFrame()
|
||||
}
|
||||
}
|
||||
|
||||
function createTwitchFrame(){
|
||||
options = { width: "100%", height: 480, channel: "{{ conf['twitch-channel'] }}" }
|
||||
new Twitch.Embed("twitch-embed", options);
|
||||
}
|
||||
|
||||
function destroyTwitchFrame(){
|
||||
window.location.reload()
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<!-- Create a Twitch.Embed object that will render within the "twitch-embed" root element. -->
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% for section in vereinSections %}
|
||||
<div class="{% if loop.index %2 == 1 %} bg-secondary {% else %} bg-dark {% endif %} pt-2 pb-2">
|
||||
<div class="container text-color-special">
|
||||
<div class="row" {% if loop.index %2 == 1 %} style="flex-direction: row-reverse;" {% endif %}>
|
||||
<div class="mt-3 col image-min-dimensions">
|
||||
<img class="img-responsive w-100" src="{{ section['picture'] }}">
|
||||
</div>
|
||||
<div class="mt-3 col text-min-dimensions">
|
||||
<h1>{{ section['title'] }}</h1>
|
||||
<p class="mt-3">
|
||||
{{ section["text"] }}
|
||||
</p>
|
||||
{% if section["moreInfoButtonText"] %}
|
||||
<button type=button
|
||||
onclick="window.location.href='{{ section['moreInfoButtonHref'] }}'"
|
||||
class="mt-3 btn btn-light">{{ section["moreInfoButtonText"] }}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% include 'footer.html' %}
|
||||
</body>
|
||||
</html>
|
||||
61
templates/navbar.html
Normal file
61
templates/navbar.html
Normal file
@@ -0,0 +1,61 @@
|
||||
<nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark">
|
||||
<a class="navbar-brand" href="#"></a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbar"
|
||||
aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbar">
|
||||
|
||||
<!-- left side -->
|
||||
<ul class="navbar-nav mr-auto">
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/">Home</a>
|
||||
</li>
|
||||
|
||||
{% if conf["teamspeak-server"] %}
|
||||
<li class="nav-item right">
|
||||
<a class="nav-link" href="ts3server://{{ conf['teamspeak-server'] }}">Teamspeak</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if conf["discord-server"] %}
|
||||
<li class="nav-item right">
|
||||
<a class="nav-link" href="{{ conf['discord-server'] }}">Discord</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
</ul>
|
||||
|
||||
<!-- right side -->
|
||||
<ul class="navbar-nav">
|
||||
|
||||
{% if conf["instragram"] %}
|
||||
<li class="nav-item right">
|
||||
<a class="nav-link" href="{{ conf['instragram'] }}">Instagram</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if conf["facebook"] %}
|
||||
<li class="nav-item right">
|
||||
<a class="nav-link" href="{{ conf['facebook'] }}">Facebook</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if conf["twitter"] %}
|
||||
<li>
|
||||
<a class="nav-link" href="{{ conf['twitter'] }}">Twitter</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
</nav>
|
||||
<nav class="navbar navbar-dark bg-dark fake">
|
||||
<li class="navbar-nav">
|
||||
<a class="nav-link" href="#">placeholder</a>
|
||||
</li>
|
||||
</nav>
|
||||
28
templates/news.html
Normal file
28
templates/news.html
Normal file
@@ -0,0 +1,28 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
|
||||
{% include 'head.html' %}
|
||||
<title>{{ article["title"] }}</title>
|
||||
|
||||
</head>
|
||||
<body class="bg-special">
|
||||
<style>
|
||||
p{
|
||||
margin-top: 30px;
|
||||
}
|
||||
</style>
|
||||
{% include 'navbar.html' %}
|
||||
<div class="container mt-5 mb-5">
|
||||
<div class="row impressum mt-5"></div>
|
||||
<div class="col-lg-12" style="font-size: large;">
|
||||
<h3 mb-2>{{ article["parsed-time"].strftime("%d.%m. %Y") }}</h3>
|
||||
{{ article["markdown-content"] | safe }}
|
||||
<div class="pb-3"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'footer.html' %}
|
||||
|
||||
</body>
|
||||
</html>
|
||||
34
templates/people.html
Normal file
34
templates/people.html
Normal file
@@ -0,0 +1,34 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
|
||||
<title>Menschen</title>
|
||||
{% include 'head.html' %}
|
||||
|
||||
</head>
|
||||
<body class="bg-special">
|
||||
|
||||
{% include 'navbar.html' %}
|
||||
|
||||
<div class="container mt-5 mb-5">
|
||||
{% for p in people if not p.get("inactive") %}
|
||||
<div class="row impressum mt-5">
|
||||
<div class="col text-min-dimensions">
|
||||
<h2>{{ p["title"] }}</h2>
|
||||
<h4>{{ p["subtitle"] }}</h4>
|
||||
<p>
|
||||
{{ p["text"] }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="col image-min-dimensions">
|
||||
<img class="img-responsive w-100 image-max-dimensions"
|
||||
src="/static/pictures/{{ p['image'] }}"></img>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% include 'footer.html' %}
|
||||
|
||||
</body>
|
||||
</html>
|
||||
23
templates/subpage_example.html
Normal file
23
templates/subpage_example.html
Normal file
@@ -0,0 +1,23 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
|
||||
{% include 'head.html' %}
|
||||
<title>Stammtisch</title>
|
||||
|
||||
</head>
|
||||
<body class="h-100 bg-special">
|
||||
|
||||
{% include 'navbar.html' %}
|
||||
<div class="container mt-5 mb-5 h-100">
|
||||
<div class="row impressum mt-5"></div>
|
||||
<div class="col-lg-12">
|
||||
{% include 'stammtisch_text.html' %}
|
||||
<div class="pb-3"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'footer.html' %}
|
||||
|
||||
</body>
|
||||
</html>
|
||||
21
templates/subpage_example_text.html
Normal file
21
templates/subpage_example_text.html
Normal file
@@ -0,0 +1,21 @@
|
||||
<h1>HTML Ipsum</h1>
|
||||
|
||||
<p><strong>Pellentesque habitant morbi tristique</strong> senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. <em>Aenean ultricies mi vitae est.</em> Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, <code>commodo vitae</code>, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. <a href="#">Donec non enim</a> in turpis pulvinar facilisis. Ut felis.</p>
|
||||
|
||||
<h2>Header Level 2</h2>
|
||||
|
||||
<ol>
|
||||
<li>Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</li>
|
||||
<li>Aliquam tincidunt mauris eu risus.</li>
|
||||
</ol>
|
||||
|
||||
<blockquote><p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus magna. Cras in mi at felis aliquet congue. Ut a est eget ligula molestie gravida. Curabitur massa. Donec eleifend, libero at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. Vivamus pretium ornare est.</p></blockquote>
|
||||
|
||||
<h3>Header Level 3</h3>
|
||||
|
||||
<ul>
|
||||
<li>Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</li>
|
||||
<li>Aliquam tincidunt mauris eu risus.</li>
|
||||
</ul>
|
||||
|
||||
<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus</p>
|
||||
Reference in New Issue
Block a user