From 1034cd7ae2139354c467f46050d17bb09e2247b2 Mon Sep 17 00:00:00 2001 From: Matthew Stanciu Date: Fri, 13 Nov 2020 11:43:04 -0500 Subject: [PATCH 01/51] fix details --- workshops/memory_game/README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/workshops/memory_game/README.md b/workshops/memory_game/README.md index aaea1fa22..fb85ffc30 100644 --- a/workshops/memory_game/README.md +++ b/workshops/memory_game/README.md @@ -310,12 +310,12 @@ function flipCard() { ```
- Here's what the code looks like so far: +Here's what the code looks like so far: -```javascript +```js document.addEventListener('DOMContentLoaded', () => { const cardArray = [....]// the cardArray we created before - + const board = document.querySelector('.board') const result = document.querySelector('#score') const placeholder = "https://cloud-5ystxzer7.vercel.app/7placeholder.png" @@ -346,7 +346,7 @@ document.addEventListener('DOMContentLoaded', () => { } } createBoard() - }) +}) ```
@@ -429,7 +429,8 @@ result.textContent = cardsMatched.length ```
- Our code so far: + + Our code so far: ```javascript document.addEventListener('DOMContentLoaded', () => { From 353946eb2cfa64089d514e8cef1c1ca39f53ebbd Mon Sep 17 00:00:00 2001 From: Matthew Stanciu Date: Fri, 13 Nov 2020 12:07:22 -0500 Subject: [PATCH 02/51] fix list(?) --- workshops/spinning_wheel/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/workshops/spinning_wheel/README.md b/workshops/spinning_wheel/README.md index 7f0b128bf..89eddcee3 100644 --- a/workshops/spinning_wheel/README.md +++ b/workshops/spinning_wheel/README.md @@ -29,6 +29,7 @@ You can get started with it by going to [repl.it/languages/html](https://repl.it # Part 3: Inspecting The Default Files Here on the right side in the files section, you can see 3 files that are: + 1. index.html 2. script.js 3. style.css From 274c9bc0c8bbb09b0cb3ef3e887815f031324413 Mon Sep 17 00:00:00 2001 From: Sam Poder <39828164+sampoder@users.noreply.github.com> Date: Thu, 26 Nov 2020 05:07:04 +0800 Subject: [PATCH 03/51] Add a Fetch that Hack Clubber Workshop (#1334) * Create README.md * Update README.md * Update README.md * Update README.md * Update README.md * Update README.md * Update README.md * Update README.md * Update README.md * Update README.md * Update README.md * Update README.md * Update README.md * Update README.md * Update README.md * Update README.md * Update README.md * Update README.md * add javascript to some tags * fix typo Co-authored-by: Matthew Stanciu --- workshops/fetch_a_hack_clubber/README.md | 249 +++++++++++++++++++++++ 1 file changed, 249 insertions(+) create mode 100644 workshops/fetch_a_hack_clubber/README.md diff --git a/workshops/fetch_a_hack_clubber/README.md b/workshops/fetch_a_hack_clubber/README.md new file mode 100644 index 000000000..a90512aa2 --- /dev/null +++ b/workshops/fetch_a_hack_clubber/README.md @@ -0,0 +1,249 @@ +--- +name: Fetch a Hack Clubber +description: Learn data-fetching with Next.js and meet someone new! +author: '@sampoder' +--- + +# Fetch a Hack Clubber with Next.js + +Hack Club is a global community full of amazing people, but how can we meet them? ~Like everything the solution is coding!~ We're going to be building a site that introduces you to a random Hack Clubber each time you visit the site. We're going to be using [Next.js](https://nextjs.org) and the [Hack Club Scrapbook API](https://scrapbook.hackclub.com/about/). + +### Our data source + +To get our data we'll need a massive database of Hack Clubbers. The most accessible and up to date of which is the [Hack Club Scrapbook API](https://scrapbook.hackclub.com/about/). We can query [`/api/users`](https://scrapbook.hackclub.com/api/users) to get all of the users of the platform (which includes a lot of Hack Clubbers). + +When we query that endpoint we get an array of JSON objects like this: + +``` +{ + "id": "recOdZms0k16sfKB8", + "username": "sampoder", + "avatar": "https://dl.airtable.com/.attachmentThumbnails/f6b8ebb05997ec8e5f17410ae338ba7e/8267718d", + "webring": [ + "recwVYLZVDjBVbO0w", + "recY0GDaua5hyJRyk", + "recpnOXPx5gPZzfqc", + "recMVSvrnWJjAfpVw", + "recoVFgJY4md62oGZ", + "recVrVJNGjlUj5E54", + "recrnD6YZioRmwSQt", + "recp3nz4WcaiGla4H" + ], + "css": "https://deadspryintelligence.sampoder.repl.co/style.css", + "audio": "https://dl.airtable.com/.attachments/d6ddca0c63dff2e05ef91673697461d9/cceb30f2/everything_is_awesome____--_the_lego_movie_--_tegan_and_sara_feat._the_lonely_island.mp3", + "streakCount": 101, + "updatesCount":137, + "displayStreak": true, + "slack": "USNPNJXNX", + "github": "https://github.com/sampoder", + "website": "https://sampoder.com/" +} +``` + +From this object, we can get my Scrapbook username (which we can use to identify them), my Slack user ID, my GitHub and my website. There is a lot more data which you can play around with later! + +### Getting started + +To help you out, I've prepared some [starter code](https://repl.it/@sampoder/fetchahackclubberstarter). Open it up with Repl.it (Google Docs for coding) and then fork it so we can get coding! + +Click `Run ➤` and you'll see what our interface is going to look like. Right now, it's just introducing me ([@sampoder](https://github.com/sampoder)) + +Our starter project + +Next, you're going to want to open `pages/index.js`, this is where we're going to be writing all of our code for the project. + +Take a look at the code, the basic explanation is: + +* We import a few things at the top. We have `next/head` which allows us to add metadata to our head. We import our CSS from `../styles/Home.module.css` to style the web app. We import `isomorphic-unfetch`, which is a utility that helps us fetch data from web APIs. + +* Then inside the function called `Home` (which is our default export), we return all the elements of our site. + + * These are very similar to HTML elements + + * To style elments we add a class by adding a code snippet like `className={styles.card}` to the element. + +> Sidenote: I'd like to give credit to everyone who contributed to `create-next-app` as the starter code that you're using was based on their project. Thank you for your tireless efforts. + +### Fetching that data + +Let's get to the juicy stuff, fetching all that data! + +To do this we will use the `getServerSideProps` feature of Next.js. This means whenever a person visits the site, the server running the site will make a data request (which we'll put in a `getServerSideProps` function) and then render the site in preparation to be served to the user. + +Let's add a basic `getServerSideProps()` function at the bottom of `pages/index.js`: + +```javascript +export async function getServerSideProps() { + return { + props: { 'number': 1 }, + }; +} +``` + +This isn't doing anything at the moment, but that will change soon! For now, just take note that we return an JSON object called `props` that we can use in our page. + +Next up, let's try out fetching data! Using the following snippet we're going to fetch all of the users on Scrapbook, convert the result into JSON and then log it to the console. Make sure you are editing your function, not creating a new one. + +```javascript +export async function getServerSideProps() { + const users = await fetch( + "https://scrapbook.hackclub.com/api/users/" + ).then((r) => r.json()); + console.log(users) + return { + props: { 'number': 1 }, + }; +} +``` + +Open the site in a new tab (or reload) and then check the console in repl.it. You should see a big array of JSON objects for users (that is sadly cut off). Those are our Hack Clubbers!!! + +Now we got all our people, we need to pick one! We can use MATH in Javascript to do this, don't worry the computer does the math we just boss it around :D + +Let's add a bit to our `getServerSideProps()` function: + +```javascript +export async function getServerSideProps() { + let users = await fetch( + "https://scrapbook.hackclub.com/api/users/" + ).then((r) => r.json()); + let user = users[Math.floor(Math.random() * users.length)]; + console.log(user) + return { + props: { 'number': 1 }, + }; +} +``` + +Reload the site again, now should only see one user and it should change everytime you reload! + +Last thing, we need to give our page access to this data, we can do this by replacing `'number': 1` with `user`. We also don't need to log to the console, so our `getServerSideProps()` function should look like: + +```javascript +export async function getServerSideProps(context) { + let users = await fetch( + "https://scrapbook.hackclub.com/api/users/" + ).then((r) => r.json()); + let user = users[Math.floor(Math.random() * users.length)]; + return { + props: { user }, // will be passed to the page component as props + }; +} +``` +Let's pass the page the data, we can do this by editing: + +```javascript +export default function Home() { +``` + +To be: + +```javascript +export default function Home(props) { +``` + +We've got the data, now we need to get it onto the site! + +### Displaying our data + +In Next.js we can use any variable in our page using by putting it in curly braces (`{}`). For example if we wanted to use the person's username we could use `{props.user.username}`. + +Let's get started by changing `sampoder` to `{props.user.username}` in our `h1` tag. Try reloading the site, you should now see a random username on every new load. Can you do the same by changing the source image for the avatar? The variable is `{props.user.avatar}`. + +Now we need to change the links to use our variables. This is a bit more of a challenge, we're going to want to replace `{"https://hackclub.slack.com/team/USNPNJXNX"}` with `{"https://hackclub.slack.com/team/" + props.user.slack}` to get the Slack link to work. Knowing that our Scrapbook username is `props.user.scrapbook` can you do the same for the Scrapbook link? + +At the moment your code inside `main` should look like: + +```javascript + +

+ Meet @{props.user.username} +

+
+ +

Message them Slack →

+

They're on the Hack Club Slack, just like you (I hope)!

+
+ + +

Visit their Scrapbook →

+

Where Hack Clubbers share what they get up to!

+
+
+``` + +You may notice that we're not displaying all the data we have :( The other two pieces of data we'll want to display are GitHub links and the person's website. The issue is these fields are optional so not everyone has them. To fix this we need to wrap the card with `{props.user.github && ( YOUR_CONTENT_HERE )}` to show it only when the GitHub link field is available. Let's add the following to the end of our grid after the ``: + +```javascript +{props.user.github && ( + +

Visit their GitHub →

+

I'm sure it's full of coding projects and a lot of green.

+
+)} +``` + +Challenge! Can you do the same for the website link? The variable is: `{props.user.website}`. + +No peaking, the solution is: + +```javascript +{props.user.website && ( + +

Visit their website →

+

Their little corner of the internet, who knows what you'll find here!

+
+)} +``` + +It's working! + +### Polishing off our site ✨ + +Hooray! You've made something awesome, let's make it epic! + +You may have noticed that we sometimes get bot or inactive users, we want people we can meet! I mean we all love Orpheus, but they're not a real person and don't even get me started on `@wb_bot_a01a9mk4fqw`. + +It's working! + +Let's change this by filtering out users. + +How can we determine is someone is active? The easiest way is to check if they've posted on Scrapbook. Each user object has a field called `updatesCount` that tells us how many posts they've made. + +To filter in Javascript we can use `.filter`, with it we give it an array, some criteria and it'll return an array only with items that meet that criteria. + +In our case we want to add this (↓) code just below the line where we fetch our users + +```javascript +users = users.filter(u => u.updatesCount != 0) +``` + +Try reloading, we shouldn't get any more inactive people and bots! + +Our site is basically there! I hope you're feeling proud because now it's time to take credit for the site :D + +If you scroll down you'll find `@yourname` replace that with your name! + +### Hacking time! + +With great data, comes great oppourtinity! There's a lot we can do with this data, so play around with it! + +Not sure where to go, here a few examples of next steps: + +* Using the custom CSS from Scrapbook, [this version of the project](https://meet-hackclub.vercel.app) makes each profile more personal! +* Want to get a specfic user? I've built a version that allows you to do that by appending their username to the URL, [find me here](https://meet-hackclub.vercel.app/sampoder). +* What more data could we get? We could get a post of their's from Scrapbook, like [I did here](https://meet-hackclub.vercel.app/post-ver)! + +Make something cool? Awesomeeee!!!! Share it on [#ship](https://hackclub.slack.com/archives/C0M8PUPU6/) in the Slack and tag me with [@sampoder](https://hackclub.slack.com/archives/DT08DHJKF/)! From a7ff221d9377d75155b45f43c73c296edaa202f7 Mon Sep 17 00:00:00 2001 From: Talla Giridhar Date: Thu, 26 Nov 2020 02:37:36 +0530 Subject: [PATCH 04/51] Deleted
(#1438) --- workshops/memory_game/README.md | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/workshops/memory_game/README.md b/workshops/memory_game/README.md index fb85ffc30..e389b8bc6 100644 --- a/workshops/memory_game/README.md +++ b/workshops/memory_game/README.md @@ -308,11 +308,10 @@ function flipCard() { } } ``` - -
-Here's what the code looks like so far: -```js +Here's what the code looks like so far: + +```javascript document.addEventListener('DOMContentLoaded', () => { const cardArray = [....]// the cardArray we created before @@ -349,8 +348,6 @@ document.addEventListener('DOMContentLoaded', () => { }) ``` -
- Don't forget to uncomment the event-listener of the card. Comment the `if` statement in `flipCard` function and check whether the images are changing or not. The output works like this. @@ -429,8 +426,7 @@ result.textContent = cardsMatched.length ```
- - Our code so far: + Our code so far will be: ```javascript document.addEventListener('DOMContentLoaded', () => { @@ -483,8 +479,8 @@ document.addEventListener('DOMContentLoaded', () => { createBoard() }) - ``` +
One thing you might notice that the cards are not random. So we have to shuffle the `cardArray`, every time before creating the board, using `sort()` method. The [sort()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort) method sorts the elements of an array in place and returns the sorted array. From 49f004a3305c461d4c92bf6abc55a1dad83766ce Mon Sep 17 00:00:00 2001 From: Sam Poder <39828164+sampoder@users.noreply.github.com> Date: Thu, 26 Nov 2020 19:35:49 +0800 Subject: [PATCH 05/51] Add image for Fetch the Hack Clubber --- workshops/fetch_a_hack_clubber/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/workshops/fetch_a_hack_clubber/README.md b/workshops/fetch_a_hack_clubber/README.md index a90512aa2..ae13b695b 100644 --- a/workshops/fetch_a_hack_clubber/README.md +++ b/workshops/fetch_a_hack_clubber/README.md @@ -2,6 +2,7 @@ name: Fetch a Hack Clubber description: Learn data-fetching with Next.js and meet someone new! author: '@sampoder' +image: 'https://cloud-a1hqcjanz.vercel.app/ezgif-7-3455d319b9c1.gif' --- # Fetch a Hack Clubber with Next.js From a3fcb86a734808864492c176eb79292bfee22119 Mon Sep 17 00:00:00 2001 From: Sam Poder <39828164+sampoder@users.noreply.github.com> Date: Thu, 26 Nov 2020 19:38:37 +0800 Subject: [PATCH 06/51] Wrong variable name corrected for image --- workshops/fetch_a_hack_clubber/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workshops/fetch_a_hack_clubber/README.md b/workshops/fetch_a_hack_clubber/README.md index ae13b695b..c2ed8fc80 100644 --- a/workshops/fetch_a_hack_clubber/README.md +++ b/workshops/fetch_a_hack_clubber/README.md @@ -2,7 +2,7 @@ name: Fetch a Hack Clubber description: Learn data-fetching with Next.js and meet someone new! author: '@sampoder' -image: 'https://cloud-a1hqcjanz.vercel.app/ezgif-7-3455d319b9c1.gif' +img: 'https://cloud-a1hqcjanz.vercel.app/ezgif-7-3455d319b9c1.gif' --- # Fetch a Hack Clubber with Next.js From 112d452ba7351df38f004741388e5f8e201ef520 Mon Sep 17 00:00:00 2001 From: "Ali A. Saleh" Date: Sun, 29 Nov 2020 18:25:51 -0500 Subject: [PATCH 07/51] Fixed broken links. (#1471) - Was prepping for a Hack Club meeting, to use Teachable Machine workshop (very cool, btw). - Realized some hyperlinks were redirecting to 404 pages, came on GitHub to check it out. - For "Decision Trees" & "Regression Analysis," the hyperlinks were missing https://, and were initially redirecting to 404 pages of directories in workshop.hackclub.com which did not exist. - To fix the problems, I've added https:// where necessary. - I'll do more fixes like these if I notice any in the future. --- workshops/teachable_machine/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workshops/teachable_machine/README.md b/workshops/teachable_machine/README.md index c4967a561..ba7d68066 100644 --- a/workshops/teachable_machine/README.md +++ b/workshops/teachable_machine/README.md @@ -10,7 +10,7 @@ author: '@MatthewStanciu' Before we get going, some quick background info: -A machine learning model is a mathematical model for the process of machine learning. Some examples of machine learning models include [artificial neural networks](https://en.wikipedia.org/wiki/Artificial_neural_network), [decision trees](en.wikipedia.org/wiki/Decision_tree_learning), and [regression analysis](en.wikipedia.org/wiki/Regression_analysis). Don’t worry, you don’t have to know what any of these things actually are—Teachable Machine will magically create its own model behind the scenes. +A machine learning model is a mathematical model for the process of machine learning. Some examples of machine learning models include [artificial neural networks](https://en.wikipedia.org/wiki/Artificial_neural_network), [decision trees](https://en.wikipedia.org/wiki/Decision_tree_learning), and [regression analysis](https://en.wikipedia.org/wiki/Regression_analysis). Don’t worry, you don’t have to know what any of these things actually are—Teachable Machine will magically create its own model behind the scenes. Machine learning models are trained with large amounts of data that attempt to “teach” the model how to correctly solve a specific problem (e.g. what a hot dog looks like). If you want to learn more about how machine learning models are trained, CGP Grey made [a fantastic video](https://youtu.be/R9OHn5ZF4Uo) about it. From 3e4649abdc020a4567e8cd8d88cd2e9b82fffdf1 Mon Sep 17 00:00:00 2001 From: Sam Poder <39828164+sampoder@users.noreply.github.com> Date: Mon, 30 Nov 2020 14:15:05 +0800 Subject: [PATCH 08/51] Remove extra div in Chrome Extension to fix prettier errors (#1465) going to try it coz I got another error --- workshops/popup_chrome_extension/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/workshops/popup_chrome_extension/index.html b/workshops/popup_chrome_extension/index.html index 1d7b9a8a1..ff6cdf8de 100644 --- a/workshops/popup_chrome_extension/index.html +++ b/workshops/popup_chrome_extension/index.html @@ -53,7 +53,7 @@

- +
@@ -81,4 +81,4 @@

+ +2. Choose "NodeJS" + + * It will take you a new Repl environment with a [NodeJS](https://www.w3schools.com/nodejs/) configuration. NodeJS is essentially Javascript for servers. It will allow us to manage our database and frontend web pages easily. + +3. Delete all of the code existing in `index.js`. Your Repl environment should only have 1 file and no code in that file. We will be coding in scratch. Here is a reference of how everything should look like: + + ![Deleting the code existing](https://cloud-53f8aawmx.vercel.app/0screenshot_taken_by_shrey_on_11-11-2020_at_21.11__48_.gif) + +## Down to business + +Let's start planning our code! My favorite quote from a mentor is "Programming is 80% hacking and 20% planning. Good code is always preceded by great planning." We should figure out how this link shortener is going to work. + +**Creating new links:** + +1. User makes a form submission to our server +2. We add that data to our database + +**Accessing the short links:** + +1. User goes to the link they want on our domain +2. We check the [database](https://docs.repl.it/misc/database) for the "[path](https://en.wikipedia.org/wiki/URL)" they're on. + * A "path" is whatever comes after the [`.com` or `.org`](https://en.wikipedia.org/wiki/Top-level_domain), etc +3. Based on what the database gave back, we should redirect them to their long link + +**What kind of modules do we need to import?** + +1. [Express](https://www.npmjs.com/package/express), of course +2. [Repl's built-in key:value database](https://docs.repl.it/misc/database) +3. [A `path` module](https://www.npmjs.com/package/path), so we can get the location of the user and send them to where they need to go! + +## Server-side code + +Inside of your `index.js`, let's start importing the modules you need! Add this to the file: + +```javascript +const express = require('express'); +const database = require("@replit/database") +const path = require('path'); +``` + +These are the same modules mentioned above. You don't need to do any `package.json` tasks or install any [NPM](https://en.wikipedia.org/wiki/Npm_(software)) modules. Repl.it will do all of that for you. + +Now, let's add the initial starting code that lets us access the module data. + +```javascript +const db = new database() +const app = express(); +app.use(express.urlencoded({ extended: true })); + +app.listen(3000, () => { + // console.log('server started'); +}); +``` + +**Things to learn from this:** + +* `db` will let us initialize our database +* `app` will initialize our Express app + * `app.use()` is function that will let our server understand what kind of data we're accepting. In our case, we will take URL-encoded data. + * `app.listen()` will start the Express server on the port 3000 (the standard port for web interfaces) + +Let's starting making our first function. This is going to be a "create new link" function, one that our frontend will send data to. A key-value database is exactly how it sounds. A `key` is like the name of a file and a `value` is the contents of that file. The files don't have to be large, they can hold any type of computer data (numbers, booleans, strings/chars, JSON objects, or null). + +Add this code, this is the first function. Try analyzing how the data is aligned and moving through: + +```javascript +app.post('/link', (req, res) => { + + let key = '' + req.body.key; + let value = '' + req.body.value; + + db.set(key, value).then(() => { + db.get(key).then(link => { + res.send(path.join(__dirname + '/' + key)) + }); + }); + +}); +``` + +**Let's go through this code**, after all, you should always know what you're coding! + +1. You can do `app.get()` to force a GET request on the end-user's network requests. In our case, people will POST data to this function. They will do it to `/link` (the path for this function). +2. We are forcing the variables `key` and `value` to be strings by doing `'' + req.body...;` +3. There are a few functions that repl.it gives you in their built-in database. We're using `db.set()` and `db.get()`. Respectively, **set** will add/replace a key:value combo and **get** will get a value from a key value. + 1. `res.send()` returns the data that the user's network requests sees. + 2. `path.join()` will combine the current host path with the key. + +Notice how this code is formatting. There will be no response until the data is set in the database. + +## Frontend code + +Let's create the form where people can submit now. HTML is a web interface language that can be used to make up web pages. So far, everything we've created is on the server, it's time for the user to be able to input new things for our app. + +![GIF of creating a new HTML file](https://cloud-la3kgwf9v.vercel.app/0screenshot_taken_by_shrey_on_11-11-2020_at_22.11__28_.gif) + +1. As outlined by the GIF above, create a new file called: `new.html`. This will let us create a *new* link for our app. +2. HTML is written in [tags](https://www.w3schools.com/tags/default.asp). There are three tags needed for forms: `
`, ``, and `

+ +You've done great so far! And I recommend you to relax and take a 5 minutes break! + +![A cute frog relaxing just like I told you to](https://media.giphy.com/media/9u1J84ZtCSl9K/giphy.gif) + +### 3) Creating the `App` component. + +Let's render the `Button` component in our `App` component. We'll first import it in the `App` component. + +```jsx +import Button from "./components/Button"; + +export default function App() { + return ( +
+

React Calculator

+
+ +
+ + + + +
+
+
+ ) +} +``` + +Explanation: Inside the `calc-wrapper`, we first add a `Button` with a prop of `isInput`. This means that `isInput` will be `true` for this component and as we haven't passed the `isInput` prop to any other `Button`, it will be `false` for those components. Next, we create a `row` and add 4 `Buttons` to it, and as `row` has a property of `flex`, it will be displayed nicely on the browser as a row! We also passed the numbers `7, 8, 9` and the operator `/` as children to those buttons respectively. + +Your preview should look something like this: + +![Preview output of the code written so far](https://cloud-5g2s0vw25.vercel.app/0image.png) + +Excellent! Wondering how they got different colors even if they were the same component? This is why we created the functions `isNum()` and `isEqual()`. They check what the value of the children is and give the `className` accordingly! Isn't it cool? + +Also wondering how the first `Button` component looks different than the others? This is because the `isInput` boolean prop is true for that component and the way we have built our `Button` component is it checks whether the `isInput` is truthy and displays a different `div` accordingly! + +We just created the 4 buttons of our calculator! + +**Challenge:** Can you try to add the rest of the buttons in a similar manner? + +**Hints:** + +1. Use rows for each group of buttons. + +2. Check how the final output looks like and try to implement exactly like that! + +Here's the solution: + +```jsx +export default function App() { + + return ( +
+

React Calculator

+
+ +
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ +
+
+
+ ); +} +``` + +
+ +Now your preview window should look something like this! + +![Preview output of our written code!](https://cloud-kvixq3evc.vercel.app/0image.png) + +You might have understood that we have finally completed our calculator's UI! Now all that remains is to make appropriate functions for our calculator to work as well as pass appropriate `props` to the `Buttons`! + +![Woohoo you did a great job!](https://media.giphy.com/media/3NtY188QaxDdC/giphy.gif) + +### 4) Creating The Functions For Our Calculator. + +Let us first create an `input` state which will store all the buttons pressed by the user and also help us in displaying the calculations. + +```jsx +export default function App() { + const [input, setInput] = useState(""); // Creating the state + + // ... +} +``` + +Next, let's create a function which will store the numbers clicked by the user in the `input` state. + +**NOTE:** We'll create seperate functions for operators and numbers as we don't want the ability to press the operators more than once simultaneously but the numbers can be pressed any number of times we want. If you are confused, don't worry! You'll get it in a minute. + +```jsx +export default function App() { + const [input, setInput] = useState(""); + + function inputNum(val) { + setInput(input + val); + } + + // ... +} +``` + +Explanation: We create a function `inputNum` which will take `val` as an argument. Then, we simply append it to the current stored value in the `input`. + +Now, we'll make a similar but tricky function for operators. Don't worry, It'll be a little confusing if you look at it the first time. + +```jsx +function inputNum(val) {...} + +function inputOperator(val) { + if (input === "" || (operatorsArr.includes(input[input.length - 1]) && operatorsArr.includes(val)) + ) { + return; + } else { + setInput(input + val); + } +} + + // ... +``` + +Explanation: We first create a function `inputOperator` which also takes in a `val` as an argument. Then, we make use of `if-else` statements to check certain conditions. + +First, it checks whether the `input` is empty or not. (We definitely don't want the user to click on the operator if there's no number present in the `input`). If this condition is not true, then it moves ahead to the next condition. + +`input[input.length - 1]` means the last value of the `input` string. Suppose `input` is `12%2*`, then the last value here is `*`, therefore `input[input.length - 1]` here is equal to `*`. Also, `operatorsArr.includes()` is a function which checks whether a certain value is in an array or not. + +So we basically check whether the `operatorsArr` includes the last value of the `input` or not. If this condition is true, it again moves to the next condition which checks whether the `val` argument is included in the `operatorsArr` or not. + +**What does this mean?** In simple language, it simply checks if the previously pressed value by the user is an operator or not while also checking if the newly pressed value (`val`) is again, an operator or not. This will mean that the user pressed the operators 2 times simultaneously. Thus, it will be prevented. + +If the first condition is true alone, the function will simply return nothing. Also if the second and the third condition both are true, it will do the same. + +If all the conditions turn out to be false, finally it will update the `input` state with the `val`. + +Woah that was a pretty big brain! I hope you understood what and why we wrote this code. + +![WOAH!!!](https://media.giphy.com/media/3o6ZtmGkSCwGWQNTOg/giphy.gif) + + +Now, we'll make use of the library `mathjs` which, if you look in the project dependencies, it is already installed for you. We only need to import it in our project and the functions in it will be ready to use. + +![mathjs already installed](https://cloud-9gtba7h1z.vercel.app/0image.png) + +Next, on line 4 of `App.js` we'll import it. + +```jsx +import * as math from "mathjs"; +``` + +Now, every function inside that library is stored in `math`. + +Next, we'll create a function to evaluate our calculations. + +```jsx +function inputOperator(val) {...} + +function evaluate() { + if (input === "" || operatorsArr.includes(input[input.length - 1])) { + return input; + } else { + setInput(math.evaluate(input)); + } +} +``` + +Explanation: This function checks if the `input` is empty or if the last value of the input is an operator. If these conditions are true, this will mean that the `input` can't be evaluated. So, it will simply return the `input`. If everything's perfect and the conditions turn out to be false, it will make use of the `evaluate()` function in the `mathjs` library and simply evaluate the `input`! + +And here we complete all the necessary functions for the calculator to work! + +The last thing which is remaining is passing the appropriate `props` to the `Button` components which will complete our project! + +### 5) Passing The Appropriate `props` To The `Button` Components. + +For the very first `Button` component, we have passed the `isInput` prop to it, we'll also pass `input` as the `children` to that component. + +```jsx +return ( +
+

React Calculator

+
+ + {/* ... */} +
+
+) +``` + +Next, for each `Button` which has a number as their `children`, we'll pass `onClick={inputNum}` prop to it. And for each `Button` which has an operator as their `children` (including the decimal `.`) we'll pass `onClick={inputOperator}` prop to it. + +For the button which has '`=`' as its children, we'll pass `onClick={evaluate}` prop to it. + +Lastly, for the Button which has '`C`' (clear) as its children, we'll pass `onClick` and also create an inline function which will simply clear the state. + +
+After passing all the props, here's what it should look like: + +```jsx +return ( +
+

React Calculator

+
+ +
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ +
+
+
+); +``` + +
+ +With this, we finish building our calculator! It also works as we expected it to. + +![working of our calculator](https://cloud-lp3s1zn91.vercel.app/0final_preview.gif) + + +## Part 4: The End + +You should be proud of yourself that you built this wonderful calculator! + +![yay](https://media.giphy.com/media/xUPGcMzwkOY01nj6hi/giphy.gif) + +Make sure you create an account on CodeSandbox to save this wonderful piece of creation or you'll loose it 😧. + +Now it is up to you! Do crazy things with this project! + +Here are some tasks for you: + +1. Add more buttons to the calculator such as square, square root etc. +[Example](https://codesandbox.io/s/reactjs-calculator-0fkbb). + +2. Add keyboard support to your calculator! +[Example](https://another-simple-calculator.vercel.app/). + +Also checkout these cool examples! + +1. [Example 1](https://codepen.io/giana/pen/GJMBEv) + +2. [Example 2](https://codepen.io/tbremer/pen/wKpaWe) + +3. [Example 3](https://codepen.io/anthonykoch/pen/xVQOwb) + +Check out what other Hack Clubbers built! + +1. [Khushraj](https://codesandbox.io/s/workshopcalculatorstarter-forked-b3h2o) + +2. [Jack](https://codesandbox.io/s/kalukalator-forked-to4tz) + +Now that you have finished building it, you should share your beautiful creation with other people! (I can't wait to see you ship this!) + +You probably know the best ways to get in touch with your friends and family, but if you want to share your project with the worldwide Hack Club community there is no better place to do that than on Slack. +1. In a new tab, open and follow [these directions][slack] to signup for our Slack. +2. Then, post the link to the [`#ship`](https://hackclub.slack.com/messages/ship) channel to share it with everyone and also ping me! + +[slack]: https://slack.hackclub.com/ + + +PS. I'm `@fayd` on slack. From 7f4b906b56e82b9fabb65bbf6e925606c4ea8167 Mon Sep 17 00:00:00 2001 From: Matthew Stanciu Date: Mon, 30 Nov 2020 17:44:38 -0500 Subject: [PATCH 11/51] Add img to react calculator --- workshops/react_calculator/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/workshops/react_calculator/README.md b/workshops/react_calculator/README.md index 8d3548e0f..828a3ecd9 100644 --- a/workshops/react_calculator/README.md +++ b/workshops/react_calculator/README.md @@ -2,6 +2,7 @@ name: 'Simple Calculator' description: 'Build a simple calculator with ReactJS' author: '@faisalsayed10' +img: 'https://cloud-fmzgn1t1z.vercel.app/0screen_shot_2020-11-30_at_5.43.36_pm.png' --- # Simple Calculator From c8d176cc70c09aca3e138c8af204dde50920cff4 Mon Sep 17 00:00:00 2001 From: Khushraj Rathod Date: Wed, 2 Dec 2020 00:03:25 +0530 Subject: [PATCH 12/51] [Workshop Bounty] Add Hack Club CDN Uploader workshop (#1427) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add Hackclub CDN Uploader workshop * Use latest version of Deno and version yargs Deno 1.5.x was broken with yargs -- this has been fixed in the latest version of yargs * Change spelling of Hackclub to Hack Club * Fix description * Convert demo video to GIF * Remove the (If you aren't on Slack...) stuff * Make workshop software requirements clearer * Replace video with clickable-image-which-leads-to-video * Clarify filePath and explain why we're passing the LICENSE file * Remove unneeded explanations * Remove 'Here's what file base is' prefix * Wrap Deno.exit(0) in backticks * Remove full code file and replace with diff * Add clarification for abc.ext * Indent using 2 spaces (ew, ik, but matthew says so so i'll do it lol) * Restructure workshop to not use repl.it, add explanation for Deno * Change code URLs to GitHub * Explain wrapping in a promise * edit disclaimer * Hackclub –> Hack Club Co-authored-by: Matthew Stanciu --- workshops/hcload/README.md | 477 +++++++++++++++++++++++++++++++++++++ 1 file changed, 477 insertions(+) create mode 100644 workshops/hcload/README.md diff --git a/workshops/hcload/README.md b/workshops/hcload/README.md new file mode 100644 index 000000000..9249756d4 --- /dev/null +++ b/workshops/hcload/README.md @@ -0,0 +1,477 @@ +--- +name: Hack Club CDN Uploader +description: Make a Library and CLI to upload to the Hack Club CDN +author: '@KhushrajRathod' +img: https://cloud-bb4wyl0oo.vercel.app/0screenshot_2020-11-11_at_10.00.26_pm.png +--- + +# Hack Club CDN Uploader + +![Preview of Final Demo](https://cloud-67l800cdw.vercel.app/0ezgif-7-277bc9913850.gif) + +Everyone likes unlimited storage, especially when it's available as a cdn (i.e. you can directly embed items uploaded into webpages). If you're on the [Hack Club Slack](https://hackclub.com/slack/), you've probably uploaded something or the other to [#cdn](https://hackclub.slack.com/archives/C016DEDUL87) + +Today, we'll be creating a library that works with the Hack Club CDN. Along with that, we'll also create a command-line tool to upload files using our library. We'll be using [Deno](https://deno.land/) to accomplish this. (If you've previously used Node.js, check out [this talk](https://www.youtube.com/watch?v=M3BM9TB-8yA) on Deno) + +You should know a bit of Javascript to follow along -- but don't worry, if you don't know JS, you'll just take a bit longer and you'll have to Google somewhat. + +Here's the [final code](https://github.com/KhushrajRathod/hcload/tree/workshop/FinalCode) + +If you get stuck anywhere in this workshop, feel free to ask me questions! I'm @KhushrajRathod on the [Hack Club Slack](https://hackclub.com/slack/). + +*Note: This workshop does not use an online code editor, like most other workshops. This workshop will involve using a terminal and downloading a Code Editor + Deno, so school Chromebooks, iPads, and other devices that don't have command line access and program installation access cannot be used for this workshop.* + +## Part 1: Theory + +The Hack Club CDN API only accepts an array of URLs and not direct file uploads. This means we can mirror anything already hosted on a URL, but we can't upload files from our device. To get around this, we'll have to + +1. Start a file server on our device +2. Expose the file server to the internet using [ngrok](https://ngrok.com/) +3. Send the URL of the ngrok instance to the API + +![Visual diagram of the process above: 1. A request is sent from the program to the CDN with the array of URLs. 2. The server sends a request to the ngrok instance as per the URL provided. 3. The ngrok instance responds with the file contents 4. The server hosts the files and responds with the hosted file URLs](https://cloud-hrkhc4qna.vercel.app/0cdn-workflow.png) + +To make it possible to reuse our program in other programs, we'll be creating a [Library](https://en.wikipedia.org/wiki/Library_(computing)) (we'll call this `mod.ts`) and a [CLI](https://en.wikipedia.org/wiki/Command-line_interface) (we'll call this `hcload.ts`) for it. + + +## Part 2: Preparing your environment +### Part 2.1: Getting a code editor + +Since we're going to be editing files locally, you'll need a code editor. I recommend [Visual Studio Code](https://code.visualstudio.com/), here's an article that introduces the basics: https://code.visualstudio.com/docs/introvideos/basics. + +### Part 2.2: Installing deno + +If you've never heard about Deno before, it's a [Secure JavaScript and TypeScript Runtime](https://stackoverflow.com/questions/30838412/what-is-javascript-runtime#:~:text=Javascript%20runtime%20refers%20to%20where,node%2C%20again%20its%20v8.%20%E2%80%93) like Node.js. It let's you execute [JavaScript](https://en.wikipedia.org/wiki/JavaScript) code on your machine. It also has built in [TypeScript](https://www.typescriptlang.org/) support. TypeScript is a superset of JavaScript with useful features like types. Since Deno has built in TypeScript, we can simply create files with a .ts extension and run them without any additional [transpilation](https://en.wikipedia.org/wiki/Source-to-source_compiler) steps. + +- [Deno installation docs](https://deno.land/manual/getting_started/installation) + +In a nutshell: + +- If you're on Windows, open PowerShell (Windows logo in taskbar > Search > PowerShell), and then run + +``` +iwr https://deno.land/x/install/install.ps1 -useb | iex +``` + +- If you're on macOS / Linux, open a Terminal (For macOS, CMD + Space and type "Terminal", for Linux, the command shortcut is usually Ctrl + Alt + T) and run + +``` +curl -fsSL https://deno.land/x/install/install.sh | sh +``` + +After you've done this (you may need to close and reopen your terminal / PowerShell), running `deno --version` should display something like: + +``` +deno 1.5.3 +v8 8.7.220.3 +typescript 4.0.5 +``` + +Awesome! You have Deno setup successfully! + +### Part 2.3: Setting up your Code Editor + +To get started, first create a Folder somewhere memorable on your computer, like your Desktop. Next, open that folder in your Code Editor and create two files in it + +- `hcload.ts` (our CLI) +- `mod.ts` (our library - This is where most of our code will be) + +Inside hcload.ts, add + +```ts +export default async function (): Promise { + console.log("Hello, world!") +} +``` + +Explanation: We're exporting a default function from our library that others will be able to use in their code. + +Inside mods.ts, add + +```ts +import hcload from "./mod.ts" +hcload() +``` + +Explanation: We're using the library we created in `hcload.ts` and calling its default function. + +In your Terminal / Powershell, navigate to the folder in which you have your code. (See [Navigating a terminal](https://www.codingdojo.com/blog/introduction-terminal-navigation)) + +After you're in the folder you created your files in run `ls` to see a list of the names of files in your current directory. You should see `hcload.ts` and `mod.ts` as files present in your folder. + +Run + +```bash +deno install -A --unstable ./hcload.ts +``` + +This adds a [symlink](https://devdojo.com/devdojo/what-is-a-symlink) to your `hcload.ts` file that lets you simply run + +``` +hcload +``` + +instead of running + +``` +deno run -A --unstable ./hcload.ts +``` + +every time + +> Tip: To stop running your code, use CTRL + C + +You just finished Part 2!! + +![Minions cheering](https://cloud-pq5lbfiab.vercel.app/0cheer.gif) + +## Part 3: Programming the library +### Part 3.1: Setting up the server + +Open up `mod.ts`, and let's get started! + +- First, let's create our HTTP server. We'll be using [Oak](https://github.com/oakserver/oak), a server middleware framework for Deno. + +```js +import { Application, send } from 'https://deno.land/x/oak@v6.3.1/mod.ts' + +export default async function (): Promise { + const app = new Application() + app.use(async (context: any) => { + context.response.body = "Server running" + }) + + app.addEventListener("listen", async ({ port }) => { + console.log("HTTP server ready") + }) + + await app.listen({ port: 20685 }) +} +``` + +At the moment, the server simply replies with "Server running" when anything is requested. If you trying running the program (simply run hcload and open `localhost:20685` in your Browser), you should see `HTTP server ready` in your terminal, and "Server running" in your browser + +- Next, let's ask for a filePath (a path to a file) in our library's default function, and serve the file when an HTTP request reaches our server + +```js +import { Application, send } from 'https://deno.land/x/oak@v6.3.1/mod.ts' + +export default async function (filePath: string): Promise { // <--- Changed + const app = new Application() + app.use(async (context: any) => { + await send(context, filePath, { root: '/' }) // <--- Changed + }) + + app.addEventListener("listen", async ({ port }) => { + console.log("HTTP server ready") + }) + + await app.listen({ port: 20685 }) +} +``` + +Create a test file in the folder containing your code, or copy a small (< 1MB) file into that folder. +Modify `hcload.ts` to pass a filePath to the library. + +```js +import * as path from "https://deno.land/std@0.75.0/path/mod.ts" +import hcload from "./mod.ts" + +hcload(path.resolve("./yourFile.txt")) +``` + +where `yourFile.txt` is the file you created / moved into your code directory. For e.x., if you created `hello.txt`, your code would be + +```js +import * as path from "https://deno.land/std@0.75.0/path/mod.ts" +import hcload from "./mod.ts" + +hcload(path.resolve("./hello.txt")) +``` + +Run `hcload` and you should see the contents of the file in your browser preview. Congratulations on making it so far! + +### Part 3.2: Exposing localhost via ngrok + +At the moment, files served will only be accessible over your home network. Since the Hack Club CDN needs to be able to make a request to your server over the internet, we'll need to expose the server over the internet. + +- Import the "ngrok" module and expose the server to the web (in `mod.ts`) + +```js +import { connect, disconnect } from 'https://deno.land/x/ngrok@2.2.3/mod.ts' +import { Application, send } from 'https://deno.land/x/oak@v6.3.1/mod.ts' +import * as path from "https://deno.land/std@0.75.0/path/mod.ts" + +export default async function (filePath: string): Promise { + const fileBase = path.parse(filePath).base + + const app = new Application() + app.use(async (context: any) => { + await send(context, filePath, { root: '/' }) + }) + + app.addEventListener("listen", async ({ port }) => { + const ngrokUrl = `https://${await connect({ protocol: 'http', port })}/${fileBase}` + console.log("NGROK: " + ngrokUrl) + }) + + await app.listen({ port: 20685 }) +} +``` + +- fileBase is the last part of the path to the file, e.g. For "/home/runner/myFile.png", the fileBase is "myFile.png" + +In your terminal, you should see a ngrok URL, visiting this URL should display the LICENSE contents. + +### Part 3.3: Making requests to the Hack Club CDN API with Ky + +- [Ky](https://github.com/sindresorhus/ky) is an HTTP client that lets you easily make requests to any server. + +- We'll be using a POST request, which sends data to a server -- In this case, we'll be sending the ngrok URL. + +- Now, the Hack Club CDN API only accepts an array of URLs, so we'll need to wrap our URL in an array + +Add the following to `mod.ts`: + +```js +import { connect, disconnect } from 'https://deno.land/x/ngrok@2.2.3/mod.ts' +import { Application, send } from 'https://deno.land/x/oak@v6.3.1/mod.ts' +import ky from 'https://unpkg.com/ky/index.js' +import * as path from "https://deno.land/std@0.75.0/path/mod.ts" + +export default async function (filePath: string): Promise { + const fileBase = path.parse(filePath).base + + const app = new Application() + app.use(async (context: any) => { + await send(context, filePath, { root: '/' }) + }) + + app.addEventListener("listen", async ({ port }) => { + const ngrokUrl = `https://${await connect({ protocol: 'http', port })}/${fileBase}` + + // @ts-ignore + let response: string[] = await ky.post('https://cdn.hackclub.com/api/new', { json: [ngrokUrl] }).json() // <--- Wrapped in array [ ngrokUrl ] + + console.log(response[0]) + disconnect() + }) + + await app.listen({ port: 20685 }) +} +``` + +Run `hcload` and you should get a URL like `https://cloud-something.vercel.app/yourFile.ext`. If opening the URL loads the file in your browser, or your browser asks you to download the file, CONGRATULATIONS! You've just hosted a file successfully on the CDN! + +Now, instead of logging the response from our library, we want to return it to the calling function. We'll log it from the calling function instead. + +- First, wrap return value of the function in a Promise. + +```js +return new Promise(async resolve => { + const fileBase = path.parse(filePath).base + + const app = new Application() + app.use(async (context: any) => { + await send(context, filePath, { root: '/' }) + }) + + app.addEventListener("listen", async ({ port }) => { + const ngrokUrl = `https://${await connect({ protocol: 'http', port })}/${fileBase}` + + // @ts-ignore + let response: string[] = await ky.post('https://cdn.hackclub.com/api/new', { json: [ngrokUrl] }).json() + + disconnect() + return resolve(response[0]) // We're resolving the promise here instead of just returning + }) + await app.listen({ port: 20685 }) +}) as Promise +``` + +- Next, change the Return type of the function to `Promise` from `Promise` + +```js +export default async function (filePath: string): Promise { + return new Promise(async resolve => { + // ... + }) as Promise +} +``` + +
+ Here's how `hcload.ts` should look now + +```js +import { connect, disconnect } from 'https://deno.land/x/ngrok@2.2.3/mod.ts' +import { Application, send } from 'https://deno.land/x/oak@v6.3.1/mod.ts' +import ky from 'https://unpkg.com/ky/index.js' +import * as path from "https://deno.land/std@0.75.0/path/mod.ts" + +export default async function (filePath: string): Promise { // <--- Changed + return new Promise(async resolve => { // <--- Changed + const fileBase = path.parse(filePath).base + + const app = new Application() + app.use(async (context: any) => { + await send(context, filePath, { root: '/' }) + }) + + app.addEventListener("listen", async ({ port }) => { + const ngrokUrl = `https://${await connect({ protocol: 'http', port })}/${fileBase}` + + // @ts-ignore + let response: string[] = await ky.post('https://cdn.hackclub.com/api/new', { json: [ngrokUrl] }).json() + + disconnect() + return resolve(response[0]) + }) + + await app.listen({ port: 20685 }) + }) as Promise // <--- Changed +} +``` + +
+ +In `hcload.ts`: + +```js +import * as path from "https://deno.land/std@0.75.0/path/mod.ts" +import hcload from "./mod.ts" + +console.log(await hcload(path.resolve("./LICENSE"))) +Deno.exit(0) +``` + +Note that we can now simply `Deno.exit(0)` after we get and log the URL -- we no longer have to exit the program manually using CTRL + C. + +EXCELLENT! We've successfully made the library, and we've also used it in `hcload.ts`. + +![Hermione, Ron and Seamus cheering](https://cloud-pq5lbfiab.vercel.app/1cheer2.gif) + +## Part 4: Programming the CLI + +Now that our library is ready, let's make `hcload.ts` use arguments from the CLI instead of [hardcoding](https://stackoverflow.com/questions/1895789/what-does-hard-coded-mean) a file. + +- First, let's import [Yargs](https://yargs.js.org/). Yargs makes it easy to parse command-line arguments. + +```js +import Yargs from "https://deno.land/x/yargs@v16.1.1-deno/deno.ts" +import * as path from "https://deno.land/std@0.75.0/path/mod.ts" +import hcload from "./mod.ts" + +console.log(await hcload(path.resolve("./LICENSE"))) +Deno.exit(0) +``` + +Next, let's parse the args using Yargs (just after all the import statements in `hcload.ts`) + +```js +const args = Yargs(Deno.args) + .usage("Usage: hcload -f file") + .option("file", { + alias: "f", + description: "Path to file to upload", + demandOption: true, + }) + .example('hcload -f myPic.png', 'Upload a file') + .argv + +console.log(args) +``` + +Run `hcload -f test` and you should probably see something like + +``` +{ _: [], f: "test", file: "test", "$0": "deno run" } +``` + +We want to pass the full path of the property "file" to our library's default function + +```js +import Yargs from "https://deno.land/x/yargs@v16.1.1-deno/deno.ts" +import * as path from "https://deno.land/std@0.75.0/path/mod.ts" +import hcload from "./mod.ts" + +const args = Yargs(Deno.args) + .usage("Usage: hcload -f file") + .option("file", { + alias: "f", + description: "Path to file to upload", + demandOption: true, + }) + .example('hcload -f myPic.png', 'Upload a file') + .argv + +console.log("Working...") + +const fullPath = path.resolve(args.file) +const url: string = await hcload(fullPath) + +console.log(url) + +Deno.exit(0) +``` + +Try running + +```bash +hcload -f abc.ext +``` + +where `abc.ext` is a file in your current directory. For e.x., if the output of `ls` was + +``` +myMovie.mp4 +mySong.mp3 +myDocument.pdf +``` + +and you wanted to upload the document, you would type + +```bash +hcload -f myDocument.pdf +``` + +The program should return a Hack Club CDN Url + +CONGRATULATIONS! You've successfully managed to build a Deno Library and a CLI for it! + +![Dumbledore and Snape partying](https://cloud-pq5lbfiab.vercel.app/3dumbledoreparty.gif) + +## Hacking! + +### Using your library / CLI in other projects + +Whenever you want to quickly upload a small file to a CDN, you know what to use! + +- If your project is a Deno project, you can simply import the `mod.ts` into your project like we did from `hcload.ts`. If you want to, you can publish your library to https://deno.land/x/ to use it easily in other projects. + +```js +import hcload from "./mod.ts" +``` + +- If your project is not a Deno project, you can use the CLI as a library too -- just make sure you pass the --quiet switch to Deno and remove the `console.log()`s when installing/running hcload. + +```js +deno install -f -A --unstable --quiet hcload.ts +``` + +then, run `hcload` and parse stdout. + +### Extending your library + +By design, the Hack club CDN accepts an _array_ of **URLs**. This means we can: + +- [Mirror a URL instead of uploading a file](https://github.com/KhushrajRathod/hcload/tree/workshop/URLonly) + +- Add support for multiple URLs and files - Check this out on [GitHub](https://github.com/KhushrajRathod/hcload) or [deno.land/x](https://deno.land/x/KhushrajRathod/hcload) + +### Host a website on the Hack Club CDN + +By getting a bit creative, we can host websites on the Hack club CDN (Note: this is by far not the best way to do this, but it _is_ fun :) ) + +- [Here's](https://cloud-2gxi88gfk.vercel.app/0index.html) the website made from the [personal website workshop](https://workshops.hackclub.com/personal_website/) hosted using the Hack Club CDN. Can you figure out how it's done? Hint: Right-click > Inspect element to view the website code. + +Did you make something awesome? Share it on [#ship](https://hackclub.slack.com/archives/C0M8PUPU6) in the Hack Club Slack and tag me with [@KhushrajRathod](https://hackclub.slack.com/team/U01C21G88QM)! From 795e6aeff62637876b263b10512936df45052b20 Mon Sep 17 00:00:00 2001 From: Vitor Vavolizza <57807200+vitorrv10@users.noreply.github.com> Date: Tue, 1 Dec 2020 17:16:12 -0300 Subject: [PATCH 13/51] Fix random radius range (#1451) Changed Math.floor(Math.random() * 30) + 5 to Math.round(Math.random() * 25) + 5 to correctly generate radius numbers between 5 and 30. --- workshops/splatter_paint/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workshops/splatter_paint/README.md b/workshops/splatter_paint/README.md index 0212a48ae..8495f5d52 100644 --- a/workshops/splatter_paint/README.md +++ b/workshops/splatter_paint/README.md @@ -156,7 +156,7 @@ We’re getting somewhere, but this still doesn’t feel very splattery. Part of what makes splatter paint so fun to create and look at is the chaotic randomness of everything on the canvas. So, if you want to get your website as close to splatter paint as possible, the best way to do it is to introduce some randomness. -Change the radius of your circles from `10` to `Math.floor(Math.random() * 30) + 5`. This makes the radius a random number between 5 and 30. Then run the repl again. +Change the radius of your circles from `10` to `Math.round(Math.random() * 25) + 5`. This makes the radius a random number between 5 and 30. Then run the repl again. ![](img/random-radius.JPG) From 290bf93aa99f2bd427b40dc59daaa2da1072c06a Mon Sep 17 00:00:00 2001 From: Soham Bhattacharya Date: Wed, 2 Dec 2020 03:42:11 -0800 Subject: [PATCH 14/51] (Workshop Bounty) Logging In With Flask (#1458) * Started on the Requests with Python Stock Viewer project * Finished markdown * Oops accidentally left in a file * Final * Started work * Flask-login * Added more content * Delete README.MD * Update README.md * Update README.md * Update README.md * Add hacks Will do one last reread tomorrow before submitting * Cleaned it up and friendlified. Might add gifs. * Finished Workshop * Added a link to the code * Fix merge conflict * Update workshops/flask-login/README.md Co-authored-by: Sam Poder <39828164+sampoder@users.noreply.github.com> --- workshops/flask-login/README.md | 306 ++++++++++++++++++++++++++++++++ 1 file changed, 306 insertions(+) create mode 100644 workshops/flask-login/README.md diff --git a/workshops/flask-login/README.md b/workshops/flask-login/README.md new file mode 100644 index 000000000..7a26d9e31 --- /dev/null +++ b/workshops/flask-login/README.md @@ -0,0 +1,306 @@ +--- +name: Logging In +description: Make a Flask Webserver that allows you to register and log in users. +author: '@sohamb117' +--- + +# Logging in + +Open a website, say [Github](https://github.com). Okay, now sign in. What just happened? What did the code do? How does "Logging In" work? Let's try building it ourselves. + +We'll have to: +* Create a Flask webserver +* Make app routes that write to and read from a JSON file +* Test our webserver with Insomnia. + +Here's a quick look at what we'll be making: +![Gif](https://cloud-1x9c5m3zh.vercel.app/0screen-capture.gif) +This is a simple backend demo but you can hook this up to ANY frontend you want - and I've provided a demo on how to do that at the bottom. +Here's a link to the [code](https://repl.it/@sohamb117/FlaskTutorial#main.py) + +This workshop does assume a very basic knowledge of Python and HTTP Requests but I hope I've broken things down so that beginners can understand too. I've also linked sources at the bottom so you can read up on these topics. + +## What does Flask do? + +The [Flask](https://pypi.org/project/flask/) library in Python is what we'll use to create a server for us to write (POST) and read (GET) data from. We'll test our Flask webserver with [Insomnia](https://insomnia.rest/), a desktop program or Chrome extension that allows us to test APIs by sending GET or POST requests. (If you don't know what GET or POST requests are, I've linked some helpful sources at the bottom). Flask works with routes, which allow us to define what the code does when a certain request is recieved. We'll be defining routes to write to and read from the JSON file we'll be creating. + +## Downloading Insomnia + +In order to test our Flask server, we'll need to send requests to the server. We can use Insomnia to do this. Insomnia is great because it is available for so many platforms. All you need is a Chrome browser. You can get Insomnia at the [Chrome Web Store](https://chrome.google.com/webstore/detail/insomnia-rest-client/gmodihnfibbjdecbanmpmbmeffnmloel?hl=en-US). You should see a screen like this: +![Insomnia Web Store](https://cloud-a7eisnzme.vercel.app/0image.png) + +Press the "Add to Chrome" button, and then press "Add App." +![Add to Chrome](https://cloud-6j7h3tfkw.vercel.app/0image.png) + +We'll work more with Insomnia after writing our code. + +## Getting Started + +We'll be using [Repl.it](https://repl.it) to work on this, because it makes it very easy for us to run our webserver and code in Python. Let's set up our accounts. + +Go to [repl.it](https://repl.it). You should see something like this: +![repl.it](https://cloud-i5iknsq3z.vercel.app/0image.png) + +Either press "Log In" or "Sign Up," depending on whether or not you already have an account. +Once you're done, you should see the "Home" page. +Press either the plus icon in the top right or the "New Repl" button in the top left. You should see this screen: + +![repl create](https://cloud-f0le2m5gi.vercel.app/0image.png) + +Set the language to Python and the name to anything you want. + +## Starting the Code + +Let's start with importing the libraries we'll be using. +```python +from flask import Flask, request +import json +``` + +This just imports the `Flask` and `request` modules from the `flask` library, because they're the only ones we need from that library. We also need `json` because that's what we'll be using for storing our data. +In order to create a webserver with Flask we'll need to define a Flask app. We do this with the following line: +```python +app = Flask("MyApp") +``` +The parameter passed into `Flask()` can be any string, and it's the name of your app. You can set it to any valid string you want as it doesn't really make a difference in the project. +We've now defined our app, but it won't do anything unless we run it. Let's add a line at the bottom of our code to run this. +```python +app.run(host='0.0.0.0') +``` +This runs our newly created app on the default port, 5000. It doesn't really matter which port you use because repl.it makes it possible to use any port you want here (as long as it's not port 80). + +## Adding Routes +Right now, our app is kind of useless. If you run it, it should start a new webserver, but it won't do anything. Let's start by creating our first route. + +```python +@app.route("/", methods=['GET']) +def helloworld(): + return("Hello World!") +``` +The first line adds a [decorator](https://pythonbasics.org/decorators/) that tells Flask to treat the function as a route. A route is essentially an path that we can send a request to. +You can see routes being used in almost any website - they use the `/` character to specify a route. For example, you can see `https://www.iana.org/domains/reserved` uses `/` to indicate that from the main site, you want to go to `domains`, and from there you want to go to `reserved`. +In Flask, `/` without any text after it indicates that this is the route you'll see when first going to the URL. +Flask handles routes by creating functions, the return values of which Flask displays to the user. In this function we see: +```python + return("Hello World!") +``` +This function has a return value of "Hello World!" so that's what Flask will display to the user. + +Repl.it should automatically open a panel to allow you to view your Flask app. It should also have a URL to the app. +![Link](https://cloud-94isv6yfc.vercel.app/0image.png) +If you open the URL in a new tab and you should see "Hello World!" displayed. Congratulations, you've successfully finished creating your first route! + +Now let's start on the routes we want to define for our webserver to handle logging in. + +## Registration Route + +We want our new route to be `/register`. This means that in order to register, we'll send a request to `yourURL/register` (of course, yourURL will be the URL given to you by repl.it). + +The way you specify the route is as simple as specifying the route in the decorator. It should look something like this: +```python +@app.route("/register", methods=['POST']) +``` +The `POST` method tells Flask that we want to send a POST request here to write data instead of a GET request to receive data. + +Now let's write the code that will run when we send this request. We'll need to create the file to write to. We can do this within repl.it. +![Add File](https://cloud-j1ica8j2r.vercel.app/0image.png) + +Name this file "registered.json" +Let's define our function now. Here's the code: +```python +def register(): + username = request.args.get("username") + password = request.args.get("password") + data = request.args.get('data') + with open('registered.json', 'a+') as db: + db.seek(0) + check = (f"\"username\": \"{username}\"") + contents = db.read() + if not(check in contents): + db.write(json.dumps({'username': username, 'password': password, 'data':data})+'\n') + return("Success") + else: + return("User already registered.") +``` + +What does this code do? Let's break it down part by part. + +```python + username = request.args.get("username") + password = request.args.get("password") + data = request.args.get('data') +``` +`request.args.get(ARGUMENT)` is a method from the `request` module in Flask that allows you to get arguments from a webrequest. We're saving the value for the argument `"username"` in the variable `username`. This also applies for `"password"` and `"data"`. Arguments for this route will look something like this: +``` +yourURL/register?username=USERNAME&password=PASSWORD&data=DATA +``` +As you can see, the `?` character specifies that arguments will follow. We separate the arguments using `&`. + +Now let's take a look at the rest of the route. +```python +with open('registered.json', 'a+') as db: + db.seek(0) + check = (f"\"username\": \"{username}\"") + contents = db.read() + if not(check in contents): + db.write(json.dumps({'username': username, 'password': password, 'data':data})+'\n') + return("Success") + else: + return("User already registered.") +``` + +In this block, we are using a context manager to open `registered.json` with `a+` to allow us to read and write to the file but only to write to the end of the file. + +The line `db.seek(0)` tells us to position the cursor at the beginning of the file. +We define a variable `check` that is set to a formatted string containing the username. For example, if the username was "user1" the string would be `"username" = "user1"`. +We are also setting `contents` to be the contents of our file. + +The `if` statement here determines if the file's contents, `contents`, has the username string, `check`. If it does not, the following lines of code are executed: +```python +db.write(json.dumps({'username': username, 'password': password, 'data':data})+'\n') +return("Success") +``` +The `json.dumps` method converts a Python dictionary into a JSON string. We can pass the arguments we took from the request into this dictionary. We add a `\n` to the end of the string to ensure that the next entry starts on the next line. We write this line to the file with `db.write()` and return `"Success"` as the return value. +If `check` is in `contents`, this code will not run. Instead, the route will not append anything to the file, and will just return `"User already registered"`. + +## Login Route + +We've finished registration, but there's little point to that if the users cannot login. Let's create a GET request for logging in. +Let's start by creating the route. +```python +@app.route("/login", methods=['GET']) +``` +This route will be `/login` and we want to use a GET request, which tells the server that we just want to read data, not add it. + +The code in this route is more simple than the code for the other route. +```python +def login(): + username = request.args.get("username") + password = request.args.get("password") + with open('registered.json', 'r') as db: + jsonList = [] + for x in db.readlines(): + jsonList.append(json.loads(str(x))) + for i in jsonList: + if (i["username"] == username) and (i["password"] == password): + return(i["data"]) + return("User not found") +``` + +Once again, we are taking in arguments with `request.args.get` and saving them in variables. However, since we are trying to log in and GET the value of `data` we aren't passing that in. +We once again use a context manager to open `registered.json` but this time we use `r` because we only want to read. +We define an empty list called `jsonList` that we will populate soon. +```python +for x in db.readlines(): + jsonList.append(json.loads(str(x))) +``` +This loop populates the list with Python dictionaries loaded from the JSON strings on each line with `json.loads()` + +```python +for i in jsonList: + if (i["username"] == username) and (i["password"] == password): + return(i["data"]) +return("User not found") +``` + +This `for` loop goes through `jsonList` for each value `i` in the list. If `username` and `password` match the values in the dictionary, the method returns `i[data]` or the data value associated with that username and password. We have an `return` statement at the end that runs if the function does not exit. (This works because return statements stop the function from running any further). + +## Testing the Code + +Finally, we'll be using Insomnia to test if our code works. +Let's launch the app (you can do this by going to `chrome://apps` in the URL bar of Chrome). +You should see something like this: +![Insomnia](https://cloud-m4ukm1v5f.vercel.app/0image.png) + +Click "Create a Request" and enter any request name, it doesn't really matter. + +You should see this: +![Request](https://cloud-ottonqsbw.vercel.app/0image.png) + +In the dropdown menu labeled "GET" by default, click it and set to POST. Insomnia will send a post request now. + +Next to that, there's a text field. Since we're testing our POST route, or our `/register` route, we want to set that to `yourURL/register?username=USERNAME&password=PASSWORD&data=DATA`. Replace `yourURL` with the URL you got from repl.it, and replace `USERNAME`, `PASSWORD`, and `DATA` to be any values you want. +For example, I might put `https://FlaskTutorial.sohamb117.repl.co/register?username=soham&password=insecure&data=testing`. +Press the purple "Send" button, and then check your `registered.json` file. If everything worked, you should see a new entry in the file. + +Now that we've tested the POST request, we should test the GET request. Set the dropdown back to GET and replace the url with this: +`yourURL/login?username=USERNAME&password=PASSWORD` + +Again, replace the necessary fields and hit "Send". If all goes well, you should see the value for `data` returned back to you. For me, that's `testing`. + +If everything works, your webserver is complete! + + +## Final Look at the Code + +We've finished the code! It should look like this: + +```python +from flask import Flask, request +import json + +app = Flask("MyApp") + +@app.route("/", methods=['GET']) +def helloworld(): + return("Hello World!") + +@app.route("/register", methods=['POST']) +def register(): + username = request.args.get("username") + password = request.args.get("password") + data = request.args.get('data') + with open('registered.json', 'a+') as db: + db.seek(0) + check = (f"\"username\": \"{username}\"") + contents = db.read() + if not(check in contents): + db.write(json.dumps({'username': username, 'password': password, 'data':data})+'\n') + return("Success") + else: + return("User already registered.") + +@app.route("/login", methods=['GET']) +def login(): + username = request.args.get("username") + password = request.args.get("password") + with open('registered.json', 'r') as db: + jsonList = [] + for x in db.readlines(): + jsonList.append(json.loads(str(x))) + for i in jsonList: + if (i["username"] == username) and (i["password"] == password): + return(i["data"]) + return("User not found") + +app.run(host='0.0.0.0') +``` + +To recap, this code +* Creates a Flask app +* Creates routes for the app +* Reads values from arguments +* Stores values in a JSON file +* Returns values as a response to a GET or POST request + +What this does NOT have: +* Security +* Password requirements +* Encryption +* CAPTCHA or any anti-bot measures + +Note: While you can modify this code to have these features, you should **NEVER** store passwords in plaintext like this webserver does. If you were to implement security features, this webserver would work for login. + +## Hacks and Further Reading + +Here are some things to check out or read. +* [Get and Post Requests](https://www.w3schools.com/tags/ref_httpmethods.asp) +* [Flask Documentation](https://flask.palletsprojects.com/en/1.1.x/) +* [How JSON Works](https://www.tutorialspoint.com/json/json_overview.htm) +* [Password Hashing and Security](https://thycotic.com/company/blog/2020/05/07/how-do-passwords-work/) +* [Article on How Login Works](https://iam.harvard.edu/resources/behind-login-screen) + +Here are some hacks that show what you can do with this: +* [Make a frontend with HTML and serve it with Flask too](https://repl.it/@sohamb117/FlaskTutorial-1#main.py) +* [Ensure that passwords meet certain conditions](https://repl.it/@sohamb117/FlaskTutorial-2#main.py) +* [Take in more data, and store timestamps](https://repl.it/@sohamb117/FlaskTutorial-3#main.py) From a0f902fa59af83b75d23b6a5bb9676fb16fcf1cb Mon Sep 17 00:00:00 2001 From: Sam Poder <39828164+sampoder@users.noreply.github.com> Date: Wed, 2 Dec 2020 19:45:55 +0800 Subject: [PATCH 15/51] Revert "(Workshop Bounty) Logging In With Flask (#1458)" (#1483) This reverts commit 290bf93aa99f2bd427b40dc59daaa2da1072c06a. --- workshops/flask-login/README.md | 306 -------------------------------- 1 file changed, 306 deletions(-) delete mode 100644 workshops/flask-login/README.md diff --git a/workshops/flask-login/README.md b/workshops/flask-login/README.md deleted file mode 100644 index 7a26d9e31..000000000 --- a/workshops/flask-login/README.md +++ /dev/null @@ -1,306 +0,0 @@ ---- -name: Logging In -description: Make a Flask Webserver that allows you to register and log in users. -author: '@sohamb117' ---- - -# Logging in - -Open a website, say [Github](https://github.com). Okay, now sign in. What just happened? What did the code do? How does "Logging In" work? Let's try building it ourselves. - -We'll have to: -* Create a Flask webserver -* Make app routes that write to and read from a JSON file -* Test our webserver with Insomnia. - -Here's a quick look at what we'll be making: -![Gif](https://cloud-1x9c5m3zh.vercel.app/0screen-capture.gif) -This is a simple backend demo but you can hook this up to ANY frontend you want - and I've provided a demo on how to do that at the bottom. -Here's a link to the [code](https://repl.it/@sohamb117/FlaskTutorial#main.py) - -This workshop does assume a very basic knowledge of Python and HTTP Requests but I hope I've broken things down so that beginners can understand too. I've also linked sources at the bottom so you can read up on these topics. - -## What does Flask do? - -The [Flask](https://pypi.org/project/flask/) library in Python is what we'll use to create a server for us to write (POST) and read (GET) data from. We'll test our Flask webserver with [Insomnia](https://insomnia.rest/), a desktop program or Chrome extension that allows us to test APIs by sending GET or POST requests. (If you don't know what GET or POST requests are, I've linked some helpful sources at the bottom). Flask works with routes, which allow us to define what the code does when a certain request is recieved. We'll be defining routes to write to and read from the JSON file we'll be creating. - -## Downloading Insomnia - -In order to test our Flask server, we'll need to send requests to the server. We can use Insomnia to do this. Insomnia is great because it is available for so many platforms. All you need is a Chrome browser. You can get Insomnia at the [Chrome Web Store](https://chrome.google.com/webstore/detail/insomnia-rest-client/gmodihnfibbjdecbanmpmbmeffnmloel?hl=en-US). You should see a screen like this: -![Insomnia Web Store](https://cloud-a7eisnzme.vercel.app/0image.png) - -Press the "Add to Chrome" button, and then press "Add App." -![Add to Chrome](https://cloud-6j7h3tfkw.vercel.app/0image.png) - -We'll work more with Insomnia after writing our code. - -## Getting Started - -We'll be using [Repl.it](https://repl.it) to work on this, because it makes it very easy for us to run our webserver and code in Python. Let's set up our accounts. - -Go to [repl.it](https://repl.it). You should see something like this: -![repl.it](https://cloud-i5iknsq3z.vercel.app/0image.png) - -Either press "Log In" or "Sign Up," depending on whether or not you already have an account. -Once you're done, you should see the "Home" page. -Press either the plus icon in the top right or the "New Repl" button in the top left. You should see this screen: - -![repl create](https://cloud-f0le2m5gi.vercel.app/0image.png) - -Set the language to Python and the name to anything you want. - -## Starting the Code - -Let's start with importing the libraries we'll be using. -```python -from flask import Flask, request -import json -``` - -This just imports the `Flask` and `request` modules from the `flask` library, because they're the only ones we need from that library. We also need `json` because that's what we'll be using for storing our data. -In order to create a webserver with Flask we'll need to define a Flask app. We do this with the following line: -```python -app = Flask("MyApp") -``` -The parameter passed into `Flask()` can be any string, and it's the name of your app. You can set it to any valid string you want as it doesn't really make a difference in the project. -We've now defined our app, but it won't do anything unless we run it. Let's add a line at the bottom of our code to run this. -```python -app.run(host='0.0.0.0') -``` -This runs our newly created app on the default port, 5000. It doesn't really matter which port you use because repl.it makes it possible to use any port you want here (as long as it's not port 80). - -## Adding Routes -Right now, our app is kind of useless. If you run it, it should start a new webserver, but it won't do anything. Let's start by creating our first route. - -```python -@app.route("/", methods=['GET']) -def helloworld(): - return("Hello World!") -``` -The first line adds a [decorator](https://pythonbasics.org/decorators/) that tells Flask to treat the function as a route. A route is essentially an path that we can send a request to. -You can see routes being used in almost any website - they use the `/` character to specify a route. For example, you can see `https://www.iana.org/domains/reserved` uses `/` to indicate that from the main site, you want to go to `domains`, and from there you want to go to `reserved`. -In Flask, `/` without any text after it indicates that this is the route you'll see when first going to the URL. -Flask handles routes by creating functions, the return values of which Flask displays to the user. In this function we see: -```python - return("Hello World!") -``` -This function has a return value of "Hello World!" so that's what Flask will display to the user. - -Repl.it should automatically open a panel to allow you to view your Flask app. It should also have a URL to the app. -![Link](https://cloud-94isv6yfc.vercel.app/0image.png) -If you open the URL in a new tab and you should see "Hello World!" displayed. Congratulations, you've successfully finished creating your first route! - -Now let's start on the routes we want to define for our webserver to handle logging in. - -## Registration Route - -We want our new route to be `/register`. This means that in order to register, we'll send a request to `yourURL/register` (of course, yourURL will be the URL given to you by repl.it). - -The way you specify the route is as simple as specifying the route in the decorator. It should look something like this: -```python -@app.route("/register", methods=['POST']) -``` -The `POST` method tells Flask that we want to send a POST request here to write data instead of a GET request to receive data. - -Now let's write the code that will run when we send this request. We'll need to create the file to write to. We can do this within repl.it. -![Add File](https://cloud-j1ica8j2r.vercel.app/0image.png) - -Name this file "registered.json" -Let's define our function now. Here's the code: -```python -def register(): - username = request.args.get("username") - password = request.args.get("password") - data = request.args.get('data') - with open('registered.json', 'a+') as db: - db.seek(0) - check = (f"\"username\": \"{username}\"") - contents = db.read() - if not(check in contents): - db.write(json.dumps({'username': username, 'password': password, 'data':data})+'\n') - return("Success") - else: - return("User already registered.") -``` - -What does this code do? Let's break it down part by part. - -```python - username = request.args.get("username") - password = request.args.get("password") - data = request.args.get('data') -``` -`request.args.get(ARGUMENT)` is a method from the `request` module in Flask that allows you to get arguments from a webrequest. We're saving the value for the argument `"username"` in the variable `username`. This also applies for `"password"` and `"data"`. Arguments for this route will look something like this: -``` -yourURL/register?username=USERNAME&password=PASSWORD&data=DATA -``` -As you can see, the `?` character specifies that arguments will follow. We separate the arguments using `&`. - -Now let's take a look at the rest of the route. -```python -with open('registered.json', 'a+') as db: - db.seek(0) - check = (f"\"username\": \"{username}\"") - contents = db.read() - if not(check in contents): - db.write(json.dumps({'username': username, 'password': password, 'data':data})+'\n') - return("Success") - else: - return("User already registered.") -``` - -In this block, we are using a context manager to open `registered.json` with `a+` to allow us to read and write to the file but only to write to the end of the file. - -The line `db.seek(0)` tells us to position the cursor at the beginning of the file. -We define a variable `check` that is set to a formatted string containing the username. For example, if the username was "user1" the string would be `"username" = "user1"`. -We are also setting `contents` to be the contents of our file. - -The `if` statement here determines if the file's contents, `contents`, has the username string, `check`. If it does not, the following lines of code are executed: -```python -db.write(json.dumps({'username': username, 'password': password, 'data':data})+'\n') -return("Success") -``` -The `json.dumps` method converts a Python dictionary into a JSON string. We can pass the arguments we took from the request into this dictionary. We add a `\n` to the end of the string to ensure that the next entry starts on the next line. We write this line to the file with `db.write()` and return `"Success"` as the return value. -If `check` is in `contents`, this code will not run. Instead, the route will not append anything to the file, and will just return `"User already registered"`. - -## Login Route - -We've finished registration, but there's little point to that if the users cannot login. Let's create a GET request for logging in. -Let's start by creating the route. -```python -@app.route("/login", methods=['GET']) -``` -This route will be `/login` and we want to use a GET request, which tells the server that we just want to read data, not add it. - -The code in this route is more simple than the code for the other route. -```python -def login(): - username = request.args.get("username") - password = request.args.get("password") - with open('registered.json', 'r') as db: - jsonList = [] - for x in db.readlines(): - jsonList.append(json.loads(str(x))) - for i in jsonList: - if (i["username"] == username) and (i["password"] == password): - return(i["data"]) - return("User not found") -``` - -Once again, we are taking in arguments with `request.args.get` and saving them in variables. However, since we are trying to log in and GET the value of `data` we aren't passing that in. -We once again use a context manager to open `registered.json` but this time we use `r` because we only want to read. -We define an empty list called `jsonList` that we will populate soon. -```python -for x in db.readlines(): - jsonList.append(json.loads(str(x))) -``` -This loop populates the list with Python dictionaries loaded from the JSON strings on each line with `json.loads()` - -```python -for i in jsonList: - if (i["username"] == username) and (i["password"] == password): - return(i["data"]) -return("User not found") -``` - -This `for` loop goes through `jsonList` for each value `i` in the list. If `username` and `password` match the values in the dictionary, the method returns `i[data]` or the data value associated with that username and password. We have an `return` statement at the end that runs if the function does not exit. (This works because return statements stop the function from running any further). - -## Testing the Code - -Finally, we'll be using Insomnia to test if our code works. -Let's launch the app (you can do this by going to `chrome://apps` in the URL bar of Chrome). -You should see something like this: -![Insomnia](https://cloud-m4ukm1v5f.vercel.app/0image.png) - -Click "Create a Request" and enter any request name, it doesn't really matter. - -You should see this: -![Request](https://cloud-ottonqsbw.vercel.app/0image.png) - -In the dropdown menu labeled "GET" by default, click it and set to POST. Insomnia will send a post request now. - -Next to that, there's a text field. Since we're testing our POST route, or our `/register` route, we want to set that to `yourURL/register?username=USERNAME&password=PASSWORD&data=DATA`. Replace `yourURL` with the URL you got from repl.it, and replace `USERNAME`, `PASSWORD`, and `DATA` to be any values you want. -For example, I might put `https://FlaskTutorial.sohamb117.repl.co/register?username=soham&password=insecure&data=testing`. -Press the purple "Send" button, and then check your `registered.json` file. If everything worked, you should see a new entry in the file. - -Now that we've tested the POST request, we should test the GET request. Set the dropdown back to GET and replace the url with this: -`yourURL/login?username=USERNAME&password=PASSWORD` - -Again, replace the necessary fields and hit "Send". If all goes well, you should see the value for `data` returned back to you. For me, that's `testing`. - -If everything works, your webserver is complete! - - -## Final Look at the Code - -We've finished the code! It should look like this: - -```python -from flask import Flask, request -import json - -app = Flask("MyApp") - -@app.route("/", methods=['GET']) -def helloworld(): - return("Hello World!") - -@app.route("/register", methods=['POST']) -def register(): - username = request.args.get("username") - password = request.args.get("password") - data = request.args.get('data') - with open('registered.json', 'a+') as db: - db.seek(0) - check = (f"\"username\": \"{username}\"") - contents = db.read() - if not(check in contents): - db.write(json.dumps({'username': username, 'password': password, 'data':data})+'\n') - return("Success") - else: - return("User already registered.") - -@app.route("/login", methods=['GET']) -def login(): - username = request.args.get("username") - password = request.args.get("password") - with open('registered.json', 'r') as db: - jsonList = [] - for x in db.readlines(): - jsonList.append(json.loads(str(x))) - for i in jsonList: - if (i["username"] == username) and (i["password"] == password): - return(i["data"]) - return("User not found") - -app.run(host='0.0.0.0') -``` - -To recap, this code -* Creates a Flask app -* Creates routes for the app -* Reads values from arguments -* Stores values in a JSON file -* Returns values as a response to a GET or POST request - -What this does NOT have: -* Security -* Password requirements -* Encryption -* CAPTCHA or any anti-bot measures - -Note: While you can modify this code to have these features, you should **NEVER** store passwords in plaintext like this webserver does. If you were to implement security features, this webserver would work for login. - -## Hacks and Further Reading - -Here are some things to check out or read. -* [Get and Post Requests](https://www.w3schools.com/tags/ref_httpmethods.asp) -* [Flask Documentation](https://flask.palletsprojects.com/en/1.1.x/) -* [How JSON Works](https://www.tutorialspoint.com/json/json_overview.htm) -* [Password Hashing and Security](https://thycotic.com/company/blog/2020/05/07/how-do-passwords-work/) -* [Article on How Login Works](https://iam.harvard.edu/resources/behind-login-screen) - -Here are some hacks that show what you can do with this: -* [Make a frontend with HTML and serve it with Flask too](https://repl.it/@sohamb117/FlaskTutorial-1#main.py) -* [Ensure that passwords meet certain conditions](https://repl.it/@sohamb117/FlaskTutorial-2#main.py) -* [Take in more data, and store timestamps](https://repl.it/@sohamb117/FlaskTutorial-3#main.py) From d5444e53db99153cf0647eddf150ff4d00abe1c4 Mon Sep 17 00:00:00 2001 From: Sam Poder <39828164+sampoder@users.noreply.github.com> Date: Wed, 2 Dec 2020 22:43:56 +0800 Subject: [PATCH 16/51] Update README.md --- workshops/tunes/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/workshops/tunes/README.md b/workshops/tunes/README.md index cc303e3ce..fa2c1311b 100644 --- a/workshops/tunes/README.md +++ b/workshops/tunes/README.md @@ -102,6 +102,8 @@ document.onkeydown = function (e) { } ``` +What this code does is it get's the event (the key being pressed) information happening. It then extracts the key code from that information. The use of `||` is as a browser fallback as some browsers have different APIs. + You'll see that when running this, our key is a number!? That's because each key has it's own code. [keycode.info](https://keycode.info/) is a super handy tool to help you identify the codes for each key. keycode.info's UI From 74eb27927d2d42108a0f59fff5ec4666b8277f32 Mon Sep 17 00:00:00 2001 From: "Yash.Kalbnde" <53596464+YashKalbande@users.noreply.github.com> Date: Wed, 2 Dec 2020 21:46:02 +0530 Subject: [PATCH 17/51] Pac-Man Game readme.md(Resubmission #2) (#1477) * Pac-Man Game readme.md @MatthewStanciu, I have completed all requested changes in workshop. Please review it. I think it is ready to merge. * Update Readme.md * Update Readme.md * Rename workshops/pac-man Game/Readme.md to workshops/pacman_game/README.md --- workshops/pacman_game/README.md | 335 ++++++++++++++++++++++++++++++++ 1 file changed, 335 insertions(+) create mode 100644 workshops/pacman_game/README.md diff --git a/workshops/pacman_game/README.md b/workshops/pacman_game/README.md new file mode 100644 index 000000000..79fc8cf07 --- /dev/null +++ b/workshops/pacman_game/README.md @@ -0,0 +1,335 @@ +--- +name: 'Pac-Man Game' +description: 'Simple Pac-Man Game using Python' +author: '@YashKalbande' +img: 'https://cloud-k9b5z9yni.vercel.app/1pac-man.png' +--- + +![Pac-Man gif](https://cloud-k9b5z9yni.vercel.app/0pacman.gif) + +## Introduction + +[Pac-Man](https://en.wikipedia.org/wiki/Pac-Man) is a famous video game featuring a yellow character roaming around a maze collecting different things along the way. In this workshop, you'll learn how to create your own Pac-Man game using the [Python](https://en.wikipedia.org/wiki/Python_(programming_language)) [Freegames](https://pypi.org/project/freegames/) library. + +Here's a demo of what we'll be making. You can see both the final code and test it out on this Repl environment: https://repl.it/@YashKalbande/Easy-Level + +* **NOTE:** If you ever get lost, feel free to open the Repl above and see where the code is being placed. All the code explained in this workshop is in order of first line to last line :) Happy hacking and good luck! + +We're going to be coding this using a principle called [modular programming](https://en.wikipedia.org/wiki/Modular_programming). This means we're going to be writing different functions that we will call at the end. A function is written by putting the function name, what parameters it needs, and then what happens with the parameters. Here's an example function: + +```python +def functionName(parameter1, parameter2): + total = parameter1 + parameter 2 + return total +``` + +Now, whenever I call `functionName`, I will get the sum of its two parameters. You can call a function by just filling in the parameters: + +```python +sum = functionName(5, 6) +print(sum) #prints 11 +``` + +### Planning + +Because this is a relatively complex project, we should plan out the functions and variables we are going to need. Think about what we might use for this project. What kind of actions are possible for a Pac-Man game? You can use a flow chart or write down on a piece of paper what we will need. Here is a list of things we need to be able to do: + +* Create a map to our specification that will let the user know how to navigate through the game +* Enforce the Pac-Man (the user) to stay within that map +* Let the Pac-Man move. We will probably need two functions for this: + * Choosing a direction for the Pac-Man + * Letting the Pac-Man move in the specified direction +* Add different ghosts and dots into the game to spice it up +* Making sure initial placement of the ghosts and dots are random +* Scoreboard that updates based on how many dots the Pac-Man eats + +## Setting up our code! + +For this workshop, we're going to be using an online code editor called [Repl.it](https://repl.it/site/about). + +1. Head over to [Repl.it/languages/Python3](https://repl.it/languages/python3) + * This will create a new [Python 3](https://www.w3schools.com/python/) Repl for you to use to code. +2. Edit your new `main.py` file with the following code: + +```python +from random import choice +from turtle import * +from freegames import floor, vector +``` + +* The code above essentially says the following: + * Import the `choice` function from the module `random`. This is a built-in Python module that gets a random value for us to use when spawning. + * Add all the functions from the `turtle` library. Learn more about the Turtle library [here](https://docs.python.org/3/library/turtle.html). It is a way to implement graphics in Python projects + * Get the [`floor`](http://www.grantjenks.com/docs/freegames/api.html#freegames.floor) and [`vector`](http://www.grantjenks.com/docs/freegames/api.html#freegames.vector) functions from the `freegames` library. The [freegames library](http://www.grantjenks.com/docs/freegames/index.html) is a basic module with built-in functions that do standard game tasks like moving around and setting a playing field. + +3. Next, we’ll want to declare and initialize all of our global variables: + +```python +state = {'score': 0} +path = Turtle(visible=False) +writer = Turtle(visible=False) +aim = vector(5, 0) +pacman = vector(-40, -80) +ghosts = [ + [vector(-180, 160), vector(5, 0)], + [vector(-180, -160), vector(0, 5)], + [vector(100, 160), vector(0, -5)], + [vector(100, -160), vector(-5, 0)], +] +``` + +* Take a moment and try to understand what the code above is trying to say. The best way to interpret code is by turning it into plain English. Here are some helpful tips: + * The `state` variable is in a [JSON object](https://www.w3schools.com/python/python_json.asp) with the first key being `score` + * Both `path` and `writer` use the `turtle` library we imported earlier. Respectively, they are used to draw the game map and to update the score. + * We imported `vector` earlier. This will let us choose the direction and placement of initial objects. This is assigned using a two-dimensional (x, y) axis. Think of a graph... + * `aim` sets the **direction** of movement for the Pac-Man as (5, 0). This means that we are setting a speed of 5 to the right on the axis. + * We are also using it in an array called `ghosts`, this will spawn the ghosts at each corner of our game and set their speed at a default of 5. + +## Planning the maze + +Now comes the fun part! We want to plan what the maze is going to look like for the user. We do it by drawing `tiles` in an array. + +* `0` represents an empty space and `1` represents a filled space +* For the purpose of this workshop, we'll be using a 20x20 grid. To make it easy visually, let's code this with the same idea in mind. + +You can use your own map, or use this sample. Add this code to the bottom of your `main.py` file: + +```python +tiles = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, + 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, + 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, + 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, + 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, + 0, 1, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, + 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, + 0, 1, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, + 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, + 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, + 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, + 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, + 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 1, 0, 0, 0, 0, + 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, + 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + +] +``` + +For reference, this is what the above sample code will look like, once we implement the correct functions to draw it on: + +![maze](https://cloud-k9b5z9yni.vercel.app/2maze.png) + +## Drawing our maze + +Now just because we see what the map looks like in the code, doesn't mean Python knows what it is. We need to use our `turtle` library to make bricks that follow the path we created. Add this code, which tells the library to follow the blueprint we made above. + +```python +def brick(x, y): + path.up() + path.goto(x, y) + path.down() + path.begin_fill() + + for count in range(4): + path.forward(20) + path.left(90) + + path.end_fill() +``` + +What to take away from this code: + +* We are using the `path` function from the `turtle` library. The function names are pretty standard and describe what direction it should draw in. +* Take note of the parameters `(x, y)`. This means that we are getting a value and adding a brick in that spot. + +The way that bricks work are by taking a smaller scale 20x20 grid and scaling it up. We don't want the user to only see a 20 pixel view, we are turning each pixel into a larger square. + +## Adding some physics! + +We're going to add two physics principles into our code: + +* Offset - to make sure our Pac-Man returns back into the maze path, in case they run into the bricks we created. + + ```python + def offset(point): + "Return offset of point in tiles." + x = (floor(point.x, 20) + 200) / 20 + y = (180 - floor(point.y, 20)) / 20 + index = int(x + y * 20) + return index + ``` + +* Validity - ensuring that our Pac-Man can't do anything suspicious and run through the bricks! + + ```python + def valid(point): + "Return True if a point is valid in tiles." + index = offset(point) + + if tiles[index] == 0: + return False + + index = offset(point + 19) + + if tiles[index] == 0: + return False + + return point.x % 20 == 0 or point.y % 20 == 0 + ``` + +Both of those code blocks can be added to the bottom of your code. + +## Finishing the maze + +We need to put all of our functions together to make our actual world look a little nicer than just a few code blocks laying around. We're going to make a new function called `world` that does the following: + +* Uses the `brick` function we coded earlier and goes along the `tiles` array we coded as the blueprint. +* Adds some color into our map + * You can choose any color you want, but for the purpose of the workshop, the sample will go with the standard Pac-Man colors (blue and black) + +```python +def world(): + Screen().bgcolor('black') + path.color('blue') + + for index in range(len(tiles)): + tile = tiles[index] + + if tile > 0: + x = (index % 20) * 20 - 200 + y = 180 - (index // 20) * 20 + brick(x, y) + + if tile == 1: + path.up() + path.goto(x + 10, y + 10) + path.dot(2, 'white') + + update() +``` + +* Notice the numbers that we use. In blueprint array we programmed earlier, we kept the dimensions 20x20 in mind. This is why you can see that we're using different mathematically expressions to calculate which line we're on. +* We're using a for loop to traverse through the array to add the array items one by one. + * If the value at a specific spot is `0`, we'll add a `brick` (from the function we coded earlier). + * If the value is `1`, then we'll fill it in for the Pac-Man to walk through. +* Remember to be very careful about the indentation of each line — for example, make sure the `path.up()` is lined up exactly under the `if` syntax. + +## Moving the Pac-Man + +We want our Pac-Man to be able to move through the world we created. We're going to create a new function called `move` that will allow the character to move. While coding, notice the different variables we're using and functions that correspond. + +```python +def move(): + writer.undo() + writer.write(state['score']) + clear() + if valid(pacman + aim): + pacman.move(aim) + + index = offset(pacman) + + if tiles[index] == 1: + tiles[index] = 2 + state['score'] += 1 + x = (index % 20) * 20 - 200 + y = 180 - (index // 20) * 20 + brick(x, y) + + up() + goto(pacman.x + 10, pacman.y + 10) + dot(20, 'yellow') + + for point, course in ghosts: + if valid(point + course): + point.move(course) + else: + options = [ + vector(5, 0), + vector(-5, 0), + vector(0, 5), + vector(0, -5), + ] + plan = choice(options) + course.x = plan.x + course.y = plan.y + + up() + goto(point.x + 10, point.y + 10) + dot(20, 'red') + + update() + + for point, course in ghosts: + if abs(pacman - point) < 20: + return + ontimer(move, 100) +``` + +* **NOTE:** Our Pac-Man has no sense of direction yet. It's using the default `aim` we assigned earlier when we were initializing variables. At the moment, our Pac-Man is only programmed to go right, at a speed of 5. We'll add some direction in the next step. +* The `writer` variable we implemented at the beginning is being used here with the `undo()` and `write()` functions. These are built-in functions that come with `Turtle`. +* As we go down further the code, we're checking if the movement is `valid`. Remember those physics functions we made? This is where we're implementing it. A move is `valid` when it doesn't involve touching a brick. +* We're also utilizing the `ghosts` array with vectors we initialized before. This lets us have them in the game and move them around. +* We're creating `dots` (a function from `Turtle`) that our Pac-Man will eat. We are ensuring they're being created dynamically as they are attached to the Pac-Man's moves. If the Pac-Man doesn't move, it won't spawn more dots. +* **Going a step further:** If you want to take things a step further and introduce an animation, sound effect, or other prettiness factor, we would introduce it after `brick(x, y)`. Try adding `print(“PAC-MAN”)` as an example. + +## Giving Direction to Pac-Man + +We created a function that lets our Pac-Man move, but it doesn't know where to go. It only followed the direction that we provided to it in the `aim` variable. Let's create a function that lets our Pac-Man change that `aim` value, or in other terms, change it's direction. Add this code into the bottom of your code: + +```python +def change(x, y): + if valid(pacman + vector(x, y)): + aim.x = x + aim.y = y +``` + +We first check if the direction is valid (we aren't inside of a brick or anything), then we set the `aim` we initialized at the start of the code. + +## The Final Stretch! + +As we mentioned earlier, this game is coded using a principle called modular programming. This means that everything we code is in functions, now we can bring it together. We can call these functions by writing the function name and the parameters it needs in parentheses. This code block will put all the pieces together: + +```python +setup(420, 420, 370, 0) +hideturtle() +tracer(False) +writer.goto(160, 160) +writer.color('white') +writer.write(state['score']) +listen() +onkey(lambda: change(5, 0), 'Right') +onkey(lambda: change(-5, 0), 'Left') +onkey(lambda: change(0, 5), 'Up') +onkey(lambda: change(0, -5), 'Down') +world() +move() +done() +``` +**Congrats! You've just made your own Pacman game! 🎉** + +**Things to note:** + +* These are all functions we created earlier, we are just putting the pieces together. +* The grid, as mentioned earlier, is scaled up to 420x420 (from 20x20). We use `turtle`'s built in `setup()` function to accomplish this. +* `hideturtle()`, `listen()`, and `tracer(False)` are all default `turtle` functions that will let us remove any unnecessary default items/features and enable us to listen for keystrokes. + * We are picking up the user's keystrokes by using Python's built-in `onkey()` function. + +## Extending the game + +Our version of Pac-Man is pretty basic. We are utilizing 2D vector graphics and Python’s turtle graphics library to draw shapes and dots on our screen. + +* This art style is very much in-line with the look and feel of the original incarnation of the game in the 1980s. + * If you want to take the game a step further you could introduce graphical sprites, add more details to Pac-Man or ghosts like eyes, etc. + * You can design end numbers of levels for Pac-Man by doing changes in the `tiles` variable. Different color schemes, size, number of ghosts, etc. + +You can also add other types of roadblocks into the app to make it more difficult, which usually which making the game more addictive: + +- [Easy Level](https://repl.it/@YashKalbande/Easy-Level#main.py) +- [Medium Level](https://repl.it/@YashKalbande/Medium-Level#main.py) +- [Hard Level](https://repl.it/@YashKalbande/Hard-Level#main.py) + From 1eac81ccabc8a1e52bb9788ee7c708bd9b0f343c Mon Sep 17 00:00:00 2001 From: Matthew Stanciu Date: Wed, 2 Dec 2020 17:12:08 -0500 Subject: [PATCH 18/51] Fix overflow on web login --- workshops/login_auth/README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/workshops/login_auth/README.md b/workshops/login_auth/README.md index bbc7d5e94..5e0233a58 100644 --- a/workshops/login_auth/README.md +++ b/workshops/login_auth/README.md @@ -227,11 +227,15 @@ For that, you need to use JavaScript. But before moving on, remember you saved s Here is an **important** step, look at very first line of code: -``. +```html + +``` This will give you an error, as it only loads the Firebase library. So for that, add the line below under that first line. -``. +```html + +``` After adding it, your code should look like this: From 7757803f3eed55d8215aee4d9e6126e6c03a5b19 Mon Sep 17 00:00:00 2001 From: Aaryan Porwal <54525904+aaryanporwal@users.noreply.github.com> Date: Sat, 5 Dec 2020 10:40:00 +0530 Subject: [PATCH 19/51] Fixes typos (#1500) Fixed some typos and some grammatical errors --- workshops/cli_app_with_nodejs/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/workshops/cli_app_with_nodejs/README.md b/workshops/cli_app_with_nodejs/README.md index b06df84f6..f5088c7a1 100644 --- a/workshops/cli_app_with_nodejs/README.md +++ b/workshops/cli_app_with_nodejs/README.md @@ -100,9 +100,9 @@ The `yargs` module is one such module for Node.js designed to support the most c ``` Code Explanation: -We want our super awesome app to greet the user, for that our app will require a `name` attribute. That's what the code does, it imports yargs module, and then we define the `help` page of our app and ask for `name` attribute, after taking the `name` attribute, our app will greets the user. +We want our super awesome app to greet the user, for that our app will require a `name` attribute. That's what the code does, it imports yargs module, and with the help of yargs, we make a `help` page and ask for a `name` attribute, after taking the `name` attribute, our app greets the user. ->Note: The yargs module automatically builds a great response for displaying help! Your CLI is not only ready to accept `-n` and `--name` arguments but also `--help` and `--version`. Try running your CLI application with any of the above arguments! +>Note: The yargs module automatically builds a great response for displaying help! Your CLI is not only ready to accept `-n` and `--name` arguments but also `--help` and `--version`. Try running your CLI application with any of the arguments! So try running our code so far by executing `node -n `. From d800cc052458c909127ebffe2298fd22c0dcd3e9 Mon Sep 17 00:00:00 2001 From: Aaryan Porwal <54525904+aaryanporwal@users.noreply.github.com> Date: Sun, 6 Dec 2020 16:44:08 +0530 Subject: [PATCH 20/51] Typo Fixes (#1503) The CodeSandbox link was redirecting to: https://workshops.hackclub.com/react_calendar/codesandbox.io which was returning 404, replaced with https://codesandbox.io to point to the correct URL. --- workshops/react_calendar/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workshops/react_calendar/README.md b/workshops/react_calendar/README.md index a458d7cf4..bbad208b1 100644 --- a/workshops/react_calendar/README.md +++ b/workshops/react_calendar/README.md @@ -23,7 +23,7 @@ You should have some familiarity with HTML and JavaScript as well as programming ## Part 2: Setup -So far, we've been using repl.it for most of our workshops. But today, I want to introduce you to another online code editor, [CodeSandbox](codesandbox.io). For making any React projects, CodeSandbox is the best one out there. +So far, we've been using repl.it for most of our workshops. But today, I want to introduce you to another online code editor, [CodeSandbox](https://codesandbox.io). For making any React projects, CodeSandbox is the best one out there. To get started, go to this [starter code](https://codesandbox.io/s/calendarstartercode-thk00). Press **`ctrl+s`** / **`cmd+s`** and it will automatically fork it for you. Now, we have everything set up so let's get started! From 9d6cc29242e06a48968654623ccebd977bb619d0 Mon Sep 17 00:00:00 2001 From: Sam Poder <39828164+sampoder@users.noreply.github.com> Date: Mon, 7 Dec 2020 16:00:17 +0800 Subject: [PATCH 21/51] Update README.md --- workshops/tunes/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/workshops/tunes/README.md b/workshops/tunes/README.md index fa2c1311b..44a453fda 100644 --- a/workshops/tunes/README.md +++ b/workshops/tunes/README.md @@ -80,6 +80,8 @@ I hope you understand the starter, now you may be getting bored so let's get mak # Detecting a key press +From here on out, we'll be writing our code in the `script.js` file so load that up in the editor. + To play a note in our project we will have the user press a key on their keyboard. Javascript has an event for this: `document.onkeydown`. This is fired when ever a key is pressed down. For example, in the below snippet: From 5fedb8f579b0e97967bf087391be2797ace2da82 Mon Sep 17 00:00:00 2001 From: HariOm Dwivedi Date: Mon, 7 Dec 2020 22:29:25 +0530 Subject: [PATCH 22/51] (WORKSHOP BOUNTY) Resubmission#3 asked by SAM (#1489) * (WORKSHOP BOUNTY) Resubmission#3 asked by SAM Past [pull request](https://github.com/hackclub/hackclub/pull/1482) * Update password-generator/README.md Co-authored-by: Sam Poder <39828164+sampoder@users.noreply.github.com> * Update password-generator/README.md Co-authored-by: Sam Poder <39828164+sampoder@users.noreply.github.com> * Update README.md * a few initial edits Co-authored-by: Sam Poder <39828164+sampoder@users.noreply.github.com> Co-authored-by: Matthew Stanciu --- password-generator/README.md | 362 +++++++++++++++++++++++++++++++++++ 1 file changed, 362 insertions(+) create mode 100644 password-generator/README.md diff --git a/password-generator/README.md b/password-generator/README.md new file mode 100644 index 000000000..608eb4d9a --- /dev/null +++ b/password-generator/README.md @@ -0,0 +1,362 @@ +--- +name: 'Password Generator' +description: 'Build a password genertor tool with HTML, CSS, and JavaScript' +author: '@HariOm987' +img: https://cloud-nu1ftbbxy.vercel.app/0sample-demo.png +--- + +In this workshop, you are going to make a random alphanumeric password generator using which you can safeguard your social media accounts and much more, in just 20 minutes! 🤩 + +**Note:** The purpose of this workshop is to teach you how to generate and store passwords. For the purposes of this workshop, the passwords are stored in a table in plaintext. In real life, passwords are encrypted and stored behind many layers of security. Don't store your real-life passwords on the website this workshop makes! + +Here’s the [live demo](https://password-security.hariom04.repl.co/) and its [source code](https://repl.it/@hariom04/password-security#index.html). For entering inside the website, use `hariom` as password. + +![final output of the codes provided in this workshop](https://cloud-nu1ftbbxy.vercel.app/0sample-demo.png) + +# Part 1: Prerequisites + +Basic knowledge of HTML, CSS, and JavaScript would be helpful for better understanding but is not necessary to continue further in this workshop. + +# Part 2: Setup +## Setting up our code environment +We’ll be using repl.it, a free, online code editor, to make this website. Get started by going to [repl.it/languages/html](https://repl.it/languages/html). Your coding environment will instantly spin up! + +![starting window of repl.it on navigating using above link](https://cloud-plumff9h8.vercel.app/0repl-starting-window.png) + +^ Right now, your page will look like this. + +# Part 3: Building the Project +## 1) HTML +### Adding image and welcoming text + +Inside the `body` tags, we will insert an image using + +```html + +``` + +Here, `img` and `src` stand for **‘image’** and **‘source’** respectively. You are free to replace that URL with your image and set its width & height accordingly. + +Next, we will add a welcome message. Under your image, add `

` & `

` tags like this: + +```html +

Hiii HariOm👋 Welcome Back! Let's safeguard your accounts.

+``` + +Now we want to leave the next line as blank in our output to separate our main tool from the welcoming line. For that, we use a `
` tag. So, in the next line, type `
`. + +### Making our password generator tool. + +Make a new section with the assistance of the `
` tag below your break (`
`) line. Add the attribute `class="inputbox"` inside that segment. Adding `class` attribute gives this particular div its own **“name”**, which will allow us to reference it directly using CSS (as we’ll see later in this workshop!). +```html +
+``` +Now from here, we will add some elements inside this section. + +Add a sub heading using `

` tag stating the topic of our project that is “Random Password Generator”. Then underline it using `` to make it more attractive. 😍 +```html +

Random Password Generator

+``` +Now we will create a box where we will get the passwords, for that we will have to use `` tag. Add the attributes `type="text"`, `placeholder="Create Password"`, `id="password"` and `readonly=""` inside that. +```html + +``` +Uses of these attributes: +* The `type` attribute specifies the type of ``, the element should display. +* The `placeholder` attribute specifies a short hint inside the input field. +* The `id` attribute specifies an unique id for the element. We will learn more about it later in this workshop. +* The `readonly` attribute specifies that the input field is read-only. + +Add a new section with the help of `
` tag and give attributes `id="btn"` and `onclick="getPassword()"`. Then put the text you want to display on the button responsible for generating our passwords. When you are done with that, close that section using `
` tag. We will learn more about `onclick="getPassword()"` later in this workshop. +```html +
Generate Password
+``` +Now we will navigate out of our first section. Your code block will look something like this: +```html +
+

Random Password Generator

+ +
Generate Password
+
+``` +Leave next line as blank using `
` tag. + +### 3- Creating a table to store our passwords. + +Add a new section using `
` tag and give attribute `class=”mypasswords”`. +```html +
+
+``` +Give a heading using `

` tag that you want to show above your password's table like this: +```html +

Your Passwords

+``` +Now we will create actual table using the following codes: +```html + + + + + + + + + + + + + + + + + +
ServicePassword
Facebookgug_Jais}T
Instagramf!Mh4s}09{
TwitterZd1EU%OzD(
+``` +#### Explanation: +Here `` tag specifies that we want to create a table and its attribute `border=10px` describes the thickness of the border. Next, we have a `` tag that stands for **table row** and it specifies the occurrence of a new row. We write our table entries inside `` to ensure they are in same line. After that, we have `
` tag that stands for **table heading** and specifies that the text written inside this will be bold in appearance. We use `` to describe the heading of our table. For entering the normal (default entries) we use `` that stands for **table data**. After we are done with all the row entries, we close the table tag using `
`. + +Our output of this code will look something like this. 👇 + + ![final output of HTML codes](https://cloud-bzcoffe31.vercel.app/0html-output.png) + +Finally, we are done with the HTML part, but our webpage doesn’t look like the final output we saw in the beginning. So, let’s jump to the next part of our workshop where we will add some styling to our project. + +## 2) CSS + +Navigate to the `style.css` file appearing on your sidebar. + +Now add the following code: +```css +*{ + margin: 0; + padding: 0; + font-family: Comic sans MS; + user-select: none; +} +body{ + background: rgb(133,255,147); + background: radial-gradient(circle, rgba(133,255,147,1) 16%, rgba(34,193,195,1) 55%, rgba(255,153,0,0.9110994739692753) 86%); +} +``` +#### Explanation: +Here we opened a new section (code block) using `{`. In line 2 & 3, `padding` and `margin` is marked as zero. After that in line 4 (`font-family: Comic sans MS;`), we described the specific font that should be used on our webpage. + +In line 5, we gave the `user-select` value as `none` because we don’t want the user to select the content of our webpage due to the following reason. 👇 + +![problem of text selection without user-select](https://cloud-kix9zfmih.vercel.app/0user-select-issue.gif) + +If we don’t give `user-select: none;` then when the user clicks on the button that says **Generate Password** for more than once simultaneously, that button's text will be selected that looks little weird. So to get rid of this, we have used that line of code. + +Now in the next block (line 8), we have applied background to our webpage, you can use any solid color or gradient of your choice. + +For making your gradient, simply navigate to [cssgradient.io](https://cssgradient.io/) and create a gradient of your imagination by dragging the markers. After that, copy the code given below and paste it into the `background` section. By this, your custom gradient background will be applied. + +![steps for designing css gradient](https://cloud-2d97a84ef.vercel.app/0designing-gradient.gif) + +Now add the following code below the body block. + +```css +.inputBox{ + position: relative; + width: 450px; +} +.inputBox h2{ + font-size: 28px; + color:rgb(47, 0, 255); +} +.inputBox input{ + position: relative; + width: 100%; + height: 60px; + border: none; + margin: 15px 0 20px; + background: rgb(255, 217, 0); + padding: 0 20px; + font-size: 24px; + Letter-spacing: 4px; + box-sizing: border-box; + border-radius: 8px; + color: rgb(0, 0, 0); + box-shadow: 10px 10px; +} +.inputBox input::placeholder{ + Letter-spacing: 0px; +} +``` +#### Explanation: +Here in the first block that is `.inputBox`, we described the `position` and `width` of the box (the one containing our tool). + +In the second block that is `.inputBox h2`, we specified the size of the `

` tag and its `color`, you are free to replace those values and customize it differently. + +Then in the third block that is `.inputBox input`, we have customized the box which will return (show) the generated passwords. We marked its height as `60px` and removed its border. In the line number 14, margin of the box is changed to (`15px 0 20px`). After that in line 15, we have changed the background color of the box, you can set any color of your choice. Later we have customized its `padding`, `font-size`, and `box-sizing` according to our requirements. `box-radius` specifies the amount of curve in corners. In line 21, `color` represents the font color of the password text. `box-shadows` specifies the direction of shadow in the `x` and `y`-axis respectively. + +In the fourth block that is `.inputBox input::placeholder`, we described the space that is to be given between the letters for prefilled text in that box that is “Create Password”. In the third block, we have specified that space as `4px` as we want to keep the password text separated from each other. + +Add the following code given below: + +```css +.inputBox #btn{ + position: relative; + cursor: pointer; + color: #fff; + background: #333; + font-size: 24px; + display: inline-block; + padding: 10px 15px; + border-radius: 8px; +} +.inputBox #btn:active{ + background: #9c27b0; +} +``` +#### Explanation: +Now we are going to customize the button that says “Generate Password”. + +In the first block that is `.inputBox #btn`, we specified its cursor as a `pointer`, which means when you will hover your cursor over that area then your cursor will convert into a pointer. After that, we described its `color` that is font color, `background` color, and font size as required. You are free to change the values of these fields. + +In the second block that is `.inputBox #btn:active`, we changed the background color, this color will be applied to the button for the time we will keep that button pressed. + +Add the codes below: +```css +.mypasswords{ + position: relative; + background: rgb(255, 119, 183); + font-size: 24px; + display: inline-block; + padding: 10px 15px; + border-radius: 8px; +} +th{ +background-color: #65ffea; +} +``` +#### Explanation: +In the first block that is `.mypasswords`, we described the `position`, `background`, `font-size`, `display`, `padding`, and `border-radius` as per our requirements like we did earlier in this workshop. + +Then in the next block that is `th`, we have a customized heading of the table where we have applied `` tag. We have changed its background color. + +Finally, we are done with the CSS part of our workshop. + +![final output of codes after completetion of CSS part](https://cloud-2td3hm71p.vercel.app/0css-final-output.png) + +^^^Final output of our codes after completetion of CSS part. But it is not functional yet. + +### What we did so far: +1- We built the structure of the password generator tool and webpage. + +2- We customized it to make it look more attractive. + +Now with JavaScript, we will add functionality to our tool and will set up a password to the website. + +## 3) JavaScript +Navigate to `script.js` file. + +### 1- Adding a password to the website. +**Disclaimer: This isn't the greatest method of securing a web app as password is visible in source code.** + +You should run all authentication code **server side** when shipping a final product. + +Create a variable “password” with help of `var` and set it to empty. +```javascript +var password = ''; +``` +Create another variable “pass1” like we did above and set its value of own your choice (this value will be the password for opening your website.) +```javascript + var pass1="hariom"; + ``` +Set the `password` variable as `prompt(‘Please enter your password!’)`. Here prompt event will display a message asking for the password. +```javascript +password = prompt('Please enter your password!'); +``` +By this step we stored the value we will enter in the prompt box, then later we can apply conditions based on which we can redirect our user. + +Lets apply the condition. Add this code below. +```javascript +if (password == pass1) + alert('Password Correct! Click OK to enter!'); +else { + location = "https://password-security.hariom04.repl.co/"; +} +``` +Here in line 1, we checked whether the password user-entered inside the prompt box matches with the one that we added in our code (`var pass1="hariom";`). If it does then the user will get a message to "Click OK" and then he will enter the webpage. + +In line 3, we used `else` statement that means if the above condition is not followed then this part will take place. Here we forwarded the user to the URL of our website to make a loop. So whenever the user enters the wrong password, the website will refresh, and the prompt will appear again. If you want to have fun then you can add any website of your choice like [this one](https://www.youtube.com/watch?v=dQw4w9WgXcQ). 😂 + +### 2- Making our tool functional +Now we will add the function named `getPassword()`. +```javascript +function getPassword() { +} +``` +This function will be work on the section of HTML code where we wrote `onclick="getPassword()"`. + +Create a variable `chars` and give the alphanumeric symbols you want to include in your password. + +Name | Characters +----- | ---------- +Numbers | 0123456789 +Lower alphabets | abcdefghijklmnopqrstuvwxtz +Capital alphabet | ABCDEFGHIJKLMNOPQRSTUVWXYZ +Other symbols | !@#$%^&*()_+?><:{}[] + +Suppose if you want to include all of them then just copy them one by one and paste it inside that variable without any space like this. 👇 +```javascript +var chars = "0123456789abcdefghijklmnopqrstuvwxtzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()_+?><:{}[]"; +``` +Create another variable named `passwordLength` and set it to the size of the password you want to be generated. Suppose if you want a password with `10` symbols then: +```javascript +var passwordLength = 10; +``` +In the next line, set the value of the variable `password` as empty using blank quotation marks. +```javascript +password = "" +``` + +After you are done with that, use the below codes and add it. +```javascript +for (var i = 0; i < passwordLength; i++) { + var randomNumber = Math.floor(Math.random() * chars.length); + password += chars.substring(randomNumber, randomNumber + 1); +} +document.getElementById("password").value = password +``` +#### Explanation: +We have used `for` loop, it will run repeatedly until the condition inside it is true. After that, we did some basic mathematics and stored our password in `password` variable which we have used in HTML to print the password. + +##### Understanding the logic: +In the first line, we created a variable `i` and marked its value as `0`. Then, we put a condition `i < passwordLength` and increased the value of `i` after each loop. + +Later we created a new variable `randomNumber` which will give a random index of any character inside the `chars`. + +`math.random` will find a number between `0` and `1`. Multiplying it by `chars.length` will make it a number between `0` and `chars.length`. + +`.floor` is applied to remove the decimal part of the number like `7.9` when floored will convert into `7`. + +Now, we have a random integer number from `0` to the `passwordLength`. + +In line 3, we searched for the character having that particular index number then for each loop we concatenated that with the past one. + +So, at the end of our loop, we got a random password selected from the characters we gave. + +Finally, we are done with the JavaScript part of our project. + +![final output of codes after completetion of javascript](https://cloud-r9k5bhazs.vercel.app/0ezgif-6-2a4189e70248.gif) + +^^^Your webpage will look something similar to this. 😍🤩 + +## Congratulations +![congratulations for completing your project](https://cloud-gqymfxotk.vercel.app/0finally_we_are_done__1_.gif) + +You accomplished it! Now your tool is fully functional and looks creative. 😍🥳 + +# Hacking +Here are some hacks you can try in your project. + +1- Try creating a button saying "**add**", using which you can add more entries to your password's table. Here's [live demo](https://workshop-2.hariom04.repl.co/) and [final output](https://repl.it/@hariom04/workshop-2#index.html). + +2- Try linking the website of the services used in password's table. Here's [live demo](https://workshop-2-example-2.hariom04.repl.co/) and [final code](https://repl.it/@hariom04/Workshop-2-example-2#index.html). + +3- Try creating a tool in which user can select characters that needs to be included and excluded. Insert copy to clipboard button too. Here's [live demo](https://workshop2-example-3.hariom04.repl.co/) and [final code](https://repl.it/@hariom04/workshop2-example-3#index.html). + +**Happy Hacking!** From 1de5aa3a0a83c0804e9388f4a9b612385ea959fb Mon Sep 17 00:00:00 2001 From: Matthew Stanciu Date: Mon, 7 Dec 2020 12:03:31 -0500 Subject: [PATCH 23/51] fix password generator folder --- {password-generator => workshops/password_generator}/README.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {password-generator => workshops/password_generator}/README.md (100%) diff --git a/password-generator/README.md b/workshops/password_generator/README.md similarity index 100% rename from password-generator/README.md rename to workshops/password_generator/README.md From fb8a86771b10023d9b9eadf5d1ca52356f56e8ec Mon Sep 17 00:00:00 2001 From: Khushraj Rathod Date: Tue, 8 Dec 2020 00:12:24 +0530 Subject: [PATCH 24/51] [Workshop Bounty] Add slack todo list workshop (#1457) * Add slack todo list workshop * Add Todo List Plus links * Add Todo List Random links * Add Todo List Community links * Fix missing context for repl.it preview * fix slack capitalization Co-authored-by: Matthew Stanciu --- workshops/slack_todo_list/README.md | 293 ++++++++++++++++++++++++++++ 1 file changed, 293 insertions(+) create mode 100644 workshops/slack_todo_list/README.md diff --git a/workshops/slack_todo_list/README.md b/workshops/slack_todo_list/README.md new file mode 100644 index 000000000..da05a3457 --- /dev/null +++ b/workshops/slack_todo_list/README.md @@ -0,0 +1,293 @@ +--- +name: Slack Todo List +description: Make a todo list Slack bot with Node.js and Bolt +author: '@KhushrajRathod' +img: https://cloud-2xos4hsas.vercel.app/0screenshot_2020-11-16_at_1.00.14_pm.png +--- + +Ever wanted a todo list that's simple, easy, and integrated into your favorite messaging service? Well, today, we're building one! + +Today, we'll be creating a Slack bot that maintains a todo list using [Bolt.js](https://slack.dev/bolt-js/) and [Node.js](https://nodejs.org/) + +Here's the [final code](https://repl.it/@KhushrajRathod/TodoSlackApp). A live demo is available as "@Todo Bot" on the [Hack Club Slack](https://hackclub.com/slack/). + +If you get stuck anywhere in this workshop, feel free to ask me questions! I'm @KhushrajRathod. + +## Part 1: Preparing your environment +### Part 1.1: Preparing repl.it + +Today we'll be using repl.it. Repl.it is an online code editor that we can use so we don't have to download a lot of stuff or resort to _Notepad/TextEdit_. Think of it as Google Docs, but for code (and also much more fun :D). + +Follow these steps: + +- Open https://repl.it/ +- Click "Sign up" + +![Arrow to sign up button on top right](https://cloud-pq5lbfiab.vercel.app/9signup-step1.png) + +- Fill in some details + +![Arrow to "Username", "Email" and "Password" fields in center of screen](https://cloud-91xu3gqm8.vercel.app/0signup-step2.png) + +- You now have a Repl.it account! Next, click "New repl" + +![Arrow to New repl button on the top left](https://cloud-pq5lbfiab.vercel.app/6new-step1.png) + +- Search for "Node.js" in the search box, click "Node.js" and click "Create repl". + +![Find node.js in the search box](https://cloud-9vsssxvna.vercel.app/0screenshot_2020-11-16_at_1.29.29_pm.png) + +- You now have a Node.js repl setup successfully + +![Arrow pointing to preview URL on top right of repl.it](https://cloud-ccj49d17x.vercel.app/3copyurlfromreplit.png) + +#### Repl.it basics + + + +### Part 1.2: Getting Slack tokens + +- First, go to [Slack's sign in page](https://slack.com/signin) and sign in to your workspace. If you don't have an existing workspace to use, you can simply create a new one [here](https://slack.com/get-started) + +- Next, go to https://api.slack.com/apps and create a new app. + +![Arrow pointing to "Create New App"](https://cloud-ccj49d17x.vercel.app/1createnewapp.png) + +- Give your app a name, select your workspace, and click "Create App". + +![Arrow pointing to "Create App"](https://cloud-ccj49d17x.vercel.app/0createslackappdetails.png) + +- Inside Basic Information, go to "Add features and functionality" and click "Slash Commands" + +![Arrows pointing to various places: 1. "Basic Information" on left sidebar, 2. "Add features and functionality" and 3. "Slash commands" in the dropdown](https://cloud-ccj49d17x.vercel.app/2basicinfo-features-slashcommands.png) + +- Click "Create new command" and fill in the details for your command: + - Command: /todolist (Note: If you're using the hackclub workspace, you'll need to use a different name for your command, such as /yournametodolist) + - Request URL: https://yourappname.yourusername.repl.co/slack/events where `yourappname` is your repl's name and `yourusername` is your repl.it username. For e.x., my repl's name is TodoSlackApp and my repl.it username is KhushrajRathod, so my request URL is https://TodoSlackApp.KhushrajRathod.repl.co/slack/events + - Short Description: Show your todo list + +![Filled in details](https://cloud-ccj49d17x.vercel.app/4createnewcommand.png) + +- Click save. + +#### Challenge + +Create two more commands, /todolistadd and /todolistremove. Use the same request URL, you may add a description and usage hint as you please. + +--- + +- This is how your Slack API page should look now + +![Three commands shown, /todolist, /todolistadd and /todolistremove](https://cloud-ccj49d17x.vercel.app/5finalslashcommands.png) + +- Now, go to "OAuth & Permissions" in the sidebar, scroll down to "Scopes" and add chat:write and chat:write.public as Bot Token Scopes + +![Three scopes present, commands, chat:write and chat:write.public](https://cloud-b17s5cy5p.vercel.app/0scopes.png) + +- Scroll back to the top of "OAuth & Permissions" and click "Add to Workspace". Slack will ask you to authenticate with your workspace, once you're done with that you should be able to see a Bot User OAuth Access Token, copy that -- we'll need this later. + +- Next, go back to "Basic Information" in the sidebar, scroll down to "App Credentials" and click "Show" next to Signing Secret. Copy the signing secret too -- we'll need this later. + +We've finished setting up the Slack API side of our app! + +![Minions cheering](https://cloud-pq5lbfiab.vercel.app/0cheer.gif) + +## Part 2: Programming the bot backend + +Head back to repl.it and open your Repl so we can get started! First, create a new file named ".env" (without quotes). This file will contain your signing secret and bot token, as `.env` is not visible to people viewing / forking your repl. + +Inside `.env`, define your variables as follows (replace yourbottoken and yoursigningsecret with the tokens you copied above): + +```ini +SLACK_BOT_TOKEN=yourbottoken +SLACK_SIGNING_SECRET=yoursigningsecret +``` + +This file will automatically be injected by repl.it into process.env in Node.js. Open the `index.js` file, and add the following: + +```js +const { App } = require('@slack/bolt') + +const app = new App({ + signingSecret: process.env.SLACK_SIGNING_SECRET, + token: process.env.SLACK_BOT_TOKEN, +}) + +;(async () => { + await app.start(process.env.PORT || 3000) + + app.command('/todolist', async ({ command, ack, say }) => { + await ack() + await say(`Hello!`) + }) + + console.log('⚡️ Server ready') +})() +``` + +Explanation: + +- First, we're importing Slack's [Bolt library](https://slack.dev/bolt-js/concepts) for Node.js, and using it to create a new instance of `App`. + +- Next, we're starting the app and listening for the `/todolist` command. + +- When someone calls `/todolist`, we want to + 1. Acknowledge that command (Slack requires you to acknowledge all requests - otherwise it displays a "Timed out" error to the user) + 2. Reply with "Hello!" + +Run your Repl, and open a channel in your Slack workspace. Try running `/todolist` -- You should see "Hello!" as a response from the bot. + +### Challenge + +Create similar commands for `/todolistadd` and `/todolistremove` - They just need to make the bot reply with "Hello!". + +--- + +Here's how your code should look now: + +```js +const { App } = require('@slack/bolt') + +const app = new App({ + signingSecret: process.env.SLACK_SIGNING_SECRET, + token: process.env.SLACK_BOT_TOKEN, +}) + +;(async () => { + await app.start(process.env.PORT || 3000) + + app.command('/todolist', async ({ command, ack, say }) => { + await ack() + await say(`Hello!`) + }) + + app.command('/todolistadd', async ({ command, ack, say }) => { + await ack() + await say(`Hello!`) + }) + + app.command('/todolistremove', async ({ command, ack, say }) => { + await ack() + await say(`Hello!`) + }) + + console.log('⚡️ Server ready') +})() +``` + +To store users' todo lists, we'll be using the [repl.it database](https://docs.repl.it/misc/database). Repl.it provides a handy client for the database to use with Node.js. + +- First, import the library and create a client (at the very top of your file) + +```js +const Client = require("@replit/database") +const database = new Client() +``` + +Repl.it's database is a key-value database. For us, we'll set the keys as the users' ids and the values as arrays containing their todo lists. + +```js +user1 = [Todo1, Todo2, Todo3, Todo4...] +user2 = [Todo1, Todo2...] +user3 = [Todo1...] +// and so on +``` + +Every time a user requests their todo list, we'll get their value from the database, parse it to store as an array, and display the array formatted nicely. For updating, we'll follow the same procedure, except instead of displaying the array, we'll make the changes, convert back to a string and set it in the database. + +- Now, for the `/todolist` command we want to get the user's array and display it. + +```js +app.command('/todolist', async ({ command, ack, say }) => { + await ack() + + let currentUserTodo = JSON.parse(await database.get(command.user_id)) || [] // Get the user's array, and if the user has never used this todo list before, use an empty array + + // Nicely format the array as a list with numbers + let response = "" + currentUserTodo.forEach((todo, index) => { + response += `\n${index + 1}. ${todo}` + }) + + // If the user has items in the todo list, respond with it. Otherwise, send a message stating the list is empty + if (response) { + await say("Your todo list:" + response) + } else { + await say(`Your todo list is currently empty!`) + } +}) +``` + +- Next, for the `/todolistadd` command, we want to get the user's array, add the item to the list, and set the array in the database. + +```js +app.command('/todolistadd', async ({ command, ack, say }) => { + await ack() + let currentUserTodo = JSON.parse(await database.get(command.user_id)) || [] // Same as previous command, get the array, if it doesn't exist, use an empty one + currentUserTodo.push(command.text) // Add the new item to the array + await database.set(command.user_id, JSON.stringify(currentUserTodo)) // Set the array in the database + await say(`Added\n• ${command.text}\n to your todo list`) // Confirm adding the item to the user +}) +``` + +- For the `/todolistremove` command, we want to remove the item from the array and then set it in the database. + +```js +app.command('/todolistremove', async ({ command, ack, say }) => { + await ack() + let currentUserTodo = JSON.parse(await database.get(command.user_id)) || [] // Same as previous command + let removed = currentUserTodo[command.text - 1] // Store the value that will be removed so we can show "Removed xxxxxxxx from your todo list" later + currentUserTodo.splice(command.text - 1, 1) // Splice the array and remove the item at the provided item number from todo list + await database.set(command.user_id, JSON.stringify(currentUserTodo)) // Set the array in the database + await say(`Removed\n• ${removed}\n from your todo list`) +}) +``` + +We're done setting up the bot! Run the repl and go to your Slack workspace, you should be able to run `/todolist`, `/todolistadd` and `/todolistremove`! + +## Setting up uptimerobot + +At the moment, your bot will go offline after an hour due to your repl going to sleep. Uptimerobot will send a HTTP request every 5 minutes to keep your repl alive. + +- Go to https://uptimerobot.com and click "Register for FREE" + +![Arrow pointing to "Register for FREE" on top right](https://cloud-qbitd29kl.vercel.app/4register.png) + +- Enter your name, email and create a password + +![Arrows pointing to various fields on register page](https://cloud-qbitd29kl.vercel.app/3details.png) + +- Click "Add New Monitor" + +![Arrow pointing to "Add New Monitor" on top left](https://cloud-qbitd29kl.vercel.app/2addnewmonitor.png) + +- Click the dropdown next to monitor type, and click "HTTP(s)" + +![Arrow pointing to "HTTP(s)" in monitor type dropdown](https://cloud-qbitd29kl.vercel.app/1typehttp.png) + +- Enter anything for friendly name, set the URL as your repl.it preview URL, and make sure you uncheck any alert contacts. + +![Arrows pointing to empty URL field and unchecked alert contacts - Bolt doesn't actually serve any HTTP requests to it and will return 404s, creating useless alerts](https://cloud-qbitd29kl.vercel.app/0monitordetails.png) + +- Click "Create Monitor", and you're done! + +![Dumbledore and Snape partying](https://cloud-pq5lbfiab.vercel.app/3dumbledoreparty.gif) + +## What's next? + +Now that you've managed to build a simple Todo list bot, build upon it and MAKE IT EXTREMELY USEFUL! This is for you to hack on, but here's some inspiration: + +| Repl.it code | Live Demo on the Hack Club Slack | Description | +| ------------- | -------------------------------- | ----------- | +| [Here](https://repl.it/@KhushrajRathod/TodoSlackApp-Markable) | "@Todo List Plus" | This todo list bot let's you mark items as done (and un-mark them) without removing them from the todo list by striking through them. | +| [Here](https://repl.it/@KhushrajRathod/TodoSlackApp-Random) | "@Todo List Random" | This todo list bot simply doesn't like listening to it's users and keeps replying with random messages. | +| [Here](https://TodoSlackApp-Community.khushrajrathod.repl.co) | "@Todo List Community" | This todo list bot maintains a generic todo list that everyone can add to and remove from | + +Did you make something awesome? Share it on [#ship](https://hackclub.slack.com/archives/C0M8PUPU6) in the Hack Club Slack and tag me with [@KhushrajRathod](https://hackclub.slack.com/team/U01C21G88QM)! From eb0680f44d2eedfd0cd982c6b5ea128669b95463 Mon Sep 17 00:00:00 2001 From: Shayan Halder Date: Mon, 7 Dec 2020 11:22:20 -0800 Subject: [PATCH 25/51] Weather Grapher Application (Workshop Bounty) (#1455) * Create readme.md * Update readme.md * Update readme.md * Rename readme.md to README.md * some edits * a few more changes Co-authored-by: Matthew Stanciu --- workshops/weather_grapher/README.md | 409 ++++++++++++++++++++++++++++ 1 file changed, 409 insertions(+) create mode 100644 workshops/weather_grapher/README.md diff --git a/workshops/weather_grapher/README.md b/workshops/weather_grapher/README.md new file mode 100644 index 000000000..16d366235 --- /dev/null +++ b/workshops/weather_grapher/README.md @@ -0,0 +1,409 @@ +--- +name: 'Weather Grapher' +description: 'Graph the average temperature in any major city by using a Web API' +author: '@shayanhalder' +img: 'https://cloud-hj0zqh901.vercel.app/0summary_image.png' +--- + +# Overview + +In this workshop, you'll build a weather grapher program which will be able to graph the average temeprature in any city for a given time range that the user inputs. We'll be using the Chart.js library to graph the data and the Meteostat Web API to let us get weather data. This workshop should take around 20-30 minutes to complete. + +This is what you'll build by the end of this workshop: +![Example Gif With End Result](https://cloud-7seldit0m.vercel.app/0end_result.gif) + +Live Demo with Code: [Repl](https://repl.it/@shayanhalder1/Weather-Grapher-Workshop#index.html) +Final Product (without code): [Live](https://weather-grapher-workshop.shayanhalder1.repl.co/) + +### Prerequisites +To complete this workshop you should have a basic understanding of the fundamentals of: +- HTML +- CSS +- JavaScript (DOM, loops, arrays, objects, functions, and conditionals) + +You do NOT need any prior understanding of: +- Web APIs +- How to make API calls with `fetch()` +- Chart.js Library + +If any of these concepts don't sound familiar to you, don't worry! We'll be learning them as we go. + +Let's begin! + +# Getting started +We'll be using [repl.it](repl.it) to make this project. Head on over to [https://repl.it/languages/HTML](https://repl.it/languages/HTML) to start coding. It's suggested that you make an account so you don't lose your code. + +## Loading Bootstrap and Chart.js +We'll be using Bootstrap to quickly style our interface and give it a clean look. We're simply using a CDN, a content delivery network, to load Bootstrap and the Chart.js library so we can use it later. [*What's a CDN?*](https://www.sitepoint.com/what-is-a-cdn-and-how-does-it-work/) + +Start by copying the following lines of code between the `` tags of your `index.html` file: + +```HTML + + +``` +Next, add these two ` + +``` + +If you want to read about everything that Bootstrap has to offer, the [documentation](https://getbootstrap.com/docs/4.1/getting-started/introduction/) is the best place to start. + +## Building the User Interface +Inside of the `` tag, add a `

` tag with property `style="text-align:center;"` that displays "Weather Grapher" with `class="mt-3"`. This class adds a slight margin to the top so it doesn't look too cramped on the screen. + +Add a `
` after the header with the styling `style="display: flex; justify-content: center;"`, which centers it horizontally on the screen. This `
` will hold the input fields for the city, start date, and end date. Inside this `
` add an `` with the following properties: `type="text" class="form-control mr-1" id="city" placeholder="City Name" style="width; 200px"`. Repeat this type of `` two more times, except replacing the id and placeholder attributes with "Start Date" and "End Date" respectively. + +```HTML + +

Weather Grapher

+
+ + + +
+ +``` +The `form-control` class applies some preset styling to make the input field look sleek and the `mr-1` class adds a slight margin to the right. The `placeholder` attribute adds preset text to the input field when nothing is entered in it. We also added (YYYY-MM-DD) to the placeholder because that it is the only date format that the API will accept, which we'll see later when we look at the documentation. We also made the date input fields a little longer so the placeholder text is able to fit. + +Add a ` +
+ +``` + +We're almost done with HTML! + +Add another `
` after the first `
` and also center it using `justify-content`. Nest another `
` inside with the styling `position: relative; width: 140vh; height: 85vh;` so it's big enough to hold the chart. A vh unit represents 1% of the width/height of the viewport. + +Inside of this, add a `` element with an `id` of `myChart` and the same width and height as its parent `
`. The canvas will hold the chart once we add it in our Javascript. We have to nest the canvas inside of a `
` for us to adjust its size. We also have to nest that inside yet another `
` for us to center it. Once we implement the chart in Javascript, feel free to play around with the size and ajust it to your liking. + +At this point, the body of your HTML file should look like this: + +```HTML + +

Weather Grapher

+
+ + + + +
+
+
+ +
+
+ +``` +This is what we've built so far: + +![Image of Current User Interface](https://cloud-iqs31da3d.vercel.app/0user_interface.png) + + +We're finally done with the HTML! It's time to tackle the JavaScript. + +# JavaScript +Yay! Now that we've finished writing HTML, it's time to start making it work with JavaScript. + +## Setting up the Chart +To get started with the Chart.js library, head on over to the documentation [here](https://www.chartjs.org/docs/latest/). You'll see a starter template for us to use. Copy only the `ctx` and `myChart` variables. Then, navigate to the `script.js` file on the left side of your repl and paste those two variables in. This code creates a `Chart` object which we store in the variable `myChart`. The first argument in the constructor must be a drawing context that refers to the HTML canvas element that we want to draw to, which is the `ctx` variable. + +Change `myChart` variable to be a `let` variable and the `ctx` to a `const` variable, since we want to avoid using `var` to declare variables because of [ES6 conventions](https://javascript.info/var). The second argument in the constructor is an object with data that specifies how our chart will look like. Because we want our graph to start off empty, do the following: + +- Delete all the values in the `labels` array. +- Empty the content of the `label` string. +- Empty the `data` array since we don't have any data yet. +- Delete the `backgroundColor` property and add replace it with the property `fill: false`. + - This removes any shading that would be below the line in our chart. +- Delete the array in the `borderColor` property and replace it with `borderColor: 'rgba(255, 99, 132, 1)`. +- Add the property `borderWidth: 1` so our line isn't too thick. + +```javascript +const ctx = document.getElementById('myChart').getContext('2d'); +let myChart = new Chart(ctx, { + type: 'line', + data: { + labels: [], + datasets: [{ + label: "", + data: [], + fill: false, + borderColor: 'rgba(255, 99, 132, 1)', + borderWidth: 1 + }], + options: { + scales: { + yAxes: [{ + ticks: { + beginAtZero: true + } + }] + } + } +}); +``` + +We're almost done setting up the chart! + +Empty everything inside of the `options` object, and add the following properties (not including the options property): + +```javascript +options: { + responsive: true, + scales: { + xAxes: [{ + ticks: { + maxTicksLimit: 10, + } + }], + yAxes: [{ + scaleLabel: { + display: true, + labelString: "Temperature in Celsius", + padding: 20 + } + }] + } +} +``` + +**Explanation**: The `responsive: true` property allows us the chart to resize itself with respect to its parent `div`. We add an `xAxes` object with a `ticks` object to describe the scaling of the graph. `mackTickLimits: 10` sets the maximum number of x-axis ticks as 10. For the y-axis, we add add a `scaleLabel` object. + +We can then add the properties `labelString: Temperature in Celsius` and `display: true` which will change the y-axis label to "Temperature in Celsius" and make it visible. Lastly, we add `padding: 20` which adds some space between the y-axis and its label. Customizable options like these can be found in the [Chart.js documentation](https://www.chartjs.org/docs/latest/axes/cartesian/#tick-configuration). + +We're done setting up the chart! + +## Setting up the API +We'll be using the Meteostat Web API to get weather data. Before we go to the documentation, head over [here](https://auth.meteostat.net/) to register for an API key, which we'll need when we make requests to the API. Check your email for your API key and save it to your clipboard, we'll use it later. + +[Here](https://dev.meteostat.net/api/#api-key) is the documentation for the API if you want to learn about it more in depth. The basic idea is that the API works by retrieving data made publicly available from various weather stations. So in order to retrieve weather data, we first have to search for weather stations in the city we're looking for, and then pick a weather station in that city to retreive data from. First, add the following variables at the very top of your JS file. + +```javascript +const stationURL = "https://api.meteostat.net/v2/stations/search"; +const dataURL = "https://api.meteostat.net/v2/stations/daily"; +``` + +`stationURL` is the API endpoint for retrieving various weather stations in an area. `dataURL` is the API endpoint for retreiving weather data from a given weather station. An API endpoint is simply a point of entry, or URL, for the client to receive a specific request from a web API. These URLs can be found in the [documentation](https://dev.meteostat.net/api/#json-api). + +## Retrieving a Weather Station +It's time to implement the `getData()` function that we applied to our "Graph" button. Note: All new code segments should be placed directly after the previous code segment unless otherwise specified. Let's start by retreiving the inputs of the city, start date, and end date. At the end of your `script.js` file, add: + +```javascript +async function getData() { // Retrieve the values of city, start date, and end date from input fields. + const city = document.getElementById('city').value; + const startDate = document.getElementById('startDate').value; + const endDate = document.getElementById('endDate').value; +} +``` +We simply retrieve the values of the inputs from the `index.html` file using the `.getElementById()` method and the `.value` property. We've made this an `async` function, which means that it will always return a promise. A promise is essentially an object returned by the function to handle a `fetch()` request. + +For our purposes, we use it because it is always required if the `await` expression is used anywhere inside the function, which we'll use in a bit. We also make it an `async` function because then it will pause the function until a promise is returned, which we'll need when making API calls. + +Read more in-depth about aync/await in JavaScript [here](https://javascript.info/async-await). + +**Note:** According to the documentation, the start and end dates for the API requests must be entered in `YYYY-MM-DD` format. A detailed description of the API parameters and response parameters can be found [here](https://dev.meteostat.net/api/stations/daily.html#parameters). For the purposes of this workshop, we won't be going into input validation. However, we recommended that you try it as an exercise after completing this workshop! + +We should make sure that the user inputted all the required values, like so: +```javascript +if(!(city && startDate && endDate)) { + alert('Please input all data.'); + return; +} +``` +Our boolean expression in the if statement checks if one or more of the values from the input field are not filled out. If this is true, then we alert the user and terminate the program. + +The API documentation specifies that request must contain a `query` parameter which contains the name of the city we're looking for. So to make our request URL, we can use the Javascript `URL` and `URLSearchParams` class to add a query property to the URL with the name of the city we're looking for: + +```javascript +let url = new URL(stationURL); // Create a URL object. +url.search = new URLSearchParams({ // Use URLSearchParams class to modify its query parameter. + query: city +}); +``` +The `URL` constructor creates an object that allows us to easily work with the parts and components of URLs. We then modify `search` property of the url by using the `URLSearchParams` constructor to quickly add a query parameter with the value of the city we're looking for. After this, the URL would look something like: `https://api.meteostat.net/v2/stations/search?query=Austin` if we were to search for Austin, Texas. We are now ready to fetch data! + +We can use the Javascript `fetch()` function to make an API request to meteostat to get back a list of weather stations in the city we're looking for: + +```javascript +let promise = await fetch(url, { // Make a request to find weather stations based on the city we want. + headers: { + "x-api-key": "(paste your API key here)" // Include API Key to authenticate the request. + }, +}); +let data = await promise.json(); // Convert the response to JSON format. +``` +The first argument in the `fetch()` function is always the API endpoint, or URL, if we're making an API request. The second arugment is an object with some data about the request. We have to include our API Key attatched to the `x-api-key` property and we have to nest this property inside the `headers` object, which is specified in the [documentation](https://dev.meteostat.net/api/#authentication). + +The `await` keyword pauses the function until the promise from the API call is resolved. We then convert the response data to JSON format using the `.json()` method. This workshop will cover just one of many ways to use `fetch()`. + +Read more about everything that `fetch()` has to offer [here](https://javascript.info/fetch). + +If we `console.log(data)` with what have now, you'll see the response object with `meta` and `data` properties. The `meta` property contains additonal information about the API request. The `data` property contains a list of objects, each with a unique station ID in the area. + +![Image of Station Data Response Object](https://cloud-ks3i4va0o.vercel.app/0station_data.png) +(Note: This image was taken in the console of the Chrome browser.) + +If we happen to pick a weather station without any data, we should handle it with an alert message, shown below. If there is data, we'll just always pick the first weather station in the response for the sake of this workshop. + +To get weather data, we have to make another API call using that specific station ID which we'll do in a `retrieveData()` function. However, if there's no data available, the function will return false and we'll end the program. After that, we'll use the weather data we got to update the chart in the `updateChart()` method. + +```javascript +if (!data.data) { // If no data then return the function. + alert('No data available for the city.'); + return; +} +const stationID = data.data[0].id; // Pick the ID of the first weather station. +const graphData = await retrieveData(stationID, startDate, endDate); // Use the Station ID to get weather data. +if (!graphData) return; // If no data from first weather station then return. +updateChart(graphData, city); // Update the chart. +``` + +## Retrieving Weather Data +Create a new `async` function named `retrieveData()` with parameters `id`, `start`, and `end`. The function will take those parameters and return an object with an array of dates for the x-axis and an array of values for the y-axis. + +Inside the function, create two empty arrays named `xDates` and `yTemps`, respectively. Use the `URL` and `URLSearchParams` class to create a new request URL for a specific weather station using the URL at the top of your JS file like we did for the first request URL. The documentation requires the parameter names in the request to be named `station`, `start`, and `end`. + +```javascript +async function retrieveData(id, start, end) { // This function also has to be async because we'll be making another fetch() request. + const xDates = []; + const yTemps = []; + + let url = new URL(dataURL); // Remember, dataURL is the global variable we defined at the beginning of the JS file. + url.search = new URLSearchParams({ // Add required parameters to retrieve weather data from a specific station. + station: id, + start: start, + end: end + }); +} +``` + +Now we can add a `fetch()` request using the request URL and convert the response data to JSON format, like we did before. + +```javascript +let promise = await fetch(url, { + headers: { + "x-api-key": "(paste your API key here)" // Include API Key to authenticate the request. + }, +}); +let data = await promise.json(); // Convert the response to JSON format. +``` +If you `console.log(data)`, then you'll see an object with the `data` and `meta` properties, like we saw before. The `data` property contains an array of many objects, each object containing weather data for one specific data. + +![Image of Weather Data Response Object](https://cloud-25az1z0r1.vercel.app/0weather_data.png) + +First we must check if there is data in the response, which we may not get in some weather stations. If there is, we can use a `for` loop to iterate through all the objects in the `data` property of the response object and add the date to the `xDates` array and the average temperatures to the `yTemps` array. The function will then return these variables in an object so we can graph it. + +However, if the weather station that we use doesn't return any data, we'll simply alert it to the user and terminate the program for the sake of this workshop. Once you're done with this workshop, you could try to make the program loop through all the other weather stations until it gets weather data if you don't get any data from the first station. + +```javascript +if (data.data && data.data[0].tavg) { // Make sure that there is weather data in the API response. + for (day of data.data) { // Iterate through the array of objects and add the average temperature and date to the xDates and yTemps array. + xDates.push(day.date); + yTemps.push(day.tavg); + } + return { xDates, yTemps }; +} else { // If there's no weather data for us to use, simply alert to user and terminate the program. + alert("No data available for this city."); + return false; +} +``` + +Note that when we check if there is any data, we check both the `data` property as well as the `tavg` property of the first object in the `data` array because some weather stations may return data for other weather measurements but not `tavg`, so we handle that possibility as well. To iterate through the weather data, we use a "for of" loop. In the loop, `day` represents each object inside of the array that belongs to the `data` property. + +Now we just have to graph this data and we're done! + +## Graphing the Weather Data + +At the end of the `script.js` file, create a new function named `updateChart()` which takes two parameters, `inputData` and `cityName`. `inputData` is the data that was returned from the previous function. + +Create a constant variable named `newData` with a `label` property of ``Average temperature in ${cityName}``. We're using a *template literal*, which is a formatting tool that lets us easily combine strings and variable names. Anything between the backticks is a string. If a variable is needed, it can be referenced inside curly braces with a dollar sign in front. + +To load the y-axis data, add a `data` property with a value of `inputData.yTemps` since the return value from our previous function returned the x-axis and y-axis data in an object. Add a `fill` property with a value of `false` so the graph is not colored in. Lastly, add the properties of `borderColor: 'rgba(255, 99, 132, 1)'` and `borderWidth: 1` to make the lines red and thin. + +```javascript +function updateChart(inputData, cityName) { + const newData = { + label: `Average temperature in ${cityName}`, + data: inputData.yTemps, + fill: false, + borderColor: 'rgba(255, 99, 132, 1)', + borderWidth: 1 + }; +} +``` +To add this data to our graph, set the first object of the `datasets` property in the `myChart` variable equal to the `newData` variable. To update the x-axis, set the `labels` property of the `myChart` variable equal to `inputData.xDates`. Finally, call the `.update()` method on the `myChart` variable so the updates are made visible to the user. + +```javascript +function updateChart(inputData, cityName) { + const newData = { + label: `Average temperature in ${cityName}`, + data: inputData.yTemps, + fill: false, + borderColor: 'rgba(255, 99, 132, 1)', + borderWidth: 1 + }; + myChart.data.datasets[0] = newData; + myChart.data.labels = inputData.xDates; + myChart.update(); +} +``` + +Now, if you click the green "Run" button at the top of your repl, you should see a graph! + +Congratulations! You just built an awesome weather grapher project while also learning about `fetch()`, `async/await`, and the Chart.js library! + +## Extending the Project +Now it's your turn to apply what you know to make this project even more awesome! Feel free to use the resources linked at the bottom which provide further depth into the concepts and tools used in this workshop. Here's some inspiration on ways you can extend the project! + +#### Multiple Datasets + +In this version, you can graph the average temperature of multiple cities, not just one. + +- [Code](https://repl.it/@shayanhalder1/Weather-Grapher-Extended-Version-1) +- [Live Demo](https://weather-grapher-extended-version-1.shayanhalder1.repl.co/) + +#### Temperature Conversion +In this version, you can convert the graph data between Fahrenheit and Celsius. + +- [Code](https://repl.it/@shayanhalder1/Weather-Grapher-Extended-Version-2) +- [Live example](https://weather-grapher-extended-version-2.soshayanhalder1.repl.co/) + +#### Bar Chart +In this version, you can create bar charts of the monthly average temperature by inputting a specific range of months in a given year. + +- [Code](https://repl.it/@shayanhalder1/Weather-Grapher-Extended-Version-3) +- [Live example](https://weather-grapher-extended-version-3.shayanhalder1.repl.co/) + +#### Average Dataset +In this version, you can add a dataset to the graph that represents the average temperature of all the cities on the graph. + +- [Code](https://repl.it/@shayanhalder1/Weather-Grapher-Extended-Version-4) +- [Live example](https://weather-grapher-extended-version-4.shayanhalder1.repl.co/) + +In each of these examples, including this workshop, there is not much input validation. If we enter dates that aren't in chronological order, the program won't work. As an extra exercise, you can try to implement this input validation on your own. Happy hacking! + +## Supplemental Resources + +- [The Coding Train's Course on Data and APIs](https://www.youtube.com/watch?v=DbcLg8nRWEg&list=PLRqwX-V7Uu6YxDKpFzf_2D84p0cyk4T7X) +- [Definition of API](https://www.investopedia.com/terms/a/application-programming-interface.asp) +- [Promises, Async/Await](https://javascript.info/async) +- [Network Requests with Fetch API](https://javascript.info/network) +- [Meteostat API Documentation](https://dev.meteostat.net/api/#api-key) +- [Chart.js Documentation](https://www.chartjs.org/docs/latest/) +- [Boostrap Documentation](https://getbootstrap.com/docs/4.5/getting-started/introduction/) + + + + From a13a64ec6a8772159bd26023a3458763f12c1758 Mon Sep 17 00:00:00 2001 From: s1ntaxe770r <53065463+s1ntaxe770r@users.noreply.github.com> Date: Mon, 7 Dec 2020 23:18:26 +0100 Subject: [PATCH 26/51] Add flask quote generator ( resubmission ) (#1505) * Add flask quote generator * lots of edits * one small thing * small change * slightly better intro * small edit * small wording * oops fix mistake * small change * h * hh * Update README.md Co-authored-by: Matthew Stanciu --- workshops/Flask_quote_generator/README.md | 290 ++++++++++++++++++++++ 1 file changed, 290 insertions(+) create mode 100644 workshops/Flask_quote_generator/README.md diff --git a/workshops/Flask_quote_generator/README.md b/workshops/Flask_quote_generator/README.md new file mode 100644 index 000000000..31e9def8b --- /dev/null +++ b/workshops/Flask_quote_generator/README.md @@ -0,0 +1,290 @@ +--- +name: 'KanyeRest Quote Generator' +description: 'Make a quote generator with Flask ' +author: '@s1ntaxe770r' +--- + +So you listened to "Mercy", "Flashing Lights" and "FourFiveSeconds". Now you're looking for insipration from Kanye? Yeah me too!! In this workshop, you will build a Kanye quote generator. By the end of this workshop, you will learn how to do three things: + +- Fetch from an API using web requests +- Parse request data +- Render the data + +This workshop assumes some basic knowledge of Python, HTML, and CSS. + +Let's get started! + +## So what's Flask? + +Flask is a web application framework for Python. With it, you can super-easily build fully-functioning web APIs. If you don't totally understand what that means, don't worry—you'll begin to understand it as you go along. + +## Getting started + +We're going to be using [repl.it](https://repl.it), a free, online code editor, to write this workshop. To get started, visit the starter project [here](https://repl.it/@JubrilOyetunji/kanyerest). Your coding environment will spin up in a few seconds! + +### But where's the data? + +![where](https://cloud-c2egtgknk.vercel.app/0where.gif) + +We're going to pull our Kanye quotes from [kanye.rest](https://kanye.rest), a free API that generates random Kanye quotes. + +### Alright lets do it! + +Let's start by importing a few libraries in `main.py`: + +```python +from flask import Flask,render_template +import requests +``` + +On the first line, we import `Flask` and `render_template`. `render_template` allows us to return a "template"—in our case, the HTML file in the `templates` folder—along with some data. + +## Creating your first Flask route + +A route in Flask is how we define paths on our app. An example would be http://hackclub.com/workshops — the route would be `/workshops`. + +Let's create an instance of `Flask` and create our first route. Under the first two lines you wrote, add: + +```python +app = Flask(__name__) + +@app.route("/") +def index(): + # do stuff here +``` + +First, we're assigning a Flask instance to a variable called `app`. Then, we create our first route. + +As you can see, Flask routes are defined with the `@flaskinstance.route("/routename")`—in our case, `@app.route("/routename")`— decorator right above a function that runs whenever a user visits the route in their web browser. + +## Getting that data! + +![give me your phone meme but it's give me your data](https://cloud-fosrs2x3k.vercel.app/03e0-2.jpg) + +Earlier, we imported the `requests` module, which we will use to make [http requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages). + +Within the `index` function, add the following code: + +```python +@app.route("/") +def index(): + url = "https://api.kanye.rest" + data = requests.get(url) + response = data.json() + quote = response["quote"] + + return render_template("index.html",quote=quote) +``` + +- We start by declaring a variable `url` which holds the url of the API we're trying to fetch +- Then, we make an [HTTP GET request](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/GET) to the url using the `requests` library and assign it to a variable `data` +- Next, we parse the HTTP response using `data.json()` + - The response from the API looks something like this: + ```json + {"quote":"Sometimes I push the door close button on people running towards the elevator. I just need my own elevator sometimes. My sanctuary."} + ``` +- Then, we get the "quote" from the HTTP response and assign it to a variable `quote` +- Finally, we render the data on a webpage using the `render_data` function. + +
+ + Protip! + + If you want to see the response you get from `response`, add: + + ```python + print(response) + ``` + + right after the line that starts with `response =`. + +
+ +## Rendering the data + +![present](https://cloud-8ec0u6szu.vercel.app/0garfield.gif) + +By default, Flask looks for any HTML files you pass to the `render_template` function in a folder called `templates`. On the sidebar on the left of your repl, click on the folder called `templates` to open it. Then, open the `index.html` file inside it. Inside, you should see the following code: + +```html + + + + + + + + Kanye Quotes + + +
+

Kanye once said

+
+
+

{{ quote }}

+
+ refresh + + +``` + +Flask uses a templating language called [jinja](https://jinja.palletsprojects.com/en/2.11.x/). This is how we can pass data and even use things like loops and conditional statements. The begining and end of jinja syntax are denoted by `{{ }}` or `{% %}` in the case of things like conditionals. + +There are three main parts to pay attention to here: + +```html + +``` +- Here we use `url_for` tell flask to look in the static folder in the current directory and return a file named `style.css`. + +```html +

{{ quote }}

+``` +- Here we render the quote we passed to the `render_template` function earlier. + +```html +refresh +``` +- Once more use the `url_for()` function but this time we pass the name of the function handling the index route since each the page loads it fetches a new quote. + +## CSS!!! + +To make things look a little nicer, open up the `style.css` file located in the `static` folder. Feel free to play around with it and add your own styles, or you can use the CSS below: + +```CSS +.head { + margin: 0 auto; + width: 60%; +} + +h1 { + text-align: center; +} + +.quote { + text-align: center; + font-size: 2em; + margin: 0 auto; + margin-top: 2em; +} + +.refresh { + text-decoration: none; + text-align: center; + font-size: 2em; + width: 4em; + margin: 0 auto; + margin-top: 2em; +} +``` + +Once you've added your styles, navigate back to `main.py` and add this to the bottom of the file: + +```python +if __name__ == "__main__": + app.run(host="0.0.0.0") +``` + +This will make sure our app continuously runs once we run it. The `host="0.0.0.0"` parameter makes it accessible to everyone on the web. + +
+ + Here's the final code: + + `main.py`: + + ```python + from flask import Flask,render_template + import requests + + app = Flask(__name__) + + @app.route("/") + def index(): + url = "https://api.kanye.rest" + data = requests.get(url) + response = data.json() + quote = response["quote"] + + return render_template("index.html",quote=quote) + + if __name__ == "__main__": + app.run(host="0.0.0.0") + ``` + + `index.html`: + + ```html + + + + + + + + Kanye Quotes + + +
+

Kanye once said

+
+
+

{{ quote }}

+
+ refresh + + + ``` + + `style.css`: + + ```css + .head { + margin: 0 auto; + width: 60%; + } + + h1 { + text-align: center; + } + + .quote { + text-align: center; + font-size: 2em; + margin: 0 auto; + margin-top: 2em; + } + + .refresh { + text-decoration: none; + text-align: center; + font-size: 2em; + width: 4em; + margin: 0 auto; + margin-top: 2em; + } + ``` + +
+ +To see what you made, click the green "Run" button at the top of your repl. + +Yay!!! You did it!!!! + +# Hacking + +![hacking](https://cloud-hjufepegf.vercel.app/0hacker_cat.gif) + +Check out what other Hack Clubbers have made! + +- [Khushraj Rathod](https://repl.it/@KhushrajRathod/RandomJokeGenerator#main.py) used the dad joke API to make a dad joke generator +- [Jason Antwi-Appah](https://repl.it/@JasonAntwiAppah/kanyerest2#main.py) used a food API to build to build a food suggestion app + +Check out [this](https://apilist.fun) list for other cool APIs you could build stuff with. + +The source code for this workshop can be found [here](https://github.com/s1ntaxe770r/KQG) + +### Further resources + +- The [Flask](https://flask.palletsprojects.com/en/1.1.x/) documentation is super friendly so it's worth checking out. +- If you're more of a visual learner, check out [Pretty Printed](https://prettyprinted.com)—they've got some great Flask tutorials. From 9e890eddb31a8de50c39ecee5a8a2d4f63f2d180 Mon Sep 17 00:00:00 2001 From: Matthew Stanciu Date: Mon, 7 Dec 2020 17:23:55 -0500 Subject: [PATCH 27/51] fix quote generator case --- .../{Flask_quote_generator => flask_quote_generator}/README.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename workshops/{Flask_quote_generator => flask_quote_generator}/README.md (100%) diff --git a/workshops/Flask_quote_generator/README.md b/workshops/flask_quote_generator/README.md similarity index 100% rename from workshops/Flask_quote_generator/README.md rename to workshops/flask_quote_generator/README.md From 522ffa007321b9fb6017b4951b24bb34f9e34c4f Mon Sep 17 00:00:00 2001 From: Matthew Stanciu Date: Mon, 7 Dec 2020 17:26:52 -0500 Subject: [PATCH 28/51] add img to flask workshop --- workshops/flask_quote_generator/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/workshops/flask_quote_generator/README.md b/workshops/flask_quote_generator/README.md index 31e9def8b..24953ef70 100644 --- a/workshops/flask_quote_generator/README.md +++ b/workshops/flask_quote_generator/README.md @@ -2,6 +2,7 @@ name: 'KanyeRest Quote Generator' description: 'Make a quote generator with Flask ' author: '@s1ntaxe770r' +img: 'https://cloud-57v29xozt.vercel.app/0screen_shot_2020-12-07_at_5.26.13_pm.png' --- So you listened to "Mercy", "Flashing Lights" and "FourFiveSeconds". Now you're looking for insipration from Kanye? Yeah me too!! In this workshop, you will build a Kanye quote generator. By the end of this workshop, you will learn how to do three things: From 5e0f11ee06cc4e5c1e59e5bea9a90f0ce42f67d3 Mon Sep 17 00:00:00 2001 From: Anirudh Balaji Date: Mon, 7 Dec 2020 14:31:41 -0800 Subject: [PATCH 29/51] Rust discord bot (workshop bounty) (#1452) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Initial code * push readme changes * Finished up bot code * NaN fix * Fix technical details part * Add write-up for poll types * Small changes * More explaining * Halfway done! * Small changes to make compile * Finished workshop!! * formatting * nits * bug fix * changes * remove code directory * Set spacing to 2 * ps * hack 1 * Finished hacks! * how to invite your bot * address review nits * tutorial –> workshop Co-authored-by: Matthew Stanciu --- workshops/discord_bot_with_rust/README.md | 1336 +++++++++++++++++++++ workshops/wishlist/README.md | 1 + 2 files changed, 1337 insertions(+) create mode 100644 workshops/discord_bot_with_rust/README.md diff --git a/workshops/discord_bot_with_rust/README.md b/workshops/discord_bot_with_rust/README.md new file mode 100644 index 000000000..ad275b782 --- /dev/null +++ b/workshops/discord_bot_with_rust/README.md @@ -0,0 +1,1336 @@ +--- +name: 'Discord poll bot in Rust' +description: 'Make a Discord polling bot in Rust using the Serenity library' +author: '@anirudhb' +--- + +# Make a Discord bot in Rust! + +Discord bots are cool, right? Haven't you ever wanted to make your own? + +Well, today we're going to do exactly that, but this time we're using Rust! We're going to build a Discord bot that allows you to setup polls, and updates counts in real-time! + +Depending on how well you know Rust from an intermediate to advanced level, this workshop could take anywhere from 40 minutes to an hour to complete. Don't let that scare you, though! You'll learn a lot of new concepts about how to structure complex applications in Rust along the way :) + +## Prerequisites + +For this workshop, I do recommend an intermediate understanding of low-level concepts such as memory management, and some experience with Rust as well. Here are the concepts in particular that I recommend you have a good understanding of: +* Lifetimes, borrowing and move semantics +* How Rust structures code (i.e. Cargo projects) +* The general idea behind macros (not the exact syntax but a good idea of their general purpose) +* Basic Rust knowledge (I recommend the [Rust book](https://doc.rust-lang.org/book/) for this!) + +## Demo + +Here's a demo of the polling bot in action: + +![Polling bot demo](https://cloud-kfuekwrsa.vercel.app/0poll-bot-example.gif) + +The full code can be viewed [here](https://repl.it/@anirudhb/Rust-discord-bot-finished). Alternatively, you can open the below section for a full listing. + +
+Full code + +`main.rs`: +```rust +fn main() { std::process::Command::new("cargo").arg("run").status().unwrap(); } +``` + +`Cargo.toml`: +```toml +[package] +name = "polling-bot" +version = "0.1.0" +authors = ["runner"] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +serenity = "0.9.1" +tokio = { version = "^0.2.23", features = ["macros"] } + +[[bin]] +name = "polling-bot" +path = "real_main.rs" +``` + +`real_main.rs`: +```rust +use serenity::async_trait; +use serenity::framework::standard::{ + macros::{command, group}, + Args, CommandResult, StandardFramework, +}; +use serenity::model::{ + channel::{Message, Reaction}, + gateway::Ready, +}; +use serenity::prelude::*; +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::Mutex; +use serenity::model::channel::ReactionType; +use serenity::model::id::{MessageId, ChannelId}; + +enum ReactionEvent<'a> { + Reaction(&'a Reaction), + RemoveAll(ChannelId, MessageId), +} + + +macro_rules! perform_reaction { + (($ctx:expr, $reaction_event:expr) $body:expr) => { + use ReactionEvent::*; + // Discard if it's our own reaction. + if let Reaction(r) = $reaction_event { + if r.user_id == Some($ctx.cache.current_user_id().await) { + println!("Reaction added by self, ignoring"); + return; + } + } + + let key = match $reaction_event { + Reaction(r) => (r.channel_id, r.message_id), + RemoveAll(c, m) => (c, m), + }; + + // Try to get poll for the given message otherwise return + { + let poll_data = $ctx.data.read().await; + let poll_map = poll_data + .get::() + .expect("Failed to retrieve polls map!") + .lock() + .await; + if !poll_map.contains_key(&key) { + println!("Message not in polls map, ignoring"); + return; + } + } + + // reretrieve the map as writable + let mut poll_data = $ctx.data.write().await; + let mut poll_map = poll_data + .get_mut::() + .expect("Failed to retrieve polls map!") + .lock() + .await; + let poll = match poll_map.get_mut(&key) { + None => { + println!("Failed to get poll for {:?}", key); + return; + } + Some(poll) => poll, + }; + + // nudges Rust towards the right type :) + fn get_f)>(f: F) -> F { + f + } + let f = get_f($body); + + match $reaction_event { + Reaction(r) => match r.emoji { + ReactionType::Unicode(ref s) => { + let c = s.chars().nth(0).unwrap(); + let end_char = std::char::from_u32('🇦' as u32 + poll.answers.len() as u32 - 1) + .expect("Failed to format emoji"); + if c < '🇦' || c > end_char { + println!("Emoji is not regional indicator or is not in range, ignoring"); + return; + } + let number = (c as u32 - '🇦' as u32) as usize; + + f(poll, Some(number)); + } + _ => { + println!("Unknown emoji in reaction, ignoring"); + return; + } + }, + RemoveAll(..) => f(poll, None), + } + + let content = render_message(&poll); + key.0 + .edit_message(&$ctx.http, key.1, |edit| edit.content(&content)) + .await + .expect("Failed to edit message"); + + println!("Rerendered message"); + }; +} + +struct Handler; + +#[async_trait] +impl EventHandler for Handler { + async fn ready(&self, _: Context, ready: Ready) { + println!("Bot ready with username {}", ready.user.name); + } + + async fn reaction_add(&self, ctx: Context, add_reaction: Reaction) { + println!("Reaction add"); + + perform_reaction! { (ctx, ReactionEvent::Reaction(&add_reaction)) |poll, number| { + poll.answerers[number.unwrap()] += 1; + }} + } + + async fn reaction_remove(&self, ctx: Context, removed_reaction: Reaction) { + println!("Single reaction remove"); + + perform_reaction! { (ctx, ReactionEvent::Reaction(&removed_reaction)) |poll, number| { + poll.answerers[number.unwrap()] -= 1; + }} + } + + async fn reaction_remove_all(&self, ctx: Context, channel_id: ChannelId, removed_from_message_id: MessageId) { + println!("All reactions removed"); + + perform_reaction! { (ctx, ReactionEvent::RemoveAll(channel_id, removed_from_message_id)) |poll, _| { + for answers in poll.answerers.iter_mut() { + *answers = 0; + } + }} + } +} + +fn render_message(poll: &Poll) -> String { + let mut message_text = format!("**Poll:** {}\n", poll.question); + let total_answerers = poll.answerers.iter().sum::(); + + for (i, (answer, &num)) in poll.answers.iter().zip(poll.answerers.iter()).enumerate() { + let emoji = std::char::from_u32('🇦' as u32 + i as u32).expect("Failed to format emoji"); + message_text.push(emoji); + if total_answerers > 0 { + let percent = num as f64 / total_answerers as f64 * 100.; + message_text.push_str(&format!(" {:.0}%", percent)); + } + message_text.push(' '); + message_text.push_str(answer); + message_text.push_str(&format!(" ({} votes)", num)); + message_text.push('\n'); + } + + message_text +} + +struct PollsKey; + +impl TypeMapKey for PollsKey { + type Value = Arc>; +} + +type PollsMap = HashMap<(ChannelId, MessageId), Poll>; + +struct Poll { + pub question: String, + pub answers: Vec, + pub answerers: Vec, +} + +#[group] +#[commands(poll)] +struct General; + +#[command] +async fn poll(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { + let question = args.single_quoted::()?; + let answers = args + .quoted() + .iter::() + .filter_map(|x| x.ok()) + .collect::>(); + + let answers_len = answers.len(); + let poll = Poll { + question: question, + answerers: vec![0; answers_len], + answers: answers, + }; + + let message_text = render_message(&poll); + let emojis = (0..answers_len) + .map(|i| std::char::from_u32('🇦' as u32 + i as u32).expect("Failed to format emoji")) + .collect::>(); + + let poll_msg = msg.channel_id.say(&ctx.http, &message_text).await?; + + for &emoji in &emojis { + poll_msg + .react(&ctx.http, ReactionType::Unicode(emoji.to_string())) + .await?; + } + + let mut poll_data = ctx.data.write().await; + + let poll_map = poll_data + .get_mut::() + .expect("Failed to retrieve polls map!"); + + poll_map + .lock() + .await + .insert((msg.channel_id, poll_msg.id), poll); + + Ok(()) +} + +#[tokio::main] +async fn main() { + let token = std::env::var("DISCORD_TOKEN").expect("Expected DISCORD_TOKEN to be set!"); + + let framework = StandardFramework::new() + .configure(|c| c.case_insensitivity(true)) + .group(&GENERAL_GROUP); + + let mut client = Client::builder(&token) + .event_handler(Handler) + .framework(framework) + .type_map_insert::(Arc::new(Mutex::new(PollsMap::new()))) + .await + .expect("Failed to build client"); + + if let Err(why) = client.start().await { + println!("Client error: {:?}", why); + } +} +``` + +
+ +## Getting started + +We're going to host our Discord bot on [repl.it](https://repl.it). + +To get started, [create an account](https://repl.it/signup). I personally recommend you sign in with your GitHub account if you have one, but email is fine too. + +Now let's create a new Rust project by going to https://repl.it/languages/rust: + +![Initial Rust project](https://cloud-gjndvi3vx.vercel.app/0image.png) + +## Adding the library and setting up a basic bot + +Right now, our Rust program isn't a Cargo project. Cargo is Rust's package manager. Without it, we wouldn't be able to easily depend on libraries. So since our program isn't a Cargo project, it won't be able to use the `serenity` library! + +Let's fix that by running `cargo init --name polling-bot` in the terminal. This command initializes a new Cargo project for us. You can replace `polling-bot` with whatever you want your program to be named. This should create two new files on the side: `.gitignore` and `Cargo.toml`, which we'll be using to add the Serenity library: + +![New files created by cargo init](https://cloud-bxfulgo22.vercel.app/0image.png) + +P.S. Here's a hint when working with Repl.it: If you ever get a `disk quota exceeded` error just delete the `target` directory and try again. Additionally, delete the `target` directory when you're done playing around with your bot or else it'll take up a bunch of space! + +Next, create a new file called `real_main.rs` and put this code in it: +```rust +fn main() { + // todo +} +``` + +Replace the contents of `main.rs` with this: +```rust +fn main() { std::process::Command::new("cargo").arg("run").status().unwrap(); } +``` + +
+Magic?!?!?!? + +This line seems kinda magic but it's just running `cargo run` from a Rust program. Repl.it is kind of weird in this way since it doesn't _natively_ support Cargo projects but it mostly works if we do this. If you're running this code locally, you can skip this and the `real_main.rs` and just write all your code in `main.rs`. + +
+ +This is kind of a hack but it's necessary to make sure that the environment variables are passed through correctly. You'll be doing all your coding in `real_main.rs`. + +One last thing: Update the path to the source file in `Cargo.toml`, changing `main.rs` to `real_main.rs`. Your Cargo.toml should look like this: + +![Cargo.toml after fixing source filename](https://cloud-fvk4e7lkf.vercel.app/0image.png) + +### What is Serenity? + +Serenity is a Rust _crate_ (or library) that helps you write Discord bots in Rust. If you've heard of Discord.py for Python, or Discord.js for JavaScript, you can kind of think of Serenity like that, except for Rust. + +### Adding the bot token + +Now, we need to create a new bot in the [Discord Developer Portal](https://discord.com/developers/applications). Click "New Application" in the top right corner, highlighted in red here: + +![Discord Developer Portal- New Application](https://cloud-4fax6pert.vercel.app/0inkedscreenshot_2020-11-19_discord_developer_portal_____api_docs_for_bots_and_developers_li.jpg) + +Give your bot a nice name (I'm using "Polling Bot" for this tutorial), then hit the create button! + +Now, go to the "Bot" section of your application, highlighted in red here: + +![Polling Bot- Bot section](https://cloud-jufkijale.vercel.app/0inkedscreenshot_2020-11-19_discord_developer_portal_____api_docs_for_bots_and_developers_1__li.jpg) + +Click the "Add Bot" button to enable the bot for this application, highlighted in red here: + +![Polling Bot, bot section- Add Bot button](https://cloud-jdkfymmpt.vercel.app/0inkedscreenshot_2020-11-19_discord_developer_portal_____api_docs_for_bots_and_developers_2__li.jpg) + +If you want to, feel free to rename the bot's username or give it an avatar. I'm skipping that here since it's up to you to add your own creative touch ✨ + +Alright, now that we've setup our bot, copy the bot's token by clicking the "Copy" button next to the token field, highlighted in light cyan here: + +![Polling Bot, bot section- Copy button next to Token field](https://cloud-7f7l7crt1.vercel.app/0inkedscreenshot_2020-11-19_discord_developer_portal_____api_docs_for_bots_and_developers_3__li.jpg) + +Great, you've copied your token! + +Let's put it into Repl.it. Create a new `.env` file in Repl.it. The `.env` file is a special file that allows you to store secrets, such as your Discord bot's token, for example. + +Inside the `.env` file, add a new line that looks like `DISCORD_TOKEN=`. Replace `` with the token that you previously copied. It should look like this (I've redacted my token): + +![.env file with DISCORD_TOKEN variable set to your token](https://cloud-nn0n8t195.vercel.app/0inkedscreenshot_2020-11-19_fluffyprevailingmarketing_li.jpg) + +Now, let's add the Serenity library! In Rust, projects manage their dependencies using `Cargo.toml`, so that's where we need to add Serenity. Head over to `Cargo.toml` and add this line under your `[dependencies]` section: `serenity = "0.9.1"`. At the time of writing, the latest version of Serenity is 0.9.1, but you can replace it with the latest version which can be found [here](https://crates.io/crates/serenity). + +We'll also need another helper library, `tokio`. Add `tokio = { version = "^0.2.23", features = ["macros"] }` to your `Cargo.toml` as well. This library just helps us out with some async stuff, but you don't need to worry too much about it for now. Don't use the latest version of Tokio (0.3 or later) as this will cause incompatibilities with Serenity! + +Your `Cargo.toml` should now look like this: + +![Cargo.toml with Serenity dependency](https://cloud-epv4dnmyn.vercel.app/0image.png) + +### Inviting your bot to a server + +To invite your bot to a server, you'll need to go to the OAuth2 tab in your application (highlighted in green here): + +![Polling Bot- OAuth2 section](https://cloud-oe7fro27r.vercel.app/0inkedscreenshot_2020-11-24_discord_developer_portal_____api_docs_for_bots_and_developers_li.jpg) + +Next, select the "bot" scope for OAuth2, highlighted in pink here: + +![Polling Bot- OAuth2 section, "bot" scope](https://cloud-677psdnfr.vercel.app/0inkedscreenshot_2020-11-24_discord_developer_portal_____api_docs_for_bots_and_developers_1__li.jpg) + +Then, scroll down and check these permissions we'll need (highlighted in orange in the picture): +* View Channels (under General Permissions) +* Send Messages (under Text Permissions) +* Read Message History (under Text Permissions) +* Add Reactions (under Text Permissions) + +![Polling Bot- OAuth2 section, permissions selected: View Channels, Send Messages, Read Message History, Add Reactions](https://cloud-ga2iol69b.vercel.app/0inkedscreenshot_2020-11-24_discord_developer_portal_____api_docs_for_bots_and_developers_2__li.jpg) + +Finally, copy the OAuth2 link, highlighted in brown here: + +![Polling Bot- OAuth2 section, "copy" button and OAuth2 URL highlighted](https://cloud-11kxy9z13.vercel.app/0inkedscreenshot_2020-11-24_discord_developer_portal_____api_docs_for_bots_and_developers_3__li.jpg) + +Paste it into your browser and invite your bot to a server for testing! (Preferably with other people to test the polling.) You'll need the "Manage Server" permission in order to invite the bot to a server. + +### A basic template + +Now, let's add a basic Discord bot template. At this point, you should have invited your bot to a server for testing. + +Delete the contents of `real_main.rs` and replace it with this: +```rust +use serenity::async_trait; +use serenity::framework::standard::{ + macros::{command, group}, + Args, CommandResult, StandardFramework, +}; +use serenity::model::{ + channel::{Message, Reaction}, + gateway::Ready, +}; +use serenity::prelude::*; + +struct Handler; + +#[async_trait] +impl EventHandler for Handler { + async fn ready(&self, _: Context, ready: Ready) { + println!("Bot ready with username {}", ready.user.name); + } +} + +#[group] +#[commands(ping)] +struct General; + +#[command] +async fn ping(ctx: &Context, msg: &Message, mut _args: Args) -> CommandResult { + msg.channel_id.say(&ctx.http, "Pong!").await?; + + Ok(()) +} + +#[tokio::main] +async fn main() { + let token = std::env::var("DISCORD_TOKEN").expect("Expected DISCORD_TOKEN to be set!"); + + let framework = StandardFramework::new() + .configure(|c| c.case_insensitivity(true)) + .group(&GENERAL_GROUP); + + let mut client = Client::builder(&token) + .event_handler(Handler) + .framework(framework) + .await + .expect("Failed to build client"); + + if let Err(why) = client.start().await { + println!("Client error: {:?}", why); + } +} +``` + +This is just a super simple template that has a `~ping` command which makes the bot respond with `Pong!`. + +By the way, I highly recommend having the [Serenity](https://docs.rs/serenity) docs open on the side while going through this workshop! You can search for all of the functions and structs we use in there, with very detailed explanations. + +
+Technical details + +```rust +use serenity::async_trait; +use serenity::framework::standard::{ + macros::{command, group}, + Args, CommandResult, StandardFramework, +}; +use serenity::model::{ + channel::{Message, Reaction}, + gateway::Ready, +}; +use serenity::prelude::*; +``` +These are just some imports that we need. + +```rust +struct Handler; +``` +This creates a new type called `Handler` which has no data. We're going to be implementing the `EventHandler` trait on it so that we can handle ready events. + +```rust +#[async_trait] +impl EventHandler for Handler { + async fn ready(&self, _: Context, ready: Ready) { + println!("Bot ready with username {}", ready.user.name); + } +} +``` +This implements the `EventHandler` trait for `Handler`. By default all of the event handlers don't do anything, but here we override the `ready` method, so that we can print our bot's username once it's ready. + +Since we're defining an `async fn` in the implementation, we have to use the `#[async_trait]` attribute (imported above) to allow it, because [currently Rust does not natively support async traits](https://github.com/rust-lang/rfcs/issues/2739). + +```rust +#[group] +#[commands(ping)] +struct General; +``` +This sets up a command group. In Serenity, commands can only be added through command groups, so we just setup a `General` command group with the `ping` command. + +```rust +#[command] +async fn ping(ctx: &Context, msg: &Message, mut _args: Args) -> CommandResult { + msg.channel_id.say(&ctx.http, "Pong!").await?; + + Ok(()) +} +``` +This is the `ping` command. We just take the command's message, get the channel it was sent in, and send `Pong!` in that channel. (So, we just reply with `Pong!`.) + +```rust +#[tokio::main] +async fn main() { + let token = std::env::var("DISCORD_TOKEN").expect("Expected DISCORD_TOKEN to be set!"); +``` +This is the start of the main function. We mark it with the `#[tokio::main]` annotation to make it into an `async fn` which it is not by default. + +Then, we retrieve the token from the `DISCORD_TOKEN` environment variable and panic if it's not set. + +```rust + + let framework = StandardFramework::new() + .configure(|c| c.case_insensitivity(true)) + .group(&GENERAL_GROUP); +``` +Here we setup the Serenity standard command framework. We configure it to allow case insensitivity, so that commands can be typed like `~poll`, `~pOlL`, `~poLL` or any other combination. Then we also add the `General` group of commands which includes the `ping` command we defined above. + +```rust + + let mut client = Client::builder(&token) + .event_handler(Handler) + .framework(framework) + .await + .expect("Failed to build client"); + + if let Err(why) = client.start().await { + println!("Client error: {:?}", why); + } +} +``` +Now we setup our Discord bot client with the token we got earlier, our event handler (which prints the bot's username once the ready event is received), and our command framework. + +Then, we start the bot, and if there's an error after running it, we print the error. + +--- +That's all of the details! + +
+ +Run the bot to make sure that everything works. It may take a while to build at first but subsequent builds will be faster. + +Now it's time to actually add polling to the bot! + +## Representing polls in code + +Before we add the `poll` command, we'll need to define some types to represent polls. + +Add these lines to your imports: +```rust +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::Mutex; +``` + +This just imports some things we'll be using soon. + +Now let's start adding the types for polls. Add this code before `struct General` in `real_main.rs`: +```rust +struct PollsKey; + +impl TypeMapKey for PollsKey { + type Value = Arc>; +} +``` + +Here, we create a new type `PollsKey` which we'll use to retrieve the current polls. We also implement the `TypeMapKey` trait for `PollsKey`, which lets us use it as a key in a type map. Serenity uses type maps for data storage so that's why we have to make this type. We set the type of the key's value to be `Arc>`. `Arc` is atomic reference-counting, which lets us share an object across threads (this is necessary for async). `Mutex` is used for exclusive access to the map, when we are changing it. `Arc>` is a common pattern used to share mutable data across threads. + +Next, let's define the `PollsMap` type: +```rust +type PollsMap = HashMap<(ChannelId, MessageId), Poll>; +``` + +`PollsMap` is just a type alias to a `HashMap` with key type `(ChannelId, MessageId)` (a tuple) and value type `Poll`. + +Finally, let's define the `Poll` type: +```rust +struct Poll { + pub question: String, + pub answers: Vec, + pub answerers: Vec, +} +``` + +The `Poll` type just has a question, list of answers and how many people answered for each response. + +Now that we've defined our type key and the type value, let's actually add that to our global data map that Serenity provides. Add this in your `main` function: +```rust +#[tokio::main] +async fn main() { + // -- snip -- + + let mut client = Client::builder(&token) + .event_handler(Handler) + .framework(framework) + .type_map_insert::(Arc::new(Mutex::new(PollsMap::new()))) // new! + .await + .expect("Failed to build client"); + + // -- snip -- +} +``` +This just inserts an empty polls map with the type key `PollsKey` we defined earlier. + +Now, let's finally create the `poll` command! + +## Creating the `poll` command + +Let's define the `poll` command. + +Before we do anything else, we'll need to add two new imports: +```rust +use serenity::model::channel::ReactionType; +use serenity::model::id::{MessageId, ChannelId}; +``` +We'll use this a little later. + +Next, remove the `ping` function and change the `ping` in group `General` to be our new `poll` command. Removing it should look something like this: +```diff + #[group] +-#[commands(ping)] ++#[commands(poll)] + struct General; + +-#[command] +-async fn ping(ctx: &Context, msg: &Message, mut _args: Args) -> CommandResult { +- msg.channel_id.say(&ctx.http, "Pong!").await?; +- +- Ok(()) +-} +``` + +Now, let's create the `poll` command. Right under group `General`, let's add this: +```rust +#[command] +async fn poll(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { +``` +This is basic scaffolding used for all commands in Serenity. We take a context (which we can use to send messages, etc.), the message containing the command and an `Args` object allowing us to easily retrieve arguments to our command. Then we return a `CommandResult` which lets us handle errors in our command easily. + +
+What is Result<T, E>? + +Rust doesn't do error handling like most other languages, where you would `throw` or `raise` an Exception and then catch it later. In general it doesn't work that well as a error handling model, so Rust uses `Result` instead. It's very simply defined like this: +```rust +pub enum Result { + Ok(T), + Err(E), +} +``` +We have a success type `T` and an error type `E`. It's just an enum that contains an `Ok` variant and an `Err` variant. This makes it really easy to handle since we can use things like `match` on it and such. + +But `Result` has one more superpower: the `?` operator. The `?` operator, when used in a function that returns `Result`, makes it _super_ easy to handle errors and propagate them. As an example: +```rust +fn error1() -> std::io::Result<()> { + // returns the error if the function fails + do_some_fallible_io_operation()?; + + Ok(()) // default case, everything ok +} + +// is equivalent to: + +fn error2() -> std::io::Result<()> { + match do_some_fallible_io_operation() { + Ok(_) => Ok(()) // ok, then ok + Err(e) => Err(e), // error? propagate the error + } +} +``` +This makes it much easier to work with errors in Rust especially with `Result`. + +*Note:* The `?` operator works with any `Result` where `E2: Into` and `E` is the error type of the outer function's `Result`. Also see [`std::ops::Try`](https://doc.rust-lang.org/std/ops/trait.Try.html#impl-Try-2). +
+ +Next, we're going to get the question, which will be the first argument: +```rust + let question = args.single_quoted::()?; +``` +The [`single_quoted`](https://docs.rs/serenity/0.9.1/serenity/framework/standard/struct.Args.html#method.single_quoted) function returns one argument (delimited by quotes, if there are spaces in it) of the given type. Since we give it type `String`, this accepts anything. For what the `?` operator means, you can read the above section. + +Now let's get all the answers that the user provided: +```rust + let answers = args + .quoted() // 1) Enable quoting for answers with spaces + .iter::() // 2) Iterate over the rest of the arguments (as Strings) + .filter_map(|x| x.ok()) // 3) Filter out any arguments that failed to parse + .collect::>(); // 4) Collect all the arguments into a Vec +``` + +Let's count the total number of answers (we'll need this later): +```rust + + let answers_len = answers.len(); +``` + +Now we can create our `Poll` struct with the data that we got: +```rust + let poll = Poll { + question: question, + // no responses yet + answerers: vec![0; answers_len], + answers: answers, + }; +``` +The `vec![0; answers_len]` is a shorthand way to create a Vec with length `answers_len` and fill it with zeros. Since we don't have any responses yet, they should all be zero. + +Now we'll have to create a fancy message for the users to respond on: +```rust + + // Build the message contents + let message_text = render_message(&poll); +``` +We'll define this function later, but for now just know that it takes a `Poll` reference and returns a `String` of the message contents. + +We have to accumulate all the emojis to react with, so that the user can easily click to respond. We're using the "regional indicator" section of Unicode, which looks like this in Discord: + +![What regional indicator characters look like in Discord](https://cloud-d9pv3tesy.vercel.app/0image.png) + +This code creates a list of all the regional indicator characters we need. For example if we have 5 total answers, we'll need regional indicators A, B, C, D and E. +```rust + let emojis = (0..answers_len) + .map(|i| std::char::from_u32('🇦' as u32 + i as u32).expect("Failed to format emoji")) + .collect::>(); +``` +We take a range of `0..answers_len` (which is an Iterator), and then we transform it using the `map` function. We add the regional indicator `🇦` to it, which is like an offset. Then once we have all the characters, we collect them into a `Vec` to be iterated over a little later. + +
+Why answers_len and not answers.len()? + +If you tried to use `answers.len()` instead of `answers_len` above, you'd get an error that looks something like this: +``` +error[E0382]: borrow of moved value: `answers` + --> src\main.rs:203:22 + | +187 | let answers = args + | ------- move occurs because `answers` has type `Vec`, which does not implement the `Copy` trait +... +198 | answers: answers, + | ------- value moved here +... +203 | let emojis = (0..answers.len()) + | ^^^^^^^ value borrowed here after move +``` + +What this means is that we're moving the data of `answers` into the `Poll`, so we can't use `answers` anymore since its data is invalid. Therefore, we just get the length of `answers` before moving it into the `Poll` so we can use it later. +
+ +Now let's actually create the message with the contents we got before: +```rust + + let poll_msg = msg.channel_id.say(&ctx.http, &message_text).await?; +``` +So we're taking the channel ID of the command's message, and sending our own message in that same channel. + +Now let's add reactions for each of the answers: +```rust + + for &emoji in &emojis { + poll_msg + .react(&ctx.http, ReactionType::Unicode(emoji.to_string())) + .await?; + } +``` +So for each emoji character we're going to convert it to a string so we can add that reaction (as a Unicode emoji, since we aren't using custom emojis) to our message. + +Now that we've setup the message, we need to add our new poll to the polls map. First we need to retrieve the global `data` as writable: +```rust + + let mut poll_data = ctx.data.write().await; +``` + +Next, we need to get the polls map by retrieving key `PollsKey`: +```rust + + let poll_map = poll_data + .get_mut::() + .expect("Failed to retrieve polls map!"); +``` + +Now, we can finally insert our poll, which is keyed by the channel & message ID (that's all we need to be unique): +```rust + + poll_map + .lock() + .await + .insert((msg.channel_id, poll_msg.id), poll); +``` + +We succeeded! Let's return with a successful value: +```rust + + Ok(()) +} +``` + +And that's the end of the `poll` command! + +But... do you remember the `render_message` function which we were going to define later? Let's do that. + +## The `render_message` function + +The `render_message` function is pretty simple: it just takes a `Poll` reference and formats it to look nice in a message. Let's start defining that right above `struct PollsKey`: +```rust +fn render_message(poll: &Poll) -> String { +``` +We're taking a `Poll` reference as input (we don't need to take ownership since we are just reading it) and returning a `String` of the formatted message contents. + +```rust + // Build the message contents + let mut message_text = format!("**Poll:** {}\n", poll.question); + let total_answerers = poll.answerers.iter().sum::(); +``` +We start the message with a bolded **Poll:** then we put the question after it. We mark it as `mut` since we'll add to it. Also, we create a total_answerers variable which contains the total number of responses (used for percentage calculation.) + +```rust + + for (i, (answer, &num)) in poll.answers.iter().zip(poll.answerers.iter()).enumerate() { +``` +We're iterating over each answer string, how many votes it got and the number of the answer we are iterating over (used for creating emoji). + +We create the emoji similarly to the way we did in the `poll` command, and then add it to the message: +```rust + let emoji = std::char::from_u32('🇦' as u32 + i as u32).expect("Failed to format emoji"); + // add answerers and percent + message_text.push(emoji); +``` + +If we got at least one response in total we add a percentage (if we have zero responses, we get NaN therefore we don't show it in that case): +```rust + if total_answerers > 0 { + let percent = num as f64 / total_answerers as f64 * 100.; + message_text.push_str(&format!(" ({:.0}%)", percent)); + } +``` + +Lastly, we add the answer string and how many votes it got (as well as a newline): +```rust + message_text.push(' '); + message_text.push_str(answer); + message_text.push_str(&format!(" ({} votes)", num)); + message_text.push('\n'); + } +``` + +Now we just return the message that we've built up! +```rust + + message_text +} +``` + +That's the end of the `render_message` function! + +Give yourself a pat on the back, we're halfway done!! + +![Halfway done GIF](https://media.giphy.com/media/l0HlRey3XbrNJGRzi/giphy.gif) + +If you want to, feel free to run your program now and try out the `poll` command (prefix is `~`). It won't work yet but the message should be printed. + +The full code at this point can be viewed [here](https://repl.it/@anirudhb/Rust-discord-bot-checkpoint-1). Alternatively, you can open the below section for a full listing. + +
+Full code at this point + +`main.rs`: +```rust +fn main() { std::process::Command::new("cargo").arg("run").status().unwrap(); } +``` + +`Cargo.toml`: +```toml +[package] +name = "polling-bot" +version = "0.1.0" +authors = ["runner"] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +serenity = "0.9.1" +tokio = { version = "^0.2.23", features = ["macros"] } + +[[bin]] +name = "polling-bot" +path = "real_main.rs" +``` + +`real_main.rs`: +```rust +use serenity::async_trait; +use serenity::framework::standard::{ + macros::{command, group}, + Args, CommandResult, StandardFramework, +}; +use serenity::model::{ + channel::{Message, Reaction}, + gateway::Ready, +}; +use serenity::prelude::*; +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::Mutex; +use serenity::model::channel::ReactionType; +use serenity::model::id::{MessageId, ChannelId}; + + +struct Handler; + +#[async_trait] +impl EventHandler for Handler { + async fn ready(&self, _: Context, ready: Ready) { + println!("Bot ready with username {}", ready.user.name); + } +} + +fn render_message(poll: &Poll) -> String { + let mut message_text = format!("**Poll:** {}\n", poll.question); + let total_answerers = poll.answerers.iter().sum::(); + + for (i, (answer, &num)) in poll.answers.iter().zip(poll.answerers.iter()).enumerate() { + let emoji = std::char::from_u32('🇦' as u32 + i as u32).expect("Failed to format emoji"); + message_text.push(emoji); + if total_answerers > 0 { + let percent = num as f64 / total_answerers as f64 * 100.; + message_text.push_str(&format!(" {:.0}%", percent)); + } + message_text.push(' '); + message_text.push_str(answer); + message_text.push_str(&format!(" ({} votes)", num)); + message_text.push('\n'); + } + + message_text +} + +struct PollsKey; + +impl TypeMapKey for PollsKey { + type Value = Arc>; +} + +type PollsMap = HashMap<(ChannelId, MessageId), Poll>; + +struct Poll { + pub question: String, + pub answers: Vec, + pub answerers: Vec, +} + +#[group] +#[commands(poll)] +struct General; + +#[command] +async fn poll(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { + let question = args.single_quoted::()?; + let answers = args + .quoted() + .iter::() + .filter_map(|x| x.ok()) + .collect::>(); + + let answers_len = answers.len(); + let poll = Poll { + question: question, + answerers: vec![0; answers_len], + answers: answers, + }; + + let message_text = render_message(&poll); + let emojis = (0..answers_len) + .map(|i| std::char::from_u32('🇦' as u32 + i as u32).expect("Failed to format emoji")) + .collect::>(); + + let poll_msg = msg.channel_id.say(&ctx.http, &message_text).await?; + + for &emoji in &emojis { + poll_msg + .react(&ctx.http, ReactionType::Unicode(emoji.to_string())) + .await?; + } + + let mut poll_data = ctx.data.write().await; + + let poll_map = poll_data + .get_mut::() + .expect("Failed to retrieve polls map!"); + + poll_map + .lock() + .await + .insert((msg.channel_id, poll_msg.id), poll); + + Ok(()) +} + +#[tokio::main] +async fn main() { + let token = std::env::var("DISCORD_TOKEN").expect("Expected DISCORD_TOKEN to be set!"); + + let framework = StandardFramework::new() + .configure(|c| c.case_insensitivity(true)) + .group(&GENERAL_GROUP); + + let mut client = Client::builder(&token) + .event_handler(Handler) + .framework(framework) + .type_map_insert::(Arc::new(Mutex::new(PollsMap::new()))) + .await + .expect("Failed to build client"); + + if let Err(why) = client.start().await { + println!("Client error: {:?}", why); + } +} +``` + +
+ +Alright, take a quick break and relax a little before we jump into coding the rest of our bot! + +--- + +## Reacting to reactions + +Now that we have our bot sending properly-formatted messages, the next step is to make it actually react when someone adds a reaction. + +Discord sends us 3 reaction related events which we will need to handle: +1. [reaction_add](https://docs.rs/serenity/0.9.1/serenity/prelude/trait.EventHandler.html#method.reaction_add), which gives us a [`Reaction`](https://docs.rs/serenity/0.9.1/serenity/model/channel/struct.Reaction.html) struct containing the channel and message ID of the reaction, as well as the emoji, +2. [reaction_remove](https://docs.rs/serenity/0.9.1/serenity/prelude/trait.EventHandler.html#method.reaction_remove), with similar parameters to reaction_add and +3. [reaction_remove_all](https://docs.rs/serenity/0.9.1/serenity/prelude/trait.EventHandler.html#method.reaction_remove_all), which doesn't give us an emoji but gives us the channel and message ID. (Emoji doesn't make sense here since every reaction is removed.) + +Note that there will probably be a lot of common code shared between these events: for each one we will have to (1) validate the message it is referring to, (2) retrieve the corresponding poll object and (3) perform the action indicated by the event. Therefore, we are going to implement this using macros to reduce code duplication! + +Given either a reaction or a (channel id, message id) pair we will need to extract the channel and message ID from it. Let's create an enum representing these two states, and put it right below your imports: +```rust +enum ReactionEvent<'a> { + Reaction(&'a Reaction), + RemoveAll(ChannelId, MessageId), +} +``` +The first variant represents some kind of single-reaction event (such as add/remove), and the second variant represents the remove all event which only has channel and message ID. + +Now, let's start writing our macro to handle most of the shared code: +```rust +macro_rules! perform_reaction { +``` +This starts a macro declaration. We're using one type of macro known as declarative macros, which are created using [`macro_rules!`](https://doc.rust-lang.org/rust-by-example/macros.html). The other type (procedural macros) is out of scope for this tutorial. + +```rust + (($ctx:expr, $reaction_event:expr) $body:expr) => { +``` +This is a match rule. One invocation of the macro that would match this rule looks something like this: +```rust +// you can open a invocation with either { (curly bracket), [ (bracket), or ( (parenthesis) +perform_reaction! { + /*opening parenthesis*/( + /*$ctx:expr*/ &ctx, + /*$reaction_event:expr*/ ReactionEvent::Reaction(&reaction), + /*closing parenthesis*/) + /*$body:expr*/ |poll, i| { + // stuff + } +} +``` + +In fact, that's what most of our invocations will look like. Now, let's move on to what we're going to do with the parameters, now that we have them: +```rust + use ReactionEvent::*; +``` +We use the variants of this enum a lot so bring it into top-level scope temporarily. + +```rust + // Discard if it's our own reaction. + if let Reaction(r) = $reaction_event { + if r.user_id == Some($ctx.cache.current_user_id().await) { + println!("Reaction added by self, ignoring"); + return; + } + } +``` +Due to the `if let`, we are only evaluating this if the `ReactionEvent` provided is a reaction (and not a remove all). If we added the reaction, then we ignore it and return. + +```rust + + let key = match $reaction_event { + Reaction(r) => (r.channel_id, r.message_id), + RemoveAll(c, m) => (c, m), + }; +``` +We are turning our `ReactionEvent` into the key we can lookup in our polls map. In Rust, `match` is an expression so it works fine here. + +```rust + // Try to get poll for the given message otherwise return + { +``` +Here, we're starting a new scope. This is very important because otherwise we would get a deadlock. + +
+Deadlock? Why? + +~~Consider that in order to check if a key is present in a map, we only need read access to the map. However, to modify the map, we need write access. But we can only tell if we need to modify the map if we read the map first. Therefore, this scope will be the scope that holds read access to the map. If we don't need to write to it, we will early return from here. Otherwise, we will drop our read access so that later, we can take write access without deadlocking.~~ + +Sound confusing? Let me try to illustrate with an example: +```rust +// How RwLocks work: +// * multiple readers allowed concurrently +// * writers require exclusive access (no readers or other writers) + +let read_access = get_read_access(); // \- read access begins here +if need_write_access(read_access) { // | + let write_access = get_write_access(); // | \- write access begins here + // | ? DEADLOCK! Already have read access, + // | ? so this will never complete! + modify(write_access); // | ? +} else { // | + return; // /- read access ends due to return +} +``` + +To fix this, we add a scope: +```rust +{ + let read_access = get_read_access(); // \- read access begins here + // By inverting the condition we | + // prevent the two accesses from | + // overlapping. | + if !need_write_access(read_access) { // | + return; // |- read access possibly ends due to return + } // | +} // /- read access ends due to scope + +let write_access = get_write_access(); // \- write access begins here + // | OK! No other references! + // | No deadlock! +modify(write_access); // | +``` + +This makes Rust happy and we don't get any deadlock! + +
+ +Now, we check if the key is present in the polls map (if it is not, the message is not a poll): +```rust + let poll_data = $ctx.data.read().await; + let poll_map = poll_data + .get::() + .expect("Failed to retrieve polls map!") + .lock() + .await; + if !poll_map.contains_key(&key) { + println!("Message not in polls map, ignoring"); + return; + } +``` +First we acquire read access to the data map (from the provided `$ctx`). Next, we try to get the polls map by looking up the `PollsKey`. Then we check if the polls map contains our message's key. If not, we return since it is not a poll. (This is similar to what we did in the `poll` command.) + +```rust + } +``` +Now that we're done reading the map we relenquish read access by closing the scope. + +Now we re-retrieve the polls map again but this time with write access: +```rust + + // reretrieve the map as writable + let mut poll_data = $ctx.data.write().await; + let mut poll_map = poll_data + .get_mut::() + .expect("Failed to retrieve polls map!") + .lock() + .await; + let poll = match poll_map.get_mut(&key) { + None => { + println!("Failed to get poll for {:?}", key); + return; + } + Some(poll) => poll, + }; +``` +If we were not able to find the poll in the map (even though we checked above), we just return. + +Now we need to do something a little wacky. The `$body:expr` we declared in our rule above? That's going to actually be a function that takes an `&mut Poll` (to modify the poll) and an `Option` indicating which answer was reacted to (if it is not a remove all event.) Since Rust can't infer the type of the function for some reason, we need to nudge it: +```rust + + // nudges Rust towards the right type :) + fn get_f)>(f: F) -> F { + f + } + let f = get_f($body); +``` + +Next, if the event was a reaction, we need to validate it to ensure that we should actually process the reaction: +```rust + + match $reaction_event { + Reaction(r) => match r.emoji { + ReactionType::Unicode(ref s) => { + let c = s.chars().nth(0).unwrap(); + let end_char = std::char::from_u32('🇦' as u32 + poll.answers.len() as u32 - 1) + .expect("Failed to format emoji"); + if c < '🇦' || c > end_char { + println!("Emoji is not regional indicator or is not in range, ignoring"); + return; + } + let number = (c as u32 - '🇦' as u32) as usize; +``` +First we ensure that the reaction's emoji is a Unicode emoji, since all regional indicators (the emojis we are using) are Unicode. Then we check that the emoji is actually a regional indicator, and is in range. Next, we figure out which answer it would be (where 0 is the first answer.) + +Now, we can call the body and it can modify the poll: +```rust + + f(poll, Some(number)); + } +``` + +We also have to handle non-Unicode emojis, which we simply ignore: +```rust + _ => { + println!("Unknown emoji in reaction, ignoring"); + return; + } + }, +``` + +And if the event was a remove all, we just call the function, this time without an answer number: +```rust + RemoveAll(..) => f(poll, None), + } +``` + +Now that we've let the body update the poll appropriately, we need to update the message with the new value of the poll: +```rust + + let content = render_message(&poll); + key.0 + .edit_message(&$ctx.http, key.1, |edit| edit.content(&content)) + .await + .expect("Failed to edit message"); + + println!("Rerendered message"); + }; +} +``` +We're using that `render_message` function we defined earlier (this is why we made it a function 😉) to get the new contents of our message. Then we edit the message's contents. + +And that's it! We're done writing our macro! The rest is gonna be pretty easy from here since it's just a few more lines! + +## Handling reaction events + +Now we just need to write the event handlers for the reaction events which will be super easy. + +Inside the `impl` block where you defined the `ready` method on `Handler`, let's add the `reaction_add` handler as well: +```rust + async fn reaction_add(&self, ctx: Context, add_reaction: Reaction) { + println!("Reaction add"); + + perform_reaction! { (ctx, ReactionEvent::Reaction(&add_reaction)) |poll, number| { + poll.answerers[number.unwrap()] += 1; + }} + } +``` +We're calling our `perform_reaction` macro defined above with the context, an event with reaction (the reaction we got) and a body which just increments the vote count for the given answer by one. + +We do pretty much the same thing for `reaction_remove`, but we are decrementing this time: +```rust + async fn reaction_remove(&self, ctx: Context, removed_reaction: Reaction) { + println!("Single reaction remove"); + + perform_reaction! { (ctx, ReactionEvent::Reaction(&removed_reaction)) |poll, number| { + poll.answerers[number.unwrap()] -= 1; + }} + } +``` + +And for `reaction_remove_all`, we just iterate through each vote count and set it to zero: +```rust + async fn reaction_remove_all(&self, ctx: Context, channel_id: ChannelId, removed_from_message_id: MessageId) { + println!("All reactions removed"); + + perform_reaction! { (ctx, ReactionEvent::RemoveAll(channel_id, removed_from_message_id)) |poll, _| { + for answers in poll.answerers.iter_mut() { + *answers = 0; + } + }} + } +``` + +## Wrap-up + +**YAY!! We did it!! You're finally done with your bot!** + +Let's go ahead and try out all of this new stuff by hitting the run button. You should be able to add and remove reactions, having the message update as you do so. Additionally, if you try removing all reactions, it should update properly as well. + +![You did it! Congratulations! GIF](https://media.giphy.com/media/J5Xr9k7qK5KGRi45vp/giphy.gif) + +The full code is in the [Demo](#demo) section. + +## Going further + +There are many ways to improve and hack on this project further. Here are 3 examples of possible hacks you could do: +* Showing who voted for what ([code](https://repl.it/@anirudhb/Rust-discord-bot-hack-1)) +* Reaction roulette - your vote only has a 20% chance of counting! (This is kind of annoying, and on purpose :-) ([code](https://repl.it/@anirudhb/Rust-discord-bot-hack-2)) +* Modifying polls after they are created ([code](https://repl.it/@anirudhb/Rust-discord-bot-hack-3)) + +Here are some links if you'd like to learn more about writing Discord bots with Serenity: +* [Serenity docs](https://docs.rs/serenity) +* [Tokio docs](https://docs.rs/tokio/0.2.23/tokio/index.html) +* [Discord developer docs](https://discord.com/developers/docs/intro) +* [Rust Book chapter 4, ownership](https://doc.rust-lang.org/book/ch04-00-understanding-ownership.html) diff --git a/workshops/wishlist/README.md b/workshops/wishlist/README.md index 10b795b71..6ac5425bc 100644 --- a/workshops/wishlist/README.md +++ b/workshops/wishlist/README.md @@ -43,3 +43,4 @@ If you’d like to see someone make a workshop on building a specific type of pr - How to make a Discord bot ([@zachlatta](https://github.com/zachlatta)) - A collaborative Spotify playlist using the Spotify API ([@MatthewStanciu](https://github.com/MatthewStanciu)) - (Update) GitHub Pages and/or Netlify ([discussion](https://github.com/hackclub/hackclub/issues/1181), [@itsmingjie](https://github.com/itsmingjie)) +- Low-level programming with Rust ([@anirudhb](https://github.com/anirudhb)) From 3be9ed130d0ec5eeb2b80fc2c5687f3d837c5c8e Mon Sep 17 00:00:00 2001 From: Matthew Stanciu Date: Mon, 7 Dec 2020 17:34:19 -0500 Subject: [PATCH 30/51] Add img to rust discord bot workshop --- workshops/discord_bot_with_rust/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/workshops/discord_bot_with_rust/README.md b/workshops/discord_bot_with_rust/README.md index ad275b782..1b8b46335 100644 --- a/workshops/discord_bot_with_rust/README.md +++ b/workshops/discord_bot_with_rust/README.md @@ -2,6 +2,7 @@ name: 'Discord poll bot in Rust' description: 'Make a Discord polling bot in Rust using the Serenity library' author: '@anirudhb' +img: 'https://cloud-kfuekwrsa.vercel.app/0poll-bot-example.gif' --- # Make a Discord bot in Rust! From 622515227fac2beb6f22d69b378f8d4e41ab5505 Mon Sep 17 00:00:00 2001 From: Sam Poder <39828164+sampoder@users.noreply.github.com> Date: Tue, 8 Dec 2020 06:49:10 +0800 Subject: [PATCH 31/51] Add Automating your Slack PFP Workshop (#1456) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * First draft * add me multiplayer! * Add images + sample code * Update README.md * Revert "Update README.md" This reverts commit 1f665d8e38845be8a67856f45ab737c271f273bc. * Revert "Add images + sample code" This reverts commit b27fcde81954793baae9711bca5e413971b474ac. * Revert "add me multiplayer!" This reverts commit 128757d2564872202528e77cc0d70dbd6847dc5c. * Revert "First draft" This reverts commit 851cb294012575a7bba3d6a5870d03d4c21a5e2d. * Create README.md * Update README.md * Update workshops/slack-pfp/README.md Co-authored-by: fayd * Update workshops/slack-pfp/README.md Co-authored-by: fayd * Update workshops/slack-pfp/README.md Co-authored-by: fayd * Update README.md * server less –> serverless * fix small misatke * fix code indentation * fix some typos and stuff Co-authored-by: fayd Co-authored-by: Matthew Stanciu --- workshops/slack-pfp/README.md | 309 ++++++++++++++++++++++++++++++++++ 1 file changed, 309 insertions(+) create mode 100644 workshops/slack-pfp/README.md diff --git a/workshops/slack-pfp/README.md b/workshops/slack-pfp/README.md new file mode 100644 index 000000000..5941bd70f --- /dev/null +++ b/workshops/slack-pfp/README.md @@ -0,0 +1,309 @@ +--- +name: Automating your Slack Profile Picture +description: Make a program that changes your Slack profile picture based on the time of day. +author: '@sampoder' +image: 'https://cloud-45zk83i41.vercel.app/0screenshot_2020-11-22_at_3.27.11_pm.png' +--- + +# Automating your Slack Profile Picture + +Having a plain old profile picture is so 2019. Having a Slack Profile Picture that changes throughout the day is a great way to mix things up on Slack. In this workshop, we'll be learning how to make a dynamic profile picture that changes as the day progresses. We'll be using Node.js deployed as a [Vercel](https://vercel.com) Serverless Function and triggered by [cron-job.org](https://cron-job.org/en/). You'll be learning the basics of getting a Slack User Token, creating a Node.js program and deploying that program as a serverless function. + +## Obtaining a Slack Token + +In my opinion this is the most painful part of this process. + +To begin you will need to be a member of a Slack Workspace. The easiest way to join one is to join [Hack Club's](https://hackclub.com/slack). + +Then visit the [Slack "Build" page](https://api.slack.com/apps). Click the large green button that says `Create New App`. + +Name it whatever you'd like, and choose the workspace you'd like the app to be for. + +On the next screen, find the `Add features and functionality` section and select `Permissions`. + + +![Slack API Configuration Screen with an arrow pointing to permissions](https://cloud-25utju6i9.vercel.app/0screenshot_2020-11-19_at_11.50.18_pm.png) + + + +On this screen, scroll down to the `User Token Scopes` section and select the button that says `Add an OAuth Scope`. + + + +![OAuth Scope Selection on Slack API page](https://cloud-1xkvojjl4.vercel.app/0screenshot_2020-11-19_at_11.55.09_pm.png) + + + +In the pop-up box that follows, select `users.profile:write` and add that scope. + + + +![OAuth Scope Selection on Slack API page completed](https://cloud-2ru7t5yz0.vercel.app/0screenshot_2020-11-20_at_12.00.03_am.png) + + + +Scroll to the top of the page, click `Install to Workspace`, then `Allow` and then you will receive a token in return. + + + +![Sample Token from Slack](https://cloud-1spwslhyd.vercel.app/0screenshot_2020-11-20_at_12.05.54_am.png) + + + +Copy the generated token and store it in a safe space. Don't share it with anyone else (this is a fake token in the image, don't worry). + + + +## Building our program + +To get started you are going to want to create a new Node.js [repl.it](http://repl.it/) project, [click here to do so](https://repl.it/languages/nodejs). + +Let's get going by adding your Slack token to your environment variables. What are environment variables? These are super secret variables that you don't want to store publicly. + +Create a `.env` file and fill it in like: + +``` +SLACK_TOKEN=XOXP-YOUR-TOKEN +``` + +Remember we want to keep this a secret, so create a `.gitignore` file and make it's contents: + +``` +.env +``` + +Now, let's add two packages (`axios` & `@slack/web-api`) to our `index.js` file. + +```javascript +const { WebClient } = require('@slack/web-api'); +const axios = require('axios').default; +``` + +Axios will be used to fetch the images from URLs and the Slack Web API package will be used to interact with Slack. + +Next, let's create a data object with each of our images. Replace each URL with your own to make this program your own. + +```javascript +const images = { + "morning": "https://images.unsplash.com/photo-1517188206596-1e1f7c954177?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1650&q=80", + "afternoon": "https://images.unsplash.com/photo-1566452348683-91f934cd9051?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2090&q=80", + "night": "https://images.unsplash.com/photo-1519446251021-1c4ee77fec1e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=913&q=80" +} +``` + +### Setting our profile picture + +Now that we've got our images we need to create an async function that will handle setting our profile picture. + +```javascript +async function setPFP() { + console.log(images) +} +``` + +An async function is important as it allows us to wait for a line of code to complete before moving to the next line using the `await` keyword. + +What should we do first? How about setting a profile picture! Inside our `setPFP` function add the following lines. This will fetch the contents of the image. + +```javascript +const image = await axios.get(images.afternoon, { + responseType: "arraybuffer" +}); +``` + +Awesome! We now have the image, let's set it to our profile picture on Slack. We can do this using the [users.setPhoto](https://api.slack.com/methods/users.setPhoto) API endpoint. +```javascript +const client = new WebClient(); +const slackRequest = await client.users.setPhoto({ + image: image.data, + token: process.env.SLACK_TOKEN +}); +``` +In the above code we are creating an instance of the Slack Web API Client. Then we make a request to the users.setPhoto API endpoint, with our image data and our token. We need the token so that Slack knows who we are and that we are allowed to make this change. We're sending the data as an array buffer, these are a bit complicated but if you're curious you can read more about them [here](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer). + +Now to run this function, just add the following to the bottom of you `index.js` file: + +```javascript +setPFP() +``` +This simply calls your function, now click `Run ►`. + +Check Slack... do you see any difference in your profile? Fingers crossed you do but if you don't, check the console for any errors and check your code for any typos ;) + +### Changing the Profile Picture Based on Time + +Sooooo it may be the afternoon for you but it may not ¯\\\_(ツ)_/¯ + +Try changing `images.afternoon` to `images.morning` and running your program. You should see a different image as your profile picture after a couple of seconds. + +To do so we'll need to know how many hours have passed in the day, right? + +```javascript +var hour = new Date().getHours() +``` + +Add this code to your program at the **start** of your function, it creates a variable and assigns it the value of how many hours have passed. If you add `console.log(hour)` you'll notice that these hours aren't in your timezone (unless you are in an exact UTC zone). This is because these times are in UTC. I'm not going to go on and on about [timezone nonsense](http://www.physicalgeography.net/fundamentals/images/world_time2.gif) but you'll need to add or remove the UTC offset for your region. + +For example, I live in Singapore so my timezone is UTC+8 and has an offset of +8 so I would write: + +```javascript +var hour = new Date().getHours() + 8 +``` + +Now let's set the image based on hours using a couple of `if` statements and an `else` statement: + +```javascript +let image +if (5 < hour && hour < 12) { + image = await axios.get(images.morning, { + responseType: "arraybuffer", + }); +} +else if (12 < hour && hour < 20) { + image = await axios.get(images.afternoon, { + responseType: "arraybuffer", + }); +} +else { + image = await axios.get(images.afternoon, { + responseType: "arraybuffer", + }); +} +``` + +We want to replace our original image fetching code block with this to make our final code file this: + +```javascript +const { WebClient } = require('@slack/web-api'); +const axios = require('axios').default; +const images = { + "morning": "https://images.unsplash.com/photo-1517188206596-1e1f7c954177?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1650&q=80", + "afternoon": "https://images.unsplash.com/photo-1566452348683-91f934cd9051?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2090&q=80", + "night": "https://images.unsplash.com/photo-1519446251021-1c4ee77fec1e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=913&q=80" +} +async function setPFP() { + var hour = new Date().getHours() + 8 + let image + if (5 < hour && hour < 12) { + image = await axios.get(images.afternoon, { + responseType: "arraybuffer", + }); + } + else if (12 < hour && hour < 20) { + image = await axios.get(images.afternoon, { + responseType: "arraybuffer", + }); + } + else { + image = await axios.get(images.afternoon, { + responseType: "arraybuffer", + }); + } + const client = new WebClient(); + const slackRequest = await client.users.setPhoto({ + image: image.data, + token: process.env.SLACK_TOKEN + }); +} + +setPFP() +``` +## Making it serverless on Vercel + +Serverless is basically a buzz word by hosting companies that means running code server side, without managing your own server. Yep, it's not really serverless. + +Anyhow, we'll need to move our code inside of an `api` folder. So create a new folder called `api` , create a `index.js` file in there and copy all of your code into it. + +Now we'll need to replace `setPFP()` with: + +```javascript +export default async (req, res) => { + await setPFP() + res.send("Started changing your PFP!") +} +``` +That's all the code changes we'll need. Now we need to deploy it! + +First, we'll add it to GitHub (a code hosting platform). If you don't have an account please register one [here](https://github.com) and then come back :) + +Click the `Version Control` button: + +![Version Control Icon on Repl.it](https://cloud-sg9h5hqot.vercel.app/0screenshot_2020-11-21_at_5.36.01_pm.png) + +Then click the `Create a Git Repo` button + +![Create a Git Repo Button](https://cloud-anmjphf23.vercel.app/0screenshot_2020-11-21_at_5.40.05_pm.png) + +Then click `Connect to GitHub`. The following process is different for different people so please follow the onscreen instructions. + +![Connect to GitHub Button](https://cloud-f8t5l4xpe.vercel.app/0screenshot_2020-11-21_at_5.47.33_pm.png) + +Once you get to this screen, copy this link. + +![Link to copy with arrow](https://cloud-p50jnssri.vercel.app/0screenshot_2020-11-21_at_5.43.50_pm.png) + +Now head to vercel.com and register for an account. + +Once you have registered for Vercel, visit https://vercel.com/import and click this button: + +![Vercel Git Repo Button](https://cloud-ah3kdl8r4.vercel.app/0screenshot_2020-11-21_at_5.52.09_pm.png) + +Paste in the link you previously copied and click continue. Click continue again and then deploy! + +![Congrats page](https://cloud-o1laihfer.vercel.app/0screenshot_2020-11-21_at_5.56.17_pm.png) + +If all goes to plan you should see this page, yay!! We have deployed it. Now we need to configure our environment variables. Open the dashboard up. + +![Click to open dashboard](https://cloud-mbfalxjtt.vercel.app/0screenshot_2020-11-21_at_5.59.06_pm.png) + +Then open up the settings: + +![Open settings](https://cloud-bw0zco82g.vercel.app/0screenshot_2020-11-21_at_6.00.33_pm.png) + +Create an environment secret for your Slack token with the following settings: + +![Add env variable](https://cloud-1rw8v8j7g.vercel.app/0screenshot_2020-11-21_at_6.05.48_pm.png) + +Now we need to redeploy our app. You can do this by going to Deployments: + +![Deployments button](https://cloud-ofbgibav5.vercel.app/0screenshot_2020-11-21_at_6.10.44_pm.png) + +Then select the top one: + +![Selecting the top one](https://cloud-hkb7mvp1m.vercel.app/0screenshot_2020-11-21_at_6.12.10_pm.png) + +Then in the top right, click the three dots and then the redeploy button. + +![Redeploy button location](https://cloud-fcenvvko0.vercel.app/0screenshot_2020-11-21_at_6.13.35_pm.png) + +Now try visiting `/api` on your url. You should see your profile picture change on Slack ;) Check it out! But... we don't want to have to visit this URL every time. So, let's automate that. + +## Scheduling a trigger + +Now let's schedule when to trigger this API. Head on over to cron-job.org and create a new account. + +Once you've logged into that account, click `Cronjobs`. + +![Cronjobs button](https://cloud-6ep1r0ykr.vercel.app/0screenshot_2020-11-22_at_1.42.39_pm.png) + +Then create a new one! + +![Create new button](https://cloud-rhrfjtuo7.vercel.app/0screenshot_2020-11-22_at_1.43.43_pm.png) + +Fill in the form with the following settings and click create: + +![Recommended Settings](https://cloud-7lnp5ap0k.vercel.app/0screenshot_2020-11-22_at_1.45.03_pm.png) + +And wow... we're done!! Now throughout the day your profile picture will change depending on the time. + +## Hacking + +Slack profile pictures are semi-unexplored territory but there's certainly a lot of hacking potential in them! So go make something EPIC! + +Here are a couple of examples of cool Slack profile picture hacks: + + - Build a web dashboard focused on your PFP. Here's [an example](http://change-my-pfp.now.sh) of one I made, here's [the source](https://github.com/sampoder/pfp). + - Have random photos on a cycle for your profile picture, check out [my Slack profile](https://hackclub.slack.com/archives/DT08DHJKF/) as an example and here's [the source](https://github.com/sampoder/pfp/blob/7e6294bfed50b6c7a0867e84c67b415f1213b179/pages/api/set-profile.js#L12). + - Let others set your profile picture (WARNING: weird stuff can happen), [here's an example](https://draw.clb.li) made by Caleb and here's [the source](https://github.com/cjdenio/draw-on-avatar) for their app. + +Make something cool? Awesomeeee!!!! Share it on [#ship](https://hackclub.slack.com/archives/C0M8PUPU6/) in the Slack and tag me with [@sampoder](https://hackclub.slack.com/archives/DT08DHJKF/)! From 8f9b5e3c300d0f48199f85a5d57139f4abf08274 Mon Sep 17 00:00:00 2001 From: Matthew Stanciu Date: Mon, 7 Dec 2020 17:49:42 -0500 Subject: [PATCH 32/51] fix slack pfp folder name --- workshops/{slack-pfp => slack_pfp}/README.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename workshops/{slack-pfp => slack_pfp}/README.md (100%) diff --git a/workshops/slack-pfp/README.md b/workshops/slack_pfp/README.md similarity index 100% rename from workshops/slack-pfp/README.md rename to workshops/slack_pfp/README.md From 3ab411607d915f805521e2181fa3ae13d0fe609e Mon Sep 17 00:00:00 2001 From: Matthew Stanciu Date: Mon, 7 Dec 2020 17:54:31 -0500 Subject: [PATCH 33/51] fix details tag i think --- workshops/flask_quote_generator/README.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/workshops/flask_quote_generator/README.md b/workshops/flask_quote_generator/README.md index 24953ef70..8e742429f 100644 --- a/workshops/flask_quote_generator/README.md +++ b/workshops/flask_quote_generator/README.md @@ -89,14 +89,14 @@ def index():
- Protip! + Protip! If you want to see the response you get from `response`, add: - + ```python print(response) ``` - + right after the line that starts with `response =`.
@@ -191,9 +191,9 @@ This will make sure our app continuously runs once we run it. The `host="0.0.0.0
Here's the final code: - + `main.py`: - + ```python from flask import Flask,render_template import requests @@ -212,9 +212,9 @@ This will make sure our app continuously runs once we run it. The `host="0.0.0.0 if __name__ == "__main__": app.run(host="0.0.0.0") ``` - + `index.html`: - + ```html @@ -236,9 +236,9 @@ This will make sure our app continuously runs once we run it. The `host="0.0.0.0 ``` - + `style.css`: - + ```css .head { margin: 0 auto; @@ -264,8 +264,8 @@ This will make sure our app continuously runs once we run it. The `host="0.0.0.0 margin: 0 auto; margin-top: 2em; } - ``` - + ``` +
To see what you made, click the green "Run" button at the top of your repl. From 683be202236375a3467030b55f899f8f1190a420 Mon Sep 17 00:00:00 2001 From: Matthew Stanciu Date: Tue, 8 Dec 2020 09:39:16 -0500 Subject: [PATCH 34/51] fix img on slack pfp workshop --- workshops/slack_pfp/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workshops/slack_pfp/README.md b/workshops/slack_pfp/README.md index 5941bd70f..0f21210c8 100644 --- a/workshops/slack_pfp/README.md +++ b/workshops/slack_pfp/README.md @@ -2,7 +2,7 @@ name: Automating your Slack Profile Picture description: Make a program that changes your Slack profile picture based on the time of day. author: '@sampoder' -image: 'https://cloud-45zk83i41.vercel.app/0screenshot_2020-11-22_at_3.27.11_pm.png' +img: 'https://cloud-6yjkou2ru.vercel.app/0screen_shot_2020-12-08_at_9.38.36_am.png' --- # Automating your Slack Profile Picture From b876fdea697e34516f61a4a1c5f2ccd87dcf0e81 Mon Sep 17 00:00:00 2001 From: Harsh Bajpai Date: Wed, 9 Dec 2020 21:11:42 +0530 Subject: [PATCH 35/51] Added 3 parts of the functional programming series (#1497) * hackide workshop * added a new IOT workshop and fixed a link in my previous hackide workshop * added a new workshop on selenium twitter automation * selenium workshop added * added new workshop * added new workshop on how to publish an npm package * added new workshop on functional programming part-1 * added new workshop on functional programming part-1 * added 3 parts of functional programming series --- workshops/functional_programming_1/README.md | 182 +++++++++++ workshops/functional_programming_2/README.md | 270 +++++++++++++++++ workshops/functional_programming_2/test.js | 8 + workshops/functional_programming_3/README.md | 300 +++++++++++++++++++ workshops/functional_programming_3/test.js | 8 + 5 files changed, 768 insertions(+) create mode 100644 workshops/functional_programming_1/README.md create mode 100644 workshops/functional_programming_2/README.md create mode 100644 workshops/functional_programming_2/test.js create mode 100644 workshops/functional_programming_3/README.md create mode 100644 workshops/functional_programming_3/test.js diff --git a/workshops/functional_programming_1/README.md b/workshops/functional_programming_1/README.md new file mode 100644 index 000000000..bc4a0f451 --- /dev/null +++ b/workshops/functional_programming_1/README.md @@ -0,0 +1,182 @@ +--- +name: 'Functional Programming with js ( part-1 )' +description: 'Learn the basics of functional programming with js!' +author: '@bajpai244' +--- + +Welcome to the workshop. This is part-1 of the functional programming with js series, this workshop will discuss what is functional programming, why we need it?, and the basics of functional programming. + +This workshop will serve as an introduction to the upcoming parts of this series. + +Functional Programming Intro + +This workshop should take around **_20 minutes_** to complete. + +## Prerequisites + +The workshop is for anyone familiar with: + +- Javascript +- General programming concecpts + +jsimage + +You don't need to be a Guru in these topics, a basic understanding of them is more than enough! + +## What is functional programming? + +Okay, so you have been coding for a pretty long time and you have been hearing people talk about this mysterious beast called functional programming. + +Do you wonder what it is? let me provide a simple and clear definition of functional programming for you. + +what is it GIF + +Functional programming is a way of writing your programs ( a paradigm ) so that your code is: + +- Clean +- Readable +- Less Error-Prone +- Easy to Debug + +## Why should I even learn functional programming? + +To start with something, we deep down need a reason to get ourselves going. So, let me tell you why you need to learn functional programming. + +tell me why gif + +We all write code and often find ourselves in messy situations. Like, we don’t understand what the flow of the program is, how is it even working, debugging almost kills most of our time. + +The reason why these things happen is that we lack a paradigm for our programming style. A paradigm brings order to our programming, making our programming style more predictable, easy to debug, and easy to share. + +I get if GIF + +There are many programming paradigms like Imperative programming, Object-Oriented Programming, Functional Programming, etc. + +different types of programming languages image + +People advocate their love for different programming paradigm and so do I. Functional Programming is my favorite as it offers a cleaner and a smaller codebase and offers great predictability of code! + +## Let’s take the plunge! + +Now, you finally know why you need to learn this programming paradigm so let’s take a deep plunge into the realm of functional programming! + +plunge gif +

+ +## Functional programming is based on Lambda Calculus (λ) + +[Lambda calculus](https://en.wikipedia.org/wiki/Lambda_calculus) is the mathematical foundation on which the functional programming paradigm is built. We don’t need to understand its mathematical implication although. + +Lambda Symbol Image + +The reason I am telling you about it is a lot of time functional programming is accompanied by the Lambda Symbol (λ) and this Lamda basically represents the fact that it is based on lambda calculus (see, now you know it!). + +## What makes a language a Functional Programming language? + +Although there can be a lot of specifications on what makes a language functional, for simplicity’s sake considered any programming language which has [higher-order functions](https://medium.com/javascript-scene/higher-order-functions-composing-software-5365cf2cbe99) **is a functional programming language.** + +## What are higher order functions? + +Higher-order functions are functions that can either or both take a _function as a parameter ( i.e argument ) or can return a function._ + +Higher order function Image + +Higher-order functions exist in JavaScript which makes it eligible to qualify as a functional programming language. + +## Functional programming does not allow loops! + +I know it is hard to believe, but when you are writing your code functional programming paradigm then you don't use loops! + +What do I do then GIF +

+ +### So, what do I use instead of loops then? + + We use **Higher-order functions and recursion** to mimic loops. Loops are ugly and make code hard to understand. So, we use Higher-order functions and recursion for the job, which makes our code a lot more cleaner! + + programming without loops image + + We will be using a Javascript library ( will discuss it in a later section of this workshop ) to assist us in writing functional programming code and mimic this behavior! + + Let me give you an example of what it will look like, these topics are discussed in details in the latter part of this series, this example will be more than enough to show you how clean functional programming can make your code look. + +The below is a program to print squares of elements of an Array, it is implemented via loops. + + ```js +/* Program to print square of number via loops */ + +const numbers = [1, 2, 3, 4, 5] +const squares = [] // will store the squares of the numbers of array numbers +let i; + +for (i = 0; i < numbers.length; i++) { + squares[i] = numbers[i]*2 +} + +for (i = 0; i < squares.length; i++) { + console.log("The square of",numbers[i],"is =",squares[i]) +} + + ``` +The below is a program to print squares of elements of an Array, it is implemented via functional programming. + + ```js +/* Program to print square of number via functional programming */ + +const numbers = [1, 2, 3, 4, 5] + +const squares = numbers.map(ele => ele * 2) // stores the squares of the numbers of array numbers + +squares.forEach((square, i) => + console.log("The square of", numbers[i], "is =", square)) + +``` +### What is map and foreach doing here? + +You don’t need to worry about them, **they are Higher-order functions to mimic loops.** We will be discussing the programming part from the next part of the series. There they will be made more clear. + +## Functions are Pure!!! + +In simple words, they only depend on the arguments passed to them and always produce the same output for a give a given input! Meaning you should not use any variable value apart from your function’s input to calculate its output. + +Pure functions image + +In simple words, they only depend on the arguments passed to them and always produce the same output for a given input! Meaning you should not use any variable value apart from your function’s input to calculate its output. + +## The data is immutable! + +In functional programming we create values, which we then provide as inputs to our functions for processing, Now if these functions want to do some transformation to the object’s state, then they create a new object and initialize it with the transformed value. + +Immutablity Image + +This value will be returned by the function without changing the original input object’s state. This gives birth to immutability! ( In the next parts of this series we will elaborate on it via some code examples! ) + +## No side-effects allowed (: + +Changing state outside of a function is referred to as a side effect. + +no side-effects image + + +This means, that a function cannot change any state outside of the function. This makes sure that our code is free from the issues that regularly occur in programs due to functions having side effects. + + +## What is Ramda? + +[Ramda](https://ramdajs.com/) is a functional programming library for Javascript that we will use throughout this series to implement functional programming in Javascript. + +Ramda replacement + +## Introduction ends here! + +Yes, this was your introduction to the most atomic concepts of functional programming. We are ending this part here so that we don’t throw up a huge chunk of knowledge to you that you can’t digest. We want to go slow and steady so that the knowledge we provide is sustainable for a longer duration. + +you did it + +The next part of this series will start with the programming part, there you will learn many new concepts ( and also the already discussed one ), and will learn how to implement them in Javascript. + +## Move to the next workshop! + +Now, the introduction is complete and I would strongly suggest moving to the next part of this series. If, it is not available yet then wait for a day or two and it will be there. Thanks for taking the first step towards becoming a functional style programmer! + +here I come GIF \ No newline at end of file diff --git a/workshops/functional_programming_2/README.md b/workshops/functional_programming_2/README.md new file mode 100644 index 000000000..24f8d74b6 --- /dev/null +++ b/workshops/functional_programming_2/README.md @@ -0,0 +1,270 @@ +--- +name: 'Functional Programming with js ( part-2 )' +description: 'Understand Currying in Functional Programming with Ramda!' +author: '@bajpai244' +--- + +Welcome to the workshop. This is part-2 of the functional programming with js series. + +In this part, we will learn about Currying in Functional Programming. If you haven’t gone through [part-1](../functional_programming_1) then I strongly recommend checking it out first. + +Functional Programming Intro + +This workshop should take around **_25 minutes_** to complete. + +## Prerequisites + +The workshop is for anyone familiar with: + +- Javascript +- How to run a Node.js program +- General programming concepts + +jsimage + +You don't need to be a Guru in these topics, a basic understanding of them is more than enough! + +## Workspace + +### Fork It :- + +I will be using [repl.it](https://repl.it/) as my workspace for this workshop, You can fork my repl from [here](https://repl.it/@HARSHBAJPAI1/currying#index.js). + +### Nah! would do it myself (: + +Or you can setup your own repl, make sure you create a node.js repl, and install the ramda package ( not rambda, because it too exists! ) + +### Search Ramda + +search for ramda in repl, make sure you don't search rambda ( because it too exists! ). + +replt help-1 image +

+ +### Install It + +Now, install it! Click the Run button to run your program. + +replt help-2 image + +If you prefer to work offline, then install ramda via + +```vim +npm install ramda +``` + + +## Hello Ramda! + +So, we have already discussed Ramda in our first part. We will use its pre-built functions to implement functional programming. No one wants to re-invent the wheel ( although some people might! ) and wants to make the car instead. + +Ramda js Image + +The same goes here, we are leveraging the pre-built functions from Ramda to boost our development process! + +## Currying = Yummy-Yummy-Yum-Yum! + +Lol! Don’t focus much on the Yummy-Yummy-Yum-Yum part, if after reading the section you find it funny, then it was written by purpose and if not then It was just a typo! + +funny GIF + +So, what is Currying in Functional Programming, Is it related to cooking? Let me answer that for you! + +Currying is a core part of writing code via the functional paradigm. It helps us transform a function of form f(a,b,c) to f(a)(b)(c). Sounds confusing right? + +Confused GIF + +Let me explain this with a simple example! This example will give you a practical insight into what currying is for. + +Suppose you have a function sum(a,b), it takes two numbers and add them and then returns the result. This is what it will look like: + +```js + +function sum(a,b){ + return a+b +} + +console.log(“The sum of 5 and 2 is”, sum(5+2)) + +// Output:- The sum of 5 and 2 is 7 + +``` +Hmm, So this was simple! Where is the real challenge? Okay! So, what if I tell you to create three functions which would increase the value of a number by 1,2, and 3 respectively. This is what you might come up with as a solution: + +```js + +function inc_1(a){ +return a+1; +} + + +function inc_2(a){ +return a+2; +} + + +function inc_3(a){ +return a+2; +} + +console.log(‘1 increase 1 times is’,inc_1(1)) // Output: 1 increase 1 times is 2 +console.log(‘1 increase 2 times is’,inc_2(1)) // Output: 1 increase 1 times is 3 +console.log(‘1 increase 3 times is’,inc_3(1)) // Output: 1 increase 1 times is 4 + +``` + +You see the problem here inc_1,inc_2, and inc_3 all three of them are doing the same thing, i.e all of them are basically the addition of a number with different values, but we already have a function sum(a,b) for that remember. + +hmmmmmmm gif + +So, If somehow we could use this function sum(a,b) to create all three of these functions then how awesome it could be. The problem is that sum(a,b) expects two values as its parameters. + +Okay, so let’s think about how can we overcome this limitation. + +## Maybe, we need this? + +What we need is a way so that we could transform our sum(a,b) function into a function, which when provided a single input a, would return a new function, this new function must already have the earlier passed value to add ( i.e a ) so that it can basically add whatever number we pass to it to a. ( meaning b+a ). + +Hmm, still confusing? Okay, let me simplify! + +cofusing gif + +In short, we need something like this: + +```js + +inc_a = sum(a) // here inc_a is a new function + +inc_a(b) // adds b to a , i.e b+a + +``` + + +But, the question is how do we get a function like this? This is where currying comes into play! + + +## Here comes the savior! + +Okay, so we have a function **curry** in Ramda, which lets us do exactly this. Okay, just be with me here and go through the below example, after that things will be more clear, which finally will help us to understand the concept of currying! ( I don’t want to rush to the theoretical explanation without going through a practical scenario ). + +superman on batman gif + +The same inc_1,inc_2, and inc_3 can be obtained via this way in functional programming: + +```js + +const _ = require('ramda') // requiring ramda as _ + +function sum(a,b){ + return a+b; +} + +const curried_sum = _.curry(sum) // currying function sum(a,b) + +const inc_1 = curried_sum(1) +const inc_2 = curried_sum(2) +const inc_3 = curried_sum(3) + +console.log('1 increase 1 times is',inc_1(1)) // Output: 1 increase 1 times is 2 +console.log('1 increase 2 times is',inc_2(1)) // Output: 1 increase 1 times is 3 +console.log('1 increase 3 times is',inc_3(1)) // Output: 1 increase 1 times is 4 + +``` +## So, what just happened? + +Okay, so what happened is that when we passed sum to _.curry as a parameter in the function call, it created a new function, which we stored in curried_sum ( See, Ramda respected immutability here ). + +spidey image + +Now, this curried_ sum is a function which does the same thing our function sum(a,b) used to do, i.e it also adds two numbers, but it can take one argument at a time! + +Meaning, when we call curried_sum(1), then it gives us another function in which the value of **a** has-been replaced/inlined by 1 ( This is an abstract representation of the process ). + +So, this new function, inc_1, when called with an argument b, i.e inc_1(b) would give us the addition of the b with 1. This is what happens when you call inc_1(b). + + +## Another Way (: + +Okay, so because of this magical thing called Currying, we get something else really interesting. Let me stop the talk and take you through the walk. ( I know this talk-walk sounds lame (: but it rhymed!!!!! ) + +```js + +// Run the program in repl + +const inc_5 = curried_sum(5) + +console.log( "Hey! Siri is it true that inc_5(5), curried_sum(5,5) and curried_sum(5)(5) gives the same result? " ) + +console.log("Siri: I know it might baffle you but you need face it, Yes it is", inc_5(5) === curried_sum(5,5) && inc_5(5) === curried_sum(5)(5) ) + +``` + +### How is curried_sum(5,5) working? + +Currying transforms a function to accept one argument at a time, It doesn’t mean that it can accept only one argument at a time, it just means that it will even work if you provide it only one argument. + +spidey image + +Therefore, when we call curried_sum(5,5) then we get the result 10, because when all the arguments are provided to a curried function at once then it evaluates the function in the same way as if it never was curried, this is why it yeilds a result equal to inc\_(5). + + +### How, is curried_sum(5)(5) even working? + +This is because the curried_sum(5) returns a function and by doing curried_sum(5)(5) we are immediately invoking it. Hence it becomes similar to calling inc_5(5) but in this case, we are immediately invoking the function, therefore we don’t need a variable to store it! + +brilliant GIF + +These types of functions are pretty common and are known as [Immediately Invoked Function Expression (IIFE)!](https://developer.mozilla.org/en-US/docs/Glossary/IIFE) + +## f(a,b,c) -> f(a)(b)(c) + +I think now you know what I meant when I said currying transforms a function f(a,b,c) to f(a)(b)(c). + +I know gif + + When we curried function sum(a,b) then we transformed it to accept one argument at a time ( although it can take all of them at the same time too). Immediately Invoking the functions returned from curried functions made the sum(a)(b) pattern possible! + +Here are some other patterns for you to play with! + +```js + +const _ = require (‘ramda’) + +function sum(a,b,c,d){ + return (a+b+c+d) +} + +const curried_sum = _.curry(sum) + +// different patterns + +console.log(curried_sum(1)(2)(3)(4)) +console.log(curried_sum(1,2)(3,4)) +console.log(curried_sum(1,2,3)(4)) +console.log(curried_sum(1)(2,3,4)) + +// you can try some other patterns too + +``` + + ## Why should we use currying? + +Currying makes it really easy to re-use existing functions in your code-base to create new functions. It advocates re-using existing functions to create new ones. + +This makes your programming experience really delightful and makes code easy to manage! + +For example, if inc_1 is not working fine, then you have to debug the inc_1 function itself (**if you are not using Currying**), as the number of functions would increase ( like inc_2, inc_3 ) then the number of function that could cause error will also increase ( because all of them are independent functions ). This will make your job really hard as a programmer. + +But, if you have used Currying then you just need to debug the sum(a,b) function ( i.e only one function ) in case of an error. It makes your code reliable and buys you more time! + +smart meme image + +## Thanks! + +This was my small introduction to Currying for you, If you want to plunge deep into it and want to know how the wheel itself is made, then go to a magical place known as the internet (shhh! It’s a secret), there lies your answer! + +Thanks, for taking out the time to go through this workshop of mine, It makes me really happy when I can teach people something new and interesting. Thanks once again! + +Thanks Image + +If you have any doubts or queries regarding this workshop then reach out to me on Hack Club's Slack, My username is Harsh Bajpai! diff --git a/workshops/functional_programming_2/test.js b/workshops/functional_programming_2/test.js new file mode 100644 index 000000000..0edda82b9 --- /dev/null +++ b/workshops/functional_programming_2/test.js @@ -0,0 +1,8 @@ +/***Program to print square of number via functional programming */ + +const numbers = [1, 2, 3, 4, 5] + +const squares = numbers.map(ele => ele * 2) // stores stores the squares of the numbers of array numbers + +squares.forEach((square, i) => + console.log("The square of", numbers[i], "is =", square)) \ No newline at end of file diff --git a/workshops/functional_programming_3/README.md b/workshops/functional_programming_3/README.md new file mode 100644 index 000000000..01c7d479c --- /dev/null +++ b/workshops/functional_programming_3/README.md @@ -0,0 +1,300 @@ +--- +name: 'Functional Programming with js ( part-3 )' +description: 'Learn how to deal with Arrays in Functional Programming with Ramda!' +author: '@bajpai244' +--- + +Welcome to the workshop. This is part-3 of the functional programming with js series. + +In this part, we will learn how to work with Arrays in Functional Programming. If you haven’t gone through [part-2](../functional_programming_2) then I strongly recommend checking it out first. + +Functional Programming Intro + +This workshop should take around **_30 minutes_** to complete. + +## Prerequisites + +The workshop is for anyone familiar with: + +- Javascript +- How to run a Node.js program +- General programming concepts + +jsimage + +You don't need to be a Guru in these topics, a basic understanding of them is more than enough! + +## Workspace + +### Fork It :- + +I will be using [repl.it](https://repl.it/) as my workspace for this workshop, You can fork my repl from [here](https://repl.it/@HARSHBAJPAI1/Arrays-with-Ramda#index.js). + +### Nah! would do it myself (: + +Or you can setup your own repl, make sure you create a node.js repl, and install the ramda package ( not rambda, because it too exists! ) + +### Search Ramda + +search for ramda in repl, make sure you don't search rambda ( because it too exists! ). + +replt help-1 image +

+ +### Install It + +Now, install it! Click the Run button to run your program. + +replt help-2 image + +If you prefer to work offline, then install ramda via + +```vim +npm install ramda +``` + + +## Hello Ramda! + +So, we have already discussed Ramda in our [first part](../functional_programming_1). We will use its pre-built functions to implement functional programming. No one wants to re-invent the wheel ( although some people might! ) and wants to make the car instead. + +Ramda js Image + +The same goes here, we are leveraging the pre-built functions from Ramda to boost our development process! + +## Arrays 👀 + +Okay, so Arrays are one of the most used data structures in programming. Now, let’s first start by thinking about what are the operations that are associated with Arrays. + +Arrays Everywhere meme Image + +To be honest, there are too many! right? So let’s list some basic operations that we do with Arrays: + +- We loop through them. +- We reduce an Array to a single value. +- We filter out some values from Arrays. +- We remove duplicate entries from the Array. +- We do certain transformation on every item of an Array. + +There are many others. But, for the sake of simplicity, I am showing you some which are regularly used. + +My goal here is to teach you the basic approach towards Array in functional programming, so that you can figure out the rest on your own, So, let’s get started. + +## map 🗺️ + +Okay, sometimes we need to do some calculations on each item of an Array and need the Array with updated items after transformation, for example, An Array containing square of every number in an Array. + +We generally use loops for doing it. But, remember in functional programming we don’t use loops. + +did you forget meme + +We have a function _map_ in Ramda, it’s first parameter is a **function that receives each item of the array which is passed as the second parameter to the map function.** + +Map returns a new Array ( because in functional programming data is immutable ), the value of each item in the returned Array is **the value returned by the function in the map parameter for the corresponding index item in the original Array (passed as second Argument to map).** + +confusing meme + +I know it sounds confusing! Let's take a practical example to understand it better. + +Let’s create a program to create a new array containing the square values of a given Array. + +```js + +const _ = require('ramda') + +const numbers = [1,2,3,4,5] + +const ret_sqr = (num) => num*num // return square of a given number + +const square_list_generator = _.map(ret_sqr) // _.map takes the ret_sqr function as first parameter, here the concept of Currying is being used, go to part 2 to understand currying + +const squares = square_list_generator(numbers) + +// square_list_generator will loop through numbers array and provide each item to ret_sqr function +// the value returned from ret_sqr will be the value of the new Array returned by map for the given index +// so when 1 is passed to numbers ret_sqr by map, the value of new array returned by map at index 0 will be 1*1 i.e 1 +// similarly when 2 of numbers will be passed the value of new array returned by map at index 1 will be 2*2 i.e 4 + +console.log("The square array is",squares) +// Output :- The square array is [ 1, 4, 9, 16, 25 ] + +``` + +So, now let me just summarize what happened in the above case: + +- _.map(function, array) takes a function that will iterate over each value of the array provided as it’s second argument. +- Because we are using Ramda, **therefore _.map() is curried functions** ( in-built functions of Ramda are already Curried ) +- Therefore we can provide _.map one argument at a time. +- square_list_generator stores the function returned from \_.map(ret_sqr). {because of Currying} +- square_list_generator when provided data will now return a new Array containing the square of each number ( New Array because data is immutable in functional programming ). +- This new Array is obtained by calling **ret_sqr** on each data item of numbers ( Array ) and storing the value returned from it ( which is square of a number ) for the given index in the new Array. + + +Below is a graphical representation of what is happening in the above case + +map working image +

+ +## filter + +It does what its name says, It uses a function to filter out items from an Array. The function is in the following format: + +```js + + filter( func, arr ) // returns a new filtered Array + +``` + +Here the function **func** receives each item from the Array **arr**, for the items it returns false are not part of the new filtered Array, **if it returns true for an item then it is part of the new filtered Array.** + +Let’s take an example to demonstrate it: + +```js + +const _ = require('ramda') + +const numbers = [1,2,3,4,5,6,7,8,9] + +const isOdd = (num) => (num%2) != 0 // returns true if an element is odd + +const oddArr = _.filter(isOdd,numbers) + +// we can also pass one argument at a time via Currying! +//I have already demonstrated that in the map example + +console.log("The odd array is", oddArr) + +// output : The odd array is [ 1, 3, 5, 7, 9 ] + +``` + +## forEach + +forEach simply iterates over a given Array and calls function on its each item. + +This is forEach’s format: + +```js + +forEach(func,arr) // func is called for every item of arr + +``` +A good example ( of using it ) would be if you want to print every item of an Array. Here is how you can do that with forEach: + +```js + + +const _ = require('ramda') + +const numbers = [1,2,3,4,5,6,7,8,9] + +const printe = (num) => console.log(num) // print's an item + +_.forEach(printe,numbers) // we can use one argument at a time via Currying (: + +``` +forEach returns the original Array passed to it. + +## reduce + +reduce transforms an Array to a single value. In reduce we pass three things to it: + +1. A function +2. An accumulator +3. The Array + +This is how its format looks like: + +```js + +reduce(func,acc,arr) // func -> function, acc -> accumulator , arr -> array + +``` +## what is this accumulator? + +The accumulator is a value **that is passed to the function along with the Array item.** The value that the function returns is the new value for the accumulator and will be passed back again to the function in the next iteration. + +This cycle keeps on going until unless the iteration is done. +The last value returned by the function is the total accumulated value during the whole iteration **and is the value that our reduce function will return.** + +accumalator gif + +The first argument of the function is the accumulator and the second argument is the Array item. + +```js + +func ( acc, item ) // acc -> accumalator and item -> array item + +``` + +Let me just jump to some code to illustrate the art of reducing! + +```js + +const _ = require('ramda') + +const numbers = [1,2,3,4,5,6,7,8,9] + +const add = (acc,item) => acc + item // adds accumalator and array item + +const sum = _.reduce(add,0,numbers) // add is the function, 0 is accumalator, and numbers is the array + +console.log("Sum of the Array items is",sum) + +``` + +The above program prints the sum of all the items in the **numbers** Array. + +Here each value is passed to the function **add** and the accumulator value is 0 during the 1st iteration, **add** adds the accumulator and the Array item and the value which it returns becomes the new value for the accumulator (for the next iteration). + +This way by the end of the iteration the value it returns comes out to be the sum of all items in the Array **numbers.** + +## Where is the index? + +Sometimes, we need to have the index of the Array item with which we are dealing with. Don’t worry we have **addIndex** for it in Ramda. + +Where GIF + +addIndex will take any of your iteration functions like map,forEach,reduce, etc, and will convert it into a new indexed Array iteration function. + +I know code is the best language to sing the melody of programming ( If it sounds interesting then it's my creativity at its best, if not then just consider it a typo 😂 ) + +Okay, so let’s take an example to demonstrate this idea. + + +```js + +const _ = require('ramda') + +const numbers = [1,2,3,4,5,6,7,8,9] + +const print = (num,index) => console.log("Number is",num," Its index is", index) // print's the item and it's index + +const forEachIndexed = _.addIndex(_.forEach) + +forEachIndexed( print,numbers) // we can use one argument at a time via Currying (: + +``` + +In the above case, we converted forEach to forEachIndexed via _.addIndex function. +The **print** function now receives two arguments 1st is the Array item and the second one is the index of the item. + +## Conclusion! + +Here, we discussed how to use Arrays with Ramda. The benefit of using Arrays with Ramda is that you can easily curry these Array functions and can create some beautiful Reusable functions which in turn will make your codebase modular! + +## Explore! + +Ramda has a universe of functions to deal with Arrays, go to its docs and search for functions labeled list ( you can type list in the search bar ). Try learning some more functions from there because learning must never stop! + +Explore Image + +## Thanks! + +This was my small introduction to Arrays with Ramda for you, If you want to plunge deep into it and want to know how the wheel itself is made, then go to a magical place known as the internet (shhh! It’s a secret), there lies your answer! + +Thanks, for taking out the time to go through this workshop of mine, It makes me really happy when I can teach people something new and interesting. Thanks once again! + +Thanks Image + +If you have any doubts or queries regarding this workshop then reach out to me on Hack Club's Slack, My username is Harsh Bajpai! \ No newline at end of file diff --git a/workshops/functional_programming_3/test.js b/workshops/functional_programming_3/test.js new file mode 100644 index 000000000..0edda82b9 --- /dev/null +++ b/workshops/functional_programming_3/test.js @@ -0,0 +1,8 @@ +/***Program to print square of number via functional programming */ + +const numbers = [1, 2, 3, 4, 5] + +const squares = numbers.map(ele => ele * 2) // stores stores the squares of the numbers of array numbers + +squares.forEach((square, i) => + console.log("The square of", numbers[i], "is =", square)) \ No newline at end of file From 398d5f29bb3cd03f4f7b33e2817daacdd33427a6 Mon Sep 17 00:00:00 2001 From: Matthew Stanciu Date: Wed, 9 Dec 2020 10:52:07 -0500 Subject: [PATCH 36/51] update wording on functional programming --- workshops/functional_programming_1/README.md | 38 +++++------ workshops/functional_programming_2/README.md | 66 ++++++++++---------- workshops/functional_programming_3/README.md | 40 ++++++------ 3 files changed, 72 insertions(+), 72 deletions(-) diff --git a/workshops/functional_programming_1/README.md b/workshops/functional_programming_1/README.md index bc4a0f451..383d74d93 100644 --- a/workshops/functional_programming_1/README.md +++ b/workshops/functional_programming_1/README.md @@ -1,10 +1,10 @@ --- -name: 'Functional Programming with js ( part-1 )' -description: 'Learn the basics of functional programming with js!' +name: 'Functional Programming with JS (Part 1)' +description: 'Learn the basics of functional programming with JS!' author: '@bajpai244' --- -Welcome to the workshop. This is part-1 of the functional programming with js series, this workshop will discuss what is functional programming, why we need it?, and the basics of functional programming. +Welcome to the workshop. This is part 1 of the functional programming with js series, this workshop will discuss what is functional programming, why we need it?, and the basics of functional programming. This workshop will serve as an introduction to the upcoming parts of this series. @@ -33,7 +33,7 @@ Do you wonder what it is? let me provide a simple and clear definition of functi Functional programming is a way of writing your programs ( a paradigm ) so that your code is: -- Clean +- Clean - Readable - Less Error-Prone - Easy to Debug @@ -44,21 +44,21 @@ To start with something, we deep down need a reason to get ourselves going. So, tell me why gif -We all write code and often find ourselves in messy situations. Like, we don’t understand what the flow of the program is, how is it even working, debugging almost kills most of our time. +We all write code and often find ourselves in messy situations. Like, we don’t understand what the flow of the program is, how is it even working, debugging almost kills most of our time. The reason why these things happen is that we lack a paradigm for our programming style. A paradigm brings order to our programming, making our programming style more predictable, easy to debug, and easy to share. I get if GIF -There are many programming paradigms like Imperative programming, Object-Oriented Programming, Functional Programming, etc. +There are many programming paradigms like Imperative programming, Object-Oriented Programming, Functional Programming, etc. different types of programming languages image -People advocate their love for different programming paradigm and so do I. Functional Programming is my favorite as it offers a cleaner and a smaller codebase and offers great predictability of code! +People advocate their love for different programming paradigm and so do I. Functional Programming is my favorite as it offers a cleaner and a smaller codebase and offers great predictability of code! ## Let’s take the plunge! -Now, you finally know why you need to learn this programming paradigm so let’s take a deep plunge into the realm of functional programming! +Now, you finally know why you need to learn this programming paradigm so let’s take a deep plunge into the realm of functional programming! plunge gif

@@ -77,7 +77,7 @@ Although there can be a lot of specifications on what makes a language functiona ## What are higher order functions? -Higher-order functions are functions that can either or both take a _function as a parameter ( i.e argument ) or can return a function._ +Higher-order functions are functions that can either or both take a _function as a parameter ( i.e argument ) or can return a function._ Higher order function Image @@ -102,29 +102,29 @@ I know it is hard to believe, but when you are writing your code functional pro The below is a program to print squares of elements of an Array, it is implemented via loops. - ```js + ```js /* Program to print square of number via loops */ const numbers = [1, 2, 3, 4, 5] const squares = [] // will store the squares of the numbers of array numbers let i; -for (i = 0; i < numbers.length; i++) { +for (i = 0; i < numbers.length; i++) { squares[i] = numbers[i]*2 } -for (i = 0; i < squares.length; i++) { +for (i = 0; i < squares.length; i++) { console.log("The square of",numbers[i],"is =",squares[i]) } ``` The below is a program to print squares of elements of an Array, it is implemented via functional programming. - ```js + ```js /* Program to print square of number via functional programming */ const numbers = [1, 2, 3, 4, 5] - + const squares = numbers.map(ele => ele * 2) // stores the squares of the numbers of array numbers squares.forEach((square, i) => @@ -137,11 +137,11 @@ You don’t need to worry about them, **they are Higher-order functions to mimic ## Functions are Pure!!! -In simple words, they only depend on the arguments passed to them and always produce the same output for a give a given input! Meaning you should not use any variable value apart from your function’s input to calculate its output. +In simple words, they only depend on the arguments passed to them and always produce the same output for a give a given input! Meaning you should not use any variable value apart from your function’s input to calculate its output. Pure functions image -In simple words, they only depend on the arguments passed to them and always produce the same output for a given input! Meaning you should not use any variable value apart from your function’s input to calculate its output. +In simple words, they only depend on the arguments passed to them and always produce the same output for a given input! Meaning you should not use any variable value apart from your function’s input to calculate its output. ## The data is immutable! @@ -153,12 +153,12 @@ This value will be returned by the function without changing the original input ## No side-effects allowed (: -Changing state outside of a function is referred to as a side effect. +Changing state outside of a function is referred to as a side effect. no side-effects image -This means, that a function cannot change any state outside of the function. This makes sure that our code is free from the issues that regularly occur in programs due to functions having side effects. +This means, that a function cannot change any state outside of the function. This makes sure that our code is free from the issues that regularly occur in programs due to functions having side effects. ## What is Ramda? @@ -179,4 +179,4 @@ The next part of this series will start with the programming part, there you wil Now, the introduction is complete and I would strongly suggest moving to the next part of this series. If, it is not available yet then wait for a day or two and it will be there. Thanks for taking the first step towards becoming a functional style programmer! -here I come GIF \ No newline at end of file +here I come GIF diff --git a/workshops/functional_programming_2/README.md b/workshops/functional_programming_2/README.md index 24f8d74b6..d73f0e97b 100644 --- a/workshops/functional_programming_2/README.md +++ b/workshops/functional_programming_2/README.md @@ -1,10 +1,10 @@ --- -name: 'Functional Programming with js ( part-2 )' +name: 'Functional Programming with JS (Part 2)' description: 'Understand Currying in Functional Programming with Ramda!' author: '@bajpai244' --- -Welcome to the workshop. This is part-2 of the functional programming with js series. +Welcome to the workshop. This is part 2 of the functional programming with JavaScript series. In this part, we will learn about Currying in Functional Programming. If you haven’t gone through [part-1](../functional_programming_1) then I strongly recommend checking it out first. @@ -28,11 +28,11 @@ You don't need to be a Guru in these topics, a basic understanding of them is mo ### Fork It :- -I will be using [repl.it](https://repl.it/) as my workspace for this workshop, You can fork my repl from [here](https://repl.it/@HARSHBAJPAI1/currying#index.js). +I will be using [repl.it](https://repl.it/) as my workspace for this workshop, You can fork my repl from [here](https://repl.it/@HARSHBAJPAI1/currying#index.js). ### Nah! would do it myself (: -Or you can setup your own repl, make sure you create a node.js repl, and install the ramda package ( not rambda, because it too exists! ) +Or you can setup your own repl, make sure you create a node.js repl, and install the ramda package ( not rambda, because it too exists! ) ### Search Ramda @@ -76,22 +76,22 @@ Currying is a core part of writing code via the functional paradigm. It helps us Let me explain this with a simple example! This example will give you a practical insight into what currying is for. -Suppose you have a function sum(a,b), it takes two numbers and add them and then returns the result. This is what it will look like: +Suppose you have a function sum(a,b), it takes two numbers and add them and then returns the result. This is what it will look like: ```js - + function sum(a,b){ - return a+b + return a+b } console.log(“The sum of 5 and 2 is”, sum(5+2)) -// Output:- The sum of 5 and 2 is 7 +// Output:- The sum of 5 and 2 is 7 ``` Hmm, So this was simple! Where is the real challenge? Okay! So, what if I tell you to create three functions which would increase the value of a number by 1,2, and 3 respectively. This is what you might come up with as a solution: -```js +```js function inc_1(a){ return a+1; @@ -123,7 +123,7 @@ Okay, so let’s think about how can we overcome this limitation. ## Maybe, we need this? -What we need is a way so that we could transform our sum(a,b) function into a function, which when provided a single input a, would return a new function, this new function must already have the earlier passed value to add ( i.e a ) so that it can basically add whatever number we pass to it to a. ( meaning b+a ). +What we need is a way so that we could transform our sum(a,b) function into a function, which when provided a single input a, would return a new function, this new function must already have the earlier passed value to add ( i.e a ) so that it can basically add whatever number we pass to it to a. ( meaning b+a ). Hmm, still confusing? Okay, let me simplify! @@ -154,21 +154,21 @@ The same inc_1,inc_2, and inc_3 can be obtained via this way in functional progr ```js const _ = require('ramda') // requiring ramda as _ - + function sum(a,b){ return a+b; } - + const curried_sum = _.curry(sum) // currying function sum(a,b) - + const inc_1 = curried_sum(1) const inc_2 = curried_sum(2) const inc_3 = curried_sum(3) - + console.log('1 increase 1 times is',inc_1(1)) // Output: 1 increase 1 times is 2 console.log('1 increase 2 times is',inc_2(1)) // Output: 1 increase 1 times is 3 console.log('1 increase 3 times is',inc_3(1)) // Output: 1 increase 1 times is 4 - + ``` ## So, what just happened? @@ -192,11 +192,11 @@ Okay, so because of this magical thing called Currying, we get something else re // Run the program in repl const inc_5 = curried_sum(5) - + console.log( "Hey! Siri is it true that inc_5(5), curried_sum(5,5) and curried_sum(5)(5) gives the same result? " ) - + console.log("Siri: I know it might baffle you but you need face it, Yes it is", inc_5(5) === curried_sum(5,5) && inc_5(5) === curried_sum(5)(5) ) - + ``` ### How is curried_sum(5,5) working? @@ -219,41 +219,41 @@ These types of functions are pretty common and are known as [Immediately Invoked ## f(a,b,c) -> f(a)(b)(c) I think now you know what I meant when I said currying transforms a function f(a,b,c) to f(a)(b)(c). - -I know gif + +I know gif When we curried function sum(a,b) then we transformed it to accept one argument at a time ( although it can take all of them at the same time too). Immediately Invoking the functions returned from curried functions made the sum(a)(b) pattern possible! - + Here are some other patterns for you to play with! - -```js - + +```js + const _ = require (‘ramda’) - + function sum(a,b,c,d){ return (a+b+c+d) } - + const curried_sum = _.curry(sum) - + // different patterns - -console.log(curried_sum(1)(2)(3)(4)) + +console.log(curried_sum(1)(2)(3)(4)) console.log(curried_sum(1,2)(3,4)) console.log(curried_sum(1,2,3)(4)) console.log(curried_sum(1)(2,3,4)) - + // you can try some other patterns too - + ``` ## Why should we use currying? -Currying makes it really easy to re-use existing functions in your code-base to create new functions. It advocates re-using existing functions to create new ones. +Currying makes it really easy to re-use existing functions in your code-base to create new functions. It advocates re-using existing functions to create new ones. This makes your programming experience really delightful and makes code easy to manage! -For example, if inc_1 is not working fine, then you have to debug the inc_1 function itself (**if you are not using Currying**), as the number of functions would increase ( like inc_2, inc_3 ) then the number of function that could cause error will also increase ( because all of them are independent functions ). This will make your job really hard as a programmer. +For example, if inc_1 is not working fine, then you have to debug the inc_1 function itself (**if you are not using Currying**), as the number of functions would increase ( like inc_2, inc_3 ) then the number of function that could cause error will also increase ( because all of them are independent functions ). This will make your job really hard as a programmer. But, if you have used Currying then you just need to debug the sum(a,b) function ( i.e only one function ) in case of an error. It makes your code reliable and buys you more time! diff --git a/workshops/functional_programming_3/README.md b/workshops/functional_programming_3/README.md index 01c7d479c..9a6f17417 100644 --- a/workshops/functional_programming_3/README.md +++ b/workshops/functional_programming_3/README.md @@ -1,6 +1,6 @@ --- -name: 'Functional Programming with js ( part-3 )' -description: 'Learn how to deal with Arrays in Functional Programming with Ramda!' +name: 'Functional Programming with JS (Part 3)' +description: 'Learn how to deal with arrays in Functional Programming with Ramda!' author: '@bajpai244' --- @@ -28,11 +28,11 @@ You don't need to be a Guru in these topics, a basic understanding of them is mo ### Fork It :- -I will be using [repl.it](https://repl.it/) as my workspace for this workshop, You can fork my repl from [here](https://repl.it/@HARSHBAJPAI1/Arrays-with-Ramda#index.js). +I will be using [repl.it](https://repl.it/) as my workspace for this workshop, You can fork my repl from [here](https://repl.it/@HARSHBAJPAI1/Arrays-with-Ramda#index.js). ### Nah! would do it myself (: -Or you can setup your own repl, make sure you create a node.js repl, and install the ramda package ( not rambda, because it too exists! ) +Or you can setup your own repl, make sure you create a node.js repl, and install the ramda package ( not rambda, because it too exists! ) ### Search Ramda @@ -64,7 +64,7 @@ The same goes here, we are leveraging the pre-built functions from Ramda to boos ## Arrays 👀 -Okay, so Arrays are one of the most used data structures in programming. Now, let’s first start by thinking about what are the operations that are associated with Arrays. +Okay, so Arrays are one of the most used data structures in programming. Now, let’s first start by thinking about what are the operations that are associated with Arrays. Arrays Everywhere meme Image @@ -76,13 +76,13 @@ To be honest, there are too many! right? So let’s list some basic operations t - We remove duplicate entries from the Array. - We do certain transformation on every item of an Array. -There are many others. But, for the sake of simplicity, I am showing you some which are regularly used. +There are many others. But, for the sake of simplicity, I am showing you some which are regularly used. My goal here is to teach you the basic approach towards Array in functional programming, so that you can figure out the rest on your own, So, let’s get started. ## map 🗺️ -Okay, sometimes we need to do some calculations on each item of an Array and need the Array with updated items after transformation, for example, An Array containing square of every number in an Array. +Okay, sometimes we need to do some calculations on each item of an Array and need the Array with updated items after transformation, for example, An Array containing square of every number in an Array. We generally use loops for doing it. But, remember in functional programming we don’t use loops. @@ -90,7 +90,7 @@ We generally use loops for doing it. But, remember in functional programming we We have a function _map_ in Ramda, it’s first parameter is a **function that receives each item of the array which is passed as the second parameter to the map function.** -Map returns a new Array ( because in functional programming data is immutable ), the value of each item in the returned Array is **the value returned by the function in the map parameter for the corresponding index item in the original Array (passed as second Argument to map).** +Map returns a new Array ( because in functional programming data is immutable ), the value of each item in the returned Array is **the value returned by the function in the map parameter for the corresponding index item in the original Array (passed as second Argument to map).** confusing meme @@ -108,7 +108,7 @@ const ret_sqr = (num) => num*num // return square of a given number const square_list_generator = _.map(ret_sqr) // _.map takes the ret_sqr function as first parameter, here the concept of Currying is being used, go to part 2 to understand currying -const squares = square_list_generator(numbers) +const squares = square_list_generator(numbers) // square_list_generator will loop through numbers array and provide each item to ret_sqr function // the value returned from ret_sqr will be the value of the new Array returned by map for the given index @@ -135,17 +135,17 @@ Below is a graphical representation of what is happening in the above case map working image

-## filter +## filter It does what its name says, It uses a function to filter out items from an Array. The function is in the following format: ```js - filter( func, arr ) // returns a new filtered Array + filter( func, arr ) // returns a new filtered Array ``` -Here the function **func** receives each item from the Array **arr**, for the items it returns false are not part of the new filtered Array, **if it returns true for an item then it is part of the new filtered Array.** +Here the function **func** receives each item from the Array **arr**, for the items it returns false are not part of the new filtered Array, **if it returns true for an item then it is part of the new filtered Array.** Let’s take an example to demonstrate it: @@ -159,10 +159,10 @@ const isOdd = (num) => (num%2) != 0 // returns true if an element is odd const oddArr = _.filter(isOdd,numbers) -// we can also pass one argument at a time via Currying! +// we can also pass one argument at a time via Currying! //I have already demonstrated that in the map example -console.log("The odd array is", oddArr) +console.log("The odd array is", oddArr) // output : The odd array is [ 1, 3, 5, 7, 9 ] @@ -176,7 +176,7 @@ This is forEach’s format: ```js -forEach(func,arr) // func is called for every item of arr +forEach(func,arr) // func is called for every item of arr ``` A good example ( of using it ) would be if you want to print every item of an Array. Here is how you can do that with forEach: @@ -214,7 +214,7 @@ reduce(func,acc,arr) // func -> function, acc -> accumulator , arr -> array The accumulator is a value **that is passed to the function along with the Array item.** The value that the function returns is the new value for the accumulator and will be passed back again to the function in the next iteration. -This cycle keeps on going until unless the iteration is done. +This cycle keeps on going until unless the iteration is done. The last value returned by the function is the total accumulated value during the whole iteration **and is the value that our reduce function will return.** accumalator gif @@ -247,7 +247,7 @@ The above program prints the sum of all the items in the **numbers** Array. Here each value is passed to the function **add** and the accumulator value is 0 during the 1st iteration, **add** adds the accumulator and the Array item and the value which it returns becomes the new value for the accumulator (for the next iteration). -This way by the end of the iteration the value it returns comes out to be the sum of all items in the Array **numbers.** +This way by the end of the iteration the value it returns comes out to be the sum of all items in the Array **numbers.** ## Where is the index? @@ -262,7 +262,7 @@ I know code is the best language to sing the melody of programming ( If it sound Okay, so let’s take an example to demonstrate this idea. -```js +```js const _ = require('ramda') @@ -281,7 +281,7 @@ The **print** function now receives two arguments 1st is the Array item and the ## Conclusion! -Here, we discussed how to use Arrays with Ramda. The benefit of using Arrays with Ramda is that you can easily curry these Array functions and can create some beautiful Reusable functions which in turn will make your codebase modular! +Here, we discussed how to use Arrays with Ramda. The benefit of using Arrays with Ramda is that you can easily curry these Array functions and can create some beautiful Reusable functions which in turn will make your codebase modular! ## Explore! @@ -297,4 +297,4 @@ Thanks, for taking out the time to go through this workshop of mine, It makes me Thanks Image -If you have any doubts or queries regarding this workshop then reach out to me on Hack Club's Slack, My username is Harsh Bajpai! \ No newline at end of file +If you have any doubts or queries regarding this workshop then reach out to me on Hack Club's Slack, My username is Harsh Bajpai! From 9af08b44e52155571d98b0ae6204be41acbd1a89 Mon Sep 17 00:00:00 2001 From: Matthew Stanciu Date: Thu, 10 Dec 2020 10:18:54 -0500 Subject: [PATCH 37/51] fix
on memory game workshop i think --- workshops/memory_game/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/workshops/memory_game/README.md b/workshops/memory_game/README.md index e389b8bc6..5c1512796 100644 --- a/workshops/memory_game/README.md +++ b/workshops/memory_game/README.md @@ -426,8 +426,9 @@ result.textContent = cardsMatched.length ```
+ Our code so far will be: - + ```javascript document.addEventListener('DOMContentLoaded', () => { const cardArray = [....] From 4716cb41b28759e53fd9a570d38f73a37f475fd9 Mon Sep 17 00:00:00 2001 From: Matthew Stanciu Date: Thu, 10 Dec 2020 10:22:00 -0500 Subject: [PATCH 38/51] small code block edit on memory game --- workshops/memory_game/README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/workshops/memory_game/README.md b/workshops/memory_game/README.md index 5c1512796..79bb7320d 100644 --- a/workshops/memory_game/README.md +++ b/workshops/memory_game/README.md @@ -475,10 +475,9 @@ document.addEventListener('DOMContentLoaded', () => { if (cardsMatched.length === cardArray.length/2) { result.textContent = 'Congratulations! You found them all!' } + } -} - -createBoard() + createBoard() }) ``` From 93add17a0a5f3baead118069c3b842e7abbcc08e Mon Sep 17 00:00:00 2001 From: Harsh Bajpai Date: Fri, 11 Dec 2020 21:44:05 +0530 Subject: [PATCH 39/51] added help_orepheus (#1512) --- workshops/help_orpheus/README.md | 210 ++++++++++++++++++++++++++++ workshops/help_orpheus/img/demo.png | Bin 0 -> 180921 bytes 2 files changed, 210 insertions(+) create mode 100644 workshops/help_orpheus/README.md create mode 100644 workshops/help_orpheus/img/demo.png diff --git a/workshops/help_orpheus/README.md b/workshops/help_orpheus/README.md new file mode 100644 index 000000000..cc31019ce --- /dev/null +++ b/workshops/help_orpheus/README.md @@ -0,0 +1,210 @@ +--- +name: Help Orpheus! +description: Supercop Orpheus needs help to save Hack Island! and you are the one who can help them. +author: '@bajpai244' +--- + +Welcome to this new adventure of yours. Supercop Orpheus needs your help saving Hack Island. What?? What is Hack Island? And what is going to happen to it? And Orpheus is also a Supercop! When did all of these happen? + +I know all of these sounds confusing to you. You are going to discover some of the secrets of Hack Club in this journey. We have been hiding them from you a very long time ( very very long time ). + +## Secrets! + +We at Hack Club have been hiding some secrets from you! But the time has come that we finally unveil them for you. Welcome to the club! + +## Hack Island! + +Hack Island is a place which till this far has been hidden from the world! Here we have people with superpowers! Yeah! These people are known for making really cool things! This is their habitat. + +We are better than Wakanda and Hogwarts combined (a bit of promotion here (: ) and use advanced technology to hide us from the world. + +Hack Island image +

+ +## Badlo Escobar 🤡 + +Badlo Escobar is a constant trouble maker for people of Hack Island. He owns the Badlo society and their society has its own social network. Badlo has only one aspiration, to destroy Hack Island’s resources and trouble its people! + +badlo escobar image +

+ +### Why is he doing it? + +Hmm, so very very long ago he submitted a PR for a workshop for the bounty program in which he copied someone else’s workshop ( from Hack Club ) and made the PR before them. + +The case was taken to Supercop Orpheus where the victim proved that they made the workshop first and hence Badlo was deprived of the bounty. This is what made Badlo the person he is today ( shh, keep it a secret ) + +## Supercop Orpheus + +Orpheus is the Supercop of Hack Island. They just came to know that Badlo has deployed a bomb in Hack Island’s central library’s server room. If he gets away with it then all of the sacred knowledge and wisdom of Hack Club will be gone forever. + +This is why they need your help to stop Badlo! + +Supercop Orpheus Image +

+ +## The adventure begins! + +Okay, so before you could help Supercop Orpheus you need some info, right? We have a special person, **Agent Squirrel**, who is an agent of Hack Island. He has some info for us: + +SQUIRREL image + +- Badlo uses https://badlonetwork.vercel.app/ to chat with his associates. +- His username is **badlo escobar** +- The database the website uses is **written in SQL ( more about this coming ahead)**. + + +## Let’s visit the website + +Okay, so our friend Squirrel tells us that Badlo uses [https://badlonetwork.vercel.app/](https://badlonetwork.vercel.app/) to chat with his associates. If we can see his previous chats then it can lead us to something helpful! Let’s visit this website. + +So, after visiting [https://badlonetwork.vercel.app/](https://badlonetwork.vercel.app/) this is what we see. + +badlo network website + +Okay, so it is asking for credentials, we have Badlo's username which is **badlo escobar** but we don't have his password. We need to bypass this security to be able to see his chats. + +Hmm, so remember Squirrel told us the website’s database is written in SQL. Let’s dig into that first! + +## SQL + +The way you have HTML to make webpages in a similar way you have SQL to write databases. Databases store information. + +SQL Logo + +SQL stands for Structured Query Language. Databases written in SQL are **in the form of tables.** + +The below is a sample database storing data in the form of tables. + +SQL table image + +We can retrieve information from this table by using SQL’s SELECT statement. Each row of the table is called a **record**. + +To retrieve all the rows in the table we will write the following command: + +```sql + +SELECT * from TABEL_NAME ; + +``` +Here * means all records and TABLE_NAME is the name of the table whose records you want to have! + +If you want to get some specific records in SQL then you use the WHERE clause. For example if you want a record with username “badlo escoabr” and password “12345”. Then you would write the following SQL. + +```sql + +SELECT * from TABEL_NAME WHERE username = 'badlo escoabar' AND password = '12345' ; + +``` + +Here the record with USERNAME “badlo escobar” and password “12345” will be shown to us ( if exists ). The keyword **AND** tells that both of the STATEMENT needs to be true i.e USERNAME = “badlo escobar” and Password = “12345”. + +This was a small introduction to SQL for you. + + +## SQL Injection + +Agent Squirrel told us that the website uses SQL for its database. The website which uses SQL is prone to a vulnerability which is known as SQL injection. Hmmm? Lemme explain it to you. + +SQL Injection + +So, to retrieve data from their database, websites run SQL commands in their backend and we can inject some SQL into these commands to mutate these statements for getting desirable results! + +This is the technique you will use to bypass the security. The way Orpheus has you for help, you can rely on me for some help too, see how friendly Hack Clubbers are! + + +## More INFO + +Agent Squirrel has some new inputs for us. They say that they just got a sneak peek into badlonetworks’s source code and saw a line like this somewhere : + +```sql + +`SELECT * from people WHERE username = '${username}' AND password = '${password}' ` + +``` + +squirrel image 1 + +The SQL is written between **`** strings. Which are [template string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals) in JS. + +Awesome! so ${username} and ${password} are placeholders for the variable username and password in a js template string. + +**This means that whatever we are typing in the username and password text fields might be being injected directly into the template string containing the SQL statement.** + +So, if in username I type **badlo esocbar** and password I type **password** then the template string will become : + +```sql + +`SELECT * from people WHERE username = 'badlo escobar' and password = 'password'` + +``` + +Okay! so, what **if in password** we type ' OR 1=1 -- ( _after **--** there should be a space_ ) and make username = 'badlo escobar'. + +Then the SQL statement will become: + +```SQL + +SELECT * from people WHERE username = 'badlo escobar' AND password = '' OR 1=1 -- ' + +``` + +When we make our password = ' OR 1=1 -- , then **'** closes the string of our password variable making **it an empty string.** + +In SQL **--** comments down whatever is next to it. So, **--** comments down the leftover **'** string here and we get the above SQL command. + +Now in an SQL command with **OR** condition in it, any one of the conditions must be true. Here 1 = 1 is always true ( because 1=1, is it? ). So the SQL command will give a result back to the system which is not Empty. This would fool a system into thinking that such a user exists. + +So if we set our password = ‘ OR 1=1 -- ( remember after **--** there should be a space ). Then no matter what the username and password are, we will be able to bypass the system. Which makes our hint that the username is **badlo escobar** useless. + +Because as far as the password is equal to what we have discussed then no matter which username you use, you will bypass the security! + +## Hack Time 🐱‍💻 + +Okay! So finally time to perform the Hack. Open the website [https://badlonetwork.vercel.app](https://badlonetwork.vercel.app) and type the following into username and password: + +- username = badlo escobar +- Password = ‘ OR 1=1 -- + +Remember after **--** there should be a space. And then click on the Login Button. + +thehack image + + +## Woooo! + +Yes, you just bypassed the system and what is in front of you is the chat of Badlo with one of his associates. Let’s see what we can find here. + +Badlo's secret chat Image + +Okay, so from the highlighted portion of the chat we know one that: + +- We need to go to https://stopbadlomission.vercel.app/ +- We need to enter **badlo-is-great** as a passcode in the text field and then click on The cancel mission Button. +- Try typing the passcode and not copy-pasting it, as sometimes along with the passcode we might end up copying a space character with it ( which is not visible to us! ). + +passcode entering image + +Yesss! after entering the passcode you are only one click away from completing this mission! Just click the ** CANCEL MISSION ** button and see the magic! + +## Thanks, Saviour! + +Do you know what you just did by clicking that button? You saved Hack Island’s sacred knowledge and wisdom! You are a hero for the people, and now Orephues knows they have someone to rely on if they get into trouble again! + +hacker man image + +Be ready and keep learning. Badlo will not like this, and we need to hedge the Island from him. We need to be ready for his upcoming plans. + +This is just the start of your adventure, be ready for the next one! + +## When you have superpowers then use them for good! + +I know you just learned a new trick. This was just for educational purposes and hacking someone’s system without their consent is a cybercrime. + +This was made so that you can be made aware of the various ways in which people do cyber-attack in the form of a story and how you can hedge against them! + +super power image + +Hack Club doesn’t promote any kind of misuse of the knowledge being provided here and won’t be responsible for your participation in any sort of unlawful activities. So using the workshop for educational purposes only is strictly advised. + +When you have superpowers then use them for good! \ No newline at end of file diff --git a/workshops/help_orpheus/img/demo.png b/workshops/help_orpheus/img/demo.png new file mode 100644 index 0000000000000000000000000000000000000000..622d71af0ec549c15ee76324e56022d1a1614933 GIT binary patch literal 180921 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%0A*Zr9=N7>-k!6Z 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 literal 0 HcmV?d00001 From 740d06477c2bcc9144473eb181d362c3463465c1 Mon Sep 17 00:00:00 2001 From: Harsh Bajpai Date: Fri, 11 Dec 2020 22:09:06 +0530 Subject: [PATCH 40/51] corrected spelling in help-orphues (#1513) * added help_orepheus * correct spelling of table in help orphues --- workshops/help_orpheus/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/workshops/help_orpheus/README.md b/workshops/help_orpheus/README.md index cc31019ce..162692f8d 100644 --- a/workshops/help_orpheus/README.md +++ b/workshops/help_orpheus/README.md @@ -84,7 +84,7 @@ To retrieve all the rows in the table we will write the following command: ```sql -SELECT * from TABEL_NAME ; +SELECT * from TABLE_NAME ; ``` Here * means all records and TABLE_NAME is the name of the table whose records you want to have! @@ -93,7 +93,7 @@ If you want to get some specific records in SQL then you use the WHERE clause. F ```sql -SELECT * from TABEL_NAME WHERE username = 'badlo escoabar' AND password = '12345' ; +SELECT * from TABLE_NAME WHERE username = 'badlo escoabar' AND password = '12345' ; ``` From 8724a55312ea13caf94277bf261c6c106352b7fe Mon Sep 17 00:00:00 2001 From: Hugo Hu <69009840+Hugoyhu@users.noreply.github.com> Date: Fri, 11 Dec 2020 12:01:49 -0500 Subject: [PATCH 41/51] Update README.md (#1514) LOL corrected a spelling error: Orephues -> orpheus --- workshops/help_orpheus/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/workshops/help_orpheus/README.md b/workshops/help_orpheus/README.md index 162692f8d..681def00b 100644 --- a/workshops/help_orpheus/README.md +++ b/workshops/help_orpheus/README.md @@ -189,7 +189,7 @@ Yesss! after entering the passcode you are only one click away from completing t ## Thanks, Saviour! -Do you know what you just did by clicking that button? You saved Hack Island’s sacred knowledge and wisdom! You are a hero for the people, and now Orephues knows they have someone to rely on if they get into trouble again! +Do you know what you just did by clicking that button? You saved Hack Island’s sacred knowledge and wisdom! You are a hero for the people, and now Orpheus knows they have someone to rely on if they get into trouble again! hacker man image @@ -207,4 +207,4 @@ This was made so that you can be made aware of the various ways in which people Hack Club doesn’t promote any kind of misuse of the knowledge being provided here and won’t be responsible for your participation in any sort of unlawful activities. So using the workshop for educational purposes only is strictly advised. -When you have superpowers then use them for good! \ No newline at end of file +When you have superpowers then use them for good! From c24b895c1a82714a7c9827cb5e533db59ff45e7e Mon Sep 17 00:00:00 2001 From: Ishan Goel Date: Tue, 15 Dec 2020 16:37:08 +0400 Subject: [PATCH 42/51] Brevity, formatting and minor grammatical changes to the help_orpheus workshop (#1517) * Brevity, formatting and minor changes to the help_orpheus workshop * Correct typo --- workshops/help_orpheus/README.md | 79 +++++++++++++++----------------- 1 file changed, 38 insertions(+), 41 deletions(-) diff --git a/workshops/help_orpheus/README.md b/workshops/help_orpheus/README.md index 681def00b..fba1a01c3 100644 --- a/workshops/help_orpheus/README.md +++ b/workshops/help_orpheus/README.md @@ -6,37 +6,37 @@ author: '@bajpai244' Welcome to this new adventure of yours. Supercop Orpheus needs your help saving Hack Island. What?? What is Hack Island? And what is going to happen to it? And Orpheus is also a Supercop! When did all of these happen? -I know all of these sounds confusing to you. You are going to discover some of the secrets of Hack Club in this journey. We have been hiding them from you a very long time ( very very long time ). +I know all of these sounds confusing to you. You are going to discover some of the secrets of Hack Club in this journey. We have been hiding them from you for a very long time (very very long time). ## Secrets! -We at Hack Club have been hiding some secrets from you! But the time has come that we finally unveil them for you. Welcome to the club! +We at Hack Club have been hiding some secrets from you! But the time has come that we finally unveil them to you. Welcome to the club! ## Hack Island! Hack Island is a place which till this far has been hidden from the world! Here we have people with superpowers! Yeah! These people are known for making really cool things! This is their habitat. -We are better than Wakanda and Hogwarts combined (a bit of promotion here (: ) and use advanced technology to hide us from the world. +We are better than Wakanda and Hogwarts combined (a bit of a promotion here :) ) and we use advanced technology to hide from the world. Hack Island image

## Badlo Escobar 🤡 -Badlo Escobar is a constant trouble maker for people of Hack Island. He owns the Badlo society and their society has its own social network. Badlo has only one aspiration, to destroy Hack Island’s resources and trouble its people! +Badlo Escobar is a constant trouble maker for the people of Hack Island. He owns the Badlo society and their society has its own social network. Badlo has only one aspiration, to destroy Hack Island’s resources and trouble its people! badlo escobar image

### Why is he doing it? -Hmm, so very very long ago he submitted a PR for a workshop for the bounty program in which he copied someone else’s workshop ( from Hack Club ) and made the PR before them. +Hmm, so very very long ago he submitted a PR for a workshop for the bounty program in which he copied someone else’s workshop (from Hack Club) and made the PR before them. -The case was taken to Supercop Orpheus where the victim proved that they made the workshop first and hence Badlo was deprived of the bounty. This is what made Badlo the person he is today ( shh, keep it a secret ) +The case was taken to Supercop Orpheus where the victim proved that they made the workshop first and hence Badlo was deprived of the bounty. This is what made Badlo the person he is today (shh, keep it a secret) ## Supercop Orpheus -Orpheus is the Supercop of Hack Island. They just came to know that Badlo has deployed a bomb in Hack Island’s central library’s server room. If he gets away with it then all of the sacred knowledge and wisdom of Hack Club will be gone forever. +Orpheus is the Supercop of Hack Island. They just came to know that Badlo has deployed a bomb in Hack Island’s central library’s server room. If he gets away with it, all of the sacred knowledge and wisdom of Hack Club will be gone forever. This is why they need your help to stop Badlo! @@ -45,13 +45,13 @@ This is why they need your help to stop Badlo! ## The adventure begins! -Okay, so before you could help Supercop Orpheus you need some info, right? We have a special person, **Agent Squirrel**, who is an agent of Hack Island. He has some info for us: +Okay, so before you can help Supercop Orpheus you need some info, right? We have a special person, **Agent Squirrel**, who is an agent of Hack Island. He has some info for us: SQUIRREL image - Badlo uses https://badlonetwork.vercel.app/ to chat with his associates. - His username is **badlo escobar** -- The database the website uses is **written in SQL ( more about this coming ahead)**. +- The database the website uses is **written in SQL (more about this coming ahead)**. ## Let’s visit the website @@ -78,7 +78,7 @@ The below is a sample database storing data in the form of tables. SQL table image -We can retrieve information from this table by using SQL’s SELECT statement. Each row of the table is called a **record**. +We can retrieve information from this table by using SQL’s `SELECT` statement. Each row of the table is called a **record**. To retrieve all the rows in the table we will write the following command: @@ -89,33 +89,32 @@ SELECT * from TABLE_NAME ; ``` Here * means all records and TABLE_NAME is the name of the table whose records you want to have! -If you want to get some specific records in SQL then you use the WHERE clause. For example if you want a record with username “badlo escoabr” and password “12345”. Then you would write the following SQL. +If you want to get some specific records in SQL then you use the `WHERE` clause. For example, if you want a record with username “badlo escobar” and password “12345", you would write the following SQL. ```sql -SELECT * from TABLE_NAME WHERE username = 'badlo escoabar' AND password = '12345' ; +SELECT * from TABLE_NAME WHERE username = 'badlo escobar' AND password = '12345' ; ``` -Here the record with USERNAME “badlo escobar” and password “12345” will be shown to us ( if exists ). The keyword **AND** tells that both of the STATEMENT needs to be true i.e USERNAME = “badlo escobar” and Password = “12345”. +Here, the record with username “badlo escobar” and password “12345” will be shown to us (if it exists). The keyword **AND** tells us that both of the statements need to be true i.e `username = 'badlo escobar'` and `password = '12345'`. This was a small introduction to SQL for you. - ## SQL Injection -Agent Squirrel told us that the website uses SQL for its database. The website which uses SQL is prone to a vulnerability which is known as SQL injection. Hmmm? Lemme explain it to you. +Agent Squirrel told us that the website uses SQL for its database. Websites which use SQL are prone to a vulnerability known as SQL injection. Hmmm? Lemme explain it to you. SQL Injection -So, to retrieve data from their database, websites run SQL commands in their backend and we can inject some SQL into these commands to mutate these statements for getting desirable results! +So, to retrieve data from their database, websites run SQL commands in their backend and we can inject some SQL into these commands to mutate these statements for desirable results! -This is the technique you will use to bypass the security. The way Orpheus has you for help, you can rely on me for some help too, see how friendly Hack Clubbers are! +This is the technique you will use to bypass the security. The way Orpheus has you for help, you can rely on me for some help too! See how friendly Hack Clubbers are! ## More INFO -Agent Squirrel has some new inputs for us. They say that they just got a sneak peek into badlonetworks’s source code and saw a line like this somewhere : +Agent Squirrel has some new inputs for us. They say that they just got a sneak peek into badlonetworks’s source code and saw a line like this somewhere: ```sql @@ -125,13 +124,13 @@ Agent Squirrel has some new inputs for us. They say that they just got a sneak p squirrel image 1 -The SQL is written between **`** strings. Which are [template string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals) in JS. +The SQL is written between **\`** strings, which are [template strings](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals) in JS. -Awesome! so ${username} and ${password} are placeholders for the variable username and password in a js template string. +Awesome! so ${username} and ${password} are placeholders for the variable username and password in a JS template string. **This means that whatever we are typing in the username and password text fields might be being injected directly into the template string containing the SQL statement.** -So, if in username I type **badlo esocbar** and password I type **password** then the template string will become : +So, if in username I type **badlo escobar** and in password I type **password** then the template string will become: ```sql @@ -139,9 +138,7 @@ So, if in username I type **badlo esocbar** and password I type **password** the ``` -Okay! so, what **if in password** we type ' OR 1=1 -- ( _after **--** there should be a space_ ) and make username = 'badlo escobar'. - -Then the SQL statement will become: +Okay! So, **if in password** we type ' OR 1=1 -- (_after **--** there should be a space_) and make the username 'badlo escobar', the SQL statement will become: ```SQL @@ -149,39 +146,37 @@ SELECT * from people WHERE username = 'badlo escobar' AND password = '' OR 1=1 - ``` -When we make our password = ' OR 1=1 -- , then **'** closes the string of our password variable making **it an empty string.** +When we make our password = ' OR 1=1 -- , then the **'** closes the string of our password variable making it an **empty string.** -In SQL **--** comments down whatever is next to it. So, **--** comments down the leftover **'** string here and we get the above SQL command. +In SQL **--** comments whatever is after it. So, **--** comments the leftover **'** string here and we get the above SQL command. -Now in an SQL command with **OR** condition in it, any one of the conditions must be true. Here 1 = 1 is always true ( because 1=1, is it? ). So the SQL command will give a result back to the system which is not Empty. This would fool a system into thinking that such a user exists. +Now in an SQL command with an **OR** condition in it, any one of the conditions can be true. Here `1 = 1` is always true (because 1 = 1, isn't it?). So the SQL command will give a result back to the system which is not empty. This would fool a system into thinking that such a user exists. -So if we set our password = ‘ OR 1=1 -- ( remember after **--** there should be a space ). Then no matter what the username and password are, we will be able to bypass the system. Which makes our hint that the username is **badlo escobar** useless. - -Because as far as the password is equal to what we have discussed then no matter which username you use, you will bypass the security! +So if we set our password = ' OR 1=1 -- ( remember after **--** there should be a space ). Then no matter what the username and password are, we will be able to bypass the system. Which makes our hint that the username is **badlo escobar** useless. Why? Because if the password is equal to what we have discussed then no matter which username you use, you will bypass the security! ## Hack Time 🐱‍💻 -Okay! So finally time to perform the Hack. Open the website [https://badlonetwork.vercel.app](https://badlonetwork.vercel.app) and type the following into username and password: +Okay! It's finally time to perform the hack. Open the website [https://badlonetwork.vercel.app](https://badlonetwork.vercel.app) and type the following into the username and password inputs: - username = badlo escobar -- Password = ‘ OR 1=1 -- +- Password = ' OR 1=1 -- -Remember after **--** there should be a space. And then click on the Login Button. +Remember after **--** there should be a space. And then click on the Login button. thehack image ## Woooo! -Yes, you just bypassed the system and what is in front of you is the chat of Badlo with one of his associates. Let’s see what we can find here. +Yes, you just bypassed the system and what is in front of you is Badlo's chat with one of his associates. Let’s see what we can find here. Badlo's secret chat Image -Okay, so from the highlighted portion of the chat we know one that: +Okay, so from the highlighted portion of the chat we know that: - We need to go to https://stopbadlomission.vercel.app/ - We need to enter **badlo-is-great** as a passcode in the text field and then click on The cancel mission Button. -- Try typing the passcode and not copy-pasting it, as sometimes along with the passcode we might end up copying a space character with it ( which is not visible to us! ). +- Try typing the passcode and not copy-pasting it, as sometimes along with the passcode we might end up copying a space character (which is not visible to us!). passcode entering image @@ -195,16 +190,18 @@ Do you know what you just did by clicking that button? You saved Hack Island’s Be ready and keep learning. Badlo will not like this, and we need to hedge the Island from him. We need to be ready for his upcoming plans. -This is just the start of your adventure, be ready for the next one! +You can learn to protect against SQL Injections here: https://www.ptsecurity.com/ww-en/analytics/knowledge-base/how-to-prevent-sql-injection-attacks/ + +This is just the start of your adventure. Be ready for the next one! ## When you have superpowers then use them for good! -I know you just learned a new trick. This was just for educational purposes and hacking someone’s system without their consent is a cybercrime. +I know you just learned a new trick. This was for educational purposes and hacking someone's system without their consent is a cybercrime. -This was made so that you can be made aware of the various ways in which people do cyber-attack in the form of a story and how you can hedge against them! +This was made so you can be aware of the various ways in which people do cyber-attacks in the form of a story and how you can hedge against them! super power image -Hack Club doesn’t promote any kind of misuse of the knowledge being provided here and won’t be responsible for your participation in any sort of unlawful activities. So using the workshop for educational purposes only is strictly advised. +Hack Club doesn’t promote any misuse of the knowledge being provided here and is not responsible for your participation in any unlawful activities. Using the workshop for educational purposes is strictly advised. -When you have superpowers then use them for good! +**When you have superpowers, use them for good!** From adc57601ed4af9d700b976ee13b0c1d2f1374774 Mon Sep 17 00:00:00 2001 From: Jake Gerber Date: Fri, 18 Dec 2020 18:23:46 -0800 Subject: [PATCH 43/51] Added New Workshop for Workshop Bounty discord_bot_saveMessages (Resubmission #3) (#1480) * Create discord_bot_saveMessages * Delete discord_bot_saveMessages * Create j * Create images * Delete images * Delete j * Create temp * Create README.md * Update README.md * Update README.md * Update README.md * Update README.md * Update README.md * Update README.md * Update README.md * Update README.md * Create Source Code * Delete Source Code * Create temp * Update README.md * Update README.md * Update README.md * Delete temp * Update README.md * Update README.md * Update README.md * Update README.md * Update README.md * Update README.md * Update README.md * Update README.md * Add files via upload * Delete Discord-Bot-SourceCode.zip * Add files via upload * Add files via upload * Add files via upload * Delete Discord-Bot-WorkshopExpanded-Community.zip * Add files via upload * Add files via upload * Delete Discord-Bot-WorkshopExpanded-RandomMessage.zip * Add files via upload * Update README.md * Delete temp * Create Original Discord Bot * Delete Original Discord Bot * Update README.md * Update README.md * Delete Discord-Bot-WorkshopExpanded-RandomMessage.zip * Delete Discord-Bot-WorkshopExpanded-MessageLimit.zip * Delete Discord-Bot-WorkshopExpanded-Community.zip * Delete Discord-Bot-Workshop-SourceCode.zip * Update README.md * Update README.md * Format with Prettier * Update README.md * Update README.md * Update README.md * Update README.md * Update README.md * Update README.md * Create README.md * Update README.md * Delete README.md * Update README.md * some changes * remove title (it'll appear automatically on the website) * small change Co-authored-by: Lachlan Campbell Co-authored-by: Matthew Stanciu --- workshops/discord_message_bot/README.md | 380 ++++++++++++++++++++++++ 1 file changed, 380 insertions(+) create mode 100644 workshops/discord_message_bot/README.md diff --git a/workshops/discord_message_bot/README.md b/workshops/discord_message_bot/README.md new file mode 100644 index 000000000..b879b6ec7 --- /dev/null +++ b/workshops/discord_message_bot/README.md @@ -0,0 +1,380 @@ +--- +name: 'Discord Custom Message Bot' +description: 'Save your own own custom messages with a Discord Bot!' +author: '@JakeGerber' +--- + +In this workshop, we're going to create a Discord bot that allows users to save custom messages that they can have the bot send at any time. By the end you will have programmed a sweet bot to add to your Discord server! + +Message Example + +## Bot Setup + +Let's set up a bot through Discord before creating its features! Click [here](https://discord.com/developers/applications). + +Click the "New Application" button at the top right. +New Application Button + +This creates a new bot where you can customize the name, description, and profile picture! +Bot Profile + +Now click the "Bot" tab on the right side of your screen. +Bot Side Bar + +Click "Add Bot" to generate a bot token. This identifies the bot. Give it to nobody! +Bot Token + +Zelda Gif + +## Repl.it Setup + +We're going to use [Repl.it](https://repl.it/~) to host the bot. It is an online IDE that makes it easy to setup and run the bot! + +Create a new repl and use Node.js as the language. + +Node.js Repl + +Make sure to set it to private. You'll be adding sensitive information to this project, so you don't want other people accessing your code. + +Node.js Repl + +## Initial Setup + +Let's start coding! + +Shawn Sheep Gif + +You should see a file called `index.js`. This is where you'll write the code for the bot. + +But first, create a file called `msgs.json` by clicking on the "Add file" button at the top left of your sidebar. This file is where your bot's messages will be stored. + +JSON is a file format that allows you to store data as a JavaScript object (key/value pair). If you want to learn more, [check here](https://www.json.org/json-en.html). + +Once you create the `msgs.json` file, add two curly braces, like so: + +Write Command Example + +### Writing JavaScript! + +In your `index.js` file, start off by adding the following code. It will be explained below. + +```js +const Discord = require('discord.js') +const client = new Discord.Client() +const fs = require('fs') +client.msgs = require('./msgs.json') +prefix = '!' + +client.once('ready', () => { + console.log('Ready!') +}) + +client.on('message', (message) => {}) + +client.login('token') // make sure you replace token with your bot token +``` + +Let's start by explaining what the prefix is. It allows you to use an exclamation mark to call the bot! Some people have other prefixes such as a dash or pound symbol. It is up to you! + +```js +prefix = '!' +``` + +Now let's explain these lines. + +```js +const fs = require('fs') +client.msgs = require('./msgs.json') +``` + +[fs](https://nodejs.org/api/fs.html#fs_file_system) is a file system node module. It allows you to interact with file systems, which we will need later when writing to your JSON file. + +We then specify that our JSON file will have messages written to it. + +Right after the lines where you import everything you need, add: + +```js +client.once('ready', () => { + console.log('Ready!') +}) +``` + +This code lets you know the bot is on when you run it. + +## Write Command + +Yay! We've successfully completed our initial setup! + +Snoopy Gif + +Now let's start with a command that allows you to add your own custom messages. The user will provide a key that the message will be saved to and the message itself. + +Write Command Example + +We'll want the bot to respond to `!write {messageKey} {message}`. + +Within your `client.on` brackets add this if statement: + +```js +client.on('message', (message) => { + if (message.content.startsWith(`${prefix}write `)) { + + } +}) +``` + +This makes sure your bot command starts with `!write`. The `message.content` part of this just looks at the message that the user typed. + +Within the if statement, add: + +```js +client.on('message', (message) => { + if (message.content.startsWith(`${prefix}write `)) { + var tempSplits = message.content.split(' ', 2) + var keyVal = tempSplits[1] + var messageVal = message.content.slice(tempSplits[0].length + tempSplits[1].length + 2) + } +}) +``` + +These three lines of code seperate the key value and the message into two separate strings. + +- The first line splits into two strings into an array based on the spaces. The two represents how big the array will be. Documentation is [here](https://www.w3schools.com/jsref/jsref_split.asp). The key value will be the second of the two strings. +- The second line assigns the second array value to the keyVal string. +- The third line takes the original user message and cuts out everything but the message part of the command. + +Now, let's add the user to the json file. + +```js +client.on('message', (message) => { + if (message.content.startsWith(`${prefix}write `)) { + // Code we already wrote previously would be here + if (client.msgs[message.author.id] == undefined) { + client.msgs[message.author.id] = {} + } + client.msgs[message.author.id][keyVal] = messageVal + } +}) +``` + +If the user does not exist in the json, we are adding them. We are doing this based on ID rather than username because every ID is unique. + +Then, we are adding the message under the user ID in the json. + +```js +client.on('message', (message) => { + if (message.content.startsWith(`${prefix}write `)) { + //Code we already wrote previously would be here + if (client.msgs[message.author.id] == undefined) { + client.msgs[message.author.id] = {} + } + client.msgs[message.author.id][keyVal] = messageVal + //New Stuff! + fs.writeFile('./msgs.json', JSON.stringify(client.msgs, null, 4), (err) => { + if (err) throw err + message.channel.send('message written') + }) +``` + +This writes the message to a JSON and sends a message to the Discord channel to confirm that you saved your message. + +Your entire command should look like this! + +```js +client.on('message', (message) => { + if (message.content.startsWith(`${prefix}write `)) { + var tempSplits = message.content.split(' ', 2) + var keyVal = tempSplits[1] + var messageVal = message.content.slice(tempSplits[0].length + tempSplits[1].length + 2) + + if (client.msgs[message.author.id] == undefined) { + client.msgs[message.author.id] = {} + } + client.msgs[message.author.id][keyVal] = messageVal + + fs.writeFile('./msgs.json', JSON.stringify(client.msgs, null, 4), (err) => { + if (err) throw err + message.channel.send('message written') + }) + } +} +``` + +Yay! The `!write` command is done! + +## Get Command + +Spongebob Gif + +Now let's do the `!get` command. This allows you to get the message you saved! + +Get Command Example + +```js +client.on('message', (message) => { + // The !write command we previously wrote would be here + if (message.content.startsWith(`${prefix}get `)) { + let getMessage = message.content.slice(5) + let _msg = client.msgs[message.author.id][getMessage] + message.channel.send(_msg) + } +} +``` + +- The if statement checks if the message starts with `!get` +- The first line in the if statement gets rid of the `!get` part of the message to isolate the message +- The second line gets the message in the JSON file. +- The third line has the bot send the message in the Discord channel. + +## Delete Command + +Delete Gif + +Now, let's write a command to delete a message. + +Delete Command Example + +```js +client.on('message', (message) => { + // The Write & Get Commands we wrote would be here + if (message.content.startsWith(`${prefix}delete `)) { + let getMessage = message.content.slice(8) + delete client.msgs[message.author.id][getMessage] + fs.writeFileSync('./msgs.json', JSON.stringify(client.msgs)) + message.channel.send(getMessage + ' has been deleted.') + } +} +``` + +- As usual, the if statement makes sure the message starts with `!delete` +- The first line in the if statement isolates the message +- The second line deletes the message in the JSON +- The third line updates the JSON with the message now deleted +- The fourth line sends a message to let the user know the message was deleted + +## List Command + +List Gif + +Now let's allow the user to get the list of all their saved messages. + +List Command Example + +```js +client.on('message', (message) => { + // The other commands we wrote would be here! + if (message.content == `${prefix}list`) { + var messageList = '' + + for (var key in client.msgs[message.author.id]) { + messageList += key + ', ' + } + + message.channel.send(messageList) + } +} +``` + +- The if statement just makes sure the message starts with `!list` +- The first line inside the if statement creates an empty string named messageList +- The for loop cycles through all the key value pairs messages that the user has saved. +- The inside of the loop adds the messageKey to the messageList. +- The final line sends the messageList string to the Discord channel. + +## Help Command + +![Help Gif](https://media1.tenor.com/images/361687b43fe908864be1cffd6beda642/tenor.gif?itemid=16034496) + +Finally, let's create a help command that allows the user to see all the available commands. + +Help Command Example + +```js +client.on('message', (message) => { + // The other commands we wrote would be here + if (message.content == `${prefix}help`) { + message.channel.send("To send a message do: !write {messageKey} {message}\nTo get a message do: !get {messageKey}\nTo delete a message !delete {messageKey}\nTo view your messages !list"); + } +} +``` + +This message just sends a message to Discord with all the available commands. You can add to it if you create more! + +## Final Source Code + +```js +const Discord = require('discord.js') +const client = new Discord.Client() +const fs = require('fs') +client.msgs = require('./msgs.json') +prefix = '!' + +client.once('ready', () => { + console.log('Ready!') +}) + +client.on('message', (message) => { + if (message.content.startsWith(`${prefix}write `)) { + var tempSplits = message.content.split(' ', 2) + var keyVal = tempSplits[1] + var messageVal = message.content.slice(tempSplits[0].length + tempSplits[1].length + 2) + + + if (client.msgs[message.author.id] == undefined) { + client.msgs[message.author.id] = {} + } + client.msgs[message.author.id][keyVal] = messageVal + + fs.writeFile('./msgs.json', JSON.stringify(client.msgs, null, 4), (err) => { + if (err) throw err + message.channel.send('message written') + }) + } + + if (message.content.startsWith(`${prefix}get `)) { + let getMessage = message.content.slice(5) + let _msg = client.msgs[message.author.id][getMessage] + message.channel.send(_msg) + } + + if (message.content.startsWith(`${prefix}delete `)) { + let getMessage = message.content.slice(8) + delete client.msgs[message.author.id][getMessage] + fs.writeFileSync('./msgs.json', JSON.stringify(client.msgs)) + message.channel.send(getMessage + ' has been deleted.') + } + + if (message.content == `${prefix}list`) { + var messageList = '' + + for (var key in client.msgs[message.author.id]) { + messageList += key + ', ' + } + + message.channel.send(messageList) + } + + if (message.content == `${prefix}help`) { + message.channel.send("To send a message do: !write {messageKey} {message}\nTo get a message do: !get {messageKey}\nTo delete a message !delete {messageKey}\nTo view your messages !list"); + } +}) + +client.login('token') +``` + +## Add the bot to your server + +Now that we've written all of the code, it's time to add this bot to your Discord server! + +Go [here](https://discordapi.com/permissions.html#). + +Adding Bot to Your Server + +Add your permissions, click the link at the bottom, and choose what server you want to add it to! + +## More that you can make with Source Code + +- [Original Bot](https://repl.it/@CosmicSnowman/Discord-Bot-Workshop#index.js) +- [Expanded Bot with Community Messages](https://repl.it/@CosmicSnowman/WorkshopExpanded1#index.js) +- [Expanded Bot with Message Limits](https://repl.it/@CosmicSnowman/WorkshopExpanded2#index.js) +- [Expanded Bot with Random Messages](https://repl.it/@CosmicSnowman/WorkshopExpanded3#index.js) From 58fb1ac5b523c79e4d260d36e02a846274fbe6be Mon Sep 17 00:00:00 2001 From: Sam Poder <39828164+sampoder@users.noreply.github.com> Date: Sat, 19 Dec 2020 11:54:12 +0800 Subject: [PATCH 44/51] add img! --- workshops/discord_message_bot/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/workshops/discord_message_bot/README.md b/workshops/discord_message_bot/README.md index b879b6ec7..17de05e0e 100644 --- a/workshops/discord_message_bot/README.md +++ b/workshops/discord_message_bot/README.md @@ -2,6 +2,7 @@ name: 'Discord Custom Message Bot' description: 'Save your own own custom messages with a Discord Bot!' author: '@JakeGerber' +img: 'https://cloud-bj4vorj8t.vercel.app/examplebot.png' --- In this workshop, we're going to create a Discord bot that allows users to save custom messages that they can have the bot send at any time. By the end you will have programmed a sweet bot to add to your Discord server! From 5924e56265823052c7a4806b70b1a6a96461f51c Mon Sep 17 00:00:00 2001 From: Sam Poder <39828164+sampoder@users.noreply.github.com> Date: Sat, 19 Dec 2020 12:18:03 +0800 Subject: [PATCH 45/51] Remove \ from Rick Roll Leaders Prep --- workshops/rick_roll_leader/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workshops/rick_roll_leader/README.md b/workshops/rick_roll_leader/README.md index 7b4cdf008..84c34dd23 100644 --- a/workshops/rick_roll_leader/README.md +++ b/workshops/rick_roll_leader/README.md @@ -33,7 +33,7 @@ Once you verify your email, you'll be taken to another page which asks you to in ![](img/dashboard.png) -Once you verify your phone number, you should be redirected to the Nexmo dashboard. You should also notice that you have \$2 in free credit. Calls cost 1.39 cents per minute, so plan accordingly. If you lead a large club, you may want to consider adding some extra money to your account to ensure your funds don't run out during your meeting. +Once you verify your phone number, you should be redirected to the Nexmo dashboard. You should also notice that you have $2 in free credit. Calls cost 1.39 cents per minute, so plan accordingly. If you lead a large club, you may want to consider adding some extra money to your account to ensure your funds don't run out during your meeting. Copy your API key and API secret and store them somewhere where you can easily retrieve them. From a7ffcee06d6bbe1650162ae31b58f51eb80c8871 Mon Sep 17 00:00:00 2001 From: Emmanuel Haankwenda Date: Sun, 20 Dec 2020 16:23:11 +0200 Subject: [PATCH 46/51] Drum Pad (#Resubmission) (#1516) * Create README.md * Update README.md * Update README.md --- workshops/drumpad/README.md | 308 ++++++++++++++++++++++++++++++++++++ 1 file changed, 308 insertions(+) create mode 100644 workshops/drumpad/README.md diff --git a/workshops/drumpad/README.md b/workshops/drumpad/README.md new file mode 100644 index 000000000..2ade8beea --- /dev/null +++ b/workshops/drumpad/README.md @@ -0,0 +1,308 @@ +--- +name: 'Drum Pad' +description: 'Creating a Drum Pad with HTML, CSS & JS' +author: '@emmanuel39hanks' +img: 'https://cloud-edj42rbl8.vercel.app/0screencapture-drum-emmanuel39hanks-repl-co-2020-11-07-23_43_18.png' +--- + +Ever wondered how you can play sounds with code? Well, if yes, then you will love this workshop! We will be creating a drum pad with less than 230 lines of code that plays actual sounds. + +![Am ready GIF](https://media.giphy.com/media/CjmvTCZf2U3p09Cn0h/giphy.gif) +# Overview + +_Preview of the Drum Pad we are going to be creating_ + +![Drum Pad Preview](https://cloud-edj42rbl8.vercel.app/0screencapture-drum-emmanuel39hanks-repl-co-2020-11-07-23_43_18.png) + +Apart from building the drum pad, you will also be learning about different types of events, functions, styling, and more with Vanilla JavaScript, HTML, and CSS. + +Final Code: [GitHub](https://github.com/emmanuel39hanks/beat_maker), Demo: [Live](https://drum.emmanuel39hanks.repl.co) + +## Getting started + +Let's start by setting up our coding environment using [repl.it](https://repl.it/), a free, online code editor. +To begin, navigate to [repl.it](https://repl.it/languages/html), and create a new repl. + +You will see that there are already three files: index.html, style.css, and script.js. Navigate to your `index.html` file, and we will work on the structure of our drum pad there. + +## HTML: + +We will write most of our HTML code inside the `body` tag. Let's start by creating a header that displays the text `DRUM PAD` using the `h1` tag: + +```html +

DRUM PAD

+``` + +Just under the `h1` tag, we will have three rows and four columns of buttons. Each button will be created with a `div` tag. You can think of a `div` tag as a box or container, and we are using it because each of our buttons will have a boxy look. + +```html + +
+ +
A
+
B
+
C
+
D
+
E
+
F
+
G
+
H
+
I
+
J
+
K
+
L
+
+``` + +To quickly break this down. We have a parent `div` tag that nests our button `divs`, and then we label our buttons with letters to easily identify them, and like I mentioned above, each button will be given a boxy look, and that's why we are using `div` tags. + +
+Here's what your entire index.html file should look like so far: + +```html + + + + + + repl.it + + + +

DRUM PAD

+
+
A
+
B
+
C
+
D
+
E
+
F
+
G
+
H
+
I
+
J
+
K
+
L
+
+ + + +``` +
+ +When we run our code, it will look like this at the moment: + +![Preview of HTML with no CSS](https://cloud-hqtl5tea3.vercel.app/0screencapture-drumpad-emmanuel39hanks-repl-co-2020-11-03-08_36_46.png) + +# CSS: + +Now let's write some CSS styles for our drum pad to make it look visually appealing! + +Let's navigate to our `style.css` file and add the following code: + +```css +body { + background-color: #fff; + height: 100%; + width: 100%; + + /* To learn more about the CSS flex box, check out the hacking section */ + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + /* To learn more about the overflow property, check out the hacking section */ + overflow: hidden; + + /* To learn more about the font-family property, check out the hacking section */ + font-family: sans-serif; +} +``` + +When we run our code, you will see that our content has been aligned to the center, and that's because we changed the properties of our `body` tag, and the tag basically renders the content on a web page. + +![Preview of HTML with CSS applied, to change the layout](https://cloud-bp7m8g6di.vercel.app/0screencapture-drumpad-emmanuel39hanks-repl-co-2020-11-03-08_35_57.png) + +We're going to be using classes to add styling to our `div` tags, a class name is an HTML attribute that points to a tag or a group of tags that have the same class name. Classes are used by CSS and JavaScript to select and access specific tags, the class attribute can be used on any HTML tag by adding the keyword `class=""` to it. + +Let's navigate back to our `index.html`, We will give our parent `div` tag the class name `pad`, and all our nested `div` tags will get the class name `box` which will apply the styling that our classes have. + +```html +
+
A
+
B
+
C
+
D
+
E
+
F
+
G
+
H
+
I
+
J
+
K
+
L
+
+``` + +Now navigate to your `style.css`, and we will change our header's font size, color, and letter spacing using the following code: + +```css +h1 { + color: #000; + + /* To learn more about the font-size property, check out the hacking section */ + font-size: 5vw; + + /* To learn more about the letter-spacing property, check out the hacking section */ + letter-spacing: 6px; +} +``` + +Then we create three rows and four columns to correctly align our header and buttons. + +```css +.pad { + width: 500px; + display: flex; + + /* To learn more about the justify-content property, check out the hacking section */ + justify-content: space-between; + + /* To learn more about the flex-wrap property, check out the hacking section */ + flex-wrap: wrap; +} +``` + +We will write styling for our class name `.box`, to specify our buttons unique styling. + +```css +.box { + width: 100px; + height: 100px; + margin: 10px 0; + + /* To learn more about the box-shadow property, check out the hacking section */ + box-shadow: 0 8px 6px -6px black; + background-color: #444; + display: flex; + + justify-content: center; + + /* To learn more about the align-items property, check out the hacking section */ + align-items: center; + font-size: 20px; + + /* To learn more about the rgba function, check out the hacking section */ + color: rgba(255, 255, 255, 0.4); + + /* To learn more about the border-radius property, check out the hacking section */ + border: 4px solid; +} +``` + +And when we run our code again, it should look like this: + +![Preview of HTML with layout CSS applied, the pads are in a grid and all gray borders](https://cloud-edj42rbl8.vercel.app/0screencapture-drum-emmanuel39hanks-repl-co-2020-11-07-23_43_18.png) + +We will then add styling that adds hovering effects, inactive or active states to our buttons: + +```css +/* To learn more about the lighten function and :hover pseudo class, check out the hacking section */ +.box:hover { + background-color: lighten(#444, 10%); + + /* To learn more about the cursor property, check out the hacking section */ + cursor: pointer; +} + +/* To learn more about the :active pseudo class, check out the hacking section */ +.box:active { + /* To learn more about the darken function, check out the hacking section */ + background-color: darken(#444, 10%); + + /* To learn more about the transform property, check out the hacking section */ + transform: scale(1.1); + + /* To learn more about the transition property, check out the hacking section */ + transition: all 0.2s; +} +``` + +Now that we have finished our styling, let's work on our drum pad functionality. + +## JavaScript: + +When you click on the buttons, we have no sound. We need to write some JavaScript code that will play sounds. + +Navigate to your `script.js` file and add the following code: + +```javascript +function play(link) { + let audio = new Audio(link); + audio.load(); + audio.play(); +} +``` + +To explain what we did, we created a function called `play()`, it receives a parameter, which is `link`. This is the link to the sound. We then create an audio object and pass the link to the object. Now we can just load the audio with the `load()` function and with the `play()` function we can play our sound. A function is a block of code designed to perform a particular task, it is executed when "something" invokes it (calls it). + +All we need to do now is find a way to play sound when a button is clicked. + +Navigate to your `index.html` file, we want a sound to play when a button is click, we will need a way to call our `play()` function. We will use an HTML attribute called `onclick=""`, learn more about the onclick event here: [onclick events](https://www.w3schools.com/jsref/event_onclick.asp), it helps us call a function when a tag with the attribute is clicked on, inside the quotation marks, we pass the `play()` function and pass a link as the parameter to the function. And when a button is clicked, it will get triggered and call the `play()` function and play the sound from that link: + +```html +
+
A
+
B
+
C
+
D
+
E
+
F
+
G
+
H
+
I
+
J
+
K
+
L
+
+``` + +If you run your code now, you should see a working drum pad! + +![We did it GIF](https://media.giphy.com/media/11sBLVxNs7v6WA/giphy.gif) + +## Hacking + +Now that you have finished building, you can share your beautiful creation with other people! Remember, it's as easy as giving them your URL! Don't forget to share it with me on Slack @emmanuel39hanks. + +**Resources:** + +- [JavaScript Audio Object](https://www.w3schools.com/JSREF/dom_obj_audio.asp) +- [JavaScript onclick event](https://www.w3schools.com/jsref/event_onclick.asp) +- [CSS flex-direction](https://developer.mozilla.org/en-US/docs/Web/CSS/flex-direction) +- [CSS flex-wrap](https://developer.mozilla.org/en-US/docs/Web/CSS/flex-wrap) +- [CSS justify-content](https://developer.mozilla.org/en-US/docs/Web/CSS/justify-content) +- [CSS Flex box](https://www.w3schools.com/css/css3_flexbox.asp) +- [CSS overflow](https://developer.mozilla.org/en-US/docs/Web/CSS/overflow) +- [CSS font-size](https://developer.mozilla.org/en-US/docs/Web/CSS/font-size) +- [CSS letter-spacing](https://developer.mozilla.org/en-US/docs/Web/CSS/letter-spacing) +- [CSS transition](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Transitions/Using_CSS_transitions) +- [CSS transform](https://developer.mozilla.org/en-US/docs/Web/CSS/transform) +- [CSS :hover pseudo class](https://developer.mozilla.org/en-US/docs/Web/CSS/:hover) +- [CSS :hover pseudo class](https://developer.mozilla.org/en-US/docs/Web/CSS/:active) +- [CSS cursor](https://developer.mozilla.org/en-US/docs/Web/CSS/cursor) +- [CSS lighten & darken function](https://css-tricks.com/snippets/javascript/lighten-darken-color/) +- [CSS border](https://developer.mozilla.org/en-US/docs/Web/CSS/border) +- [CSS rgba function](https://www.w3schools.com/cssref/func_rgba.asp) +- [CSS box-shadow](https://developer.mozilla.org/en-US/docs/Web/CSS/box-shadow) +- [CSS align-items](https://developer.mozilla.org/en-US/docs/Web/CSS/align-items) +- [CSS font-family](https://www.w3schools.com/css/css_font.asp) + +Now it's up to you! Do anything with this project, go on and implement something crazy. + +To finish, here are some examples of what can be built on top of this project: + +- **Play an automated beat track:** [demo and code](https://repl.it/@emmanuel39hanks/drumpadwithabeattrack) +- **Play the drum pad with your keyboard:** [demo and code](https://repl.it/@emmanuel39hanks/drumpadwithkeyboard) +- **A beautifully styled drum pad:** [demo and code](https://repl.it/@emmanuel39hanks/beautifulstyleddrumpad) From a01e7d3ec6b652bccf6fc4f5e91d029d96631129 Mon Sep 17 00:00:00 2001 From: Sam Poder <39828164+sampoder@users.noreply.github.com> Date: Sun, 20 Dec 2020 22:25:24 +0800 Subject: [PATCH 47/51] Revert "Drum Pad (#Resubmission) (#1516)" (#1523) This reverts commit a7ffcee06d6bbe1650162ae31b58f51eb80c8871. --- workshops/drumpad/README.md | 308 ------------------------------------ 1 file changed, 308 deletions(-) delete mode 100644 workshops/drumpad/README.md diff --git a/workshops/drumpad/README.md b/workshops/drumpad/README.md deleted file mode 100644 index 2ade8beea..000000000 --- a/workshops/drumpad/README.md +++ /dev/null @@ -1,308 +0,0 @@ ---- -name: 'Drum Pad' -description: 'Creating a Drum Pad with HTML, CSS & JS' -author: '@emmanuel39hanks' -img: 'https://cloud-edj42rbl8.vercel.app/0screencapture-drum-emmanuel39hanks-repl-co-2020-11-07-23_43_18.png' ---- - -Ever wondered how you can play sounds with code? Well, if yes, then you will love this workshop! We will be creating a drum pad with less than 230 lines of code that plays actual sounds. - -![Am ready GIF](https://media.giphy.com/media/CjmvTCZf2U3p09Cn0h/giphy.gif) -# Overview - -_Preview of the Drum Pad we are going to be creating_ - -![Drum Pad Preview](https://cloud-edj42rbl8.vercel.app/0screencapture-drum-emmanuel39hanks-repl-co-2020-11-07-23_43_18.png) - -Apart from building the drum pad, you will also be learning about different types of events, functions, styling, and more with Vanilla JavaScript, HTML, and CSS. - -Final Code: [GitHub](https://github.com/emmanuel39hanks/beat_maker), Demo: [Live](https://drum.emmanuel39hanks.repl.co) - -## Getting started - -Let's start by setting up our coding environment using [repl.it](https://repl.it/), a free, online code editor. -To begin, navigate to [repl.it](https://repl.it/languages/html), and create a new repl. - -You will see that there are already three files: index.html, style.css, and script.js. Navigate to your `index.html` file, and we will work on the structure of our drum pad there. - -## HTML: - -We will write most of our HTML code inside the `body` tag. Let's start by creating a header that displays the text `DRUM PAD` using the `h1` tag: - -```html -

DRUM PAD

-``` - -Just under the `h1` tag, we will have three rows and four columns of buttons. Each button will be created with a `div` tag. You can think of a `div` tag as a box or container, and we are using it because each of our buttons will have a boxy look. - -```html - -
- -
A
-
B
-
C
-
D
-
E
-
F
-
G
-
H
-
I
-
J
-
K
-
L
-
-``` - -To quickly break this down. We have a parent `div` tag that nests our button `divs`, and then we label our buttons with letters to easily identify them, and like I mentioned above, each button will be given a boxy look, and that's why we are using `div` tags. - -
-Here's what your entire index.html file should look like so far: - -```html - - - - - - repl.it - - - -

DRUM PAD

-
-
A
-
B
-
C
-
D
-
E
-
F
-
G
-
H
-
I
-
J
-
K
-
L
-
- - - -``` -
- -When we run our code, it will look like this at the moment: - -![Preview of HTML with no CSS](https://cloud-hqtl5tea3.vercel.app/0screencapture-drumpad-emmanuel39hanks-repl-co-2020-11-03-08_36_46.png) - -# CSS: - -Now let's write some CSS styles for our drum pad to make it look visually appealing! - -Let's navigate to our `style.css` file and add the following code: - -```css -body { - background-color: #fff; - height: 100%; - width: 100%; - - /* To learn more about the CSS flex box, check out the hacking section */ - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - - /* To learn more about the overflow property, check out the hacking section */ - overflow: hidden; - - /* To learn more about the font-family property, check out the hacking section */ - font-family: sans-serif; -} -``` - -When we run our code, you will see that our content has been aligned to the center, and that's because we changed the properties of our `body` tag, and the tag basically renders the content on a web page. - -![Preview of HTML with CSS applied, to change the layout](https://cloud-bp7m8g6di.vercel.app/0screencapture-drumpad-emmanuel39hanks-repl-co-2020-11-03-08_35_57.png) - -We're going to be using classes to add styling to our `div` tags, a class name is an HTML attribute that points to a tag or a group of tags that have the same class name. Classes are used by CSS and JavaScript to select and access specific tags, the class attribute can be used on any HTML tag by adding the keyword `class=""` to it. - -Let's navigate back to our `index.html`, We will give our parent `div` tag the class name `pad`, and all our nested `div` tags will get the class name `box` which will apply the styling that our classes have. - -```html -
-
A
-
B
-
C
-
D
-
E
-
F
-
G
-
H
-
I
-
J
-
K
-
L
-
-``` - -Now navigate to your `style.css`, and we will change our header's font size, color, and letter spacing using the following code: - -```css -h1 { - color: #000; - - /* To learn more about the font-size property, check out the hacking section */ - font-size: 5vw; - - /* To learn more about the letter-spacing property, check out the hacking section */ - letter-spacing: 6px; -} -``` - -Then we create three rows and four columns to correctly align our header and buttons. - -```css -.pad { - width: 500px; - display: flex; - - /* To learn more about the justify-content property, check out the hacking section */ - justify-content: space-between; - - /* To learn more about the flex-wrap property, check out the hacking section */ - flex-wrap: wrap; -} -``` - -We will write styling for our class name `.box`, to specify our buttons unique styling. - -```css -.box { - width: 100px; - height: 100px; - margin: 10px 0; - - /* To learn more about the box-shadow property, check out the hacking section */ - box-shadow: 0 8px 6px -6px black; - background-color: #444; - display: flex; - - justify-content: center; - - /* To learn more about the align-items property, check out the hacking section */ - align-items: center; - font-size: 20px; - - /* To learn more about the rgba function, check out the hacking section */ - color: rgba(255, 255, 255, 0.4); - - /* To learn more about the border-radius property, check out the hacking section */ - border: 4px solid; -} -``` - -And when we run our code again, it should look like this: - -![Preview of HTML with layout CSS applied, the pads are in a grid and all gray borders](https://cloud-edj42rbl8.vercel.app/0screencapture-drum-emmanuel39hanks-repl-co-2020-11-07-23_43_18.png) - -We will then add styling that adds hovering effects, inactive or active states to our buttons: - -```css -/* To learn more about the lighten function and :hover pseudo class, check out the hacking section */ -.box:hover { - background-color: lighten(#444, 10%); - - /* To learn more about the cursor property, check out the hacking section */ - cursor: pointer; -} - -/* To learn more about the :active pseudo class, check out the hacking section */ -.box:active { - /* To learn more about the darken function, check out the hacking section */ - background-color: darken(#444, 10%); - - /* To learn more about the transform property, check out the hacking section */ - transform: scale(1.1); - - /* To learn more about the transition property, check out the hacking section */ - transition: all 0.2s; -} -``` - -Now that we have finished our styling, let's work on our drum pad functionality. - -## JavaScript: - -When you click on the buttons, we have no sound. We need to write some JavaScript code that will play sounds. - -Navigate to your `script.js` file and add the following code: - -```javascript -function play(link) { - let audio = new Audio(link); - audio.load(); - audio.play(); -} -``` - -To explain what we did, we created a function called `play()`, it receives a parameter, which is `link`. This is the link to the sound. We then create an audio object and pass the link to the object. Now we can just load the audio with the `load()` function and with the `play()` function we can play our sound. A function is a block of code designed to perform a particular task, it is executed when "something" invokes it (calls it). - -All we need to do now is find a way to play sound when a button is clicked. - -Navigate to your `index.html` file, we want a sound to play when a button is click, we will need a way to call our `play()` function. We will use an HTML attribute called `onclick=""`, learn more about the onclick event here: [onclick events](https://www.w3schools.com/jsref/event_onclick.asp), it helps us call a function when a tag with the attribute is clicked on, inside the quotation marks, we pass the `play()` function and pass a link as the parameter to the function. And when a button is clicked, it will get triggered and call the `play()` function and play the sound from that link: - -```html -
-
A
-
B
-
C
-
D
-
E
-
F
-
G
-
H
-
I
-
J
-
K
-
L
-
-``` - -If you run your code now, you should see a working drum pad! - -![We did it GIF](https://media.giphy.com/media/11sBLVxNs7v6WA/giphy.gif) - -## Hacking - -Now that you have finished building, you can share your beautiful creation with other people! Remember, it's as easy as giving them your URL! Don't forget to share it with me on Slack @emmanuel39hanks. - -**Resources:** - -- [JavaScript Audio Object](https://www.w3schools.com/JSREF/dom_obj_audio.asp) -- [JavaScript onclick event](https://www.w3schools.com/jsref/event_onclick.asp) -- [CSS flex-direction](https://developer.mozilla.org/en-US/docs/Web/CSS/flex-direction) -- [CSS flex-wrap](https://developer.mozilla.org/en-US/docs/Web/CSS/flex-wrap) -- [CSS justify-content](https://developer.mozilla.org/en-US/docs/Web/CSS/justify-content) -- [CSS Flex box](https://www.w3schools.com/css/css3_flexbox.asp) -- [CSS overflow](https://developer.mozilla.org/en-US/docs/Web/CSS/overflow) -- [CSS font-size](https://developer.mozilla.org/en-US/docs/Web/CSS/font-size) -- [CSS letter-spacing](https://developer.mozilla.org/en-US/docs/Web/CSS/letter-spacing) -- [CSS transition](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Transitions/Using_CSS_transitions) -- [CSS transform](https://developer.mozilla.org/en-US/docs/Web/CSS/transform) -- [CSS :hover pseudo class](https://developer.mozilla.org/en-US/docs/Web/CSS/:hover) -- [CSS :hover pseudo class](https://developer.mozilla.org/en-US/docs/Web/CSS/:active) -- [CSS cursor](https://developer.mozilla.org/en-US/docs/Web/CSS/cursor) -- [CSS lighten & darken function](https://css-tricks.com/snippets/javascript/lighten-darken-color/) -- [CSS border](https://developer.mozilla.org/en-US/docs/Web/CSS/border) -- [CSS rgba function](https://www.w3schools.com/cssref/func_rgba.asp) -- [CSS box-shadow](https://developer.mozilla.org/en-US/docs/Web/CSS/box-shadow) -- [CSS align-items](https://developer.mozilla.org/en-US/docs/Web/CSS/align-items) -- [CSS font-family](https://www.w3schools.com/css/css_font.asp) - -Now it's up to you! Do anything with this project, go on and implement something crazy. - -To finish, here are some examples of what can be built on top of this project: - -- **Play an automated beat track:** [demo and code](https://repl.it/@emmanuel39hanks/drumpadwithabeattrack) -- **Play the drum pad with your keyboard:** [demo and code](https://repl.it/@emmanuel39hanks/drumpadwithkeyboard) -- **A beautifully styled drum pad:** [demo and code](https://repl.it/@emmanuel39hanks/beautifulstyleddrumpad) From bc6ad9b3b42420a4a530873ce71363e23b86ebdf Mon Sep 17 00:00:00 2001 From: Sam Poder <39828164+sampoder@users.noreply.github.com> Date: Mon, 21 Dec 2020 14:28:13 +0800 Subject: [PATCH 48/51] shipit -> ship --- workshops/collaborative_sketch/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workshops/collaborative_sketch/README.md b/workshops/collaborative_sketch/README.md index c85ca5887..cd9715dc0 100644 --- a/workshops/collaborative_sketch/README.md +++ b/workshops/collaborative_sketch/README.md @@ -440,7 +440,7 @@ Congratulations! Your collaborative drawing app is now complete. Make sure you're logged into your Repl.it account and press **Run** with the most recent code. -Share your URL to the [`#shipit`](https://hackclub.slack.com/messages/shipit/) channel on Slack, so that everyone can collaborate together! +Share your URL to the [`#ship`](https://hackclub.slack.com/messages/ship/) channel on Slack, so that everyone can collaborate together! ## Part V: Hacking From ecee2ef53c5b207b8f8894dd17e1e9437834f7c2 Mon Sep 17 00:00:00 2001 From: Sam Poder <39828164+sampoder@users.noreply.github.com> Date: Mon, 21 Dec 2020 14:31:29 +0800 Subject: [PATCH 49/51] index.js -> script.js sorry if I end up sending loads of these, I'm actually doing this workshop so helping out as I go --- workshops/collaborative_sketch/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workshops/collaborative_sketch/README.md b/workshops/collaborative_sketch/README.md index cd9715dc0..91700d6e1 100644 --- a/workshops/collaborative_sketch/README.md +++ b/workshops/collaborative_sketch/README.md @@ -46,7 +46,7 @@ And we'll add script tags for Firebase, p5.js, jQuery, and our own `script.js` f - + ``` ### Setting up the Firebase App From 3555190b89eb14ab1e1d966492c10056ff7c5de9 Mon Sep 17 00:00:00 2001 From: Ishan Goel Date: Wed, 23 Dec 2020 14:20:17 +0400 Subject: [PATCH 50/51] Remove merge conflict remnants (#1520) --- workshops/firstnpmpackage/README.md | 164 +--------------------------- 1 file changed, 6 insertions(+), 158 deletions(-) diff --git a/workshops/firstnpmpackage/README.md b/workshops/firstnpmpackage/README.md index 8dd48fd62..b9e11eda5 100644 --- a/workshops/firstnpmpackage/README.md +++ b/workshops/firstnpmpackage/README.md @@ -6,11 +6,8 @@ author: '@bajpai244' In this workshop, we will be creating your first npm package and then we will be publishing it. This workshop will be a quick read and will be fun to do 🤠 So let’s get started! -<<<<<<< HEAD - ![Demo GIF](img/showtime.gif) -======= ![Demo GIF](img/showtime.gif) ->>>>>>> upstream/main + ## Prerequisites @@ -27,22 +24,15 @@ The source code of this workshop is available at [https://github.com/bajpai244/n ## What will we make? -<<<<<<< HEAD We will be making a calculator package, which will provide us with -======= -We will be making a calculator package, which will provide us with ->>>>>>> upstream/main + functions to do some basic arithmetic calculations! ## Setup You need to have Node.js and npm installed in your system. -<<<<<<< HEAD -### Node.js -======= ### Node.js ->>>>>>> upstream/main [Node.js](https://nodejs.org/en/) is an open-source cross-platform server environment. At the time of writing this workshop, we are using Node **version:- v12.18.3.** @@ -54,8 +44,6 @@ To check Node.js is working fine type **node -v** in your terminal, if a version ![node check image](img/nodecheck.png) -<<<<<<< HEAD - ## Creating your npm account An npm account is required for publishing an npm package. So make sure you have an npm account before proceeding further. @@ -64,24 +52,10 @@ Creating an npm account is very simple, just follow instructions from this
link to create your npm account. After that **login to your npm** via terminal using the following command: -```bash -npm login -``` -======= -## Creating your npm account - -An npm account is required for publishing an npm package. So make sure you have an npm account before proceeding further. - -Creating an npm account is very simple, just follow instructions from this -link to create your npm account. - -After that **login to your npm** via terminal using the following command: - ```bash npm login ``` ->>>>>>> upstream/main You can also take help from this link to login into your npm account. ![that was easy gif](img/itwaseasy.gif) @@ -92,21 +66,11 @@ Now, find a folder on your computer where you would like to keep your project. T ![calculatorfolder image](img/calculatorfolder.png) -<<<<<<< HEAD Now, open this project inside a terminal and type the following command: ```bash npm init -y ``` Then press the **Enter key** to run it. -======= -Now, open this project inside a terminal and type the following command: - -```bash -npm init -y -``` - -Then press the **Enter key** to run it. ->>>>>>> upstream/main ![ npm init command ](img/npminitcommand.png) @@ -118,11 +82,8 @@ Okay, so now we will discuss some questions that may arise from the above steps ### What is this command and what will it do? -<<<<<<< HEAD -The above command will create a **package.json** file for your project. You need to fill some meta information when you run this command like name,version and description etc. -======= -The above command will create a **package.json** file for your project. You need to fill some meta information when you run this command like name,version and description etc. ->>>>>>> upstream/main + +The above command will create a **package.json** file for your project. You need to fill some meta information when you run this command like name, version and description etc. The -y ( it is a command line flag ) **will autofill these fields** with the defualt values for them and write them to your package.json file! @@ -146,11 +107,7 @@ Open your project folder inside an editor. I will be using [VSCode](https://code ### Change the name in package.json -<<<<<<< HEAD Now, in the name field( this is going to be your package name ) of your package.json change it from whatever is the default value( here it is calculator ) to @username/calculator. -======= -Now, in the name field( this is going to be your package name ) of your package.json change it from whatever is the default value( here it is calculator ) to @username/calculator. ->>>>>>> upstream/main Now, **here @username is your npm username with @ as a prefix.** My npm username is bajpai244 so I will name it as @bajpai244/calculator. Packages with names in this format are called **scoped packages.** @@ -183,8 +140,6 @@ Now create a file index.js in your project folder. Now, add the following code to it: ```js -<<<<<<< HEAD - function add(x, y) { return x+y } @@ -207,52 +162,20 @@ module.exports = { multiply: multiply, divide: divide } - -======= -function add(x, y) { - return x + y -} - -function subtract(x, y) { - return x - y -} - -function multiply(x, y) { - return x * y -} - -function divide(x, y) { - return x / y -} - -module.exports = { - add: add, - subtract: subtract, - multiply: multiply, - divide: divide -} ->>>>>>> upstream/main ``` This is what the file will look like: ![ created index.js file ](img/indexjs1.png) -<<<<<<< HEAD - -======= ->>>>>>> upstream/main We declared four arithmetic functions that perform the addition, subtraction, multiplication, and division operation respectively. ### What is this module.exports? In the Node.js module system, **each file is treated as a separate module**. Each module can export its properties and methods which can then be imported by some other modules ( This is how we import properties and methods from npm packages! ). -<<<<<<< HEAD + module.exports exports a default value from a Node.js module and here we are exporting an object with keys add, subtract, multiply and divide which are then mapped to their respective arithmetic functions. -======= -module.exports exports a default value from a Node.js module and here we are exporting an object with keys add, subtract, multiply and divide which are then mapped to their respective arithmetic functions. ->>>>>>> upstream/main This will make sure that we can import our arithmetic functions in another node js file ( i.e module ). @@ -265,10 +188,7 @@ Now, we will be publishing this npm package. Open your project folder inside the ```bash npm publish --access public ``` -<<<<<<< HEAD -======= ->>>>>>> upstream/main After typing that press the **Enter key** to run this command. This is what it will look like: @@ -288,24 +208,14 @@ Scoped packages are by default published as private npm packages and hence to ma Now, before we go ahead and test it out I want to show you something. Add the following code to your existing index.js code: ```js -<<<<<<< HEAD - function remainder(x,y){ - return x%y -} - -======= -function remainder(x, y) { - return x % y + return x % y } ->>>>>>> upstream/main ``` Now in your module.exports add the remainder function. ```js -<<<<<<< HEAD - module.exports = { add: add, subtract: subtract, @@ -313,37 +223,20 @@ module.exports = { divide: divide, remainder:remainder } - -======= -module.exports = { - add: add, - subtract: subtract, - multiply: multiply, - divide: divide, - remainder: remainder -} ->>>>>>> upstream/main ``` After making all these changes your file will look like this: ![indexjs2 image](img/indexjs2.png) -<<<<<<< HEAD ## Now publish this change -======= -## Now publish this change ->>>>>>> upstream/main Now, open your project inside a terminal and type the following command to publish it: ```bash npm publish ``` -<<<<<<< HEAD -======= ->>>>>>> upstream/main We are not adding **--access public** because during the first publish we made it clear that it is going to be a public package. Now, press the **Enter key** and see what you get. @@ -378,11 +271,7 @@ Now, open your package.json and there you will see a version number, change it f ![versionchange image](img/versionchange.png) -<<<<<<< HEAD We have added new functionality and the code is still backward-compatible and hence we are **increasing the MINOR version to 1 from 0.** -======= -We have added new functionality and the code is still backward-compatible and hence we are **increasing the MINOR version to 1 from 0.** ->>>>>>> upstream/main ## Now try publishing it! @@ -391,10 +280,7 @@ Open your project inside a terminal and type the following command: ```bash npm publish ``` -<<<<<<< HEAD -======= ->>>>>>> upstream/main Press the **Enter key** to run the above command. This is what it will look like: @@ -418,10 +304,7 @@ Now, open this folder inside a terminal and type the following command. ```bash npm init -y ``` -<<<<<<< HEAD -======= ->>>>>>> upstream/main Press **Enter key** to run it. Now, in the next command, I am going to use my scope which is **@bajpai244 and you should change it to your scope i.e your @username.** @@ -431,10 +314,7 @@ After that type the following command in the terminal: ```bash npm -i @bajpai244/calculator ``` -<<<<<<< HEAD -======= ->>>>>>> upstream/main **You should use @username/calculator** where @username is your username with @ as prefix. ![testcommands image](img/testcommands.png) @@ -448,13 +328,6 @@ The first command made our package.json file and second command installed our np Now open this folder inside a code editor and make a new file **index.js** and type the following code inside it: ```js -<<<<<<< HEAD - -const { add, subtract, multiply, divide, remainder } = require('@bajpai244/calculator') - -function log(val) { - console.log(val) -======= const { add, subtract, @@ -465,7 +338,6 @@ const { function log(val) { console.log(val) ->>>>>>> upstream/main } log(add(1, 1)) @@ -477,10 +349,6 @@ log(multiply(3, 3)) log(divide(15, 3)) log(remainder(6, 3)) -<<<<<<< HEAD - -======= ->>>>>>> upstream/main ``` Now, here we have imported our package's functions and are testing them with the help of our function **log** which will log their result on a console. @@ -489,10 +357,6 @@ This is what it will look like: ![testfile image](img/testfile.png) -<<<<<<< HEAD - -======= ->>>>>>> upstream/main ## Running the test Now, we will run this file via Node.js. Open this folder inside your terminal and type the following command: @@ -500,10 +364,7 @@ Now, we will run this file via Node.js. Open this folder inside your terminal an ```bash node index.js ``` -<<<<<<< HEAD -======= ->>>>>>> upstream/main Press the **Enter key** to run this command. This is what it will look like: @@ -514,11 +375,7 @@ We will get the output of our file logged on the terminal. ## Done! -<<<<<<< HEAD -Congratulations! Now you are an npm ninja. You have created and published your first npm package! -======= Congratulations! Now you are an npm ninja. You have created and published your first npm package! ->>>>>>> upstream/main ![crazy image](https://workshops.hackclub.com/content/workshops/hackide/img/awesome.gif) @@ -528,17 +385,8 @@ You will still encounter more challenges in your journey as a package-manager an ## Next Steps! -<<<<<<< HEAD -I know it feels awesome to make it but don't stop here, Create whatever you can from this crazy trick and share it with us in the [```#ship```](https://app.slack.com/client/T0266FRGM/C0M8PUPU6) channel of [Hack Club's Slack](https://hackclub.com/slack/). - - -![nailed it gif](img/nailedit.gif) - -I am available on Hack Club's Slack by the username **Harsh Bajpai**, If you have any doubt or query regarding this workshop then feel free to reach out to me! -======= I know it feels awesome to make it but don't stop here, Create whatever you can from this crazy trick and share it with us in the [`#ship`](https://app.slack.com/client/T0266FRGM/C0M8PUPU6) channel of [Hack Club's Slack](https://hackclub.com/slack/). ![nailed it gif](img/nailedit.gif) I am available on Hack Club's Slack by the username **Harsh Bajpai**, If you have any doubt or query regarding this workshop then feel free to reach out to me! ->>>>>>> upstream/main From fd109481cc15430fd694869088a51f272bf4e19b Mon Sep 17 00:00:00 2001 From: eilla1 <72365100+eilla1@users.noreply.github.com> Date: Thu, 24 Dec 2020 18:31:23 -0800 Subject: [PATCH 51/51] Fix typo to reflect pfp change based on time (#1536) Change `images.afternoon` to `images.morning` and `images.night` for appropriate profile picture change based on time of day --- workshops/slack_pfp/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/workshops/slack_pfp/README.md b/workshops/slack_pfp/README.md index 0f21210c8..3dbde90ef 100644 --- a/workshops/slack_pfp/README.md +++ b/workshops/slack_pfp/README.md @@ -165,7 +165,7 @@ else if (12 < hour && hour < 20) { }); } else { - image = await axios.get(images.afternoon, { + image = await axios.get(images.night, { responseType: "arraybuffer", }); } @@ -185,7 +185,7 @@ async function setPFP() { var hour = new Date().getHours() + 8 let image if (5 < hour && hour < 12) { - image = await axios.get(images.afternoon, { + image = await axios.get(images.morning, { responseType: "arraybuffer", }); } @@ -195,7 +195,7 @@ async function setPFP() { }); } else { - image = await axios.get(images.afternoon, { + image = await axios.get(images.night, { responseType: "arraybuffer", }); }