Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions src/public/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,47 @@ textarea {
scrollbar-color: var(--cambridge-blue) transparent;
}

.status-bar {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
justify-content: space-between;
align-items: center;
padding: 0.5rem 1rem;
margin: 0.5rem auto 0;
width: 100%;
max-width: 1200px;
font-size: 0.9rem;
border-top: 1px solid var(--cambridge-blue);
}

body.dark-mode .status-bar {
border-top: 1px solid var(--eggshell);
}

.status-group {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
align-items: center;
}

.status-item {
display: flex;
align-items: center;
gap: 0.3rem;
color: inherit;
}

.status-item i {
font-size: 0.9rem;
color: inherit;
}

.status-text {
white-space: nowrap;
}

body.light-mode textarea {
background-color: white;
color: var(--delft-blue);
Expand Down Expand Up @@ -442,6 +483,26 @@ footer {
padding: 1rem;
font-size: 0.95rem;
}
.status-bar {
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
padding: 0.75rem 0;
border-top: 1px solid var(--cambridge-blue);
}

body.dark-mode .status-bar {
border-top: 1px solid var(--eggshell);
}

.status-group {
width: 100%;
justify-content: space-between;
}

.status-item {
font-size: 0.85rem;
}

footer {
padding: 0.75rem;
Expand Down
147 changes: 130 additions & 17 deletions src/views/index.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,32 @@
</button>
</div>

<div class="status-bar" role="status" aria-live="polite">
<div class="status-group">
<div class="status-item">
<i class="fas fa-i-cursor" aria-hidden="true"></i>
<span id="wordCount" class="status-text">Words: 0</span>
</div>
<div class="status-item">
<i class="fas fa-sort-alpha-down" aria-hidden="true"></i>
<span id="charCount" class="status-text">Characters: 0</span>
</div>
</div>
<div class="status-group">
<div class="status-item">
<i class="fas fa-sync" aria-hidden="true"></i>
<span id="saveStatus" class="status-text">Last saved: --</span>
</div>
<div class="status-item">
<i class="fas fa-language" aria-hidden="true"></i>
<span id="languageStatus" class="status-text">Language: --</span>
</div>
<div class="status-item">
<i class="fas fa-universal-access" aria-hidden="true"></i>
<span id="accessibilityStatus" class="status-text">Accessibility: Ready</span>
</div>
</div>
</div>

<footer>
<div class="footer-content">
Expand Down Expand Up @@ -147,15 +173,101 @@

const url = window.location.pathname.split('/').pop();
let typingTimeout;
let saveStatusTimeout;
let lastSavedAt = null;

const textarea = document.getElementById('notepad');
const wordCountElement = document.getElementById('wordCount');
const charCountElement = document.getElementById('charCount');
const saveStatusElement = document.getElementById('saveStatus');
const languageStatusElement = document.getElementById('languageStatus');
const accessibilityStatusElement = document.getElementById('accessibilityStatus');

function formatCount(label, count) {
return `${label}: ${count}`;
}

function markSaving() {
saveStatusElement.textContent = 'Saving…';
}

function markSaved() {
lastSavedAt = new Date();
const timeStamp = lastSavedAt.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
saveStatusElement.textContent = `Last saved: ${timeStamp}`;
}

function detectLanguage(text) {
const sample = text.trim();
if (!sample) {
return 'Language: --';
}

const checks = [
{ regex: /[\u0400-\u04FF]/, label: 'Language: Cyrillic script' },
{ regex: /[\u0600-\u06FF]/, label: 'Language: Arabic script' },
{ regex: /[\u0900-\u097F]/, label: 'Language: Indic script' },
{ regex: /[\u4E00-\u9FFF]/, label: 'Language: Chinese characters' },
{ regex: /[\u3040-\u30FF]/, label: 'Language: Japanese script' },
{ regex: /[A-Za-z]/, label: 'Language: Latin script' }
];

const match = checks.find(check => check.regex.test(sample));
return match ? match.label : 'Language: Unknown';
}

function describeAccessibility(text) {
const trimmed = text.trim();
if (!trimmed) {
return 'Accessibility: Ready';
}

const sentences = trimmed.split(/[.!?]+/).filter(Boolean);
const words = trimmed.split(/\s+/).filter(Boolean);
const averageLength = sentences.length ? Math.round((words.length / sentences.length) * 10) / 10 : words.length;

if (averageLength > 25) {
return 'Accessibility: Consider shorter sentences';
}

if (averageLength < 12) {
return 'Accessibility: Easy to read';
}

return 'Accessibility: Looks good';
}

function updateStatusPanels() {
const text = textarea.value;
const words = text.trim() ? text.trim().split(/\s+/).filter(Boolean).length : 0;
const characters = text.length;

wordCountElement.textContent = formatCount('Words', words);
charCountElement.textContent = formatCount('Characters', characters);
languageStatusElement.textContent = detectLanguage(text);
accessibilityStatusElement.textContent = describeAccessibility(text);
}

updateStatusPanels();
if (textarea.value.trim()) {
markSaved();
}

let lastContent = textarea.value;

socket.emit('joinNote', url);

socket.on('loadNote', (note) => {
document.getElementById('notepad').value = note.content;
const content = note?.content ?? '';
textarea.value = content;
lastContent = content;
updateStatusPanels();
if (content.trim()) {
markSaved();
}
});

socket.on('noteUpdated', (content) => {
const textarea = document.getElementById('notepad');
const cursorPosition = textarea.selectionStart;

if (textarea.value !== content) {
Expand All @@ -164,24 +276,16 @@

textarea.selectionStart = cursorPosition;
textarea.selectionEnd = cursorPosition;
updateStatusPanels();
markSaved();
});

socket.on('userCount', (count) => {
const userCountElement = document.getElementById('userCount');
const statusDot = document.querySelector('.status-dot');
const userIcon = document.querySelector('.status-indicator i');

userCountElement.textContent = count;

if (count > 1) {
statusDot.style.backgroundColor = '#fefae0';
userIcon.style.color = '#fefae0';
statusDot.style.animationDuration = '1s';
} else {
statusDot.style.backgroundColor = '#fefae0';
userIcon.style.color = '#fefae0';
statusDot.style.animationDuration = '2s';
}
statusDot.style.animationDuration = count > 1 ? '1s' : '2s';
});

function debounce(func, delay) {
Expand All @@ -191,14 +295,23 @@
};
}

let lastContent = '';
textarea.addEventListener('input', () => {
updateStatusPanels();
markSaving();
});

document.getElementById('notepad').addEventListener('input', debounce(() => {
const content = document.getElementById('notepad').value;
textarea.addEventListener('input', debounce(() => {
const content = textarea.value;

if (content !== lastContent) {
lastContent = content;
socket.emit('updateNote', { url, content });
if (saveStatusTimeout) {
clearTimeout(saveStatusTimeout);
}
saveStatusTimeout = setTimeout(() => {
markSaved();
}, 600);
}
}, 100));

Expand Down Expand Up @@ -246,7 +359,7 @@
window.onload = function() {
const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
document.getElementById('notepad').focus();
textarea.focus();
document.body.classList.remove('light-mode', 'dark-mode');
document.body.classList.add(savedTheme);
const icon = document.querySelector('#themeToggle i');
Expand Down