WIP PWA for Grain
at main 187 lines 4.7 kB view raw
1import { LitElement, html, css } from 'lit'; 2import { router } from '../../router.js'; 3import '../atoms/grain-avatar.js'; 4import '../atoms/grain-rich-text.js'; 5 6export class GrainComment extends LitElement { 7 static properties = { 8 uri: { type: String }, 9 handle: { type: String }, 10 displayName: { type: String }, 11 avatarUrl: { type: String }, 12 text: { type: String }, 13 facets: { type: Array }, 14 createdAt: { type: String }, 15 isReply: { type: Boolean }, 16 isOwner: { type: Boolean }, 17 focusImageUrl: { type: String }, 18 focusImageAlt: { type: String } 19 }; 20 21 static styles = css` 22 :host { 23 display: block; 24 padding: var(--space-xs) 0; 25 } 26 :host([is-reply]) { 27 padding-left: 40px; 28 } 29 .comment { 30 display: flex; 31 gap: var(--space-sm); 32 cursor: pointer; 33 } 34 .content { 35 flex: 1; 36 min-width: 0; 37 } 38 .text-line { 39 font-size: var(--font-size-sm); 40 color: var(--color-text-primary); 41 line-height: 1.4; 42 } 43 .handle { 44 font-weight: var(--font-weight-semibold); 45 cursor: pointer; 46 } 47 .handle:hover { 48 text-decoration: underline; 49 } 50 .text { 51 margin-left: var(--space-xs); 52 word-break: break-word; 53 } 54 .meta { 55 display: flex; 56 gap: var(--space-sm); 57 margin-top: var(--space-xxs); 58 } 59 .time { 60 font-size: var(--font-size-xs); 61 color: var(--color-text-secondary); 62 } 63 .reply-btn { 64 font-size: var(--font-size-xs); 65 color: var(--color-text-secondary); 66 background: none; 67 border: none; 68 padding: 0; 69 cursor: pointer; 70 font-family: inherit; 71 font-weight: var(--font-weight-semibold); 72 } 73 .reply-btn:hover, 74 .delete-btn:hover { 75 color: var(--color-text-primary); 76 } 77 .delete-btn { 78 font-size: var(--font-size-xs); 79 color: var(--color-text-secondary); 80 background: none; 81 border: none; 82 padding: 0; 83 cursor: pointer; 84 font-family: inherit; 85 font-weight: var(--font-weight-semibold); 86 } 87 .delete-btn:hover { 88 color: var(--color-error, #e53935); 89 } 90 .focus-image { 91 width: 40px; 92 height: 40px; 93 border-radius: 4px; 94 object-fit: cover; 95 flex-shrink: 0; 96 } 97 `; 98 99 constructor() { 100 super(); 101 this.uri = ''; 102 this.handle = ''; 103 this.displayName = ''; 104 this.avatarUrl = ''; 105 this.text = ''; 106 this.facets = []; 107 this.createdAt = ''; 108 this.isReply = false; 109 this.isOwner = false; 110 this.focusImageUrl = ''; 111 this.focusImageAlt = ''; 112 } 113 114 #handleProfileClick(e) { 115 e.stopPropagation(); 116 router.push(`/profile/${this.handle}`); 117 } 118 119 #handleReplyClick(e) { 120 e.stopPropagation(); 121 this.dispatchEvent(new CustomEvent('reply', { 122 detail: { uri: this.uri, handle: this.handle }, 123 bubbles: true, 124 composed: true 125 })); 126 } 127 128 #handleDeleteClick(e) { 129 e.stopPropagation(); 130 this.dispatchEvent(new CustomEvent('delete', { 131 detail: { uri: this.uri }, 132 bubbles: true, 133 composed: true 134 })); 135 } 136 137 #formatTime(iso) { 138 const date = new Date(iso); 139 const now = new Date(); 140 const diffMs = now - date; 141 const diffMins = Math.floor(diffMs / 60000); 142 const diffHours = Math.floor(diffMs / 3600000); 143 const diffDays = Math.floor(diffMs / 86400000); 144 145 if (diffMins < 1) return 'now'; 146 if (diffMins < 60) return `${diffMins}m`; 147 if (diffHours < 24) return `${diffHours}h`; 148 if (diffDays < 7) return `${diffDays}d`; 149 return `${Math.floor(diffDays / 7)}w`; 150 } 151 152 render() { 153 return html` 154 <div class="comment"> 155 <grain-avatar 156 src=${this.avatarUrl} 157 size="sm" 158 @click=${this.#handleProfileClick} 159 ></grain-avatar> 160 <div class="content"> 161 <div class="text-line"> 162 <span class="handle" @click=${this.#handleProfileClick}> 163 ${this.handle} 164 </span> 165 <span class="text"><grain-rich-text .text=${this.text} .facets=${this.facets}></grain-rich-text></span> 166 </div> 167 <div class="meta"> 168 <span class="time">${this.#formatTime(this.createdAt)}</span> 169 <button class="reply-btn" @click=${this.#handleReplyClick}>Reply</button> 170 ${this.isOwner ? html` 171 <button class="delete-btn" @click=${this.#handleDeleteClick}>Delete</button> 172 ` : ''} 173 </div> 174 </div> 175 ${this.focusImageUrl ? html` 176 <img 177 class="focus-image" 178 src=${this.focusImageUrl} 179 alt=${this.focusImageAlt} 180 /> 181 ` : ''} 182 </div> 183 `; 184 } 185} 186 187customElements.define('grain-comment', GrainComment);