Add post voting
This commit is contained in:
+1
-1
@@ -34,7 +34,7 @@ request_timeout_sec 5
|
|||||||
access_log yes
|
access_log yes
|
||||||
|
|
||||||
# These are debug options
|
# These are debug options
|
||||||
show_io yes
|
show_io no
|
||||||
show_requests no
|
show_requests no
|
||||||
|
|
||||||
# Address and port the HTTP server will listen on.
|
# Address and port the HTTP server will listen on.
|
||||||
|
|||||||
+5
-3
@@ -3,7 +3,7 @@
|
|||||||
<title>Log in</title>
|
<title>Log in</title>
|
||||||
<link rel="stylesheet" media="screen" href="/static/global.css" />
|
<link rel="stylesheet" media="screen" href="/static/global.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body hx-boost="true">
|
||||||
<main>
|
<main>
|
||||||
<nav>
|
<nav>
|
||||||
<table>
|
<table>
|
||||||
@@ -20,11 +20,13 @@
|
|||||||
</table>
|
</table>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<form method="POST" action="/action/login">
|
<script src="/static/htmx.js"></script>
|
||||||
|
<div id="login-error-message"></div>
|
||||||
|
<form hx-post="/action/login" hx-target="#login-error-message">
|
||||||
<input type="text" name="name" placeholder="name" />
|
<input type="text" name="name" placeholder="name" />
|
||||||
<input type="password" name="pass" placeholder="pass" />
|
<input type="password" name="pass" placeholder="pass" />
|
||||||
<input type="submit" value="login" />
|
<input type="submit" value="login" />
|
||||||
</form>
|
</form>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+10
-9
@@ -3,7 +3,7 @@
|
|||||||
<title>{{title}}</title>
|
<title>{{title}}</title>
|
||||||
<link rel="stylesheet" media="screen" href="/static/global.css" />
|
<link rel="stylesheet" media="screen" href="/static/global.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body hx-boost="true">
|
||||||
<main>
|
<main>
|
||||||
<nav>
|
<nav>
|
||||||
<table>
|
<table>
|
||||||
@@ -26,8 +26,11 @@
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<article>
|
<article>
|
||||||
<h1>{{title}} (by {{author}})</h1>
|
|
||||||
<p>{{content}}</p>
|
<div class="post">
|
||||||
|
<h1>{{title}} (by {{author}})</h1>
|
||||||
|
<p>{{content}}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% if login %}
|
{% if login %}
|
||||||
<form action="/posts/{{id}}/comments" method="POST">
|
<form action="/posts/{{id}}/comments" method="POST">
|
||||||
@@ -36,14 +39,12 @@
|
|||||||
</form>
|
</form>
|
||||||
{% end %}
|
{% end %}
|
||||||
|
|
||||||
<table>
|
|
||||||
{% for comment in comments %}
|
{% for comment in comments %}
|
||||||
<tr>
|
<div class="comment">
|
||||||
<td>{{comment.author}}:</td>
|
<a href="/users/{{comment.author}}">{{comment.author}}</a>:
|
||||||
<td>{{comment.content}}</td>
|
{{comment.content}}
|
||||||
</tr>
|
</div>
|
||||||
{% end %}
|
{% end %}
|
||||||
</table>
|
|
||||||
</article>
|
</article>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
+45
-10
@@ -3,7 +3,7 @@
|
|||||||
<title>Posts</title>
|
<title>Posts</title>
|
||||||
<link rel="stylesheet" media="screen" href="/static/global.css" />
|
<link rel="stylesheet" media="screen" href="/static/global.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body hx-boost="true">
|
||||||
<main>
|
<main>
|
||||||
<nav>
|
<nav>
|
||||||
<table>
|
<table>
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
{% if login %}
|
{% if login %}
|
||||||
|
<td><a href="#" id="button-create-post">create post</a></td>
|
||||||
<td><a href="/users/{{login_username}}">{{login_username}}</a></td>
|
<td><a href="/users/{{login_username}}">{{login_username}}</a></td>
|
||||||
<td><a href="/action/logout">logout</a></td>
|
<td><a href="/action/logout">logout</a></td>
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -25,19 +26,53 @@
|
|||||||
</table>
|
</table>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{% if login %}
|
|
||||||
<form action="/action/post" method="POST">
|
|
||||||
<input type="text" name="title" placeholder="title" />
|
|
||||||
<textarea name="content" placeholder="content"></textarea>
|
|
||||||
<input type="submit" value="publish" />
|
|
||||||
</form>
|
|
||||||
{% end %}
|
|
||||||
|
|
||||||
{% for post in posts %}
|
{% for post in posts %}
|
||||||
<div class="post-preview">
|
<div class="post-preview">
|
||||||
<a href="/posts/{{post.id}}">{{post.title}}</a> by <a href="/users/{{post.author}}">{{post.author}}</a>
|
<div class="post-preview-title">
|
||||||
|
<a href="/posts/{{post.id}}">
|
||||||
|
{{post.title}}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="post-preview-details">
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a hx-post="/posts/{{post.id}}/upvotes" hx-target='.post-preview-details' hx-swap='outerHTML'>{{post.upvotes}}</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a hx-post="/posts/{{post.id}}/downvotes" hx-target='.post-preview-details' hx-swap='outerHTML'>{{post.downvotes}}</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span>by <a href="/users/{{post.author}}">{{post.author}}</a> at {{post.created_}}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span>{{post.num_comments}} comments</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% end %}
|
{% end %}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
{% if login %}
|
||||||
|
<dialog id="modal-create-post">
|
||||||
|
<form method="dialog" id="modal-create-post-close-dialog-form"></form>
|
||||||
|
<form action="/action/post" method="POST" id="modal-create-post-publish-form">
|
||||||
|
<input type="text" name="title" placeholder="title" />
|
||||||
|
<textarea name="content" placeholder="content"></textarea>
|
||||||
|
</form>
|
||||||
|
<button form="modal-create-post-close-dialog-form">cancel</button>
|
||||||
|
<input type="submit" value="publish" form="modal-create-post-publish-form" />
|
||||||
|
</dialog>
|
||||||
|
{% end %}
|
||||||
|
|
||||||
|
<script src="/static/htmx.js"></script>
|
||||||
|
<script>
|
||||||
|
const modal_create_post = document.querySelector("#modal-create-post");
|
||||||
|
const button_create_post = document.querySelector("#button-create-post")
|
||||||
|
|
||||||
|
button_create_post.onclick = function (event) { modal_create_post.showModal(); }
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
<title>Sign up</title>
|
<title>Sign up</title>
|
||||||
<link rel="stylesheet" media="screen" href="/static/global.css" />
|
<link rel="stylesheet" media="screen" href="/static/global.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body hx-boost="true">
|
||||||
<main>
|
<main>
|
||||||
<nav>
|
<nav>
|
||||||
<table>
|
<table>
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
<title>{{name}}'s profile</title>
|
<title>{{name}}'s profile</title>
|
||||||
<link rel="stylesheet" media="screen" href="/static/global.css" />
|
<link rel="stylesheet" media="screen" href="/static/global.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body hx-boost="true">
|
||||||
<main>
|
<main>
|
||||||
<nav>
|
<nav>
|
||||||
<table>
|
<table>
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
<title>Users</title>
|
<title>Users</title>
|
||||||
<link rel="stylesheet" media="screen" href="/static/global.css" />
|
<link rel="stylesheet" media="screen" href="/static/global.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body hx-boost="true">
|
||||||
<main>
|
<main>
|
||||||
<nav>
|
<nav>
|
||||||
<table>
|
<table>
|
||||||
|
|||||||
+164
-14
@@ -40,20 +40,38 @@ char schema[] =
|
|||||||
" bio TEXT\n"
|
" bio TEXT\n"
|
||||||
");\n"
|
");\n"
|
||||||
"CREATE TABLE IF NOT EXISTS Posts(\n"
|
"CREATE TABLE IF NOT EXISTS Posts(\n"
|
||||||
" id INTEGER PRIMARY KEY AUTOINCREMENT,\n"
|
" id INTEGER PRIMARY KEY AUTOINCREMENT,\n"
|
||||||
" title TEXT NOT NULL,\n"
|
" title TEXT NOT NULL,\n"
|
||||||
" content TEXT NOT NULL,\n"
|
" content TEXT NOT NULL,\n"
|
||||||
" author TEXT NOT NULL,\n"
|
" author TEXT NOT NULL,\n"
|
||||||
|
" created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,"
|
||||||
" FOREIGN KEY (author) REFERENCES Users(name)\n"
|
" FOREIGN KEY (author) REFERENCES Users(name)\n"
|
||||||
");\n"
|
");\n"
|
||||||
"CREATE TABLE IF NOT EXISTS Comments(\n"
|
"CREATE TABLE IF NOT EXISTS Comments(\n"
|
||||||
" id INTEGER PRIMARY KEY AUTOINCREMENT,\n"
|
" id INTEGER PRIMARY KEY AUTOINCREMENT,\n"
|
||||||
" content TEXT NOT NULL,\n"
|
" content TEXT NOT NULL,\n"
|
||||||
" author TEXT NOT NULL,\n"
|
" author TEXT NOT NULL,\n"
|
||||||
" parent INTEGER NOT NULL,\n"
|
" parent INTEGER NOT NULL,\n"
|
||||||
|
" created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,"
|
||||||
" FOREIGN KEY (author) REFERENCES Users(name),\n"
|
" FOREIGN KEY (author) REFERENCES Users(name),\n"
|
||||||
" FOREIGN KEY (parent) REFERENCES Posts(id)\n"
|
" FOREIGN KEY (parent) REFERENCES Posts(id)\n"
|
||||||
");\n"
|
");\n"
|
||||||
|
"CREATE TABLE IF NOT EXISTS PostVotes(\n"
|
||||||
|
" user TEXT,\n"
|
||||||
|
" post INTEGER,\n"
|
||||||
|
" up BOOLEAN NOT NULL,\n"
|
||||||
|
" PRIMARY KEY (user, post),\n"
|
||||||
|
" FOREIGN KEY (user) REFERENCES Users(name),\n"
|
||||||
|
" FOREIGN KEY (post) REFERENCES Posts(id)\n"
|
||||||
|
");\n"
|
||||||
|
"CREATE TABLE IF NOT EXISTS CommentVotes(\n"
|
||||||
|
" user TEXT,\n"
|
||||||
|
" comment INTEGER,\n"
|
||||||
|
" up BOOLEAN NOT NULL,\n"
|
||||||
|
" PRIMARY KEY (user, comment),\n"
|
||||||
|
" FOREIGN KEY (user) REFERENCES Users(name),\n"
|
||||||
|
" FOREIGN KEY (comment) REFERENCES Comments(id)\n"
|
||||||
|
");\n"
|
||||||
"PRAGMA foreign_keys = ON;\n";
|
"PRAGMA foreign_keys = ON;\n";
|
||||||
|
|
||||||
Session sessions[MAX_SESSIONS];
|
Session sessions[MAX_SESSIONS];
|
||||||
@@ -89,6 +107,128 @@ void free_endpoints(void)
|
|||||||
sqlite3_close(db);
|
sqlite3_close(db);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void respond_with_post_details_html(ResponseBuilder *b, int details_post_id)
|
||||||
|
{
|
||||||
|
sqlite3_stmt *stmt = sqlite3_utils_prepare(db,
|
||||||
|
"SELECT P.author, "
|
||||||
|
" STRFTIME('%d/%m/%Y, %H.%M', P.created) AS created_, "
|
||||||
|
" (SELECT COUNT(*) FROM Comments AS C WHERE C.parent = P.id) AS num_comments, "
|
||||||
|
" (SELECT COUNT(*) FROM PostVotes AS PV WHERE PV.post = P.id AND PV.up=1) AS upvotes, "
|
||||||
|
" (SELECT COUNT(*) FROM PostVotes AS PV WHERE PV.post = P.id AND PV.up=0) AS downvotes "
|
||||||
|
"FROM Posts AS P");
|
||||||
|
if (stmt == NULL) {
|
||||||
|
status_line(b, 500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
string author;
|
||||||
|
string created;
|
||||||
|
int num_comments;
|
||||||
|
int upvotes;
|
||||||
|
int downvotes;
|
||||||
|
if (sqlite3_utils_fetch(stmt, "ssiii", &author, &created, &num_comments, &upvotes, &downvotes)) {
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
status_line(b, 500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
status_line(b, 200);
|
||||||
|
append_content_f(b,
|
||||||
|
"<div class='post-preview-details'> \
|
||||||
|
<table> \
|
||||||
|
<tr> \
|
||||||
|
<td> \
|
||||||
|
<a hx-post='/posts/%d/upvotes' hx-target='.post-preview-details' hx-swap='outerHTML'>%d</a> \
|
||||||
|
</td> \
|
||||||
|
<td> \
|
||||||
|
<a hx-post='/posts/%d/downvotes' hx-target='.post-preview-details' hx-swap='outerHTML'>%d</a> \
|
||||||
|
</td> \
|
||||||
|
<td> \
|
||||||
|
<span>by <a href='/users/%.*s'>%.*s</a> at %.*s</span> \
|
||||||
|
</td> \
|
||||||
|
<td> \
|
||||||
|
<span>%d comments</span> \
|
||||||
|
</td> \
|
||||||
|
</tr> \
|
||||||
|
</table> \
|
||||||
|
</div>",
|
||||||
|
details_post_id, upvotes, details_post_id, downvotes,
|
||||||
|
(int) author.size, author.data,
|
||||||
|
(int) author.size, author.data,
|
||||||
|
(int) created.size, created.data,
|
||||||
|
num_comments);
|
||||||
|
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns true iff the request was handled
|
||||||
|
static bool voting_endpoints(Request request, ResponseBuilder *b, string login_username)
|
||||||
|
{
|
||||||
|
int vote_post_id;
|
||||||
|
|
||||||
|
if (!match_path_format(request.url.path, "/posts/:n/upvotes", &vote_post_id)) {
|
||||||
|
|
||||||
|
if (login_username.size == 0) {
|
||||||
|
status_line(b, 400);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int up = 1;
|
||||||
|
|
||||||
|
// Create the UP row
|
||||||
|
if (sqlite3_utils_exec(db, "INSERT INTO PostVotes(user, post, up) VALUES (:s, :i, :i)", login_username, vote_post_id, up)) {
|
||||||
|
respond_with_post_details_html(b, vote_post_id);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we failed, maybe we need to delete the current UP row
|
||||||
|
if (sqlite3_utils_exec(db, "DELETE FROM PostVotes WHERE up=:i AND user=:s AND post=:i", up, login_username, vote_post_id) && sqlite3_changes(db) > 0) {
|
||||||
|
respond_with_post_details_html(b, vote_post_id);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we failed, maybe we need to invert the UP row
|
||||||
|
if (sqlite3_utils_exec(db, "UPDATE PostVotes SET up=:i WHERE user=:s AND post=:i", up, login_username, vote_post_id)) {
|
||||||
|
respond_with_post_details_html(b, vote_post_id);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
respond_with_post_details_html(b, vote_post_id);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!match_path_format(request.url.path, "/posts/:n/downvotes", &vote_post_id)) {
|
||||||
|
|
||||||
|
if (login_username.size == 0) {
|
||||||
|
status_line(b, 400);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int up = 0;
|
||||||
|
|
||||||
|
// Create the UP row
|
||||||
|
if (sqlite3_utils_exec(db, "INSERT INTO PostVotes(user, post, up) VALUES (:s, :i, :i)", login_username, vote_post_id, up)) {
|
||||||
|
respond_with_post_details_html(b, vote_post_id);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we failed, maybe we need to delete the current UP row
|
||||||
|
if (sqlite3_utils_exec(db, "DELETE FROM PostVotes WHERE up=:i AND user=:s AND post=:i", up, login_username, vote_post_id) && sqlite3_changes(db) > 0) {
|
||||||
|
respond_with_post_details_html(b, vote_post_id);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we failed, maybe we need to invert the UP row
|
||||||
|
if (sqlite3_utils_exec(db, "UPDATE PostVotes SET up=:i WHERE user=:s AND post=:i", up, login_username, vote_post_id)) {
|
||||||
|
respond_with_post_details_html(b, vote_post_id);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
respond_with_post_details_html(b, vote_post_id);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
void respond(Request request, ResponseBuilder *b)
|
void respond(Request request, ResponseBuilder *b)
|
||||||
{
|
{
|
||||||
if (request.major != 1 || request.minor > 1) {
|
if (request.major != 1 || request.minor > 1) {
|
||||||
@@ -108,44 +248,48 @@ void respond(Request request, ResponseBuilder *b)
|
|||||||
add_header(b, LIT("Location: /"));
|
add_header(b, LIT("Location: /"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
char namebuf[MAX_USER_NAME];
|
char namebuf[MAX_USER_NAME];
|
||||||
char passbuf[MAX_USER_PASS];
|
char passbuf[MAX_USER_PASS];
|
||||||
string name; string pass;
|
string name; string pass;
|
||||||
if (!get_query_string_param(request.content, LIT("name"), LIT(namebuf), &name)) {
|
if (!get_query_string_param(request.content, LIT("name"), LIT(namebuf), &name)) {
|
||||||
status_line(b, 500);
|
status_line(b, 200);
|
||||||
append_content_s(b, LIT("Invalid name"));
|
append_content_s(b, LIT("Invalid name"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!get_query_string_param(request.content, LIT("pass"), LIT(passbuf), &pass)) {
|
if (!get_query_string_param(request.content, LIT("pass"), LIT(passbuf), &pass)) {
|
||||||
status_line(b, 500);
|
status_line(b, 200);
|
||||||
append_content_s(b, LIT("Invalid pass"));
|
append_content_s(b, LIT("Invalid pass"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
int res = sqlite3_utils_rows_exist(db, "SELECT name FROM Users WHERE name=:s AND pass=:s", name, pass);
|
int res = sqlite3_utils_rows_exist(db, "SELECT name FROM Users WHERE name=:s AND pass=:s", name, pass);
|
||||||
if (res == -1) {
|
if (res == -1) {
|
||||||
status_line(b, 500);
|
status_line(b, 200);
|
||||||
append_content_s(b, LIT("Internal error"));
|
append_content_s(b, LIT("Internal error"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (res == 1) {
|
if (res == 1) {
|
||||||
// No such user
|
// No such user
|
||||||
status_line(b, 400);
|
status_line(b, 200);
|
||||||
append_content_s(b, LIT("Invalid credentials"));
|
append_content_s(b, LIT("Invalid credentials"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
SessionID sessid = create_session(name);
|
SessionID sessid = create_session(name);
|
||||||
if (sessid == NO_SESSION) {
|
if (sessid == NO_SESSION) {
|
||||||
status_line(b, 500);
|
status_line(b, 200);
|
||||||
append_content_s(b, LIT("Internal error"));
|
append_content_s(b, LIT("Internal error"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// User exist
|
// User exist
|
||||||
status_line(b, 303);
|
status_line(b, 303);
|
||||||
add_header_f(b, "Set-Cookie: sessid=%d; Path=/", sessid);
|
add_header_f(b, "Set-Cookie: sessid=%d; Path=/", sessid);
|
||||||
add_header(b, LIT("Location: /"));
|
add_header(b, LIT("HX-Redirect: /"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (voting_endpoints(request, b, login_username))
|
||||||
|
return;
|
||||||
|
|
||||||
if (streq(request.url.path, LIT("/action/signup"))) {
|
if (streq(request.url.path, LIT("/action/signup"))) {
|
||||||
if (login_username.size > 0) {
|
if (login_username.size > 0) {
|
||||||
status_line(b, 303);
|
status_line(b, 303);
|
||||||
@@ -245,7 +389,13 @@ void respond(Request request, ResponseBuilder *b)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!match_path_format(request.url.path, "/posts")) {
|
if (!match_path_format(request.url.path, "/posts")) {
|
||||||
sqlite3_stmt *stmt = sqlite3_utils_prepare(db, "SELECT id, title, author FROM Posts");
|
sqlite3_stmt *stmt = sqlite3_utils_prepare(db,
|
||||||
|
"SELECT P.id, P.title, P.author, "
|
||||||
|
" STRFTIME('%d/%m/%Y, %H.%M', P.created) AS created_, "
|
||||||
|
" (SELECT COUNT(*) FROM Comments AS C WHERE C.parent = P.id) AS num_comments, "
|
||||||
|
" (SELECT COUNT(*) FROM PostVotes AS PV WHERE PV.post = P.id AND PV.up=1) AS upvotes, "
|
||||||
|
" (SELECT COUNT(*) FROM PostVotes AS PV WHERE PV.post = P.id AND PV.up=0) AS downvotes "
|
||||||
|
"FROM Posts AS P");
|
||||||
if (stmt == NULL) {
|
if (stmt == NULL) {
|
||||||
status_line(b, 500);
|
status_line(b, 500);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -138,7 +138,6 @@ static sqlite3_stmt *vprep(sqlite3 *handle, const char *fmt, va_list args)
|
|||||||
case 'i':
|
case 'i':
|
||||||
{
|
{
|
||||||
int v = va_arg(args, int);
|
int v = va_arg(args, int);
|
||||||
DEBUG("binding param %d to int %d\n", i+1, v);
|
|
||||||
code = sqlite3_bind_int (stmt, i+1, v);
|
code = sqlite3_bind_int (stmt, i+1, v);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -146,7 +145,6 @@ static sqlite3_stmt *vprep(sqlite3 *handle, const char *fmt, va_list args)
|
|||||||
case 's':
|
case 's':
|
||||||
{
|
{
|
||||||
string str = va_arg(args, string);
|
string str = va_arg(args, string);
|
||||||
DEBUG("binding param %d to str %.*s\n", i+1, (int) str.size, str.data);
|
|
||||||
code = sqlite3_bind_text(stmt, i+1, str.data, str.size, NULL);
|
code = sqlite3_bind_text(stmt, i+1, str.data, str.size, NULL);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -155,7 +153,6 @@ static sqlite3_stmt *vprep(sqlite3 *handle, const char *fmt, va_list args)
|
|||||||
{
|
{
|
||||||
void *ptr = va_arg(args, void*);
|
void *ptr = va_arg(args, void*);
|
||||||
size_t len = va_arg(args, size_t);
|
size_t len = va_arg(args, size_t);
|
||||||
DEBUG("binding param %d to blob %p %d\n", i+1, ptr, (int) len);
|
|
||||||
code = sqlite3_bind_blob(stmt, i+1, ptr, len, NULL);
|
code = sqlite3_bind_blob(stmt, i+1, ptr, len, NULL);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|||||||
+2
-2
@@ -84,7 +84,7 @@ bool append_template(ResponseBuilder *b, string file, TemplateParam *params)
|
|||||||
|
|
||||||
status = tinytemplate_compile(template_str.data, template_str.size, program, COUNTOF(program), &num_instr, errmsg, sizeof(errmsg));
|
status = tinytemplate_compile(template_str.data, template_str.size, program, COUNTOF(program), &num_instr, errmsg, sizeof(errmsg));
|
||||||
if (status != TINYTEMPLATE_STATUS_DONE) {
|
if (status != TINYTEMPLATE_STATUS_DONE) {
|
||||||
log_data(STR(errmsg));
|
log_format("Template Compile Error: %s", errmsg);
|
||||||
myfree(template_str.data, template_str.size);
|
myfree(template_str.data, template_str.size);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -94,7 +94,7 @@ bool append_template(ResponseBuilder *b, string file, TemplateParam *params)
|
|||||||
context.params = params;
|
context.params = params;
|
||||||
status = tinytemplate_eval(template_str.data, program, &context, template_param_callback, template_output_callback, errmsg, sizeof(errmsg));
|
status = tinytemplate_eval(template_str.data, program, &context, template_param_callback, template_output_callback, errmsg, sizeof(errmsg));
|
||||||
if (status != TINYTEMPLATE_STATUS_DONE)
|
if (status != TINYTEMPLATE_STATUS_DONE)
|
||||||
log_data(STR(errmsg));
|
log_format("Template Runtime Error: %s", errmsg);
|
||||||
myfree(template_str.data, template_str.size);
|
myfree(template_str.data, template_str.size);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
+82
-4
@@ -1,3 +1,7 @@
|
|||||||
|
body {
|
||||||
|
background-color: #282a36;
|
||||||
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
width: 600px;
|
width: 600px;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
@@ -5,22 +9,39 @@ main {
|
|||||||
|
|
||||||
nav {
|
nav {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
background: #fab8b8;
|
|
||||||
border: 1px solid #c16464;
|
|
||||||
border-radius: 3px;
|
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
nav table {
|
nav table {
|
||||||
float: left;
|
float: left;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
}
|
}
|
||||||
|
|
||||||
nav table:last-child {
|
nav table:last-child {
|
||||||
float: right;
|
float: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
nav table td {
|
nav table td {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nav table td a {
|
||||||
|
color: white;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
::backdrop {
|
||||||
|
background-image: linear-gradient(
|
||||||
|
45deg,
|
||||||
|
magenta,
|
||||||
|
rebeccapurple,
|
||||||
|
dodgerblue,
|
||||||
|
green
|
||||||
|
);
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
|
||||||
form {
|
form {
|
||||||
width: calc(100% - 20px);
|
width: calc(100% - 20px);
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
@@ -29,6 +50,7 @@ form {
|
|||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
border: 1px solid #76c97d;
|
border: 1px solid #76c97d;
|
||||||
}
|
}
|
||||||
|
|
||||||
form input,
|
form input,
|
||||||
form textarea {
|
form textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -37,18 +59,33 @@ form textarea {
|
|||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
border: 1px solid #76c97d;
|
border: 1px solid #76c97d;
|
||||||
}
|
}
|
||||||
|
|
||||||
form input:last-child,
|
form input:last-child,
|
||||||
form textarea:last-child {
|
form textarea:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
form textarea {
|
form textarea {
|
||||||
height: 150px;
|
height: 150px;
|
||||||
}
|
}
|
||||||
|
|
||||||
form input[type=submit] {
|
form input[type=submit] {
|
||||||
width: 200px;
|
width: 200px;
|
||||||
float: right;
|
float: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#login-error-message {
|
||||||
|
padding: 10px;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid red;
|
||||||
|
border-radius: 3px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#login-error-message:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.user-preview {
|
.user-preview {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
@@ -59,8 +96,49 @@ form input[type=submit] {
|
|||||||
|
|
||||||
.post-preview {
|
.post-preview {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-preview-title {
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-preview-title a {
|
||||||
|
color: white;
|
||||||
|
font-size: 30px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-preview-details {
|
||||||
|
border-top: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-preview-details table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-preview-details table td {
|
||||||
|
border-right: 1px solid #ccc;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-preview-details span,
|
||||||
|
.post-preview-details a {
|
||||||
|
color: #ccc;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post {
|
||||||
|
padding: 10px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 3px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
background: #ccd6fb;
|
background: #ccd6fb;
|
||||||
border: 1px solid #6478c1;
|
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
+3925
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user