diff --git a/package-lock.json b/package-lock.json index 50dc1e1e..d7c81aa6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,11 +22,14 @@ "@tiptap/react": "^2.12.0", "@tiptap/starter-kit": "^2.12.0", "axios": "^1.6.5", + "browser-image-compression": "^2.0.2", + "classnames": "^2.5.1", "date-fns": "^3.3.1", "leaflet": "^1.9.4", "leaflet.locatecontrol": "^0.79.0", "radash": "^12.1.0", "react-colorful": "^5.6.1", + "react-dropzone": "^14.3.8", "react-icons": "^5.5.0", "react-image-crop": "^10.1.8", "react-inlinesvg": "^4.2.0", @@ -3852,6 +3855,15 @@ "node": ">= 4.0.0" } }, + "node_modules/attr-accept": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz", + "integrity": "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -3989,6 +4001,15 @@ "node": ">=8" } }, + "node_modules/browser-image-compression": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/browser-image-compression/-/browser-image-compression-2.0.2.tgz", + "integrity": "sha512-pBLlQyUf6yB8SmmngrcOw3EoS4RpQ1BcylI3T9Yqn7+4nrQTXJD4sJDe5ODnJdrvNMaio5OicFo75rDyJD2Ucw==", + "license": "MIT", + "dependencies": { + "uzip": "0.20201231.0" + } + }, "node_modules/browserslist": { "version": "4.24.4", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", @@ -4333,6 +4354,12 @@ "node": ">=8" } }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT" + }, "node_modules/clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -6407,6 +6434,18 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/file-selector": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-2.1.2.tgz", + "integrity": "sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==", + "license": "MIT", + "dependencies": { + "tslib": "^2.7.0" + }, + "engines": { + "node": ">= 12" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -9586,7 +9625,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -10811,7 +10849,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", @@ -10823,7 +10860,6 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true, "license": "MIT" }, "node_modules/property-information": { @@ -11148,6 +11184,23 @@ "react": "^18.3.1" } }, + "node_modules/react-dropzone": { + "version": "14.3.8", + "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.3.8.tgz", + "integrity": "sha512-sBgODnq+lcA4P296DY4wacOZz3JFpD99fp+hb//iBO2HHnyeZU3FwWyXJ6salNpqQdsZrgMrotuko/BdJMV8Ug==", + "license": "MIT", + "dependencies": { + "attr-accept": "^2.2.4", + "file-selector": "^2.1.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">= 10.13" + }, + "peerDependencies": { + "react": ">= 16.8 || 18.0.0" + } + }, "node_modules/react-from-dom": { "version": "0.7.5", "resolved": "https://registry.npmjs.org/react-from-dom/-/react-from-dom-0.7.5.tgz", @@ -12805,7 +12858,6 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, "license": "0BSD" }, "node_modules/tsutils": { @@ -13248,6 +13300,12 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/uzip": { + "version": "0.20201231.0", + "resolved": "https://registry.npmjs.org/uzip/-/uzip-0.20201231.0.tgz", + "integrity": "sha512-OZeJfZP+R0z9D6TmBgLq2LHzSSptGMGDGigGiEe0pr8UBe/7fdflgHlHBNDASTXB5jnFuxHpNaJywSg8YFeGng==", + "license": "MIT" + }, "node_modules/verror": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", diff --git a/package.json b/package.json index e4cd2f84..ac3ef109 100644 --- a/package.json +++ b/package.json @@ -110,11 +110,14 @@ "@tiptap/react": "^2.12.0", "@tiptap/starter-kit": "^2.12.0", "axios": "^1.6.5", + "browser-image-compression": "^2.0.2", + "classnames": "^2.5.1", "date-fns": "^3.3.1", "leaflet": "^1.9.4", "leaflet.locatecontrol": "^0.79.0", "radash": "^12.1.0", "react-colorful": "^5.6.1", + "react-dropzone": "^14.3.8", "react-icons": "^5.5.0", "react-image-crop": "^10.1.8", "react-inlinesvg": "^4.2.0", diff --git a/patches/react-dropzone/attr-accept.js b/patches/react-dropzone/attr-accept.js new file mode 100644 index 00000000..c34ee24c --- /dev/null +++ b/patches/react-dropzone/attr-accept.js @@ -0,0 +1,38 @@ +/** + * Patched version of attr-accept to fix compatibility issues with react-dropzone + */ + +function attrAccept(file, acceptedFiles) { + if (file && acceptedFiles) { + const acceptedFilesArray = Array.isArray(acceptedFiles) + ? acceptedFiles + : acceptedFiles.split(',') + + if (acceptedFilesArray.length === 0) { + return true + } + + const fileName = file.name || '' + const mimeType = (file.type || '').toLowerCase() + const baseMimeType = mimeType.replace(/\/.*$/, '') + + return acceptedFilesArray.some(function (type) { + const validType = type.trim().toLowerCase() + + if (validType.charAt(0) === '.') { + return fileName.toLowerCase().endsWith(validType) + } else if (validType.endsWith('/*')) { + // This is something like a image/* mime type + return baseMimeType === validType.replace(/\/.*$/, '') + } + + return mimeType === validType + }) + } + + return true +} + +// Export as both default and named export to support different import styles +export default attrAccept +export { attrAccept } diff --git a/src/Components/Profile/ItemFunctions.spec.tsx b/src/Components/Profile/ItemFunctions.spec.tsx index a36498c5..70ebef02 100644 --- a/src/Components/Profile/ItemFunctions.spec.tsx +++ b/src/Components/Profile/ItemFunctions.spec.tsx @@ -2,6 +2,8 @@ import { describe, it, expect, vi } from 'vitest' import { linkItem } from './itemFunctions' +import type { Item } from '#types/Item' + const toastErrorMock: (t: string) => void = vi.fn() const toastSuccessMock: (t: string) => void = vi.fn() @@ -14,8 +16,45 @@ vi.mock('react-toastify', () => ({ describe('linkItem', () => { const id = 'some-id' - let updateApi: () => void = vi.fn() - const item = { layer: { api: { updateItem: () => updateApi() } } } + let updateApi: (item: Partial) => Promise = vi.fn() + const item: Item = { + layer: { + api: { + updateItem: (item) => updateApi(item), + getItems: vi.fn(), + }, + name: '', + menuIcon: '', + menuColor: '', + menuText: '', + markerIcon: { + image: '', + }, + markerShape: 'square', + markerDefaultColor: '', + itemType: { + name: 'Test Item Type', + show_name_input: true, + show_profile_button: false, + show_start_end: true, + show_start_end_input: true, + show_text: true, + show_text_input: true, + custom_text: 'This is a custom text for the item type.', + profileTemplate: [ + { collection: 'users', id: null, item: {} }, + { collection: 'posts', id: '123', item: {} }, + ], + offers_and_needs: true, + icon_as_labels: {}, + relations: true, + template: 'default', + questlog: false, + }, + }, + id: '', + name: '', + } const updateItem = vi.fn() beforeEach(() => { diff --git a/src/Components/Profile/ProfileForm.tsx b/src/Components/Profile/ProfileForm.tsx index 1af74ff0..b4d82862 100644 --- a/src/Components/Profile/ProfileForm.tsx +++ b/src/Components/Profile/ProfileForm.tsx @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/restrict-template-expressions */ /* eslint-disable @typescript-eslint/no-unnecessary-condition */ -/* eslint-disable @typescript-eslint/no-unsafe-argument */ +import classNames from 'classnames' import { useEffect, useState } from 'react' import { useLocation, useNavigate } from 'react-router-dom' @@ -46,6 +46,8 @@ export function ProfileForm() { start: '', end: '', openCollectiveSlug: '', + gallery: [], + uploadingImages: [], }) const [updatePermission, setUpdatePermission] = useState(false) @@ -132,6 +134,8 @@ export function ProfileForm() { start: item.start ?? '', end: item.end ?? '', openCollectiveSlug: item.openCollectiveSlug ?? '', + gallery: item.gallery ?? [], + uploadingImages: [], }) // eslint-disable-next-line react-hooks/exhaustive-deps }, [item, tags, items]) @@ -147,6 +151,8 @@ export function ProfileForm() { } }, [item, layers]) + const isUpdatingGallery = state.uploadingImages.length > 0 + return ( <> linkItem(id, item, updateItem)} - unlinkItem={(id) => unlinkItem(id, item, updateItem)} + linkItem={(id: string) => linkItem(id, item, updateItem)} + unlinkItem={(id: string) => unlinkItem(id, item, updateItem)} setUrlParams={setUrlParams} > )}
+ )} +
+ ))} + +
+ +
+ + Upload Image +
+
+ + +
e.stopPropagation()}> + Do you want to delete this image? +
+
+ + +
+
+
+
+ + ) +} diff --git a/src/Components/Profile/Subcomponents/GroupSubheaderForm.tsx b/src/Components/Profile/Subcomponents/GroupSubheaderForm.tsx index 06645602..fa37db9a 100644 --- a/src/Components/Profile/Subcomponents/GroupSubheaderForm.tsx +++ b/src/Components/Profile/Subcomponents/GroupSubheaderForm.tsx @@ -1,6 +1,4 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/prefer-nullish-coalescing */ -/* eslint-disable @typescript-eslint/no-unsafe-return */ import { useEffect } from 'react' import ComboBoxInput from '#components/Input/ComboBoxInput' @@ -24,7 +22,7 @@ export const GroupSubheaderForm = ({ groupTypes, }: { state: FormState - setState: React.Dispatch> + setState: React.Dispatch> item: Item groupStates?: string[] groupTypes?: groupType[] diff --git a/src/Components/Profile/Subcomponents/ProfileStartEndForm.tsx b/src/Components/Profile/Subcomponents/ProfileStartEndForm.tsx index fa7d20c4..81699ec9 100644 --- a/src/Components/Profile/Subcomponents/ProfileStartEndForm.tsx +++ b/src/Components/Profile/Subcomponents/ProfileStartEndForm.tsx @@ -1,7 +1,6 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/no-unsafe-return */ import { PopupStartEndInput } from '#components/Map/Subcomponents/ItemPopupComponents' +import type { FormState } from '#types/FormState' import type { Item } from '#types/Item' export const ProfileStartEndForm = ({ @@ -9,7 +8,7 @@ export const ProfileStartEndForm = ({ setState, }: { item: Item - setState: React.Dispatch> + setState: React.Dispatch> }) => { return ( > + setState: React.Dispatch> dataField?: string heading: string size: string diff --git a/src/Components/Profile/Subcomponents/__snapshots__/GalleryForm.spec.tsx.snap b/src/Components/Profile/Subcomponents/__snapshots__/GalleryForm.spec.tsx.snap new file mode 100644 index 00000000..d762ae6e --- /dev/null +++ b/src/Components/Profile/Subcomponents/__snapshots__/GalleryForm.spec.tsx.snap @@ -0,0 +1,227 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`GalleryForm > with previous images > renders 1`] = ` +
+
+
+ Gallery image 1 + +
+
+ Gallery image 2 + +
+ +
+
+`; + +exports[`GalleryForm > with uploading images > renders 1`] = ` +
+
+
+ Gallery image 1 + +
+
+ Gallery image 2 + +
+
+ Gallery image 3 + +
+ +
+
+`; + +exports[`GalleryForm > without previous images > renders 1`] = ` +
+
+ +
+
+`; diff --git a/src/Components/Profile/Templates/FlexForm.tsx b/src/Components/Profile/Templates/FlexForm.tsx index 0ca8ec25..87a5dca1 100644 --- a/src/Components/Profile/Templates/FlexForm.tsx +++ b/src/Components/Profile/Templates/FlexForm.tsx @@ -1,8 +1,8 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { ContactInfoForm } from '#components/Profile/Subcomponents/ContactInfoForm' import { CrowdfundingForm } from '#components/Profile/Subcomponents/CrowdfundingForm' +import { GalleryForm } from '#components/Profile/Subcomponents/GalleryForm' import { GroupSubheaderForm } from '#components/Profile/Subcomponents/GroupSubheaderForm' import { ProfileStartEndForm } from '#components/Profile/Subcomponents/ProfileStartEndForm' import { ProfileTextForm } from '#components/Profile/Subcomponents/ProfileTextForm' @@ -16,6 +16,7 @@ const componentMap = { contactInfos: ContactInfoForm, startEnd: ProfileStartEndForm, crowdfundings: CrowdfundingForm, + gallery: GalleryForm, // weitere Komponenten hier } @@ -25,7 +26,7 @@ export const FlexForm = ({ setState, }: { state: FormState - setState: React.Dispatch> + setState: React.Dispatch> item: Item }) => { return ( diff --git a/src/Components/Profile/Templates/OnepagerForm.tsx b/src/Components/Profile/Templates/OnepagerForm.tsx index f7612160..1532a5b5 100644 --- a/src/Components/Profile/Templates/OnepagerForm.tsx +++ b/src/Components/Profile/Templates/OnepagerForm.tsx @@ -1,5 +1,3 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/no-unsafe-return */ import { TextAreaInput } from '#components/Input' import { ContactInfoForm } from '#components/Profile/Subcomponents/ContactInfoForm' import { GroupSubheaderForm } from '#components/Profile/Subcomponents/GroupSubheaderForm' @@ -13,7 +11,7 @@ export const OnepagerForm = ({ setState, }: { state: FormState - setState: React.Dispatch> + setState: React.Dispatch> item: Item }) => { return ( diff --git a/src/Components/Profile/itemFunctions.ts b/src/Components/Profile/itemFunctions.ts index c20fe63d..1c333bbe 100644 --- a/src/Components/Profile/itemFunctions.ts +++ b/src/Components/Profile/itemFunctions.ts @@ -15,6 +15,7 @@ import { encodeTag } from '#utils/FormatTags' import { hashTagRegex } from '#utils/HashTagRegex' import { randomColor } from '#utils/RandomColor' +import type { FormState } from '#types/FormState' import type { Item } from '#types/Item' // eslint-disable-next-line promise/avoid-new @@ -77,8 +78,8 @@ export const submitNewItem = async ( setAddItemPopupType('') } -export const linkItem = async (id: string, item, updateItem) => { - const newRelations = item.relations || [] +export const linkItem = async (id: string, item: Item, updateItem) => { + const newRelations = item.relations ?? [] newRelations?.push({ items_id: item.id, related_items_id: id }) const updatedItem = { id: item.id, relations: newRelations } @@ -96,7 +97,7 @@ export const linkItem = async (id: string, item, updateItem) => { } } -export const unlinkItem = async (id: string, item, updateItem) => { +export const unlinkItem = async (id: string, item: Item, updateItem) => { const newRelations = item.relations?.filter((r) => r.related_items_id !== id) const updatedItem = { id: item.id, relations: newRelations } @@ -116,7 +117,7 @@ export const unlinkItem = async (id: string, item, updateItem) => { export const handleDelete = async ( event: React.MouseEvent, - item, + item: Item, setLoading, removeItem, map, @@ -144,8 +145,8 @@ export const handleDelete = async ( } export const onUpdateItem = async ( - state, - item, + state: FormState, + item: Item, tags, addTag, setLoading, @@ -159,19 +160,20 @@ export const onUpdateItem = async ( const offerUpdates: any[] = [] // check for new offers - await state.offers?.map((o) => { + state.offers?.map((o) => { const existingOffer = item?.offers?.find((t) => t.tags_id === o.id) - existingOffer && offerUpdates.push(existingOffer.id) - if (!existingOffer && !tags.some((t) => t.id === o.id)) addTag({ ...o, offer_or_need: true }) + existingOffer && offerUpdates.push(existingOffer.tags_id) + if (!existingOffer && !tags.some((t: { id: string }) => t.id === o.id)) + addTag({ ...o, offer_or_need: true }) !existingOffer && offerUpdates.push({ items_id: item?.id, tags_id: o.id }) return null }) const needsUpdates: any[] = [] - await state.needs?.map((n) => { + state.needs?.map((n) => { const existingNeed = item?.needs?.find((t) => t.tags_id === n.id) - existingNeed && needsUpdates.push(existingNeed.id) + existingNeed && needsUpdates.push(existingNeed.tags_id) !existingNeed && needsUpdates.push({ items_id: item?.id, tags_id: n.id }) !existingNeed && !tags.some((t) => t.id === n.id) && addTag({ ...n, offer_or_need: true }) return null @@ -197,6 +199,7 @@ export const onUpdateItem = async ( ...(state.offers.length > 0 && { offers: offerUpdates }), ...(state.needs.length > 0 && { needs: needsUpdates }), ...(state.openCollectiveSlug && { openCollectiveSlug: state.openCollectiveSlug }), + gallery: state.gallery, } const offersState: any[] = [] @@ -216,7 +219,7 @@ export const onUpdateItem = async ( setLoading(true) - await state.text + state.text .toLocaleLowerCase() .match(hashTagRegex) ?.map((tag) => { @@ -234,7 +237,7 @@ export const onUpdateItem = async ( await sleep(200) if (!item.new) { - item?.layer?.api?.updateItem && + await (item?.layer?.api?.updateItem && toast .promise(item?.layer?.api?.updateItem(changedItem), { pending: 'updating Item ...', @@ -251,10 +254,10 @@ export const onUpdateItem = async ( setLoading(false) navigate(`/item/${item.id}${params && '?' + params}`) return null - }) + })) } else { item.new = false - item.layer?.api?.createItem && + await (item.layer?.api?.createItem && toast .promise(item.layer?.api?.createItem(changedItem), { pending: 'updating Item ...', @@ -280,6 +283,6 @@ export const onUpdateItem = async ( setLoading(false) navigate(`/${params && '?' + params}`) return null - }) + })) } } diff --git a/src/Utils/getImageDimensions.spec.ts b/src/Utils/getImageDimensions.spec.ts new file mode 100644 index 00000000..80688cd6 --- /dev/null +++ b/src/Utils/getImageDimensions.spec.ts @@ -0,0 +1,33 @@ +/* Currently this test suite is skipped due to the need for a browser environment. We could set + up a headless browser test environment in the future, e.g. https://vitest.dev/guide/browser/ */ +import { readFileSync } from 'node:fs' +import path from 'node:path' + +import { describe, it, expect } from 'vitest' + +import { getImageDimensions } from './getImageDimensions' + +const testImagePaths = ['./tests/image1.jpg', './tests/image2.jpg', './tests/image3.jpg'] + +const testImages = testImagePaths.map((imagePath) => + // eslint-disable-next-line security/detect-non-literal-fs-filename + readFileSync(path.join(__dirname, '../../', imagePath)), +) + +describe('getImageDimensions', () => { + it.skip('returns the correct dimensions for a valid image file', async () => { + const file = new File([testImages[0]], 'image1.jpg', { type: 'image/jpeg' }) + const dimensions = await getImageDimensions(file) + expect(dimensions).toEqual({ width: 800, height: 600 }) // Adjust expected values based on actual test image dimensions + }) + + it.skip('throws an error for an invalid file type', async () => { + const file = new File(['not an image'], 'invalid.txt', { type: 'text/plain' }) + await expect(getImageDimensions(file)).rejects.toThrow('Error reading image file') + }) + + it.skip('throws an error if the image cannot be loaded', async () => { + const file = new File([''], 'empty.jpg', { type: 'image/jpeg' }) + await expect(getImageDimensions(file)).rejects.toThrow('Error loading image') + }) +}) diff --git a/src/Utils/getImageDimensions.ts b/src/Utils/getImageDimensions.ts new file mode 100644 index 00000000..9f6f1f72 --- /dev/null +++ b/src/Utils/getImageDimensions.ts @@ -0,0 +1,30 @@ +export const getImageDimensions = ( + file: File, +): Promise<{ + width: number + height: number +}> => + // eslint-disable-next-line promise/avoid-new + new Promise((resolve, reject) => { + try { + const fileReader = new FileReader() + + fileReader.onload = () => { + try { + const img = new Image() + + img.onload = () => resolve({ width: img.width, height: img.height }) + + img.src = fileReader.result as string // is the data URL because called with readAsDataURL + } catch (error) { + reject(error) + throw new Error('Error loading image') + } + } + + fileReader.readAsDataURL(file) + } catch (error) { + reject(error) + throw new Error('Error reading image file') + } + }) diff --git a/src/assets/css/custom-file-upload.css b/src/assets/css/custom-file-upload.css index 8594a921..67f30a39 100644 --- a/src/assets/css/custom-file-upload.css +++ b/src/assets/css/custom-file-upload.css @@ -14,5 +14,5 @@ input[type="file"] { transition: .5s ease; opacity: 0; position: absolute; - transform: translate(8px, 8px); + transform: translate(16px, 16px); } \ No newline at end of file diff --git a/src/assets/image-placeholder.svg b/src/assets/image-placeholder.svg new file mode 100644 index 00000000..92c4e5ab --- /dev/null +++ b/src/assets/image-placeholder.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/types/FormState.d.ts b/src/types/FormState.d.ts index 2384e6d2..7322b58d 100644 --- a/src/types/FormState.d.ts +++ b/src/types/FormState.d.ts @@ -1,5 +1,5 @@ import type { markerIcon } from '#utils/MarkerIconFactory' -import type { Item } from './Item' +import type { GalleryItem, Item } from './Item' import type { Tag } from './Tag' export interface FormState { @@ -21,4 +21,6 @@ export interface FormState { start: string end: string openCollectiveSlug: string + gallery: GalleryItem[] + uploadingImages: File[] } diff --git a/src/types/Item.d.ts b/src/types/Item.d.ts index d1073884..54f3b00b 100644 --- a/src/types/Item.d.ts +++ b/src/types/Item.d.ts @@ -9,7 +9,7 @@ type TagIds = { tags_id: string }[] interface GalleryItem { directus_files_id: { - id: number + id: string width: number height: number } diff --git a/tests/image1.jpg b/tests/image1.jpg new file mode 100644 index 00000000..14516b6f Binary files /dev/null and b/tests/image1.jpg differ diff --git a/tests/image2.jpg b/tests/image2.jpg new file mode 100644 index 00000000..ad6af0c4 Binary files /dev/null and b/tests/image2.jpg differ diff --git a/tests/image3.jpg b/tests/image3.jpg new file mode 100644 index 00000000..356c7d24 Binary files /dev/null and b/tests/image3.jpg differ