diff --git a/client/src/pages/presentationEditor/components/BackgroundImageSelect.tsx b/client/src/pages/presentationEditor/components/BackgroundImageSelect.tsx new file mode 100644 index 0000000000000000000000000000000000000000..14dbaa63ed2cc267a080264ebe3d7abd243220ba --- /dev/null +++ b/client/src/pages/presentationEditor/components/BackgroundImageSelect.tsx @@ -0,0 +1,126 @@ +import { ListItem, ListItemText, Typography } from '@material-ui/core' +import React, { useState } from 'react' +import { useAppDispatch, useAppSelector } from '../../../hooks' +import { + AddButton, + AddBackgroundButton, + Center, + HiddenInput, + ImportedImage, + SettingsList, + ImageNameText, + ImageTextContainer, +} from './styled' +import CloseIcon from '@material-ui/icons/Close' +import axios from 'axios' +import { Media } from '../../../interfaces/ApiModels' +import { getEditorCompetition } from '../../../actions/editor' +import { uploadFile } from '../../../utils/uploadImage' + +type BackgroundImageSelectProps = { + variant: 'competition' | 'slide' +} + +const BackgroundImageSelect = ({ variant }: BackgroundImageSelectProps) => { + const activeSlideId = useAppSelector((state) => state.editor.activeSlideId) + const backgroundImage = useAppSelector((state) => { + if (variant === 'competition') return state.editor.competition.background_image + else return state.editor.competition.slides.find((slide) => slide.id === activeSlideId)?.background_image + }) + const competitionId = useAppSelector((state) => state.editor.competition.id) + const dispatch = useAppDispatch() + + const updateBackgroundImage = async (mediaId: number) => { + // Creates a new image component on the database using API call. + if (variant === 'competition') { + await axios + .put(`/api/competitions/${competitionId}`, { background_image_id: mediaId }) + .then(() => { + dispatch(getEditorCompetition(competitionId.toString())) + }) + .catch(console.log) + } else { + await axios + .put(`/api/competitions/${competitionId}/slides/${activeSlideId}`, { background_image_id: mediaId }) + .then(() => { + dispatch(getEditorCompetition(competitionId.toString())) + }) + .catch(console.log) + } + } + + const removeBackgroundImage = async () => { + // Removes background image media and from competition using API calls. + await axios.delete(`/api/media/images/${backgroundImage?.id}`).catch(console.log) + if (variant === 'competition') { + await axios + .put(`/api/competitions/${competitionId}`, { background_image_id: null }) + .then(() => { + dispatch(getEditorCompetition(competitionId.toString())) + }) + .catch(console.log) + } else { + await axios + .put(`/api/competitions/${competitionId}/slides/${activeSlideId}`, { background_image_id: null }) + .then(() => { + dispatch(getEditorCompetition(competitionId.toString())) + }) + .catch(console.log) + } + } + + const handleFileSelected = async (e: React.ChangeEvent<HTMLInputElement>) => { + // Reads the selected image file and uploads it to the server. + // Creates a new image component containing the file. + if (e.target.files !== null && e.target.files[0]) { + const files = Array.from(e.target.files) + const file = files[0] + const formData = new FormData() + formData.append('image', file) + const media = await uploadFile(formData, competitionId.toString()) + if (media) { + updateBackgroundImage(media.id) + } + } + } + + return ( + <SettingsList> + {!backgroundImage && ( + <ListItem button style={{ padding: 0 }}> + <HiddenInput + accept="image/*" + id="background-button-file" + multiple + type="file" + onChange={handleFileSelected} + /> + <AddBackgroundButton htmlFor="background-button-file"> + <Center> + <AddButton variant="button">Välj bakgrundsbild...</AddButton> + </Center> + </AddBackgroundButton> + </ListItem> + )} + {backgroundImage && ( + <> + <ListItem divider> + <ImageTextContainer> + <ListItemText>Bakgrundsbild</ListItemText> + <Typography variant="body2">(Bilden bör ha sidförhållande 16:9)</Typography> + </ImageTextContainer> + </ListItem> + <ListItem divider button> + <ImportedImage src={`/static/images/thumbnail_${backgroundImage.filename}`} /> + <Center> + <ImageNameText primary={backgroundImage.filename} /> + </Center> + <CloseIcon onClick={removeBackgroundImage} /> + </ListItem> + </> + )} + </SettingsList> + ) +} + +export default BackgroundImageSelect diff --git a/client/src/pages/presentationEditor/components/CompetitionSettings.tsx b/client/src/pages/presentationEditor/components/CompetitionSettings.tsx index 7ea2a84f99171fd75555e606abe2681a7f3c0c0d..9482b660550e729f2a5a4c443d0f935193679b1f 100644 --- a/client/src/pages/presentationEditor/components/CompetitionSettings.tsx +++ b/client/src/pages/presentationEditor/components/CompetitionSettings.tsx @@ -17,6 +17,7 @@ import { getEditorCompetition } from '../../../actions/editor' import { useAppDispatch, useAppSelector } from '../../../hooks' import { City } from '../../../interfaces/ApiModels' import Teams from './Teams' +import BackgroundImageSelect from './BackgroundImageSelect' interface CompetitionParams { competitionId: string @@ -92,17 +93,7 @@ const CompetitionSettings: React.FC = () => { <Teams competitionId={competitionId} /> - <SettingsList> - <ListItem button> - <ImportedImage - id="temp source, todo: add image source to elements of pictureList" - src="https://i1.wp.com/stickoutmedia.se/wp-content/uploads/2021/01/placeholder-3.png?ssl=1" - /> - <Center> - <ListItemText>Välj bakgrundsbild ...</ListItemText> - </Center> - </ListItem> - </SettingsList> + <BackgroundImageSelect variant="competition" /> </PanelContainer> ) } diff --git a/client/src/pages/presentationEditor/components/RndComponent.tsx b/client/src/pages/presentationEditor/components/RndComponent.tsx index e1cbc0f21652fe4eb65ed45fc65896c0c9f1bbab..77ed5de337f5ed620e77994cea2e512166d7b7d9 100644 --- a/client/src/pages/presentationEditor/components/RndComponent.tsx +++ b/client/src/pages/presentationEditor/components/RndComponent.tsx @@ -26,6 +26,9 @@ const RndComponent = ({ component, width, height, scale }: RndComponentProps) => const competitionId = useAppSelector((state) => state.editor.competition.id) const slideId = useAppSelector((state) => state.editor.activeSlideId) const [shiftPressed, setShiftPressed] = useState(false) + const typeName = useAppSelector( + (state) => state.types.componentTypes.find((componentType) => componentType.id === component.type_id)?.name + ) const handleUpdatePos = (pos: Position) => { axios.put(`/api/competitions/${competitionId}/slides/${slideId}/components/${component.id}`, { x: pos.x, @@ -98,6 +101,8 @@ const RndComponent = ({ component, width, height, scale }: RndComponentProps) => setCurrentPos({ x: d.x / scale, y: d.y / scale }) handleUpdatePos({ x: d.x / scale, y: d.y / scale }) }} + //Makes text appear on images + style={{ zIndex: typeName === 'Text' ? 2 : 1 }} lockAspectRatio={shiftPressed} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)} @@ -114,6 +119,7 @@ const RndComponent = ({ component, width, height, scale }: RndComponentProps) => w: ref.offsetWidth / scale, h: ref.offsetHeight / scale, }) + setCurrentPos({ x: position.x / scale, y: position.y / scale }) }} > {hover && ( diff --git a/client/src/pages/presentationEditor/components/SlideDisplay.tsx b/client/src/pages/presentationEditor/components/SlideDisplay.tsx index 42096d254b4746fa260863935a23328a6be80ef4..aef4ca7c48d29bbfc47cac8f29b214ec6866f360 100644 --- a/client/src/pages/presentationEditor/components/SlideDisplay.tsx +++ b/client/src/pages/presentationEditor/components/SlideDisplay.tsx @@ -17,6 +17,17 @@ const SlideDisplay = ({ variant, activeViewTypeId }: SlideDisplayProps) => { return state.editor.competition.slides.find((slide) => slide.id === state.editor.activeSlideId)?.components return state.presentation.competition.slides.find((slide) => slide.id === state.presentation.slide?.id)?.components }) + const competitionBackgroundImage = useAppSelector((state) => { + if (variant === 'editor') return state.editor.competition.background_image + return state.presentation.competition.background_image + }) + + const slideBackgroundImage = useAppSelector((state) => { + if (variant === 'editor') + return state.editor.competition.slides.find((slide) => slide.id === state.editor.activeSlideId)?.background_image + return state.presentation.competition.slides.find((slide) => slide.id === state.presentation.slide.id) + ?.background_image + }) const dispatch = useAppDispatch() const editorPaperRef = useRef<HTMLDivElement>(null) const [width, setWidth] = useState(0) @@ -42,6 +53,16 @@ const SlideDisplay = ({ variant, activeViewTypeId }: SlideDisplayProps) => { <SlideEditorContainer> <SlideEditorContainerRatio> <SlideEditorPaper ref={editorPaperRef}> + {(competitionBackgroundImage || slideBackgroundImage) && ( + <img + src={`/static/images/${ + slideBackgroundImage ? slideBackgroundImage.filename : competitionBackgroundImage?.filename + }`} + height={height} + width={width} + draggable={false} + /> + )} {components && components .filter((component) => component.view_type_id === activeViewTypeId) diff --git a/client/src/pages/presentationEditor/components/SlideSettings.tsx b/client/src/pages/presentationEditor/components/SlideSettings.tsx index 41c1354fb8e1415bf9b81822f65b2acab2cb92ad..d48b4565712d043d52d3b1e93e3633ec6b927d38 100644 --- a/client/src/pages/presentationEditor/components/SlideSettings.tsx +++ b/client/src/pages/presentationEditor/components/SlideSettings.tsx @@ -12,6 +12,7 @@ import Timer from './slideSettingsComponents/Timer' import Images from './slideSettingsComponents/Images' import Texts from './slideSettingsComponents/Texts' import QuestionSettings from './slideSettingsComponents/QuestionSettings' +import BackgroundImageSelect from './BackgroundImageSelect' interface CompetitionParams { competitionId: string @@ -56,17 +57,7 @@ const SlideSettings: React.FC = () => { <Images activeViewTypeId={activeViewTypeId} activeSlide={activeSlide} competitionId={competitionId} /> )} - <SettingsList> - <ListItem button> - <ImportedImage - id="temp source, todo: add image source to elements of pictureList" - src="https://i1.wp.com/stickoutmedia.se/wp-content/uploads/2021/01/placeholder-3.png?ssl=1" - /> - <Center> - <ListItemText>Välj bakgrundsbild ...</ListItemText> - </Center> - </ListItem> - </SettingsList> + <BackgroundImageSelect variant="slide" /> </PanelContainer> ) } diff --git a/client/src/pages/presentationEditor/components/slideSettingsComponents/Images.tsx b/client/src/pages/presentationEditor/components/slideSettingsComponents/Images.tsx index a76075c4631f6c2d38777e0e1ee631eeec4d94b8..49f41e7f87fa6c53dac59704540ca3c186ef708b 100644 --- a/client/src/pages/presentationEditor/components/slideSettingsComponents/Images.tsx +++ b/client/src/pages/presentationEditor/components/slideSettingsComponents/Images.tsx @@ -58,7 +58,7 @@ const Images = ({ activeViewTypeId, activeSlide, competitionId }: ImagesProps) = formData.append('image', file) const response = await uploadFile(formData) if (response) { - const newComponent = createImageComponent(response) + createImageComponent(response) } } } diff --git a/client/src/pages/presentationEditor/components/styled.tsx b/client/src/pages/presentationEditor/components/styled.tsx index 7255c2bbc8a57475662191cd57428986a105348b..605972d2c4ee798388f9437f373cbabeab777d3c 100644 --- a/client/src/pages/presentationEditor/components/styled.tsx +++ b/client/src/pages/presentationEditor/components/styled.tsx @@ -9,6 +9,7 @@ import { ListItem, Select, InputLabel, + ListItemText, } from '@material-ui/core' import styled from 'styled-components' @@ -71,6 +72,14 @@ export const Center = styled.div` width: 100%; ` +export const ImageTextContainer = styled.div` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; +` + export const PanelContainer = styled.div` padding: 10px; width: 100%; @@ -91,7 +100,16 @@ export const Clickable = styled.div` export const AddImageButton = styled.label` padding: 8px 13px 8px 13px; - cursor: 'pointer'; + display: flex; + justify-content: center; + text-align: center; + height: 100%; + width: 100%; + cursor: pointer; +` + +export const AddBackgroundButton = styled.label` + padding: 16px 29px 16px 29px; display: flex; justify-content: center; text-align: center; @@ -126,3 +144,7 @@ export const HoverContainer = styled.div<HoverContainerProps>` padding: ${(props) => (props.hover ? 0 : 1)}px; border: solid ${(props) => (props.hover ? 1 : 0)}px; ` + +export const ImageNameText = styled(ListItemText)` + word-break: break-all; +` diff --git a/client/src/utils/uploadImage.ts b/client/src/utils/uploadImage.ts new file mode 100644 index 0000000000000000000000000000000000000000..151a34a400f2d2e632b1f18b8f249206c043a02f --- /dev/null +++ b/client/src/utils/uploadImage.ts @@ -0,0 +1,16 @@ +import axios from 'axios' +import { getEditorCompetition } from '../actions/editor' +import { Media } from '../interfaces/ApiModels' +import store from '../store' + +export const uploadFile = async (formData: FormData, competitionId: string) => { + // Uploads the file to the server and creates a Media object in database. + // Returns media object data. + return await axios + .post(`/api/media/images`, formData) + .then((response) => { + getEditorCompetition(competitionId)(store.dispatch, store.getState) + return response.data as Media + }) + .catch(console.log) +} diff --git a/server/populate.py b/server/populate.py index 8bd2badc54ba2f666e2e18b5cb8e042f623d0c91..e0bb0bd94faf1b210069d7507d48e3f740157209 100644 --- a/server/populate.py +++ b/server/populate.py @@ -87,6 +87,12 @@ def _add_items(): w = random.randrange(150, 400) h = random.randrange(150, 400) dbc.add.component(1, item_slide.id, 1, x, y, w, h, text=f"hej{k}") + for k in range(3): + x = random.randrange(1, 500) + y = random.randrange(1, 500) + w = random.randrange(150, 400) + h = random.randrange(150, 400) + dbc.add.component(1, item_slide.id, 3, x, y, w, h, text=f"hej{k}") # item_slide = dbc.add.slide(item_comp) # item_slide.title = f"Slide {len(item_comp.slides)}"