From 341d51b17aff3c9b5f32d4758da993206c73c0bf Mon Sep 17 00:00:00 2001 From: Mann Patel <130435633+Patel-Mann@users.noreply.github.com> Date: Sun, 15 Feb 2026 07:59:54 -0700 Subject: [PATCH] feat: some frontend and backend chnages --- .../__pycache__/api_routes.cpython-314.pyc | Bin 37562 -> 41137 bytes .../__pycache__/db_queries.cpython-314.pyc | Bin 28576 -> 32290 bytes backend/api_routes.py | 211 ++++++++- backend/db_queries.py | 31 ++ frontend/index.html | 2 +- frontend/src/App.jsx | 22 +- frontend/src/api.js | 19 +- frontend/src/components/AudioPostCard.jsx | 78 ++-- frontend/src/components/EditPostModal.jsx | 156 +++++++ frontend/src/components/Header.jsx | 4 +- frontend/src/pages/History.jsx | 98 ++-- frontend/src/pages/PostDetail.jsx | 424 ++++++++++++++++++ frontend/src/pages/Search.jsx | 14 +- 13 files changed, 980 insertions(+), 79 deletions(-) create mode 100644 frontend/src/components/EditPostModal.jsx create mode 100644 frontend/src/pages/PostDetail.jsx diff --git a/backend/__pycache__/api_routes.cpython-314.pyc b/backend/__pycache__/api_routes.cpython-314.pyc index 59964d3ca048def95e3faeb59bb008d8b6a1fa8d..8d912e36cbf94508a7a5b6c1eab94c4c1a138440 100644 GIT binary patch delta 9860 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

6h|`NQyK$hA)DczqPSbk*GPt;+MMEHk zsa-txF5<*y?jcr>fE39`j9mCJV*f-yYLbRdei<8ZIa5F~pkq3ZbjHZFB%+5Z zj=#mHDSqDE%D(`9N9-qcTWqaVb9#VYMY0vU2b2YpAQgE0*9a^dlPo7}W(6R-+JK)} zi~C?AcH%~8&woRhQ=TKrQO z1j<^$W2Wu1&}!=Y31Z(9M-)Q2*r5Z#H6gV5npAOLyNHCF$>%WAVC{S>&OqLE1BStb zzxZZLl^rscN`c00Fz}JsbE|B&X;ceT9D4!4jr{x)6PH`k+y$2<2g~Gmu+l_yP1?e^ zw=-}K%i9ocIH_UBD93OsjEiDt|^P3x@u~RxC6Di07Gsh%0(OCK_<-r$5T5LKCjB!(0s zqdEi8a1eI5fp(=dMw`M351Bj#W;iRpURMUGS+YJbG|B!tP_j%0z6S8HojxF0k2fQB zas3%vX}^_elcL1NMBpI=w+xoZ2O`VF^sU8WX4A1lQIx?B<=H6DpdcexqHVa^(hA3%$={GO~unbxiH~RRhQ(@I2akh1kRmQ4Av`U%SrzXPbl)4eaDSKNX zg_Ge{!*G((;TB<<$vmF+U!II4Rf8_NKisSs4vcBjeJQ8`+3_vQjude6fPgCnv1!*e zqfUmNR9Gl~b4)EUfty>S;cXhPfbP>V>JEufjd`MEqFb=gpN3YB=fk+q6B~sK%Q1lC zb@YL9+!cLL{HpEIYtJbp;D@Mysr@i>3%rOelvh+&NzHMVniRgM>xp=By@xD3s$MU- zELWnEZqRZjXjvP3H~cQExqdmMYtD7b;ot+B__jUWIS1xiW)qv2OcP5wR?$OHYD2s6f?u6t8Gdos`Itjc)xh@u9Za?=Uuj>B!g|_zd+nxTCugu zq2-t4^CyVbnv&p>UcUR8l&64R1(w9G!Akf8;_B`^HcMpeDPymTrF#}Tkps<#_7t*L z#PfUBj7BS8{0@G_`SEj#$lW`)hSI1E<-?Sr>%Jl56NUzlPwgQH0xBSP<3jFEuENWQ z&dukT148UA8vZa2qwITAyt=n+I60S&@XsCUo#FH&`)M(|H;46!b-j74O+h z`%()-ByAD^JdxU&<1&J)i)<2`dRCfpc*GI?&!Y;?NK-zmvjGE2&-SBpYI>l5aWlsW z4VTXsj5Pav<`V}#e;Bri#(#qdyt?VFAvM4u{v^I0y7ODbCr2kRpBQ=ULbu`e`S5uc z2BQRq6SfE?yq%vyLT=j@R5@RbBc~#;-l_S^NY)})fPf0v8pP-wCx}=F0`gTSVq{-@ zz~lJ7#m^zQ)Qu2t*mUvbc>m>#cUE8ki4?M%w2a4Ji13iN-e(!y1}2M6^VJr zODq5Fkyze2spz~^az-jSFD*MKO}XmGV79B$_khb4vB!?TVjuCDt(CDlMlY!U1;}nv Ay#N3J diff --git a/backend/__pycache__/db_queries.cpython-314.pyc b/backend/__pycache__/db_queries.cpython-314.pyc index e072a9891f91d56a71ceae12257b1d00e53f2f23..1be6bc1181d5fbd01da643abf6c6336d759ac347 100644 GIT binary patch delta 2617 zcmbtWUrd`-6u-Az%D?Ypls|*INP2rW+E8kx7P#rnyzOTWmlviG=d^X>Mca!LmxO_#s*0; z6e20N5CwCBI9w?8$sglUL%(DJ?Y_9STskY)>!zE0(qYgZiEC}rIjIrkqjA|TJtw(9 zJ{Fg&r6!P?nG|+N1M+=c$-qg_oZ^~V=``?doUfDGf$!jaz0?W(8O|S++`xM{@07ZL z_j2AWbpzkSd6R@K$a?Y-1@vFzXqmm>F6%d(9=L}VQQAYFE+V8 z#|VXXgC>F=BaGd6gVTjj>`U3yDA*(h0gi7xCtq{4%FC8?`KRUrjO7i7kx8>AB0{(BkPthgg60EVHN-52>B#u-=)}y_OCg#M z)7(`D7(~z-5nlJc>b-VMCFL5aR7j;ttQx6SNOkq%Kr~GyEgI=iNXH5}lZ3DZV|#1_ z78|+?CTR|qTQZHNFMv%zs0eyjE@(~>n3B7j~u{TOH9b!iBJYIqCp~m zCY8zen=|G1i4r-tFJJ!IA>uswJ3}#xe2N8?BNHk^ZW$q0N*M}ZE1@3Pn8FV6>#%0h zC=;BF@QXRlxE3I*dg`B?zU1duMf1Srq+lag|s!VpE7sCDj_KQAo{FM)a^sPHM!h5cdk{`fWWGXBbw6oZmwNxbkLe%863l zC#AX*dsF$72S54P;k~%lz%cP%Fz)3!)QcDTSPu2#kou*@=(tMSG~!i=cZKvM z{bWb+u@mwK+mUy33JsH*LT3ZRUAiV#sbsB z7pFsEY6fq(N85{IlS=k#q)H)GDzU}xelZX&R7tBwIu+8nLfi@Ha$zu6zESLy-!P}; z#nSv8aKx&kX6BEN^$N?63^l80tXJr;b%T+G&^r6P*Uu&S!=e(rz&eA@hvmq5<3HNM zU=}Rp!03AUH?hG(MgRZ+ delta 265 zcmZ4VhjGDuMm}vmUM>b8uXqAE~Pg&_CIMojs(9zo_nQ46T3R*+|qC6I3g") +# def api_delete_post(post_id: int): +# """ +# Permanently delete a post and all associated data. +# Only the post owner can delete their posts. +# """ +# user_id = request.args.get("user_id", type=int) +# +# if not user_id: +# return _error("'user_id' is required for authorization.", 400) +# +# # Get the post +# post = get_audio_post_by_id(post_id) +# if not post: +# return _error("Post not found.", 404) +# +# # Check ownership +# if post.get("user_id") != user_id: +# return _error("You don't have permission to delete this post.", 403) +# +# try: +# # Delete associated data in order +# # 1. Delete RAG chunks +# supabase.table("rag_chunks").delete().eq("post_id", post_id).execute() +# +# # 2. Delete archive files (and from storage if needed) +# files = list_archive_files(post_id) +# for file_info in files: +# # Optionally delete from Supabase storage +# try: +# bucket, object_path = _parse_bucket_path(file_info["path"]) +# supabase.storage.from_(bucket).remove([object_path]) +# except: +# pass # Continue even if storage delete fails +# +# supabase.table("archive_files").delete().eq("post_id", post_id).execute() +# +# # 3. Delete metadata +# supabase.table("archive_metadata").delete().eq("post_id", post_id).execute() +# +# # 4. Delete rights +# supabase.table("archive_rights").delete().eq("post_id", post_id).execute() +# +# # 5. Delete the post itself +# supabase.table("audio_posts").delete().eq("post_id", post_id).execute() +# +# # Log the deletion +# add_audit_log({ +# "post_id": post_id, +# "user_id": user_id, +# "action": "post.deleted", +# "details": json.dumps({"title": post.get("title")}) +# }) +# +# return jsonify({"message": "Post deleted successfully", "post_id": post_id}) +# +# except Exception as e: +# return _error(f"Failed to delete post: {str(e)}", 500) +# +# +# # ==================== UPDATE POST (Edit) ==================== +# +# @api.put("/posts//edit") +# def api_edit_post(post_id: int): +# """ +# Update post title, description, and visibility. +# Only the post owner can edit their posts. +# """ +# payload = request.get_json(force=True, silent=False) or {} +# user_id = payload.get("user_id") +# +# if not user_id: +# return _error("'user_id' is required for authorization.", 400) +# +# # Get the post +# post = get_audio_post_by_id(post_id) +# if not post: +# return _error("Post not found.", 404) +# +# # Check ownership +# if post.get("user_id") != user_id: +# return _error("You don't have permission to edit this post.", 403) +# +# # Prepare updates +# updates = {} +# +# if "title" in payload: +# title = (payload["title"] or "").strip() +# if not title: +# return _error("Title cannot be empty.", 400) +# updates["title"] = title +# +# if "description" in payload: +# updates["description"] = payload["description"] +# +# if "visibility" in payload: +# visibility = (payload["visibility"] or "").strip().lower() +# if visibility not in {"private", "public"}: +# return _error("'visibility' must be 'private' or 'public'.", 400) +# updates["visibility"] = visibility +# +# if not updates: +# return _error("No valid fields to update.", 400) +# +# try: +# updated_post = update_audio_post(post_id, updates) +# +# # Log the edit +# add_audit_log({ +# "post_id": post_id, +# "user_id": user_id, +# "action": "post.edited", +# "details": json.dumps({"changes": list(updates.keys())}) +# }) +# +# return jsonify(updated_post) +# +# except Exception as e: +# return _error(f"Failed to update post: {str(e)}", 500) +# +# +# # ==================== Helper function for _parse_bucket_path ==================== +# +# def _parse_bucket_path(stored_path: str) -> tuple: +# """ +# Parse stored path like 'archives/user/uuid/file.mp4' +# Returns: ('archives', 'user/uuid/file.mp4') +# """ +# parts = (stored_path or "").split("/", 1) +# if len(parts) != 2: +# raise ValueError(f"Invalid storage path: {stored_path}") +# return parts[0], parts[1] + +@api.delete("/posts/") +def api_delete_post(post_id: int): + user_id = request.args.get("user_id", type=int) + if not user_id: + return _error("'user_id' is required for authorization.", 400) + + post = get_audio_post_by_id(post_id) + if not post: + return _error("Post not found.", 404) + if post.get("user_id") != user_id: + return _error("You don't have permission to delete this post.", 403) + + try: + delete_rag_chunks(post_id) + delete_archive_files(post_id) + delete_metadata(post_id) + delete_rights(post_id) + delete_audio_post(post_id) + + # ❌ Skip audit log for now + + return jsonify({"message": "Post and all related data deleted successfully", "post_id": post_id}) + + except Exception as e: + return _error(f"Failed to delete post: {str(e)}", 500) + + +@api.put("/posts//edit") +def api_edit_post(post_id: int): + payload = request.get_json(force=True) or {} + user_id = payload.get("user_id") + if not user_id: + return _error("'user_id' is required for authorization.", 400) + + post = get_audio_post_by_id(post_id) + if not post: + return _error("Post not found.", 404) + if post.get("user_id") != user_id: + return _error("You don't have permission to edit this post.", 403) + + updates = {} + if "title" in payload: + title = (payload["title"] or "").strip() + if not title: + return _error("Title cannot be empty.", 400) + updates["title"] = title + if "description" in payload: + updates["description"] = payload["description"] + if "visibility" in payload: + visibility = (payload["visibility"] or "").strip().lower() + if visibility not in {"private", "public"}: + return _error("'visibility' must be 'private' or 'public'.", 400) + updates["visibility"] = visibility + if not updates: + return _error("No valid fields to update.", 400) + + try: + updated_post = update_audio_post(post_id, updates) + add_audit_log({ + "post_id": post_id, + "user_id": user_id, + "action": "post.edited", + "details": json.dumps({"changes": list(updates.keys())}) + }) + return jsonify(updated_post) + except Exception as e: + return _error(f"Failed to update post: {str(e)}", 500) diff --git a/backend/db_queries.py b/backend/db_queries.py index 441e4fd..5dd7edd 100644 --- a/backend/db_queries.py +++ b/backend/db_queries.py @@ -458,3 +458,34 @@ def get_post_bundle(post_id: int) -> Dict[str, Any]: "rag_chunks": list_rag_chunks(post_id, page=1, limit=1000), "audit_log": list_audit_logs(post_id=post_id, page=1, limit=200), } + + +def delete_rag_chunks(post_id: int): + supabase.table("rag_chunks").delete().eq("post_id", post_id).execute() + +def delete_archive_files(post_id: int): + files = list_archive_files(post_id) + for file_info in files: + try: + bucket, object_path = _parse_bucket_path(file_info["path"]) + supabase.storage.from_(bucket).remove([object_path]) + except: + pass + supabase.table("archive_files").delete().eq("post_id", post_id).execute() + +def delete_metadata(post_id: int): + supabase.table("archive_metadata").delete().eq("post_id", post_id).execute() + +def delete_rights(post_id: int): + supabase.table("archive_rights").delete().eq("post_id", post_id).execute() + +def delete_audio_post(post_id: int): + supabase.table("audio_posts").delete().eq("post_id", post_id).execute() + +def update_audio_post(post_id: int, updates: dict): + supabase.table("audio_posts").update(updates).eq("post_id", post_id).execute() + return get_audio_post_by_id(post_id) + +def get_audio_post_by_id(post_id: int): + result = supabase.table("audio_posts").select("*").eq("post_id", post_id).single().execute() + return result.data if result.data else None diff --git a/frontend/index.html b/frontend/index.html index 7bf9ce9..26adc87 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -5,7 +5,7 @@ - frontend + VoiceVault

diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index a6564e5..d48ffae 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -6,6 +6,7 @@ import CreatePost from './pages/CreatePost' import History from './pages/History' import Settings from './pages/Settings' import Search from './pages/Search' +import PostDetail from './pages/PostDetail' import { api } from './api' export default function App() { @@ -18,6 +19,7 @@ export default function App() { const [loginError, setLoginError] = useState(null) const [searchQuery, setSearchQuery] = useState('') const [headerSearchQuery, setHeaderSearchQuery] = useState('') + const [viewingPostId, setViewingPostId] = useState(null) // Check for saved user on mount useEffect(() => { @@ -78,6 +80,15 @@ export default function App() { const handleNavigateToSearch = (query) => { setActiveTab('search') setHeaderSearchQuery(query) + setViewingPostId(null) // Clear any post detail view + } + + const handleViewPost = (postId) => { + setViewingPostId(postId) + } + + const handleBackFromPost = () => { + setViewingPostId(null) } const handlePostCreated = () => { @@ -94,17 +105,22 @@ export default function App() { const renderPage = () => { if (!user) return null + // If viewing a specific post, show PostDetail + if (viewingPostId) { + return + } + switch (activeTab) { case 'create': return case 'search': - return + return case 'history': - return + return case 'settings': return default: - return + return } } diff --git a/frontend/src/api.js b/frontend/src/api.js index dd85265..bdb09d8 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -3,7 +3,7 @@ * Handles all communication with Flask API */ -const API_BASE_URL= 'http://localhost:5000/api'; +const API_BASE_URL = 'http://localhost:5000/api'; class ApiClient { constructor() { @@ -128,9 +128,20 @@ class ApiClient { }); } - async deletePost(postId) { - // Update status to mark as deleted - return this.updatePost(postId, { status: 'deleted' }); + async deletePost(postId, userId) { + // Proper DELETE request with user authorization + return this.request(`/posts/${postId}?user_id=${userId}`, { + method: 'DELETE', + }); + } + + async editPost(postId, updates) { + // Updates can include: title, description, visibility + // Must include user_id for authorization + return this.request(`/posts/${postId}/edit`, { + method: 'PUT', + body: JSON.stringify(updates), + }); } async getPostMetadata(postId) { diff --git a/frontend/src/components/AudioPostCard.jsx b/frontend/src/components/AudioPostCard.jsx index 040dbfa..3c46b4b 100644 --- a/frontend/src/components/AudioPostCard.jsx +++ b/frontend/src/components/AudioPostCard.jsx @@ -1,8 +1,8 @@ -import { Play, Pause, Volume2, MoreVertical, Clock, ChevronDown, ChevronUp, Download } from 'lucide-react' +import { Play, Pause, Volume2, Clock, ChevronDown, ChevronUp, Download, ExternalLink } from 'lucide-react' import { useState, useRef, useEffect } from 'react' import { api } from '../api' -export default function AudioPostCard({ post }) { +export default function AudioPostCard({ post, onViewPost }) { const [isPlaying, setIsPlaying] = useState(false) const [currentTime, setCurrentTime] = useState(0) const [duration, setDuration] = useState(0) @@ -151,6 +151,13 @@ export default function AudioPostCard({ post }) { setCurrentTime(0) } + const handleView = (postId) => { + if (onViewPost) { + onViewPost(post.postId) + } + } + + const progress = duration > 0 ? (currentTime / duration) * 100 : 0 return ( @@ -178,39 +185,51 @@ export default function AudioPostCard({ post }) { }`}> {post.status} - + {post.language && ( - - {post.language.toUpperCase()} - - )} + + {post.language.toUpperCase()} + + )} - {post.status === 'ready' && ( - - )} + {/* Action Buttons */} +
+ + {post.status === 'ready' && ( + + )} + {post.status === 'ready' && ( + + + )} +
- - {/* Description */} {post.description && (

@@ -218,7 +237,6 @@ export default function AudioPostCard({ post }) {

)} - {/* Audio Player - Only show if ready */} {post.status === 'ready' && ( <> diff --git a/frontend/src/components/EditPostModal.jsx b/frontend/src/components/EditPostModal.jsx new file mode 100644 index 0000000..3d941f4 --- /dev/null +++ b/frontend/src/components/EditPostModal.jsx @@ -0,0 +1,156 @@ +import { useState } from 'react' +import { X } from 'lucide-react' +import { api } from '../api' + +export default function EditPostModal({ post, user, onClose, onSave }) { + const [title, setTitle] = useState(post.title) + const [description, setDescription] = useState(post.description || '') + const [visibility, setVisibility] = useState(post.visibility) + const [saving, setSaving] = useState(false) + const [error, setError] = useState(null) + + const handleSubmit = async (e) => { + e.preventDefault() + + if (!title.trim()) { + setError('Title is required') + return + } + + setSaving(true) + setError(null) + + try { + const updates = { + user_id: user.user_id, + title: title.trim(), + description: description.trim(), + visibility + } + + await api.editPost(post.post_id, updates) + onSave?.() + onClose() + } catch (err) { + setError(err.message || 'Failed to update post') + } finally { + setSaving(false) + } + } + + return ( +
+
e.stopPropagation()} + > + {/* Header */} +
+

Edit Post

+ +
+ + {/* Body */} +
+ {error && ( +
+ {error} +
+ )} + + {/* Title */} +
+ + setTitle(e.target.value)} + className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#f4b840] focus:border-transparent" + required + disabled={saving} + /> +
+ + {/* Description */} +
+ +