Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions quotevote-frontend/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,17 @@ const nextConfig: NextConfig = {
// Compress output
compress: true,

// Redirect legacy /post/... URLs (stored in the database) to the app route
async redirects() {
return [
{
source: '/post/:path*',
destination: '/dashboard/post/:path*',
permanent: false,
},
]
},

// Security headers
async headers() {
return [
Expand Down
1 change: 1 addition & 0 deletions quotevote-frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"avataaars": "^2.0.0",
"axios": "^1.13.2",
Comment on lines 34 to 38
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

avataaars@2.0.0 declares a peer dependency on react: ^17.0.0 (see lockfile), but this app is on React 19. That mismatch can lead to pnpm peer warnings and potential runtime issues. Consider switching to an avatar package that supports React 19, or using a maintained fork/version with compatible peer deps.

Copilot uses AI. Check for mistakes.
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
Expand Down
14 changes: 14 additions & 0 deletions quotevote-frontend/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -169,8 +169,6 @@ describe('PostActionCard', () => {
// Component should render without error boundary
// Check for content that should be present
expect(screen.getByText(/Selected text for vote/)).toBeInTheDocument()
// Check for vote tags
expect(screen.getByText('#agree #like')).toBeInTheDocument()
// Check for user name (may appear in multiple places - use getAllByText)
expect(screen.getAllByText(/Voter User/).length).toBeGreaterThan(0)
})
Expand All @@ -190,8 +188,8 @@ describe('PostActionCard', () => {
/>,
)

// Check for disagree tag
expect(screen.getByText('#disagree')).toBeInTheDocument()
// Check content renders for downvote
expect(screen.getByText(/Selected text for vote/)).toBeInTheDocument()
})

it('shows delete button for vote owner', async () => {
Expand All @@ -215,7 +213,7 @@ describe('PostActionCard', () => {
// Delete button should be present for vote owner
// Check if component rendered successfully first
expect(screen.getByText(/Selected text for vote/)).toBeInTheDocument()
const deleteButton = document.querySelector('button.text-red-500')
const deleteButton = document.querySelector('[aria-label="Delete"]')
expect(deleteButton).toBeInTheDocument()
})

Expand Down Expand Up @@ -244,15 +242,15 @@ describe('PostActionCard', () => {
// Find delete button by its red color class using querySelector
// First verify component rendered
expect(screen.getByText(/Selected text for vote/)).toBeInTheDocument()
const deleteButton = document.querySelector('button.text-red-500') as HTMLElement
const deleteButton = document.querySelector('[aria-label="Delete"]') as HTMLElement
expect(deleteButton).toBeInTheDocument()
fireEvent.click(deleteButton)

await waitFor(() => {
expect(mockMutate).toHaveBeenCalledWith({
variables: { voteId: 'vote1' },
})
expect(toast.success).toHaveBeenCalledWith('Vote deleted successfully')
expect(toast.success).toHaveBeenCalledWith('Vote deleted')
expect(mockRefetchPost).toHaveBeenCalled()
})
})
Expand Down Expand Up @@ -281,12 +279,12 @@ describe('PostActionCard', () => {
// Find delete button by its red color class using querySelector
// First verify component rendered
expect(screen.getByText(/Selected text for vote/)).toBeInTheDocument()
const deleteButton = document.querySelector('button.text-red-500') as HTMLElement
const deleteButton = document.querySelector('[aria-label="Delete"]') as HTMLElement
expect(deleteButton).toBeInTheDocument()
fireEvent.click(deleteButton)

await waitFor(() => {
expect(toast.error).toHaveBeenCalledWith(expect.stringContaining('Delete Error'))
expect(toast.error).toHaveBeenCalledWith(expect.stringContaining('Error:'))
})
})
})
Expand Down Expand Up @@ -345,15 +343,15 @@ describe('PostActionCard', () => {
// Find delete button by its red color class using querySelector
// First verify component rendered
expect(screen.getByText(/This is a comment/)).toBeInTheDocument()
const deleteButton = document.querySelector('button.text-red-500') as HTMLElement
const deleteButton = document.querySelector('[aria-label="Delete"]') as HTMLElement
expect(deleteButton).toBeInTheDocument()
fireEvent.click(deleteButton)

await waitFor(() => {
expect(mockMutate).toHaveBeenCalledWith({
variables: { commentId: 'comment1' },
})
expect(toast.success).toHaveBeenCalledWith('Comment deleted successfully')
expect(toast.success).toHaveBeenCalledWith('Comment deleted')
})
})
})
Expand Down Expand Up @@ -412,15 +410,15 @@ describe('PostActionCard', () => {
// Find delete button by its red color class using querySelector
// First verify component rendered
expect(screen.getByText(/This is a quote/)).toBeInTheDocument()
const deleteButton = document.querySelector('button.text-red-500') as HTMLElement
const deleteButton = document.querySelector('[aria-label="Delete"]') as HTMLElement
expect(deleteButton).toBeInTheDocument()
fireEvent.click(deleteButton)

await waitFor(() => {
expect(mockMutate).toHaveBeenCalledWith({
variables: { quoteId: 'quote1' },
})
expect(toast.success).toHaveBeenCalledWith('Quote deleted successfully')
expect(toast.success).toHaveBeenCalledWith('Quote deleted')
})
})
})
Expand Down Expand Up @@ -476,9 +474,8 @@ describe('PostActionCard', () => {
/>,
)

// Share button has Link2 icon, find by role and SVG
const buttons = screen.getAllByRole('button')
const shareButton = buttons.find((btn) => btn.querySelector('svg')) || buttons[buttons.length - 1]
// Share button has aria-label="Copy link"
const shareButton = screen.getByLabelText('Copy link')
fireEvent.click(shareButton)

await waitFor(() => {
Expand All @@ -487,9 +484,9 @@ describe('PostActionCard', () => {
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(expectedUrl)
})

// Dialog should appear
// Toast should be triggered
await waitFor(() => {
expect(screen.getByText('Link copied!')).toBeInTheDocument()
expect(toast.success).toHaveBeenCalledWith('Link copied!')
})
})

Expand All @@ -505,9 +502,8 @@ describe('PostActionCard', () => {
/>,
)

// Share button has Link2 icon, find by role and SVG
const buttons = screen.getAllByRole('button')
const shareButton = buttons.find((btn) => btn.querySelector('svg')) || buttons[buttons.length - 1]
// Share button has aria-label="Copy link"
const shareButton = screen.getByLabelText('Copy link')
fireEvent.click(shareButton)

await waitFor(() => {
Expand Down Expand Up @@ -602,7 +598,7 @@ describe('PostActionCard', () => {
/>,
)

const card = document.querySelector('[class*="bg-amber-50"]')
const card = document.querySelector('[class*="border-l-primary"]')
expect(card).toBeInTheDocument()
})

Expand Down Expand Up @@ -665,10 +661,7 @@ describe('PostActionCard', () => {
/>,
)

const buttons = screen.getAllByRole('button')
const deleteButton = buttons.find((btn) =>
btn.className.includes('text-red-500') || btn.querySelector('svg')
)
const deleteButton = document.querySelector('[aria-label="Delete"]')
expect(deleteButton).toBeInTheDocument()
})
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ describe('PostActionList', () => {
/>,
)

expect(screen.getByText('Start the discussion...')).toBeInTheDocument()
expect(screen.getByText('No activity yet')).toBeInTheDocument()
})

it('does not render empty state when loading', () => {
Expand Down Expand Up @@ -456,7 +456,7 @@ describe('PostActionList', () => {
/>,
)

const listItem = container.querySelector('li[id="action1"]')
const listItem = container.querySelector('div[id="action1"]')
expect(listItem).toBeInTheDocument()
})
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,23 +119,20 @@ describe('PostChatSend', () => {
it('renders message input and send button', () => {
render(<PostChatSend messageRoomId="room1" title="Test Post" postId="post1" />)

expect(screen.getByPlaceholderText('type a message...')).toBeInTheDocument()
expect(screen.getByPlaceholderText('Add to the discussion...')).toBeInTheDocument()
expect(screen.getByLabelText('Send message')).toBeInTheDocument()
})

it('displays chat label on larger screens', () => {
it('renders send button with correct aria-label', () => {
render(<PostChatSend messageRoomId="room1" title="Test Post" postId="post1" />)

// Chat label should be present (hidden on mobile)
const chatLabel = screen.queryByText('Chat')
// May be hidden with CSS, but should exist in DOM
expect(chatLabel).toBeInTheDocument()
expect(screen.getByLabelText('Send message')).toBeInTheDocument()
})

it('updates input value when typing', () => {
render(<PostChatSend messageRoomId="room1" title="Test Post" postId="post1" />)

const input = screen.getByPlaceholderText('type a message...')
const input = screen.getByPlaceholderText('Add to the discussion...')
fireEvent.change(input, { target: { value: 'Hello world' } })

expect(input).toHaveValue('Hello world')
Expand All @@ -154,7 +151,7 @@ describe('PostChatSend', () => {

render(<PostChatSend messageRoomId="room1" title="Test Post" postId="post1" />)

const input = screen.getByPlaceholderText('type a message...')
const input = screen.getByPlaceholderText('Add to the discussion...')
const sendButton = screen.getByLabelText('Send message')

fireEvent.change(input, { target: { value: 'Test message' } })
Expand Down Expand Up @@ -184,7 +181,7 @@ describe('PostChatSend', () => {

render(<PostChatSend messageRoomId="room1" title="Test Post" postId="post1" />)

const input = screen.getByPlaceholderText('type a message...')
const input = screen.getByPlaceholderText('Add to the discussion...')
fireEvent.change(input, { target: { value: 'Test message' } })
fireEvent.keyDown(input, { key: 'Enter', shiftKey: false })

Expand All @@ -206,7 +203,7 @@ describe('PostChatSend', () => {

render(<PostChatSend messageRoomId="room1" title="Test Post" postId="post1" />)

const input = screen.getByPlaceholderText('type a message...')
const input = screen.getByPlaceholderText('Add to the discussion...')
fireEvent.change(input, { target: { value: 'Test message' } })
fireEvent.keyDown(input, { key: 'Enter', shiftKey: true })

Expand All @@ -226,7 +223,7 @@ describe('PostChatSend', () => {

render(<PostChatSend messageRoomId="room1" title="Test Post" postId="post1" />)

const input = screen.getByPlaceholderText('type a message...')
const input = screen.getByPlaceholderText('Add to the discussion...')
fireEvent.change(input, { target: { value: ' ' } })
fireEvent.keyDown(input, { key: 'Enter', shiftKey: false })

Expand Down Expand Up @@ -258,7 +255,7 @@ describe('PostChatSend', () => {

render(<PostChatSend messageRoomId="room1" title="Test Post" postId="post1" />)

const input = screen.getByPlaceholderText('type a message...')
const input = screen.getByPlaceholderText('Add to the discussion...')
const sendButton = screen.getByLabelText('Send message')

fireEvent.change(input, { target: { value: 'Test message' } })
Expand Down Expand Up @@ -297,7 +294,7 @@ describe('PostChatSend', () => {

render(<PostChatSend messageRoomId={null} title="Test Post" postId="post1" />)

const input = screen.getByPlaceholderText('type a message...')
const input = screen.getByPlaceholderText('Add to the discussion...')
const sendButton = screen.getByLabelText('Send message')

fireEvent.change(input, { target: { value: 'Test message' } })
Expand All @@ -321,7 +318,7 @@ describe('PostChatSend', () => {

render(<PostChatSend messageRoomId="room1" title="Test Post" postId="post1" />)

const input = screen.getByPlaceholderText('type a message...')
const input = screen.getByPlaceholderText('Add to the discussion...')
const sendButton = screen.getByLabelText('Send message')

fireEvent.change(input, { target: { value: 'Test message' } })
Expand Down
20 changes: 13 additions & 7 deletions quotevote-frontend/src/app/dashboard/explore/ExploreContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,15 @@ export default function ExploreContent() {
const q = searchParams.get('q') || ''
const from = searchParams.get('from') || ''
const to = searchParams.get('to') || ''
const sortParam = (searchParams.get('sort') || '') as SortOrder
const sortOrder = (searchParams.get('sort') || '') as SortOrder
const interactions = searchParams.get('interactions') === 'true'
const friends = searchParams.get('friends') === 'true'

// Local UI state
const [submitDialogOpen, setSubmitDialogOpen] = useState(false)
const [inputValue, setInputValue] = useState(q)
const [searchFocused, setSearchFocused] = useState(false)
const [selectedUser, setSelectedUser] = useState<UsernameSearchUser | null>(null)
const [sortOrder, setSortOrder] = useState<SortOrder>(sortParam)
const [interactions, setInteractions] = useState(false)
const [friends, setFriends] = useState(false)
const [totalCount, setTotalCount] = useState(0)
const searchRef = useRef<HTMLDivElement>(null)

Expand Down Expand Up @@ -120,10 +119,17 @@ export default function ExploreContent() {

const handleSortCycle = useCallback(() => {
const next: SortOrder = sortOrder === '' ? 'desc' : sortOrder === 'desc' ? 'asc' : ''
setSortOrder(next)
updateParams({ sort: next || null })
}, [sortOrder, updateParams])

const handleToggleInteractions = useCallback(() => {
updateParams({ interactions: interactions ? null : 'true' })
}, [interactions, updateParams])

const handleToggleFriends = useCallback(() => {
updateParams({ friends: friends ? null : 'true' })
}, [friends, updateParams])

// Username search — real-time, fires on every keystroke when @ is detected
const {
loading: usersLoading,
Expand Down Expand Up @@ -243,7 +249,7 @@ export default function ExploreContent() {
{isLoggedIn && (
<button
type="button"
onClick={() => setFriends((f) => !f)}
onClick={handleToggleFriends}
title={friends ? 'Showing friends posts' : 'Show posts from people you follow'}
className={filterIconBtn(friends)}
>
Expand All @@ -254,7 +260,7 @@ export default function ExploreContent() {

<button
type="button"
onClick={() => setInteractions((i) => !i)}
onClick={handleToggleInteractions}
title="Sort by most interactions"
className={filterIconBtn(interactions)}
>
Expand Down
Loading
Loading