|
|
| (20 intermediate revisions by the same user not shown) |
| Line 1: |
Line 1: |
| <html>
| | <div id="social-app"> |
| <div id="social-feed-app"> | | <div style="text-align:center;padding:60px;color:#8e8e8e"> |
| <style> | | <div style="font-size:48px;margin-bottom:16px">⏳</div> |
| #social-feed-app * { box-sizing: border-box; }
| | <div>Loading Social Feed...</div> |
| #social-feed-app {
| |
| max-width: 680px;
| |
| margin: 0 auto;
| |
| font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif;
| |
| background: #f0f2f5;
| |
| min-height: 100vh;
| |
| padding: 20px;
| |
| }
| |
| .sf-composer {
| |
| background: white;
| |
| border-radius: 12px;
| |
| box-shadow: 0 1px 3px rgba(0,0,0,0.1);
| |
| margin-bottom: 20px;
| |
| overflow: hidden;
| |
| }
| |
| .sf-composer-main {
| |
| display: flex;
| |
| padding: 16px 20px;
| |
| gap: 12px;
| |
| }
| |
| .sf-avatar {
| |
| width: 48px;
| |
| height: 48px;
| |
| border-radius: 50%;
| |
| object-fit: cover;
| |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
| |
| display: flex;
| |
| align-items: center;
| |
| justify-content: center;
| |
| color: white;
| |
| font-weight: 600;
| |
| font-size: 18px;
| |
| flex-shrink: 0;
| |
| cursor: pointer;
| |
| position: relative;
| |
| overflow: hidden;
| |
| }
| |
| .sf-avatar img {
| |
| width: 100%;
| |
| height: 100%;
| |
| object-fit: cover;
| |
| border-radius: 50%;
| |
| }
| |
| .sf-avatar-small { width: 36px; height: 36px; font-size: 14px; }
| |
| .sf-avatar-edit {
| |
| position: absolute;
| |
| bottom: 0;
| |
| left: 0;
| |
| right: 0;
| |
| background: rgba(0,0,0,0.6);
| |
| color: white;
| |
| font-size: 10px;
| |
| padding: 2px;
| |
| text-align: center;
| |
| opacity: 0;
| |
| transition: opacity 0.2s;
| |
| }
| |
| .sf-avatar:hover .sf-avatar-edit { opacity: 1; }
| |
| .sf-composer-input {
| |
| flex: 1;
| |
| background: #f0f2f5;
| |
| border: none;
| |
| border-radius: 20px;
| |
| padding: 14px 20px;
| |
| font-size: 17px;
| |
| resize: none;
| |
| min-height: 80px;
| |
| outline: none;
| |
| color: #050505;
| |
| font-family: inherit;
| |
| }
| |
| .sf-composer-input::placeholder { color: #65676b; }
| |
| .sf-composer-actions {
| |
| display: flex;
| |
| align-items: center;
| |
| justify-content: space-between;
| |
| padding: 12px 20px;
| |
| border-top: 1px solid #e4e6eb;
| |
| gap: 12px;
| |
| }
| |
| .sf-action-buttons { display: flex; gap: 8px; }
| |
| .sf-action-btn {
| |
| width: 40px;
| |
| height: 40px;
| |
| border-radius: 8px;
| |
| border: none;
| |
| background: #f0f2f5;
| |
| cursor: pointer;
| |
| font-size: 18px;
| |
| }
| |
| .sf-action-btn:hover { background: #e4e6eb; }
| |
| .sf-post-btn {
| |
| background: linear-gradient(135deg, #1877f2 0%, #0d65d9 100%);
| |
| color: white;
| |
| border: none;
| |
| border-radius: 8px;
| |
| padding: 10px 24px;
| |
| font-size: 15px;
| |
| font-weight: 600;
| |
| cursor: pointer;
| |
| }
| |
| .sf-post-btn:disabled { opacity: 0.5; }
| |
| .sf-post {
| |
| background: white;
| |
| border-radius: 12px;
| |
| box-shadow: 0 1px 3px rgba(0,0,0,0.1);
| |
| margin-bottom: 16px;
| |
| }
| |
| .sf-post-header {
| |
| display: flex;
| |
| align-items: center;
| |
| padding: 16px;
| |
| gap: 12px;
| |
| }
| |
| .sf-post-user-info { flex: 1; }
| |
| .sf-post-username { font-weight: 600; color: #050505; font-size: 15px; }
| |
| .sf-post-meta { font-size: 13px; color: #65676b; }
| |
| .sf-post-menu { background: none; border: none; font-size: 18px; color: #65676b; cursor: pointer; padding: 8px; border-radius: 50%; }
| |
| .sf-post-menu:hover { background: #f0f2f5; }
| |
| .sf-post-content { padding: 0 16px 16px; font-size: 15px; line-height: 1.5; color: #050505; white-space: pre-wrap; }
| |
| .sf-post-image { width: 100%; max-height: 500px; object-fit: cover; }
| |
| .sf-post-stats { display: flex; justify-content: space-between; padding: 12px 16px; font-size: 14px; color: #65676b; border-bottom: 1px solid #e4e6eb; }
| |
| .sf-post-stats-left { display: flex; align-items: center; gap: 6px; }
| |
| .sf-reaction-icons { display: flex; }
| |
| .sf-reaction-icon { width: 20px; height: 20px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 12px; margin-left: -4px; }
| |
| .sf-reaction-icon:first-child { margin-left: 0; }
| |
| .sf-reaction-icon.like { background: #1877f2; color: white; }
| |
| .sf-reaction-icon.love { background: #e91e63; color: white; }
| |
| .sf-post-actions { display: flex; padding: 4px 16px; }
| |
| .sf-post-action { flex: 1; display: flex; align-items: center; justify-content: center; gap: 8px; padding: 12px; background: none; border: none; font-size: 15px; font-weight: 600; color: #65676b; cursor: pointer; border-radius: 8px; }
| |
| .sf-post-action:hover { background: #f0f2f5; }
| |
| .sf-post-action.liked { color: #1877f2; }
| |
| .sf-comments { padding: 12px 16px; background: #f7f8fa; }
| |
| .sf-comment { display: flex; gap: 8px; margin-bottom: 12px; }
| |
| .sf-comment-bubble { background: white; border-radius: 18px; padding: 10px 14px; }
| |
| .sf-comment-author { font-weight: 600; font-size: 13px; }
| |
| .sf-comment-text { font-size: 14px; }
| |
| .sf-add-comment { display: flex; gap: 8px; align-items: center; }
| |
| .sf-comment-input { flex: 1; background: white; border: none; border-radius: 20px; padding: 10px 16px; font-size: 14px; outline: none; }
| |
| .sf-empty { text-align: center; padding: 60px 20px; background: white; border-radius: 12px; color: #65676b; }
| |
| .sf-empty-icon { font-size: 64px; margin-bottom: 16px; }
| |
| .sf-empty-text { font-size: 18px; font-weight: 500; }
| |
| .sf-empty-sub { font-size: 14px; margin-top: 8px; color: #8a8d91; }
| |
| .sf-stories { display: flex; gap: 12px; padding: 16px; background: white; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 20px; overflow-x: auto; }
| |
| .sf-story { display: flex; flex-direction: column; align-items: center; gap: 8px; cursor: pointer; }
| |
| .sf-story-ring { width: 68px; height: 68px; border-radius: 50%; padding: 3px; background: linear-gradient(45deg, #f09433, #e6683c, #dc2743, #cc2366, #bc1888); }
| |
| .sf-story-avatar { width: 100%; height: 100%; border-radius: 50%; border: 3px solid white; object-fit: cover; }
| |
| .sf-story-name { font-size: 12px; color: #050505; }
| |
| .sf-create-story { width: 68px; height: 68px; border-radius: 50%; background: #e4e6eb; display: flex; align-items: center; justify-content: center; font-size: 28px; color: #1877f2; cursor: pointer; border: none; }
| |
| .sf-create-story:hover { background: #d8dadf; }
| |
| .sf-modal { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.6); z-index: 1000; align-items: center; justify-content: center; }
| |
| .sf-modal.active { display: flex; }
| |
| .sf-modal-content { background: white; border-radius: 12px; padding: 24px; max-width: 400px; width: 90%; }
| |
| .sf-modal-title { font-size: 20px; font-weight: 600; margin-bottom: 16px; }
| |
| .sf-modal-input { width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 8px; font-size: 14px; margin-bottom: 12px; }
| |
| .sf-modal-buttons { display: flex; gap: 12px; justify-content: flex-end; }
| |
| .sf-modal-btn { padding: 10px 20px; border-radius: 8px; border: none; font-weight: 600; cursor: pointer; }
| |
| .sf-modal-btn-cancel { background: #e4e6eb; color: #050505; }
| |
| .sf-modal-btn-save { background: #1877f2; color: white; }
| |
| </style> | |
| | |
| <div class="sf-stories" id="stories-container">
| |
| <div class="sf-story">
| |
| <button class="sf-create-story">+</button>
| |
| <span class="sf-story-name">Add Story</span>
| |
| </div>
| |
| </div> | | </div> |
|
| |
| <div class="sf-composer">
| |
| <div class="sf-composer-main">
| |
| <div class="sf-avatar" id="composer-avatar" onclick="openProfileModal()">
| |
| <span id="avatar-initials"></span>
| |
| <div class="sf-avatar-edit">Edit</div>
| |
| </div>
| |
| <textarea class="sf-composer-input" id="post-content" placeholder="What is on your mind?"></textarea>
| |
| </div>
| |
| <div class="sf-composer-actions">
| |
| <div class="sf-action-buttons">
| |
| <button class="sf-action-btn" title="Add Image" onclick="promptImage()">🖼️</button>
| |
| <button class="sf-action-btn" title="Emoji">😊</button>
| |
| <button class="sf-action-btn" title="GIF">🎬</button>
| |
| <button class="sf-action-btn" title="Location">📍</button>
| |
| </div>
| |
| <button class="sf-post-btn" id="post-btn">Post</button>
| |
| </div>
| |
| </div> | | </div> |
|
| |
| <div id="image-preview" style="display:none; padding: 0 20px 16px; background: white; margin-top: -20px; border-radius: 0 0 12px 12px;">
| |
| <div style="position: relative; display: inline-block;">
| |
| <img id="preview-img" style="max-width: 100%; max-height: 300px; border-radius: 8px;">
| |
| <button onclick="removeImage()" style="position: absolute; top: 8px; right: 8px; background: rgba(0,0,0,0.6); color: white; border: none; border-radius: 50%; width: 28px; height: 28px; cursor: pointer;">×</button>
| |
| </div>
| |
| </div>
| |
|
| |
| <div id="timeline-container">
| |
| <div class="sf-empty">
| |
| <div class="sf-empty-icon">📝</div>
| |
| <div class="sf-empty-text">No posts yet</div>
| |
| <div class="sf-empty-sub">Be the first to share something!</div>
| |
| </div>
| |
| </div>
| |
|
| |
| <div class="sf-modal" id="profile-modal">
| |
| <div class="sf-modal-content">
| |
| <div class="sf-modal-title">Set Profile Photo</div>
| |
| <input type="text" class="sf-modal-input" id="profile-url-input" placeholder="Enter image URL...">
| |
| <p style="font-size: 12px; color: #65676b; margin: 0 0 12px;">Tip: Upload an image to your wiki, then paste the URL here</p>
| |
| <div class="sf-modal-buttons">
| |
| <button class="sf-modal-btn sf-modal-btn-cancel" onclick="closeProfileModal()">Cancel</button>
| |
| <button class="sf-modal-btn sf-modal-btn-save" onclick="saveProfilePhoto()">Save</button>
| |
| </div>
| |
| </div>
| |
| </div>
| |
|
| |
| <script>
| |
| (function() {
| |
| var currentUser = mw.config.get("wgUserName") || "Guest";
| |
| var profilePhoto = localStorage.getItem("sf_profile_photo_" + currentUser) || "";
| |
| var pendingImageUrl = "";
| |
|
| |
| function getInitials(name) {
| |
| return name.split(/[\s_]+/).map(function(w) { return w[0]; }).join("").substring(0,2).toUpperCase();
| |
| }
| |
|
| |
| function setAvatar(container, photo, name) {
| |
| if (photo) {
| |
| container.innerHTML = "<img src=\"" + photo + "\" onerror=\"this.parentNode.innerHTML= + getInitials(name) + \"><div class=\"sf-avatar-edit\">Edit</div>";
| |
| } else {
| |
| container.innerHTML = getInitials(name) + "<div class=\"sf-avatar-edit\">Edit</div>";
| |
| }
| |
| }
| |
|
| |
| // Initialize
| |
| var composerAvatar = document.getElementById("composer-avatar");
| |
| setAvatar(composerAvatar, profilePhoto, currentUser);
| |
| document.getElementById("post-content").placeholder = "What is on your mind, " + currentUser + "?";
| |
|
| |
| document.getElementById("post-btn").addEventListener("click", createPost);
| |
|
| |
| loadPosts();
| |
|
| |
| window.openProfileModal = function() {
| |
| document.getElementById("profile-modal").classList.add("active");
| |
| document.getElementById("profile-url-input").value = profilePhoto;
| |
| };
| |
|
| |
| window.closeProfileModal = function() {
| |
| document.getElementById("profile-modal").classList.remove("active");
| |
| };
| |
|
| |
| window.saveProfilePhoto = function() {
| |
| var url = document.getElementById("profile-url-input").value.trim();
| |
| profilePhoto = url;
| |
| localStorage.setItem("sf_profile_photo_" + currentUser, url);
| |
| setAvatar(composerAvatar, profilePhoto, currentUser);
| |
| closeProfileModal();
| |
| };
| |
|
| |
| window.promptImage = function() {
| |
| var url = prompt("Enter image URL:");
| |
| if (url) {
| |
| pendingImageUrl = url;
| |
| document.getElementById("preview-img").src = url;
| |
| document.getElementById("image-preview").style.display = "block";
| |
| }
| |
| };
| |
|
| |
| window.removeImage = function() {
| |
| pendingImageUrl = "";
| |
| document.getElementById("image-preview").style.display = "none";
| |
| };
| |
|
| |
| function loadPosts() {
| |
| new mw.Api().get({
| |
| action: "socialfeed",
| |
| sfaction: "getposts",
| |
| limit: 20
| |
| }).done(function(data) {
| |
| renderPosts(data.socialfeed.posts || []);
| |
| }).fail(function(code, data) {
| |
| console.error("API Error:", code, data);
| |
| document.getElementById("timeline-container").innerHTML = "<div class=\"sf-empty\"><div class=\"sf-empty-icon\">📝</div><div class=\"sf-empty-text\">No posts yet</div><div class=\"sf-empty-sub\">Be the first to share something!</div></div>";
| |
| });
| |
| }
| |
|
| |
| function renderPosts(posts) {
| |
| var container = document.getElementById("timeline-container");
| |
| if (posts.length === 0) {
| |
| container.innerHTML = "<div class=\"sf-empty\"><div class=\"sf-empty-icon\">📝</div><div class=\"sf-empty-text\">No posts yet</div><div class=\"sf-empty-sub\">Be the first to share something!</div></div>";
| |
| return;
| |
| }
| |
| var html = "";
| |
| posts.forEach(function(post) {
| |
| var timeAgo = getTimeAgo(new Date(post.created));
| |
| var totalReactions = 0;
| |
| for (var k in post.reaction_counts) totalReactions += post.reaction_counts[k];
| |
| var userPhoto = localStorage.getItem("sf_profile_photo_" + post.username) || "";
| |
| var avatarContent = userPhoto ? "<img src=\"" + userPhoto + "\">" : getInitials(post.username);
| |
|
| |
| html += "<div class=\"sf-post\" data-id=\"" + post.id + "\">";
| |
| html += "<div class=\"sf-post-header\"><div class=\"sf-avatar sf-avatar-small\">" + avatarContent + "</div><div class=\"sf-post-user-info\"><div class=\"sf-post-username\">" + escapeHtml(post.username) + "</div><div class=\"sf-post-meta\">" + timeAgo + " · 🌐</div></div>";
| |
| if (post.username === currentUser) html += "<button class=\"sf-post-menu\" onclick=\"deletePost(" + post.id + ")\">🗑️</button>";
| |
| html += "</div>";
| |
| html += "<div class=\"sf-post-content\">" + escapeHtml(post.content) + "</div>";
| |
| if (post.image_url) html += "<img class=\"sf-post-image\" src=\"" + post.image_url + "\">";
| |
| html += "<div class=\"sf-post-stats\"><div class=\"sf-post-stats-left\">";
| |
| if (totalReactions > 0) {
| |
| html += "<div class=\"sf-reaction-icons\">";
| |
| if (post.reaction_counts && post.reaction_counts.like) html += "<span class=\"sf-reaction-icon like\">👍</span>";
| |
| if (post.reaction_counts && post.reaction_counts.love) html += "<span class=\"sf-reaction-icon love\">❤️</span>";
| |
| html += "</div><span>" + totalReactions + "</span>";
| |
| }
| |
| html += "</div><span>" + post.comments + " comments</span></div>";
| |
| html += "<div class=\"sf-post-actions\">";
| |
| html += "<button class=\"sf-post-action " + (post.user_reaction === "like" ? "liked" : "") + "\" onclick=\"toggleReaction(" + post.id + ")\">👍 Like</button>";
| |
| html += "<button class=\"sf-post-action\" onclick=\"toggleComments(" + post.id + ")\">💬 Comment</button>";
| |
| html += "<button class=\"sf-post-action\">↗️ Share</button></div>";
| |
| html += "<div class=\"sf-comments\" id=\"comments-" + post.id + "\" style=\"display:none\"></div></div>";
| |
| });
| |
| container.innerHTML = html;
| |
| }
| |
|
| |
| window.createPost = function() {
| |
| var content = document.getElementById("post-content").value.trim();
| |
| if (!content && !pendingImageUrl) return;
| |
| document.getElementById("post-btn").disabled = true;
| |
|
| |
| var params = {
| |
| action: "socialfeed",
| |
| sfaction: "createpost",
| |
| content: content || "(photo)"
| |
| };
| |
| if (pendingImageUrl) params.image_url = pendingImageUrl;
| |
|
| |
| new mw.Api().postWithToken("csrf", params).done(function() {
| |
| document.getElementById("post-content").value = "";
| |
| pendingImageUrl = "";
| |
| document.getElementById("image-preview").style.display = "none";
| |
| document.getElementById("post-btn").disabled = false;
| |
| loadPosts();
| |
| }).fail(function(c, d) {
| |
| alert("Error: " + (d && d.error ? d.error.info : c));
| |
| document.getElementById("post-btn").disabled = false;
| |
| });
| |
| };
| |
|
| |
| window.deletePost = function(id) {
| |
| if (!confirm("Delete this post?")) return;
| |
| new mw.Api().postWithToken("csrf", { action: "socialfeed", sfaction: "deletepost", post_id: id }).done(loadPosts);
| |
| };
| |
|
| |
| window.toggleReaction = function(id) {
| |
| new mw.Api().postWithToken("csrf", { action: "socialfeed", sfaction: "react", post_id: id, reaction_type: "like" }).done(loadPosts);
| |
| };
| |
|
| |
| window.toggleComments = function(id) {
| |
| var c = document.getElementById("comments-" + id);
| |
| if (c.style.display === "none") { c.style.display = "block"; loadComments(id); }
| |
| else { c.style.display = "none"; }
| |
| };
| |
|
| |
| function loadComments(id) {
| |
| var c = document.getElementById("comments-" + id);
| |
| new mw.Api().get({ action: "socialfeed", sfaction: "getcomments", post_id: id }).done(function(data) {
| |
| var comments = data.socialfeed.comments || [];
| |
| var html = "";
| |
| comments.forEach(function(cm) {
| |
| var cPhoto = localStorage.getItem("sf_profile_photo_" + cm.username) || "";
| |
| var cAvatar = cPhoto ? "<img src=\"" + cPhoto + "\">" : getInitials(cm.username);
| |
| html += "<div class=\"sf-comment\"><div class=\"sf-avatar sf-avatar-small\">" + cAvatar + "</div><div class=\"sf-comment-bubble\"><div class=\"sf-comment-author\">" + escapeHtml(cm.username) + "</div><div class=\"sf-comment-text\">" + escapeHtml(cm.content) + "</div></div></div>";
| |
| });
| |
| var myPhoto = profilePhoto ? "<img src=\"" + profilePhoto + "\">" : getInitials(currentUser);
| |
| html += "<div class=\"sf-add-comment\"><div class=\"sf-avatar sf-avatar-small\">" + myPhoto + "</div><input class=\"sf-comment-input\" placeholder=\"Write a comment...\" onkeypress=\"if(event.key===String.fromCharCode(13))addComment(" + id + ",this)\"></div>";
| |
| c.innerHTML = html;
| |
| });
| |
| }
| |
|
| |
| window.addComment = function(id, input) {
| |
| var content = input.value.trim();
| |
| if (!content) return;
| |
| new mw.Api().postWithToken("csrf", { action: "socialfeed", sfaction: "comment", post_id: id, content: content }).done(function() { loadComments(id); loadPosts(); });
| |
| };
| |
|
| |
| function getTimeAgo(date) {
| |
| var s = Math.floor((new Date() - date) / 1000);
| |
| if (s < 60) return "Just now";
| |
| var m = Math.floor(s / 60); if (m < 60) return m + "m";
| |
| var h = Math.floor(m / 60); if (h < 24) return h + "h";
| |
| var d = Math.floor(h / 24); if (d < 7) return d + "d";
| |
| return date.toLocaleDateString();
| |
| }
| |
|
| |
| function escapeHtml(t) { var d = document.createElement("div"); d.textContent = t; return d.innerHTML; }
| |
| })();
| |
| </script>
| |
| </div>
| |
| </html>
| |