From a0a4d55469127747076f666522ec068bd67faeb6 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 12 Oct 2025 22:10:11 +0000 Subject: [PATCH] Menambahkan Fitur Hashtag, Profil Pengguna, dan Simpan Konten Fitur-fitur berikut telah ditambahkan: - Hashtag: Pengguna sekarang dapat menambahkan hashtag ke postingan, dan mengklik hashtag akan menampilkan umpan postingan dengan hashtag yang sama. - Profil Pengguna: Pengguna sekarang dapat melihat halaman profil mereka sendiri dan pengguna lain, yang menampilkan informasi pengguna dan postingan mereka. - Simpan Konten: Pengguna sekarang dapat menyimpan atau mem-bookmark postingan dan melihatnya di halaman "Disimpan" yang baru. Perubahan ini juga mencakup perbaikan bug yang diidentifikasi selama tinjauan kode untuk memastikan fungsionalitas yang benar dari fitur hashtag. --- app.js | 260 +++++++++++++++++- index.html | 4 + jules-scratch/verification/verify_features.py | 78 ------ style.css | 102 +++++++ 4 files changed, 353 insertions(+), 91 deletions(-) delete mode 100644 jules-scratch/verification/verify_features.py diff --git a/app.js b/app.js index 0afda91..18aba8a 100644 --- a/app.js +++ b/app.js @@ -295,7 +295,7 @@ if (!imageUrl) return; // Upload failed } - const { data, error } = await supabase + const { data: postData, error } = await supabase .from('posts') .insert([ { @@ -304,10 +304,19 @@ image_url: imageUrl } ]) - .select() - .single(); + .select(); if (error) throw error; + if (!postData || postData.length === 0) { + throw new Error("Failed to create post."); + } + const post = postData[0]; + + // Extract and add hashtags + const hashtags = extractHashtags(content); + if (hashtags.length > 0) { + await addHashtagsToPost(post.id, hashtags); + } // Reset form document.getElementById('postContent').value = ''; @@ -332,7 +341,8 @@ *, profiles!posts_user_id_fkey (*), likes (*), - comments (count) + comments (count), + bookmarks (*) `) .order('created_at', { ascending: false }); @@ -387,9 +397,87 @@ } function searchHashtag(hashtag) { - const searchInput = document.getElementById('searchInputModal'); - searchInput.value = `#${hashtag}`; - handleSearch(); + // No need to use the search modal, just load the posts directly + loadPostsByHashtag(hashtag); + } + + async function loadPostsByHashtag(hashtag) { + try { + document.getElementById('feedTitle').textContent = `#${hashtag}`; + + const { data, error } = await supabase + .from('posts') + .select(` + *, + profiles!posts_user_id_fkey (*), + likes (*), + comments (count), + post_hashtags!inner(hashtags!inner(name)) + `) + .eq('post_hashtags.hashtags.name', hashtag) + .order('created_at', { ascending: false }); + + if (error) throw error; + + posts = data || []; + renderPosts(); + closeSearch(); // Close search modal after clicking a hashtag + + } catch (error) { + console.error('Error loading posts by hashtag:', error); + showNotification(`Gagal memuat zen untuk #${hashtag}`, 'error'); + } + } + + function extractHashtags(content) { + const regex = /#(\w+)/g; + const matches = content.match(regex); + if (matches) { + // Return unique hashtags without the '#' symbol + return [...new Set(matches.map(tag => tag.substring(1)))]; + } + return []; + } + + async function addHashtagsToPost(postId, hashtags) { + try { + // 1. Find existing hashtags or create new ones + const { data: existingHashtags, error: findError } = await supabase + .from('hashtags') + .select('id, name') + .in('name', hashtags); + + if (findError) throw findError; + + const existingHashtagNames = existingHashtags.map(h => h.name); + const newHashtagNames = hashtags.filter(h => !existingHashtagNames.includes(h)); + + let newHashtagIds = []; + if (newHashtagNames.length > 0) { + const { data: createdHashtags, error: createError } = await supabase + .from('hashtags') + .insert(newHashtagNames.map(name => ({ name }))) + .select('id'); + + if (createError) throw createError; + newHashtagIds = createdHashtags.map(h => h.id); + } + + const allHashtagIds = [...existingHashtags.map(h => h.id), ...newHashtagIds]; + + // 2. Link hashtags to the post in the join table + const links = allHashtagIds.map(hashtagId => ({ + post_id: postId, + hashtag_id: hashtagId + })); + + const { error: linkError } = await supabase.from('post_hashtags').insert(links); + if (linkError) throw linkError; + + } catch (error) { + console.error('Error adding hashtags:', error); + // We don't show a notification here to not bother the user for a background task. + } } function createPostElement(post) { @@ -399,6 +487,7 @@ const timeAgo = getTimeAgo(new Date(post.created_at)); const isLiked = post.likes && post.likes.some(like => like.user_id === currentUser?.id); + const isBookmarked = post.bookmarks && post.bookmarks.some(bookmark => bookmark.user_id === currentUser?.id); const verifiedBadge = getVerifiedBadge(post.profiles.username); div.innerHTML = ` @@ -440,9 +529,9 @@
Bagikan Tautan Postingan
- @@ -452,6 +541,68 @@ return div; } + async function toggleBookmark(postId) { + if (!currentUser) { + toggleAuth(); + return; + } + + const post = posts.find(p => p.id === postId); + if (!post) return; + + const isBookmarked = post.bookmarks.some(b => b.user_id === currentUser.id); + + try { + if (isBookmarked) { + // Unbookmark + const { error } = await supabase + .from('bookmarks') + .delete() + .match({ post_id: postId, user_id: currentUser.id }); + if (error) throw error; + showNotification('Bookmark dihapus'); + } else { + // Bookmark + const { error } = await supabase + .from('bookmarks') + .insert({ post_id: postId, user_id: currentUser.id }); + if (error) throw error; + showNotification('Postingan disimpan!'); + } + await updatePostInState(postId); // Refresh post state + } catch (error) { + showNotification('Gagal menyimpan bookmark: ' + error.message, 'error'); + } + } + + async function showBookmarkedPosts() { + if (!currentUser) { + toggleAuth(); + return; + } + try { + document.getElementById('feedTitle').textContent = 'Disimpan'; + const { data, error } = await supabase + .from('posts') + .select(` + *, + profiles!posts_user_id_fkey (*), + likes (*), + comments (count), + bookmarks!inner(*) + `) + .eq('bookmarks.user_id', currentUser.id) + .order('created_at', { ascending: false }); + + if (error) throw error; + + renderPosts(data || []); + + } catch (error) { + showNotification('Gagal memuat postingan yang disimpan', 'error'); + } + } + async function toggleLike(postId) { if (!currentUser) { toggleAuth(); @@ -868,9 +1019,92 @@ } } - function showUserProfile(userId) { - // Show user profile - document.getElementById('feedTitle').textContent = 'Profil Zen Master'; + async function showUserProfile(userId) { + try { + // Fetch profile data + const { data: profile, error: profileError } = await supabase + .from('profiles') + .select('*') + .eq('id', userId) + .single(); + + if (profileError) throw profileError; + if (!profile) { + showNotification('Pengguna tidak ditemukan', 'error'); + return; + } + + // Fetch user's posts + const { data: userPosts, error: postsError } = await supabase + .from('posts') + .select(` + *, + profiles!inner(*), + likes (*), + comments (count) + `) + .eq('user_id', userId) + .order('created_at', { ascending: false }); + + if (postsError) throw postsError; + + // Update UI + document.getElementById('feedTitle').textContent = profile.name; + const feed = document.getElementById('postsFeed'); + feed.innerHTML = ''; // Clear the feed + + // Create and append profile header + const profileHeader = createProfileHeader(profile, userPosts.length); + feed.appendChild(profileHeader); + + // Render user's posts + renderPosts(userPosts); + closeSearch(); + + } catch (error) { + console.error('Error showing user profile:', error); + showNotification('Gagal memuat profil pengguna', 'error'); + } + } + + function createProfileHeader(profile, postCount) { + const header = document.createElement('div'); + header.className = 'profile-header'; // You'll need to style this class + + const isFollowing = false; // Placeholder for follow logic + const followButtonText = isFollowing ? 'Mengikuti' : 'Ikuti'; + const verifiedBadge = getVerifiedBadge(profile.username); + + header.innerHTML = ` +
+
+
+ ${profile.name} +
+
+ ${currentUser && currentUser.id === profile.id + ? `` + : `` + } +
+
+

${profile.name} ${verifiedBadge}

+

@${profile.username}

+

${profile.bio || 'Pengguna ZenCom'}

+
+ ${profile.following_count || 0} Mengikuti + ${profile.followers_count || 0} Pengikut +
+
+
+
+
Zen (${postCount})
+
Balasan
+
Media
+
Suka
+
+ `; + return header; } function showComments(postId) { diff --git a/index.html b/index.html index 6f5c830..e742fe5 100644 --- a/index.html +++ b/index.html @@ -64,6 +64,10 @@

ZenCom

Pesan + + + Disimpan + Profil diff --git a/jules-scratch/verification/verify_features.py b/jules-scratch/verification/verify_features.py deleted file mode 100644 index 29c5c91..0000000 --- a/jules-scratch/verification/verify_features.py +++ /dev/null @@ -1,78 +0,0 @@ -import asyncio -from playwright.async_api import async_playwright, expect -import os - -async def main(): - async with async_playwright() as p: - browser = await p.chromium.launch(headless=True) - page = await browser.new_page() - - # Get the absolute path of the index.html file - file_path = os.path.abspath('index.html') - - # 1. Go to the application page and wait for the main button to be ready - await page.goto(f'file://{file_path}') - await expect(page.get_by_role("button", name="Masuk")).to_be_visible(timeout=10000) # Wait up to 10s - - # --- Registration --- - # 2. Open the auth modal - await page.get_by_role("button", name="Masuk").click() - - # 3. Switch to the registration form - await page.get_by_role("link", name="Daftar").click() - - # 4. Fill out the registration form - await page.get_by_placeholder("Nama Lengkap").fill("Test User") - await page.get_by_placeholder("Username").fill("testuser") - # Use a unique email to avoid conflicts on re-runs - import time - timestamp = int(time.time()) - await page.get_by_placeholder("Email").fill(f"test.user.{timestamp}@example.com") - await page.get_by_placeholder("Password").fill("password123") - await page.get_by_label("Saya setuju dengan Syarat dan Ketentuan").check() - - # 5. Submit registration - await page.get_by_role("button", name="Daftar").click() - - # Wait for registration notification - await expect(page.locator("#notificationToast")).to_contain_text("Registrasi berhasil!") - - # --- Create a Post --- - # 6. Fill in the post content - await page.get_by_placeholder("Bagikan zen Anda...").fill("This is a test post from a Playwright script!") - - # 7. Click the "Zen" button to create the post - await page.get_by_role("button", name="Zen", exact=True).click() - await expect(page.locator("#notificationToast")).to_contain_text("Zen Anda telah dibagikan!") - - # Wait for the post to appear in the feed - await expect(page.locator(".zen-card").first).to_contain_text("This is a test post") - - # --- Like and Comment --- - first_post = page.locator(".zen-card").first - - # 8. Like the post - like_button = first_post.get_by_role("button", name="0") - await like_button.click() - await expect(like_button).to_have_class("liked") - await expect(like_button.get_by_text("1")).to_be_visible() - - # 9. Open the post detail view (comments) - await first_post.get_by_role("button", name="0").nth(0).click() # Click the comment button - - # 10. Wait for the modal to appear and add a comment - await expect(page.locator("#postDetailModal")).to_be_visible() - comment_textarea = page.locator("#postDetailContent textarea") - await comment_textarea.fill("This is a test comment!") - await page.get_by_role("button", name="Kirim").click() - - # 11. Verify the comment appears - await expect(page.locator(".comment-card")).to_contain_text("This is a test comment!") - - # 12. Take a screenshot of the post detail modal - await page.screenshot(path="jules-scratch/verification/verification.png") - - await browser.close() - -if __name__ == "__main__": - asyncio.run(main()) \ No newline at end of file diff --git a/style.css b/style.css index c74d4e0..a986312 100644 --- a/style.css +++ b/style.css @@ -479,6 +479,15 @@ color: white; } + .post-action.bookmarked { + background: #3498db; + color: white; + } + + .post-action.bookmarked .action-icon { + color: white; + } + .action-count { font-size: 0.875rem; } @@ -948,4 +957,97 @@ display: flex; flex-direction: column; gap: 1rem; +} + +/* Profile Page Styles */ +.profile-header { + background: white; + border-radius: 1.25rem; + margin-bottom: 1.5rem; + box-shadow: 0 10px 20px rgba(0,0,0,0.1); + overflow: hidden; /* To contain the banner */ +} + +.profile-banner { + height: 200px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); +} + +.profile-details { + padding: 1rem 1.5rem 1.5rem; + position: relative; +} + +.profile-avatar-container { + position: absolute; + top: -50px; /* Pulls the avatar up */ + left: 1.5rem; + border: 5px solid white; + border-radius: 50%; + background-color: white; /* Prevents banner showing through border */ +} + +.profile-avatar { + width: 100px; + height: 100px; + border-radius: 50%; +} + +.profile-actions { + display: flex; + justify-content: flex-end; + margin-bottom: 1rem; +} + +.profile-info { + margin-top: 2.5rem; /* Space for the avatar */ +} + +.profile-name { + font-size: 1.5rem; + font-weight: bold; +} + +.profile-username { + color: #6b7280; + margin-bottom: 0.75rem; +} + +.profile-bio { + margin-bottom: 1rem; + line-height: 1.5; +} + +.profile-stats { + display: flex; + gap: 1.5rem; + color: #6b7280; +} + +.profile-stats strong { + color: #1f2937; +} + +.profile-tabs { + display: flex; + border-top: 1px solid #e5e7eb; +} + +.profile-tab { + flex: 1; + text-align: center; + padding: 1rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + color: #6b7280; +} + +.profile-tab:hover { + background-color: #f9fafb; +} + +.profile-tab.active { + color: #667eea; + border-bottom: 2px solid #667eea; } \ No newline at end of file