Merge pull request #68 from 30-seconds/from-starter

Use 30-seconds-starter template
This commit is contained in:
Angelos Chalaris
2019-08-22 08:48:06 +03:00
committed by GitHub
118 changed files with 26061 additions and 2294 deletions

20
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,20 @@
---
name: Bug report
about: Create a report to help us improve
---
## Bug description
<!-- A clear and concise description of what the bug is. -->
## Steps to reproduce
<!-- Where did you encounter the bug/What code caused the bug to appear? -->
## Expected behavior
<!-- A clear and concise description of what you expected to happen. -->
## Screenshots
<!-- If applicable, add screenshots to help explain your problem. -->
## Environment
<!-- Provide some information about your OS, Browser or mobile device (if applicable). -->

8
.github/ISSUE_TEMPLATE/discussion.md vendored Normal file
View File

@ -0,0 +1,8 @@
---
name: Discussion
about: Discuss something with us
---
## Description
<!-- A clear and concise description of what you want to discuss. -->

View File

@ -0,0 +1,9 @@
---
name: Feature request
about: Suggest an idea for this project
---
## Description
<!-- A clear and concise description of what you want to happen. -->
<!-- Provide sample code, useful information, possible solutions and examples whenever possible. -->

15
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,15 @@
<!-- Use a descriptive title, prefix it with [FIX], [FEATURE] or [ENHANCEMENT] if applicable (use only one) -->
## Description
<!-- Write a detailed description of your changes/additions here -->
<!-- If your PR resolves an issue, please state "Resolves #(issue number)" to help maintainers process it faster -->
<!-- If you think your PR will cause breaking changes, require changes in the documentation etc, please be so kind as to explain what, where and how -->
## PR Type
- [ ] Snippets, Tests & Tags (new snippets, updated snippets, re-tagging of snippets, added/updated tests)
- [ ] Tools, Scripts & Automation (anything related to files in the scripts folder, Gatsby, website, Travis CI or Netlify)
- [ ] General, Typos, Misc. & Meta (everything related to content, typos, general stuff and meta files in the repository - e.g. the issue template)
- [ ] Other (please specifiy in the description above)
## Guidelines
- [ ] I have read the guidelines in the [CONTRIBUTING](https://github.com/30-seconds/30-seconds-of-react/blob/master/CONTRIBUTING.md) document.

22
.gitignore vendored
View File

@ -20,7 +20,7 @@ coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
@ -51,23 +51,23 @@ typings/
# Output of 'npm pack'
*.tgz
# Yarn
yarn-error.log
.pnp/
.pnp.js
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# parcel-bundler cache (https://parceljs.org/)
.cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# gatsby files
.cache/
public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless
# Mac files
.DS_Store

12
.travis.yml Normal file
View File

@ -0,0 +1,12 @@
language: node_js
cache:
directories:
- node_modules
node_js:
- lts/*
script:
- npm run extractor
- npm run builder
after_success:
- chmod +x .travis/push.sh
- .travis/push.sh

35
.travis/push.sh Normal file
View File

@ -0,0 +1,35 @@
#!/bin/bash
setup_git() {
git config --global user.email "30secondsofcode@gmail.com"
git config --global user.name "30secondsofcode"
}
commit_website_files() {
if [ $TRAVIS_EVENT_TYPE != "pull_request" ]; then
if [ $TRAVIS_BRANCH == "master" ]; then
echo "Committing to master branch..."
git checkout master
git add *
if [ $TRAVIS_EVENT_TYPE == "cron" ]; then
git commit --message "Travis build: $TRAVIS_BUILD_NUMBER [cron]"
elif [ $TRAVIS_EVENT_TYPE == "api" ]; then
git commit --message "Travis build: $TRAVIS_BUILD_NUMBER [custom]"
else
git commit --message "Travis build: $TRAVIS_BUILD_NUMBER"
fi
fi
fi
}
upload_files() {
if [ $TRAVIS_EVENT_TYPE != "pull_request" ]; then
if [ $TRAVIS_BRANCH == "master" ]; then
echo "Pushing to master branch..."
git push --force --quiet "https://${GH_TOKEN}@github.com/30-seconds/30-seconds-of-react.git" master > /dev/null 2>&1
fi
fi
}
setup_git
commit_website_files
upload_files

46
CODE_OF_CONDUCT.md Normal file
View File

@ -0,0 +1,46 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at 30secondsofcode@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/

View File

@ -11,12 +11,18 @@ Here's what you can do to help:
### Snippet submission and Pull request guidelines
- **DO NOT MODIFY THE README.md or index.html FILES!** Make changes to individual snippet files. **Travis CI** will automatically build the `README.md` and `index.html` files when your pull request is merged.
- **DO NOT MODIFY THE README.md FILE!** Make changes to individual snippet files. **Travis CI** will automatically build the `README.md` file when your pull request is merged.
- **Snippet filenames** must correspond to the title of the snippet. For example, if your snippet is titled `### AwesomeComponent` the filename should be `AwesomeComponent.md`.
- Use `TitleCase`, not `camelCase`, `kebab-case` or `snake_case` when naming components.
- Use `camelCase`, not `TitleCase`, `kebab-case` or `snake_case` when naming custom hooks.
- Avoid capitalization of words, except if the whole word is capitalized (e.g. `URL` should be capitalized in the filename and the snippet title).
- **Snippet titles** should be the same as the name of the component that is present in the snippet.
- All snippet titles must be prefixed with `###` and be at the very first line of your snippet.
- **Snippet metadata** must be included in all snippets in the form of frontmatter.
- All snippets must contain a title.
- All snippets must contain tags, prefixed with `tags:` and separated by commas (optional spaces in-between).
- Make sure the first tag in your snippet's tags is one of the main categories, as seen in the `README.md` file or the website.
- Snippet tags must include a difficulty setting (`begginer`, `intermediate` or `advanced`), preferrably at the end of the list.
- **Snippet titles** should be the same as the name of the component or hook that is present in the snippet.
- All snippet titles must be prefixed with `title:` and be at the very first line of your snippet's frontmatter.
- Snippet titles must be unique (although if you cannot find a better title, just add some placeholder at the end of the filename and title and we will figure it out).
- Follow snippet titles with an empty line.
- **Snippet descriptions** must be short and to the point. Try to explain _what_ the snippet does and _how_ the snippet works and what Javascript/React features are used. Remember to include what functions you are using and why.

2387
README.md

File diff suppressed because it is too large Load Diff

4
_headers Normal file
View File

@ -0,0 +1,4 @@
[[headers]]
for = "/static/*"
[headers.values]
Cache-Control = "public, max-age=360000"

1
advanced.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="78" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="a"><rect height="20" rx="3" fill="#fff" width="64"/></clipPath><g clip-path="url(#a)"><path fill="#555" d="M0 0h65v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="110"><text x="325" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="530">advanced</text><text x="325" y="140" transform="scale(.1)" textLength="530">advanced</text></g> </svg>

After

Width:  |  Height:  |  Size: 695 B

BIN
assets/30s-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
assets/NotoSans-Italic.ttf Normal file

Binary file not shown.

BIN
assets/NotoSans-Light.ttf Normal file

Binary file not shown.

Binary file not shown.

BIN
assets/NotoSans-Medium.ttf Normal file

Binary file not shown.

Binary file not shown.

BIN
assets/NotoSans-Regular.ttf Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

16
config.js Normal file
View File

@ -0,0 +1,16 @@
module.exports = {
// Project metadata
name: `30 seconds starter`,
description: `Curated collection of useful React snippets that you can understand in 30 seconds or less.`,
shortName: `30s`,
repositoryUrl: `https://github.com/30-seconds/30-seconds-of-react`,
// Path information
snippetPath: `snippets`,
snippetDataPath: `snippet_data`,
assetPath: `assets`,
pagePath: `src/docs/pages`,
staticPartsPath: `src/static-parts`,
// General information
language: `jsx`,
optionalLanguage: `css`
};

View File

@ -1,395 +0,0 @@
[
{
"name": "Accordion.md",
"title": "Accordion",
"text": "Renders an accordion menu with multiple collapsible content components.\n\n* Define an `AccordionItem` component, pass it to the `Accordion` and remove unnecessary nodes expect for `AccordionItem` by identifying the function's name in `props.children`.\n* Each `AccordionItem` component renders a `<button>` that is used to update the `Accordion` via the `props.handleClick` callback and the content of the component, passed down via `props.children`, while its appearance is determined by `props.isCollapsed` and based on `style`.\n* In the `Accordion` component, use the `React.useState()` hook to initialize the value of the `bindIndex` state variable to `props.defaultIndex`.\n* Use `Array.prototype.map` on the collected nodes to render the individual collapsiple elements.\n* Define `changeItem`, which will be executed when clicking an `AccordionItem`'s `<button>`.\n`changeItem` executes the passed callback, `onItemClick` and updates `bindIndex` based on the clicked element.\n\n",
"codeBlocks": [
"```jsx\nfunction AccordionItem(props) {\n const style = {\n collapsed: {\n display: 'none'\n },\n expanded: {\n display: 'block'\n },\n buttonStyle: {\n display: 'block',\n width: '100%'\n }\n };\n\n return (\n <div>\n <button style={style.buttonStyle} onClick={() => props.handleClick()}>\n {props.label}\n </button>\n <div\n className=\"collapse-content\"\n style={props.isCollapsed ? style.collapsed : style.expanded}\n aria-expanded={props.isCollapsed}\n >\n {props.children}\n </div>\n </div>\n );\n}\n\nfunction Accordion(props) {\n const [bindIndex, setBindIndex] = React.useState(props.defaultIndex);\n\n const changeItem = itemIndex => {\n if (typeof props.onItemClick === 'function') props.onItemClick(itemIndex);\n if (itemIndex !== bindIndex) setBindIndex(itemIndex);\n };\n const items = props.children.filter(item => item.type.name === 'AccordionItem');\n\n return (\n <div className=\"wrapper\">\n {items.map(({ props }) => (\n <AccordionItem\n isCollapsed={bindIndex === props.index}\n label={props.label}\n handleClick={() => changeItem(props.index)}\n children={props.children}\n />\n ))}\n </div>\n );\n}\n```",
"```jsx\nReactDOM.render(\n <Accordion defaultIndex=\"1\" onItemClick={console.log}>\n <AccordionItem label=\"A\" index=\"1\">\n Lorem ipsum\n </AccordionItem>\n <AccordionItem label=\"B\" index=\"2\">\n Dolor sit amet\n </AccordionItem>\n </Accordion>,\n document.getElementById('root')\n);\n```"
],
"expertise": 2,
"tags": [
"visual",
"children",
"state"
],
"notes": []
},
{
"name": "AutoLink.md",
"title": "AutoLink",
"text": "Renders a string as plaintext, with URLs converted to appropriate `<a>` elements.\n\n* Use `String.prototype.split()` and `String.prototype.match()` with a regular expression to find URLs in a string.\n* Return a `<React.Fragment>` with matched URLs rendered as `<a>` elements, dealing with missing protocol prefixes if necessary, and the rest of the string rendered as plaintext.\n\n",
"codeBlocks": [
"```jsx\nfunction AutoLink({ text }) {\n const delimiter = /((?:https?:\\/\\/)?(?:(?:[a-z0-9]?(?:[a-z0-9\\-]{1,61}[a-z0-9])?\\.[^\\.|\\s])+[a-z\\.]*[a-z]+|(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?:\\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3})(?::\\d{1,5})*[a-z0-9.,_\\/~#&=;%+?\\-\\\\(\\\\)]*)/gi;\n\n return (\n <React.Fragment>\n {text.split(delimiter).map(word => {\n let match = word.match(delimiter);\n if (match) {\n let url = match[0];\n return <a href={url.startsWith('http') ? url : `http://${url}`}>{url}</a>;\n }\n return word;\n })}\n </React.Fragment>\n );\n}\n```",
"```jsx\nReactDOM.render(\n <AutoLink text=\"foo bar baz http://example.org bar\" />,\n document.getElementById('root')\n);\n```"
],
"expertise": 2,
"tags": [
"string",
"fragment",
"regexp"
],
"notes": []
},
{
"name": "Carousel.md",
"title": "Carousel",
"text": "Renders a carousel component.\n\n* Use the `React.setState()` hook to create the `active` state variable and give it a value of `0` (index of the first item).\n* Use an object, `style`, to hold the styles for the individual components.\n* Use the `React.setEffect()` hook to update the value of `active` to the index of the next item, using `setTimeout`.\n* Destructure `props`, compute if visibility style should be set to `visible` or not for each carousel item while mapping over and applying the combined style to the carousel item component accordingly.\n* Render the carousel items using `React.cloneElement()` and pass down rest `props` along with the computed styles.\n\n",
"codeBlocks": [
"```jsx\nfunction Carousel(props) {\n const [active, setActive] = React.useState(0);\n let scrollInterval = null;\n const style = {\n carousel: {\n position: 'relative'\n },\n carouselItem: {\n position: 'absolute',\n visibility: 'hidden'\n },\n visible: {\n visibility: 'visible'\n }\n };\n React.useEffect(() => {\n scrollInterval = setTimeout(() => {\n const { carouselItems } = props;\n setActive((active + 1) % carouselItems.length);\n }, 2000);\n });\n const { carouselItems, ...rest } = props;\n return (\n <div style={style.carousel}>\n {carouselItems.map((item, index) => {\n const activeStyle = active === index ? style.visible : {};\n return React.cloneElement(item, {\n ...rest,\n style: {\n ...style.carouselItem,\n ...activeStyle\n }\n });\n })}\n </div>\n );\n}\n```",
"```jsx\nReactDOM.render(\n <Carousel\n carouselItems={[\n <div>carousel item 1</div>,\n <div>carousel item 2</div>,\n <div>carousel item 3</div>\n ]}\n />,\n document.getElementById('root')\n);\n```"
],
"expertise": 2,
"tags": [
"visual",
"children",
"state",
"effect"
],
"notes": []
},
{
"name": "Collapse.md",
"title": "Collapse",
"text": "Renders a component with collapsible content.\n\n* Use the `React.setState()` hook to create the `isCollapsed` state variable with an initial value of `props.collapsed`.\n* Use an object, `style`, to hold the styles for individual components and their states.\n* Use a `<div>` to wrap both the `<button>` that alters the component's `isCollapsed` state and the content of the component, passed down via `props.children`.\n* Determine the appearance of the content, based on `isCollapsed` and apply the appropriate CSS rules from the `style` object.\n* Finally, update the value of the `aria-expanded` attribute based on `isCollapsed` to make the component accessible.\n\n",
"codeBlocks": [
"```jsx\nfunction Collapse(props) {\n const [isCollapsed, setIsCollapsed] = React.useState(props.collapsed);\n\n const style = {\n collapsed: {\n display: 'none'\n },\n expanded: {\n display: 'block'\n },\n buttonStyle: {\n display: 'block',\n width: '100%'\n }\n };\n\n return (\n <div>\n <button style={style.buttonStyle} onClick={() => setIsCollapsed(!isCollapsed)}>\n {isCollapsed ? 'Show' : 'Hide'} content\n </button>\n <div\n className=\"collapse-content\"\n style={isCollapsed ? style.collapsed : style.expanded}\n aria-expanded={isCollapsed}\n >\n {props.children}\n </div>\n </div>\n );\n}\n```",
"```jsx\nReactDOM.render(\n <Collapse>\n <h1>This is a collapse</h1>\n <p>Hello world!</p>\n </Collapse>,\n document.getElementById('root')\n);\n```"
],
"expertise": 2,
"tags": [
"visual",
"children",
"state"
],
"notes": []
},
{
"name": "CountDown.md",
"title": "CountDown",
"text": "Renders a countdown timer that prints a message when it reaches zero.\n\n* Use object destructuring to set defaults for the `hours`, `minutes` and `seconds` props.\n* Use the `React.useState()` hook to create the `time`, `paused` and `over` state variables and set their values to the values of the passed props, `false` and `false` respectively.\n* Create a method `tick`, that updates the value of `time` based on the current value (i.e. decreasing the time by one second).\n* If `paused` or `over` is `true`, `tick` will return immediately.\n* Create a method `reset`, that resets all state variables to their initial states.\n* Use the the `React.useEffect()` hook to call the `tick` method every second via the use of `setInterval()` and use `clearInterval()` to cleanup when the component is unmounted.\n* Use a `<div>` to wrap a `<p>` element with the textual representation of the components `time` state variable, as well as two `<button>` elements that will pause/unpause and restart the timer respectively.\n* If `over` is `true`, the timer will display a message instead of the value of `time`.\n\n",
"codeBlocks": [
"```jsx\nfunction CountDown({ hours = 0, minutes = 0, seconds = 0 }) {\n const [paused, setPaused] = React.useState(false);\n const [over, setOver] = React.useState(false);\n const [time, setTime] = React.useState({\n hours: parseInt(hours),\n minutes: parseInt(minutes),\n seconds: parseInt(seconds)\n });\n\n const tick = () => {\n if (paused || over) return;\n if (time.hours == 0 && time.minutes == 0 && time.seconds == 0) setOver(true);\n else if (time.minutes == 0 && time.seconds == 0)\n setTime({\n hours: time.hours - 1,\n minutes: 59,\n seconds: 59\n });\n else if (time.seconds == 0)\n setTime({\n hours: time.hours,\n minutes: time.minutes - 1,\n seconds: 59\n });\n else\n setTime({\n hours: time.hours,\n minutes: time.minutes,\n seconds: time.seconds - 1\n });\n };\n\n const reset = () => {\n setTime({\n hours: parseInt(hours),\n minutes: parseInt(minutes),\n seconds: parseInt(seconds)\n });\n setPaused(false);\n setOver(false);\n };\n\n React.useEffect(() => {\n let timerID = setInterval(() => tick(), 1000);\n return () => clearInterval(timerID);\n });\n\n return (\n <div>\n <p>{`${time.hours.toString().padStart(2, '0')}:${time.minutes\n .toString()\n .padStart(2, '0')}:${time.seconds.toString().padStart(2, '0')}`}</p>\n <div>{over ? \"Time's up!\" : ''}</div>\n <button onClick={() => setPaused(!paused)}>{paused ? 'Resume' : 'Pause'}</button>\n <button onClick={() => reset()}>Restart</button>\n </div>\n );\n}\n```",
"```jsx\nReactDOM.render(<CountDown hours=\"1\" minutes=\"45\" />, document.getElementById('root'));\n```"
],
"expertise": 2,
"tags": [
"visual",
"state"
],
"notes": []
},
{
"name": "DataList.md",
"title": "DataList",
"text": "Renders a list of elements from an array of primitives.\n\n* Use the value of the `isOrdered` prop to conditionally render a `<ol>` or `<ul>` list.\n* Use `Array.prototype.map` to render every item in `data` as a `<li>` element, give it a `key` produced from the concatenation of the its index and value.\n* Omit the `isOrdered` prop to render a `<ul>` list by default.\n\n",
"codeBlocks": [
"```jsx\nfunction DataList({ isOrdered, data }) {\n const list = data.map((val, i) => <li key={`${i}_${val}`}>{val}</li>);\n return isOrdered ? <ol>{list}</ol> : <ul>{list}</ul>;\n}\n```",
"```jsx\nconst names = ['John', 'Paul', 'Mary'];\nReactDOM.render(<DataList data={names} />, document.getElementById('root'));\nReactDOM.render(<DataList data={names} isOrdered />, document.getElementById('root'));\n```"
],
"expertise": 0,
"tags": [
"array"
],
"notes": []
},
{
"name": "DataTable.md",
"title": "DataTable",
"text": "Renders a table with rows dynamically created from an array of primitives.\n\n* Render a `<table>` element with two columns (`ID` and `Value`).\n* Use `Array.prototype.map` to render every item in `data` as a `<tr>` element, consisting of its index and value, give it a `key` produced from the concatenation of the two.\n\n",
"codeBlocks": [
"```jsx\nfunction DataTable({ data }) {\n return (\n <table>\n <thead>\n <tr>\n <th>ID</th>\n <th>Value</th>\n </tr>\n </thead>\n <tbody>\n {data.map((val, i) => (\n <tr key={`${i}_${val}`}>\n <td>{i}</td>\n <td>{val}</td>\n </tr>\n ))}\n </tbody>\n </table>\n );\n}\n```",
"```jsx\nconst people = ['John', 'Jesse'];\nReactDOM.render(<DataTable data={people} />, document.getElementById('root'));\n```"
],
"expertise": 0,
"tags": [
"array"
],
"notes": []
},
{
"name": "FileDrop.md",
"title": "FileDrop",
"text": "Renders a file drag and drop component for a single file.\n\n* Create a ref called `dropRef` for this component.\n* Use the `React.useState()` hook to create the `drag` and `filename` variables, initialized to `false` and `''` respectively.\nThe variables `dragCounter` and `drag` are used to determine if a file is being dragged, while `filename` is used to store the dropped file's name.\n* Create the `handleDrag`, `handleDragIn`, `handleDragOut` and `handleDrop` methods to handle drag and drop functionality, bind them to the component's context.\n* Each of the methods will handle a specific event, the listeners for which are created and removed in the `React.useEffect()` hook and its attached `cleanup()` method.\n* `handleDrag` prevents the browser from opening the dragged file, `handleDragIn` and `handleDragOut` handle the dragged file entering and exiting the component, while `handleDrop` handles the file being dropped and passes it to `props.handleDrop`.\n* Return an appropriately styled `<div>` and use `drag` and `filename` to determine its contents and style.\n* Finally, bind the `ref` of the created `<div>` to `dropRef`.\n\n",
"codeBlocks": [
"```css\n.filedrop {\n min-height: 120px;\n border: 3px solid #d3d3d3;\n text-align: center;\n font-size: 24px;\n padding: 32px;\n border-radius: 4px;\n}\n\n.filedrop.drag {\n border: 3px dashed #1e90ff;\n}\n\n.filedrop.ready {\n border: 3px solid #32cd32;\n}\n```",
"```jsx\nfunction FileDrop(props) {\n const [drag, setDrag] = React.useState(false);\n const [filename, setFilename] = React.useState('');\n let dropRef = React.createRef();\n let dragCounter = 0;\n\n const handleDrag = e => {\n e.preventDefault();\n e.stopPropagation();\n };\n\n const handleDragIn = e => {\n e.preventDefault();\n e.stopPropagation();\n dragCounter++;\n if (e.dataTransfer.items && e.dataTransfer.items.length > 0) setDrag(true);\n };\n\n const handleDragOut = e => {\n e.preventDefault();\n e.stopPropagation();\n dragCounter--;\n if (dragCounter === 0) setDrag(false);\n };\n\n const handleDrop = e => {\n e.preventDefault();\n e.stopPropagation();\n setDrag(false);\n if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {\n props.handleDrop(e.dataTransfer.files[0]);\n setFilename(e.dataTransfer.files[0].name);\n e.dataTransfer.clearData();\n dragCounter = 0;\n }\n };\n\n React.useEffect(() => {\n let div = dropRef.current;\n div.addEventListener('dragenter', handleDragIn);\n div.addEventListener('dragleave', handleDragOut);\n div.addEventListener('dragover', handleDrag);\n div.addEventListener('drop', handleDrop);\n return function cleanup() {\n div.removeEventListener('dragenter', handleDragIn);\n div.removeEventListener('dragleave', handleDragOut);\n div.removeEventListener('dragover', handleDrag);\n div.removeEventListener('drop', handleDrop);\n };\n });\n\n return (\n <div\n ref={dropRef}\n className={drag ? 'filedrop drag' : filename ? 'filedrop ready' : 'filedrop'}\n >\n {filename && !drag ? <div>{filename}</div> : <div>Drop files here!</div>}\n </div>\n );\n}\n```",
"```jsx\nReactDOM.render(<FileDrop handleDrop={console.log} />, document.getElementById('root'));\n```"
],
"expertise": 2,
"tags": [
"visual",
"input",
"state",
"effect"
],
"notes": []
},
{
"name": "Input.md",
"title": "Input",
"text": "Renders an `<input>` element that uses a callback function to pass its value to the parent component.\n\n* Use object destructuring to set defaults for certain attributes of the `<input>` element.\n* Render an `<input>` element with the appropriate attributes and use the `callback` function in the `onChange` event to pass the value of the input to the parent.\n\n",
"codeBlocks": [
"```jsx\nfunction Input({ callback, type = 'text', disabled = false, readOnly = false, placeholder = '' }) {\n return (\n <input\n type={type}\n disabled={disabled}\n readOnly={readOnly}\n placeholder={placeholder}\n onChange={({ target: { value } }) => callback(value)}\n />\n );\n}\n```",
"```jsx\nReactDOM.render(\n <Input type=\"text\" placeholder=\"Insert some text here...\" callback={val => console.log(val)} />,\n document.getElementById('root')\n);\n```"
],
"expertise": 0,
"tags": [
"input"
],
"notes": []
},
{
"name": "LimitedTextarea.md",
"title": "LimitedTextarea",
"text": "Renders a textarea component with a character limit.\n\n* Use the `React.useState()` hook to create the `content` state variable and set its value to `value`.\nCreate a method `setFormattedContent`, which trims the content of the input if it's longer than `limit`.\n* Use the `React.useEffect()` hook to call the `setFormattedContent` method on the value of the `content` state variable.\n* Use a`<div>` to wrap both the`<textarea>` and the `<p>` element that displays the character count and bind the `onChange` event of the `<textarea>` to call `setFormattedContent` with the value of `event.target.value`.\n\n",
"codeBlocks": [
"```jsx\nfunction LimitedTextarea({ rows, cols, value, limit }) {\n const [content, setContent] = React.useState(value);\n\n const setFormattedContent = text => {\n text.length > limit ? setContent(text.slice(0, limit)) : setContent(text);\n };\n\n React.useEffect(() => {\n setFormattedContent(content);\n }, []);\n\n return (\n <div>\n <textarea\n rows={rows}\n cols={cols}\n onChange={event => setFormattedContent(event.target.value)}\n value={content}\n />\n <p>\n {content.length}/{limit}\n </p>\n </div>\n );\n}\n```",
"```jsx\nReactDOM.render(<LimitedTextarea limit={32} value=\"Hello!\" />, document.getElementById('root'));\n```"
],
"expertise": 0,
"tags": [
"input",
"state",
"effect"
],
"notes": []
},
{
"name": "LimitedWordTextarea.md",
"title": "LimitedWordTextarea",
"text": "Renders a textarea component with a word limit.\n\n* Use the `React.useState()` hook to create the `content` and `wordCount` state variables and set their values to `value` and `0` respectively.\n* Create a method `setFormattedContent`, which uses `String.prototype.split(' ')` to turn the input into an array of words and check if the result of applying `Array.prototype.filter(Boolean)` has a `length` longer than `limit`.\n* If the afforementioned `length` exceeds the `limit`, trim the input, otherwise return the raw input, updating `content` and `wordCount` accordingly in both cases.\n* Use the `React.useEffect()` hook to call the `setFormattedContent` method on the value of the `content` state variable.\n* Use a`<div>` to wrap both the`<textarea>` and the `<p>` element that displays the character count and bind the `onChange` event of the `<textarea>` to call `setFormattedContent` with the value of `event.target.value`.\n\n",
"codeBlocks": [
"```jsx\nfunction LimitedWordTextarea({ rows, cols, value, limit }) {\n const [content, setContent] = React.useState(value);\n const [wordCount, setWordCount] = React.useState(0);\n\n const setFormattedContent = text => {\n let words = text.split(' ');\n if (words.filter(Boolean).length > limit) {\n setContent(\n text\n .split(' ')\n .slice(0, limit)\n .join(' ')\n );\n setWordCount(limit);\n } else {\n setContent(text);\n setWordCount(words.filter(Boolean).length);\n }\n };\n\n React.useEffect(() => {\n setFormattedContent(content);\n }, []);\n\n return (\n <div>\n <textarea\n rows={rows}\n cols={cols}\n onChange={event => setFormattedContent(event.target.value)}\n value={content}\n />\n <p>\n {wordCount}/{limit}\n </p>\n </div>\n );\n}\n```",
"```jsx\nReactDOM.render(\n <LimitedWordTextArea limit={5} value=\"Hello there!\" />,\n document.getElementById('root')\n);\n```"
],
"expertise": 0,
"tags": [
"input",
"state",
"effect"
],
"notes": []
},
{
"name": "Mailto.md",
"title": "Mailto",
"text": "Renders a link formatted to send an email.\n\n* Destructure the component's props, use `email`, `subject` and `body` to create a `<a>` element with an appropriate `href` attribute.\n* Render the link with `props.children` as its content.\n\n",
"codeBlocks": [
"```jsx\nfunction Mailto({ email, subject, body, ...props }) {\n return (\n <a href={`mailto:${email}?subject=${subject || ''}&body=${body || ''}`}>{props.children}</a>\n );\n}\n```",
"```jsx\nReactDOM.render(\n <Mailto email=\"foo@bar.baz\" subject=\"Hello\" body=\"Hello world!\">\n Mail me!\n </Mailto>,\n document.getElementById('root')\n);\n```"
],
"expertise": 0,
"tags": [
"visual"
],
"notes": []
},
{
"name": "MappedTable.md",
"title": "MappedTable",
"text": "Renders a table with rows dynamically created from an array of objects and a list of property names.\n\n* Use `Object.keys()`, `Array.prototype.filter()`, `Array.prototype.includes()` and `Array.prototype.reduce()` to produce a `filteredData` array, containing all objects with the keys specified in `propertyNames`.\n* Render a `<table>` element with a set of columns equal to the amount of values in `propertyNames`.\n* Use `Array.prototype.map` to render each value in the `propertyNames` array as a `<th>` element.\n* Use `Array.prototype.map` to render each object in the `filteredData` array as a `<tr>` element, containing a `<td>` for each key in the object.\n\n",
"codeBlocks": [
"```jsx\nfunction MappedTable({ data, propertyNames }) {\n let filteredData = data.map(v =>\n Object.keys(v)\n .filter(k => propertyNames.includes(k))\n .reduce((acc, key) => ((acc[key] = v[key]), acc), {})\n );\n return (\n <table>\n <thead>\n <tr>\n {propertyNames.map(val => (\n <th key={`h_${val}`}>{val}</th>\n ))}\n </tr>\n </thead>\n <tbody>\n {filteredData.map((val, i) => (\n <tr key={`i_${i}`}>\n {propertyNames.map(p => (\n <td key={`i_${i}_${p}`}>{val[p]}</td>\n ))}\n </tr>\n ))}\n </tbody>\n </table>\n );\n}\n```",
"```jsx\nconst people = [\n { name: 'John', surname: 'Smith', age: 42 },\n { name: 'Adam', surname: 'Smith', gender: 'male' }\n];\nconst propertyNames = ['name', 'surname', 'age'];\nReactDOM.render(\n <MappedTable data={people} propertyNames={propertyNames} />,\n document.getElementById('root')\n);\n```"
],
"expertise": 1,
"tags": [
"array",
"object"
],
"notes": [
"This component does not work with nested objects and will break if there are nested objects inside any of the properties specified in `propertyNames`.",
"<!-tags: array,object -->",
"<!-expertise: 1 -->"
]
},
{
"name": "Modal.md",
"title": "Modal",
"text": "Renders a Modal component, controllable through events.\nTo use the component, import `Modal` only once and then display it by passing a boolean value to the `isVisible` attribute.\n\n* Use object destructuring to set defaults for certain attributes of the modal component.\n* Define `keydownHandler`, a method which handles all keyboard events, which can be used according to your needs to dispatch actions (e.g. close the modal when <kbd>Esc</kbd> is pressed).\n* Use `React.useEffect()` hook to add or remove the `keydown` event listener, which calls `keydownHandler`.\n* Use the `isVisible` prop to determine if the modal should be shown or not.\n* Use CSS to style and position the modal component.\n\n",
"codeBlocks": [
"```css\n.modal {\n position: fixed;\n top: 0;\n bottom: 0;\n left: 0;\n right:0;\n width: 100%;\n z-index: 9999; \n display: flex;\n align-items: center;\n justify-content: center;\n background-color: rgba(0, 0, 0, 0.25);\n animation-name: appear;\n animation-duration: 300ms;\n}\n\n.modal-dialog{\n width: 100%;\n max-width: 550px;\n background: white;\n position: relative;\n margin: 0 20px;\n max-height: calc(100vh - 40px);\n text-align: left;\n display: flex;\n flex-direction: column;\n overflow:hidden;\n box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19);\n -webkit-animation-name: animatetop;\n -webkit-animation-duration: 0.4s;\n animation-name: slide-in;\n animation-duration: 0.5s;\n}\n\n.modal-header,.modal-footer{\n display: flex;\n align-items: center;\n padding: 1rem;\n}\n.modal-header{\n border-bottom: 1px solid #dbdbdb;\n justify-content: space-between;\n}\n.modal-footer{\n border-top: 1px solid #dbdbdb;\n justify-content: flex-end;\n}\n.modal-close{\n cursor: pointer;\n padding: 1rem;\n margin: -1rem -1rem -1rem auto;\n}\n.modal-body{\n overflow: auto;\n}\n.modal-content{\n padding: 1rem;\n}\n\n@keyframes appear {\n from {opacity: 0;}\n to {opacity: 1;}\n}\n@keyframes slide-in {\n from {transform: translateY(-150px);}\n to { transform: translateY(0);}\n}\n```",
"```jsx\nfunction Modal({ isVisible = false, title, content, footer, onClose }){ \n React.useEffect(() => {\n document.addEventListener('keydown', keydownHandler);\n return () => document.removeEventListener('keydown', keydownHandler);\n });\n\n function keydownHandler({ key }) {\n switch (key) {\n case 'Escape': onClose(); break;\n default:\n }\n }\n\n return !isVisible ? null : (\n <div className=\"modal\" onClick={onClose}>\n <div className=\"modal-dialog\" onClick={e => e.stopPropagation()}>\n <div className=\"modal-header\">\n <h3 className=\"modal-title\">{title}</h3>\n <span className=\"modal-close\" onClick={onClose}>&times;</span>\n </div>\n <div className=\"modal-body\">\n <div className=\"modal-content\">{ content }</div>\n </div>\n {footer && <div className=\"modal-footer\">{footer}</div>}\n </div>\n </div>\n )\n}\n```",
"```jsx\n//Add the component to the render function\nfunction App() {\n const [ isModal, setModal] = React.useState(false);\n \n return (\n <React.Fragment>\n <button onClick={()=> setModal(true)}>Click Here</button>\n <Modal \n isVisible={ isModal }\n title= \"Modal Title\"\n content = {<p>Add your content here</p>}\n footer = {<button>Cancel</button>}\n onClose ={()=> setModal(false)}\n />\n </React.Fragment>\n )\n}\n\nReactDOM.render( <App/>, document.getElementById('root'));\n```"
],
"expertise": 2,
"tags": [
"visual",
"effect"
],
"notes": []
},
{
"name": "MultiselectCheckbox.md",
"title": "MultiselectCheckbox",
"text": "Renders a checkbox list that uses a callback function to pass its selected value/values to the parent component.\n\n* Use `React.setState()` to create a `data` state variable and set its initial value equal to the `options` prop.\n* Create a function `toggle` that is used to toggle the `checked` to update the `data` state variable and call the `onChange` callback passed via the component's props.\n* Render a `<ul>` element and use `Array.prototype.map()` to map the `data` state variable to individual `<li>` elements with `<input>` elements as their children.\n* Each `<input>` element has the `type='checkbox'` attribute and is marked as `readOnly`, as its click events are handled by the parent `<li>` element's `onClick` handler.\n\n",
"codeBlocks": [
"```jsx\nconst style = {\n listContainer: {\n listStyle: 'none',\n paddingLeft: 0\n },\n itemStyle: {\n cursor: 'pointer',\n padding: 5\n }\n};\n\nfunction MultiselectCheckbox({ options, onChange }) {\n const [data, setData] = React.useState(options);\n\n const toggle = item => {\n data.map((_, key) => {\n if (data[key].label === item.label) data[key].checked = !item.checked;\n });\n setData([...data]);\n onChange(data);\n };\n\n return (\n <ul style={style.listContainer}>\n {data.map(item => {\n return (\n <li key={item.label} style={style.itemStyle} onClick={() => toggle(item)}>\n <input readOnly type=\"checkbox\" checked={item.checked || false} />\n {item.label}\n </li>\n );\n })}\n </ul>\n );\n}\n```",
"```jsx\nconst options = [{ label: 'Item One' }, { label: 'Item Two' }];\n\nReactDOM.render(\n <MultiselectCheckbox\n options={options}\n onChange={data => {\n console.log(data);\n }}\n />,\n document.getElementById('root')\n);\n```"
],
"expertise": 1,
"tags": [
"input",
"state",
"array"
],
"notes": []
},
{
"name": "PasswordRevealer.md",
"title": "PasswordRevealer",
"text": "Renders a password input field with a reveal button.\n\n* Use the `React.useState()` hook to create the `shown` state variable and set its value to `false`.\n* Use a`<div>` to wrap both the`<input>` and the `<button>` element that toggles the type of the input field between `\"text\"` and `\"password\"`.\n\n",
"codeBlocks": [
"```jsx\nfunction PasswordRevealer({ value }) {\n const [shown, setShown] = React.useState(false);\n\n return (\n <div>\n <input type={shown ? 'text' : 'password'} value={value} onChange={() => {}} />\n <button onClick={() => setShown(!shown)}>Show/Hide</button>\n </div>\n );\n}\n```",
"```jsx\nReactDOM.render(<PasswordRevealer />, document.getElementById('root'));\n```"
],
"expertise": 0,
"tags": [
"input",
"state"
],
"notes": []
},
{
"name": "Select.md",
"title": "Select",
"text": "Renders a `<select>` element that uses a callback function to pass its value to the parent component.\n\n* Use object destructuring to set defaults for certain attributes of the `<select>` element.\n* Render a `<select>` element with the appropriate attributes and use the `callback` function in the `onChange` event to pass the value of the textarea to the parent.\n* Use destructuring on the `values` array to pass an array of `value` and `text` elements and the `selected` attribute to define the initial `value` of the `<select>` element.\n\n",
"codeBlocks": [
"```jsx\nfunction Select({ values, callback, disabled = false, readonly = false, selected }) {\n return (\n <select\n disabled={disabled}\n readOnly={readonly}\n onChange={({ target: { value } }) => callback(value)}\n >\n {values.map(([value, text]) => (\n <option selected={selected === value} value={value}>\n {text}\n </option>\n ))}\n </select>\n );\n}\n```",
"```jsx\nlet choices = [\n ['grapefruit', 'Grapefruit'],\n ['lime', 'Lime'],\n ['coconut', 'Coconut'],\n ['mango', 'Mango']\n];\nReactDOM.render(\n <Select values={choices} selected=\"lime\" callback={val => console.log(val)} />,\n document.getElementById('root')\n);\n```"
],
"expertise": 0,
"tags": [
"input"
],
"notes": []
},
{
"name": "Slider.md",
"title": "Slider",
"text": "Renders a slider element that uses a callback function to pass its value to the parent component.\n\n* Use object destructuring to set defaults for certain attributes of the `<input>` element.\n* Render an `<input>` element of type `\"range\"` and the appropriate attributes, use the `callback` function in the `onChange` event to pass the value of the input to the parent.\n\n",
"codeBlocks": [
"```jsx\nfunction Slider({ callback, disabled = false, readOnly = false }) {\n return (\n <input\n type=\"range\"\n disabled={disabled}\n readOnly={readOnly}\n onChange={({ target: { value } }) => callback(value)}\n />\n );\n}\n```",
"```jsx\nReactDOM.render(<Slider callback={val => console.log(val)} />, document.getElementById('root'));\n```"
],
"expertise": 0,
"tags": [
"input"
],
"notes": []
},
{
"name": "StarRating.md",
"title": "StarRating",
"text": "Renders a star rating component.\n\n* Define a component, called `Star` that will render each individual star with the appropriate appearance, based on the parent component's state.\n* In the `StarRating` component, use the `React.useState()` hook to define the `rating` and `selection` state variables with the initial values of `props.rating` (or `0` if invalid or not supplied) and `0`.\n* Create a method, `hoverOver`, that updates `selected` and `rating` according to the provided `event`.\n* Create a `<div>` to wrap the `<Star>` components, which are created using `Array.prototype.map` on an array of 5 elements, created using `Array.from`, and handle the `onMouseLeave` event to set `selection` to `0`, the `onClick` event to set the `rating` and the `onMouseOver` event to set `selection` to the `star-id` attribute of the `event.target` respectively.\n* Finally, pass the appropriate values to each `<Star>` component (`starId` and `marked`).\n\n",
"codeBlocks": [
"```jsx\nfunction Star({ marked, starId }) {\n return (\n <span star-id={starId} style={{ color: '#ff9933' }} role=\"button\">\n {marked ? '\\u2605' : '\\u2606'}\n </span>\n );\n}\n\nfunction StarRating(props) {\n const [rating, setRating] = React.useState(typeof props.rating == 'number' ? props.rating : 0);\n const [selection, setSelection] = React.useState(0);\n const hoverOver = event => {\n let val = 0;\n if (event && event.target && event.target.getAttribute('star-id'))\n val = event.target.getAttribute('star-id');\n setSelection(val);\n };\n return (\n <div\n onMouseOut={() => hoverOver(null)}\n onClick={(event) => setRating(event.target.getAttribute('star-id') || this.state.rating)}\n onMouseOver={hoverOver}\n >\n {Array.from({ length: 5 }, (v, i) => (\n <Star\n starId={i + 1}\n key={`star_${i + 1} `}\n marked={selection ? selection >= i + 1 : rating >= i + 1}\n />\n ))}\n </div>\n );\n}\n```",
"```jsx\nReactDOM.render(<StarRating />, document.getElementById('root'));\nReactDOM.render(<StarRating rating={2} />, document.getElementById('root'));\n```"
],
"expertise": 2,
"tags": [
"visual",
"children",
"input",
"state"
],
"notes": []
},
{
"name": "Tabs.md",
"title": "Tabs",
"text": "Renders a tabbed menu and view component.\n\n* Define a `TabItem` component, pass it to the `Tab` and remove unnecessary nodes expect for `TabItem` by identifying the function's name in `props.children`.\n* Use the `React.useState()` hook to initialize the value of the `bindIndex` state variable to `props.defaultIndex`.\n* Use `Array.prototype.map` on the collected nodes to render the `tab-menu` and `tab-view`.\n* Define `changeTab`, which will be executed when clicking a `<button>` from the `tab-menu`.\n* `changeTab` executes the passed callback, `onTabClick` and updates `bindIndex`, which in turn causes a re-render, evaluating the `style` and `className` of the `tab-view` items and `tab-menu` buttons according to their `index`.\n\n",
"codeBlocks": [
"```css\n.tab-menu > button {\n cursor: pointer;\n padding: 8px 16px;\n border: 0;\n border-bottom: 2px solid transparent;\n background: none;\n}\n.tab-menu > button.focus {\n border-bottom: 2px solid #007bef;\n}\n.tab-menu > button:hover {\n border-bottom: 2px solid #007bef;\n}\n```",
"```jsx\nfunction TabItem(props) {\n return <div {...props} />;\n}\n\nfunction Tabs(props) {\n const [bindIndex, setBindIndex] = React.useState(props.defaultIndex);\n const changeTab = newIndex => {\n if (typeof props.onTabClick === 'function') props.onTabClick(newIndex);\n setBindIndex(newIndex);\n };\n const items = props.children.filter(item => item.type.name === 'TabItem');\n\n return (\n <div className=\"wrapper\">\n <div className=\"tab-menu\">\n {items.map(({ props: { index, label } }) => (\n <button onClick={() => changeTab(index)} className={bindIndex === index ? 'focus' : ''}>\n {label}\n </button>\n ))}\n </div>\n <div className=\"tab-view\">\n {items.map(({ props }) => (\n <div\n {...props}\n className=\"tab-view_item\"\n key={props.index}\n style={{ display: bindIndex === props.index ? 'block' : 'none' }}\n />\n ))}\n </div>\n </div>\n );\n}\n```",
"```jsx\nReactDOM.render(\n <Tabs defaultIndex=\"1\" onTabClick={console.log}>\n <TabItem label=\"A\" index=\"1\">\n Lorem ipsum\n </TabItem>\n <TabItem label=\"B\" index=\"2\">\n Dolor sit amet\n </TabItem>\n </Tabs>,\n document.getElementById('root')\n);\n```"
],
"expertise": 1,
"tags": [
"visual",
"state",
"children"
],
"notes": []
},
{
"name": "TextArea.md",
"title": "TextArea",
"text": "Renders a `<textarea>` element that uses a callback function to pass its value to the parent component.\n\n* Use object destructuring to set defaults for certain attributes of the `<textarea>` element.\n* Render a `<textarea>` element with the appropriate attributes and use the `callback` function in the `onChange` event to pass the value of the textarea to the parent.\n\n",
"codeBlocks": [
"```jsx\nfunction TextArea({\n callback,\n cols = 20,\n rows = 2,\n disabled = false,\n readOnly = false,\n placeholder = ''\n}) {\n return (\n <textarea\n cols={cols}\n rows={rows}\n disabled={disabled}\n readOnly={readOnly}\n placeholder={placeholder}\n onChange={({ target: { value } }) => callback(value)}\n />\n );\n}\n```",
"```jsx\nReactDOM.render(\n <TextArea placeholder=\"Insert some text here...\" callback={val => console.log(val)} />,\n document.getElementById('root')\n);\n```"
],
"expertise": 0,
"tags": [
"input"
],
"notes": []
},
{
"name": "Ticker.md",
"title": "Ticker",
"text": "Renders a ticker component.\n\n* Use the `React.useState()` hook to initialize the `ticker` state variable to `0`.\n* Define two methods, `tick` and `reset`, that will periodically increment `timer` based on `interval` and reset `interval` respectively.\n* Return a `<div>` with two `<button>` elements, each of which calls `tick` and `reset` respectively.\n\n",
"codeBlocks": [
"```jsx\nfunction Ticker(props) {\n const [ticker, setTicker] = React.useState(0);\n let interval = null;\n\n const tick = () => {\n reset();\n interval = setInterval(() => {\n if (ticker < props.times) setTicker(ticker + 1);\n else clearInterval(interval);\n }, props.interval);\n };\n\n const reset = () => {\n setTicker(0);\n clearInterval(interval);\n };\n\n return (\n <div>\n <span style={{ fontSize: 100 }}>{this.state.ticker}</span>\n <button onClick={this.tick}>Tick!</button>\n <button onClick={this.reset}>Reset</button>\n </div>\n );\n}\n```",
"```jsx\nReactDOM.render(<Ticker times={5} interval={1000} />, document.getElementById('root'));\n```"
],
"expertise": 1,
"tags": [
"visual",
"state"
],
"notes": []
},
{
"name": "Toggle.md",
"title": "Toggle",
"text": "Renders a toggle component.\n\n* Use the `React.useState()` to initialize the `isToggleOn` state variable to `false`.\n* Use an object, `style`, to hold the styles for individual components and their states.\n* Return a `<button>` that alters the component's `isToggledOn` when its `onClick` event is fired and determine the appearance of the content based on `isToggleOn`, applying the appropriate CSS rules from the `style` object.\n\n",
"codeBlocks": [
"```jsx\nfunction Toggle(props) {\n const [isToggleOn, setIsToggleOn] = React.useState(false);\n style = {\n on: {\n backgroundColor: 'green'\n },\n off: {\n backgroundColor: 'grey'\n }\n };\n\n return (\n <button onClick={() => setIsToggleOn(!isToggleOn)} style={isToggleOn ? style.on : style.off}>\n {isToggleOn ? 'ON' : 'OFF'}\n </button>\n );\n}\n```",
"```jsx\nReactDOM.render(<Toggle />, document.getElementById('root'));\n```"
],
"expertise": 0,
"tags": [
"visual",
"state"
],
"notes": []
},
{
"name": "Tooltip.md",
"title": "Tooltip",
"text": "Renders a tooltip component.\n\n* Use the `React.useState()` hook to create the `show` variable and initialize it to `false`.\n* Return a `<div>` element that contains the `<div>` that will be the tooltip and the `children` passed to the component.\n* Handle the `onMouseEnter` and `onMouseLeave` methods, by altering the value of the `show` variable.\n\n",
"codeBlocks": [
"```css\n.tooltip {\n position: relative;\n background: rgba(0, 0, 0, 0.7);\n color: white;\n visibility: hidden;\n padding: 5px;\n border-radius: 5px;\n}\n.tooltip-arrow {\n position: absolute;\n top: 100%;\n left: 50%;\n border-width: 5px;\n border-style: solid;\n border-color: rgba(0, 0, 0, 0.7) transparent transparent;\n}\n```",
"```jsx\nfunction Tooltip({ children, text, ...rest }) {\n const [show, setShow] = React.useState(false);\n\n return (\n <div>\n <div className=\"tooltip\" style={show ? { visibility: 'visible' } : {}}>\n {text}\n <span className=\"tooltip-arrow\" />\n </div>\n <div {...rest} onMouseEnter={() => setShow(true)} onMouseLeave={() => setShow(false)}>\n {children}\n </div>\n </div>\n );\n}\n```",
"```jsx\nReactDOM.render(\n <Tooltip text=\"Simple tooltip\">\n <button>Hover me!</button>\n </Tooltip>,\n document.getElementById('root')\n);\n```"
],
"expertise": 1,
"tags": [
"visual",
"state",
"children"
],
"notes": []
},
{
"name": "TreeView.md",
"title": "TreeView",
"text": "Renders a tree view of a JSON object or array with collapsible content.\n\n* Use object destructuring to set defaults for certain props.\n* Use the value of the `toggled` prop to determine the initial state of the content (collapsed/expanded).\n* Use the `React.setState()` hook to create the `isToggled` state variable and give it the value of the `toggled` prop initially.\n* Return a `<div>` to wrap the contents of the component and the `<span>` element, used to alter the component's `isToggled` state.\n* Determine the appearance of the component, based on `isParentToggled`, `isToggled`, `name` and `Array.isArray()` on `data`.\n* For each child in `data`, determine if it is an object or array and recursively render a sub-tree.\n* Otherwise, render a `<p>` element with the appropriate style.\n\n",
"codeBlocks": [
"```css\n.tree-element {\n margin: 0;\n position: relative;\n}\n\ndiv.tree-element:before {\n content: '';\n position: absolute;\n top: 24px;\n left: 1px;\n height: calc(100% - 48px);\n border-left: 1px solid gray;\n}\n\n.toggler {\n position: absolute;\n top: 10px;\n left: 0px;\n width: 0;\n height: 0;\n border-top: 4px solid transparent;\n border-bottom: 4px solid transparent;\n border-left: 5px solid gray;\n cursor: pointer;\n}\n\n.toggler.closed {\n transform: rotate(90deg);\n}\n\n.collapsed {\n display: none;\n}\n```",
"```jsx\nfunction TreeView({\n data,\n toggled = true,\n name = null,\n isLast = true,\n isChildElement = false,\n isParentToggled = true\n}) {\n const [isToggled, setIsToggled] = React.useState(toggled);\n\n return (\n <div\n style={{ marginLeft: isChildElement ? 16 : 4 + 'px' }}\n className={isParentToggled ? 'tree-element' : 'tree-element collapsed'}\n >\n <span\n className={isToggled ? 'toggler' : 'toggler closed'}\n onClick={() => setIsToggled(!isToggled)}\n />\n {name ? <strong>&nbsp;&nbsp;{name}: </strong> : <span>&nbsp;&nbsp;</span>}\n {Array.isArray(data) ? '[' : '{'}\n {!isToggled && '...'}\n {Object.keys(data).map((v, i, a) =>\n typeof data[v] == 'object' ? (\n <TreeView\n data={data[v]}\n isLast={i === a.length - 1}\n name={Array.isArray(data) ? null : v}\n isChildElement\n isParentToggled={isParentToggled && isToggled}\n />\n ) : (\n <p\n style={{ marginLeft: 16 + 'px' }}\n className={isToggled ? 'tree-element' : 'tree-element collapsed'}\n >\n {Array.isArray(data) ? '' : <strong>{v}: </strong>}\n {data[v]}\n {i === a.length - 1 ? '' : ','}\n </p>\n )\n )}\n {Array.isArray(data) ? ']' : '}'}\n {!isLast ? ',' : ''}\n </div>\n );\n}\n```",
"```jsx\nlet data = {\n lorem: {\n ipsum: 'dolor sit',\n amet: {\n consectetur: 'adipiscing',\n elit: [\n 'duis',\n 'vitae',\n {\n semper: 'orci'\n },\n {\n est: 'sed ornare'\n },\n 'etiam',\n ['laoreet', 'tincidunt'],\n ['vestibulum', 'ante']\n ]\n },\n ipsum: 'primis'\n }\n};\nReactDOM.render(<TreeView data={data} name=\"data\" />, document.getElementById('root'));\n```"
],
"expertise": 2,
"tags": [
"object",
"visual",
"state",
"recursion"
],
"notes": []
}
]

8
gatsby-browser.js Normal file
View File

@ -0,0 +1,8 @@
/**
* Implement Gatsby's Browser APIs in this file.
*
* See: https://www.gatsbyjs.org/docs/browser-apis/
*/
// You can delete this file if you're not using it
export { default as wrapRootElement } from './src/docs/state/ReduxWrapper';

82
gatsby-config.js Normal file
View File

@ -0,0 +1,82 @@
const config = require('./config');
module.exports = {
siteMetadata: {
title: `${config.name}`,
description: `${config.description}`,
author: `@30-seconds`,
},
plugins: [
`gatsby-plugin-transition-link`,
{
resolve: `gatsby-source-filesystem`,
options: {
name: `snippets`,
path: `${__dirname}/${config.snippetPath}`,
},
},
{
resolve: `gatsby-source-filesystem`,
options: {
name: `snippet_data`,
path: `${__dirname}/${config.snippetDataPath}`,
},
},
{
resolve: `gatsby-source-filesystem`,
options: {
name: `assets`,
path: `${__dirname}/${config.assetPath}`,
},
},
{
resolve: `gatsby-plugin-page-creator`,
options: {
path: `${__dirname}/${config.pagePath}`,
},
},
{
resolve: `gatsby-transformer-remark`,
options: {
plugins: [
{
resolve: `gatsby-remark-images`,
options: {
maxWidth: 590,
},
},
`gatsby-remark-prismjs`,
`gatsby-remark-copy-linked-files`,
],
},
},
`gatsby-plugin-sass`,
`gatsby-transformer-json`,
`gatsby-transformer-sharp`,
`gatsby-plugin-sharp`,
{
resolve: `gatsby-plugin-google-analytics`,
options: {
trackingId: `UA-117141635-1`,
anonymize: true, // Always set this to true, try to comply with GDPR out of the box
respectDNT: true, // Always set to true, be respectful of people who ask not to be tracked
cookieExpires: 0, // Always set to 0, minimum tracking for your users
},
},
{
resolve: `gatsby-plugin-manifest`,
options: {
name: `${config.name}`,
short_name: `${config.shortName}`,
start_url: `/`,
background_color: `#1e253d`,
theme_color: `#1e253d`,
display: `standalone`,
icon: `assets/30s-icon.png`, // This path is relative to the root of the site.
},
},
`gatsby-plugin-offline`,
`gatsby-plugin-react-helmet`,
`gatsby-plugin-netlify`,
],
};

93
gatsby-node.js Normal file
View File

@ -0,0 +1,93 @@
const path = require(`path`);
const { createFilePath } = require(`gatsby-source-filesystem`);
const toKebabCase = str =>
str &&
str
.match(/[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g)
.map(x => x.toLowerCase())
.join('-');
exports.createPages = ({ graphql, actions }) => {
const { createPage } = actions;
const snippetPage = path.resolve(`./src/docs/templates/SnippetPage.js`);
const tagPage = path.resolve(`./src/docs/templates/TagPage.js`);
return graphql(
`
{
allMarkdownRemark(
sort: { fields: [frontmatter___title], order: ASC }
limit: 1000
) {
edges {
node {
fields {
slug
}
frontmatter {
tags
}
fileAbsolutePath
}
}
}
}
`,
).then(result => {
if (result.errors) {
throw result.errors;
}
// Create individual snippet pages.
const snippets = result.data.allMarkdownRemark.edges;
snippets.forEach((post, index) => {
if (post.node.fileAbsolutePath.indexOf('README') !== -1)
return;
createPage({
path: `/snippet${post.node.fields.slug}`,
component: snippetPage,
context: {
slug: post.node.fields.slug,
},
});
});
// Create tag pages.
const tags = snippets.reduce((acc, post) => {
if (!post.node.frontmatter || !post.node.frontmatter.tags) return acc;
const primaryTag = post.node.frontmatter.tags.split(',')[0];
if (!acc.includes(primaryTag)) acc.push(primaryTag);
return acc;
}, []);
tags.forEach(tag => {
const tagPath = `/tag/${toKebabCase(tag)}/`;
const tagRegex = `/^\\s*${tag}/`;
createPage({
path: tagPath,
component: tagPage,
context: {
tag,
tagRegex,
},
});
});
return null;
});
};
exports.onCreateNode = ({ node, actions, getNode }) => {
const { createNodeField } = actions;
if (node.internal.type === `MarkdownRemark`) {
const value = createFilePath({ node, getNode });
createNodeField({
name: `slug`,
node,
value,
});
}
};

8
gatsby-ssr.js Normal file
View File

@ -0,0 +1,8 @@
/**
* Implement Gatsby's SSR (Server Side Rendering) APIs in this file.
*
* See: https://www.gatsbyjs.org/docs/ssr-apis/
*/
// You can delete this file if you're not using it
export { default as wrapRootElement } from './src/docs/state/ReduxWrapper';

BIN
logo.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 35 KiB

6
netlify.toml Normal file
View File

@ -0,0 +1,6 @@
[build]
publish = "public"
command = "npm run webber"
[build.environment]
YARN_VERSION = "1.9.4"
YARN_FLAGS = "--no-ignore-optional"

19499
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -6,15 +6,53 @@
"url": "git+https://github.com/30-seconds/30-seconds-of-react.git"
},
"scripts": {
"extractor": "node ./scripts/extract.js",
"builder": "node ./scripts/build.js",
"webber": "gatsby build",
"webber:dev": "gatsby develop",
"webber:serve": "gatsby serve",
"extractor": "node ./scripts/extract.js",
"linter": "prettier **/*.{js,json,css,md} --write"
},
"devDependencies": {
"chalk": "^2.4.2",
"fs-extra": "^7.0.1",
"@babel/plugin-proposal-class-properties": "^7.5.0",
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/preset-env": "^7.5.4",
"@babel/preset-react": "^7.0.0",
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.2",
"front-matter": "^3.0.2",
"fs-extra": "^8.1.0",
"gatsby": "^2.12.0",
"gatsby-image": "^2.2.6",
"gatsby-plugin-google-analytics": "^2.1.6",
"gatsby-plugin-manifest": "^2.2.3",
"gatsby-plugin-netlify": "^2.1.3",
"gatsby-plugin-offline": "^2.2.4",
"gatsby-plugin-page-creator": "^2.1.5",
"gatsby-plugin-react-helmet": "^3.1.2",
"gatsby-plugin-sass": "^2.1.3",
"gatsby-plugin-sharp": "^2.2.7",
"gatsby-plugin-transition-link": "^1.12.4",
"gatsby-remark-copy-linked-files": "^2.1.3",
"gatsby-remark-images": "^3.1.6",
"gatsby-remark-prismjs": "^3.3.3",
"gatsby-source-filesystem": "^2.1.5",
"gatsby-transformer-json": "^2.2.2",
"gatsby-transformer-remark": "^2.6.6",
"gatsby-transformer-sharp": "^2.2.3",
"gsap": "^2.1.3",
"kleur": "^3.0.3",
"markdown-builder": "^0.9.0",
"prettier": "^1.14.3"
"node-sass": "^4.12.0",
"prettier": "^1.18.2",
"prismjs": "^1.16.0",
"prop-types": "^15.7.2",
"react": "^16.8.6",
"react-copy-to-clipboard": "^5.0.1",
"react-css-transition-replace": "^3.0.3",
"react-dom": "^16.8.6",
"react-helmet": "^5.2.1",
"react-redux": "^7.1.0",
"redux": "^4.0.4"
},
"prettier": {
"singleQuote": true,

View File

@ -1,97 +1,118 @@
/*
This is the builder script that generates the README file.
Run using `npm run builder`.
*/
// Load modules
const fs = require('fs-extra');
const path = require('path');
const chalk = require('chalk');
const util = require('./util.js');
const { green, red } = require('kleur');
const util = require('./util');
const markdown = require('markdown-builder');
const snippets = require('../data/snippet_data.json');
const { headers, misc, lists } = markdown;
const TAG_NAMES = [...new Set(snippets.reduce((acc, v) => [...acc, v.tags[0]], []))].sort((a, b) =>
a.localeCompare(b)
);
console.log(TAG_NAMES);
const config = require('../config');
const STATIC_PARTS_PATH = './static-parts';
// Paths (relative to package.json)
const SNIPPETS_PATH = `./${config.snippetPath}`;
const STATIC_PARTS_PATH = `./${config.staticPartsPath}`;
let startPart = '';
let endPart = '';
let output = '';
// Terminate if parent commit is a Travis build
if (util.isTravisCI() && /^Travis build: \d+/g.test(process.env['TRAVIS_COMMIT_MESSAGE'])) {
console.log(`${green('NOBUILD')} README build terminated, parent commit is a Travis build!`);
process.exit(0);
}
const detailsTOC = (title, snippetsArray) =>
`\n${misc
.collapsible(
title,
lists
.ul(snippetsArray, snippet =>
misc.link(
snippet.title
.replace('\n', '')
.split('```')[0]
.trim(),
misc.anchor(
snippet.title
.replace('\n', '')
.split('```')[0]
.trim()
)
)
)
.trim()
)
.trim()}\n\n`;
// Setup everything
let snippets = {},
snippetsArray = [],
startPart = '',
endPart = '',
output = '';
const EMOJIS = {};
console.time('Builder');
// Synchronously read all snippets from snippets folder and sort them as necessary (case-insensitive)
snippets = util.readSnippets(SNIPPETS_PATH);
snippetsArray = Object.keys(snippets).reduce((acc, key) => {
acc.push(snippets[key]);
return acc;
}, []);
// Load static parts for the README file
try {
startPart = fs.readFileSync(path.join(STATIC_PARTS_PATH, 'README-start.md'), 'utf8');
endPart = fs.readFileSync(path.join(STATIC_PARTS_PATH, 'README-end.md'), 'utf8');
} catch (err) {
console.log(`${chalk.red('ERROR!')} During static part loading: ${err}`);
console.log(`${red('ERROR!')} During static part loading: ${err}`);
process.exit(1);
}
// Create the output for the README file
try {
// add static part for start
const tags = util.prepTaggedData(
Object.keys(snippets).reduce((acc, key) => {
acc[key] = snippets[key].attributes.tags;
return acc;
}, {})
);
output += `${startPart}\n`;
const snippetsInTag = {};
// Loop over tags and snippets to create the table of contents
for (const tag of tags) {
const capitalizedTag = util.capitalize(tag, true);
const taggedSnippets = snippetsArray.filter(snippet => snippet.attributes.tags[0] === tag);
output += headers.h3((EMOJIS[tag] || '') + ' ' + capitalizedTag).trim();
TAG_NAMES.forEach(tag => (snippetsInTag[tag] = snippets.filter(v => v.tags[0] == tag)));
// write Table of Contents
TAG_NAMES.forEach(tag => {
const taggedSnippets = snippetsInTag[tag];
output += headers.h3(util.capitalize(tag));
output += detailsTOC('View contents', taggedSnippets);
});
// delimeter after TOC
output += misc.hr();
// write actual snippets
TAG_NAMES.forEach(tag => {
output += headers.h2(util.capitalize(tag));
const taggedSnippets = snippetsInTag[tag];
taggedSnippets.forEach(snippet => {
output += headers.h3(snippet.title).trim();
output += `\n\n${snippet.text}${snippet.codeBlocks.slice(0, -1).join('\n\n')}`;
if (snippet.notes && snippet.notes.length) {
output += headers.h4('Notes');
output += `\n${snippet.notes}`;
output +=
misc.collapsible(
'View contents',
lists.ul(taggedSnippets, snippet =>
misc.link(
`\`${snippet.title}\``,
`${misc.anchor(snippet.title)}${
snippet.attributes.tags.includes('advanced') ? '-' : ''
}`
)
)
) + '\n';
}
output += misc.collapsible('Examples', snippet.codeBlocks.slice(-1));
output += `\n<br>${misc.link('⬆ Back to top', misc.anchor('Table of Contents'))}\n\n`;
});
});
// add static part for end
for (const tag of tags) {
const capitalizedTag = util.capitalize(tag, true);
const taggedSnippets = snippetsArray.filter(snippet => snippet.attributes.tags[0] === tag);
output += misc.hr() + headers.h2((EMOJIS[tag] || '') + ' ' + capitalizedTag) + '\n';
for (let snippet of taggedSnippets) {
if (snippet.attributes.tags.includes('advanced'))
output += headers.h3(snippet.title + ' ' + misc.image('advanced', '/advanced.svg')) + '\n';
else output += headers.h3(snippet.title) + '\n';
output += snippet.attributes.text;
if (snippet.attributes.codeBlocks.style !== '')
output += `\`\`\`${config.optionalLanguage}\n${snippet.attributes.codeBlocks.style}\n\`\`\`\n\n`;
output += `\`\`\`${config.language}\n${snippet.attributes.codeBlocks.code}\n\`\`\``;
output += misc.collapsible(
'Examples',
`\`\`\`${config.language}\n${snippet.attributes.codeBlocks.example}\n\`\`\``
);
output += '\n<br>' + misc.link('⬆ Back to top', misc.anchor('Contents')) + '\n';
}
}
// Add the ending static part
output += `\n${endPart}\n`;
// Write to the README file
fs.writeFileSync('README.md', output);
} catch (err) {
console.log(`${chalk.red('ERROR!')} During README generation: ${err}`);
console.log(`${red('ERROR!')} During README generation: ${err}`);
process.exit(1);
}
console.log(`${chalk.green('SUCCESS!')} README file generated!`);
console.log(`${green('SUCCESS!')} README file generated!`);
console.timeEnd('Builder');

View File

@ -1,42 +1,68 @@
/*
This is the extractor script that generates the snippets.json file.
Run using `npm run extractor`.
*/
// Load modules
const fs = require('fs-extra');
const path = require('path');
const chalk = require('chalk');
const {
attempt,
readSnippets,
getCodeBlocks,
getSection,
getTitle,
getTextualContent
} = require('./util');
const { green } = require('kleur');
const util = require('./util');
const config = require('../config');
// Paths (relative to package.json)
const SNIPPETS_PATH = `./${config.snippetPath}`;
const OUTPUT_PATH = `./${config.snippetDataPath}`;
// Check if running on Travis, only build for cron jobs and custom builds
if (
util.isTravisCI() &&
process.env['TRAVIS_EVENT_TYPE'] !== 'cron' &&
process.env['TRAVIS_EVENT_TYPE'] !== 'api'
) {
console.log(`${green('NOBUILD')} snippet extraction terminated, not a cron or api build!`);
process.exit(0);
}
// Setup everything
let snippets = {},
snippetsArray = [];
console.time('Extractor');
attempt('snippet_data.json generation', () => {
const output = Object.entries(readSnippets()).map(([name, contents]) => {
const title = getTitle(contents);
const text = getTextualContent(contents);
const codeBlocks = getCodeBlocks(contents);
const notes = getSection('#### Notes', contents, false)
.split('\n')
.map(v => v.replace(/[*-] /g, ''))
.filter(v => v.trim() !== '');
// Synchronously read all snippets from snippets folder and sort them as necessary (case-insensitive)
snippets = util.readSnippets(SNIPPETS_PATH);
snippetsArray = Object.keys(snippets).reduce((acc, key) => {
acc.push(snippets[key]);
return acc;
}, []);
return {
name,
title,
text,
codeBlocks,
expertise: parseInt((contents.match(/<!--\s*expertise:\s*\(*(.+)\)*/) || [])[1], 10),
tags: (contents.match(/<!--\s*tags:\s*\(*(.+)\)*\s*-->/) || [])[1]
.split(',')
.map(v => v.trim()),
notes
const completeData = {
data: [...snippetsArray],
meta: {
specification: 'http://jsonapi.org/format/',
type: 'snippetArray'
}
};
});
fs.writeFileSync('./data/snippet_data.json', JSON.stringify(output, null, 2));
});
console.log(`${chalk.green('SUCCESS!')} snippet_data.json file generated!`);
let listingData = {
data: completeData.data.map(v => ({
id: v.id,
type: 'snippetListing',
title: v.title,
attributes: {
text: v.attributes.text,
tags: v.attributes.tags
},
meta: {
hash: v.meta.hash
}
})),
meta: {
specification: 'http://jsonapi.org/format/',
type: 'snippetListingArray'
}
};
// Write files
fs.writeFileSync(path.join(OUTPUT_PATH, 'snippets.json'), JSON.stringify(completeData, null, 2));
fs.writeFileSync(path.join(OUTPUT_PATH, 'snippetList.json'), JSON.stringify(listingData, null, 2));
// Display messages and time
console.log(`${green('SUCCESS!')} snippets.json and snippetList.json files generated!`);
console.timeEnd('Extractor');

View File

@ -1,91 +0,0 @@
const fs = require('fs-extra');
const path = require('path');
const chalk = require('chalk');
const SNIPPETS_PATH = './snippets';
const attempt = (task, cb) => {
try {
return cb();
} catch (e) {
console.log(`${chalk.red('ERROR!')} During ${task}: ${e}`);
process.exit(1);
return null;
}
};
const capitalize = ([first, ...rest], lowerRest = false) =>
first.toUpperCase() + (lowerRest ? rest.join('').toLowerCase() : rest.join(''));
const readSnippets = () =>
attempt('read snippets', () =>
fs
.readdirSync(SNIPPETS_PATH)
.sort((a, b) => (a.toLowerCase() < b.toLowerCase() ? -1 : 1))
.reduce((acc, name) => {
acc[name] = fs.readFileSync(path.join(SNIPPETS_PATH, name), 'utf8').replace(/\r\n/g, '\n');
return acc;
}, {})
);
const getCodeBlocks = str => {
const regex = /```[.\S\s]*?```/g;
const results = [];
let m = null;
while ((m = regex.exec(str)) !== null) {
if (m.index === regex.lastIndex) {
regex.lastIndex += 1;
}
m.forEach(match => results.push(match));
}
return results;
};
const getSection = (searchString, contents, includeSubsections = true) => {
const indexOfSearch = contents.indexOf(searchString);
if (indexOfSearch < 0) return '';
let endSearch = '\\n#';
if (includeSubsections) {
let i;
for (i = 0; searchString[i] === '#' && i < searchString.length; i++);
if (i > 0) {
endSearch += `{${i - 1},${i}}[^#]`;
}
}
const endRegex = new RegExp(endSearch);
const sliceStart = indexOfSearch + searchString.length + 1;
const endIndex = contents.slice(sliceStart).search(endRegex);
const sliceEnd = endIndex === -1 ? undefined : endIndex + sliceStart;
return contents.slice(sliceStart, sliceEnd).trim();
};
const getTextualContent = str => {
const regex = /###.*\n*([\s\S]*?)```/g;
const results = [];
let m = null;
while ((m = regex.exec(str)) !== null) {
if (m.index === regex.lastIndex) regex.lastIndex += 1;
m.forEach((match, groupIndex) => {
results.push(match);
});
}
return results[1];
};
const getTitle = contents => contents.split('\n')[0].replace(/^#+\s+/g, '');
module.exports = {
attempt,
readSnippets,
SNIPPETS_PATH,
capitalize,
getTextualContent,
getCodeBlocks,
getSection,
getTitle
};

View File

@ -0,0 +1,11 @@
// Checks if current environment is Travis CI, Cron builds, API builds
const isTravisCI = () => 'TRAVIS' in process.env && 'CI' in process.env;
const isTravisCronOrAPI = () =>
process.env['TRAVIS_EVENT_TYPE'] === 'cron' || process.env['TRAVIS_EVENT_TYPE'] === 'api';
const isNotTravisCronOrAPI = () => !isTravisCronOrAPI();
module.exports = {
isTravisCI,
isTravisCronOrAPI,
isNotTravisCronOrAPI
};

54
scripts/util/helpers.js Normal file
View File

@ -0,0 +1,54 @@
const config = require('../../config');
const getMarkDownAnchor = paragraphTitle =>
paragraphTitle
.trim()
.toLowerCase()
.replace(/[^\w\- ]+/g, '')
.replace(/\s/g, '-')
.replace(/\-+$/, '');
// Creates an object from pairs
const objectFromPairs = arr => arr.reduce((a, v) => ((a[v[0]] = v[1]), a), {});
// Optimizes nodes in an HTML document
const optimizeNodes = (data, regexp, replacer) => {
let count = 0;
let output = data;
do {
output = output.replace(regexp, replacer);
count = 0;
while (regexp.exec(output) !== null) ++count;
} while (count > 0);
return output;
};
// Capitalizes the first letter of a string
const capitalize = (str, lowerRest = false) =>
str.slice(0, 1).toUpperCase() + (lowerRest ? str.slice(1).toLowerCase() : str.slice(1));
const prepTaggedData = tagDbData =>
[...new Set(Object.entries(tagDbData).map(t => t[1][0]))]
.filter(v => v)
.sort((a, b) =>
capitalize(a, true) === 'Uncategorized'
? 1
: capitalize(b, true) === 'Uncategorized'
? -1
: a.localeCompare(b)
);
const makeExamples = data => {
data =
data.slice(0, data.lastIndexOf(`\`\`\`${config.language}`)).trim() +
misc.collapsible(
'Examples',
data.slice(data.lastIndexOf(`\`\`\`${config.language}`), data.lastIndexOf('```')) +
data.slice(data.lastIndexOf('```'))
);
return `${data}\n<br>${misc.link('⬆ Back to top', misc.anchor('Contents'))}\n\n`;
};
module.exports = {
getMarkDownAnchor,
objectFromPairs,
optimizeNodes,
capitalize,
prepTaggedData,
makeExamples
};

33
scripts/util/index.js Normal file
View File

@ -0,0 +1,33 @@
const { isTravisCI, isTravisCronOrAPI, isNotTravisCronOrAPI } = require('./environmentCheck');
const {
getMarkDownAnchor,
objectFromPairs,
optimizeNodes,
capitalize,
prepTaggedData,
makeExamples
} = require('./helpers');
const {
getFilesInDir,
hashData,
getCodeBlocks,
getTextualContent,
readSnippets
} = require('./snippetParser');
module.exports = {
isTravisCI,
isTravisCronOrAPI,
isNotTravisCronOrAPI,
getMarkDownAnchor,
objectFromPairs,
optimizeNodes,
capitalize,
prepTaggedData,
makeExamples,
getFilesInDir,
hashData,
getCodeBlocks,
getTextualContent,
readSnippets
};

View File

@ -0,0 +1,123 @@
const fs = require('fs-extra'),
path = require('path'),
{ red } = require('kleur'),
crypto = require('crypto'),
frontmatter = require('front-matter');
const config = require('../../config');
// Reade all files in a directory
const getFilesInDir = (directoryPath, withPath, exclude = null) => {
try {
let directoryFilenames = fs.readdirSync(directoryPath);
directoryFilenames.sort((a, b) => {
a = a.toLowerCase();
b = b.toLowerCase();
if (a < b) return -1;
if (a > b) return 1;
return 0;
});
if (withPath) {
// a hacky way to do conditional array.map
return directoryFilenames.reduce((fileNames, fileName) => {
if (exclude == null || !exclude.some(toExclude => fileName === toExclude))
fileNames.push(`${directoryPath}/${fileName}`);
return fileNames;
}, []);
}
return directoryFilenames.filter(v => v !== 'README.md');
} catch (err) {
console.log(`${red('ERROR!')} During snippet loading: ${err}`);
process.exit(1);
}
};
// Creates a hash for a value using the SHA-256 algorithm.
const hashData = val =>
crypto
.createHash('sha256')
.update(val)
.digest('hex');
// Gets the code blocks for a snippet file.
const getCodeBlocks = str => {
const regex = /```[.\S\s]*?```/g;
let results = [];
let m = null;
while ((m = regex.exec(str)) !== null) {
if (m.index === regex.lastIndex) regex.lastIndex += 1;
m.forEach((match, groupIndex) => {
results.push(match);
});
}
const replacer = new RegExp(`\`\`\`${config.language}([\\s\\S]*?)\`\`\``, 'g');
const optionalReplacer = new RegExp(`\`\`\`${config.optionalLanguage}([\\s\\S]*?)\`\`\``, 'g');
results = results.map(v =>
v
.replace(replacer, '$1')
.replace(optionalReplacer, '$1')
.trim()
);
if (results.length > 2)
return {
style: results[0],
code: results[1],
example: results[2]
};
return {
style: '',
code: results[0],
example: results[1]
};
};
// Gets the textual content for a snippet file.
const getTextualContent = str => {
const regex = /([\s\S]*?)```/g;
const results = [];
let m = null;
while ((m = regex.exec(str)) !== null) {
if (m.index === regex.lastIndex) regex.lastIndex += 1;
m.forEach((match, groupIndex) => {
results.push(match);
});
}
return results[1].replace(/\r\n/g, '\n');
};
// Synchronously read all snippets and sort them as necessary (case-insensitive)
const readSnippets = snippetsPath => {
const snippetFilenames = getFilesInDir(snippetsPath, false);
let snippets = {};
try {
for (let snippet of snippetFilenames) {
let data = frontmatter(fs.readFileSync(path.join(snippetsPath, snippet), 'utf8'));
snippets[snippet] = {
id: snippet.slice(0, -3),
title: data.attributes.title,
type: 'snippet',
attributes: {
fileName: snippet,
text: getTextualContent(data.body),
codeBlocks: getCodeBlocks(data.body),
tags: data.attributes.tags.split(',').map(t => t.trim())
},
meta: {
hash: hashData(data.body)
}
};
}
} catch (err) {
console.log(`${red('ERROR!')} During snippet loading: ${err}`);
process.exit(1);
}
return snippets;
};
module.exports = {
getFilesInDir,
hashData,
getCodeBlocks,
getTextualContent,
readSnippets
};

View File

@ -1,10 +1,15 @@
---
title: ComponentName
tags: visual,state,effect,intermediate
---
### ComponentName
Explain briefly what the snippet does.
* Explain briefly how the snippet works.
* Use bullet points for your snippet's explanation.
* Try to explain everything briefly but clearly.
- Explain briefly how the snippet works.
- Use bullet points for your snippet's explanation.
- Try to explain everything briefly but clearly.
```jsx
function ComponentName(props) {
@ -19,21 +24,3 @@ function ComponentName(props) {
```jsx
ReactDOM.render(<ComponentName />, document.getElementById('root'));
```
<!-- OPTIONAL -->
#### Notes:
- Things to remember when using this
- Other options that might be less appealing or have lower compatibility
- Common mistakes and issues
<!-- tags: (separate each by a comma) -->
<!-- expertise: (0,1,2,3) -->
<!-- Expertise levels (pick only one, no parentheses):
0: beginner
1: intermediate
2: advanced
3: expert
-->

View File

@ -0,0 +1,521 @@
{
"data": [
{
"id": "Accordion",
"type": "snippetListing",
"title": "Accordion",
"attributes": {
"text": "Renders an accordion menu with multiple collapsible content components.\n\n- Define an `AccordionItem` component, pass it to the `Accordion` and remove unnecessary nodes expect for `AccordionItem` by identifying the function's name in `props.children`.\n- Each `AccordionItem` component renders a `<button>` that is used to update the `Accordion` via the `props.handleClick` callback and the content of the component, passed down via `props.children`, while its appearance is determined by `props.isCollapsed` and based on `style`.\n- In the `Accordion` component, use the `React.useState()` hook to initialize the value of the `bindIndex` state variable to `props.defaultIndex`.\n- Use `Array.prototype.map` on the collected nodes to render the individual collapsiple elements.\n- Define `changeItem`, which will be executed when clicking an `AccordionItem`'s `<button>`.\n `changeItem` executes the passed callback, `onItemClick` and updates `bindIndex` based on the clicked element.\n\n",
"tags": [
"visual",
"children",
"state",
"advanced"
]
},
"meta": {
"hash": "0300c924ea29110f2982ae5564a63ff01519a5d0ffc8ae6dc8d175363fb77534"
}
},
{
"id": "AutoLink",
"type": "snippetListing",
"title": "AutoLink",
"attributes": {
"text": "Renders a string as plaintext, with URLs converted to appropriate `<a>` elements.\n\n- Use `String.prototype.split()` and `String.prototype.match()` with a regular expression to find URLs in a string.\n- Return a `<React.Fragment>` with matched URLs rendered as `<a>` elements, dealing with missing protocol prefixes if necessary, and the rest of the string rendered as plaintext.\n\n",
"tags": [
"visual",
"string",
"fragment",
"regexp",
"advanced"
]
},
"meta": {
"hash": "8a4373d9d191111ec6644ca315399c78d48c4d1a8c29124e48a88741ef826bea"
}
},
{
"id": "Carousel",
"type": "snippetListing",
"title": "Carousel",
"attributes": {
"text": "Renders a carousel component.\n\n- Use the `React.setState()` hook to create the `active` state variable and give it a value of `0` (index of the first item).\n- Use an object, `style`, to hold the styles for the individual components.\n- Use the `React.setEffect()` hook to update the value of `active` to the index of the next item, using `setTimeout`.\n- Destructure `props`, compute if visibility style should be set to `visible` or not for each carousel item while mapping over and applying the combined style to the carousel item component accordingly.\n- Render the carousel items using `React.cloneElement()` and pass down rest `props` along with the computed styles.\n\n",
"tags": [
"visual",
"children",
"state",
"effect",
"intermediate"
]
},
"meta": {
"hash": "24c0b339ea3131a8bde8081b1f44caac1d5014ecc1ac6d25ebf4b14d053a83a8"
}
},
{
"id": "Collapse",
"type": "snippetListing",
"title": "Collapse",
"attributes": {
"text": "Renders a component with collapsible content.\n\n- Use the `React.setState()` hook to create the `isCollapsed` state variable with an initial value of `props.collapsed`.\n- Use an object, `style`, to hold the styles for individual components and their states.\n- Use a `<div>` to wrap both the `<button>` that alters the component's `isCollapsed` state and the content of the component, passed down via `props.children`.\n- Determine the appearance of the content, based on `isCollapsed` and apply the appropriate CSS rules from the `style` object.\n- Finally, update the value of the `aria-expanded` attribute based on `isCollapsed` to make the component accessible.\n\n",
"tags": [
"visual",
"children",
"state",
"intermediate"
]
},
"meta": {
"hash": "bb14a75971ab3a395e7a7e4458f636b9309f3e9a376494fe10b716be256bd9d2"
}
},
{
"id": "ControlledInput",
"type": "snippetListing",
"title": "ControlledInput",
"attributes": {
"text": "Renders an `<input>` element with internal state, that uses a callback function to pass its value to the parent component.\n\n- Use object destructuring to set defaults for certain attributes of the `<input>` element.\n- Use the `React.setState()` hook to create the `value` state variable and give it a value of equal to the `defaultValue` prop.\n- Use the `React.useEffect()` hook with a second parameter set to the `value` state variable to call the `callback` function every time `value` is updated.\n- Render an `<input>` element with the appropriate attributes and use the the `onChange` event to upda the `value` state variable.\n\n",
"tags": [
"input",
"state",
"effect",
"intermediate"
]
},
"meta": {
"hash": "a9c8332455bad3c81dd1dbaf7361b98406ed11eaa35e9be8e72e88d8139eb2d6"
}
},
{
"id": "CountDown",
"type": "snippetListing",
"title": "CountDown",
"attributes": {
"text": "Renders a countdown timer that prints a message when it reaches zero.\n\n- Use object destructuring to set defaults for the `hours`, `minutes` and `seconds` props.\n- Use the `React.useState()` hook to create the `time`, `paused` and `over` state variables and set their values to the values of the passed props, `false` and `false` respectively.\n- Create a method `tick`, that updates the value of `time` based on the current value (i.e. decreasing the time by one second).\n- If `paused` or `over` is `true`, `tick` will return immediately.\n- Create a method `reset`, that resets all state variables to their initial states.\n- Use the the `React.useEffect()` hook to call the `tick` method every second via the use of `setInterval()` and use `clearInterval()` to cleanup when the component is unmounted.\n- Use a `<div>` to wrap a `<p>` element with the textual representation of the components `time` state variable, as well as two `<button>` elements that will pause/unpause and restart the timer respectively.\n- If `over` is `true`, the timer will display a message instead of the value of `time`.\n\n",
"tags": [
"visual",
"state",
"advanced"
]
},
"meta": {
"hash": "b5ac7580e6a96df21478030c6c2b30fa2e56df2573ab66e2405bdbbdce60aa61"
}
},
{
"id": "DataList",
"type": "snippetListing",
"title": "DataList",
"attributes": {
"text": "Renders a list of elements from an array of primitives.\n\n- Use the value of the `isOrdered` prop to conditionally render a `<ol>` or `<ul>` list.\n- Use `Array.prototype.map` to render every item in `data` as a `<li>` element, give it a `key` produced from the concatenation of the its index and value.\n- Omit the `isOrdered` prop to render a `<ul>` list by default.\n\n",
"tags": [
"array",
"beginner"
]
},
"meta": {
"hash": "38eb402c926fbc919a04b55fe8d6fb2fbdd5ba54341783852f909f9181e745da"
}
},
{
"id": "DataTable",
"type": "snippetListing",
"title": "DataTable",
"attributes": {
"text": "Renders a table with rows dynamically created from an array of primitives.\n\n- Render a `<table>` element with two columns (`ID` and `Value`).\n- Use `Array.prototype.map` to render every item in `data` as a `<tr>` element, consisting of its index and value, give it a `key` produced from the concatenation of the two.\n\n",
"tags": [
"array",
"beginner"
]
},
"meta": {
"hash": "c4aba21d546f519469e8a65101e9cd44b25dae028b48a223b8cb6c19f83b60d4"
}
},
{
"id": "FileDrop",
"type": "snippetListing",
"title": "FileDrop",
"attributes": {
"text": "Renders a file drag and drop component for a single file.\n\n- Create a ref called `dropRef` for this component.\n- Use the `React.useState()` hook to create the `drag` and `filename` variables, initialized to `false` and `''` respectively.\n The variables `dragCounter` and `drag` are used to determine if a file is being dragged, while `filename` is used to store the dropped file's name.\n- Create the `handleDrag`, `handleDragIn`, `handleDragOut` and `handleDrop` methods to handle drag and drop functionality, bind them to the component's context.\n- Each of the methods will handle a specific event, the listeners for which are created and removed in the `React.useEffect()` hook and its attached `cleanup()` method.\n- `handleDrag` prevents the browser from opening the dragged file, `handleDragIn` and `handleDragOut` handle the dragged file entering and exiting the component, while `handleDrop` handles the file being dropped and passes it to `props.handleDrop`.\n- Return an appropriately styled `<div>` and use `drag` and `filename` to determine its contents and style.\n- Finally, bind the `ref` of the created `<div>` to `dropRef`.\n\n",
"tags": [
"visual",
"input",
"state",
"effect",
"event",
"intermediate"
]
},
"meta": {
"hash": "2e35dd3fa6b4808f03aff6b542b293f8be0fd9aa1cbb7bd85a2729f808cf8888"
}
},
{
"id": "LimitedTextarea",
"type": "snippetListing",
"title": "LimitedTextarea",
"attributes": {
"text": "Renders a textarea component with a character limit.\n\n- Use the `React.useState()` hook to create the `content` state variable and set its value to `value`.\n Create a method `setFormattedContent`, which trims the content of the input if it's longer than `limit`.\n- Use the `React.useEffect()` hook to call the `setFormattedContent` method on the value of the `content` state variable.\n- Use a`<div>` to wrap both the`<textarea>` and the `<p>` element that displays the character count and bind the `onChange` event of the `<textarea>` to call `setFormattedContent` with the value of `event.target.value`.\n\n",
"tags": [
"input",
"state",
"effect",
"event",
"beginner"
]
},
"meta": {
"hash": "eabc464d70da892c84d00925de7c8c22ea3f54d5337e7b2efd6e4043fb447aa8"
}
},
{
"id": "LimitedWordTextarea",
"type": "snippetListing",
"title": "LimitedWordTextarea",
"attributes": {
"text": "Renders a textarea component with a word limit.\n\n- Use the `React.useState()` hook to create the `content` and `wordCount` state variables and set their values to `value` and `0` respectively.\n- Create a method `setFormattedContent`, which uses `String.prototype.split(' ')` to turn the input into an array of words and check if the result of applying `Array.prototype.filter(Boolean)` has a `length` longer than `limit`.\n- If the afforementioned `length` exceeds the `limit`, trim the input, otherwise return the raw input, updating `content` and `wordCount` accordingly in both cases.\n- Use the `React.useEffect()` hook to call the `setFormattedContent` method on the value of the `content` state variable.\n- Use a`<div>` to wrap both the`<textarea>` and the `<p>` element that displays the character count and bind the `onChange` event of the `<textarea>` to call `setFormattedContent` with the value of `event.target.value`.\n\n",
"tags": [
"input",
"state",
"effect",
"event",
"beginner"
]
},
"meta": {
"hash": "8c0f35b8cba144ddf8611fa48659ef9a83bd235a961466ccbf1ca5f43bd6b602"
}
},
{
"id": "Mailto",
"type": "snippetListing",
"title": "Mailto",
"attributes": {
"text": "Renders a link formatted to send an email.\n\n- Destructure the component's props, use `email`, `subject` and `body` to create a `<a>` element with an appropriate `href` attribute.\n- Render the link with `props.children` as its content.\n\n",
"tags": [
"visual",
"beginner"
]
},
"meta": {
"hash": "e22d307cb43e589e7bd1ec5caab16c1101aa764102f35cd7295876033b44578e"
}
},
{
"id": "MappedTable",
"type": "snippetListing",
"title": "MappedTable",
"attributes": {
"text": "Renders a table with rows dynamically created from an array of objects and a list of property names.\n\n- Use `Object.keys()`, `Array.prototype.filter()`, `Array.prototype.includes()` and `Array.prototype.reduce()` to produce a `filteredData` array, containing all objects with the keys specified in `propertyNames`.\n- Render a `<table>` element with a set of columns equal to the amount of values in `propertyNames`.\n- Use `Array.prototype.map` to render each value in the `propertyNames` array as a `<th>` element.\n- Use `Array.prototype.map` to render each object in the `filteredData` array as a `<tr>` element, containing a `<td>` for each key in the object.\n\n_This component does not work with nested objects and will break if there are nested objects inside any of the properties specified in `propertyNames`_\n\n",
"tags": [
"array",
"object",
"intermediate"
]
},
"meta": {
"hash": "9ff4e1580f53ab64c740aee2bfdc85f1cfc46f7cf2f39d6324ec4c2b7d6c7270"
}
},
{
"id": "Modal",
"type": "snippetListing",
"title": "Modal",
"attributes": {
"text": "Renders a Modal component, controllable through events.\nTo use the component, import `Modal` only once and then display it by passing a boolean value to the `isVisible` attribute.\n\n- Use object destructuring to set defaults for certain attributes of the modal component.\n- Define `keydownHandler`, a method which handles all keyboard events, which can be used according to your needs to dispatch actions (e.g. close the modal when <kbd>Esc</kbd> is pressed).\n- Use `React.useEffect()` hook to add or remove the `keydown` event listener, which calls `keydownHandler`.\n- Use the `isVisible` prop to determine if the modal should be shown or not.\n- Use CSS to style and position the modal component.\n\n",
"tags": [
"visual",
"effect",
"intermediate"
]
},
"meta": {
"hash": "07c392eff204d3fe475cba116d25ca84b48f7f37ae18f0adade7e65c98ec509d"
}
},
{
"id": "MultiselectCheckbox",
"type": "snippetListing",
"title": "MultiselectCheckbox",
"attributes": {
"text": "Renders a checkbox list that uses a callback function to pass its selected value/values to the parent component.\n\n- Use `React.setState()` to create a `data` state variable and set its initial value equal to the `options` prop.\n- Create a function `toggle` that is used to toggle the `checked` to update the `data` state variable and call the `onChange` callback passed via the component's props.\n- Render a `<ul>` element and use `Array.prototype.map()` to map the `data` state variable to individual `<li>` elements with `<input>` elements as their children.\n- Each `<input>` element has the `type='checkbox'` attribute and is marked as `readOnly`, as its click events are handled by the parent `<li>` element's `onClick` handler.\n\n",
"tags": [
"input",
"state",
"array",
"intermediate"
]
},
"meta": {
"hash": "ec2d246e3520208cf67146e9674f576b4d13fd31854892113396409edcb68db6"
}
},
{
"id": "PasswordRevealer",
"type": "snippetListing",
"title": "PasswordRevealer",
"attributes": {
"text": "Renders a password input field with a reveal button.\n\n- Use the `React.useState()` hook to create the `shown` state variable and set its value to `false`.\n- Use a`<div>` to wrap both the`<input>` and the `<button>` element that toggles the type of the input field between `\"text\"` and `\"password\"`.\n\n",
"tags": [
"input",
"state",
"beginner"
]
},
"meta": {
"hash": "7e019374ec668fe2bc3053bbcb3e8a2f3ac419ac0e316d3ee6e94f99a1fb53ee"
}
},
{
"id": "Select",
"type": "snippetListing",
"title": "Select",
"attributes": {
"text": "Renders a `<select>` element that uses a callback function to pass its value to the parent component.\n\n- Use object destructuring to set defaults for certain attributes of the `<select>` element.\n- Render a `<select>` element with the appropriate attributes and use the `callback` function in the `onChange` event to pass the value of the textarea to the parent.\n- Use destructuring on the `values` array to pass an array of `value` and `text` elements and the `selected` attribute to define the initial `value` of the `<select>` element.\n\n",
"tags": [
"input",
"beginner"
]
},
"meta": {
"hash": "b82ad131475042bd38682aba03964494680fc2dfe3a61b0a1478215f74e993de"
}
},
{
"id": "Slider",
"type": "snippetListing",
"title": "Slider",
"attributes": {
"text": "Renders a slider element that uses a callback function to pass its value to the parent component.\n\n- Use object destructuring to set defaults for certain attributes of the `<input>` element.\n- Render an `<input>` element of type `\"range\"` and the appropriate attributes, use the `callback` function in the `onChange` event to pass the value of the input to the parent.\n\n",
"tags": [
"input",
"beginner"
]
},
"meta": {
"hash": "db77adcc300072c41b4d096cd385f7c3616601fcace16d7c28032036a6eed7e7"
}
},
{
"id": "StarRating",
"type": "snippetListing",
"title": "StarRating",
"attributes": {
"text": "Renders a star rating component.\n\n- Define a component, called `Star` that will render each individual star with the appropriate appearance, based on the parent component's state.\n- In the `StarRating` component, use the `React.useState()` hook to define the `rating` and `selection` state variables with the initial values of `props.rating` (or `0` if invalid or not supplied) and `0`.\n- Create a method, `hoverOver`, that updates `selected` and `rating` according to the provided `event`.\n- Create a `<div>` to wrap the `<Star>` components, which are created using `Array.prototype.map` on an array of 5 elements, created using `Array.from`, and handle the `onMouseLeave` event to set `selection` to `0`, the `onClick` event to set the `rating` and the `onMouseOver` event to set `selection` to the `star-id` attribute of the `event.target` respectively.\n- Finally, pass the appropriate values to each `<Star>` component (`starId` and `marked`).\n\n",
"tags": [
"visual",
"children",
"input",
"state",
"intermediate"
]
},
"meta": {
"hash": "a057b14315a578a5c27d6534b965c8a567be6ffd7ba0b32882282e6e99076ebc"
}
},
{
"id": "Tabs",
"type": "snippetListing",
"title": "Tabs",
"attributes": {
"text": "Renders a tabbed menu and view component.\n\n- Define a `TabItem` component, pass it to the `Tab` and remove unnecessary nodes expect for `TabItem` by identifying the function's name in `props.children`.\n- Use the `React.useState()` hook to initialize the value of the `bindIndex` state variable to `props.defaultIndex`.\n- Use `Array.prototype.map` on the collected nodes to render the `tab-menu` and `tab-view`.\n- Define `changeTab`, which will be executed when clicking a `<button>` from the `tab-menu`.\n- `changeTab` executes the passed callback, `onTabClick` and updates `bindIndex`, which in turn causes a re-render, evaluating the `style` and `className` of the `tab-view` items and `tab-menu` buttons according to their `index`.\n\n",
"tags": [
"visual",
"state",
"children",
"intermediate"
]
},
"meta": {
"hash": "54a00cb6ac87079622e3806aa55816332e353eb88ba263a5059dd40942955ff7"
}
},
{
"id": "TextArea",
"type": "snippetListing",
"title": "TextArea",
"attributes": {
"text": "Renders a `<textarea>` element that uses a callback function to pass its value to the parent component.\n\n- Use object destructuring to set defaults for certain attributes of the `<textarea>` element.\n- Render a `<textarea>` element with the appropriate attributes and use the `callback` function in the `onChange` event to pass the value of the textarea to the parent.\n\n",
"tags": [
"input",
"beginner"
]
},
"meta": {
"hash": "4c0c3bcdb3a7a7f858d1e938e7a1926804ceca983f5eeae3edf4b28f36548fb4"
}
},
{
"id": "Ticker",
"type": "snippetListing",
"title": "Ticker",
"attributes": {
"text": "Renders a ticker component.\n\n- Use the `React.useState()` hook to initialize the `ticker` state variable to `0`.\n- Define two methods, `tick` and `reset`, that will periodically increment `timer` based on `interval` and reset `interval` respectively.\n- Return a `<div>` with two `<button>` elements, each of which calls `tick` and `reset` respectively.\n\n",
"tags": [
"visual",
"state",
"beginner"
]
},
"meta": {
"hash": "d64dece795561241f3b0d7b3fa1e87971994bda2fbe406c6ce740af866e3d9a7"
}
},
{
"id": "Toggle",
"type": "snippetListing",
"title": "Toggle",
"attributes": {
"text": "Renders a toggle component.\n\n- Use the `React.useState()` to initialize the `isToggleOn` state variable to `false`.\n- Use an object, `style`, to hold the styles for individual components and their states.\n- Return a `<button>` that alters the component's `isToggledOn` when its `onClick` event is fired and determine the appearance of the content based on `isToggleOn`, applying the appropriate CSS rules from the `style` object.\n\n",
"tags": [
"visual",
"state",
"beginner"
]
},
"meta": {
"hash": "c921c0adeca337f2cf804acf35e8ab231befdd451d4dcca09a73fecf6ad66836"
}
},
{
"id": "Tooltip",
"type": "snippetListing",
"title": "Tooltip",
"attributes": {
"text": "Renders a tooltip component.\n\n- Use the `React.useState()` hook to create the `show` variable and initialize it to `false`.\n- Return a `<div>` element that contains the `<div>` that will be the tooltip and the `children` passed to the component.\n- Handle the `onMouseEnter` and `onMouseLeave` methods, by altering the value of the `show` variable.\n\n",
"tags": [
"visual",
"state",
"children",
"beginner"
]
},
"meta": {
"hash": "db3b0f49f674b5a64ee1b9a9098aab360331901425faa382c5de5cb8cae33167"
}
},
{
"id": "TreeView",
"type": "snippetListing",
"title": "TreeView",
"attributes": {
"text": "Renders a tree view of a JSON object or array with collapsible content.\n\n- Use object destructuring to set defaults for certain props.\n- Use the value of the `toggled` prop to determine the initial state of the content (collapsed/expanded).\n- Use the `React.setState()` hook to create the `isToggled` state variable and give it the value of the `toggled` prop initially.\n- Return a `<div>` to wrap the contents of the component and the `<span>` element, used to alter the component's `isToggled` state.\n- Determine the appearance of the component, based on `isParentToggled`, `isToggled`, `name` and `Array.isArray()` on `data`.\n- For each child in `data`, determine if it is an object or array and recursively render a sub-tree.\n- Otherwise, render a `<p>` element with the appropriate style.\n\n",
"tags": [
"visual",
"object",
"state",
"recursion",
"advanced"
]
},
"meta": {
"hash": "305bd408515e5c575411f40b1bb24f75f6c776cd6387bb083818f2feb9175a70"
}
},
{
"id": "UncontrolledInput",
"type": "snippetListing",
"title": "UncontrolledInput",
"attributes": {
"text": "Renders an `<input>` element that uses a callback function to pass its value to the parent component.\n\n- Use object destructuring to set defaults for certain attributes of the `<input>` element.\n- Render an `<input>` element with the appropriate attributes and use the `callback` function in the `onChange` event to pass the value of the input to the parent.\n\n",
"tags": [
"input",
"beginner"
]
},
"meta": {
"hash": "5aff6673123594949dd0d1e72c8fbb586f402b2eb9fdcf8dd9a2b789b4089adb"
}
},
{
"id": "useClickInside",
"type": "snippetListing",
"title": "useClickInside",
"attributes": {
"text": "A hook that handles the event of clicking inside the wrapped component.\n\n- Create a custom hook that takes a `ref` and a `callback` to handle the `click` event.\n- Use the `React.useEffect()` hook to append and clean up the `click` event.\n- Use the `React.useRef()` hook to create a `ref` for your click component and pass it to the `useClickInside` hook.\n\n",
"tags": [
"hooks",
"effect",
"event",
"intermediate"
]
},
"meta": {
"hash": "f2e5c4b9e8c44a5fb9a1f8002cfbeb2d6090cfe97d4d15dcc575ce89611bb599"
}
},
{
"id": "useClickOutside",
"type": "snippetListing",
"title": "useClickOutside",
"attributes": {
"text": "A hook that handles the event of clicking outside of the wrapped component.\n\n- Create a custom hook that takes a `ref` and a `callback` to handle the `click` event.\n- Use the `React.useEffect()` hook to append and clean up the `click` event.\n- Use the `React.useRef()` hook to create a `ref` for your click component and pass it to the `useClickOutside` hook.\n\n",
"tags": [
"hooks",
"effect",
"event",
"intermediate"
]
},
"meta": {
"hash": "e8f3e64cd0cf616dd42843328234e37b1b7a77ed51da8956204fdb02e088ce33"
}
},
{
"id": "useFetch",
"type": "snippetListing",
"title": "useFetch",
"attributes": {
"text": "A hook that implements `fetch` in a declarative manner.\n\n- Create a custom hook that takes a `url` and `options`.\n- Use the `React.useState()` hook to initialize the `response` and `error` state variables.\n- Use the `React.useEffect()` hook to anychronously call `fetch()` and update the state varaibles accordingly.\n- Return an object containting the `response` and `error` state variables.\n\n",
"tags": [
"hooks",
"effect",
"state",
"intermediate"
]
},
"meta": {
"hash": "848cb6ba4f8bd5dad012a38e6bd0e7c829a79b3215a23939c30a3f652627da4f"
}
},
{
"id": "useInterval",
"type": "snippetListing",
"title": "useInterval",
"attributes": {
"text": "A hook that implements `setInterval` in a declarative manner.\n\n- Create a custom hook that takes a `callback` and a `delay`.\n- Use the `React.useRef()` hook to create a `ref` for the callback function.\n- Use the `React.useEffect()` hook to remember the latest callback.\n- Use the `Rect.useEffect()` hook to set up the interval and clean up.\n\n",
"tags": [
"hooks",
"effect",
"intermediate"
]
},
"meta": {
"hash": "04b22b339da3652cd044203a1d9723af542afe13572a38da825bd093ab1c99af"
}
},
{
"id": "useTimeout",
"type": "snippetListing",
"title": "useTimeout",
"attributes": {
"text": "A hook that implements `setTimeout` in a declarative manner.\n\n- Create a custom hook that takes a `callback` and a `delay`.\n- Use the `React.useRef()` hook to create a `ref` for the callback function.\n- Use the `React.useEffect()` hook to remember the latest callback.\n- Use the `Rect.useEffect()` hook to set up the timeout and clean up.\n\n",
"tags": [
"hooks",
"effect",
"intermediate"
]
},
"meta": {
"hash": "507c6fc6be62127e00738e39c6b22686a100b8af7252e1bbe589480b126c3d79"
}
}
],
"meta": {
"specification": "http://jsonapi.org/format/",
"type": "snippetListingArray"
}
}

707
snippet_data/snippets.json Normal file
View File

@ -0,0 +1,707 @@
{
"data": [
{
"id": "Accordion",
"title": "Accordion",
"type": "snippet",
"attributes": {
"fileName": "Accordion.md",
"text": "Renders an accordion menu with multiple collapsible content components.\n\n- Define an `AccordionItem` component, pass it to the `Accordion` and remove unnecessary nodes expect for `AccordionItem` by identifying the function's name in `props.children`.\n- Each `AccordionItem` component renders a `<button>` that is used to update the `Accordion` via the `props.handleClick` callback and the content of the component, passed down via `props.children`, while its appearance is determined by `props.isCollapsed` and based on `style`.\n- In the `Accordion` component, use the `React.useState()` hook to initialize the value of the `bindIndex` state variable to `props.defaultIndex`.\n- Use `Array.prototype.map` on the collected nodes to render the individual collapsiple elements.\n- Define `changeItem`, which will be executed when clicking an `AccordionItem`'s `<button>`.\n `changeItem` executes the passed callback, `onItemClick` and updates `bindIndex` based on the clicked element.\n\n",
"codeBlocks": {
"style": "",
"code": "function AccordionItem(props) {\r\n const style = {\r\n collapsed: {\r\n display: 'none'\r\n },\r\n expanded: {\r\n display: 'block'\r\n },\r\n buttonStyle: {\r\n display: 'block',\r\n width: '100%'\r\n }\r\n };\r\n\r\n return (\r\n <div>\r\n <button style={style.buttonStyle} onClick={() => props.handleClick()}>\r\n {props.label}\r\n </button>\r\n <div\r\n className=\"collapse-content\"\r\n style={props.isCollapsed ? style.collapsed : style.expanded}\r\n aria-expanded={props.isCollapsed}\r\n >\r\n {props.children}\r\n </div>\r\n </div>\r\n );\r\n}\r\n\r\nfunction Accordion(props) {\r\n const [bindIndex, setBindIndex] = React.useState(props.defaultIndex);\r\n\r\n const changeItem = itemIndex => {\r\n if (typeof props.onItemClick === 'function') props.onItemClick(itemIndex);\r\n if (itemIndex !== bindIndex) setBindIndex(itemIndex);\r\n };\r\n const items = props.children.filter(item => item.type.name === 'AccordionItem');\r\n\r\n return (\r\n <div className=\"wrapper\">\r\n {items.map(({ props }) => (\r\n <AccordionItem\r\n isCollapsed={bindIndex === props.index}\r\n label={props.label}\r\n handleClick={() => changeItem(props.index)}\r\n children={props.children}\r\n />\r\n ))}\r\n </div>\r\n );\r\n}",
"example": "ReactDOM.render(\r\n <Accordion defaultIndex=\"1\" onItemClick={console.log}>\r\n <AccordionItem label=\"A\" index=\"1\">\r\n Lorem ipsum\r\n </AccordionItem>\r\n <AccordionItem label=\"B\" index=\"2\">\r\n Dolor sit amet\r\n </AccordionItem>\r\n </Accordion>,\r\n document.getElementById('root')\r\n);"
},
"tags": [
"visual",
"children",
"state",
"advanced"
]
},
"meta": {
"hash": "0300c924ea29110f2982ae5564a63ff01519a5d0ffc8ae6dc8d175363fb77534"
}
},
{
"id": "AutoLink",
"title": "AutoLink",
"type": "snippet",
"attributes": {
"fileName": "AutoLink.md",
"text": "Renders a string as plaintext, with URLs converted to appropriate `<a>` elements.\n\n- Use `String.prototype.split()` and `String.prototype.match()` with a regular expression to find URLs in a string.\n- Return a `<React.Fragment>` with matched URLs rendered as `<a>` elements, dealing with missing protocol prefixes if necessary, and the rest of the string rendered as plaintext.\n\n",
"codeBlocks": {
"style": "",
"code": "function AutoLink({ text }) {\r\n const delimiter = /((?:https?:\\/\\/)?(?:(?:[a-z0-9]?(?:[a-z0-9\\-]{1,61}[a-z0-9])?\\.[^\\.|\\s])+[a-z\\.]*[a-z]+|(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?:\\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3})(?::\\d{1,5})*[a-z0-9.,_\\/~#&=;%+?\\-\\\\(\\\\)]*)/gi;\r\n\r\n return (\r\n <React.Fragment>\r\n {text.split(delimiter).map(word => {\r\n let match = word.match(delimiter);\r\n if (match) {\r\n let url = match[0];\r\n return <a href={url.startsWith('http') ? url : `http://${url}`}>{url}</a>;\r\n }\r\n return word;\r\n })}\r\n </React.Fragment>\r\n );\r\n}",
"example": "ReactDOM.render(\r\n <AutoLink text=\"foo bar baz http://example.org bar\" />,\r\n document.getElementById('root')\r\n);"
},
"tags": [
"visual",
"string",
"fragment",
"regexp",
"advanced"
]
},
"meta": {
"hash": "8a4373d9d191111ec6644ca315399c78d48c4d1a8c29124e48a88741ef826bea"
}
},
{
"id": "Carousel",
"title": "Carousel",
"type": "snippet",
"attributes": {
"fileName": "Carousel.md",
"text": "Renders a carousel component.\n\n- Use the `React.setState()` hook to create the `active` state variable and give it a value of `0` (index of the first item).\n- Use an object, `style`, to hold the styles for the individual components.\n- Use the `React.setEffect()` hook to update the value of `active` to the index of the next item, using `setTimeout`.\n- Destructure `props`, compute if visibility style should be set to `visible` or not for each carousel item while mapping over and applying the combined style to the carousel item component accordingly.\n- Render the carousel items using `React.cloneElement()` and pass down rest `props` along with the computed styles.\n\n",
"codeBlocks": {
"style": "",
"code": "function Carousel(props) {\r\n const [active, setActive] = React.useState(0);\r\n let scrollInterval = null;\r\n const style = {\r\n carousel: {\r\n position: 'relative'\r\n },\r\n carouselItem: {\r\n position: 'absolute',\r\n visibility: 'hidden'\r\n },\r\n visible: {\r\n visibility: 'visible'\r\n }\r\n };\r\n React.useEffect(() => {\r\n scrollInterval = setTimeout(() => {\r\n const { carouselItems } = props;\r\n setActive((active + 1) % carouselItems.length);\r\n }, 2000);\r\n });\r\n const { carouselItems, ...rest } = props;\r\n return (\r\n <div style={style.carousel}>\r\n {carouselItems.map((item, index) => {\r\n const activeStyle = active === index ? style.visible : {};\r\n return React.cloneElement(item, {\r\n ...rest,\r\n style: {\r\n ...style.carouselItem,\r\n ...activeStyle\r\n }\r\n });\r\n })}\r\n </div>\r\n );\r\n}",
"example": "ReactDOM.render(\r\n <Carousel\r\n carouselItems={[\r\n <div>carousel item 1</div>,\r\n <div>carousel item 2</div>,\r\n <div>carousel item 3</div>\r\n ]}\r\n />,\r\n document.getElementById('root')\r\n);"
},
"tags": [
"visual",
"children",
"state",
"effect",
"intermediate"
]
},
"meta": {
"hash": "24c0b339ea3131a8bde8081b1f44caac1d5014ecc1ac6d25ebf4b14d053a83a8"
}
},
{
"id": "Collapse",
"title": "Collapse",
"type": "snippet",
"attributes": {
"fileName": "Collapse.md",
"text": "Renders a component with collapsible content.\n\n- Use the `React.setState()` hook to create the `isCollapsed` state variable with an initial value of `props.collapsed`.\n- Use an object, `style`, to hold the styles for individual components and their states.\n- Use a `<div>` to wrap both the `<button>` that alters the component's `isCollapsed` state and the content of the component, passed down via `props.children`.\n- Determine the appearance of the content, based on `isCollapsed` and apply the appropriate CSS rules from the `style` object.\n- Finally, update the value of the `aria-expanded` attribute based on `isCollapsed` to make the component accessible.\n\n",
"codeBlocks": {
"style": "",
"code": "function Collapse(props) {\r\n const [isCollapsed, setIsCollapsed] = React.useState(props.collapsed);\r\n\r\n const style = {\r\n collapsed: {\r\n display: 'none'\r\n },\r\n expanded: {\r\n display: 'block'\r\n },\r\n buttonStyle: {\r\n display: 'block',\r\n width: '100%'\r\n }\r\n };\r\n\r\n return (\r\n <div>\r\n <button style={style.buttonStyle} onClick={() => setIsCollapsed(!isCollapsed)}>\r\n {isCollapsed ? 'Show' : 'Hide'} content\r\n </button>\r\n <div\r\n className=\"collapse-content\"\r\n style={isCollapsed ? style.collapsed : style.expanded}\r\n aria-expanded={isCollapsed}\r\n >\r\n {props.children}\r\n </div>\r\n </div>\r\n );\r\n}",
"example": "ReactDOM.render(\r\n <Collapse>\r\n <h1>This is a collapse</h1>\r\n <p>Hello world!</p>\r\n </Collapse>,\r\n document.getElementById('root')\r\n);"
},
"tags": [
"visual",
"children",
"state",
"intermediate"
]
},
"meta": {
"hash": "bb14a75971ab3a395e7a7e4458f636b9309f3e9a376494fe10b716be256bd9d2"
}
},
{
"id": "ControlledInput",
"title": "ControlledInput",
"type": "snippet",
"attributes": {
"fileName": "ControlledInput.md",
"text": "Renders an `<input>` element with internal state, that uses a callback function to pass its value to the parent component.\n\n- Use object destructuring to set defaults for certain attributes of the `<input>` element.\n- Use the `React.setState()` hook to create the `value` state variable and give it a value of equal to the `defaultValue` prop.\n- Use the `React.useEffect()` hook with a second parameter set to the `value` state variable to call the `callback` function every time `value` is updated.\n- Render an `<input>` element with the appropriate attributes and use the the `onChange` event to upda the `value` state variable.\n\n",
"codeBlocks": {
"style": "",
"code": "function ControlledInput({\r\n callback,\r\n type = 'text',\r\n disabled = false,\r\n readOnly = false,\r\n defaultValue,\r\n placeholder = ''\r\n}) {\r\n const [value, setValue] = React.useState(defaultValue);\r\n\r\n React.useEffect(() => {\r\n callback(value);\r\n }, [value]);\r\n\r\n return (\r\n <input\r\n defaultValue={defaultValue}\r\n type={type}\r\n disabled={disabled}\r\n readOnly={readOnly}\r\n placeholder={placeholder}\r\n onChange={({ target: { value } }) => setValue(value)}\r\n />\r\n );\r\n}",
"example": "ReactDOM.render(\r\n <ControlledInput\r\n type=\"text\"\r\n placeholder=\"Insert some text here...\"\r\n callback={val => console.log(val)}\r\n />,\r\n document.getElementById('root')\r\n);"
},
"tags": [
"input",
"state",
"effect",
"intermediate"
]
},
"meta": {
"hash": "a9c8332455bad3c81dd1dbaf7361b98406ed11eaa35e9be8e72e88d8139eb2d6"
}
},
{
"id": "CountDown",
"title": "CountDown",
"type": "snippet",
"attributes": {
"fileName": "CountDown.md",
"text": "Renders a countdown timer that prints a message when it reaches zero.\n\n- Use object destructuring to set defaults for the `hours`, `minutes` and `seconds` props.\n- Use the `React.useState()` hook to create the `time`, `paused` and `over` state variables and set their values to the values of the passed props, `false` and `false` respectively.\n- Create a method `tick`, that updates the value of `time` based on the current value (i.e. decreasing the time by one second).\n- If `paused` or `over` is `true`, `tick` will return immediately.\n- Create a method `reset`, that resets all state variables to their initial states.\n- Use the the `React.useEffect()` hook to call the `tick` method every second via the use of `setInterval()` and use `clearInterval()` to cleanup when the component is unmounted.\n- Use a `<div>` to wrap a `<p>` element with the textual representation of the components `time` state variable, as well as two `<button>` elements that will pause/unpause and restart the timer respectively.\n- If `over` is `true`, the timer will display a message instead of the value of `time`.\n\n",
"codeBlocks": {
"style": "",
"code": "function CountDown({ hours = 0, minutes = 0, seconds = 0 }) {\r\n const [paused, setPaused] = React.useState(false);\r\n const [over, setOver] = React.useState(false);\r\n const [time, setTime] = React.useState({\r\n hours: parseInt(hours),\r\n minutes: parseInt(minutes),\r\n seconds: parseInt(seconds)\r\n });\r\n\r\n const tick = () => {\r\n if (paused || over) return;\r\n if (time.hours == 0 && time.minutes == 0 && time.seconds == 0) setOver(true);\r\n else if (time.minutes == 0 && time.seconds == 0)\r\n setTime({\r\n hours: time.hours - 1,\r\n minutes: 59,\r\n seconds: 59\r\n });\r\n else if (time.seconds == 0)\r\n setTime({\r\n hours: time.hours,\r\n minutes: time.minutes - 1,\r\n seconds: 59\r\n });\r\n else\r\n setTime({\r\n hours: time.hours,\r\n minutes: time.minutes,\r\n seconds: time.seconds - 1\r\n });\r\n };\r\n\r\n const reset = () => {\r\n setTime({\r\n hours: parseInt(hours),\r\n minutes: parseInt(minutes),\r\n seconds: parseInt(seconds)\r\n });\r\n setPaused(false);\r\n setOver(false);\r\n };\r\n\r\n React.useEffect(() => {\r\n let timerID = setInterval(() => tick(), 1000);\r\n return () => clearInterval(timerID);\r\n });\r\n\r\n return (\r\n <div>\r\n <p>{`${time.hours.toString().padStart(2, '0')}:${time.minutes\r\n .toString()\r\n .padStart(2, '0')}:${time.seconds.toString().padStart(2, '0')}`}</p>\r\n <div>{over ? \"Time's up!\" : ''}</div>\r\n <button onClick={() => setPaused(!paused)}>{paused ? 'Resume' : 'Pause'}</button>\r\n <button onClick={() => reset()}>Restart</button>\r\n </div>\r\n );\r\n}",
"example": "ReactDOM.render(<CountDown hours=\"1\" minutes=\"45\" />, document.getElementById('root'));"
},
"tags": [
"visual",
"state",
"advanced"
]
},
"meta": {
"hash": "b5ac7580e6a96df21478030c6c2b30fa2e56df2573ab66e2405bdbbdce60aa61"
}
},
{
"id": "DataList",
"title": "DataList",
"type": "snippet",
"attributes": {
"fileName": "DataList.md",
"text": "Renders a list of elements from an array of primitives.\n\n- Use the value of the `isOrdered` prop to conditionally render a `<ol>` or `<ul>` list.\n- Use `Array.prototype.map` to render every item in `data` as a `<li>` element, give it a `key` produced from the concatenation of the its index and value.\n- Omit the `isOrdered` prop to render a `<ul>` list by default.\n\n",
"codeBlocks": {
"style": "",
"code": "function DataList({ isOrdered, data }) {\r\n const list = data.map((val, i) => <li key={`${i}_${val}`}>{val}</li>);\r\n return isOrdered ? <ol>{list}</ol> : <ul>{list}</ul>;\r\n}",
"example": "const names = ['John', 'Paul', 'Mary'];\r\nReactDOM.render(<DataList data={names} />, document.getElementById('root'));\r\nReactDOM.render(<DataList data={names} isOrdered />, document.getElementById('root'));"
},
"tags": [
"array",
"beginner"
]
},
"meta": {
"hash": "38eb402c926fbc919a04b55fe8d6fb2fbdd5ba54341783852f909f9181e745da"
}
},
{
"id": "DataTable",
"title": "DataTable",
"type": "snippet",
"attributes": {
"fileName": "DataTable.md",
"text": "Renders a table with rows dynamically created from an array of primitives.\n\n- Render a `<table>` element with two columns (`ID` and `Value`).\n- Use `Array.prototype.map` to render every item in `data` as a `<tr>` element, consisting of its index and value, give it a `key` produced from the concatenation of the two.\n\n",
"codeBlocks": {
"style": "",
"code": "function DataTable({ data }) {\r\n return (\r\n <table>\r\n <thead>\r\n <tr>\r\n <th>ID</th>\r\n <th>Value</th>\r\n </tr>\r\n </thead>\r\n <tbody>\r\n {data.map((val, i) => (\r\n <tr key={`${i}_${val}`}>\r\n <td>{i}</td>\r\n <td>{val}</td>\r\n </tr>\r\n ))}\r\n </tbody>\r\n </table>\r\n );\r\n}",
"example": "const people = ['John', 'Jesse'];\r\nReactDOM.render(<DataTable data={people} />, document.getElementById('root'));"
},
"tags": [
"array",
"beginner"
]
},
"meta": {
"hash": "c4aba21d546f519469e8a65101e9cd44b25dae028b48a223b8cb6c19f83b60d4"
}
},
{
"id": "FileDrop",
"title": "FileDrop",
"type": "snippet",
"attributes": {
"fileName": "FileDrop.md",
"text": "Renders a file drag and drop component for a single file.\n\n- Create a ref called `dropRef` for this component.\n- Use the `React.useState()` hook to create the `drag` and `filename` variables, initialized to `false` and `''` respectively.\n The variables `dragCounter` and `drag` are used to determine if a file is being dragged, while `filename` is used to store the dropped file's name.\n- Create the `handleDrag`, `handleDragIn`, `handleDragOut` and `handleDrop` methods to handle drag and drop functionality, bind them to the component's context.\n- Each of the methods will handle a specific event, the listeners for which are created and removed in the `React.useEffect()` hook and its attached `cleanup()` method.\n- `handleDrag` prevents the browser from opening the dragged file, `handleDragIn` and `handleDragOut` handle the dragged file entering and exiting the component, while `handleDrop` handles the file being dropped and passes it to `props.handleDrop`.\n- Return an appropriately styled `<div>` and use `drag` and `filename` to determine its contents and style.\n- Finally, bind the `ref` of the created `<div>` to `dropRef`.\n\n",
"codeBlocks": {
"style": ".filedrop {\r\n min-height: 120px;\r\n border: 3px solid #d3d3d3;\r\n text-align: center;\r\n font-size: 24px;\r\n padding: 32px;\r\n border-radius: 4px;\r\n}\r\n\r\n.filedrop.drag {\r\n border: 3px dashed #1e90ff;\r\n}\r\n\r\n.filedrop.ready {\r\n border: 3px solid #32cd32;\r\n}",
"code": "function FileDrop(props) {\r\n const [drag, setDrag] = React.useState(false);\r\n const [filename, setFilename] = React.useState('');\r\n let dropRef = React.createRef();\r\n let dragCounter = 0;\r\n\r\n const handleDrag = e => {\r\n e.preventDefault();\r\n e.stopPropagation();\r\n };\r\n\r\n const handleDragIn = e => {\r\n e.preventDefault();\r\n e.stopPropagation();\r\n dragCounter++;\r\n if (e.dataTransfer.items && e.dataTransfer.items.length > 0) setDrag(true);\r\n };\r\n\r\n const handleDragOut = e => {\r\n e.preventDefault();\r\n e.stopPropagation();\r\n dragCounter--;\r\n if (dragCounter === 0) setDrag(false);\r\n };\r\n\r\n const handleDrop = e => {\r\n e.preventDefault();\r\n e.stopPropagation();\r\n setDrag(false);\r\n if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {\r\n props.handleDrop(e.dataTransfer.files[0]);\r\n setFilename(e.dataTransfer.files[0].name);\r\n e.dataTransfer.clearData();\r\n dragCounter = 0;\r\n }\r\n };\r\n\r\n React.useEffect(() => {\r\n let div = dropRef.current;\r\n div.addEventListener('dragenter', handleDragIn);\r\n div.addEventListener('dragleave', handleDragOut);\r\n div.addEventListener('dragover', handleDrag);\r\n div.addEventListener('drop', handleDrop);\r\n return function cleanup() {\r\n div.removeEventListener('dragenter', handleDragIn);\r\n div.removeEventListener('dragleave', handleDragOut);\r\n div.removeEventListener('dragover', handleDrag);\r\n div.removeEventListener('drop', handleDrop);\r\n };\r\n });\r\n\r\n return (\r\n <div\r\n ref={dropRef}\r\n className={drag ? 'filedrop drag' : filename ? 'filedrop ready' : 'filedrop'}\r\n >\r\n {filename && !drag ? <div>{filename}</div> : <div>Drop files here!</div>}\r\n </div>\r\n );\r\n}",
"example": "ReactDOM.render(<FileDrop handleDrop={console.log} />, document.getElementById('root'));"
},
"tags": [
"visual",
"input",
"state",
"effect",
"event",
"intermediate"
]
},
"meta": {
"hash": "2e35dd3fa6b4808f03aff6b542b293f8be0fd9aa1cbb7bd85a2729f808cf8888"
}
},
{
"id": "LimitedTextarea",
"title": "LimitedTextarea",
"type": "snippet",
"attributes": {
"fileName": "LimitedTextarea.md",
"text": "Renders a textarea component with a character limit.\n\n- Use the `React.useState()` hook to create the `content` state variable and set its value to `value`.\n Create a method `setFormattedContent`, which trims the content of the input if it's longer than `limit`.\n- Use the `React.useEffect()` hook to call the `setFormattedContent` method on the value of the `content` state variable.\n- Use a`<div>` to wrap both the`<textarea>` and the `<p>` element that displays the character count and bind the `onChange` event of the `<textarea>` to call `setFormattedContent` with the value of `event.target.value`.\n\n",
"codeBlocks": {
"style": "",
"code": "function LimitedTextarea({ rows, cols, value, limit }) {\r\n const [content, setContent] = React.useState(value);\r\n\r\n const setFormattedContent = text => {\r\n text.length > limit ? setContent(text.slice(0, limit)) : setContent(text);\r\n };\r\n\r\n React.useEffect(() => {\r\n setFormattedContent(content);\r\n }, []);\r\n\r\n return (\r\n <div>\r\n <textarea\r\n rows={rows}\r\n cols={cols}\r\n onChange={event => setFormattedContent(event.target.value)}\r\n value={content}\r\n />\r\n <p>\r\n {content.length}/{limit}\r\n </p>\r\n </div>\r\n );\r\n}",
"example": "ReactDOM.render(<LimitedTextarea limit={32} value=\"Hello!\" />, document.getElementById('root'));"
},
"tags": [
"input",
"state",
"effect",
"event",
"beginner"
]
},
"meta": {
"hash": "eabc464d70da892c84d00925de7c8c22ea3f54d5337e7b2efd6e4043fb447aa8"
}
},
{
"id": "LimitedWordTextarea",
"title": "LimitedWordTextarea",
"type": "snippet",
"attributes": {
"fileName": "LimitedWordTextarea.md",
"text": "Renders a textarea component with a word limit.\n\n- Use the `React.useState()` hook to create the `content` and `wordCount` state variables and set their values to `value` and `0` respectively.\n- Create a method `setFormattedContent`, which uses `String.prototype.split(' ')` to turn the input into an array of words and check if the result of applying `Array.prototype.filter(Boolean)` has a `length` longer than `limit`.\n- If the afforementioned `length` exceeds the `limit`, trim the input, otherwise return the raw input, updating `content` and `wordCount` accordingly in both cases.\n- Use the `React.useEffect()` hook to call the `setFormattedContent` method on the value of the `content` state variable.\n- Use a`<div>` to wrap both the`<textarea>` and the `<p>` element that displays the character count and bind the `onChange` event of the `<textarea>` to call `setFormattedContent` with the value of `event.target.value`.\n\n",
"codeBlocks": {
"style": "",
"code": "function LimitedWordTextarea({ rows, cols, value, limit }) {\r\n const [content, setContent] = React.useState(value);\r\n const [wordCount, setWordCount] = React.useState(0);\r\n\r\n const setFormattedContent = text => {\r\n let words = text.split(' ');\r\n if (words.filter(Boolean).length > limit) {\r\n setContent(\r\n text\r\n .split(' ')\r\n .slice(0, limit)\r\n .join(' ')\r\n );\r\n setWordCount(limit);\r\n } else {\r\n setContent(text);\r\n setWordCount(words.filter(Boolean).length);\r\n }\r\n };\r\n\r\n React.useEffect(() => {\r\n setFormattedContent(content);\r\n }, []);\r\n\r\n return (\r\n <div>\r\n <textarea\r\n rows={rows}\r\n cols={cols}\r\n onChange={event => setFormattedContent(event.target.value)}\r\n value={content}\r\n />\r\n <p>\r\n {wordCount}/{limit}\r\n </p>\r\n </div>\r\n );\r\n}",
"example": "ReactDOM.render(\r\n <LimitedWordTextArea limit={5} value=\"Hello there!\" />,\r\n document.getElementById('root')\r\n);"
},
"tags": [
"input",
"state",
"effect",
"event",
"beginner"
]
},
"meta": {
"hash": "8c0f35b8cba144ddf8611fa48659ef9a83bd235a961466ccbf1ca5f43bd6b602"
}
},
{
"id": "Mailto",
"title": "Mailto",
"type": "snippet",
"attributes": {
"fileName": "Mailto.md",
"text": "Renders a link formatted to send an email.\n\n- Destructure the component's props, use `email`, `subject` and `body` to create a `<a>` element with an appropriate `href` attribute.\n- Render the link with `props.children` as its content.\n\n",
"codeBlocks": {
"style": "",
"code": "function Mailto({ email, subject, body, ...props }) {\r\n return (\r\n <a href={`mailto:${email}?subject=${subject || ''}&body=${body || ''}`}>{props.children}</a>\r\n );\r\n}",
"example": "ReactDOM.render(\r\n <Mailto email=\"foo@bar.baz\" subject=\"Hello\" body=\"Hello world!\">\r\n Mail me!\r\n </Mailto>,\r\n document.getElementById('root')\r\n);"
},
"tags": [
"visual",
"beginner"
]
},
"meta": {
"hash": "e22d307cb43e589e7bd1ec5caab16c1101aa764102f35cd7295876033b44578e"
}
},
{
"id": "MappedTable",
"title": "MappedTable",
"type": "snippet",
"attributes": {
"fileName": "MappedTable.md",
"text": "Renders a table with rows dynamically created from an array of objects and a list of property names.\n\n- Use `Object.keys()`, `Array.prototype.filter()`, `Array.prototype.includes()` and `Array.prototype.reduce()` to produce a `filteredData` array, containing all objects with the keys specified in `propertyNames`.\n- Render a `<table>` element with a set of columns equal to the amount of values in `propertyNames`.\n- Use `Array.prototype.map` to render each value in the `propertyNames` array as a `<th>` element.\n- Use `Array.prototype.map` to render each object in the `filteredData` array as a `<tr>` element, containing a `<td>` for each key in the object.\n\n_This component does not work with nested objects and will break if there are nested objects inside any of the properties specified in `propertyNames`_\n\n",
"codeBlocks": {
"style": "",
"code": "function MappedTable({ data, propertyNames }) {\r\n let filteredData = data.map(v =>\r\n Object.keys(v)\r\n .filter(k => propertyNames.includes(k))\r\n .reduce((acc, key) => ((acc[key] = v[key]), acc), {})\r\n );\r\n return (\r\n <table>\r\n <thead>\r\n <tr>\r\n {propertyNames.map(val => (\r\n <th key={`h_${val}`}>{val}</th>\r\n ))}\r\n </tr>\r\n </thead>\r\n <tbody>\r\n {filteredData.map((val, i) => (\r\n <tr key={`i_${i}`}>\r\n {propertyNames.map(p => (\r\n <td key={`i_${i}_${p}`}>{val[p]}</td>\r\n ))}\r\n </tr>\r\n ))}\r\n </tbody>\r\n </table>\r\n );\r\n}",
"example": "const people = [\r\n { name: 'John', surname: 'Smith', age: 42 },\r\n { name: 'Adam', surname: 'Smith', gender: 'male' }\r\n];\r\nconst propertyNames = ['name', 'surname', 'age'];\r\nReactDOM.render(\r\n <MappedTable data={people} propertyNames={propertyNames} />,\r\n document.getElementById('root')\r\n);"
},
"tags": [
"array",
"object",
"intermediate"
]
},
"meta": {
"hash": "9ff4e1580f53ab64c740aee2bfdc85f1cfc46f7cf2f39d6324ec4c2b7d6c7270"
}
},
{
"id": "Modal",
"title": "Modal",
"type": "snippet",
"attributes": {
"fileName": "Modal.md",
"text": "Renders a Modal component, controllable through events.\nTo use the component, import `Modal` only once and then display it by passing a boolean value to the `isVisible` attribute.\n\n- Use object destructuring to set defaults for certain attributes of the modal component.\n- Define `keydownHandler`, a method which handles all keyboard events, which can be used according to your needs to dispatch actions (e.g. close the modal when <kbd>Esc</kbd> is pressed).\n- Use `React.useEffect()` hook to add or remove the `keydown` event listener, which calls `keydownHandler`.\n- Use the `isVisible` prop to determine if the modal should be shown or not.\n- Use CSS to style and position the modal component.\n\n",
"codeBlocks": {
"style": ".modal {\r\n position: fixed;\r\n top: 0;\r\n bottom: 0;\r\n left: 0;\r\n right: 0;\r\n width: 100%;\r\n z-index: 9999;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n background-color: rgba(0, 0, 0, 0.25);\r\n animation-name: appear;\r\n animation-duration: 300ms;\r\n}\r\n\r\n.modal-dialog {\r\n width: 100%;\r\n max-width: 550px;\r\n background: white;\r\n position: relative;\r\n margin: 0 20px;\r\n max-height: calc(100vh - 40px);\r\n text-align: left;\r\n display: flex;\r\n flex-direction: column;\r\n overflow: hidden;\r\n box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);\r\n -webkit-animation-name: animatetop;\r\n -webkit-animation-duration: 0.4s;\r\n animation-name: slide-in;\r\n animation-duration: 0.5s;\r\n}\r\n\r\n.modal-header,\r\n.modal-footer {\r\n display: flex;\r\n align-items: center;\r\n padding: 1rem;\r\n}\r\n.modal-header {\r\n border-bottom: 1px solid #dbdbdb;\r\n justify-content: space-between;\r\n}\r\n.modal-footer {\r\n border-top: 1px solid #dbdbdb;\r\n justify-content: flex-end;\r\n}\r\n.modal-close {\r\n cursor: pointer;\r\n padding: 1rem;\r\n margin: -1rem -1rem -1rem auto;\r\n}\r\n.modal-body {\r\n overflow: auto;\r\n}\r\n.modal-content {\r\n padding: 1rem;\r\n}\r\n\r\n@keyframes appear {\r\n from {\r\n opacity: 0;\r\n }\r\n to {\r\n opacity: 1;\r\n }\r\n}\r\n@keyframes slide-in {\r\n from {\r\n transform: translateY(-150px);\r\n }\r\n to {\r\n transform: translateY(0);\r\n }\r\n}",
"code": "function Modal({ isVisible = false, title, content, footer, onClose }) {\r\n React.useEffect(() => {\r\n document.addEventListener('keydown', keydownHandler);\r\n return () => document.removeEventListener('keydown', keydownHandler);\r\n });\r\n\r\n function keydownHandler({ key }) {\r\n switch (key) {\r\n case 'Escape':\r\n onClose();\r\n break;\r\n default:\r\n }\r\n }\r\n\r\n return !isVisible ? null : (\r\n <div className=\"modal\" onClick={onClose}>\r\n <div className=\"modal-dialog\" onClick={e => e.stopPropagation()}>\r\n <div className=\"modal-header\">\r\n <h3 className=\"modal-title\">{title}</h3>\r\n <span className=\"modal-close\" onClick={onClose}>\r\n &times;\r\n </span>\r\n </div>\r\n <div className=\"modal-body\">\r\n <div className=\"modal-content\">{content}</div>\r\n </div>\r\n {footer && <div className=\"modal-footer\">{footer}</div>}\r\n </div>\r\n </div>\r\n );\r\n}",
"example": "//Add the component to the render function\r\nfunction App() {\r\n const [isModal, setModal] = React.useState(false);\r\n\r\n return (\r\n <React.Fragment>\r\n <button onClick={() => setModal(true)}>Click Here</button>\r\n <Modal\r\n isVisible={isModal}\r\n title=\"Modal Title\"\r\n content={<p>Add your content here</p>}\r\n footer={<button>Cancel</button>}\r\n onClose={() => setModal(false)}\r\n />\r\n </React.Fragment>\r\n );\r\n}\r\n\r\nReactDOM.render(<App />, document.getElementById('root'));"
},
"tags": [
"visual",
"effect",
"intermediate"
]
},
"meta": {
"hash": "07c392eff204d3fe475cba116d25ca84b48f7f37ae18f0adade7e65c98ec509d"
}
},
{
"id": "MultiselectCheckbox",
"title": "MultiselectCheckbox",
"type": "snippet",
"attributes": {
"fileName": "MultiselectCheckbox.md",
"text": "Renders a checkbox list that uses a callback function to pass its selected value/values to the parent component.\n\n- Use `React.setState()` to create a `data` state variable and set its initial value equal to the `options` prop.\n- Create a function `toggle` that is used to toggle the `checked` to update the `data` state variable and call the `onChange` callback passed via the component's props.\n- Render a `<ul>` element and use `Array.prototype.map()` to map the `data` state variable to individual `<li>` elements with `<input>` elements as their children.\n- Each `<input>` element has the `type='checkbox'` attribute and is marked as `readOnly`, as its click events are handled by the parent `<li>` element's `onClick` handler.\n\n",
"codeBlocks": {
"style": "",
"code": "const style = {\r\n listContainer: {\r\n listStyle: 'none',\r\n paddingLeft: 0\r\n },\r\n itemStyle: {\r\n cursor: 'pointer',\r\n padding: 5\r\n }\r\n};\r\n\r\nfunction MultiselectCheckbox({ options, onChange }) {\r\n const [data, setData] = React.useState(options);\r\n\r\n const toggle = item => {\r\n data.forEach((_, key) => {\r\n if (data[key].label === item.label) data[key].checked = !item.checked;\r\n });\r\n setData([...data]);\r\n onChange(data);\r\n };\r\n\r\n return (\r\n <ul style={style.listContainer}>\r\n {data.map(item => {\r\n return (\r\n <li key={item.label} style={style.itemStyle} onClick={() => toggle(item)}>\r\n <input readOnly type=\"checkbox\" checked={item.checked || false} />\r\n {item.label}\r\n </li>\r\n );\r\n })}\r\n </ul>\r\n );\r\n}",
"example": "const options = [{ label: 'Item One' }, { label: 'Item Two' }];\r\n\r\nReactDOM.render(\r\n <MultiselectCheckbox\r\n options={options}\r\n onChange={data => {\r\n console.log(data);\r\n }}\r\n />,\r\n document.getElementById('root')\r\n);"
},
"tags": [
"input",
"state",
"array",
"intermediate"
]
},
"meta": {
"hash": "ec2d246e3520208cf67146e9674f576b4d13fd31854892113396409edcb68db6"
}
},
{
"id": "PasswordRevealer",
"title": "PasswordRevealer",
"type": "snippet",
"attributes": {
"fileName": "PasswordRevealer.md",
"text": "Renders a password input field with a reveal button.\n\n- Use the `React.useState()` hook to create the `shown` state variable and set its value to `false`.\n- Use a`<div>` to wrap both the`<input>` and the `<button>` element that toggles the type of the input field between `\"text\"` and `\"password\"`.\n\n",
"codeBlocks": {
"style": "",
"code": "function PasswordRevealer({ value }) {\r\n const [shown, setShown] = React.useState(false);\r\n\r\n return (\r\n <div>\r\n <input type={shown ? 'text' : 'password'} value={value} onChange={() => {}} />\r\n <button onClick={() => setShown(!shown)}>Show/Hide</button>\r\n </div>\r\n );\r\n}",
"example": "ReactDOM.render(<PasswordRevealer />, document.getElementById('root'));"
},
"tags": [
"input",
"state",
"beginner"
]
},
"meta": {
"hash": "7e019374ec668fe2bc3053bbcb3e8a2f3ac419ac0e316d3ee6e94f99a1fb53ee"
}
},
{
"id": "Select",
"title": "Select",
"type": "snippet",
"attributes": {
"fileName": "Select.md",
"text": "Renders a `<select>` element that uses a callback function to pass its value to the parent component.\n\n- Use object destructuring to set defaults for certain attributes of the `<select>` element.\n- Render a `<select>` element with the appropriate attributes and use the `callback` function in the `onChange` event to pass the value of the textarea to the parent.\n- Use destructuring on the `values` array to pass an array of `value` and `text` elements and the `selected` attribute to define the initial `value` of the `<select>` element.\n\n",
"codeBlocks": {
"style": "",
"code": "function Select({ values, callback, disabled = false, readonly = false, selected }) {\r\n return (\r\n <select\r\n disabled={disabled}\r\n readOnly={readonly}\r\n onChange={({ target: { value } }) => callback(value)}\r\n >\r\n {values.map(([value, text]) => (\r\n <option selected={selected === value} value={value}>\r\n {text}\r\n </option>\r\n ))}\r\n </select>\r\n );\r\n}",
"example": "let choices = [\r\n ['grapefruit', 'Grapefruit'],\r\n ['lime', 'Lime'],\r\n ['coconut', 'Coconut'],\r\n ['mango', 'Mango']\r\n];\r\nReactDOM.render(\r\n <Select values={choices} selected=\"lime\" callback={val => console.log(val)} />,\r\n document.getElementById('root')\r\n);"
},
"tags": [
"input",
"beginner"
]
},
"meta": {
"hash": "b82ad131475042bd38682aba03964494680fc2dfe3a61b0a1478215f74e993de"
}
},
{
"id": "Slider",
"title": "Slider",
"type": "snippet",
"attributes": {
"fileName": "Slider.md",
"text": "Renders a slider element that uses a callback function to pass its value to the parent component.\n\n- Use object destructuring to set defaults for certain attributes of the `<input>` element.\n- Render an `<input>` element of type `\"range\"` and the appropriate attributes, use the `callback` function in the `onChange` event to pass the value of the input to the parent.\n\n",
"codeBlocks": {
"style": "",
"code": "function Slider({ callback, disabled = false, readOnly = false }) {\r\n return (\r\n <input\r\n type=\"range\"\r\n disabled={disabled}\r\n readOnly={readOnly}\r\n onChange={({ target: { value } }) => callback(value)}\r\n />\r\n );\r\n}",
"example": "ReactDOM.render(<Slider callback={val => console.log(val)} />, document.getElementById('root'));"
},
"tags": [
"input",
"beginner"
]
},
"meta": {
"hash": "db77adcc300072c41b4d096cd385f7c3616601fcace16d7c28032036a6eed7e7"
}
},
{
"id": "StarRating",
"title": "StarRating",
"type": "snippet",
"attributes": {
"fileName": "StarRating.md",
"text": "Renders a star rating component.\n\n- Define a component, called `Star` that will render each individual star with the appropriate appearance, based on the parent component's state.\n- In the `StarRating` component, use the `React.useState()` hook to define the `rating` and `selection` state variables with the initial values of `props.rating` (or `0` if invalid or not supplied) and `0`.\n- Create a method, `hoverOver`, that updates `selected` and `rating` according to the provided `event`.\n- Create a `<div>` to wrap the `<Star>` components, which are created using `Array.prototype.map` on an array of 5 elements, created using `Array.from`, and handle the `onMouseLeave` event to set `selection` to `0`, the `onClick` event to set the `rating` and the `onMouseOver` event to set `selection` to the `star-id` attribute of the `event.target` respectively.\n- Finally, pass the appropriate values to each `<Star>` component (`starId` and `marked`).\n\n",
"codeBlocks": {
"style": "",
"code": "function Star({ marked, starId }) {\r\n return (\r\n <span star-id={starId} style={{ color: '#ff9933' }} role=\"button\">\r\n {marked ? '\\u2605' : '\\u2606'}\r\n </span>\r\n );\r\n}\r\n\r\nfunction StarRating(props) {\r\n const [rating, setRating] = React.useState(typeof props.rating == 'number' ? props.rating : 0);\r\n const [selection, setSelection] = React.useState(0);\r\n const hoverOver = event => {\r\n let val = 0;\r\n if (event && event.target && event.target.getAttribute('star-id'))\r\n val = event.target.getAttribute('star-id');\r\n setSelection(val);\r\n };\r\n return (\r\n <div\r\n onMouseOut={() => hoverOver(null)}\r\n onClick={event => setRating(event.target.getAttribute('star-id') || rating)}\r\n onMouseOver={hoverOver}\r\n >\r\n {Array.from({ length: 5 }, (v, i) => (\r\n <Star\r\n starId={i + 1}\r\n key={`star_${i + 1} `}\r\n marked={selection ? selection >= i + 1 : rating >= i + 1}\r\n />\r\n ))}\r\n </div>\r\n );\r\n}",
"example": "ReactDOM.render(<StarRating />, document.getElementById('root'));\r\nReactDOM.render(<StarRating rating={2} />, document.getElementById('root'));"
},
"tags": [
"visual",
"children",
"input",
"state",
"intermediate"
]
},
"meta": {
"hash": "a057b14315a578a5c27d6534b965c8a567be6ffd7ba0b32882282e6e99076ebc"
}
},
{
"id": "Tabs",
"title": "Tabs",
"type": "snippet",
"attributes": {
"fileName": "Tabs.md",
"text": "Renders a tabbed menu and view component.\n\n- Define a `TabItem` component, pass it to the `Tab` and remove unnecessary nodes expect for `TabItem` by identifying the function's name in `props.children`.\n- Use the `React.useState()` hook to initialize the value of the `bindIndex` state variable to `props.defaultIndex`.\n- Use `Array.prototype.map` on the collected nodes to render the `tab-menu` and `tab-view`.\n- Define `changeTab`, which will be executed when clicking a `<button>` from the `tab-menu`.\n- `changeTab` executes the passed callback, `onTabClick` and updates `bindIndex`, which in turn causes a re-render, evaluating the `style` and `className` of the `tab-view` items and `tab-menu` buttons according to their `index`.\n\n",
"codeBlocks": {
"style": ".tab-menu > button {\r\n cursor: pointer;\r\n padding: 8px 16px;\r\n border: 0;\r\n border-bottom: 2px solid transparent;\r\n background: none;\r\n}\r\n.tab-menu > button.focus {\r\n border-bottom: 2px solid #007bef;\r\n}\r\n.tab-menu > button:hover {\r\n border-bottom: 2px solid #007bef;\r\n}",
"code": "function TabItem(props) {\r\n return <div {...props} />;\r\n}\r\n\r\nfunction Tabs(props) {\r\n const [bindIndex, setBindIndex] = React.useState(props.defaultIndex);\r\n const changeTab = newIndex => {\r\n if (typeof props.onTabClick === 'function') props.onTabClick(newIndex);\r\n setBindIndex(newIndex);\r\n };\r\n const items = props.children.filter(item => item.type.name === 'TabItem');\r\n\r\n return (\r\n <div className=\"wrapper\">\r\n <div className=\"tab-menu\">\r\n {items.map(({ props: { index, label } }) => (\r\n <button onClick={() => changeTab(index)} className={bindIndex === index ? 'focus' : ''}>\r\n {label}\r\n </button>\r\n ))}\r\n </div>\r\n <div className=\"tab-view\">\r\n {items.map(({ props }) => (\r\n <div\r\n {...props}\r\n className=\"tab-view_item\"\r\n key={props.index}\r\n style={{ display: bindIndex === props.index ? 'block' : 'none' }}\r\n />\r\n ))}\r\n </div>\r\n </div>\r\n );\r\n}",
"example": "ReactDOM.render(\r\n <Tabs defaultIndex=\"1\" onTabClick={console.log}>\r\n <TabItem label=\"A\" index=\"1\">\r\n Lorem ipsum\r\n </TabItem>\r\n <TabItem label=\"B\" index=\"2\">\r\n Dolor sit amet\r\n </TabItem>\r\n </Tabs>,\r\n document.getElementById('root')\r\n);"
},
"tags": [
"visual",
"state",
"children",
"intermediate"
]
},
"meta": {
"hash": "54a00cb6ac87079622e3806aa55816332e353eb88ba263a5059dd40942955ff7"
}
},
{
"id": "TextArea",
"title": "TextArea",
"type": "snippet",
"attributes": {
"fileName": "TextArea.md",
"text": "Renders a `<textarea>` element that uses a callback function to pass its value to the parent component.\n\n- Use object destructuring to set defaults for certain attributes of the `<textarea>` element.\n- Render a `<textarea>` element with the appropriate attributes and use the `callback` function in the `onChange` event to pass the value of the textarea to the parent.\n\n",
"codeBlocks": {
"style": "",
"code": "function TextArea({\r\n callback,\r\n cols = 20,\r\n rows = 2,\r\n disabled = false,\r\n readOnly = false,\r\n placeholder = ''\r\n}) {\r\n return (\r\n <textarea\r\n cols={cols}\r\n rows={rows}\r\n disabled={disabled}\r\n readOnly={readOnly}\r\n placeholder={placeholder}\r\n onChange={({ target: { value } }) => callback(value)}\r\n />\r\n );\r\n}",
"example": "ReactDOM.render(\r\n <TextArea placeholder=\"Insert some text here...\" callback={val => console.log(val)} />,\r\n document.getElementById('root')\r\n);"
},
"tags": [
"input",
"beginner"
]
},
"meta": {
"hash": "4c0c3bcdb3a7a7f858d1e938e7a1926804ceca983f5eeae3edf4b28f36548fb4"
}
},
{
"id": "Ticker",
"title": "Ticker",
"type": "snippet",
"attributes": {
"fileName": "Ticker.md",
"text": "Renders a ticker component.\n\n- Use the `React.useState()` hook to initialize the `ticker` state variable to `0`.\n- Define two methods, `tick` and `reset`, that will periodically increment `timer` based on `interval` and reset `interval` respectively.\n- Return a `<div>` with two `<button>` elements, each of which calls `tick` and `reset` respectively.\n\n",
"codeBlocks": {
"style": "",
"code": "function Ticker(props) {\r\n const [ticker, setTicker] = React.useState(0);\r\n let interval = null;\r\n\r\n const tick = () => {\r\n reset();\r\n interval = setInterval(() => {\r\n if (ticker < props.times) setTicker(ticker + 1);\r\n else clearInterval(interval);\r\n }, props.interval);\r\n };\r\n\r\n const reset = () => {\r\n setTicker(0);\r\n clearInterval(interval);\r\n };\r\n\r\n return (\r\n <div>\r\n <span style={{ fontSize: 100 }}>{ticker}</span>\r\n <button onClick={tick}>Tick!</button>\r\n <button onClick={reset}>Reset</button>\r\n </div>\r\n );\r\n}",
"example": "ReactDOM.render(<Ticker times={5} interval={1000} />, document.getElementById('root'));"
},
"tags": [
"visual",
"state",
"beginner"
]
},
"meta": {
"hash": "d64dece795561241f3b0d7b3fa1e87971994bda2fbe406c6ce740af866e3d9a7"
}
},
{
"id": "Toggle",
"title": "Toggle",
"type": "snippet",
"attributes": {
"fileName": "Toggle.md",
"text": "Renders a toggle component.\n\n- Use the `React.useState()` to initialize the `isToggleOn` state variable to `false`.\n- Use an object, `style`, to hold the styles for individual components and their states.\n- Return a `<button>` that alters the component's `isToggledOn` when its `onClick` event is fired and determine the appearance of the content based on `isToggleOn`, applying the appropriate CSS rules from the `style` object.\n\n",
"codeBlocks": {
"style": "",
"code": "function Toggle(props) {\r\n const [isToggleOn, setIsToggleOn] = React.useState(false);\r\n style = {\r\n on: {\r\n backgroundColor: 'green'\r\n },\r\n off: {\r\n backgroundColor: 'grey'\r\n }\r\n };\r\n\r\n return (\r\n <button onClick={() => setIsToggleOn(!isToggleOn)} style={isToggleOn ? style.on : style.off}>\r\n {isToggleOn ? 'ON' : 'OFF'}\r\n </button>\r\n );\r\n}",
"example": "ReactDOM.render(<Toggle />, document.getElementById('root'));"
},
"tags": [
"visual",
"state",
"beginner"
]
},
"meta": {
"hash": "c921c0adeca337f2cf804acf35e8ab231befdd451d4dcca09a73fecf6ad66836"
}
},
{
"id": "Tooltip",
"title": "Tooltip",
"type": "snippet",
"attributes": {
"fileName": "Tooltip.md",
"text": "Renders a tooltip component.\n\n- Use the `React.useState()` hook to create the `show` variable and initialize it to `false`.\n- Return a `<div>` element that contains the `<div>` that will be the tooltip and the `children` passed to the component.\n- Handle the `onMouseEnter` and `onMouseLeave` methods, by altering the value of the `show` variable.\n\n",
"codeBlocks": {
"style": ".tooltip {\r\n position: relative;\r\n background: rgba(0, 0, 0, 0.7);\r\n color: white;\r\n visibility: hidden;\r\n padding: 5px;\r\n border-radius: 5px;\r\n}\r\n.tooltip-arrow {\r\n position: absolute;\r\n top: 100%;\r\n left: 50%;\r\n border-width: 5px;\r\n border-style: solid;\r\n border-color: rgba(0, 0, 0, 0.7) transparent transparent;\r\n}",
"code": "function Tooltip({ children, text, ...rest }) {\r\n const [show, setShow] = React.useState(false);\r\n\r\n return (\r\n <div>\r\n <div className=\"tooltip\" style={show ? { visibility: 'visible' } : {}}>\r\n {text}\r\n <span className=\"tooltip-arrow\" />\r\n </div>\r\n <div {...rest} onMouseEnter={() => setShow(true)} onMouseLeave={() => setShow(false)}>\r\n {children}\r\n </div>\r\n </div>\r\n );\r\n}",
"example": "ReactDOM.render(\r\n <Tooltip text=\"Simple tooltip\">\r\n <button>Hover me!</button>\r\n </Tooltip>,\r\n document.getElementById('root')\r\n);"
},
"tags": [
"visual",
"state",
"children",
"beginner"
]
},
"meta": {
"hash": "db3b0f49f674b5a64ee1b9a9098aab360331901425faa382c5de5cb8cae33167"
}
},
{
"id": "TreeView",
"title": "TreeView",
"type": "snippet",
"attributes": {
"fileName": "TreeView.md",
"text": "Renders a tree view of a JSON object or array with collapsible content.\n\n- Use object destructuring to set defaults for certain props.\n- Use the value of the `toggled` prop to determine the initial state of the content (collapsed/expanded).\n- Use the `React.setState()` hook to create the `isToggled` state variable and give it the value of the `toggled` prop initially.\n- Return a `<div>` to wrap the contents of the component and the `<span>` element, used to alter the component's `isToggled` state.\n- Determine the appearance of the component, based on `isParentToggled`, `isToggled`, `name` and `Array.isArray()` on `data`.\n- For each child in `data`, determine if it is an object or array and recursively render a sub-tree.\n- Otherwise, render a `<p>` element with the appropriate style.\n\n",
"codeBlocks": {
"style": ".tree-element {\r\n margin: 0;\r\n position: relative;\r\n}\r\n\r\ndiv.tree-element:before {\r\n content: '';\r\n position: absolute;\r\n top: 24px;\r\n left: 1px;\r\n height: calc(100% - 48px);\r\n border-left: 1px solid gray;\r\n}\r\n\r\n.toggler {\r\n position: absolute;\r\n top: 10px;\r\n left: 0px;\r\n width: 0;\r\n height: 0;\r\n border-top: 4px solid transparent;\r\n border-bottom: 4px solid transparent;\r\n border-left: 5px solid gray;\r\n cursor: pointer;\r\n}\r\n\r\n.toggler.closed {\r\n transform: rotate(90deg);\r\n}\r\n\r\n.collapsed {\r\n display: none;\r\n}",
"code": "function TreeView({\r\n data,\r\n toggled = true,\r\n name = null,\r\n isLast = true,\r\n isChildElement = false,\r\n isParentToggled = true\r\n}) {\r\n const [isToggled, setIsToggled] = React.useState(toggled);\r\n\r\n return (\r\n <div\r\n style={{ marginLeft: isChildElement ? 16 : 4 + 'px' }}\r\n className={isParentToggled ? 'tree-element' : 'tree-element collapsed'}\r\n >\r\n <span\r\n className={isToggled ? 'toggler' : 'toggler closed'}\r\n onClick={() => setIsToggled(!isToggled)}\r\n />\r\n {name ? <strong>&nbsp;&nbsp;{name}: </strong> : <span>&nbsp;&nbsp;</span>}\r\n {Array.isArray(data) ? '[' : '{'}\r\n {!isToggled && '...'}\r\n {Object.keys(data).map((v, i, a) =>\r\n typeof data[v] == 'object' ? (\r\n <TreeView\r\n data={data[v]}\r\n isLast={i === a.length - 1}\r\n name={Array.isArray(data) ? null : v}\r\n isChildElement\r\n isParentToggled={isParentToggled && isToggled}\r\n />\r\n ) : (\r\n <p\r\n style={{ marginLeft: 16 + 'px' }}\r\n className={isToggled ? 'tree-element' : 'tree-element collapsed'}\r\n >\r\n {Array.isArray(data) ? '' : <strong>{v}: </strong>}\r\n {data[v]}\r\n {i === a.length - 1 ? '' : ','}\r\n </p>\r\n )\r\n )}\r\n {Array.isArray(data) ? ']' : '}'}\r\n {!isLast ? ',' : ''}\r\n </div>\r\n );\r\n}",
"example": "let data = {\r\n lorem: {\r\n ipsum: 'dolor sit',\r\n amet: {\r\n consectetur: 'adipiscing',\r\n elit: [\r\n 'duis',\r\n 'vitae',\r\n {\r\n semper: 'orci'\r\n },\r\n {\r\n est: 'sed ornare'\r\n },\r\n 'etiam',\r\n ['laoreet', 'tincidunt'],\r\n ['vestibulum', 'ante']\r\n ]\r\n },\r\n ipsum: 'primis'\r\n }\r\n};\r\nReactDOM.render(<TreeView data={data} name=\"data\" />, document.getElementById('root'));"
},
"tags": [
"visual",
"object",
"state",
"recursion",
"advanced"
]
},
"meta": {
"hash": "305bd408515e5c575411f40b1bb24f75f6c776cd6387bb083818f2feb9175a70"
}
},
{
"id": "UncontrolledInput",
"title": "UncontrolledInput",
"type": "snippet",
"attributes": {
"fileName": "UncontrolledInput.md",
"text": "Renders an `<input>` element that uses a callback function to pass its value to the parent component.\n\n- Use object destructuring to set defaults for certain attributes of the `<input>` element.\n- Render an `<input>` element with the appropriate attributes and use the `callback` function in the `onChange` event to pass the value of the input to the parent.\n\n",
"codeBlocks": {
"style": "",
"code": "function UncontrolledInput({\r\n callback,\r\n type = 'text',\r\n disabled = false,\r\n readOnly = false,\r\n placeholder = ''\r\n}) {\r\n return (\r\n <input\r\n type={type}\r\n disabled={disabled}\r\n readOnly={readOnly}\r\n placeholder={placeholder}\r\n onChange={({ target: { value } }) => callback(value)}\r\n />\r\n );\r\n}",
"example": "ReactDOM.render(\r\n <UncontrolledInput\r\n type=\"text\"\r\n placeholder=\"Insert some text here...\"\r\n callback={val => console.log(val)}\r\n />,\r\n document.getElementById('root')\r\n);"
},
"tags": [
"input",
"beginner"
]
},
"meta": {
"hash": "5aff6673123594949dd0d1e72c8fbb586f402b2eb9fdcf8dd9a2b789b4089adb"
}
},
{
"id": "useClickInside",
"title": "useClickInside",
"type": "snippet",
"attributes": {
"fileName": "useClickInside.md",
"text": "A hook that handles the event of clicking inside the wrapped component.\n\n- Create a custom hook that takes a `ref` and a `callback` to handle the `click` event.\n- Use the `React.useEffect()` hook to append and clean up the `click` event.\n- Use the `React.useRef()` hook to create a `ref` for your click component and pass it to the `useClickInside` hook.\n\n",
"codeBlocks": {
"style": "",
"code": "const useClickInside = (ref, callback) => {\r\n const handleClick = e => {\r\n if (ref.current && ref.current.contains(e.target)) {\r\n callback();\r\n }\r\n };\r\n React.useEffect(() => {\r\n document.addEventListener('click', handleClick);\r\n return () => {\r\n document.removeEventListener('click', handleClick);\r\n };\r\n });\r\n};",
"example": "const ClickBox = ({ onClickInside }) => {\r\n const clickRef = React.useRef();\r\n useClickInside(clickRef, onClickInside);\r\n return (\r\n <div\r\n className=\"click-box\"\r\n ref={clickRef}\r\n style={{\r\n border: '2px dashed orangered',\r\n height: 200,\r\n width: 400,\r\n display: 'flex',\r\n justifyContent: 'center',\r\n alignItems: 'center'\r\n }}\r\n >\r\n <p>Click inside this element</p>\r\n </div>\r\n );\r\n};\r\n\r\nReactDOM.render(\r\n <ClickBox onClickInside={() => alert('click inside')} />,\r\n document.getElementById('root')\r\n);"
},
"tags": [
"hooks",
"effect",
"event",
"intermediate"
]
},
"meta": {
"hash": "f2e5c4b9e8c44a5fb9a1f8002cfbeb2d6090cfe97d4d15dcc575ce89611bb599"
}
},
{
"id": "useClickOutside",
"title": "useClickOutside",
"type": "snippet",
"attributes": {
"fileName": "useClickOutside.md",
"text": "A hook that handles the event of clicking outside of the wrapped component.\n\n- Create a custom hook that takes a `ref` and a `callback` to handle the `click` event.\n- Use the `React.useEffect()` hook to append and clean up the `click` event.\n- Use the `React.useRef()` hook to create a `ref` for your click component and pass it to the `useClickOutside` hook.\n\n",
"codeBlocks": {
"style": "",
"code": "const useClickOutside = (ref, callback) => {\r\n const handleClick = e => {\r\n if (ref.current && !ref.current.contains(e.target)) {\r\n callback();\r\n }\r\n };\r\n React.useEffect(() => {\r\n document.addEventListener('click', handleClick);\r\n return () => {\r\n document.removeEventListener('click', handleClick);\r\n };\r\n });\r\n};",
"example": "const ClickBox = ({ onClickOutside }) => {\r\n const clickRef = React.useRef();\r\n useClickOutside(clickRef, onClickOutside);\r\n return (\r\n <div\r\n className=\"click-box\"\r\n ref={clickRef}\r\n style={{\r\n border: '2px dashed orangered',\r\n height: 200,\r\n width: 400,\r\n display: 'flex',\r\n justifyContent: 'center',\r\n alignItems: 'center'\r\n }}\r\n >\r\n <p>Click out of this element</p>\r\n </div>\r\n );\r\n};\r\n\r\nReactDOM.render(\r\n <ClickBox onClickOutside={() => alert('click outside')} />,\r\n document.getElementById('root')\r\n);"
},
"tags": [
"hooks",
"effect",
"event",
"intermediate"
]
},
"meta": {
"hash": "e8f3e64cd0cf616dd42843328234e37b1b7a77ed51da8956204fdb02e088ce33"
}
},
{
"id": "useFetch",
"title": "useFetch",
"type": "snippet",
"attributes": {
"fileName": "useFetch.md",
"text": "A hook that implements `fetch` in a declarative manner.\n\n- Create a custom hook that takes a `url` and `options`.\n- Use the `React.useState()` hook to initialize the `response` and `error` state variables.\n- Use the `React.useEffect()` hook to anychronously call `fetch()` and update the state varaibles accordingly.\n- Return an object containting the `response` and `error` state variables.\n\n",
"codeBlocks": {
"style": "",
"code": "const useFetch = (url, options) => {\r\n const [response, setResponse] = React.useState(null);\r\n const [error, setError] = React.useState(null);\r\n\r\n React.useEffect(() => {\r\n const fetchData = async () => {\r\n try {\r\n const res = await fetch(url, options);\r\n const json = await res.json();\r\n setResponse(json);\r\n } catch (error) {\r\n setError(error);\r\n }\r\n };\r\n fetchData();\r\n }, []);\r\n\r\n return { response, error };\r\n};",
"example": "const ImageFetch = props => {\r\n const res = useFetch('https://dog.ceo/api/breeds/image/random', {});\r\n if (!res.response) {\r\n return <div>Loading...</div>;\r\n }\r\n const dogName = res.response.status;\r\n const imageUrl = res.response.message;\r\n return (\r\n <div>\r\n <img src={imageUrl} alt=\"avatar\" width={400} height=\"auto\" />\r\n </div>\r\n );\r\n};\r\n\r\nReactDOM.render(<ImageFetch />, document.getElementById('root'));"
},
"tags": [
"hooks",
"effect",
"state",
"intermediate"
]
},
"meta": {
"hash": "848cb6ba4f8bd5dad012a38e6bd0e7c829a79b3215a23939c30a3f652627da4f"
}
},
{
"id": "useInterval",
"title": "useInterval",
"type": "snippet",
"attributes": {
"fileName": "useInterval.md",
"text": "A hook that implements `setInterval` in a declarative manner.\n\n- Create a custom hook that takes a `callback` and a `delay`.\n- Use the `React.useRef()` hook to create a `ref` for the callback function.\n- Use the `React.useEffect()` hook to remember the latest callback.\n- Use the `Rect.useEffect()` hook to set up the interval and clean up.\n\n",
"codeBlocks": {
"style": "",
"code": "const useInterval = (callback, delay) => {\r\n const savedCallback = React.useRef();\r\n\r\n React.useEffect(() => {\r\n savedCallback.current = callback;\r\n }, [callback]);\r\n\r\n React.useEffect(() => {\r\n function tick() {\r\n savedCallback.current();\r\n }\r\n if (delay !== null) {\r\n let id = setInterval(tick, delay);\r\n return () => clearInterval(id);\r\n }\r\n }, [delay]);\r\n};",
"example": "const Timer = props => {\r\n const [seconds, setSeconds] = React.useState(0);\r\n useInterval(() => {\r\n setSeconds(seconds + 1);\r\n }, 1000);\r\n\r\n return <p>{seconds}</p>;\r\n};\r\n\r\nReactDOM.render(<Timer />, document.getElementById('root'));"
},
"tags": [
"hooks",
"effect",
"intermediate"
]
},
"meta": {
"hash": "04b22b339da3652cd044203a1d9723af542afe13572a38da825bd093ab1c99af"
}
},
{
"id": "useTimeout",
"title": "useTimeout",
"type": "snippet",
"attributes": {
"fileName": "useTimeout.md",
"text": "A hook that implements `setTimeout` in a declarative manner.\n\n- Create a custom hook that takes a `callback` and a `delay`.\n- Use the `React.useRef()` hook to create a `ref` for the callback function.\n- Use the `React.useEffect()` hook to remember the latest callback.\n- Use the `Rect.useEffect()` hook to set up the timeout and clean up.\n\n",
"codeBlocks": {
"style": "",
"code": "const useTimeout = (callback, delay) => {\r\n const savedCallback = React.useRef();\r\n\r\n React.useEffect(() => {\r\n savedCallback.current = callback;\r\n }, [callback]);\r\n\r\n React.useEffect(() => {\r\n function tick() {\r\n savedCallback.current();\r\n }\r\n if (delay !== null) {\r\n let id = setTimeout(tick, delay);\r\n return () => clearTimeout(id);\r\n }\r\n }, [delay]);\r\n};",
"example": "const OneSecondTimer = props => {\r\n const [seconds, setSeconds] = React.useState(0);\r\n useTimeout(() => {\r\n setSeconds(seconds + 1);\r\n }, 1000);\r\n\r\n return <p>{seconds}</p>;\r\n};\r\n\r\nReactDOM.render(<OneSecondTimer />, document.getElementById('root'));"
},
"tags": [
"hooks",
"effect",
"intermediate"
]
},
"meta": {
"hash": "507c6fc6be62127e00738e39c6b22686a100b8af7252e1bbe589480b126c3d79"
}
}
],
"meta": {
"specification": "http://jsonapi.org/format/",
"type": "snippetArray"
}
}

View File

@ -1,12 +1,15 @@
### Accordion
---
title: Accordion
tags: visual,children,state,advanced
---
Renders an accordion menu with multiple collapsible content components.
* Define an `AccordionItem` component, pass it to the `Accordion` and remove unnecessary nodes expect for `AccordionItem` by identifying the function's name in `props.children`.
* Each `AccordionItem` component renders a `<button>` that is used to update the `Accordion` via the `props.handleClick` callback and the content of the component, passed down via `props.children`, while its appearance is determined by `props.isCollapsed` and based on `style`.
* In the `Accordion` component, use the `React.useState()` hook to initialize the value of the `bindIndex` state variable to `props.defaultIndex`.
* Use `Array.prototype.map` on the collected nodes to render the individual collapsiple elements.
* Define `changeItem`, which will be executed when clicking an `AccordionItem`'s `<button>`.
- Define an `AccordionItem` component, pass it to the `Accordion` and remove unnecessary nodes expect for `AccordionItem` by identifying the function's name in `props.children`.
- Each `AccordionItem` component renders a `<button>` that is used to update the `Accordion` via the `props.handleClick` callback and the content of the component, passed down via `props.children`, while its appearance is determined by `props.isCollapsed` and based on `style`.
- In the `Accordion` component, use the `React.useState()` hook to initialize the value of the `bindIndex` state variable to `props.defaultIndex`.
- Use `Array.prototype.map` on the collected nodes to render the individual collapsiple elements.
- Define `changeItem`, which will be executed when clicking an `AccordionItem`'s `<button>`.
`changeItem` executes the passed callback, `onItemClick` and updates `bindIndex` based on the clicked element.
```jsx
@ -77,7 +80,3 @@ ReactDOM.render(
document.getElementById('root')
);
```
<!-- tags: visual,children,state -->
<!-- expertise: 2 -->

View File

@ -1,9 +1,12 @@
### AutoLink
---
title: AutoLink
tags: visual,string,fragment,regexp,advanced
---
Renders a string as plaintext, with URLs converted to appropriate `<a>` elements.
* Use `String.prototype.split()` and `String.prototype.match()` with a regular expression to find URLs in a string.
* Return a `<React.Fragment>` with matched URLs rendered as `<a>` elements, dealing with missing protocol prefixes if necessary, and the rest of the string rendered as plaintext.
- Use `String.prototype.split()` and `String.prototype.match()` with a regular expression to find URLs in a string.
- Return a `<React.Fragment>` with matched URLs rendered as `<a>` elements, dealing with missing protocol prefixes if necessary, and the rest of the string rendered as plaintext.
```jsx
function AutoLink({ text }) {
@ -30,7 +33,3 @@ ReactDOM.render(
document.getElementById('root')
);
```
<!-- tags: string,fragment,regexp -->
<!-- expertise: 2 -->

View File

@ -1,12 +1,15 @@
### Carousel
---
title: Carousel
tags: visual,children,state,effect,intermediate
---
Renders a carousel component.
* Use the `React.setState()` hook to create the `active` state variable and give it a value of `0` (index of the first item).
* Use an object, `style`, to hold the styles for the individual components.
* Use the `React.setEffect()` hook to update the value of `active` to the index of the next item, using `setTimeout`.
* Destructure `props`, compute if visibility style should be set to `visible` or not for each carousel item while mapping over and applying the combined style to the carousel item component accordingly.
* Render the carousel items using `React.cloneElement()` and pass down rest `props` along with the computed styles.
- Use the `React.setState()` hook to create the `active` state variable and give it a value of `0` (index of the first item).
- Use an object, `style`, to hold the styles for the individual components.
- Use the `React.setEffect()` hook to update the value of `active` to the index of the next item, using `setTimeout`.
- Destructure `props`, compute if visibility style should be set to `visible` or not for each carousel item while mapping over and applying the combined style to the carousel item component accordingly.
- Render the carousel items using `React.cloneElement()` and pass down rest `props` along with the computed styles.
```jsx
function Carousel(props) {
@ -60,7 +63,3 @@ ReactDOM.render(
document.getElementById('root')
);
```
<!-- tags: visual,children,state,effect -->
<!-- expertise: 2 -->

View File

@ -1,73 +0,0 @@
### ClickInside and ClickOutside
Two handy hooks to handle the click outside and inside event on the wrapped component.
* Create customized hooks that take in a `ref` component(node) and a `callback` function to hanlde the customized `click` event
* Use the `React.useEffect()` hook to append and clean up the `click` event.
* Use the `React.useRef()` hook to create a `ref` for your click component and pass it to `useClickInside` and `useClickOutside` hooks.
```css
.click-box {
border: 2px dashed orangered;
height: 200px;
width: 400px;
display: flex;
justify-content: center;
align-items: center;
}
p {
border: 2px solid blue;
padding: 16px;
}
```
```jsx
const useClickInside = (ref, callback) => {
const handleClick = e => {
//use the node contains to verify if we click inside
if (ref.current && ref.current.contains(e.target)) {
callback();
}
};
//clean up using useEffect
useEffect(() => {
document.addEventListener("click", handleClick);
return () => {
document.removeEventListener("click", handleClick);
};
});
};
const useClickOutside = (ref, callback) => {
const handleClick = e => {
//use the node contains to verify if we click outside
if (ref.current && !ref.current.contains(e.target)) {
callback();
}
};
// clean up using useEffect
useEffect(() => {
document.addEventListener("click", handleClick);
return () => {
document.removeEventListener("click", handleClick);
};
});
};
function ClickBox({onClickOutside,onClickInside}) {
const clickRef = useRef();
useClickOutside(clickRef, onClickOutside);
useClickInside(clickRef, onClickInside);
return (
<div className="click-box" ref={clickRef}>
<p>Hello Click Me Inside!</p>
</div>
);
}
```
```jsx
ReactDOM.render(<ClickBox onClickOutside={()=> alert("click outside")} onClickInside={()=> alert("click inside")}/>,document.getElementById('root'))
```

View File

@ -1,12 +1,15 @@
### Collapse
---
title: Collapse
tags: visual,children,state,intermediate
---
Renders a component with collapsible content.
* Use the `React.setState()` hook to create the `isCollapsed` state variable with an initial value of `props.collapsed`.
* Use an object, `style`, to hold the styles for individual components and their states.
* Use a `<div>` to wrap both the `<button>` that alters the component's `isCollapsed` state and the content of the component, passed down via `props.children`.
* Determine the appearance of the content, based on `isCollapsed` and apply the appropriate CSS rules from the `style` object.
* Finally, update the value of the `aria-expanded` attribute based on `isCollapsed` to make the component accessible.
- Use the `React.setState()` hook to create the `isCollapsed` state variable with an initial value of `props.collapsed`.
- Use an object, `style`, to hold the styles for individual components and their states.
- Use a `<div>` to wrap both the `<button>` that alters the component's `isCollapsed` state and the content of the component, passed down via `props.children`.
- Determine the appearance of the content, based on `isCollapsed` and apply the appropriate CSS rules from the `style` object.
- Finally, update the value of the `aria-expanded` attribute based on `isCollapsed` to make the component accessible.
```jsx
function Collapse(props) {
@ -51,7 +54,3 @@ ReactDOM.render(
document.getElementById('root')
);
```
<!-- tags: visual,children,state -->
<!-- expertise: 2 -->

View File

@ -0,0 +1,50 @@
---
title: ControlledInput
tags: input,state,effect,intermediate
---
Renders an `<input>` element with internal state, that uses a callback function to pass its value to the parent component.
- Use object destructuring to set defaults for certain attributes of the `<input>` element.
- Use the `React.setState()` hook to create the `value` state variable and give it a value of equal to the `defaultValue` prop.
- Use the `React.useEffect()` hook with a second parameter set to the `value` state variable to call the `callback` function every time `value` is updated.
- Render an `<input>` element with the appropriate attributes and use the the `onChange` event to upda the `value` state variable.
```jsx
function ControlledInput({
callback,
type = 'text',
disabled = false,
readOnly = false,
defaultValue,
placeholder = ''
}) {
const [value, setValue] = React.useState(defaultValue);
React.useEffect(() => {
callback(value);
}, [value]);
return (
<input
defaultValue={defaultValue}
type={type}
disabled={disabled}
readOnly={readOnly}
placeholder={placeholder}
onChange={({ target: { value } }) => setValue(value)}
/>
);
}
```
```jsx
ReactDOM.render(
<ControlledInput
type="text"
placeholder="Insert some text here..."
callback={val => console.log(val)}
/>,
document.getElementById('root')
);
```

View File

@ -1,15 +1,18 @@
### CountDown
---
title: CountDown
tags: visual,state,advanced
---
Renders a countdown timer that prints a message when it reaches zero.
* Use object destructuring to set defaults for the `hours`, `minutes` and `seconds` props.
* Use the `React.useState()` hook to create the `time`, `paused` and `over` state variables and set their values to the values of the passed props, `false` and `false` respectively.
* Create a method `tick`, that updates the value of `time` based on the current value (i.e. decreasing the time by one second).
* If `paused` or `over` is `true`, `tick` will return immediately.
* Create a method `reset`, that resets all state variables to their initial states.
* Use the the `React.useEffect()` hook to call the `tick` method every second via the use of `setInterval()` and use `clearInterval()` to cleanup when the component is unmounted.
* Use a `<div>` to wrap a `<p>` element with the textual representation of the components `time` state variable, as well as two `<button>` elements that will pause/unpause and restart the timer respectively.
* If `over` is `true`, the timer will display a message instead of the value of `time`.
- Use object destructuring to set defaults for the `hours`, `minutes` and `seconds` props.
- Use the `React.useState()` hook to create the `time`, `paused` and `over` state variables and set their values to the values of the passed props, `false` and `false` respectively.
- Create a method `tick`, that updates the value of `time` based on the current value (i.e. decreasing the time by one second).
- If `paused` or `over` is `true`, `tick` will return immediately.
- Create a method `reset`, that resets all state variables to their initial states.
- Use the the `React.useEffect()` hook to call the `tick` method every second via the use of `setInterval()` and use `clearInterval()` to cleanup when the component is unmounted.
- Use a `<div>` to wrap a `<p>` element with the textual representation of the components `time` state variable, as well as two `<button>` elements that will pause/unpause and restart the timer respectively.
- If `over` is `true`, the timer will display a message instead of the value of `time`.
```jsx
function CountDown({ hours = 0, minutes = 0, seconds = 0 }) {
@ -75,7 +78,3 @@ function CountDown({ hours = 0, minutes = 0, seconds = 0 }) {
```jsx
ReactDOM.render(<CountDown hours="1" minutes="45" />, document.getElementById('root'));
```
<!-- tags: visual,state -->
<!-- expertise: 2 -->

View File

@ -1,10 +1,13 @@
### DataList
---
title: DataList
tags: array,beginner
---
Renders a list of elements from an array of primitives.
* Use the value of the `isOrdered` prop to conditionally render a `<ol>` or `<ul>` list.
* Use `Array.prototype.map` to render every item in `data` as a `<li>` element, give it a `key` produced from the concatenation of the its index and value.
* Omit the `isOrdered` prop to render a `<ul>` list by default.
- Use the value of the `isOrdered` prop to conditionally render a `<ol>` or `<ul>` list.
- Use `Array.prototype.map` to render every item in `data` as a `<li>` element, give it a `key` produced from the concatenation of the its index and value.
- Omit the `isOrdered` prop to render a `<ul>` list by default.
```jsx
function DataList({ isOrdered, data }) {
@ -18,7 +21,3 @@ const names = ['John', 'Paul', 'Mary'];
ReactDOM.render(<DataList data={names} />, document.getElementById('root'));
ReactDOM.render(<DataList data={names} isOrdered />, document.getElementById('root'));
```
<!-- tags: array -->
<!-- expertise: 0 -->

View File

@ -1,9 +1,12 @@
### DataTable
---
title: DataTable
tags: array,beginner
---
Renders a table with rows dynamically created from an array of primitives.
* Render a `<table>` element with two columns (`ID` and `Value`).
* Use `Array.prototype.map` to render every item in `data` as a `<tr>` element, consisting of its index and value, give it a `key` produced from the concatenation of the two.
- Render a `<table>` element with two columns (`ID` and `Value`).
- Use `Array.prototype.map` to render every item in `data` as a `<tr>` element, consisting of its index and value, give it a `key` produced from the concatenation of the two.
```jsx
function DataTable({ data }) {
@ -32,7 +35,3 @@ function DataTable({ data }) {
const people = ['John', 'Jesse'];
ReactDOM.render(<DataTable data={people} />, document.getElementById('root'));
```
<!-- tags: array -->
<!-- expertise: 0 -->

View File

@ -1,15 +1,18 @@
### FileDrop
---
title: FileDrop
tags: visual,input,state,effect,event,intermediate
---
Renders a file drag and drop component for a single file.
* Create a ref called `dropRef` for this component.
* Use the `React.useState()` hook to create the `drag` and `filename` variables, initialized to `false` and `''` respectively.
- Create a ref called `dropRef` for this component.
- Use the `React.useState()` hook to create the `drag` and `filename` variables, initialized to `false` and `''` respectively.
The variables `dragCounter` and `drag` are used to determine if a file is being dragged, while `filename` is used to store the dropped file's name.
* Create the `handleDrag`, `handleDragIn`, `handleDragOut` and `handleDrop` methods to handle drag and drop functionality, bind them to the component's context.
* Each of the methods will handle a specific event, the listeners for which are created and removed in the `React.useEffect()` hook and its attached `cleanup()` method.
* `handleDrag` prevents the browser from opening the dragged file, `handleDragIn` and `handleDragOut` handle the dragged file entering and exiting the component, while `handleDrop` handles the file being dropped and passes it to `props.handleDrop`.
* Return an appropriately styled `<div>` and use `drag` and `filename` to determine its contents and style.
* Finally, bind the `ref` of the created `<div>` to `dropRef`.
- Create the `handleDrag`, `handleDragIn`, `handleDragOut` and `handleDrop` methods to handle drag and drop functionality, bind them to the component's context.
- Each of the methods will handle a specific event, the listeners for which are created and removed in the `React.useEffect()` hook and its attached `cleanup()` method.
- `handleDrag` prevents the browser from opening the dragged file, `handleDragIn` and `handleDragOut` handle the dragged file entering and exiting the component, while `handleDrop` handles the file being dropped and passes it to `props.handleDrop`.
- Return an appropriately styled `<div>` and use `drag` and `filename` to determine its contents and style.
- Finally, bind the `ref` of the created `<div>` to `dropRef`.
```css
.filedrop {
@ -96,7 +99,3 @@ function FileDrop(props) {
```jsx
ReactDOM.render(<FileDrop handleDrop={console.log} />, document.getElementById('root'));
```
<!-- tags: visual,input,state,effect -->
<!-- expertise: 2 -->

View File

@ -1,11 +1,14 @@
### LimitedTextarea
---
title: LimitedTextarea
tags: input,state,effect,event,beginner
---
Renders a textarea component with a character limit.
* Use the `React.useState()` hook to create the `content` state variable and set its value to `value`.
- Use the `React.useState()` hook to create the `content` state variable and set its value to `value`.
Create a method `setFormattedContent`, which trims the content of the input if it's longer than `limit`.
* Use the `React.useEffect()` hook to call the `setFormattedContent` method on the value of the `content` state variable.
* Use a`<div>` to wrap both the`<textarea>` and the `<p>` element that displays the character count and bind the `onChange` event of the `<textarea>` to call `setFormattedContent` with the value of `event.target.value`.
- Use the `React.useEffect()` hook to call the `setFormattedContent` method on the value of the `content` state variable.
- Use a`<div>` to wrap both the`<textarea>` and the `<p>` element that displays the character count and bind the `onChange` event of the `<textarea>` to call `setFormattedContent` with the value of `event.target.value`.
```jsx
function LimitedTextarea({ rows, cols, value, limit }) {
@ -38,7 +41,3 @@ function LimitedTextarea({ rows, cols, value, limit }) {
```jsx
ReactDOM.render(<LimitedTextarea limit={32} value="Hello!" />, document.getElementById('root'));
```
<!-- tags: input,state,effect -->
<!-- expertise: 0 -->

View File

@ -1,12 +1,15 @@
### LimitedWordTextarea
---
title: LimitedWordTextarea
tags: input,state,effect,event,beginner
---
Renders a textarea component with a word limit.
* Use the `React.useState()` hook to create the `content` and `wordCount` state variables and set their values to `value` and `0` respectively.
* Create a method `setFormattedContent`, which uses `String.prototype.split(' ')` to turn the input into an array of words and check if the result of applying `Array.prototype.filter(Boolean)` has a `length` longer than `limit`.
* If the afforementioned `length` exceeds the `limit`, trim the input, otherwise return the raw input, updating `content` and `wordCount` accordingly in both cases.
* Use the `React.useEffect()` hook to call the `setFormattedContent` method on the value of the `content` state variable.
* Use a`<div>` to wrap both the`<textarea>` and the `<p>` element that displays the character count and bind the `onChange` event of the `<textarea>` to call `setFormattedContent` with the value of `event.target.value`.
- Use the `React.useState()` hook to create the `content` and `wordCount` state variables and set their values to `value` and `0` respectively.
- Create a method `setFormattedContent`, which uses `String.prototype.split(' ')` to turn the input into an array of words and check if the result of applying `Array.prototype.filter(Boolean)` has a `length` longer than `limit`.
- If the afforementioned `length` exceeds the `limit`, trim the input, otherwise return the raw input, updating `content` and `wordCount` accordingly in both cases.
- Use the `React.useEffect()` hook to call the `setFormattedContent` method on the value of the `content` state variable.
- Use a`<div>` to wrap both the`<textarea>` and the `<p>` element that displays the character count and bind the `onChange` event of the `<textarea>` to call `setFormattedContent` with the value of `event.target.value`.
```jsx
function LimitedWordTextarea({ rows, cols, value, limit }) {
@ -55,7 +58,3 @@ ReactDOM.render(
document.getElementById('root')
);
```
<!-- tags: input,state,effect -->
<!-- expertise: 0 -->

View File

@ -1,9 +1,12 @@
### Mailto
---
title: Mailto
tags: visual,beginner
---
Renders a link formatted to send an email.
* Destructure the component's props, use `email`, `subject` and `body` to create a `<a>` element with an appropriate `href` attribute.
* Render the link with `props.children` as its content.
- Destructure the component's props, use `email`, `subject` and `body` to create a `<a>` element with an appropriate `href` attribute.
- Render the link with `props.children` as its content.
```jsx
function Mailto({ email, subject, body, ...props }) {
@ -21,7 +24,3 @@ ReactDOM.render(
document.getElementById('root')
);
```
<!-- tags: visual -->
<!-- expertise: 0 -->

View File

@ -1,11 +1,16 @@
### MappedTable
---
title: MappedTable
tags: array,object,intermediate
---
Renders a table with rows dynamically created from an array of objects and a list of property names.
* Use `Object.keys()`, `Array.prototype.filter()`, `Array.prototype.includes()` and `Array.prototype.reduce()` to produce a `filteredData` array, containing all objects with the keys specified in `propertyNames`.
* Render a `<table>` element with a set of columns equal to the amount of values in `propertyNames`.
* Use `Array.prototype.map` to render each value in the `propertyNames` array as a `<th>` element.
* Use `Array.prototype.map` to render each object in the `filteredData` array as a `<tr>` element, containing a `<td>` for each key in the object.
- Use `Object.keys()`, `Array.prototype.filter()`, `Array.prototype.includes()` and `Array.prototype.reduce()` to produce a `filteredData` array, containing all objects with the keys specified in `propertyNames`.
- Render a `<table>` element with a set of columns equal to the amount of values in `propertyNames`.
- Use `Array.prototype.map` to render each value in the `propertyNames` array as a `<th>` element.
- Use `Array.prototype.map` to render each object in the `filteredData` array as a `<tr>` element, containing a `<td>` for each key in the object.
_This component does not work with nested objects and will break if there are nested objects inside any of the properties specified in `propertyNames`_
```jsx
function MappedTable({ data, propertyNames }) {
@ -48,11 +53,3 @@ ReactDOM.render(
document.getElementById('root')
);
```
#### Notes:
- This component does not work with nested objects and will break if there are nested objects inside any of the properties specified in `propertyNames`.
<!-- tags: array,object -->
<!-- expertise: 1 -->

View File

@ -1,13 +1,16 @@
### Modal
---
title: Modal
tags: visual,effect,intermediate
---
Renders a Modal component, controllable through events.
To use the component, import `Modal` only once and then display it by passing a boolean value to the `isVisible` attribute.
* Use object destructuring to set defaults for certain attributes of the modal component.
* Define `keydownHandler`, a method which handles all keyboard events, which can be used according to your needs to dispatch actions (e.g. close the modal when <kbd>Esc</kbd> is pressed).
* Use `React.useEffect()` hook to add or remove the `keydown` event listener, which calls `keydownHandler`.
* Use the `isVisible` prop to determine if the modal should be shown or not.
* Use CSS to style and position the modal component.
- Use object destructuring to set defaults for certain attributes of the modal component.
- Define `keydownHandler`, a method which handles all keyboard events, which can be used according to your needs to dispatch actions (e.g. close the modal when <kbd>Esc</kbd> is pressed).
- Use `React.useEffect()` hook to add or remove the `keydown` event listener, which calls `keydownHandler`.
- Use the `isVisible` prop to determine if the modal should be shown or not.
- Use CSS to style and position the modal component.
```css
.modal {
@ -44,7 +47,8 @@ To use the component, import `Modal` only once and then display it by passing a
animation-duration: 0.5s;
}
.modal-header,.modal-footer{
.modal-header,
.modal-footer {
display: flex;
align-items: center;
padding: 1rem;
@ -70,12 +74,20 @@ To use the component, import `Modal` only once and then display it by passing a
}
@keyframes appear {
from {opacity: 0;}
to {opacity: 1;}
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slide-in {
from {transform: translateY(-150px);}
to { transform: translateY(0);}
from {
transform: translateY(-150px);
}
to {
transform: translateY(0);
}
}
```
@ -88,7 +100,9 @@ function Modal({ isVisible = false, title, content, footer, onClose }){
function keydownHandler({ key }) {
switch (key) {
case 'Escape': onClose(); break;
case 'Escape':
onClose();
break;
default:
}
}
@ -98,7 +112,9 @@ function Modal({ isVisible = false, title, content, footer, onClose }){
<div className="modal-dialog" onClick={e => e.stopPropagation()}>
<div className="modal-header">
<h3 className="modal-title">{title}</h3>
<span className="modal-close" onClick={onClose}>&times;</span>
<span className="modal-close" onClick={onClose}>
&times;
</span>
</div>
<div className="modal-body">
<div className="modal-content">{content}</div>
@ -106,7 +122,7 @@ function Modal({ isVisible = false, title, content, footer, onClose }){
{footer && <div className="modal-footer">{footer}</div>}
</div>
</div>
)
);
}
```
@ -126,12 +142,8 @@ function App() {
onClose={() => setModal(false)}
/>
</React.Fragment>
)
);
}
ReactDOM.render(<App />, document.getElementById('root'));
```
<!-- tags: visual,effect -->
<!-- expertise: 2 -->

View File

@ -1,11 +1,14 @@
### MultiselectCheckbox
---
title: MultiselectCheckbox
tags: input,state,array,intermediate
---
Renders a checkbox list that uses a callback function to pass its selected value/values to the parent component.
* Use `React.setState()` to create a `data` state variable and set its initial value equal to the `options` prop.
* Create a function `toggle` that is used to toggle the `checked` to update the `data` state variable and call the `onChange` callback passed via the component's props.
* Render a `<ul>` element and use `Array.prototype.map()` to map the `data` state variable to individual `<li>` elements with `<input>` elements as their children.
* Each `<input>` element has the `type='checkbox'` attribute and is marked as `readOnly`, as its click events are handled by the parent `<li>` element's `onClick` handler.
- Use `React.setState()` to create a `data` state variable and set its initial value equal to the `options` prop.
- Create a function `toggle` that is used to toggle the `checked` to update the `data` state variable and call the `onChange` callback passed via the component's props.
- Render a `<ul>` element and use `Array.prototype.map()` to map the `data` state variable to individual `<li>` elements with `<input>` elements as their children.
- Each `<input>` element has the `type='checkbox'` attribute and is marked as `readOnly`, as its click events are handled by the parent `<li>` element's `onClick` handler.
```jsx
const style = {
@ -58,7 +61,3 @@ ReactDOM.render(
document.getElementById('root')
);
```
<!-- tags: input,state,array -->
<!-- expertise: 1 -->

View File

@ -1,9 +1,12 @@
### PasswordRevealer
---
title: PasswordRevealer
tags: input,state,beginner
---
Renders a password input field with a reveal button.
* Use the `React.useState()` hook to create the `shown` state variable and set its value to `false`.
* Use a`<div>` to wrap both the`<input>` and the `<button>` element that toggles the type of the input field between `"text"` and `"password"`.
- Use the `React.useState()` hook to create the `shown` state variable and set its value to `false`.
- Use a`<div>` to wrap both the`<input>` and the `<button>` element that toggles the type of the input field between `"text"` and `"password"`.
```jsx
function PasswordRevealer({ value }) {
@ -21,7 +24,3 @@ function PasswordRevealer({ value }) {
```jsx
ReactDOM.render(<PasswordRevealer />, document.getElementById('root'));
```
<!--tags: input,state -->
<!--expertise: 0 -->

View File

@ -1,10 +1,13 @@
### Select
---
title: Select
tags: input,beginner
---
Renders a `<select>` element that uses a callback function to pass its value to the parent component.
* Use object destructuring to set defaults for certain attributes of the `<select>` element.
* Render a `<select>` element with the appropriate attributes and use the `callback` function in the `onChange` event to pass the value of the textarea to the parent.
* Use destructuring on the `values` array to pass an array of `value` and `text` elements and the `selected` attribute to define the initial `value` of the `<select>` element.
- Use object destructuring to set defaults for certain attributes of the `<select>` element.
- Render a `<select>` element with the appropriate attributes and use the `callback` function in the `onChange` event to pass the value of the textarea to the parent.
- Use destructuring on the `values` array to pass an array of `value` and `text` elements and the `selected` attribute to define the initial `value` of the `<select>` element.
```jsx
function Select({ values, callback, disabled = false, readonly = false, selected }) {
@ -36,7 +39,3 @@ ReactDOM.render(
document.getElementById('root')
);
```
<!-- tags: input -->
<!-- expertise: 0 -->

View File

@ -1,9 +1,12 @@
### Slider
---
title: Slider
tags: input,beginner
---
Renders a slider element that uses a callback function to pass its value to the parent component.
* Use object destructuring to set defaults for certain attributes of the `<input>` element.
* Render an `<input>` element of type `"range"` and the appropriate attributes, use the `callback` function in the `onChange` event to pass the value of the input to the parent.
- Use object destructuring to set defaults for certain attributes of the `<input>` element.
- Render an `<input>` element of type `"range"` and the appropriate attributes, use the `callback` function in the `onChange` event to pass the value of the input to the parent.
```jsx
function Slider({ callback, disabled = false, readOnly = false }) {
@ -21,7 +24,3 @@ function Slider({ callback, disabled = false, readOnly = false }) {
```jsx
ReactDOM.render(<Slider callback={val => console.log(val)} />, document.getElementById('root'));
```
<!-- tags: input -->
<!-- expertise: 0 -->

View File

@ -1,12 +1,15 @@
### StarRating
---
title: StarRating
tags: visual,children,input,state,intermediate
---
Renders a star rating component.
* Define a component, called `Star` that will render each individual star with the appropriate appearance, based on the parent component's state.
* In the `StarRating` component, use the `React.useState()` hook to define the `rating` and `selection` state variables with the initial values of `props.rating` (or `0` if invalid or not supplied) and `0`.
* Create a method, `hoverOver`, that updates `selected` and `rating` according to the provided `event`.
* Create a `<div>` to wrap the `<Star>` components, which are created using `Array.prototype.map` on an array of 5 elements, created using `Array.from`, and handle the `onMouseLeave` event to set `selection` to `0`, the `onClick` event to set the `rating` and the `onMouseOver` event to set `selection` to the `star-id` attribute of the `event.target` respectively.
* Finally, pass the appropriate values to each `<Star>` component (`starId` and `marked`).
- Define a component, called `Star` that will render each individual star with the appropriate appearance, based on the parent component's state.
- In the `StarRating` component, use the `React.useState()` hook to define the `rating` and `selection` state variables with the initial values of `props.rating` (or `0` if invalid or not supplied) and `0`.
- Create a method, `hoverOver`, that updates `selected` and `rating` according to the provided `event`.
- Create a `<div>` to wrap the `<Star>` components, which are created using `Array.prototype.map` on an array of 5 elements, created using `Array.from`, and handle the `onMouseLeave` event to set `selection` to `0`, the `onClick` event to set the `rating` and the `onMouseOver` event to set `selection` to the `star-id` attribute of the `event.target` respectively.
- Finally, pass the appropriate values to each `<Star>` component (`starId` and `marked`).
```jsx
function Star({ marked, starId }) {
@ -29,7 +32,7 @@ function StarRating(props) {
return (
<div
onMouseOut={() => hoverOver(null)}
onClick={(event) => setRating(event.target.getAttribute('star-id') || rating)}
onClick={event => setRating(event.target.getAttribute('star-id') || rating)}
onMouseOver={hoverOver}
>
{Array.from({ length: 5 }, (v, i) => (
@ -48,7 +51,3 @@ function StarRating(props) {
ReactDOM.render(<StarRating />, document.getElementById('root'));
ReactDOM.render(<StarRating rating={2} />, document.getElementById('root'));
```
<!-- tags: visual,children,input,state -->
<!-- expertise: 2 -->

View File

@ -1,12 +1,15 @@
### Tabs
---
title: Tabs
tags: visual,state,children,intermediate
---
Renders a tabbed menu and view component.
* Define a `TabItem` component, pass it to the `Tab` and remove unnecessary nodes expect for `TabItem` by identifying the function's name in `props.children`.
* Use the `React.useState()` hook to initialize the value of the `bindIndex` state variable to `props.defaultIndex`.
* Use `Array.prototype.map` on the collected nodes to render the `tab-menu` and `tab-view`.
* Define `changeTab`, which will be executed when clicking a `<button>` from the `tab-menu`.
* `changeTab` executes the passed callback, `onTabClick` and updates `bindIndex`, which in turn causes a re-render, evaluating the `style` and `className` of the `tab-view` items and `tab-menu` buttons according to their `index`.
- Define a `TabItem` component, pass it to the `Tab` and remove unnecessary nodes expect for `TabItem` by identifying the function's name in `props.children`.
- Use the `React.useState()` hook to initialize the value of the `bindIndex` state variable to `props.defaultIndex`.
- Use `Array.prototype.map` on the collected nodes to render the `tab-menu` and `tab-view`.
- Define `changeTab`, which will be executed when clicking a `<button>` from the `tab-menu`.
- `changeTab` executes the passed callback, `onTabClick` and updates `bindIndex`, which in turn causes a re-render, evaluating the `style` and `className` of the `tab-view` items and `tab-menu` buttons according to their `index`.
```css
.tab-menu > button {
@ -74,7 +77,3 @@ ReactDOM.render(
document.getElementById('root')
);
```
<!-- tags: visual,state,children -->
<!-- expertise: 1 -->

View File

@ -1,9 +1,12 @@
### TextArea
---
title: TextArea
tags: input,beginner
---
Renders a `<textarea>` element that uses a callback function to pass its value to the parent component.
* Use object destructuring to set defaults for certain attributes of the `<textarea>` element.
* Render a `<textarea>` element with the appropriate attributes and use the `callback` function in the `onChange` event to pass the value of the textarea to the parent.
- Use object destructuring to set defaults for certain attributes of the `<textarea>` element.
- Render a `<textarea>` element with the appropriate attributes and use the `callback` function in the `onChange` event to pass the value of the textarea to the parent.
```jsx
function TextArea({
@ -33,7 +36,3 @@ ReactDOM.render(
document.getElementById('root')
);
```
<!-- tags: input -->
<!-- expertise: 0 -->

View File

@ -1,10 +1,13 @@
### Ticker
---
title: Ticker
tags: visual,state,beginner
---
Renders a ticker component.
* Use the `React.useState()` hook to initialize the `ticker` state variable to `0`.
* Define two methods, `tick` and `reset`, that will periodically increment `timer` based on `interval` and reset `interval` respectively.
* Return a `<div>` with two `<button>` elements, each of which calls `tick` and `reset` respectively.
- Use the `React.useState()` hook to initialize the `ticker` state variable to `0`.
- Define two methods, `tick` and `reset`, that will periodically increment `timer` based on `interval` and reset `interval` respectively.
- Return a `<div>` with two `<button>` elements, each of which calls `tick` and `reset` respectively.
```jsx
function Ticker(props) {
@ -37,7 +40,3 @@ function Ticker(props) {
```jsx
ReactDOM.render(<Ticker times={5} interval={1000} />, document.getElementById('root'));
```
<!-- tags: visual,state -->
<!-- expertise: 1 -->

View File

@ -1,10 +1,13 @@
### Toggle
---
title: Toggle
tags: visual,state,beginner
---
Renders a toggle component.
* Use the `React.useState()` to initialize the `isToggleOn` state variable to `false`.
* Use an object, `style`, to hold the styles for individual components and their states.
* Return a `<button>` that alters the component's `isToggledOn` when its `onClick` event is fired and determine the appearance of the content based on `isToggleOn`, applying the appropriate CSS rules from the `style` object.
- Use the `React.useState()` to initialize the `isToggleOn` state variable to `false`.
- Use an object, `style`, to hold the styles for individual components and their states.
- Return a `<button>` that alters the component's `isToggledOn` when its `onClick` event is fired and determine the appearance of the content based on `isToggleOn`, applying the appropriate CSS rules from the `style` object.
```jsx
function Toggle(props) {
@ -29,7 +32,3 @@ function Toggle(props) {
```jsx
ReactDOM.render(<Toggle />, document.getElementById('root'));
```
<!-- tags: visual,state -->
<!-- expertise: 0 -->

View File

@ -1,10 +1,13 @@
### Tooltip
---
title: Tooltip
tags: visual,state,children,beginner
---
Renders a tooltip component.
* Use the `React.useState()` hook to create the `show` variable and initialize it to `false`.
* Return a `<div>` element that contains the `<div>` that will be the tooltip and the `children` passed to the component.
* Handle the `onMouseEnter` and `onMouseLeave` methods, by altering the value of the `show` variable.
- Use the `React.useState()` hook to create the `show` variable and initialize it to `false`.
- Return a `<div>` element that contains the `<div>` that will be the tooltip and the `children` passed to the component.
- Handle the `onMouseEnter` and `onMouseLeave` methods, by altering the value of the `show` variable.
```css
.tooltip {
@ -51,7 +54,3 @@ ReactDOM.render(
document.getElementById('root')
);
```
<!-- tags: visual,state,children -->
<!-- expertise: 1 -->

View File

@ -1,14 +1,17 @@
### TreeView
---
title: TreeView
tags: visual,object,state,recursion,advanced
---
Renders a tree view of a JSON object or array with collapsible content.
* Use object destructuring to set defaults for certain props.
* Use the value of the `toggled` prop to determine the initial state of the content (collapsed/expanded).
* Use the `React.setState()` hook to create the `isToggled` state variable and give it the value of the `toggled` prop initially.
* Return a `<div>` to wrap the contents of the component and the `<span>` element, used to alter the component's `isToggled` state.
* Determine the appearance of the component, based on `isParentToggled`, `isToggled`, `name` and `Array.isArray()` on `data`.
* For each child in `data`, determine if it is an object or array and recursively render a sub-tree.
* Otherwise, render a `<p>` element with the appropriate style.
- Use object destructuring to set defaults for certain props.
- Use the value of the `toggled` prop to determine the initial state of the content (collapsed/expanded).
- Use the `React.setState()` hook to create the `isToggled` state variable and give it the value of the `toggled` prop initially.
- Return a `<div>` to wrap the contents of the component and the `<span>` element, used to alter the component's `isToggled` state.
- Determine the appearance of the component, based on `isParentToggled`, `isToggled`, `name` and `Array.isArray()` on `data`.
- For each child in `data`, determine if it is an object or array and recursively render a sub-tree.
- Otherwise, render a `<p>` element with the appropriate style.
```css
.tree-element {
@ -121,7 +124,3 @@ let data = {
};
ReactDOM.render(<TreeView data={data} name="data" />, document.getElementById('root'));
```
<!-- tags: object,visual,state,recursion -->
<!-- expertise: 2 -->

View File

@ -1,12 +1,21 @@
### Input
---
title: UncontrolledInput
tags: input,beginner
---
Renders an `<input>` element that uses a callback function to pass its value to the parent component.
* Use object destructuring to set defaults for certain attributes of the `<input>` element.
* Render an `<input>` element with the appropriate attributes and use the `callback` function in the `onChange` event to pass the value of the input to the parent.
- Use object destructuring to set defaults for certain attributes of the `<input>` element.
- Render an `<input>` element with the appropriate attributes and use the `callback` function in the `onChange` event to pass the value of the input to the parent.
```jsx
function Input({ callback, type = 'text', disabled = false, readOnly = false, placeholder = '' }) {
function UncontrolledInput({
callback,
type = 'text',
disabled = false,
readOnly = false,
placeholder = ''
}) {
return (
<input
type={type}
@ -21,11 +30,11 @@ function Input({ callback, type = 'text', disabled = false, readOnly = false, pl
```jsx
ReactDOM.render(
<Input type="text" placeholder="Insert some text here..." callback={val => console.log(val)} />,
<UncontrolledInput
type="text"
placeholder="Insert some text here..."
callback={val => console.log(val)}
/>,
document.getElementById('root')
);
```
<!-- tags: input -->
<!-- expertise: 0 -->

View File

@ -0,0 +1,54 @@
---
title: useClickInside
tags: hooks,effect,event,intermediate
---
A hook that handles the event of clicking inside the wrapped component.
- Create a custom hook that takes a `ref` and a `callback` to handle the `click` event.
- Use the `React.useEffect()` hook to append and clean up the `click` event.
- Use the `React.useRef()` hook to create a `ref` for your click component and pass it to the `useClickInside` hook.
```jsx
const useClickInside = (ref, callback) => {
const handleClick = e => {
if (ref.current && ref.current.contains(e.target)) {
callback();
}
};
React.useEffect(() => {
document.addEventListener('click', handleClick);
return () => {
document.removeEventListener('click', handleClick);
};
});
};
```
```jsx
const ClickBox = ({ onClickInside }) => {
const clickRef = React.useRef();
useClickInside(clickRef, onClickInside);
return (
<div
className="click-box"
ref={clickRef}
style={{
border: '2px dashed orangered',
height: 200,
width: 400,
display: 'flex',
justifyContent: 'center',
alignItems: 'center'
}}
>
<p>Click inside this element</p>
</div>
);
};
ReactDOM.render(
<ClickBox onClickInside={() => alert('click inside')} />,
document.getElementById('root')
);
```

View File

@ -0,0 +1,54 @@
---
title: useClickOutside
tags: hooks,effect,event,intermediate
---
A hook that handles the event of clicking outside of the wrapped component.
- Create a custom hook that takes a `ref` and a `callback` to handle the `click` event.
- Use the `React.useEffect()` hook to append and clean up the `click` event.
- Use the `React.useRef()` hook to create a `ref` for your click component and pass it to the `useClickOutside` hook.
```jsx
const useClickOutside = (ref, callback) => {
const handleClick = e => {
if (ref.current && !ref.current.contains(e.target)) {
callback();
}
};
React.useEffect(() => {
document.addEventListener('click', handleClick);
return () => {
document.removeEventListener('click', handleClick);
};
});
};
```
```jsx
const ClickBox = ({ onClickOutside }) => {
const clickRef = React.useRef();
useClickOutside(clickRef, onClickOutside);
return (
<div
className="click-box"
ref={clickRef}
style={{
border: '2px dashed orangered',
height: 200,
width: 400,
display: 'flex',
justifyContent: 'center',
alignItems: 'center'
}}
>
<p>Click out of this element</p>
</div>
);
};
ReactDOM.render(
<ClickBox onClickOutside={() => alert('click outside')} />,
document.getElementById('root')
);
```

51
snippets/useFetch.md Normal file
View File

@ -0,0 +1,51 @@
---
title: useFetch
tags: hooks,effect,state,intermediate
---
A hook that implements `fetch` in a declarative manner.
- Create a custom hook that takes a `url` and `options`.
- Use the `React.useState()` hook to initialize the `response` and `error` state variables.
- Use the `React.useEffect()` hook to anychronously call `fetch()` and update the state varaibles accordingly.
- Return an object containting the `response` and `error` state variables.
```jsx
const useFetch = (url, options) => {
const [response, setResponse] = React.useState(null);
const [error, setError] = React.useState(null);
React.useEffect(() => {
const fetchData = async () => {
try {
const res = await fetch(url, options);
const json = await res.json();
setResponse(json);
} catch (error) {
setError(error);
}
};
fetchData();
}, []);
return { response, error };
};
```
```jsx
const ImageFetch = props => {
const res = useFetch('https://dog.ceo/api/breeds/image/random', {});
if (!res.response) {
return <div>Loading...</div>;
}
const dogName = res.response.status;
const imageUrl = res.response.message;
return (
<div>
<img src={imageUrl} alt="avatar" width={400} height="auto" />
</div>
);
};
ReactDOM.render(<ImageFetch />, document.getElementById('root'));
```

44
snippets/useInterval.md Normal file
View File

@ -0,0 +1,44 @@
---
title: useInterval
tags: hooks,effect,intermediate
---
A hook that implements `setInterval` in a declarative manner.
- Create a custom hook that takes a `callback` and a `delay`.
- Use the `React.useRef()` hook to create a `ref` for the callback function.
- Use the `React.useEffect()` hook to remember the latest callback.
- Use the `Rect.useEffect()` hook to set up the interval and clean up.
```jsx
const useInterval = (callback, delay) => {
const savedCallback = React.useRef();
React.useEffect(() => {
savedCallback.current = callback;
}, [callback]);
React.useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
};
```
```jsx
const Timer = props => {
const [seconds, setSeconds] = React.useState(0);
useInterval(() => {
setSeconds(seconds + 1);
}, 1000);
return <p>{seconds}</p>;
};
ReactDOM.render(<Timer />, document.getElementById('root'));
```

44
snippets/useTimeout.md Normal file
View File

@ -0,0 +1,44 @@
---
title: useTimeout
tags: hooks,effect,intermediate
---
A hook that implements `setTimeout` in a declarative manner.
- Create a custom hook that takes a `callback` and a `delay`.
- Use the `React.useRef()` hook to create a `ref` for the callback function.
- Use the `React.useEffect()` hook to remember the latest callback.
- Use the `Rect.useEffect()` hook to set up the timeout and clean up.
```jsx
const useTimeout = (callback, delay) => {
const savedCallback = React.useRef();
React.useEffect(() => {
savedCallback.current = callback;
}, [callback]);
React.useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
let id = setTimeout(tick, delay);
return () => clearTimeout(id);
}
}, [delay]);
};
```
```jsx
const OneSecondTimer = props => {
const [seconds, setSeconds] = React.useState(0);
useTimeout(() => {
setSeconds(seconds + 1);
}, 1000);
return <p>{seconds}</p>;
};
ReactDOM.render(<OneSecondTimer />, document.getElementById('root'));
```

View File

@ -1,148 +0,0 @@
### ModalDialog
Renders a dialog component in a modal, controllable through events.
To use the component, import `ModalDialog` only once and then display it using `ModalDialog.show()`, passing the JSX templates and data as parameters.
Define `modalHandler`, a method that will handle showing the modal dialog, set `state` to the default values initially and bind the `close` and `modalClick` methods to the component's context.
Define `close` and `modalClick` to toggle the visibility of the modal dialog, based on `state.closeOnClick`.
Use the CustomEvent API to listen for `modal` events, that can be dispatched from the `static` `show()` method, handle listeners appropriately from `componentDidMount` and `componentWillUnmount`.
The `show()` method accepts an argument, that should contain three parameters:
- `title`, a string for the dialog's title
- `closeOnClick`, `true` if the modal should close on click or `false` if it should only close when clicking the _X_ button
- `content`, which is the JSX content to be rendered inside the dialog
Finally, in the `render()` method, use a `<div>` to wrap everything and render the modal dialog with the content passed to `show()`.
```css
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.6);
z-index: 9998;
display: flex;
justify-content: center;
align-items: center;
}
.dialog {
background-color: white;
border-radius: 5px;
overflow: hidden;
}
.dialog-title {
box-sizing: border-box;
width: 100%;
height: 48px;
padding: 0 16px;
border-bottom: 0.5px solid #c3c3c3;
display: flex;
justify-content: space-between;
align-items: center;
}
.dialog-close {
font-size: 32px;
color: #c3c3c3;
cursor: pointer;
transform: rotate(45deg);
user-select: none;
}
.dialog-close:hover {
color: red;
}
.dialog-content {
min-width: 300px;
}
```
```jsx
class ModalDialog extends React.Component {
constructor() {
super();
this.modalHandler = e => {
this.setState({
data: e.detail.data,
visible: true
});
};
this.state = {
data: {
title: '',
closeOnClick: false,
content: ''
},
visible: false
};
this.close = this.close.bind(this);
this.modalClick = this.modalClick.bind(this);
}
render() {
return !this.state.visible ? null : (
<div className="modal" onClick={this.modalClick}>
<div className="dialog">
<div className="dialog-title">
{this.state.data.title}
<span className="dialog-close" onClick={this.close}>
+
</span>
</div>
<div className="dialog-content">{this.state.data.content}</div>
</div>
</div>
);
}
componentDidMount() {
document.addEventListener('modal', this.modalHandler);
}
componentWillUnmount() {
document.removeEventListener('modal', this.modalHandler);
}
close() {
this.setState({
visible: false,
data: {
title: '',
closeOnClick: false,
content: ''
}
});
}
static show(data) {
document.dispatchEvent(
new CustomEvent('modal', {
detail: {
data
}
})
);
}
modalClick() {
if (this.state.data.closeOnClick) this.close();
}
}
```
```jsx
// add to render function
<ModalDialog />;
// every time you wanna call the dialog
// content is a jsx element
ModalDialog.show({
title: 'Hello, world!',
closeOnClick: true,
content: <img src="https://github.com/30-seconds/30-seconds-of-react/blob/master/logo.png" />
});
```
#### Notes:
- This component includes a lot of CSS, which might conflict with other CSS in your project. It is recomended for the modal to be a direct child of the body tag.
- A more up-to-date method with lower compatibility is to use [Portals](https://reactjs.org/docs/portals.html) in React 16+.
<!-- tags: visual,static,children,state,class -->
<!-- expertise: 1 -->

View File

@ -0,0 +1,78 @@
import React from 'react';
import Helmet from 'react-helmet';
import { useStaticQuery, graphql } from 'gatsby';
require('../styles/index.scss'); // Do not change this to `import`, it's not going to work, no clue why
// ===================================================
// Page metadata (using Helmet)
// ===================================================
const Meta = ({ description = '', lang = 'en', meta = [], title }) => {
const { site, file } = useStaticQuery(
graphql`
query {
site {
siteMetadata {
title
description
author
}
}
file(relativePath: { eq: "logo.png" }) {
id
childImageSharp {
fluid(maxHeight: 400) {
src
}
}
}
}
`,
);
const metaDescription = description || site.siteMetadata.description;
return (
<Helmet
htmlAttributes={{
lang,
}}
title={title ? title : site.siteMetadata.title}
titleTemplate={title ? `%s - ${site.siteMetadata.title}` : '%s'}
meta={[
{
name: `description`,
content: metaDescription,
},
{
name: `author`,
content: site.siteMetadata.author,
},
{
name: `viewport`,
content: `width=device-width, initial-scale=1`,
},
{
name: `og:title`,
content: site.siteMetadata.title,
},
{
name: `og:description`,
content: metaDescription,
},
{
name: `og:type`,
content: `website`,
},
{
name: `og:image`,
content: file.childImageSharp.fluid.src,
},
].concat(meta)}
bodyAttributes={{
class: 'card-page',
}}
/>
);
};
export default Meta;

View File

@ -0,0 +1,22 @@
import React from 'react';
const BackArrowIcon = ({ className, onClick }) => (
<svg
onClick={onClick}
xmlns='http://www.w3.org/2000/svg'
width='18'
height='18'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
className={`feather feather-arrow-left ${className}`}
>
<line x1='19' y1='12' x2='5' y2='12'></line>
<polyline points='12 19 5 12 12 5'></polyline>
</svg>
);
export default BackArrowIcon;

View File

@ -0,0 +1,22 @@
import React from 'react';
const ClipboardIcon = ({ className, onClick }) => (
<svg
onClick={onClick}
xmlns='http://www.w3.org/2000/svg'
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
className={`feather feather-clipboard ${className}`}
>
<path d='M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2'></path>
<rect x='8' y='2' width='8' height='4' rx='1' ry='1'></rect>
</svg>
);
export default ClipboardIcon;

View File

@ -0,0 +1,23 @@
import React from 'react';
const CollapseClosedIcon = ({ className, onClick }) => (
<svg
onClick={onClick}
xmlns='http://www.w3.org/2000/svg'
width='16'
height='16'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='1'
strokeLinecap='round'
strokeLinejoin='round'
className={`feather feather-plus-square ${className}`}
>
<rect x='3' y='3' width='18' height='18' rx='2' ry='2'></rect>
<line x1='12' y1='8' x2='12' y2='16'></line>
<line x1='8' y1='12' x2='16' y2='12'></line>
</svg>
);
export default CollapseClosedIcon;

View File

@ -0,0 +1,22 @@
import React from 'react';
const CollapseOpenIcon = ({ className, onClick }) => (
<svg
onClick={onClick}
xmlns='http://www.w3.org/2000/svg'
width='16'
height='16'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='1'
strokeLinecap='round'
strokeLinejoin='round'
className={`feather feather-minus-square ${className}`}
>
<rect x='3' y='3' width='18' height='18' rx='2' ry='2'></rect>
<line x1='8' y1='12' x2='16' y2='12'></line>
</svg>
);
export default CollapseOpenIcon;

View File

@ -0,0 +1,21 @@
import React from 'react';
const DarkModeIcon = ({ className, onClick }) => (
<svg
onClick={onClick}
xmlns='http://www.w3.org/2000/svg'
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
className={`feather feather-moon ${className}`}
>
<path d='M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z'></path>
</svg>
);
export default DarkModeIcon;

View File

@ -0,0 +1,21 @@
import React from 'react';
const GithubIcon = ({ className, onClick }) => (
<svg
onClick={onClick}
xmlns='http://www.w3.org/2000/svg'
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
className={`feather feather-github ${className}`}
>
<path d='M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22'></path>
</svg>
);
export default GithubIcon;

View File

@ -0,0 +1,29 @@
import React from 'react';
const LightModeIcon = ({ className, onClick }) => (
<svg
onClick={onClick}
xmlns='http://www.w3.org/2000/svg'
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
className={`feather feather-sun ${className}`}
>
<circle cx='12' cy='12' r='5'></circle>
<line x1='12' y1='1' x2='12' y2='3'></line>
<line x1='12' y1='21' x2='12' y2='23'></line>
<line x1='4.22' y1='4.22' x2='5.64' y2='5.64'></line>
<line x1='18.36' y1='18.36' x2='19.78' y2='19.78'></line>
<line x1='1' y1='12' x2='3' y2='12'></line>
<line x1='21' y1='12' x2='23' y2='12'></line>
<line x1='4.22' y1='19.78' x2='5.64' y2='18.36'></line>
<line x1='18.36' y1='5.64' x2='19.78' y2='4.22'></line>
</svg>
);
export default LightModeIcon;

View File

@ -0,0 +1,26 @@
import React from 'react';
const ListIcon = ({ className, onClick }) => (
<svg
onClick={onClick}
xmlns='http://www.w3.org/2000/svg'
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
className={`feather feather-list ${className}`}
>
<line x1='8' y1='6' x2='21' y2='6'></line>
<line x1='8' y1='12' x2='21' y2='12'></line>
<line x1='8' y1='18' x2='21' y2='18'></line>
<line x1='3' y1='6' x2='3' y2='6'></line>
<line x1='3' y1='12' x2='3' y2='12'></line>
<line x1='3' y1='18' x2='3' y2='18'></line>
</svg>
);
export default ListIcon;

View File

@ -0,0 +1,24 @@
import React from 'react';
const SearchIcon = ({ className, onClick }) => {
return (
<svg
onClick={onClick}
xmlns='http://www.w3.org/2000/svg'
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
className={`feather feather-search ${className}`}
>
<circle cx='11' cy='11' r='8'></circle>
<line x1='21' y1='21' x2='16.65' y2='16.65'></line>
</svg>
);
};
export default SearchIcon;

View File

@ -0,0 +1,25 @@
import React from 'react';
const ShareIcon = ({ className, onClick }) => (
<svg
onClick={onClick}
xmlns='http://www.w3.org/2000/svg'
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
className={`feather feather-share-2 ${className}`}
>
<circle cx='18' cy='5' r='3'></circle>
<circle cx='6' cy='12' r='3'></circle>
<circle cx='18' cy='19' r='3'></circle>
<line x1='8.59' y1='13.51' x2='15.42' y2='17.49'></line>
<line x1='15.41' y1='6.51' x2='8.59' y2='10.49'></line>
</svg>
);
export default ShareIcon;

View File

@ -0,0 +1,28 @@
import React from 'react';
// ===================================================
// Simple, stateful search component
// ===================================================
const Search = ({ defaultValue = '', setSearchQuery, className = '' }) => {
const [value, setValue] = React.useState(defaultValue);
React.useEffect(() => {
setSearchQuery(value);
}, [value]);
return (
<input
defaultValue={defaultValue}
className='search-box'
type='search'
id='searchInput'
placeholder='Search...'
aria-label='Snippet search'
onKeyUp={e => {
setValue(e.target.value);
}}
/>
);
};
export default Search;

View File

@ -0,0 +1,149 @@
import React from 'react';
import { graphql, useStaticQuery } from 'gatsby';
import { connect } from 'react-redux';
import AniLink from 'gatsby-plugin-transition-link/AniLink';
import ReactCSSTransitionReplace from 'react-css-transition-replace';
import config from '../../../config';
import { toggleDarkMode } from '../state/app';
import SearchIcon from './SVGs/SearchIcon';
import GithubIcon from './SVGs/GithubIcon';
import DarkModeIcon from './SVGs/DarkModeIcon';
import LightModeIcon from './SVGs/LightModeIcon';
import ListIcon from './SVGs/ListIcon';
// ===================================================
// Application-level UI component
// ===================================================
const Shell = ({
isDarkMode,
isSearch,
isList,
dispatch,
withIcon = true,
withTitle = true,
children,
}) => {
const data = useStaticQuery(graphql`
query SiteTitleQuery {
site {
siteMetadata {
title
description
}
}
file(relativePath: { eq: "30s-icon.png" }) {
id
childImageSharp {
original {
src
}
}
}
snippetDataJson(meta: { type: { eq: "snippetListingArray" } }) {
data {
title
id
attributes {
tags
}
}
}
}
`);
let viewportWidth = typeof window !== 'undefined' && window.innerWidth;
return (
<div className={isDarkMode ? 'page-container dark' : 'page-container'}>
{/* Menu */}
<header className='menu'>
<AniLink
cover
direction={viewportWidth < 600 ? 'up' : 'right'}
bg={isDarkMode ? '#434E76' : '#FFFFFF'}
to='/search'
aria-label='Search'
className={isSearch ? 'menu-button active' : 'menu-button'}
>
<SearchIcon />
</AniLink>
<AniLink
cover
direction={viewportWidth < 600 ? 'up' : 'right'}
bg={isDarkMode ? '#434E76' : '#FFFFFF'}
to='/list'
aria-label='Snippet list'
className={isList ? 'menu-button active' : 'menu-button'}
>
<ListIcon />
</AniLink>
{/* eslint-disable-next-line */}
<a target='_blank'
rel='noopener'
href={config.repositoryUrl}
aria-label='View on GitHub'
className='menu-button'
>
<GithubIcon />
</a>
{/*
The use of React.Fragment here will cause a lot of errors to show up in webber:dev.
Truth is, this is the only decent way I found to deal with this module's odd behavior.
Keep as is, unless you can provide a better solution, in which case please send a PR.
*/}
<ReactCSSTransitionReplace
transitionName='cross-fade'
transitionEnterTimeout={300}
transitionLeaveTimeout={300}
component='button'
aria-label={isDarkMode ? 'Switch to light mode' : 'Switch to dark mode'}
className='menu-button'
childComponent={React.Fragment}
>
{isDarkMode ? (
<LightModeIcon
key='lmit'
onClick={() => dispatch(toggleDarkMode(!isDarkMode))}
/>
) : (
<DarkModeIcon
key='dmit'
onClick={() => dispatch(toggleDarkMode(!isDarkMode))}
/>
)}
</ReactCSSTransitionReplace>
</header>
{/* Content */}
<div className='content'>
{withTitle ? (
<h1 className='website-title'>
{data.site.siteMetadata.title}
{withIcon ? (
<img
src={data.file.childImageSharp.original.src}
alt='Logo'
className='website-logo'
/>
) : (
''
)}
</h1>
) : (
''
)}
{children}
</div>
</div>
);
};
export default connect(
state => ({
isDarkMode: state.app.isDarkMode,
lastPageTitle: state.app.lastPageTitle,
lastPageUrl: state.app.lastPageUrl,
searchQuery: state.app.searchQuery,
}),
null,
)(Shell);

View File

@ -0,0 +1,21 @@
import React from 'react';
// ===================================================
// Generic card (displays textual content)
// ===================================================
const SimpleCard = ({
title,
children,
isDarkMode
}) => (
<div className='card short'>
<h4 className='card-title'>
{title}
</h4>
<div className='card-description'>
{children}
</div>
</div>
);
export default SimpleCard;

View File

@ -0,0 +1,186 @@
import React from 'react';
import { CopyToClipboard } from 'react-copy-to-clipboard';
import config from '../../../config';
import { getTextualContent, getCodeBlocks, optimizeAllNodes } from '../util';
// import ShareIcon from './SVGs/ShareIcon';
import AniLink from 'gatsby-plugin-transition-link/AniLink';
import CollapseOpenIcon from './SVGs/CollapseOpenIcon';
import CollapseClosedIcon from './SVGs/CollapseClosedIcon';
import ReactCSSTransitionReplace from 'react-css-transition-replace';
// ===================================================
// Snippet Card HOC - check components below for more
// ===================================================
const SnippetCard = ({ short, snippetData, ...rest }) => {
let difficulty = snippetData.tags.includes('advanced')
? 'advanced'
: snippetData.tags.includes('beginner')
? 'beginner'
: 'intermediate';
return short ? (
<ShortCard snippetData={snippetData} difficulty={difficulty} {...rest} />
) : (
<FullCard snippetData={snippetData} difficulty={difficulty} {...rest} />
);
};
// ===================================================
// Simple card corner for difficulty display
// ===================================================
const CardCorner = ({ difficulty = 'intermediate' }) => (
<div
className={`card-corner ${difficulty}`}
aria-label={difficulty}
title={difficulty}
/>
);
// ===================================================
// Full snippet view (tags, code, title, description)
// ===================================================
const FullCard = ({ snippetData, difficulty, isDarkMode }) => {
const [examplesOpen, setExamplesOpen] = React.useState(false);
const tags = snippetData.tags;
let cardCodeHtml = `${optimizeAllNodes(
getCodeBlocks(snippetData.html).code,
)}`;
let cardStyleHtml = `${optimizeAllNodes(
getCodeBlocks(snippetData.html).style,
)}`;
let cardExamplesHtml = `${optimizeAllNodes(
getCodeBlocks(snippetData.html).example,
)}`;
return (
<div className='card'>
<CardCorner difficulty={difficulty} />
<h4 className='card-title'>{snippetData.title}</h4>
{tags.map(tag => (
<span className='tag' key={`tag_${tag}`}>{tag}</span>
))}
<div
className='card-description'
dangerouslySetInnerHTML={{
__html: `${getTextualContent(snippetData.html)}`,
}}
/>
<div className='card-bottom'>
<CopyToClipboard
text={snippetData.code}
onCopy={() => {
let tst = document.createElement('div');
tst.classList = 'toast';
tst.innerHTML = 'Snippet copied to clipboard!';
document.body.appendChild(tst);
setTimeout(function() {
tst.style.opacity = 0;
setTimeout(function() {
document.body.removeChild(tst);
}, 300);
}, 1700);
}}
>
<button
className='button button-a button-copy'
aria-label='Copy to clipboard'
/>
</CopyToClipboard>
{/* <button className="button button-b button-social-sh" aria-label="Share">
<ShareIcon />
</button> */}
{
cardStyleHtml && <pre
className={`card-code language-${config.optionalLanguage}`}
dangerouslySetInnerHTML={{ __html: cardStyleHtml }}
/>
}
<pre
className={`card-code language-${config.language}`}
dangerouslySetInnerHTML={{ __html: cardCodeHtml }}
/>
<button
className='button button-example-toggler'
onClick={() => setExamplesOpen(!examplesOpen)}
>
{examplesOpen ? <CollapseOpenIcon /> : <CollapseClosedIcon />}Examples
</button>
<ReactCSSTransitionReplace
transitionName='roll-up'
transitionEnterTimeout={300}
transitionLeaveTimeout={300}
>
{examplesOpen && (
<pre
className='section card-examples language-jsx'
dangerouslySetInnerHTML={{ __html: cardExamplesHtml }}
/>
)}
</ReactCSSTransitionReplace>
</div>
</div>
);
};
// ===================================================
// Short snippet view (title, description, code*)
// ===================================================
const ShortCard = ({
snippetData,
withCode = false,
difficulty,
isDarkMode
}) => {
let cardCodeHtml;
if (withCode)
cardCodeHtml = `${optimizeAllNodes(
getCodeBlocks(snippetData.html).code,
)}`;
return (
<div className='card short'>
<CardCorner difficulty={difficulty} />
<h4 className='card-title'>
<AniLink
paintDrip
to={`/snippet/${snippetData.id}`}
hex={isDarkMode ? '#434E76' : '#FFFFFF'}
>
{snippetData.title}
</AniLink>
</h4>
<div
className='card-description'
dangerouslySetInnerHTML={{
__html: `${getTextualContent(snippetData.html)}`,
}}
/>
{withCode ? <div className='card-bottom'>
<CopyToClipboard
text={snippetData.code}
onCopy={() => {
let tst = document.createElement('div');
tst.classList = 'toast';
tst.innerHTML = 'Snippet copied to clipboard!';
document.body.appendChild(tst);
setTimeout(function() {
tst.style.opacity = 0;
setTimeout(function() {
document.body.removeChild(tst);
}, 300);
}, 1700);
}}
>
<button
className='button button-a button-copy'
aria-label='Copy to clipboard'
/>
</CopyToClipboard>
<pre
className={`card-code language-${config.language}`}
dangerouslySetInnerHTML={{ __html: cardCodeHtml }}
/>
</div> : '' }
</div>
);
};
export default SnippetCard;

60
src/docs/pages/404.js Normal file
View File

@ -0,0 +1,60 @@
import React from 'react';
import { connect } from 'react-redux';
import AniLink from 'gatsby-plugin-transition-link/AniLink';
import Shell from '../components/Shell';
import Meta from '../components/Meta';
// ===================================================
// Not found page
// ===================================================
const NotFoundPage = ({ isDarkMode }) => (
<>
<Meta title='Page not found' />
<Shell withIcon={true}>
<h2 className='page-title'>404</h2>
<div className='page-graphic page-not-found'>
<p className='empty-page-text'>
<strong>Page not found</strong>
<br />
</p>
<p className='empty-page-subtext'>
Seems like you have reached a page that does not exist.
</p>
<AniLink
paintDrip
to='/'
hex={isDarkMode ? '#434E76' : '#FFFFFF'}
className='button button-a button-home'
>
<svg
xmlns='http://www.w3.org/2000/svg'
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
stroke-linecap='round'
strokeLinejoin='round'
className='feather feather-home'
>
<path d='M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z'></path>
<polyline points='9 22 9 12 15 12 15 22'></polyline>
</svg>
&nbsp;&nbsp;Go home
</AniLink>
</div>
</Shell>
</>
);
export default connect(
state => ({
isDarkMode: state.app.isDarkMode,
lastPageTitle: state.app.lastPageTitle,
lastPageUrl: state.app.lastPageUrl,
searchQuery: state.app.searchQuery,
}),
null,
)(NotFoundPage);

99
src/docs/pages/about.js Normal file
View File

@ -0,0 +1,99 @@
import React from 'react';
import { connect } from 'react-redux';
import Shell from '../components/Shell';
import Meta from '../components/Meta';
import SimpleCard from '../components/SimpleCard';
// ===================================================
// About page
// ===================================================
const AboutPage = ({ isDarkMode }) => (
<>
<Meta title='About' />
<Shell withIcon={true}>
<h2 className='page-title'>About</h2>
<p className='light-sub'>
A few word about us, our goals and our projects.
</p>
<SimpleCard title='Our philosophy'>
<p style={{textAlign: 'justify'}}>
The core goal of <strong>30 seconds</strong> is to provide a quality resource for beginner and advanced developers alike. We want to help improve the software development ecosystem, by lowering the barrier of entry for newcomers and help seasoned veterans pick up new tricks and remember old ones.
</p>
<p style={{textAlign: 'justify'}}>
In order to achieve this, we have collected hundreds of snippets that can be of use in a wide range of situations. We welcome new contributors and we like fresh ideas, as long as the code is short and easy to grasp in about 30 seconds.
</p>
<p style={{textAlign: 'justify'}}>
The only catch, if you may, is that a few of our snippets are not perfectly optimized for large, enterprise applications and they might not be deemed production-ready.
</p>
</SimpleCard>
<SimpleCard title='Our story'>
<p style={{ textAlign: 'justify' }}>
The <strong>30 seconds</strong> movement started back in December, 2017, with the release of <a href='https://github.com/30-seconds/30-seconds-of-code' target='_blank' rel='noopener noreferrer'>30 seconds of code</a> by <a href='https://github.com/Chalarangelo' target='_blank' rel='noopener noreferrer'>Angelos Chalaris</a>. Since then, hundreds of developers have contributed snippets to over 6 repositories, creating a thriving community of people willing to help each other write better code.
</p>
<p style={{ textAlign: 'justify' }}>
In late 2018, the <a href='https://github.com/30-seconds'>30 seconds organization</a> was formed on GitHub, in order to expand upon existing projects and ensure that they will remain high-quality resources in the future.
</p>
</SimpleCard>
<SimpleCard title='Who we are'>
<p style={{ textAlign: 'justify' }}>
The <strong>30 seconds</strong> movement and, to some extent, the associated GitHub organization consists of all the people who have invested time and ideas to be part of this amazing community. Meanwhile, these fine folks are currently responsible for maintaining the codebases and dealing with organizational matters:
</p>
<div className='flex-row'>
<div class="flex-item">
<img className='media-section' src='https://github.com/Chalarangelo.png' alt='chalarangelo' />
<a href='https://github.com/Chalarangelo' className='button-section' target='_blank' rel='noopener noreferrer'>Angelos Chalaris</a>
</div>
<div class="flex-item">
<img className='media-section' src='https://github.com/fejes713.png' alt='fejes713' />
<a href='https://github.com/fejes713' className='button-section' target='_blank' rel='noopener noreferrer'>Stefan Fejes</a>
</div>
<div class="flex-item">
<img className='media-section' src='https://github.com/flxwu.png' alt='flxwu' />
<a href='https://github.com/flxwu' className='button-section' target='_blank' rel='noopener noreferrer'>Felix Wu</a>
</div>
<div class="flex-item">
<img className='media-section' src='https://github.com/atomiks.png' alt='atomiks' />
<a href='https://github.com/atomiks' className='button-section' target='_blank' rel='noopener noreferrer'>atomiks</a>
</div>
</div>
<div className='flex-row'>
<div class="flex-item">
<img className='media-section' src='https://github.com/skatcat31.png' alt='skatcat31' />
<a href='https://github.com/skatcat31' className='button-section' target='_blank' rel='noopener noreferrer'>Robert Mennell</a>
</div>
<div class="flex-item">
<img className='media-section' src='https://github.com/petrovicstefanrs.png' alt='petrovicstefanrs' />
<a href='https://github.com/petrovicstefanrs' className='button-section' target='_blank' rel='noopener noreferrer'>Stefan Petrovic</a>
</div>
<div class="flex-item">
<img className='media-section' src='https://github.com/kirjs.png' alt='kirjs' />
<a href='https://github.com/kirjs' className='button-section' target='_blank' rel='noopener noreferrer'>Kirill Cherkashin</a>
</div>
<div class="flex-item">
<img className='media-section' src='https://github.com/sohelamin.png' alt='sohelamin' />
<a href='https://github.com/sohelamin' className='button-section' target='_blank' rel='noopener noreferrer'>Sohel Amin</a>
</div>
</div>
</SimpleCard>
<SimpleCard title='License'>
<p style={{ textAlign: 'justify' }}>
In order for the code provided via the <strong>30 seconds</strong> projects to be as accessible and useful as possible, all of the snippets in these collections are licensed under the <a href='https://creativecommons.org/publicdomain/zero/1.0/' target='_blank' rel='noopener noreferrer'>CC0-1.0 License</a> meaning they are absolutely free to use in any project you like. If you like what we do, you can always credit us, but that is not mandatory.
</p>
<p style={{ textAlign: 'justify' }}>
Logos, names and trademarks are not to be used without the explicit consent of the maintainers or owners of the <strong>30 seconds</strong> GitHub organization. The only exception to this is the usage of the <strong>30-seconds-of-</strong> name in open source projects, licensed under the <a href='https://creativecommons.org/publicdomain/zero/1.0/' target='_blank' rel='noopener noreferrer'>CC0-1.0 License</a> and hosted on GitHub, if their README and website clearly states that they are unofficial projects and in no way affiliated with the organization.
</p>
</SimpleCard>
</Shell>
</>
);
export default connect(
state => ({
isDarkMode: state.app.isDarkMode,
lastPageTitle: state.app.lastPageTitle,
lastPageUrl: state.app.lastPageUrl,
searchQuery: state.app.searchQuery,
}),
null,
)(AboutPage);

157
src/docs/pages/index.js Normal file
View File

@ -0,0 +1,157 @@
import React from 'react';
import { graphql } from 'gatsby';
import { connect } from 'react-redux';
import { pushNewPage, pushNewQuery } from '../state/app';
import Shell from '../components/Shell';
import Meta from '../components/Meta';
import Search from '../components/Search';
import SnippetCard from '../components/SnippetCard';
import { getRawCodeBlocks as getCodeBlocks } from '../util';
// ===================================================
// Home page (splash and search)
// ===================================================
const IndexPage = props => {
const snippets = props.data.snippetDataJson.data.map(snippet => ({
title: snippet.title,
html: props.data.allMarkdownRemark.edges.find(
v => v.node.frontmatter.title === snippet.title,
).node.html,
tags: snippet.attributes.tags,
text: snippet.attributes.text,
id: snippet.id,
code: getCodeBlocks(
props.data.allMarkdownRemark.edges.find(
v => v.node.frontmatter.title === snippet.title,
).node.rawMarkdownBody,
).code,
}));
const [searchQuery, setSearchQuery] = React.useState(props.searchQuery);
const [searchResults, setSearchResults] = React.useState(snippets);
React.useEffect(() => {
props.dispatch(pushNewQuery(searchQuery));
let q = searchQuery.toLowerCase();
let results = snippets;
if (q.trim().length)
results = snippets.filter(
v =>
v.tags.filter(t => t.indexOf(q) !== -1).length ||
v.title.toLowerCase().indexOf(q) !== -1,
);
setSearchResults(results);
}, [searchQuery]);
React.useEffect(() => {
props.dispatch(pushNewPage('Search', '/search'));
}, []);
return (
<>
<Meta />
<Shell withIcon={false} withTitle={false}>
<img
src={props.data.file.childImageSharp.original.src}
alt='Logo'
className='index-logo'
/>
<h1 className='index-title'>{props.data.site.siteMetadata.title}</h1>
<p className='index-sub-title'>
{props.data.site.siteMetadata.description}
</p>
<Search
setSearchQuery={setSearchQuery}
defaultValue={props.searchQuery}
/>
{searchQuery.length === 0 ? (
<p className='light-sub'>
Start typing a keyword to see matching snippets.
</p>
) : searchResults.length === 0 ? (
<p className='light-sub'>
We couldn't find any results for the keyword{' '}
<strong>{searchQuery}</strong>.
</p>
) : (
<>
<p className='light-sub'>
Click on a snippet's name to view its code.
</p>
<h2 className='page-sub-title'>Search results</h2>
{searchResults.map(snippet => (
<SnippetCard
short
key={`snippet_${snippet.id}`}
snippetData={snippet}
isDarkMode={props.isDarkMode}
/>
))}
</>
)}
</Shell>
</>
);
};
export default connect(
state => ({
isDarkMode: state.app.isDarkMode,
lastPageTitle: state.app.lastPageTitle,
lastPageUrl: state.app.lastPageUrl,
searchQuery: state.app.searchQuery,
}),
null,
)(IndexPage);
export const indexPageQuery = graphql`
query snippetList {
site {
siteMetadata {
title
description
author
}
}
file(relativePath: { eq: "30s-icon.png" }) {
id
childImageSharp {
original {
src
}
}
}
snippetDataJson(meta: { type: { eq: "snippetListingArray" } }) {
data {
id
title
attributes {
tags
text
}
}
}
allMarkdownRemark(
limit: 1000
sort: { fields: [frontmatter___title], order: ASC }
) {
totalCount
edges {
node {
id
html
rawMarkdownBody
fields {
slug
}
frontmatter {
title
tags
}
}
}
}
}
`;

148
src/docs/pages/list.js Normal file
View File

@ -0,0 +1,148 @@
import React from 'react';
import { graphql } from 'gatsby';
import { connect } from 'react-redux';
import { pushNewPage } from '../state/app';
import { capitalize } from '../util';
import Shell from '../components/Shell';
import Meta from '../components/Meta';
import AniLink from 'gatsby-plugin-transition-link/AniLink';
import SnippetCard from '../components/SnippetCard';
import { getRawCodeBlocks as getCodeBlocks } from '../util';
import SimpleCard from '../components/SimpleCard';
// ===================================================
// Snippet list page
// ===================================================
const ListPage = props => {
const snippets = props.data.snippetDataJson.data.map(snippet => ({
title: snippet.title,
html: props.data.allMarkdownRemark.edges.find(
v => v.node.frontmatter.title === snippet.title,
).node.html,
tags: snippet.attributes.tags,
text: snippet.attributes.text,
id: snippet.id,
code: getCodeBlocks(
props.data.allMarkdownRemark.edges.find(
v => v.node.frontmatter.title === snippet.title,
).node.rawMarkdownBody,
).code,
}));
const tags = snippets.reduce((acc, snippet) => {
if (!snippet.tags) return acc;
const primaryTag = snippet.tags[0];
if (!acc.includes(primaryTag)) acc.push(primaryTag);
return acc;
}, []);
const staticPages = [
{
url: 'about',
title: 'About',
description: 'A few word about us, our goals and our projects.'
}
];
React.useEffect(() => {
props.dispatch(pushNewPage('Snippet List', '/list'));
}, []);
return (
<>
<Meta title='Snippet List' />
<Shell withIcon={false} isList>
<h2 className='page-title'>Snippet List</h2>
<p className='light-sub'>
Click on a snippets name to view its code or a tag name to view all
snippets in that category.
</p>
{tags.map(tag => (
<>
<h3 className='tag-title' key={`tag_title_${tag}`}>
<AniLink
key={`tag_link_${tag}`}
paintDrip
to={`/tag/${tag}`}
hex={props.isDarkMode ? '#434E76' : '#FFFFFF'}
>
{capitalize(tag)}
</AniLink>
</h3>
{snippets
.filter(snippet => snippet.tags[0] === tag)
.map(snippet => (
<SnippetCard
key={`snippet_${snippet.id}`}
short
snippetData={snippet}
isDarkMode={props.isDarkMode}
/>
))}
</>
))}
<br/>
{staticPages.map(page => (
<SimpleCard
title={(
<AniLink
paintDrip
to={`/${page.url}`}
hex={props.isDarkMode ? '#434E76' : '#FFFFFF'}
>
{page.title}
</AniLink>
)}
>
<p>{page.description}</p>
</SimpleCard>
))}
</Shell>
</>
);
};
export default connect(
state => ({
isDarkMode: state.app.isDarkMode,
lastPageTitle: state.app.lastPageTitle,
lastPageUrl: state.app.lastPageUrl,
searchQuery: state.app.searchQuery,
}),
null,
)(ListPage);
export const listPageQuery = graphql`
query snippetListing {
snippetDataJson(meta: { type: { eq: "snippetListingArray" } }) {
data {
id
title
attributes {
tags
text
}
}
}
allMarkdownRemark(
limit: 1000
sort: { fields: [frontmatter___title], order: ASC }
) {
totalCount
edges {
node {
id
html
rawMarkdownBody
fields {
slug
}
frontmatter {
title
tags
}
}
}
}
}
`;

144
src/docs/pages/search.js Normal file
View File

@ -0,0 +1,144 @@
import React from 'react';
import { graphql } from 'gatsby';
import { connect } from 'react-redux';
import { pushNewPage, pushNewQuery } from '../state/app';
import Shell from '../components/Shell';
import Meta from '../components/Meta';
import Search from '../components/Search';
import SnippetCard from '../components/SnippetCard';
import { getRawCodeBlocks as getCodeBlocks } from '../util';
// ===================================================
// Search page
// ===================================================
const SearchPage = props => {
const snippets = props.data.snippetDataJson.data.map(snippet => ({
title: snippet.title,
html: props.data.allMarkdownRemark.edges.find(
v => v.node.frontmatter.title === snippet.title,
).node.html,
tags: snippet.attributes.tags,
text: snippet.attributes.text,
id: snippet.id,
code: getCodeBlocks(
props.data.allMarkdownRemark.edges.find(
v => v.node.frontmatter.title === snippet.title,
).node.rawMarkdownBody,
).code,
}));
const [searchQuery, setSearchQuery] = React.useState(props.searchQuery);
const [searchResults, setSearchResults] = React.useState(snippets);
React.useEffect(() => {
props.dispatch(pushNewQuery(searchQuery));
let q = searchQuery.toLowerCase();
let results = snippets;
if (q.trim().length)
results = snippets.filter(
v =>
v.tags.filter(t => t.indexOf(q) !== -1).length ||
v.title.toLowerCase().indexOf(q) !== -1,
);
setSearchResults(results);
}, [searchQuery]);
React.useEffect(() => {
props.dispatch(pushNewPage('Search', '/search'));
}, []);
return (
<>
<Meta title='Search' />
<Shell withIcon={false} isSearch>
<Search
setSearchQuery={setSearchQuery}
defaultValue={props.searchQuery}
/>
<p className='light-sub'>Click on a snippet's name to view its code.</p>
{/* Display page background or results depending on state */}
{searchQuery.length === 0 ? (
<>
<div className='page-graphic search-empty'>
<p className='empty-page-text search-page-text'>
Start typing a keyword to see matching snippets.
</p>
</div>
</>
) : searchResults.length === 0 ? (
<>
<div className='page-graphic search-no-results'>
<p className='empty-page-text'>
<strong>No results found</strong>
<br />
</p>
<p className='empty-page-subtext'>
We couldn't find any results for the keyword{' '}
<strong>{searchQuery}</strong>.
</p>
</div>
</>
) : (
<>
<h2 className='page-sub-title'>Search results</h2>
{searchResults.map(snippet => (
<SnippetCard
key={`snippet_${snippet.id}`}
short
snippetData={snippet}
isDarkMode={props.isDarkMode}
/>
))}
</>
)}
</Shell>
</>
);
};
export default connect(
state => ({
isDarkMode: state.app.isDarkMode,
lastPageTitle: state.app.lastPageTitle,
lastPageUrl: state.app.lastPageUrl,
searchQuery: state.app.searchQuery,
}),
null,
)(SearchPage);
export const searchPageQuery = graphql`
query searchSnippetList {
snippetDataJson(meta: { type: { eq: "snippetListingArray" } }) {
data {
id
title
attributes {
tags
text
}
}
}
allMarkdownRemark(
limit: 1000
sort: { fields: [frontmatter___title], order: ASC }
) {
totalCount
edges {
node {
id
html
rawMarkdownBody
fields {
slug
}
frontmatter {
title
tags
}
}
}
}
}
`;

View File

@ -0,0 +1,13 @@
import React from 'react';
import { Provider } from 'react-redux';
import { createStore as reduxCreateStore } from 'redux';
import rootReducer from '.';
const createStore = () => reduxCreateStore(rootReducer);
// ===================================================
// Wrapper for Gatsby
// ===================================================
export default ({ element }) => (
<Provider store={createStore()}>{element}</Provider>
);

49
src/docs/state/app.js Normal file
View File

@ -0,0 +1,49 @@
// Defalt state
const initialState = {
isDarkMode: false,
lastPageTitle: 'Home',
lastPageUrl: '/',
searchQuery: '',
};
// Actions
const TOGGLE_DARKMODE = 'TOGGLE_DARKMODE';
const PUSH_NEW_PAGE = 'PUSH_NEW_PAGE';
const PUSH_NEW_QUERY = 'PUSH_NEW_QUERY';
export const toggleDarkMode = isDarkMode => ({
type: TOGGLE_DARKMODE,
isDarkMode,
});
export const pushNewPage = (pageTitle, pageUrl) => ({
type: PUSH_NEW_PAGE,
pageTitle,
pageUrl,
});
export const pushNewQuery = query => ({
type: PUSH_NEW_QUERY,
query,
});
// Reducer
export default (state = initialState, action) => {
switch (action.type) {
case TOGGLE_DARKMODE:
return {
...state,
isDarkMode: action.isDarkMode,
};
case PUSH_NEW_PAGE:
return {
...state,
lastPageTitle: action.pageTitle,
lastPageUrl: action.pageUrl,
};
case PUSH_NEW_QUERY:
return {
...state,
searchQuery: action.query,
};
default:
return state;
}
};

Some files were not shown because too many files have changed in this diff Show More