Posts, Search, Voting, Comments & API Validation#310
Merged
motirebuma merged 3 commits intomainfrom Mar 18, 2026
Merged
Conversation
- Add Sidebar to dashboard layout for desktop navigation - Fix PostController: uses GET_POST query to render actual Post component - Fix Post detail page: two-column layout (Post+Comments | LatestQuotes) - Connect VotingBoard + VotingPopup in Post.tsx (remove placeholders) - Create full SearchContainer with URL-synced tabs (Trending/Featured/Friends/Search) - Create DateSearchBar with collapsible date range filter synced to URL params - Create SearchGuestSections for unauthenticated user CTAs - Update search page to use SearchContainer with Suspense - Add dashboard loading.tsx, error.tsx, and route-level loading files - Update tests: PostController, SearchPage, SearchContainer (1711 passing total) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
Implements Phase 3 “core content” UX in the Next.js dashboard (search with URL-synced tabs + date range, post detail view, voting/comments widgets) and updates frontend networking/config to better align with the local backend (http://localhost:4000) across REST + GraphQL + WebSocket.
Changes:
- Added the new tabbed
SearchContainer(debounced query + URL params) andDateSearchBar, plus guest-only teaser sections. - Implemented post detail rendering via
PostController, re-enabled voting UI components, and adjusted comments UI (add/delete + reactions). - Updated API/config plumbing (server URL derivation, REST login, CSP
connect-src, query arg fixes) and added/updated tests.
Reviewed changes
Copilot reviewed 25 out of 25 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| quotevote-frontend/src/types/post.ts | Adds searchKey prop to support highlighting search terms in PostCard. |
| quotevote-frontend/src/lib/utils/getServerUrl.ts | Refines server URL selection/fallbacks and supports deriving base URL from GraphQL endpoint. |
| quotevote-frontend/src/lib/auth.ts | Switches REST login to use validated env.serverUrl. |
| quotevote-frontend/src/graphql/queries.ts | Updates SEARCH_USERNAMES query argument name. |
| quotevote-frontend/src/components/SearchContainer/SearchGuestSections.tsx | Adds unauthenticated CTA section below search results. |
| quotevote-frontend/src/components/SearchContainer/SearchContainer.tsx | Introduces tabbed, URL-synced, debounced search with multiple feed tabs. |
| quotevote-frontend/src/components/Quotes/LatestQuotes.tsx | Reworks “LatestQuotes” to derive quotes from recent posts. |
| quotevote-frontend/src/components/Post/PostController.tsx | Fetches a post by ID and renders the post view with skeleton/error handling. |
| quotevote-frontend/src/components/Post/PostCard.tsx | Highlights matching search terms in post titles via HighlightText. |
| quotevote-frontend/src/components/Post/Post.tsx | Replaces voting placeholders with real VotingBoard + VotingPopup integration. |
| quotevote-frontend/src/components/DateSearchBar/DateSearchBar.tsx | Adds URL-synced collapsible date range filter UI. |
| quotevote-frontend/src/components/Comment/CommentInput.tsx | Removes edit-comment flow; keeps add-comment behavior. |
| quotevote-frontend/src/components/Comment/Comment.tsx | Removes inline edit UI; adds CommentReactions and keeps delete/copy. |
| quotevote-frontend/src/app/dashboard/search/page.tsx | Uses new SearchContainer under Suspense. |
| quotevote-frontend/src/app/dashboard/post/[group]/[title]/[postId]/page.tsx | Adds two-column post detail layout (post+comments + latest quotes sidebar). |
| quotevote-frontend/src/app/dashboard/layout.tsx | Adds persistent sidebar layout structure. |
| quotevote-frontend/src/app/auths/signup/PageContent.tsx | Adds “invite token” path + REST fallback signup. |
| quotevote-frontend/src/app/auths/login/PageContent.tsx | Switches login to REST loginUser() instead of GraphQL mutation. |
| quotevote-frontend/src/tests/utils/getServerUrl.test.ts | Expands server URL derivation test coverage (env var precedence + ws URL). |
| quotevote-frontend/src/tests/components/SearchContainer/NewSearchContainer.test.tsx | Adds tests for the new tabbed SearchContainer. |
| quotevote-frontend/src/tests/components/Post/PostController.test.tsx | Updates tests for new PostController behavior (loading/missing postId). |
| quotevote-frontend/src/tests/app/dashboard/search/page.test.tsx | Updates search page tests to reflect SearchContainer. |
| quotevote-frontend/src/tests/app/auths/login.test.tsx | Updates login tests to mock REST login and validate redirects/errors. |
| quotevote-frontend/next.config.ts | Updates CSP connect-src to include ws://localhost:4000. |
| quotevote-frontend/.env.local | Changes local env defaults to point to localhost backend. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+233
to
+269
| // Determine active tab — if no query, don't show 'search' tab as active | ||
| const activeTab = q ? tab : tab === 'search' ? 'trending' : tab | ||
| const isLoggedIn = !!(user?._id || user?.id) | ||
|
|
||
| return ( | ||
| <div className="space-y-4"> | ||
| {/* Search input */} | ||
| <div className="relative"> | ||
| <SearchIcon className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" /> | ||
| <Input | ||
| type="text" | ||
| placeholder="Search posts..." | ||
| value={inputValue} | ||
| onChange={handleInputChange} | ||
| className="pl-9" | ||
| aria-label="Search posts" | ||
| /> | ||
| {debouncedQuery && ( | ||
| <UsernameResults | ||
| users={usersData?.searchUser ?? []} | ||
| loading={usersLoading} | ||
| error={usersError ?? null} | ||
| query={debouncedQuery} | ||
| /> | ||
| )} | ||
| </div> | ||
|
|
||
| {/* Date range filter */} | ||
| <DateSearchBar /> | ||
|
|
||
| {/* Tabs */} | ||
| <Tabs value={activeTab} onValueChange={handleTabChange}> | ||
| <TabsList> | ||
| <TabsTrigger value="trending">Trending</TabsTrigger> | ||
| <TabsTrigger value="featured">Featured</TabsTrigger> | ||
| {isLoggedIn && <TabsTrigger value="friends">Friends</TabsTrigger>} | ||
| {q && <TabsTrigger value="search">Search</TabsTrigger>} |
Comment on lines
56
to
60
| "style-src 'self' 'unsafe-inline'", | ||
| "img-src 'self' data: https:", | ||
| "font-src 'self' data:", | ||
| "connect-src 'self' http://localhost:4000", | ||
| "connect-src 'self' http://localhost:4000 ws://localhost:4000", | ||
| "frame-ancestors 'none'", |
Comment on lines
+1
to
+8
| # Quote.Vote API — local development | ||
| NEXT_PUBLIC_SERVER_URL=http://localhost:4000 | ||
| NEXT_PUBLIC_GRAPHQL_ENDPOINT=http://localhost:4000/graphql | ||
|
|
||
| # WebSocket is auto-derived by getGraphqlWsServerUrl(): | ||
| # wss://api.quote.vote/graphql | ||
| # ws://localhost:4000/graphql | ||
| # Login is a REST endpoint (not GraphQL): | ||
| # POST https://api.quote.vote/login → { username, password } → { token } | ||
| # POST http://localhost:4000/login → { username, password } → { token } |
Comment on lines
+37
to
+52
| <PostController postId={postId} /> | ||
| <CommentsSection postId={postId} /> | ||
| </div> | ||
| {/* Right column - LatestQuotes sidebar */} | ||
| <div className="w-full lg:w-80 flex-shrink-0"> | ||
| <LatestQuotes limit={5} /> | ||
| </div> | ||
| </div> | ||
| ) | ||
| } | ||
|
|
||
| function CommentsSection({ postId }: { postId: string }) { | ||
| const { loading, data } = useQuery<PostQueryData>(GET_POST, { | ||
| variables: { postId }, | ||
| fetchPolicy: 'cache-first', | ||
| }) |
Comment on lines
+396
to
399
| const handleVoting = async (obj: { type: VoteType; tags: VoteOption }) => { | ||
| if (!ensureAuth()) return | ||
| if (hasVoted) { | ||
| toast('You have already voted on this post') |
| export const SEARCH_USERNAMES = gql` | ||
| query searchUsernames($query: String!) { | ||
| searchUser(query: $query) { | ||
| searchUser(queryName: $query) { |
Comment on lines
21
to
+92
| @@ -39,19 +54,23 @@ const signupSchema = z | |||
|
|
|||
| type SignupFormData = z.infer<typeof signupSchema> | |||
|
|
|||
| function SubmitButton() { | |||
| const { pending } = useFormStatus() | |||
| return ( | |||
| <Button type="submit" disabled={pending} className="w-full"> | |||
| {pending && <Loader2 className="animate-spin mr-2 h-4 w-4" />} | |||
| Create Account | |||
| </Button> | |||
| ) | |||
| } | |||
|
|
|||
| export default function SignupPageContent() { | |||
| const router = useRouter() | |||
| const [signupMutation] = useMutation(SIGNUP_MUTATION) | |||
| const searchParams = useSearchParams() | |||
| const token = searchParams.get('token') || '' | |||
| const setUserData = useAppStore((s) => s.setUserData) | |||
| const [submitting, setSubmitting] = useState(false) | |||
|
|
|||
| // Verify the invite token | |||
| const { data: tokenData, loading: tokenLoading, error: tokenError } = useQuery<VerifyTokenData>(VERIFY_PASSWORD_RESET_TOKEN, { | |||
| variables: { token }, | |||
| skip: !token, | |||
| }) | |||
|
|
|||
| const verifiedUser = tokenData?.verifyUserPasswordResetToken | |||
|
|
|||
| const [updateUser] = useMutation<UpdateUserData>(UPDATE_USER) | |||
|
|
|||
| const { | |||
| register, | |||
| handleSubmit, | |||
| @@ -61,21 +80,84 @@ export default function SignupPageContent() { | |||
| }) | |||
|
|
|||
| const onSubmit = async (values: SignupFormData) => { | |||
| setSubmitting(true) | |||
| try { | |||
| await signupMutation({ | |||
| variables: { | |||
| username: values.username, | |||
| email: values.email, | |||
| password: values.password, | |||
| }, | |||
| }) | |||
| toast.success('Account created! Please sign in.') | |||
| router.push('/auths/login') | |||
| if (token && verifiedUser) { | |||
| // Invite-based signup: update the existing user via GraphQL | |||
| setToken(token) | |||
| const result = await updateUser({ | |||
| variables: { | |||
| user: { | |||
| _id: verifiedUser._id, | |||
| email: values.email, | |||
Comment on lines
+43
to
+46
| if (error) { | ||
| router.push('/error') | ||
| return null | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
input, DateSearchBar, PostCard, PostController, VotingBoard, CommentInput/CommentList, LatestQuotes sidebar, and SearchGuestSections
http://localhost:4000) for Phases 1–3, resolving8 critical mismatches between frontend GraphQL operations and the backend schema
API Fixes
.env.localpointed to productionhttps://api.quote.votehttp://localhost:4000getServerUrl.tsreadNEXT_PUBLIC_SERVER(wrong env var)NEXT_PUBLIC_SERVER_URLfirst, falls backloginUser()used rawprocess.envenv.serverUrlfrom@/config/envloginGraphQL mutationPOST /login)loginUser()callregisterGraphQL mutationPOST /register)UPDATE_USER) + direct REST signupSEARCH_USERNAMESquery had wrong arg namequery:, backend expectsqueryName:searchUser(queryName: $query)|
|
LatestQuotesused nonexistentlatestQuotesquery | Query doesn't exist on backend | Rewrote to extract quotes frompostsquery ||
CommentInputused nonexistentupdateCommentmutation | Mutation doesn't exist on backend | Removed edit-comment feature; add + delete workcorrectly |
| CSP blocked WebSocket |
connect-srcmissingws://localhost:4000| Added WebSocket origin to CSP headers |New Components
SearchContainer— URL-synced tabs with debounced search (?q=&tab=&from=&to=)DateSearchBar— collapsible date range filter synced to URL paramsSearchGuestSections— blurred teaser for unauthenticated visitorsUsernameResults— dropdown showing user matches with HighlightTextPostController— full post detail withnetwork-onlyfetchLatestQuotes— sidebar widget sourced from recent postsTest plan
pnpm type-check→ 0 errorspnpm lint→ 0 errorspnpm build→ succeedsposts,featuredPosts,searchUser(queryName:)queries against live backend/loginand/registerendpoints respond correctlyws://localhost:4000/graphqlcloses #309