diff options
| author | Armand Philippot <git@armandphilippot.com> | 2022-02-20 16:11:50 +0100 |
|---|---|---|
| committer | Armand Philippot <git@armandphilippot.com> | 2022-02-20 16:15:08 +0100 |
| commit | 73a5c7fae9ffbe9ada721148c8c454a643aceebe (patch) | |
| tree | c8fad013ed9b5dd589add87f8d45cf02bbfc6e91 /public/projects/react-small-apps/apps/meme-generator/src/components | |
| parent | b01239fbdcc5bbc5921f73ec0e8fee7bedd5c8e8 (diff) | |
chore!: restructure repo
I separated public files from the config/dev files. It improves repo
readability.
I also moved dotenv helper to public/inc directory and extract the
Matomo tracker in the same directory.
Diffstat (limited to 'public/projects/react-small-apps/apps/meme-generator/src/components')
14 files changed, 530 insertions, 0 deletions
diff --git a/public/projects/react-small-apps/apps/meme-generator/src/components/Footer/Footer.js b/public/projects/react-small-apps/apps/meme-generator/src/components/Footer/Footer.js new file mode 100644 index 0000000..fbbe582 --- /dev/null +++ b/public/projects/react-small-apps/apps/meme-generator/src/components/Footer/Footer.js @@ -0,0 +1,11 @@ +function Footer() { + return ( + <footer className="footer"> + <p className="footer__copyright"> + Meme Generator. MIT 2021. Armand Philippot. + </p> + </footer> + ); +} + +export default Footer; diff --git a/public/projects/react-small-apps/apps/meme-generator/src/components/Header/Header.js b/public/projects/react-small-apps/apps/meme-generator/src/components/Header/Header.js new file mode 100644 index 0000000..118ca1a --- /dev/null +++ b/public/projects/react-small-apps/apps/meme-generator/src/components/Header/Header.js @@ -0,0 +1,9 @@ +function Header() { + return ( + <header className="header"> + <h1 className="header__branding">Meme Generator</h1> + </header> + ); +} + +export default Header; diff --git a/public/projects/react-small-apps/apps/meme-generator/src/components/Main/Main.js b/public/projects/react-small-apps/apps/meme-generator/src/components/Main/Main.js new file mode 100644 index 0000000..8878002 --- /dev/null +++ b/public/projects/react-small-apps/apps/meme-generator/src/components/Main/Main.js @@ -0,0 +1,16 @@ +import { useState } from "react"; +import MemeForm from "../MemeForm/MemeForm"; +import MemePreview from "../MemePreview/MemePreview"; + +function Main() { + const [headlines, setHeadlines] = useState([]); + + return ( + <main className="main"> + <MemePreview headlines={headlines} setHeadlines={setHeadlines} /> + <MemeForm headlines={headlines} setHeadlines={setHeadlines} /> + </main> + ); +} + +export default Main; diff --git a/public/projects/react-small-apps/apps/meme-generator/src/components/MemeForm/MemeFieldset/MemeFieldset.js b/public/projects/react-small-apps/apps/meme-generator/src/components/MemeForm/MemeFieldset/MemeFieldset.js new file mode 100644 index 0000000..2c0520e --- /dev/null +++ b/public/projects/react-small-apps/apps/meme-generator/src/components/MemeForm/MemeFieldset/MemeFieldset.js @@ -0,0 +1,103 @@ +import { useEffect, useState } from "react"; +import Button from "../../commons/Button"; +import Fieldset from "../../commons/Fieldset"; +import Input from "../../commons/Input"; +import InputRange from "../../commons/InputRange"; +import Select from "../../commons/Select"; + +function MemeFieldset({ headline, setHeadline, xOptions, yOptions }) { + const { id, legend, text, fontSize, fontUnit, xPos, yPos } = headline; + const [inputTextValue, setInputTextValue] = useState(text); + const [inputRangeValue, setInputRangeValue] = useState(fontSize); + const [selectX, setSelectX] = useState(xPos); + const [selectY, setSelectY] = useState(yPos); + + useEffect(() => { + setInputTextValue(text); + }, [text]); + + useEffect(() => { + setHeadline((previous) => { + return previous.map((object) => { + if (object.id !== id) return object; + return { + ...object, + text: inputTextValue, + fontSize: inputRangeValue, + xPos: selectX, + yPos: selectY, + }; + }); + }); + }, [setHeadline, id, inputTextValue, inputRangeValue, selectX, selectY]); + + const onChange = (e) => { + switch (e.target.name) { + case "inputText": + setInputTextValue(e.target.value); + break; + case "inputRange": + setInputRangeValue(Number(e.target.value)); + break; + case "selectX": + setSelectX(e.target.value); + break; + case "selectY": + setSelectY(e.target.value); + break; + default: + break; + } + }; + + const onClick = (e) => { + setHeadline((previous) => previous.filter((object) => object.id !== id)); + }; + + return ( + <Fieldset id={id} legend={legend}> + <Button body="Delete" modifier="delete" onClick={onClick} /> + <div className="form__item"> + <Input + label="Enter your text:" + id="inputText" + name="inputText" + value={inputTextValue} + onChangeHandler={onChange} + /> + </div> + <div className="form__item"> + <InputRange + label="Choose a font-size:" + id="inputRange" + name="inputRange" + value={inputRangeValue} + unit={fontUnit} + onChangeHandler={onChange} + /> + </div> + <div className="form__item"> + <Select + label="Select the vertical position:" + id="selectY" + name="selectY" + options={yOptions} + value={selectY} + onChangeHandler={onChange} + /> + </div> + <div className="form__item"> + <Select + label="Select the horizontal position:" + id="selectX" + name="selectX" + options={xOptions} + value={selectX} + onChangeHandler={onChange} + /> + </div> + </Fieldset> + ); +} + +export default MemeFieldset; diff --git a/public/projects/react-small-apps/apps/meme-generator/src/components/MemeForm/MemeForm.js b/public/projects/react-small-apps/apps/meme-generator/src/components/MemeForm/MemeForm.js new file mode 100644 index 0000000..b6ce40f --- /dev/null +++ b/public/projects/react-small-apps/apps/meme-generator/src/components/MemeForm/MemeForm.js @@ -0,0 +1,54 @@ +import { useState } from "react"; +import Button from "../commons/Button"; +import Form from "../commons/Form"; +import MemeFieldset from "./MemeFieldset/MemeFieldset"; + +function MemeForm({ headlines, setHeadlines }) { + const [fieldsetId, setFieldsetId] = useState(1); + const horizontalOptions = ["Left", "Right", "Center"]; + const verticalOptions = ["Top", "Bottom", "Middle"]; + + const fieldsetData = { + id: fieldsetId, + legend: `Text settings ${fieldsetId}`, + text: "Edit here...", + fontSize: 100, + fontUnit: "%", + xPos: horizontalOptions[(fieldsetId - 1) % horizontalOptions.length], + yPos: verticalOptions[(fieldsetId - 1) % verticalOptions.length], + }; + + const onSubmit = (e) => { + e.preventDefault(); + }; + + const fieldsetsList = headlines.map((headline) => { + return ( + <MemeFieldset + key={headline.id} + headline={headline} + setHeadline={setHeadlines} + xOptions={horizontalOptions} + yOptions={verticalOptions} + /> + ); + }); + + const addNewFieldset = () => { + setFieldsetId((previous) => previous + 1); + setHeadlines((array) => [...array, fieldsetData]); + }; + + return ( + <div className="meme-form"> + <Form onSubmitHandler={onSubmit}> + {fieldsetsList} + {fieldsetsList.length < 4 && ( + <Button body="Add new text" onClick={addNewFieldset} modifier="add" /> + )} + </Form> + </div> + ); +} + +export default MemeForm; diff --git a/public/projects/react-small-apps/apps/meme-generator/src/components/MemePreview/Headline/Headline.js b/public/projects/react-small-apps/apps/meme-generator/src/components/MemePreview/Headline/Headline.js new file mode 100644 index 0000000..e7ed579 --- /dev/null +++ b/public/projects/react-small-apps/apps/meme-generator/src/components/MemePreview/Headline/Headline.js @@ -0,0 +1,120 @@ +import { useEffect, useRef, useState } from "react"; +import Form from "../../commons/Form"; +import Input from "../../commons/Input"; + +function Headline({ id, text, fontSize, xPos, yPos, setHeadlines }) { + const inputRef = useRef(null); + const [isEditing, setIsEditing] = useState(false); + useEffect(() => { + isEditing && inputRef.current.focus(); + }); + + const [inputValue, setInputValue] = useState(text); + useEffect(() => { + setInputValue(text); + }, [text]); + + const getXPos = () => { + let styles = {}; + switch (xPos) { + case "Left": + styles = { gridColumn: 1, textAlign: "left" }; + break; + case "Right": + styles = { gridColumn: 2, textAlign: "right" }; + break; + case "Center": + styles = { + gridColumnStart: 1, + gridColumnEnd: "span 2", + justifySelf: "center", + textAlign: "center", + }; + break; + default: + break; + } + return styles; + }; + + const getYPos = () => { + let styles = {}; + switch (yPos) { + case "Top": + styles = { gridRow: 1 }; + break; + case "Bottom": + styles = { gridRow: 3, alignSelf: "end" }; + break; + case "Middle": + styles = { gridRow: 2, alignSelf: "center" }; + break; + default: + break; + } + return styles; + }; + + const styles = { + fontSize: fontSize, + ...getYPos(), + ...getXPos(), + }; + + const onSubmit = (e) => { + e.preventDefault(); + setIsEditing(false); + }; + + const updateText = (e) => { + setInputValue(e.target.value); + }; + + useEffect(() => { + setHeadlines((previous) => { + return previous.map((headline) => { + if (headline.id !== id) return headline; + return { ...headline, text: inputValue }; + }); + }); + }, [setHeadlines, id, inputValue]); + + useEffect(() => { + setHeadlines((previous) => { + return previous.map((headline) => { + if (headline.id !== id) return headline; + return { ...headline, text: inputValue }; + }); + }); + }, [setHeadlines, id, inputValue]); + + const onBlur = () => { + setIsEditing(false); + }; + + return ( + <> + {isEditing ? ( + <Form onSubmitHandler={onSubmit} styles={styles}> + <Input + value={inputValue} + ref={inputRef} + onChangeHandler={updateText} + onBlurHandler={onBlur} + additionalClasses="meme-preview__headline" + /> + </Form> + ) : ( + <p + className="meme-preview__headline" + onClick={() => setIsEditing(true)} + style={styles} + > + {inputValue} + </p> + )} + </> + ); +} + +export default Headline; diff --git a/public/projects/react-small-apps/apps/meme-generator/src/components/MemePreview/MemePreview.js b/public/projects/react-small-apps/apps/meme-generator/src/components/MemePreview/MemePreview.js new file mode 100644 index 0000000..6577e53 --- /dev/null +++ b/public/projects/react-small-apps/apps/meme-generator/src/components/MemePreview/MemePreview.js @@ -0,0 +1,61 @@ +import { useEffect, useState } from "react"; +import Button from "../commons/Button"; +import Headline from "./Headline/Headline"; + +async function fetchMemes() { + const response = await fetch("https://api.imgflip.com/get_memes"); + const result = await response.json(); + return await result; +} + +function MemePreview({ headlines, setHeadlines }) { + const [memesList, setMemesList] = useState([]); + const [isFetched, setIsFetched] = useState(false); + useEffect(() => { + fetchMemes().then((object) => setMemesList(object.data.memes)); + setIsFetched(true); + return () => setIsFetched(false); + }, [setIsFetched]); + + const [selectedMeme, setSelectedMeme] = useState({}); + useEffect(() => { + setSelectedMeme(memesList[5]); + }, [memesList]); + + const getRandomMeme = () => { + const randomIndex = Math.floor(Math.random() * memesList.length); + setSelectedMeme(memesList[randomIndex]); + }; + + const headlinesList = headlines.map((headline) => ( + <Headline + key={headline.id} + id={headline.id} + text={headline.text} + fontSize={`${headline.fontSize}${headline.fontUnit}`} + xPos={headline.xPos} + yPos={headline.yPos} + setHeadlines={setHeadlines} + /> + )); + + return ( + <div className="meme-preview"> + <div className="meme-preview__meme"> + {isFetched && selectedMeme ? ( + <img + src={selectedMeme.url} + alt={selectedMeme.name} + className="meme-preview__image" + /> + ) : ( + "Loading..." + )} + {headlinesList} + </div> + <Button body="Random image" modifier="random" onClick={getRandomMeme} /> + </div> + ); +} + +export default MemePreview; diff --git a/public/projects/react-small-apps/apps/meme-generator/src/components/commons/Button.js b/public/projects/react-small-apps/apps/meme-generator/src/components/commons/Button.js new file mode 100644 index 0000000..98967a8 --- /dev/null +++ b/public/projects/react-small-apps/apps/meme-generator/src/components/commons/Button.js @@ -0,0 +1,11 @@ +function Button({ body, modifier, onClick }) { + const classNames = `btn ${modifier ? `btn--${modifier}` : ""}`; + + return ( + <button className={classNames} onClick={onClick}> + {body} + </button> + ); +} + +export default Button; diff --git a/public/projects/react-small-apps/apps/meme-generator/src/components/commons/Fieldset.js b/public/projects/react-small-apps/apps/meme-generator/src/components/commons/Fieldset.js new file mode 100644 index 0000000..d76e3e7 --- /dev/null +++ b/public/projects/react-small-apps/apps/meme-generator/src/components/commons/Fieldset.js @@ -0,0 +1,10 @@ +function Fieldset({ children, legend = "Legend" }) { + return ( + <fieldset className="form__fieldset"> + <legend className="form__legend">{legend}</legend> + {children} + </fieldset> + ); +} + +export default Fieldset; diff --git a/public/projects/react-small-apps/apps/meme-generator/src/components/commons/Form.js b/public/projects/react-small-apps/apps/meme-generator/src/components/commons/Form.js new file mode 100644 index 0000000..5ab1948 --- /dev/null +++ b/public/projects/react-small-apps/apps/meme-generator/src/components/commons/Form.js @@ -0,0 +1,21 @@ +function Form({ + children, + action = "#", + method = "post", + styles, + onSubmitHandler, +}) { + return ( + <form + action={action} + method={method} + onSubmit={onSubmitHandler} + className="form" + style={styles} + > + {children} + </form> + ); +} + +export default Form; diff --git a/public/projects/react-small-apps/apps/meme-generator/src/components/commons/Input.js b/public/projects/react-small-apps/apps/meme-generator/src/components/commons/Input.js new file mode 100644 index 0000000..68e4e77 --- /dev/null +++ b/public/projects/react-small-apps/apps/meme-generator/src/components/commons/Input.js @@ -0,0 +1,41 @@ +import { forwardRef } from "react"; + +function Input( + { + label, + id, + name, + type = "text", + value, + onChangeHandler, + onBlurHandler, + additionalClasses = "", + }, + ref +) { + const classNames = `form__input ${additionalClasses}`; + + return ( + <> + {label ? ( + <label className="form__label" htmlFor={id}> + {label} + </label> + ) : ( + "" + )} + <input + id={id} + name={name} + ref={ref} + type={type} + value={value} + onChange={onChangeHandler} + onBlur={onBlurHandler} + className={classNames} + /> + </> + ); +} + +export default forwardRef(Input); diff --git a/public/projects/react-small-apps/apps/meme-generator/src/components/commons/InputRange.js b/public/projects/react-small-apps/apps/meme-generator/src/components/commons/InputRange.js new file mode 100644 index 0000000..1172966 --- /dev/null +++ b/public/projects/react-small-apps/apps/meme-generator/src/components/commons/InputRange.js @@ -0,0 +1,37 @@ +function InputRange({ + label, + id, + name, + min = 5, + max = 200, + step = 1, + unit = "px", + value, + onChangeHandler, +}) { + return ( + <> + {label ? ( + <label className="form__label" htmlFor={id}> + {label} + </label> + ) : ( + "" + )} + <input + type="range" + id={id} + name={name} + min={min} + max={max} + step={step} + value={value} + onChange={onChangeHandler} + title={`${value}${unit}`} + className="form__input form__input--range" + /> + </> + ); +} + +export default InputRange; diff --git a/public/projects/react-small-apps/apps/meme-generator/src/components/commons/Option.js b/public/projects/react-small-apps/apps/meme-generator/src/components/commons/Option.js new file mode 100644 index 0000000..4064798 --- /dev/null +++ b/public/projects/react-small-apps/apps/meme-generator/src/components/commons/Option.js @@ -0,0 +1,5 @@ +function Option({ value, body }) { + return <option value={value}>{body}</option>; +} + +export default Option; diff --git a/public/projects/react-small-apps/apps/meme-generator/src/components/commons/Select.js b/public/projects/react-small-apps/apps/meme-generator/src/components/commons/Select.js new file mode 100644 index 0000000..9517b23 --- /dev/null +++ b/public/projects/react-small-apps/apps/meme-generator/src/components/commons/Select.js @@ -0,0 +1,31 @@ +import Option from "./Option"; + +function Select({ id, name, label, options, value, onChangeHandler }) { + const optionsList = options.map((option) => { + const optionValue = option.replace(" ", "-"); + return <Option key={optionValue} value={optionValue} body={option} />; + }); + + return ( + <> + {label ? ( + <label className="form__label" htmlFor={id}> + {label} + </label> + ) : ( + "" + )} + <select + id={id} + name={name} + className="form__select" + value={value} + onChange={onChangeHandler} + > + {optionsList} + </select> + </> + ); +} + +export default Select; |
