diff --git a/src/app/components.d.ts b/src/app/components.d.ts index 2210e214a0fd1c2a21e2c86f177a07706a03444a..be12a63384b6d68e8883778613f7f2343e6768b6 100644 --- a/src/app/components.d.ts +++ b/src/app/components.d.ts @@ -7,6 +7,9 @@ export {} /* prettier-ignore */ declare module 'vue' { export interface GlobalComponents { + AlignCenterIcon: typeof import('./../shared/icons/AlignCenterIcon.vue')['default'] + AlignLeftIcon: typeof import('./../shared/icons/AlignLeftIcon.vue')['default'] + AlignRightIcon: typeof import('./../shared/icons/AlignRightIcon.vue')['default'] App: typeof import('./App.vue')['default'] AuthorizationForm: typeof import('./../modules/authorization/AuthorizationForm.vue')['default'] BaseLoader: typeof import('./../shared/BaseLoader.vue')['default'] @@ -35,6 +38,7 @@ declare module 'vue' { ImageMenu: typeof import('./../components/entities/settings/ImageMenu.vue')['default'] ImagePositionMenu: typeof import('./../components/entities/image/ImagePositionMenu.vue')['default'] ImageSettings: typeof import('./../components/entities/settings/ImageSettings.vue')['default'] + ImageSettingsList: typeof import('./../components/entities/settings/lists/ImageSettingsList.vue')['default'] ImageSizeMenu: typeof import('./../components/entities/image/ImageSizeMenu.vue')['default'] ImageStateMenu: typeof import('./../components/entities/image/ImageStateMenu.vue')['default'] LogoAndLabel: typeof import('./../components/LogoAndLabel.vue')['default'] @@ -68,6 +72,7 @@ declare module 'vue' { TextPositionMenu: typeof import('./../components/entities/text/TextPositionMenu.vue')['default'] TextSettings: typeof import('./../components/entities/settings/TextSettings.vue')['default'] TextStateMenu: typeof import('./../components/entities/text/TextStateMenu.vue')['default'] + ToggleButton: typeof import('./../shared/ui/ToggleButton.vue')['default'] ToggleSwitch: typeof import('./../shared/ui/ToggleSwitch.vue')['default'] TrashIcon: typeof import('./../shared/icons/TrashIcon.vue')['default'] Tree: typeof import('./../shared/ui/Tree.vue')['default'] diff --git a/src/app/helpers/index.ts b/src/app/helpers/index.ts index ecfbbd7bd6a6d24153ece9e7aee436e9a9b00ff5..af4a3b0db4b87c8b0361364faffd2bb33a6cd3ae 100644 --- a/src/app/helpers/index.ts +++ b/src/app/helpers/index.ts @@ -54,11 +54,12 @@ export const editEntity = (newState: IEntity) => { export const deleteEntity = (entityUuid: string) => { const dataStore = useDataStore(); const websocketStore = useWebsocketStore(); + const page_uuid = cookies.get('current_page_uuid'); const entities = dataStore.entities; const entityToDelete = entities.find((entity) => entity.entity_uuid === entityUuid); const data = { event: 'deleteEntity', - body: { ...entityToDelete } + body: { ...entityToDelete, page_uuid } }; websocketStore.sendData(data); }; diff --git a/src/app/interfaces/ui.ts b/src/app/interfaces/ui.ts new file mode 100644 index 0000000000000000000000000000000000000000..cf6b6fe107fddfeb317f3a24c9a5a165955c4e0f --- /dev/null +++ b/src/app/interfaces/ui.ts @@ -0,0 +1,15 @@ +import type { TTheme } from '@/app/interfaces/environment'; + +export interface IToggleButtonItem { + label: string; + textColor?: TTheme; + backgroundColor?: TTheme; + isLabelHidden?: boolean; + iconPos?: string; + textStyle?: 'bold' | 'italic'; +} +export interface ISliderOption { + label: string; + value: number; + color?: string; +} diff --git a/src/components/entities/settings/ImageSettings.vue b/src/components/entities/settings/ImageSettings.vue index ce7ba0daccdd32cb3297383371f7683cac6b1c5e..e14ccf843bfb7ce6460d58b401880fbdbe7eee7e 100644 --- a/src/components/entities/settings/ImageSettings.vue +++ b/src/components/entities/settings/ImageSettings.vue @@ -1,34 +1,20 @@ <script setup lang="ts"> -import { useVModel } from '@vueuse/core'; -import { useDataStore } from '@/app/stores/data'; import type { IImage } from '@/app/interfaces/entities'; -import { deleteEntity, editEntity } from '@/app/helpers'; +import { convertThemeToColorWhiteDefault, deleteEntity, editEntity } from '@/app/helpers'; +import type { TTheme } from '@/app/interfaces/environment'; +import cookies from '@/app/plugins/Cookie'; +import type { IToggleButtonItem } from '@/app/interfaces/ui'; interface Props { entityData: IImage; } const props = defineProps<Props>(); -const emit = defineEmits(['update:entityData', 'openCropImageModal']); -const entityData = useVModel(props, 'entityData', emit); +const emit = defineEmits(['saveChanges']); +const entityData = computed(() => props.entityData); +const newEntityData = ref({ ...entityData.value }); +watch(entityData, () => (newEntityData.value = entityData.value)); +const isModal = ref<boolean>(false); -const dataStore = useDataStore(); -const entities = computed(() => dataStore.entities); - -const addTitle = () => { - editEntity({ - ...entityData.value, - title: 'Title', - entity_title_position: 'center', - font_size: entityData.value.font_size ?? '24' - }); - entityData.value = { ...entityData.value, title: 'Title', entity_title_position: 'center' }; -}; -const removeTitle = () => { - const newState = { ...entityData.value }; - newState.title = null; - editEntity({ ...newState }); - entityData.value = newState; -}; const addText = () => { editEntity({ ...entityData.value, @@ -37,7 +23,7 @@ const addText = () => { font_size: entityData.value.font_size ?? '24', paragraph_size: 'full' }); - entityData.value = { ...entityData.value, text: 'Text' }; + newEntityData.value = { ...entityData.value, text: 'Text' }; }; const removeText = () => { const newState = { ...entityData.value }; @@ -45,55 +31,259 @@ const removeText = () => { newState[item] = null; }); editEntity({ ...newState }); - entityData.value = newState; -}; -const editPosition = (position: 'left' | 'center' | 'right') => { - entityData.value.entity_position = position; - editEntity({ ...entityData.value, entity_position: position }); -}; -const editTitlePosition = (position: 'left' | 'center' | 'right') => { - entityData.value.entity_title_position = position; - editEntity({ ...entityData.value, entity_title_position: position }); -}; -const editTextPosition = (position: 'left' | 'right') => { - entityData.value.text_position = position; - editEntity({ ...entityData.value, text_position: position }); -}; -const editParagraphWidth = (widthMode: 'full' | 'half') => { - entityData.value.paragraph_size = widthMode; - editEntity({ ...entityData.value, paragraph_size: widthMode }); + newEntityData.value = newState; }; const changeFontSize = (newSize: '16' | '20' | '24' | '40' | '64') => { entityData.value.font_size = newSize; editEntity({ ...entityData.value, font_size: newSize }); }; +const themeColor: TTheme = cookies.get('favorite_color'); +const themeColorConverted = convertThemeToColorWhiteDefault(themeColor); +const isTitle = ref(!!entityData.value.title); +const isText = ref(!!entityData.value.text); +const isEntityWidthFull = ref(entityData.value.paragraph_size === 'full'); + +const maxLines = computed(() => { + if (isTitle.value) { + return Math.floor(168 / 24); + } else { + return Math.floor(240 / 24); + } +}); +const entityIsTitleOptions = ref([ + { + label: 'Off', + value: false, + textStyle: 'bold' + }, + { + label: 'On', + value: true, + textStyle: 'bold' + } +]); +const entityIsTextOptions = ref([ + { + label: 'Off', + value: false, + textStyle: 'bold' + }, + { + label: 'On', + value: true, + textStyle: 'bold' + } +]); +const entityPositionOptions = ref([ + { + label: 'left', + isLabelHidden: true + }, + { + label: 'center', + isLabelHidden: true + }, + { + label: 'right', + isLabelHidden: true + } +]); +const entityTitlePositionOptions = ref([ + { + label: 'left', + isLabelHidden: true + }, + { + label: 'center', + isLabelHidden: true + }, + { + label: 'right', + isLabelHidden: true + } +]); +const entityTextPositionOptions = ref([ + { + label: 'Left', + value: 'left', + textStyle: 'bold' + }, + { + label: 'Right', + value: 'right', + textStyle: 'bold' + } +]); +const entityParagraphSizeOptions = ref([ + { + label: 'Half', + value: 'half', + textStyle: 'bold' + }, + { + label: 'Full', + value: 'full', + textStyle: 'bold' + } +]); +const imageScaleOptions = ref([ + { + label: 'x0.25', + value: 0, + color: 'var(--purple-700)' + }, + { + label: 'x0.5', + value: 1, + color: 'var(--indigo-500)' + }, + { + label: 'x0.75', + value: 2, + color: 'var(--sky-500)' + }, + { + label: 'x1', + value: 3, + color: 'var(--green-500)' + }, + { + label: 'x1.25', + value: 4, + color: 'var(--yellow-500)' + }, + { + label: 'x1.5', + value: 5, + color: 'var(--orange-500)' + }, + { + label: 'x1.75', + value: 6, + color: 'var(--red-500)' + }, + { + label: 'x2', + value: 7, + color: 'var(--red-800)' + } +]); +const saveChanges = () => { + const entityPosition = isEntityWidthFull.value ? 'full' : 'half'; + if (entityPosition !== entityData.value.entity_position) { + newEntityData.value.paragraph_size = entityPosition; + } + if (isTitle.value !== !!entityData.value.title) { + if (isTitle.value) { + newEntityData.value.title = 'Title'; + } else { + newEntityData.value.title = null; + } + } + if (isText.value !== !!entityData.value.title) { + if (isText.value) { + newEntityData.value.title = 'Text'; + } else { + newEntityData.value.title = null; + } + } + if (JSON.stringify(entityData) !== JSON.stringify(newEntityData.value)) { + emit('saveChanges', newEntityData.value); + } + isModal.value = false; +}; </script> <template> - <section style="height: 146px" class="speedDial absolute left-2 top-0 transition-all select-none"> - <ImageStateMenu - :entityData="entityData" - class="h-12" - @deleteEntity="deleteEntity" - @addTitle="addTitle" - @removeTitle="removeTitle" - @addText="addText" - @removeText="removeText" - @openCropImageModal="$emit('openCropImageModal')" - /> - <div v-if="entityData?.text || entityData?.title"> - <TextFontMenu :entityData="entityData" class="h-12" @changeFontSize="changeFontSize" /> - </div> - <div v-if="entities.length > 1"> - <ImagePositionMenu - :entityData="entityData" - @editPosition="editPosition" - @editTitlePosition="editTitlePosition" - @editTextPosition="editTextPosition" - @editParagraphWidth="editParagraphWidth" + <button + :style="`background-color: ${themeColorConverted}`" + class="settings absolute left-2 top-0 select-none size-10 hover:brightness-75 transition-all cursor-pointer" + @click.prevent="isModal = true" + > + <SettingsIcon color="white" size="25" /> + </button> + <Modal v-model:isVisible="isModal" theme="black" width="90%" + ><template #header><h3 class="w-max mx-auto">Edit paragraph</h3></template> + <div class="p-10 flex gap-16 items-center"> + <ImageSettingsList + v-model:newEntityData="newEntityData" + v-model:isTitle="isTitle" + v-model:isText="isText" + v-model:isEntityWidthFull="isEntityWidthFull" + :themeColor="themeColor" + :entityIsTitleOptions="entityIsTitleOptions" + :entityIsTextOptions="entityIsTextOptions" + :entityPositionOptions="entityPositionOptions" + :entityTitlePositionOptions="entityTitlePositionOptions" + :entityTextPositionOptions="entityTextPositionOptions" + :entityParagraphSizeOptions="entityParagraphSizeOptions" + :imageScaleOptions="imageScaleOptions" /> + <section + :style="`border-color: var(--${themeColor}-200); height: 450px`" + class="grow flex flex-col gap-4 p-4 min-h-full border-2 border-slate-100 border-dashed rounded-2xl" + > + <div :style="`justify-content: ${newEntityData.entity_position};`" class="flex"> + <div + v-show="isTitle" + :style="`border-color: var(--${themeColor}-800); + justify-content: ${newEntityData.entity_title_position}; width: ${isEntityWidthFull ? '100%' : '50%'}`" + class="flex text-2xl font-bold text-center px-2 py-4 border-2 border-dashed rounded-2xl" + > + <h3 class="w-2/3 overflow-ellipsis overflow-hidden whitespace-nowrap"> + {{ newEntityData.title ?? 'Title' }} + </h3> + </div> + </div> + <div class="flex"> + <div :style="`justify-content: ${newEntityData.entity_position}`" class="flex"> + <div + v-show="isText" + :style="`border-color: var(--${themeColor}-400); width: ${isEntityWidthFull ? '100%' : '50%'};`" + class="h-full p-4 pb-2 border-2 border-dashed rounded-2xl overflow-hidden" + > + <p class="pb-0 overflow-hidden contain-inline-size text"> + {{ newEntityData.text ?? 'Text' }} + </p> + </div> + </div> + <img class="h-max" :src="newEntityData.imageUrl" alt="" /> + </div> + </section> + <div class="absolute top-4 right-16 z-10 hover:brightness-80 transition-all"> + <Button label="Delete" textColor="white" theme="red" textStyle="bold" size="medium"> + <template #icon> + <TrashIcon color="white" size="25" /> + </template> + </Button> + </div> + <div + class="absolute top-4 left-4 z-10 hover:brightness-80 transition-all" + @click.prevent="saveChanges" + > + <Button label="Save" textColor="white" :theme="themeColor" textStyle="bold" size="medium"> + <template #icon> + <SaveIcon color="white" size="25" /> + </template> + </Button> + </div> </div> - </section> + </Modal> </template> -<style scoped></style> +<style scoped> +.text { + --max-lines: v-bind(maxLines); + overflow: hidden; + + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: var(--max-lines); +} +.settings { + display: flex; + justify-content: center; + align-items: center; + border-radius: 50%; +} +</style> diff --git a/src/components/entities/settings/ParagraphSettings.vue b/src/components/entities/settings/ParagraphSettings.vue index d9f1dd10d5926a687b9b30ae11e5a5d257163415..8f434d8726775d1bde5ab9d7ad6aa1fb9df52379 100644 --- a/src/components/entities/settings/ParagraphSettings.vue +++ b/src/components/entities/settings/ParagraphSettings.vue @@ -1,5 +1,5 @@ <script setup lang="ts"> -import { convertThemeToColorWhiteDefault, editEntity } from '@/app/helpers'; +import { convertThemeToColorWhiteDefault, deleteEntity, editEntity } from '@/app/helpers'; import type { IParagraph } from '@/app/interfaces/entities'; import type { TTheme } from '@/app/interfaces/environment'; import cookies from '@/app/plugins/Cookie'; @@ -9,18 +9,19 @@ interface Props { } const props = defineProps<Props>(); const emit = defineEmits(['saveChanges']); -const entityData = props.entityData; -let newEntityData = ref({ ...entityData }); +const entityData = computed(() => props.entityData); +const newEntityData = ref({ ...entityData.value }); +watch(entityData, () => (newEntityData.value = entityData.value)); const isModal = ref<boolean>(false); - +const isModalToDeleteParagraph = ref<boolean>(false); const changeFontSize = (newSize: '16' | '20' | '24' | '40' | '64') => { - entityData.font_size = newSize; - editEntity({ ...entityData, font_size: newSize }); + entityData.value.font_size = newSize; + editEntity({ ...entityData.value, font_size: newSize }); }; const themeColor: TTheme = cookies.get('favorite_color'); const themeColorConverted = convertThemeToColorWhiteDefault(themeColor); -const isTitle = ref(!!entityData.title); -const isEntityWidthFull = ref(entityData.paragraph_size === 'full'); +const isTitle = ref(!!entityData.value.title); +const isEntityWidthFull = ref(entityData.value.paragraph_size === 'full'); const maxLines = computed(() => { if (isTitle.value) { @@ -29,51 +30,81 @@ const maxLines = computed(() => { return Math.floor(240 / 24); } }); -const entityPositionOptions = [ +const entityIsTitleOptions = ref([ + { + label: 'Off', + value: false, + textStyle: 'bold' + }, + { + label: 'On', + value: true, + textStyle: 'bold' + } +]); +const isEntityWidthFullOptions = ref([ + { + label: 'Half', + value: false, + textStyle: 'bold' + }, + { + label: 'Full', + value: true, + textStyle: 'bold' + } +]); +const entityPositionOptions = ref([ { label: 'left', - value: 0 + isLabelHidden: true }, { label: 'center', - value: 1 + isLabelHidden: true }, { label: 'right', - value: 2 + isLabelHidden: true } -]; -const entityTitlePositionOptions = [ +]); +const entityTitlePositionOptions = ref([ { label: 'left', - value: 0 + isLabelHidden: true }, { label: 'center', - value: 1 + isLabelHidden: true }, { label: 'right', - value: 2 + isLabelHidden: true } -]; +]); const saveChanges = () => { const entityPosition = isEntityWidthFull.value ? 'full' : 'half'; - if (entityPosition !== entityData.entity_position) { + if (entityPosition !== entityData.value.entity_position) { newEntityData.value.paragraph_size = entityPosition; } - if (isTitle.value !== !!entityData.title) { + if (isTitle.value !== !!entityData.value.title) { if (isTitle.value) { newEntityData.value.title = 'Title'; } else { newEntityData.value.title = null; } } - if (JSON.stringify(entityData) !== JSON.stringify(newEntityData.value)) { - console.log('they are different, I will save it. New data:', newEntityData.value); + if (JSON.stringify(entityData.value) !== JSON.stringify(newEntityData.value)) { emit('saveChanges', newEntityData.value); } - console.log('newEntityData.value :', newEntityData.value); + isModal.value = false; +}; +const toggleConfirmToDeleteParagraph = () => { + isModalToDeleteParagraph.value = !isModalToDeleteParagraph.value; +}; +const deleteParagraph = () => { + deleteEntity(entityData.value.entity_uuid); + isModalToDeleteParagraph.value = false; isModal.value = false; }; </script> @@ -86,14 +117,33 @@ const saveChanges = () => { > <SettingsIcon color="white" size="25" /> </button> - <Modal v-model:isVisible="isModal" theme="black" width="70%" + <Modal v-model:isVisible="isModal" theme="black" width="90%" ><template #header><h3 class="w-max mx-auto">Edit paragraph</h3></template> + <Modal v-model:isVisible="isModalToDeleteParagraph" theme="black" width="30%" + ><p class="font-bold pt-4 mb-4 text-center">Are you sure you want to delete this element?</p> + <div class="flex justify-between"> + <Button + label="Yes, delete" + theme="red" + textColor="white" + textStyle="bold" + @click.prevent="deleteParagraph" + /> + <Button + label="Cancel" + theme="white" + textColor="black" + @click.prevent="toggleConfirmToDeleteParagraph" + /></div + ></Modal> <div class="p-10 flex gap-16 items-center"> <ParagraphSettingsList v-model:newEntityData="newEntityData" v-model:isTitle="isTitle" v-model:isEntityWidthFull="isEntityWidthFull" :themeColor="themeColor" + :entityIsTitleOptions="entityIsTitleOptions" + :isEntityWidthFullOptions="isEntityWidthFullOptions" :entityPositionOptions="entityPositionOptions" :entityTitlePositionOptions="entityTitlePositionOptions" /> @@ -121,7 +171,10 @@ const saveChanges = () => { </div> </div> </section> - <div class="absolute top-4 right-16 z-10 hover:brightness-80 transition-all"> + <div + class="absolute top-4 right-16 z-10 hover:brightness-80 transition-all" + @click.prevent="toggleConfirmToDeleteParagraph" + > <Button label="Delete" textColor="white" theme="red" textStyle="bold" size="medium"> <template #icon> <TrashIcon color="white" size="25" /> diff --git a/src/components/entities/settings/lists/ImageSettingsList.vue b/src/components/entities/settings/lists/ImageSettingsList.vue new file mode 100644 index 0000000000000000000000000000000000000000..535f2a6a5730e6fbf0ed61d5aa5f66e23b5e1af4 --- /dev/null +++ b/src/components/entities/settings/lists/ImageSettingsList.vue @@ -0,0 +1,145 @@ +<script setup lang="ts"> +import type { TTheme } from '@/app/interfaces/environment'; +import { useVModels } from '@vueuse/core'; +import type { IImage } from '@/app/interfaces/entities'; +import ToggleButton from '@/shared/ui/ToggleButton.vue'; +import type { ISliderOption, IToggleButtonItem } from '@/app/interfaces/ui'; + +interface Props { + newEntityData: IImage; + isTitle: boolean; + isText: boolean; + isEntityWidthFull: boolean; + themeColor: TTheme; + entityIsTitleOptions: IToggleButtonItem[]; + entityIsTextOptions: IToggleButtonItem[]; + entityPositionOptions: IToggleButtonItem[]; + entityTitlePositionOptions: IToggleButtonItem[]; + entityParagraphSizeOptions: IToggleButtonItem[]; + entityTextPositionOptions: IToggleButtonItem[]; + imageScaleOptions: ISliderOption[]; +} +const props = defineProps<Props>(); +const emit = defineEmits([ + 'update:newEntityData', + 'update:isTitle', + 'update:isText', + 'update:isEntityWidthFull' +]); +const { newEntityData, isTitle, isText, isEntityWidthFull } = useVModels(props, emit); +</script> + +<template> + <div> + <div class="flex flex-col items-center" style="min-width: 30%; min-height: 100px"> + <p class="py-2">Image size</p> + <Slider + v-model:value="newEntityData.image_scale" + width="300px" + size="small" + max="7" + :options="imageScaleOptions" + :isSmooth="true" + backgroundColor="white" + :theme="themeColor" + /> + </div> + <ul class="flex gap-2 h-full" style="min-width: 35%"> + <li class="flex flex-col items-center justify-between gap-4" style="min-width: 150px"> + <div> + <p class="py-2 text-center">Title</p> + <ToggleButton + v-model:value="isTitle" + :theme="themeColor" + :options="entityIsTitleOptions" + rounded="true" + :border="themeColor" + :activeBGColor="themeColor" + /> + </div> + <div style="height: 108px" class="flex gap-8 items-center justify-between col-span-2"> + <Transition name="fading"> + <div v-show="isTitle" class="flex flex-col items-center"> + <p class="py-2 text-center">Title position</p> + <ToggleButton + v-model:value="newEntityData.entity_title_position" + :theme="themeColor" + :options="entityTitlePositionOptions" + rounded="true" + :border="themeColor" + :activeBGColor="themeColor" + > + <template #1Icon><AlignLeftIcon /></template> + <template #2Icon><AlignCenterIcon /></template> + <template #3Icon><AlignRightIcon /></template + ></ToggleButton> + </div> + </Transition> + </div> + <div style="height: 108px" class="flex gap-8 items-center justify-between col-span-2"> + <Transition name="fading"> + <div v-show="isText" class="flex flex-col items-center"> + <p class="py-2">Text position</p> + <ToggleButton + v-model:value="newEntityData.text_position" + :theme="themeColor" + :options="entityTextPositionOptions" + rounded="true" + :border="themeColor" + :activeBGColor="themeColor" + /> + </div> + </Transition> + </div> + </li> + <li class="flex flex-col items-center justify-between gap-4" style="min-width: 150px"> + <div> + <p class="py-2 text-center">Text</p> + <div class="flex items-center"> + <ToggleButton + v-model:value="isText" + :theme="themeColor" + :options="entityIsTextOptions" + rounded="true" + :border="themeColor" + :activeBGColor="themeColor" + /> + </div> + </div> + <div style="height: 108px" class="flex gap-8 items-center justify-between"> + <div class="flex flex-col items-center"> + <p class="py-2 text-center">Block position</p> + <ToggleButton + v-model:value="newEntityData.entity_position" + :theme="themeColor" + :options="entityPositionOptions" + rounded="true" + :border="themeColor" + :activeBGColor="themeColor" + ><template #1Icon><AlignLeftIcon /></template> + <template #2Icon><AlignCenterIcon /></template> + <template #3Icon><AlignRightIcon /></template + ></ToggleButton> + </div> + </div> + <div style="height: 108px" class="flex gap-8 items-center justify-between"> + <Transition name="fading"> + <div v-show="isText" class="flex flex-col items-center"> + <p class="py-2">Text width</p> + <ToggleButton + v-model:value="newEntityData.paragraph_size" + :theme="themeColor" + :options="entityParagraphSizeOptions" + rounded="true" + :border="themeColor" + :activeBGColor="themeColor" + /> + </div> + </Transition> + </div> + </li> + </ul> + </div> +</template> + +<style scoped></style> diff --git a/src/components/entities/settings/lists/ParagraphSettingsList.vue b/src/components/entities/settings/lists/ParagraphSettingsList.vue index 236e3242fd4379c963c46782341afa138fcd6d16..ce2277af291be37d051c66b37cdb6551b95e16ff 100644 --- a/src/components/entities/settings/lists/ParagraphSettingsList.vue +++ b/src/components/entities/settings/lists/ParagraphSettingsList.vue @@ -2,20 +2,18 @@ import type { TTheme } from '@/app/interfaces/environment'; import { useVModels } from '@vueuse/core'; import type { IParagraph } from '@/app/interfaces/entities'; +import ToggleButton from '@/shared/ui/ToggleButton.vue'; +import type { IToggleButtonItem } from '@/app/interfaces/ui'; interface Props { newEntityData: IParagraph; isTitle: boolean; isEntityWidthFull: boolean; themeColor: TTheme; - entityPositionOptions: { - label: string; - value: number; - }[]; - entityTitlePositionOptions: { - label: string; - value: number; - }[]; + entityIsTitleOptions: IToggleButtonItem[]; + isEntityWidthFullOptions: IToggleButtonItem[]; + entityPositionOptions: IToggleButtonItem[]; + entityTitlePositionOptions: IToggleButtonItem[]; } const props = defineProps<Props>(); const emit = defineEmits(['update:newEntityData', 'update:isTitle', 'update:isEntityWidthFull']); @@ -23,66 +21,72 @@ const { newEntityData, isTitle, isEntityWidthFull } = useVModels(props, emit); </script> <template> - <ul class="flex gap-8 h-full" style="min-width: 35%"> - <li class="flex flex-col items-center gap-4" style="min-width: 150px"> + <ul class="flex gap-2 h-full" style="min-width: 35%"> + <li class="flex flex-col items-center gap-4" style="min-width: 150px; min-height: 108px"> <div> <p class="py-2 text-center">Title</p> <div class="flex items-center"> - Off - <ToggleSwitch v-model:isActive="isTitle" class="mx-2" :theme="themeColor" /> - On + <ToggleButton + v-model:value="isTitle" + :theme="themeColor" + :options="entityIsTitleOptions" + rounded="true" + :border="themeColor" + :activeBGColor="themeColor" + /> </div> </div> - <div style="height: 108px" class="flex gap-8 items-center justify-between col-span-2"> + <div class="flex items-center justify-between"> <Transition name="fading"> <div v-show="isTitle" class="flex flex-col items-center"> <p class="py-2 text-center">Title position</p> - <Slider + <ToggleButton v-model:value="newEntityData.entity_title_position" :theme="themeColor" - width="150" - size="small" - isSmooth="true" - backgroundColor="white" - min="0" - max="2" - step="1" :options="entityTitlePositionOptions" - /> + rounded="true" + size="small" + :border="themeColor" + :activeBGColor="themeColor" + > + <template #1Icon><AlignLeftIcon /></template> + <template #2Icon><AlignCenterIcon /></template> + <template #3Icon><AlignRightIcon /></template> + </ToggleButton> </div> </Transition> </div> </li> - <li class="flex flex-col items-center gap-4" style="min-width: 150px"> + <li class="flex flex-col items-center gap-4" style="min-width: 150px; min-height: 108px"> <div> - <p class="py-2 text-center">Paragraph width</p> - <div class="flex items-center"> - Half - <ToggleSwitch - v-model:isActive="isEntityWidthFull" - class="mx-2" + <div class="flex flex-col items-center"> + <p class="py-2 text-center">Paragraph width</p> + <ToggleButton + v-model:value="isEntityWidthFull" :theme="themeColor" - :negativeTheme="themeColor" + :options="isEntityWidthFullOptions" + rounded="true" + :border="themeColor" + :activeBGColor="themeColor" /> - Full </div> </div> - <div style="height: 108px" class="flex gap-8 items-center justify-between col-span-2"> + <div class="flex gap-8 items-center justify-between"> <Transition name="fading"> <div v-show="!isEntityWidthFull" class="flex flex-col items-center"> <p class="py-2">Paragraph position</p> - <Slider + <ToggleButton v-model:value="newEntityData.entity_position" :theme="themeColor" - width="150" - size="small" - isSmooth="true" - backgroundColor="white" - min="0" - max="2" - step="1" :options="entityPositionOptions" - /> + rounded="true" + size="small" + :border="themeColor" + :activeBGColor="themeColor" + ><template #1Icon><AlignLeftIcon /></template> + <template #2Icon><AlignCenterIcon /></template> + <template #3Icon><AlignRightIcon /></template> + </ToggleButton> </div> </Transition> </div> diff --git a/src/modules/entities/ImageItem.vue b/src/modules/entities/ImageItem.vue index 34303dfcba73abd9747f98dd8f41b8d6e5f6fe2f..a674a221599fdbaac6a8bd599860b9680bda129b 100644 --- a/src/modules/entities/ImageItem.vue +++ b/src/modules/entities/ImageItem.vue @@ -1,16 +1,24 @@ <script setup lang="ts"> -import { useVModel, useWindowSize } from '@vueuse/core'; +import { useWindowSize } from '@vueuse/core'; import type { IImage } from '@/app/interfaces/entities'; import { editEntity } from '@/app/helpers'; import { cropImage } from '@/app/helpers/images'; +import type { IEntity } from '@/app/interfaces/environment'; +import { useDataStore } from '@/app/stores/data'; interface Props { entityData: IImage; isEditMode: boolean; } const props = defineProps<Props>(); -const emit = defineEmits(['update:entityData']); -const entityData = useVModel(props, 'entityData', emit); +const entityData = ref(props.entityData); + +const dataStore = useDataStore(); +const entities = computed(() => dataStore.entities); +const entityIndex = computed(() => + entities.value.findIndex((entity: IEntity) => entity.entity_uuid === props.entityData.entity_uuid) +); +const entitiesLength = computed(() => entities.value.length); const isModalCropImage = ref<boolean>(false); const { width: windowWidth } = useWindowSize(); @@ -27,6 +35,10 @@ const editTitle = () => { const editText = () => { editEntity({ ...entityData.value, text: entityData.value.text }); }; +const saveChanges = (newState: IImage) => { + editEntity(newState); + entityData.value = newState; +}; const saveImage = async (newUrl: string, newWidth: number, newHeight: number) => { entityData.value.imageUrl = newUrl; entityData.value.image_width = newWidth; @@ -47,7 +59,6 @@ const openCropImageModal = () => (isModalCropImage.value = true); <template> <section - ref="container" :class="[ 'entityContainer relative flex px-16 transition-all', { @@ -87,9 +98,9 @@ const openCropImageModal = () => (isModalCropImage.value = true); style="min-height: 100px; max-height: 700px" class="object-contain order-1" /> - <div class="speedDialSize absolute left-0 top-0 transition-all select-none"> - <ImageSizeMenu v-if="isEditMode" :entityData="entityData" @scaleImage="scaleImage" /> - </div> + <!-- <div class="speedDialSize absolute left-0 top-0 transition-all select-none">--> + <!-- <ImageSizeMenu v-if="isEditMode" :entityData="entityData" @scaleImage="scaleImage" />--> + <!-- </div>--> </div> <div v-if="entityData.text_position" @@ -113,12 +124,13 @@ const openCropImageModal = () => (isModalCropImage.value = true); /> </div> </div> - <ImageMenu - v-if="isEditMode" - v-model:entityData="entityData" - @openCropImageModal="openCropImageModal" + <ImageSettings v-if="isEditMode" :entityData="entityData" @saveChanges="saveChanges" /> + <EntityPositionSettings + v-if="isEditMode && entitiesLength > 1" + :entityUuid="entityData.entity_uuid" + :entityIndex="entityIndex" + :entitiesLength="entitiesLength" /> - <EntityPositionSettings :entityUuid="entityData.entity_uuid" /> </div> </section> </template> diff --git a/src/modules/entities/ParagraphItem.vue b/src/modules/entities/ParagraphItem.vue index a925c204266f6a4e4ace85f4cbc74d8422acd611..fc61f3af746409da604482a8c7e679cb5d68f9dc 100644 --- a/src/modules/entities/ParagraphItem.vue +++ b/src/modules/entities/ParagraphItem.vue @@ -1,5 +1,5 @@ <script setup lang="ts"> -import { useElementSize, useTextareaAutosize } from '@vueuse/core'; +import { useTextareaAutosize } from '@vueuse/core'; import type { IParagraph } from '@/app/interfaces/entities'; import { editEntity } from '@/app/helpers'; import { useDataStore } from '@/app/stores/data'; diff --git a/src/output.css b/src/output.css index f527fbd40ed9425ac883879d3db2078703bd53d9..54856cd50812632e36a725664016ed8a3b49731c 100644 --- a/src/output.css +++ b/src/output.css @@ -554,40 +554,6 @@ video { --tw-contain-style: ; } -.container { - width: 100%; -} - -@media (min-width: 640px) { - .container { - max-width: 640px; - } -} - -@media (min-width: 768px) { - .container { - max-width: 768px; - } -} - -@media (min-width: 1024px) { - .container { - max-width: 1024px; - } -} - -@media (min-width: 1280px) { - .container { - max-width: 1280px; - } -} - -@media (min-width: 1536px) { - .container { - max-width: 1536px; - } -} - .pointer-events-none { pointer-events: none; } @@ -716,6 +682,11 @@ video { margin-bottom: 1rem; } +.mx-4 { + margin-left: 1rem; + margin-right: 1rem; +} + .-mb-2 { margin-bottom: -0.5rem; } @@ -825,6 +796,11 @@ video { height: 100%; } +.h-max { + height: -moz-max-content; + height: max-content; +} + .min-h-full { min-height: 100%; } @@ -985,6 +961,10 @@ video { gap: 2rem; } +.gap-0 { + gap: 0px; +} + .overflow-auto { overflow: auto; } diff --git a/src/shared/icons/AlignCenterIcon.vue b/src/shared/icons/AlignCenterIcon.vue new file mode 100644 index 0000000000000000000000000000000000000000..e7edd7ae5ee1765acb29e3a86fa24297cbe28db6 --- /dev/null +++ b/src/shared/icons/AlignCenterIcon.vue @@ -0,0 +1,27 @@ +<script setup lang="ts"> +interface Props { + color?: string; + size?: string | number; +} +defineProps<Props>(); +</script> + +<template> + <svg + :width="size ?? '25px'" + :height="size ?? '25px'" + viewBox="0 0 24 24" + fill="none" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M3 6H21M3 14H21M17 10H7M17 18H7" + :stroke="color ?? '#000000'" + stroke-width="2" + stroke-linecap="round" + stroke-linejoin="round" + /> + </svg> +</template> + +<style scoped></style> diff --git a/src/shared/icons/AlignLeftIcon.vue b/src/shared/icons/AlignLeftIcon.vue new file mode 100644 index 0000000000000000000000000000000000000000..52a46b689aa0332c8d6fc111bb5a3ba1fd1e98eb --- /dev/null +++ b/src/shared/icons/AlignLeftIcon.vue @@ -0,0 +1,27 @@ +<script setup lang="ts"> +interface Props { + color?: string; + size?: string | number; +} +defineProps<Props>(); +</script> + +<template> + <svg + :width="size ?? '25px'" + :height="size ?? '25px'" + viewBox="0 0 24 24" + fill="none" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M3 10H16M3 14H21M3 18H16M3 6H21" + :stroke="color ?? '#000000'" + stroke-width="2" + stroke-linecap="round" + stroke-linejoin="round" + /> + </svg> +</template> + +<style scoped></style> diff --git a/src/shared/icons/AlignRightIcon.vue b/src/shared/icons/AlignRightIcon.vue new file mode 100644 index 0000000000000000000000000000000000000000..b9d49d8541827e7a1953c1d5ce40e047cda12d4b --- /dev/null +++ b/src/shared/icons/AlignRightIcon.vue @@ -0,0 +1,27 @@ +<script setup lang="ts"> +interface Props { + color?: string; + size?: string | number; +} +defineProps<Props>(); +</script> + +<template> + <svg + :width="size ?? '25px'" + :height="size ?? '25px'" + viewBox="0 0 24 24" + fill="none" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M8 10H21M3 14H21M8 18H21M3 6H21" + :stroke="color ?? '#000000'" + stroke-width="2" + stroke-linecap="round" + stroke-linejoin="round" + /> + </svg> +</template> + +<style scoped></style> diff --git a/src/shared/ui/Button.vue b/src/shared/ui/Button.vue index e5be3f0c151a07c8bf5e029749a7219e6a2e513d..161a8fd62ab11eee226785e1d92061d55184c828 100644 --- a/src/shared/ui/Button.vue +++ b/src/shared/ui/Button.vue @@ -59,14 +59,14 @@ interface Props { const props = defineProps<Props>(); const themeColor = computed(() => convertThemeToColorWhiteDefault(props.theme)); const textColor = computed(() => convertThemeToColorBlackDefault(props.textColor)); -const borderColor = computed(() => convertThemeToColorBlackDefault(props.border)); +const borderColor = computed(() => + props.border ? convertThemeToColorBlackDefault(props.border) : '' +); const textSize = computed(() => { - if (!props?.size) return '16px'; + if (!props?.size || props.size === 'medium') return '16px'; switch (props.size) { case 'small': return '12px'; - case 'medium': - return '16px'; case 'large': return '20px'; case 'extraLarge': @@ -74,12 +74,10 @@ const textSize = computed(() => { } }); const buttonPadding = computed(() => { - if (!props?.size) return '0.75rem 0.5rem'; + if (!props?.size || props.size === 'medium') return '0.75rem 0.5rem'; switch (props.size) { case 'small': return '0.5rem 0.375rem'; - case 'medium': - return '0.75rem 0.5rem'; case 'large': return '1.2rem 0.8rem'; case 'extraLarge': @@ -93,10 +91,11 @@ const buttonPadding = computed(() => { :class="[ 'button', { - 'flex-column': iconPos === 'top' || iconPos === 'bottom' + 'flex-column': iconPos === 'top' || iconPos === 'bottom', + border: borderColor } ]" - :style="`padding: ${buttonPadding}; border: 1px solid ${borderColor}`" + :style="`padding: ${buttonPadding}`" > <span :style="`background-color: ${themeColor}`" class="background"></span> <span @@ -154,7 +153,7 @@ const buttonPadding = computed(() => { flex-direction: column; } .order-1 { - order: 1; + order: -1; } .bold { font-weight: bold; @@ -162,4 +161,7 @@ const buttonPadding = computed(() => { .italic { font-style: italic; } +.border { + border: 2px solid v-bind(borderColor); +} </style> diff --git a/src/shared/ui/Slider.vue b/src/shared/ui/Slider.vue index 59fff7f82ab02a3c3f052166040d81c207ef42f8..0a4fcf6cf0009717eae64d86af9af2c659b7e82e 100644 --- a/src/shared/ui/Slider.vue +++ b/src/shared/ui/Slider.vue @@ -17,6 +17,7 @@ interface Props { options?: { label: string; value: number; + color?: string; }[]; } const props = defineProps<Props>(); @@ -75,7 +76,7 @@ const themeBackground = computed(() => convertThemeToColorBlackDefault(props.bac <div v-if="options?.length"> <ul class="marksList" - :style="`width: ${width ?? 200}px; margin-bottom: 5px; font-size: 10px`" + :style="`width: ${width ?? 200}px; margin-bottom: 5px; font-size: 10px; padding: 0 15px`" > <li v-for="option of options" :key="option">|</li> </ul> @@ -89,7 +90,11 @@ const themeBackground = computed(() => convertThemeToColorBlackDefault(props.bac ]" > <template v-for="option of options" :key="option.value"> - <option :value="option.value" :label="option.label"></option> + <option + :value="option.value" + :label="option.label" + :style="`color: ${option.color ?? 'white'}`" + ></option> </template> </datalist> </div> @@ -98,7 +103,7 @@ const themeBackground = computed(() => convertThemeToColorBlackDefault(props.bac <style scoped> .slideContainer { - width: 100%; + width: v-bind(width); } .slider { -webkit-appearance: none; diff --git a/src/shared/ui/ToggleButton.vue b/src/shared/ui/ToggleButton.vue new file mode 100644 index 0000000000000000000000000000000000000000..d47fd2777886c08cb8cff6adb9838cc58162cf83 --- /dev/null +++ b/src/shared/ui/ToggleButton.vue @@ -0,0 +1,202 @@ +<script setup lang="ts"> +import { computed } from 'vue'; +import { convertThemeToColorBlackDefault, convertThemeToColorWhiteDefault } from '@/app/helpers'; +import { useVModel } from '@vueuse/core'; +type TTheme = + | 'white' + | 'slate' + | 'blue' + | 'sky' + | 'teal' + | 'lime' + | 'green' + | 'yellow' + | 'orange' + | 'pink' + | 'fuchsia' + | 'purple' + | 'indigo' + | 'rose' + | 'red' + | 'black'; +interface Props { + value: any; + options: { + label: string; + value?: any; + textColor?: TTheme; + backgroundColor?: TTheme; + isLabelHidden?: boolean; + iconPos?: string; + textStyle?: 'bold' | 'italic'; + }[]; + border?: TTheme; + size?: 'small' | 'medium' | 'large' | 'extraLarge'; + theme?: TTheme; + rounded?: any; + activeBGColor?: TTheme; +} +const props = defineProps<Props>(); +const emit = defineEmits(['update:value']); +const value = useVModel(props, 'value', emit); +const activeBGColor = computed(() => + props.activeBGColor ? convertThemeToColorBlackDefault(props.activeBGColor) : '' +); +const borderColor = computed(() => + props.border ? convertThemeToColorBlackDefault(props.border) : '' +); +const textSize = computed(() => { + if (!props?.size || props.size === 'medium') return '16px'; + switch (props.size) { + case 'small': + return '12px'; + case 'large': + return '20px'; + case 'extraLarge': + return '24px'; + } +}); +const buttonPadding = computed(() => { + if (!props?.size || props.size === 'medium') return '0.75rem 0.5rem'; + switch (props.size) { + case 'small': + return '0.5rem 0.375rem'; + case 'large': + return '1.2rem 0.8rem'; + case 'extraLarge': + return '1.8rem 1.2rem'; + } +}); +const buttonHeight = computed(() => { + if (!props?.size || props.size === 'medium') return '40px'; + switch (props.size) { + case 'small': + return '24px'; + case 'large': + return '68px'; + case 'extraLarge': + return '114px'; + } +}); +</script> + +<template> + <div + :class="[ + 'buttonGroup', + { + 'rounded-full': props.rounded, + border: borderColor + } + ]" + > + <button + v-for="(item, index) of options" + :key="item.label" + :class="[ + 'button', + { + 'flex-column': item.iconPos === 'top' || item.iconPos === 'bottom' + } + ]" + :style="`padding: ${buttonPadding}`" + @click.prevent="value = item?.value ?? item.label" + > + <span + :style="`background-color: ${activeBGColor && (value === item.value || value === item.label) ? activeBGColor : convertThemeToColorWhiteDefault(item.backgroundColor)}`" + :class="[ + 'background', + { + 'rounded-left': index === 0, + 'rounded-left-full': index === 0 && props.rounded, + 'rounded-right': index === options.length - 1, + 'rounded-right-full': index === options.length - 1 && props.rounded + } + ]" + ></span> + <span + v-if="!item.isLabelHidden" + :style="`color: ${convertThemeToColorBlackDefault(item.textColor)}; font-size: ${textSize}`" + :class="[ + 'text', + { + bold: item.textStyle === 'bold', + italic: item.textStyle === 'italic' + } + ]" + >{{ item.label ?? 'Button' }}</span + > + <slot + :class="[ + 'icon', + { + 'order-1': item.iconPos === 'left' || item.iconPos === 'top' + } + ]" + :name="`${index + 1}Icon`" + /> + </button> + </div> +</template> + +<style scoped> +.buttonGroup { + display: flex; + border-radius: 0.5rem; + position: relative; +} +.button { + position: relative; + display: flex; + gap: 8px; + align-items: center; + user-select: none; +} +.button:hover .background { + filter: brightness(90%); +} +.background { + width: 100%; + height: 100%; + position: absolute; + z-index: -2; + top: 0; + left: 0; + transition: filter 0.2s ease-in-out; +} +.icon { + display: flex; + align-items: center; + justify-content: center; +} +.flex-column { + flex-direction: column; +} +.order-1 { + order: -1; +} +.bold { + font-weight: bold; +} +.italic { + font-style: italic; +} +.border { + border: 2px solid v-bind(borderColor); +} +.rounded-left { + border-radius: 0.5rem 0 0 0.5rem; +} +.rounded-left-full { + border-radius: v-bind(buttonHeight) 0 0 v-bind(buttonHeight); +} +.rounded-right { + border-radius: 0 0.5rem 0.5rem 0; +} +.rounded-right-full { + border-radius: 0 v-bind(buttonHeight) v-bind(buttonHeight) 0; +} +.rounded-full { + border-radius: v-bind(buttonHeight); +} +</style>