100 Commits
Author SHA1 Message Date
Rafael Silva 4b1af6f717 cache fix 2025-01-15 12:06:06 -03:00
Rafael dos Santos Silva 19af3cb107 Merge pull request #7 from jjaffeux/category-links
UX: Match category cards with tag cards
2025-01-15 09:45:32 -03:00
Jordan Vidrine 7cc39605f5 linting 2025-01-14 19:02:34 -06:00
Jordan Vidrine fed8205a64 DEV: Add fake category data 2025-01-14 15:57:23 -06:00
Jordan Vidrine d669a39a00 UX: Make card clickable link 2025-01-14 15:55:14 -06:00
Jordan Vidrine 6a55f56b91 Merge pull request #6 from jjaffeux/reading-series 2025-01-14 15:37:39 -06:00
Rafael Silva 17e30c3933 lint 2025-01-14 18:30:10 -03:00
Jordan Vidrine ef21ee6b35 DEV: Format 2025-01-14 15:27:23 -06:00
Jordan Vidrine b358456b65 more 2025-01-14 15:19:16 -06:00
Jordan Vidrine 472783d1c1 DEV: Add correct data to method 2025-01-14 15:17:10 -06:00
Jordan Vidrine 2bb1072d29 Merge pull request #5 from jjaffeux/long-words 2025-01-14 11:38:43 -06:00
Jordan Vidrine ff72e3ed37 UX: Adjust styles for long words 2025-01-14 11:30:13 -06:00
Jordan Vidrine 2398ab4fe6 Merge branch 'main' of https://github.com/jjaffeux/discourse-rewind into main 2025-01-14 11:02:12 -06:00
Jordan Vidrine 951a405c4c UX: Simplify hours and minutes shown 2025-01-14 11:02:03 -06:00
Jordan VidrineandJoffrey JAFFEUX 2e8e292b01 UX: More books styling (#4)
Allows to style books as series

---------

Co-authored-by: Joffrey JAFFEUX <[email protected]>
2025-01-14 17:49:57 +01:00
Rafael dos Santos Silva b045b23e04 Merge pull request #3 from jjaffeux/more-books
More books suggested by sam
2025-01-14 12:45:12 -03:00
Jordan Vidrine 444085caeb Titles 2025-01-14 09:05:45 -06:00
Jordan Vidrine 6982e76294 more book images 2025-01-14 09:04:12 -06:00
Rafael Silva 33476eafb4 More books suggested by sam 2025-01-14 11:17:55 -03:00
Jordan Vidrine 68097b3120 increase trigger for media query 2025-01-13 20:00:12 -06:00
Jordan Vidrine 0d25800241 Merge branch 'main' of https://github.com/jjaffeux/discourse-rewind into main 2025-01-13 17:30:57 -06:00
Jordan Vidrine db3a7e67a4 fix 2025-01-13 17:30:47 -06:00
Joffrey JAFFEUX 797924b018 bottom padding on mobile 2025-01-13 23:51:05 +01:00
Rafael Silva b6b36f9e4a Fix reaction ordering 2025-01-13 18:56:40 -03:00
Rafael Silva 5fa624a0e7 Fix reaction counting 2025-01-13 18:38:52 -03:00
Joffrey JAFFEUX ac95880bdb disabling cache for now 2025-01-13 22:29:42 +01:00
Joffrey JAFFEUX 566c605cca absolute image url 2025-01-13 22:17:28 +01:00
Rafael Silva 266e77dfed Fix logo case 2025-01-13 17:25:55 -03:00
Joffrey JAFFEUX 9a566a706a Merge branch 'main' of https://github.com/jjaffeux/discourse-rewind 2025-01-13 20:54:33 +01:00
Jordan Vidrine a42428442f more 2025-01-13 13:49:26 -06:00
Jordan Vidrine 135af147fa Merge branch 'main' of https://github.com/jjaffeux/discourse-rewind into main 2025-01-13 13:19:31 -06:00
Jordan Vidrine 3aa0610252 shimmer 2025-01-13 13:14:33 -06:00
Jordan Vidrine b92ceee74a more 2025-01-13 12:58:59 -06:00
Jordan Vidrine 98c08d19b3 color scheme issue 2025-01-13 11:52:21 -06:00
Joffrey JAFFEUX 193f92c89b requires_login 2025-01-13 18:17:02 +01:00
Joffrey JAFFEUX f88ab32434 linting 2025-01-13 18:13:24 +01:00
Joffrey JAFFEUX 8586610ee5 rewind border for categories 2025-01-13 18:08:42 +01:00
Jordan Vidrine 823f908cff more 2025-01-13 10:56:52 -06:00
Jordan Vidrine 9aef7c285a more 2025-01-13 10:55:59 -06:00
Jordan Vidrine 535aeba9fa more 2025-01-13 10:48:23 -06:00
Joffrey JAFFEUX d02e4bd5d2 removes log 2025-01-13 17:01:02 +01:00
Joffrey JAFFEUX 6bd637b7b0 fixes 2025-01-13 17:00:32 +01:00
Joffrey JAFFEUX 3049646b65 favorite categories/tags 2025-01-13 14:40:07 +01:00
Joffrey JAFFEUX 29a54cf075 rewind title 2025-01-13 13:46:45 +01:00
Joffrey JAFFEUX 935a290407 mobile stylesheet 2025-01-13 13:46:41 +01:00
Joffrey JAFFEUX b985dc8d25 i18n 2025-01-13 13:12:12 +01:00
Jordan Vidrine 4fef35cd3f final touches 2025-01-12 15:32:46 -06:00
Jordan Vidrine 73c20cb267 more 2025-01-12 15:24:56 -06:00
Jordan Vidrine ee4590d9d7 last bit 2025-01-10 21:10:14 -06:00
Jordan Vidrine 27b3547bbe hand off to joffrey 2025-01-10 21:08:47 -06:00
Jordan Vidrine 57ebde0836 more 2025-01-10 17:03:18 -06:00
Jordan Vidrine 7dc6b55b85 fbff 2025-01-10 17:02:27 -06:00
Jordan Vidrine 412a126e22 more solid styling choices 2025-01-10 14:26:35 -06:00
Jordan Vidrine 5427b90a93 Cards 2025-01-10 08:38:17 -06:00
Jordan Vidrine 1aa62c5760 more testing 2025-01-10 08:23:31 -06:00
Joffrey JAFFEUX 2f7cd88bf2 more reports and fake data 2025-01-09 17:55:58 +01:00
Joffrey JAFFEUX 88d928c315 fake data 2025-01-09 16:52:32 +01:00
Jordan Vidrine d970dbba68 edits 2025-01-09 09:27:58 -06:00
Jordan Vidrine 2e4343fff4 latest 2025-01-09 09:18:46 -06:00
Jordan Vidrine d32072e974 books 2025-01-08 15:11:42 -06:00
Joffrey JAFFEUX 90a03dec31 create kitchen sink spec for fake data 2025-01-07 23:26:17 +01:00
Jordan Vidrine 80210f11d2 styles and animation aditions 2025-01-06 15:04:58 -06:00
Jordan Vidrine 5e7daba027 styling: 2025-01-06 11:53:18 -06:00
Jordan Vidrine 4d7aad782d fonts 2025-01-03 20:20:20 -06:00
Jordan Vidrine 6c4142b4d4 better card flip 2025-01-03 20:03:03 -06:00
Jordan Vidrine 4f381a5da7 loading kind of works 2025-01-03 17:04:03 -06:00
Jordan Vidrine 4a996e6c80 animation, need to figure out how to preload images 2025-01-03 16:56:49 -06:00
Jordan Vidrine c04ca33c1d animation 2025-01-03 16:51:46 -06:00
Jordan Vidrine a36c61c92c animations 2025-01-03 11:44:06 -06:00
Jordan Vidrine 72b5ab50c5 merge 2025-01-03 10:50:18 -06:00
Jordan Vidrine 5e2fce3fde class names 2025-01-03 10:49:43 -06:00
Joffrey JAFFEUX 39dcfc159d Merge branch 'main' of https://github.com/jjaffeux/discourse-rewind 2025-01-03 17:43:09 +01:00
Joffrey JAFFEUX f28f894b97 favorite tags/categories 2025-01-03 17:43:02 +01:00
Jordan Vidrine d0f813207b Merge branch 'main' of https://github.com/jjaffeux/discourse-rewind into main 2025-01-03 10:40:24 -06:00
Jordan Vidrine bf79dc2512 parallax works : ) 2025-01-03 10:40:06 -06:00
Joffrey JAFFEUX 9847ecfe6e bests posts/topics 2025-01-03 17:25:18 +01:00
Joffrey JAFFEUX 0ef77503de clean 2025-01-03 17:12:19 +01:00
Joffrey JAFFEUX e9e6d80eac simple word cloud 2025-01-03 17:11:20 +01:00
Joffrey JAFFEUX 5b916dcf1c parallax 2025-01-03 16:52:21 +01:00
Jordan Vidrine 1ce828080f more 2025-01-03 09:42:42 -06:00
Jordan Vidrine 1fd84fff3b parallax 2025-01-03 09:35:18 -06:00
Jordan Vidrine 35628ccc33 more 2025-01-03 09:17:00 -06:00
Jordan Vidrine d31b2495e2 testing out parralax 2025-01-03 09:15:32 -06:00
Joffrey JAFFEUX bbab3a8f6f Merge branch 'main' of https://github.com/jjaffeux/discourse-rewind 2025-01-02 22:17:04 +01:00
Joffrey JAFFEUX 27b843f2c5 various tweaks 2025-01-02 22:15:10 +01:00
Jordan Vidrine a2a21c12de images 2025-01-02 15:15:05 -06:00
Jordan Vidrine 42e31a7f24 more 2024-12-20 15:50:01 -06:00
Jordan Vidrine 87b7de3ad3 more 2024-12-20 15:41:12 -06:00
Jordan Vidrine c62efa4819 more 2024-12-20 15:39:07 -06:00
Jordan Vidrine 409367588a styling 2024-12-20 14:55:55 -06:00
Joffrey JAFFEUX 3af6a12a55 start implementing reports 2024-12-20 18:56:02 +01:00
Rafael Silva bc80558e2d More reports 2024-12-19 15:31:12 -03:00
Joffrey JAFFEUX 1ce447e37f correct class name 2024-12-17 21:21:22 +01:00
Rafael Silva bf0f068938 Word Cloud report 2024-12-13 14:14:13 -03:00
Joffrey JAFFEUX baf19cbe9f base design 2024-12-13 17:51:06 +01:00
Joffrey JAFFEUX 66ec74bf2b DEV: move to an ember component 2024-12-13 17:00:17 +01:00
Joffrey JAFFEUX df21bc6e73 Fix: visible_post_types expects a user 2024-12-13 16:57:40 +01:00
Rafael Silva 589d2e9761 FBFF report 2024-12-13 10:48:20 -03:00
Rafael dos Santos Silva bc6ad108ea Reaction report (#2) 2024-12-12 21:45:22 +01:00
Rafael dos Santos Silva 906f9d6e9e Add reading_time and posting calendar reports (#1) 2024-12-12 15:12:04 +01:00
99 changed files with 2998 additions and 171 deletions
+1 -6
View File
@@ -4,9 +4,4 @@ Display stats on your last year of Discourse usage.
## Usage
- add `http://127.0.0.1:4200/rewinds?` in `SiteSetting.allowed_iframes`
- example embed of a rewind in a post:
```
<iframe src="http://127.0.0.1:4200/rewinds?year=2024&username=j.jaffeux" width="320" height="500" frameborder="0" style="border:0;" allowfullscreen="" aria-hidden="false" tabindex="0" />
```
Navigate to `/my/activity/rewind`
@@ -1,32 +0,0 @@
# frozen_string_literal: true
module ::DiscourseRewind
class RewindsAssetsController < ::ApplicationController
requires_plugin PLUGIN_NAME
skip_before_action :preload_json, :check_xhr, only: %i[show]
skip_before_action :verify_authenticity_token, only: %i[show]
def show
no_cookies
name = params[:name]
path, content_type =
if name == "rewind"
%w[rewind.css text/css]
else
raise Discourse::NotFound
end
content = File.read(DiscourseRewind.public_asset_path("css/#{path}"))
# note, path contains a ":version" which automatically busts the cache
# based on file content, so this is safe
response.headers["Last-Modified"] = 10.years.ago.httpdate
response.headers["Content-Length"] = content.bytesize.to_s
immutable_for 1.year
render plain: content, disposition: :nil, content_type: content_type
end
end
end
@@ -4,16 +4,15 @@ module ::DiscourseRewind
class RewindsController < ::ApplicationController
requires_plugin PLUGIN_NAME
skip_before_action :preload_json, :check_xhr, :redirect_to_login_if_required, only: %i[show]
requires_login
def show
# expires_in 1.minute, public: false
response.headers["X-Robots-Tag"] = "noindex"
DiscourseRewind::Rewind::Fetch.call(service_params) do
on_model_not_found(:year) { raise Discourse::NotFound }
on_model_not_found(:user) { raise Discourse::NotFound }
on_success do |reports:|
@reports = reports
render "show", layout: false
render json: MultiJson.dump(reports), status: 200
end
end
end
@@ -1,25 +0,0 @@
# frozen_string_literal: true
module DiscourseRewind
module RewindsHelper
# keeping it here for caching
def self.rewind_asset_url(asset_name)
if !%w[rewind.css].include?(asset_name)
raise StandardError, "unknown asset type #{asset_name}"
end
@urls ||= {}
url = @urls[asset_name]
return url if url
content = File.read(DiscourseRewind.public_asset_path("css/#{asset_name}"))
sha1 = Digest::SHA1.hexdigest(content)
url = "/rewinds/assets/#{sha1}/#{asset_name}"
@urls[asset_name] = GlobalPath.cdn_path(url)
end
def rewind_asset_url(asset_name)
DiscourseRewind::RewindsHelper.rewind_asset_url(asset_name)
end
end
end
@@ -0,0 +1,47 @@
# frozen_string_literal: true
# For a GitHub like calendar
# https://docs.github.com/assets/cb-35216/mw-1440/images/help/profile/contributions-graph.webp
module DiscourseRewind
class Rewind::Action::ActivityCalendar < Rewind::Action::BaseReport
FakeData = {
data:
(Date.new(2024, 1, 1)..Date.new(2024, 12, 31)).map do |date|
{ date: date.strftime("%Y-%m-%d"), post_count: rand(0..20), visited: false }
end,
identifier: "activity-calendar",
}
def call
return FakeData if Rails.env.development?
calendar =
Post
.unscoped
.joins(<<~SQL)
RIGHT JOIN
generate_series('#{date.first}', '#{date.last}', '1 day'::interval) ON
posts.created_at::date = generate_series::date AND
posts.user_id = #{user.id} AND
posts.deleted_at IS NULL
SQL
.joins(
"LEFT JOIN user_visits ON generate_series::date = visited_at AND user_visits.user_id = #{user.id}",
)
.select(
"generate_series::date as date, count(posts.id) as post_count, COUNT(visited_at) > 0 as visited",
)
.group("generate_series, user_visits.id")
.order("generate_series")
.map do |row|
{
date: row.date.strftime("%Y-%m-%d"),
post_count: row.post_count,
visited: row.visited,
}
end
{ data: calendar, identifier: "activity-calendar" }
end
end
end
@@ -0,0 +1,16 @@
# frozen_string_literal: true
module DiscourseRewind
class Rewind::Action::BaseReport < Service::ActionBase
option :user
option :date
def call
raise NotImplementedError
end
def self.enabled?
true
end
end
end
@@ -0,0 +1,19 @@
# frozen_string_literal: true
module DiscourseRewind
class Rewind::Action::BestPosts < Rewind::Action::BaseReport
def call
best_posts =
Post
.where(user_id: user.id)
.where(created_at: date)
.where(deleted_at: nil)
.where("post_number > 1")
.order("like_count DESC NULLS LAST")
.limit(3)
.pluck(:post_number, :topic_id, :like_count, :reply_count, :raw, :cooked)
{ data: best_posts, identifier: "best-posts" }
end
end
end
@@ -0,0 +1,21 @@
# frozen_string_literal: true
module DiscourseRewind
class Rewind::Action::BestTopics < Rewind::Action::BaseReport
def call
best_topics =
TopTopic
.includes(:topic)
.references(:topic)
.where(topic: { deleted_at: nil, created_at: date, user_id: user.id })
.order("yearly_score DESC NULLS LAST")
.limit(3)
.pluck(:topic_id, :title, :excerpt, :yearly_score)
.map do |topic_id, title, excerpt, yearly_score|
{ topic_id: topic_id, title: title, excerpt: excerpt, yearly_score: yearly_score }
end
{ data: best_topics, identifier: "best-topics" }
end
end
end
@@ -1,23 +0,0 @@
# frozen_string_literal: true
module DiscourseRewind
class Rewind::Action::CreatePostsCountReport < Service::ActionBase
option :params
option :guardian
option :date
option :user
delegate :username, :year, to: :params, private: true
def call
p date
p user
p guardian
p username
p year
p params
{ data: { count: 5 }, identifier: "posts-count-report" }
end
end
end
@@ -0,0 +1,34 @@
# frozen_string_literal: true
# Topics visited grouped by category
module DiscourseRewind
class Rewind::Action::FavoriteCategories < Rewind::Action::BaseReport
FakeData = {
data: [
{ category_id: 1, name: "cats" },
{ category_id: 2, name: "dogs" },
{ category_id: 3, name: "countries" },
{ category_id: 4, name: "management" },
{ category_id: 5, name: "things" },
],
identifier: "favorite-categories",
}
def call
return FakeData if Rails.env.development?
favorite_categories =
TopicViewItem
.joins(:topic)
.joins("INNER JOIN categories ON categories.id = topics.category_id")
.where(user: user)
.where(viewed_at: date)
.group("categories.id, categories.name")
.order("COUNT(*) DESC")
.limit(5)
.pluck("categories.id, categories.name")
.map { |category_id, name| { category_id: category_id, name: name } }
{ data: favorite_categories, identifier: "favorite-categories" }
end
end
end
@@ -0,0 +1,36 @@
# frozen_string_literal: true
# Topics visited grouped by tag
module DiscourseRewind
class Rewind::Action::FavoriteTags < Rewind::Action::BaseReport
FakeData = {
data: [
{ tag_id: 1, name: "cats" },
{ tag_id: 2, name: "dogs" },
{ tag_id: 3, name: "countries" },
{ tag_id: 4, name: "management" },
{ tag_id: 5, name: "things" },
],
identifier: "favorite-tags",
}
def call
return FakeData if Rails.env.development?
favorite_tags =
TopicViewItem
.joins(:topic)
.joins("INNER JOIN topic_tags ON topic_tags.topic_id = topics.id")
.joins("INNER JOIN tags ON tags.id = topic_tags.tag_id")
.where(user: user)
.where(viewed_at: date)
.group("tags.id, tags.name")
.order("COUNT(DISTINCT topic_views.topic_id) DESC")
.limit(5)
.pluck("tags.id, tags.name")
.map { |tag_id, name| { tag_id: tag_id, name: name } }
{ data: favorite_tags, identifier: "favorite-tags" }
end
end
end
@@ -0,0 +1,112 @@
# frozen_string_literal: true
# Forum Best Friend Forever ranking
# Score is informative only, do not show in UI
module DiscourseRewind
class Rewind::Action::Fbff < Rewind::Action::BaseReport
MAX_SUMMARY_RESULTS ||= 50
LIKE_SCORE ||= 1
REPLY_SCORE ||= 10
def call
most_liked_users =
like_query(date)
.where(acting_user_id: user.id)
.group(:user_id)
.order("COUNT(*) DESC")
.limit(MAX_SUMMARY_RESULTS)
.pluck("user_actions.user_id, COUNT(*)")
.map { |user_id, count| { user_id => count } }
.reduce({}, :merge)
most_liked_by_users =
like_query(date)
.where(user: user)
.group(:acting_user_id)
.order("COUNT(*) DESC")
.limit(MAX_SUMMARY_RESULTS)
.pluck("acting_user_id, COUNT(*)")
.map { |acting_user_id, count| { acting_user_id => count } }
.reduce({}, :merge)
users_who_most_replied_me =
post_query(user, date)
.where(posts: { user_id: user.id })
.group("replies.user_id")
.order("COUNT(*) DESC")
.limit(MAX_SUMMARY_RESULTS)
.pluck("replies.user_id, COUNT(*)")
.map { |user_id, count| { user_id => count } }
.reduce({}, :merge)
users_i_most_replied =
post_query(user, date)
.where("replies.user_id = ?", user.id)
.group("posts.user_id")
.order("COUNT(*) DESC")
.limit(MAX_SUMMARY_RESULTS)
.pluck("posts.user_id, COUNT(*)")
.map { |user_id, count| { user_id => count } }
.reduce({}, :merge)
#TODO include chat?
fbffs = [
apply_score(most_liked_users, LIKE_SCORE),
apply_score(most_liked_by_users, LIKE_SCORE),
apply_score(users_who_most_replied_me, REPLY_SCORE),
apply_score(users_i_most_replied, REPLY_SCORE),
]
fbff_id =
fbffs
.flatten
.inject { |h1, h2| h1.merge(h2) { |_, v1, v2| v1 + v2 } }
&.sort_by { |_, v| -v }
&.first
&.first
return if !fbff_id
{
data: {
fbff: BasicUserSerializer.new(User.find(fbff_id), root: false).as_json,
yourself: BasicUserSerializer.new(user, root: false).as_json,
},
identifier: "fbff",
}
end
def post_query(user, date)
Post
.joins(:topic)
.includes(:topic)
.where(
"posts.post_type IN (?)",
Topic.visible_post_types(user, include_moderator_actions: false),
)
.joins(
"INNER JOIN posts replies ON posts.topic_id = replies.topic_id AND posts.reply_to_post_number = replies.post_number",
)
.joins(
"INNER JOIN topics ON replies.topic_id = topics.id AND topics.archetype <> 'private_message'",
)
.joins(
"AND replies.post_type IN (#{Topic.visible_post_types(user, include_moderator_actions: false).join(",")})",
)
.where("replies.created_at BETWEEN ? AND ?", date.first, date.last)
.where("replies.user_id <> posts.user_id")
end
def like_query(date)
UserAction
.joins(:target_topic, :target_post)
.where(created_at: date)
.where(action_type: UserAction::WAS_LIKED)
end
def apply_score(users, score)
users.map { |user_id, count| { user_id => count * score } }
end
end
end
@@ -0,0 +1,90 @@
# frozen_string_literal: true
# For a most user / received reactions cards
module DiscourseRewind
class Rewind::Action::Reactions < Rewind::Action::BaseReport
FakeData = {
data: {
post_received_reactions: {
open_mouth: 2,
cat: 32,
dog: 34,
heart: 45,
grinning: 82,
},
post_used_reactions: {
open_mouth: 2,
cat: 32,
dog: 34,
heart: 45,
grinning: 82,
},
chat_used_reactions: {
open_mouth: 2,
cat: 32,
dog: 34,
heart: 45,
grinning: 82,
},
chat_received_reactions: {
open_mouth: 2,
cat: 32,
dog: 34,
heart: 45,
grinning: 82,
},
},
identifier: "reactions",
}
def call
return FakeData if Rails.env.development?
data = {}
if defined?(DiscourseReactions::Reaction)
# This is missing heart reactions (default like)
data[:post_used_reactions] = sort_and_limit(
DiscourseReactions::Reaction
.by_user(user)
.where(created_at: date)
.group(:reaction_value)
.count,
)
data[:post_received_reactions] = sort_and_limit(
DiscourseReactions::Reaction
.includes(:post)
.where(posts: { user_id: user.id })
.where(created_at: date)
.group(:reaction_value)
.count,
)
end
if SiteSetting.chat_enabled
data[:chat_used_reactions] = sort_and_limit(
Chat::MessageReaction.where(user: user).where(created_at: date).group(:emoji).count,
)
data[:chat_received_reactions] = sort_and_limit(
Chat::MessageReaction
.includes(:chat_message)
.where(chat_message: { user_id: user.id })
.where(created_at: date)
.group(:emoji)
.count,
)
end
{ data:, identifier: "reactions" }
end
def enabled?
SiteSetting.discourse_reactions_enabled || SiteSetting.chat_enabled
end
def sort_and_limit(reactions)
reactions.sort_by { |_, v| -v }.first(5).reverse.to_h
end
end
end
@@ -0,0 +1,200 @@
# frozen_string_literal: true
# For showcasing the reading time of a user
# Should we show book covers or just the names?
module DiscourseRewind
class Rewind::Action::ReadingTime < Rewind::Action::BaseReport
FakeData = {
data: {
reading_time: 2_880_000,
book: "The Combined Cosmere works + Wheel of Time",
isbn: "978-0812511819",
series: true,
},
identifier: "reading-time",
}
def call
return FakeData if Rails.env.development?
reading_time = UserVisit.where(user_id: user.id).where(visited_at: date).sum(:time_read)
book = best_book_fit(reading_time)
return if book.nil?
{
data: {
reading_time: reading_time,
book: book[:title],
isbn: book[:isbn],
series: book[:series],
},
identifier: "reading-time",
}
end
def popular_books
{
"The Hunger Games" => {
reading_time: 19_740,
isbn: "978-0439023481",
series: false,
},
"The Metamorphosis" => {
reading_time: 3120,
isbn: "978-0553213690",
series: false,
},
"To Kill a Mockingbird" => {
reading_time: 22_800,
isbn: "978-0061120084",
series: false,
},
"Pride and Prejudice" => {
reading_time: 25_200,
isbn: "978-1503290563",
series: false,
},
"1984" => {
reading_time: 16_800,
isbn: "978-0451524935",
series: false,
},
"The Lord of the Rings" => {
reading_time: 108_000,
isbn: "978-0544003415",
series: true,
},
"Harry Potter and the Sorcerer's Stone" => {
reading_time: 24_600,
isbn: "978-0590353427",
series: true,
},
"The Great Gatsby" => {
reading_time: 12_600,
isbn: "978-0743273565",
series: false,
},
"The Little Prince" => {
reading_time: 5400,
isbn: "978-0156012195",
series: false,
},
"Animal Farm" => {
reading_time: 7200,
isbn: "978-0451526342",
series: false,
},
"The Catcher in the Rye" => {
reading_time: 18_000,
isbn: "978-0316769488",
series: false,
},
"Jane Eyre" => {
reading_time: 34_200,
isbn: "978-0141441146",
series: false,
},
"Fahrenheit 451" => {
reading_time: 15_000,
isbn: "978-1451673319",
series: false,
},
"The Hobbit" => {
reading_time: 27_000,
isbn: "978-0547928227",
series: false,
},
"The Da Vinci Code" => {
reading_time: 37_800,
isbn: "978-0307474278",
series: false,
},
"Little Women" => {
reading_time: 30_000,
isbn: "978-0147514011",
series: false,
},
"One Hundred Years of Solitude" => {
reading_time: 46_800,
isbn: "978-0060883287",
series: false,
},
"And Then There Were None" => {
reading_time: 16_200,
isbn: "978-0062073488",
series: false,
},
"The Alchemist" => {
reading_time: 10_800,
isbn: "978-0061122415",
series: false,
},
"The Hitchhiker's Guide to the Galaxy" => {
reading_time: 12_600,
isbn: "978-0345391803",
series: false,
},
"The Complete works of Shakespeare" => {
reading_time: 180_000,
isbn: "978-1853268953",
series: true,
},
"The Game of Thrones Series" => {
reading_time: 360_000,
isbn: "978-0007477159",
series: true,
},
"Malazan Book of the Fallen" => {
reading_time: 720_000,
isbn: "978-0765348821",
series: true,
},
"Terry Pratchett’s Discworld series" => {
reading_time: 1_440_000,
isbn: "978-9123684458",
series: true,
},
"The Wandering Inn web series" => {
reading_time: 2_160_000,
isbn: "the-wandering-inn",
series: true,
},
"The Combined Cosmere works + Wheel of Time" => {
reading_time: 2_880_000,
isbn: "978-0812511819",
series: true,
},
"The Star Trek novels" => {
reading_time: 3_600_000,
isbn: "978-1852860691",
series: true,
},
}.symbolize_keys
end
def best_book_fit(reading_time)
reading_time_rest = reading_time
books = []
while reading_time_rest > 0
best_fit = popular_books.min_by { |_, v| (v[:reading_time] - reading_time_rest).abs }
break if best_fit.nil?
books << best_fit.first
reading_time_rest -= best_fit.last[:reading_time]
end
return if books.empty?
book_title =
books.group_by { |book| book }.transform_values(&:count).max_by { |_, count| count }.first
{
title: book_title,
isbn: popular_books[book_title][:isbn],
series: popular_books[book_title][:series],
}
end
end
end
@@ -0,0 +1,86 @@
# frozen_string_literal: true
module DiscourseRewind
class Rewind::Action::TopWords < Rewind::Action::BaseReport
FakeData = {
data: [
{ word: "what", score: 100 },
{ word: "have", score: 90 },
{ word: "you", score: 80 },
{ word: "overachieved", score: 70 },
{ word: "this", score: 60 },
{ word: "week", score: 50 },
],
identifier: "top-words",
}
def call
return FakeData if Rails.env.development?
words = DB.query(<<~SQL, user_id: user.id, date_start: date.first, date_end: date.last)
WITH popular_words AS (
SELECT
*
FROM
ts_stat(
$INNERSQL$
SELECT
search_data
FROM
post_search_data
INNER JOIN
posts ON posts.id = post_search_data.post_id
WHERE
posts.user_id = :user_id
AND posts.created_at BETWEEN :date_start AND :date_end
$INNERSQL$
) AS search_data
ORDER BY
nentry DESC,
ndoc DESC,
word
LIMIT
100
), lex AS (
SELECT
DISTINCT ON (lexeme) to_tsvector('english', word) as lexeme,
word as original_word
FROM
ts_stat ($INNERSQL$
SELECT
to_tsvector('simple', raw)
FROM
posts AS p
WHERE
p.created_at BETWEEN :date_start AND :date_end
AND p.user_id = :user_id
$INNERSQL$)
), ranked_words AS (
SELECT
popular_words.*, lex.original_word,
ROW_NUMBER() OVER (PARTITION BY word ORDER BY LENGTH(original_word)) AS rn
FROM
popular_words
INNER JOIN
lex ON lex.lexeme @@ to_tsquery('english', popular_words.word)
)
SELECT
word,
ndoc,
nentry,
original_word
FROM
ranked_words
WHERE
rn = 1
ORDER BY
ndoc + nentry DESC
LIMIT 100
SQL
word_score = words.map { { word: _1.original_word, score: _1.ndoc + _1.nentry } }
{ data: word_score, identifier: "top-words" }
end
end
end
+34 -29
View File
@@ -5,11 +5,7 @@ module DiscourseRewind
#
# @example
# ::DiscourseRewind::Rewind::Fetch.call(
# guardian: guardian,
# params: {
# username: "falco",
# year: 2024,
# }
# guardian: guardian
# )
#
class Rewind::Fetch
@@ -22,42 +18,51 @@ module DiscourseRewind
# @option params [Integer] :username of the rewind
# @return [Service::Base::Context]
# order matters, rewinds are displayed in the order they are defined
REPORTS = [DiscourseRewind::Rewind::Action::CreatePostsCountReport]
CACHE_DURATION = 5.minutes
params do
attribute :year, :integer
attribute :username, :string
validates :year, presence: true
validates :username, presence: true
end
model :year
model :user
model :date
model :reports
private
def fetch_user(params:)
User.find_by_username(params.username)
def fetch_user(guardian:)
User.find_by_username(guardian.user.username)
end
def fetch_date(params:)
Date.new(params.year).all_year
end
def fetch_year
current_date = Time.zone.now
current_month = current_date.month
current_year = current_date.year
def fetch_reports(params:, date:, user:, guardian:)
key = "rewind:#{params.username}:#{params.year}"
reports = Discourse.redis.get(key)
if Rails.env.development? || !reports
reports = REPORTS.map { |report| report.call(params:, date:, user:, guardian:) }
Discourse.redis.setex(key, CACHE_DURATION, MultiJson.dump(reports))
case current_month
when 1
current_year - 1
when 12
current_year
else
reports = MultiJson.load(reports, symbolize_keys: true)
false
end
end
def fetch_date(params:, year:)
Date.new(year).all_year
end
def fetch_reports(date:, user:, guardian:, year:)
key = "rewind:#{guardian.user.id}:#{year}"
cached_reports = Discourse.redis.get(key)
if !cached_reports
reports =
::DiscourseRewind::Rewind::Action::BaseReport
.descendants
.filter { _1.enabled? }
.map { |report| report.call(date:, user:, guardian:) }
Discourse.redis.setex(key, CACHE_DURATION, reports.to_json)
else
reports = JSON.parse(cached_reports, symbolize_names: true)
end
reports
@@ -1,31 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title><%= I18n.t("discourse_rewind.title", title: "RETRO TITLE", site_name: SiteSetting.title) %></title>
<meta property="og:title" content="<%= I18n.t("discourse_rewind.title", title: "RETRO TITLE", site_name: SiteSetting.title) %>">
<meta property="og:description" content="DESC">
<meta property="og:type" content="website">
<meta property="og:url" content="<%= request.original_url %>">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="<%= I18n.t("discourse_rewind.title", title: "RETRO TITLE", site_name: SiteSetting.title) %>">
<meta name="twitter:description" content="DESC">
<meta name="viewport" content="width=device-width">
<link rel="stylesheet" href="<%= rewind_asset_url("rewind.css") %>">
</head>
<body>
<div class="rewind__wrapper">
<div class="rewind">
<%- @reports.each do |report| %>
<div class="rewind__report">
<div class="rewind__report__header">
<%= report[:identifier] %>
</div>
<div class="rewind__report__body">
<%= report[:data][:count] %>
</div>
</div>
<%- end %>
</div>
</div>
</body>
</html>
@@ -0,0 +1,124 @@
import Component from "@glimmer/component";
import { action } from "@ember/object";
import concatClass from "discourse/helpers/concat-class";
import { i18n } from "discourse-i18n";
const ROWS = 7;
const COLS = 53;
export default class ActivityCalendar extends Component {
get rowsArray() {
const data = this.args.report.data;
let rowsArray = [];
for (let r = 0; r < ROWS; r++) {
let rowData = [];
for (let c = 0; c < COLS; c++) {
const index = c * ROWS + r;
rowData.push(data[index] ? data[index] : "");
}
rowsArray.push(rowData);
}
return rowsArray;
}
@action
getAbbreviatedMonth(monthIndex) {
return moment().month(monthIndex).format("MMM");
}
@action
computeClass(count) {
if (!count) {
return "-empty";
} else if (count < 10) {
return "-low";
} else if (count < 20) {
return "-medium";
} else {
return "-high";
}
}
<template>
<div class="rewind-report-page -activity-calendar">
<h2 class="rewind-report-title">{{i18n
"discourse_rewind.reports.activity_calendar.title"
}}</h2>
<div class="rewind-card">
<table class="rewind-calendar">
<thead>
<tr>
<td
colspan="5"
class="activity-header-cell"
>{{this.getAbbreviatedMonth 0}}</td>
<td
colspan="4"
class="activity-header-cell"
>{{this.getAbbreviatedMonth 1}}</td>
<td
colspan="4"
class="activity-header-cell"
>{{this.getAbbreviatedMonth 2}}</td>
<td
colspan="5"
class="activity-header-cell"
>{{this.getAbbreviatedMonth 3}}</td>
<td
colspan="4"
class="activity-header-cell"
>{{this.getAbbreviatedMonth 4}}</td>
<td
colspan="4"
class="activity-header-cell"
>{{this.getAbbreviatedMonth 5}}</td>
<td
colspan="5"
class="activity-header-cell"
>{{this.getAbbreviatedMonth 6}}</td>
<td
colspan="4"
class="activity-header-cell"
>{{this.getAbbreviatedMonth 7}}</td>
<td
colspan="5"
class="activity-header-cell"
>{{this.getAbbreviatedMonth 8}}</td>
<td
colspan="4"
class="activity-header-cell"
>{{this.getAbbreviatedMonth 9}}</td>
<td
colspan="4"
class="activity-header-cell"
>{{this.getAbbreviatedMonth 10}}</td>
<td
colspan="4"
class="activity-header-cell"
>{{this.getAbbreviatedMonth 11}}</td>
</tr>
</thead>
<tbody>
{{#each this.rowsArray as |row|}}
<tr>
{{#each row as |cell|}}
<td
data-date={{cell.date}}
title={{cell.date}}
class={{concatClass
"rewind-calendar-cell"
(this.computeClass cell.post_count)
}}
></td>
{{/each}}
</tr>
{{/each}}
</tbody>
</table>
</div>
</div>
</template>
}
@@ -0,0 +1,41 @@
import Component from "@glimmer/component";
import { concat } from "@ember/helper";
import { get } from "@ember/object";
import { htmlSafe } from "@ember/template";
import icon from "discourse-common/helpers/d-icon";
import { i18n } from "discourse-i18n";
export default class BestPosts extends Component {
rank(idx) {
return idx + 1;
}
<template>
{{#if @report.data.length}}
<div class="rewind-report-page -best-posts">
<h2 class="rewind-report-title">{{i18n
"discourse_rewind.reports.best_posts.title"
[email protected]
}}</h2>
<div class="rewind-report-container">
{{#each @report.data as |post idx|}}
<div class={{concat "rewind-card" " rank-" (this.rank idx)}}>
<span class="best-posts -rank"></span>
<span class="best-posts -rank"></span>
<div class="best-posts__post">{{htmlSafe (get post "5")}}</div>
<div class="best-posts__metadata">
<span class="best-posts__likes">
{{icon "heart"}}{{htmlSafe (get post "2")}}</span>
<span class="best-posts__replies">
{{icon "comment"}}{{htmlSafe (get post "3")}}</span>
<a href="/t/{{get post '1'}}/{{get post '0'}}">{{i18n
"discourse_rewind.reports.best_posts.view_post"
}}</a>
</div>
</div>
{{/each}}
</div>
</div>
{{/if}}
</template>
}
@@ -0,0 +1,38 @@
import Component from "@glimmer/component";
import { concat } from "@ember/helper";
import { htmlSafe } from "@ember/template";
import { i18n } from "discourse-i18n";
export default class BestTopics extends Component {
rank(idx) {
return idx + 1;
}
<template>
{{#if @report.data.length}}
<div class="rewind-report-page -best-topics">
<h2 class="rewind-report-title">{{i18n
"discourse_rewind.reports.best_topics.title"
[email protected]
}}</h2>
<div class="rewind-report-container">
<div class="rewind-card">
{{#each @report.data as |topic idx|}}
<a
href={{concat "/t/-/" topic.topic_id}}
class={{concat "best-topics__topic" " rank-" (this.rank idx)}}
>
<span class="best-topics -rank"></span>
<span class="best-topics -rank"></span>
<h2 class="best-topics__header">{{topic.title}}</h2>
<span class="best-topics__excerpt">{{htmlSafe
topic.excerpt
}}</span>
</a>
{{/each}}
</div>
</div>
</div>
{{/if}}
</template>
}
@@ -0,0 +1,25 @@
import { concat } from "@ember/helper";
import { i18n } from "discourse-i18n";
const FavoriteCategories = <template>
{{#if @report.data.length}}
<div class="rewind-report-page -favorite-categories">
<h2 class="rewind-report-title">{{i18n
"discourse_rewind.reports.favorite_categories.title"
[email protected]
}}</h2>
<div class="rewind-report-container">
{{#each @report.data as |data|}}
<a href={{concat "/c/-/" data.category_id}} class="rewind-card">
<p
class="favorite-categories__category"
href={{concat "/c/-/" data.category_id}}
>{{data.name}}</p>
</a>
{{/each}}
</div>
</div>
{{/if}}
</template>;
export default FavoriteCategories;
@@ -0,0 +1,25 @@
import { concat } from "@ember/helper";
import { i18n } from "discourse-i18n";
const FavoriteTags = <template>
{{#if @report.data.length}}
<div class="rewind-report-page -favorite-tags">
<h2 class="rewind-report-title">{{i18n
"discourse_rewind.reports.favorite_tags.title"
[email protected]
}}</h2>
<div class="rewind-report-container">
{{#each @report.data as |data|}}
<a class="rewind-card" href={{concat "/tag/" data.name}}>
<p
class="favorite-tags__tag"
href={{concat "/tag/" data.name}}
>#{{data.name}}</p>
</a>
{{/each}}
</div>
</div>
{{/if}}
</template>;
export default FavoriteTags;
@@ -0,0 +1,39 @@
import { hash } from "@ember/helper";
import avatar from "discourse/helpers/bound-avatar-template";
import { i18n } from "discourse-i18n";
const FBFF = <template>
<div class="rewind-report-page -fbff">
<h2 class="rewind-report-title">{{i18n
"discourse_rewind.reports.fbff.title"
}}</h2>
<div class="rewind-report-container">
<div class="rewind-card">
<div class="fbff-avatar-container">
{{avatar
@report.data.fbff.avatar_template
"huge"
(hash [email protected])
}}
<p class="fbff-avatar-name">@{{@report.data.fbff.username}}</p>
</div>
<div class="fbff-gif-container">
<img
class="fbff-gif"
src="/plugins/discourse-rewind/images/fbff.gif"
/>
</div>
<div class="fbff-avatar-container">
{{avatar
@report.data.yourself.avatar_template
"huge"
(hash [email protected])
}}
<p class="fbff-avatar-name">@{{@report.data.yourself.username}}</p>
</div>
</div>
</div>
</div>
</template>;
export default FBFF;
@@ -0,0 +1,14 @@
const RewindHeader = <template>
<div class="rewind__header">
<img
class="rewind-logo -light"
src="/plugins/discourse-rewind/images/discourse-rewind-logo.png"
/>
<img
class="rewind-logo -dark"
src="/plugins/discourse-rewind/images/discourse-rewind-logo-dark.png"
/>
</div>
</template>;
export default RewindHeader;
@@ -0,0 +1,81 @@
import Component from "@glimmer/component";
import { concat } from "@ember/helper";
import { action } from "@ember/object";
import { htmlSafe } from "@ember/template";
import replaceEmoji from "discourse/helpers/replace-emoji";
import { i18n } from "discourse-i18n";
export default class Reactions extends Component {
get totalPostUsedReactions() {
return Object.values(
this.args.report.data.post_used_reactions ?? {}
).reduce((acc, count) => acc + count, 0);
}
get receivedReactions() {
return this.args.report.data.post_received_reactions ?? {};
}
@action
cleanEmoji(emojiName) {
return emojiName.replaceAll(/_/g, " ");
}
@action
computePercentage(count) {
return `${((count / this.totalPostUsedReactions) * 100).toFixed(2)}%`;
}
@action
computePercentageStyle(count) {
return htmlSafe(`width: ${this.computePercentage(count)}`);
}
<template>
<div class="rewind-report-page -post-received-reactions">
<h2 class="rewind-report-title">{{i18n
"discourse_rewind.reports.post_received_reactions.title"
}}</h2>
<div class="rewind-report-container">
{{#each-in this.receivedReactions as |emojiName count|}}
<div class="rewind-card scale">
<span class="rewind-card__emoji">{{replaceEmoji
(concat ":" emojiName ":")
}}</span>
<span class="rewind-card__data">{{count}}</span>
</div>
{{/each-in}}
</div>
</div>
<div class="rewind-report-page -post-used-reactions">
<h2 class="rewind-report-title">{{i18n
"discourse_rewind.reports.post_used_reactions.title"
}}</h2>
<div class="rewind-card">
<div class="rewind-reactions-chart">
{{#each-in @report.data.post_used_reactions as |emojiName count|}}
<div class="rewind-reactions-row">
<span class="emoji">{{replaceEmoji
(concat ":" emojiName ":")
}}</span>
<span class="percentage">{{this.computePercentage count}}</span>
<div
class="rewind-reactions-bar"
style={{this.computePercentageStyle count}}
title={{count}}
></div>
</div>
{{/each-in}}
<span class="rewind-total-reactions">
{{i18n
"discourse_rewind.reports.post_used_reactions.total_number"
count=this.totalPostUsedReactions
}}
</span>
</div>
</div>
</div>
</template>
}
@@ -0,0 +1,53 @@
import Component from "@glimmer/component";
import { htmlSafe } from "@ember/template";
import { i18n } from "discourse-i18n";
export default class ReadingTime extends Component {
get readTimeString() {
let totalMinutes = Math.floor(this.args.report.data.reading_time / 60);
let leftOverMinutes = totalMinutes % 60;
let totalHours = (totalMinutes - leftOverMinutes) / 60;
if (leftOverMinutes >= 35) {
totalHours += 1;
leftOverMinutes = 0;
return `${totalHours}h`;
} else {
return `${totalHours}h ${leftOverMinutes}m`;
}
}
<template>
{{#if @report.data}}
<div class="rewind-report-page -reading-time">
<h2 class="rewind-report-title">{{i18n
"discourse_rewind.reports.reading_time.title"
}}</h2>
<div class="rewind-card">
<p class="reading-time__text">
{{htmlSafe
(i18n
"discourse_rewind.reports.reading_time.book_comparison"
readingTitme=this.readTimeString
[email protected]
)
}}
</p>
<div class="reading-time__book">
<div class="book">
<img
alt=""
src="/plugins/discourse-rewind/images/books/{{@report.data.isbn}}.jpg"
/>
</div>
{{#if @report.data.series}}
<div class="book-series one"></div>
<div class="book-series two"></div>
<div class="book-series three"></div>
{{/if}}
</div>
</div>
</div>
{{/if}}
</template>
}
@@ -0,0 +1,28 @@
import Component from "@glimmer/component";
import { i18n } from "discourse-i18n";
import WordCard from "discourse/plugins/discourse-rewind/discourse/components/reports/top-words/word-card";
export default class WordCards extends Component {
get topWords() {
return this.args.report.data.sort((a, b) => b.score - a.score).slice(0, 5);
}
<template>
<div class="rewind-report-page -top-words">
<div class="rewind-report-container">
<h2 class="rewind-report-title">{{i18n
"discourse_rewind.reports.top_words.title"
}}</h2>
<div class="cards-container">
{{#each this.topWords as |entry index|}}
<WordCard
@word={{entry.word}}
@count={{entry.score}}
@index={{index}}
/>
{{/each}}
</div>
</div>
</div>
</template>
}
@@ -0,0 +1,107 @@
import Component from "@glimmer/component";
import { on } from "@ember/modifier";
import { action } from "@ember/object";
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
import { htmlSafe } from "@ember/template";
import concatClass from "discourse/helpers/concat-class";
import emoji from "discourse/helpers/emoji";
import discourseLater from "discourse-common/lib/later";
const MYSTERY_EMOJIS = [
"mag", // 🔍
"question", // ❓
"8ball", // 🎱
"crystal_ball", // 🔮
"crescent_moon", // 🌙
];
const BACKGROUND_COLORS = [
[
"251, 245, 175",
"40, 171, 226",
"12, 166, 78",
"240, 121, 74",
"232, 74, 81",
],
[
"197, 193, 140",
"39, 137, 178",
"17, 138, 68",
"188, 105, 65",
"183, 64, 70",
],
];
export default class WordCard extends Component {
get randomStyle() {
return `--rand: ${Math.random()}`;
}
get mysteryData() {
return {
emoji: MYSTERY_EMOJIS[this.args.index],
color: `--mystery-color-light: ${
BACKGROUND_COLORS[0][this.args.index]
}; --mystery-color-dark: ${BACKGROUND_COLORS[1][this.args.index]};`,
};
}
get longWord() {
return this.args.word.length >= 7;
}
get cardStyle() {
return htmlSafe(`${this.randomStyle}; ${this.mysteryData.color};`);
}
@action
registerCardContainer(element) {
this.cardContainer = element;
}
@action
handleClick() {
this.cardContainer.classList.toggle("flipped");
}
@action
handleLeave() {
const cardContainer = this.cardContainer;
cardContainer.classList.toggle("mouseleave");
discourseLater(() => {
cardContainer.classList.remove("mouseleave");
}, 100);
}
<template>
<div
{{on "click" this.handleClick}}
{{on "mouseleave" this.handleLeave}}
class={{concatClass
"rewind-card__wrapper"
(if this.longWord "-long-word")
}}
style={{this.cardStyle}}
{{didInsert this.registerCardContainer}}
role="button"
>
<div class="rewind-card__inner">
<div class="rewind-card -front">
<span class="rewind-card__image tl">{{emoji
this.mysteryData.emoji
}}</span>
<span class="rewind-card__image cr">{{emoji
this.mysteryData.emoji
}}</span>
<span class="rewind-card__image br">{{emoji
this.mysteryData.emoji
}}</span>
</div>
<div class="rewind-card -back">
<span class="rewind-card__title">{{@word}}</span>
<span class="rewind-card__data">{{@count}}x</span>
</div>
</div>
</div>
</template>
}
@@ -0,0 +1,152 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { on } from "@ember/modifier";
import { action } from "@ember/object";
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
import { service } from "@ember/service";
import { eq } from "truth-helpers";
import DButton from "discourse/components/d-button";
import concatClass from "discourse/helpers/concat-class";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import ActivityCalendar from "discourse/plugins/discourse-rewind/discourse/components/reports/activity-calendar";
import BestPosts from "discourse/plugins/discourse-rewind/discourse/components/reports/best-posts";
import BestTopics from "discourse/plugins/discourse-rewind/discourse/components/reports/best-topics";
import FavoriteCategories from "discourse/plugins/discourse-rewind/discourse/components/reports/favorite-categories";
import FavoriteTags from "discourse/plugins/discourse-rewind/discourse/components/reports/favorite-tags";
import FBFF from "discourse/plugins/discourse-rewind/discourse/components/reports/fbff";
import RewindHeader from "discourse/plugins/discourse-rewind/discourse/components/reports/header";
import Reactions from "discourse/plugins/discourse-rewind/discourse/components/reports/reactions";
import ReadingTime from "discourse/plugins/discourse-rewind/discourse/components/reports/reading-time";
import TopWords from "discourse/plugins/discourse-rewind/discourse/components/reports/top-words";
export default class Rewind extends Component {
@service siteSettings;
@tracked rewind = [];
@tracked fullScreen = true;
@tracked loadingRewind = false;
@action
registerScrollWrapper(element) {
this.scrollWrapper = element;
}
@action
async loadRewind() {
try {
this.loadingRewind = true;
this.rewind = await ajax("/rewinds");
} catch (e) {
popupAjaxError(e);
} finally {
this.loadingRewind = false;
}
}
@action
toggleFullScreen() {
this.fullScreen = !this.fullScreen;
}
@action
handleEscape(event) {
if (this.fullScreen && event.key === "Escape") {
this.fullScreen = false;
}
}
@action
handleScroll({ target }) {
let children = this.rewindContainer.getElementsByClassName("parallax-bg");
for (let i = 0; i < children.length; i++) {
children[i].style.transform = `translateY(-${
(target.scrollTop * (i + 1)) / 5
}px)`;
}
}
@action
registerRewindContainer(element) {
this.rewindContainer = element;
}
<template>
<div
class={{concatClass
"rewind-container"
(if this.fullScreen "-fullscreen")
}}
{{didInsert this.loadRewind}}
{{on "keydown" this.handleEscape}}
{{didInsert this.registerRewindContainer}}
tabindex="0"
>
<div class="rewind">
<RewindHeader />
<div class="background-1 parallax-bg"></div>
{{#if this.loadingRewind}}
<div class="rewind-loader">
<div class="spinner small"></div>
<div class="rewind-loader__text">Crunching your data...</div>
</div>
{{else}}
<DButton
class="rewind__exit-fullscreen-btn"
@icon={{if this.fullScreen "discourse-compress" "discourse-expand"}}
@action={{this.toggleFullScreen}}
/>
<div
class="rewind__scroll-wrapper"
{{didInsert this.registerScrollWrapper}}
{{on "scroll" this.handleScroll}}
>
{{#each this.rewind as |report|}}
<div class={{concatClass "rewind-report" report.identifier}}>
{{#if (eq report.identifier "fbff")}}
<FBFF @report={{report}} />
{{else if (eq report.identifier "reactions")}}
<Reactions @report={{report}} />
{{else if (eq report.identifier "top-words")}}
<TopWords @report={{report}} />
{{else if (eq report.identifier "best-posts")}}
<BestPosts @report={{report}} />
{{else if (eq report.identifier "best-topics")}}
<BestTopics @report={{report}} />
{{else if (eq report.identifier "activity-calendar")}}
<ActivityCalendar @report={{report}} />
{{else if (eq report.identifier "favorite-tags")}}
<FavoriteTags @report={{report}} />
{{else if (eq report.identifier "reading-time")}}
<ReadingTime @report={{report}} />
{{else if (eq report.identifier "favorite-categories")}}
<FavoriteCategories @report={{report}} />
{{/if}}
</div>
{{/each}}
</div>
{{#if this.showPrev}}
<DButton
class="rewind__prev-btn"
@icon="chevron-left"
@action={{this.prev}}
/>
{{/if}}
{{#if this.showNext}}
<DButton
class="rewind__next-btn"
@icon="chevron-right"
@action={{this.next}}
/>
{{/if}}
{{/if}}
</div>
</div>
</template>
}
@@ -0,0 +1,37 @@
import Component from "@glimmer/component";
import { TrackedObject } from "@ember-compat/tracked-built-ins";
import bodyClass from "discourse/helpers/body-class";
import KeyValueStore from "discourse/lib/key-value-store";
import isRewindActive from "discourse/plugins/discourse-rewind/discourse/lib/is-rewind-active";
export default class AvatarDecorator extends Component {
store = new TrackedObject(
new KeyValueStore("discourse_rewind_" + this.fetchYear)
);
get fetchYear() {
const currentDate = new Date();
const currentMonth = currentDate.getMonth();
const currentYear = currentDate.getFullYear();
if (currentMonth === 0) {
return currentYear - 1;
} else {
return currentYear;
}
}
get dismissed() {
return this.store.getObject("_dismissed") ?? false;
}
get showDecorator() {
return isRewindActive() && !this.dismissed;
}
<template>
{{#if this.showDecorator}}
{{bodyClass "rewind-notification-active"}}
{{/if}}
</template>
}

Some files were not shown because too many files have changed in this diff Show More