diff options
Diffstat (limited to 'public/projects/react-small-apps/apps/meme-generator')
30 files changed, 872 insertions, 0 deletions
diff --git a/public/projects/react-small-apps/apps/meme-generator/.env.example b/public/projects/react-small-apps/apps/meme-generator/.env.example new file mode 100644 index 0000000..1b48ea9 --- /dev/null +++ b/public/projects/react-small-apps/apps/meme-generator/.env.example @@ -0,0 +1,11 @@ +# Create React App config. +# See: https://create-react-app.dev/docs/advanced-configuration/ + +# Development +BROWSER='firefox-developer-edition' +BUILD_PATH='build' +PORT=3000 +HTTPS=false + +# Production +PUBLIC_URL='./' diff --git a/public/projects/react-small-apps/apps/meme-generator/.gitignore b/public/projects/react-small-apps/apps/meme-generator/.gitignore new file mode 100644 index 0000000..0732bb1 --- /dev/null +++ b/public/projects/react-small-apps/apps/meme-generator/.gitignore @@ -0,0 +1,24 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local +.vscode + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/public/projects/react-small-apps/apps/meme-generator/README.md b/public/projects/react-small-apps/apps/meme-generator/README.md new file mode 100644 index 0000000..e0d510a --- /dev/null +++ b/public/projects/react-small-apps/apps/meme-generator/README.md @@ -0,0 +1,29 @@ +# React Meme Generator + +A meme generator implementation with React. + +## Requirements + +- Yarn + +## How to + +### Start the development version + +`yarn run start` + +### Start the build version: + +1. `yarn run build` +2. (`yarn global add serve`) +3. `serve -s build` + +## Preview + +You can see a live preview here: https://demo.armandphilippot.com/#meme-generator + + + +## License + +This project is open source and available under the [MIT license](../LICENSE). diff --git a/public/projects/react-small-apps/apps/meme-generator/package.json b/public/projects/react-small-apps/apps/meme-generator/package.json new file mode 100644 index 0000000..0fd09d0 --- /dev/null +++ b/public/projects/react-small-apps/apps/meme-generator/package.json @@ -0,0 +1,39 @@ +{ + "name": "react-meme-generator", + "description": "A meme generator implementation with ReactJS.", + "version": "1.0.0", + "private": true, + "dependencies": { + "@testing-library/jest-dom": "^5.16.2", + "@testing-library/react": "^12.1.3", + "@testing-library/user-event": "^13.5.0", + "react": "^17.0.2", + "react-dom": "^17.0.2", + "react-scripts": "5.0.0", + "web-vitals": "^2.1.4" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/public/projects/react-small-apps/apps/meme-generator/public/favicon.ico b/public/projects/react-small-apps/apps/meme-generator/public/favicon.ico Binary files differnew file mode 100644 index 0000000..a11777c --- /dev/null +++ b/public/projects/react-small-apps/apps/meme-generator/public/favicon.ico diff --git a/public/projects/react-small-apps/apps/meme-generator/public/index.html b/public/projects/react-small-apps/apps/meme-generator/public/index.html new file mode 100644 index 0000000..fe333ac --- /dev/null +++ b/public/projects/react-small-apps/apps/meme-generator/public/index.html @@ -0,0 +1,20 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8" /> + <link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <meta name="theme-color" content="#000000" /> + <meta + name="description" + content="Meme generator app created using create-react-app." + /> + <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> + <link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> + <title>Meme Generator</title> + </head> + <body> + <noscript>You need to enable JavaScript to run this app.</noscript> + <div class="app" id="root"></div> + </body> +</html> diff --git a/public/projects/react-small-apps/apps/meme-generator/public/logo192.png b/public/projects/react-small-apps/apps/meme-generator/public/logo192.png Binary files differnew file mode 100644 index 0000000..fc44b0a --- /dev/null +++ b/public/projects/react-small-apps/apps/meme-generator/public/logo192.png diff --git a/public/projects/react-small-apps/apps/meme-generator/public/logo512.png b/public/projects/react-small-apps/apps/meme-generator/public/logo512.png Binary files differnew file mode 100644 index 0000000..a4e47a6 --- /dev/null +++ b/public/projects/react-small-apps/apps/meme-generator/public/logo512.png diff --git a/public/projects/react-small-apps/apps/meme-generator/public/manifest.json b/public/projects/react-small-apps/apps/meme-generator/public/manifest.json new file mode 100644 index 0000000..080d6c7 --- /dev/null +++ b/public/projects/react-small-apps/apps/meme-generator/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/public/projects/react-small-apps/apps/meme-generator/public/preview-meme-generator.jpg b/public/projects/react-small-apps/apps/meme-generator/public/preview-meme-generator.jpg Binary files differnew file mode 100644 index 0000000..7d4579a --- /dev/null +++ b/public/projects/react-small-apps/apps/meme-generator/public/preview-meme-generator.jpg diff --git a/public/projects/react-small-apps/apps/meme-generator/public/robots.txt b/public/projects/react-small-apps/apps/meme-generator/public/robots.txt new file mode 100644 index 0000000..e9e57dc --- /dev/null +++ b/public/projects/react-small-apps/apps/meme-generator/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: 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; diff --git a/public/projects/react-small-apps/apps/meme-generator/src/index.js b/public/projects/react-small-apps/apps/meme-generator/src/index.js new file mode 100644 index 0000000..09f9e0b --- /dev/null +++ b/public/projects/react-small-apps/apps/meme-generator/src/index.js @@ -0,0 +1,21 @@ +import React from "react"; +import ReactDOM from "react-dom"; +import "./style.css"; +import reportWebVitals from "./reportWebVitals"; +import Header from "./components/Header/Header"; +import Main from "./components/Main/Main"; +import Footer from "./components/Footer/Footer"; + +ReactDOM.render( + <React.StrictMode> + <Header /> + <Main /> + <Footer /> + </React.StrictMode>, + document.getElementById("root") +); + +// If you want to start measuring performance in your app, pass a function +// to log results (for example: reportWebVitals(console.log)) +// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals +reportWebVitals(); diff --git a/public/projects/react-small-apps/apps/meme-generator/src/logo.svg b/public/projects/react-small-apps/apps/meme-generator/src/logo.svg new file mode 100644 index 0000000..9dfc1c0 --- /dev/null +++ b/public/projects/react-small-apps/apps/meme-generator/src/logo.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
\ No newline at end of file diff --git a/public/projects/react-small-apps/apps/meme-generator/src/reportWebVitals.js b/public/projects/react-small-apps/apps/meme-generator/src/reportWebVitals.js new file mode 100644 index 0000000..5253d3a --- /dev/null +++ b/public/projects/react-small-apps/apps/meme-generator/src/reportWebVitals.js @@ -0,0 +1,13 @@ +const reportWebVitals = onPerfEntry => { + if (onPerfEntry && onPerfEntry instanceof Function) { + import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { + getCLS(onPerfEntry); + getFID(onPerfEntry); + getFCP(onPerfEntry); + getLCP(onPerfEntry); + getTTFB(onPerfEntry); + }); + } +}; + +export default reportWebVitals; diff --git a/public/projects/react-small-apps/apps/meme-generator/src/setupTests.js b/public/projects/react-small-apps/apps/meme-generator/src/setupTests.js new file mode 100644 index 0000000..8f2609b --- /dev/null +++ b/public/projects/react-small-apps/apps/meme-generator/src/setupTests.js @@ -0,0 +1,5 @@ +// jest-dom adds custom jest matchers for asserting on DOM nodes. +// allows you to do things like: +// expect(element).toHaveTextContent(/react/i) +// learn more: https://github.com/testing-library/jest-dom +import '@testing-library/jest-dom'; diff --git a/public/projects/react-small-apps/apps/meme-generator/src/style.css b/public/projects/react-small-apps/apps/meme-generator/src/style.css new file mode 100644 index 0000000..ba33e89 --- /dev/null +++ b/public/projects/react-small-apps/apps/meme-generator/src/style.css @@ -0,0 +1,151 @@ +*, +*::before, +*::after { + box-sizing: border-box; + padding: 0; + margin: 0; +} + +body { + background: rgb(241, 236, 236); + font-family: Arial, Helvetica, sans-serif; + font-size: 16px; + font-size: 1rem; + line-height: 1.618; +} + +.app { + display: flex; + flex-flow: column nowrap; + width: min(calc(100vw - 2rem), 1200px); + min-height: 100vh; + margin: auto; +} + +.header { + text-align: center; + padding: 2rem 0; +} + +.main { + flex: 1; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(0, 550px)); + gap: 2rem; + margin: 2rem 0; +} + +.footer { + text-align: center; + font-size: 0.9rem; + padding: 2rem 0; +} + +.btn { + display: block; + padding: 0.5rem 1rem; +} + +.btn--delete { + background: rgb(255, 193, 193); + border: 1px solid rgb(141, 68, 68); + font-weight: 500; +} + +.form__fieldset { + padding: 0 1rem 1rem; +} + +.form__legend { + font-weight: 600; + letter-spacing: 1px; + text-transform: uppercase; + padding: 0.5rem; +} + +.form__item:not(:last-child) { + margin-bottom: 0.5rem; +} + +.form__label { + display: block; + cursor: pointer; +} + +.form__input { + font-family: inherit; + font-size: inherit; + line-height: inherit; + padding: 0.5rem 0.8rem; +} + +.form__input--range { + padding: 0.5rem 0.8rem 0; +} + +.form__select { + width: 100%; + padding: 0.5rem 0.8rem; +} + +.meme-form .form { + display: flex; + flex-flow: row wrap; + align-items: flex-start; + gap: 1rem; +} + +.meme-form .form__fieldset { + position: relative; +} + +.meme-form .btn--delete { + position: absolute; + top: -1.5rem; + right: 0; + padding: 0.4rem; +} + +.meme-form .btn--add { + align-self: center; + margin: auto; +} + +.meme-preview { + font-size: 2rem; +} + +.meme-preview__meme { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + grid-template-rows: repeat(3, minmax(0, 1fr)); + position: relative; +} + +.meme-preview__image { + grid-column: 1 / -1; + grid-row: 1 / -1; + width: 100%; +} + +.meme-preview__headline { + color: #fff; + font-weight: 600; + text-shadow: 3px 3px 2px #000; + padding: 0 0.5rem; +} + +.meme-preview .form__input { + background: transparent; + border: none; + text-align: inherit; + width: 100%; +} + +.meme-preview .form__input:focus { + outline: none; +} + +.meme-preview .btn--random { + margin: 2rem auto; +} |
