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
21 changes: 21 additions & 0 deletions migrations/0004_add_user_profiles.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
-- Migration 0004: Add user profile fields and avatars table

ALTER TABLE users ADD COLUMN bio TEXT;
ALTER TABLE users ADD COLUMN avatar_url TEXT;
ALTER TABLE users ADD COLUMN is_teacher INTEGER NOT NULL DEFAULT 0;
ALTER TABLE users ADD COLUMN github_username TEXT;
ALTER TABLE users ADD COLUMN discord_username TEXT;
ALTER TABLE users ADD COLUMN slack_username TEXT;
ALTER TABLE users ADD COLUMN expertise TEXT;
ALTER TABLE users ADD COLUMN is_profile_public INTEGER NOT NULL DEFAULT 0;

CREATE TABLE IF NOT EXISTS avatars (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
avatar_url TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);

CREATE INDEX IF NOT EXISTS idx_avatars_user ON avatars(user_id);
CREATE INDEX IF NOT EXISTS idx_users_public ON users(is_profile_public);
118 changes: 88 additions & 30 deletions public/js/layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,17 +53,18 @@ function initializeDarkMode() {
}

// ── Layout injection ──────────────────────────────────────────────────
async function inject(id, file, callback) {
const _navbarPromise = fetch('/partials/navbar.html');
const _footerPromise = fetch('/partials/footer.html');

async function inject(id, fetchPromise, callback) {
const el = document.getElementById(id);
if (!el) return;
try {
const res = await fetch(file);
const res = await fetchPromise;
el.innerHTML = await res.text();
if (callback && typeof callback === 'function') {
callback();
}
} catch (err) {
console.error(`Failed to load ${file}:`, err);
if (typeof callback === 'function') callback();
} catch (e) {
console.error('Layout inject error:', e);
}
}

Expand All @@ -76,6 +77,48 @@ window.toggleProfileDropdown = function () {
};

// ── Update auth section with profile ─────────────────────────────────
// ── Avatar helpers ───────────────────────────────────────────────────
function _setNavAvatar(initialsId, imgId, name, username, avatarUrl) {
const initials = (name || username || '?').split(' ').map(w => w[0]).slice(0, 2).join('').toUpperCase();
const initialsEl = document.getElementById(initialsId);
const imgEl = document.getElementById(imgId);
if (!initialsEl) return;
if (avatarUrl) {
initialsEl.classList.add('hidden');
if (imgEl) {
imgEl.src = avatarUrl;
imgEl.classList.remove('hidden');
imgEl.onerror = function () {
this.classList.add('hidden');
initialsEl.classList.remove('hidden');
};
}
} else {
initialsEl.textContent = initials;
initialsEl.classList.remove('hidden');
if (imgEl) imgEl.classList.add('hidden');
}
}

// Keep the old profile-avatar element (button circle) in sync too
function _setNavButtonAvatar(avatarElId, name, username, avatarUrl) {
const el = document.getElementById(avatarElId);
if (!el) return;
const initials = (name || username || '?').split(' ').map(w => w[0]).slice(0, 2).join('').toUpperCase();
if (avatarUrl) {
// Build element programmatically — avoids XSS via inline onerror with user-derived initials
const img = document.createElement('img');
img.src = avatarUrl;
img.className = 'w-full h-full object-cover rounded-full';
img.alt = '';
img.addEventListener('error', () => { el.textContent = initials; });
el.innerHTML = '';
el.appendChild(img);
} else {
el.textContent = initials;
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

function updateAuthSection() {
const { token, user } = getAuth();
const notLoggedInDiv = document.getElementById('auth-not-logged-in');
Expand All @@ -84,39 +127,52 @@ function updateAuthSection() {
const mobileLoggedIn = document.getElementById('mobile-auth-logged-in');

if (token && user) {
// User is logged in
const firstName = user.name ? user.name.split(' ')[0] : user.username;
const firstLetter = firstName.charAt(0).toUpperCase();
const firstName = user.name ? user.name.split(' ')[0] : user.username;
const avatarUrl = user.avatar_url || '';
const role = user.role || '';

// Desktop view
// Desktop
if (notLoggedInDiv) notLoggedInDiv.classList.add('hidden');
if (loggedInDiv) loggedInDiv.classList.remove('hidden');

const avatarEl = document.getElementById('profile-avatar');
const greetingEl = document.getElementById('profile-greeting');
const usernameEl = document.getElementById('dropdown-username');

if (avatarEl) avatarEl.textContent = firstLetter;
if (loggedInDiv) loggedInDiv.classList.remove('hidden');

// Trigger button circle (existing id="profile-avatar")
_setNavButtonAvatar('profile-avatar', user.name, user.username, avatarUrl);

const greetingEl = document.getElementById('profile-greeting');
const usernameEl = document.getElementById('dropdown-username');
const roleEl = document.getElementById('dropdown-user-role');

if (greetingEl) greetingEl.textContent = `Hi, ${firstName}`;
if (usernameEl) usernameEl.textContent = user.username;
if (usernameEl) usernameEl.textContent = `@${user.username}`;
if (roleEl) roleEl.textContent = role;

// Mobile view
// Dropdown avatar (new elements)
_setNavAvatar('dropdown-avatar-initials', 'dropdown-avatar-img', user.name, user.username, avatarUrl);

// "My Public Profile" deep link
const profileLink = document.getElementById('dropdown-view-profile-link');
if (profileLink) profileLink.href = `/public-profile.html?username=${encodeURIComponent(user.username)}`;

// Mobile
if (mobileNotLoggedIn) mobileNotLoggedIn.classList.add('hidden');
if (mobileLoggedIn) mobileLoggedIn.classList.remove('hidden');

const mobileAvatarEl = document.getElementById('mobile-profile-avatar');
if (mobileLoggedIn) mobileLoggedIn.classList.remove('hidden');

_setNavAvatar('mobile-avatar-initials', 'mobile-avatar-img', user.name, user.username, avatarUrl);

const mobileGreetingEl = document.getElementById('mobile-profile-greeting');

if (mobileAvatarEl) mobileAvatarEl.textContent = firstLetter;
const mobileRoleEl = document.getElementById('mobile-user-role');
if (mobileGreetingEl) mobileGreetingEl.textContent = `Hi, ${firstName}`;
if (mobileRoleEl) mobileRoleEl.textContent = role;

const mobileProfileLink = document.getElementById('mobile-view-profile-link');
if (mobileProfileLink) mobileProfileLink.href = `/public-profile.html?username=${encodeURIComponent(user.username)}`;

startUnreadPolling();
} else {
// User is not logged in
if (notLoggedInDiv) notLoggedInDiv.classList.remove('hidden');
if (loggedInDiv) loggedInDiv.classList.add('hidden');
if (loggedInDiv) loggedInDiv.classList.add('hidden');
if (mobileNotLoggedIn) mobileNotLoggedIn.classList.remove('hidden');
if (mobileLoggedIn) mobileLoggedIn.classList.add('hidden');
if (mobileLoggedIn) mobileLoggedIn.classList.add('hidden');

const badge = document.getElementById('notif-unread-badge');
if (badge) badge.classList.add('hidden');
Expand Down Expand Up @@ -253,8 +309,10 @@ async function initLayout() {
if (!layoutInitPromise) {
layoutInitPromise = (async () => {
initializeDarkMode();
await inject('site-navbar', '/partials/navbar.html', updateAuthSection);
await inject('site-footer', '/partials/footer.html');
await Promise.all([
inject('site-navbar', _navbarPromise, updateAuthSection),
inject('site-footer', _footerPromise),
]);
updateDarkModeIcon();
})();
}
Expand Down
Loading
Loading