{post.title}
{post.summary}
{post.repo}
import { useEffect, useState } from "preact/hooks";
import { MarkdownContent, stripLeadingTitleHeading } from "./MarkdownContent";
import type {
CourseCard,
CourseChapter,
CourseLesson,
DiscussionCard,
DiscussionReply,
EventCard,
PostCard,
PrototypeData,
} from "./types";
function formatTimestamp(value: string): string {
if (!value) {
return "Unknown time";
}
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return "Unknown time";
}
return new Intl.DateTimeFormat("en-US", {
month: "short",
day: "numeric",
hour: "numeric",
minute: "2-digit",
}).format(date);
}
function normalizePathname(pathname: string): string {
if (!pathname || pathname === "/") {
return "/";
}
return pathname.replace(/\/+$/, "") || "/";
}
function parseDiscussionRoute(pathname: string): number | null {
const match = normalizePathname(pathname).match(/^\/discussions\/(\d+)$/);
if (!match) {
return null;
}
const issueId = Number(match[1]);
return Number.isFinite(issueId) ? issueId : null;
}
function parseCourseRoute(pathname: string): { owner: string; repo: string } | null {
const match = normalizePathname(pathname).match(/^\/courses\/([^/]+)\/([^/]+)$/);
if (!match) {
return null;
}
return {
owner: decodeURIComponent(match[1]),
repo: decodeURIComponent(match[2]),
};
}
function parseLessonRoute(pathname: string): {
owner: string;
repo: string;
chapter: string;
lesson: string;
} | null {
const match = normalizePathname(pathname).match(
/^\/courses\/([^/]+)\/([^/]+)\/lessons\/([^/]+)\/([^/]+)$/,
);
if (!match) {
return null;
}
return {
owner: decodeURIComponent(match[1]),
repo: decodeURIComponent(match[2]),
chapter: decodeURIComponent(match[3]),
lesson: decodeURIComponent(match[4]),
};
}
function normalizeRouteKey(value: string): string {
return decodeURIComponent(value).trim().toLowerCase();
}
function findCourseByRoute(
courses: CourseCard[],
route: { owner: string; repo: string },
): CourseCard | undefined {
const routeOwner = normalizeRouteKey(route.owner);
const routeRepo = normalizeRouteKey(route.repo);
const routeFullName = `${routeOwner}/${routeRepo}`;
return courses.find((course) => {
const [repoOwner = "", repoName = ""] = course.repo.split("/", 2);
const courseOwner = normalizeRouteKey(course.owner || repoOwner);
const courseName = normalizeRouteKey(course.name || repoName);
const courseFullName = normalizeRouteKey(course.repo);
return (
(courseOwner === routeOwner && courseName === routeRepo) ||
courseFullName === routeFullName ||
courseName === routeRepo
);
});
}
function findLessonByRoute(
course: CourseCard,
route: { chapter: string; lesson: string },
): { chapter: CourseChapter; lesson: CourseLesson } | undefined {
const routeChapter = normalizeRouteKey(route.chapter);
const routeLesson = normalizeRouteKey(route.lesson);
for (const chapter of course.outline) {
if (normalizeRouteKey(chapter.slug) !== routeChapter) {
continue;
}
const lesson = chapter.lessons.find((entry) => normalizeRouteKey(entry.slug) === routeLesson);
if (lesson) {
return { chapter, lesson };
}
}
return undefined;
}
function usePathname() {
const [pathname, setPathname] = useState(() => normalizePathname(window.location.pathname));
useEffect(() => {
function handlePopState() {
setPathname(normalizePathname(window.location.pathname));
}
window.addEventListener("popstate", handlePopState);
return () => {
window.removeEventListener("popstate", handlePopState);
};
}, []);
function navigate(nextPath: string) {
const normalized = normalizePathname(nextPath);
if (normalized === pathname) {
return;
}
window.history.pushState({}, "", normalized);
window.scrollTo({ top: 0, behavior: "auto" });
setPathname(normalized);
}
return { pathname, navigate };
}
function SectionHeader(props: { title: string }) {
return (
{props.title}
{props.copy}
; } function CourseItem(props: { course: CourseCard; onOpenCourse: (course: CourseCard) => void }) { const { course, onOpenCourse } = props; return ( ); } function PostItem(props: { post: PostCard }) { const { post } = props; return ({post.summary}
{post.repo}
{event.when}
{event.source}
{reply.author}
{formatTimestamp(reply.created_at)}
{course.repo} · {course.lessons} lessons · {course.chapters} chapters
{course.summary}
{course.repo} · {chapter.title}
{lesson.summary}
: null}{discussion.repo} · Issue #{discussion.number} · {discussion.author} ·{" "} {formatTimestamp(discussion.updated_at)}
{discussion.comments.length}
Robot U
A single place for lessons, member work, and community conversation.
{error}