aboutsummaryrefslogtreecommitdiffstats
path: root/public/projects/react-small-apps/apps/meme-generator/src/components
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2022-02-20 16:11:50 +0100
committerArmand Philippot <git@armandphilippot.com>2022-02-20 16:15:08 +0100
commit73a5c7fae9ffbe9ada721148c8c454a643aceebe (patch)
treec8fad013ed9b5dd589add87f8d45cf02bbfc6e91 /public/projects/react-small-apps/apps/meme-generator/src/components
parentb01239fbdcc5bbc5921f73ec0e8fee7bedd5c8e8 (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')
-rw-r--r--public/projects/react-small-apps/apps/meme-generator/src/components/Footer/Footer.js11
-rw-r--r--public/projects/react-small-apps/apps/meme-generator/src/components/Header/Header.js9
-rw-r--r--public/projects/react-small-apps/apps/meme-generator/src/components/Main/Main.js16
-rw-r--r--public/projects/react-small-apps/apps/meme-generator/src/components/MemeForm/MemeFieldset/MemeFieldset.js103
-rw-r--r--public/projects/react-small-apps/apps/meme-generator/src/components/MemeForm/MemeForm.js54
-rw-r--r--public/projects/react-small-apps/apps/meme-generator/src/components/MemePreview/Headline/Headline.js120
-rw-r--r--public/projects/react-small-apps/apps/meme-generator/src/components/MemePreview/MemePreview.js61
-rw-r--r--public/projects/react-small-apps/apps/meme-generator/src/components/commons/Button.js11
-rw-r--r--public/projects/react-small-apps/apps/meme-generator/src/components/commons/Fieldset.js10
-rw-r--r--public/projects/react-small-apps/apps/meme-generator/src/components/commons/Form.js21
-rw-r--r--public/projects/react-small-apps/apps/meme-generator/src/components/commons/Input.js41
-rw-r--r--public/projects/react-small-apps/apps/meme-generator/src/components/commons/InputRange.js37
-rw-r--r--public/projects/react-small-apps/apps/meme-generator/src/components/commons/Option.js5
-rw-r--r--public/projects/react-small-apps/apps/meme-generator/src/components/commons/Select.js31
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;