From ef063d5cd5076bbd5eec969585fbe0f9a5a9f1f5 Mon Sep 17 00:00:00 2001 From: VolodymyrS Date: Wed, 10 Dec 2025 00:41:59 +0200 Subject: [PATCH 1/9] feat/73: enhance fact creation process with image handling and improved data structures --- .../api/streetcode/text-content/facts.api.ts | 4 +- .../InterestingFactsAdminModal.component.tsx | 66 +++++++++++++++---- src/app/stores/facts-store.ts | 26 ++++++-- src/app/stores/image-store.ts | 13 ++-- src/app/stores/root-store.ts | 2 + src/models/streetcode/text-contents.model.ts | 18 +++-- 6 files changed, 100 insertions(+), 29 deletions(-) diff --git a/src/app/api/streetcode/text-content/facts.api.ts b/src/app/api/streetcode/text-content/facts.api.ts index b167a83..31fdee6 100644 --- a/src/app/api/streetcode/text-content/facts.api.ts +++ b/src/app/api/streetcode/text-content/facts.api.ts @@ -1,6 +1,6 @@ import Agent from '@api/agent.api'; import { API_ROUTES } from '@constants/api-routes.constants'; -import { Fact } from '@models/streetcode/text-contents.model'; +import { Fact, FactCreate } from '@models/streetcode/text-contents.model'; const FactsApi = { getById: (id: number) => Agent.get(`${API_ROUTES.FACTS.GET}/${id}`), @@ -11,7 +11,7 @@ const FactsApi = { `${API_ROUTES.FACTS.GET_BY_STREETCODE_ID}/${streetcodeId}`, ), - create: (fact: Fact) => Agent.post(`${API_ROUTES.FACTS.CREATE}`, fact), + create: (fact: FactCreate) => Agent.post(`${API_ROUTES.FACTS.CREATE}`, fact), update: (fact: Fact) => Agent.put(`${API_ROUTES.FACTS.UPDATE}`, fact), diff --git a/src/app/common/components/modals/InterestingFacts/FactsAdminModal/InterestingFactsAdminModal.component.tsx b/src/app/common/components/modals/InterestingFacts/FactsAdminModal/InterestingFactsAdminModal.component.tsx index 6ddcb8f..1752f38 100644 --- a/src/app/common/components/modals/InterestingFacts/FactsAdminModal/InterestingFactsAdminModal.component.tsx +++ b/src/app/common/components/modals/InterestingFacts/FactsAdminModal/InterestingFactsAdminModal.component.tsx @@ -1,4 +1,4 @@ -import './InterestingFactsAdminModal.style.scss'; +// import './InterestingFactsAdminModal.style.scss'; import { observer } from 'mobx-react-lite'; import { useState } from 'react'; @@ -13,34 +13,78 @@ import { UploadFile } from 'antd/lib/upload/interface'; import Image, { ImageCreate } from '@/models/media/image.model'; import { Fact, FactCreate } from '@/models/streetcode/text-contents.model'; -const InterestingFactsModal = () => { - const { modalStore, factsStore, imagesStore: { getImageArray } } = useMobx(); +interface InterestingFactsModalProps { + streetcodeId: number; +} + +const InterestingFactsModal = ({ streetcodeId = 1 }: InterestingFactsModalProps) => { + const { modalStore, factsStore, imagesStore: { getImageArray, createImage } } = useMobx(); const { setModal, modalsState: { adminFacts } } = modalStore; const [factContent, setFactContent] = useState(''); const characterCount = factContent.length | 0; - const onFinish = (values: any) => { + // Допоміжна функція: конвертує File в base64 string (data URL format) + const fileToBase64 = (file: File): Promise => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.onload = () => { + const result = reader.result as string; + const base64 = result.split(',')[1]; + resolve(base64); + }; + reader.onerror = (error) => reject(error); + }); + }; + + const onFinish = async (values: any) => { + // 1. Отримуємо файл з форми (Ant Design Upload component) const uploadedFile = values.picture.file as UploadFile; + const file = uploadedFile.originFileObj as File; - const image: ImageCreate = { - blobName: uploadedFile.name ?? '', - mimeType: uploadedFile.type ?? '', + // 2. Витягуємо ім'я файлу (якщо є, інакше порожній рядок) + const fileName = uploadedFile.name ?? ''; + + // 3. Витягуємо розширення файлу (наприклад, "jpg", "png") + const extension = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase(); + + const base64 = await fileToBase64(file); + + // 4. Створюємо об'єкт ImageCreate для завантаження зображення + const imageToCreate: ImageCreate = { + title: values.title || fileName, // Назва зображення + baseFormat: base64, + mimeType: file.type ?? 'image/jpeg', // image/jpeg, image/png, etc. + extension: extension, // jpg, png, jpeg, webp }; + const createdImage = await createImage(imageToCreate); + + if (createdImage === null) + { + if (createdImage === null) { + throw new Error('Не вдалося створити зображення'); + } + } + const fact: FactCreate = { title: values.title, - factContent, - image, + factContent: factContent, + imageId: createdImage.id, + streetcodeId: streetcodeId }; - factsStore.addFactToCreate(fact); + await factsStore.createFact(fact); + + // Close the modal + setModal('adminFacts'); }; return ( setModal('adminFacts')} footer={null} maskClosable diff --git a/src/app/stores/facts-store.ts b/src/app/stores/facts-store.ts index 07b5e35..e340cd7 100644 --- a/src/app/stores/facts-store.ts +++ b/src/app/stores/facts-store.ts @@ -29,7 +29,7 @@ export default class FactsStore { public setImageDetails = (fact: FactCreate, imageDetailId: number) => { this.factImageDetailsMap.set(fact.imageId, { id: imageDetailId, imageId: fact.imageId, - alt: fact.imageDescription, + alt: fact.title, title: '' }); }; @@ -59,7 +59,7 @@ export default class FactsStore { this.setItem(fact); this.factImageDetailsMap.set( fact.imageId, - { id: 0, imageId: fact.imageId, alt: fact.imageDescription, title: '' }, + { id: 0, imageId: fact.imageId, alt: fact.title, title: '' }, ); }; @@ -91,11 +91,25 @@ export default class FactsStore { return Array(0); }; - public createFact = async (fact: Fact) => { + public createFact = async (factCreate: FactCreate): Promise => { try { - await factsApi.create(fact); - this.setItem(fact); - } catch (error: unknown) { /* empty */ } + const createdFact = await factsApi.create(factCreate); + + runInAction(() => { + // Додаємо створений факт до map + const factToStore: FactUpdate = { + ...createdFact, + isPersisted: true, + modelState: ModelState.Updated, + }; + this.setItem(factToStore); + }); + + return createdFact; + } catch (error: unknown) { + console.error('Error creating fact:', error); + return null; + } }; public updateFact = async (fact: Fact) => { diff --git a/src/app/stores/image-store.ts b/src/app/stores/image-store.ts index 9e689eb..3184582 100644 --- a/src/app/stores/image-store.ts +++ b/src/app/stores/image-store.ts @@ -51,12 +51,15 @@ export default class ImageStore { } catch (error: unknown) {} }; - public createImage = async (image: ImageCreate) => { + public createImage = async (image: ImageCreate): Promise => { try { - await imagesApi.create(image).then((resp) => { - this.setItem(resp); - }); - } catch (error: unknown) {} + const createdImage = await imagesApi.create(image); + this.setItem(createdImage); + return createdImage; + } catch (error: unknown) { + console.error('Error creating image:', error); + return null; + } }; public updateImage = async (image: Image) => { diff --git a/src/app/stores/root-store.ts b/src/app/stores/root-store.ts index 09ea9f5..cead227 100644 --- a/src/app/stores/root-store.ts +++ b/src/app/stores/root-store.ts @@ -62,6 +62,7 @@ interface Store { streetcodeMainPageStore: StreetcodesMainPageStore, relatedByTag: StreetcodesByTagStore, createUpdateMediaStore: CreateUpdateMediaStore, + modalStore: ModalStore, } export interface StreetcodeDataStore { @@ -100,6 +101,7 @@ export const store: Store = { streetcodeMainPageStore: new StreetcodesMainPageStore(), relatedByTag: new StreetcodesByTagStore(), createUpdateMediaStore: new CreateUpdateMediaStore(), + modalStore: new ModalStore() }; export const streetcodeDataStore:StreetcodeDataStore = { streetcodeStore: new StreetcodeStore(), diff --git a/src/models/streetcode/text-contents.model.ts b/src/models/streetcode/text-contents.model.ts index 2c51d88..d2c3701 100644 --- a/src/models/streetcode/text-contents.model.ts +++ b/src/models/streetcode/text-contents.model.ts @@ -4,18 +4,26 @@ import Image, { ImageCreate } from '@models/media/image.model'; import Streetcode from './streetcode-types.model'; +// Базовий інтерфейс для відображення факту export interface Fact { id: number; title: string; factContent: string; - image?: Image; imageId: number; + image?: Image; } -export interface FactCreate extends Fact { - imageDescription?: string + +// Для створення нового факту +export interface FactCreate { + title: string; + factContent: string; + imageId: number; + streetcodeId: number; } -export interface FactUpdate extends FactCreate, IModelState, IPersisted { - streetcodeId?: number; + +// Для оновлення факту +export interface FactUpdate extends Fact, IModelState, IPersisted { + streetcodeId?: number; } export interface Term { From 4c1ecdabab39a7f37da12e8a8bae6f3eb14c1bb2 Mon Sep 17 00:00:00 2001 From: VolodymyrShapoval Date: Thu, 11 Dec 2025 00:37:40 +0200 Subject: [PATCH 2/9] feat/73: partialy implement frontend for admin interesting facts block with styling and form enhancements --- .../InterestingFactsAdminModal.component.tsx | 50 +++++--- .../InterestingFactsAdminModal.styles.scss | 115 ++++++++++++++++++ 2 files changed, 145 insertions(+), 20 deletions(-) create mode 100644 src/app/common/components/modals/InterestingFacts/FactsAdminModal/InterestingFactsAdminModal.styles.scss diff --git a/src/app/common/components/modals/InterestingFacts/FactsAdminModal/InterestingFactsAdminModal.component.tsx b/src/app/common/components/modals/InterestingFacts/FactsAdminModal/InterestingFactsAdminModal.component.tsx index 1752f38..3edc6ce 100644 --- a/src/app/common/components/modals/InterestingFacts/FactsAdminModal/InterestingFactsAdminModal.component.tsx +++ b/src/app/common/components/modals/InterestingFacts/FactsAdminModal/InterestingFactsAdminModal.component.tsx @@ -1,4 +1,4 @@ -// import './InterestingFactsAdminModal.style.scss'; +import './InterestingFactsAdminModal.styles.scss'; import { observer } from 'mobx-react-lite'; import { useState } from 'react'; @@ -93,35 +93,45 @@ const InterestingFactsModal = ({ streetcodeId = 1 }: InterestingFactsModalProps) >

Wow-Факт

-

Заголовок

+

Заголовок

+
+

Основний текст

-