This commit is contained in:
parent
4e26ad740e
commit
51832664c4
@ -969,3 +969,20 @@ img {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
}
|
||||
|
||||
.card-link-host {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.card-link {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.card-link-host a:not(.card-link),
|
||||
.card-link-host button,
|
||||
.card-link-host form {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
@ -6,7 +6,6 @@ export class DomUtils {
|
||||
this.initShareButtons();
|
||||
this.initTogglers();
|
||||
this.initStopPropagation();
|
||||
this.initCardLinks();
|
||||
}
|
||||
|
||||
initClipboardCopy() {
|
||||
@ -54,14 +53,4 @@ export class DomUtils {
|
||||
el.addEventListener("click", (e) => e.stopPropagation());
|
||||
});
|
||||
}
|
||||
|
||||
initCardLinks() {
|
||||
document.querySelectorAll("[data-href]").forEach((el) => {
|
||||
el.addEventListener("click", (e) => {
|
||||
if (e.target.closest("[data-stop-propagation]")) return;
|
||||
const href = el.dataset.href;
|
||||
if (href) window.location.href = href;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,36 +1,8 @@
|
||||
import { Http } from "./Http.js";
|
||||
|
||||
export class NotificationManager {
|
||||
constructor() {
|
||||
this.initCardNavigation();
|
||||
this.initDismiss();
|
||||
this.initHashScroll();
|
||||
}
|
||||
|
||||
initCardNavigation() {
|
||||
document.querySelectorAll(".notification-card[data-href]").forEach((card) => {
|
||||
card.addEventListener("click", (e) => {
|
||||
if (e.target.closest("a, button")) {
|
||||
return;
|
||||
}
|
||||
window.location.href = card.dataset.href;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
initDismiss() {
|
||||
document.querySelectorAll(".notification-dismiss").forEach((btn) => {
|
||||
btn.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
const uid = btn.dataset.uid;
|
||||
if (!uid) {
|
||||
return;
|
||||
}
|
||||
Http.postForm(`/notifications/mark-read/${uid}`);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
initHashScroll() {
|
||||
const hash = window.location.hash;
|
||||
if (!hash.startsWith("#comment-")) {
|
||||
|
||||
3
devplacepy/static/vendor/purify.min.js
vendored
Normal file
3
devplacepy/static/vendor/purify.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
devplacepy/templates/_card_link.html
Normal file
1
devplacepy/templates/_card_link.html
Normal file
@ -0,0 +1 @@
|
||||
<a class="card-link" href="{{ _href }}" aria-label="{{ _label | default('', true) }}"></a>
|
||||
@ -29,10 +29,10 @@
|
||||
{% include "_attachment_display.html" %}
|
||||
{% endif %}
|
||||
<div class="comment-actions">
|
||||
<button class="comment-action-btn" data-action="reply"><span class="icon">💬</span>Reply</button>
|
||||
<button class="comment-action-btn" data-action="reply"><span class="icon">💬</span> Reply</button>
|
||||
{% if user and item.comment['user_uid'] == user['uid'] %}
|
||||
<form method="POST" action="/comments/delete/{{ item.comment['uid'] }}" class="inline-form">
|
||||
<button type="submit" class="comment-action-btn"><span class="icon">🗑️</span>Delete</button>
|
||||
<button type="submit" class="comment-action-btn"><span class="icon">🗑️</span> Delete</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
@ -67,7 +67,7 @@
|
||||
data-max-size="{{ max_upload_size_mb() }}"
|
||||
data-max-files="{{ max_attachments_per_resource() }}"
|
||||
data-allowed-types="{{ allowed_file_types() }}"></div>
|
||||
<button type="submit" class="comment-form-submit"><span class="icon">📤</span>Post</button>
|
||||
<button type="submit" class="comment-form-submit"><span class="icon">📤</span> Post</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
@ -52,13 +52,13 @@
|
||||
{% if u['uid'] != user['uid'] %}
|
||||
<form method="POST" action="/admin/users/{{ u['uid'] }}/toggle" class="admin-inline-form">
|
||||
<button type="submit" class="admin-btn admin-btn-sm">
|
||||
<span class="icon">{% if u.get('is_active', True) %}⚡{% else %}🔒{% endif %}</span>{% if u.get('is_active', True) %}Disable{% else %}Enable{% endif %}
|
||||
<span class="icon">{% if u.get('is_active', True) %}⚡{% else %}🔒{% endif %}</span> {% if u.get('is_active', True) %}Disable{% else %}Enable{% endif %}
|
||||
</button>
|
||||
</form>
|
||||
<button type="button" class="admin-btn admin-btn-sm" data-toggle="pw-{{ u['uid'] }}"><span class="icon">🔑</span>Password</button>
|
||||
<button type="button" class="admin-btn admin-btn-sm" data-toggle="pw-{{ u['uid'] }}"><span class="icon">🔑</span> Password</button>
|
||||
<form id="pw-{{ u['uid'] }}" method="POST" action="/admin/users/{{ u['uid'] }}/password" class="admin-pw-form hidden">
|
||||
<input type="password" name="password" placeholder="New password" minlength="6" class="admin-input-sm">
|
||||
<button type="submit" class="admin-btn admin-btn-sm"><span class="icon">💾</span>Set</button>
|
||||
<button type="submit" class="admin-btn admin-btn-sm"><span class="icon">💾</span> Set</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<span class="admin-text-muted">You</span>
|
||||
|
||||
@ -85,11 +85,11 @@
|
||||
</div>
|
||||
</a>
|
||||
<div class="dropdown-menu">
|
||||
<a href="/auth/logout" class="dropdown-item"><span class="icon">🚪</span>Logout</a>
|
||||
<a href="/auth/logout" class="dropdown-item"><span class="icon">🚪</span> Logout</a>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<a href="/auth/login" class="topnav-link"><span class="icon">🔑</span>Login</a>
|
||||
<a href="/auth/login" class="topnav-link"><span class="icon">🔑</span> Login</a>
|
||||
<a href="/auth/signup" class="btn btn-primary btn-sm"><span class="icon">✨</span>Sign Up</a>
|
||||
{% endif %}
|
||||
<button class="topnav-hamburger" id="hamburger-btn" aria-label="Toggle menu">☰</button>
|
||||
|
||||
@ -30,7 +30,7 @@
|
||||
<label for="email">Email address</label>
|
||||
<input type="email" id="email" name="email" required maxlength="255" placeholder="you@example.com">
|
||||
</div>
|
||||
<button type="submit" class="auth-submit"><span class="icon">📧</span>Send Reset Link</button>
|
||||
<button type="submit" class="auth-submit"><span class="icon">📧</span> Send Reset Link</button>
|
||||
</form>
|
||||
|
||||
<div class="auth-footer">
|
||||
|
||||
@ -49,7 +49,10 @@
|
||||
|
||||
<div class="gists-grid">
|
||||
{% for item in gists %}
|
||||
<div class="gist-card fade-in" data-href="/gists/{{ item.gist['slug'] or item.gist['uid'] }}">
|
||||
<div class="gist-card fade-in card-link-host">
|
||||
{% set _href = "/gists/" ~ (item.gist['slug'] or item.gist['uid']) %}
|
||||
{% set _label = item.gist['title'] %}
|
||||
{% include "_card_link.html" %}
|
||||
<div class="gist-card-header">
|
||||
<h3 class="gist-card-title">{{ item.gist['title'] }}</h3>
|
||||
<form method="POST" action="/votes/gist/{{ item.gist['uid'] }}" class="inline-form" data-stop-propagation>
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
<section class="landing-hero">
|
||||
<h1>Devplace.net — The Developer <span>Social Network</span></h1>
|
||||
<p>Track industry shifts. Discover bold releases. Share what you're building in an open, uncensored environment.</p>
|
||||
<a href="/auth/signup" class="landing-cta"><span class="icon">✨</span>Join DevPlace Free</a>
|
||||
<a href="/auth/signup" class="landing-cta"><span class="icon">✨</span> Join DevPlace Free</a>
|
||||
|
||||
<div class="landing-features">
|
||||
<div class="landing-feature">
|
||||
@ -117,11 +117,11 @@
|
||||
<footer class="landing-footer">
|
||||
<p>© DevPlace — The Developer Social Network</p>
|
||||
<div class="landing-footer-links">
|
||||
<a href="/feed"><span class="icon">📝</span>Posts</a>
|
||||
<a href="/news"><span class="icon">📰</span>News</a>
|
||||
<a href="/projects"><span class="icon">🚀</span>Projects</a>
|
||||
<a href="/auth/login"><span class="icon">🔑</span>Login</a>
|
||||
<a href="/auth/signup"><span class="icon">✨</span>Sign Up</a>
|
||||
<a href="/feed"><span class="icon">📝</span> Posts</a>
|
||||
<a href="/news"><span class="icon">📰</span> News</a>
|
||||
<a href="/projects"><span class="icon">🚀</span> Projects</a>
|
||||
<a href="/auth/login"><span class="icon">🔑</span> Login</a>
|
||||
<a href="/auth/signup"><span class="icon">✨</span> Sign Up</a>
|
||||
<a href="/bugs"><span class="icon">🐛</span> Bug Report</a>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@ -40,7 +40,7 @@
|
||||
<a href="/auth/forgot-password">Forgot your password?</a>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="auth-submit"><span class="icon">🔑</span>Sign in</button>
|
||||
<button type="submit" class="auth-submit"><span class="icon">🔑</span> Sign in</button>
|
||||
</form>
|
||||
|
||||
<div class="auth-footer">
|
||||
|
||||
@ -17,8 +17,11 @@
|
||||
<div class="notification-group">
|
||||
<div class="notification-group-label">{{ group.label }}</div>
|
||||
{% for item in group.entries %}
|
||||
<div class="notification-card {% if not item.notification['read'] %}unread{% endif %}" data-href="/notifications/open/{{ item.notification['uid'] }}">
|
||||
<div class="notification-card card-link-host {% if not item.notification['read'] %}unread{% endif %}">
|
||||
{% set actor_username = item.actor['username'] if item.actor else '#' %}
|
||||
{% set _href = "/notifications/open/" ~ item.notification['uid'] %}
|
||||
{% set _label = item.notification['message'] %}
|
||||
{% include "_card_link.html" %}
|
||||
<a href="/profile/{{ actor_username }}" style="flex-shrink:0">
|
||||
<img src="{{ avatar_url('multiavatar', actor_username, 32) }}" class="avatar-img avatar-sm" alt="{{ actor_username }}" loading="lazy">
|
||||
</a>
|
||||
@ -27,7 +30,7 @@
|
||||
<div class="notification-time">{{ item.time_ago }}</div>
|
||||
</div>
|
||||
<form method="POST" action="/notifications/mark-read/{{ item.notification['uid'] }}" style="display:inline;">
|
||||
<button type="submit" class="notification-dismiss" data-uid="{{ item.notification['uid'] }}">×</button>
|
||||
<button type="submit" class="notification-dismiss">×</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
@ -145,10 +145,10 @@
|
||||
<a href="/feed" class="back-link">← Back</a>
|
||||
|
||||
<div class="profile-tabs">
|
||||
<a href="/profile/{{ profile_user['username'] }}?tab=posts" class="profile-tab {% if current_tab == 'posts' %}active{% endif %}"><span class="icon">📝</span>Posts</a>
|
||||
<a href="/profile/{{ profile_user['username'] }}?tab=projects" class="profile-tab {% if current_tab == 'projects' %}active{% endif %}"><span class="icon">🚀</span>Projects</a>
|
||||
<a href="/profile/{{ profile_user['username'] }}?tab=gists" class="profile-tab {% if current_tab == 'gists' %}active{% endif %}"><span class="icon">📝</span>Gists</a>
|
||||
<a href="/profile/{{ profile_user['username'] }}?tab=activity" class="profile-tab {% if current_tab == 'activity' %}active{% endif %}"><span class="icon">📊</span>Activity</a>
|
||||
<a href="/profile/{{ profile_user['username'] }}?tab=posts" class="profile-tab {% if current_tab == 'posts' %}active{% endif %}"><span class="icon">📝</span> Posts</a>
|
||||
<a href="/profile/{{ profile_user['username'] }}?tab=projects" class="profile-tab {% if current_tab == 'projects' %}active{% endif %}"><span class="icon">🚀</span> Projects</a>
|
||||
<a href="/profile/{{ profile_user['username'] }}?tab=gists" class="profile-tab {% if current_tab == 'gists' %}active{% endif %}"><span class="icon">📝</span> Gists</a>
|
||||
<a href="/profile/{{ profile_user['username'] }}?tab=activity" class="profile-tab {% if current_tab == 'activity' %}active{% endif %}"><span class="icon">📊</span> Activity</a>
|
||||
<button class="btn-ghost btn-icon profile-tab-btn">△</button>
|
||||
</div>
|
||||
|
||||
|
||||
@ -141,7 +141,7 @@
|
||||
{% endif %}
|
||||
{% if is_owner %}
|
||||
<form method="POST" action="/projects/delete/{{ project['slug'] or project['uid'] }}" style="display:inline;">
|
||||
<button type="submit" class="project-star-btn" data-confirm="Delete this project?"><span class="icon">🗑️</span>Delete</button>
|
||||
<button type="submit" class="project-star-btn" data-confirm="Delete this project?"><span class="icon">🗑️</span> Delete</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@ -53,7 +53,10 @@
|
||||
|
||||
<div class="projects-grid">
|
||||
{% for project in projects %}
|
||||
<div class="project-card fade-in" data-href="/projects/{{ project['slug'] or project['uid'] }}">
|
||||
<div class="project-card fade-in card-link-host">
|
||||
{% set _href = "/projects/" ~ (project['slug'] or project['uid']) %}
|
||||
{% set _label = project['title'] %}
|
||||
{% include "_card_link.html" %}
|
||||
<div class="project-card-header">
|
||||
<h3 class="project-card-title">{{ project['title'] }}</h3>
|
||||
<form method="POST" action="/votes/project/{{ project['uid'] }}" class="inline-form">
|
||||
|
||||
@ -34,7 +34,7 @@
|
||||
<button type="button" class="auth-toggle-pw" aria-label="Toggle password visibility">👁</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="auth-submit"><span class="icon">🔒</span>Reset Password</button>
|
||||
<button type="submit" class="auth-submit"><span class="icon">🔒</span> Reset Password</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -47,7 +47,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="auth-submit"><span class="icon">✨</span>Create account</button>
|
||||
<button type="submit" class="auth-submit"><span class="icon">✨</span> Create account</button>
|
||||
</form>
|
||||
|
||||
<div class="auth-footer">
|
||||
|
||||
@ -391,12 +391,12 @@ def test_comment_notification_click_opens_comment(app_server, browser, seeded_db
|
||||
pb.wait_for_timeout(1500)
|
||||
|
||||
pa.goto(f"{BASE_URL}/notifications", wait_until="domcontentloaded")
|
||||
card = pa.locator(".notification-card[data-href]").filter(has_text="commented on your post").first
|
||||
card = pa.locator(".notification-card").filter(has_text="commented on your post").first
|
||||
card.wait_for(state="visible", timeout=10000)
|
||||
href = card.get_attribute("data-href")
|
||||
assert href.startswith("/notifications/open/"), f"unexpected data-href: {href}"
|
||||
href = card.locator("a.card-link").get_attribute("href")
|
||||
assert href.startswith("/notifications/open/"), f"unexpected href: {href}"
|
||||
|
||||
card.locator(".notification-text").click()
|
||||
card.locator("a.card-link").click()
|
||||
pa.wait_for_url("**/posts/**", timeout=10000, wait_until="domcontentloaded")
|
||||
assert "#comment-" in pa.url, f"click did not deep-link to a comment: {pa.url}"
|
||||
|
||||
@ -406,7 +406,7 @@ def test_comment_notification_click_opens_comment(app_server, browser, seeded_db
|
||||
|
||||
pa.goto(f"{BASE_URL}/notifications", wait_until="domcontentloaded")
|
||||
pa.wait_for_timeout(1000)
|
||||
target = pa.locator(f'.notification-card[data-href="{href}"]')
|
||||
target = pa.locator(f'.notification-card:has(a.card-link[href="{href}"])')
|
||||
target.wait_for(state="visible", timeout=10000)
|
||||
assert "unread" not in (target.get_attribute("class") or ""), "opening a notification should mark it read"
|
||||
|
||||
@ -434,9 +434,9 @@ def test_follow_notification_click_opens_profile(app_server, browser, seeded_db)
|
||||
pb.wait_for_timeout(1000)
|
||||
|
||||
pa.goto(f"{BASE_URL}/notifications", wait_until="domcontentloaded")
|
||||
card = pa.locator(".notification-card[data-href]").filter(has_text="started following you").first
|
||||
card = pa.locator(".notification-card").filter(has_text="started following you").first
|
||||
card.wait_for(state="visible", timeout=10000)
|
||||
card.locator(".notification-text").click()
|
||||
card.locator("a.card-link").click()
|
||||
pa.wait_for_url("**/profile/bob_test", timeout=10000, wait_until="domcontentloaded")
|
||||
assert pa.url.endswith("/profile/bob_test"), f"follow notification should open the follower profile: {pa.url}"
|
||||
|
||||
@ -465,9 +465,9 @@ def test_message_notification_click_opens_conversation(app_server, browser, seed
|
||||
pb.wait_for_timeout(1500)
|
||||
|
||||
pa.goto(f"{BASE_URL}/notifications", wait_until="domcontentloaded")
|
||||
card = pa.locator(".notification-card[data-href]").filter(has_text="sent you a message").first
|
||||
card = pa.locator(".notification-card").filter(has_text="sent you a message").first
|
||||
card.wait_for(state="visible", timeout=10000)
|
||||
card.locator(".notification-text").click()
|
||||
card.locator("a.card-link").click()
|
||||
pa.wait_for_url("**/messages**", timeout=10000, wait_until="domcontentloaded")
|
||||
assert "with_uid=" in pa.url, f"message notification should open the conversation: {pa.url}"
|
||||
|
||||
@ -503,9 +503,9 @@ def test_vote_notification_click_opens_target(app_server, browser, seeded_db):
|
||||
pa.wait_for_timeout(1500)
|
||||
|
||||
pb.goto(f"{BASE_URL}/notifications", wait_until="domcontentloaded")
|
||||
card = pb.locator(".notification-card[data-href]").filter(has_text="++'d").first
|
||||
card = pb.locator(".notification-card").filter(has_text="++'d").first
|
||||
card.wait_for(state="visible", timeout=10000)
|
||||
card.locator(".notification-text").click()
|
||||
card.locator("a.card-link").click()
|
||||
pb.wait_for_url(f"**{post_path}", timeout=10000, wait_until="domcontentloaded")
|
||||
assert post_path in pb.url, f"vote notification should open the voted post: {pb.url}"
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user