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:
@@ -3,6 +3,7 @@ import { usePage } from '@inertiajs/react'
|
||||
import TagInput from '../../components/tags/TagInput'
|
||||
import UploadWizard from '../../components/upload/UploadWizard'
|
||||
import Checkbox from '../../Components/ui/Checkbox'
|
||||
import { mapUploadErrorNotice, mapUploadResultNotice } from '../../lib/uploadNotices'
|
||||
|
||||
const phases = {
|
||||
idle: 'idle',
|
||||
@@ -179,13 +180,21 @@ function useUploadMachine({ draftId, filesCdnUrl, chunkSize, userId }) {
|
||||
}, [])
|
||||
|
||||
const pushNotice = useCallback((type, message) => {
|
||||
const normalizedType = ['success', 'warning', 'error'].includes(String(type || '').toLowerCase())
|
||||
? String(type).toLowerCase()
|
||||
: 'error'
|
||||
const id = `${Date.now()}-${Math.random().toString(16).slice(2)}`
|
||||
dispatch({ type: 'PUSH_NOTICE', notice: { id, type, message } })
|
||||
dispatch({ type: 'PUSH_NOTICE', notice: { id, type: normalizedType, message } })
|
||||
window.setTimeout(() => {
|
||||
dispatch({ type: 'REMOVE_NOTICE', id })
|
||||
}, 4500)
|
||||
}, [])
|
||||
|
||||
const pushMappedNotice = useCallback((notice) => {
|
||||
if (!notice?.message) return
|
||||
pushNotice(notice.type || 'error', notice.message)
|
||||
}, [pushNotice])
|
||||
|
||||
const previewUrl = useMemo(() => {
|
||||
if (state.previewUrl) return state.previewUrl
|
||||
if (!state.filePreviewUrl) return null
|
||||
@@ -276,12 +285,12 @@ function useUploadMachine({ draftId, filesCdnUrl, chunkSize, userId }) {
|
||||
}
|
||||
return { sessionId: data.session_id, uploadToken: data.upload_token }
|
||||
} catch (error) {
|
||||
const message = extractErrorMessage(error, 'Failed to initialize upload session.')
|
||||
dispatch({ type: 'INIT_ERROR', error: message })
|
||||
pushNotice('error', message)
|
||||
const notice = mapUploadErrorNotice(error, 'Failed to initialize upload session.')
|
||||
dispatch({ type: 'INIT_ERROR', error: notice.message })
|
||||
pushMappedNotice(notice)
|
||||
return null
|
||||
}
|
||||
}, [state.file, userId, extractErrorMessage, pushNotice])
|
||||
}, [state.file, userId, pushMappedNotice])
|
||||
|
||||
const createDraft = useCallback(async () => {
|
||||
if (state.artworkId) return state.artworkId
|
||||
@@ -302,12 +311,12 @@ function useUploadMachine({ draftId, filesCdnUrl, chunkSize, userId }) {
|
||||
}
|
||||
throw new Error('missing_artwork_id')
|
||||
} catch (error) {
|
||||
const message = extractErrorMessage(error, 'Unable to create draft metadata.')
|
||||
dispatch({ type: 'FINISH_ERROR', error: message })
|
||||
pushNotice('error', message)
|
||||
const notice = mapUploadErrorNotice(error, 'Unable to create draft metadata.')
|
||||
dispatch({ type: 'FINISH_ERROR', error: notice.message })
|
||||
pushMappedNotice(notice)
|
||||
return null
|
||||
}
|
||||
}, [state.artworkId, state.metadata, state.draftId, extractErrorMessage, pushNotice])
|
||||
}, [state.artworkId, state.metadata, state.draftId, pushMappedNotice])
|
||||
|
||||
const syncArtworkTags = useCallback(async (artworkId) => {
|
||||
const tags = Array.from(new Set(parseUiTags(state.metadata.tags)))
|
||||
@@ -319,11 +328,11 @@ function useUploadMachine({ draftId, filesCdnUrl, chunkSize, userId }) {
|
||||
await window.axios.put(`/api/artworks/${artworkId}/tags`, { tags })
|
||||
return true
|
||||
} catch (error) {
|
||||
const message = extractErrorMessage(error, 'Tag sync failed. Upload will continue.')
|
||||
pushNotice('error', message)
|
||||
const notice = mapUploadErrorNotice(error, 'Tag sync failed. Upload will continue.')
|
||||
pushMappedNotice({ ...notice, type: 'warning' })
|
||||
return false
|
||||
}
|
||||
}, [state.metadata.tags, extractErrorMessage, pushNotice])
|
||||
}, [state.metadata.tags, pushMappedNotice])
|
||||
|
||||
const fetchStatus = useCallback(async (sessionId, uploadToken) => {
|
||||
const res = await window.axios.get(`/api/uploads/status/${sessionId}`, {
|
||||
@@ -393,9 +402,9 @@ function useUploadMachine({ draftId, filesCdnUrl, chunkSize, userId }) {
|
||||
try {
|
||||
status = await fetchStatus(sessionId, uploadToken)
|
||||
} catch (error) {
|
||||
const message = extractErrorMessage(error, 'Unable to resume upload.')
|
||||
dispatch({ type: 'UPLOAD_ERROR', error: message })
|
||||
pushNotice('error', message)
|
||||
const notice = mapUploadErrorNotice(error, 'Unable to resume upload.')
|
||||
dispatch({ type: 'UPLOAD_ERROR', error: notice.message })
|
||||
pushMappedNotice(notice)
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -414,39 +423,53 @@ function useUploadMachine({ draftId, filesCdnUrl, chunkSize, userId }) {
|
||||
offset = nextOffset
|
||||
}
|
||||
} catch (error) {
|
||||
const message = extractErrorMessage(error, 'File upload failed. Please retry.')
|
||||
dispatch({ type: 'UPLOAD_ERROR', error: message })
|
||||
pushNotice('error', message)
|
||||
const notice = mapUploadErrorNotice(error, 'File upload failed. Please retry.')
|
||||
dispatch({ type: 'UPLOAD_ERROR', error: notice.message })
|
||||
pushMappedNotice(notice)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}, [chunkSize, fetchStatus, uploadChunk])
|
||||
}, [chunkSize, fetchStatus, uploadChunk, pushMappedNotice])
|
||||
|
||||
const finishUpload = useCallback(async (sessionId, uploadToken, artworkId) => {
|
||||
dispatch({ type: 'FINISH_START' })
|
||||
try {
|
||||
const res = await window.axios.post(
|
||||
'/api/uploads/finish',
|
||||
{ session_id: sessionId, artwork_id: artworkId, upload_token: uploadToken },
|
||||
{
|
||||
session_id: sessionId,
|
||||
artwork_id: artworkId,
|
||||
upload_token: uploadToken,
|
||||
file_name: String(state.file?.name || ''),
|
||||
},
|
||||
{ headers: { 'X-Upload-Token': uploadToken } }
|
||||
)
|
||||
const data = res.data || {}
|
||||
const previewPath = data.preview_path
|
||||
const previewUrl = previewPath ? `${filesCdnUrl}/${previewPath}` : null
|
||||
dispatch({ type: 'FINISH_SUCCESS', status: data.status, previewUrl })
|
||||
|
||||
const finishNotice = mapUploadResultNotice(data, {
|
||||
fallbackType: String(data.status || '').toLowerCase() === 'queued' ? 'warning' : 'success',
|
||||
fallbackMessage: String(data.status || '').toLowerCase() === 'queued'
|
||||
? 'Upload received. Processing is queued.'
|
||||
: 'Upload finalized successfully.',
|
||||
})
|
||||
pushMappedNotice(finishNotice)
|
||||
|
||||
if (userId) {
|
||||
clearStoredSession(userId)
|
||||
}
|
||||
return true
|
||||
} catch (error) {
|
||||
const message = extractErrorMessage(error, 'Upload finalization failed.')
|
||||
dispatch({ type: 'FINISH_ERROR', error: message })
|
||||
pushNotice('error', message)
|
||||
const notice = mapUploadErrorNotice(error, 'Upload finalization failed.')
|
||||
dispatch({ type: 'FINISH_ERROR', error: notice.message })
|
||||
pushMappedNotice(notice)
|
||||
return false
|
||||
}
|
||||
}, [filesCdnUrl, userId])
|
||||
}, [filesCdnUrl, userId, pushMappedNotice])
|
||||
|
||||
const startUpload = useCallback(async () => {
|
||||
if (!state.file) {
|
||||
@@ -529,6 +552,7 @@ function useUploadMachine({ draftId, filesCdnUrl, chunkSize, userId }) {
|
||||
}
|
||||
dispatch({ type: 'CANCEL_SUCCESS' })
|
||||
dispatch({ type: 'RESET' })
|
||||
pushNotice('warning', 'Upload cancelled.')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -547,7 +571,8 @@ function useUploadMachine({ draftId, filesCdnUrl, chunkSize, userId }) {
|
||||
}
|
||||
dispatch({ type: 'CANCEL_SUCCESS' })
|
||||
dispatch({ type: 'RESET' })
|
||||
}, [state.sessionId, state.uploadToken, userId])
|
||||
pushNotice('warning', 'Upload cancelled.')
|
||||
}, [state.sessionId, state.uploadToken, userId, pushNotice])
|
||||
|
||||
return {
|
||||
state,
|
||||
@@ -683,7 +708,9 @@ export default function UploadPage({ draftId, filesCdnUrl, chunkSize }) {
|
||||
aria-live="polite"
|
||||
className={`rounded-xl border px-4 py-3 text-sm ${notice.type === 'error'
|
||||
? 'border-red-500/40 bg-red-500/10 text-red-100'
|
||||
: 'border-emerald-400/40 bg-emerald-400/10 text-emerald-100'}`}
|
||||
: notice.type === 'warning'
|
||||
? 'border-amber-400/40 bg-amber-400/10 text-amber-100'
|
||||
: 'border-emerald-400/40 bg-emerald-400/10 text-emerald-100'}`}
|
||||
>
|
||||
{notice.message}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user