feat: forum rich-text editor, emoji picker, mentions, discover nav, feed, uploads, profile

Forum:
- TipTap WYSIWYG editor with full toolbar
- @emoji-mart/react emoji picker (consistent with tweets)
- @mention autocomplete with user search API
- Fix PHP 8.4 parse errors in Blade templates
- Fix thread data display (paginator items)
- Align forum page widths to max-w-5xl

Discover:
- Extract shared _nav.blade.php partial
- Add missing nav links to for-you page
- Add Following link for authenticated users

Feed/Posts:
- Post model, controllers, policies, migrations
- Feed page components (PostComposer, FeedCard, etc)
- Post reactions, comments, saves, reports, sharing
- Scheduled publishing support
- Link preview controller

Profile:
- Profile page components (ProfileHero, ProfileTabs)
- Profile API controller

Uploads:
- Upload wizard enhancements
- Scheduled publish picker
- Studio status bar and readiness checklist
This commit is contained in:
2026-03-03 09:48:31 +01:00
parent 1266f81d35
commit dc51d65440
178 changed files with 14308 additions and 665 deletions

View File

@@ -5,7 +5,7 @@ import { cleanup, render, screen, waitFor, within } from '@testing-library/react
import userEvent from '@testing-library/user-event'
import UploadWizard from '../UploadWizard'
function installAxiosStubs({ statusValue = 'ready', initError = null, holdChunk = false } = {}) {
function installAxiosStubs({ statusValue = 'ready', initError = null, holdChunk = false, finishError = null } = {}) {
window.axios = {
post: vi.fn((url, payload, config = {}) => {
if (url === '/api/uploads/init') {
@@ -44,6 +44,7 @@ function installAxiosStubs({ statusValue = 'ready', initError = null, holdChunk
}
if (url === '/api/uploads/finish') {
if (finishError) return Promise.reject(finishError)
return Promise.resolve({ data: { processing_state: statusValue, status: statusValue } })
}
@@ -281,6 +282,30 @@ describe('UploadWizard step flow', () => {
expect((bar.className || '').includes('bottom-0')).toBe(true)
})
it('shows mapped duplicate hash toast when finish returns duplicate_hash', async () => {
installAxiosStubs({
finishError: {
response: {
status: 409,
data: {
reason: 'duplicate_hash',
message: 'Duplicate upload is not allowed. This file already exists.',
},
},
},
})
await renderWizard({ initialDraftId: 310 })
await uploadPrimary(new File(['img'], 'duplicate.png', { type: 'image/png' }))
await act(async () => {
await userEvent.click(await screen.findByRole('button', { name: /start upload/i }))
})
const toast = await screen.findByRole('alert')
expect(toast.textContent).toMatch(/already exists in skinbase/i)
})
it('locks step 1 file input after upload and unlocks after reset', async () => {
installAxiosStubs({ statusValue: 'ready' })
await renderWizard({ initialDraftId: 309 })