71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/learn/tasks/404.js:
--------------------------------------------------------------------------------
1 | import Task from '../Task.js';
2 | import { mutant, hide_image, show_image } from '../script.js';
3 |
4 | // can't use a number :)
5 | const four_oh_four = new Task({
6 | name: '404',
7 | description: "Let people know when a page is missing.",
8 | group: 'Going further',
9 | requires_tasks: ['The config file', 'Layouts', 'Includes', 'Sass'],
10 | }).with_callback(async () => {
11 | mutant.emote = 'slight_smile';
12 | })
13 |
14 | export default four_oh_four;
--------------------------------------------------------------------------------
/learn/tasks/a_feature_of_your_own.js:
--------------------------------------------------------------------------------
1 | import Task from '../Task.js';
2 | import { mutant, hide_image, show_image } from '../script.js';
3 |
4 | const a_feature_of_your_own = new Task({
5 | name: 'A feature of your own',
6 | description: "Time to get creative!",
7 | group: 'Going further',
8 | requires_tasks: ['More elements', '404', 'Liquid'],
9 | updates_on_complete: {
10 | 'The README file': 3,
11 | 'Element showcase': 3,
12 | 'Gems': 1,
13 | 'Using your theme': 1,
14 | },
15 | }).with_callback(async () => {
16 | mutant.emote = 'slight_smile';
17 | })
18 |
19 | export default a_feature_of_your_own;
--------------------------------------------------------------------------------
/learn/tasks/all.js:
--------------------------------------------------------------------------------
1 | import github_setup from "./github_setup.js";
2 | import jekyll_setup from "./jekyll_setup.js";
3 | import your_first_page from "./your_first_page.js";
4 | import the_config_file from './the_config_file.js';
5 | import layouts from './layouts.js';
6 | import includes from './includes.js';
7 | import sass from './sass.js';
8 | import more_elements from "./more_elements.js";
9 | import four_oh_four from "./404.js";
10 | import liquid from "./liquid.js";
11 | import a_feature_of_your_own from "./a_feature_of_your_own.js";
12 | import the_readme_file from "./the_readme_file.js";
13 | import element_showcase from "./element_showcase.js";
14 | import gems from "./gems.js";
15 | import using_your_theme from "./using_your_theme.js";
16 | import hackatime_setup from "./hackatime_setup.js";
17 |
18 | export default {
19 | github_setup,
20 | hackatime_setup,
21 | jekyll_setup,
22 | your_first_page,
23 | the_config_file,
24 | layouts,
25 | includes,
26 | sass,
27 | // more_elements,
28 | // four_oh_four,
29 | // liquid,
30 | // a_feature_of_your_own,
31 | // the_readme_file,
32 | // element_showcase,
33 | // gems,
34 | // using_your_theme,
35 | }
--------------------------------------------------------------------------------
/learn/tasks/element_showcase.js:
--------------------------------------------------------------------------------
1 | import Task from '../Task.js';
2 | import { mutant, hide_image, show_image } from '../script.js';
3 |
4 | const element_showcase = new Task({
5 | name: 'Element showcase',
6 | description: "All your theme's elements in one central place!",
7 | group: 'Show the world',
8 | }).with_callback(async () => {
9 | mutant.emote = 'slight_smile';
10 | })
11 |
12 | export default element_showcase;
--------------------------------------------------------------------------------
/learn/tasks/gems.js:
--------------------------------------------------------------------------------
1 | import Task from '../Task.js';
2 | import { mutant, hide_image, show_image } from '../script.js';
3 |
4 | const gems = new Task({
5 | name: 'Gems',
6 | description: "Use RubyGems to publish your Jekyll theme!",
7 | group: 'Show the world',
8 | requires_tasks: ['The README file', 'Element showcase'],
9 | }).with_callback(async () => {
10 | mutant.emote = 'slight_smile';
11 | })
12 |
13 | export default gems;
--------------------------------------------------------------------------------
/learn/tasks/github_setup.js:
--------------------------------------------------------------------------------
1 | import Task from '../Task.js';
2 | import { mutant, hide_image, show_image } from '../script.js';
3 |
4 | const github_setup = new Task({
5 | name: 'GitHub setup',
6 | description: 'Use GitHub to store the code for your theme!',
7 | group: 'Setting up',
8 | updates_on_complete: {
9 | 'Hackatime setup': 3,
10 | },
11 | }).with_callback(async () => {
12 | mutant.emote = 'slight_smile';
13 | await mutant.say('Before I can teach you how to build a Jekyll theme...');
14 | await mutant.grinning.say("...we'll need a place for your Jekyll theme to live!");
15 | await mutant.hushed.say("We're going to use a website called *GitHub* for this.");
16 | await mutant.thinking.say('Have you heard of *GitHub* before?');
17 | await mutant.thinking.choice2({
18 | option_a: 'Yes, I have',
19 | option_b: "No, I haven't",
20 | callback_a: async () => await mutant.grinning.say('Amazing!'),
21 | callback_b: async () => await mutant.grinning.say("That's okay!"),
22 | });
23 | await mutant.thinking.say('You really only need to remember two things about GitHub.', { image: 'manufacturetocat' });
24 | await mutant.hand_over_mouth_open_eyes.say('First, it lets you _share_ your code with others...');
25 | await mutant.grinning.say("...and second, it lets you keep track of all the _changes_ you've made to your code over time!");
26 | await mutant.slight_smile.say('Also, using GitHub will make it really easy for people to use your Jekyll theme on their site.');
27 | await mutant.thinking.say('As you work on your tasks, I might ask you to open another page or add some code to your theme.', { image: null });
28 | await mutant.hushed.say('When you do that, *remember not to close this page.*');
29 | await mutant.smile_with_tear.say('Otherwise, you might have to start the whole task over again!');
30 | await mutant.grinning.say('Sound good?');
31 | await mutant.slight_smile.choice1({
32 | option_a: 'I think so',
33 | callback_a: async () => await mutant.grinning.say('Fantastic!'),
34 | });
35 | await mutant.thinking.say('Do you already have a GitHub account?');
36 | await mutant.thinking.choice2({
37 | option_a: 'Yes, I do',
38 | option_b: "No, I don't",
39 | callback_a: async () => await mutant.slight_smile.say("In that case, let's go to ^GitHub$https://github.com^ and *sign in*."),
40 | callback_b: async () => await mutant.grinning.say("In that case, let's go to ^GitHub$https://github.com^ and *sign up*!")
41 | });
42 | await mutant.slight_smile.choice1({
43 | option_a: 'I did it',
44 | callback_a: async () => await mutant.smile_hearts.say('Lovely!'),
45 | });
46 | await mutant.slight_smile.say("We're going to create a *new repository* to store your Jekyll theme in.")
47 | await mutant.thinking.say("We'll be using a _template repository_ called *tonic-starter* as a baseline.", { image: 'public_template', image_width: 300 });
48 | await mutant.slight_smile.say("Let's visit the ^tonic-starter$https://github.com/hackclub/tonic-starter^ repository now.");
49 | await mutant.slight_smile.choice1({
50 | option_a: 'I did it',
51 | callback_a: async () => await mutant.grinning.say('Excellent!'),
52 | })
53 | await mutant.thinking.say('Now, click *"Use this template"*, then click *"Create a new repository"*.', { image: 'use_this_template' });
54 | await mutant.slight_smile.say("Make sure that _you're the owner_, then give your new repository a _fun name_.", { image: 'repository_name' });
55 | await mutant.hushed.say("Don't just call it *\"my-theme\"*, like I've done here!");
56 | await mutant.thinking.say('Make sure the repository is set to *Public* so anyone can see it...', { image: 'repository_visibility', image_width: 400 });
57 | await mutant.grinning.say('Then, click *"Create repository"*!', { image: 'create_repository', image_width: 200 });
58 | await mutant.slight_smile.say("Come back here when you're done and I'll ask you for the repository link.", { image: null });
59 | await mutant.slight_smile.choice1({
60 | option_a: 'All done',
61 | callback_a: async () => await mutant.grinning.say('Excellent!'),
62 | });
63 | await mutant.thinking.say('What is the link to your GitHub repository?');
64 | await mutant.thinking.text_entry({
65 | placeholder: 'github.com/username/theme-name',
66 | exp: /^(https:\/\/)?github.com\/[\w-]+\/[\w-]+$/gm,
67 | callback: async () => await mutant.grinning.say('Looks good to me!'),
68 | });
69 | await mutant.slight_smile.say('Great work getting that online.');
70 | await mutant.grinning.say('Just one more thing before we move on!');
71 | await mutant.thinking.say('Each time you upload changes to GitHub, your repository gains one *commit*.');
72 | await mutant.thinking.say("I'm going to have you *regularly push commits* to the repository you just made...");
73 | await mutant.thinking.say('...and give me *direct links* to them afterwards.');
74 | await mutant.hushed.say('That means no uploading your entire theme all at once at the end!');
75 | await mutant.grinning.say("To accomplish this, I'd like to have you create a *codespace*.");
76 | await mutant.slight_smile.say('This is a feature provided by GitHub that lets you develop your theme completely online.');
77 | await mutant.hand_over_mouth_open_eyes.say("If you know what you're doing, it's possible to push commits without one...");
78 | await mutant.grinning.say('However, I *strongly recommend* that you set one up!');
79 | await mutant.slight_smile.say("It'll make installing Jekyll and other required tools *much easier*.");
80 | await mutant.hushed.say("If you aren't allowed to download things on your computer, it might even be your only option!");
81 | await mutant.grinning.say("So, how do you create a codespace?");
82 | await mutant.thinking.say("You'll want to go to your repository and click *\"Code\"*.", { image: 'code_button', image_width: 200 });
83 | await mutant.thinking.say('Click *"Codespaces"*, then click *"Create codespace on main"*.', { image: 'create_codespace_on_main', image_width: 400 });
84 | await mutant.grinning.say('This will open a fresh codespace in a new tab!');
85 | await mutant.slight_smile.say("It'll take a couple of minutes to set itself up for the first time...");
86 | await mutant.grinning.say("...but as soon as you see something like this, that means it's all done!", { image: 'shell_prompt' });
87 | // TODO: use the same link that the user provided in text entry
88 | await mutant.slight_smile.say("Let's go to your repository and create a codespace now.", { image: null });
89 | await mutant.slight_smile.choice1({
90 | option_a: 'I did it',
91 | callback_a: async () => await mutant.grinning.say('Wonderful!'),
92 | });
93 | await mutant.slight_smile.say("I'll show you how to push a commit from your codespace in a little while.");
94 | await mutant.grinning.say("Let's move on to the next task!");
95 | })
96 |
97 | export default github_setup;
--------------------------------------------------------------------------------
/learn/tasks/hackatime_setup.js:
--------------------------------------------------------------------------------
1 | import Task from '../Task.js';
2 | import { mutant, hide_image, show_image, show_code, hide_code } from '../script.js';
3 |
4 | const hackatime_setup = new Task({
5 | name: 'Hackatime setup',
6 | description: 'Use Hackatime to track time spent working on your theme!',
7 | group: 'Setting up',
8 | updates_on_complete: {
9 | 'Jekyll setup': 3,
10 | },
11 | }).with_callback(async () => {
12 | mutant.emote = 'slight_smile';
13 | await mutant.hand_over_mouth_open_eyes.say("Now that you've set up the Tonic starter and created a codespace...");
14 | await mutant.grinning.say("...there's something that I'd like you to install inside it!");
15 | await mutant.hushed.say('I want to have you track the *amount of time* you spend working on your theme...');
16 | await mutant.thinking.say("...by setting up a tool called *Hackatime*.");
17 | await mutant.slight_smile.say("It doesn't really matter if you spend more or less time than others...");
18 | await mutant.grinning.say("I'm happy to help, no matter how much time you need!");
19 | await mutant.hand_over_mouth_open_eyes.say("I just want to make sure we're doing good work together...");
20 | await mutant.hand_over_mouth.say('...and your amount of time spent is one piece of that puzzle.');
21 | await mutant.hushed.say('Does that sound okay?');
22 | await mutant.hushed.choice1({
23 | option_a: 'Sure thing',
24 | callback_a: async () => await mutant.grin.say('Wonderful!'),
25 | });
26 | await mutant.thinking.say('*Hackatime* has to connect to a tool called *WakaTime* in order to do any time tracking.');
27 | await mutant.thinking.say("To set it up in your codespace, you'll need to install the *WakaTime extension*.");
28 | await mutant.thinking.say('First, open your codespace and click on this icon on the left side.', { image: 'extensions_icon', image_width: 50 });
29 | await mutant.hushed.say('Then, search for *"WakaTime"*, and click *"Install"* on the top result.', { image: 'wakatime_extension', image_width: 300 });
30 | await mutant.thinking.say("When you do this, you'll get a warning asking if you trust the publisher.", { image: 'trust_publisher', image_width: 400 });
31 | await mutant.thinking.say('Click *"Trust Publisher & Install"* to install the extension!');
32 | await mutant.grinning.say("Let me know when you've completed these steps.", { image: null });
33 | await mutant.slight_smile.choice1({
34 | option_a: 'All done',
35 | callback_a: async () => await mutant.grinning.say('Fantastic!'),
36 | });
37 | await mutant.hushed.say('Now, we need to connect the WakaTime extension to *Hackatime*.');
38 | await mutant.slight_smile.say("Let's go to ^Hackatime$https://hackatime.hackclub.com/^ and *sign in with Slack*.");
39 | await mutant.thinking.choice1({
40 | option_a: 'I did it',
41 | callback_a: async () => await mutant.grinning.say('Excellent!'),
42 | });
43 | await mutant.thinking.say("You'll want to click *Settings*, on the left side of the screen...", { image: 'hackatime_settings', image_width: 200 });
44 | await mutant.thinking.say('...then click *Set up time tracking*, under _Time tracking wizard_.', { image: 'time_tracking_wizard', image_width: 400 });
45 | await mutant.thinking.say('Then, click on *Advanced/Custom Setup*.', { image: 'advanced_custom_setup', image_width: 200 });
46 | await mutant.hushed.say("Here, you'll be able find your *API key*.", { image: 'api_key', image_width: 400 });
47 | await mutant.grinning.say('This string lets WakaTime know who you are!');
48 | await mutant.hand_over_mouth_open_eyes.say('Copy *just the API key* from here (everything after *api@_key =*)...', { image: 'api_key_highlighted' });
49 | await mutant.grinning.say("Then, we'll head back to your codespace to paste it.");
50 | await mutant.thinking.say('Press *Control+Shift+P* or *Command+Shift+P*, then search for *"API Key"* and press *Enter*.', { image: 'command_palette_api_key' });
51 | await mutant.thinking.say('Then, paste your API key in the box that appears, and press *Enter* again.');
52 | await mutant.grinning.say("Let me know when you've completed these steps.", { image: null });
53 | await mutant.slight_smile.choice1({
54 | option_a: 'All done',
55 | callback_a: async () => await mutant.grinning.say('Great!'),
56 | });
57 | await mutant.hushed.say("Now, the WakaTime extension knows whose time to track, but not where to send it!");
58 | await mutant.grinning.say('To fix this, the last thing we need to do is provide an *API URL*.');
59 | await mutant.thinking.say('Go back to your codespace, and press *Control+Shift+P* or *Command+Shift+P* again...', { image: 'command_palette_api_url' });
60 | await mutant.thinking.say('...but this time, search for *"API URL*" and press *Enter*.');
61 | show_code('https://hackatime.hackclub.com/api/hackatime/v1');
62 | await mutant.thinking.say("This is the URL you need to provide.", { image: null });
63 | await mutant.grinning.say("It's a link that goes directly to the Hackatime servers!");
64 | await mutant.slight_smile.say('As before, paste it in the box, then press *Enter*.');
65 | await mutant.grinning.say("Let me know when you've completed these steps.")
66 | await mutant.slight_smile.choice1({
67 | option_a: 'All done',
68 | callback_a: async () => await mutant.smile_hearts.say('Lovely!'),
69 | });
70 | hide_code();
71 | await mutant.hand_over_mouth_open_eyes.say('That was a lot of ground to cover...');
72 | await mutant.grinning.say('But we made it through!');
73 | await mutant.slight_smile.say('If all goes well, you should start to see a time on the bottom of the screen as you work.', { image: '30m', image_width: 100 });
74 | await mutant.slight_smile.say("I'm going to trust that you were able to do this - I won't ask you for a link this time.", { image: null });
75 | await mutant.hand_over_mouth_open_eyes.say('I just need you to promise me that you did it all...');
76 | await mutant.hand_over_mouth_open_eyes.text_entry({
77 | placeholder: 'I promise!',
78 | exp: /^I promise!$/gm,
79 | callback: async () => await mutant.grinning.say('Perfect!'),
80 | });
81 | await mutant.slight_smile.say("Let's move on.");
82 | })
83 |
84 | export default hackatime_setup;
--------------------------------------------------------------------------------
/learn/tasks/includes.js:
--------------------------------------------------------------------------------
1 | import Task from '../Task.js';
2 | import { mutant, hide_image, show_image, hide_code, show_code } from '../script.js';
3 |
4 | const includes = new Task({
5 | name: 'Includes',
6 | description: "Move parts of your theme into their own files.",
7 | group: 'Theme structure',
8 | updates_on_complete: {
9 | 'Sass': 3,
10 | }
11 | }).with_callback(async () => {
12 | mutant.emote = 'slight_smile';
13 | await mutant.grinning.say('In the last task, I showed you how to add a basic layout to your Jekyll theme using HTML.');
14 | await mutant.hand_over_mouth_open_eyes.say('As you add more features, however, that layout will become more difficult to manage.');
15 | await mutant.thinking.say("Let's say you wanted to add some navigation to your theme, like in sporeball's theme *lifeblood*.", { image: 'lifeblood_navigation' });
16 | await mutant.thinking.say("Wouldn't it be nice if you could split things up and keep all the navigation code in its own file?");
17 | await mutant.thinking.say("That way, the main layout file could stay nice and clean.");
18 | await mutant.grinning.say('Luckily, Jekyll has a feature called *includes* that allows you to do exactly that!');
19 | await mutant.thinking.say("All of your theme's includes live in a folder called *@_includes*.", { image: 'includes_folder', image_width: 200 });
20 | await mutant.hand_over_mouth_open_eyes.say('Similar to layouts, you create an include by adding a *.html* file to this folder...');
21 | await mutant.hand_over_mouth_open_eyes.say('...and you can have as many of them as you want.')
22 | await mutant.hand_over_mouth.say("This time, however, the files don't have to be full pages!");
23 | await mutant.slight_smile.say("Let's open your codespace and create the *@_includes* folder now.");
24 | await mutant.slight_smile.choice1({
25 | option_a: 'I did it',
26 | callback_a: async () => await mutant.grinning.say('Fantastic!'),
27 | });
28 | await mutant.hushed.say("We're going to make an include for the HTML *head* tag.", { image: null });
29 | await mutant.thinking.say('Rather than containing content you can see, like the *body* tag...');
30 | await mutant.thinking.say("...the *head* tag allows you to _configure_ a page using settings you can't always see.");
31 | await mutant.grinning.say("It's sort of like HTML's version of the @_config.yml file!");
32 | await mutant.slight_smile.say("Let's create a file inside the @_includes folder called *head.html*.");
33 | show_code(
34 | `
35 | {{ page.title }} | {{ site.title }}
36 |
37 |
38 |
39 | `
40 | );
41 | await mutant.thinking.say("Here's the code I'd like you to put inside of it.");
42 | await mutant.thinking.say('The first line inside the head tag uses some keywords from Liquid to fill in the page title...');
43 | await mutant.hand_over_mouth_open_eyes.say('...and the next two lines contain some settings that are used on almost every website.');
44 | await mutant.thinking.say("In particular, the last line will help your Jekyll theme look good on both desktop and mobile devices.");
45 | await mutant.grinning.say("Let me know when you've added this code.");
46 | await mutant.slight_smile.choice1({
47 | option_a: 'I did it',
48 | callback_a: async () => await mutant.grinning.say('Excellent!'),
49 | });
50 | show_code(`{% include head.html %}`);
51 | await mutant.thinking.say('Next, open *default.html* in the *@_layouts* folder, and add this code on a new line above the *body* tag.');
52 | await mutant.grinning.say("Let me know when you've added this code.");
53 | await mutant.slight_smile.choice1({
54 | option_a: 'I did it',
55 | callback_a: async () => await mutant.smile_hearts.say('Lovely!'),
56 | });
57 | hide_code();
58 | await mutant.hand_over_mouth_open_eyes.say("Now, when you run *jekyll serve*, the page will still look the same...", { image: 'my_theme', image_width: 400 });
59 | await mutant.hand_over_mouth.say('...but the title of the page will change, to match what we put in the include!', { image: 'title', image_width: 200 });
60 | await mutant.thinking.say('Can you commit your changes and give me the link?', { image: null });
61 | await mutant.thinking.text_entry({
62 | placeholder: 'github.com/x/y/commit/...',
63 | exp: /^(https:\/\/)?github.com\/[\w-]+\/[\w-]+\/commit\/[0-9a-f]{40}$/gm,
64 | callback: async () => await mutant.grinning.say('Looks good to me!'),
65 | });
66 | await mutant.slight_smile.say("Let's move on.");
67 | })
68 |
69 | export default includes;
--------------------------------------------------------------------------------
/learn/tasks/jekyll_setup.js:
--------------------------------------------------------------------------------
1 | import Task from '../Task.js';
2 | import { mutant, hide_image, show_image, show_code, hide_code } from '../script.js';
3 |
4 | const jekyll_setup = new Task({
5 | name: 'Jekyll setup',
6 | description: 'Set up Jekyll so you can start building your theme!',
7 | group: 'Setting up',
8 | updates_on_complete: {
9 | 'Your first page': 3
10 | },
11 | }).with_callback(async () => {
12 | mutant.emote = 'slight_smile';
13 | await mutant.grinning.say('This task will be an easy one!');
14 | await mutant.hand_over_mouth_open_eyes.say("Now that you've set up the Tonic starter and created a codespace...");
15 | await mutant.grinning.say('...the last thing we need to do before we begin is install *Jekyll*!');
16 | await mutant.thinking.say('Jekyll is based on a programming language called *Ruby*.', { image: 'ruby-logo-2x', image_width: 125 });
17 | await mutant.thinking.say("If you wanted to work on your theme offline, you'd have to install Ruby yourself first...");
18 | await mutant.hushed.say('...which can be a little tricky!');
19 | await mutant.grinning.say('Luckily, your codespace already has it installed!');
20 | await mutant.slight_smile.say("Let's go to your codespace now.", { image: null });
21 | await mutant.slight_smile.choice1({
22 | option_a: 'I have it open',
23 | callback_a: async () => await mutant.grinning.say('Great!'),
24 | })
25 | await mutant.slight_smile.say("To install Jekyll, we'll need to run a couple of commands in your codespace's *terminal*.");
26 | await mutant.thinking.say("All you need to do is click on the *Terminal* tab in your codespace...", { image: 'terminal_tab', image_width: 400 });
27 | show_code(`sudo gem install bundler jekyll`);
28 | await mutant.thinking.say('...type in this command, and press *Enter*.', { image: null });
29 | await mutant.thinking.say('This will install both *Jekyll* and a tool called *Bundler* which is needed by the codespace.');
30 | await mutant.thinking.say("After a minute or two, you'll see a line that reads _\"Successfully installed jekyll-4.4.1\".");
31 | await mutant.grinning.say('That means Jekyll is ready to use!');
32 | await mutant.slight_smile.say("Let me know when this command is done.");
33 | await mutant.slight_smile.choice1({
34 | option_a: 'All done',
35 | callback_a: async () => await mutant.grinning.say('Wonderful!'),
36 | });
37 | show_code(`bundle exec jekyll serve --watch`);
38 | await mutant.thinking.say('To see what the Tonic starter looks like, type in this command.');
39 | await mutant.grinning.say("This is the command you'll use every time you want to see your theme in action!");
40 | await mutant.slight_smile.say('The *--watch* flag will allow you to see your changes every time you refresh the page.');
41 | await mutant.slight_smile.say("Let me know when this command is done.");
42 | await mutant.slight_smile.choice1({
43 | option_a: 'All done',
44 | callback_a: async () => await mutant.grinning.say('Excellent!'),
45 | });
46 | hide_code();
47 | await mutant.relaxed.say("If you're crafty, you'll have seen this popup in the bottom right corner.", { image: 'application_is_available' });
48 | await mutant.hand_over_mouth_open_eyes.say("If you missed that, there's another way to see what the popup is talking about.");
49 | await mutant.thinking.say('Click on the *Ports* tab, and find the port labeled *4000*.', { image: 'ports_tab' });
50 | await mutant.thinking.say('Right-click on the *forwarded address*, then choose *Open in Browser*.', { image: 'forwarded_address' });
51 | await mutant.grinning.say('The end result should be...', { image: 'tonic_starter', sleep_ms: 1500 });
52 | await mutant.grimace.say("Well, that's not very exciting, is it?");
53 | await mutant.grinning.say("Don't worry, though - in a little while I'll teach you how to make it look amazing!");
54 | await mutant.slight_smile.say("Again, I'm going to trust that you were able to do this.", { image: null });
55 | await mutant.hand_over_mouth_open_eyes.say('I just need you to promise me that you did it...');
56 | await mutant.hand_over_mouth_open_eyes.text_entry({
57 | placeholder: 'I promise!',
58 | exp: /^I promise!$/gm,
59 | callback: async () => await mutant.grinning.say('Perfect!'),
60 | });
61 | await mutant.grinning.say("Let's move on!");
62 | })
63 |
64 | export default jekyll_setup;
--------------------------------------------------------------------------------
/learn/tasks/layouts.js:
--------------------------------------------------------------------------------
1 | import Task from '../Task.js';
2 | import { mutant, hide_image, show_image, hide_code, show_code } from '../script.js';
3 |
4 | const layouts = new Task({
5 | name: 'Layouts',
6 | description: 'Decide how each page should be structured.',
7 | group: 'Theme structure',
8 | updates_on_complete: {
9 | 'Includes': 3,
10 | }
11 | }).with_callback(async () => {
12 | mutant.emote = 'slight_smile';
13 | await mutant.grinning.say('Earlier, I mentioned that you would be using *HTML* to create your theme.');
14 | await mutant.hushed.say('But what is it, and what is it actually used for?');
15 | await mutant.thinking.say('*HTML* is the language that websites are made of.', { image: 'html5', image_width: 150 });
16 | await mutant.grinning.say('If you think of a website like a building, then HTML is like the foundation!');
17 | await mutant.slight_smile.say("It may not look that pretty, but you can't build your site without it.");
18 | await mutant.thinking.say('HTML uses _tags_ to place content on a webpage.', { image: 'html_p', image_width: 400 });
19 | await mutant.thinking.say('Each tag has an opening part, some content, and a closing part.');
20 | await mutant.hand_over_mouth_open_eyes.say('Tags are allowed to contain other tags, like this...', { image: 'html_body' });
21 | await mutant.grinning.say('...and if you build up enough of them, you get a complete page!', { image: 'html' })
22 | await mutant.thinking.say('When you write individual pages using *Markdown*, like I showed you earlier...', { image: null });
23 | await mutant.thinking.say('Jekyll has to convert them into HTML before it can show you the contents of the page.');
24 | await mutant.slight_smile.say("In order to tell Jekyll how to make that conversion, you use what's called a *layout*!");
25 | await mutant.thinking.say("All of your theme's layouts live in a folder called *@_layouts*.", { image: 'layouts_folder', image_width: 200 });
26 | await mutant.hushed.say("The Tonic starter doesn't include this folder, so you'll have to create it yourself.")
27 | await mutant.thinking.say('You can create a new folder in your codespace by right-clicking underneath *tonic-starter.gemspec*...');
28 | await mutant.thinking.say('...then clicking *"New Folder..."*.');
29 | await mutant.slight_smile.say("Let's open your codespace and create the *@_layouts* folder now.");
30 | await mutant.slight_smile.choice1({
31 | option_a: 'I did it',
32 | callback_a: async () => await mutant.grinning.say('Excellent!'),
33 | });
34 | await mutant.slight_smile.say('Creating a layout is as simple as adding a *.html* file to the @_layouts folder.', { image: null });
35 | await mutant.hand_over_mouth_open_eyes.say("To allow for different types of content, your theme can have as many layouts as you want.");
36 | await mutant.slight_smile.say("For now, though, let's right-click on the @_layouts folder and create just one file called *default.html*.");
37 | show_code(
38 | `
39 |
40 |
41 |
42 | `
43 | );
44 | await mutant.thinking.say("Here's the code I'd like you to put inside of it.");
45 | await mutant.thinking.say('This creates an HTML page with nothing in it except a *body* to hold the contents.');
46 | await mutant.grinning.say("Let me know when you've added this code.");
47 | await mutant.slight_smile.choice1({
48 | option_a: 'I did it',
49 | callback_a: async () => await mutant.grinning.say('Fantastic!'),
50 | });
51 | hide_code();
52 | await mutant.hushed.say('But wait... how do we actually add our content?');
53 | show_code('{{ content }}');
54 | await mutant.slight_smile.say("There's a special keyword for that: *{{ content }}*.");
55 | await mutant.hand_over_mouth_open_eyes.say("This comes from a language called *Liquid*, which you'll learn about later.");
56 | await mutant.grinning.say('Add it inside the *body* tag, and Jekyll will fill in the content for each page automatically!');
57 | await mutant.slight_smile.choice1({
58 | option_a: 'I did it',
59 | callback_a: async () => await mutant.smile_hearts.say('Lovely!'),
60 | });
61 | hide_code();
62 | await mutant.slight_smile.say("I'll show you how to expand upon this layout in a little while.");
63 | await mutant.grinning.say("For now, let's use it on your theme's front page!");
64 | show_code('layout: default');
65 | await mutant.slight_smile.say('All you need to do is open *index.md* and add this line inside the front matter block.');
66 | await mutant.slight_smile.choice1({
67 | option_a: 'I did it',
68 | callback_a: async () => await mutant.grin.say('Wonderful!'),
69 | });
70 | hide_code();
71 | await mutant.hand_over_mouth_open_eyes.say('After all of these changes, your theme will still look exactly the same.');
72 | await mutant.grinning.say("However, these changes will be helpful to us later on!");
73 | await mutant.thinking.say('Can you commit your changes and give me the link?');
74 | await mutant.thinking.text_entry({
75 | placeholder: 'github.com/x/y/commit/...',
76 | exp: /^(https:\/\/)?github.com\/[\w-]+\/[\w-]+\/commit\/[0-9a-f]{40}$/gm,
77 | callback: async () => await mutant.grinning.say('Looks good to me!'),
78 | });
79 | await mutant.slight_smile.say("Let's move on.");
80 | })
81 |
82 | export default layouts;
--------------------------------------------------------------------------------
/learn/tasks/liquid.js:
--------------------------------------------------------------------------------
1 | import Task from '../Task.js';
2 | import { mutant, hide_image, show_image } from '../script.js';
3 |
4 | const liquid = new Task({
5 | name: 'Liquid',
6 | description: "Do more complex things with your Jekyll theme!",
7 | group: 'Going further',
8 | requires_tasks: ['The config file', 'Layouts', 'Includes', 'Sass'],
9 | }).with_callback(async () => {
10 | mutant.emote = 'slight_smile';
11 | })
12 |
13 | export default liquid;
--------------------------------------------------------------------------------
/learn/tasks/more_elements.js:
--------------------------------------------------------------------------------
1 | import Task from '../Task.js';
2 | import { mutant, hide_image, show_image } from '../script.js';
3 |
4 | const more_elements = new Task({
5 | name: 'More elements',
6 | description: "Learn about lists, code blocks, tables, and more.",
7 | group: 'Going further',
8 | requires_group: 'Theme structure',
9 | requires_tasks: ['The config file', 'Layouts', 'Includes', 'Sass'],
10 | }).with_callback(async () => {
11 | mutant.emote = 'slight_smile';
12 | await mutant.hand_over_mouth_open_eyes.say('Back in *Your first page*, I showed you some of the most important Markdown elements...');
13 | await mutant.slight_smile.say('...including plain, bold, and italic text, headings, and links.');
14 | await mutant.grinning.say("However, those aren't the only things that Markdown provides!");
15 | })
16 |
17 | export default more_elements;
--------------------------------------------------------------------------------
/learn/tasks/the_config_file.js:
--------------------------------------------------------------------------------
1 | import Task from '../Task.js';
2 | import { mutant, hide_image, show_image } from '../script.js';
3 |
4 | const the_config_file = new Task({
5 | name: 'The config file',
6 | description: 'Apply settings to your entire theme.',
7 | group: 'Theme structure',
8 | requires_group: 'Setting up',
9 | updates_on_complete: {
10 | 'Layouts': 3,
11 | },
12 | }).with_callback(async () => {
13 | mutant.emote = 'slight_smile';
14 | await mutant.grinning.say("So, you've created a codespace and set up Jekyll inside of it.");
15 | await mutant.hand_over_mouth_open_eyes.say("At this point, you're probably wondering something...");
16 | await mutant.hushed.say('*How is a Jekyll theme actually built?*')
17 | await mutant.slight_smile.say("This is the section of the task list where we'll find out.");
18 | await mutant.thinking.say('The Tonic starter is purposely missing several key parts of a complete Jekyll theme.');
19 | await mutant.hand_over_mouth_open_eyes.say("Before I teach you how to add them, however...");
20 | await mutant.hand_over_mouth.say("I should teach you about the one key part that's already there!");
21 | await mutant.thinking.say("In your codespace, there's a file called *@_config.yml*.", { image: 'config_yml', image_width: 200 });
22 | await mutant.thinking.say('Jekyll uses this file to apply settings to your entire theme.');
23 | await mutant.grinning.say("Let's look inside!");
24 | await mutant.thinking.say('The Tonic starter config includes four keys: *title*, *description*, *encoding*, and *exclude*.', { image: 'config_yml_contents', image_width: 400 });
25 | await mutant.thinking.say('*title* and *description* are self-explanatory.');
26 | await mutant.hand_over_mouth_open_eyes.say('*encoding* should always be set to *utf-8*.');
27 | await mutant.slight_smile.say('Finally, *exclude* takes a list of files which should not be included when the site is served.');
28 | await mutant.thinking.say("If you look in the *@_site* folder, you'll notice that the only file inside is *index.html*.", { image: 'site_contents', image_width: 200 });
29 | await mutant.thinking.say('That means when you bring it online with *jekyll serve*, none of the other files will be accessible.');
30 | await mutant.hushed.say('Does all of this make sense?', { image: null });
31 | await mutant.hushed.choice1({
32 | option_a: 'I think so',
33 | callback_a: async () => await mutant.grinning.say('Excellent!'),
34 | });
35 | await mutant.hand_over_mouth_open_eyes.say("There's something wrong with your @_config.yml file right now.", { image: 'config_yml_contents', image_width: 400 });
36 | await mutant.hushed.say('The title still says *"tonic-starter"*, and the description is empty!');
37 | await mutant.hushed.say("Let's update the @_config.yml file to use your theme's name, and add a short description inside the quotes.");
38 | await mutant.grinning.say('Remember to commit your changes, too!');
39 | await mutant.slight_smile.say("Let me know when you've done that, and I'll ask you for the link.", { image: null });
40 | await mutant.slight_smile.choice1({
41 | option_a: 'All done',
42 | callback_a: async () => await mutant.grinning.say('Wonderful!'),
43 | });
44 | await mutant.thinking.say('What is the link to the commit you made?');
45 | await mutant.thinking.text_entry({
46 | placeholder: 'github.com/x/y/commit/...',
47 | exp: /^(https:\/\/)?github.com\/[\w-]+\/[\w-]+\/commit\/[0-9a-f]{40}$/gm,
48 | callback: async () => await mutant.grinning.say('Looks good to me!'),
49 | });
50 | await mutant.hand_over_mouth_open_eyes.say('Before we move on, there are three more important things you should keep in mind.');
51 | await mutant.thinking.say("First, every time you change the @_config.yml file, you'll have to *fully restart Jekyll* to see the changes.");
52 | await mutant.thinking.say('That means pressing *Control+C* in the terminal and running *bundle exec jekyll serve --watch* again.');
53 | await mutant.grinning.say('Second, you can add your own options to the @_config.yml file!');
54 | await mutant.thinking.say('For example, you might want to add an option to allow people to use a dark mode provided by your theme.', { image: 'enable_dark_theme' });
55 | await mutant.grinning.say("This is not part of Jekyll by default - you'd have to write the code to make this happen yourself!")
56 | await mutant.slight_smile.say("I'll show you how to do that later on.");
57 | await mutant.thinking.say('Lastly, any option in the @_config.yml file can be *overwritten* by a site that uses it.', { image: 'footer', image_width: 400 });
58 | await mutant.thinking.say('For example, look at this *footer* option from the *nimmoi* theme, by sporeball.');
59 | await mutant.hand_over_mouth_open_eyes.say("On nimmoi's website, the footer links to _sporeball_'s website, since she's the one who made it...");
60 | await mutant.hand_over_mouth.say('...but if you wanted to use nimmoi on your own site, you could use your own @_config.yml file to change it.');
61 | await mutant.slight_smile.say("Let's move on.", { image: null });
62 | })
63 |
64 | export default the_config_file;
--------------------------------------------------------------------------------
/learn/tasks/the_readme_file.js:
--------------------------------------------------------------------------------
1 | import Task from '../Task.js';
2 | import { mutant, hide_image, show_image } from '../script.js';
3 |
4 | const the_readme_file = new Task({
5 | name: 'The README file',
6 | description: "Add more information to your GitHub repository!",
7 | group: 'Show the world',
8 | requires_group: 'Going further',
9 | }).with_callback(async () => {
10 | mutant.emote = 'slight_smile';
11 | })
12 |
13 | export default the_readme_file;
--------------------------------------------------------------------------------
/learn/tasks/using_your_theme.js:
--------------------------------------------------------------------------------
1 | import Task from '../Task.js';
2 | import { mutant, hide_image, show_image } from '../script.js';
3 |
4 | const using_your_theme = new Task({
5 | name: 'Using your theme',
6 | description: "Learn how to use your published theme!",
7 | group: 'Show the world',
8 | requires_tasks: ['Gems'],
9 | }).with_callback(async () => {
10 | mutant.emote = 'slight_smile';
11 | })
12 |
13 | export default using_your_theme;
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tonic",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "server.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "start": "node server.js"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "git+https://github.com/hackclub/tonic.git"
13 | },
14 | "keywords": [],
15 | "author": "",
16 | "license": "ISC",
17 | "bugs": {
18 | "url": "https://github.com/hackclub/tonic/issues"
19 | },
20 | "homepage": "https://github.com/hackclub/tonic#readme",
21 | "dependencies": {
22 | "cookie-parser": "^1.4.7",
23 | "express": "^5.1.0",
24 | "morgan": "^1.10.0"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const morgan = require('morgan');
3 | const { promisify } = require('util');
4 | const exec = promisify(require('child_process').exec)
5 | const fs = require('fs/promises');
6 | const crypto = require('crypto');
7 | const cookieParser = require('cookie-parser');
8 |
9 | const app = express();
10 | app.use(express.json());
11 | app.use(morgan('dev'));
12 | app.use(cookieParser());
13 |
14 | const port = process.env.PORT || 3000;
15 | const redirect_url = process.env.NODE_ENV === 'production'
16 | ? process.env.PRODUCTION_REDIRECT_URL
17 | : `http://localhost:${process.env.PORT}`
18 |
19 | // static files
20 | app.use('/assets', express.static('assets'));
21 | app.use('/learn', express.static('learn'));
22 | app.get('/', (req, res) => {
23 | res.sendFile(__dirname + '/index.html');
24 | });
25 | app.get('/index.css', (req, res) => {
26 | res.sendFile(__dirname + '/index.css');
27 | });
28 | app.get('/style.css', (req, res) => {
29 | res.sendFile(__dirname + '/style.css');
30 | });
31 | app.get('/attribution.js', (req, res) => {
32 | res.sendFile(__dirname + '/attribution.js');
33 | });
34 | app.get('/index.js', (req, res) => {
35 | res.sendFile(__dirname + '/index.js');
36 | });
37 | app.get("/favicon.svg", (req, res) => {
38 | res.sendFile(__dirname + '/favicon.svg');
39 | });
40 |
41 | app.get('/auth', (req, res) => {
42 | res.json({ auth: !!req.cookies.uid });
43 | });
44 |
45 | app.get('/auth/slack', async (req, res) => {
46 | const R = await fetch(`https://slack.com/api/oauth.v2.access?code=${req.query.code}&client_id=${process.env.CLIENT_ID}&client_secret=${process.env.CLIENT_SECRET}&redirect_uri=${redirect_url}/auth/slack`, {
47 | headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
48 | method: 'POST',
49 | }).then(R => R.json());
50 | if (R.ok && R.authed_user?.access_token) {
51 | res.cookie('uid', R.authed_user.id, {
52 | httpOnly: true,
53 | secure: process.env.NODE_ENV === 'production',
54 | // sameSite: 'lax',
55 | maxAge: 1000 * 60 * 60 * 24 * 30, // 30 days
56 | });
57 | // Make internal request to /scrap
58 | // await fetch(`${redirect_url}/scrap`, {
59 | // method: 'POST',
60 | // headers: {
61 | // 'Content-Type': 'application/json',
62 | // 'Cookie': `uid=${R.authed_user.id}`
63 | // },
64 | // body: JSON.stringify({
65 | // task: 'Login',
66 | // text_entry: '-'
67 | // })
68 | // });
69 | }
70 | res.redirect('/');
71 | });
72 |
73 | app.post('/scrap', async (req, res) => {
74 | // sanity check
75 | if (req.cookies.uid === undefined) {
76 | res.status(500).json({ success: false, lost_id: true });
77 | return;
78 | }
79 | // ...
80 | const task = req.body.task;
81 | const text_entry = req.body.text_entry;
82 | const R = await fetch(`https://api.airtable.com/v0/${process.env.AIRTABLE_BASE_ID}/${process.env.AIRTABLE_SCRAPS_TABLE_ID}`, {
83 | headers: {
84 | 'Authorization': `Bearer ${process.env.AIRTABLE_PAT}`,
85 | 'Content-Type': 'application/json'
86 | },
87 | // method: 'PATCH',
88 | method: 'POST',
89 | body: JSON.stringify({
90 | // performUpsert: { fieldsToMergeOn: ['Slack ID'] },
91 | records: [
92 | {
93 | fields: {
94 | 'Slack ID': req.cookies.uid,
95 | 'Task': task,
96 | 'Text Entry': text_entry,
97 | },
98 | },
99 | ],
100 | }),
101 | }).then(R => R.json());
102 | console.log(R);
103 | if (R.error) {
104 | res.status(500).json({ success: false })
105 | } else {
106 | res.status(200).json({ success: true });
107 | }
108 | });
109 |
110 | app.get('/scraps', async (req, res) => {
111 | const R = await fetch(`https://api.airtable.com/v0/${process.env.AIRTABLE_BASE_ID}/Scraps?fields%5B%5D=Task&filterByFormula=%7BSlack+ID%7D%3D%22${req.cookies.uid}%22`, {
112 | headers: {
113 | 'Authorization': `Bearer ${process.env.AIRTABLE_PAT}`,
114 | 'Content-Type': 'application/json'
115 | },
116 | method: 'GET',
117 | }).then(R => R.json());
118 | console.log(R);
119 | if (R.error) {
120 | res.status(500).json({ success: false })
121 | } else {
122 | res.status(200).json({ success: true, ...R });
123 | }
124 | });
125 |
126 | app.get('/auth/logout', (req, res) => {
127 | res.cookie('uid', '', {
128 | httpOnly: true,
129 | secure: process.env.NODE_ENV === 'production',
130 | // sameSite: 'lax',
131 | expires: new Date(0)
132 | });
133 | res.redirect('/');
134 | });
135 |
136 | app.listen(port, () => {
137 | console.log(`Server is running at http://localhost:${port}`);
138 | });
--------------------------------------------------------------------------------