import { Component, ViewEncapsulation, OnInit } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { AuthService } from '../auth/auth.service'; import { firstValueFrom } from 'rxjs'; type Post = { id: number; userId: number; userName: string; title?: string; category?: string; tags?: string; body: string; createdAt: string; likeCount?: number; dislikeCount?: number; commentCount?: number; }; @Component({ selector: 'app-community', standalone: true, imports: [CommonModule, FormsModule, ReactiveFormsModule], templateUrl: './community.component.html', styleUrls: ['./community.component.scss'], encapsulation: ViewEncapsulation.None }) export class CommunityComponent implements OnInit { showModal = false; // modal bound fields title = ''; category = 'General Discussion'; tags = ''; tickers = ''; body = ''; posts: Post[] = []; loading = false; loadError = ''; // reply state per post replyDraft: Record = {}; private replyOpen = new Set(); private readonly baseUrl = location.hostname.endsWith('hf.space') ? 'https://pykara-pytrade-backend.hf.space' : 'http://127.0.0.1:5000'; constructor(private auth: AuthService) {} ngOnInit(): void { this.loadPosts(); } async loadPosts() { this.loading = true; this.loadError = ''; try { const res = await fetch(`${this.baseUrl}/posts?limit=50&offset=0`, { method: 'GET', credentials: 'omit', }); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(err.error || err.message || `Failed (${res.status})`); } const data = await res.json(); const list = (data?.results as Post[]) ?? []; // Ensure counts exist for UI this.posts = list.map(p => ({ likeCount: 0, dislikeCount: 0, commentCount: 0, ...p })); } catch (e: any) { console.error('Load posts failed:', e); this.loadError = e?.message || 'Failed to load posts'; } finally { this.loading = false; } } splitTags(tags?: string): string[] { return (tags ?? '') .split(',') .map(t => t.trim()) .filter(t => t.length > 0); } toggleModal(show: boolean) { this.showModal = show; document.body.style.overflow = show ? 'hidden' : ''; } openThread(id: any) { const view = document.getElementById('threadView'); if (view) view.scrollIntoView({ behavior: 'smooth', block: 'start' }); } backToList() { const list = document.getElementById('threadList'); if (list) list.scrollIntoView({ behavior: 'smooth', block: 'start' }); } private async ensureAccessToken(): Promise { let token = this.auth.getAccessToken(); if (!token) return null; if (this.auth.tokenExpired()) { try { token = await firstValueFrom(this.auth.refreshAccessToken()); } catch { return null; } } return token || null; } private parseJwt(token: string): any | null { try { const base64 = token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/'); const json = decodeURIComponent(atob(base64).split('').map(c => { return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); }).join('')); return JSON.parse(json); } catch { return null; } } private getUserContextFromToken(token: string): { userId?: number; userName?: string } { const payload = this.parseJwt(token); if (!payload) return {}; const userId = payload.sub ? Number(payload.sub) : undefined; const userName = payload.name || payload.preferred_username || [payload.given_name, payload.family_name].filter(Boolean).join(' ') || undefined; return { userId, userName }; } isReplyOpen(postId: number): boolean { return this.replyOpen.has(postId); } toggleReply(postId: number) { if (this.replyOpen.has(postId)) { this.replyOpen.delete(postId); } else { this.replyOpen.add(postId); } } async likePost(p: Post) { // optimistic UI p.likeCount = (p.likeCount ?? 0) + 1; const token = await this.ensureAccessToken(); if (!token) return; // if not signed-in, keep optimistic or revert if you prefer try { await fetch(`${this.baseUrl}/posts/${p.id}/like`, { method: 'POST', credentials: 'omit', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` } }); } catch { // optionally revert UI on failure p.likeCount = Math.max(0, (p.likeCount ?? 1) - 1); } } async dislikePost(p: Post) { p.dislikeCount = (p.dislikeCount ?? 0) + 1; const token = await this.ensureAccessToken(); if (!token) return; try { await fetch(`${this.baseUrl}/posts/${p.id}/dislike`, { method: 'POST', credentials: 'omit', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` } }); } catch { p.dislikeCount = Math.max(0, (p.dislikeCount ?? 1) - 1); } } async submitReply(postId: number) { const draft = (this.replyDraft[postId] || '').trim(); if (!draft) return; const token = await this.ensureAccessToken(); if (!token) { alert('Please sign in to reply.'); return; } try { const res = await fetch(`${this.baseUrl}/posts/${postId}/comments`, { method: 'POST', credentials: 'omit', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ body: draft }) }); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(err.error || err.message || `Failed (${res.status})`); } // success: clear draft and bump counter delete this.replyDraft[postId]; const post = this.posts.find(x => x.id === postId); if (post) post.commentCount = (post.commentCount ?? 0) + 1; this.replyOpen.delete(postId); } catch (e: any) { console.error('Reply failed', e); alert(e?.message || 'Failed to post reply'); } } async publish() { if (!this.body.trim()) { alert('Post body is required'); return; } const token = await this.ensureAccessToken(); if (!token) { alert('Please sign in to publish.'); return; } const { userId, userName } = this.getUserContextFromToken(token); try { const res = await fetch(`${this.baseUrl}/posts`, { method: 'POST', credentials: 'omit', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ userId, // optional; backend may ignore and derive from JWT/DB userName, // optional; backend may ignore and derive from DB title: this.title, category: this.category, tags: this.tags, body: this.body }) }); if (res.status === 401) { alert('Session expired. Please sign in again.'); return; } if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(err.error || err.message || `Failed (${res.status})`); } // Clear and reload this.title = ''; this.category = 'General Discussion'; this.tags = ''; this.tickers = ''; this.body = ''; this.toggleModal(false); await this.loadPosts(); } catch (e: any) { console.error('Publish error:', e); alert(e?.message || 'Failed to publish'); } } }