Newer
Older
hello-programmer-world / src / components / Toc.astro
@h.sakamoto h.sakamoto 10 days ago 3 KB reference
---
type PageItem = {
	url: string;
	title: string;
};

type Section = {
	title: string;
	items: PageItem[];
};

async function getSectionPages(sectionPath: string): Promise<PageItem[]> {
	const allPages = await import.meta.glob(`../pages/**/*.mdx`);

	const filteredPages = Object.entries(allPages).filter(([path, _]) =>
		path.includes(`/pages/${sectionPath}/`),
	);

	const files = await Promise.all(
		filteredPages.map(async ([filePath, resolver]) => {
			const module = await resolver();
			return {
				file: filePath,
				title: module.title || module.frontmatter?.title || "",
				url: module.url || "",
			};
		}),
	);

	return files;
}

const currentPath = Astro.url.pathname;

const sections: Section[] = [];
const sectionPaths = [
	"html",
	"js",
	"cli",
	"php",
	"sql",
	"git",
	"tips",
	"reference",
];

for (const path of sectionPaths) {
	const pages = await getSectionPages(path);
	if (pages.length > 0) {
		sections.push({
			title: path.charAt(0).toUpperCase() + path.slice(1),
			items: pages,
		});
	}
}
---

<div class="toc-wrapper">
  <nav class="toc">
    <ul class="toc-list">
      <li class="toc-item">
        <a href="/" class="toc-link">Introduction</a>
      </li>

      {sections.map(section => {
      const isActive = section.items.some(item => currentPath.startsWith(item.url));
      return (
      <li class="toc-item">
        <details open={isActive}>
          <summary class="toc-section-title">{section.title}</summary>
          <ul class="toc-sublist">
            {section.items.map(item => (
            <li class="toc-subitem">
              <a href={item.url} class="toc-link">{item.title}</a>
            </li>
            ))}
          </ul>
        </details>
      </li>
      );
      })}
    </ul>
  </nav>
</div>

<style>
  .toc {
    font-size: 14px;
    position: sticky;
    top: 32px;
  }

  .toc-list {
    list-style: none;
    padding: 0;
    margin: 0;
  }

  .toc-item {
    margin: 0;
  }

  .toc-link {
    display: block;
    padding: 4px 8px;
    color: #0969da;
    text-decoration: none;
    border-radius: 6px;
  }

  .toc-link:hover {
    background-color: #f6f8fa;
    text-decoration: none;
  }

  details {
    margin: 4px 0;
  }

  summary {
    cursor: pointer;
    padding: 4px 8px;
    font-weight: 600;
    color: #1f2328;
    border-radius: 6px;
    list-style: none;
  }

  summary::-webkit-details-marker {
    display: none;
  }

  summary::before {
    content: '▶';
    display: inline-block;
    margin-right: 6px;
    font-size: 10px;
    transition: transform 0.2s;
  }

  details[open] summary::before {
    transform: rotate(90deg);
  }

  summary:hover {
    background-color: #f6f8fa;
  }

  .toc-sublist {
    list-style: none;
    padding-left: 16px;
    margin: 4px 0;
  }

  .toc-subitem {
    margin: 0;
  }

  @media (prefers-color-scheme: dark) {
    .toc-link {
      color: #4493f8;
    }

    .toc-link:hover {
      background-color: #161b22;
    }

    summary {
      color: #e6edf3;
    }

    summary:hover {
      background-color: #161b22;
    }
  }
</style>