1421 lines
49 KiB
HTML
Raw Normal View History

2025-08-03 20:36:36 +02:00
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DevRant 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 -->
<nav>
<div class="nav-container">
<a href="#" class="logo" onclick="showFeed()">DevRant</a>
<div class="nav-links">
<a href="#" onclick="showFeed()">Feed</a>
<a href="#" onclick="showSearch()">Search</a>
<span id="navProfile" style="display: none;">
<a href="#" onclick="showMyProfile()">Profile</a>
<a href="#" onclick="showNotifications()">Notifications <span id="notifCount"></span></a>
</span>
<span id="navAuth">
<button class="btn btn-secondary" onclick="showLogin()">Login</button>
<button class="btn" onclick="showRegister()">Sign Up</button>
</span>
<span id="navUser" style="display: none;">
<button class="btn btn-secondary" onclick="logout()">Logout</button>
</span>
</div>
</div>
</nav>
<!-- Main Content -->
<div id="content" class="container">
<!-- Feed will be loaded here -->
</div>
<!-- Floating Action Button -->
<button class="fab" id="createRantBtn" style="display: none;" onclick="showCreateRant()">+</button>
<!-- Modals -->
<div id="modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2 id="modalTitle">Modal Title</h2>
<button class="close-btn" onclick="closeModal()">&times;</button>
</div>
<div id="modalBody">
<!-- Modal content will be loaded here -->
</div>
</div>
</div>
<script>
// Global state
let authToken = null;
let currentUser = null;
let currentSort = 'recent';
let currentView = 'feed';
// API Configuration
2025-08-03 20:43:26 +02:00
const API_URL = '/api';
2025-08-03 20:36:36 +02:00
const APP_ID = 3;
// Initialize
document.addEventListener('DOMContentLoaded', () => {
checkAuth();
showFeed();
});
// Auth functions
function checkAuth() {
const token = localStorage.getItem('authToken');
if (token) {
authToken = JSON.parse(token);
currentUser = {
id: authToken.user_id,
token_id: authToken.id,
token_key: authToken.key
};
updateNavigation(true);
checkNotifications();
} else {
updateNavigation(false);
}
}
function updateNavigation(isLoggedIn) {
document.getElementById('navAuth').style.display = isLoggedIn ? 'none' : 'inline';
document.getElementById('navUser').style.display = isLoggedIn ? 'inline' : 'none';
document.getElementById('navProfile').style.display = isLoggedIn ? 'inline' : 'none';
document.getElementById('createRantBtn').style.display = isLoggedIn ? 'flex' : 'none';
}
async function apiCall(endpoint, options = {}) {
const url = `${API_URL}${endpoint}`;
// Add auth to FormData or URLSearchParams if logged in
if (currentUser && options.body) {
if (options.body instanceof FormData) {
options.body.append('app', APP_ID);
options.body.append('token_id', currentUser.token_id);
options.body.append('token_key', currentUser.token_key);
options.body.append('user_id', currentUser.id);
} else if (options.body instanceof URLSearchParams) {
options.body.append('app', APP_ID);
options.body.append('token_id', currentUser.token_id);
options.body.append('token_key', currentUser.token_key);
options.body.append('user_id', currentUser.id);
}
}
// Add auth to query params for GET requests
if (currentUser && options.method === 'GET' || !options.method) {
const separator = endpoint.includes('?') ? '&' : '?';
endpoint += `${separator}app=${APP_ID}&token_id=${currentUser.token_id}&token_key=${currentUser.token_key}&user_id=${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 };
}
}
// View functions
async function showFeed(sort = 'recent') {
currentView = 'feed';
currentSort = sort;
const content = document.getElementById('content');
content.innerHTML = `
<div class="sort-options">
<button class="sort-btn ${sort === 'recent' ? 'active' : ''}" onclick="showFeed('recent')">Recent</button>
<button class="sort-btn ${sort === 'top' ? 'active' : ''}" onclick="showFeed('top')">Top</button>
<button class="sort-btn ${sort === 'algo' ? 'active' : ''}" onclick="showFeed('algo')">Algorithm</button>
</div>
<div class="loading">
<div class="spinner"></div>
<p>Loading rants...</p>
</div>
`;
const params = new URLSearchParams({ sort, limit: 50, skip: 0, app: APP_ID });
const data = await apiCall(`/devrant/rants?${params}`);
if (data.success) {
content.innerHTML = `
<div class="sort-options">
<button class="sort-btn ${sort === 'recent' ? 'active' : ''}" onclick="showFeed('recent')">Recent</button>
<button class="sort-btn ${sort === 'top' ? 'active' : ''}" onclick="showFeed('top')">Top</button>
<button class="sort-btn ${sort === 'algo' ? 'active' : ''}" onclick="showFeed('algo')">Algorithm</button>
</div>
`;
data.rants.forEach(rant => {
content.innerHTML += createRantCard(rant);
});
}
}
function createRantCard(rant) {
const tags = rant.tags.map(tag => `<span class="tag">${tag}</span>`).join('');
const voteClass = rant.vote_state === 1 ? 'voted' : rant.vote_state === -1 ? 'downvoted' : '';
return `
<div class="rant-card" onclick="showRant(${rant.id})">
<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">${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}" onclick="event.stopPropagation(); voteRant(${rant.id}, ${rant.vote_state === 1 ? 0 : 1})">
++ ${rant.score}
</button>
<button class="action-btn" onclick="event.stopPropagation(); showRant(${rant.id})">
💬 ${rant.num_comments}
</button>
</div>
<div>${formatTime(rant.created_time)}</div>
</div>
</div>
`;
}
async function showRant(rantId) {
currentView = 'rant';
const content = document.getElementById('content');
content.innerHTML = `
<div class="loading">
<div class="spinner"></div>
<p>Loading rant...</p>
</div>
`;
const params = new URLSearchParams({ app: APP_ID });
const data = await apiCall(`/devrant/rants/${rantId}?${params}`);
if (data.success) {
const rant = data.rant;
const tags = rant.tags.map(tag => `<span class="tag">${tag}</span>`).join('');
const voteClass = rant.vote_state === 1 ? 'voted' : rant.vote_state === -1 ? 'downvoted' : '';
content.innerHTML = `
<button class="btn btn-secondary" onclick="showFeed()">← 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" onclick="showProfile(${rant.user_id})" style="cursor: pointer;">${rant.user_username}</div>
<div class="score">${rant.user_score} points</div>
</div>
${currentUser && currentUser.id === rant.user_id ? `
<button class="btn btn-secondary" onclick="editRant(${rant.id})">Edit</button>
<button class="btn btn-secondary" onclick="deleteRant(${rant.id})" 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}" onclick="voteRant(${rant.id}, ${rant.vote_state === 1 ? 0 : 1})">
++ ${rant.score}
</button>
<button class="action-btn ${rant.vote_state === -1 ? 'downvoted' : ''}" onclick="voteRant(${rant.id}, ${rant.vote_state === -1 ? 0 : -1})">
--
</button>
<button class="action-btn ${data.subscribed ? 'voted' : ''}" onclick="toggleFavorite(${rant.id}, ${data.subscribed})">
${data.subscribed ? '★' : '☆'} Favorite
</button>
</div>
<div>${formatTime(rant.created_time)}</div>
</div>
</div>
<div class="comments-section">
<h3>Comments (${data.comments.length})</h3>
<div id="commentsList">
${data.comments.map(comment => createCommentHTML(comment)).join('')}
</div>
${currentUser ? `
<div class="comment-form">
<h4>Add a comment</h4>
<form onsubmit="postComment(event, ${rant.id})">
<div class="form-group">
<textarea name="comment" placeholder="Write your comment..." required></textarea>
</div>
<button type="submit" class="btn">Post Comment</button>
</form>
</div>
` : '<p style="text-align: center; margin-top: 2rem;">Login to comment</p>'}
</div>
`;
}
}
function createCommentHTML(comment) {
const voteClass = comment.vote_state === 1 ? 'voted' : comment.vote_state === -1 ? 'downvoted' : '';
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" onclick="showProfile(${comment.user_id})" style="cursor: pointer;">${comment.user_username}</div>
<div class="score">${comment.user_score} points</div>
</div>
${currentUser && currentUser.id === comment.user_id ? `
<button class="btn btn-secondary" onclick="deleteComment(${comment.id}, ${comment.rant_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}" onclick="voteComment(${comment.id}, ${comment.vote_state === 1 ? 0 : 1})">
++ ${comment.score}
</button>
</div>
<div>${formatTime(comment.created_time)}</div>
</div>
</div>
`;
}
async function showProfile(userId) {
currentView = 'profile';
const content = document.getElementById('content');
content.innerHTML = `
<div class="loading">
<div class="spinner"></div>
<p>Loading profile...</p>
</div>
`;
const params = new URLSearchParams({ app: APP_ID });
const data = await apiCall(`/users/${userId}?${params}`);
if (data.success) {
const profile = data.profile;
const isOwnProfile = currentUser && currentUser.id === userId;
content.innerHTML = `
<button class="btn btn-secondary" onclick="showFeed()">← 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" onclick="showEditProfile()" style="margin-top: 1rem;">Edit Profile</button>` : ''}
</div>
<div class="tabs">
<button class="tab active" onclick="showProfileTab('rants', ${userId})">Rants</button>
<button class="tab" onclick="showProfileTab('comments', ${userId})">Comments</button>
<button class="tab" onclick="showProfileTab('favorites', ${userId})">Favorites</button>
</div>
<div id="profileContent">
${profile.content.content.rants.map(rant => createRantCard(rant)).join('')}
</div>
`;
}
}
async function showProfileTab(tab, userId) {
// Update active tab
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
event.target.classList.add('active');
const params = new URLSearchParams({ app: APP_ID });
const data = await apiCall(`/users/${userId}?${params}`);
if (data.success) {
const profile = data.profile;
const contentDiv = document.getElementById('profileContent');
if (tab === 'rants') {
contentDiv.innerHTML = profile.content.content.rants.map(rant => createRantCard(rant)).join('');
} else if (tab === 'comments') {
contentDiv.innerHTML = profile.content.content.comments.map(comment => `
<div class="rant-card" onclick="showRant(${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 (tab === 'favorites') {
contentDiv.innerHTML = profile.content.content.favorites.map(rant => createRantCard(rant)).join('');
}
}
}
function showMyProfile() {
if (currentUser) {
showProfile(currentUser.id);
}
}
function showSearch() {
currentView = 'search';
const content = document.getElementById('content');
content.innerHTML = `
<div class="search-box">
<form class="search-form" onsubmit="search(event)">
<input type="text" name="term" placeholder="Search rants..." required>
<button type="submit" class="btn">Search</button>
</form>
</div>
<div id="searchResults"></div>
`;
}
async function search(event) {
event.preventDefault();
const term = event.target.term.value;
const resultsDiv = document.getElementById('searchResults');
resultsDiv.innerHTML = `
<div class="loading">
<div class="spinner"></div>
<p>Searching...</p>
</div>
`;
const params = new URLSearchParams({ term, app: APP_ID });
const data = await apiCall(`/devrant/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.map(rant => createRantCard(rant)).join('');
}
}
}
async function showNotifications() {
currentView = 'notifications';
const content = document.getElementById('content');
content.innerHTML = `
<div class="loading">
<div class="spinner"></div>
<p>Loading notifications...</p>
</div>
`;
const params = new URLSearchParams({
ext_prof: 1,
last_time: Math.floor(Date.now() / 1000) - 86400,
app: APP_ID
});
const data = await apiCall(`/users/me/notif-feed?${params}`);
if (data.success) {
const items = data.data.items;
content.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" onclick="showRant(${notif.rant_id})">
<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('')}
`;
// Update notification count
updateNotificationCount(0);
}
}
async function checkNotifications() {
if (!currentUser) return;
const params = new URLSearchParams({
ext_prof: 1,
last_time: Math.floor(Date.now() / 1000) - 86400,
app: APP_ID
});
const data = await apiCall(`/users/me/notif-feed?${params}`);
if (data.success) {
updateNotificationCount(data.data.num_unread);
}
}
function updateNotificationCount(count) {
const notifCount = document.getElementById('notifCount');
if (count > 0) {
notifCount.textContent = `(${count})`;
notifCount.style.color = 'var(--error)';
} else {
notifCount.textContent = '';
}
}
// Modal functions
function showModal(title, content) {
document.getElementById('modalTitle').textContent = title;
document.getElementById('modalBody').innerHTML = content;
document.getElementById('modal').classList.add('active');
}
function closeModal() {
document.getElementById('modal').classList.remove('active');
}
function showLogin() {
showModal('Login', `
<form onsubmit="login(event)">
<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="#" onclick="showRegister()">Sign up</a>
</p>
</form>
`);
}
function showRegister() {
showModal('Sign Up', `
<form onsubmit="register(event)">
<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="#" onclick="showLogin()">Login</a>
</p>
</form>
`);
}
function showCreateRant() {
if (!currentUser) {
showLogin();
return;
}
showModal('Create Rant', `
<form onsubmit="createRant(event)">
<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>
`);
}
function showEditProfile() {
showModal('Edit Profile', `
<form onsubmit="updateProfile(event)">
<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>
`);
}
// Action functions
async function login(event) {
event.preventDefault();
const formData = new FormData(event.target);
formData.append('app', APP_ID);
const data = await apiCall('/users/auth-token', {
method: 'POST',
body: formData
});
if (data.success) {
authToken = data.auth_token;
currentUser = {
id: authToken.user_id,
token_id: authToken.id,
token_key: authToken.key
};
localStorage.setItem('authToken', JSON.stringify(authToken));
updateNavigation(true);
closeModal();
showFeed();
checkNotifications();
} else {
showError('loginError', data.error);
}
}
async function register(event) {
event.preventDefault();
const formData = new FormData(event.target);
formData.append('app', APP_ID);
formData.append('type', 1);
const data = await apiCall('/users', {
method: 'POST',
body: formData
});
if (data.success) {
closeModal();
showLogin();
alert('Registration successful! Please login.');
} else {
showError('registerError', data.error);
}
}
function logout() {
localStorage.removeItem('authToken');
authToken = null;
currentUser = null;
updateNavigation(false);
showFeed();
}
async function createRant(event) {
event.preventDefault();
const formData = new FormData(event.target);
const data = await apiCall('/devrant/rants', {
method: 'POST',
body: formData
});
if (data.success) {
closeModal();
showRant(data.rant_id);
} else {
showError('rantError', data.error);
}
}
async function voteRant(rantId, vote) {
if (!currentUser) {
showLogin();
return;
}
const formData = new FormData();
formData.append('vote', vote);
if (vote === -1) {
formData.append('reason', 0); // Not for me
}
const data = await apiCall(`/devrant/rants/${rantId}/vote`, {
method: 'POST',
body: formData
});
if (data.success && currentView === 'rant') {
showRant(rantId);
} else if (data.success) {
showFeed(currentSort);
}
}
async function voteComment(commentId, vote) {
if (!currentUser) {
showLogin();
return;
}
const formData = new FormData();
formData.append('vote', vote);
const data = await apiCall(`/comments/${commentId}/vote`, {
method: 'POST',
body: formData
});
if (data.success && currentView === 'rant') {
// Refresh current rant view
const rantCard = document.querySelector('.rant-card');
if (rantCard) {
const rantId = parseInt(rantCard.getAttribute('onclick').match(/\d+/)[0]);
showRant(rantId);
}
}
}
async function toggleFavorite(rantId, isFavorited) {
if (!currentUser) {
showLogin();
return;
}
const endpoint = isFavorited ? 'unfavorite' : 'favorite';
const formData = new FormData();
const data = await apiCall(`/devrant/rants/${rantId}/${endpoint}`, {
method: 'POST',
body: formData
});
if (data.success) {
showRant(rantId);
}
}
async function postComment(event, rantId) {
event.preventDefault();
const formData = new FormData(event.target);
const data = await apiCall(`/devrant/rants/${rantId}/comments`, {
method: 'POST',
body: formData
});
if (data.success) {
showRant(rantId);
}
}
async function deleteRant(rantId) {
if (!confirm('Are you sure you want to delete this rant?')) return;
const params = new URLSearchParams();
params.append('app', APP_ID);
params.append('token_id', currentUser.token_id);
params.append('token_key', currentUser.token_key);
params.append('user_id', currentUser.id);
const data = await apiCall(`/devrant/rants/${rantId}?${params}`, {
method: 'DELETE'
});
if (data.success) {
showFeed();
}
}
async function deleteComment(commentId, rantId) {
if (!confirm('Are you sure you want to delete this comment?')) return;
const params = new URLSearchParams();
params.append('app', APP_ID);
params.append('token_id', currentUser.token_id);
params.append('token_key', currentUser.token_key);
params.append('user_id', currentUser.id);
const data = await apiCall(`/comments/${commentId}?${params}`, {
method: 'DELETE'
});
if (data.success) {
showRant(rantId);
}
}
async function updateProfile(event) {
event.preventDefault();
const formData = new FormData(event.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 apiCall('/users/me/edit-profile', {
method: 'POST',
body: profileData
});
if (data.success) {
showSuccess('profileSuccess', 'Profile updated successfully!');
setTimeout(() => {
closeModal();
showMyProfile();
}, 1500);
}
}
// 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();
}
function showError(elementId, message) {
const errorEl = document.getElementById(elementId);
if (errorEl) {
errorEl.textContent = message;
errorEl.classList.add('active');
}
}
function showSuccess(elementId, message) {
const successEl = document.getElementById(elementId);
if (successEl) {
successEl.textContent = message;
successEl.classList.add('active');
}
}
// Poll for notifications every minute
setInterval(() => {
if (currentUser) {
checkNotifications();
}
}, 60000);
</script>
</body>
</html>