diff --git a/packages/core/upload/admin/src/pages/App/components/DocAssetCard.js b/packages/core/upload/admin/src/pages/App/components/DocAssetCard.js new file mode 100644 index 0000000000..81a2d702f0 --- /dev/null +++ b/packages/core/upload/admin/src/pages/App/components/DocAssetCard.js @@ -0,0 +1,69 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import styled from 'styled-components'; +import { + Card, + CardAction, + CardAsset, + CardBadge, + CardBody, + CardCheckbox, + CardContent, + CardHeader, + CardTitle, + CardSubtitle, +} from '@strapi/parts/Card'; +import { IconButton } from '@strapi/parts/IconButton'; +import EditIcon from '@strapi/icons/EditIcon'; +import IconDocumentation from '@strapi/icons/IconDocumentation'; +import { useIntl } from 'react-intl'; +import { getTrad } from '../../../utils'; + +const Extension = styled.span` + text-transform: uppercase; +`; + +const IconWrapper = styled.span` + svg { + font-size: 3rem; + } +`; + +export const DocAssetCard = ({ name, extension }) => { + const { formatMessage } = useIntl(); + + return ( + + + + + } + /> + + + + + + + + + + {name} + + {extension.replace('.', '')} + + + + {formatMessage({ id: getTrad('settings.section.doc.label'), defaultMessage: 'Doc' })} + + + + ); +}; + +DocAssetCard.propTypes = { + extension: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, +}; diff --git a/packages/core/upload/admin/src/pages/App/components/ImageAssetCard.js b/packages/core/upload/admin/src/pages/App/components/ImageAssetCard.js new file mode 100644 index 0000000000..c61c592a0b --- /dev/null +++ b/packages/core/upload/admin/src/pages/App/components/ImageAssetCard.js @@ -0,0 +1,61 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import styled from 'styled-components'; +import { + Card, + CardAction, + CardAsset, + CardBadge, + CardBody, + CardCheckbox, + CardContent, + CardHeader, + CardTitle, + CardSubtitle, +} from '@strapi/parts/Card'; +import { IconButton } from '@strapi/parts/IconButton'; +import EditIcon from '@strapi/icons/EditIcon'; +import { useIntl } from 'react-intl'; +import { getTrad } from '../../../utils'; + +const Extension = styled.span` + text-transform: uppercase; +`; + +export const ImageAssetCard = ({ name, extension, height, width, thumbnail }) => { + const { formatMessage } = useIntl(); + + return ( + + + + + } + /> + + + + + + {name} + + {extension.replace('.', '')} - {height}✕{width} + + + + {formatMessage({ id: getTrad('settings.section.image.label'), defaultMessage: 'Image' })} + + + + ); +}; + +ImageAssetCard.propTypes = { + extension: PropTypes.string.isRequired, + height: PropTypes.number.isRequired, + name: PropTypes.string.isRequired, + width: PropTypes.number.isRequired, + thumbnail: PropTypes.string.isRequired, +}; diff --git a/packages/core/upload/admin/src/pages/App/components/ListView.js b/packages/core/upload/admin/src/pages/App/components/ListView.js index f48a6468b2..1550c21f06 100644 --- a/packages/core/upload/admin/src/pages/App/components/ListView.js +++ b/packages/core/upload/admin/src/pages/App/components/ListView.js @@ -1,9 +1,50 @@ import React from 'react'; import PropTypes from 'prop-types'; +import { GridLayout } from '@strapi/parts/Layout'; +import { KeyboardNavigable } from '@strapi/parts/KeyboardNavigable'; +import { ImageAssetCard } from './ImageAssetCard'; +import { VideoAssetCard } from './VideoAssetCard'; +import { DocAssetCard } from './DocAssetCard'; -// TODO: implement the view export const ListView = ({ assets }) => { - return
Number of assets: {assets.length}
; + return ( + + + {assets.map(asset => { + if (asset.mime.includes('video')) { + return ( + + ); + } + + if (asset.mime.includes('image')) { + return ( + + ); + } + + return ( + + ); + })} + + + ); }; ListView.propTypes = { diff --git a/packages/core/upload/admin/src/pages/App/components/VideoAssetCard.js b/packages/core/upload/admin/src/pages/App/components/VideoAssetCard.js new file mode 100644 index 0000000000..fb41e24b67 --- /dev/null +++ b/packages/core/upload/admin/src/pages/App/components/VideoAssetCard.js @@ -0,0 +1,67 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import styled from 'styled-components'; +import { + Card, + CardAction, + CardAsset, + CardBadge, + CardBody, + CardCheckbox, + CardContent, + CardHeader, + CardTitle, + CardSubtitle, + CardTimer, +} from '@strapi/parts/Card'; +import { IconButton } from '@strapi/parts/IconButton'; +import EditIcon from '@strapi/icons/EditIcon'; +import { useIntl } from 'react-intl'; +import { VideoPreview } from './VideoPreview'; +import { getTrad, formatDuration } from '../../../utils'; + +const Extension = styled.span` + text-transform: uppercase; +`; + +export const VideoAssetCard = ({ name, extension, url, mime }) => { + const { formatMessage } = useIntl(); + const [duration, setDuration] = useState(); + const formattedDuration = duration ? formatDuration(duration) : undefined; + + return ( + + + + + } + /> + + + + + {formattedDuration || '...'} + + + + {name} + + {extension.replace('.', '')} + + + + {formatMessage({ id: getTrad('settings.section.video.label'), defaultMessage: 'Doc' })} + + + + ); +}; + +VideoAssetCard.propTypes = { + extension: PropTypes.string.isRequired, + mime: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + url: PropTypes.string.isRequired, +}; diff --git a/packages/core/upload/admin/src/pages/App/components/VideoPreview.js b/packages/core/upload/admin/src/pages/App/components/VideoPreview.js new file mode 100644 index 0000000000..f5474c8bda --- /dev/null +++ b/packages/core/upload/admin/src/pages/App/components/VideoPreview.js @@ -0,0 +1,52 @@ +/* eslint-disable jsx-a11y/media-has-caption */ +import React, { useRef, useState } from 'react'; +import PropTypes from 'prop-types'; +import styled from 'styled-components'; + +const VideoPreviewWrapper = styled.div` + canvas { + display: block; + max-width: 100%; + max-height: 100%; + } +`; + +export const VideoPreview = ({ url, mime, onLoadDuration }) => { + const [loaded, setLoaded] = useState(false); + const videoRef = useRef(null); + const canvasRef = useRef(null); + + const handleThumbnailVisibility = () => { + const video = videoRef.current; + const canvas = canvasRef.current; + + canvas.height = video.videoHeight; + canvas.width = video.videoWidth; + canvas.getContext('2d').drawImage(video, 0, 0); + + onLoadDuration(video.duration); + setLoaded(true); + }; + + return ( + + {!loaded && ( + + )} + + + + ); +}; + +VideoPreview.propTypes = { + url: PropTypes.string.isRequired, + mime: PropTypes.string.isRequired, + onLoadDuration: PropTypes.func.isRequired, +}; diff --git a/packages/core/upload/admin/src/pages/App/components/tests/ListView.test.js b/packages/core/upload/admin/src/pages/App/components/tests/ListView.test.js new file mode 100644 index 0000000000..96ea02eabc --- /dev/null +++ b/packages/core/upload/admin/src/pages/App/components/tests/ListView.test.js @@ -0,0 +1,792 @@ +import React from 'react'; +import { ThemeProvider, lightTheme } from '@strapi/parts'; +import { render as renderTL } from '@testing-library/react'; +import { ListView } from '../ListView'; +import en from '../../../../translations/en.json'; + +jest.mock('../../../../utils', () => ({ + ...jest.requireActual('../../../../utils'), + getTrad: x => x, +})); + +jest.mock('react-intl', () => ({ + FormattedMessage: ({ id }) => id, + useIntl: () => ({ formatMessage: jest.fn(({ id }) => en[id]) }), +})); + +const data = [ + { + id: 1, + name: 'strapi-cover_1fabc982ce.png', + alternativeText: '', + caption: '', + width: 1066, + height: 551, + formats: { + thumbnail: { + name: 'thumbnail_strapi-cover_1fabc982ce.png', + hash: 'thumbnail_strapi_cover_1fabc982ce_5b43615ed5', + ext: '.png', + mime: 'image/png', + width: 245, + height: 127, + size: 3.37, + path: null, + url: '/uploads/thumbnail_strapi_cover_1fabc982ce_5b43615ed5.png', + }, + large: { + name: 'large_strapi-cover_1fabc982ce.png', + hash: 'large_strapi_cover_1fabc982ce_5b43615ed5', + ext: '.png', + mime: 'image/png', + width: 1000, + height: 517, + size: 22.43, + path: null, + url: '/uploads/large_strapi_cover_1fabc982ce_5b43615ed5.png', + }, + medium: { + name: 'medium_strapi-cover_1fabc982ce.png', + hash: 'medium_strapi_cover_1fabc982ce_5b43615ed5', + ext: '.png', + mime: 'image/png', + width: 750, + height: 388, + size: 14.62, + path: null, + url: '/uploads/medium_strapi_cover_1fabc982ce_5b43615ed5.png', + }, + small: { + name: 'small_strapi-cover_1fabc982ce.png', + hash: 'small_strapi_cover_1fabc982ce_5b43615ed5', + ext: '.png', + mime: 'image/png', + width: 500, + height: 258, + size: 8.38, + path: null, + url: '/uploads/small_strapi_cover_1fabc982ce_5b43615ed5.png', + }, + }, + hash: 'strapi_cover_1fabc982ce_5b43615ed5', + ext: '.png', + mime: 'image/png', + size: 6.85, + url: '/uploads/strapi_cover_1fabc982ce_5b43615ed5.png', + previewUrl: null, + provider: 'local', + provider_metadata: null, + created_at: '2021-09-14T07:32:50.816Z', + updated_at: '2021-09-14T07:32:50.816Z', + }, + { + id: 5, + name: 'mov_bbb.mp4', + alternativeText: '', + caption: '', + width: null, + height: null, + formats: null, + hash: 'mov_bbb_2f3907f7aa', + ext: '.mp4', + mime: 'video/mp4', + size: 788.49, + url: '/uploads/mov_bbb_2f3907f7aa.mp4', + previewUrl: null, + provider: 'local', + provider_metadata: null, + created_at: '2021-09-14T07:48:30.882Z', + updated_at: '2021-09-14T07:48:30.882Z', + }, + { + id: 6, + name: 'CARTE MARIAGE AVS - Printemps.pdf', + alternativeText: '', + caption: '', + width: null, + height: null, + formats: null, + hash: 'CARTE_MARIAGE_AVS_Printemps_1f87b19e18', + ext: '.pdf', + mime: 'application/pdf', + size: 422.37, + url: '/uploads/CARTE_MARIAGE_AVS_Printemps_1f87b19e18.pdf', + previewUrl: null, + provider: 'local', + provider_metadata: null, + created_at: '2021-09-14T07:51:59.845Z', + updated_at: '2021-09-14T07:51:59.845Z', + }, +]; + +describe('MediaLibrary / ListView', () => { + it('snapshots the listview', () => { + const { container } = renderTL( + + + + ); + + expect(container).toMatchInlineSnapshot(` + .c29 { + border: 0; + -webkit-clip: rect(0 0 0 0); + clip: rect(0 0 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; + } + + .c0 { + display: grid; + grid-template-columns: repeat(auto-fit,minmax(250px,1fr)); + grid-gap: 16px; + } + + .c1 { + background: #ffffff; + border-radius: 4px; + box-shadow: 0px 1px 4px rgba(33,33,52,0.1); + } + + .c11 { + padding-top: 8px; + padding-right: 12px; + padding-bottom: 8px; + padding-left: 12px; + } + + .c16 { + background: #f6f6f9; + color: #666687; + padding: 4px; + border-radius: 4px; + } + + .c23 { + background: #32324d; + color: #ffffff; + padding: 4px; + border-radius: 4px; + } + + .c4 { + position: absolute; + top: 12px; + left: 12px; + } + + .c6 { + position: absolute; + top: 12px; + right: 12px; + } + + .c2 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + } + + .c12 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + } + + .c10 { + margin: 0; + padding: 0; + max-height: 100%; + max-width: 100%; + } + + .c9 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + height: 10.25rem; + width: 100%; + background: repeating-conic-gradient(#f6f6f9 0% 25%,transparent 0% 50%) 50% / 20px 20px; + } + + .c13 { + font-weight: 500; + font-size: 0.75rem; + line-height: 1.33; + color: #32324d; + } + + .c14 { + font-weight: 400; + font-size: 0.75rem; + line-height: 1.33; + color: #666687; + } + + .c19 { + font-weight: 400; + font-size: 0.875rem; + line-height: 1.43; + color: #32324d; + } + + .c25 { + font-weight: 400; + font-size: 0.75rem; + line-height: 1.33; + color: #ffffff; + } + + .c20 { + font-weight: 600; + line-height: 1.14; + } + + .c21 { + font-weight: 600; + font-size: 0.6875rem; + line-height: 1.45; + text-transform: uppercase; + } + + .c17 { + display: inline-block; + } + + .c18 { + margin-left: auto; + } + + .c5 { + margin: 0; + height: 18px; + min-width: 18px; + border-radius: 4px; + border: 1px solid #c0c0cf; + -webkit-appearance: none; + background-color: #ffffff; + } + + .c5:checked { + background-color: #4945ff; + border: 1px solid #4945ff; + } + + .c5:checked:after { + content: ''; + display: block; + position: relative; + background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAiIGhlaWdodD0iOCIgdmlld0JveD0iMCAwIDEwIDgiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGgKICAgIGQ9Ik04LjU1MzIzIDAuMzk2OTczQzguNjMxMzUgMC4zMTYzNTUgOC43NjA1MSAwLjMxNTgxMSA4LjgzOTMxIDAuMzk1NzY4TDkuODYyNTYgMS40MzQwN0M5LjkzODkzIDEuNTExNTcgOS45MzkzNSAxLjYzNTkgOS44NjM0OSAxLjcxMzlMNC4wNjQwMSA3LjY3NzI0QzMuOTg1OSA3Ljc1NzU1IDMuODU3MDcgNy43NTgwNSAzLjc3ODM0IDcuNjc4MzRMMC4xMzg2NiAzLjk5MzMzQzAuMDYxNzc5OCAzLjkxNTQ5IDAuMDYxNzEwMiAzLjc5MDMyIDAuMTM4NTA0IDMuNzEyNEwxLjE2MjEzIDIuNjczNzJDMS4yNDAzOCAyLjU5NDMyIDEuMzY4NDMgMi41OTQyMiAxLjQ0NjggMi42NzM0OEwzLjkyMTc0IDUuMTc2NDdMOC41NTMyMyAwLjM5Njk3M1oiCiAgICBmaWxsPSJ3aGl0ZSIKICAvPgo8L3N2Zz4=) no-repeat no-repeat center center; + width: 10px; + height: 10px; + left: 50%; + top: 50%; + -webkit-transform: translateX(-50%) translateY(-50%); + -ms-transform: translateX(-50%) translateY(-50%); + transform: translateX(-50%) translateY(-50%); + } + + .c5:checked:disabled:after { + background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAiIGhlaWdodD0iOCIgdmlld0JveD0iMCAwIDEwIDgiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGgKICAgIGQ9Ik04LjU1MzIzIDAuMzk2OTczQzguNjMxMzUgMC4zMTYzNTUgOC43NjA1MSAwLjMxNTgxMSA4LjgzOTMxIDAuMzk1NzY4TDkuODYyNTYgMS40MzQwN0M5LjkzODkzIDEuNTExNTcgOS45MzkzNSAxLjYzNTkgOS44NjM0OSAxLjcxMzlMNC4wNjQwMSA3LjY3NzI0QzMuOTg1OSA3Ljc1NzU1IDMuODU3MDcgNy43NTgwNSAzLjc3ODM0IDcuNjc4MzRMMC4xMzg2NiAzLjk5MzMzQzAuMDYxNzc5OCAzLjkxNTQ5IDAuMDYxNzEwMiAzLjc5MDMyIDAuMTM4NTA0IDMuNzEyNEwxLjE2MjEzIDIuNjczNzJDMS4yNDAzOCAyLjU5NDMyIDEuMzY4NDMgMi41OTQyMiAxLjQ0NjggMi42NzM0OEwzLjkyMTc0IDUuMTc2NDdMOC41NTMyMyAwLjM5Njk3M1oiCiAgICBmaWxsPSIjOEU4RUE5IgogIC8+Cjwvc3ZnPg==) no-repeat no-repeat center center; + } + + .c5:disabled { + background-color: #dcdce4; + border: 1px solid #c0c0cf; + } + + .c5:indeterminate { + background-color: #4945ff; + border: 1px solid #4945ff; + } + + .c5:indeterminate:after { + content: ''; + display: block; + position: relative; + color: white; + height: 2px; + width: 10px; + background-color: #ffffff; + left: 50%; + top: 50%; + -webkit-transform: translateX(-50%) translateY(-50%); + -ms-transform: translateX(-50%) translateY(-50%); + transform: translateX(-50%) translateY(-50%); + } + + .c5:indeterminate:disabled { + background-color: #dcdce4; + border: 1px solid #c0c0cf; + } + + .c5:indeterminate:disabled:after { + background-color: #8e8ea9; + } + + .c3 { + position: relative; + border-bottom: 1px solid #eaeaef; + } + + .c24 { + position: absolute; + bottom: 4px; + right: 4px; + } + + .c7 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + cursor: pointer; + padding: 8px; + border-radius: 4px; + background: #ffffff; + border: 1px solid #dcdce4; + } + + .c7 svg { + height: 12px; + width: 12px; + } + + .c7 svg > g, + .c7 svg path { + fill: #ffffff; + } + + .c7[aria-disabled='true'] { + pointer-events: none; + } + + .c8 svg > g, + .c8 svg path { + fill: #8e8ea9; + } + + .c8:hover svg > g, + .c8:hover svg path { + fill: #666687; + } + + .c8:active svg > g, + .c8:active svg path { + fill: #a5a5ba; + } + + .c8[aria-disabled='true'] { + background-color: #eaeaef; + } + + .c8[aria-disabled='true'] svg path { + fill: #666687; + } + + .c15 { + text-transform: uppercase; + } + + .c22 canvas { + display: block; + max-width: 100%; + max-height: 100%; + } + + .c26 { + text-transform: uppercase; + } + + .c28 { + text-transform: uppercase; + } + + .c27 svg { + font-size: 3rem; + } + +
+
+
+
+
+
+ +
+
+ + + +
+
+ +
+ +
+
+
+
+

+ strapi-cover_1fabc982ce.png +

+
+ + png + + - + 551 + ✕ + 1066 +
+
+
+ + Image + +
+
+
+
+
+
+
+ +
+
+ + + +
+
+
+
+ + +
+
+
+ +
+
+
+
+

+ mov_bbb.mp4 +

+
+ + mp4 + +
+
+
+ + Video + +
+
+
+
+
+
+
+ +
+
+ + + +
+
+
+ + + + + +
+
+
+
+
+
+

+ CARTE MARIAGE AVS - Printemps.pdf +

+
+ + pdf + +
+
+
+ + Doc + +
+
+
+
+
+
+
+

+

+

+
+ `); + }); +}); diff --git a/packages/core/upload/admin/src/translations/en.json b/packages/core/upload/admin/src/translations/en.json index 6235a99665..63352e9ef0 100644 --- a/packages/core/upload/admin/src/translations/en.json +++ b/packages/core/upload/admin/src/translations/en.json @@ -75,8 +75,9 @@ "settings.form.videoPreview.description": "It will generate a six-second preview of the video (GIF)", "settings.form.videoPreview.label": "Preview", "settings.header.label": "Media Library - Settings", - "settings.section.image.label": "IMAGE", - "settings.section.video.label": "VIDEO", + "settings.section.image.label": "Image", + "settings.section.video.label": "Video", + "settings.section.doc.label": "Doc", "settings.sub-header.label": "Configure the settings for the media library", "sort.created_at_asc": "Oldest uploads", "sort.created_at_desc": "Most recent uploads", diff --git a/packages/core/upload/admin/src/utils/formatDuration.js b/packages/core/upload/admin/src/utils/formatDuration.js new file mode 100644 index 0000000000..98a84b2bb7 --- /dev/null +++ b/packages/core/upload/admin/src/utils/formatDuration.js @@ -0,0 +1,12 @@ +export const formatDuration = durationInSecond => { + const formatter = new Intl.DateTimeFormat('default', { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + }); + + const date = new Date(1970, 0, 1); + date.setSeconds(durationInSecond); + + return formatter.format(date); +}; diff --git a/packages/core/upload/admin/src/utils/index.js b/packages/core/upload/admin/src/utils/index.js index f1f63f28bd..381fdc35da 100644 --- a/packages/core/upload/admin/src/utils/index.js +++ b/packages/core/upload/admin/src/utils/index.js @@ -18,3 +18,4 @@ export { default as getYupError } from './getYupError'; export { default as ItemTypes } from './ItemTypes'; export { default as unformatBytes } from './unformatBytes'; export { default as urlSchema } from './urlYupSchema'; +export * from './formatDuration';