Features - devrica-collections/Gritsquare-femcoders-FE25 GitHub Wiki
| Feature | Description | Contributor |
|---|---|---|
| Write a message and post it on homepage | Users can create and submit positive messages to the message board | Elin - Core Team |
| Like others messages | Users can like messages from other community members | Jenny - Core Team |
| Remove messages | Users can delete their own messages from the board | Jenny - Core Team |
| Like animation | When users like a post there is an animation | Bianca - Core Team |
| Bubble sound when new message gets added | Audio feedback plays when a new message is posted | Elin - Core Team |
| Pick a gif with your message | Users can attach GIFs to their messages | Erica - Core team |
| Pick a color of your post it note | When typing message, users can choose the color of their post it note | Emma - Core Team |
| Guidelines popup | A guideline popup that shows rules and how to act | Emma - Core Team |
| Profanity filter* | Filters out inappropriate language in messages | Mohammed - External |
| Likes leaderboard* | Displays top liked messages and users in a leaderboard | Isak - External |
| Button to spawn bubbles that can be popped* | Interactive bubble elements that users can click to pop | Elias - External |
| Add emojis to your message* | Users can add emoji reactions to their messages | Jack - External |
| Translate the page feature* | Allows users to translate the entire page into different languages | Ali - External |
| Delay on sending messages, Anti-Spam* | Implements a cooldown period between message submissions to prevent spam | Ali - External |
| Word counter* | Displays the character or word count for messages | Isabelle - External |
| Nickname* | Choose nickname and display it | Isabelle - External |
*Added by contributor
<form id="msgForm">
<input type="text" id="messageInput" placeholder="Write something..." required />
<button type="submit">Send</button>
</form>const form = document.querySelector('#msgForm');
const input = document.querySelector('#messageInput');
form.addEventListener("submit", (e) => {
e.preventDefault();
const text = input.value.trim();
if (!text) return;
form.reset();
});This feature allows users to write and send positive messages through a text input field. When submitted, the message is processed and added to the application. The form has a textfield (#messageinput) and a send button. It collects the value from the input field. The form resets after it’s been sent. The application uses Firebase Realtime Database to store and retrieve messages.
Users can not only spread good vibes with positive messages but also likes! Users can spread unlimited good vibes with unlimited likes for every message. Not only can the user read the positive messages but also see how much appreciation the messages they post get!
likeBtn.addEventListener('click', async () => {
const postRef = ref(db, `messages/${id}/likes`);
await setTimeout(()=>{
noteCard.style.overflow="overflow"
runTransaction(postRef, (currentLikes) => {
return (currentLikes || 0) + 1;
})
})The like button is created in a function called render. It is handled with an event-listener. When the user interacts with the button the amount of likes goes up by one. By default when a message is created it has 0 likes.
Although the website is enforced with a profanity filter, sometimes bad vibes can slip in. If there is a message that makes the user feel bad, the user has full control to remove it from the website. Plus we all have accidentally sent/posted something at least once in our lives. But on our website an accident is not the end of the world with a delete button.
const deleteBtn = document.createElement('button');
deleteBtn.className = 'delete-btn';
deleteBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#BB271A"><path d="M280-120q-33 0-56.5-23.5T200-200v-520h-40v-80h200v-40h240v40h200v80h-40v520q0 33-23.5 56.5T680-120H280Zm400-600H280v520h400v-520ZM360-280h80v-360h-80v360Zm160 0h80v-360h-80v360ZM280-720v520-520Z"/></svg>';
deleteBtn.title = 'Delete message';
deleteBtn.addEventListener('click', async () => {
const confirmDelete = confirm("Are you sure you want to remove the good vibes? This cannot be undone.");
if (confirmDelete) {
try {
const postRef = ref(db, `messages/${id}`);
await remove(postRef);
} catch (error) {
console.error("Error deleting message:", error);
alert("Could not delete message. Try again later.");
}
}
});
noteCard.appendChild(deleteBtn);The delete button is represented by an SVG-icon that looks like a trashcan, to give users familiarity to what the button does. SVG-icons are practical because they are scalable and customizable. The delete button is also handled with an eventlistener. When the button is pressed the user will be alerted by what is about to happen so there won’t be accidental deletions. Once the user agree to proceed the program will look for the right message id and remove it from the firebase.
This code generates a heart explosion animation when the like button is clicked. The heart explosion container is appended to each (noteCard) article element.
function heartExplosion (container, options = {}){
if(!container) return;
const num = options.num || 35;
const sizeMin = options.sizeMin || 10;
const sizeMax = options.sizeMax || 20;
const spread = options.spread ||300;
const colors = options.colors || ["#ff4d6d", "#ff758f", "#ff8fa3"];
const duration = options.duration || 900;
await heartExplosion(likeBtn, {
num: 40,
sizeMin: 8,
sizeMax: 18,
spread: 300,
duration: 500,
colors: ["#ff4d6d", "#ff758f", "#ff8fa3"]});<audio id="notifSound" src="./sound/Bubbles-sound-effect.mp3" preload="auto"></audio>const notifSound = document.getElementById("notifSound");
notifSound.currentTime = 0;
notifSound.play().catch(() => {});The code above generates a bubble sound notification every time a message is displayed in the chat. The element is used to load the bubble sound effect. It is preloaded so it can play immediately when a message appears. The audio file (Bubbles-sound-effect.mp3) is gathered from https://pixabay.com/sound-effects/search/bubbles/
if (gifUrl) {
const gifImg = document.createElement('img');
gifImg.src = gifUrl;
gifImg.style.maxWidth = '100%';
gifImg.style.borderRadius = '8px';
gifImg.style.marginBottom = '10px';
noteCard.appendChild(gifImg);
}
searchGifBtn.addEventListener('click', async () => {
clearSelectedGif();
const search = gifSearchInput.value.trim();
const gifs = await searchGifs(search);
displayGifResults(gifs, '#gifResults')
})The code above uses the exported functions from modules/gifapi and its a search button that calls the exported functions. On the main page the gifs are being rendered via liveupdate.
Used with klipy
This code allows you to choose a color for your post-it note before sending it. The colors the user can choose between are selected and put in to stay on the theme of the website, but still make the chatforum a little bit more fun and realistic with the different colored post-it notes.
<div id="selectColor">
<div id="color-1" class="color-option selected"
data-color="#d6c5ef"></div>
<div id="color-2" class="color-option"
data-color="#edcbf6"></div>
<!-- More color options.. -->
</div>Color options listed as separate empty div elements with the color code for each option, later to be styled in the css code
let selectedColor = '#d6c5ef';
function initColorPicker() {
const colorOptions = document.querySelectorAll('.color-option');
colorOptions.forEach(option => {
option.style.backgroundColor = option.dataset.color;
option.addEventListener('click', () => {
colorOptions.forEach(opt => opt.classList.remove('selected'));
option.classList.add('selected');
selectedColor = option.dataset.color;
});
});
}
initColorPicker();selectedColor is put in functions that decides what data is stored in the database
// Open and close message form + guidelines
const openBtn = document.querySelector('#openCard');
const card = document.querySelector('#card');
const guidelinesCard = document.querySelector('#guidelines');
const closeBtn = document.querySelector('#closeBtn');
const readGuidelinesBtn = document.getElementById('readGuidelinesBtn');
// Function to check if 24h has passed since guidelines was closed
function hasOneDayPassed() {
const lastClosed = localStorage.getItem("guidelinesClosedAt");
const oneDay = 24 * 60 * 60 * 1000; // 24 timmar i ms
const now = Date.now();
// If no timestamp → assume never shown → show guidelines
if (!lastClosed) return true;
return (now - lastClosed) > oneDay;
}
// When “Write a message” button is clicked
openBtn.addEventListener('click', () => {
const cardWasHidden = card.classList.contains('hidden');
card.classList.toggle('hidden');
if (cardWasHidden) {
// Automatically show guidelines IF 24h has passed
if (hasOneDayPassed()) {
guidelinesCard.classList.remove('hidden');
}
} else {
// If form is manually closed → guidelines is closed
guidelinesCard.classList.add('hidden');
}
});
// Guidelines manually closed
closeBtn.addEventListener('click', () => {
guidelinesCard.classList.add('hidden');
// Timestamp is saved when user manually closes guidelines
localStorage.setItem("guidelinesClosedAt", Date.now());
});
//Open to read guidelines regardless of 24h timer (“Guidelines” button)
readGuidelinesBtn.addEventListener('click', () => {
guidelinesCard.classList.remove('hidden');
});The guidelines feature shows up when the user clicks on the “Write a message” button for the first time then the timestamp is stored in Local Storage and the guidelines feature will automatically show up again like the first time after 24 hours. During the 24h period the user can still access the guidelines manually by clicking the “Guidelines” button at the top of the “Write a message” form. When closing the guidelines by clicking the X button the 24h timer restarts regardless of whether it opens automatically or manually.
The guidelines js code is somewhat integrated with the forms code since guidelines is connected to sending messages
profanityfilter.mp4
import { profanityList } from "./profanityList.js";
function escapeRegex(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
export function censorBadWords(text = "") {
const escapedWords = profanityList.map(escapeRegex);
const regex = new RegExp(`\\b(${escapedWords.join("|")})\\b`, "gi");
return text.replace(regex, (match) => "*".repeat(match.length));
}This functions parameter is the original text input. The text goes through a filter that replaces the bad words with stars. RegEx is used to fight "leetspeak", trying to circumvent regular characters. Filter function
Possible to add censored words via Profanitylist
leaderboard.mp4
onValue(leaderBoardQuery, (snapshot) => {
const data = snapshot.val()
if (!data) return;
const leaderboard = Object.entries(data)
.map(([id, msg]) => ({
id,
text: msg.text,
likes: msg.likes
}))
.sort((a, b) => b.likes - a.likes)
console.log(leaderboard)
const listEl = document.querySelector('#leaderBoardList')
listEl.innerHTML = ''
leaderboard.forEach((item, index) => {
const li = document.createElement('li')
li.textContent = `${ index + 1 }. ${ item.text } - ❤️ ${ item.likes }`
listEl.appendChild(li)
})
})The like leaderboard uses firebase method orderByChild to sort the messages from most likes to least likes
leaderboard function Made by Isak
bubble.mp4
emoji.mp4
selectEl.addEventListener('change', () => {
const emoji = selectEl.value
if (!emoji) return
const start = message.selectionStart
const end = message.selectionEnd
const text = message.value
if (Array.from(message.value).length >= message.maxLength) return
message.value = text.slice(0, start) + emoji + text.slice(end)
message.focus()
message.selectionStart = message.selectionEnd = start + emoji.length
message.dispatchEvent(new Event('input'))
})translate.mp4
function loadGoogleScript() {
const script = document.createElement('script');
script.src = "https://translate.google.com/translate_a/element.js?cb=googleTranslateElementInit";
script.async = true;
script.defer = true;
document.body.appendChild(script);
}anti-spam.mp4
characterlimit.mp4
const input = document.getElementById("messageInput");
const counter = document.getElementById("counter");
const maxLength = 200;
input.addEventListener("input", () => {
if (input.value.length > maxLength) {
input.value = input.value.slice(0, maxLength);
}
counter.textContent = `${input.value.length}/${maxLength}`;
});