From 40ecfc285f79bcb569422aa9d33347bbd06a8925 Mon Sep 17 00:00:00 2001 From: Josh Pullen Date: Mon, 8 Jul 2024 12:22:30 -0400 Subject: [PATCH] Add hidden /learn page with the beginning of a "JavaScript for Scratchers" book --- app/(content)/layout.tsx | 10 +- .../page.tsx | 73 +++++++++ app/learn/layout.tsx | 121 +++++++++++++++ app/learn/page.tsx | 42 ++++++ components/Center.tsx | 18 ++- components/Code.tsx | 15 +- components/InlineBlock.tsx | 17 +++ components/Nav.tsx | 30 +++- components/ReadonlyCodeDemo.tsx | 142 ++++++++++++++++++ components/ScratchBlocks.tsx | 10 +- lib/useScrollPosition.tsx | 28 ++++ lib/useWindowScrollPosition.tsx | 18 --- 12 files changed, 493 insertions(+), 31 deletions(-) create mode 100644 app/learn/differences-between-scratch-and-javascript/page.tsx create mode 100644 app/learn/layout.tsx create mode 100644 app/learn/page.tsx create mode 100644 components/InlineBlock.tsx create mode 100644 components/ReadonlyCodeDemo.tsx create mode 100644 lib/useScrollPosition.tsx delete mode 100644 lib/useWindowScrollPosition.tsx diff --git a/app/(content)/layout.tsx b/app/(content)/layout.tsx index afe5bcd..5352435 100644 --- a/app/(content)/layout.tsx +++ b/app/(content)/layout.tsx @@ -1,6 +1,5 @@ -import classNames from "classnames"; import { Footer } from "../../components/Footer"; -import Nav from "../../components/Nav"; +import Nav, { NavLink, NavLinks, NavSpace } from "../../components/Nav"; import TopBorder from "../../components/TopBorder"; interface ContentLayoutProps { @@ -11,7 +10,12 @@ export default function ContentLayout({ children }: ContentLayoutProps) { return (
-
diff --git a/app/learn/differences-between-scratch-and-javascript/page.tsx b/app/learn/differences-between-scratch-and-javascript/page.tsx new file mode 100644 index 0000000..f520d49 --- /dev/null +++ b/app/learn/differences-between-scratch-and-javascript/page.tsx @@ -0,0 +1,73 @@ +import { InlineBlock } from "../../../components/InlineBlock"; + +import dynamic from "next/dynamic"; +const ReadonlyCodeDemo = dynamic( + () => import("../../../components/ReadonlyCodeDemo"), + { ssr: false }, +); + +export default function LearnPage() { + return ( +
+

Major differences between Scratch and JavaScript

+

+ Scratch and JavaScript are completely different languages that serve + different purposes, so they work entirely differently. +

+

+ Some Scratch blocks have identical replicas in JavaScript. For example, + the JavaScript code alert(1 + 1) is a lot like the Scratch + code say ((1) + (1)). But in most cases, the + comparison is not so simple. +

+ +

JavaScript does not have sprites

+

+ JavaScript is used to add interactive functionality to websites. Instead + of using sprites to display content on screen, websites use a separate + language called HTML to put information on the page. HTML has many + different "elements". For example, the <button>{" "} + element is used to create a button, the <img />{" "} + element is used to create an image, and the <a>{" "} + element to create a link. +

+

+ JavaScript can take these elements and make them interactive. For + example, the following HTML code sets up a button and the JavaScript + code pops up a message when you click it. +

+ + Click me`} + js={`const myButton = document.getElementById("myButton"); + +myButton.addEventListener("click", () => { + alert("You clicked the button!"); +});`} + /> + +

+ Additionally, if you want to change the way something looks, you can + style it using another{" "} + language called CSS. +

+ + `} + css={`img {\n rotate: 45deg;\n}`} + previewCSS={` + img { + width: 100px; + margin: 32px 16; + } + `} + /> +

+ If you're making a web application, this style of programming is + perfect. You can create pages of content with interactive buttons and + everything works wonderfully. But if you're trying to create a game, + this doesn't really work. +

+
+ ); +} diff --git a/app/learn/layout.tsx b/app/learn/layout.tsx new file mode 100644 index 0000000..24cb6a6 --- /dev/null +++ b/app/learn/layout.tsx @@ -0,0 +1,121 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; +import { Footer } from "../../components/Footer"; +import Nav, { NavLink, NavLinks, NavSpace } from "../../components/Nav"; +import TopBorder from "../../components/TopBorder"; + +import resolveConfig from "tailwindcss/resolveConfig"; +import tailwindConfig from "../../tailwind.config"; +import Center from "../../components/Center"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import classNames from "classnames"; + +const { theme } = resolveConfig(tailwindConfig); + +interface TOCSection { + title: string; + url: string; + children?: TOCSection[]; +} + +const tableOfContents: TOCSection = { + title: "JavaScript for Scratchers", + url: "/learn", + children: [ + { + title: "Major differences between Scratch and JavaScript", + url: "/learn/differences-between-scratch-and-javascript", + }, + ], +}; + +interface LearnLayoutProps { + children: React.ReactNode; +} + +export default function LearnLayout({ children }: LearnLayoutProps) { + const width = useWindowWidth(); + const xlWidth = Number(theme.screens.xl.replace("px", "")); + const xl = width >= xlWidth; + + const scrollRef = useRef(null); + + return ( +
+
+ + +
+ {xl && ( + + )} +
+
+ {!xl && ( + + )} +
{children}
+
+
+
+
+ ); +} + +function useWindowWidth() { + const [width, setWidth] = useState(window.innerWidth); + + useEffect(() => { + function handleResize() { + setWidth(window.innerWidth); + } + window.addEventListener("resize", handleResize); + return () => window.removeEventListener("resize", handleResize); + }, []); + + return width; +} + +function TableOfContents() { + const pathname = usePathname(); + + return ( +
+

+ {tableOfContents.title} +

+
    + {tableOfContents.children?.map((section, i) => ( +
  • + {section.title} +
  • + ))} +
+
+ ); +} diff --git a/app/learn/page.tsx b/app/learn/page.tsx new file mode 100644 index 0000000..73b6563 --- /dev/null +++ b/app/learn/page.tsx @@ -0,0 +1,42 @@ +import Link from "next/link"; + +export default function LearnPage() { + return ( + <> +
+

JavaScript for Scratchers

+

+ If you already know Scratch, you might be ready to level up your + programming skills and learn JavaScript. +

+ +

Why learn JavaScript?

+

+ Scratch is extremely limited. Your project is confined to a 480x360 + window, you can only use the blocks that are provided to you, and + Scratch's variables, lists, and custom blocks are not powerful enough + to easily build great projects. +

+

+ In contrast, JavaScript is the most popular programming language in + the world and it is vastly more capable. JavaScript is the coding + language that is used to power the interactive parts of every webpage + on the internet. And that's a big deal, because more and more of our + apps run in web browsers every year. Even many native apps are{" "} + + powered by web technologies + + . +

+

+ If you want to make a game, app, or website, JavaScript is an + excellent tool for the job. This tutorial will teach you how. +

+ + + Up next: Differences between Scratch and JavaScript + +
+ + ); +} diff --git a/components/Center.tsx b/components/Center.tsx index 1953f5b..0cd05d5 100644 --- a/components/Center.tsx +++ b/components/Center.tsx @@ -3,11 +3,25 @@ import classNames from "classnames"; interface CenterProps { children: React.ReactNode; className?: string; + width?: "default" | "wide"; } -export default function Center({ children, className }: CenterProps) { +export default function Center({ + children, + className, + width = "default", +}: CenterProps) { return ( -
+
{children}
); diff --git a/components/Code.tsx b/components/Code.tsx index 2404715..34af022 100644 --- a/components/Code.tsx +++ b/components/Code.tsx @@ -1,6 +1,10 @@ import hljs from "highlight.js/lib/core"; +import html from "highlight.js/lib/languages/xml"; +import css from "highlight.js/lib/languages/css"; import javascript from "highlight.js/lib/languages/javascript"; +hljs.registerLanguage("html", html); +hljs.registerLanguage("css", css); hljs.registerLanguage("javascript", javascript); import "./Code.css"; @@ -9,12 +13,15 @@ import classNames from "classnames"; interface CodeProps { children: string; className?: string; + language?: "html" | "css" | "javascript"; } -export function Code({ children, className }: CodeProps) { - const highlightedCode = hljs.highlight(children, { - language: "javascript", - }).value; +export function Code({ + children, + className, + language = "javascript", +}: CodeProps) { + const highlightedCode = hljs.highlight(children, { language }).value; return (
 import("./ScratchBlocks"), { ssr: false });
+
+interface InlineBlockProps {
+  children: string;
+}
+
+export function InlineBlock({ children }: InlineBlockProps) {
+  return (
+    
+      {children}
+    
+  );
+}
diff --git a/components/Nav.tsx b/components/Nav.tsx
index 79558bd..7e4818d 100644
--- a/components/Nav.tsx
+++ b/components/Nav.tsx
@@ -16,13 +16,14 @@ import { RemixIcon } from "./RemixIcon";
 import { ErrorIcon } from "./icons/ErrorIcon";
 import { useRouter } from "next/navigation";
 import { ProjectTitleEditor } from "./ProjectTitleEditor";
-import { useWindowScrollPosition } from "../lib/useWindowScrollPosition";
+import { useScrollPosition } from "../lib/useScrollPosition";
 
 interface NavProps {
   width?: "default" | "wide" | "full";
   title?: string | null;
   titleHref?: string;
   children?: React.ReactNode;
+  scrollRef?: React.RefObject;
 }
 
 const NavContext = createContext<{
@@ -34,8 +35,9 @@ export default function Nav({
   title = null,
   titleHref = "/",
   children = ,
+  scrollRef,
 }: NavProps) {
-  const scrollY = useWindowScrollPosition();
+  const scrollY = useScrollPosition(scrollRef);
 
   return (
     
@@ -113,6 +115,30 @@ export function NavSpace() {
   return 
; } +interface NavLinksProps { + children: React.ReactNode; +} + +export function NavLinks({ children }: NavLinksProps) { + return
{children}
; +} + +interface NavLinkProps { + href: string; + children: React.ReactNode; +} + +export function NavLink({ href, children }: NavLinkProps) { + return ( + + {children} + + ); +} + interface NavProjectDescriptionProps { id: string; title: string; diff --git a/components/ReadonlyCodeDemo.tsx b/components/ReadonlyCodeDemo.tsx new file mode 100644 index 0000000..a2333e3 --- /dev/null +++ b/components/ReadonlyCodeDemo.tsx @@ -0,0 +1,142 @@ +"use client"; + +import { useMemo } from "react"; +import { Code } from "./Code"; + +interface ReadonlyCodeDemoProps { + html?: string; + css?: string; + js?: string; + describeLanguages?: boolean; + previewCSS?: string; +} + +export default function ReadonlyCodeDemo({ + html, + css, + js, + describeLanguages = [html, css, js].filter(Boolean).length > 1, + previewCSS, +}: ReadonlyCodeDemoProps) { + const previewURL = useCodePreviewURL({ + html, + css: `${css || ""}${previewCSS || ""}`, + js, + }); + + return ( +
+
+ {html && ( +
+ + HTML + +
+ + {html} + +
+
+ )} + {css && ( +
+ + CSS + +
+ + {css} + +
+
+ )} + {js && ( +
+ + JS + +
+ + {js} + +
+
+ )} +
+
+ Preview +