UX: New visualizations for new reports (#34)
This adds some new visualizations and styles and reports and cleans up some existing styles and class names. Daily activity rhythm <img width="1588" height="412" alt="image" src="https://github.com/user-attachments/assets/0c7980e4-7bb6-4e08-9957-24c3c5718572" /> Helping new members <img width="1456" height="310" alt="image" src="https://github.com/user-attachments/assets/096a13da-ab63-45d4-9b68-53a01df62a78" /> Chat activity <img width="1442" height="926" alt="image" src="https://github.com/user-attachments/assets/07ce2cab-1963-4918-a9ff-805c251a1377" /> Assigns <img width="1388" height="844" alt="image" src="https://github.com/user-attachments/assets/ee49c6ee-2da6-4bfa-b500-715ed7ceb13a" /> AI usage <img width="1442" height="1254" alt="image" src="https://github.com/user-attachments/assets/b382bffb-010d-4072-9144-cb7e6a8680e3" /> Invitations <img width="1486" height="1346" alt="image" src="https://github.com/user-attachments/assets/020f68a9-69d2-46ed-9f38-b0d5e98d02de" /> Writing analysis <img width="917" height="409" alt="image" src="https://github.com/user-attachments/assets/1147427c-513f-4737-8f28-99bfc6f4aff9" /> --------- Co-authored-by: awesomerobot <[email protected]> Co-authored-by: Martin Brennan <[email protected]>
This commit is contained in:
co-authored by
awesomerobot
Martin Brennan
parent
26a77d4c69
commit
d9f9e0c461
@@ -33,66 +33,45 @@ module DiscourseRewind
|
||||
return if new_user_ids.empty?
|
||||
|
||||
# Count likes given to new users
|
||||
likes_scope =
|
||||
UserAction.where(
|
||||
acting_user_id: user.id,
|
||||
user_id: new_user_ids,
|
||||
action_type: UserAction::WAS_LIKED,
|
||||
).where(created_at: date)
|
||||
likes_given = likes_scope.count
|
||||
liked_user_ids = likes_scope.distinct.pluck(:user_id)
|
||||
liked_user_ids =
|
||||
UserAction
|
||||
.where(
|
||||
acting_user_id: user.id,
|
||||
user_id: new_user_ids,
|
||||
action_type: UserAction::WAS_LIKED,
|
||||
)
|
||||
.where(created_at: date)
|
||||
.distinct
|
||||
.pluck(:user_id)
|
||||
|
||||
# Count replies to new users' posts
|
||||
replies_scope =
|
||||
replied_user_ids =
|
||||
Post
|
||||
.joins(
|
||||
"INNER JOIN posts AS parent_posts ON posts.reply_to_post_number = parent_posts.post_number AND posts.topic_id = parent_posts.topic_id",
|
||||
)
|
||||
.where(posts: { user_id: user.id, deleted_at: nil, created_at: date })
|
||||
.where("parent_posts.user_id": new_user_ids)
|
||||
replies_to_new_users = replies_scope.count
|
||||
replied_user_ids = replies_scope.distinct.pluck("parent_posts.user_id")
|
||||
|
||||
# Count topics created by user that new users participated in
|
||||
topics_with_new_users =
|
||||
Topic
|
||||
.joins(:posts)
|
||||
.where(topics: { user_id: user.id, deleted_at: nil })
|
||||
.where(posts: { user_id: new_user_ids, deleted_at: nil })
|
||||
.where(topics: { created_at: date })
|
||||
.distinct
|
||||
.count
|
||||
.pluck("parent_posts.user_id")
|
||||
|
||||
# Count direct messages/mentions to new users
|
||||
mentions_scope =
|
||||
# Count direct mentions to new users
|
||||
mentioned_user_ids =
|
||||
Post
|
||||
.joins(
|
||||
"INNER JOIN user_actions ON user_actions.target_post_id = posts.id AND user_actions.action_type = #{UserAction::MENTION}",
|
||||
)
|
||||
.where(posts: { user_id: user.id, deleted_at: nil, created_at: date })
|
||||
.where(user_actions: { user_id: new_user_ids })
|
||||
mentions_to_new_users = mentions_scope.distinct.count
|
||||
mentioned_user_ids = mentions_scope.distinct.pluck("user_actions.user_id")
|
||||
.distinct
|
||||
.pluck("user_actions.user_id")
|
||||
|
||||
# Unique new users interacted with
|
||||
unique_new_users = (liked_user_ids + replied_user_ids + mentioned_user_ids).uniq.count
|
||||
|
||||
total_interactions = likes_given + replies_to_new_users + mentions_to_new_users
|
||||
return if unique_new_users == 0
|
||||
|
||||
return if total_interactions == 0
|
||||
|
||||
{
|
||||
data: {
|
||||
total_interactions: total_interactions,
|
||||
likes_given: likes_given,
|
||||
replies_to_new_users: replies_to_new_users,
|
||||
mentions_to_new_users: mentions_to_new_users,
|
||||
topics_with_new_users: topics_with_new_users,
|
||||
unique_new_users: unique_new_users,
|
||||
new_users_count: new_user_ids.count,
|
||||
},
|
||||
identifier: "new-user-interactions",
|
||||
}
|
||||
{ data: { unique_new_users: unique_new_users }, identifier: "new-user-interactions" }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module DiscourseRewind
|
||||
module Action
|
||||
class WritingAnalysis < BaseReport
|
||||
FakeData = {
|
||||
data: {
|
||||
total_words: 45_230,
|
||||
total_posts: 197,
|
||||
average_post_length: 230,
|
||||
readability_score: 65.4,
|
||||
},
|
||||
identifier: "writing-analysis",
|
||||
}
|
||||
|
||||
def call
|
||||
return FakeData if Rails.env.development?
|
||||
|
||||
total_words =
|
||||
DB.query_single(<<~SQL, user_id: user.id, date_start: date.first, date_end: date.last)
|
||||
SELECT SUM(word_count) FROM posts
|
||||
WHERE user_id = :user_id
|
||||
AND created_at BETWEEN :date_start AND :date_end
|
||||
AND deleted_at IS NULL
|
||||
SQL
|
||||
|
||||
post_count =
|
||||
DB.query_single(<<~SQL, user_id: user.id, date_start: date.first, date_end: date.last)
|
||||
SELECT COUNT(*) FROM posts
|
||||
WHERE user_id = :user_id
|
||||
AND created_at BETWEEN :date_start AND :date_end
|
||||
AND deleted_at IS NULL
|
||||
SQL
|
||||
|
||||
average_post_length =
|
||||
post_count.first > 0 ? (total_words.first.to_f / post_count.first).round(2) : 0
|
||||
|
||||
# Calculated using the Flesch Reading Ease formula,
|
||||
# with an approximation for syllables since this can
|
||||
# be tricky to get right in SQL.
|
||||
#
|
||||
# Tries to handle short sentences or ones without delmiters
|
||||
# and ending with emojis by treating them as a single sentence.
|
||||
readability_score =
|
||||
DB.query_single(<<~SQL, user_id: user.id, start: date.first, end: date.last)
|
||||
WITH cleaned AS (
|
||||
SELECT
|
||||
p.id AS post_id,
|
||||
p.user_id,
|
||||
p.created_at,
|
||||
p.word_count,
|
||||
regexp_replace(p.cooked, '<[^>]+>', ' ', 'g') AS plain
|
||||
FROM posts p
|
||||
WHERE p.user_id = :user_id
|
||||
AND p.created_at BETWEEN :start AND :end
|
||||
),
|
||||
metrics AS (
|
||||
SELECT
|
||||
post_id,
|
||||
user_id,
|
||||
created_at,
|
||||
plain,
|
||||
word_count AS words,
|
||||
regexp_count(plain, '[.!?;:](\s|$)') AS sentences_raw,
|
||||
regexp_count(lower(plain), '[aeiouy]+') AS syllables
|
||||
FROM cleaned
|
||||
),
|
||||
scores AS (
|
||||
SELECT
|
||||
post_id,
|
||||
user_id,
|
||||
created_at,
|
||||
words,
|
||||
syllables,
|
||||
plain,
|
||||
|
||||
CASE
|
||||
WHEN sentences_raw = 0 AND words > 5 THEN 1
|
||||
ELSE sentences_raw
|
||||
END AS sentences_fixed,
|
||||
|
||||
-- Flesch Reading Ease formula
|
||||
CASE
|
||||
WHEN words = 0 THEN NULL
|
||||
WHEN (CASE WHEN sentences_raw = 0 AND words > 5 THEN 1 ELSE sentences_raw END) = 0 THEN NULL
|
||||
ELSE (
|
||||
206.835
|
||||
- 1.015 * (
|
||||
words::float /
|
||||
(CASE WHEN sentences_raw = 0 AND words > 5 THEN 1 ELSE sentences_raw END)
|
||||
)
|
||||
- 84.6 * (syllables::float / words)
|
||||
)
|
||||
END AS readability_score
|
||||
|
||||
FROM metrics
|
||||
)
|
||||
SELECT AVG(readability_score) AS avg_readability_score
|
||||
FROM scores
|
||||
GROUP BY user_id;
|
||||
SQL
|
||||
|
||||
{
|
||||
data: {
|
||||
total_words: total_words.first,
|
||||
total_posts: post_count.first,
|
||||
average_post_length: average_post_length,
|
||||
readability_score: readability_score.first,
|
||||
},
|
||||
identifier: "writing-analysis",
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -25,6 +25,7 @@ module DiscourseRewind
|
||||
REPORTS = [
|
||||
Action::TopWords,
|
||||
Action::ReadingTime,
|
||||
Action::WritingAnalysis,
|
||||
Action::Reactions,
|
||||
Action::Fbff,
|
||||
Action::MostViewedTags,
|
||||
|
||||
@@ -57,18 +57,18 @@ export default class ActivityCalendar extends Component {
|
||||
@action
|
||||
computeClass(count) {
|
||||
if (!count) {
|
||||
return "-empty";
|
||||
return "--empty";
|
||||
} else if (count < 10) {
|
||||
return "-low";
|
||||
return "--low";
|
||||
} else if (count < 20) {
|
||||
return "-medium";
|
||||
return "--medium";
|
||||
} else {
|
||||
return "-high";
|
||||
return "--high";
|
||||
}
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="rewind-report-page -activity-calendar">
|
||||
<div class="rewind-report-page --activity-calendar">
|
||||
<h2 class="rewind-report-title">{{i18n
|
||||
"discourse_rewind.reports.activity_calendar.title"
|
||||
}}</h2>
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { get } from "@ember/helper";
|
||||
import { action } from "@ember/object";
|
||||
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
||||
import willDestroy from "@ember/render-modifiers/modifiers/will-destroy";
|
||||
import { service } from "@ember/service";
|
||||
import number from "discourse/helpers/number";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
export default class AiUsage extends Component {
|
||||
@service currentUser;
|
||||
|
||||
matrixInterval = null;
|
||||
|
||||
get totalRequests() {
|
||||
return this.args.report.data.total_requests ?? 0;
|
||||
}
|
||||
|
||||
get totalTokens() {
|
||||
return this.args.report.data.total_tokens ?? 0;
|
||||
}
|
||||
|
||||
get successRate() {
|
||||
return this.args.report.data.success_rate ?? 0;
|
||||
}
|
||||
|
||||
get featureUsage() {
|
||||
return Object.entries(this.args.report.data.feature_usage ?? {})
|
||||
.filter(([name]) => name && name.trim().length > 0)
|
||||
.slice(0, 3);
|
||||
}
|
||||
|
||||
get modelUsage() {
|
||||
return Object.entries(this.args.report.data.model_usage ?? {})
|
||||
.filter(([name]) => name && name.trim().length > 0)
|
||||
.slice(0, 3);
|
||||
}
|
||||
|
||||
@action
|
||||
formatFeatureName(featureName) {
|
||||
return featureName.replace(/_/g, " ");
|
||||
}
|
||||
|
||||
@action
|
||||
setupMatrix(element) {
|
||||
const canvas = element;
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
||||
canvas.width = element.offsetWidth;
|
||||
canvas.height = element.offsetHeight;
|
||||
|
||||
const characters =
|
||||
"01アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン";
|
||||
const fontSize = 14;
|
||||
const columns = Math.floor(canvas.width / fontSize);
|
||||
const drops = Array(columns).fill(1);
|
||||
|
||||
const draw = () => {
|
||||
ctx.fillStyle = "rgba(0, 0, 0, 0.05)";
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
ctx.fillStyle = "#0f0";
|
||||
ctx.font = `${fontSize}px monospace`;
|
||||
|
||||
for (let i = 0; i < drops.length; i++) {
|
||||
const char = characters[Math.floor(Math.random() * characters.length)];
|
||||
const x = i * fontSize;
|
||||
const y = drops[i] * fontSize;
|
||||
|
||||
ctx.fillText(char, x, y);
|
||||
|
||||
if (y > canvas.height && Math.random() > 0.975) {
|
||||
drops[i] = 0;
|
||||
}
|
||||
|
||||
drops[i]++;
|
||||
}
|
||||
};
|
||||
|
||||
this.matrixInterval = setInterval(draw, 33);
|
||||
}
|
||||
|
||||
@action
|
||||
cleanupMatrix() {
|
||||
if (this.matrixInterval) {
|
||||
clearInterval(this.matrixInterval);
|
||||
}
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="rewind-report-page --ai-usage">
|
||||
<div class="matrix-container">
|
||||
<canvas
|
||||
class="matrix-rain"
|
||||
{{didInsert this.setupMatrix}}
|
||||
{{willDestroy this.cleanupMatrix}}
|
||||
></canvas>
|
||||
|
||||
<div class="matrix-content">
|
||||
<h2 class="matrix-title">
|
||||
<div class="matrix-subhead">
|
||||
{{i18n
|
||||
"discourse_rewind.reports.ai_usage.wake_up"
|
||||
username=this.currentUser.username
|
||||
}}
|
||||
</div>
|
||||
{{i18n "discourse_rewind.reports.ai_usage.system_title"}}
|
||||
</h2>
|
||||
|
||||
<div class="matrix-stats">
|
||||
<div class="matrix-stat">
|
||||
<div class="matrix-stat__label">
|
||||
{{i18n "discourse_rewind.reports.ai_usage.total_requests"}}
|
||||
</div>
|
||||
<div class="matrix-stat__value">
|
||||
{{number this.totalRequests}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="matrix-stat">
|
||||
<div class="matrix-stat__label">
|
||||
{{i18n "discourse_rewind.reports.ai_usage.total_tokens"}}
|
||||
</div>
|
||||
<div class="matrix-stat__value">{{number this.totalTokens}}</div>
|
||||
</div>
|
||||
|
||||
<div class="matrix-stat">
|
||||
<div class="matrix-stat__label">
|
||||
{{i18n "discourse_rewind.reports.ai_usage.success_rate"}}
|
||||
</div>
|
||||
<div class="matrix-stat__value">
|
||||
<span class="number">
|
||||
{{this.successRate}}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#if this.featureUsage.length}}
|
||||
<div class="matrix-section">
|
||||
<div class="matrix-section__title">>
|
||||
{{i18n "discourse_rewind.reports.ai_usage.section_features"}}
|
||||
</div>
|
||||
<div class="matrix-list">
|
||||
{{#each this.featureUsage as |entry|}}
|
||||
<div class="matrix-list__item">
|
||||
<span class="matrix-list__name">
|
||||
{{this.formatFeatureName (get entry "0")}}
|
||||
</span>
|
||||
<span class="matrix-list__count">{{get entry "1"}}</span>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.modelUsage.length}}
|
||||
<div class="matrix-section">
|
||||
<div class="matrix-section__title">>
|
||||
{{i18n "discourse_rewind.reports.ai_usage.section_models"}}
|
||||
</div>
|
||||
<div class="matrix-list">
|
||||
{{#each this.modelUsage as |entry|}}
|
||||
<div class="matrix-list__item">
|
||||
<span class="matrix-list__name">{{get entry "0"}}</span>
|
||||
<span class="matrix-list__count">{{get entry "1"}}</span>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import number from "discourse/helpers/number";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
const Assignments = <template>
|
||||
<div class="rewind-report-page --assignments">
|
||||
<div class="sticky-board">
|
||||
<div class="sticky-note --yellow --rotate-left">
|
||||
<div class="sticky-note__content">
|
||||
<div class="sticky-note__title">
|
||||
{{i18n "discourse_rewind.reports.assignments.completed"}}
|
||||
</div>
|
||||
<div class="sticky-note__value">
|
||||
{{number @report.data.completed}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sticky-note --pink --rotate-right">
|
||||
<div class="sticky-note__content">
|
||||
<div class="sticky-note__title">
|
||||
{{i18n "discourse_rewind.reports.assignments.pending"}}
|
||||
</div>
|
||||
<div class="sticky-note__value">{{number @report.data.pending}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sticky-note --blue --rotate-left-small">
|
||||
<div class="sticky-note__content">
|
||||
<div class="sticky-note__title">
|
||||
{{i18n "discourse_rewind.reports.assignments.total_assigned"}}
|
||||
</div>
|
||||
<div class="sticky-note__value">
|
||||
{{number @report.data.total_assigned}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sticky-note --green --rotate-right-small">
|
||||
<div class="sticky-note__content">
|
||||
<div class="sticky-note__title">
|
||||
{{i18n "discourse_rewind.reports.assignments.assigned_by_user"}}
|
||||
</div>
|
||||
<div class="sticky-note__value">
|
||||
{{number @report.data.assigned_by_user}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sticky-note --orange">
|
||||
<div class="sticky-note__content">
|
||||
<div class="sticky-note__title">
|
||||
{{i18n "discourse_rewind.reports.assignments.completion_rate"}}
|
||||
</div>
|
||||
<div
|
||||
class="sticky-note__value"
|
||||
>{{@report.data.completion_rate}}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>;
|
||||
|
||||
export default Assignments;
|
||||
@@ -11,7 +11,7 @@ export default class BestPosts extends Component {
|
||||
|
||||
<template>
|
||||
{{#if @report.data.length}}
|
||||
<div class="rewind-report-page -best-posts">
|
||||
<div class="rewind-report-page --best-posts">
|
||||
<h2 class="rewind-report-title">
|
||||
{{i18n
|
||||
"discourse_rewind.reports.best_posts.title"
|
||||
@@ -21,19 +21,21 @@ export default class BestPosts extends Component {
|
||||
<div class="rewind-report-container">
|
||||
{{#each @report.data as |post idx|}}
|
||||
<div class={{concatClass "rewind-card" (this.rankClass idx)}}>
|
||||
<span class="best-posts -rank"></span>
|
||||
<span class="best-posts -rank"></span>
|
||||
<div class="best-posts__post"><p>{{htmlSafe
|
||||
post.excerpt
|
||||
}}</p></div>
|
||||
<span class="best-posts --rank"></span>
|
||||
<span class="best-posts --rank"></span>
|
||||
<div class="best-posts__post">
|
||||
<p>{{htmlSafe post.excerpt}}</p>
|
||||
</div>
|
||||
<div class="best-posts__metadata">
|
||||
<span class="best-posts__likes">
|
||||
{{icon "heart"}}{{post.like_count}}</span>
|
||||
{{icon "heart"}}{{post.like_count}}
|
||||
</span>
|
||||
<span class="best-posts__replies">
|
||||
{{icon "comment"}}{{post.reply_count}}</span>
|
||||
<a href="/t/{{post.topic_id}}/{{post.post_number}}">{{i18n
|
||||
"discourse_rewind.reports.best_posts.view_post"
|
||||
}}</a>
|
||||
{{icon "comment"}}{{post.reply_count}}
|
||||
</span>
|
||||
<a href="/t/{{post.topic_id}}/{{post.post_number}}">
|
||||
{{i18n "discourse_rewind.reports.best_posts.view_post"}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
|
||||
@@ -13,7 +13,7 @@ export default class BestTopics extends Component {
|
||||
|
||||
<template>
|
||||
{{#if @report.data.length}}
|
||||
<div class="rewind-report-page -best-topics">
|
||||
<div class="rewind-report-page --best-topics">
|
||||
<h2 class="rewind-report-title">
|
||||
{{i18n
|
||||
"discourse_rewind.reports.best_topics.title"
|
||||
@@ -27,12 +27,12 @@ export default class BestTopics extends Component {
|
||||
href={{getURL (concat "/t/-/" topic.topic_id)}}
|
||||
class={{concatClass "best-topics__topic" (this.rankClass idx)}}
|
||||
>
|
||||
<span class="best-topics -rank"></span>
|
||||
<span class="best-topics -rank"></span>
|
||||
<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">{{replaceEmoji
|
||||
(htmlSafe topic.excerpt)
|
||||
}}</span>
|
||||
<span class="best-topics__excerpt">
|
||||
{{replaceEmoji (htmlSafe topic.excerpt)}}
|
||||
</span>
|
||||
</a>
|
||||
{{/each}}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { concat } from "@ember/helper";
|
||||
import { service } from "@ember/service";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
import avatar from "discourse/helpers/avatar";
|
||||
import number from "discourse/helpers/number";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
const BotMessage = <template>
|
||||
<div class="chat-message__avatar">🤖</div>
|
||||
<div class="chat-message__bubble">
|
||||
<div class="chat-message__author">
|
||||
{{i18n "discourse_rewind.reports.chat_usage.bot_name"}}
|
||||
</div>
|
||||
{{#if @message}}
|
||||
<div class="chat-message__text">
|
||||
{{htmlSafe @message}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{yield}}
|
||||
</div>
|
||||
</template>;
|
||||
|
||||
const UserMessage = <template>
|
||||
<div class="chat-message__bubble">
|
||||
<div class="chat-message__author">
|
||||
{{i18n "discourse_rewind.reports.chat_usage.you"}}
|
||||
</div>
|
||||
{{#if @replyKey}}
|
||||
<div class="chat-message__text">
|
||||
{{i18n (concat "discourse_rewind.reports.chat_usage." @replyKey)}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{yield}}
|
||||
</div>
|
||||
<div class="chat-message__avatar">
|
||||
{{avatar @user imageSize="small"}}
|
||||
</div>
|
||||
</template>;
|
||||
|
||||
export default class ChatUsage extends Component {
|
||||
@service currentUser;
|
||||
|
||||
get favoriteChannels() {
|
||||
return this.args.report.data.favorite_channels ?? [];
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="rewind-report-page --chat-usage">
|
||||
<h2 class="rewind-report-title">{{i18n
|
||||
"discourse_rewind.reports.chat_usage.title"
|
||||
}}</h2>
|
||||
|
||||
<div class="chat-window">
|
||||
<div class="chat-window__header">
|
||||
<span class="chat-window__title">
|
||||
{{i18n "discourse_rewind.reports.chat_usage.channel_title"}}
|
||||
</span>
|
||||
<span class="chat-window__status">
|
||||
{{i18n "discourse_rewind.reports.chat_usage.status_online"}}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="chat-window__messages">
|
||||
<div class="chat-message --left">
|
||||
<BotMessage
|
||||
@message={{i18n
|
||||
"discourse_rewind.reports.chat_usage.message_1"
|
||||
count=(number @report.data.total_messages)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="chat-message --right">
|
||||
<UserMessage @user={{this.currentUser}} @replyKey="reply_1" />
|
||||
</div>
|
||||
|
||||
<div class="chat-message --left">
|
||||
<BotMessage
|
||||
@message={{htmlSafe
|
||||
(i18n
|
||||
"discourse_rewind.reports.chat_usage.message_2"
|
||||
dm_count=(number @report.data.dm_message_count)
|
||||
channel_count=(number @report.data.unique_dm_channels)
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="chat-message --right">
|
||||
<UserMessage @user={{this.currentUser}} @replyKey="reply_2" />
|
||||
</div>
|
||||
|
||||
<div class="chat-message --left">
|
||||
<BotMessage
|
||||
@message={{htmlSafe
|
||||
(i18n
|
||||
"discourse_rewind.reports.chat_usage.message_3"
|
||||
count=(number @report.data.total_reactions_received)
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="chat-message --right">
|
||||
<UserMessage @user={{this.currentUser}} @replyKey="reply_3" />
|
||||
</div>
|
||||
|
||||
<div class="chat-message --left">
|
||||
<BotMessage
|
||||
@message={{htmlSafe
|
||||
(i18n
|
||||
"discourse_rewind.reports.chat_usage.message_4"
|
||||
[email protected]_message_length
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{{#if this.favoriteChannels.length}}
|
||||
<div class="chat-message --left">
|
||||
<BotMessage
|
||||
@message={{i18n
|
||||
"discourse_rewind.reports.chat_usage.message_5"
|
||||
}}
|
||||
>
|
||||
<div class="chat-message__channels">
|
||||
{{#each this.favoriteChannels as |channel|}}
|
||||
<a
|
||||
class="chat-channel-link"
|
||||
href={{concat "/chat/c/-/" channel.channel_id}}
|
||||
>
|
||||
<span
|
||||
class="chat-channel-link__name"
|
||||
>#{{channel.channel_name}}</span>
|
||||
<span class="chat-channel-link__count">
|
||||
{{number channel.message_count}}
|
||||
</span>
|
||||
</a>
|
||||
{{/each}}
|
||||
</div>
|
||||
</BotMessage>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="chat-message --right">
|
||||
<UserMessage @user={{this.currentUser}}>
|
||||
<img
|
||||
src="/plugins/discourse-rewind/images/dancing_baby.gif"
|
||||
alt={{i18n
|
||||
"discourse_rewind.reports.chat_usage.dancing_baby_alt"
|
||||
}}
|
||||
class="chat-message__gif"
|
||||
/>
|
||||
</UserMessage>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import Component from "@glimmer/component";
|
||||
import icon from "discourse/helpers/d-icon";
|
||||
import number from "discourse/helpers/number";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
export default class FavoriteGifs extends Component {
|
||||
get favoriteGifs() {
|
||||
return this.args.report.data.favorite_gifs ?? [];
|
||||
}
|
||||
|
||||
<template>
|
||||
{{#if this.favoriteGifs.length}}
|
||||
<div class="rewind-report-page --favorite-gifs">
|
||||
<h2 class="rewind-report-title">{{i18n
|
||||
"discourse_rewind.reports.favorite_gifs.title"
|
||||
count=this.favoriteGifs.length
|
||||
}}</h2>
|
||||
<div class="rewind-report-subtitle">{{i18n
|
||||
"discourse_rewind.reports.favorite_gifs.total_usage"
|
||||
[email protected]_gif_usage
|
||||
}}</div>
|
||||
<div class="rewind-report-container">
|
||||
{{#each this.favoriteGifs as |gif idx|}}
|
||||
<div class="rewind-card scale">
|
||||
<div class="favorite-gifs__gif">
|
||||
<img
|
||||
src={{gif.url}}
|
||||
alt="GIF #{{idx}}"
|
||||
class="favorite-gifs__image"
|
||||
loading="lazy"
|
||||
/>
|
||||
<div class="favorite-gifs__stats">
|
||||
<span class="favorite-gifs__stat">
|
||||
{{icon "repeat"}}
|
||||
{{number gif.usage_count}}
|
||||
</span>
|
||||
<span class="favorite-gifs__stat">
|
||||
{{icon "heart"}}
|
||||
{{number gif.likes}}
|
||||
</span>
|
||||
<span class="favorite-gifs__stat">
|
||||
{{icon "smile"}}
|
||||
{{number gif.reactions}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</template>
|
||||
}
|
||||
@@ -3,10 +3,10 @@ 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-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">
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { concat } from "@ember/helper";
|
||||
import avatar from "discourse/helpers/bound-avatar-template";
|
||||
import number from "discourse/helpers/number";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
export default class Invites extends Component {
|
||||
get mostActiveInvitee() {
|
||||
return this.args.report.data.most_active_invitee;
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="rewind-report-page --invites">
|
||||
<div class="guest-book">
|
||||
<div class="guest-book__cover">
|
||||
<div class="guest-book__title">
|
||||
{{i18n "discourse_rewind.reports.invites.guest_book_title"}}
|
||||
</div>
|
||||
<div class="guest-book__subtitle">
|
||||
{{i18n "discourse_rewind.reports.invites.guest_book_subtitle"}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="guest-book__page">
|
||||
<div class="guest-book__entry">
|
||||
<div class="guest-book__entry-label">
|
||||
{{i18n "discourse_rewind.reports.invites.label_invitations_sent"}}
|
||||
</div>
|
||||
<div class="guest-book__entry-value">
|
||||
{{number @report.data.total_invites}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="guest-book__entry">
|
||||
<div class="guest-book__entry-label">
|
||||
{{i18n "discourse_rewind.reports.invites.label_guests_joined"}}
|
||||
</div>
|
||||
<div class="guest-book__entry-value">
|
||||
{{number @report.data.redeemed_count}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="guest-book__entry">
|
||||
<div class="guest-book__entry-label">
|
||||
{{i18n "discourse_rewind.reports.invites.label_acceptance_rate"}}
|
||||
</div>
|
||||
<div class="guest-book__entry-value">
|
||||
{{@report.data.redemption_rate}}%
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="guest-book__divider"></div>
|
||||
|
||||
<div class="guest-book__section-title">
|
||||
{{i18n "discourse_rewind.reports.invites.section_contributions"}}
|
||||
</div>
|
||||
|
||||
<div class="guest-book__entry --small">
|
||||
<div class="guest-book__entry-label">
|
||||
{{i18n "discourse_rewind.reports.invites.label_posts_written"}}
|
||||
</div>
|
||||
<div class="guest-book__entry-value">
|
||||
{{number @report.data.invitee_post_count}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="guest-book__entry --small">
|
||||
<div class="guest-book__entry-label">
|
||||
{{i18n "discourse_rewind.reports.invites.label_topics_started"}}
|
||||
</div>
|
||||
<div class="guest-book__entry-value">
|
||||
{{number @report.data.invitee_topic_count}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="guest-book__entry --small">
|
||||
<div class="guest-book__entry-label">
|
||||
{{i18n "discourse_rewind.reports.invites.label_likes_given"}}
|
||||
</div>
|
||||
<div class="guest-book__entry-value">
|
||||
{{number @report.data.invitee_like_count}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#if this.mostActiveInvitee}}
|
||||
<div class="guest-book__divider"></div>
|
||||
|
||||
<div class="guest-book__section-title">
|
||||
{{i18n "discourse_rewind.reports.invites.section_most_active"}}
|
||||
</div>
|
||||
|
||||
<a
|
||||
href={{concat "/u/" this.mostActiveInvitee.username}}
|
||||
class="guest-book__signature"
|
||||
>
|
||||
{{avatar
|
||||
this.mostActiveInvitee.avatar_template
|
||||
"large"
|
||||
username=this.mostActiveInvitee.username
|
||||
name=this.mostActiveInvitee.name
|
||||
}}
|
||||
<div class="guest-book__signature-info">
|
||||
<span class="guest-book__signature-name">
|
||||
{{this.mostActiveInvitee.username}}
|
||||
</span>
|
||||
{{#if this.mostActiveInvitee.name}}
|
||||
<span class="guest-book__signature-realname">
|
||||
{{this.mostActiveInvitee.name}}
|
||||
</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
</a>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
}
|
||||
@@ -3,11 +3,13 @@ import { i18n } from "discourse-i18n";
|
||||
|
||||
const MostViewedCategories = <template>
|
||||
{{#if @report.data.length}}
|
||||
<div class="rewind-report-page -most-viewed-categories">
|
||||
<h2 class="rewind-report-title">{{i18n
|
||||
<div class="rewind-report-page --most-viewed-categories">
|
||||
<h2 class="rewind-report-title">
|
||||
{{i18n
|
||||
"discourse_rewind.reports.most_viewed_categories.title"
|
||||
[email protected]
|
||||
}}</h2>
|
||||
}}
|
||||
</h2>
|
||||
<div class="rewind-report-container">
|
||||
{{#each @report.data as |data|}}
|
||||
<a class="folder-wrapper" href={{concat "/c/-/" data.category_id}}>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { i18n } from "discourse-i18n";
|
||||
|
||||
const MostViewedTags = <template>
|
||||
{{#if @report.data.length}}
|
||||
<div class="rewind-report-page -most-viewed-tags">
|
||||
<div class="rewind-report-page --most-viewed-tags">
|
||||
<h2 class="rewind-report-title">{{i18n
|
||||
"discourse_rewind.reports.most_viewed_tags.title"
|
||||
[email protected]
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
export default class NewUserInteractions extends Component {
|
||||
get wavyWords() {
|
||||
const num = this.args.report.data.unique_new_users;
|
||||
const memberText = i18n(
|
||||
"discourse_rewind.reports.new_user_interactions.new_member",
|
||||
{ count: num }
|
||||
);
|
||||
return memberText.split(" ").map((word) => word.split(""));
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="rewind-report-page --new-user-interactions">
|
||||
<div class="wordart-container">
|
||||
<div class="wordart-text">your contributions helped</div>
|
||||
<div class="wordart-3d">
|
||||
{{#each this.wavyWords as |word|}}
|
||||
<span class="wordart-word">
|
||||
{{#each word as |char|}}
|
||||
<span class="wordart-letter">{{char}}</span>
|
||||
{{/each}}
|
||||
</span>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
}
|
||||
@@ -32,33 +32,33 @@ export default class Reactions extends Component {
|
||||
}
|
||||
|
||||
<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-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__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-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="emoji">
|
||||
{{replaceEmoji (concat ":" emojiName ":")}}
|
||||
</span>
|
||||
<span class="percentage">{{this.computePercentage count}}</span>
|
||||
<div
|
||||
class="rewind-reactions-bar"
|
||||
|
||||
@@ -19,10 +19,10 @@ export default class ReadingTime extends Component {
|
||||
|
||||
<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-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
|
||||
|
||||
@@ -0,0 +1,457 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { action } from "@ember/object";
|
||||
import { service } from "@ember/service";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
export default class TimeOfDayActivity extends Component {
|
||||
@service currentUser;
|
||||
|
||||
@tracked isPlaying = false;
|
||||
@tracked playbackProgress = 0;
|
||||
@tracked isGlitching = false;
|
||||
|
||||
audioContext = null;
|
||||
audioSource = null;
|
||||
playbackTimeout = null;
|
||||
stereoPanner = null;
|
||||
animationFrame = null;
|
||||
hasGlitched = false;
|
||||
|
||||
SVG_WIDTH = 1200;
|
||||
SVG_HEIGHT = 200;
|
||||
SVG_PADDING = 40;
|
||||
BIT_CRUSH_STEPS = 5;
|
||||
|
||||
get activityByHour() {
|
||||
return this.args.report?.data?.activity_by_hour ?? {};
|
||||
}
|
||||
|
||||
get mostActiveHour() {
|
||||
return this.args.report?.data?.most_active_hour ?? 0;
|
||||
}
|
||||
|
||||
get maxActivity() {
|
||||
const counts = Object.values(this.activityByHour);
|
||||
return Math.max(...counts, 1);
|
||||
}
|
||||
|
||||
get plotDimensions() {
|
||||
return {
|
||||
width: this.SVG_WIDTH,
|
||||
height: this.SVG_HEIGHT,
|
||||
padding: this.SVG_PADDING,
|
||||
plotWidth: this.SVG_WIDTH - this.SVG_PADDING * 2,
|
||||
plotHeight: this.SVG_HEIGHT - this.SVG_PADDING * 2,
|
||||
};
|
||||
}
|
||||
|
||||
calculatePoint(hour) {
|
||||
const { height, padding, plotWidth, plotHeight } = this.plotDimensions;
|
||||
const count = this.activityByHour[hour] || 0;
|
||||
const x = padding + (hour / 23) * plotWidth;
|
||||
const y = height - padding - (count / this.maxActivity) * plotHeight;
|
||||
return { x, y };
|
||||
}
|
||||
|
||||
get personalizedAudioParams() {
|
||||
const username = this.currentUser?.username || "default";
|
||||
|
||||
// convert username to seed
|
||||
const hash = (str) => {
|
||||
let h = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
h = (h * 31 + str.charCodeAt(i)) | 0; // eslint-disable-line no-bitwise
|
||||
}
|
||||
return Math.abs(h);
|
||||
};
|
||||
|
||||
const seed = hash(username);
|
||||
|
||||
// Three distinct scales for variety
|
||||
const scales = [
|
||||
// C minor pentatonic
|
||||
[
|
||||
130.81, 155.56, 174.61, 196.0, 233.08, 261.63, 311.13, 349.23, 392.0,
|
||||
466.16, 523.25,
|
||||
],
|
||||
// C major pentatonic
|
||||
[
|
||||
130.81, 146.83, 164.81, 196.0, 220.0, 261.63, 293.66, 329.63, 392.0,
|
||||
440.0, 523.25,
|
||||
],
|
||||
// Blues scale
|
||||
[
|
||||
130.81, 155.56, 164.81, 174.61, 196.0, 233.08, 261.63, 311.13, 329.63,
|
||||
349.23, 392.0,
|
||||
],
|
||||
];
|
||||
|
||||
const harmonyRatios = [1.5, 1.25, 2.0]; // Perfect fifth, major third, octave
|
||||
|
||||
return {
|
||||
scale: scales[seed % scales.length],
|
||||
harmonyRatio: harmonyRatios[(seed >> 4) % harmonyRatios.length], // eslint-disable-line no-bitwise
|
||||
};
|
||||
}
|
||||
|
||||
get waveformPath() {
|
||||
const points = Array.from({ length: 24 }, (_, hour) =>
|
||||
this.calculatePoint(hour)
|
||||
);
|
||||
|
||||
const tension = 0.3;
|
||||
let path = `M ${points[0].x} ${points[0].y}`;
|
||||
|
||||
for (let i = 0; i < points.length - 1; i++) {
|
||||
const p0 = points[Math.max(i - 1, 0)];
|
||||
const p1 = points[i];
|
||||
const p2 = points[i + 1];
|
||||
const p3 = points[Math.min(i + 2, points.length - 1)];
|
||||
|
||||
const cp1x = p1.x + ((p2.x - p0.x) / 6) * tension;
|
||||
const cp1y = p1.y + ((p2.y - p0.y) / 6) * tension;
|
||||
const cp2x = p2.x - ((p3.x - p1.x) / 6) * tension;
|
||||
const cp2y = p2.y - ((p3.y - p1.y) / 6) * tension;
|
||||
|
||||
path += ` C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${p2.x} ${p2.y}`;
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
get waveformPoints() {
|
||||
return Array.from({ length: 24 }, (_, hour) => {
|
||||
const { x, y } = this.calculatePoint(hour);
|
||||
const isActive = hour === this.mostActiveHour;
|
||||
return {
|
||||
x,
|
||||
y,
|
||||
hour,
|
||||
radius: isActive ? 6 : 2.5,
|
||||
class: isActive ? "oscilloscope__dot active" : "oscilloscope__dot",
|
||||
showLabel: hour % 3 === 0,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
get gridLines() {
|
||||
return Array.from({ length: 5 }, (_, i) => ({
|
||||
y: this.SVG_PADDING + (i * (this.SVG_HEIGHT - 2 * this.SVG_PADDING)) / 4,
|
||||
style: htmlSafe(`opacity: ${i === 0 || i === 4 ? 0.3 : 0.15}`),
|
||||
}));
|
||||
}
|
||||
|
||||
get playbackPosition() {
|
||||
if (!this.isPlaying) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { height, padding, plotWidth, plotHeight } = this.plotDimensions;
|
||||
|
||||
const currentHour = this.playbackProgress * 23;
|
||||
const hourIndex = Math.floor(currentHour);
|
||||
const nextHourIndex = Math.min(hourIndex + 1, 23);
|
||||
const t = currentHour - hourIndex;
|
||||
|
||||
const currentActivity = this.activityByHour[hourIndex] || 0;
|
||||
const nextActivity = this.activityByHour[nextHourIndex] || 0;
|
||||
const activity = currentActivity + (nextActivity - currentActivity) * t;
|
||||
|
||||
const x = padding + (currentHour / 23) * plotWidth;
|
||||
const y = height - padding - (activity / this.maxActivity) * plotHeight;
|
||||
|
||||
return { x, y };
|
||||
}
|
||||
|
||||
formatHour(hour) {
|
||||
const hourNum = parseInt(hour, 10);
|
||||
const period = hourNum >= 12 ? "PM" : "AM";
|
||||
const displayHour =
|
||||
hourNum === 0 ? 12 : hourNum > 12 ? hourNum - 12 : hourNum;
|
||||
return `${displayHour}${period}`;
|
||||
}
|
||||
|
||||
@action
|
||||
stopWaveform() {
|
||||
if (this.audioSource) {
|
||||
this.audioSource.stop();
|
||||
}
|
||||
if (this.audioContext) {
|
||||
this.audioContext.close();
|
||||
}
|
||||
if (this.animationFrame) {
|
||||
cancelAnimationFrame(this.animationFrame);
|
||||
}
|
||||
if (this.playbackTimeout) {
|
||||
clearTimeout(this.playbackTimeout);
|
||||
}
|
||||
this.isPlaying = false;
|
||||
this.playbackProgress = 0;
|
||||
this.isGlitching = false;
|
||||
this.hasGlitched = false;
|
||||
this.audioContext = null;
|
||||
this.audioSource = null;
|
||||
this.playbackTimeout = null;
|
||||
this.stereoPanner = null;
|
||||
}
|
||||
|
||||
@action
|
||||
async playWaveform() {
|
||||
if (this.isPlaying) {
|
||||
this.stopWaveform();
|
||||
return;
|
||||
}
|
||||
|
||||
this.isPlaying = true;
|
||||
this.playbackProgress = 0;
|
||||
|
||||
try {
|
||||
this.audioContext = new (window.AudioContext ||
|
||||
window.webkitAudioContext)();
|
||||
|
||||
// Musical timing: 24 hours at 200 BPM
|
||||
const duration = 7.2; // (24 / 200) * 60 seconds
|
||||
|
||||
const sampleRate = this.audioContext.sampleRate;
|
||||
const numSamples = duration * sampleRate;
|
||||
|
||||
const buffer = this.audioContext.createBuffer(1, numSamples, sampleRate);
|
||||
const channelData = buffer.getChannelData(0);
|
||||
|
||||
const params = this.personalizedAudioParams;
|
||||
|
||||
const hours = Array.from({ length: 24 }, (_, i) => i);
|
||||
const samplesPerHour = numSamples / 24;
|
||||
|
||||
for (let i = 0; i < numSamples; i++) {
|
||||
const hourIndex = Math.floor(i / samplesPerHour);
|
||||
const nextHourIndex = Math.min(hourIndex + 1, 23);
|
||||
const t = (i % samplesPerHour) / samplesPerHour;
|
||||
|
||||
// Get activity for current and next hour
|
||||
const currentActivity = this.activityByHour[hours[hourIndex]] || 0;
|
||||
const nextActivity = this.activityByHour[hours[nextHourIndex]] || 0;
|
||||
|
||||
// Interpolate between hours
|
||||
const activity = currentActivity + (nextActivity - currentActivity) * t;
|
||||
|
||||
// Map activity to personalized musical scale
|
||||
const scale = params.scale;
|
||||
const normalizedActivity = activity / this.maxActivity;
|
||||
|
||||
// Map to scale index with smooth interpolation
|
||||
const scalePosition = normalizedActivity * (scale.length - 1);
|
||||
const lowerIndex = Math.floor(scalePosition);
|
||||
const upperIndex = Math.min(lowerIndex + 1, scale.length - 1);
|
||||
const blend = scalePosition - lowerIndex;
|
||||
|
||||
let frequency =
|
||||
scale[lowerIndex] + (scale[upperIndex] - scale[lowerIndex]) * blend;
|
||||
|
||||
const time = i / sampleRate;
|
||||
|
||||
// Generate triangle waveform for retro feel
|
||||
const generateWave = (freq) => {
|
||||
const sine = Math.sin(2 * Math.PI * freq * time);
|
||||
return (2 / Math.PI) * Math.asin(sine);
|
||||
};
|
||||
|
||||
// Generate main voice and harmony
|
||||
const mainWave = generateWave(frequency);
|
||||
const harmonyWave = generateWave(frequency * params.harmonyRatio);
|
||||
|
||||
// Mix voices (70/30 split)
|
||||
const mixedWave = mainWave * 0.7 + harmonyWave * 0.3;
|
||||
|
||||
// Add bit crushing effect for retro feel
|
||||
const crushed =
|
||||
Math.round(mixedWave * this.BIT_CRUSH_STEPS) / this.BIT_CRUSH_STEPS;
|
||||
|
||||
// Add minimal noise (sparkle at peak hour)
|
||||
const peakHourTime = (this.mostActiveHour / 23) * duration;
|
||||
const peakWindow = 0.2;
|
||||
const isPeakMoment =
|
||||
Math.abs(time - peakHourTime) < peakWindow && time >= peakHourTime;
|
||||
|
||||
const noise = (Math.random() - 0.5) * (isPeakMoment ? 0.08 : 0.02);
|
||||
|
||||
// Reduce volume for zero/very low activity sections
|
||||
const lowActivityVolume = normalizedActivity < 0.05 ? 0.3 : 1;
|
||||
|
||||
// Output with all effects applied
|
||||
channelData[i] = (crushed * 0.15 + noise) * lowActivityVolume;
|
||||
}
|
||||
|
||||
// Create source and play
|
||||
this.audioSource = this.audioContext.createBufferSource();
|
||||
this.audioSource.buffer = buffer;
|
||||
|
||||
// Add stereo panner (pan from left to right as time progresses)
|
||||
this.stereoPanner = this.audioContext.createStereoPanner();
|
||||
this.stereoPanner.pan.value = -1; // Start at left
|
||||
|
||||
// Add filter for retro feel
|
||||
const filter = this.audioContext.createBiquadFilter();
|
||||
filter.type = "lowpass";
|
||||
filter.frequency.value = 2500; // Tame the highs
|
||||
|
||||
// Simple delay for subtle depth
|
||||
const delay = this.audioContext.createDelay();
|
||||
delay.delayTime.value = 0.15; // 150ms delay
|
||||
|
||||
const delayGain = this.audioContext.createGain();
|
||||
delayGain.gain.value = 0.2; // Subtle echo
|
||||
|
||||
// Connect audio graph: source -> filter -> panner + delay feedback
|
||||
this.audioSource.connect(filter);
|
||||
filter.connect(this.stereoPanner);
|
||||
|
||||
// Add subtle delay feedback
|
||||
filter.connect(delay);
|
||||
delay.connect(delayGain);
|
||||
delayGain.connect(this.stereoPanner);
|
||||
|
||||
this.stereoPanner.connect(this.audioContext.destination);
|
||||
|
||||
this.audioSource.start();
|
||||
|
||||
// Animate playback progress and stereo panning
|
||||
const startTime = Date.now();
|
||||
const animate = () => {
|
||||
const elapsed = (Date.now() - startTime) / 1000;
|
||||
this.playbackProgress = Math.min(elapsed / duration, 1);
|
||||
|
||||
// Pan from left (-1) to right (1) as we progress
|
||||
if (this.stereoPanner) {
|
||||
this.stereoPanner.pan.value = -1 + this.playbackProgress * 2;
|
||||
}
|
||||
|
||||
// Trigger visual glitch when hitting peak hour
|
||||
const peakHourProgress = this.mostActiveHour / 23;
|
||||
if (
|
||||
!this.hasGlitched &&
|
||||
this.playbackProgress >= peakHourProgress &&
|
||||
this.playbackProgress < peakHourProgress + 0.05
|
||||
) {
|
||||
this.isGlitching = true;
|
||||
this.hasGlitched = true;
|
||||
setTimeout(() => {
|
||||
this.isGlitching = false;
|
||||
}, 200);
|
||||
}
|
||||
|
||||
if (this.playbackProgress < 1) {
|
||||
this.animationFrame = requestAnimationFrame(animate);
|
||||
}
|
||||
};
|
||||
this.animationFrame = requestAnimationFrame(animate);
|
||||
|
||||
// Reset playing state when done
|
||||
this.playbackTimeout = setTimeout(() => {
|
||||
this.stopWaveform();
|
||||
}, duration * 1000);
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Error playing waveform:", error);
|
||||
this.stopWaveform();
|
||||
}
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="rewind-report-page --time-of-day-activity">
|
||||
<h2 class="rewind-report-title">{{i18n
|
||||
"discourse_rewind.reports.time_of_day_activity.title"
|
||||
}}
|
||||
</h2>
|
||||
|
||||
<div class="rewind-card">
|
||||
<div
|
||||
class="time-of-day__oscilloscope
|
||||
{{if this.isGlitching '--glitching'}}"
|
||||
>
|
||||
<DButton
|
||||
@action={{this.playWaveform}}
|
||||
@icon={{if this.isPlaying "volume-xmark" "volume-high"}}
|
||||
class="oscilloscope__play-btn {{if this.isPlaying '--playing'}}"
|
||||
@title={{if
|
||||
this.isPlaying
|
||||
"discourse_rewind.reports.time_of_day_activity.stop_button"
|
||||
"discourse_rewind.reports.time_of_day_activity.play_button"
|
||||
}}
|
||||
/>
|
||||
<svg viewBox="0 0 1200 200" class="oscilloscope__svg">
|
||||
<defs>
|
||||
<filter id="glow">
|
||||
<feGaussianBlur stdDeviation="2" result="coloredBlur" />
|
||||
<feMerge>
|
||||
<feMergeNode in="coloredBlur" />
|
||||
<feMergeNode in="SourceGraphic" />
|
||||
</feMerge>
|
||||
</filter>
|
||||
<filter id="glow-strong">
|
||||
<feGaussianBlur stdDeviation="4" result="coloredBlur" />
|
||||
<feMerge>
|
||||
<feMergeNode in="coloredBlur" />
|
||||
<feMergeNode in="coloredBlur" />
|
||||
<feMergeNode in="SourceGraphic" />
|
||||
</feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
{{#each this.gridLines as |gridLine|}}
|
||||
<line
|
||||
x1="40"
|
||||
y1={{gridLine.y}}
|
||||
x2="1160"
|
||||
y2={{gridLine.y}}
|
||||
class="oscilloscope__grid-line"
|
||||
style={{gridLine.style}}
|
||||
/>
|
||||
{{/each}}
|
||||
|
||||
{{#each this.waveformPoints as |point|}}
|
||||
{{#if point.showLabel}}
|
||||
<line
|
||||
x1={{point.x}}
|
||||
y1="40"
|
||||
x2={{point.x}}
|
||||
y2="160"
|
||||
class="oscilloscope__grid-line --vertical"
|
||||
/>
|
||||
<text
|
||||
x={{point.x}}
|
||||
y="180"
|
||||
class="oscilloscope__time-label"
|
||||
>{{this.formatHour point.hour}}</text>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
|
||||
<path d={{this.waveformPath}} class="oscilloscope__waveform" />
|
||||
|
||||
{{#each this.waveformPoints as |point|}}
|
||||
<circle
|
||||
cx={{point.x}}
|
||||
cy={{point.y}}
|
||||
r={{point.radius}}
|
||||
class={{point.class}}
|
||||
/>
|
||||
{{/each}}
|
||||
|
||||
{{#if this.playbackPosition}}
|
||||
<circle
|
||||
cx={{this.playbackPosition.x}}
|
||||
cy={{this.playbackPosition.y}}
|
||||
r="12"
|
||||
class="oscilloscope__playback-dot"
|
||||
/>
|
||||
{{/if}}
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
}
|
||||
@@ -1,23 +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";
|
||||
|
||||
const WordCards = <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 @report.data as |entry index|}}
|
||||
<WordCard
|
||||
@word={{entry.word}}
|
||||
@count={{entry.score}}
|
||||
@index={{index}}
|
||||
/>
|
||||
{{/each}}
|
||||
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>
|
||||
</div>
|
||||
</template>;
|
||||
|
||||
export default WordCards;
|
||||
</template>
|
||||
}
|
||||
|
||||
@@ -79,25 +79,25 @@ export default class WordCard extends Component {
|
||||
{{on "mouseleave" this.handleLeave}}
|
||||
class={{concatClass
|
||||
"rewind-card__wrapper"
|
||||
(if this.longWord "-long-word")
|
||||
(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 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">
|
||||
<div class="rewind-card --back">
|
||||
<span class="rewind-card__title">{{@word}}</span>
|
||||
<span class="rewind-card__data">{{@count}}x</span>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { action } from "@ember/object";
|
||||
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
||||
import willDestroy from "@ember/render-modifiers/modifiers/will-destroy";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
export default class WritingAnalysis extends Component {
|
||||
@tracked currentColorIndex = 0;
|
||||
|
||||
terminalColors = ["#0f0", "#ffbf00", "#00ffff", "#ff00ff"];
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.handleKeyDown = this.handleKeyDown.bind(this);
|
||||
}
|
||||
|
||||
@action
|
||||
setupKeyListener(element) {
|
||||
document.addEventListener("keydown", this.handleKeyDown);
|
||||
this.element = element;
|
||||
}
|
||||
|
||||
@action
|
||||
teardownKeyListener() {
|
||||
document.removeEventListener("keydown", this.handleKeyDown);
|
||||
}
|
||||
|
||||
handleKeyDown(event) {
|
||||
if (event.key === "F1") {
|
||||
event.preventDefault();
|
||||
this.cycleColor();
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
cycleColor() {
|
||||
this.currentColorIndex =
|
||||
(this.currentColorIndex + 1) % this.terminalColors.length;
|
||||
const newColor = this.terminalColors[this.currentColorIndex];
|
||||
document.documentElement.style.setProperty("--rewind-green", newColor);
|
||||
}
|
||||
|
||||
get scoreLabel() {
|
||||
const score = this.args.report.data.readability_score;
|
||||
const randomNum = Math.floor(Math.random() * 4) + 1;
|
||||
|
||||
switch (true) {
|
||||
case score >= 80 && score <= 100:
|
||||
return i18n(
|
||||
`discourse_rewind.reports.writing_analysis.readability_score.over_80.${randomNum}`
|
||||
);
|
||||
case score >= 60 && score < 80:
|
||||
return i18n(
|
||||
`discourse_rewind.reports.writing_analysis.readability_score.over_60.${randomNum}`
|
||||
);
|
||||
case score >= 40 && score < 60:
|
||||
return i18n(
|
||||
`discourse_rewind.reports.writing_analysis.readability_score.over_40.${randomNum}`
|
||||
);
|
||||
case score >= 20 && score < 40:
|
||||
return i18n(
|
||||
`discourse_rewind.reports.writing_analysis.readability_score.over_20.${randomNum}`
|
||||
);
|
||||
default:
|
||||
return i18n(
|
||||
`discourse_rewind.reports.writing_analysis.readability_score.over_0.${randomNum}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="rewind-report-page --writing-analysis"
|
||||
{{didInsert this.setupKeyListener}}
|
||||
{{willDestroy this.teardownKeyListener}}
|
||||
>
|
||||
<h2 class="rewind-report-title">
|
||||
{{i18n "discourse_rewind.reports.writing_analysis.title"}}
|
||||
</h2>
|
||||
|
||||
<div class="writing-analysis">
|
||||
|
||||
<div class="writing-analysis__menubar">
|
||||
<span class="writing-analysis__menu-item">{{i18n
|
||||
"discourse_rewind.reports.writing_analysis.menu_file"
|
||||
}}</span>
|
||||
<span class="writing-analysis__menu-item">{{i18n
|
||||
"discourse_rewind.reports.writing_analysis.menu_other"
|
||||
}}</span>
|
||||
<span class="writing-analysis__menu-item">{{i18n
|
||||
"discourse_rewind.reports.writing_analysis.menu_additional"
|
||||
}}</span>
|
||||
<span
|
||||
class="writing-analysis__menu-item writing-analysis__menu-item--right"
|
||||
>{{i18n
|
||||
"discourse_rewind.reports.writing_analysis.menu_opening"
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
<div class="writing-analysis__frame">
|
||||
|
||||
<div class="writing-analysis__header-row">
|
||||
|
||||
<div class="writing-analysis__helpbox">
|
||||
{{i18n "discourse_rewind.reports.writing_analysis.help_text"}}
|
||||
</div>
|
||||
|
||||
<div class="writing-analysis__release">
|
||||
<div class="writing-analysis__release-name">{{i18n
|
||||
"discourse_rewind.reports.writing_analysis.app_name"
|
||||
}}</div>
|
||||
<div class="writing-analysis__release-meta">
|
||||
{{i18n
|
||||
"discourse_rewind.reports.writing_analysis.release_info"
|
||||
}}
|
||||
<span><3</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="writing-analysis__stats">
|
||||
|
||||
<div class="writing-analysis__stats-col">
|
||||
<div class="writing-analysis__stats-label">{{i18n
|
||||
"discourse_rewind.reports.writing_analysis.total_words"
|
||||
}}</div>
|
||||
<div
|
||||
class="writing-analysis__stats-value"
|
||||
>{{@report.data.total_words}}</div>
|
||||
|
||||
<div class="writing-analysis__stats-label">{{i18n
|
||||
"discourse_rewind.reports.writing_analysis.total_posts"
|
||||
}}</div>
|
||||
<div
|
||||
class="writing-analysis__stats-value"
|
||||
>{{@report.data.total_posts}}</div>
|
||||
</div>
|
||||
|
||||
<div class="writing-analysis__stats-col">
|
||||
<div class="writing-analysis__stats-label">{{i18n
|
||||
"discourse_rewind.reports.writing_analysis.avg_post_length"
|
||||
}}</div>
|
||||
<div
|
||||
class="writing-analysis__stats-value"
|
||||
>{{@report.data.average_post_length}}</div>
|
||||
|
||||
<div class="writing-analysis__stats-label">{{i18n
|
||||
"discourse_rewind.reports.writing_analysis.readability_score_label"
|
||||
}}</div>
|
||||
<div
|
||||
class="writing-analysis__stats-value"
|
||||
>{{@report.data.readability_score}}</div>
|
||||
</div>
|
||||
|
||||
<div class="writing-analysis__stats-col">
|
||||
<div class="writing-analysis__stats-label">{{i18n
|
||||
"discourse_rewind.reports.writing_analysis.readability_level"
|
||||
}}</div>
|
||||
<div
|
||||
class="writing-analysis__stats-value"
|
||||
>{{this.scoreLabel}}</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
}
|
||||
@@ -7,18 +7,25 @@ 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 { eq } from "discourse/truth-helpers";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import ActivityCalendar from "discourse/plugins/discourse-rewind/discourse/components/reports/activity-calendar";
|
||||
import AiUsage from "discourse/plugins/discourse-rewind/discourse/components/reports/ai-usage";
|
||||
import Assignments from "discourse/plugins/discourse-rewind/discourse/components/reports/assignments";
|
||||
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 ChatUsage from "discourse/plugins/discourse-rewind/discourse/components/reports/chat-usage";
|
||||
// import FavoriteGifs from "discourse/plugins/discourse-rewind/discourse/components/reports/favorite-gifs";
|
||||
import FBFF from "discourse/plugins/discourse-rewind/discourse/components/reports/fbff";
|
||||
import RewindHeader from "discourse/plugins/discourse-rewind/discourse/components/reports/header";
|
||||
import Invites from "discourse/plugins/discourse-rewind/discourse/components/reports/invites";
|
||||
import MostViewedCategories from "discourse/plugins/discourse-rewind/discourse/components/reports/most-viewed-categories";
|
||||
import MostViewedTags from "discourse/plugins/discourse-rewind/discourse/components/reports/most-viewed-tags";
|
||||
import NewUserInteractions from "discourse/plugins/discourse-rewind/discourse/components/reports/new-user-interactions";
|
||||
import Reactions from "discourse/plugins/discourse-rewind/discourse/components/reports/reactions";
|
||||
import ReadingTime from "discourse/plugins/discourse-rewind/discourse/components/reports/reading-time";
|
||||
import TimeOfDayActivity from "discourse/plugins/discourse-rewind/discourse/components/reports/time-of-day-activity";
|
||||
import TopWords from "discourse/plugins/discourse-rewind/discourse/components/reports/top-words";
|
||||
import WritingAnalysis from "discourse/plugins/discourse-rewind/discourse/components/reports/writing-analysis";
|
||||
|
||||
export default class Rewind extends Component {
|
||||
@tracked rewind = [];
|
||||
@@ -66,11 +73,52 @@ export default class Rewind extends Component {
|
||||
this.rewindContainer = element;
|
||||
}
|
||||
|
||||
getReportComponent(identifier) {
|
||||
switch (identifier) {
|
||||
case "fbff":
|
||||
return FBFF;
|
||||
case "reactions":
|
||||
return Reactions;
|
||||
case "top-words":
|
||||
return TopWords;
|
||||
case "best-posts":
|
||||
return BestPosts;
|
||||
case "best-topics":
|
||||
return BestTopics;
|
||||
case "activity-calendar":
|
||||
return ActivityCalendar;
|
||||
case "most-viewed-tags":
|
||||
return MostViewedTags;
|
||||
case "reading-time":
|
||||
return ReadingTime;
|
||||
case "most-viewed-categories":
|
||||
return MostViewedCategories;
|
||||
case "ai-usage":
|
||||
return AiUsage;
|
||||
case "assignments":
|
||||
return Assignments;
|
||||
case "chat-usage":
|
||||
return ChatUsage;
|
||||
// case "favorite-gifs":
|
||||
// return FavoriteGifs;
|
||||
case "invites":
|
||||
return Invites;
|
||||
case "new-user-interactions":
|
||||
return NewUserInteractions;
|
||||
case "time-of-day-activity":
|
||||
return TimeOfDayActivity;
|
||||
case "writing-analysis":
|
||||
return WritingAnalysis;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
<template>
|
||||
<div
|
||||
class={{concatClass
|
||||
"rewind-container"
|
||||
(if this.fullScreen "-fullscreen")
|
||||
(if this.fullScreen "--fullscreen")
|
||||
}}
|
||||
{{didInsert this.loadRewind}}
|
||||
{{on "keydown" this.handleEscape}}
|
||||
@@ -99,27 +147,16 @@ export default class Rewind extends Component {
|
||||
>
|
||||
|
||||
{{#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 "most-viewed-tags")}}
|
||||
<MostViewedTags @report={{report}} />
|
||||
{{else if (eq report.identifier "reading-time")}}
|
||||
<ReadingTime @report={{report}} />
|
||||
{{else if (eq report.identifier "most-viewed-categories")}}
|
||||
<MostViewedCategories @report={{report}} />
|
||||
{{#let
|
||||
(this.getReportComponent report.identifier)
|
||||
as |ReportComponent|
|
||||
}}
|
||||
{{#if ReportComponent}}
|
||||
<div class={{concatClass "rewind-report" report.identifier}}>
|
||||
<ReportComponent @report={{report}} />
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/let}}
|
||||
{{/each}}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -13,6 +13,13 @@
|
||||
@import "fonts";
|
||||
@import "reading-time";
|
||||
@import "fbff";
|
||||
@import "writing-analysis";
|
||||
@import "rewind-header";
|
||||
@import "rewind-callout";
|
||||
@import "folder-styles";
|
||||
@import "time-of-day-activity";
|
||||
@import "chat-usage";
|
||||
@import "new-user-interactions";
|
||||
@import "ai-usage";
|
||||
@import "assignments";
|
||||
@import "invites";
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
.-activity-calendar {
|
||||
margin-bottom: 5em;
|
||||
.--activity-calendar {
|
||||
margin-bottom: 2em;
|
||||
|
||||
.rewind-report-title {
|
||||
border: none;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.rewind-card {
|
||||
@include rewind-border;
|
||||
|
||||
@media screen and (width <= 475px) {
|
||||
padding: 0.5em;
|
||||
}
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.rewind-calendar {
|
||||
border-collapse: unset;
|
||||
border-spacing: 3px;
|
||||
width: 100%;
|
||||
|
||||
@media screen and (width <= 475px) {
|
||||
border-spacing: 1px;
|
||||
@@ -38,20 +42,32 @@
|
||||
width: 5px;
|
||||
}
|
||||
|
||||
&.-empty {
|
||||
&.--empty {
|
||||
background: var(--primary-low);
|
||||
}
|
||||
|
||||
&.-low {
|
||||
background: var(--success);
|
||||
&.--low {
|
||||
background: color-mix(in srgb, var(--rewind-green) 40%, transparent);
|
||||
}
|
||||
|
||||
&.-medium {
|
||||
background: var(--success-medium);
|
||||
&.--medium {
|
||||
background: color-mix(in srgb, var(--rewind-green) 70%, transparent);
|
||||
}
|
||||
|
||||
&.-high {
|
||||
background: var(--success-low);
|
||||
&.--high {
|
||||
background: var(--rewind-green);
|
||||
animation: pulse-high 2s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse-high {
|
||||
0%,
|
||||
100% {
|
||||
box-shadow: 0 0 0 rgb(0, 255, 0, 0);
|
||||
}
|
||||
|
||||
50% {
|
||||
box-shadow: 0 0 8px 2px var(--rewind-green);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
.--ai-usage {
|
||||
.rewind-report-title {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.matrix-container {
|
||||
position: relative;
|
||||
min-height: 600px;
|
||||
background: var(--rewind-black);
|
||||
overflow: hidden;
|
||||
border-radius: 0;
|
||||
outline: inset 1px solid var(--rewind-green);
|
||||
outline-offset: 5px;
|
||||
border: 2px solid var(--rewind-green);
|
||||
box-shadow: 0 0 20px 10px rgb(0 255 0 / 0.2);
|
||||
}
|
||||
|
||||
.matrix-rain {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.matrix-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: 3em 1em 1em;
|
||||
color: var(--rewind-green);
|
||||
font-family: "Courier New", monospace;
|
||||
}
|
||||
|
||||
.matrix-title {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
margin-bottom: 2em;
|
||||
letter-spacing: 4px;
|
||||
text-shadow: 0 0 5px var(--rewind-green);
|
||||
|
||||
.matrix-subhead {
|
||||
font-size: var(--font-down-5);
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.matrix-stats {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
margin-bottom: 3em;
|
||||
flex-wrap: wrap;
|
||||
gap: 2em;
|
||||
}
|
||||
|
||||
.matrix-stat {
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.matrix-stat__label {
|
||||
font-size: 12px;
|
||||
letter-spacing: 2px;
|
||||
margin-bottom: 0.5em;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.matrix-stat__value {
|
||||
font-size: 36px;
|
||||
font-weight: bold;
|
||||
text-shadow: 0 0 5px var(--rewind-green);
|
||||
}
|
||||
|
||||
.matrix-section {
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
|
||||
.matrix-section__title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 1em;
|
||||
letter-spacing: 2px;
|
||||
text-shadow: 0 0 5px var(--rewind-green);
|
||||
}
|
||||
|
||||
.matrix-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75em;
|
||||
}
|
||||
|
||||
.matrix-list__item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0.75em 1em;
|
||||
background: rgb(0, 255, 0, 0.05);
|
||||
border: 1px solid rgb(0, 255, 0, 0.3);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.matrix-list__name {
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.matrix-list__count {
|
||||
font-weight: bold;
|
||||
text-shadow: 0 0 5px var(--rewind-green);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
.--assignments {
|
||||
.sticky-board {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
margin: 1em 0;
|
||||
z-index: 9; // above the scanlines
|
||||
}
|
||||
|
||||
.sticky-note {
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
box-shadow: 3px 3px 8px rgb(0 0 0 / 0.2);
|
||||
position: relative;
|
||||
font-family: "Brush Script MT", cursive, sans-serif;
|
||||
transition: transform 0.2s ease;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 30px;
|
||||
background: linear-gradient(to bottom, rgb(0 0 0 / 0.05), transparent);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&.--yellow {
|
||||
background: linear-gradient(to bottom, #fef9c3 0%, #fde68a 100%);
|
||||
}
|
||||
|
||||
&.--pink {
|
||||
background: linear-gradient(to bottom, #fecaca 0%, #fca5a5 100%);
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
border-width: 0 0 20px 20px;
|
||||
border-color: transparent transparent #000 transparent;
|
||||
box-shadow: -2px 2px 4px rgb(0 0 0 / 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
&.--blue {
|
||||
background: linear-gradient(to bottom, #bfdbfe 0%, #93c5fd 100%);
|
||||
}
|
||||
|
||||
&.--green {
|
||||
background: linear-gradient(to bottom, #bbf7d0 0%, #86efac 100%);
|
||||
}
|
||||
|
||||
&.--orange {
|
||||
background: linear-gradient(to bottom, #fed7aa 0%, #fdba74 100%);
|
||||
}
|
||||
|
||||
&.--rotate-left {
|
||||
transform: rotate(-4deg);
|
||||
}
|
||||
|
||||
&.--rotate-right {
|
||||
transform: rotate(3deg);
|
||||
}
|
||||
|
||||
&.--rotate-left-small {
|
||||
transform: rotate(-2deg);
|
||||
}
|
||||
|
||||
&.--rotate-right-small {
|
||||
transform: rotate(6deg);
|
||||
}
|
||||
}
|
||||
|
||||
.sticky-note__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sticky-note__title {
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 12px;
|
||||
line-height: 1;
|
||||
padding: 0 5em;
|
||||
}
|
||||
|
||||
.sticky-note__value {
|
||||
font-size: 36px;
|
||||
font-weight: bold;
|
||||
color: #1a1a1a;
|
||||
|
||||
.number {
|
||||
font-family: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
.-best-posts {
|
||||
.--best-posts {
|
||||
margin-bottom: 1em;
|
||||
|
||||
.rewind-report-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, calc(32% - (1em / 3)));
|
||||
@@ -16,10 +18,15 @@
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
border: none;
|
||||
margin-bottom: 1em;
|
||||
|
||||
@media screen and (width >= 550px) {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.rewind-card {
|
||||
border-radius: 0;
|
||||
min-width: 0;
|
||||
box-sizing: border-box;
|
||||
padding: 0.5em;
|
||||
position: relative;
|
||||
@@ -77,15 +84,6 @@
|
||||
|
||||
@media screen and (width <= 475px) {
|
||||
max-height: 300px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
|
||||
p {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
.-best-topics {
|
||||
.--best-topics {
|
||||
padding-bottom: 0;
|
||||
margin-top: 2em;
|
||||
|
||||
.rewind-report-container {
|
||||
flex-direction: column;
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
.chat-usage {
|
||||
margin-inline: 2em;
|
||||
}
|
||||
|
||||
.--chat-usage {
|
||||
box-shadow:
|
||||
inset -1px -1px #0a0a0a,
|
||||
inset 1px 1px #dfdfdf,
|
||||
inset -2px -2px var(--rewind-grey),
|
||||
inset 2px 2px var(--rewind-white);
|
||||
padding: 2px;
|
||||
max-width: 660px;
|
||||
transform: rotate(0.25deg);
|
||||
margin-bottom: 2em;
|
||||
|
||||
.rewind-report-title {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.chat-window {
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
overflow-y: auto;
|
||||
max-height: 400px;
|
||||
background: var(--rewind-light-grey);
|
||||
}
|
||||
|
||||
.chat-window__header {
|
||||
background: var(--rewind-blue);
|
||||
color: var(--rewind-white);
|
||||
padding: 12px 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-weight: bold;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.chat-window__title {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.chat-window__status {
|
||||
font-size: 12px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.chat-window__messages {
|
||||
padding: 1em 0.5em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.chat-message {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
min-width: 100%;
|
||||
|
||||
&.--left {
|
||||
flex-direction: row;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
&.--right {
|
||||
align-self: flex-end;
|
||||
justify-content: end;
|
||||
|
||||
.chat-message__author {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-message__avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.chat-message__bubble {
|
||||
max-width: 70%;
|
||||
padding: 10px 14px;
|
||||
box-shadow:
|
||||
inset -1px -1px #0a0a0a,
|
||||
inset 1px 1px #dfdfdf,
|
||||
inset -2px -2px var(--rewind-grey),
|
||||
inset 2px 2px var(--rewind-white);
|
||||
|
||||
.chat-message.--left & {
|
||||
background: var(--rewind-grey);
|
||||
border-bottom-left-radius: 4px;
|
||||
}
|
||||
|
||||
.chat-message.--right & {
|
||||
background: var(--rewind-magenta);
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-message__author {
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 4px;
|
||||
opacity: 0.7;
|
||||
color: var(--rewind-grey);
|
||||
}
|
||||
|
||||
.chat-message__text {
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
color: var(--rewind-black);
|
||||
|
||||
strong {
|
||||
color: var(--rewind-blue);
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-message__gif {
|
||||
max-width: 200px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.chat-message__channels {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.chat-channel-link {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease;
|
||||
border: 1px solid var(--rewind-blue);
|
||||
gap: 0.5em;
|
||||
font-size: var(--font-down-2);
|
||||
|
||||
&:hover {
|
||||
background: var(--rewind-white);
|
||||
border-color: var(--rewind-blue);
|
||||
}
|
||||
}
|
||||
|
||||
.chat-channel-link__name {
|
||||
font-weight: bold;
|
||||
color: var(--rewind-blue);
|
||||
}
|
||||
|
||||
.chat-channel-link__count {
|
||||
font-size: 12px;
|
||||
color: var(--rewind-blue);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
.-fbff {
|
||||
.--fbff {
|
||||
--border-size: 6px;
|
||||
margin-block: 1em;
|
||||
|
||||
@media screen and (width <= 768px) {
|
||||
--border-size: 4px;
|
||||
@@ -68,6 +69,7 @@
|
||||
justify-self: center;
|
||||
width: 75px;
|
||||
height: 75px;
|
||||
border-radius: 0;
|
||||
|
||||
@media screen and (width <= 768px) {
|
||||
width: 50px;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.-most-viewed-tags,
|
||||
.-most-viewed-categories {
|
||||
.--most-viewed-tags,
|
||||
.--most-viewed-categories {
|
||||
.rewind-card {
|
||||
@include rewind-border;
|
||||
flex-grow: 1;
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
.--invites {
|
||||
margin-bottom: 10em;
|
||||
|
||||
.guest-book {
|
||||
max-width: 700px;
|
||||
width: 100%;
|
||||
margin: 2em auto;
|
||||
background: #c0c0c0;
|
||||
border: 4px outset #dfdfdf;
|
||||
box-shadow: 5px 5px 0 rgb(0 0 0 / 0.2);
|
||||
}
|
||||
|
||||
.guest-book__cover {
|
||||
padding: 2em;
|
||||
text-align: center;
|
||||
background: linear-gradient(135deg, #f0f 0%, #0ff 100%);
|
||||
border-bottom: 4px ridge #fff;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-image: repeating-linear-gradient(
|
||||
45deg,
|
||||
transparent,
|
||||
transparent 10px,
|
||||
rgb(255 255 255 / 0.1) 10px,
|
||||
rgb(255 255 255 / 0.1) 20px
|
||||
);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.guest-book__title {
|
||||
font-family: "Comic Sans MS", "Comic Sans", cursive;
|
||||
font-size: 48px;
|
||||
color: #ff0;
|
||||
margin-bottom: 0.25em;
|
||||
text-shadow:
|
||||
3px 3px 0 #f0f,
|
||||
-1px -1px 0 #000,
|
||||
1px -1px 0 #000,
|
||||
-1px 1px 0 #000,
|
||||
1px 1px 0 #000;
|
||||
font-weight: bold;
|
||||
animation: rainbow-text 3s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes rainbow-text {
|
||||
0% {
|
||||
color: #ff0;
|
||||
}
|
||||
|
||||
16% {
|
||||
color: #0f0;
|
||||
}
|
||||
|
||||
33% {
|
||||
color: #0ff;
|
||||
}
|
||||
|
||||
50% {
|
||||
color: #f0f;
|
||||
}
|
||||
|
||||
66% {
|
||||
color: #f00;
|
||||
}
|
||||
|
||||
83% {
|
||||
color: #ff0;
|
||||
}
|
||||
|
||||
100% {
|
||||
color: #0f0;
|
||||
}
|
||||
}
|
||||
|
||||
.guest-book__subtitle {
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
letter-spacing: 3px;
|
||||
text-shadow: 2px 2px 0 #000;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.guest-book__page {
|
||||
padding: 2em;
|
||||
background: #fff;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.guest-book__entry {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1em;
|
||||
padding: 0.75em;
|
||||
background: linear-gradient(to right, #ffd700 0%, #ff69b4 100%);
|
||||
border: 3px ridge #fff;
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
|
||||
&.--small {
|
||||
background: linear-gradient(to right, #87ceeb 0%, #98fb98 100%);
|
||||
font-size: 13px;
|
||||
padding: 0.5em 0.75em;
|
||||
}
|
||||
}
|
||||
|
||||
.guest-book__entry-label {
|
||||
color: #000;
|
||||
text-shadow: 1px 1px 0 rgb(255 255 255 / 0.5);
|
||||
}
|
||||
|
||||
.guest-book__entry-value {
|
||||
color: #000;
|
||||
font-size: 1.2em;
|
||||
background: #fff;
|
||||
padding: 0.25em 0.5em;
|
||||
border: 2px inset #c0c0c0;
|
||||
|
||||
.number {
|
||||
font-family: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.guest-book__divider {
|
||||
height: 3px;
|
||||
background: repeating-linear-gradient(
|
||||
to right,
|
||||
#f00 0,
|
||||
#f00 10px,
|
||||
#ff0 10px,
|
||||
#ff0 20px,
|
||||
#0f0 20px,
|
||||
#0f0 30px,
|
||||
#0ff 30px,
|
||||
#0ff 40px,
|
||||
#00f 40px,
|
||||
#00f 50px,
|
||||
#f0f 50px,
|
||||
#f0f 60px
|
||||
);
|
||||
margin: 1.5em 0;
|
||||
border: 1px solid #000;
|
||||
}
|
||||
|
||||
.guest-book__section-title {
|
||||
font-family: "Comic Sans MS", "Comic Sans", cursive;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #f0f;
|
||||
margin-bottom: 1em;
|
||||
text-align: center;
|
||||
text-shadow: 2px 2px 0 #0ff;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.guest-book__signature {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1em;
|
||||
padding: 1em;
|
||||
background: #ff0;
|
||||
border: 4px double #000;
|
||||
text-decoration: none;
|
||||
box-shadow:
|
||||
inset 0 0 0 2px #f0f,
|
||||
inset 0 0 0 4px #0ff;
|
||||
|
||||
&:hover {
|
||||
background: #ff9;
|
||||
animation: blink 0.5s infinite;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
flex-shrink: 0;
|
||||
border: 3px solid #f0f;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%,
|
||||
100% {
|
||||
background: #ff0;
|
||||
}
|
||||
|
||||
50% {
|
||||
background: #f0f;
|
||||
}
|
||||
}
|
||||
|
||||
.guest-book__signature-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25em;
|
||||
}
|
||||
|
||||
.guest-book__signature-name {
|
||||
font-family: "Comic Sans MS", "Comic Sans", cursive;
|
||||
font-size: 22px;
|
||||
color: #f0f;
|
||||
font-weight: bold;
|
||||
text-shadow: 1px 1px 0 #0ff;
|
||||
}
|
||||
|
||||
.guest-book__signature-realname {
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
color: #000;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
.-most-viewed-categories {
|
||||
.--most-viewed-categories {
|
||||
.rewind-report-container {
|
||||
display: flex;
|
||||
gap: 0.5em;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.-most-viewed-tags {
|
||||
.--most-viewed-tags {
|
||||
.rewind-report-title {
|
||||
box-sizing: border-box;
|
||||
border: none;
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
.--new-user-interactions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 1em;
|
||||
|
||||
.wordart-container {
|
||||
text-align: center;
|
||||
margin: 2em 0 2.5em;
|
||||
}
|
||||
|
||||
.wordart-text {
|
||||
font-size: 32px;
|
||||
color: var(--rewind-white);
|
||||
font-family: "Comic Sans MS", cursive, sans-serif;
|
||||
}
|
||||
|
||||
.wordart-3d {
|
||||
font-size: 64px;
|
||||
font-weight: 900;
|
||||
font-family: Impact, Arial, sans-serif;
|
||||
text-transform: uppercase;
|
||||
display: inline-block;
|
||||
transform: rotate(-3deg);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.wordart-word {
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
margin-right: 0.75em;
|
||||
}
|
||||
|
||||
.wordart-letter {
|
||||
font-family: impact, arial, sans-serif;
|
||||
font-weight: bold;
|
||||
display: inline-block;
|
||||
color: #0ff;
|
||||
letter-spacing: -0.4em;
|
||||
text-shadow:
|
||||
1px 1px 0 #00d,
|
||||
2px 2px 0 #00d,
|
||||
3px 3px 0 #00b,
|
||||
4px 4px 0 #00b,
|
||||
5px 5px 0 #009,
|
||||
6px 6px 0 #009,
|
||||
7px 7px 0 #007,
|
||||
8px 8px 0 #007,
|
||||
9px 9px 0 #005,
|
||||
10px 10px 0 #005,
|
||||
11px 11px 0 #003,
|
||||
12px 12px 0 #003,
|
||||
13px 13px 0 #001,
|
||||
14px 14px 0 #001,
|
||||
15px 15px 20px rgb(0 0 0 / 0.5);
|
||||
animation: wordart-wave 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
// Wave pattern using nth-child
|
||||
@for $i from 1 through 30 {
|
||||
.wordart-letter:nth-child(#{$i}) {
|
||||
animation-delay: #{$i * 0.05}s;
|
||||
transform: translateY(#{sin($i * 0.5) * 25}px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes wordart-wave {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(-35px);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
.-post-received-reactions {
|
||||
.--post-received-reactions {
|
||||
margin-block: 1em;
|
||||
|
||||
.rewind-report-container {
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
.-post-used-reactions {
|
||||
.--post-used-reactions {
|
||||
.rewind-card {
|
||||
@include rewind-border;
|
||||
border-radius: 0 var(--rewind-border-radius) var(--rewind-border-radius)
|
||||
var(--rewind-border-radius);
|
||||
}
|
||||
|
||||
.rewind-reactions-chart {
|
||||
|
||||
@@ -125,6 +125,8 @@ p.reading-time__text {
|
||||
--book-color: var(--primary-very-high);
|
||||
|
||||
@include rewind-border;
|
||||
border-radius: 0 var(--rewind-border-radius) var(--rewind-border-radius)
|
||||
var(--rewind-border-radius);
|
||||
|
||||
@media screen and (width <= 475px) {
|
||||
padding: 0.75em;
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
align-self: start;
|
||||
padding: 0.25em 1em 0.25em;
|
||||
margin-bottom: -2px;
|
||||
background: #111;
|
||||
background: var(--rewind-black);
|
||||
margin-left: 0;
|
||||
position: relative;
|
||||
font-size: var(--font-down-1);
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
.rewind-callout__container {
|
||||
height: 47px;
|
||||
border-bottom: 1px solid var(--primary-low);
|
||||
|
||||
@media screen and (width <= 639px) {
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
#rewind-vhs {
|
||||
@@ -46,6 +50,10 @@
|
||||
top: 0.75em;
|
||||
right: 1em;
|
||||
font-size: var(--font-down-3);
|
||||
|
||||
@media screen and (width <= 639px) {
|
||||
top: 0.45em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
.rewind-logo {
|
||||
height: 40px;
|
||||
|
||||
&.-light {
|
||||
&.--light {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.-dark {
|
||||
&.--dark {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
&.-light {
|
||||
&.--light {
|
||||
display: block;
|
||||
}
|
||||
|
||||
&.-dark {
|
||||
&.--dark {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@if is-dark-color-scheme() {
|
||||
&.-dark {
|
||||
&.--dark {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.-light {
|
||||
&.--light {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,11 @@
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
&.-fullscreen {
|
||||
&:not(.--fullscreen) {
|
||||
height: 65vh;
|
||||
}
|
||||
|
||||
&.--fullscreen {
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
@import "variables";
|
||||
|
||||
.time-of-day__oscilloscope {
|
||||
background: var(--rewind-black);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
margin-bottom: 1em;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: radial-gradient(
|
||||
ellipse at center,
|
||||
rgb(0, 255, 0, 0.02) 0%,
|
||||
transparent 70%
|
||||
);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&.--glitching {
|
||||
svg {
|
||||
animation: peak-glitch 0.2s step-end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.oscilloscope__play-btn {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
z-index: 10;
|
||||
background: rgb(0, 0, 0, 0.7);
|
||||
border: 1px solid var(--rewind-green);
|
||||
color: var(--rewind-green);
|
||||
padding: 0.5em;
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
.discourse-no-touch & {
|
||||
&:hover {
|
||||
color: var(--rewind-white);
|
||||
background: rgb(0, 255, 0, 0.2) !important;
|
||||
box-shadow: 0 0 10px var(--rewind-green);
|
||||
|
||||
.d-icon {
|
||||
color: var(--rewind-white);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.--playing {
|
||||
background: rgb(0, 255, 0, 0.3);
|
||||
animation: audio-pulse 0.5s ease-in-out infinite;
|
||||
|
||||
.d-icon {
|
||||
color: var(--rewind-magenta);
|
||||
}
|
||||
}
|
||||
|
||||
.d-icon {
|
||||
color: var(--rewind-green);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes audio-pulse {
|
||||
0%,
|
||||
100% {
|
||||
box-shadow: 0 0 5px var(--rewind-green);
|
||||
}
|
||||
|
||||
50% {
|
||||
box-shadow: 0 0 10px var(--rewind-green);
|
||||
}
|
||||
}
|
||||
|
||||
.oscilloscope__svg {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.oscilloscope__grid-line {
|
||||
stroke: var(--rewind-green);
|
||||
stroke-width: 0.5;
|
||||
opacity: 0.2;
|
||||
|
||||
&.--vertical {
|
||||
opacity: 0.15;
|
||||
}
|
||||
}
|
||||
|
||||
.oscilloscope__time-label {
|
||||
fill: var(--rewind-green);
|
||||
font-size: 16px;
|
||||
text-anchor: middle;
|
||||
font-family: monospace;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.oscilloscope__waveform {
|
||||
fill: none;
|
||||
stroke: var(--rewind-green);
|
||||
stroke-width: 2;
|
||||
filter: url("#glow");
|
||||
animation: pulse-waveform 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.oscilloscope__dot {
|
||||
fill: var(--rewind-green);
|
||||
filter: url("#glow");
|
||||
opacity: 0.8;
|
||||
transform-origin: center;
|
||||
transform-box: fill-box;
|
||||
|
||||
&.active {
|
||||
fill: var(--rewind-yellow);
|
||||
filter: url("#glow-strong");
|
||||
opacity: 1;
|
||||
animation: pulse-dot 2s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
|
||||
.oscilloscope__playback-dot {
|
||||
fill: var(--rewind-magenta);
|
||||
filter: url("#glow-strong");
|
||||
opacity: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@keyframes pulse-waveform {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes peak-glitch {
|
||||
0% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
|
||||
10% {
|
||||
transform: translate(-10px, 5px);
|
||||
filter: blur(2px);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
20% {
|
||||
transform: translate(12px, -8px);
|
||||
filter: blur(1px);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
30% {
|
||||
transform: translate(-8px, 3px);
|
||||
filter: blur(3px);
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
40% {
|
||||
transform: translate(15px, -5px);
|
||||
filter: blur(0.5px);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translate(-12px, 7px);
|
||||
filter: blur(2px);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
60% {
|
||||
transform: translate(10px, -4px);
|
||||
filter: blur(1.5px);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
70% {
|
||||
transform: translate(-6px, 6px);
|
||||
filter: blur(1px);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
80% {
|
||||
transform: translate(8px, -3px);
|
||||
filter: blur(0.5px);
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
90% {
|
||||
transform: translate(-4px, 2px);
|
||||
filter: blur(0.3px);
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translate(0, 0);
|
||||
filter: none;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse-dot {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.--time-of-day-activity {
|
||||
padding: 0;
|
||||
|
||||
@media screen and (width >= 500px) {
|
||||
margin-block: 1em;
|
||||
}
|
||||
|
||||
.rewind-card {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.rewind-report-title {
|
||||
border: none;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
.-top-words .rewind-report-container {
|
||||
.--top-words .rewind-report-container {
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
width: 100%;
|
||||
@@ -129,7 +129,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.rewind-card.-front {
|
||||
.rewind-card.--front {
|
||||
padding: 0.5em;
|
||||
display: grid;
|
||||
grid-template:
|
||||
@@ -150,25 +150,25 @@
|
||||
var(--rewind-white);
|
||||
}
|
||||
|
||||
.rewind-card.-back {
|
||||
.rewind-card.--back {
|
||||
transform: rotateY(180deg);
|
||||
background-color: var(--rewind-white);
|
||||
color: var(--rewind-black);
|
||||
}
|
||||
|
||||
.rewind-card.-front,
|
||||
.rewind-card.-back {
|
||||
.rewind-card.--front,
|
||||
.rewind-card.--back {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%; /* Safari */
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
&.-long-word .rewind-card__title {
|
||||
&.--long-word .rewind-card__title {
|
||||
font-size: var(--font-0);
|
||||
}
|
||||
|
||||
&.-long-word .rewind-card {
|
||||
&.--long-word .rewind-card {
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
.--writing-analysis {
|
||||
.writing-analysis {
|
||||
border: 2px solid var(--rewind-green);
|
||||
width: 100%;
|
||||
color: var(--rewind-green);
|
||||
font-size: 0.6em;
|
||||
font-family: "Pixelify Sans", sans-serif;
|
||||
}
|
||||
|
||||
.rewind-report-title {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.writing-analysis__titlebar {
|
||||
background: var(--rewind-green);
|
||||
color: #000;
|
||||
padding: 6px 10px;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.writing-analysis__menubar {
|
||||
background: var(--rewind-green);
|
||||
color: #000;
|
||||
padding: 4px 10px 6px;
|
||||
display: flex;
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
.writing-analysis__menu-item--right {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.writing-analysis__frame {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.writing-analysis__header-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.writing-analysis__helpbox {
|
||||
border: 2px solid var(--rewind-green);
|
||||
margin-left: 10px;
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
width: 180px;
|
||||
text-align: center;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.writing-analysis__release {
|
||||
text-align: center;
|
||||
margin-top: 10px;
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
.writing-analysis__release-name {
|
||||
font-weight: bold;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.writing-analysis__release-meta {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.writing-analysis__stats {
|
||||
border-top: 14px solid var(--rewind-green);
|
||||
padding: 0 20px;
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.writing-analysis__stats-col {
|
||||
padding-top: 10px;
|
||||
width: 140px;
|
||||
border-right: 10px solid var(--rewind-green);
|
||||
|
||||
&:last-of-type {
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
|
||||
.writing-analysis__stats-label {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.writing-analysis__stats-value {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
}
|
||||
@@ -48,3 +48,129 @@ en:
|
||||
title:
|
||||
one: Your best post
|
||||
other: Your %{count} best posts
|
||||
ai_usage:
|
||||
title: AI Usage
|
||||
total_requests: Total Requests
|
||||
total_tokens: Total Tokens
|
||||
success_rate: Success Rate
|
||||
favorite_features: Most Used Features
|
||||
favorite_models: Most Used AI Models
|
||||
wake_up: "Wake up, %{username}..."
|
||||
system_title: "AI USAGE DETECTED"
|
||||
section_features: "TOP FEATURES"
|
||||
section_models: "TOP MODELS"
|
||||
assignments:
|
||||
title: Assignments
|
||||
total_assigned: Assigned to me
|
||||
completed: Assignments completed
|
||||
pending: Assignments pending
|
||||
assigned_by_user: Assignments given
|
||||
completion_rate: Completion rate
|
||||
chat_usage:
|
||||
title: Chat Activity
|
||||
total_messages: Total Messages
|
||||
dm_messages: DM Messages
|
||||
unique_dm_channels: DM Conversations
|
||||
reactions_received: Reactions Received
|
||||
avg_message_length: Avg Message Length
|
||||
favorite_channels: Favorite Channels
|
||||
messages: messages
|
||||
bot_name: XxRewindBotxX
|
||||
you: You
|
||||
channel_title: "#chat-activity"
|
||||
status_online: "● online"
|
||||
message_1: "You sent <strong>%{count}</strong> messages this year!"
|
||||
reply_1: "lol awesome B-)"
|
||||
message_2: "You had <strong>%{dm_count}</strong> DM conversations with <strong>%{channel_count}</strong> different people"
|
||||
reply_2: "cOo0oL :D"
|
||||
message_3: "People loved your messages! You got <strong>%{count}</strong> reactions ❤️"
|
||||
reply_3: "r0fl rAwr 8-)"
|
||||
message_4: "Your average message length was <strong>%{length}</strong> characters"
|
||||
message_5: "Your favorite channels:"
|
||||
dancing_baby_alt: "Dancing baby"
|
||||
favorite_gifs:
|
||||
title:
|
||||
one: Your favorite GIF
|
||||
other: Your %{count} favorite GIFs
|
||||
total_usage:
|
||||
one: Used %{count} time
|
||||
other: Used %{count} times total
|
||||
invites:
|
||||
title: Invites
|
||||
total_invites: Total Invites Sent
|
||||
redeemed: Accepted Invites
|
||||
redemption_rate: Acceptance Rate
|
||||
avg_trust_level: Avg Trust Level
|
||||
invitee_impact: Impact of Your Invitees
|
||||
invitee_posts: Posts Created
|
||||
invitee_topics: Topics Started
|
||||
invitee_likes: Likes Given
|
||||
most_active_invitee: Most Active Invitee
|
||||
guest_book_title: Guest Book
|
||||
guest_book_subtitle: Your Community Invitations
|
||||
label_invitations_sent: "Invitations sent:"
|
||||
label_guests_joined: "Invites accepted:"
|
||||
label_acceptance_rate: "Acceptance rate:"
|
||||
section_contributions: Invitee Contributions
|
||||
label_posts_written: "Posts written:"
|
||||
label_topics_started: "Topics started:"
|
||||
label_likes_given: "Likes given:"
|
||||
section_most_active: Most Active Guest
|
||||
new_user_interactions:
|
||||
title: Mentoring New Users
|
||||
subtitle:
|
||||
one: You helped %{count} new user this year
|
||||
other: You helped %{count} new users this year
|
||||
new_member:
|
||||
one: "%{count} new member"
|
||||
other: "%{count} new members"
|
||||
total_interactions: Total Interactions
|
||||
unique_new_users: Unique New Users
|
||||
likes_given: Likes Given
|
||||
replies: Replies to Posts
|
||||
mentions: Mentions
|
||||
topics_with_new_users: Topics with New Users
|
||||
time_of_day_activity:
|
||||
title: Daily Activity Rhythm
|
||||
play_button: Play your activity as sound
|
||||
stop_button: Stop audio playback
|
||||
writing_analysis:
|
||||
title: Writing Analysis
|
||||
menu_file: File
|
||||
menu_other: Other
|
||||
menu_additional: Additional
|
||||
menu_opening: OPENING
|
||||
help_text: Press F1 for help
|
||||
app_name: DiscoStar Professional
|
||||
release_info: Release 2025 from CDCK
|
||||
total_words: Total Words
|
||||
total_posts: Total Posts
|
||||
avg_post_length: Avg Post Length
|
||||
readability_score_label: Readability Score
|
||||
readability_level: Readability Level
|
||||
readability_score:
|
||||
over_80:
|
||||
1: "Ernest Hemingway"
|
||||
2: "Raymond Carver"
|
||||
3: "Michael Crichton"
|
||||
4: "Roald Dahl"
|
||||
over_60:
|
||||
1: "J.K. Rowling"
|
||||
2: "Stephen King"
|
||||
3: "Agatha Christie"
|
||||
4: "George R.R. Martin"
|
||||
over_40:
|
||||
1: "George Orwell"
|
||||
2: "F. Scott Fitzgerald"
|
||||
3: "Aldous Huxley"
|
||||
4: "Joan Didion"
|
||||
over_20:
|
||||
1: "Cormac McCarthy"
|
||||
2: "William Faulkner"
|
||||
3: "Virgina Woolf"
|
||||
4: "Gabriel Garcia Marquez"
|
||||
over_0:
|
||||
1: "James Joyce"
|
||||
2: "Thomas Pynchon"
|
||||
3: "David Foster Wallace"
|
||||
4: "Fyodor Dostoevsky"
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
enabled_site_setting :discourse_rewind_enabled
|
||||
|
||||
register_svg_icon "repeat"
|
||||
register_svg_icon "volume-high"
|
||||
register_svg_icon "volume-xmark"
|
||||
|
||||
register_asset "stylesheets/common/_index.scss"
|
||||
register_asset "stylesheets/mobile/_index.scss", :mobile
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 325 KiB |
@@ -13,6 +13,11 @@ RSpec.describe DiscourseRewind::Action::BestTopics do
|
||||
|
||||
describe ".call" do
|
||||
it "returns top 3 topics ordered by yearly_score" do
|
||||
[topic_1, topic_2, topic_3, topic_4, topic_5].each do |topic|
|
||||
topic.update!(created_at: random_datetime)
|
||||
end
|
||||
TopTopic.refresh!
|
||||
|
||||
TopTopic.find_by(topic_id: topic_1.id).update!(yearly_score: 15)
|
||||
TopTopic.find_by(topic_id: topic_2.id).update!(yearly_score: 10)
|
||||
TopTopic.find_by(topic_id: topic_3.id).update!(yearly_score: 6)
|
||||
@@ -24,19 +29,19 @@ RSpec.describe DiscourseRewind::Action::BestTopics do
|
||||
topic_id: topic_1.id,
|
||||
title: topic_1.title,
|
||||
excerpt: topic_1.excerpt,
|
||||
yearly_score: 15,
|
||||
yearly_score: 15.0,
|
||||
},
|
||||
{
|
||||
topic_id: topic_5.id,
|
||||
title: topic_5.title,
|
||||
excerpt: topic_5.excerpt,
|
||||
yearly_score: 13,
|
||||
yearly_score: 13.0,
|
||||
},
|
||||
{
|
||||
topic_id: topic_4.id,
|
||||
title: topic_4.title,
|
||||
excerpt: topic_4.excerpt,
|
||||
yearly_score: 11,
|
||||
yearly_score: 11.0,
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user