Browse Source

adjust profile page

fix missing reply buttons
master
Silberengel 1 month ago
parent
commit
8ddf905555
  1. 4
      public/healthz.json
  2. 37
      src/lib/components/EventMenu.svelte
  3. 190
      src/lib/components/profile/ProfileMenu.svelte
  4. 25
      src/lib/components/ui/IconButton.svelte
  5. 56
      src/lib/modules/comments/Comment.svelte
  6. 26
      src/lib/modules/feed/FeedPost.svelte
  7. 102
      src/lib/modules/profiles/ProfilePage.svelte
  8. 2
      src/routes/profile/[pubkey]/+page.svelte

4
public/healthz.json

@ -2,7 +2,7 @@
"status": "ok", "status": "ok",
"service": "aitherboard", "service": "aitherboard",
"version": "0.2.0", "version": "0.2.0",
"buildTime": "2026-02-06T23:46:07.275Z", "buildTime": "2026-02-07T00:27:17.672Z",
"gitCommit": "unknown", "gitCommit": "unknown",
"timestamp": 1770421567275 "timestamp": 1770424037672
} }

37
src/lib/components/EventMenu.svelte

@ -97,10 +97,11 @@
const buttonRect = menuButtonElement.getBoundingClientRect(); const buttonRect = menuButtonElement.getBoundingClientRect();
const viewportWidth = window.innerWidth; const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight; const viewportHeight = window.innerHeight;
const padding = 8; // Padding from viewport edges const isMobile = viewportWidth < 640;
const padding = isMobile ? 4 : 8; // Smaller padding on mobile
// Get dropdown dimensions (estimate or use actual if available) // Get dropdown dimensions (estimate or use actual if available)
let dropdownWidth = 200; // min-width from CSS let dropdownWidth = isMobile ? 180 : 200; // min-width from CSS
let dropdownHeight = 300; // Estimate, will be updated let dropdownHeight = 300; // Estimate, will be updated
// Position below button by default, aligned to right edge // Position below button by default, aligned to right edge
@ -155,6 +156,11 @@
adjustedRight = padding; adjustedRight = padding;
} }
// On mobile, ensure menu doesn't go off left edge
if (isMobile && adjustedLeft < padding) {
adjustedRight = Math.max(padding, viewportWidth - dropdownWidth - padding);
}
menuPosition = { top: adjustedTop, right: adjustedRight }; menuPosition = { top: adjustedTop, right: adjustedRight };
}); });
@ -422,7 +428,7 @@
{#if isLoggedIn && onReply} {#if isLoggedIn && onReply}
<div class="menu-divider"></div> <div class="menu-divider"></div>
<button class="menu-item" onclick={() => { onReply(); closeMenu(); }}> <button class="menu-item menu-item-reply" onclick={() => { onReply(); closeMenu(); }}>
<Icon name="message-square" size={16} /> <Icon name="message-square" size={16} />
<span>Reply</span> <span>Reply</span>
</button> </button>
@ -561,6 +567,31 @@
overflow-x: hidden; overflow-x: hidden;
} }
.menu-item-reply {
display: flex !important;
visibility: visible !important;
}
@media (max-width: 640px) {
.menu-dropdown {
min-width: 180px;
max-width: calc(100vw - 8px);
font-size: 0.875rem;
}
.menu-item {
padding: 0.625rem 0.75rem;
font-size: 0.875rem;
min-height: 2.5rem;
}
.menu-button {
padding: 0.375rem 0.5rem;
min-width: 2.5rem;
min-height: 2.5rem;
}
}
:global(.dark) .menu-dropdown { :global(.dark) .menu-dropdown {
background: var(--fog-dark-post, #1f2937); background: var(--fog-dark-post, #1f2937);
border-color: var(--fog-dark-border, #374151); border-color: var(--fog-dark-border, #374151);

190
src/lib/components/profile/ProfileMenu.svelte

@ -25,6 +25,7 @@
let profileEvent = $state<NostrEvent | null>(null); let profileEvent = $state<NostrEvent | null>(null);
let muting = $state(false); let muting = $state(false);
let following = $state(false); let following = $state(false);
let showJsonModal = $state(false);
let isLoggedIn = $derived(sessionManager.isLoggedIn()); let isLoggedIn = $derived(sessionManager.isLoggedIn());
let currentUserPubkey = $derived(sessionManager.getCurrentPubkey()); let currentUserPubkey = $derived(sessionManager.getCurrentPubkey());
@ -243,6 +244,18 @@
} }
} }
async function viewJson() {
if (!profileEvent) {
await loadProfileEvent();
}
showJsonModal = true;
closeMenu();
}
function closeJsonModal() {
showJsonModal = false;
}
function shareWithAitherboard() { function shareWithAitherboard() {
const url = `${window.location.origin}/profile/${pubkey}`; const url = `${window.location.origin}/profile/${pubkey}`;
navigator.clipboard.writeText(url).then(() => { navigator.clipboard.writeText(url).then(() => {
@ -331,6 +344,11 @@
{/if} {/if}
</button> </button>
<button class="menu-item" onclick={viewJson} role="menuitem" disabled={!profileEvent}>
<span class="menu-item-icon">📄</span>
<span class="menu-item-text">View Json</span>
</button>
<div class="menu-divider"></div> <div class="menu-divider"></div>
<button class="menu-item" onclick={shareWithAitherboard} role="menuitem"> <button class="menu-item" onclick={shareWithAitherboard} role="menuitem">
@ -379,6 +397,38 @@
</div> </div>
{/if} {/if}
{#if showJsonModal}
<div
class="json-modal-overlay"
role="dialog"
aria-modal="true"
aria-labelledby="json-modal-title"
>
<button
class="json-modal-backdrop"
onclick={closeJsonModal}
onkeydown={(e) => e.key === 'Escape' && closeJsonModal()}
aria-label="Close modal"
></button>
<div
class="json-modal-content"
role="document"
>
<div class="json-modal-header">
<h3 id="json-modal-title">Profile Event JSON</h3>
<button class="json-modal-close" onclick={closeJsonModal} aria-label="Close">×</button>
</div>
<div class="json-modal-body">
{#if profileEvent}
<pre class="json-content">{JSON.stringify(profileEvent, null, 2)}</pre>
{:else}
<p class="text-fog-text-light dark:text-fog-dark-text-light">Loading profile event...</p>
{/if}
</div>
</div>
</div>
{/if}
</div> </div>
<style> <style>
@ -513,4 +563,144 @@
:global(.dark) .menu-divider { :global(.dark) .menu-divider {
background: var(--fog-dark-border, #374151); background: var(--fog-dark-border, #374151);
} }
.json-modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 2000;
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
}
.json-modal-backdrop {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
border: none;
padding: 0;
cursor: pointer;
}
:global(.dark) .json-modal-backdrop {
background: rgba(0, 0, 0, 0.7);
}
.json-modal-content {
position: relative;
z-index: 1;
background: var(--fog-post, #ffffff);
border: 1px solid var(--fog-border, #e5e7eb);
border-radius: 0.5rem;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
max-width: 90vw;
max-height: 90vh;
width: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
}
:global(.dark) .json-modal-content {
background: var(--fog-dark-post, #1f2937);
border-color: var(--fog-dark-border, #374151);
}
.json-modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem;
border-bottom: 1px solid var(--fog-border, #e5e7eb);
}
:global(.dark) .json-modal-header {
border-bottom-color: var(--fog-dark-border, #374151);
}
.json-modal-header h3 {
margin: 0;
font-size: 1.125rem;
font-weight: 600;
color: var(--fog-text, #1f2937);
}
:global(.dark) .json-modal-header h3 {
color: var(--fog-dark-text, #f9fafb);
}
.json-modal-close {
background: none;
border: none;
font-size: 1.5rem;
line-height: 1;
cursor: pointer;
color: var(--fog-text, #1f2937);
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
transition: background 0.2s;
}
:global(.dark) .json-modal-close {
color: var(--fog-dark-text, #f9fafb);
}
.json-modal-close:hover {
background: var(--fog-highlight, #f3f4f6);
}
:global(.dark) .json-modal-close:hover {
background: var(--fog-dark-highlight, #374151);
}
.json-modal-body {
padding: 1rem;
overflow: auto;
flex: 1;
}
.json-content {
margin: 0;
font-family: 'Courier New', monospace;
font-size: 0.875rem;
line-height: 1.5;
color: var(--fog-text, #1f2937);
white-space: pre-wrap;
word-break: break-word;
overflow-wrap: break-word;
}
:global(.dark) .json-content {
color: var(--fog-dark-text, #f9fafb);
}
@media (max-width: 640px) {
.json-modal-content {
max-width: 95vw;
max-height: 95vh;
}
.json-modal-header {
padding: 0.75rem;
}
.json-modal-header h3 {
font-size: 1rem;
}
.json-modal-body {
padding: 0.75rem;
}
.json-content {
font-size: 0.75rem;
}
}
</style> </style>

25
src/lib/components/ui/IconButton.svelte

@ -32,11 +32,12 @@
title={label} title={label}
> >
<Icon name={icon} {size} /> <Icon name={icon} {size} />
<span class="icon-button-label">{label}</span>
</button> </button>
<style> <style>
.icon-button { .icon-button {
display: inline-flex; display: inline-flex !important;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 0.25rem 0.75rem; padding: 0.25rem 0.75rem;
@ -49,6 +50,10 @@
font-size: 0.875rem; font-size: 0.875rem;
line-height: 1.5; line-height: 1.5;
gap: 0.375rem; gap: 0.375rem;
visibility: visible !important;
opacity: 1 !important;
min-width: 2rem;
min-height: 2rem;
} }
:global(.dark) .icon-button { :global(.dark) .icon-button {
@ -87,4 +92,22 @@
outline: 2px solid var(--fog-accent, #64748b); outline: 2px solid var(--fog-accent, #64748b);
outline-offset: 2px; outline-offset: 2px;
} }
.icon-button-label {
display: none;
}
/* Show label on mobile for better visibility */
@media (max-width: 640px) {
.icon-button {
padding: 0.375rem 0.5rem;
font-size: 0.75rem;
min-height: 2rem;
}
.icon-button-label {
display: inline;
margin-left: 0.25rem;
}
}
</style> </style>

56
src/lib/modules/comments/Comment.svelte

@ -122,7 +122,7 @@
{#if getClientName()} {#if getClientName()}
<span class="text-fog-text-light dark:text-fog-dark-text-light whitespace-nowrap" style="font-size: 0.75em;">via {getClientName()}</span> <span class="text-fog-text-light dark:text-fog-dark-text-light whitespace-nowrap" style="font-size: 0.75em;">via {getClientName()}</span>
{/if} {/if}
<div class="ml-auto flex items-center gap-2"> <div class="ml-auto flex items-center gap-2 comment-header-actions">
<IconButton <IconButton
icon="eye" icon="eye"
label="View event" label="View event"
@ -169,8 +169,7 @@
{/if} {/if}
<button <button
onclick={handleReply} onclick={handleReply}
class="text-fog-accent dark:text-fog-dark-accent hover:underline" class="reply-button text-fog-accent dark:text-fog-dark-accent hover:underline"
style="font-size: 0.75em;"
> >
Reply Reply
</button> </button>
@ -232,6 +231,57 @@
border-top-color: var(--fog-dark-border, #374151); border-top-color: var(--fog-dark-border, #374151);
} }
.reply-button {
font-size: 0.875rem;
padding: 0.25rem 0.5rem;
background: none;
border: none;
cursor: pointer;
transition: opacity 0.2s;
min-height: 2rem;
display: inline-flex;
align-items: center;
}
.reply-button:hover {
opacity: 0.8;
}
.comment-header-actions {
display: flex !important;
visibility: visible !important;
flex-shrink: 0;
}
@media (max-width: 640px) {
.comment-actions {
padding-right: 4rem;
flex-wrap: wrap;
gap: 0.5rem;
}
.reply-button {
font-size: 0.875rem;
padding: 0.375rem 0.75rem;
min-height: 2.25rem;
font-weight: 500;
}
.comment-header {
flex-wrap: wrap;
gap: 0.5rem;
}
.comment-header .ml-auto {
margin-left: auto;
flex-shrink: 0;
}
.comment-header-actions {
gap: 0.375rem;
}
}
.card-content { .card-content {
max-height: 500px; max-height: 500px;
overflow: hidden; overflow: hidden;

26
src/lib/modules/feed/FeedPost.svelte

@ -1231,6 +1231,32 @@
.post-header-actions { .post-header-actions {
flex-shrink: 0; flex-shrink: 0;
display: flex !important;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
visibility: visible !important;
}
.feed-card-actions {
display: flex !important;
visibility: visible !important;
}
@media (max-width: 640px) {
.post-header-actions {
gap: 0.375rem;
}
.feed-card-actions {
gap: 0.375rem;
flex-wrap: wrap;
}
.post-header {
flex-wrap: wrap;
gap: 0.5rem;
}
} }

102
src/lib/modules/profiles/ProfilePage.svelte

@ -722,10 +722,10 @@
<img <img
src={profile.picture} src={profile.picture}
alt={profile.name || 'Profile picture'} alt={profile.name || 'Profile picture'}
class="profile-picture w-24 h-24 rounded-full mb-4" class="profile-picture rounded-full mb-4"
/> />
{/if} {/if}
<h1 class="font-bold mb-2" style="font-size: 2em;">{profile.name || 'Anonymous'}</h1> <h1 class="profile-name font-bold mb-2">{profile.name || 'Anonymous'}</h1>
{#if profile.about} {#if profile.about}
<p class="text-fog-text dark:text-fog-dark-text mb-2">{profile.about}</p> <p class="text-fog-text dark:text-fog-dark-text mb-2">{profile.about}</p>
{/if} {/if}
@ -795,10 +795,10 @@
</div> </div>
<div class="profile-posts"> <div class="profile-posts">
<div class="tabs mb-4 flex gap-4 border-b border-fog-border dark:border-fog-dark-border"> <div class="tabs mb-4 flex gap-2 sm:gap-4 border-b border-fog-border dark:border-fog-dark-border overflow-x-auto scrollbar-hide">
<button <button
onclick={() => activeTab = 'pins'} onclick={() => activeTab = 'pins'}
class="px-4 py-2 font-semibold {activeTab === 'pins' ? 'border-b-2 border-fog-accent dark:border-fog-dark-accent' : ''}" class="px-2 sm:px-4 py-2 font-semibold whitespace-nowrap flex-shrink-0 {activeTab === 'pins' ? 'border-b-2 border-fog-accent dark:border-fog-dark-accent' : ''}"
> >
Pins ({pins.length}) Pins ({pins.length})
</button> </button>
@ -810,28 +810,28 @@
await loadWallComments(profileEvent.id); await loadWallComments(profileEvent.id);
} }
}} }}
class="px-4 py-2 font-semibold {activeTab === 'wall' ? 'border-b-2 border-fog-accent dark:border-fog-dark-accent' : ''}" class="px-2 sm:px-4 py-2 font-semibold whitespace-nowrap flex-shrink-0 {activeTab === 'wall' ? 'border-b-2 border-fog-accent dark:border-fog-dark-accent' : ''}"
> >
Wall ({wallComments.length}) Wall ({wallComments.length})
</button> </button>
{#if isOwnProfile} {#if isOwnProfile}
<button <button
onclick={() => activeTab = 'notifications'} onclick={() => activeTab = 'notifications'}
class="px-4 py-2 font-semibold {activeTab === 'notifications' ? 'border-b-2 border-fog-accent dark:border-fog-dark-accent' : ''}" class="px-2 sm:px-4 py-2 font-semibold whitespace-nowrap flex-shrink-0 {activeTab === 'notifications' ? 'border-b-2 border-fog-accent dark:border-fog-dark-accent' : ''}"
> >
Notifications ({notifications.length}) Notifications ({notifications.length})
</button> </button>
{:else if currentUserPubkey && currentUserPubkey !== profilePubkey} {:else if currentUserPubkey && currentUserPubkey !== profilePubkey}
<button <button
onclick={() => activeTab = 'interactions'} onclick={() => activeTab = 'interactions'}
class="px-4 py-2 font-semibold {activeTab === 'interactions' ? 'border-b-2 border-fog-accent dark:border-fog-dark-accent' : ''}" class="px-2 sm:px-4 py-2 font-semibold whitespace-nowrap flex-shrink-0 {activeTab === 'interactions' ? 'border-b-2 border-fog-accent dark:border-fog-dark-accent' : ''}"
> >
Interactions with me ({interactionsWithMe.length}) Interactions ({interactionsWithMe.length})
</button> </button>
{/if} {/if}
<button <button
onclick={() => activeTab = 'bookmarks'} onclick={() => activeTab = 'bookmarks'}
class="px-4 py-2 font-semibold {activeTab === 'bookmarks' ? 'border-b-2 border-fog-accent dark:border-fog-dark-accent' : ''}" class="px-2 sm:px-4 py-2 font-semibold whitespace-nowrap flex-shrink-0 {activeTab === 'bookmarks' ? 'border-b-2 border-fog-accent dark:border-fog-dark-accent' : ''}"
> >
Bookmarks ({bookmarks.length}) Bookmarks ({bookmarks.length})
</button> </button>
@ -932,11 +932,26 @@
.profile-page { .profile-page {
max-width: var(--content-width); max-width: var(--content-width);
margin: 0 auto; margin: 0 auto;
padding: 1rem; padding: 0.75rem;
}
@media (min-width: 640px) {
.profile-page {
padding: 1rem;
}
} }
.profile-picture { .profile-picture {
object-fit: cover; object-fit: cover;
width: 4rem;
height: 4rem;
}
@media (min-width: 640px) {
.profile-picture {
width: 6rem;
height: 6rem;
}
} }
.profile-header { .profile-header {
@ -949,6 +964,17 @@
word-break: break-word; word-break: break-word;
} }
.profile-name {
font-size: 1.5rem;
line-height: 1.2;
}
@media (min-width: 640px) {
.profile-name {
font-size: 2rem;
}
}
.profile-header p { .profile-header p {
overflow-wrap: break-word; overflow-wrap: break-word;
word-break: break-word; word-break: break-word;
@ -1092,4 +1118,60 @@
flex-direction: column; flex-direction: column;
gap: 1rem; gap: 1rem;
} }
/* Hide scrollbar for tabs on mobile */
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
/* Responsive tabs */
.tabs {
scrollbar-width: thin;
scrollbar-color: var(--fog-border, #e5e7eb) transparent;
}
:global(.dark) .tabs {
scrollbar-color: var(--fog-dark-border, #374151) transparent;
}
@media (max-width: 640px) {
.tabs {
-webkit-overflow-scrolling: touch;
}
.tabs button {
font-size: 0.875rem;
}
}
/* Responsive profile header */
@media (max-width: 640px) {
.profile-header {
margin-bottom: 1rem;
}
.profile-header p {
font-size: 0.875rem;
}
}
/* Responsive npub display */
@media (max-width: 640px) {
.npub-display {
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
}
.npub-text {
font-size: 0.75rem;
word-break: break-all;
max-width: 100%;
}
}
</style> </style>

2
src/routes/profile/[pubkey]/+page.svelte

@ -11,6 +11,6 @@
<Header /> <Header />
<main class="container mx-auto px-4 py-8"> <main class="container mx-auto px-2 sm:px-4 py-4 sm:py-8">
<ProfilePage /> <ProfilePage />
</main> </main>

Loading…
Cancel
Save