From d06bd9ba2af197ded6ed2d00e0ca1e8e9d1bdca3 Mon Sep 17 00:00:00 2001 From: Ajay Dhangar Date: Tue, 3 Nov 2026 19:57:01 +0530 Subject: [PATCH] added Bookmarks compo --- src/components/auth/DashboardLayout.tsx | 588 +++++++++------ src/components/learning/BookmarkButton.tsx | 42 +- src/pages/bookmarks/bookmarks.module.css | 139 ++++ src/pages/bookmarks/index.tsx | 694 ++++++++++++++---- src/theme/DocBreadcrumbs/index.tsx | 155 ++++ src/theme/DocBreadcrumbs/styles.module.css | 87 +++ src/theme/MDXComponents.js | 12 - src/types/learning.ts | 64 +- tutorials/html/14-seo/index.mdx | 2 - tutorials/html/15-performance/async.mdx | 0 .../html/15-performance/core-web-vitals.mdx | 0 .../15-performance/critical-resources.mdx | 0 tutorials/html/15-performance/defer.mdx | 0 .../html/15-performance/dns-prefetch.mdx | 432 +++++++++++ .../html-performance-basics.mdx | 663 +++++++++++++++++ tutorials/html/15-performance/index.mdx | 130 +--- .../html/15-performance/lazy-loading.mdx | 535 ++++++++++++++ tutorials/html/15-performance/preconnect.mdx | 470 ++++++++++++ tutorials/html/15-performance/prefetch.mdx | 435 +++++++++++ tutorials/html/15-performance/preload.mdx | 520 +++++++++++++ .../html/15-performance/resource-hints.mdx | 526 +++++++++++++ .../responsive-images-performance.mdx | 0 .../html/15-performance/script-loading.mdx | 0 tutorials/html/index.mdx | 8 - 24 files changed, 4904 insertions(+), 598 deletions(-) create mode 100644 src/pages/bookmarks/bookmarks.module.css create mode 100644 src/theme/DocBreadcrumbs/index.tsx create mode 100644 src/theme/DocBreadcrumbs/styles.module.css create mode 100644 tutorials/html/15-performance/async.mdx create mode 100644 tutorials/html/15-performance/core-web-vitals.mdx create mode 100644 tutorials/html/15-performance/critical-resources.mdx create mode 100644 tutorials/html/15-performance/defer.mdx create mode 100644 tutorials/html/15-performance/dns-prefetch.mdx create mode 100644 tutorials/html/15-performance/lazy-loading.mdx create mode 100644 tutorials/html/15-performance/preconnect.mdx create mode 100644 tutorials/html/15-performance/prefetch.mdx create mode 100644 tutorials/html/15-performance/preload.mdx create mode 100644 tutorials/html/15-performance/resource-hints.mdx create mode 100644 tutorials/html/15-performance/responsive-images-performance.mdx create mode 100644 tutorials/html/15-performance/script-loading.mdx diff --git a/src/components/auth/DashboardLayout.tsx b/src/components/auth/DashboardLayout.tsx index 615c9c69e..f10453ddc 100644 --- a/src/components/auth/DashboardLayout.tsx +++ b/src/components/auth/DashboardLayout.tsx @@ -1,9 +1,27 @@ import React, { useEffect, useState } from "react"; -// @ts-ignore import Layout from "@theme/Layout"; -// @ts-ignore import Link from "@docusaurus/Link"; -// @ts-ignore +import { useLocation } from "@docusaurus/router"; +import { + ArrowUpRight, + BookOpen, + Bookmark, + ChevronRight, + Code2, + ExternalLink, + FolderKanban, + LayoutDashboard, + LogOut, + Menu, + Moon, + Settings, + ShieldCheck, + Sparkles, + Sun, + UserRound, + X, +} from "lucide-react"; + import { useAuth } from "@site/src/context/AuthContext"; type DashboardLayoutProps = { @@ -12,11 +30,53 @@ type DashboardLayoutProps = { children: React.ReactNode; }; -const navigation = [ - { label: "Overview", href: "/dashboard", icon: "⌂" }, - { label: "My profile", href: "/profile", icon: "◎" }, - { label: "Account", href: "/settings/account", icon: "⚙" }, - { label: "Security", href: "/settings/security", icon: "◇" }, +const workspaceNavigation = [ + { + label: "Overview", + href: "/dashboard", + icon: LayoutDashboard, + }, + { + label: "My profile", + href: "/profile", + icon: UserRound, + }, + { + label: "Bookmarks", + href: "/bookmarks", + icon: Bookmark, + }, +]; + +const accountNavigation = [ + { + label: "Account settings", + href: "/settings/account", + icon: Settings, + }, + { + label: "Security", + href: "/settings/security", + icon: ShieldCheck, + }, +]; + +const exploreNavigation = [ + { + label: "Tutorials", + href: "/tutorials/", + icon: BookOpen, + }, + { + label: "Learning paths", + href: "/roadmap", + icon: Code2, + }, + { + label: "Projects", + href: "/projects", + icon: FolderKanban, + }, ]; function getInitials(name?: string) { @@ -30,36 +90,70 @@ function getInitials(name?: string) { .join(""); } -function getGreeting(hour: number) { +function getGreeting() { + const hour = new Date().getHours(); + if (hour < 12) return "Good morning"; if (hour < 17) return "Good afternoon"; + return "Good evening"; } +function normalizePath(path: string) { + return path.replace(/\/+$/, "") || "/"; +} + +function isRouteActive(currentPath: string, href: string) { + const current = normalizePath(currentPath); + const target = normalizePath(href); + + return current === target || current.startsWith(`${target}/`); +} + export default function DashboardLayout({ title, eyebrow = "CODEHARBORHUB WORKSPACE", children, }: DashboardLayoutProps) { const { user, logout } = useAuth(); + const location = useLocation(); - const [now, setNow] = useState(() => new Date()); const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const [signingOut, setSigningOut] = useState(false); + const displayName = user?.name?.trim() || "Developer"; + const initials = getInitials(user?.name); + const greeting = getGreeting(); + + const currentDate = new Date().toLocaleDateString([], { + weekday: "long", + day: "numeric", + month: "long", + year: "numeric", + }); + + // Close the mobile drawer when navigating or pressing Escape. useEffect(() => { - const timer = window.setInterval(() => { - setNow(new Date()); - }, 1000); + setMobileMenuOpen(false); + }, [location.pathname]); - return () => window.clearInterval(timer); - }, []); + useEffect(() => { + if (!mobileMenuOpen) return; - const displayName = user?.name?.trim() || "Developer"; - const initials = getInitials(user?.name); - const greeting = getGreeting(now.getHours()); + function handleKeyDown(event: KeyboardEvent) { + if (event.key === "Escape") { + setMobileMenuOpen(false); + } + } + + window.addEventListener("keydown", handleKeyDown); - function signOut() { + return () => { + window.removeEventListener("keydown", handleKeyDown); + }; + }, [mobileMenuOpen]); + + function handleSignOut() { if (signingOut) return; setSigningOut(true); @@ -72,262 +166,320 @@ export default function DashboardLayout({ } } + const renderNavigation = ( + items: typeof workspaceNavigation, + label: string + ) => ( +
+

+ {label} +

+ + {items.map((item) => { + const active = isRouteActive( + location.pathname, + item.href + ); + + const Icon = item.icon; + + return ( + + + + {item.label} + + {active && ( + + )} + + ); + })} +
+ ); + + const sidebar = ( +
+ {/* Brand */} + +
+ CH +
+ +
+

+ CodeHarborHub +

+ +

+ Developer workspace +

+
+ + + {/* Main navigation */} + + + {/* Community card */} +
+
+ +
+ +

+ Keep learning +

+ +

+ Explore tutorials, build projects, and grow your developer skills. +

+ + + Explore tutorials + + +
+ + {/* Session status */} +
+
+ + + + Demo environment + +
+ +

+ Your session is stored locally in this browser. +

+
+ + {/* Sign out */} + +
+ ); + return ( -
- {/* Workspace background decoration */} +
+ {/* Background decoration */}
); -} +} \ No newline at end of file diff --git a/src/components/learning/BookmarkButton.tsx b/src/components/learning/BookmarkButton.tsx index 1d309c168..b9a6caed7 100644 --- a/src/components/learning/BookmarkButton.tsx +++ b/src/components/learning/BookmarkButton.tsx @@ -1,9 +1,9 @@ - -import React from "react"; +import React, { useCallback } from "react"; // @ts-ignore import { useLearning } from "@site/src/context/LearningContext"; // @ts-ignore import type { BookmarkType } from "@site/src/types/learning"; +import { FaRegBookmark, FaBookmark } from 'react-icons/fa'; interface BookmarkButtonProps { id: string; @@ -23,10 +23,10 @@ export default function BookmarkButton({ compact = false, }: BookmarkButtonProps) { const { isBookmarked, toggleBookmark } = useLearning(); - const saved = isBookmarked(id); - function handleClick() { + const handleClick = useCallback((e: React.MouseEvent) => { + e.preventDefault(); toggleBookmark({ id, title, @@ -34,32 +34,30 @@ export default function BookmarkButton({ type, description, }); - } + }, [id, title, url, type, description, toggleBookmark]); return ( ); } diff --git a/src/pages/bookmarks/bookmarks.module.css b/src/pages/bookmarks/bookmarks.module.css new file mode 100644 index 000000000..4eb5898db --- /dev/null +++ b/src/pages/bookmarks/bookmarks.module.css @@ -0,0 +1,139 @@ +.container { + width: min(1100px, calc(100% - 2rem)); + margin: 3rem auto; + min-height: 70vh; +} + +.hero { + text-align: center; + padding: 3rem 1rem; + border-radius: 24px; + background: var(--ifm-color-emphasis-100); +} + +.eyebrow { + font-size: 0.75rem; + font-weight: 800; + letter-spacing: 0.15em; + color: var(--ifm-color-primary); +} + +.hero h1 { + margin: 1rem 0; + font-size: clamp(2.2rem, 6vw, 3.5rem); +} + +.hero h1 span { + color: var(--ifm-color-primary); +} + +.hero p { + max-width: 600px; + margin: auto; + color: var(--ifm-color-emphasis-700); +} + +.stats { + display: flex; + justify-content: center; + gap: 3rem; + margin-top: 2rem; +} + +.stats div { + display: flex; + flex-direction: column; + gap: 0.3rem; +} + +.stats strong { + font-size: 1.8rem; +} + +.stats span { + color: var(--ifm-color-emphasis-700); + font-size: 0.85rem; +} + +.toolbar { + display: grid; + grid-template-columns: 2fr 1fr 1fr; + gap: 1rem; + margin: 2rem 0 1rem; +} + +.toolbar input, +.toolbar select { + min-width: 0; + padding: 0.8rem; + border: 1px solid var(--ifm-color-emphasis-300); + border-radius: 10px; + background: var(--ifm-background-surface-color); + color: var(--ifm-font-color-base); +} + +.actions { + display: flex; + gap: 0.75rem; + flex-wrap: wrap; + margin-bottom: 1.5rem; +} + +.actions button { + padding: 0.65rem 1rem; + border: 1px solid var(--ifm-color-emphasis-300); + border-radius: 9px; + background: var(--ifm-background-surface-color); + color: var(--ifm-font-color-base); + cursor: pointer; +} + +.actions button:hover { + border-color: var(--ifm-color-primary); +} + +.actions .danger { + color: var(--ifm-color-danger); +} + +.grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 1rem; +} + +.empty { + padding: 4rem 1rem; + text-align: center; + border: 1px dashed var(--ifm-color-emphasis-400); + border-radius: 20px; +} + +.empty p { + color: var(--ifm-color-emphasis-700); +} + +.emptyIcon { + font-size: 3rem; + margin-bottom: 1rem; +} + +@media (max-width: 800px) { + .grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .toolbar { + grid-template-columns: 1fr; + } +} + +@media (max-width: 550px) { + .grid { + grid-template-columns: 1fr; + } + + .stats { + gap: 1.5rem; + } +} \ No newline at end of file diff --git a/src/pages/bookmarks/index.tsx b/src/pages/bookmarks/index.tsx index e1b1b406d..2eb26cffa 100644 --- a/src/pages/bookmarks/index.tsx +++ b/src/pages/bookmarks/index.tsx @@ -1,227 +1,601 @@ import React, { useMemo, useState } from "react"; -// @ts-ignore import Link from "@docusaurus/Link"; + +import { + Bookmark, + BookOpen, + GraduationCap, + Map, + Code2, + Link2, + Search, + Clock3, + ArrowUpRight, + Trash2, + X, + Library, + SlidersHorizontal, + BookMarked, + Sparkles, + RotateCcw, + Layers3, +} from "lucide-react"; + // @ts-ignore import ProtectedRoute from "@site/src/components/auth/ProtectedRoute"; // @ts-ignore import DashboardLayout from "@site/src/components/auth/DashboardLayout"; // @ts-ignore import { useLearning } from "@site/src/context/LearningContext"; -// import type { BookmarkType } from "@site/src/types/learning"; - -const filters: Array<{ label: string; value: string }> = [ - { label: "All", value: "all" }, - { label: "Tutorials", value: "tutorial" }, - { label: "Lessons", value: "lesson" }, - { label: "Roadmaps", value: "roadmap" }, - { label: "Challenges", value: "challenge" }, - { label: "Resources", value: "resource" }, +// @ts-ignore +import type { BookmarkType } from "@site/src/types/learning"; + +// -------------------------------------------------- +// Configuration +// -------------------------------------------------- + +const filters: { + label: string; + value: "all" | BookmarkType; + icon: React.ElementType; +}[] = [ + { label: "All items", value: "all", icon: Layers3 }, + { label: "Tutorials", value: "tutorial", icon: BookOpen }, + { label: "Lessons", value: "lesson", icon: GraduationCap }, + { label: "Roadmaps", value: "roadmap", icon: Map }, + { label: "Challenges", value: "challenge", icon: Code2 }, + { label: "Resources", value: "resource", icon: Link2 }, ]; -function BookmarksContent() { +function getCategoryIcon(type: string) { + switch (type) { + case "tutorial": + return BookOpen; + case "lesson": + return GraduationCap; + case "roadmap": + return Map; + case "challenge": + return Code2; + case "resource": + return Link2; + default: + return Bookmark; + } +} + +function formatDate(date?: string) { + if (!date) return "Recently saved"; + + const parsed = new Date(date); + + if (Number.isNaN(parsed.getTime())) { + return "Previously saved"; + } + + return parsed.toLocaleDateString(undefined, { + day: "numeric", + month: "short", + year: "numeric", + }); +} + +// -------------------------------------------------- +// Reusable Stat Card +// -------------------------------------------------- + +interface StatCardProps { + label: string; + value: number; + icon: React.ElementType; + description: string; +} + +function StatCard({ + label, + value, + icon: Icon, + description, +}: StatCardProps) { + return ( +
+
+ + {label} + + +
+ +
+
+ +
+ {value} +
+ +

+ {description} +

+
+ ); +} + +// -------------------------------------------------- +// Bookmark Card +// -------------------------------------------------- + +interface BookmarkCardProps { + bookmark: { + id: string; + title: string; + url: string; + type: BookmarkType; + description?: string; + addedAt: string; + }; + onRemove: (id: string) => void; +} + +function BookmarkCard({ + bookmark, + onRemove, +}: BookmarkCardProps) { + const Icon = getCategoryIcon(bookmark.type); + + return ( +
+ + {/* Card Header */} +
+ +
+
+ +
+ + + {bookmark.type} + +
+ + +
+ + {/* Card Content */} +
+

+ {bookmark.title} +

+ +

+ {bookmark.description || + "Continue exploring this learning resource on CodeHarborHub."} +

+
+ + {/* Card Footer */} +
+ + + + {formatDate(bookmark.addedAt)} + + + + Open resource + + +
+
+ ); +} + +// -------------------------------------------------- +// Empty State +// -------------------------------------------------- + +function EmptyState({ + hasFilters, + onReset, +}: { + hasFilters: boolean; + onReset: () => void; +}) { + return ( +
+ +
+ {hasFilters ? ( + + ) : ( + + )} +
+ +

+ {hasFilters + ? "No matching resources" + : "Your library is waiting"} +

+ +

+ {hasFilters + ? "We couldn't find any bookmarks matching your search or selected category. Try adjusting your filters." + : "Save your favorite tutorials, lessons, roadmaps, and coding challenges to build your personal learning library."} +

+ + {hasFilters ? ( + + ) : ( + + + Explore tutorials + + + )} +
+ ); +} + +// -------------------------------------------------- +// Main Dashboard +// -------------------------------------------------- + +function BookmarksContent(): JSX.Element { const { learning, removeBookmark, isReady } = useLearning(); const [search, setSearch] = useState(""); - const [activeFilter, setActiveFilter] = useState("all"); + const [activeFilter, setActiveFilter] = + useState<"all" | BookmarkType>("all"); + + const [sortBy, setSortBy] = useState< + "newest" | "oldest" | "title" + >("newest"); + const allBookmarks = learning?.bookmarks ?? []; + + // Search, filter and sort. const bookmarks = useMemo(() => { - return learning.bookmarks.filter((bookmark: any) => { + const query = search.trim().toLowerCase(); + + const result = allBookmarks.filter((bookmark) => { + const title = bookmark.title ?? ""; + const description = bookmark.description ?? ""; + const url = bookmark.url ?? ""; + const matchesSearch = - bookmark.title.toLowerCase().includes(search.toLowerCase()) || - bookmark.description?.toLowerCase().includes(search.toLowerCase()); + title.toLowerCase().includes(query) || + description.toLowerCase().includes(query) || + url.toLowerCase().includes(query); const matchesFilter = - activeFilter === "all" || bookmark.type === activeFilter; + activeFilter === "all" || + bookmark.type === activeFilter; return matchesSearch && matchesFilter; }); - }, [learning.bookmarks, search, activeFilter]); + + return result.sort((a, b) => { + if (sortBy === "title") { + return a.title.localeCompare(b.title); + } + + const first = Date.parse(a.addedAt) || 0; + const second = Date.parse(b.addedAt) || 0; + + return sortBy === "newest" + ? second - first + : first - second; + }); + }, [allBookmarks, search, activeFilter, sortBy]); + + const countByType = (type: BookmarkType) => + allBookmarks.filter((item) => item.type === type).length; + + const hasFilters = + Boolean(search.trim()) || activeFilter !== "all"; + + const resetFilters = () => { + setSearch(""); + setActiveFilter("all"); + setSortBy("newest"); + }; if (!isReady) { return ( - -

- Loading your saved resources... -

+ +
+
+ +

+ Loading your personal library... +

+
); } return ( - -
- {/* Header */} -
-
-
- - YOUR PERSONAL LIBRARY - - -

- Saved for later -

- -

- Keep useful tutorials, roadmaps, and coding resources - together. -

-
+ +
-
- 🔖 -
-
+ {/* ---------------------------------------- + HERO SECTION + ---------------------------------------- */} -
-
-

- Total saved -

-

- {learning.bookmarks.length} -

-
+
+ + {/* Decorative elements */} +
+ +
+ +
+ +
+
+ + CodeHarborHub Library +
-
-

- Tutorials -

-

- {learning.bookmarks.filter((b) => b.type === "tutorial").length} -

+

+ Your knowledge. +
+ + Always within reach. + +

+ +

+ Your personal collection of saved tutorials, + programming lessons, roadmaps, challenges, + and developer resources. Pick up exactly + where you left off. +

+
+ +
+ +
-
-

- Challenges -

-

- {learning.bookmarks.filter((b) => b.type === "challenge").length} -

+ {/* Statistics */} +
+ + + + + + +
- {/* Search */} + {/* ---------------------------------------- + SEARCH & FILTER TOOLBAR + ---------------------------------------- */} +
- setSearch(event.target.value)} - placeholder="Search your saved resources..." - aria-label="Search bookmarks" - className="w-full rounded-xl border border-[var(--ifm-color-emphasis-300)] bg-[var(--ifm-background-surface-color)] px-4 py-3 text-sm text-[var(--ifm-font-color-base)] outline-none transition focus:border-[var(--ifm-color-primary)]" - /> -
- {filters.map((filter) => ( - + )} +
+ +
+ + +
-
+ Sort by + - {/* Bookmark cards */} - {bookmarks.length > 0 ? ( -
- {bookmarks.map((bookmark: any) => ( -
+ setSortBy(event.target.value as typeof sortBy) + } + className="rounded-xl border border-[var(--ifm-color-emphasis-300)] bg-[var(--ifm-background-surface-color)] px-3 py-2.5 text-sm text-[var(--ifm-font-color-base)] outline-none focus:border-[var(--ifm-color-primary)]" > -
- - {bookmark.type} - + + + + +
+
- -
+ {/* Category filters */} +
-

- {bookmark.title} -

+ {filters.map((filter) => { + const active = activeFilter === filter.value; + const Icon = filter.icon; -

- {bookmark.description || "Saved learning resource."} -

+ const count = + filter.value === "all" + ? allBookmarks.length + : countByType(filter.value); -
- - Saved {new Date(bookmark.addedAt).toLocaleDateString()} - + return ( +
- - ))} + {count} + + + ); + })}
- ) : ( -
-
🔖
+
+ + {/* ---------------------------------------- + RESULTS HEADER + ---------------------------------------- */} -

- {search || activeFilter !== "all" - ? "No matching bookmarks" - : "Your library is waiting"} +
+ +
+

+ + Saved resources

-

- {search || activeFilter !== "all" - ? "Try another search or select a different category." - : "Bookmark tutorials, programming lessons, roadmaps, and challenges to find them here."} +

+ Showing {bookmarks.length} of {allBookmarks.length} resources

+
- - Explore tutorials → - + + Reset filters + + )} +
+ + {/* ---------------------------------------- + BOOKMARK GRID + ---------------------------------------- */} + + {bookmarks.length > 0 ? ( +
+ + {bookmarks.map((bookmark) => ( + + ))} + +
+ ) : ( + + )} + + {/* Footer */} + {allBookmarks.length > 0 && ( +
+ + Your learning library is saved in this browser.
)} +

); } -export default function BookmarksPage() { +// -------------------------------------------------- +// Protected Dashboard Page +// -------------------------------------------------- + +export default function BookmarksPage(): JSX.Element { return ( diff --git a/src/theme/DocBreadcrumbs/index.tsx b/src/theme/DocBreadcrumbs/index.tsx new file mode 100644 index 000000000..e72505a39 --- /dev/null +++ b/src/theme/DocBreadcrumbs/index.tsx @@ -0,0 +1,155 @@ +import React, { type ReactNode } from 'react'; +import clsx from 'clsx'; +import Link from '@docusaurus/Link'; +import { translate } from '@docusaurus/Translate'; +import { ThemeClassNames } from '@docusaurus/theme-common'; +import { useHomePageRoute } from '@docusaurus/theme-common/internal'; +import { + useSidebarBreadcrumbs, + useDoc, +} from '@docusaurus/plugin-content-docs/client'; + +import HomeBreadcrumbItem from '@theme/DocBreadcrumbs/Items/Home'; +import BookmarkButton from "@site/src/components/learning/BookmarkButton"; +import styles from './styles.module.css'; + +interface BreadcrumbsItemLinkProps { + children: ReactNode; + href?: string; + isLast: boolean; +} + +/** + * BreadcrumbsItemLink Component + * Renders a breadcrumb link or a non-clickable span for the last item in the trail. + */ +function BreadcrumbsItemLink({ + children, + href, + isLast, +}: BreadcrumbsItemLinkProps): ReactNode { + const className = clsx('breadcrumbs__link', { + [styles.activeItem]: isLast, + [styles.breadcrumbLink]: !isLast, + }); + + if (isLast) { + return ( + + {children} + + ); + } + + return href ? ( + + {children} + + ) : ( + {children} + ); +} + +interface BreadcrumbsItemProps { + children: ReactNode; + active?: boolean; +} + +/** + * Wrapper for individual breadcrumb `
  • ` items. + */ +function BreadcrumbsItem({ children, active }: BreadcrumbsItemProps): ReactNode { + return ( +
  • + {children} +
  • + ); +} + +/** + * Safely extract doc metadata without throwing outside of + */ +function useSafeDocMetadata() { + try { + const { metadata } = useDoc(); + return metadata; + } catch { + return null; + } +} + +/** + * Enhanced DocBreadcrumbs Component for CodeHarborHub + */ +export default function DocBreadcrumbs(): ReactNode { + const breadcrumbs = useSidebarBreadcrumbs(); + const homePageRoute = useHomePageRoute(); + const docMetadata = useSafeDocMetadata(); + + // If there are no breadcrumbs to display, omit component + if (!breadcrumbs || breadcrumbs.length === 0) { + return null; + } + + const lastBreadcrumb = breadcrumbs[breadcrumbs.length - 1]; + + // 1. DYNAMIC COMPONENT STATE KEYS: Fixes multi-page global click tracking bug + const bookmarkId = docMetadata?.unversionedId || docMetadata?.id || lastBreadcrumb?.href || lastBreadcrumb?.label || 'unknown-page'; + const bookmarkTitle = docMetadata?.title ?? lastBreadcrumb?.label ?? 'Documentation'; + const bookmarkUrl = docMetadata?.permalink ?? lastBreadcrumb?.href ?? ''; + const bookmarkDescription = docMetadata?.description ?? ''; + + const ariaLabel = translate({ + id: 'theme.docs.breadcrumbs.navAriaLabel', + message: 'Breadcrumbs', + description: 'The ARIA label for the breadcrumbs', + }); + + return ( + + ); +} diff --git a/src/theme/DocBreadcrumbs/styles.module.css b/src/theme/DocBreadcrumbs/styles.module.css new file mode 100644 index 000000000..af642d2be --- /dev/null +++ b/src/theme/DocBreadcrumbs/styles.module.css @@ -0,0 +1,87 @@ +/* Container bar holding everything */ +.breadcrumbContainer { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem; + width: 100%; + margin-bottom: 1.25rem; + padding: 0.25rem 0; +} + +/* Action button wrappers */ +.actionGroup { + display: flex; + align-items: center; + flex-shrink: 0; +} + +/* Scrollable Breadcrumb Trail for Mobile & Desktop */ +.breadcrumbsWrapper { + display: flex; + align-items: center; + flex-grow: 1; + overflow-x: auto; + white-space: nowrap; + /* Hide scrollbar for Chrome, Safari, and Opera */ + -webkit-overflow-scrolling: touch; + scrollbar-width: none; /* Firefox */ +} + +.breadcrumbsWrapper::-webkit-scrollbar { + display: none; /* Chrome, Safari */ +} + +/* Breadcrumbs List (Ol) */ +.breadcrumbsList { + display: flex; + align-items: center; + margin: 0; + padding: 0; + list-style: none; +} + +.breadcrumbItem { + display: inline-flex; + align-items: center; +} + +.breadcrumbLink { + transition: opacity 0.2s ease, color 0.2s ease; + font-weight: 500; +} + +.breadcrumbLink:hover { + text-decoration: none; + opacity: 0.8; +} + +.activeItem { + font-weight: 600; + color: var(--ifm-color-primary); + cursor: default; +} + +/* Responsive adjustments */ +@media (max-width: 640px) { + .breadcrumbContainer { + flex-wrap: wrap; + gap: 0.5rem; + } + + /* Make trail take priority on top row */ + .breadcrumbsWrapper { + order: 1; + width: 100%; + } + + /* Buttons align to bottom bar on small devices */ + .leftAction { + order: 2; + } + + .rightAction { + order: 3; + margin-left: auto; + } +} \ No newline at end of file diff --git a/src/theme/MDXComponents.js b/src/theme/MDXComponents.js index f557effc0..7102ca371 100644 --- a/src/theme/MDXComponents.js +++ b/src/theme/MDXComponents.js @@ -19,7 +19,6 @@ import Admonition from '@theme/Admonition'; import TOCInline from '@theme/TOCInline'; import CodePenEmbed from '@site/src/components/CodePenEmbed'; import CodePreview from '@site/src/components/CodePreview'; -import BookmarkButton from "@site/src/components/learning/BookmarkButton"; export default { // Re-use the default mapping @@ -43,15 +42,4 @@ export default { TOCInline, CodePenEmbed, CodePreview, - BookmarkButton, - // }; diff --git a/src/types/learning.ts b/src/types/learning.ts index 51a1a0902..057d6df81 100644 --- a/src/types/learning.ts +++ b/src/types/learning.ts @@ -1,12 +1,13 @@ - export type BookmarkType = | "tutorial" - | "lesson" - | "roadmap" - | "challenge" - | "resource"; + | "article" + | "course" + | "documentation" + | "resource" + | "project" + | "other"; -export interface LearningBookmark { +export interface BookmarkItem { id: string; title: string; url: string; @@ -15,45 +16,12 @@ export interface LearningBookmark { addedAt: string; } -export interface CompletedLesson { - id: string; - title: string; - courseId: string; - courseTitle: string; - completedAt: string; - xpEarned: number; -} - -export interface CompletedChallenge { - id: string; - title: string; - completedAt: string; - xpEarned: number; -} - -export interface LearningActivity { - id: string; - title: string; - type: "lesson" | "challenge" | "bookmark"; - date: string; -} - -export interface LearningState { - bookmarks: LearningBookmark[]; - completedLessons: CompletedLesson[]; - completedChallenges: CompletedChallenge[]; - activities: LearningActivity[]; - xp: number; - level: number; - streak: number; - lastActiveDate: string | null; - dailyGoal: number; - dailyCompleted: number; - dailyGoalDate: string; - lastVisited: { - title: string; - url: string; - courseTitle?: string; - visitedAt: string; - } | null; -} +export interface LearningContextType { + bookmarks: BookmarkItem[]; + isBookmarked: (id: string) => boolean; + toggleBookmark: (bookmark: Omit) => void; + removeBookmark: (id: string) => void; + clearBookmarks: () => void; + exportBookmarks: () => string; + importBookmarks: (data: string) => boolean; +} \ No newline at end of file diff --git a/tutorials/html/14-seo/index.mdx b/tutorials/html/14-seo/index.mdx index e1034d226..e2c27154f 100644 --- a/tutorials/html/14-seo/index.mdx +++ b/tutorials/html/14-seo/index.mdx @@ -292,8 +292,6 @@ Test what you already believe about SEO: ## What Is in This Section -### Foundations - ## Tools You Will Use diff --git a/tutorials/html/15-performance/async.mdx b/tutorials/html/15-performance/async.mdx new file mode 100644 index 000000000..e69de29bb diff --git a/tutorials/html/15-performance/core-web-vitals.mdx b/tutorials/html/15-performance/core-web-vitals.mdx new file mode 100644 index 000000000..e69de29bb diff --git a/tutorials/html/15-performance/critical-resources.mdx b/tutorials/html/15-performance/critical-resources.mdx new file mode 100644 index 000000000..e69de29bb diff --git a/tutorials/html/15-performance/defer.mdx b/tutorials/html/15-performance/defer.mdx new file mode 100644 index 000000000..e69de29bb diff --git a/tutorials/html/15-performance/dns-prefetch.mdx b/tutorials/html/15-performance/dns-prefetch.mdx new file mode 100644 index 000000000..5ab5671c0 --- /dev/null +++ b/tutorials/html/15-performance/dns-prefetch.mdx @@ -0,0 +1,432 @@ +--- +id: html-perf-dns-prefetch +title: "DNS Prefetch in HTML" +sidebar_label: "DNS Prefetch" +sidebar_position: 6 +slug: /html/performance/dns-prefetch +description: "Learn link rel=dns-prefetch: a tiny, cheap hint that looks up a server's address early. See what DNS is, what the hint saves, how it differs from preconnect, and which hosts deserve it." +keywords: + - dns-prefetch + - link rel dns-prefetch + - DNS lookup + - dns-prefetch vs preconnect + - third-party hosts + - connection time + - HTML performance + - learn HTML + - CodeHarborHub HTML +tags: [html, web-development, beginner, performance, dns-prefetch, resource-hints, third-party, frontend] +--- + +import {useState} from 'react'; +import AdsComponent from '@site/src/components/AdsComponent'; + +export function connectionWait(o) { + const hintAt = 200; + const total = o.dns + o.tcp + o.tls; + if (o.needAt <= hintAt) return {none: total, dnsPrefetch: total, preconnect: total}; + return { + none: total, + dnsPrefetch: Math.max(0, hintAt + o.dns - o.needAt) + o.tcp + o.tls, + preconnect: Math.max(0, hintAt + total - o.needAt) + }; +} + +export function ConnectionLab() { + const [dns, setDns] = useState(60); + const [tcp, setTcp] = useState(60); + const [tls, setTls] = useState(100); + const [needAt, setNeedAt] = useState(800); + const w = connectionWait({dns: dns, tcp: tcp, tls: tls, needAt: needAt}); + const presets = [ + ["Nearby", 20, 20, 40], + ["Typical", 60, 60, 100], + ["Far or slow", 150, 200, 300] + ]; + const rows = [ + ["No hint", w.none, "#c0392b"], + ["dns-prefetch", w.dnsPrefetch, "#b9770e"], + ["preconnect", w.preconnect, "#1e8449"] + ]; + const slider = function (id, label, value, set, max) { + return ( +

    + +
    + +

    + ); + }; + return ( +
    +
    + {presets.map(function (p) { + return ( + + ); + })} +
    + {slider("cl-dns", "DNS lookup", dns, setDns, 300)} + {slider("cl-tcp", "Connecting", tcp, setTcp, 300)} + {slider("cl-tls", "Security handshake", tls, setTls, 400)} + {slider("cl-need", "The page first needs this host at", needAt, setNeedAt, 2000)} +

    Time the page waits for the connection

    + {rows.map(function (r) { + return ( +
    +
    {r[0]}: {r[1]} ms
    +
    +
    +
    +
    + ); + })} +

    + dns-prefetch saves at most the DNS part ({dns} ms). Preconnect can save the whole setup, if it has time to finish before the page needs the host. +

    +

    + This is a simplified model. It assumes the hint is found at 200 ms and that each step takes the time you set. Real numbers vary a lot with distance, network, and caching, so measure your own. +

    +
    + ); +} + +export const hostItems = [ + { + host: "fonts.gstatic.com", + use: "Your headline font loads from here, on the first screen.", + answer: "preconnect", + why: "You need this host straight away, so open the whole connection. Fonts load in CORS mode, so add crossorigin." + }, + { + host: "analytics.example.com", + use: "An analytics script that loads after the page is ready.", + answer: "dns-prefetch", + why: "It is not needed early, and nothing visual waits for it. A cheap DNS lookup is enough." + }, + { + host: "cdn.example.com", + use: "Your product photos on the first screen come from here.", + answer: "preconnect", + why: "Important, visible, and needed at once. A full connection saves the most." + }, + { + host: "chat.example.com", + use: "A chat widget that only loads when the visitor clicks the chat button.", + answer: "dns-prefetch", + why: "It may never be used, so the cheap hint fits. A full connection might sit unused." + }, + { + host: "your own site (www.example.com)", + use: "All your own pages, styles, and images.", + answer: "none", + why: "The browser has already looked up and connected to the host that served the page. A hint adds nothing." + }, + { + host: "ads.example.com", + use: "An advertising service, loaded only if the visitor gives consent.", + answer: "none", + why: "Do not contact it until consent is given. Add the hint (or load the script) only after the visitor agrees." + }, + { + host: "another-blog.example.org", + use: "A link in your footer to a friend's website. Few visitors click it.", + answer: "none", + why: "The chance is small and the benefit is tiny. Hints are best kept for hosts the page really uses." + } +]; + +export function HostSorter() { + const [picked, setPicked] = useState({}); + const [checked, setChecked] = useState(false); + const options = [["", "Choose..."], ["preconnect", "preconnect"], ["dns-prefetch", "dns-prefetch"], ["none", "No hint"]]; + const score = hostItems.filter(function (h, i) { return picked[i] === h.answer; }).length; + return ( +
    + {hostItems.map(function (h, i) { + const right = picked[i] === h.answer; + return ( +
    + +
    + + {checked ? ( +

    + {right ? "Right. " : "Not quite. "} + {h.why} +

    + ) : null} +
    + ); + })} + {!checked ? ( + + ) : ( +

    + Score: {score} of {hostItems.length}.{" "} + +

    + )} +
    + ); +} + +# DNS Prefetch in HTML + +Before your browser can fetch anything from a server, it has to find that server. Humans use names such as `cdn.example.com`. Computers use numbers. The step that turns one into the other is called a **DNS lookup**, and it takes time, every time the browser meets a new host. + +`dns-prefetch` is the smallest resource hint there is. It tells the browser, "do the lookup for this host now, while you have a spare moment." It does nothing else, and costs almost nothing. This lesson shows you what it saves, where it is useful, and why it is often paired with its bigger sibling, `preconnect`. + +## What You Will Learn + +- What a DNS lookup is, and why it takes time +- What `dns-prefetch` does, and what it does not do +- How it differs from `preconnect` +- Which hosts deserve the hint, and which do not +- How to use it as a fallback next to `preconnect` +- How to see DNS time in DevTools + + +
    + +## The Big Idea + +A DNS prefetch is a `link` element with the address of a host. Only the host matters, not the file. + + + + +```html title="index.html" + + + The Bread Bin + + + + + + + +``` + + + + + +<> + + + + + + + +In the lab, compare the three bars. Notice how `dns-prefetch` only trims the DNS part of the wait, while `preconnect` can remove much more, if it has enough time to finish. + +## What Is DNS? + +Think of DNS as the phone book of the internet. You know a name, `cdn.example.com`, and the phone book gives you a number, such as `203.0.113.7`. Only then can your browser "call" the server. + +The lookup asks a chain of servers, and the answers are remembered for a while, by the browser, your device, and your internet provider. So a host you have used recently is often quick, and a host you have never used may take longer. The difference can be small (a few milliseconds) or large (hundreds, on a slow or distant network), and it varies a lot. + +## The Steps of a New Connection + +Fetching a file from a **new** server takes several steps before any data arrives. Each hint covers different steps. + +| Step | What happens | `dns-prefetch` | `preconnect` | +| :-- | :-- | :-- | :-- | +| 1. DNS lookup | Find the server's address | Yes | Yes | +| 2. Connect | Open a line to the server | No | Yes | +| 3. Secure handshake | Agree on encryption (for https) | No | Yes | +| 4. Request | Ask for the file | No | No | + +So `dns-prefetch` does one step of three. That is why it is so cheap, and also why it saves less. Next lesson, [Preconnect](./preconnect), covers the full set. + +## How to Write It + +```html + +``` + +- **Use the origin only:** the scheme and host, such as `https://cdn.example.com`. A path adds nothing, because DNS cares only about the host. +- **You do not need `crossorigin`.** A lookup is the same however the files are later fetched. +- **Add one line per host,** and keep the list short. + +## When to Use It + +`dns-prefetch` is a good fit for hosts that are **third-party** (not your own site), and that the page **may need** later, but not at once: + +- Analytics and tag managers +- Chat or support widgets +- Embedded videos or maps further down the page +- Social buttons +- Any host that comes up after a script runs + +It does nothing for your own site, because the browser has already looked up the host that served the page. + +## Pairing It with Preconnect + +A very common pattern puts both hints on the same host: + +```html + + +``` + +Why both? Browsers that do not understand `preconnect` can still do the DNS lookup, and some browsers limit how many connections they will open early. The lookup acts as a safety net. Check [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel/dns-prefetch) for current browser support of each. + +## Sort the Hosts + +Here is a bakery page that uses seven hosts. For each one, choose the hint it deserves, or none. + + +<> + + + + +:::tip +A simple rule: **needed straight away, so `preconnect`. Maybe needed later, so `dns-prefetch`. Your own site or unlikely, so nothing.** +::: + +## When It Does Not Help + +- **Your own site.** The lookup is already done. +- **Hosts the page never uses.** You pay a small cost for nothing. +- **Hosts used much later.** The answer is only remembered for a limited time, so a lookup that is far ahead of its use may be repeated. +- **Many hosts.** A long list adds noise, and each lookup uses a little battery and network on mobile. +- **A visitor with a fast, warm DNS cache.** The lookup may be nearly free anyway. + +Because the cost is so low, a few unneeded `dns-prefetch` hints do little harm. But a careful list is still better than a long one. + +## Checking That It Worked + +1. Open DevTools, then **Network**, and reload. +2. Click a request to a third-party host, and open the **Timing** tab. +3. Look at **DNS Lookup**. With a good prefetch it may be zero, or not shown, because the lookup already finished. +4. Compare with the same page without the hint. Remember that DNS caching at several levels can hide the difference on a second visit, so clear the cache or use a test tool that starts fresh, such as WebPageTest. +5. Check that you have not hinted any host the page never uses. + +## Best Practices + +1. **Hint third-party hosts you may use,** not your own. +2. **Use origins only.** +3. **Pair it with `preconnect`** for hosts that matter most. +4. **Keep the list short.** +5. **Remove hints** when you stop using a service. +6. **Do not build hints from untrusted input,** such as user-submitted links. +7. **Test on a fresh cache** and a slow connection. +8. **Measure.** A hint that does not move a number is clutter. + +## Quick Checklist + +- [ ] Every hinted host is used by the page, or likely to be +- [ ] No hint for your own site +- [ ] Each hint is an origin, with no path +- [ ] Hosts needed immediately use `preconnect`, with `dns-prefetch` as a fallback +- [ ] No hosts that need consent are hinted before consent +- [ ] Old services have had their hints removed +- [ ] DevTools shows the DNS time dropped + +## Security and Privacy Notes + +- **A lookup tells the network something.** When your page asks for a lookup, the visitor's DNS resolver (often their internet provider) learns that this page uses that host. A hint triggers the lookup earlier, and for hosts the visitor may never actually use. If you need consent before contacting certain third parties (analytics or advertising, for example), do not hint those hosts until consent is given. This is not legal advice, so check the rules that apply to you. +- **Never create hints from untrusted input.** If a page turns a user-submitted link into a `dns-prefetch`, the visitor's browser looks up a host chosen by someone else, which can be used to track visitors. Generate hints only from a list you control. +- **Only hint hosts you trust.** Even a lookup is a small way of reaching out. +- **Encrypted DNS** (offered by some browsers and providers) hides lookups from people on the network, but not from the resolver. It is the visitor's choice, not something your HTML controls. + +## Global Audience Notes + +- **DNS speed varies a lot by country, provider, and network.** Visitors on slow or distant networks may see lookups of hundreds of milliseconds, so they gain most from an early lookup. +- **Third-party hosts can be slow, or blocked, in some places.** A page must still work if a host never answers, so never make the page depend on a hinted service. +- **Mobile networks pay a price for every lookup,** in time and battery. A short list is kind to them. +- **Test from different regions,** using a tool such as WebPageTest, since DNS behaviour differs around the world. + +## Common Mistakes to Avoid + +1. **Hinting your own site.** +2. **Including a path** and thinking it matters. +3. **Hinting dozens of hosts.** +4. **Hinting hosts the page never uses.** +5. **Using `dns-prefetch` for a host needed straight away** when `preconnect` would save more. +6. **Hinting before consent** for services that need it. +7. **Building hints from user input.** +8. **Leaving hints for services you stopped using.** +9. **Adding `crossorigin`,** which is not needed for DNS lookups. +10. **Assuming it always helps,** without testing on a fresh cache. + +## Practice Time + +1. In the Connection Lab, use the "Far or slow" preset. How much does `dns-prefetch` save? How much does `preconnect` save? +2. Set "the page first needs this host at" to 200 ms or less. Why does no hint help any more? Then slowly raise it, and watch each hint start to save time. +3. Sort the hosts. Which one surprised you? +4. List every third-party host on a page you built, using the Network panel. +5. Decide which need `preconnect`, which need `dns-prefetch`, and which need nothing. +6. Add the hints, and check the **DNS Lookup** time in the Timing tab. +7. Remove a hint for a host the page no longer uses. +8. Bonus: run WebPageTest on your page and look at the connection view. + +## Quick Recap + +Click a question to check your answer. + +
    +1. What does `dns-prefetch` do? + +It asks the browser to look up a host's address early, so that step is already done when the page needs the host. It does nothing else. + +
    + +
    +2. What does `preconnect` add? + +It also opens the connection and finishes the security handshake, so more of the setup is done in advance. It costs a little more, because the connection stays open. + +
    + +
    +3. Which hosts deserve `dns-prefetch`? + +Third-party hosts the page may need later, such as analytics, chat widgets, and embedded media. Not your own site. + +
    + +
    +4. Why pair `dns-prefetch` with `preconnect`? + +It is a safety net: browsers that do not support `preconnect`, or that limit early connections, can still do the lookup. + +
    + +
    +5. Do you need `crossorigin` on `dns-prefetch`? + +No. The lookup is the same whichever mode the files are fetched in. (`preconnect` is different, and may need it.) + +
    + +
    +6. What privacy risk does it carry? + +It triggers lookups, which tell the visitor's DNS resolver about the hosts your page uses, and may contact services before consent. Never build hints from untrusted input. + +
    + +## Key Takeaways + +- **DNS turns a host name into an address,** and it takes time for each new host. +- **`dns-prefetch` does just that step, early.** It is cheap, and saves less. +- **Use it for third-party hosts you may need,** and `preconnect` for those you need at once. +- **Origins only, no `crossorigin`.** +- **Keep the list short,** and remove hints you no longer need. +- **Mind privacy:** wait for consent where it is needed, and never hint untrusted hosts. +- **Measure** the DNS time in DevTools, on a fresh cache. + +Next up: [Preconnect](./preconnect), the fuller hint that opens the whole connection early. \ No newline at end of file diff --git a/tutorials/html/15-performance/html-performance-basics.mdx b/tutorials/html/15-performance/html-performance-basics.mdx index e69de29bb..e97c83fd5 100644 --- a/tutorials/html/15-performance/html-performance-basics.mdx +++ b/tutorials/html/15-performance/html-performance-basics.mdx @@ -0,0 +1,663 @@ +--- +id: html-performance-basics +title: "HTML Performance Basics" +sidebar_label: "HTML Performance Basics" +sidebar_position: 2 +slug: /html/performance/html-performance-basics +description: "Learn the HTML habits behind a fast page: keep the head light, never block on scripts, size and lazy-load images, and ask four simple questions about every file. Includes a live page checker." +keywords: + - HTML performance basics + - page speed for beginners + - render blocking CSS + - defer scripts + - image width and height + - lazy loading images + - layout shift + - fast HTML + - learn HTML + - CodeHarborHub HTML +tags: [html, web-development, beginner, performance, page-speed, images, scripts, frontend] +--- + +import {useState} from 'react'; +import BrowserOnly from '@docusaurus/BrowserOnly'; + +export const perfSamples = { + messy: [ + '', + '', + '', + ' ', + ' ', + ' The Bread Bin', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '', + '', + ' A golden loaf', + ' Rye loaves', + ' Sourdough on a rack', + ' ', + '', + '' + ].join("\n"), + fixed: [ + '', + '', + '', + ' ', + ' ', + ' The Bread Bin', + ' ', + ' ', + '', + '', + ' A golden loaf', + ' Rye loaves', + ' Sourdough on a rack', + ' ', + '', + '' + ].join("\n") +}; + +export function auditPerformance(html) { + const doc = new DOMParser().parseFromString(html, "text/html"); + const out = []; + const add = (level, text) => out.push({level, text}); + const headScripts = Array.from(doc.head.querySelectorAll("script[src]")); + const blocking = headScripts.filter(function (s) { + return !s.hasAttribute("defer") && !s.hasAttribute("async") && s.getAttribute("type") !== "module"; + }); + if (blocking.length > 0) { + add("problem", blocking.length + " script(s) in the head block the page. Add defer or async."); + } else if (headScripts.length > 0) { + add("good", "Every script in the head uses defer, async or type=module."); + } + const styles = Array.from(doc.querySelectorAll("style")).map(function (s) { return s.textContent; }).join("\n"); + if (/@import/i.test(styles)) { + add("problem", "A style block uses @import. The browser finds the second file only after the first arrives, so use a link element instead."); + } + const sheets = doc.querySelectorAll('link[rel~="stylesheet"]'); + if (sheets.length > 3) { + add("tip", sheets.length + " separate stylesheet files. Each one is a request that can hold up the first paint, so combine or trim them where you can."); + } else if (sheets.length > 0) { + add("good", "A small number of stylesheets (" + sheets.length + ")."); + } + const imgs = Array.from(doc.querySelectorAll("img")); + if (imgs.length > 0 && imgs[0].getAttribute("loading") === "lazy") { + add("problem", "The first image is lazy-loaded. If it is on the first screen, remove loading=lazy so it starts early."); + } + const noSize = imgs.filter(function (i) { return !i.hasAttribute("width") || !i.hasAttribute("height"); }); + if (noSize.length > 0) { + add("problem", noSize.length + " image(s) have no width and height, so the layout can jump when they arrive."); + } else if (imgs.length > 0) { + add("good", "Every image has width and height."); + } + const lazyCandidates = imgs.slice(1).filter(function (i) { return i.getAttribute("loading") !== "lazy"; }); + if (lazyCandidates.length > 0) { + add("tip", lazyCandidates.length + " later image(s) could use loading=lazy if they sit below the first screen."); + } + const frames = Array.from(doc.querySelectorAll("iframe")); + const eagerFrames = frames.filter(function (f) { return f.getAttribute("loading") !== "lazy"; }); + if (eagerFrames.length > 0) { + add("tip", eagerFrames.length + " iframe(s) load straight away. If they are below the first screen, add loading=lazy."); + } + const frameNoSize = frames.filter(function (f) { return !f.hasAttribute("width") || !f.hasAttribute("height"); }); + if (frameNoSize.length > 0) { + add("problem", frameNoSize.length + " iframe(s) have no width and height, so the page can jump when they load."); + } + if (out.length === 0) add("good", "Nothing to check here. Paste a page with a head, images or scripts."); + return out; +} + +export function PerfCheckerInner() { + const [html, setHtml] = useState(perfSamples.messy); + const results = auditPerformance(html); + const problems = results.filter(function (r) { return r.level === "problem"; }).length; + const colors = {problem: "#c0392b", tip: "#b9770e", good: "#1e8449"}; + const labels = {problem: "Problem", tip: "Tip", good: "Good"}; + return ( +
    +
    + + +
    + +