feat: add mention notification creation and user search endpoints across multiple routers
This commit is contained in:
@@ -1,65 +1,11 @@
|
||||
.admin-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 215px 1fr;
|
||||
gap: 0;
|
||||
min-height: calc(100vh - var(--nav-height) - 2rem);
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.admin-sidebar {
|
||||
background: var(--bg-secondary);
|
||||
border-right: 1px solid var(--border);
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.admin-sidebar-header {
|
||||
padding: 0 1rem 0.75rem;
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.admin-sidebar-nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.125rem;
|
||||
}
|
||||
|
||||
.admin-nav-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.admin-nav-link:hover {
|
||||
background: var(--bg-card);
|
||||
color: var(--text-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.admin-nav-link.active {
|
||||
background: var(--accent-light);
|
||||
color: var(--accent);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.admin-nav-icon {
|
||||
font-size: 1rem;
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
grid-template-columns: var(--sidebar-width) 1fr;
|
||||
gap: 1.5rem;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.admin-content {
|
||||
padding: 1.5rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,65 +5,12 @@
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.feed-sidebar {
|
||||
position: sticky;
|
||||
top: calc(var(--nav-height) + 1rem);
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.feed-sidebar h3 {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.feed-categories {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.125rem;
|
||||
}
|
||||
|
||||
.feed-category {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.625rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: var(--radius);
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.feed-category:hover {
|
||||
background: var(--bg-card-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.feed-category.active {
|
||||
background: var(--accent-light);
|
||||
color: var(--accent);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.feed-category .dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.feed-category[data-topic="devlog"] .dot { background: var(--topic-devlog); }
|
||||
.feed-category[data-topic="showcase"] .dot { background: var(--topic-showcase); }
|
||||
.feed-category[data-topic="question"] .dot { background: var(--topic-question); }
|
||||
.feed-category[data-topic="rant"] .dot { background: var(--topic-rant); }
|
||||
.feed-category[data-topic="fun"] .dot { background: var(--topic-fun); }
|
||||
.feed-category[data-topic="signals"] .dot { background: var(--topic-signals); }
|
||||
.sidebar-link[data-topic="devlog"] .sidebar-dot { background: var(--topic-devlog); }
|
||||
.sidebar-link[data-topic="showcase"] .sidebar-dot { background: var(--topic-showcase); }
|
||||
.sidebar-link[data-topic="question"] .sidebar-dot { background: var(--topic-question); }
|
||||
.sidebar-link[data-topic="rant"] .sidebar-dot { background: var(--topic-rant); }
|
||||
.sidebar-link[data-topic="fun"] .sidebar-dot { background: var(--topic-fun); }
|
||||
.sidebar-link[data-topic="signals"] .sidebar-dot { background: var(--topic-signals); }
|
||||
|
||||
.feed-nav {
|
||||
display: flex;
|
||||
@@ -373,7 +320,7 @@
|
||||
.feed-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.feed-sidebar, .feed-right {
|
||||
.feed-right {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,12 +18,43 @@
|
||||
.messages-list-header {
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.messages-list-header input {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.search-dropdown {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-top: none;
|
||||
border-radius: 0 0 var(--radius) var(--radius);
|
||||
z-index: 100;
|
||||
max-height: 240px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.search-dropdown-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.625rem;
|
||||
padding: 0.625rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-primary);
|
||||
transition: background 0.15s;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.search-dropdown-item:hover {
|
||||
background: var(--bg-card-hover);
|
||||
}
|
||||
|
||||
.messages-conversations {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
@@ -96,6 +127,7 @@
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 1rem;
|
||||
padding-bottom: 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
|
||||
@@ -5,24 +5,6 @@
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.projects-sidebar {
|
||||
position: sticky;
|
||||
top: calc(var(--nav-height) + 1rem);
|
||||
}
|
||||
|
||||
.projects-sidebar h3 {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.projects-search {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.projects-tabs {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
@@ -172,13 +154,16 @@
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@media (max-width: 1024px) {
|
||||
.projects-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.projects-sidebar {
|
||||
display: none;
|
||||
.projects-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.projects-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ class Application {
|
||||
this.initContentRenderer();
|
||||
this.initCommentReply();
|
||||
this.initEmojiPickers();
|
||||
this.initMentionInputs();
|
||||
}
|
||||
|
||||
loadCSS(href) {
|
||||
@@ -145,14 +146,60 @@ class Application {
|
||||
if (!searchInput) {
|
||||
return;
|
||||
}
|
||||
|
||||
const wrap = searchInput.parentElement;
|
||||
const dropdown = document.createElement("div");
|
||||
dropdown.className = "search-dropdown";
|
||||
wrap.appendChild(dropdown);
|
||||
|
||||
let debounceTimer = null;
|
||||
|
||||
searchInput.addEventListener("input", () => {
|
||||
clearTimeout(debounceTimer);
|
||||
const q = searchInput.value.trim();
|
||||
if (q.length < 1) {
|
||||
dropdown.innerHTML = "";
|
||||
dropdown.style.display = "none";
|
||||
return;
|
||||
}
|
||||
debounceTimer = setTimeout(async () => {
|
||||
try {
|
||||
const resp = await fetch(`/messages/search?q=${encodeURIComponent(q)}`);
|
||||
const data = await resp.json();
|
||||
const results = data.results || [];
|
||||
if (results.length === 0) {
|
||||
dropdown.style.display = "none";
|
||||
return;
|
||||
}
|
||||
dropdown.innerHTML = "";
|
||||
for (const r of results) {
|
||||
const item = document.createElement("a");
|
||||
item.className = "search-dropdown-item";
|
||||
item.href = `/messages?with_uid=${r.uid}`;
|
||||
item.innerHTML = `<img src="/avatar/multiavatar/${encodeURIComponent(r.username)}?size=24" class="avatar-img" style="width:24px;height:24px;border-radius:50%" alt="" loading="lazy"><span>${r.username}</span>`;
|
||||
dropdown.appendChild(item);
|
||||
}
|
||||
dropdown.style.display = "block";
|
||||
} catch (e) {
|
||||
// silently fail — no suggestions
|
||||
}
|
||||
}, 200);
|
||||
});
|
||||
|
||||
searchInput.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter") {
|
||||
const query = searchInput.value.trim();
|
||||
if (query) {
|
||||
window.location.href = `/messages?search=${encodeURIComponent(query)}`;
|
||||
const first = dropdown.querySelector(".search-dropdown-item");
|
||||
if (first) {
|
||||
window.location.href = first.href;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("click", (e) => {
|
||||
if (!wrap.contains(e.target)) {
|
||||
dropdown.style.display = "none";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
initNotificationDismiss() {
|
||||
@@ -223,6 +270,15 @@ class Application {
|
||||
});
|
||||
}
|
||||
|
||||
initMentionInputs() {
|
||||
if (typeof MentionInput === "undefined") return;
|
||||
document.querySelectorAll("[data-mention]").forEach((el) => {
|
||||
if (el.dataset.mentionInitialized) return;
|
||||
el.dataset.mentionInitialized = "true";
|
||||
new MentionInput(el);
|
||||
});
|
||||
}
|
||||
|
||||
initEmojiPickers() {
|
||||
document.querySelectorAll(".comment-form textarea, .emoji-picker-target").forEach((textarea) => {
|
||||
if (textarea.dataset.emojiInitialized) return;
|
||||
|
||||
@@ -68,17 +68,21 @@ class ContentRenderer {
|
||||
const parts = [];
|
||||
let lastIndex = 0;
|
||||
const urlRe = /(https?:\/\/[^\s<]+)/g;
|
||||
const mentionRe = /(?:^|[\s(])@([a-zA-Z0-9_-]+)/g;
|
||||
let match;
|
||||
let mentionMatch;
|
||||
|
||||
while ((match = urlRe.exec(text)) !== null) {
|
||||
if (match.index > lastIndex) {
|
||||
parts.push({ type: "text", value: text.substring(lastIndex, match.index) });
|
||||
const chunk = text.substring(lastIndex, match.index);
|
||||
this.splitMentions(chunk, parts);
|
||||
}
|
||||
parts.push({ type: "url", value: match[0], start: match.index, end: match.index + match[0].length });
|
||||
lastIndex = match.index + match[0].length;
|
||||
}
|
||||
if (lastIndex < text.length) {
|
||||
parts.push({ type: "text", value: text.substring(lastIndex) });
|
||||
const chunk = text.substring(lastIndex);
|
||||
this.splitMentions(chunk, parts);
|
||||
}
|
||||
|
||||
if (parts.length <= 1 && parts[0]?.type === "text") return;
|
||||
@@ -87,6 +91,12 @@ class ContentRenderer {
|
||||
for (const part of parts) {
|
||||
if (part.type === "text") {
|
||||
fragment.appendChild(document.createTextNode(part.value));
|
||||
} else if (part.type === "mention") {
|
||||
const a = document.createElement("a");
|
||||
a.href = `/profile/${part.username}`;
|
||||
a.className = "mention-link";
|
||||
a.textContent = `@${part.username}`;
|
||||
fragment.appendChild(a);
|
||||
} else {
|
||||
const el = this.urlToEmbed(part.value);
|
||||
if (el) {
|
||||
@@ -108,6 +118,26 @@ class ContentRenderer {
|
||||
return temp.innerHTML;
|
||||
}
|
||||
|
||||
splitMentions(text, parts) {
|
||||
const mentionRe = /(?:^|[\s(])@([a-zA-Z0-9_-]+)/g;
|
||||
let m;
|
||||
let last = 0;
|
||||
while ((m = mentionRe.exec(text)) !== null) {
|
||||
if (m.index > last) {
|
||||
parts.push({ type: "text", value: text.substring(last, m.index) });
|
||||
}
|
||||
const prefix = m[0][0];
|
||||
if (prefix && /\S/.test(prefix)) {
|
||||
parts.push({ type: "text", value: prefix });
|
||||
}
|
||||
parts.push({ type: "mention", username: m[1], value: m[0] });
|
||||
last = m.index + m[0].length;
|
||||
}
|
||||
if (last < text.length) {
|
||||
parts.push({ type: "text", value: text.substring(last) });
|
||||
}
|
||||
}
|
||||
|
||||
urlToEmbed(url) {
|
||||
const ytMatch = url.match(this.youtubeRe);
|
||||
if (ytMatch) {
|
||||
|
||||
Reference in New Issue
Block a user