-
Notifications
You must be signed in to change notification settings - Fork 115
feat(condo): DOMA-13015 files for news (client-side) #7465
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
7f291f1
6d7e6e9
dd9929d
694d473
36b14a7
44afa12
43e844f
7999df9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| import { Row, Col } from 'antd' | ||
| import React from 'react' | ||
|
|
||
| import { Typography } from '@open-condo/ui' | ||
|
|
||
|
|
||
| export const DocumentsPreview = ({ files }) => { | ||
| return ( | ||
| <Row gutter={[0, 8]} style={{ width: '100%' }}> | ||
| { | ||
| files?.map((file, ind) => ( | ||
| <Col key={file?.id || file?.uid || ind} span={24} style={{ width: '100%' }}> | ||
| <div style={{ borderRadius: 12, border: '1px solid #E6E8F1', padding: 12, width: '100%' }}> | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. get color from ui kit |
||
| <Typography.Text>{file?.response?.originalName}</Typography.Text> | ||
| </div> | ||
| </Col> | ||
| )) | ||
| } | ||
| </Row> | ||
| ) | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,109 @@ | ||||||||||||||
| import React, { useState } from 'react' | ||||||||||||||
|
|
||||||||||||||
| import { useDeepCompareEffect } from '@open-condo/codegen/utils/useDeepCompareEffect' | ||||||||||||||
|
|
||||||||||||||
| const getImagePreviewFromUrl = (url: string) => { | ||||||||||||||
| return new Promise<string>((resolve, reject) => { | ||||||||||||||
| const img = new Image() | ||||||||||||||
| img.crossOrigin = 'anonymous' | ||||||||||||||
|
|
||||||||||||||
| img.onload = () => { | ||||||||||||||
| const canvas = document.createElement('canvas') | ||||||||||||||
| const ctx = canvas.getContext('2d') | ||||||||||||||
|
|
||||||||||||||
|
Comment on lines
+10
to
+13
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Handle potential null canvas context.
🛡️ Proposed fix to add null checks img.onload = () => {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
+ if (!ctx) {
+ reject(new Error('Canvas 2D context not supported'))
+ return
+ }
const WIDTH = 1280Apply similar fix in const video = document.createElement('video')
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
+if (!ctx) {
+ reject(new Error('Canvas 2D context not supported'))
+ return
+}Also applies to: 43-46 🤖 Prompt for AI Agents |
||||||||||||||
| const WIDTH = 1280 | ||||||||||||||
| const HEIGHT = 720 | ||||||||||||||
|
|
||||||||||||||
| canvas.width = WIDTH | ||||||||||||||
| canvas.height = HEIGHT | ||||||||||||||
|
|
||||||||||||||
| const iw = img.width | ||||||||||||||
| const ih = img.height | ||||||||||||||
|
|
||||||||||||||
| const scale = Math.max(WIDTH / iw, HEIGHT / ih) | ||||||||||||||
|
|
||||||||||||||
| const drawWidth = iw * scale | ||||||||||||||
| const drawHeight = ih * scale | ||||||||||||||
|
|
||||||||||||||
| const offsetX = (WIDTH - drawWidth) / 2 | ||||||||||||||
| const offsetY = (HEIGHT - drawHeight) / 2 | ||||||||||||||
|
|
||||||||||||||
| ctx.drawImage(img, offsetX, offsetY, drawWidth, drawHeight) | ||||||||||||||
|
|
||||||||||||||
| resolve(canvas.toDataURL('image/jpeg', 0.85)) | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| img.onerror = reject | ||||||||||||||
| img.src = url | ||||||||||||||
| }) | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| const createVideoPreviewFromUrl = (url: string) => { | ||||||||||||||
| return new Promise<string>((resolve, reject) => { | ||||||||||||||
| const video = document.createElement('video') | ||||||||||||||
| const canvas = document.createElement('canvas') | ||||||||||||||
| const ctx = canvas.getContext('2d') | ||||||||||||||
|
|
||||||||||||||
| video.preload = 'metadata' | ||||||||||||||
| video.src = url | ||||||||||||||
| video.crossOrigin = 'anonymous' | ||||||||||||||
| video.muted = true | ||||||||||||||
| video.playsInline = true | ||||||||||||||
|
|
||||||||||||||
| video.onloadedmetadata = () => { | ||||||||||||||
| video.currentTime = Math.min(1, video.duration / 2) | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| video.onseeked = () => { | ||||||||||||||
| const WIDTH = 1280 | ||||||||||||||
| const HEIGHT = 720 | ||||||||||||||
|
|
||||||||||||||
| canvas.width = WIDTH | ||||||||||||||
| canvas.height = HEIGHT | ||||||||||||||
|
|
||||||||||||||
| const vw = video.videoWidth | ||||||||||||||
| const vh = video.videoHeight | ||||||||||||||
|
|
||||||||||||||
| const scale = Math.max(WIDTH / vw, HEIGHT / vh) | ||||||||||||||
|
|
||||||||||||||
| const drawWidth = vw * scale | ||||||||||||||
| const drawHeight = vh * scale | ||||||||||||||
|
|
||||||||||||||
| const offsetX = (WIDTH - drawWidth) / 2 | ||||||||||||||
| const offsetY = (HEIGHT - drawHeight) / 2 | ||||||||||||||
|
|
||||||||||||||
| ctx.drawImage(video, offsetX, offsetY, drawWidth, drawHeight) | ||||||||||||||
|
|
||||||||||||||
| resolve(canvas.toDataURL('image/jpeg', 0.9)) | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| video.onerror = reject | ||||||||||||||
| }) | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| export const ImageOrVideoPreview = ({ file }) => { | ||||||||||||||
| const [preview, setPreview] = useState<string>(null) | ||||||||||||||
|
|
||||||||||||||
| useDeepCompareEffect(() => { | ||||||||||||||
| const process = async () => { | ||||||||||||||
| const url = file?.response?.url || file?.url | ||||||||||||||
|
|
||||||||||||||
| let thumb = '' | ||||||||||||||
| if (file.response?.mimetype?.startsWith('image/')) { | ||||||||||||||
| thumb = await getImagePreviewFromUrl(url) | ||||||||||||||
| } | ||||||||||||||
| if (file.response?.mimetype?.startsWith('video/')) { | ||||||||||||||
| thumb = await createVideoPreviewFromUrl(url) | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| setPreview(thumb) | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| process() | ||||||||||||||
| }, [file]) | ||||||||||||||
|
Comment on lines
+88
to
+104
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Handle errors from async thumbnail generation. The 🛡️ Proposed fix to add error handling useDeepCompareEffect(() => {
const process = async () => {
const url = file?.response?.url || file?.url
+ if (!url) return
let thumb = ''
- if (file.response?.mimetype?.startsWith('image/')) {
- thumb = await getImagePreviewFromUrl(url)
- }
- if (file.response?.mimetype?.startsWith('video/')) {
- thumb = await createVideoPreviewFromUrl(url)
+ try {
+ if (file.response?.mimetype?.startsWith('image/')) {
+ thumb = await getImagePreviewFromUrl(url)
+ } else if (file.response?.mimetype?.startsWith('video/')) {
+ thumb = await createVideoPreviewFromUrl(url)
+ }
+ } catch (error) {
+ console.error('Failed to generate preview:', error)
+ return
}
setPreview(thumb)
}
process()
}, [file])🤖 Prompt for AI Agents |
||||||||||||||
|
|
||||||||||||||
| return ( | ||||||||||||||
| <img src={preview} style={{ borderRadius: 12, border: '1px solid #E6E8F1', width: '100%' }} /> | ||||||||||||||
|
Check warning on line 107 in apps/condo/domains/news/components/FilesPreview/ImageOrVideoPreview.tsx
|
||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. also about color |
||||||||||||||
| ) | ||||||||||||||
|
Comment on lines
+106
to
+108
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add alt attribute for accessibility. The 🛡️ Proposed fix- <img src={preview} style={{ borderRadius: 12, border: '1px solid `#E6E8F1`', width: '100%' }} />
+ <img src={preview} alt="" style={{ borderRadius: 12, border: '1px solid `#E6E8F1`', width: '100%' }} />Consider passing a meaningful alt text if the file name is available: - <img src={preview} style={{ borderRadius: 12, border: '1px solid `#E6E8F1`', width: '100%' }} />
+ <img
+ src={preview}
+ alt={file?.response?.originalName || 'Preview'}
+ style={{ borderRadius: 12, border: '1px solid `#E6E8F1`', width: '100%' }}
+ />📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||
| } | ||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,134 @@ | ||
| .upload-item { | ||
| position: relative; | ||
| display: flex; | ||
| flex-direction: column; | ||
| align-items: center; | ||
| justify-content: center; | ||
| width: 104px; | ||
| padding: 4px; | ||
| overflow: hidden; | ||
| background: #f5f5f5; | ||
| border-radius: 8px; | ||
| cursor: pointer; | ||
| transition: all 0.2s; | ||
| } | ||
|
|
||
| .upload-item--with-thumb { | ||
| height: 104px; | ||
| } | ||
|
|
||
| .upload-item--no-thumb { | ||
| height: 130px; | ||
| } | ||
|
|
||
| /* Контент */ | ||
| .media-wrapper { | ||
| position: relative; | ||
| width: 100%; | ||
| height: 104px; | ||
| } | ||
|
|
||
| .image { | ||
| width: 100%; | ||
| height: 100%; | ||
| object-fit: cover; | ||
| border-radius: 6px; | ||
| } | ||
|
|
||
| .icon-wrapper { | ||
| display: flex; | ||
| align-items: center; | ||
| justify-content: center; | ||
| width: 100%; | ||
| height: 100%; | ||
| font-size: 48px; | ||
| background: #e0e0e0; | ||
| border-radius: 6px; | ||
| } | ||
|
|
||
| /* Название */ | ||
| .file-name { | ||
| width: 100%; | ||
| margin-top: 4px; | ||
| overflow: hidden; | ||
| font-size: 12px; | ||
| white-space: nowrap; | ||
| text-align: center; | ||
| text-overflow: ellipsis; | ||
| } | ||
|
|
||
| /* Длительность */ | ||
| .duration { | ||
| position: absolute; | ||
| right: 4px; | ||
| bottom: 4px; | ||
| padding: 2px 6px; | ||
| color: #fff; | ||
| font-size: 10px; | ||
| background: rgb(0 0 0 / 70%); | ||
| border-radius: 4px; | ||
| } | ||
|
|
||
| /* Overlay */ | ||
| .hover-overlay { | ||
| position: absolute; | ||
| display: flex; | ||
| gap: 8px; | ||
| align-items: center; | ||
| justify-content: center; | ||
| background-color: rgb(0 0 0 / 0%); | ||
| border-radius: 6px; | ||
| transition: background 0.2s; | ||
| inset: 0; | ||
| } | ||
|
|
||
| /* Кнопки */ | ||
| .action-button { | ||
| display: flex; | ||
| align-items: center; | ||
| justify-content: center; | ||
| width: 28px; | ||
| height: 28px; | ||
| font-size: 16px; | ||
| border-radius: 50%; | ||
| cursor: pointer; | ||
| opacity: 0; | ||
| transition: opacity 0.2s, transform 0.2s; | ||
| } | ||
|
|
||
| .eye-button { | ||
| color: #000; | ||
| background: rgb(255 255 255 / 85%); | ||
| } | ||
|
|
||
| .delete-button { | ||
| color: #fff; | ||
| background: rgb(255 0 0 / 85%); | ||
|
Check warning on line 106 in apps/condo/domains/news/components/FilesUploadList.module.css
|
||
| } | ||
|
|
||
| /* Hover */ | ||
| .upload-item:hover .hover-overlay { | ||
| background-color: rgb(0 0 0 / 35%); | ||
| } | ||
|
|
||
| .upload-item:hover .action-button { | ||
| transform: scale(1.05); | ||
| opacity: 1; | ||
| } | ||
|
|
||
| .video-icon-wrapper { | ||
| position: relative; | ||
| width: 100%; | ||
| height: 100%; | ||
| } | ||
|
|
||
| .video-duration { | ||
| position: absolute; | ||
| right: 4px; | ||
| bottom: 4px; | ||
| padding: 2px 6px; | ||
| color: white; | ||
| font-size: 10px; | ||
| background: rgb(0 0 0 / 70%); | ||
| border-radius: 4px; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Returning
truehere lets any authenticatedSTAFFuser create aNewsItemFilewithoutnewsItem/organization checks. Because updates on organization-less files are later authorized bycreatedByalone incanManageNewsItemFiles, a staff user withoutcanManageNewsItemscan create an orphan file and then attach it to a news item ID they know, bypassing organization-level manage permission checks.Useful? React with 👍 / 👎.