mirror of
https://github.com/strapi/strapi.git
synced 2025-11-30 00:51:06 +00:00
Merge pull request #11049 from strapi/ml-iteration-2
Pending state, error state, uploads
This commit is contained in:
commit
5ec7a14250
@ -53,8 +53,15 @@ const CancelButton = styled.button`
|
||||
}
|
||||
`;
|
||||
|
||||
export const UploadingAssetCard = ({ name, extension, assetType, file }) => {
|
||||
const { upload, cancel, error, progress } = useUpload();
|
||||
export const UploadingAssetCard = ({
|
||||
name,
|
||||
extension,
|
||||
assetType,
|
||||
file,
|
||||
onCancel,
|
||||
onStatusChange,
|
||||
}) => {
|
||||
const { upload, cancel, error, progress, status } = useUpload();
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
let badgeContent;
|
||||
@ -81,6 +88,15 @@ export const UploadingAssetCard = ({ name, extension, assetType, file }) => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
onStatusChange(status);
|
||||
}, [status, onStatusChange]);
|
||||
|
||||
const handleCancel = () => {
|
||||
cancel();
|
||||
onCancel(file);
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack size={1}>
|
||||
<Card borderColor={error ? 'danger600' : undefined}>
|
||||
@ -97,11 +113,11 @@ export const UploadingAssetCard = ({ name, extension, assetType, file }) => {
|
||||
<>
|
||||
<Box paddingBottom={2}>
|
||||
<ProgressBar value={progress} size="S">
|
||||
{progress}/100%
|
||||
{`${progress}/100%`}
|
||||
</ProgressBar>
|
||||
</Box>
|
||||
|
||||
<CancelButton type="button" onClick={cancel}>
|
||||
<CancelButton type="button" onClick={handleCancel}>
|
||||
<Text small as="span" textColor="neutral200">
|
||||
{formatMessage({
|
||||
id: 'app.components.Button.cancel',
|
||||
@ -142,4 +158,6 @@ UploadingAssetCard.propTypes = {
|
||||
extension: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
file: PropTypes.instanceOf(File).isRequired,
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
onStatusChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Box, Row } from '@strapi/parts';
|
||||
import { PaginationURLQuery, PageSizeURLQuery } from '@strapi/helper-plugin';
|
||||
|
||||
export const PaginationFooter = ({ pagination }) => {
|
||||
return (
|
||||
<Box paddingTop={6}>
|
||||
<Row alignItems="flex-end" justifyContent="space-between">
|
||||
<PageSizeURLQuery />
|
||||
<PaginationURLQuery pagination={pagination} />
|
||||
</Row>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
PaginationFooter.defaultProps = {
|
||||
pagination: {
|
||||
pageCount: 0,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
},
|
||||
};
|
||||
|
||||
PaginationFooter.propTypes = {
|
||||
pagination: PropTypes.shape({
|
||||
page: PropTypes.number,
|
||||
pageCount: PropTypes.number,
|
||||
pageSize: PropTypes.number,
|
||||
total: PropTypes.number,
|
||||
}),
|
||||
};
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ModalHeader, ModalBody, ModalFooter } from '@strapi/parts/ModalLayout';
|
||||
import { ButtonText, Text } from '@strapi/parts/Text';
|
||||
@ -16,14 +16,45 @@ import { UploadingAssetCard } from '../../AssetCard/UploadingAssetCard';
|
||||
import { getTrad } from '../../../utils';
|
||||
import { AssetType, AssetSource } from '../../../constants';
|
||||
|
||||
export const PendingAssetStep = ({ onClose, assets, onClickAddAsset }) => {
|
||||
const Status = {
|
||||
Idle: 'IDLE',
|
||||
Uploading: 'UPLOADING',
|
||||
Intermediate: 'INTERMEDIATE',
|
||||
};
|
||||
|
||||
export const PendingAssetStep = ({
|
||||
onClose,
|
||||
assets,
|
||||
onClickAddAsset,
|
||||
onCancelUpload,
|
||||
onUploadSucceed,
|
||||
}) => {
|
||||
const assetCountRef = useRef(0);
|
||||
const { formatMessage } = useIntl();
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [uploadStatus, setUploadStatus] = useState(Status.Idle);
|
||||
|
||||
const handleSubmit = async e => {
|
||||
e.preventDefault();
|
||||
|
||||
setIsUploading(true);
|
||||
setUploadStatus(Status.Uploading);
|
||||
};
|
||||
|
||||
const handleStatusChange = (status, file) => {
|
||||
if (status === 'success' || status === 'error') {
|
||||
assetCountRef.current++;
|
||||
|
||||
// There's no "terminated" status. When all the files have called their
|
||||
// onUploadSucceed callback, the parent component filters the asset list
|
||||
// and closes the modal when the asset list is empty
|
||||
if (assetCountRef.current === assets.length) {
|
||||
assetCountRef.current = 0;
|
||||
setUploadStatus(Status.Intermediate);
|
||||
}
|
||||
}
|
||||
|
||||
if (status === 'success') {
|
||||
onUploadSucceed(file);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@ -66,10 +97,10 @@ export const PendingAssetStep = ({ onClose, assets, onClickAddAsset }) => {
|
||||
</Row>
|
||||
<KeyboardNavigable tagName="article">
|
||||
<Grid gap={4}>
|
||||
{assets.map((asset, idx) => {
|
||||
const assetKey = `${asset.url}-${idx}`;
|
||||
{assets.map(asset => {
|
||||
const assetKey = asset.url;
|
||||
|
||||
if (isUploading) {
|
||||
if (uploadStatus === Status.Uploading || uploadStatus === Status.Intermediate) {
|
||||
return (
|
||||
<GridItem col={4} key={assetKey}>
|
||||
<UploadingAssetCard
|
||||
@ -79,6 +110,8 @@ export const PendingAssetStep = ({ onClose, assets, onClickAddAsset }) => {
|
||||
assetType={asset.type}
|
||||
file={asset.rawFile}
|
||||
size="S"
|
||||
onCancel={onCancelUpload}
|
||||
onStatusChange={status => handleStatusChange(status, asset.rawFile)}
|
||||
/>
|
||||
</GridItem>
|
||||
);
|
||||
@ -146,7 +179,7 @@ export const PendingAssetStep = ({ onClose, assets, onClickAddAsset }) => {
|
||||
</Button>
|
||||
}
|
||||
endActions={
|
||||
<Button type="submit" loading={isUploading}>
|
||||
<Button type="submit" loading={uploadStatus === Status.Uploading}>
|
||||
{formatMessage(
|
||||
{
|
||||
id: getTrad('modal.upload-list.footer.button.singular'),
|
||||
@ -173,4 +206,6 @@ PendingAssetStep.propTypes = {
|
||||
).isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
onClickAddAsset: PropTypes.func.isRequired,
|
||||
onUploadSucceed: PropTypes.func.isRequired,
|
||||
onCancelUpload: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
@ -62,6 +62,8 @@ describe('PendingAssetStep', () => {
|
||||
onClose={jest.fn()}
|
||||
onAddAsset={jest.fn()}
|
||||
onClickAddAsset={jest.fn()}
|
||||
onCancelUpload={jest.fn()}
|
||||
onUploadSucceed={jest.fn()}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
</QueryClientProvider>
|
||||
|
||||
@ -23,13 +23,38 @@ export const UploadAssetDialog = ({ onSuccess, onClose }) => {
|
||||
setStep(Steps.AddAsset);
|
||||
};
|
||||
|
||||
const handleCancelUpload = file => {
|
||||
const nextAssets = assets.filter(asset => asset.rawFile !== file);
|
||||
setAssets(nextAssets);
|
||||
|
||||
// When there's no asset, transition to the AddAsset step
|
||||
if (nextAssets.length === 0) {
|
||||
moveToAddAsset();
|
||||
}
|
||||
};
|
||||
|
||||
const handleUploadSuccess = file => {
|
||||
const nextAssets = assets.filter(asset => asset.rawFile !== file);
|
||||
setAssets(nextAssets);
|
||||
|
||||
if (nextAssets.length === 0) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalLayout onClose={onClose} labelledBy="title">
|
||||
{step === Steps.AddAsset && (
|
||||
<AddAssetStep onClose={onClose} onAddAsset={handleAddToPendingAssets} />
|
||||
)}
|
||||
{step === Steps.PendingAsset && (
|
||||
<PendingAssetStep onClose={onClose} assets={assets} onClickAddAsset={moveToAddAsset} />
|
||||
<PendingAssetStep
|
||||
onClose={onClose}
|
||||
assets={assets}
|
||||
onClickAddAsset={moveToAddAsset}
|
||||
onCancelUpload={handleCancelUpload}
|
||||
onUploadSucceed={handleUploadSuccess}
|
||||
/>
|
||||
)}
|
||||
</ModalLayout>
|
||||
);
|
||||
|
||||
@ -44,5 +44,11 @@ export const useUpload = () => {
|
||||
formatMessage({ id: getTrad('modal.upload.cancelled'), defaultMessage: '' })
|
||||
);
|
||||
|
||||
return { upload, cancel, error: mutation.error, progress };
|
||||
return {
|
||||
upload,
|
||||
cancel,
|
||||
error: mutation.error,
|
||||
progress,
|
||||
status: mutation.status,
|
||||
};
|
||||
};
|
||||
|
||||
@ -7,50 +7,57 @@ import { ImageAssetCard } from '../../../components/AssetCard/ImageAssetCard';
|
||||
import { VideoAssetCard } from '../../../components/AssetCard/VideoAssetCard';
|
||||
import { DocAssetCard } from '../../../components/AssetCard/DocAssetCard';
|
||||
import { AssetType } from '../../../constants';
|
||||
import { PaginationFooter } from '../../../components/PaginationFooter';
|
||||
|
||||
export const ListView = ({ assets }) => {
|
||||
return (
|
||||
<KeyboardNavigable tagName="article">
|
||||
<GridLayout>
|
||||
{assets.map(asset => {
|
||||
if (asset.mime.includes(AssetType.Video)) {
|
||||
<>
|
||||
<KeyboardNavigable tagName="article">
|
||||
<GridLayout>
|
||||
{assets.map(asset => {
|
||||
if (asset.mime.includes(AssetType.Video)) {
|
||||
return (
|
||||
<VideoAssetCard
|
||||
id={asset.id}
|
||||
key={asset.id}
|
||||
name={asset.name}
|
||||
extension={getFileExtension(asset.ext)}
|
||||
url={prefixFileUrlWithBackendUrl(asset.url)}
|
||||
mime={asset.mime}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (asset.mime.includes(AssetType.Image)) {
|
||||
return (
|
||||
<ImageAssetCard
|
||||
id={asset.id}
|
||||
key={asset.id}
|
||||
name={asset.name}
|
||||
extension={getFileExtension(asset.ext)}
|
||||
height={asset.height}
|
||||
width={asset.width}
|
||||
thumbnail={prefixFileUrlWithBackendUrl(
|
||||
asset?.formats?.thumbnail?.url || asset.url
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<VideoAssetCard
|
||||
<DocAssetCard
|
||||
id={asset.id}
|
||||
key={asset.id}
|
||||
name={asset.name}
|
||||
extension={getFileExtension(asset.ext)}
|
||||
url={prefixFileUrlWithBackendUrl(asset.url)}
|
||||
mime={asset.mime}
|
||||
/>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</GridLayout>
|
||||
</KeyboardNavigable>
|
||||
|
||||
if (asset.mime.includes(AssetType.Image)) {
|
||||
return (
|
||||
<ImageAssetCard
|
||||
id={asset.id}
|
||||
key={asset.id}
|
||||
name={asset.name}
|
||||
extension={getFileExtension(asset.ext)}
|
||||
height={asset.height}
|
||||
width={asset.width}
|
||||
thumbnail={prefixFileUrlWithBackendUrl(asset?.formats?.thumbnail?.url || asset.url)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DocAssetCard
|
||||
id={asset.id}
|
||||
key={asset.id}
|
||||
name={asset.name}
|
||||
extension={getFileExtension(asset.ext)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</GridLayout>
|
||||
</KeyboardNavigable>
|
||||
<PaginationFooter />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { ThemeProvider, lightTheme } from '@strapi/parts';
|
||||
import { render as renderTL } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router';
|
||||
import { ListView } from '../ListView';
|
||||
import en from '../../../../translations/en.json';
|
||||
|
||||
@ -121,13 +122,85 @@ const data = [
|
||||
describe('MediaLibrary / ListView', () => {
|
||||
it('snapshots the listview', () => {
|
||||
const { container } = renderTL(
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<ListView assets={data} />
|
||||
</ThemeProvider>
|
||||
<MemoryRouter>
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<ListView assets={data} />
|
||||
</ThemeProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
expect(container).toMatchInlineSnapshot(`
|
||||
.c36 {
|
||||
font-weight: 400;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.43;
|
||||
color: #32324d;
|
||||
}
|
||||
|
||||
.c27 {
|
||||
padding-top: 24px;
|
||||
}
|
||||
|
||||
.c35 {
|
||||
padding-right: 16px;
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.c37 {
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
.c28 {
|
||||
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: justify;
|
||||
-webkit-justify-content: space-between;
|
||||
-ms-flex-pack: justify;
|
||||
justify-content: space-between;
|
||||
-webkit-align-items: flex-end;
|
||||
-webkit-box-align: flex-end;
|
||||
-ms-flex-align: flex-end;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.c29 {
|
||||
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;
|
||||
}
|
||||
|
||||
.c33 {
|
||||
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: justify;
|
||||
-webkit-justify-content: space-between;
|
||||
-ms-flex-pack: justify;
|
||||
justify-content: space-between;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.c43 {
|
||||
border: 0;
|
||||
-webkit-clip: rect(0 0 0 0);
|
||||
clip: rect(0 0 0 0);
|
||||
@ -139,6 +212,128 @@ describe('MediaLibrary / ListView', () => {
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
.c30 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.c30 > * {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.c30 > * + * {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
.c31 {
|
||||
position: relative;
|
||||
border: 1px solid #dcdce4;
|
||||
padding-right: 12px;
|
||||
border-radius: 4px;
|
||||
background: #ffffff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.c31:focus-within {
|
||||
border: 1px solid #4945ff;
|
||||
}
|
||||
|
||||
.c38 {
|
||||
background: transparent;
|
||||
border: none;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.c38 svg {
|
||||
height: 0.6875rem;
|
||||
width: 0.6875rem;
|
||||
}
|
||||
|
||||
.c38 svg path {
|
||||
fill: #666687;
|
||||
}
|
||||
|
||||
.c39 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
background: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.c39 svg {
|
||||
width: 0.375rem;
|
||||
}
|
||||
|
||||
.c40 > * + * {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.c41 {
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
-webkit-text-decoration: none;
|
||||
text-decoration: none;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.c42 {
|
||||
font-size: 0.7rem;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.c42 svg path {
|
||||
fill: #c0c0cf;
|
||||
}
|
||||
|
||||
.c42:focus svg path,
|
||||
.c42:hover svg path {
|
||||
fill: #c0c0cf;
|
||||
}
|
||||
|
||||
.c44 {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.c44 svg path {
|
||||
fill: #666687;
|
||||
}
|
||||
|
||||
.c44:focus svg path,
|
||||
.c44:hover svg path {
|
||||
fill: #4a4a6a;
|
||||
}
|
||||
|
||||
.c32 {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.c32:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.c34 {
|
||||
min-height: 2.5rem;
|
||||
}
|
||||
|
||||
.c0 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit,minmax(250px,1fr));
|
||||
@ -556,6 +751,140 @@ describe('MediaLibrary / ListView', () => {
|
||||
</div>
|
||||
<div
|
||||
class="c27"
|
||||
>
|
||||
<div
|
||||
class="c28"
|
||||
>
|
||||
<div
|
||||
class="c29"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="c30"
|
||||
>
|
||||
<div
|
||||
class="c31"
|
||||
>
|
||||
<button
|
||||
aria-disabled="false"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="listbox"
|
||||
aria-labelledby="select-1-label select-1-content"
|
||||
class="c32"
|
||||
id="select-1"
|
||||
type="button"
|
||||
/>
|
||||
<div
|
||||
class="c33 c34"
|
||||
>
|
||||
<div
|
||||
class="c29"
|
||||
>
|
||||
<div
|
||||
class="c35"
|
||||
>
|
||||
<span
|
||||
class="c36"
|
||||
id="select-1-content"
|
||||
>
|
||||
10
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c29"
|
||||
>
|
||||
<button
|
||||
aria-hidden="true"
|
||||
class="c37 c38 c39"
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
fill="none"
|
||||
height="1em"
|
||||
viewBox="0 0 14 8"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
clip-rule="evenodd"
|
||||
d="M14 .889a.86.86 0 01-.26.625L7.615 7.736A.834.834 0 017 8a.834.834 0 01-.615-.264L.26 1.514A.861.861 0 010 .889c0-.24.087-.45.26-.625A.834.834 0 01.875 0h12.25c.237 0 .442.088.615.264a.86.86 0 01.26.625z"
|
||||
fill="#32324D"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<nav
|
||||
aria-label="pagination"
|
||||
class=""
|
||||
>
|
||||
<ul
|
||||
class="c29 c40"
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
aria-current="page"
|
||||
aria-disabled="true"
|
||||
class="c41 c42 active"
|
||||
href="/"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="c43"
|
||||
/>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
height="1em"
|
||||
viewBox="0 0 10 16"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M9.88 14.12L3.773 8 9.88 1.88 8 0 0 8l8 8 1.88-1.88z"
|
||||
fill="#32324D"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
aria-current="page"
|
||||
aria-disabled="false"
|
||||
class="c41 c44 active"
|
||||
href="/?page=1"
|
||||
>
|
||||
<div
|
||||
class="c43"
|
||||
/>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
height="1em"
|
||||
viewBox="0 0 10 16"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M0 1.88L6.107 8 0 14.12 1.88 16l8-8-8-8L0 1.88z"
|
||||
fill="#32324D"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c43"
|
||||
>
|
||||
<p
|
||||
aria-live="polite"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user