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()">×</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';
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2025-08-04 00:40:37 +02:00
|
|
|
async function apiCall(endpoint, options = {}) {
|
|
|
|
let 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('?') ? '&' : '?';
|
|
|
|
url += `${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 };
|
|
|
|
}
|
|
|
|
}
|
2025-08-03 20:36:36 +02:00
|
|
|
|
|
|
|
|
|
|
|
// 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('');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-08-04 00:40:37 +02:00
|
|
|
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
|
|
|
|
});
|
|
|
|
|
|
|
|
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})" 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>
|
2025-08-03 20:36:36 +02:00
|
|
|
</div>
|
2025-08-04 00:40:37 +02:00
|
|
|
`).join('')}
|
|
|
|
`;
|
|
|
|
|
|
|
|
// Update notification count
|
|
|
|
updateNotificationCount(0);
|
|
|
|
} else {
|
|
|
|
content.innerHTML = `
|
|
|
|
<h2>Notifications</h2>
|
|
|
|
<p style="text-align: center; color: var(--error); margin-top: 2rem;">Failed to load notifications: ${data.error || 'Unknown error'}</p>
|
|
|
|
`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function checkNotifications() {
|
|
|
|
if (!currentUser) return;
|
|
|
|
|
|
|
|
const params = new URLSearchParams({
|
|
|
|
ext_prof: 1,
|
|
|
|
last_time: Math.floor(Date.now() / 1000) - 86400
|
|
|
|
});
|
|
|
|
|
|
|
|
const data = await apiCall(`/users/me/notif-feed?${params}`);
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
updateNotificationCount(data.data.num_unread);
|
|
|
|
}
|
|
|
|
}
|
2025-08-03 20:36:36 +02:00
|
|
|
|
|
|
|
|
|
|
|
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>
|