mirror of
https://github.com/philomena-dev/philomena.git
synced 2025-02-01 03:46:44 +01:00
graphs and fancy comments
This commit is contained in:
parent
ab30673208
commit
a3c6cba47e
18 changed files with 208 additions and 76 deletions
|
@ -4,6 +4,7 @@ $narrow-layout-width: 960px;
|
|||
$medium-layout-width: $min-desktop-width;
|
||||
$font-family-base: verdana, arial, helvetica, sans-serif;
|
||||
$font-family-monospace: "Droid Sans Mono", monospace;
|
||||
$font-family-heading: $font-family-base;
|
||||
|
||||
:root {
|
||||
/* Paddings */
|
||||
|
@ -84,6 +85,7 @@ $font-family-monospace: "Droid Sans Mono", monospace;
|
|||
/* Fonts */
|
||||
--font-family: $font-family-base;
|
||||
--font-family-monospace: $font-family-monospace;
|
||||
--font-family-heading: $font-family-heading;
|
||||
--font-size: 14px;
|
||||
--font-tiny-size: 12px;
|
||||
--font-micro-size: 10px;
|
||||
|
|
|
@ -150,6 +150,10 @@
|
|||
border-radius: var(--border-radius-inner);
|
||||
}
|
||||
|
||||
.button--transparent {
|
||||
background: 0 !important;
|
||||
}
|
||||
|
||||
@mixin button-type primary;
|
||||
@mixin button-type secondary;
|
||||
@mixin button-type success;
|
||||
|
|
|
@ -91,6 +91,11 @@
|
|||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
.flex__break {
|
||||
flex-basis: 100%;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
@mixin if-desktop {
|
||||
.flex--maybe-wrap {
|
||||
flex-wrap: nowrap;
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
@define-mixin heading $el {
|
||||
$(el) {
|
||||
font-family: var(--font-family-heading);
|
||||
font-size: var(--font-$(el)-size);
|
||||
font-weight: bold;
|
||||
font-variant: small-caps;
|
||||
font-weight: normal;
|
||||
padding: var(--padding-small) 0;
|
||||
margin-top: 0;
|
||||
border-bottom: 1px solid var(--primary-border-color);
|
||||
|
@ -30,12 +30,6 @@
|
|||
@mixin heading h4;
|
||||
@mixin heading h5;
|
||||
@mixin heading h6;
|
||||
|
||||
/* To differentiate between h5 and h6, simply make h6 not bold */
|
||||
h6 {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
@mixin heading-type success;
|
||||
@mixin heading-type warning;
|
||||
@mixin heading-type danger;
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
|
||||
.column-layout__main {
|
||||
flex: 1 0 auto;
|
||||
max-width: calc(100% - var(--column-left-width) - var(--padding-normal));
|
||||
}
|
||||
|
||||
@mixin if-mobile {
|
||||
|
@ -21,6 +22,10 @@
|
|||
.column-layout__left {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.column-layout__main {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
#container {
|
||||
|
|
5
assets/css/options/system-fonts.css
Normal file
5
assets/css/options/system-fonts.css
Normal file
|
@ -0,0 +1,5 @@
|
|||
:root {
|
||||
--font-family: sans-serif;
|
||||
--font-family-monospace: monospace;
|
||||
--font-family-heading: sans-serif;
|
||||
}
|
|
@ -80,6 +80,11 @@
|
|||
padding: var(--padding-small);
|
||||
}
|
||||
|
||||
.communication__options {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
}
|
||||
|
||||
.communication__options__staff {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -87,19 +92,40 @@
|
|||
margin-top: var(--padding-small);
|
||||
}
|
||||
|
||||
.communication__options__post-time {
|
||||
.communication__post-time {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--padding-small);
|
||||
flex-wrap: wrap;
|
||||
padding: var(--padding-small);
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.communication__post-time > time,
|
||||
.communication__options__edit-time {
|
||||
width: 100%;
|
||||
margin-bottom: var(--padding-small);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@mixin if-mobile {
|
||||
.communication__options {
|
||||
flex-flow: column;
|
||||
}
|
||||
|
||||
.communication__options__edit-time {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.communication__post-time > time,
|
||||
.communication__options__edit-time {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.communication__post-time {
|
||||
border-radius: var(--border-radius-inner);
|
||||
margin-top: var(--padding-small);
|
||||
background: var(--primary-muted-color);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.communication__body__text {
|
||||
margin-top: var(--padding-normal);
|
||||
}
|
||||
|
|
|
@ -22,13 +22,33 @@
|
|||
.sparkline {
|
||||
border-bottom: 1px solid var(--primary-border-color);
|
||||
display: flex;
|
||||
height: var(--padding-normal);
|
||||
height: var(--padding-large);
|
||||
padding: var(--padding-tiny);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sparkline svg {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.barline__bar {
|
||||
fill: var(--primary-border-color);
|
||||
fill: var(--primary-link-color);
|
||||
}
|
||||
|
||||
.barline__bar:hover {
|
||||
fill: var(--danger-color);
|
||||
}
|
||||
|
||||
.barline__dot {
|
||||
fill: var(--primary-border-color);
|
||||
stroke: var(--link-color);
|
||||
stroke-width: 1px;
|
||||
}
|
||||
|
||||
.barline__dot:hover {
|
||||
fill: var(--danger-color);
|
||||
}
|
||||
|
||||
.barline__dot[cy="21.25"] {
|
||||
display: none;
|
||||
}
|
||||
|
|
30
assets/js/graph.ts
Normal file
30
assets/js/graph.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
/**
|
||||
* Graph Logic
|
||||
*
|
||||
* Scales graphs, makes graphs resizable, and stuff like that
|
||||
*/
|
||||
|
||||
import { $, $$ } from './utils/dom';
|
||||
|
||||
function resizeGraphs() {
|
||||
$$<SVGSVGElement>('#js-sparkline-svg').forEach(el => {
|
||||
const parent: HTMLElement | null = el.parentElement;
|
||||
|
||||
if (parent) {
|
||||
el.viewBox.baseVal.width = parent.clientWidth;
|
||||
|
||||
const graph: SVGPathElement | null = $<SVGPathElement>('#js-barline-graph', el);
|
||||
|
||||
if (graph) {
|
||||
graph.setAttribute('style', `transform: scale(${parent.clientWidth / 375});`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function sizeGraphs() {
|
||||
resizeGraphs();
|
||||
window.addEventListener('resize', resizeGraphs);
|
||||
}
|
||||
|
||||
export { sizeGraphs };
|
|
@ -35,6 +35,7 @@ import { hideStaffTools } from './staffhider';
|
|||
import { pollOptionCreator } from './poll';
|
||||
import { warnAboutPMs } from './pmwarning';
|
||||
import { imageSourcesCreator } from './sources';
|
||||
import { sizeGraphs } from './graph';
|
||||
|
||||
whenReady(() => {
|
||||
|
||||
|
@ -68,5 +69,6 @@ whenReady(() => {
|
|||
pollOptionCreator();
|
||||
warnAboutPMs();
|
||||
imageSourcesCreator();
|
||||
sizeGraphs();
|
||||
|
||||
});
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
- options = render PhilomenaWeb.CommentView, "_comment_link.html", comment: @comment, conn: @conn
|
||||
|
||||
article.block.communication id="comment_#{@comment.id}"
|
||||
= if not @comment.approved and not @comment.hidden_from_users and (can?(@conn, :hide, @comment) or @comment.user_id == @conn.assigns.current_user.id) do
|
||||
.block__content
|
||||
|
@ -22,13 +24,14 @@ article.block.communication id="comment_#{@comment.id}"
|
|||
= submit "Delete", class: "button"
|
||||
|
||||
.block__content.flex.flex--no-wrap class=communication_body_class(@comment)
|
||||
= render PhilomenaWeb.CommunicationView, "_body.html", object: @comment, body: @body, conn: @conn, name: "comment"
|
||||
= render PhilomenaWeb.CommunicationView, "_body.html", object: @comment, body: @body, conn: @conn, name: "comment", options: options
|
||||
|
||||
.block__content.communication__options
|
||||
.flex.flex--wrap.flex--spaced-out
|
||||
= render PhilomenaWeb.CommentView, "_comment_options.html", comment: @comment, conn: @conn
|
||||
|
||||
= if can?(@conn, :hide, @comment) do
|
||||
.flex__spacer
|
||||
.js-staff-action
|
||||
.communication__options__staff
|
||||
= cond do
|
||||
|
@ -43,16 +46,16 @@ article.block.communication id="comment_#{@comment.id}"
|
|||
' Delete Contents
|
||||
|
||||
- not @comment.hidden_from_users and not @comment.destroyed_content ->
|
||||
a.button.button--danger.togglable-delete-form-link href="#" data-click-toggle="#inline-del-form-comment-#{@comment.id}"
|
||||
a.button.button--danger.button--transparent.togglable-delete-form-link href="#" data-click-toggle="#inline-del-form-comment-#{@comment.id}"
|
||||
i.fas.fa-times>
|
||||
' Delete
|
||||
|
||||
- true ->
|
||||
|
||||
= if can?(@conn, :show, :ip_address) do
|
||||
.button.button--information.js-staff-action
|
||||
.button.button--warning.button--transparent.js-staff-action
|
||||
=<> link_to_ip(@conn, @comment.ip)
|
||||
.button.button--information.js-staff-action
|
||||
.button.button--warning.button--transparent.js-staff-action
|
||||
=<> link_to_fingerprint(@conn, @comment.fingerprint)
|
||||
|
||||
= form_for :comment, Routes.image_comment_hide_path(@conn, :create, @comment.image_id, @comment), [class: "togglable-delete-form hidden flex", id: "inline-del-form-comment-#{@comment.id}"], fn f ->
|
||||
|
|
18
lib/philomena_web/templates/comment/_comment_link.html.slime
Normal file
18
lib/philomena_web/templates/comment/_comment_link.html.slime
Normal file
|
@ -0,0 +1,18 @@
|
|||
- comment_link = Routes.image_path(@conn, :show, @comment.image) <> "#comment_#{@comment.id}"
|
||||
|
||||
.flex__spacer.hidden--desktop
|
||||
a.communication__interaction.hidden--desktop title="Link to comment" href=comment_link
|
||||
i.fa.fa-link
|
||||
= if not is_nil(@comment.edited_at) and can?(@conn, :show, @comment) do
|
||||
a.communication__options__edit-time href=Routes.image_comment_history_path(@conn, :index, @comment.image, @comment)
|
||||
span.hidden--mobile> - edited
|
||||
span.hidden--desktop> Edited
|
||||
=> pretty_time(@comment.edited_at)
|
||||
|
||||
= if @comment.edit_reason not in [nil, ""] do
|
||||
| (
|
||||
= @comment.edit_reason
|
||||
| )
|
||||
.flex__spacer.hidden--mobile
|
||||
a.communication__interaction.hidden--mobile title="Link to comment" href=comment_link
|
||||
i.fa.fa-link
|
|
@ -1,29 +1,11 @@
|
|||
.communication__options__post-time
|
||||
span
|
||||
' Posted
|
||||
=> pretty_time(@comment.created_at)
|
||||
|
||||
a.button.button--primary href=Routes.image_comment_report_path(@conn, :new, @comment.image, @comment)
|
||||
i.fa.fa-flag>
|
||||
' Report
|
||||
|
||||
= if not is_nil(@comment.edited_at) and can?(@conn, :show, @comment) do
|
||||
a.communication__options__edit-time href=Routes.image_comment_history_path(@conn, :index, @comment.image, @comment)
|
||||
' Edited
|
||||
=> pretty_time(@comment.edited_at)
|
||||
|
||||
= if @comment.edit_reason not in [nil, ""] do
|
||||
' because:
|
||||
=> @comment.edit_reason
|
||||
|
||||
.flex.flex--normal-gap.flex--centered
|
||||
- link_path = Routes.image_path(@conn, :show, @comment.image) <> "#comment_#{@comment.id}"
|
||||
- safe_author = PhilomenaWeb.PostView.markdown_safe_author(@comment)
|
||||
- quote_body = if @comment.hidden_from_users, do: "", else: @comment.body
|
||||
|
||||
a.button.button--primary title="Link to comment" href=link_path
|
||||
i.fa.fa-link
|
||||
' Link
|
||||
a.button.button--primary href=Routes.image_comment_report_path(@conn, :new, @comment.image, @comment)
|
||||
i.fa.fa-flag>
|
||||
' Report
|
||||
|
||||
a.button.button--primary.post-reply.post-reply-quote href=link_path data-reply-url=link_path data-author=safe_author data-post=quote_body
|
||||
i.fa.fa-quote-right
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
- options = render PhilomenaWeb.CommentView, "_comment_link.html", comment: @comment, conn: @conn
|
||||
|
||||
article.block.communication id="comment_#{@comment.id}"
|
||||
.block__content.flex.flex--no-wrap class=communication_body_class(@comment)
|
||||
= render PhilomenaWeb.CommunicationView, "_body.html", object: @comment, image: @comment.image, body: @body, conn: @conn, name: "comment"
|
||||
= render PhilomenaWeb.CommunicationView, "_body.html", object: @comment, image: @comment.image, body: @body, conn: @conn, name: "comment", options: options
|
||||
|
||||
.block__content.communication__options
|
||||
.flex.flex--wrap.flex--spaced-out
|
||||
|
@ -15,21 +17,21 @@ article.block.communication id="comment_#{@comment.id}"
|
|||
' Restore
|
||||
|
||||
= if can?(@conn, :delete, @comment) do
|
||||
= link(to: Routes.image_comment_delete_path(@conn, :create, @comment.image_id, @comment), data: [confirm: "Are you sure?"], method: "post", class: "button button--danger") do
|
||||
= link(to: Routes.image_comment_delete_path(@conn, :create, @comment.image_id, @comment), data: [confirm: "Are you sure?"], method: "post", class: "button button--danger button--transparent") do
|
||||
i.fas.fa-times>
|
||||
' Delete Contents
|
||||
|
||||
- not @comment.hidden_from_users and not @comment.destroyed_content ->
|
||||
a.button.button--danger.togglable-delete-form-link href="#" data-click-toggle="#inline-del-form-comment-#{@comment.id}"
|
||||
a.button.button--danger.button--transparent.togglable-delete-form-link href="#" data-click-toggle="#inline-del-form-comment-#{@comment.id}"
|
||||
i.fas.fa-times>
|
||||
' Delete
|
||||
|
||||
- true ->
|
||||
|
||||
= if can?(@conn, :show, :ip_address) do
|
||||
.button.button--warning.js-staff-action
|
||||
.button.button--warning.button--transparent.js-staff-action
|
||||
=<> link_to_ip(@conn, @comment.ip)
|
||||
.button.button--information.js-staff-action
|
||||
.button.button--warning.button--transparent.js-staff-action
|
||||
=<> link_to_fingerprint(@conn, @comment.fingerprint)
|
||||
|
||||
= form_for :comment, Routes.image_comment_hide_path(@conn, :create, @comment.image_id, @comment), [class: "togglable-delete-form hidden flex", id: "inline-del-form-comment-#{@comment.id}"], fn f ->
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
- anon = is_nil(assigns[:noanon]) or @noanon == false
|
||||
- options = if is_nil(assigns[:options]), do: "", else: @options
|
||||
|
||||
- avatar = cond do
|
||||
- not is_nil(assigns[:image]) ->
|
||||
|
@ -45,6 +46,11 @@
|
|||
.communication__sender-block
|
||||
span.communication__sender-name
|
||||
= username
|
||||
.flex__spacer
|
||||
span.communication__post-time
|
||||
' Posted
|
||||
=< pretty_time(@object.created_at)
|
||||
= options
|
||||
= title
|
||||
.communication__body__text
|
||||
= contents
|
||||
|
@ -55,6 +61,10 @@
|
|||
span.communication__sender-name
|
||||
= username
|
||||
= title
|
||||
span.communication__post-time
|
||||
' Posted
|
||||
= pretty_time(@object.created_at)
|
||||
= options
|
||||
.communication__body.communication__body__text
|
||||
= contents
|
||||
|
|
@ -1,3 +1,17 @@
|
|||
- post_link = Routes.forum_topic_path(@conn, :show, @post.topic.forum, @post.topic, post_id: @post.id) <> "#post_#{@post.id}"
|
||||
- options = if true do
|
||||
.flex__spacer.hidden--desktop
|
||||
a.communication__interaction.hidden--desktop title="Link to post" href=post_link
|
||||
i.fa.fa-link
|
||||
= if not is_nil(@post.edited_at) and can?(@conn, :show, @post) do
|
||||
a href=Routes.forum_topic_post_history_path(@conn, :index, @post.topic.forum, @post.topic, @post)
|
||||
' Edited
|
||||
=> pretty_time(@post.edited_at)
|
||||
|
||||
= if @post.edit_reason not in [nil, ""] do
|
||||
' because:
|
||||
=> @post.edit_reason
|
||||
|
||||
article.block.communication id="post_#{@post.id}"
|
||||
= if not @post.approved and not @post.hidden_from_users and (can?(@conn, :hide, @post) or @post.user_id == @conn.assigns.current_user.id) do
|
||||
.block__content
|
||||
|
@ -22,7 +36,7 @@ article.block.communication id="post_#{@post.id}"
|
|||
= submit "Delete", class: "button"
|
||||
|
||||
.block__content.flex.flex--no-wrap class=communication_body_class(@post)
|
||||
= render PhilomenaWeb.CommunicationView, "_body.html", object: @post, body: @body, conn: @conn, name: "post"
|
||||
= render PhilomenaWeb.CommunicationView, "_body.html", object: @post, body: @body, conn: @conn, name: "post", options: options
|
||||
|
||||
.block__content.communication__options
|
||||
.flex.flex--wrap.flex--spaced-out
|
||||
|
|
|
@ -1,29 +1,11 @@
|
|||
div
|
||||
' Posted
|
||||
=> pretty_time(@post.created_at)
|
||||
|
||||
a.communication__interaction href=Routes.forum_topic_post_report_path(@conn, :new, @post.topic.forum, @post.topic, @post)
|
||||
i.fa.fa-flag>
|
||||
' Report
|
||||
|
||||
= if not is_nil(@post.edited_at) and can?(@conn, :show, @post) do
|
||||
br
|
||||
a href=Routes.forum_topic_post_history_path(@conn, :index, @post.topic.forum, @post.topic, @post)
|
||||
' Edited
|
||||
=> pretty_time(@post.edited_at)
|
||||
|
||||
= if @post.edit_reason not in [nil, ""] do
|
||||
' because:
|
||||
=> @post.edit_reason
|
||||
|
||||
div
|
||||
- link_path = Routes.forum_topic_path(@conn, :show, @post.topic.forum, @post.topic, post_id: @post.id) <> "#post_#{@post.id}"
|
||||
- safe_author = markdown_safe_author(@post)
|
||||
- quote_body = if @post.hidden_from_users, do: "", else: @post.body
|
||||
|
||||
a.communication__interaction title="Link to post" href=link_path
|
||||
i.fa.fa-link>
|
||||
' Link
|
||||
a.communication__interaction href=Routes.forum_topic_post_report_path(@conn, :new, @post.topic.forum, @post.topic, @post)
|
||||
i.fa.fa-flag>
|
||||
' Report
|
||||
|
||||
a.communication__interaction.post-reply.post-reply-quote href=link_path data-reply-url=link_path data-author=safe_author data-post=quote_body
|
||||
i.fa.fa-quote-right>
|
||||
|
|
|
@ -36,25 +36,53 @@ defmodule PhilomenaWeb.ProfileView do
|
|||
def commission_status(%{open: true}), do: "Open"
|
||||
def commission_status(_commission), do: "Closed"
|
||||
|
||||
def sparkline_data(data) do
|
||||
defp sparkline_y(val, max) do
|
||||
# Filter out negative values
|
||||
calc = max(val, 0)
|
||||
|
||||
# Lerp or 0 if not present
|
||||
height = zero_div(calc * 20, max)
|
||||
|
||||
# In SVG coords, y grows down
|
||||
20 - height
|
||||
end
|
||||
|
||||
def sparkline_data(data, width \\ 375, height \\ 20) do
|
||||
# Normalize range
|
||||
max = max(Enum.max(data), 0)
|
||||
sx = width / 90
|
||||
sy = height / 20
|
||||
factor = 100 / 90
|
||||
|
||||
content_tag :svg, width: "100%", preserveAspectRatio: "none", viewBox: "0 0 90 20" do
|
||||
for {val, i} <- Enum.with_index(data) do
|
||||
# Filter out negative values
|
||||
calc = max(val, 0)
|
||||
content_tag :svg, id: "js-sparkline-svg", width: "100%", preserveAspectRatio: "xMinYMin", viewBox: "0 0 #{width} #{height}" do
|
||||
first = List.first(data)
|
||||
last = List.last(data)
|
||||
first_y = sparkline_y(first, max) * sy
|
||||
last_y = sparkline_y(last, max) * sy
|
||||
indexed_data = data
|
||||
|> Enum.with_index()
|
||||
points =
|
||||
indexed_data
|
||||
|> Enum.chunk_every(2, 1, :discard)
|
||||
|> Enum.map(fn [{cv, ci}, {nv, ni}] ->
|
||||
cy = sparkline_y(cv, max)
|
||||
ny = sparkline_y(nv, max)
|
||||
|
||||
# Lerp or 0 if not present
|
||||
height = zero_div(calc * 20, max)
|
||||
"C #{(ci * sx) + 0.5 * sx},#{cy * sy} #{(ni * sx) - 0.5 * sx},#{ny * sy} #{ni * sx},#{ny * sy}"
|
||||
end)
|
||||
|> Enum.join("")
|
||||
|
||||
# In SVG coords, y grows down
|
||||
y = 20 - height
|
||||
circles = for {val, i} <- indexed_data do
|
||||
y = sparkline_y(val, max) * sy
|
||||
|
||||
content_tag :rect, class: "barline__bar", x: i, y: y, width: 1, height: height do
|
||||
content_tag :circle, class: "barline__dot", cx: "#{i * factor}%", cy: y * sy + 1.25, r: 2.5 do
|
||||
content_tag(:title, val)
|
||||
end
|
||||
end
|
||||
|
||||
graph = content_tag :path, "", id: "js-barline-graph", class: "barline__bar", d: "M0,#{first_y}#{points}L#{width - sx},#{last_y}L#{width - sx},#{height}L0,#{height}L0,#{first_y}"
|
||||
|
||||
[graph, circles]
|
||||
end
|
||||
end
|
||||
|
||||
|
|
Loading…
Reference in a new issue