<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Rant Community</title>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
:root {
|
|
--primary: #d55161;
|
|
--primary-dark: #c44154;
|
|
--secondary: #7bc8a4;
|
|
--background: #0a0a0a;
|
|
--surface: #1a1a1a;
|
|
--surface-light: #2a2a2a;
|
|
--text: #e0e0e0;
|
|
--text-dim: #a0a0a0;
|
|
--success: #4caf50;
|
|
--error: #f44336;
|
|
--warning: #ff9800;
|
|
}
|
|
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
background: var(--background);
|
|
color: var(--text);
|
|
line-height: 1.6;
|
|
min-height: 100vh;
|
|
}
|
|
|
|
/* Navigation */
|
|
nav {
|
|
background: var(--surface);
|
|
padding: 1rem 0;
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 1000;
|
|
box-shadow: 0 2px 10px rgba(0,0,0,0.5);
|
|
}
|
|
|
|
.nav-container {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
padding: 0 2rem;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.logo {
|
|
font-size: 1.5rem;
|
|
font-weight: bold;
|
|
color: var(--primary);
|
|
text-decoration: none;
|
|
}
|
|
|
|
.nav-links {
|
|
display: flex;
|
|
gap: 2rem;
|
|
align-items: center;
|
|
}
|
|
|
|
.nav-links a {
|
|
color: var(--text);
|
|
text-decoration: none;
|
|
transition: color 0.3s;
|
|
}
|
|
|
|
.nav-links a:hover {
|
|
color: var(--primary);
|
|
}
|
|
|
|
.btn {
|
|
background: var(--primary);
|
|
color: white;
|
|
border: none;
|
|
padding: 0.5rem 1.5rem;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 1rem;
|
|
transition: all 0.3s;
|
|
text-decoration: none;
|
|
display: inline-block;
|
|
}
|
|
|
|
.btn:hover {
|
|
background: var(--primary-dark);
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
.btn-secondary {
|
|
background: var(--surface-light);
|
|
}
|
|
|
|
.btn-secondary:hover {
|
|
background: #3a3a3a;
|
|
}
|
|
|
|
/* Container */
|
|
.container {
|
|
max-width: 800px;
|
|
margin: 2rem auto;
|
|
padding: 0 2rem;
|
|
}
|
|
|
|
/* Rant Card */
|
|
.rant-card {
|
|
background: var(--surface);
|
|
border-radius: 8px;
|
|
padding: 1.5rem;
|
|
margin-bottom: 1rem;
|
|
transition: transform 0.2s;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.rant-card:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
|
|
}
|
|
|
|
.rant-header {
|
|
display: flex;
|
|
align-items: center;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.avatar {
|
|
width: 40px;
|
|
height: 40px;
|
|
border-radius: 50%;
|
|
margin-right: 1rem;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-weight: bold;
|
|
color: white;
|
|
}
|
|
|
|
.user-info {
|
|
flex: 1;
|
|
}
|
|
|
|
.username {
|
|
font-weight: bold;
|
|
color: var(--primary);
|
|
}
|
|
|
|
.score {
|
|
color: var(--text-dim);
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.rant-content {
|
|
margin-bottom: 1rem;
|
|
white-space: pre-wrap;
|
|
word-wrap: break-word;
|
|
}
|
|
|
|
.rant-image {
|
|
max-width: 100%;
|
|
border-radius: 4px;
|
|
margin: 1rem 0;
|
|
}
|
|
|
|
.rant-footer {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
color: var(--text-dim);
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.rant-actions {
|
|
display: flex;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.action-btn {
|
|
background: none;
|
|
border: none;
|
|
color: var(--text-dim);
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.3rem;
|
|
transition: color 0.3s;
|
|
padding: 0.3rem 0.6rem;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.action-btn:hover {
|
|
color: var(--primary);
|
|
background: rgba(213, 81, 97, 0.1);
|
|
}
|
|
|
|
.action-btn.voted {
|
|
color: var(--primary);
|
|
}
|
|
|
|
.action-btn.downvoted {
|
|
color: var(--error);
|
|
}
|
|
|
|
/* Tags */
|
|
.tags {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
flex-wrap: wrap;
|
|
margin: 0.5rem 0;
|
|
}
|
|
|
|
.tag {
|
|
background: var(--surface-light);
|
|
padding: 0.2rem 0.8rem;
|
|
border-radius: 20px;
|
|
font-size: 0.85rem;
|
|
color: var(--secondary);
|
|
}
|
|
|
|
/* Forms */
|
|
.form-group {
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
label {
|
|
display: block;
|
|
margin-bottom: 0.5rem;
|
|
color: var(--text-dim);
|
|
}
|
|
|
|
input, textarea, select {
|
|
width: 100%;
|
|
padding: 0.75rem;
|
|
background: var(--surface-light);
|
|
border: 1px solid transparent;
|
|
border-radius: 4px;
|
|
color: var(--text);
|
|
font-size: 1rem;
|
|
transition: border-color 0.3s;
|
|
}
|
|
|
|
input:focus, textarea:focus, select:focus {
|
|
outline: none;
|
|
border-color: var(--primary);
|
|
}
|
|
|
|
textarea {
|
|
resize: vertical;
|
|
min-height: 120px;
|
|
}
|
|
|
|
/* Modal */
|
|
.modal {
|
|
display: none;
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: rgba(0,0,0,0.8);
|
|
z-index: 2000;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.modal.active {
|
|
display: flex;
|
|
}
|
|
|
|
.modal-content {
|
|
background: var(--surface);
|
|
padding: 2rem;
|
|
border-radius: 8px;
|
|
max-width: 500px;
|
|
width: 90%;
|
|
max-height: 90vh;
|
|
overflow-y: auto;
|
|
position: relative;
|
|
}
|
|
|
|
.modal-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.close-btn {
|
|
background: none;
|
|
border: none;
|
|
color: var(--text-dim);
|
|
font-size: 1.5rem;
|
|
cursor: pointer;
|
|
padding: 0;
|
|
width: 30px;
|
|
height: 30px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border-radius: 50%;
|
|
transition: all 0.3s;
|
|
}
|
|
|
|
.close-btn:hover {
|
|
background: var(--surface-light);
|
|
color: var(--text);
|
|
}
|
|
|
|
/* Loading */
|
|
.loading {
|
|
text-align: center;
|
|
padding: 2rem;
|
|
color: var(--text-dim);
|
|
}
|
|
|
|
.spinner {
|
|
border: 3px solid var(--surface-light);
|
|
border-top: 3px solid var(--primary);
|
|
border-radius: 50%;
|
|
width: 40px;
|
|
height: 40px;
|
|
animation: spin 1s linear infinite;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
@keyframes spin {
|
|
0% { transform: rotate(0deg); }
|
|
100% { transform: rotate(360deg); }
|
|
}
|
|
|
|
/* Comments */
|
|
.comments-section {
|
|
background: var(--surface);
|
|
border-radius: 8px;
|
|
padding: 1.5rem;
|
|
margin-top: 2rem;
|
|
}
|
|
|
|
.comment {
|
|
padding: 1rem 0;
|
|
border-bottom: 1px solid var(--surface-light);
|
|
}
|
|
|
|
.comment:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.comment-form {
|
|
margin-top: 1.5rem;
|
|
padding-top: 1.5rem;
|
|
border-top: 1px solid var(--surface-light);
|
|
}
|
|
|
|
/* Sort Options */
|
|
.sort-options {
|
|
display: flex;
|
|
gap: 1rem;
|
|
margin-bottom: 2rem;
|
|
justify-content: center;
|
|
}
|
|
|
|
.sort-btn {
|
|
background: var(--surface);
|
|
border: 1px solid var(--surface-light);
|
|
padding: 0.5rem 1.5rem;
|
|
border-radius: 20px;
|
|
color: var(--text-dim);
|
|
cursor: pointer;
|
|
transition: all 0.3s;
|
|
}
|
|
|
|
.sort-btn:hover {
|
|
border-color: var(--primary);
|
|
color: var(--primary);
|
|
}
|
|
|
|
.sort-btn.active {
|
|
background: var(--primary);
|
|
color: white;
|
|
border-color: var(--primary);
|
|
}
|
|
|
|
/* Profile */
|
|
.profile-header {
|
|
background: var(--surface);
|
|
border-radius: 8px;
|
|
padding: 2rem;
|
|
margin-bottom: 2rem;
|
|
text-align: center;
|
|
}
|
|
|
|
.profile-avatar {
|
|
width: 100px;
|
|
height: 100px;
|
|
border-radius: 50%;
|
|
margin: 0 auto 1rem;
|
|
font-size: 2rem;
|
|
}
|
|
|
|
.profile-stats {
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 3rem;
|
|
margin-top: 1.5rem;
|
|
}
|
|
|
|
.stat {
|
|
text-align: center;
|
|
}
|
|
|
|
.stat-value {
|
|
font-size: 1.5rem;
|
|
font-weight: bold;
|
|
color: var(--primary);
|
|
}
|
|
|
|
.stat-label {
|
|
color: var(--text-dim);
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
/* Tabs */
|
|
.tabs {
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 1rem;
|
|
margin-bottom: 2rem;
|
|
border-bottom: 1px solid var(--surface-light);
|
|
}
|
|
|
|
.tab {
|
|
padding: 1rem 2rem;
|
|
background: none;
|
|
border: none;
|
|
color: var(--text-dim);
|
|
cursor: pointer;
|
|
position: relative;
|
|
transition: color 0.3s;
|
|
}
|
|
|
|
.tab:hover {
|
|
color: var(--text);
|
|
}
|
|
|
|
.tab.active {
|
|
color: var(--primary);
|
|
}
|
|
|
|
.tab.active::after {
|
|
content: '';
|
|
position: absolute;
|
|
bottom: -1px;
|
|
left: 0;
|
|
right: 0;
|
|
height: 2px;
|
|
background: var(--primary);
|
|
}
|
|
|
|
/* Alert */
|
|
.alert {
|
|
padding: 1rem;
|
|
border-radius: 4px;
|
|
margin-bottom: 1rem;
|
|
display: none;
|
|
}
|
|
|
|
.alert.active {
|
|
display: block;
|
|
}
|
|
|
|
.alert.success {
|
|
background: rgba(76, 175, 80, 0.1);
|
|
border: 1px solid var(--success);
|
|
color: var(--success);
|
|
}
|
|
|
|
.alert.error {
|
|
background: rgba(244, 67, 54, 0.1);
|
|
border: 1px solid var(--error);
|
|
color: var(--error);
|
|
}
|
|
|
|
/* Search */
|
|
.search-box {
|
|
background: var(--surface);
|
|
padding: 1rem;
|
|
border-radius: 8px;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.search-form {
|
|
display: flex;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.search-form input {
|
|
flex: 1;
|
|
}
|
|
|
|
/* Floating Action Button */
|
|
.fab {
|
|
position: fixed;
|
|
bottom: 2rem;
|
|
right: 2rem;
|
|
width: 60px;
|
|
height: 60px;
|
|
border-radius: 50%;
|
|
background: var(--primary);
|
|
color: white;
|
|
border: none;
|
|
font-size: 1.5rem;
|
|
cursor: pointer;
|
|
box-shadow: 0 4px 20px rgba(213, 81, 97, 0.4);
|
|
transition: all 0.3s;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.fab:hover {
|
|
transform: scale(1.1);
|
|
box-shadow: 0 6px 30px rgba(213, 81, 97, 0.6);
|
|
}
|
|
|
|
/* Responsive */
|
|
@media (max-width: 768px) {
|
|
.nav-links {
|
|
gap: 1rem;
|
|
}
|
|
|
|
.nav-links span {
|
|
display: none;
|
|
}
|
|
|
|
.profile-stats {
|
|
gap: 1.5rem;
|
|
}
|
|
|
|
.tabs {
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.tab {
|
|
padding: 1rem;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<!-- Navigation Component -->
|
|
<rant-navigation></rant-navigation>
|
|
|
|
<!-- Main Content Container -->
|
|
<div id="content" class="container"></div>
|
|
|
|
<!-- Floating Action Button -->
|
|
<rant-fab></rant-fab>
|
|
|
|
<!-- Modal Component -->
|
|
<rant-modal></rant-modal>
|
|
|
|
<script>
|
|
// Global Event Bus for Component Communication
|
|
class EventBus {
|
|
constructor() {
|
|
this.events = {};
|
|
}
|
|
|
|
on(event, callback) {
|
|
if (!this.events[event]) {
|
|
this.events[event] = [];
|
|
}
|
|
this.events[event].push(callback);
|
|
}
|
|
|
|
off(event, callback) {
|
|
if (this.events[event]) {
|
|
this.events[event] = this.events[event].filter(cb => cb !== callback);
|
|
}
|
|
}
|
|
|
|
emit(event, data) {
|
|
if (this.events[event]) {
|
|
this.events[event].forEach(callback => callback(data));
|
|
}
|
|
}
|
|
}
|
|
|
|
const eventBus = new EventBus();
|
|
|
|
// Auth Manager Singleton
|
|
class AuthManager {
|
|
constructor() {
|
|
this.authToken = null;
|
|
this.currentUser = null;
|
|
this.API_URL = '/api';
|
|
this.APP_ID = 3;
|
|
this.checkAuth();
|
|
}
|
|
|
|
checkAuth() {
|
|
const token = localStorage.getItem('authToken');
|
|
if (token) {
|
|
this.authToken = JSON.parse(token);
|
|
this.currentUser = {
|
|
id: this.authToken.user_id,
|
|
token_id: this.authToken.id,
|
|
token_key: this.authToken.key
|
|
};
|
|
eventBus.emit('auth-changed', { isLoggedIn: true, user: this.currentUser });
|
|
} else {
|
|
eventBus.emit('auth-changed', { isLoggedIn: false });
|
|
}
|
|
}
|
|
|
|
async apiCall(endpoint, options = {}) {
|
|
let url = `${this.API_URL}${endpoint}`;
|
|
|
|
// Add auth to FormData or URLSearchParams if logged in
|
|
if (this.currentUser && options.body) {
|
|
if (options.body instanceof FormData) {
|
|
options.body.append('app', this.APP_ID);
|
|
options.body.append('token_id', this.currentUser.token_id);
|
|
options.body.append('token_key', this.currentUser.token_key);
|
|
options.body.append('user_id', this.currentUser.id);
|
|
} else if (options.body instanceof URLSearchParams) {
|
|
options.body.append('app', this.APP_ID);
|
|
options.body.append('token_id', this.currentUser.token_id);
|
|
options.body.append('token_key', this.currentUser.token_key);
|
|
options.body.append('user_id', this.currentUser.id);
|
|
}
|
|
}
|
|
|
|
// Add auth to query params for GET requests
|
|
if (this.currentUser && (options.method === 'GET' || !options.method)) {
|
|
const separator = endpoint.includes('?') ? '&' : '?';
|
|
url += `${separator}app=${this.APP_ID}&token_id=${this.currentUser.token_id}&token_key=${this.currentUser.token_key}&user_id=${this.currentUser.id}`;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(url, options);
|
|
const data = await response.json();
|
|
return data;
|
|
} catch (error) {
|
|
console.error('API Error:', error);
|
|
return { success: false, error: error.message };
|
|
}
|
|
}
|
|
|
|
async login(username, password) {
|
|
const formData = new FormData();
|
|
formData.append('username', username);
|
|
formData.append('password', password);
|
|
formData.append('app', this.APP_ID);
|
|
|
|
const data = await this.apiCall('/users/auth-token', {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
|
|
if (data.success) {
|
|
this.authToken = data.auth_token;
|
|
this.currentUser = {
|
|
id: this.authToken.user_id,
|
|
token_id: this.authToken.id,
|
|
token_key: this.authToken.key
|
|
};
|
|
localStorage.setItem('authToken', JSON.stringify(this.authToken));
|
|
eventBus.emit('auth-changed', { isLoggedIn: true, user: this.currentUser });
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
logout() {
|
|
localStorage.removeItem('authToken');
|
|
this.authToken = null;
|
|
this.currentUser = null;
|
|
eventBus.emit('auth-changed', { isLoggedIn: false });
|
|
}
|
|
|
|
isLoggedIn() {
|
|
return !!this.currentUser;
|
|
}
|
|
}
|
|
|
|
const authManager = new AuthManager();
|
|
|
|
// Helper functions
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
function formatTime(timestamp) {
|
|
const date = new Date(timestamp * 1000);
|
|
const now = new Date();
|
|
const diff = Math.floor((now - date) / 1000);
|
|
|
|
if (diff < 60) return 'just now';
|
|
if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
|
|
if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
|
|
if (diff < 604800) return `${Math.floor(diff / 86400)}d ago`;
|
|
|
|
return date.toLocaleDateString();
|
|
}
|
|
|
|
// Navigation Component
|
|
class RantNavigation extends HTMLElement {
|
|
constructor() {
|
|
super();
|
|
this.notificationCount = 0;
|
|
this.checkNotificationInterval = null;
|
|
}
|
|
|
|
connectedCallback() {
|
|
this.render();
|
|
eventBus.on('auth-changed', (data) => this.handleAuthChange(data));
|
|
eventBus.on('navigate', (view) => this.handleNavigate(view));
|
|
|
|
if (authManager.isLoggedIn()) {
|
|
this.startNotificationCheck();
|
|
}
|
|
}
|
|
|
|
disconnectedCallback() {
|
|
eventBus.off('auth-changed', this.handleAuthChange);
|
|
eventBus.off('navigate', this.handleNavigate);
|
|
this.stopNotificationCheck();
|
|
}
|
|
|
|
handleAuthChange(data) {
|
|
this.render();
|
|
if (data.isLoggedIn) {
|
|
this.startNotificationCheck();
|
|
} else {
|
|
this.stopNotificationCheck();
|
|
}
|
|
}
|
|
|
|
handleNavigate(view) {
|
|
// Handle navigation updates if needed
|
|
}
|
|
|
|
startNotificationCheck() {
|
|
this.checkNotifications();
|
|
this.checkNotificationInterval = setInterval(() => {
|
|
this.checkNotifications();
|
|
}, 60000);
|
|
}
|
|
|
|
stopNotificationCheck() {
|
|
if (this.checkNotificationInterval) {
|
|
clearInterval(this.checkNotificationInterval);
|
|
this.checkNotificationInterval = null;
|
|
}
|
|
}
|
|
|
|
async checkNotifications() {
|
|
if (!authManager.isLoggedIn()) return;
|
|
|
|
const params = new URLSearchParams({
|
|
ext_prof: 1,
|
|
last_time: Math.floor(Date.now() / 1000) - 86400
|
|
});
|
|
|
|
const data = await authManager.apiCall(`/users/me/notif-feed?${params}`);
|
|
|
|
if (data.success) {
|
|
this.notificationCount = data.data.num_unread;
|
|
this.updateNotificationCount();
|
|
}
|
|
}
|
|
|
|
updateNotificationCount() {
|
|
const notifCount = this.querySelector('#notifCount');
|
|
if (notifCount) {
|
|
if (this.notificationCount > 0) {
|
|
notifCount.textContent = `(${this.notificationCount})`;
|
|
notifCount.style.color = 'var(--error)';
|
|
} else {
|
|
notifCount.textContent = '';
|
|
}
|
|
}
|
|
}
|
|
|
|
render() {
|
|
const isLoggedIn = authManager.isLoggedIn();
|
|
|
|
this.innerHTML = `
|
|
<nav>
|
|
<div class="nav-container">
|
|
<a href="#" class="logo">Rant</a>
|
|
<div class="nav-links">
|
|
<a href="#" data-view="feed">Feed</a>
|
|
<a href="#" data-view="search">Search</a>
|
|
${isLoggedIn ? `
|
|
<span>
|
|
<a href="#" data-view="profile">Profile</a>
|
|
<a href="#" data-view="notifications">Notifications <span id="notifCount"></span></a>
|
|
</span>
|
|
<span>
|
|
<button class="btn btn-secondary" id="logoutBtn">Logout</button>
|
|
</span>
|
|
` : `
|
|
<span>
|
|
<button class="btn btn-secondary" data-modal="login">Login</button>
|
|
<button class="btn" data-modal="register">Sign Up</button>
|
|
</span>
|
|
`}
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
`;
|
|
|
|
// Add event listeners
|
|
this.querySelectorAll('[data-view]').forEach(link => {
|
|
link.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
const view = e.target.dataset.view;
|
|
eventBus.emit('navigate', view);
|
|
});
|
|
});
|
|
|
|
this.querySelectorAll('[data-modal]').forEach(btn => {
|
|
btn.addEventListener('click', (e) => {
|
|
const modalType = e.target.dataset.modal;
|
|
eventBus.emit('show-modal', modalType);
|
|
});
|
|
});
|
|
|
|
const logoutBtn = this.querySelector('#logoutBtn');
|
|
if (logoutBtn) {
|
|
logoutBtn.addEventListener('click', () => {
|
|
authManager.logout();
|
|
eventBus.emit('navigate', 'feed');
|
|
});
|
|
}
|
|
|
|
const logo = this.querySelector('.logo');
|
|
logo.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
eventBus.emit('navigate', 'feed');
|
|
});
|
|
|
|
this.updateNotificationCount();
|
|
}
|
|
}
|
|
|
|
// Rant Card Component
|
|
class RantCard extends HTMLElement {
|
|
static get observedAttributes() {
|
|
return ['rant-data', 'clickable'];
|
|
}
|
|
|
|
constructor() {
|
|
super();
|
|
this.rantData = null;
|
|
}
|
|
|
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
if (name === 'rant-data' && newValue) {
|
|
this.rantData = JSON.parse(newValue);
|
|
this.render();
|
|
}
|
|
}
|
|
|
|
connectedCallback() {
|
|
if (this.rantData) {
|
|
this.render();
|
|
}
|
|
}
|
|
|
|
render() {
|
|
if (!this.rantData) return;
|
|
|
|
const rant = this.rantData;
|
|
const tags = rant.tags.map(tag => `<span class="tag">${tag}</span>`).join('');
|
|
const voteClass = rant.vote_state === 1 ? 'voted' : rant.vote_state === -1 ? 'downvoted' : '';
|
|
const clickable = this.getAttribute('clickable') !== 'false';
|
|
|
|
this.innerHTML = `
|
|
<div class="rant-card" ${clickable ? `style="cursor: pointer;"` : ''}>
|
|
<div class="rant-header">
|
|
<div class="avatar" style="background: #${rant.user_avatar.b}">
|
|
${rant.user_username[0].toUpperCase()}
|
|
</div>
|
|
<div class="user-info">
|
|
<div class="username" data-user-id="${rant.user_id}">${rant.user_username}</div>
|
|
<div class="score">${rant.user_score} points</div>
|
|
</div>
|
|
</div>
|
|
<div class="rant-content">${escapeHtml(rant.text)}</div>
|
|
${rant.attached_image ? `<img src="${rant.attached_image}" alt="Rant image" class="rant-image">` : ''}
|
|
${tags ? `<div class="tags">${tags}</div>` : ''}
|
|
<div class="rant-footer">
|
|
<div class="rant-actions">
|
|
<button class="action-btn ${voteClass}" data-action="vote" data-vote="${rant.vote_state === 1 ? 0 : 1}">
|
|
++ ${rant.score}
|
|
</button>
|
|
<button class="action-btn" data-action="comments">
|
|
💬 ${rant.num_comments}
|
|
</button>
|
|
</div>
|
|
<div>${formatTime(rant.created_time)}</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// Add event listeners
|
|
const card = this.querySelector('.rant-card');
|
|
if (clickable) {
|
|
card.addEventListener('click', (e) => {
|
|
if (!e.target.closest('button') && !e.target.closest('.username')) {
|
|
eventBus.emit('navigate', { view: 'rant', id: rant.id });
|
|
}
|
|
});
|
|
}
|
|
|
|
this.querySelector('.username').addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
eventBus.emit('navigate', { view: 'profile', id: rant.user_id });
|
|
});
|
|
|
|
this.querySelectorAll('[data-action]').forEach(btn => {
|
|
btn.addEventListener('click', async (e) => {
|
|
e.stopPropagation();
|
|
const action = btn.dataset.action;
|
|
|
|
if (action === 'vote') {
|
|
if (!authManager.isLoggedIn()) {
|
|
eventBus.emit('show-modal', 'login');
|
|
return;
|
|
}
|
|
|
|
const vote = parseInt(btn.dataset.vote);
|
|
await this.voteRant(rant.id, vote);
|
|
} else if (action === 'comments') {
|
|
eventBus.emit('navigate', { view: 'rant', id: rant.id });
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
async voteRant(rantId, vote) {
|
|
const formData = new FormData();
|
|
formData.append('vote', vote);
|
|
if (vote === -1) {
|
|
formData.append('reason', 0);
|
|
}
|
|
|
|
const data = await authManager.apiCall(`/rant/rants/${rantId}/vote`, {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
|
|
if (data.success) {
|
|
eventBus.emit('refresh-view');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Loading Spinner Component
|
|
class LoadingSpinner extends HTMLElement {
|
|
connectedCallback() {
|
|
this.innerHTML = `
|
|
<div class="loading">
|
|
<div class="spinner"></div>
|
|
<p>${this.getAttribute('message') || 'Loading...'}</p>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
// Sort Options Component
|
|
class SortOptions extends HTMLElement {
|
|
static get observedAttributes() {
|
|
return ['active-sort'];
|
|
}
|
|
|
|
constructor() {
|
|
super();
|
|
this.activeSort = 'recent';
|
|
}
|
|
|
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
if (name === 'active-sort') {
|
|
this.activeSort = newValue;
|
|
this.render();
|
|
}
|
|
}
|
|
|
|
connectedCallback() {
|
|
this.render();
|
|
}
|
|
|
|
render() {
|
|
this.innerHTML = `
|
|
<div class="sort-options">
|
|
<button class="sort-btn ${this.activeSort === 'recent' ? 'active' : ''}" data-sort="recent">Recent</button>
|
|
<button class="sort-btn ${this.activeSort === 'top' ? 'active' : ''}" data-sort="top">Top</button>
|
|
<button class="sort-btn ${this.activeSort === 'algo' ? 'active' : ''}" data-sort="algo">Algorithm</button>
|
|
</div>
|
|
`;
|
|
|
|
this.querySelectorAll('[data-sort]').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
const sort = btn.dataset.sort;
|
|
eventBus.emit('sort-changed', sort);
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
// Feed View Component
|
|
class FeedView extends HTMLElement {
|
|
constructor() {
|
|
super();
|
|
this.currentSort = 'recent';
|
|
// Bind methods to ensure proper reference for event removal
|
|
this.handleSortChange = this.handleSortChange.bind(this);
|
|
this.handleRefreshView = this.handleRefreshView.bind(this);
|
|
}
|
|
|
|
async connectedCallback() {
|
|
this.innerHTML = `
|
|
<sort-options active-sort="${this.currentSort}"></sort-options>
|
|
<loading-spinner message="Loading rants..."></loading-spinner>
|
|
`;
|
|
|
|
eventBus.on('sort-changed', this.handleSortChange);
|
|
eventBus.on('refresh-view', this.handleRefreshView);
|
|
|
|
await this.loadFeed();
|
|
}
|
|
|
|
disconnectedCallback() {
|
|
eventBus.off('sort-changed', this.handleSortChange);
|
|
eventBus.off('refresh-view', this.handleRefreshView);
|
|
}
|
|
|
|
handleSortChange(sort) {
|
|
this.currentSort = sort;
|
|
this.loadFeed();
|
|
}
|
|
|
|
handleRefreshView() {
|
|
this.loadFeed();
|
|
}
|
|
|
|
async loadFeed() {
|
|
const params = new URLSearchParams({
|
|
sort: this.currentSort,
|
|
limit: 50,
|
|
skip: 0,
|
|
app: authManager.APP_ID
|
|
});
|
|
|
|
const data = await authManager.apiCall(`/rant/rants?${params}`);
|
|
|
|
if (data.success) {
|
|
this.innerHTML = `
|
|
<sort-options active-sort="${this.currentSort}"></sort-options>
|
|
`;
|
|
|
|
const container = document.createElement('div');
|
|
data.rants.forEach(rant => {
|
|
const rantCard = document.createElement('rant-card');
|
|
rantCard.setAttribute('rant-data', JSON.stringify(rant));
|
|
container.appendChild(rantCard);
|
|
});
|
|
|
|
this.appendChild(container);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Rant Detail View Component
|
|
class RantDetailView extends HTMLElement {
|
|
static get observedAttributes() {
|
|
return ['rant-id'];
|
|
}
|
|
|
|
constructor() {
|
|
super();
|
|
this.rantId = null;
|
|
this.rantData = null;
|
|
}
|
|
|
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
if (name === 'rant-id') {
|
|
this.rantId = newValue;
|
|
this.loadRant();
|
|
}
|
|
}
|
|
|
|
async loadRant() {
|
|
if (!this.rantId) return;
|
|
|
|
this.innerHTML = `<loading-spinner message="Loading rant..."></loading-spinner>`;
|
|
|
|
const params = new URLSearchParams({ app: authManager.APP_ID });
|
|
const data = await authManager.apiCall(`/rant/rants/${this.rantId}?${params}`);
|
|
|
|
if (data.success) {
|
|
this.rantData = data;
|
|
this.render();
|
|
}
|
|
}
|
|
|
|
render() {
|
|
const { rant, comments, subscribed } = this.rantData;
|
|
const tags = rant.tags.map(tag => `<span class="tag">${tag}</span>`).join('');
|
|
const voteClass = rant.vote_state === 1 ? 'voted' : rant.vote_state === -1 ? 'downvoted' : '';
|
|
const isOwner = authManager.currentUser && authManager.currentUser.id === rant.user_id;
|
|
|
|
this.innerHTML = `
|
|
<button class="btn btn-secondary" id="backBtn">← Back to Feed</button>
|
|
<div class="rant-card" style="margin-top: 1rem;">
|
|
<div class="rant-header">
|
|
<div class="avatar" style="background: #${rant.user_avatar.b}">
|
|
${rant.user_username[0].toUpperCase()}
|
|
</div>
|
|
<div class="user-info">
|
|
<div class="username" data-user-id="${rant.user_id}" style="cursor: pointer;">${rant.user_username}</div>
|
|
<div class="score">${rant.user_score} points</div>
|
|
</div>
|
|
${isOwner ? `
|
|
<button class="btn btn-secondary" data-action="edit">Edit</button>
|
|
<button class="btn btn-secondary" data-action="delete" style="margin-left: 0.5rem;">Delete</button>
|
|
` : ''}
|
|
</div>
|
|
<div class="rant-content">${escapeHtml(rant.text)}</div>
|
|
${rant.attached_image ? `<img src="${rant.attached_image}" alt="Rant image" class="rant-image">` : ''}
|
|
${tags ? `<div class="tags">${tags}</div>` : ''}
|
|
<div class="rant-footer">
|
|
<div class="rant-actions">
|
|
<button class="action-btn ${voteClass}" data-action="vote" data-vote="${rant.vote_state === 1 ? 0 : 1}">
|
|
++ ${rant.score}
|
|
</button>
|
|
<button class="action-btn ${rant.vote_state === -1 ? 'downvoted' : ''}" data-action="vote" data-vote="${rant.vote_state === -1 ? 0 : -1}">
|
|
--
|
|
</button>
|
|
<button class="action-btn ${subscribed ? 'voted' : ''}" data-action="favorite">
|
|
${subscribed ? '★' : '☆'} Favorite
|
|
</button>
|
|
</div>
|
|
<div>${formatTime(rant.created_time)}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<comments-section rant-id="${this.rantId}" comments='${JSON.stringify(comments)}'></comments-section>
|
|
`;
|
|
|
|
// Add event listeners
|
|
this.querySelector('#backBtn').addEventListener('click', () => {
|
|
eventBus.emit('navigate', 'feed');
|
|
});
|
|
|
|
this.querySelector('.username').addEventListener('click', () => {
|
|
eventBus.emit('navigate', { view: 'profile', id: rant.user_id });
|
|
});
|
|
|
|
this.querySelectorAll('[data-action]').forEach(btn => {
|
|
btn.addEventListener('click', async () => {
|
|
const action = btn.dataset.action;
|
|
|
|
if (action === 'vote') {
|
|
if (!authManager.isLoggedIn()) {
|
|
eventBus.emit('show-modal', 'login');
|
|
return;
|
|
}
|
|
const vote = parseInt(btn.dataset.vote);
|
|
await this.voteRant(vote);
|
|
} else if (action === 'favorite') {
|
|
if (!authManager.isLoggedIn()) {
|
|
eventBus.emit('show-modal', 'login');
|
|
return;
|
|
}
|
|
await this.toggleFavorite();
|
|
} else if (action === 'edit') {
|
|
eventBus.emit('show-modal', { type: 'edit-rant', rant });
|
|
} else if (action === 'delete') {
|
|
await this.deleteRant();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
async voteRant(vote) {
|
|
const formData = new FormData();
|
|
formData.append('vote', vote);
|
|
if (vote === -1) {
|
|
formData.append('reason', 0);
|
|
}
|
|
|
|
const data = await authManager.apiCall(`/rant/rants/${this.rantId}/vote`, {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
|
|
if (data.success) {
|
|
this.loadRant();
|
|
}
|
|
}
|
|
|
|
async toggleFavorite() {
|
|
const endpoint = this.rantData.subscribed ? 'unfavorite' : 'favorite';
|
|
const formData = new FormData();
|
|
|
|
const data = await authManager.apiCall(`/rant/rants/${this.rantId}/${endpoint}`, {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
|
|
if (data.success) {
|
|
this.loadRant();
|
|
}
|
|
}
|
|
|
|
async deleteRant() {
|
|
if (!confirm('Are you sure you want to delete this rant?')) return;
|
|
|
|
const params = new URLSearchParams();
|
|
params.append('app', authManager.APP_ID);
|
|
params.append('token_id', authManager.currentUser.token_id);
|
|
params.append('token_key', authManager.currentUser.token_key);
|
|
params.append('user_id', authManager.currentUser.id);
|
|
|
|
const data = await authManager.apiCall(`/rant/rants/${this.rantId}?${params}`, {
|
|
method: 'DELETE'
|
|
});
|
|
|
|
if (data.success) {
|
|
eventBus.emit('navigate', 'feed');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Comments Section Component
|
|
class CommentsSection extends HTMLElement {
|
|
static get observedAttributes() {
|
|
return ['rant-id', 'comments'];
|
|
}
|
|
|
|
constructor() {
|
|
super();
|
|
this.rantId = null;
|
|
this.comments = [];
|
|
}
|
|
|
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
if (name === 'rant-id') {
|
|
this.rantId = newValue;
|
|
} else if (name === 'comments') {
|
|
this.comments = JSON.parse(newValue);
|
|
this.render();
|
|
}
|
|
}
|
|
|
|
render() {
|
|
this.innerHTML = `
|
|
<div class="comments-section">
|
|
<h3>Comments (${this.comments.length})</h3>
|
|
<div id="commentsList">
|
|
${this.comments.map(comment => this.createCommentHTML(comment)).join('')}
|
|
</div>
|
|
${authManager.isLoggedIn() ? `
|
|
<form class="comment-form" id="commentForm">
|
|
<h4>Add a comment</h4>
|
|
<div class="form-group">
|
|
<textarea name="comment" placeholder="Write your comment..." required></textarea>
|
|
</div>
|
|
<button type="submit" class="btn">Post Comment</button>
|
|
</form>
|
|
` : '<p style="text-align: center; margin-top: 2rem;">Login to comment</p>'}
|
|
</div>
|
|
`;
|
|
|
|
// Add event listeners
|
|
const form = this.querySelector('#commentForm');
|
|
if (form) {
|
|
form.addEventListener('submit', (e) => this.postComment(e));
|
|
}
|
|
|
|
this.querySelectorAll('[data-comment-action]').forEach(btn => {
|
|
btn.addEventListener('click', async () => {
|
|
const action = btn.dataset.commentAction;
|
|
const commentId = btn.dataset.commentId;
|
|
|
|
if (action === 'vote') {
|
|
const vote = parseInt(btn.dataset.vote);
|
|
await this.voteComment(commentId, vote);
|
|
} else if (action === 'delete') {
|
|
await this.deleteComment(commentId);
|
|
}
|
|
});
|
|
});
|
|
|
|
this.querySelectorAll('.username').forEach(username => {
|
|
username.addEventListener('click', () => {
|
|
const userId = username.dataset.userId;
|
|
eventBus.emit('navigate', { view: 'profile', id: parseInt(userId) });
|
|
});
|
|
});
|
|
}
|
|
|
|
createCommentHTML(comment) {
|
|
const voteClass = comment.vote_state === 1 ? 'voted' : comment.vote_state === -1 ? 'downvoted' : '';
|
|
const isOwner = authManager.currentUser && authManager.currentUser.id === comment.user_id;
|
|
|
|
return `
|
|
<div class="comment">
|
|
<div class="rant-header">
|
|
<div class="avatar" style="background: #${comment.user_avatar.b}">
|
|
${comment.user_username[0].toUpperCase()}
|
|
</div>
|
|
<div class="user-info">
|
|
<div class="username" data-user-id="${comment.user_id}" style="cursor: pointer;">${comment.user_username}</div>
|
|
<div class="score">${comment.user_score} points</div>
|
|
</div>
|
|
${isOwner ? `
|
|
<button class="btn btn-secondary" data-comment-action="delete" data-comment-id="${comment.id}">Delete</button>
|
|
` : ''}
|
|
</div>
|
|
<div class="rant-content">${escapeHtml(comment.body)}</div>
|
|
<div class="rant-footer">
|
|
<div class="rant-actions">
|
|
<button class="action-btn ${voteClass}" data-comment-action="vote" data-comment-id="${comment.id}" data-vote="${comment.vote_state === 1 ? 0 : 1}">
|
|
++ ${comment.score}
|
|
</button>
|
|
</div>
|
|
<div>${formatTime(comment.created_time)}</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
async postComment(event) {
|
|
event.preventDefault();
|
|
const formData = new FormData(event.target);
|
|
|
|
const data = await authManager.apiCall(`/rant/rants/${this.rantId}/comments`, {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
|
|
if (data.success) {
|
|
eventBus.emit('refresh-view');
|
|
}
|
|
}
|
|
|
|
async voteComment(commentId, vote) {
|
|
if (!authManager.isLoggedIn()) {
|
|
eventBus.emit('show-modal', 'login');
|
|
return;
|
|
}
|
|
|
|
const formData = new FormData();
|
|
formData.append('vote', vote);
|
|
|
|
const data = await authManager.apiCall(`/comments/${commentId}/vote`, {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
|
|
if (data.success) {
|
|
eventBus.emit('refresh-view');
|
|
}
|
|
}
|
|
|
|
async deleteComment(commentId) {
|
|
if (!confirm('Are you sure you want to delete this comment?')) return;
|
|
|
|
const params = new URLSearchParams();
|
|
params.append('app', authManager.APP_ID);
|
|
params.append('token_id', authManager.currentUser.token_id);
|
|
params.append('token_key', authManager.currentUser.token_key);
|
|
params.append('user_id', authManager.currentUser.id);
|
|
|
|
const data = await authManager.apiCall(`/comments/${commentId}?${params}`, {
|
|
method: 'DELETE'
|
|
});
|
|
|
|
if (data.success) {
|
|
eventBus.emit('refresh-view');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Profile View Component
|
|
class ProfileView extends HTMLElement {
|
|
static get observedAttributes() {
|
|
return ['user-id'];
|
|
}
|
|
|
|
constructor() {
|
|
super();
|
|
this.userId = null;
|
|
this.profileData = null;
|
|
this.activeTab = 'rants';
|
|
}
|
|
|
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
if (name === 'user-id') {
|
|
this.userId = newValue;
|
|
this.loadProfile();
|
|
}
|
|
}
|
|
|
|
async loadProfile() {
|
|
if (!this.userId) return;
|
|
|
|
this.innerHTML = `<loading-spinner message="Loading profile..."></loading-spinner>`;
|
|
|
|
const params = new URLSearchParams({ app: authManager.APP_ID });
|
|
const data = await authManager.apiCall(`/users/${this.userId}?${params}`);
|
|
|
|
if (data.success) {
|
|
this.profileData = data.profile;
|
|
this.render();
|
|
}
|
|
}
|
|
|
|
render() {
|
|
const profile = this.profileData;
|
|
const isOwnProfile = authManager.currentUser && authManager.currentUser.id === parseInt(this.userId);
|
|
|
|
this.innerHTML = `
|
|
<button class="btn btn-secondary" id="backBtn">← Back to Feed</button>
|
|
<div class="profile-header">
|
|
<div class="profile-avatar avatar" style="background: #${profile.avatar.b}">
|
|
${profile.username[0].toUpperCase()}
|
|
</div>
|
|
<h1>${profile.username}</h1>
|
|
${profile.about ? `<p style="margin-top: 1rem;">${escapeHtml(profile.about)}</p>` : ''}
|
|
${profile.skills ? `<p><strong>Skills:</strong> ${escapeHtml(profile.skills)}</p>` : ''}
|
|
${profile.location ? `<p><strong>Location:</strong> ${escapeHtml(profile.location)}</p>` : ''}
|
|
${profile.github ? `<p><strong>GitHub:</strong> <a href="https://github.com/${profile.github}" target="_blank">${profile.github}</a></p>` : ''}
|
|
${profile.website ? `<p><strong>Website:</strong> <a href="${profile.website}" target="_blank">${profile.website}</a></p>` : ''}
|
|
<div class="profile-stats">
|
|
<div class="stat">
|
|
<div class="stat-value">${profile.score}</div>
|
|
<div class="stat-label">Score</div>
|
|
</div>
|
|
<div class="stat">
|
|
<div class="stat-value">${profile.content.content.rants.length}</div>
|
|
<div class="stat-label">Rants</div>
|
|
</div>
|
|
<div class="stat">
|
|
<div class="stat-value">${profile.content.content.comments.length}</div>
|
|
<div class="stat-label">Comments</div>
|
|
</div>
|
|
</div>
|
|
${isOwnProfile ? `<button class="btn" id="editProfileBtn" style="margin-top: 1rem;">Edit Profile</button>` : ''}
|
|
</div>
|
|
|
|
<div class="tabs">
|
|
<button class="tab ${this.activeTab === 'rants' ? 'active' : ''}" data-tab="rants">Rants</button>
|
|
<button class="tab ${this.activeTab === 'comments' ? 'active' : ''}" data-tab="comments">Comments</button>
|
|
<button class="tab ${this.activeTab === 'favorites' ? 'active' : ''}" data-tab="favorites">Favorites</button>
|
|
</div>
|
|
|
|
<div id="profileContent">
|
|
${this.renderTabContent()}
|
|
</div>
|
|
`;
|
|
|
|
// Add event listeners
|
|
this.querySelector('#backBtn').addEventListener('click', () => {
|
|
eventBus.emit('navigate', 'feed');
|
|
});
|
|
|
|
const editBtn = this.querySelector('#editProfileBtn');
|
|
if (editBtn) {
|
|
editBtn.addEventListener('click', () => {
|
|
eventBus.emit('show-modal', 'edit-profile');
|
|
});
|
|
}
|
|
|
|
this.querySelectorAll('[data-tab]').forEach(tab => {
|
|
tab.addEventListener('click', () => {
|
|
this.activeTab = tab.dataset.tab;
|
|
this.render();
|
|
});
|
|
});
|
|
}
|
|
|
|
renderTabContent() {
|
|
const profile = this.profileData;
|
|
|
|
if (this.activeTab === 'rants') {
|
|
return profile.content.content.rants.map(rant => {
|
|
const card = document.createElement('rant-card');
|
|
card.setAttribute('rant-data', JSON.stringify(rant));
|
|
return card.outerHTML;
|
|
}).join('');
|
|
} else if (this.activeTab === 'comments') {
|
|
return profile.content.content.comments.map(comment => `
|
|
<div class="rant-card" style="cursor: pointer;" data-rant-id="${comment.rant_id}">
|
|
<div class="rant-content">${escapeHtml(comment.body)}</div>
|
|
<div class="rant-footer">
|
|
<div class="rant-actions">
|
|
<button class="action-btn">++ ${comment.score}</button>
|
|
</div>
|
|
<div>${formatTime(comment.created_time)}</div>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
} else if (this.activeTab === 'favorites') {
|
|
return profile.content.content.favorites.map(rant => {
|
|
const card = document.createElement('rant-card');
|
|
card.setAttribute('rant-data', JSON.stringify(rant));
|
|
return card.outerHTML;
|
|
}).join('');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Search View Component
|
|
class SearchView extends HTMLElement {
|
|
connectedCallback() {
|
|
this.render();
|
|
}
|
|
|
|
render() {
|
|
this.innerHTML = `
|
|
<div class="search-box">
|
|
<form class="search-form" id="searchForm">
|
|
<input type="text" name="term" placeholder="Search rants..." required>
|
|
<button type="submit" class="btn">Search</button>
|
|
</form>
|
|
</div>
|
|
<div id="searchResults"></div>
|
|
`;
|
|
|
|
this.querySelector('#searchForm').addEventListener('submit', (e) => this.search(e));
|
|
}
|
|
|
|
async search(event) {
|
|
event.preventDefault();
|
|
const term = event.target.term.value;
|
|
|
|
const resultsDiv = this.querySelector('#searchResults');
|
|
resultsDiv.innerHTML = `<loading-spinner message="Searching..."></loading-spinner>`;
|
|
|
|
const params = new URLSearchParams({ term, app: authManager.APP_ID });
|
|
const data = await authManager.apiCall(`/rant/search?${params}`);
|
|
|
|
if (data.success) {
|
|
if (data.results.length === 0) {
|
|
resultsDiv.innerHTML = '<p style="text-align: center; color: var(--text-dim);">No results found</p>';
|
|
} else {
|
|
resultsDiv.innerHTML = '';
|
|
data.results.forEach(rant => {
|
|
const rantCard = document.createElement('rant-card');
|
|
rantCard.setAttribute('rant-data', JSON.stringify(rant));
|
|
resultsDiv.appendChild(rantCard);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Notifications View Component
|
|
class NotificationsView extends HTMLElement {
|
|
async connectedCallback() {
|
|
this.innerHTML = `<loading-spinner message="Loading notifications..."></loading-spinner>`;
|
|
await this.loadNotifications();
|
|
}
|
|
|
|
async loadNotifications() {
|
|
const params = new URLSearchParams({
|
|
ext_prof: 1,
|
|
last_time: Math.floor(Date.now() / 1000) - 86400
|
|
});
|
|
|
|
const data = await authManager.apiCall(`/users/me/notif-feed?${params}`);
|
|
|
|
if (data.success) {
|
|
const items = data.data.items;
|
|
|
|
this.innerHTML = `
|
|
<h2>Notifications</h2>
|
|
${items.length === 0 ? '<p style="text-align: center; color: var(--text-dim); margin-top: 2rem;">No notifications</p>' : ''}
|
|
${items.map(notif => `
|
|
<div class="rant-card" data-rant-id="${notif.rant_id}" style="cursor: pointer;">
|
|
<p><strong>${notif.username}</strong> ${notif.type === 'comment' ? 'commented on your rant' : 'mentioned you'}</p>
|
|
<p style="color: var(--text-dim); font-size: 0.9rem;">${formatTime(notif.created_time)}</p>
|
|
</div>
|
|
`).join('')}
|
|
`;
|
|
|
|
this.querySelectorAll('[data-rant-id]').forEach(card => {
|
|
card.addEventListener('click', () => {
|
|
const rantId = card.dataset.rantId;
|
|
eventBus.emit('navigate', { view: 'rant', id: rantId });
|
|
});
|
|
});
|
|
|
|
// Clear notification count
|
|
eventBus.emit('notifications-read');
|
|
} else {
|
|
this.innerHTML = `
|
|
<h2>Notifications</h2>
|
|
<p style="text-align: center; color: var(--error); margin-top: 2rem;">Failed to load notifications</p>
|
|
`;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Modal Component
|
|
class RantModal extends HTMLElement {
|
|
connectedCallback() {
|
|
this.innerHTML = `
|
|
<div id="modal" class="modal">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h2 id="modalTitle">Modal Title</h2>
|
|
<button class="close-btn">×</button>
|
|
</div>
|
|
<div id="modalBody">
|
|
<!-- Modal content will be loaded here -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
this.querySelector('.close-btn').addEventListener('click', () => this.close());
|
|
this.querySelector('#modal').addEventListener('click', (e) => {
|
|
if (e.target.id === 'modal') {
|
|
this.close();
|
|
}
|
|
});
|
|
|
|
eventBus.on('show-modal', (type) => this.show(type));
|
|
}
|
|
|
|
show(modalData) {
|
|
const modalType = typeof modalData === 'string' ? modalData : modalData.type;
|
|
|
|
switch (modalType) {
|
|
case 'login':
|
|
this.showLogin();
|
|
break;
|
|
case 'register':
|
|
this.showRegister();
|
|
break;
|
|
case 'create-rant':
|
|
this.showCreateRant();
|
|
break;
|
|
case 'edit-profile':
|
|
this.showEditProfile();
|
|
break;
|
|
case 'edit-rant':
|
|
this.showEditRant(modalData.rant);
|
|
break;
|
|
}
|
|
}
|
|
|
|
close() {
|
|
this.querySelector('#modal').classList.remove('active');
|
|
}
|
|
|
|
setContent(title, content) {
|
|
this.querySelector('#modalTitle').textContent = title;
|
|
this.querySelector('#modalBody').innerHTML = content;
|
|
this.querySelector('#modal').classList.add('active');
|
|
}
|
|
|
|
showLogin() {
|
|
this.setContent('Login', `
|
|
<form id="loginForm">
|
|
<div class="alert error" id="loginError"></div>
|
|
<div class="form-group">
|
|
<label>Username or Email</label>
|
|
<input type="text" name="username" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Password</label>
|
|
<input type="password" name="password" required>
|
|
</div>
|
|
<button type="submit" class="btn" style="width: 100%;">Login</button>
|
|
<p style="text-align: center; margin-top: 1rem;">
|
|
Don't have an account? <a href="#" id="showRegisterLink">Sign up</a>
|
|
</p>
|
|
</form>
|
|
`);
|
|
|
|
this.querySelector('#loginForm').addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const formData = new FormData(e.target);
|
|
const data = await authManager.login(formData.get('username'), formData.get('password'));
|
|
|
|
if (data.success) {
|
|
this.close();
|
|
eventBus.emit('navigate', 'feed');
|
|
} else {
|
|
this.showError('loginError', data.error);
|
|
}
|
|
});
|
|
|
|
this.querySelector('#showRegisterLink').addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
this.showRegister();
|
|
});
|
|
}
|
|
|
|
showRegister() {
|
|
this.setContent('Sign Up', `
|
|
<form id="registerForm">
|
|
<div class="alert error" id="registerError"></div>
|
|
<div class="form-group">
|
|
<label>Email</label>
|
|
<input type="email" name="email" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Username</label>
|
|
<input type="text" name="username" required minlength="4" maxlength="15">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Password</label>
|
|
<input type="password" name="password" required>
|
|
</div>
|
|
<button type="submit" class="btn" style="width: 100%;">Sign Up</button>
|
|
<p style="text-align: center; margin-top: 1rem;">
|
|
Already have an account? <a href="#" id="showLoginLink">Login</a>
|
|
</p>
|
|
</form>
|
|
`);
|
|
|
|
this.querySelector('#registerForm').addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const formData = new FormData(e.target);
|
|
formData.append('app', authManager.APP_ID);
|
|
formData.append('type', 1);
|
|
|
|
const data = await authManager.apiCall('/users', {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
|
|
if (data.success) {
|
|
this.close();
|
|
this.showLogin();
|
|
alert('Registration successful! Please login.');
|
|
} else {
|
|
this.showError('registerError', data.error);
|
|
}
|
|
});
|
|
|
|
this.querySelector('#showLoginLink').addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
this.showLogin();
|
|
});
|
|
}
|
|
|
|
showCreateRant() {
|
|
if (!authManager.isLoggedIn()) {
|
|
this.showLogin();
|
|
return;
|
|
}
|
|
|
|
this.setContent('Create Rant', `
|
|
<form id="createRantForm">
|
|
<div class="alert error" id="rantError"></div>
|
|
<div class="form-group">
|
|
<label>What's on your mind?</label>
|
|
<textarea name="rant" placeholder="Share your thoughts..." required></textarea>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Tags (comma separated)</label>
|
|
<input type="text" name="tags" placeholder="rant, javascript, devops">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Type</label>
|
|
<select name="type">
|
|
<option value="1">Rant</option>
|
|
<option value="2">Collab</option>
|
|
<option value="3">Question</option>
|
|
<option value="4">devRant</option>
|
|
<option value="5">Random</option>
|
|
</select>
|
|
</div>
|
|
<button type="submit" class="btn" style="width: 100%;">Post Rant</button>
|
|
</form>
|
|
`);
|
|
|
|
this.querySelector('#createRantForm').addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const formData = new FormData(e.target);
|
|
|
|
const data = await authManager.apiCall('/rant/rants', {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
|
|
if (data.success) {
|
|
this.close();
|
|
eventBus.emit('navigate', { view: 'rant', id: data.rant_id });
|
|
} else {
|
|
this.showError('rantError', data.error);
|
|
}
|
|
});
|
|
}
|
|
|
|
showEditProfile() {
|
|
this.setContent('Edit Profile', `
|
|
<form id="editProfileForm">
|
|
<div class="alert success" id="profileSuccess"></div>
|
|
<div class="form-group">
|
|
<label>About</label>
|
|
<textarea name="about" placeholder="Tell us about yourself..."></textarea>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Skills</label>
|
|
<input type="text" name="skills" placeholder="JavaScript, Python, DevOps">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Location</label>
|
|
<input type="text" name="location" placeholder="San Francisco, CA">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Website</label>
|
|
<input type="url" name="website" placeholder="https://example.com">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>GitHub Username</label>
|
|
<input type="text" name="github" placeholder="username">
|
|
</div>
|
|
<button type="submit" class="btn" style="width: 100%;">Update Profile</button>
|
|
</form>
|
|
`);
|
|
|
|
this.querySelector('#editProfileForm').addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const formData = new FormData(e.target);
|
|
|
|
// Add profile_ prefix to all fields
|
|
const profileData = new FormData();
|
|
for (let [key, value] of formData.entries()) {
|
|
profileData.append(`profile_${key}`, value);
|
|
}
|
|
|
|
const data = await authManager.apiCall('/users/me/edit-profile', {
|
|
method: 'POST',
|
|
body: profileData
|
|
});
|
|
|
|
if (data.success) {
|
|
this.showSuccess('profileSuccess', 'Profile updated successfully!');
|
|
setTimeout(() => {
|
|
this.close();
|
|
eventBus.emit('navigate', { view: 'profile', id: authManager.currentUser.id });
|
|
}, 1500);
|
|
}
|
|
});
|
|
}
|
|
|
|
showError(elementId, message) {
|
|
const errorEl = this.querySelector(`#${elementId}`);
|
|
if (errorEl) {
|
|
errorEl.textContent = message;
|
|
errorEl.classList.add('active');
|
|
}
|
|
}
|
|
|
|
showSuccess(elementId, message) {
|
|
const successEl = this.querySelector(`#${elementId}`);
|
|
if (successEl) {
|
|
successEl.textContent = message;
|
|
successEl.classList.add('active');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Floating Action Button Component
|
|
class RantFab extends HTMLElement {
|
|
connectedCallback() {
|
|
this.innerHTML = `
|
|
<button class="fab" id="createRantBtn" style="display: none;">+</button>
|
|
`;
|
|
|
|
this.querySelector('#createRantBtn').addEventListener('click', () => {
|
|
eventBus.emit('show-modal', 'create-rant');
|
|
});
|
|
|
|
eventBus.on('auth-changed', (data) => {
|
|
this.querySelector('#createRantBtn').style.display = data.isLoggedIn ? 'flex' : 'none';
|
|
});
|
|
|
|
// Initial check
|
|
if (authManager.isLoggedIn()) {
|
|
this.querySelector('#createRantBtn').style.display = 'flex';
|
|
}
|
|
}
|
|
}
|
|
|
|
// Main App Component
|
|
class RantApp extends HTMLElement {
|
|
constructor() {
|
|
super();
|
|
this.currentViewData = 'feed';
|
|
}
|
|
|
|
connectedCallback() {
|
|
eventBus.on('navigate', (viewData) => this.navigate(viewData));
|
|
eventBus.on('refresh-view', () => this.refreshView());
|
|
|
|
// Initial navigation
|
|
this.navigate('feed');
|
|
}
|
|
|
|
navigate(viewData) {
|
|
const view = typeof viewData === 'string' ? viewData : viewData.view;
|
|
const params = typeof viewData === 'object' ? viewData : {};
|
|
|
|
this.currentViewData = viewData;
|
|
const content = document.getElementById('content');
|
|
|
|
switch (view) {
|
|
case 'feed':
|
|
content.innerHTML = '<feed-view></feed-view>';
|
|
break;
|
|
case 'rant':
|
|
content.innerHTML = `<rant-detail-view rant-id="${params.id}"></rant-detail-view>`;
|
|
break;
|
|
case 'profile':
|
|
const userId = params.id || authManager.currentUser?.id;
|
|
content.innerHTML = `<profile-view user-id="${userId}"></profile-view>`;
|
|
break;
|
|
case 'search':
|
|
content.innerHTML = '<search-view></search-view>';
|
|
break;
|
|
case 'notifications':
|
|
if (authManager.isLoggedIn()) {
|
|
content.innerHTML = '<notifications-view></notifications-view>';
|
|
} else {
|
|
eventBus.emit('show-modal', 'login');
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
refreshView() {
|
|
this.navigate(this.currentViewData);
|
|
}
|
|
}
|
|
|
|
// Register all custom elements
|
|
customElements.define('rant-navigation', RantNavigation);
|
|
customElements.define('rant-card', RantCard);
|
|
customElements.define('loading-spinner', LoadingSpinner);
|
|
customElements.define('sort-options', SortOptions);
|
|
customElements.define('feed-view', FeedView);
|
|
customElements.define('rant-detail-view', RantDetailView);
|
|
customElements.define('comments-section', CommentsSection);
|
|
customElements.define('profile-view', ProfileView);
|
|
customElements.define('search-view', SearchView);
|
|
customElements.define('notifications-view', NotificationsView);
|
|
customElements.define('rant-modal', RantModal);
|
|
customElements.define('rant-fab', RantFab);
|
|
customElements.define('rant-app', RantApp);
|
|
|
|
// Initialize the app
|
|
const app = document.createElement('rant-app');
|
|
document.body.appendChild(app);
|
|
</script>
|
|
</body>
|
|
</html>
|