Skip to content

Commit 85e02a2

Browse files
committed
Scaffold course landing page
1 parent 2e8da20 commit 85e02a2

File tree

7 files changed

+326
-19
lines changed

7 files changed

+326
-19
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"fathom-client": "^3.7.2",
2323
"feed": "^4.2.2",
2424
"framer-motion": "^11.16.0",
25+
"motion": "^12.4.7",
2526
"next": "15.1.6",
2627
"open-graph-scraper-lite": "^2.1.0",
2728
"react": "^19.0.0",

pnpm-lock.yaml

Lines changed: 60 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/app/course/call-to-action.tsx

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
"use client";
2+
3+
import { useRef } from "react";
4+
import { useState } from "react";
5+
import { AnimatePresence, motion } from "motion/react";
6+
7+
export function SignUpForm() {
8+
return (
9+
<div className="flex">
10+
<div className="flex items-center rounded-full bg-white">
11+
<input
12+
autoFocus
13+
type="email"
14+
id="email"
15+
className="w-3xs rounded-full bg-transparent px-4 py-2 text-sm/6 text-gray-950 focus:outline-none"
16+
placeholder="Enter your email"
17+
aria-label="Email address"
18+
/>
19+
<button className="mr-0.5 shrink-0 overflow-hidden rounded-full bg-gray-950 px-3 py-1.5 text-sm/6 font-semibold text-nowrap text-white hover:bg-gray-950/85">
20+
Sign up
21+
</button>
22+
</div>
23+
</div>
24+
);
25+
}
26+
27+
export function HeroActions() {
28+
let [isExpanded, setIsExpanded] = useState(false);
29+
let input = useRef<HTMLInputElement>(null);
30+
let getCourseButton = useRef(null);
31+
let containerRef = useRef<HTMLInputElement>(null);
32+
33+
const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
34+
if (!input.current) return;
35+
36+
// Check if the related target (where focus is going) is within our container
37+
if (input.current.value.length === 0 && !containerRef.current?.contains(e.relatedTarget)) {
38+
setIsExpanded(false);
39+
}
40+
};
41+
42+
return (
43+
<div className="group relative flex gap-4">
44+
<div className="isolate flex items-center">
45+
<motion.div
46+
ref={containerRef}
47+
layout
48+
transition={{ duration: isExpanded ? 0.1 : 0.2, ease: "circOut" }}
49+
className="relative flex items-center bg-white data-expanded:overflow-hidden"
50+
style={{ borderRadius: 20 }}
51+
data-expanded={isExpanded || undefined}
52+
>
53+
<AnimatePresence mode="popLayout" initial={false}>
54+
{isExpanded ? (
55+
<motion.div
56+
className="flex items-center"
57+
layout
58+
onKeyDown={(e) => {
59+
if (e.key === "Escape" && input.current) {
60+
input.current.value = "";
61+
setIsExpanded(false);
62+
}
63+
}}
64+
exit={{
65+
opacity: 0,
66+
transition: {
67+
duration: 0.2,
68+
},
69+
}}
70+
initial={{
71+
opacity: 0,
72+
}}
73+
animate={{
74+
opacity: 1,
75+
}}
76+
transition={{ duration: 0.1 }}
77+
>
78+
<input
79+
autoFocus
80+
ref={input}
81+
onBlur={handleBlur}
82+
type="email"
83+
id="email"
84+
className="w-3xs rounded-full bg-transparent px-4 py-2 text-sm/6 text-gray-950 focus:outline-none"
85+
placeholder="Enter your email"
86+
aria-label="Email address"
87+
/>
88+
<button className="mr-0.5 shrink-0 overflow-hidden rounded-full bg-gray-950 px-3 py-1.5 text-sm/6 font-semibold text-nowrap text-white hover:bg-gray-950/85">
89+
Sign up
90+
</button>
91+
</motion.div>
92+
) : (
93+
<motion.button
94+
layout
95+
ref={getCourseButton}
96+
key="get-course-button"
97+
onClick={() => {
98+
setIsExpanded(true);
99+
}}
100+
type="button"
101+
className="inline-flex rounded-full bg-transparent px-4 py-2 text-sm/6 font-semibold text-gray-950 hover:bg-gray-100 focus:outline-none focus-visible:outline focus-visible:outline-offset-2 focus-visible:outline-white focus-visible:outline-solid"
102+
exit={{
103+
opacity: 0,
104+
transition: {
105+
duration: 0.05,
106+
},
107+
}}
108+
initial={{
109+
opacity: 0,
110+
}}
111+
animate={{
112+
opacity: 1,
113+
}}
114+
transition={{ duration: 0.2 }}
115+
>
116+
Get the free course &rarr;
117+
</motion.button>
118+
)}
119+
</AnimatePresence>
120+
</motion.div>
121+
</div>
122+
<AnimatePresence initial={false}>
123+
{!isExpanded && (
124+
<motion.button
125+
layout
126+
transition={{ duration: 0.3 }}
127+
className="inline-flex flex-nowrap items-baseline gap-1.5 self-center rounded-full bg-white/25 px-4 py-2 pl-3 text-sm/6 font-semibold whitespace-nowrap text-white hover:bg-white/30"
128+
initial={{
129+
opacity: 0,
130+
}}
131+
animate={{
132+
opacity: 1,
133+
}}
134+
exit={{
135+
opacity: 0,
136+
}}
137+
>
138+
<svg width={20} height={20} fill="none" className="self-center stroke-white">
139+
<path d="M19 10a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
140+
<path
141+
d="M13.91 9.67a.37.37 0 0 1 0 .66l-5.6 3.11a.38.38 0 0 1-.56-.33V6.9c0-.29.3-.47.56-.33l5.6 3.11Z"
142+
strokeLinecap="round"
143+
strokeLinejoin="round"
144+
/>
145+
</svg>
146+
Watch intro video
147+
</motion.button>
148+
)}
149+
</AnimatePresence>
150+
</div>
151+
);
152+
}

src/app/course/layout.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import React from "react";
2+
import { Header } from "@/components/header";
3+
4+
export default async function Layout({ children }: React.PropsWithChildren) {
5+
return (
6+
<div className="max-w-screen overflow-x-hidden [html:has(&)]:bg-gray-950">
7+
<div className="grid min-h-dvh grid-cols-1 grid-rows-[1fr_1px_auto_1px_auto] justify-center [--gutter-width:2.5rem] sm:grid-cols-[var(--gutter-width)_minmax(0,var(--breakpoint-2xl))_var(--gutter-width)]">
8+
{/* Candy cane */}
9+
<div className="col-start-1 row-span-full row-start-1 hidden border-x border-x-(--pattern-fg) bg-[image:repeating-linear-gradient(315deg,_var(--pattern-fg)_0,_var(--pattern-fg)_1px,_transparent_0,_transparent_50%)] bg-[size:10px_10px] bg-fixed [--pattern-fg:var(--color-white)]/10 sm:block"></div>
10+
11+
{/* Main content area */}
12+
<div className="text-white">{children}</div>
13+
14+
{/* Candy cane */}
15+
<div className="row-span-full row-start-1 hidden border-x border-x-(--pattern-fg) bg-[image:repeating-linear-gradient(315deg,_var(--pattern-fg)_0,_var(--pattern-fg)_1px,_transparent_0,_transparent_50%)] bg-[size:10px_10px] bg-fixed [--pattern-fg:var(--color-white)]/10 sm:col-start-3 sm:block"></div>
16+
</div>
17+
</div>
18+
);
19+
}

src/app/course/page.tsx

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import BaseContainer from "@/components/grid-container";
2+
import { Logo } from "@/components/logo";
3+
import { HeroActions, SignUpForm } from "./call-to-action";
4+
5+
function GridContainer({ children }: { children: React.ReactNode }) {
6+
return (
7+
<BaseContainer>
8+
<div className="p-2">{children}</div>
9+
</BaseContainer>
10+
);
11+
}
12+
13+
export default async function Course() {
14+
return (
15+
<div className="dark py-8">
16+
<GridContainer>
17+
<div className="p-2">
18+
<Logo className="h-7" />
19+
</div>
20+
</GridContainer>
21+
<div className="mt-20 flex flex-col gap-4 sm:mt-24">
22+
<GridContainer>
23+
<p className="font-mono text-sm/6 tracking-wider text-gray-400 uppercase">5-day mini-course</p>
24+
<h1 className="mt-2 text-5xl tracking-tighter text-balance text-white sm:text-8xl">
25+
Build UIs that don't suck.
26+
</h1>
27+
</GridContainer>
28+
<GridContainer>
29+
<p className="max-w-2xl text-lg/7 font-medium text-gray-400">
30+
<strong className="font-medium text-white">Short, tactical video lessons</strong> from the creator of
31+
TailwindCSS, delivered directly to your inbox{" "}
32+
<strong className="font-medium text-white">every day for a week</strong>.
33+
</p>
34+
</GridContainer>
35+
<GridContainer>
36+
<HeroActions />
37+
</GridContainer>
38+
</div>
39+
<div className="pt-14 pb-28">
40+
<div className="max-w-xl space-y-8 text-[0.9375rem]/7 text-gray-300">
41+
<p>
42+
When you build UI components that are used by tens of thousands of developers, you learn to really care
43+
about the details, like:
44+
</p>
45+
<ul className="list-[square] space-y-4 pl-8 marker:text-white/60">
46+
<li className="pl-2">
47+
Building layouts that don't break when the content is longer than you planned for in Figma
48+
</li>
49+
<li className="pl-2">Getting avatars to stand out from the page, no matter what colors are in the image</li>
50+
<li className="pl-2">
51+
Fine-tuning click targets for mobile, without making everything else harder to maintain
52+
</li>
53+
<li className="pl-2">Making sure keyboards shortcuts are perfectly aligned in menus</li>
54+
<li className="pl-2">Getting the border radius mathematically perfect on nested elements</li>
55+
<li className="pl-2">
56+
Adding horizontal scrolling to a table, without the content getting cropped by the page padding
57+
</li>
58+
</ul>
59+
<p>
60+
Build UIs that don't suck is a crash course in some of the coolest tricks I've picked up over the years
61+
building things that need to be both beautiful and bullet-proof.
62+
</p>
63+
64+
<p>
65+
Every day for a week I'll send you a short video lesson walking you through an interesting UI problem, as
66+
well as the code so you can play with it yourself and adapt it for your own projects.
67+
</p>
68+
</div>
69+
<div className="mt-32">
70+
<SignUpForm />
71+
</div>
72+
</div>
73+
</div>
74+
);
75+
}

src/components/header.tsx

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,25 +7,7 @@ import { useRouter } from "next/navigation";
77
import { useState } from "react";
88
import { IconButton } from "./icon-button";
99
import { SearchButton } from "./search";
10-
11-
function Logo(props: React.ComponentProps<"svg">) {
12-
return (
13-
<svg viewBox="0 0 162 24" fill="none" {...props}>
14-
<path
15-
fillRule="evenodd"
16-
clipRule="evenodd"
17-
d="M16.668 2c-4.445 0-7.223 2.222-8.334 6.667 1.667-2.222 3.611-3.055 5.833-2.5 1.268.317 2.175 1.236 3.178 2.255C18.979 10.081 20.87 12 25 12c4.445 0 7.223-2.222 8.334-6.666-1.666 2.222-3.61 3.055-5.833 2.5-1.269-.318-2.175-1.237-3.178-2.255C22.69 3.919 20.8 2 16.668 2zM8.334 12C3.889 12 1.11 14.222 0 18.667c1.667-2.222 3.612-3.056 5.833-2.5 1.269.316 2.175 1.236 3.178 2.255C10.645 20.081 12.536 22 16.668 22c4.444 0 7.222-2.222 8.333-6.666-1.667 2.222-3.611 3.055-5.833 2.5-1.268-.317-2.175-1.238-3.177-2.255C14.356 13.92 12.463 12 8.334 12z"
18-
className="fill-sky-400"
19-
/>
20-
<path
21-
fillRule="evenodd"
22-
clipRule="evenodd"
23-
d="M50 10.427h-2.908v5.63c0 1.501.985 1.477 2.909 1.383v2.276c-3.895.47-5.443-.61-5.443-3.66v-5.63H42.4V7.988h2.158v-3.15l2.534-.751v3.901H50v2.44zm11.088-2.44h2.533v11.729h-2.533v-1.689c-.892 1.243-2.276 1.994-4.105 1.994-3.19 0-5.841-2.698-5.841-6.17 0-3.494 2.65-6.169 5.84-6.169 1.83 0 3.215.75 4.106 1.97V7.988zm-3.706 9.618c2.111 0 3.706-1.572 3.706-3.754s-1.595-3.753-3.706-3.753c-2.111 0-3.706 1.572-3.706 3.753 0 2.182 1.595 3.754 3.706 3.754zM67.844 6.228c-.891 0-1.618-.75-1.618-1.619.002-.43.173-.842.476-1.145a1.612 1.612 0 012.283 0c.304.303.475.715.477 1.145 0 .868-.727 1.619-1.618 1.619zm-1.267 13.488V7.987h2.534v11.729h-2.534zm5.466 0V2.59h2.533v17.124h-2.533zM91.021 7.987h2.674l-3.683 11.729h-2.487l-2.44-7.905-2.463 7.905h-2.486L76.453 7.987h2.674l2.276 8.092 2.463-8.092h2.416l2.44 8.092 2.299-8.092zm5.817-1.759c-.892 0-1.619-.75-1.619-1.619.003-.43.174-.842.477-1.145a1.612 1.612 0 012.284 0c.303.303.475.715.477 1.145 0 .868-.727 1.619-1.619 1.619zm-1.266 13.488V7.987h2.533v11.729H95.57zm11.634-12.034c2.628 0 4.504 1.783 4.504 4.833v7.2h-2.533v-6.943c0-1.783-1.032-2.72-2.627-2.72-1.666 0-2.979.985-2.979 3.377v6.287h-2.534V7.987h2.534V9.49c.774-1.22 2.04-1.807 3.635-1.807zm16.515-4.386h2.533v16.42h-2.533v-1.69c-.891 1.244-2.275 1.994-4.105 1.994-3.19 0-5.841-2.697-5.841-6.17 0-3.494 2.651-6.168 5.841-6.168 1.83 0 3.214.75 4.105 1.97V3.296zm-3.707 14.309c2.112 0 3.707-1.572 3.707-3.754s-1.595-3.753-3.707-3.753c-2.111 0-3.706 1.572-3.706 3.753 0 2.182 1.595 3.754 3.706 3.754zm14.732 2.416c-3.542 0-6.193-2.698-6.193-6.17 0-3.494 2.651-6.169 6.193-6.169 2.299 0 4.293 1.196 5.231 3.026l-2.182 1.267c-.516-1.102-1.665-1.806-3.072-1.806-2.065 0-3.636 1.572-3.636 3.682 0 2.111 1.571 3.683 3.636 3.683 1.407 0 2.556-.727 3.119-1.806l2.182 1.243c-.985 1.853-2.979 3.05-5.278 3.05zm9.453-8.797c0 2.135 6.311.844 6.311 5.185 0 2.346-2.041 3.612-4.574 3.612-2.346 0-4.035-1.056-4.786-2.745l2.182-1.266c.375 1.055 1.313 1.689 2.604 1.689 1.126 0 1.993-.376 1.993-1.315 0-2.087-6.31-.914-6.31-5.113 0-2.205 1.9-3.589 4.293-3.589 1.924 0 3.519.892 4.34 2.44l-2.135 1.196c-.422-.915-1.243-1.337-2.205-1.337-.915 0-1.713.399-1.713 1.243zm10.815 0c0 2.135 6.31.844 6.31 5.185 0 2.346-2.041 3.612-4.575 3.612-2.345 0-4.034-1.056-4.785-2.745l2.182-1.266c.375 1.055 1.313 1.689 2.603 1.689 1.126 0 1.995-.376 1.995-1.315 0-2.087-6.31-.914-6.31-5.113 0-2.205 1.899-3.589 4.292-3.589 1.924 0 3.519.892 4.34 2.44l-2.135 1.196c-.422-.915-1.243-1.337-2.205-1.337-.915 0-1.712.399-1.712 1.243z"
24-
fill="currentColor"
25-
/>
26-
</svg>
27-
);
28-
}
10+
import { Logo } from "./logo";
2911

3012
function GitHubLogo(props: React.ComponentProps<"svg">) {
3113
return (

0 commit comments

Comments
 (0)