Merge pull request #68 from 30-seconds/from-starter
Use 30-seconds-starter template
This commit is contained in:
20
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal 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
8
.github/ISSUE_TEMPLATE/discussion.md
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
---
|
||||
name: Discussion
|
||||
about: Discuss something with us
|
||||
|
||||
---
|
||||
|
||||
## Description
|
||||
<!-- A clear and concise description of what you want to discuss. -->
|
||||
9
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
9
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal 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
15
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal 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
22
.gitignore
vendored
@ -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
12
.travis.yml
Normal 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
35
.travis/push.sh
Normal 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
46
CODE_OF_CONDUCT.md
Normal 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/
|
||||
@ -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.
|
||||
|
||||
4
_headers
Normal file
4
_headers
Normal file
@ -0,0 +1,4 @@
|
||||
[[headers]]
|
||||
for = "/static/*"
|
||||
[headers.values]
|
||||
Cache-Control = "public, max-age=360000"
|
||||
1
advanced.svg
Normal file
1
advanced.svg
Normal 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
BIN
assets/30s-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
BIN
assets/NotoSans-Italic.ttf
Normal file
BIN
assets/NotoSans-Italic.ttf
Normal file
Binary file not shown.
BIN
assets/NotoSans-Light.ttf
Normal file
BIN
assets/NotoSans-Light.ttf
Normal file
Binary file not shown.
BIN
assets/NotoSans-LightItalic.ttf
Normal file
BIN
assets/NotoSans-LightItalic.ttf
Normal file
Binary file not shown.
BIN
assets/NotoSans-Medium.ttf
Normal file
BIN
assets/NotoSans-Medium.ttf
Normal file
Binary file not shown.
BIN
assets/NotoSans-MediumItalic.ttf
Normal file
BIN
assets/NotoSans-MediumItalic.ttf
Normal file
Binary file not shown.
BIN
assets/NotoSans-Regular.ttf
Normal file
BIN
assets/NotoSans-Regular.ttf
Normal file
Binary file not shown.
BIN
assets/NotoSans-SemiBold.ttf
Normal file
BIN
assets/NotoSans-SemiBold.ttf
Normal file
Binary file not shown.
BIN
assets/NotoSans-SemiBoldItalic.ttf
Normal file
BIN
assets/NotoSans-SemiBoldItalic.ttf
Normal file
Binary file not shown.
BIN
assets/RobotoMono-Italic.woff2
Normal file
BIN
assets/RobotoMono-Italic.woff2
Normal file
Binary file not shown.
BIN
assets/RobotoMono-Medium.woff2
Normal file
BIN
assets/RobotoMono-Medium.woff2
Normal file
Binary file not shown.
BIN
assets/RobotoMono-Regular.woff2
Normal file
BIN
assets/RobotoMono-Regular.woff2
Normal file
Binary file not shown.
BIN
assets/logo.png
Normal file
BIN
assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
16
config.js
Normal file
16
config.js
Normal 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`
|
||||
};
|
||||
@ -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}>×</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> {name}: </strong> : <span> </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
8
gatsby-browser.js
Normal 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
82
gatsby-config.js
Normal 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
93
gatsby-node.js
Normal 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
8
gatsby-ssr.js
Normal 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
BIN
logo.png
Binary file not shown.
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 35 KiB |
6
netlify.toml
Normal file
6
netlify.toml
Normal 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
19499
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
46
package.json
46
package.json
@ -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,
|
||||
|
||||
155
scripts/build.js
155
scripts/build.js
@ -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');
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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
|
||||
};
|
||||
11
scripts/util/environmentCheck.js
Normal file
11
scripts/util/environmentCheck.js
Normal 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
54
scripts/util/helpers.js
Normal 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
33
scripts/util/index.js
Normal 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
|
||||
};
|
||||
123
scripts/util/snippetParser.js
Normal file
123
scripts/util/snippetParser.js
Normal 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
|
||||
};
|
||||
@ -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
|
||||
-->
|
||||
|
||||
521
snippet_data/snippetList.json
Normal file
521
snippet_data/snippetList.json
Normal 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
707
snippet_data/snippets.json
Normal 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 ×\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> {name}: </strong> : <span> </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"
|
||||
}
|
||||
}
|
||||
@ -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 -->
|
||||
|
||||
@ -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 -->
|
||||
|
||||
@ -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 -->
|
||||
|
||||
@ -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'))
|
||||
```
|
||||
@ -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 -->
|
||||
|
||||
50
snippets/ControlledInput.md
Normal file
50
snippets/ControlledInput.md
Normal 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')
|
||||
);
|
||||
```
|
||||
@ -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 -->
|
||||
|
||||
@ -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 -->
|
||||
|
||||
@ -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 -->
|
||||
|
||||
@ -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 -->
|
||||
|
||||
@ -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 -->
|
||||
|
||||
@ -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 -->
|
||||
|
||||
@ -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 -->
|
||||
|
||||
@ -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 -->
|
||||
|
||||
@ -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}>×</span>
|
||||
<span className="modal-close" onClick={onClose}>
|
||||
×
|
||||
</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 -->
|
||||
|
||||
@ -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 -->
|
||||
|
||||
@ -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 -->
|
||||
|
||||
@ -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 -->
|
||||
|
||||
@ -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 -->
|
||||
|
||||
@ -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 -->
|
||||
|
||||
@ -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 -->
|
||||
|
||||
@ -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 -->
|
||||
|
||||
@ -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 -->
|
||||
|
||||
@ -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 -->
|
||||
|
||||
@ -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 -->
|
||||
|
||||
@ -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 -->
|
||||
|
||||
@ -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 -->
|
||||
54
snippets/useClickInside.md
Normal file
54
snippets/useClickInside.md
Normal 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')
|
||||
);
|
||||
```
|
||||
54
snippets/useClickOutside.md
Normal file
54
snippets/useClickOutside.md
Normal 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
51
snippets/useFetch.md
Normal 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
44
snippets/useInterval.md
Normal 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
44
snippets/useTimeout.md
Normal 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'));
|
||||
```
|
||||
@ -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 -->
|
||||
78
src/docs/components/Meta.js
Normal file
78
src/docs/components/Meta.js
Normal 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;
|
||||
22
src/docs/components/SVGs/BackArrowIcon.js
Normal file
22
src/docs/components/SVGs/BackArrowIcon.js
Normal 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;
|
||||
22
src/docs/components/SVGs/ClipboardIcon.js
Normal file
22
src/docs/components/SVGs/ClipboardIcon.js
Normal 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;
|
||||
23
src/docs/components/SVGs/CollapseClosedIcon.js
Normal file
23
src/docs/components/SVGs/CollapseClosedIcon.js
Normal 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;
|
||||
22
src/docs/components/SVGs/CollapseOpenIcon.js
Normal file
22
src/docs/components/SVGs/CollapseOpenIcon.js
Normal 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;
|
||||
21
src/docs/components/SVGs/DarkModeIcon.js
Normal file
21
src/docs/components/SVGs/DarkModeIcon.js
Normal 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;
|
||||
21
src/docs/components/SVGs/GithubIcon.js
Normal file
21
src/docs/components/SVGs/GithubIcon.js
Normal 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;
|
||||
29
src/docs/components/SVGs/LightModeIcon.js
Normal file
29
src/docs/components/SVGs/LightModeIcon.js
Normal 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;
|
||||
26
src/docs/components/SVGs/ListIcon.js
Normal file
26
src/docs/components/SVGs/ListIcon.js
Normal 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;
|
||||
24
src/docs/components/SVGs/SearchIcon.js
Normal file
24
src/docs/components/SVGs/SearchIcon.js
Normal 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;
|
||||
25
src/docs/components/SVGs/ShareIcon.js
Normal file
25
src/docs/components/SVGs/ShareIcon.js
Normal 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;
|
||||
28
src/docs/components/Search.js
Normal file
28
src/docs/components/Search.js
Normal 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;
|
||||
149
src/docs/components/Shell.js
Normal file
149
src/docs/components/Shell.js
Normal 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);
|
||||
21
src/docs/components/SimpleCard.js
Normal file
21
src/docs/components/SimpleCard.js
Normal 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;
|
||||
186
src/docs/components/SnippetCard.js
Normal file
186
src/docs/components/SnippetCard.js
Normal 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
60
src/docs/pages/404.js
Normal 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>
|
||||
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
99
src/docs/pages/about.js
Normal 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
157
src/docs/pages/index.js
Normal 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
148
src/docs/pages/list.js
Normal 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 snippet’s 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
144
src/docs/pages/search.js
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
13
src/docs/state/ReduxWrapper.js
Normal file
13
src/docs/state/ReduxWrapper.js
Normal 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
49
src/docs/state/app.js
Normal 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
Reference in New Issue
Block a user