py-trade / src /app /community /community.component.ts
Oviya
update community page
6875201
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<number, string> = {};
private replyOpen = new Set<number>();
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<string | null> {
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');
}
}
}