Created templated web sqlite bootstrap project.

This commit is contained in:
Relintai 2024-02-26 19:12:25 +01:00
parent ed5cdffdbb
commit f5319e3858
17 changed files with 13830 additions and 0 deletions

View File

@ -0,0 +1,10 @@
extends HTTPSessionManagerDB
func _ready() -> void:
DatabaseManager.connect("initialized", self, "on_databases_initialized", [], CONNECT_ONESHOT)
# You could also connect to the migration signal, and write migrations if you need them
func on_databases_initialized() -> void:
# Load sessions after the databases are initialized
# This happens on the Main node.
call_deferred("load_sessions")

View File

@ -0,0 +1,172 @@
extends UserLoginWebPage
var _login_validator : FormValidator = null
#func _render_index(request: WebServerRequest) -> void:
# request.set_post_parameter("username", request.get_post_parameter("username").to_upper())
#
# ._render_index(request)
func log_login_error(uname_val : String, error_str : String) -> void:
PLogger.log_error("{0}: User login error! name: \"{1}\", error str: \"{2}\"!".format([ Time.get_datetime_string_from_system(), uname_val, error_str ]))
printerr("{0}: User login error! name: \"{1}\", error str: \"{2}\"!".format([ Time.get_datetime_string_from_system(), uname_val, error_str]))
func log_login_success(uname_val : String) -> void:
PLogger.log_error("{0}: User login success! name: \"{1}\"!".format([ Time.get_datetime_string_from_system(), uname_val ]))
printerr("{0}: User login success! name: \"{1}\"!".format([ Time.get_datetime_string_from_system(), uname_val ]))
func _render_index(request: WebServerRequest) -> void:
#request.set_post_parameter("username", request.get_post_parameter("username").to_upper())
var error_str : String
var uname_val : String
var email_val : String
var pass_val : String
var pass_check_val : String
if (request.get_method() == HTTPServerEnums.HTTP_METHOD_POST):
var errors : PoolStringArray = _login_validator.validate(request);
for i in range(errors.size()):
error_str += errors[i] + "<br>";
uname_val = request.get_parameter("username")#.to_upper();
pass_val = request.get_parameter("password");
var user : User = UserDB.get_user_name(uname_val);
if (user):
if (!user.check_password(pass_val)):
error_str += "Invalid username or password!";
else:
var session : HTTPSession = request.get_or_create_session();
session.add("user_id", user.get_user_id());
request.get_server().get_session_manager().save_session(session)
var c : WebServerCookie = WebServerCookie.new()
c.set_data("session_id", session.get_session_id());
c.set_path("/");
c.http_only = true
c.secure = false
#c.use_expiry_date = false
#c.same_site = WebServerCookie.SAME_SITE_LAX
request.response_add_cookie(c);
log_login_success(uname_val)
emit_signal("user_logged_in", request, user);
var d : Dictionary = Dictionary()
d["type"] = "render_login_success";
d["user"] = user;
_render_user_page(request, d);
return;
else:
error_str += "Invalid username or password!";
if !error_str.empty():
log_login_error(uname_val, error_str)
var d : Dictionary = Dictionary()
d["type"] = "render_login_request_default";
d["error_str"] = error_str;
d["uname_val"] = uname_val;
d["pass_val"] = pass_val;
_render_user_page(request, d);
func _render_user_page(request: WebServerRequest, data: Dictionary) -> void:
var type : String = data["type"]
if type == "render_login_success":
request.send_redirect(redirect_on_success_url)
return
var b : HTMLBuilder = HTMLBuilder.new()
# Title
b.div("row mb-4")
b.div("col-2")
b.cdiv()
b.div("col-8")
b.h2()
b.w("Login")
b.ch2()
b.cdiv()
b.div("col-2")
b.cdiv()
b.cdiv()
# Errors
var error_str : String = data["error_str"]
if !error_str.empty():
b.div("row mb-4")
b.div("col-2")
b.cdiv()
b.div("col-8")
b.div("alert alert-danger").attrib("role", "alert")
b.w(error_str)
b.cdiv()
b.cdiv()
b.div("col-2")
b.cdiv()
b.cdiv()
# Form
b.div("row")
b.div("col-2")
b.cdiv()
b.div("col-8")
if true:
b.form().method_post()
b.csrf_tokenr(request)
b.div("form-group")
b.label().fora("username_input").cls("form_label").f().w("Username").clabel()
b.input_text("username", data["uname_val"], "", "form-control", "username_input")
b.cdiv()
b.div("form-group")
b.label().fora("password_input").cls("form_label").f().w("Password").clabel()
b.input_password("password", "", "*******", "form-control", "password_input")
b.cdiv()
b.button().type("submit").cls("btn btn-outline-primary mt-3").f().w("Send").cbutton()
b.cform()
b.cdiv()
b.div("col-2")
b.cdiv()
b.cdiv()
b.write_tag()
request.body += b.result
request.compile_and_send_body()
func _ready() -> void:
_login_validator = FormValidator.new()
_login_validator.new_field("username", "Username").need_to_exist().need_to_be_alpha_numeric().need_minimum_length(5).need_maximum_length(20);
var pw : FormField = _login_validator.new_field("password", "Password");
pw.need_to_exist();
pw.need_to_have_lowercase_character().need_to_have_uppercase_character();
pw.need_minimum_length(5);

View File

@ -0,0 +1,35 @@
extends Node
export(String) var database_location : String = "user://database.sqlite"
func _ready() -> void:
var d : Directory = Directory.new()
var bd : String = database_location.get_base_dir()
var loc : String = d.get_filesystem_abspath_for(bd).append_path(database_location.get_file())
var file : File = File.new()
if !file.file_exists(loc):
PLogger.log_message("Database file doesn't exists, will run migrations!")
call_deferred("migrate")
else:
call_deferred("db_initialized")
var db : SQLite3Database = SQLite3Database.new()
db.connection_string = loc
DatabaseManager.add_database(db)
func migrate() -> void:
PLogger.log_message("Running migrations!")
DatabaseManager.connect("migration", self, "_migration")
DatabaseManager.migrate(true, false, 0)
call_deferred("db_initialized")
func db_initialized() -> void:
DatabaseManager.initialized()
func _migration(clear: bool, should_seed: bool, pseed: int) -> void:
#create admin account
pass

View File

@ -0,0 +1,98 @@
[gd_scene load_steps=11 format=2]
[ext_resource path="res://WebServerSimple.gd" type="Script" id=1]
[ext_resource path="res://WebRoot.gd" type="Script" id=2]
[ext_resource path="res://HTTPSessionManagerDB.gd" type="Script" id=3]
[ext_resource path="res://Main.gd" type="Script" id=4]
[ext_resource path="res://Login.gd" type="Script" id=5]
[ext_resource path="res://Register.gd" type="Script" id=6]
[ext_resource path="res://Settings.gd" type="Script" id=7]
[sub_resource type="SessionSetupWebServerMiddleware" id=3]
[sub_resource type="UserSessionSetupWebServerMiddleware" id=4]
[sub_resource type="CSRFTokenWebServerMiddleware" id=5]
ignored_urls = PoolStringArray( "/user/login", "/user/register" )
[node name="Main" type="Node"]
script = ExtResource( 4 )
[node name="UserManagerDB" type="UserManagerDB" parent="."]
[node name="WebServerSimple" type="WebServerSimple" parent="."]
script = ExtResource( 1 )
[node name="WebRoot" type="WebRoot" parent="WebServerSimple"]
www_root_path = "res://www/"
middlewares = [ SubResource( 3 ), SubResource( 4 ), SubResource( 5 ) ]
script = ExtResource( 2 )
[node name="StaticWebPage" type="StaticWebPage" parent="WebServerSimple/WebRoot"]
uri_segment = "/"
data = "<div class=\"row\">
<div class=\"col-2\"></div>
<div class=\"col-8\">
You can go and log in on the users page here: <a href=\"/user/login\">Login</a><br>
<br>
Note that in this demo sessions and users are saved in an SQLite database here: \"user://database.sqlite\"<br>
<br>
There are no users by default.<br>
<br>
</div>
<div class=\"col-2\"></div>
</div>"
[node name="UserWebPage" type="UserWebPage" parent="WebServerSimple/WebRoot"]
uri_segment = "user"
logged_out_render_type = 1
logged_out_redirect_url = "/user/login"
[node name="Login" type="UserLoginWebPage" parent="WebServerSimple/WebRoot/UserWebPage"]
uri_segment = "login"
logged_in_render_type = 1
logged_in_redirect_url = "/"
script = ExtResource( 5 )
[node name="Register" type="UserRegisterWebPage" parent="WebServerSimple/WebRoot/UserWebPage"]
uri_segment = "register"
logged_in_render_type = 1
logged_in_redirect_url = "/"
redirect_on_success_url = "/user/login"
script = ExtResource( 6 )
[node name="Logout" type="UserLogoutWebPage" parent="WebServerSimple/WebRoot/UserWebPage"]
uri_segment = "logout"
logged_out_render_type = 1
logged_out_redirect_url = "/"
[node name="Settings" type="UserSettingsWebPage" parent="WebServerSimple/WebRoot/UserWebPage"]
uri_segment = "settings"
logged_out_render_type = 1
logged_out_redirect_url = "/user/login"
script = ExtResource( 7 )
[node name="HTTPSessionManagerDB" type="HTTPSessionManagerDB" parent="WebServerSimple"]
script = ExtResource( 3 )
[node name="PanelContainer" type="PanelContainer" parent="."]
anchor_right = 1.0
anchor_bottom = 1.0
[node name="VBoxContainer" type="VBoxContainer" parent="PanelContainer"]
margin_left = 7.0
margin_top = 7.0
margin_right = 1017.0
margin_bottom = 593.0
alignment = 1
[node name="LinkButton" type="LinkButton" parent="PanelContainer/VBoxContainer"]
margin_left = 391.0
margin_top = 286.0
margin_right = 619.0
margin_bottom = 300.0
size_flags_horizontal = 4
text = "Click to open http://127.0.0.1:8080/"
uri = "http://127.0.0.1:8080/"
[connection signal="user_registered" from="WebServerSimple/WebRoot/UserWebPage/Register" to="WebServerSimple/WebRoot/UserWebPage/Register" method="_on_Register_user_registered"]

View File

@ -0,0 +1,252 @@
extends UserRegisterWebPage
var _registration_validator : FormValidator = null
func log_registration_error(uname_val : String, email_val : String, error_str : String) -> void:
PLogger.log_error("{0}: User registration error! name: \"{1}\", email: \"{2}\", error str: \"{3}\"!".format([ Time.get_datetime_string_from_system(), uname_val, email_val, error_str ]))
printerr("{0}: User registration error! name: \"{1}\", email: \"{2}\", error str: \"{3}\"!".format([ Time.get_datetime_string_from_system(), uname_val, email_val, error_str]))
func log_registration_success(uname_val : String, email_val : String) -> void:
PLogger.log_error("{0}: User registration success! name: \"{1}\", email: \"{2}\"!".format([ Time.get_datetime_string_from_system(), uname_val, email_val ]))
printerr("{0}: User registration success! name: \"{1}\", email: \"{2}\"!".format([ Time.get_datetime_string_from_system(), uname_val, email_val ]))
func _render_index(request: WebServerRequest) -> void:
var error_str : String
var uname_val : String
var email_val : String
var pass_val : String
var pass_check_val : String
if (request.get_method() == HTTPServerEnums.HTTP_METHOD_POST):
var errors : PoolStringArray = _registration_validator.validate(request);
for i in range(errors.size()):
error_str += errors[i] + "<br>";
uname_val = request.get_parameter("username");
#uname_val = uname_val.to_upper()
email_val = request.get_parameter("email");
pass_val = request.get_parameter("password");
pass_check_val = request.get_parameter("password_check");
# todo username length etc check
# todo pw length etc check
if (UserDB.is_username_taken(uname_val)):
error_str += "Username already taken!<br>";
if (UserDB.is_email_taken(email_val)):
error_str += "Email already in use!<br>";
if (pass_val != pass_check_val):
error_str += "The passwords did not match!<br>";
if (error_str.empty()):
var user : User = null
user = UserDB.create_user();
user.set_user_name(uname_val);
user.set_email(email_val);
user.create_password(pass_val);
user.save();
emit_signal("user_registered", request, user);
var d : Dictionary = Dictionary()
d["type"] = "render_register_success";
d["user"] = user;
log_registration_success(uname_val, email_val)
_render_user_page(request, d);
return;
else:
log_registration_error(uname_val, email_val, error_str)
var d : Dictionary = Dictionary()
d["type"] = "render_register_request_default";
d["error_str"] = error_str;
d["uname_val"] = uname_val;
d["email_val"] = email_val;
d["pass_val"] = pass_val;
d["pass_check_val"] = pass_check_val;
_render_user_page(request, d);
func _render_user_page(request: WebServerRequest, data: Dictionary) -> void:
if data["type"] == "render_register_success":
render_register_success(request, data)
return
render_register_default(request, data)
func render_register_success(request: WebServerRequest, data: Dictionary) -> void:
var b : HTMLBuilder = HTMLBuilder.new()
# Title
b.div("row mb-4")
b.div("col-2")
b.cdiv()
b.div("col-8")
b.h2()
b.w("Registration successful!")
b.ch2()
b.cdiv()
b.div("col-2")
b.cdiv()
b.cdiv()
# msg
b.div("row")
b.div("col-2")
b.cdiv()
b.div("col-8")
b.w("Login Here:").br()
b.br()
b.a(redirect_on_success_url)
b.w(">> Login <<")
b.ca()
b.cdiv()
b.div("col-2")
b.cdiv()
b.cdiv()
b.write_tag()
request.body += b.result
request.compile_and_send_body()
func render_register_default(request: WebServerRequest, data: Dictionary) -> void:
var b : HTMLBuilder = HTMLBuilder.new()
# Title
b.div("row mb-4")
b.div("col-2")
b.cdiv()
b.div("col-8")
b.h2()
b.w("Registration")
b.ch2()
b.cdiv()
b.div("col-2")
b.cdiv()
b.cdiv()
# Errors
var error_str : String = data["error_str"]
if !error_str.empty():
b.div("row mb-4")
b.div("col-2")
b.cdiv()
b.div("col-8")
b.div("alert alert-danger").attrib("role", "alert")
b.w(error_str)
b.cdiv()
b.cdiv()
b.div("col-2")
b.cdiv()
b.cdiv()
# Form
b.div("row")
b.div("col-2")
b.cdiv()
b.div("col-8")
if true:
b.form().method_post()
b.csrf_tokenr(request)
b.div("form-group")
b.label().fora("username_input").cls("form_label").f().w("Username").clabel()
b.input_text("username", data["uname_val"], "", "form-control", "username_input")
b.cdiv()
b.div("form-group")
b.label().fora("email_input").cls("form_label").f().w("Email").clabel()
b.input_text("email", data["email_val"], "", "form-control", "email_input")
b.cdiv()
b.div("form-group")
b.label().fora("password_input").cls("form_label").f().w("Password").clabel()
b.input_password("password", "", "*******", "form-control", "password_input")
b.cdiv()
b.div("form-group")
b.label().fora("password_check_input").cls("form_label").f().w("Password again").clabel()
b.input_password("password_check", "", "*******", "form-control", "password_check_input")
b.cdiv()
b.button().type("submit").cls("btn btn-outline-primary mt-3").f().w("Register").cbutton()
b.cform()
b.cdiv()
b.div("col-2")
b.cdiv()
b.cdiv()
b.write_tag()
request.body += b.result
request.compile_and_send_body()
func _on_Register_user_registered(request: WebServerRequest, user: User) -> void:
user.read_lock()
var uid : int = user.user_id
user.read_unlock()
user.write_lock()
user.rank = 1
user.write_unlock()
user.save()
# if uid == 1:
# user.write_lock()
# user.rank = 3
# user.write_unlock()
# user.save()
# else:
# var ulevel : int = ProjectSettings.get("app/registration_start_user_level")
# user.write_lock()
# user.rank = ulevel
# user.write_unlock()
# user.save()
func _ready() -> void:
_registration_validator = FormValidator.new()
_registration_validator.new_field("username", "Username").need_to_exist().need_to_be_alpha_numeric().need_minimum_length(5).need_maximum_length(20);
_registration_validator.new_field("email", "Email").need_to_exist().need_to_be_email();
var pw : FormField = _registration_validator.new_field("password", "Password");
pw.need_to_exist();
pw.need_to_have_lowercase_character().need_to_have_uppercase_character();
pw.need_minimum_length(5);
_registration_validator.new_field("password_check", "Password check").need_to_match("password");
_registration_validator.new_field("email", "Email").need_to_exist().need_to_be_email();

View File

@ -0,0 +1,153 @@
extends UserSettingsWebPage
class SettingsRequestData:
var error_str : String = ""
var pass_val : String = ""
var pass_check_val : String = ""
var _profile_validator : FormValidator = null
func _render_index(request : WebServerRequest) -> void:
var user : User = request.get_meta("user");
if !user:
PLogger.log_error("UserSettingsWebPage _render_index !user")
return
var data : SettingsRequestData = SettingsRequestData.new()
if request.get_method() == HTTPServerEnums.HTTP_METHOD_POST:
data.pass_val = request.get_parameter("password");
data.pass_check_val = request.get_parameter("password_check");
var changed : bool = false;
var errors : PoolStringArray = _profile_validator.validate(request);
for i in range(errors.size()):
data.error_str += errors[i] + "<br>";
if (errors.size() == 0):
if (data.pass_val != ""):
if (data.pass_val != data.pass_check_val):
data.error_str += "The passwords did not match!<br>";
else:
user.create_password(data.pass_val);
changed = true;
if (changed):
user.save();
emit_signal("user_settings_changed", request, user);
var d : Dictionary = Dictionary()
d["user"] = user;
d["error_str"] = data.error_str;
d["pass_val"] = data.pass_val;
d["pass_check_val"] = data.pass_check_val;
_render_user_page(request, d);
func _render_user_page(request: WebServerRequest, data: Dictionary) -> void:
#print(data)
var user : User = data["user"]
var b : HTMLBuilder = HTMLBuilder.new()
# Title
b.div("row mb-4")
b.div("col-2")
b.cdiv()
b.div("col-8")
b.h2()
b.w("User Settings")
b.ch2()
b.cdiv()
b.div("col-2")
b.cdiv()
b.cdiv()
# Errors
var error_str : String = data["error_str"]
if !error_str.empty():
b.div("row mb-4")
b.div("col-2")
b.cdiv()
b.div("col-8")
b.div("alert alert-danger").attrib("role", "alert")
b.w(error_str)
b.cdiv()
b.cdiv()
b.div("col-2")
b.cdiv()
b.cdiv()
if error_str.empty() && request.get_method() == HTTPServerEnums.HTTP_METHOD_POST:
b.div("row mb-4")
b.div("col-2")
b.cdiv()
b.div("col-8")
b.div("alert alert-success").attrib("role", "alert")
b.w("Save successful!")
b.cdiv()
b.cdiv()
b.div("col-2")
b.cdiv()
b.cdiv()
# Form
b.div("row")
b.div("col-2")
b.cdiv()
b.div("col-8")
if true:
b.form().method_post()
b.csrf_tokenr(request)
b.div("form-group")
b.label().fora("password_input").cls("form_label").f().w("New Password").clabel()
b.input_password("password", "", "*******", "form-control", "password_input")
b.cdiv()
b.div("form-group")
b.label().fora("password_check_input").cls("form_label").f().w("New Password again").clabel()
b.input_password("password_check", "", "*******", "form-control", "password_check_input")
b.cdiv()
b.button().type("submit").cls("btn btn-outline-primary mt-3").f().w("Save").cbutton()
b.cform()
b.cdiv()
b.div("col-2")
b.cdiv()
b.cdiv()
b.write_tag()
request.body += b.result
request.compile_and_send_body()
func _ready() -> void:
_profile_validator = FormValidator.new()
var pw : FormField = _profile_validator.new_field("password", "Password");
pw.ignore_if_not_exists();
pw.need_to_have_lowercase_character().need_to_have_uppercase_character();
pw.need_minimum_length(5);
_profile_validator.new_field("password_check", "Password check").ignore_if_other_field_not_exists("password").need_to_match("password");

View File

@ -0,0 +1,89 @@
extends WebRoot
var header : String
var footer : String
func _ready() -> void:
var b : HTMLBuilder = HTMLBuilder.new()
b.meta().charset_utf_8()
b.meta().attrib("name", "viewport").attrib("content", "width=device-width, initial-scale=1, shrink-to-fit=no")
b.link().rel("stylesheet").type("text/css").href("/css/bootstrap.min.css")
b.link().rel("stylesheet").type("text/css").href("/css/main.css")
b.write_tag()
header = b.result
b.result = ""
b.cdiv()
b.ctag("main")
b.script().src("/js/jquery-3.3.1.js").f().cscript()
b.script().src("/js/popper.js").f().cscript()
b.script().src("/js/bootstrap.min.js").f().cscript()
b.write_tag()
footer = b.result
func _render_main_menu(request: WebServerRequest) -> void:
request.head = header
var user : User = request.get_meta("user", null)
var b : HTMLBuilder = HTMLBuilder.new()
b.nav().cls("navbar navbar-expand-lg navbar-light bg-light")
if true:
b.a("/", "navbar-brand").f().w("USTB").ca()
b.button().cls("navbar-toggler").type("button").attrib("data-toggle", "collapse").attrib("data-target", "#navbarSupportedContent").attrib("aria-controls", "navbarSupportedContent").attrib("aria-expanded", "false").attrib("aria-label", "Toggle navigation")
b.span().cls("navbar-toggler-icon").f().cspan()
b.cbutton()
b.div("collapse navbar-collapse", "navbarSupportedContent")
b.ul().cls("navbar-nav mr-auto")
if true:
b.li().cls("nav-item")
b.a("/", "nav-link").f().w("Index").ca()
b.cli()
if user:
b.li().cls("nav-item")
b.a("/user/settings", "nav-link").f().w("User Settings").ca()
b.cli()
b.li().cls("nav-item")
b.a("/user/logout", "nav-link").f().w("Logout").ca()
b.cli()
b.li().cls("nav-item")
#b.a("/user/settings", "nav-link").f().w("(Logged in as " + user.user_name + ")!").ca()
b.a("", "nav-link").f().w("Logged in as: " + user.user_name + "!").ca()
b.cli()
else:
b.li().cls("nav-item")
b.a("/user/login", "nav-link").f().w("Login").ca()
b.cli()
b.li().cls("nav-item")
b.a("/user/register", "nav-link").f().w("Register").ca()
b.cli()
b.cul()
b.cdiv()
b.cnav()
b.tag("main").cls("mt-5")
b.div("container-fluid")
b.write_tag()
request.body += b.result
request.footer = footer

View File

@ -0,0 +1,16 @@
extends WebServerSimple
# Declare member variables here. Examples:
# var a: int = 2
# var b: String = "text"
# Called when the node enters the scene tree for the first time.
func _ready() -> void:
start()
# Called every frame. 'delta' is the elapsed time since the previous frame.
#func _process(delta: float) -> void:
# pass

View File

@ -0,0 +1,7 @@
[gd_resource type="Environment3D" load_steps=2 format=2]
[sub_resource type="ProceduralSky" id=1]
[resource]
background_mode = 2
background_sky = SubResource( 1 )

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -0,0 +1,35 @@
[remap]
importer="texture"
type="StreamTexture"
path="res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://icon.png"
dest_files=[ "res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex" ]
[params]
compress/mode=0
compress/lossy_quality=0.7
compress/hdr_mode=0
compress/bptc_ldr=0
compress/normal_map=0
flags/repeat=0
flags/filter=true
flags/mipmaps=false
flags/anisotropic=false
flags/srgb=2
process/fix_alpha_border=true
process/premult_alpha=false
process/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false
stream=false
size_limit=0
detect_3d=true
svg/scale=1.0

View File

@ -0,0 +1,25 @@
; Engine configuration file.
; It's best edited using the editor UI and not directly,
; since the parameters that go here are not all obvious.
;
; Format:
; [section] ; section goes between []
; param=value ; assign values to parameters
config_version=4
[application]
config/name="User SQLite Themed Bootstrap"
run/main_scene="res://Main.tscn"
config/icon="res://icon.png"
[physics]
common/enable_pause_aware_picking=true
[rendering]
vram_compression/import_etc=true
vram_compression/import_etc2=false
environment/default_environment="res://default_env.tres"

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,32 @@
ul.pagination {
padding: 1em 1em 1em 1em;
margin: 0;
text-align: center;
}
ul.pagination li {
display: inline;
border: 1px solid black;
padding: 0.2em 0em;
margin: 0em 0.2em 0em 0.2em;
}
ul.pagination li a {
color: black;
text-align: center;
text-decoration: none;
padding: 0.2em 0.4em;
}
ul.pagination li.disabled {
color: rgb(138, 138, 138);
text-align: center;
text-decoration: none;
padding: 0.2em 0.4em;
}
ul.pagination li a:hover {
background-color: rgb(60, 60, 60);
color: white;
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff