notification display, general web ui polish around the whole place
After Width: | Height: | Size: 30 KiB |
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 27 KiB |
After Width: | Height: | Size: 19 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 23 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 32 KiB |
After Width: | Height: | Size: 27 KiB |
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 27 KiB |
After Width: | Height: | Size: 19 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 33 KiB |
After Width: | Height: | Size: 25 KiB |
After Width: | Height: | Size: 9.2 KiB |
After Width: | Height: | Size: 34 KiB |
After Width: | Height: | Size: 23 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 33 KiB |
After Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 35 KiB |
After Width: | Height: | Size: 26 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 69 KiB |
After Width: | Height: | Size: 53 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 34 KiB |
After Width: | Height: | Size: 23 KiB |
After Width: | Height: | Size: 12 KiB |
|
@ -21,7 +21,10 @@
|
||||||
accent-color: var(--accent-color);
|
accent-color: var(--accent-color);
|
||||||
|
|
||||||
--text-color: #111;
|
--text-color: #111;
|
||||||
|
--text-color-dark: #444;
|
||||||
|
--text-color-darker: #555;
|
||||||
--background-color: #fff;
|
--background-color: #fff;
|
||||||
|
--background-color-2: #eee;
|
||||||
|
|
||||||
background-color: var(--background-color);
|
background-color: var(--background-color);
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
|
@ -45,17 +48,16 @@ a:hover {
|
||||||
}
|
}
|
||||||
|
|
||||||
.dim {
|
.dim {
|
||||||
color: #444;
|
color: var(--text-color-dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
:root {
|
:root {
|
||||||
--text-color: #fff;
|
--text-color: #fff;
|
||||||
|
--text-color-dark: #aaa;
|
||||||
|
--text-color-darker: #888;
|
||||||
--background-color: #111;
|
--background-color: #111;
|
||||||
}
|
--background-color-2: #161616;
|
||||||
|
|
||||||
.dim {
|
|
||||||
color: #aaa;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,8 +86,8 @@ pre {
|
||||||
}
|
}
|
||||||
|
|
||||||
.dir-header {
|
.dir-header {
|
||||||
background-color: rgba(150, 150, 150, 0.15);
|
background-color: var(--background-color-2);
|
||||||
color: #999;
|
color: var(--text-color-dark);
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
form {
|
form {
|
||||||
margin: auto;
|
margin: auto;
|
||||||
max-width: 700px;
|
max-width: 700px;
|
||||||
background-color: rgba(150, 150, 150, 0.15);
|
background-color: var(--background-color-2);
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
border-radius: 1em;
|
border-radius: 1em;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
.level {
|
.level {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 4em;
|
height: 4em;
|
||||||
background-color: rgba(0, 0, 0, 0.2);
|
background-color: var(--background-color-2);
|
||||||
border-radius: 2em;
|
border-radius: 2em;
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -61,21 +61,12 @@
|
||||||
</head>
|
</head>
|
||||||
<body style="display: flex; flex-direction: column; align-items: center; gap: 1em">
|
<body style="display: flex; flex-direction: column; align-items: center; gap: 1em">
|
||||||
<div class="levels">
|
<div class="levels">
|
||||||
<%- levels.each do |id, name, username, difficulty_community, difficulty_set, featured, epic| -%>
|
<%- levels.each do |id, name, username, difficulty_community, difficulty_set, demon_difficulty_int, featured, epic| -%>
|
||||||
<div class="level">
|
<div class="level">
|
||||||
<%=
|
<%=
|
||||||
difficulties = StaticArray[
|
difficulty = (difficulty_set || difficulty_community).try { |n| LevelDifficulty.new(n) }
|
||||||
"auto",
|
demon_difficulty = demon_difficulty_int.try { |n| DemonDifficulty.new(n) }
|
||||||
"easy",
|
"<img src='#{Templates.get_difficulty_icon(difficulty, featured, epic, demon_difficulty)}' class='level-img'>"
|
||||||
"normal",
|
|
||||||
"hard",
|
|
||||||
"harder",
|
|
||||||
"insane",
|
|
||||||
"demon-hard"
|
|
||||||
]
|
|
||||||
difficulty_int = difficulty_set || difficulty_community
|
|
||||||
difficulty_img = difficulty_int ? difficulties[difficulty_int] : "unrated"
|
|
||||||
"<img src='https://gdbrowser.com/assets/difficulties/#{difficulty_img + (epic ? "-epic" : (featured ? "-featured" : ""))}.png' class='level-img'>"
|
|
||||||
%>
|
%>
|
||||||
<div class="level-right">
|
<div class="level-right">
|
||||||
<span class="line"><span class="name"><%= name %></span><span class="id dim">#<%= id %></span></span>
|
<span class="line"><span class="name"><%= name %></span><span class="id dim">#<%= id %></span></span>
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<link rel="stylesheet" href="/style.css" />
|
<link rel="stylesheet" href="/style.css" />
|
||||||
<title>Account Management</title>
|
<title>Notifications</title>
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
|
@ -28,6 +28,53 @@
|
||||||
gap: 0.5em;
|
gap: 0.5em;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notification {
|
||||||
|
margin: 0.5em;
|
||||||
|
border-radius: 20px;
|
||||||
|
height: 5em;
|
||||||
|
background-color: var(--background-color-2);
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
.notification.unread {
|
||||||
|
outline: 1px solid var(--accent-color);
|
||||||
|
}
|
||||||
|
.notification > .notif-left, .notification > .notif-right {
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
.notif-left {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
|
width: 5em;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.notif-left > img, .notif-left > svg {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
.notif-right {
|
||||||
|
flex: 1 1 0px;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
border-left: 3px solid var(--background-color);
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.timestamp {
|
||||||
|
text-align: right;
|
||||||
|
color: var(--text-color-dark)
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
@ -44,5 +91,28 @@
|
||||||
File.read("public/assets/icons/bell.svg") %>
|
File.read("public/assets/icons/bell.svg") %>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
<%- notifications.each() do |notif| -%>
|
||||||
|
<div class="notification <%= notif[:read_at] == nil ? "unread" : "" %>">
|
||||||
|
<div class="notif-left">
|
||||||
|
<%=
|
||||||
|
case notif[:type]
|
||||||
|
when "authored_level_featured", "authored_level_rated"
|
||||||
|
difficulty = notif[:details]["difficulty"]?.as?(Int64).try { |n| LevelDifficulty.new(n.to_i32) }
|
||||||
|
"<img src='#{Templates.get_difficulty_icon(difficulty, notif[:type] == "authored_level_featured")}' class='notif-icon'>"
|
||||||
|
end
|
||||||
|
%>
|
||||||
|
</div>
|
||||||
|
<div class="notif-right">
|
||||||
|
<div>
|
||||||
|
<%= Notifications.format_notification(notif[:type], notif[:target], notif[:details], html_safe: true) %>
|
||||||
|
</div>
|
||||||
|
<div class="timestamp">
|
||||||
|
<time datetime="<%= notif[:created_at] %>Z">
|
||||||
|
<%= Format.fmt_value(Time.parse(notif[:created_at], Format::TIME_FORMAT, Time::Location::UTC)) %> ago
|
||||||
|
</time>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<%- end -%>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -20,7 +20,7 @@ module CrystalGauntlet::Notifications
|
||||||
"authored_level_rated" => %(Your level <b>%{level_name}</b> has been rated!)
|
"authored_level_rated" => %(Your level <b>%{level_name}</b> has been rated!)
|
||||||
}
|
}
|
||||||
|
|
||||||
def format_notification(type : String, target : Int32, details : NotificationDetails? = nil, html_safe : Bool = false)
|
def format_notification(type : String, target : Int32?, details : NotificationDetails? = nil, html_safe : Bool = false)
|
||||||
details = details || {} of String => String | Int64 | Bool | Float64 | Nil
|
details = details || {} of String => String | Int64 | Bool | Float64 | Nil
|
||||||
string = NOTIFICATION_STRINGS[type]
|
string = NOTIFICATION_STRINGS[type]
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ module CrystalGauntlet::Notifications
|
||||||
#end
|
#end
|
||||||
|
|
||||||
if html_safe
|
if html_safe
|
||||||
string % details.transform_values ->HTML.escape
|
string % details.transform_values { |v| HTML.escape v.to_s }
|
||||||
else
|
else
|
||||||
string % details
|
string % details
|
||||||
end
|
end
|
||||||
|
|
|
@ -35,6 +35,29 @@ module CrystalGauntlet::Templates
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
DIFFICULTIES = StaticArray[
|
||||||
|
"auto",
|
||||||
|
"easy",
|
||||||
|
"normal",
|
||||||
|
"hard",
|
||||||
|
"harder",
|
||||||
|
"insane",
|
||||||
|
"demon"
|
||||||
|
]
|
||||||
|
|
||||||
|
DEMON_DIFFICULTIES = StaticArray[
|
||||||
|
"easy",
|
||||||
|
"medium",
|
||||||
|
"hard",
|
||||||
|
"insane",
|
||||||
|
"extreme"
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_difficulty_icon(difficulty : LevelDifficulty?, featured : Bool = false, epic : Bool = false, demon_difficulty : DemonDifficulty? = DemonDifficulty::Hard)
|
||||||
|
"/assets/difficulties/#{DIFFICULTIES[difficulty.try &.to_i || -1]? || "na"}#{difficulty.try &.demon? ? "-#{DEMON_DIFFICULTIES[demon_difficulty.try &.to_i || -1]? || "hard"}" : ""}#{(featured && !epic) ? "-featured" : ""}#{epic ? "-epic" : ""}.png"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
module CrystalGauntlet
|
module CrystalGauntlet
|
||||||
|
|
|
@ -8,6 +8,6 @@ CrystalGauntlet.template_endpoints["/tools/levels"] = ->(context : HTTP::Server:
|
||||||
context.response.content_type = "text/html"
|
context.response.content_type = "text/html"
|
||||||
page = (context.request.query_params["page"]? || "0").to_i? || 0
|
page = (context.request.query_params["page"]? || "0").to_i? || 0
|
||||||
total_levels = DATABASE.scalar("select count(*) from levels").as(Int64)
|
total_levels = DATABASE.scalar("select count(*) from levels").as(Int64)
|
||||||
levels = DATABASE.query_all("select levels.id, name, users.username, levels.community_difficulty, levels.difficulty, levels.featured, levels.epic from levels left join users on levels.user_id = users.id order by levels.id desc limit #{levels_per_page} offset #{page * levels_per_page}", as: {Int32, String, String, Int32?, Int32?, Bool, Bool})
|
levels = DATABASE.query_all("select levels.id, name, users.username, levels.community_difficulty, levels.difficulty, levels.demon_difficulty, levels.featured, levels.epic from levels left join users on levels.user_id = users.id order by levels.id desc limit #{levels_per_page} offset #{page * levels_per_page}", as: {Int32, String, String, Int32?, Int32?, Int32?, Bool, Bool})
|
||||||
ECR.embed("./public/template/levels.ecr", context.response)
|
ECR.embed("./public/template/levels.ecr", context.response)
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,8 +13,18 @@ CrystalGauntlet.template_endpoints["/accounts/notifications"] = ->(context : HTT
|
||||||
icon_value = [cube, ship, ball, ufo, wave, robot, spider][icon_type]
|
icon_value = [cube, ship, ball, ufo, wave, robot, spider][icon_type]
|
||||||
type_str = ["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)
|
notifications = DATABASE.query_all("select type, target, details, read_at, created_at from notifications where account_id = ? order by created_at desc", account_id, as: {String, Int32?, String, String?, String})
|
||||||
unread_notifications = notification_count > 0
|
.map {|type, target, details, read_at, created_at| {
|
||||||
|
type: type,
|
||||||
|
target: target,
|
||||||
|
details: Notifications::NotificationDetails.from_json(details),
|
||||||
|
read_at: read_at,
|
||||||
|
created_at: created_at
|
||||||
|
} }
|
||||||
|
|
||||||
|
# mark all as read
|
||||||
|
DATABASE.exec("update notifications set read_at = ? where read_at is null and account_id = ?", Time.utc.to_s(Format::TIME_FORMAT), account_id)
|
||||||
|
unread_notifications = false
|
||||||
|
|
||||||
ECR.embed("./public/template/notifications.ecr", context.response)
|
ECR.embed("./public/template/notifications.ecr", context.response)
|
||||||
}
|
}
|
||||||
|
|