diff --git a/public/assets/icons/bell.svg b/public/assets/icons/bell.svg new file mode 100644 index 0000000..bba561c --- /dev/null +++ b/public/assets/icons/bell.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/style.css b/public/style.css index ba3c47b..50e4533 100644 --- a/public/style.css +++ b/public/style.css @@ -14,14 +14,17 @@ -moz-osx-font-smoothing: grayscale; -webkit-text-size-adjust: 100%; - background-color: #fff; - color: #111; - font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; --accent-color: rgb(245, 141, 210); --accent-color-bri: rgb(255, 191, 234); accent-color: var(--accent-color); + + --text-color: #111; + --background-color: #fff; + + background-color: var(--background-color); + color: var(--text-color); } ::selection { @@ -47,8 +50,8 @@ a:hover { @media (prefers-color-scheme: dark) { :root { - background-color: #111; - color: #fff; + --text-color: #fff; + --background-color: #111; } .dim { @@ -112,4 +115,37 @@ pre { .error { color: #f33; +} + +.circle-button { + border-radius: 10px; + border: 1px solid var(--accent-color); + color: var(--accent-color); + transition: 0.1s border-color, 0.1s color; + padding: 0.5em; + margin: 0.5em; + height: 48px; + width: 48px; +} +.circle-button:hover { + border-color: var(--accent-color-bri); + color: var(--accent-color-bri); +} +.circle-button > svg, .circle-button > img { + display: block; + width: 100%; + height: 100%; + object-fit: contain; +} + +@media (max-width: 650px) { + /* mobile layout gets bigger buttons */ + .circle-button { + height: 54px; + width: 54px; + } +} + +.circle-button.notifications-unread > svg { + fill: currentColor; } \ No newline at end of file diff --git a/public/template/account_management.ecr b/public/template/account_management.ecr index 7d5e525..20f7c5f 100644 --- a/public/template/account_management.ecr +++ b/public/template/account_management.ecr @@ -21,7 +21,6 @@ color: #000; border-radius: 1.5em; padding: 1em; - margin: 1em; display: flex; flex-direction: row; @@ -85,26 +84,48 @@ margin: auto; display: block; } + + .header { + height: 96px; + display: flex; + flex-direction: row; + align-items: center; + gap: 1em; + margin: 1em; + } + .header > .greeting { + flex: 1 1 0px; + min-width: 0; + } + .header > .circle-button { + flex: 0 0 auto; + max-width: 100%; + }
-
- " width="150" height="150" class="greeting-l"> -
-
-
- hewwo, <%= username %>! +
+
+ " width="150" height="150" class="greeting-l"> +
+
+
+ hewwo, <%= username %>! +
+
+ <%= stars %> <%= diamonds %> <%= coins %> <%= user_coins %> <%= demons %> +
-
- <%= stars %> <%= diamonds %> <%= coins %> <%= user_coins %> <%= demons %> +
-
- Settings · Log out -
+ "> + <%= File.read("public/assets/icons/bell.svg") %> +


blablabla lorem ipsum whatever. put stuff here later diff --git a/public/template/notifications.ecr b/public/template/notifications.ecr new file mode 100644 index 0000000..2821e5b --- /dev/null +++ b/public/template/notifications.ecr @@ -0,0 +1,48 @@ + + + + + + + + Account Management + + + + + + diff --git a/src/endpoints/levels/rateLevel.cr b/src/endpoints/levels/rateLevel.cr index 7bd876a..ad7c42d 100644 --- a/src/endpoints/levels/rateLevel.cr +++ b/src/endpoints/levels/rateLevel.cr @@ -109,12 +109,12 @@ CrystalGauntlet.endpoints["/suggestGJStars20.php"] = ->(context : HTTP::Server:: stars = params["stars"].to_i difficulty = stars_to_difficulty(params["stars"].to_i) || LevelDifficulty::Easy - author_account_id = DATABASE.query_one("select users.account_id from levels left join users on users.id = levels.user_id where levels.id = ?", level_id, as: {Int32?}) + author_account_id, level_name = DATABASE.query_one("select users.account_id, levels.name from levels left join users on users.id = levels.user_id where levels.id = ?", level_id, as: {Int32?, String}) if author_account_id notif_type = params["feature"] == "1" ? "authored_level_featured" : "authored_level_rated" Notifications.clear_previous_notifications(author_account_id, notif_type, level_id) - Notifications.send_notification(author_account_id, notif_type, level_id, {"stars" => stars.to_i64, "difficulty" => difficulty.to_i64}) + Notifications.send_notification(author_account_id, notif_type, level_id, {"stars" => stars.to_i64, "difficulty" => difficulty.to_i64, "level_name" => level_name}) end DATABASE.exec "update levels set stars = ?, featured = ?, difficulty = ? where id = ?", stars, params["feature"].to_i, difficulty.to_i, level_id diff --git a/src/lib/notifications.cr b/src/lib/notifications.cr index 785ecd0..4f2f169 100644 --- a/src/lib/notifications.cr +++ b/src/lib/notifications.cr @@ -5,13 +5,34 @@ include CrystalGauntlet module CrystalGauntlet::Notifications extend self - alias NotificationValue = Hash(String, String | Int64 | Bool | Float64 | Nil) + alias NotificationDetails = Hash(String, String | Int64 | Bool | Float64 | Nil) def clear_previous_notifications(account_id : Int32, type : String, target : Int32) DATABASE.exec("delete from notifications where account_id = ? and type = ? and target = ?", account_id, type, target) end - def send_notification(account_id : Int32, type : String, target : Int32?, details : NotificationValue? = nil) + def send_notification(account_id : Int32, type : String, target : Int32?, details : NotificationDetails? = nil) DATABASE.exec("insert into notifications (id, account_id, type, target, details) values (?, ?, ?, ?, ?)", IDs.get_next_id("notifications"), account_id, type, target, details.try &.to_json || "{}") end + + NOTIFICATION_STRINGS = { + "authored_level_featured" => %(Your level %{level_name} has been featured!), + "authored_level_rated" => %(Your level %{level_name} has been rated!) + } + + def format_notification(type : String, target : Int32, details : NotificationDetails? = nil, html_safe : Bool = false) + details = details || {} of String => String | Int64 | Bool | Float64 | Nil + string = NOTIFICATION_STRINGS[type] + + #case type + #when "authored_level_featured", "authored_level_rated" + # details["action"] = (type == "authored_level_featured") ? "featured" : "rated" + #end + + if html_safe + string % details.transform_values ->HTML.escape + else + string % details + end + end end diff --git a/src/template_endpoints/account_management.cr b/src/template_endpoints/account_management.cr index ac59afd..800c47d 100644 --- a/src/template_endpoints/account_management.cr +++ b/src/template_endpoints/account_management.cr @@ -8,6 +8,7 @@ CrystalGauntlet.template_endpoints["/#{config_get("general.append_path").as(Stri CrystalGauntlet.template_endpoints["/accounts"] = ->(context : HTTP::Server::Context) { context.response.content_type = "text/html" + account_id = nil user_id = nil username = nil Templates.auth() @@ -17,5 +18,7 @@ CrystalGauntlet.template_endpoints["/accounts"] = ->(context : HTTP::Server::Con icon_value = [cube, ship, ball, ufo, wave, robot, spider][icon_type] type_str = ["cube", "ship", "ball", "ufo", "wave", "robot", "spider"][icon_type] + unread_notifications = DATABASE.scalar("select count(*) from notifications where account_id = ? and read_at is null", account_id).as(Int64) > 0 + ECR.embed("./public/template/account_management.ecr", context.response) } diff --git a/src/template_endpoints/notifications.cr b/src/template_endpoints/notifications.cr new file mode 100644 index 0000000..47ac9ee --- /dev/null +++ b/src/template_endpoints/notifications.cr @@ -0,0 +1,20 @@ +include CrystalGauntlet + +CrystalGauntlet.template_endpoints["/accounts/notifications"] = ->(context : HTTP::Server::Context) { + context.response.content_type = "text/html" + + account_id = nil + user_id = nil + username = nil + Templates.auth() + + icon_type, color1, color2, cube, ship, ball, ufo, wave, robot, spider, glow = DATABASE.query_one("select icon_type, color1, color2, cube, ship, ball, ufo, wave, robot, spider, glow from users where id = ?", user_id, as: {Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32}) + + icon_value = [cube, ship, ball, ufo, wave, robot, spider][icon_type] + type_str = ["cube", "ship", "ball", "ufo", "wave", "robot", "spider"][icon_type] + + notification_count = DATABASE.scalar("select count(*) from notifications where account_id = ? and read_at is null", account_id).as(Int64) + unread_notifications = notification_count > 0 + + ECR.embed("./public/template/notifications.ecr", context.response) +}