add guestbook
BIN
images/emotebutton.gif
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
images/emotes/applause.gif
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
images/emotes/astonished.gif
Normal file
|
After Width: | Height: | Size: 846 B |
BIN
images/emotes/caramelldansen.gif
Normal file
|
After Width: | Height: | Size: 822 B |
BIN
images/emotes/joy.gif
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
images/emotes/kiss.gif
Normal file
|
After Width: | Height: | Size: 837 B |
BIN
images/emotes/laugh.gif
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
images/emotes/love.gif
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
images/emotes/wave.gif
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
images/emotes/yippee.gif
Normal file
|
After Width: | Height: | Size: 675 B |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB |
BIN
images/sendbutton.png
Normal file
|
After Width: | Height: | Size: 430 B |
BIN
images/sendbuttonhover.png
Normal file
|
After Width: | Height: | Size: 413 B |
20
index.html
@@ -8,7 +8,7 @@
|
||||
<link href="style.css" rel="stylesheet" type="text/css" media="all">
|
||||
<script src="js/onLoad.js" defer></script>
|
||||
<script src="js/switchPage.js" defer></script>
|
||||
<script src="js/updates.js" defer></script>
|
||||
<script src="js/guestbook.js" defer></script>
|
||||
<script src="js/player.js" defer></script>
|
||||
<script src="js/counter.js" defer></script>
|
||||
</head>
|
||||
@@ -213,15 +213,31 @@
|
||||
<div class="small-content-updates"
|
||||
style="position: relative; top: 0px; left: 0px; width: 285px; height: auto;">
|
||||
<div class="small-content-toolbar">
|
||||
<span data-text="toolbar text">UPDATES</span>
|
||||
<span data-text="toolbar text">GUESTBOOK</span>
|
||||
<img src="images/xbutton.png" id="small-xbutton-right">
|
||||
</div>
|
||||
|
||||
<div id="small-content-pictochat">
|
||||
<div id="pictochat-content">
|
||||
<div class="picto"></div>
|
||||
<img src="images/emotebutton.gif" id="gb-emote-toggle"
|
||||
style="image-rendering: pixelated; position:absolute; z-index: 100;">
|
||||
<img src="images/pitcochatmenu.png"
|
||||
style="border-radius: 0 0 10px 10px; position:relative; top: 0px; left:1px; width:257px; image-rendering: pixelated;">
|
||||
<div id="gb-emote-selector" class="gb-toggle-div">
|
||||
<img src="images/emotes/joy.gif" title=":happy:" class="gb-emote-item">
|
||||
<img src="images/emotes/love.gif" title=":love:" class="gb-emote-item">
|
||||
<img src="images/emotes/laugh.gif" title=":laugh:" class="gb-emote-item">
|
||||
<img src="images/emotes/astonished.gif" title=":shock:" class="gb-emote-item">
|
||||
<img src="images/emotes/wave.gif" title=":hello:" class="gb-emote-item">
|
||||
<img src="images/emotes/kiss.gif" title=":kiss:" class="gb-emote-item">
|
||||
<img src="images/emotes/applause.gif" title=":clap:" class="gb-emote-item">
|
||||
<img src="images/emotes/caramelldansen.gif" title=":dance:" class="gb-emote-item">
|
||||
<img src="images/emotes/yippee.gif" title=":yippee:" class="gb-emote-item">
|
||||
</div>
|
||||
<input type="text" id="gb-name" placeholder="name" maxlength="30">
|
||||
<textarea id="gb-message" placeholder="enter your message here." maxlength="500"></textarea>
|
||||
<img src="images/sendbutton.png" id="gb-send" title="Send">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
126
js/guestbook-worker.js
Normal file
@@ -0,0 +1,126 @@
|
||||
|
||||
const CORS_HEADERS = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type',
|
||||
};
|
||||
|
||||
const KV_KEY = 'messages';
|
||||
const MAX_MESSAGES = 500;
|
||||
const MAX_NAME_LENGTH = 30;
|
||||
const MAX_MESSAGE_LENGTH = 500;
|
||||
|
||||
|
||||
function jsonResponse(data, status = 200) {
|
||||
return new Response(JSON.stringify(data), {
|
||||
status,
|
||||
headers: { 'Content-Type': 'application/json', ...CORS_HEADERS },
|
||||
});
|
||||
}
|
||||
|
||||
function generateId() {
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
|
||||
function formatDate() {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
function sanitize(str) {
|
||||
return str
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
async function hashIP(ip) {
|
||||
const encoder = new TextEncoder();
|
||||
const data = encoder.encode(ip + '_guestbook_salt');
|
||||
const hash = await crypto.subtle.digest('SHA-256', data);
|
||||
return Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, '0')).join('').slice(0, 16);
|
||||
}
|
||||
|
||||
async function getMessages(env) {
|
||||
const raw = await env.GUESTBOOK.get(KV_KEY);
|
||||
return raw ? JSON.parse(raw) : [];
|
||||
}
|
||||
|
||||
async function saveMessages(env, messages) {
|
||||
await env.GUESTBOOK.put(KV_KEY, JSON.stringify(messages));
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(request, env) {
|
||||
if (request.method === 'OPTIONS') {
|
||||
return new Response(null, { headers: CORS_HEADERS });
|
||||
}
|
||||
|
||||
const url = new URL(request.url);
|
||||
const path = url.pathname;
|
||||
|
||||
if (request.method === 'GET' && path === '/messages') {
|
||||
const messages = await getMessages(env);
|
||||
const sanitized = messages.map(m => ({
|
||||
...m,
|
||||
likes: Array.isArray(m.likes) ? m.likes.length : (typeof m.likes === 'number' ? m.likes : 0),
|
||||
}));
|
||||
return jsonResponse(sanitized);
|
||||
}
|
||||
|
||||
if (request.method === 'POST' && path === '/messages') {
|
||||
const body = await request.json().catch(() => null);
|
||||
if (!body || !body.message || !body.message.trim()) {
|
||||
return jsonResponse({ error: 'Message is required' }, 400);
|
||||
}
|
||||
|
||||
const name = sanitize((body.name || 'Anonymous').trim().slice(0, MAX_NAME_LENGTH));
|
||||
const message = sanitize(body.message.trim().slice(0, MAX_MESSAGE_LENGTH));
|
||||
|
||||
const messages = await getMessages(env);
|
||||
const entry = {
|
||||
id: generateId(),
|
||||
name,
|
||||
message,
|
||||
date: formatDate(),
|
||||
likes: [],
|
||||
replies: [],
|
||||
};
|
||||
|
||||
messages.unshift(entry);
|
||||
if (messages.length > MAX_MESSAGES) messages.length = MAX_MESSAGES;
|
||||
await saveMessages(env, messages);
|
||||
|
||||
return jsonResponse(entry, 201);
|
||||
}
|
||||
|
||||
const likeMatch = path.match(/^\/messages\/([^/]+)\/like$/);
|
||||
if (request.method === 'POST' && likeMatch) {
|
||||
const messageId = likeMatch[1];
|
||||
const ip = request.headers.get('CF-Connecting-IP') || 'unknown';
|
||||
const ipHash = await hashIP(ip);
|
||||
|
||||
const messages = await getMessages(env);
|
||||
const msg = messages.find(m => m.id === messageId);
|
||||
if (!msg) return jsonResponse({ error: 'Message not found' }, 404);
|
||||
|
||||
if (!Array.isArray(msg.likes)) {
|
||||
const oldCount = typeof msg.likes === 'number' ? msg.likes : 0;
|
||||
msg.likes = [];
|
||||
|
||||
for (let i = 0; i < oldCount; i++) msg.likes.push('legacy_' + i);
|
||||
}
|
||||
const alreadyLiked = msg.likes.includes(ipHash);
|
||||
if (!alreadyLiked) {
|
||||
msg.likes.push(ipHash);
|
||||
}
|
||||
|
||||
await saveMessages(env, messages);
|
||||
|
||||
return jsonResponse({ likes: msg.likes.length, liked: true });
|
||||
}
|
||||
|
||||
return jsonResponse({ error: 'Not found' }, 404);
|
||||
},
|
||||
};
|
||||
244
js/guestbook.js
Normal file
@@ -0,0 +1,244 @@
|
||||
const GUESTBOOK_API = 'https://guestbook.bug-dev-mail.workers.dev';
|
||||
|
||||
const EMOTES = {
|
||||
':happy:': 'images/emotes/joy.gif',
|
||||
':love:': 'images/emotes/love.gif',
|
||||
':laugh:': 'images/emotes/laugh.gif',
|
||||
':shock:': 'images/emotes/astonished.gif',
|
||||
':hello:': 'images/emotes/wave.gif',
|
||||
':kiss:': 'images/emotes/kiss.gif',
|
||||
':clap:': 'images/emotes/applause.gif',
|
||||
':dance:': 'images/emotes/caramelldansen.gif',
|
||||
':yippee:': 'images/emotes/yippee.gif',
|
||||
};
|
||||
|
||||
function replaceLinks(text) {
|
||||
function cleanUrl(url) {
|
||||
return url.replace(/&/g, '&');
|
||||
}
|
||||
var result = text.replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g,
|
||||
function(_, label, url) {
|
||||
return '<a href="' + cleanUrl(url) + '" target="_blank" rel="noopener noreferrer">' + label + '</a>';
|
||||
});
|
||||
result = result.replace(/(^|[^"'])(https?:\/\/[^\s<]+)/g,
|
||||
function(_, pre, url) {
|
||||
return pre + '<a href="' + cleanUrl(url) + '" target="_blank" rel="noopener noreferrer">' + url + '</a>';
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
function replaceEmotes(text) {
|
||||
let result = replaceLinks(text);
|
||||
for (const [code, src] of Object.entries(EMOTES)) {
|
||||
const escaped = code.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
result = result.replace(new RegExp(escaped, 'g'),
|
||||
`<img src="${src}" class="gb-emote" title="${code}" alt="${code}">`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function timeAgo(dateStr) {
|
||||
if (!dateStr.includes('T') && !dateStr.includes('-')) {
|
||||
return dateStr;
|
||||
}
|
||||
const d = new Date(dateStr);
|
||||
const now = new Date();
|
||||
const diffMs = now - d;
|
||||
const diffMin = Math.floor(diffMs / 60000);
|
||||
const diffHr = Math.floor(diffMs / 3600000);
|
||||
const diffDay = Math.floor(diffMs / 86400000);
|
||||
|
||||
if (diffMin < 1) return 'just now';
|
||||
if (diffMin < 60) return diffMin + ' min ago';
|
||||
if (diffHr < 24) return diffHr + ' hr ago';
|
||||
if (diffDay === 1) return '1 day ago';
|
||||
return diffDay + ' days ago';
|
||||
}
|
||||
|
||||
function createMessageBubble(msg) {
|
||||
const entry = document.createElement('div');
|
||||
entry.style.setProperty('--col', '#90c9ff');
|
||||
|
||||
const nameDiv = document.createElement('div');
|
||||
nameDiv.textContent = msg.name || 'Anonymous';
|
||||
|
||||
const dateDiv = document.createElement('div');
|
||||
dateDiv.className = 'gb-date';
|
||||
dateDiv.textContent = timeAgo(msg.date);
|
||||
|
||||
const msgDiv = document.createElement('div');
|
||||
msgDiv.className = 'gb-msg-content';
|
||||
msgDiv.innerHTML = replaceEmotes(msg.message);
|
||||
|
||||
entry.appendChild(nameDiv);
|
||||
entry.appendChild(dateDiv);
|
||||
entry.appendChild(msgDiv);
|
||||
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'gb-actions';
|
||||
|
||||
const likeBtn = document.createElement('button');
|
||||
likeBtn.className = 'gb-like-btn';
|
||||
const likeCount = typeof msg.likes === 'number' ? msg.likes : (Array.isArray(msg.likes) ? msg.likes.length : 0);
|
||||
likeBtn.innerHTML = `<span class="gb-like-icon">♥</span> ${likeCount}`;
|
||||
likeBtn.onclick = () => likeMessage(msg.id, likeBtn);
|
||||
|
||||
actions.appendChild(likeBtn);
|
||||
entry.appendChild(actions);
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
async function loadMessages() {
|
||||
const picto = document.querySelector('#pictochat-content .picto');
|
||||
if (!picto) return;
|
||||
picto.innerHTML = '';
|
||||
|
||||
try {
|
||||
const res = await fetch(GUESTBOOK_API + '/messages');
|
||||
const messages = await res.json();
|
||||
messages.forEach(msg => {
|
||||
picto.appendChild(createMessageBubble(msg));
|
||||
});
|
||||
} catch (e) {
|
||||
const err = document.createElement('div');
|
||||
err.style.setProperty('--col', '#90c9ff');
|
||||
const nameDiv = document.createElement('div');
|
||||
nameDiv.textContent = 'system';
|
||||
const msgDiv = document.createElement('div');
|
||||
msgDiv.textContent = 'No one has commented yet. Be the first!';
|
||||
err.appendChild(nameDiv);
|
||||
err.appendChild(document.createTextNode(''));
|
||||
err.appendChild(document.createElement('br'));
|
||||
err.appendChild(msgDiv);
|
||||
picto.appendChild(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function sendMessage() {
|
||||
const nameInput = document.getElementById('gb-name');
|
||||
const msgInput = document.getElementById('gb-message');
|
||||
const sendBtn = document.getElementById('gb-send');
|
||||
|
||||
const message = msgInput.value.trim();
|
||||
if (!message) return;
|
||||
|
||||
sendBtn.classList.add('gb-disabled');
|
||||
try {
|
||||
await fetch(GUESTBOOK_API + '/messages', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
name: nameInput.value.trim() || 'Anonymous',
|
||||
message: message,
|
||||
}),
|
||||
});
|
||||
msgInput.value = '';
|
||||
await loadMessages();
|
||||
setTimeout(replaceTextWithEmotes, 500);
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
sendBtn.classList.remove('gb-disabled');
|
||||
}
|
||||
|
||||
async function likeMessage(id, btn) {
|
||||
try {
|
||||
const res = await fetch(GUESTBOOK_API + `/messages/${id}/like`, { method: 'POST' });
|
||||
const data = await res.json();
|
||||
btn.innerHTML = `<span class="gb-like-icon">♥</span> ${data.likes}`;
|
||||
btn.classList.add('gb-liked');
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
function toggleEmoteSelector() {
|
||||
const els = document.getElementsByClassName('gb-toggle-div');
|
||||
for (let i = 0; i < els.length; i++) {
|
||||
if (els[i].style.display === 'none' || els[i].style.display === '') {
|
||||
els[i].style.display = 'block';
|
||||
} else {
|
||||
els[i].style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addText(text) {
|
||||
const textarea = document.getElementById('gb-message');
|
||||
textarea.value += text;
|
||||
textarea.focus();
|
||||
}
|
||||
|
||||
function replaceTextWithEmotes() {
|
||||
const picto = document.querySelector('#pictochat-content .picto');
|
||||
if (!picto) return;
|
||||
|
||||
function replaceTextNodes(element) {
|
||||
element.childNodes.forEach(function (node) {
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
let replacedText = node.textContent;
|
||||
let changed = false;
|
||||
for (const [key, src] of Object.entries(EMOTES)) {
|
||||
const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const imgTag = "<img src='" + src + "' title='" + key + "' class='gb-emote' />";
|
||||
const newText = replacedText.replace(new RegExp(escapedKey, 'g'), imgTag);
|
||||
if (newText !== replacedText) {
|
||||
replacedText = newText;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
const newElement = document.createElement('span');
|
||||
newElement.innerHTML = replacedText;
|
||||
node.parentNode.replaceChild(newElement, node);
|
||||
}
|
||||
} else {
|
||||
replaceTextNodes(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
replaceTextNodes(picto);
|
||||
}
|
||||
|
||||
(function () {
|
||||
const picto = document.querySelector('#pictochat-content .picto');
|
||||
if (!picto) return;
|
||||
|
||||
const sendBtn = document.getElementById('gb-send');
|
||||
if (sendBtn) sendBtn.addEventListener('click', sendMessage);
|
||||
|
||||
const msgInput = document.getElementById('gb-message');
|
||||
if (msgInput) {
|
||||
msgInput.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
sendMessage();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const emoteBtn = document.getElementById('gb-emote-toggle');
|
||||
if (emoteBtn) emoteBtn.addEventListener('click', toggleEmoteSelector);
|
||||
|
||||
document.addEventListener('click', function (e) {
|
||||
const selector = document.getElementById('gb-emote-selector');
|
||||
const toggle = document.getElementById('gb-emote-toggle');
|
||||
if (selector && selector.style.display === 'block' &&
|
||||
!selector.contains(e.target) && e.target !== toggle) {
|
||||
selector.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
const emoteItems = document.querySelectorAll('#gb-emote-selector .gb-emote-item');
|
||||
emoteItems.forEach(function (img) {
|
||||
img.addEventListener('click', function () {
|
||||
addText(img.title + ' ');
|
||||
});
|
||||
});
|
||||
|
||||
loadMessages().then(function () {
|
||||
replaceTextWithEmotes();
|
||||
});
|
||||
})();
|
||||
202
style.css
@@ -847,10 +847,10 @@ a:hover {
|
||||
border-radius: 18px;
|
||||
box-shadow: inset 0px 0px 5px 0px rgb(202, 202, 202), 0px -4px 5px -3px #bbbbbb, 0px 5px 1px -3px #ffffff;
|
||||
z-index: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#pictochat-content {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
@@ -936,8 +936,9 @@ a:hover {
|
||||
color: var(--col);
|
||||
}
|
||||
|
||||
#pictochat-content .picto > div > div:last-of-type {
|
||||
#pictochat-content .picto > div > .gb-msg-content {
|
||||
padding: 3px;
|
||||
padding-top: 20px;
|
||||
text-indent: 0px;
|
||||
line-height: 18px;
|
||||
}
|
||||
@@ -975,7 +976,7 @@ a:hover {
|
||||
calc(100% - 4px) calc(100% - 0px), calc(100% - 4px) calc(100% - 2px), calc(100% - 2px) calc(100% - 2px), calc(100% - 2px) calc(100% - 4px), calc(100% - 0px) calc(100% - 4px));
|
||||
}
|
||||
|
||||
#pictochat-content .picto > div img:first-of-type {
|
||||
#pictochat-content .picto > div img:first-of-type:not(.gb-emote) {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: auto;
|
||||
@@ -1301,3 +1302,198 @@ a:hover {
|
||||
letter-spacing: 4px;
|
||||
text-shadow: 0 0 3px #93bdec66;
|
||||
}
|
||||
|
||||
|
||||
#gb-name {
|
||||
position: absolute;
|
||||
top: 153px;
|
||||
left: 24px;
|
||||
height: 17px;
|
||||
width: 125px;
|
||||
font-size: 11px;
|
||||
font-family: 'DSWare';
|
||||
color: #30baf3;
|
||||
font-weight: 500;
|
||||
border: rgba(255, 0, 0, 0) solid 1px;
|
||||
background-color: rgba(255, 255, 255, 0);
|
||||
outline: none;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
#gb-name::placeholder {
|
||||
font-size: 10px;
|
||||
font-weight: normal;
|
||||
font-family: 'DSWare';
|
||||
color: #30baf3;
|
||||
}
|
||||
|
||||
#gb-name:focus {
|
||||
outline: none;
|
||||
border: 1px solid rgba(163, 197, 11, 0);
|
||||
}
|
||||
|
||||
#gb-message {
|
||||
position: absolute;
|
||||
top: 167px;
|
||||
left: 20px;
|
||||
width: 200px;
|
||||
height: 64px;
|
||||
resize: none;
|
||||
padding-top: 1px;
|
||||
line-height: 1.6;
|
||||
font-size: 10px;
|
||||
font-family: 'DSWare';
|
||||
color: #494949;
|
||||
border: rgba(0, 255, 21, 0) solid 1px;
|
||||
background-color: rgba(255, 255, 255, 0);
|
||||
outline: none;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
#gb-message:focus {
|
||||
outline: none;
|
||||
border: 1px solid rgba(163, 197, 11, 0);
|
||||
}
|
||||
|
||||
#gb-message::placeholder {
|
||||
font-size: 10px;
|
||||
font-family: 'DSWare';
|
||||
color: #b0bcc8;
|
||||
}
|
||||
|
||||
#gb-send {
|
||||
position: absolute !important;
|
||||
top: 244px;
|
||||
left: 217.5px;
|
||||
width: 32px !important;
|
||||
height: 32px !important;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
cursor: pointer;
|
||||
z-index: 100;
|
||||
image-rendering: pixelated;
|
||||
background: none;
|
||||
border: none;
|
||||
animation: gb-sendPulse 2s infinite ease;
|
||||
}
|
||||
|
||||
#gb-send:hover {
|
||||
content: url('images/sendbuttonhover.png');
|
||||
}
|
||||
|
||||
#gb-send.gb-disabled {
|
||||
opacity: 0.4;
|
||||
cursor: default;
|
||||
animation: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@keyframes gb-sendPulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
30% { opacity: 0.3; }
|
||||
}
|
||||
|
||||
#gb-emote-toggle {
|
||||
top: 275px;
|
||||
left: 217.5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#gb-emote-selector {
|
||||
display: none;
|
||||
position: relative;
|
||||
padding: 8px 6px;
|
||||
z-index: 100;
|
||||
top: -52px;
|
||||
left: 15px;
|
||||
width: 205px;
|
||||
box-sizing: border-box;
|
||||
image-rendering: pixelated;
|
||||
animation: gbFadeIn 0.3s;
|
||||
background: linear-gradient(90deg, #fafbfb 0%, #f7f7f7 50%, #fafbfb 100%);
|
||||
border: 1.5px solid #c9c9c9;
|
||||
border-radius: 18px;
|
||||
box-shadow: inset 0px 0px 5px 0px rgb(202, 202, 202), 0px 0px 5px 0px #dadada;
|
||||
}
|
||||
|
||||
#gb-emote-selector.gb-visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.gb-emote-item {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
cursor: pointer;
|
||||
image-rendering: pixelated;
|
||||
border-radius: 3px;
|
||||
padding: 2px;
|
||||
transition: background 0.1s;
|
||||
}
|
||||
|
||||
.gb-emote-item:hover {
|
||||
background: #e0f0ff;
|
||||
}
|
||||
|
||||
.gb-date {
|
||||
position: absolute !important;
|
||||
top: 2px !important;
|
||||
right: 10px !important;
|
||||
left: auto !important;
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
text-indent: 0 !important;
|
||||
font-size: 9px;
|
||||
color: #494949;
|
||||
font-family: 'DSWare';
|
||||
z-index: 1;
|
||||
background: none !important;
|
||||
}
|
||||
|
||||
.gb-date::before,
|
||||
.gb-date::after {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.gb-emote {
|
||||
display: inline;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
vertical-align: middle;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
.gb-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-top: 3px;
|
||||
padding-top: 0;
|
||||
padding-left: 2px;
|
||||
text-indent: 0;
|
||||
}
|
||||
|
||||
.gb-like-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-family: 'DSWare';
|
||||
font-size: 9px;
|
||||
color: #30baf3;
|
||||
cursor: pointer;
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
.gb-like-btn:hover {
|
||||
color: #0080d0;
|
||||
}
|
||||
|
||||
.gb-liked {
|
||||
color: #ff6b8a !important;
|
||||
}
|
||||
|
||||
.gb-like-icon {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
@keyframes gbFadeIn {
|
||||
from { opacity: 0; transform: translateY(4px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||