This commit is contained in:
2025-09-21 11:21:55 +02:00
parent c4c8e56e48
commit c0f21d0ce0
10 changed files with 1136 additions and 0 deletions
+400
View File
@@ -0,0 +1,400 @@
#include "cweb.h"
#if 0
#define WL_STR(X) ((WL_String) { (X), (int) sizeof(X)-1})
#define HTML_STR(X) html_str(HTTP_STR(#X))
static HTTP_String html_str(HTTP_String str)
{
str = cweb_trim(str);
if (str.len > 0 && str.ptr[0] == '(') {
str.ptr++;
str.len--;
}
if (str.len > 0 && str.ptr[str.len-1] == ')')
str.len--;
return str;
}
int load_file(char *file, char **data, long *size)
{
FILE *f = fopen(file, "rb");
if (f == NULL) return -1;
fseek(f, 0, SEEK_END);
*size = ftell(f);
fseek(f, 0, SEEK_SET);
*data = malloc(*size + 1);
fread(*data, 1, *size, f);
(*data)[*size] = '\0';
fclose(f);
return 0;
}
static void *alloc(WL_Arena *arena, int num, int align)
{
int pad = -(uintptr_t) (arena->ptr + arena->cur) & (align-1);
if (arena->len - arena->cur < num + pad)
return NULL;
void *ptr = arena->ptr + arena->cur + pad;
arena->cur += num + pad;
return ptr;
}
static bool is_hex_digit(char c)
{
return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f');
}
static int hex_digit_to_int(char c)
{
if (c >= 'A' && c <= 'F')
return c - 'A' + 10;
if (c >= 'a' && c <= 'f')
return c - 'a' + 10;
return c - '0';
}
#define USERNAME_LIMIT 64
bool valid_name(HTTP_String str)
{
(void) str; // TODO
return true;
}
bool valid_email(HTTP_String str)
{
(void) str; // TODO
return true;
}
bool valid_pass(HTTP_String str)
{
(void) str; // TODO
return true;
}
bool valid_post_title(HTTP_String str)
{
(void) str; // TODO
return true;
}
bool valid_post_content(HTTP_String str)
{
(void) str; // TODO
return true;
}
bool valid_link(HTTP_String str)
{
(void) str;
return true;
}
bool valid_comment_content(HTTP_String str)
{
(void) str;
return true;
}
#endif
static int user_exists(CWEB *cweb, HTTP_String name, HTTP_String pass)
{
CWEB_QueryResult res = cweb_database_select(cweb, "SELECT id, hash FROM Users WHERE username=?", name);
int64_t user_id;
CWEB_PasswordHash hash;
int ret = cweb_next_query_row(&res, &user_id, &hash);
if (ret < 0) {
cweb_free_query_result(&res);
return -1;
}
if (ret == 0) {
cweb_free_query_result(&res);
return 0;
}
ret = check_password(pass.ptr, pass.len, hash);
if (ret < 0) {
cweb_free_query_result(&res);
return -500;
}
if (ret > 0) {
cweb_free_query_result(&res);
return -400;
}
return user_id;
}
static void endpoint_api_login(CWEB_Request *req)
{
if (cweb_get_user_id(req) != -1) {
cweb_respond_redirect(req, CWEB_STR("/index"));
return;
}
CWEB_String name = cweb_get_param_s(req, CWEB_STR("username"));
CWEB_String pass = cweb_get_param_s(req, CWEB_STR("password"));
if (!valid_name(name) || !valid_pass(pass)) {
cweb_respond_basic(req, 200, CWEB_STR("<div class=\"error\">Invalid credentials</div>"));
return;
}
int ret = user_exists(req->cweb, name, pass);
if (ret < 0) {
cweb_respond_basic(req, 200, CWEB_STR("<div class=\"error\">Invalid credentials</div>"));
return;
}
if (cweb_set_user_id(req, ret) < 0) {
// TODO
}
cweb_respond_redirect(req, CWEB_STR("/index"));
}
static void endpoint_api_signup(CWEB_Request *req)
{
if (cweb_get_user_id(req) != -1) {
cweb_respond_redirect(req, CWEB_STR("/index"));
return;
}
CWEB_String name = cweb_get_param_s(req, CWEB_STR("username"));
CWEB_String email = cweb_get_param_s(req, CWEB_STR("email"));
CWEB_String pass1 = cweb_get_param_s(req, CWEB_STR("password1"));
CWEB_String pass2 = cweb_get_param_s(req, CWEB_STR("password2"));
if (!valid_name(name) || !valid_email(email) || !valid_pass(pass1)) {
cweb_respond_basic(req, 200, CWEB_STR("<div class=\"error\">Invalid credentials</div>"));
return;
}
if (!cweb_streq(pass1, pass2)) {
cweb_respond_basic(req, 200, CWEB_STR("<div class=\"error\">The password was repeated incorrectly</div>"));
return;
}
CWEB_PasswordHash hash;
int ret = cweb_hash_password(pass1.ptr, pass1.len, 12, &hash);
if (ret) {
cweb_respond_basic(req, 400, CWEB_STR("<div class=\"error\">Internal error</div>"));
return;
}
int64_t insert_id = cweb_database_insert(req->cweb, "INSERT INTO Users(username, email, hash) VALUES (?, ?, ?)", name, email, hash);
if (insert_id < 0) {
// TODO: What if the user exists?
cweb_respond_basic(req, 400, CWEB_STR("<div class=\"error\">Internal error</div>"));
return;
}
cweb_set_user_id(req, insert_id);
cweb_respond_redirect(req, CWEB_STR("/index"));
}
static void endpoint_api_logout(CWEB_Request *req)
{
cweb_set_current_user_id(req, -1);
cweb_respond_redirect(req, CWEB_STR("/index"));
}
static void endpoint_api_post(CWEB_Request *req)
{
int user_id = cweb_get_session_user_id(req);
if (user_id == -1) {
cweb_respond_redirect(req, CWEB_STR("/index"));
return;
}
CWEB_String title = cweb_get_param_s(req, CWEB_STR("title"));
CWEB_String link = cweb_get_param_s(req, CWEB_STR("link"));
CWEB_String content = cweb_get_param_s(req, CWEB_STR("content"));
CWEB_String csrf = cweb_get_param_s(req, CWEB_STR("csrf"));
if (!cweb_streq(cweb_get_session_csrf(req), csrf)) {
cweb_respond_basic(req, 400, CWEB_STR("Invalid request"));
return;
}
if (!valid_post_title(title)) {
cweb_respond_basic(req, 200, CWEB_STR("<div class=\"error\">Invalid title</div>"));
return;
}
if (!valid_link(link)) {
cweb_respond_basic(req, 200, CWEB_STR("<div class=\"error\">Invalid link</div>"));
return;
}
if (!valid_post_content(content)) {
cweb_respond_basic(req, 200, CWEB_STR("<div class=\"error\">Invalid content</div>"));
return;
}
bool is_link = false;
if (link.len > 0) {
is_link = true;
content = link;
}
int64_t insert_id = cweb_database_insert(req->cweb,
"INSERT INTO Posts(author, title, is_link, content) VALUES (?, ?, ?, ?)",
user_id, title, is_link, content);
if (insert_id < 0) {
cweb_respond_basic(req, 500, CWEB_STR("<div class=\"error\">Internal error</div>"));
return;
}
int post_id = (int) insert_id;
cweb_respond_redirect(req, "/post?id=%d", post_id);
}
static void endpoint_api_comment(CWEB_Request *req)
{
int user_id = cweb_get_session_user_id(req);
if (user_id == -1) {
cweb_respond_basic(req, 200, CWEB_STR("<div class=\"error\">You are not logged in</div>"));
return;
}
int parent_post = cweb_get_param_i(req, CWEB_STR("parent_post"));
int parent_comment = cweb_get_param_i(req, CWEB_STR("parent_comment"));
CWEB_String content = cweb_get_param_s(req, CWEB_STR("content"));
CWEB_String csrf2 = cweb_get_param_s(req, CWEB_STR("csrf"));
if (!cweb_streq(cweb_get_session_csrf(req), csrf2)) {
cweb_respond_basic(req, 200, CWEB_STR("<div class=\"error\">Invalid request</div>"));
return;
}
content = cweb_trim(content);
if (!valid_comment_content(content)) {
cweb_respond_basic(req, 200, CWEB_STR("<div class=\"error\">Invalid content</div>"));
return;
}
int64_t insert_id;
if (parent_comment == -1) insert_id = cweb_database_insert(req->cweb, "INSERT INTO Comments(author, content, parent_post) VALUES (?, ?, ?)", user_id, content, parent_post);
else insert_id = cweb_database_insert(req->cweb, "INSERT INTO Comments(author, content, parent_post, parent_comment) VALUES (?, ?, ?, ?)", user_id, content, parent_post, parent_comment);
if (insert_id < 0) {
cweb_respond_basic(req, 200, CWEB_STR("<div class=\"error\">Internal error</div>"));
return;
}
cweb_respond_redirect(req, "/post?id=%d", parent_post);
}
static void endpoint_index(CWEB_Request *req)
{
cweb_respond_template(req, 200, CWEB_STR("pages/index.wl"), -1);
}
static void endpoint_write(CWEB_Request *req)
{
if (cweb_get_current_user_id(req) == -1) {
cweb_respond_redirect(req, CWEB_STR("/index"));
return;
}
cweb_respond_template(req, 200, CWEB_STR("pages/write.wl"), -1);
}
static void endpoint_login(CWEB_Request *req)
{
if (cweb_get_current_user_id(req) != -1) {
cweb_respond_redirect(req, CWEB_STR("/index"));
return;
}
cweb_respond_template(req, 200, CWEB_STR("pages/login.wl"), -1);
}
static void endpoint_signup(CWEB_Request *req)
{
if (cweb_get_current_user_id(req) != -1) {
cweb_respond_redirect(req, CWEB_STR("/index"));
return;
}
cweb_respond_template(req, 200, CWEB_STR("pages/signup.wl"), -1);
}
static void endpoint_post(CWEB_Request *req)
{
int post_id = cweb_request_get_parami(req, CWEB_STR("id"));
if (post_id < 0) {
cweb_respond_template(req, 404, CWEB_STR("pages/notfound.wl"), -1);
return;
}
CWEB_QueryResult res = cweb_database_select(req->cweb,
"SELECT COUNT(*) FROM Posts WHERE id=?", post_id);
int num;
if (cweb_next_query_row(&res, &num) != 1) {
cweb_respond_basic(req, 500, CWEB_STR(""));
cweb_free_query_result(&res);
return;
}
cweb_free_query_result(&res);
if (num < 0) {
cweb_respond_basic(req, 500, CWEB_STR(""));
return;
}
if (num == 0) {
cweb_respond_template(req, 404, CWEB_STR("pages/notfound.wl"), -1);
return;
}
cweb_respond_template(req, 200, CWEB_STR("pages/post.wl"), post_id);
}
static void endpoint_fallback(CWEB_Request *req)
{
cweb_respond_template(req, 404, CWEB_STR("pages/notfound.wl"), -1);
}
int main(void)
{
CWEB_String addr = CWEB_STR("127.0.0.1");
uint16_t port = 8080;
CWEB *cweb;
if (cweb_global_init() < 0)
return -1;
if (cweb_init(&cweb, addr, port) < 0)
return -1;
for (;;) {
CWEB_Request request = cweb_wait(&cweb);
cweb_add_endpoint(cweb, CWEB_STR("/api/login"), endpoint_api_login, CWEB_ENDPOINT_POST | CWEB_ENDPOINT_SECURE);
cweb_add_endpoint(cweb, CWEB_STR("/api/signup"), endpoint_api_signup, CWEB_ENDPOINT_POST | CWEB_ENDPOINT_SECURE);
cweb_add_endpoint(cweb, CWEB_STR("/api/logout"), endpoint_api_logout, CWEB_ENDPOINT_POST | CWEB_ENDPOINT_SECURE);
cweb_add_endpoint(cweb, CWEB_STR("/api/post"), endpoint_api_post, CWEB_ENDPOINT_POST);
cweb_add_endpoint(cweb, CWEB_STR("/api/comment"), endpoint_api_comment, CWEB_ENDPOINT_POST);
cweb_add_endpoint(cweb, CWEB_STR("/index"), endpoint_index, CWEB_ENDPOINT_GET);
cweb_add_endpoint(cweb, CWEB_STR("/write"), endpoint_write, CWEB_ENDPOINT_GET);
cweb_add_endpoint(cweb, CWEB_STR("/login"), endpoint_login, CWEB_ENDPOINT_GET);
cweb_add_endpoint(cweb, CWEB_STR("/signup"), endpoint_signup, CWEB_ENDPOINT_GET);
cweb_add_endpoint(cweb, CWEB_STR("/post"), endpoint_post, CWEB_ENDPOINT_GET);
cweb_fallback_endpoint(cweb, endpoint_fallback);
}
cweb_run(&cweb);
cweb_free(&cweb);
cweb_global_free();
return 0;
}
+67
View File
@@ -0,0 +1,67 @@
include "pages/page.wl"
let posts = $query("SELECT P.id, P.title, P.is_link, P.content, (SELECT COUNT(*) FROM Comments as C WHERE c.parent_post=P.id) as num_comments, CURRENT_TIMESTAMP as date FROM Posts as P")
if posts == none:
posts = []
let style =
<style>
.item {
overflow: auto;
border-bottom: 1px solid #E8D4A9;
font-size: 14px;
}
.item:last-child {
border-bottom: 0;
}
.item span {
color: #7A5F2A;
text-decoration: none;
}
.item div {
color: #E8D4A9;
float: left
}
.item div:first-child {
width: calc(100% - 250px);
}
.item div:last-child {
width: 250px;
text-align: right;
}
#no-posts {
margin: 60px auto;
width: 100%;
text-align: center;
color: #7A5F2A;
}
</style>
let main =
<main>
\if len(posts) == 0:
<div id="no-posts">There are no posts yet!</div>
\for post in posts: {
let link
if post.is_link != 0:
link = post.content
else
link = ["/post?id=", post.id]
<div class="item">
<div>
<a href=\'"'\link\'"'>\escape(post.title)</a>
</div>
<div>
<span>\escape(post.date)</span> | <span>\escape(post.num_comments) comments</span>
</div>
</div>
}
</main>
page("Index", $login_user_id, style, main)
+22
View File
@@ -0,0 +1,22 @@
include "pages/page.wl"
include "pages/login_and_signup_style.wl"
let main =
<main>
<span>Welcome back!</span>
<div id="response"></div>
<form hx-post="/api/login" hx-target="#response">
<input type="text" name="username" placeholder="username" />
<input type="password" name="password" placeholder="password" />
<input type="submit" value="Log-In" />
<div class="form-links">
<a href="">forgot password?</a>
|
<a href="">create account</a>
</div>
</form>
</main>
page("Log-In", none, style, main)
+74
View File
@@ -0,0 +1,74 @@
let style =
<style>
span {
text-align: center;
color: #1D2B42;
font-size: 18px;
display: inline-block;
width: 100%;
margin-top: 30px;
}
form {
max-width: 300px;
margin: 30px auto;
}
form input {
border: 0;
outline: 0;
width: 100%;
border-radius: 3px;
padding: 8px;
margin-bottom: 15px;
}
form input[type=text],
form input[type=email],
form input[type=password] {
background: #E8D4A9;
border: 1px solid #D4C298;
}
form input[type=text]:focus,
form input[type=email]:focus,
form input[type=password]:focus {
border-color: #5780C9;
background: #fff;
}
form input[type=submit] {
cursor: pointer;
background: #5780C9;
color: #F7E6C0;
}
form input[type=submit]:hover {
background: #1D2B42;
}
.form-links {
text-align: center;
font-size: 12px;
}
.form-links a {
color: #7A5F2A;
margin: 0 10px;
}
.form-links a:hover {
color: #1D2B42;
}
#response {
max-width: 300px;
margin: auto;
}
#response .error {
margin-top: 30px;
border-radius: 3px;
border: 1px solid #E44C82;
background: #F295B5;
padding: 5px 10px;
}
</style>
+60
View File
@@ -0,0 +1,60 @@
include "pages/page.wl"
let style =
<style>
.not-found-container {
text-align: center;
padding: 60px 20px;
}
.error-code {
font-size: 72px;
font-weight: bold;
color: #7A5F2A;
margin-bottom: 20px;
line-height: 100%;
}
.error-message {
font-size: 24px;
color: #1D2B42;
margin-bottom: 15px;
}
.error-description {
font-size: 14px;
color: #7A5F2A;
margin-bottom: 40px;
max-width: 500px;
margin-left: auto;
margin-right: auto;
line-height: 160%;
}
.home-button {
display: inline-block;
background: #5780C9;
color: #F7E6C0;
padding: 12px 24px;
text-decoration: none;
border-radius: 5px;
font-size: 14px;
margin-top: 20px;
border: none;
cursor: pointer;
}
.home-button:hover {
background: #1D2B42;
color: #F7E6C0;
}
</style>
let main =
<main>
<div class="not-found-container">
<div class="error-code">404</div>
<div class="error-message">Page Not Found</div>
<div class="error-description">
Looks like this page wandered off somewhere.
</div>
</div>
</main>
page("Not Found", $login_user_id, style, main)
+75
View File
@@ -0,0 +1,75 @@
procedure page(title, login_user_id, style, main)
<html>
<head>
<meta charset="UTF-8" />
<title>CN - \escape(title)</title>
<style>
body {
line-height: 200%;
font-family: monospace;
max-width: 800px;
margin: 20px auto;
}
nav {
overflow: auto;
background: #5780C9;
color: #6E93D4;
padding: 5px 10px;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
}
nav div {
float: left
}
nav div:last-child {
float: right
}
nav a {
font-size: 14px;
color: #1D2B42;
}
nav a.current {
font-weight: bold;
}
main {
padding: 5px 10px;
background: #F7E6C0;
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
}
footer {
padding: 5px 10px;
}
</style>
\(style)
<script src="https://cdnjs.cloudflare.com/ajax/libs/htmx/1.9.2/htmx.min.js"></script>
</head>
<body>
<nav>
<div>
<a href="/index">index</a>
\if login_user_id != none: {
"|\n"
<a href="/write">write</a>
}
</div>
\if login_user_id == none:
<div>
<a href="/login">log-in</a>
|
<a href="/signup">sign-up</a>
</div>
else
<div>
<a href="">settings</a>
|
<a href="/api/logout">log-out</a>
</div>
</nav>
\main
<footer>
Made with love by cozis
</footer>
</body>
</html>
+246
View File
@@ -0,0 +1,246 @@
include "pages/page.wl"
let posts = $query("SELECT U.username, P.title, P.content FROM Posts as P, Users as U WHERE P.id=? AND U.id=P.author", $post_id)
let comments = $query("SELECT C.id, U.username, C.content, C.parent_post, C.parent_comment FROM Comments as C, Users as U WHERE C.parent_post=? AND U.id=C.author", $post_id)
let lookup = {}
for comment in comments: {
comment.child = []
lookup[comment.id] = comment
}
let root_comments = []
for comment in comments: {
if comment.parent_comment == none:
root_comments << comment
else
lookup[comment.parent_comment].child << comment
}
let post = posts[0]
let style =
<style>
.thread-header {
padding: 15px 0;
border-bottom: 2px solid #E8D4A9;
margin-bottom: 20px;
}
.thread-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 8px;
}
.thread-title a {
color: #1D2B42;
text-decoration: none;
}
.thread-title a:hover {
text-decoration: underline;
}
.thread-meta {
font-size: 12px;
color: #7A5F2A;
margin-bottom: 10px;
}
.thread-meta a {
color: #7A5F2A;
}
.thread-text {
font-size: 14px;
color: #1D2B42;
line-height: 160%;
margin-bottom: 10px;
}
.thread-actions {
font-size: 12px;
}
.thread-actions a {
color: #7A5F2A;
margin-right: 10px;
}
/* Comment styles */
.comment {
margin-bottom: 15px;
border-left: 1px solid #E8D4A9;
padding-left: 10px;
}
.comment-meta {
font-size: 11px;
color: #7A5F2A;
margin-bottom: 5px;
}
.comment-meta a {
color: #7A5F2A;
}
.comment-text {
font-size: 13px;
color: #1D2B42;
line-height: 150%;
margin-bottom: 5px;
}
.comment-actions {
font-size: 11px;
}
.comment-actions a {
color: #7A5F2A;
margin-right: 8px;
}
.comment-actions a:hover {
color: #1D2B42;
}
.vote-buttons {
float: left;
width: 15px;
margin-right: 8px;
font-size: 10px;
text-align: center;
}
.vote-buttons a {
display: block;
color: #7A5F2A;
text-decoration: none;
line-height: 100%;
}
.vote-buttons a:hover {
color: #1D2B42;
}
.comment-content {
margin-left: 23px;
}
.comment-child {
margin-top: 10px;
margin-left: 10px;
}
.add-comment {
}
.add-comment form {
margin: 0;
overflow: auto;
}
.add-comment form textarea {
width: 100%;
height: 80px;
font-family: monospace;
font-size: 12px;
background: white;
border: 1px solid #D4C298;
border-radius: 3px;
padding: 8px;
box-sizing: border-box;
resize: vertical;
}
.add-comment form input[type=submit] {
background: #5780C9;
color: white;
border: none;
border-radius: 3px;
padding: 6px 12px;
font-family: monospace;
font-size: 12px;
cursor: pointer;
margin-top: 8px;
float: right;
}
.add-comment input[type=submit]:hover {
background: #1D2B42;
}
summary {
list-style: none;
text-decoration: underline;
color: #7A5F2A;
cursor: pointer;
}
::-webkit-details-marker {
display: none;
}
#no-comments {
margin: 30px 0;
width: 100%;
text-align: center;
color: #7A5F2A;
}
pre {
white-space: pre-wrap; /* Since CSS 2.1 */
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
white-space: -pre-wrap; /* Opera 4-6 */
white-space: -o-pre-wrap; /* Opera 7 */
word-wrap: break-word; /* Internet Explorer 5.5+ */
}
</style>
let main =
<main>
<div class="thread-header">
<div class="thread-title">
<span>\escape(post.title)</span>
</div>
<div class="thread-meta">
submitted 3 hours ago by <a href="">\escape(post.username)</a> | <a href="">\len(comments)</a>
</div>
<div class="thread-text">
<pre>\escape(post.content)</pre>
</div>
<details>
<summary>
reply
</summary>
<div class="add-comment">
<form action="/api/comment" method="POST">
<input type="hidden" name="parent_post" value=\'"'\$post_id\'"' />
<textarea name="content" placeholder="Add a comment..."></textarea>
<input type="submit" vaue="Publish" />
</form>
</div>
</details>
</div>
\procedure render_comment(comment)
<div class="comment">
<div class="vote-buttons">
<a href=""></a>
<a href=""></a>
</div>
<div class="comment-content">
<div class="comment-meta">
<a href="">\escape(comment.username)</a> 2 hours ago
</div>
<div class="comment-text">
<pre>\escape(comment.content)</pre>
</div>
\if $login_user_id != none:
<details>
<summary>
reply
</summary>
<div class="add-comment">
<form action="/api/comment" method="POST">
<input type="hidden" name="parent_post" value=\'"'\$post_id\'"' />
<input type="hidden" name="parent_comment" value=\'"'\comment.id\'"' />
<textarea name="content" placeholder="Add a comment..."></textarea>
<input type="submit" vaue="Publish" />
</form>
</div>
</details>
</div>
<div class="comment-child">
\for child in comment.child:
render_comment(child)
</div>
</div>
\if len(root_comments) == 0:
<div id="no-comments">
No comments
</div>
else for comment in root_comments:
render_comment(comment)
</main>
page(post.title, $login_user_id, style, main)
+73
View File
@@ -0,0 +1,73 @@
include "pages/page.wl"
let posts = $query("SELECT title, content FROM Posts WHERE id=?", $post_id)
let comments = $query("SELECT C.id, U.username, C.content, C.parent_post, C.parent_comment FROM Comments as C, Users as U WHERE C.parent_post=? AND U.id == C.author", $post_id)
let lookup = {}
for comment in comments: {
comment.child = []
lookup[comment.id] = comment
}
let roots = []
for comment in comments: {
if comment.parent_comment == none:
roots << comment
else
lookup[comment.parent_comment].child << comment
}
comments = roots
let post = posts[0]
let style =
<style>
.child {
border-left: 3px solid #ccc;
padding-left: 10px;
}
form textarea {
width: 100%;
}
</style>
procedure render_comment(parent)
<div>
<a href="">\parent.username</a>
<p>
\parent.content
</p>
<div class="child">
\if $login_user_id != none:
<form action="/api/comment" method="POST">
<input type="hidden" name="parent_post" value=\'"'\$post_id\'"' />
<input type="hidden" name="parent_comment" value=\'"'\parent.id\'"' />
<textarea name="content"></textarea>
<input type="submit" vaue="Publish" />
</form>
\for child in parent.child:
render_comment(child)
</div>
</div>
let main =
<main>
<h3>\post.title</h3>
<p>\post.content</p>
<div>
<form action="/api/comment" method="POST">
<input type="hidden" name="parent_post" value=\'"'\$post_id\'"' />
<textarea name="content"></textarea>
<input type="submit" vaue="Publish" />
</form>
</div>
\if len comments == 0:
<span>No comments yet!</span>
else for comment in comments:
render_comment(comment)
</main>
page(post.title, $login_user_id, style, main)
+24
View File
@@ -0,0 +1,24 @@
include "pages/page.wl"
include "pages/login_and_signup_style.wl"
let main =
<main>
<span>Welcome!</span>
<div id="response"></div>
<form hx-post="/api/signup" hx-target="#response">
<input type="text" name="username" placeholder="username" />
<input type="email" name="email" placeholder="email" />
<input type="password" name="password1" placeholder="password" />
<input type="password" name="password2" placeholder="repeat password" />
<input type="submit" value="Sign-Up" />
<div class="form-links">
<a href="">forgot password?</a>
|
<a href="">already have an account?</a>
</div>
</form>
</main>
page("Log-In", none, style, main)
+95
View File
@@ -0,0 +1,95 @@
include "pages/page.wl"
let style =
<style>
form {
max-width: 400px;
margin: 30px auto;
}
form input,
form textarea {
border: 0;
outline: 0;
width: 100%;
border-radius: 3px;
padding: 8px;
margin-bottom: 15px;
background: #E8D4A9;
border: 1px solid #D4C298;
}
form input:focus,
form textarea:focus {
border-color: #5780C9;
background: #fff;
}
form textarea {
height: 120px;
resize: vertical;
}
form input[type=submit] {
cursor: pointer;
background: #5780C9;
color: #F7E6C0;
}
form input[type=submit]:hover {
background: #1D2B42;
}
.checkbox-row {
margin-bottom: 15px;
}
.checkbox-row input[type="checkbox"] {
width: auto;
margin-right: 8px;
}
.checkbox-row label {
color: #1D2B42;
font-size: 14px;
cursor: pointer;
}
</style>
let main =
<main>
<form action="/api/post" method="POST">
<input type="hidden" name="csrf" value=\'"'\$csrf\'"' />
<input type="text" id="title" name="title" placeholder="Title" required />
<div class="checkbox-row">
<input type="checkbox" id="is_link" name="is_link" onchange="togglePostType()" />
<label>This is a link post</label>
</div>
<input type="url" id="url" name="link" placeholder="URL" style="display: none;" />
<textarea id="content" name="content" placeholder="Write your post here..."></textarea>
<input type="submit" value="Submit Post" />
</form>
<script>
function togglePostType() {
const checkbox = document.getElementById('is_link');
const urlInput = document.getElementById('url');
const contentTextarea = document.getElementById('content');
if (checkbox.checked) {
urlInput.style.display = 'block';
contentTextarea.style.display = 'none';
} else {
urlInput.style.display = 'none';
contentTextarea.style.display = 'block';
}
}
</script>
</main>
page("Write Post", $login_user_id, style, main)