forked from
grain.social/grain-pwa
WIP PWA for Grain
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);