diff --git a/api/feature/index.js b/api/feature/index.js
index 703f2524..7788522c 100644
--- a/api/feature/index.js
+++ b/api/feature/index.js
@@ -1,12 +1,14 @@
import { default as self } from "./self.js";
import { default as traps } from "./traps.js";
import { default as auth } from "./auth.js";
+import { default as server } from "./server.js";
export default function (data) {
var feature = new Feature(data);
feature.self = self(data.id);
feature.traps = traps()
feature.auth = auth()
+ feature.server = server()
feature.page = {
appendToSharedSpace: ScratchTools.appendToSharedSpace,
waitForElement: ScratchTools.waitForElement,
diff --git a/api/feature/server.js b/api/feature/server.js
new file mode 100644
index 00000000..b1f4c507
--- /dev/null
+++ b/api/feature/server.js
@@ -0,0 +1,8 @@
+export default function (id) {
+ return {
+ url: "https://data.scratchtools.app",
+ endpoint: function(path) {
+ return "https://data.scratchtools.app" + path
+ },
+ }
+}
\ No newline at end of file
diff --git a/features/features.json b/features/features.json
index bf6c47f3..36ce35c0 100644
--- a/features/features.json
+++ b/features/features.json
@@ -1,4 +1,9 @@
[
+ {
+ "version": 2,
+ "id": "project-reactions",
+ "versionAdded": "v4.0.0"
+ },
{
"version": 2,
"id": "chomp-blocks",
diff --git a/features/project-reactions/data.json b/features/project-reactions/data.json
new file mode 100644
index 00000000..c177ebac
--- /dev/null
+++ b/features/project-reactions/data.json
@@ -0,0 +1,46 @@
+{
+ "title": "Project Reactions",
+ "description": "Allows you to react to projects with different emojis.",
+ "credits": [
+ { "username": "rgantzos", "url": "https://scratch.mit.edu/users/rgantzos/" }
+ ],
+ "type": ["Website"],
+ "tags": ["New"],
+ "dynamic": true,
+ "scripts": [
+ {
+ "file": "script.js",
+ "runOn": "/projects/*"
+ }
+ ],
+ "styles": [
+ {
+ "file": "style.css",
+ "runOn": "/projects/*"
+ }
+ ],
+ "components": [
+ {
+ "type": "info",
+ "content": "Users will not receive a notification when you react to their projects."
+ }
+ ],
+ "resources": [
+ { "name": "project-reactions-beaming", "path": "/emojis/beaming.svg" },
+ { "name": "project-reactions-crying", "path": "/emojis/crying.svg" },
+ { "name": "project-reactions-fire", "path": "/emojis/fire.svg" },
+ { "name": "project-reactions-hands", "path": "/emojis/hands.svg" },
+ {
+ "name": "project-reactions-heart-eyes",
+ "path": "/emojis/heart-eyes.svg"
+ },
+ {
+ "name": "project-reactions-heart-face",
+ "path": "/emojis/heart-face.svg"
+ },
+ { "name": "project-reactions-hearts", "path": "/emojis/hearts.svg" },
+ { "name": "project-reactions-laughing", "path": "/emojis/laughing.svg" },
+ { "name": "project-reactions-popper", "path": "/emojis/popper.svg" },
+ { "name": "project-reactions-thumbsup", "path": "/emojis/thumbsup.svg" }
+ ]
+}
diff --git a/features/project-reactions/emojis/beaming.svg b/features/project-reactions/emojis/beaming.svg
new file mode 100644
index 00000000..b10147ae
--- /dev/null
+++ b/features/project-reactions/emojis/beaming.svg
@@ -0,0 +1,100 @@
+
\ No newline at end of file
diff --git a/features/project-reactions/emojis/crying.svg b/features/project-reactions/emojis/crying.svg
new file mode 100644
index 00000000..a826b814
--- /dev/null
+++ b/features/project-reactions/emojis/crying.svg
@@ -0,0 +1,75 @@
+
\ No newline at end of file
diff --git a/features/project-reactions/emojis/fire.svg b/features/project-reactions/emojis/fire.svg
new file mode 100644
index 00000000..6570b839
--- /dev/null
+++ b/features/project-reactions/emojis/fire.svg
@@ -0,0 +1,105 @@
+
\ No newline at end of file
diff --git a/features/project-reactions/emojis/hands.svg b/features/project-reactions/emojis/hands.svg
new file mode 100644
index 00000000..90475afc
--- /dev/null
+++ b/features/project-reactions/emojis/hands.svg
@@ -0,0 +1,161 @@
+
\ No newline at end of file
diff --git a/features/project-reactions/emojis/heart-eyes.svg b/features/project-reactions/emojis/heart-eyes.svg
new file mode 100644
index 00000000..76badafd
--- /dev/null
+++ b/features/project-reactions/emojis/heart-eyes.svg
@@ -0,0 +1,97 @@
+
\ No newline at end of file
diff --git a/features/project-reactions/emojis/heart-face.svg b/features/project-reactions/emojis/heart-face.svg
new file mode 100644
index 00000000..b1de943d
--- /dev/null
+++ b/features/project-reactions/emojis/heart-face.svg
@@ -0,0 +1,163 @@
+
\ No newline at end of file
diff --git a/features/project-reactions/emojis/hearts.svg b/features/project-reactions/emojis/hearts.svg
new file mode 100644
index 00000000..c61f8712
--- /dev/null
+++ b/features/project-reactions/emojis/hearts.svg
@@ -0,0 +1,140 @@
+
\ No newline at end of file
diff --git a/features/project-reactions/emojis/laughing.svg b/features/project-reactions/emojis/laughing.svg
new file mode 100644
index 00000000..0a8347e3
--- /dev/null
+++ b/features/project-reactions/emojis/laughing.svg
@@ -0,0 +1,154 @@
+
\ No newline at end of file
diff --git a/features/project-reactions/emojis/popper.svg b/features/project-reactions/emojis/popper.svg
new file mode 100644
index 00000000..06dc90b3
--- /dev/null
+++ b/features/project-reactions/emojis/popper.svg
@@ -0,0 +1,276 @@
+
\ No newline at end of file
diff --git a/features/project-reactions/emojis/thumbsup.svg b/features/project-reactions/emojis/thumbsup.svg
new file mode 100644
index 00000000..cf598a30
--- /dev/null
+++ b/features/project-reactions/emojis/thumbsup.svg
@@ -0,0 +1,238 @@
+
\ No newline at end of file
diff --git a/features/project-reactions/script.js b/features/project-reactions/script.js
new file mode 100644
index 00000000..7e82632d
--- /dev/null
+++ b/features/project-reactions/script.js
@@ -0,0 +1,167 @@
+export default async function ({ feature, console }) {
+ let username = feature.redux.getState().session?.session?.user?.username;
+
+ let projectId = window.location.pathname.split("/")[2];
+ let reactions = await (
+ await fetch(feature.server.endpoint(`/reactions/${projectId}/`))
+ ).json();
+
+ ScratchTools.waitForElements("div.flex-row.stats", function (req, res) {
+ makeReactions(reactions);
+ });
+
+ function makeReactions(data) {
+ let parent = document.querySelector("div.flex-row.stats");
+
+ let already = parent.querySelector(".ste-reactions");
+ already?.remove();
+
+ let div = document.createElement("div");
+ div.className = "ste-reactions";
+
+ let reactionsList = document.createElement("div");
+ reactionsList.className = "ste-reactions-list";
+
+ let allEmojis = [];
+ for (var i in reactions) {
+ if (!allEmojis.includes(reactions[i].emoji)) {
+ allEmojis.push({
+ emoji: reactions[i].emoji,
+ count: reactions.filter((el) => el.emoji === reactions[i].emoji)
+ .length,
+ });
+ }
+ }
+
+ if (allEmojis.length === 0) {
+ let span = document.createElement("span");
+
+ let img = document.createElement("img");
+ img.src = feature.self.getResource("project-reactions-heart-eyes");
+ span.appendChild(img);
+
+ reactionsList.appendChild(span);
+ }
+
+ for (var i in allEmojis) {
+ if (i < 3) {
+ let span = document.createElement("span");
+
+ let img = document.createElement("img");
+ img.src = feature.self.getResource(
+ "project-reactions-" + allEmojis[i].emoji
+ );
+ span.appendChild(img);
+
+ reactionsList.appendChild(span);
+ }
+ }
+
+ div.appendChild(reactionsList);
+
+ let modal = document.createElement("div");
+ modal.className = "ste-reactions-modal";
+ div.appendChild(modal);
+
+ let options = document.createElement("div");
+ options.classList.add("ste-reactions-options");
+
+ function updateOptions() {
+ allEmojis = [];
+ for (var i in reactions) {
+ if (!allEmojis.includes(reactions[i].emoji)) {
+ allEmojis.push({
+ emoji: reactions[i].emoji,
+ count: reactions.filter((el) => el.emoji === reactions[i].emoji)
+ .length,
+ });
+ }
+ }
+
+ options.innerHTML = "";
+
+ let emojis = [
+ "beaming",
+ "crying",
+ "fire",
+ "hands",
+ "heart-eyes",
+ "heart-face",
+ "hearts",
+ "laughing",
+ "popper",
+ "thumbsup",
+ ];
+
+ for (var i in emojis) {
+ let img = document.createElement("img");
+ img.src = feature.self.getResource("project-reactions-" + emojis[i]);
+ img.className = "ste-reactions-option";
+ img.dataset.emoji = emojis[i];
+ if (
+ reactions.find((el) => el.emoji === emojis[i] && el.user === username)
+ ) {
+ img.classList.add("selected");
+ }
+
+ let span = document.createElement("span");
+ span.textContent = allEmojis
+ .filter((el) => el.emoji === emojis[i])
+ .length.toString();
+ options.appendChild(span);
+
+ img.addEventListener("click", async function () {
+ let emoji = this.dataset.emoji;
+ if (!img.className.includes("selected")) {
+ this.classList.add("selected");
+ ScratchTools.verifyUser(async function (token) {
+ let data = await (
+ await fetch("https://data.scratchtools.app/react/", {
+ method: "POST",
+ headers: {
+ Accept: "application/json",
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ token,
+ emoji,
+ project: projectId,
+ }),
+ })
+ ).json();
+
+ if (data.success) {
+ reactions = data.data;
+ updateOptions();
+ }
+ });
+ } else {
+ this.classList.remove("selected");
+ ScratchTools.verifyUser(async function (token) {
+ let data = await (
+ await fetch("https://data.scratchtools.app/unreact/", {
+ method: "POST",
+ headers: {
+ Accept: "application/json",
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ token, emoji, project: projectId }),
+ })
+ ).json();
+
+ if (data.success) {
+ reactions = data.data;
+ updateOptions();
+ }
+ });
+ }
+ });
+ options.appendChild(img);
+ }
+ }
+ updateOptions();
+ modal.appendChild(options);
+
+ parent.appendChild(div);
+ }
+}
diff --git a/features/project-reactions/style.css b/features/project-reactions/style.css
new file mode 100644
index 00000000..e5aa265b
--- /dev/null
+++ b/features/project-reactions/style.css
@@ -0,0 +1,119 @@
+.ste-reactions-list {
+ display: flex;
+ transform: scale(1.3);
+}
+
+.ste-reactions-list img {
+ height: 1.25rem;
+ position: relative;
+ top: .15rem;
+}
+
+.ste-reactions-list span {
+ height: 1.75rem;
+ width: 1.75rem;
+ background: rgb(226, 226, 226);
+ border-radius: calc(1.75rem / 2);
+ text-align: center;
+ z-index: 6;
+}
+
+.ste-reactions-list span:not(:first-child) {
+ margin-left: -.5rem;
+ transition: margin-left .3s, opacity .3s;
+}
+
+.ste-reactions-list span:nth-child(2) {
+ opacity: .6;
+ z-index: 5;
+}
+
+.ste-reactions-list span:nth-child(3) {
+ opacity: .3;
+ z-index: 4;
+}
+
+.ste-reactions-list {
+ cursor: pointer;
+ transition: transform .3s;
+}
+
+.ste-reactions:hover .ste-reactions-list {
+ transform: scale(1.35);
+}
+
+.ste-reactions:hover .ste-reactions-list span:not(:first-child) {
+ margin-left: .25rem;
+ opacity: 1;
+}
+
+.ste-reactions-modal {
+ display: none;
+}
+
+.ste-reactions {
+ position: relative;
+}
+
+.ste-reactions:hover .ste-reactions-modal {
+ display: block;
+ position: absolute;
+ top: -100%;
+ left: -6rem;
+ transform: translateY(calc(-50% - 3rem));
+ z-index: 99;
+ width: 15rem;
+ background: white;
+ padding: 1rem;
+ box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.357);
+ height: fit-content;
+ border-radius: .5rem;
+ padding-left: 1.5rem;
+ padding-top: 0px;
+}
+
+.ste-reactions-option {
+ width: 100%;
+ height: 2rem;
+ opacity: .5;
+ object-fit: contain;
+ opacity: .5;
+ cursor: pointer;
+ transition: transform .3s;
+}
+
+.ste-reactions-option:hover {
+ transform: scale(1.2);
+}
+
+.ste-reactions-option.selected {
+ opacity: 1;
+}
+
+.ste-reactions-options {
+ display: flex;
+ column-count: 5;
+ column-gap: 1rem;
+ display: inline-block;
+ vertical-align: center;
+}
+
+.ste-reactions-options span {
+ position: relative;
+ bottom: -1rem;
+ left: 1rem;
+ background-color: #0fbd8c;
+ color: white;
+ padding: .1rem;
+ height: 1.8rem;
+ width: 1.6rem;
+ display: block;
+ margin: 0px;
+ text-align: center;
+ font-size: 1rem;
+ padding-top: 0px;
+ border-radius: .9rem;
+ font-weight: 600;
+ transform: scale(.7);
+ z-index: 9999;
+}
\ No newline at end of file