Full Code of markhorn-dev/astro-sphere for AI

main f7e3a40f20e1 cached
68 files
103.8 KB
30.1k tokens
37 symbols
1 requests
Download .txt
Repository: markhorn-dev/astro-sphere
Branch: main
Commit: f7e3a40f20e1
Files: 68
Total size: 103.8 KB

Directory structure:
gitextract_txjs_ovg/

├── .github/
│   └── workflows/
│       └── stale.yaml
├── .gitignore
├── .vscode/
│   ├── extensions.json
│   ├── launch.json
│   └── settings.json
├── LICENSE
├── README.md
├── astro.config.mjs
├── package.json
├── public/
│   ├── js/
│   │   ├── animate.js
│   │   ├── bg.js
│   │   ├── copy.js
│   │   ├── scroll.js
│   │   └── theme.js
│   └── robots.txt
├── src/
│   ├── components/
│   │   ├── ArrowCard.tsx
│   │   ├── BaseHead.astro
│   │   ├── Container.astro
│   │   ├── Counter.tsx
│   │   ├── Drawer.astro
│   │   ├── Footer.astro
│   │   ├── Header.astro
│   │   ├── MeteorShower.astro
│   │   ├── Search.tsx
│   │   ├── SearchBar.tsx
│   │   ├── SearchCollection.tsx
│   │   ├── StackCard.astro
│   │   └── TwinklingStars.astro
│   ├── consts.ts
│   ├── content/
│   │   ├── blog/
│   │   │   ├── 01-astro-sphere-file-structure/
│   │   │   │   └── index.md
│   │   │   ├── 02-astro-sphere-getting-started/
│   │   │   │   └── index.md
│   │   │   ├── 03-astro-sphere-add-new-post-or-projects/
│   │   │   │   └── index.md
│   │   │   ├── 04-astro-sphere-writing-markdown/
│   │   │   │   └── index.md
│   │   │   ├── 05-astro-sphere-writing-mdx/
│   │   │   │   ├── MyComponent.astro
│   │   │   │   └── index.mdx
│   │   │   └── 06-astro-sphere-social-links/
│   │   │       └── index.md
│   │   ├── config.ts
│   │   ├── legal/
│   │   │   ├── privacy.md
│   │   │   └── terms.md
│   │   ├── projects/
│   │   │   ├── project-1/
│   │   │   │   └── index.md
│   │   │   ├── project-2/
│   │   │   │   └── index.md
│   │   │   ├── project-3/
│   │   │   │   └── index.md
│   │   │   └── project-4/
│   │   │       └── index.md
│   │   └── work/
│   │       ├── apple.md
│   │       ├── facebook.md
│   │       ├── google.md
│   │       └── mcdonalds.md
│   ├── env.d.ts
│   ├── layouts/
│   │   ├── ArticleBottomLayout.astro
│   │   ├── ArticleTopLayout.astro
│   │   ├── BottomLayout.astro
│   │   ├── PageLayout.astro
│   │   └── TopLayout.astro
│   ├── lib/
│   │   └── utils.ts
│   ├── pages/
│   │   ├── blog/
│   │   │   ├── [...slug].astro
│   │   │   └── index.astro
│   │   ├── index.astro
│   │   ├── legal/
│   │   │   └── [...slug].astro
│   │   ├── projects/
│   │   │   ├── [...slug].astro
│   │   │   └── index.astro
│   │   ├── robots.txt.ts
│   │   ├── rss.xml.ts
│   │   ├── search/
│   │   │   └── index.astro
│   │   └── work/
│   │       └── index.astro
│   ├── styles/
│   │   └── global.css
│   └── types.ts
├── tailwind.config.mjs
└── tsconfig.json

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/stale.yaml
================================================
name: Close inactive issues
on:
  workflow_dispatch:
  schedule:
    - cron: "0 0 * * *"

jobs:
  close-issues:
    runs-on: ubuntu-latest
    permissions:
      issues: write
      pull-requests: write
    steps:
      - uses: actions/stale@v5
        with:
          days-before-issue-stale: 10
          days-before-issue-close: 10
          stale-issue-label: "stale"
          stale-issue-message: "This issue is stale because it has been open for 10 days with no activity."
          close-issue-message: "This issue was closed because it has been inactive for 10 days since being marked as stale."
          days-before-pr-stale: -1
          days-before-pr-close: -1
          repo-token: ${{ secrets.GITHUB_TOKEN }}


================================================
FILE: .gitignore
================================================
# build output
dist/

# generated types
.astro/

# dependencies
node_modules/

# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*

# environment variables
.env
.env.production

# macOS-specific files
.DS_Store


================================================
FILE: .vscode/extensions.json
================================================
{
  "recommendations": ["astro-build.astro-vscode", "unifiedjs.vscode-mdx"],
  "unwantedRecommendations": []
}


================================================
FILE: .vscode/launch.json
================================================
{
  "version": "0.2.0",
  "configurations": [
    {
      "command": "./node_modules/.bin/astro dev",
      "name": "Development server",
      "request": "launch",
      "type": "node-terminal"
    }
  ]
}


================================================
FILE: .vscode/settings.json
================================================
{
  "[astro]": {
    "editor.defaultFormatter": "astro-build.astro-vscode"
  }
}

================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2024 Mark Horn

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
![Astro Sphere Lighthouse Score](_astrosphere.jpg)

Astro Sphere is a static, minimalist, lightweight, lightning fast portfolio and blog theme based on my personal website.

It is primarily Astro, Tailwind and Typescript, with a very small amount of SolidJS for stateful components.

## 🚀 Deploy your own

[![Deploy with Vercel](_deploy_vercel.svg)](https://vercel.com/new/clone?repository-url=https://github.com/markhorn-dev/astro-sphere)  [![Deploy with Netlify](_deploy_netlify.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/markhorn-dev/astro-sphere)

## 📋 Features

- ✅ 100/100 Lighthouse performance
- ✅ Responsive
- ✅ Accessible
- ✅ SEO-friendly
- ✅ Typesafe
- ✅ Minimal style
- ✅ Light/Dark Theme
- ✅ Animated UI
- ✅ Tailwind styling
- ✅ Auto generated sitemap
- ✅ Auto generated RSS Feed
- ✅ Markdown support
- ✅ MDX Support (components in your markdown)
- ✅ Searchable content (posts and projects)
- ✅ Code Blocks - copy to clipboard

## 💯 Lighthouse score
![Astro Sphere Lighthouse Score](_lighthouse.png)

## 🕊️ Lightweight
All pages under 100kb (including fonts)

## ⚡︎ Fast
Rendered in ~40ms on localhost

## 📄 Configuration

The blog posts on the demo serve as the documentation and configuration.

## 💻 Commands

All commands are run from the root of the project, from a terminal:

Replace npm with your package manager of choice. `npm`, `pnpm`, `yarn`, `bun`, etc

| Command                   | Action                                           |
| :------------------------ | :----------------------------------------------- |
| `npm install`             | Installs dependencies                            |
| `npm run dev`             | Starts local dev server at `localhost:4321`      |
| `npm run dev:network`     | Starts dev server on local network               |
| `npm run sync`            | Generates TypeScript types for all Astro modules.|
| `npm run build`           | Build your production site to `./dist/`          |
| `npm run preview`         | Preview your build locally, before deploying     |
| `npm run preview:network` | Starts preview server on local network           |
| `npm run astro ...`       | Run CLI commands like `astro add`, `astro check` |
| `npm run astro -- --help` | Get help using the Astro CLI                     |
| `npm run lint`            | Run ESLint                                       |
| `npm run lint:fix`        | Auto-fix ESLint issues                           |

## 🗺️ Roadmap

A few features I plan to implement
- ⬜ Article Pages - Table of Contents
- ⬜ Article Pages - Share on social media

## ✨ Acknowledgement

Theme inspired by [Paco Coursey](https://paco.me/), [Lee Robinson](https://leerob.io/) and [Hayden Bleasel](https://www.haydenbleasel.com/)


## 🏛️ License

MIT


# 1.0.1 Update

Added ability to run dev and preview on local network.
added npm run dev:network
added npm run preview:network

Added slightly more particle density in both light and dark mode.

Added subtle dark mode star and meteor animations.

Removed eslint config



================================================
FILE: astro.config.mjs
================================================
import { defineConfig } from "astro/config"
import mdx from "@astrojs/mdx"
import sitemap from "@astrojs/sitemap"
import tailwind from "@astrojs/tailwind"
import solidJs from "@astrojs/solid-js"

// https://astro.build/config
export default defineConfig({
  site: "https://astro-sphere-demo.vercel.app",
  integrations: [mdx(), sitemap(), solidJs(), tailwind({ applyBaseStyles: false })],
})

================================================
FILE: package.json
================================================
{
  "name": "astro-sphere",
  "type": "module",
  "version": "1.0.0",
  "scripts": {
    "dev": "astro dev",
    "dev:network": "astro dev --host",
    "start": "astro dev",
    "build": "astro check && astro build",
    "preview": "astro preview",
    "preview:network": "astro dev --host",
    "astro": "astro",
    "lint": "eslint .",
    "lint:fix": "eslint . --fix"
  },
  "dependencies": {
    "@astrojs/check": "^0.5.6",
    "@astrojs/mdx": "^2.1.1",
    "@astrojs/rss": "^4.0.5",
    "@astrojs/sitemap": "^3.1.1",
    "@astrojs/solid-js": "^4.0.1",
    "@astrojs/tailwind": "^5.1.0",
    "@tailwindcss/typography": "^0.5.10",
    "astro": "^4.4.13",
    "clsx": "^2.1.0",
    "fuse.js": "^7.0.0",
    "sharp": "^0.33.2",
    "solid-js": "^1.8.15",
    "tailwind-merge": "^2.2.1",
    "tailwindcss": "^3.4.1",
    "typescript": "^5.3.3"
  }
}


================================================
FILE: public/js/animate.js
================================================
function animate() {
  const animateElements = document.querySelectorAll('.animate')

  animateElements.forEach((element, index) => {
    setTimeout(() => {
      element.classList.add('show')
    }, index * 150)
  });
}

document.addEventListener("DOMContentLoaded", animate)
document.addEventListener("astro:after-swap", animate)

================================================
FILE: public/js/bg.js
================================================

  function generateParticles(n) {
    let value = `${getRandom(2560)}px ${getRandom(2560)}px #000`;
    for (let i = 2; i <= n; i++) {
      value += `, ${getRandom(2560)}px ${getRandom(2560)}px #000`;
    }
    return value;
  }

  function generateStars(n) {
    let value = `${getRandom(2560)}px ${getRandom(2560)}px #fff`;
    for (let i = 2; i <= n; i++) {
      value += `, ${getRandom(2560)}px ${getRandom(2560)}px #fff`;
    }
    return value;
  }

  function getRandom(max) {
    return Math.floor(Math.random() * max);
  }

  function initBG() {
    const particlesSmall = generateParticles(1000);
    const particlesMedium = generateParticles(500);
    const particlesLarge = generateParticles(250);
    const particles1 = document.getElementById('particles1');
    const particles2 = document.getElementById('particles2');
    const particles3 = document.getElementById('particles3');

    if (particles1) {
      particles1.style.cssText = `
      width: 1px;
      height: 1px;
      border-radius: 50%;
      box-shadow: ${particlesSmall};
      animation: animStar 50s linear infinite;
      `;
    }

    if (particles2) {
      particles2.style.cssText = `
      width: 1.5px;
      height: 1.5px;
      border-radius: 50%;
      box-shadow: ${particlesMedium};
      animation: animateParticle 100s linear infinite;
      `;
    }

    if (particles3) {
      particles3.style.cssText = `
      width: 2px;
      height: 2px;
      border-radius: 50%;
      box-shadow: ${particlesLarge};
      animation: animateParticle 150s linear infinite;
      `;
    }

    const starsSmall = generateStars(1000);
    const starsMedium = generateStars(500);
    const starsLarge = generateStars(250);
    const stars1 = document.getElementById('stars1');
    const stars2 = document.getElementById('stars2');
    const stars3 = document.getElementById('stars3');

    if (stars1) {
      stars1.style.cssText = `
      width: 1px;
      height: 1px;
      border-radius: 50%;
      box-shadow: ${starsSmall};
      `;
    }

    if (stars2) {
      stars2.style.cssText = `
      width: 1.5px;
      height: 1.5px;
      border-radius: 50%;
      box-shadow: ${starsMedium};
      `;
    }

    if (stars3) {
      stars3.style.cssText = `
      width: 2px;
      height: 2px;
      border-radius: 50%;
      box-shadow: ${starsLarge};
      `;
    }
  }

  document.addEventListener('astro:after-swap', initBG);
  initBG();

================================================
FILE: public/js/copy.js
================================================
const codeBlocks = document.querySelectorAll('pre:has(code)');

//add copy btn to every code block on the dom
codeBlocks.forEach((code) => {
  //button icon
  const use = document.createElementNS('http://www.w3.org/2000/svg', 'use');
  use.setAttribute('href', '/copy.svg#empty');
  const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
  svg.classList.add('copy-svg');
  svg.appendChild(use);

  //create button
  const btn = document.createElement('button');
  btn.appendChild(svg);
  btn.classList.add('copy-btn');
  btn.addEventListener('click', (e) => copyCode(e));

  //container to fix copy button
  const container = document.createElement('div');
  container.classList.add('copy-cnt');
  container.appendChild(btn);

  //add to code block
  code.classList.add('relative');
  code.appendChild(container);
});

/**
* @param {MouseEvent} event
*/
function copyCode(event) {
  let codeBlock = getChildByTagName(event.currentTarget.parentElement.parentElement, 'CODE')
  navigator.clipboard.writeText(codeBlock.innerText)
  const use = getChildByTagName(getChildByTagName(event.currentTarget, 'svg'), 'use');
  use.setAttribute('href', '/copy.svg#filled')
  setTimeout(() => {
    if (use) {
      use.setAttribute('href', '/copy.svg#empty')
    }
  }, 100);
}

function getChildByTagName(element, tagName) {
  return Array.from(element.children).find((child) => child.tagName === tagName);
}


================================================
FILE: public/js/scroll.js
================================================
function onScroll() {
  const header = document.getElementById("header")
  if (window.scrollY > 0) {
    header.classList.add("scrolled")
  } else {
    header.classList.remove("scrolled")
  }
}

document.addEventListener("scroll", onScroll)


================================================
FILE: public/js/theme.js
================================================
function changeTheme() {
  const element = document.documentElement
  const theme = element.classList.contains("dark") ? "light" : "dark"

  const css = document.createElement("style")

  css.appendChild(
    document.createTextNode(
      `* {
           -webkit-transition: none !important;
           -moz-transition: none !important;
           -o-transition: none !important;
           -ms-transition: none !important;
           transition: none !important;
        }`,
    ),
  )
  document.head.appendChild(css)

  if (theme === "dark") {
    element.classList.add("dark")
  } else {
    element.classList.remove("dark")
  }

  window.getComputedStyle(css).opacity
  document.head.removeChild(css)
  localStorage.theme = theme
}

function preloadTheme() {
  const theme = (() => {
    const userTheme = localStorage.theme

    if (userTheme === "light" || userTheme === "dark") {
      return userTheme
    } else {
      return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"
    }
  })()

  const element = document.documentElement

  if (theme === "dark") {
    element.classList.add("dark")
  } else {
    element.classList.remove("dark")
  }

  localStorage.theme = theme
}

window.onload = () => {
  function initializeThemeButtons() {
    const headerThemeButton = document.getElementById("header-theme-button")
    const drawerThemeButton = document.getElementById("drawer-theme-button")
    headerThemeButton?.addEventListener("click", changeTheme)
    drawerThemeButton?.addEventListener("click", changeTheme)
  } 
  
  document.addEventListener("astro:after-swap", initializeThemeButtons)
  initializeThemeButtons()
}

document.addEventListener("astro:after-swap", preloadTheme)

preloadTheme()


================================================
FILE: public/robots.txt
================================================
User-agent: *
Allow: /

Sitemap: http://localhost:4321/sitemap-index.xml

================================================
FILE: src/components/ArrowCard.tsx
================================================
import { formatDate, truncateText } from "@lib/utils"
import type { CollectionEntry } from "astro:content"

type Props = {
  entry: CollectionEntry<"blog"> | CollectionEntry<"projects">
  pill?: boolean
}

export default function ArrowCard({ entry, pill }: Props) {
  return (
    <a href={`/${entry.collection}/${entry.slug}`} class="group p-4 gap-3 flex items-center border rounded-lg hover:bg-black/5 hover:dark:bg-white/10 border-black/15 dark:border-white/20 transition-colors duration-300 ease-in-out">
      <div class="w-full group-hover:text-black group-hover:dark:text-white blend">
        <div class="flex flex-wrap items-center gap-2">
          {pill &&
            <div class="text-sm capitalize px-2 py-0.5 rounded-full border border-black/15 dark:border-white/25">
              {entry.collection === "blog" ? "post" : "project"}
            </div>
          }
          <div class="text-sm uppercase">
            {formatDate(entry.data.date)}
          </div>
        </div>
        <div class="font-semibold mt-3 text-black dark:text-white line-clamp-2">
          {entry.data.title}
        </div>

        <div class="text-sm line-clamp-2">
          {entry.data.summary}
        </div>
        <ul class="flex flex-wrap mt-2 gap-1">
          {entry.data.tags.map((tag: string) => ( // this line has an error; Parameter 'tag' implicitly has an 'any' type.ts(7006)
            <li class="text-xs uppercase py-0.5 px-2 rounded bg-black/5 dark:bg-white/20 text-black/75 dark:text-white/75">
              {truncateText(tag, 20)}
            </li>
          ))}
        </ul>
      </div>
      <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="stroke-current group-hover:stroke-black group-hover:dark:stroke-white">
        <line x1="5" y1="12" x2="19" y2="12" class="scale-x-0 group-hover:scale-x-100 translate-x-4 group-hover:translate-x-1 transition-all duration-300 ease-in-out" />
        <polyline points="12 5 19 12 12 19" class="translate-x-0 group-hover:translate-x-1 transition-all duration-300 ease-in-out" />
      </svg>
    </a>
  )
}

================================================
FILE: src/components/BaseHead.astro
================================================
---
import { ViewTransitions } from "astro:transitions"

interface Props {
  title: string
  description: string
  image?: string
}

const canonicalURL = new URL(Astro.url.pathname, Astro.site)

const { title, description, image = "/open-graph.jpg" } = Astro.props
---

<!-- Global Metadata -->
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="generator" content={Astro.generator} />

<link rel="preload" href="/fonts/atkinson-regular.woff" as="font" type="font/woff" crossorigin>
<link rel="preload" href="/fonts/atkinson-bold.woff" as="font" type="font/woff" crossorigin>

<!-- Canonical URL -->
<link rel="canonical" href={canonicalURL} />

<!-- Primary Meta Tags -->
<title>{title}</title>
<meta name="title" content={title} />
<meta name="description" content={description} />

<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content={Astro.url} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={new URL(image, Astro.url)} />

<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content={Astro.url} />
<meta property="twitter:title" content={title} />
<meta property="twitter:description" content={description} />
<meta property="twitter:image" content={new URL(image, Astro.url)} />

<!-- Sitemap -->
<link rel="sitemap" href="/sitemap-index.xml" />

<!-- RSS Feed -->
<link rel="alternate" type="application/rss+xml" title={title} href={`${Astro.site}rss.xml`}/>

<!-- Global Scripts -->
<script is:inline src="/js/theme.js"></script>
<script is:inline src="/js/scroll.js"></script>
<script is:inline src="/js/animate.js"></script>
<script defer is:inline src="/js/copy.js"></script>

<!-- <ViewTransitions  /> -->

<script>
  import type { TransitionBeforeSwapEvent } from "astro:transitions/client"
  document.addEventListener("astro:before-swap", (e) =>
    [
      ...(e as TransitionBeforeSwapEvent).newDocument.head.querySelectorAll(
        "link[as=\"font\"]"
      ),
    ].forEach((link) => link.remove())
  )
</script>


================================================
FILE: src/components/Container.astro
================================================
---
import { cn } from "@lib/utils"

type Props = {
  size: "sm" | "md" | "lg" | "xl" | "2xl"
}

const { size } = Astro.props;
---

<div class={cn(
  "w-full h-full mx-auto px-5",
  size === "sm" && "max-w-screen-sm",
  size === "md" && "max-w-screen-md",
  size === "lg" && "max-w-screen-lg",
  size === "xl" && "max-w-screen-xl",
  size === "2xl" && "max-w-screen-2xl",
)}>
  <slot/>
</div>


================================================
FILE: src/components/Counter.tsx
================================================
import { createSignal } from "solid-js"

function CounterButton() {
  const [count, setCount] = createSignal(0)

  const increment = () => setCount(count() + 1)

  return (
    <div class="flex gap-4 items-center">
      <button onClick={increment} class="px-3 py-1 border border-black/25 dark:border-white/25 hover:bg-black/5 dark:hover:bg-white/15 blend">
        Increment
      </button>
      <div>
       Clicked {count()} {count() === 1 ? "time" : "times"}
      </div>
    </div>

  )
}

export default CounterButton


================================================
FILE: src/components/Drawer.astro
================================================
---
import { SITE, LINKS } from "@consts"
import { cn } from "@lib/utils"
const { pathname } = Astro.url
const subpath = pathname.match(/[^/]+/g)
---

<div id="drawer" class="fixed inset-0 h-0 z-40 overflow-hidden flex flex-col items-center justify-center md:hidden bg-neutral-100 dark:bg-neutral-900 transition-[height] duration-300 ease-in-out">
  <nav class="flex flex-col items-center space-y-2">
    {
      LINKS.map((LINK) => (
        <a href={LINK.HREF} class={cn("flex items-center justify-center px-3 py-1 rounded-full", "text-current hover:text-black dark:hover:text-white", "hover:bg-black/5 dark:hover:bg-white/20", "transition-colors duration-300 ease-in-out", pathname === LINK.HREF || "/" + subpath?.[0] === LINK.HREF ? "pointer-events-none bg-black dark:bg-white text-white dark:text-black" : "")}>
          {LINK.TEXT}
        </a>
      ))
    }
  </nav>

  <div class="flex gap-1 mt-5">
    <a href="/search" aria-label={`Search blog posts and projects on ${SITE.TITLE}`} class={cn("size-9 rounded-full p-2 items-center justify-center bg-transparent hover:bg-black/5 dark:hover:bg-white/20 stroke-current hover:stroke-black hover:dark:stroke-white border border-black/10 dark:border-white/25 transition-colors duration-300 ease-in-out", pathname === "/search" || "/" + subpath?.[0] === "search" ? "pointer-events-none bg-black dark:bg-white text-white dark:text-black" : "")}>
      <svg class="size-full">
        <use href="/ui.svg#search"></use>
      </svg>
    </a>

    <a href="/rss.xml" target="_blank" aria-label={`Rss feed for ${SITE.TITLE}`} class="size-9 rounded-full p-2 items-center justify-center bg-transparent hover:bg-black/5 dark:hover:bg-white/20 stroke-current hover:stroke-black hover:dark:stroke-white border border-black/10 dark:border-white/25 transition-colors duration-300 ease-in-out">
      <svg class="size-full">
        <use href="/ui.svg#rss"></use>
      </svg>
    </a>

    <button id="drawer-theme-button" aria-label={`Toggle light and dark theme`} class="size-9 rounded-full p-2 items-center justify-center bg-transparent hover:bg-black/5 dark:hover:bg-white/20 stroke-current hover:stroke-black hover:dark:stroke-white border border-black/10 dark:border-white/25 transition-colors duration-300 ease-in-out">
      <svg class="block dark:hidden size-full">
        <use href="/ui.svg#sun"></use>
      </svg>
      <svg class="hidden dark:block size-full">
        <use href="/ui.svg#moon"></use>
      </svg>
    </button>
  </div>
</div>

<style>
  #drawer.open {
    @apply h-full;
  }
</style>

================================================
FILE: src/components/Footer.astro
================================================
---
import { SITE, SOCIALS } from "@consts"
import Container from "@components/Container.astro"
---

<footer class="relative bg-white dark:bg-black">
  <div class="animate">
    <section class="py-5">
      <Container size="md">
        <div class="flex items-center justify-center sm:justify-end">
          <button id="back-to-top" aria-label="Back to top of page" class="group flex w-fit p-1.5 gap-1.5 text-sm items-center border rounded hover:bg-black/5 hover:dark:bg-white/10 border-black/15 dark:border-white/20 transition-colors duration-300 ease-in-out">
            <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="stroke-current group-hover:stroke-black group-hover:dark:stroke-white rotate-90">
              <line x1="19" y1="12" x2="5" y2="12" class="scale-x-0 group-hover:scale-x-100 translate-x-3 group-hover:translate-x-0 transition-all duration-300 ease-in-out" />
              <polyline points="12 19 5 12 12 5" class="translate-x-1 group-hover:translate-x-0 transition-all duration-300 ease-in-out" />
            </svg>
            <div class="w-full group-hover:text-black group-hover:dark:text-white transition-colors duration-300 ease-in-out">
              Back to top
            </div>
          </button>
        </div>
      </Container>
    </section>
  
    <section class=" py-5 overflow-hidden whitespace-nowrap border-t border-black/10 dark:border-white/25">
      <Container size="md">
        <div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
          <div class="flex flex-col items-center sm:items-start">
            <a href="/" class="flex gap-1 w-fit font-semibold text-current hover:text-black dark:hover:text-white transition-colors duration-300 ease-in-out">
              <svg class="size-6 fill-current">
                <use href="/brand.svg#brand"/>
              </svg>
              {SITE.TITLE}
            </a>
          </div>
          <div class="flex gap-2 justify-center sm:justify-end items-center">
            <span class="relative flex h-3 w-3">
              <span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-300"></span>
              <span class="relative inline-flex rounded-full h-3 w-3 bg-green-500"></span>
            </span>
            All systems normal
          </div>
        </div>
      </Container>
    </section>
  
    <section class=" py-5 overflow-hidden whitespace-nowrap border-t border-black/10 dark:border-white/25">
      <Container size="md">
        <div class="h-full grid grid-cols-1 sm:grid-cols-2 gap-3">
          <div class="order-2 sm:order-1 flex flex-col items-center justify-center sm:items-start">
            <div class="legal">
              <a href="/legal/terms" class="text-current hover:text-black dark:hover:text-white transition-colors duration-300 ease-in-out">
                Terms
              </a> |
              <a href="/legal/privacy" class="text-current hover:text-black dark:hover:text-white transition-colors duration-300 ease-in-out">
                Privacy
              </a>
            </div>
            <div class="text-sm mt-2">
              &copy; 2024 | All rights reserved
            </div>
          </div>
  
          <div class="order-1 sm:order-2 flex justify-center sm:justify-end">
            <div class="flex flex-wrap gap-1 items-center justify-center">
              {
                SOCIALS.map((SOCIAL) => (
                  <a 
                    href={SOCIAL.HREF} 
                    target="_blank" 
                    aria-label={`${SITE.TITLE} on ${SOCIAL.NAME}`} 
                    class="group size-10 rounded-full p-2 items-center justify-center hover:bg-black/5 dark:hover:bg-white/20  blend"
                  >
                    <svg class="size-full fill-current group-hover:fill-black group-hover:dark:fill-white blend">
                      <use href={`/social.svg#${SOCIAL.ICON}`} />
                    </svg>
                  </a>
                ))
              }
              </div>
          </div>
        </div>
      </Container>
    </section>
  </div>

</footer>

<script is:inline>
  function goBackToTop(event) {
    event.preventDefault()
    window.scrollTo({
        top: 0,
        behavior: "smooth"
    })
  }

  function initializeBackToTop() {
    const backToTop = document.getElementById("back-to-top")
    backToTop?.addEventListener("click", goBackToTop)
  }

  document.addEventListener("astro:after-swap", initializeBackToTop)
  initializeBackToTop()
</script>

================================================
FILE: src/components/Header.astro
================================================
---
import { SITE, LINKS } from "@consts"
import { cn } from "@lib/utils"
const { pathname } = Astro.url
const subpath = pathname.match(/[^/]+/g)
import Container from "@components/Container.astro"
---

<header id="header" class="fixed top-0 w-full h-16 z-50 ">
  <Container size="md">
    <div class="relative h-full w-full">
      <div class="absolute left-0 top-1/2 -translate-y-1/2 flex gap-1 font-semibold">
        <a href="/" class="flex gap-1 text-current hover:text-black dark:hover:text-white transition-colors duration-300 ease-in-out">
          <svg class="size-6 fill-current">
            <use href="/brand.svg#brand"></use>
          </svg>
          <div>
            {SITE.TITLE}
          </div>
        </a>
      </div>

    <div class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2">
      <nav class="hidden md:flex items-center justify-center text-sm gap-1">
        {
          LINKS.map((LINK) => (
            <a href={LINK.HREF} class={cn("h-8 rounded-full px-3 text-current", "flex items-center justify-center", "transition-colors duration-300 ease-in-out", pathname === LINK.HREF || "/" + subpath?.[0] === LINK.HREF ? "bg-black dark:bg-white text-white dark:text-black" : "hover:bg-black/5 dark:hover:bg-white/20 hover:text-black dark:hover:text-white")}>
              {LINK.TEXT}
            </a>
          ))
        }
      </nav>
    </div>

    <div class="buttons absolute right-0 top-1/2 -translate-y-1/2 flex gap-1">
      <a href="/search" aria-label={`Search blog posts and projects on ${SITE.TITLE}`} class={cn("hidden md:flex", "size-9 rounded-full p-2 items-center justify-center", "bg-transparent hover:bg-black/5 dark:hover:bg-white/20", "stroke-current hover:stroke-black hover:dark:stroke-white", "border border-black/10 dark:border-white/25", "transition-colors duration-300 ease-in-out", pathname === "/search" || "/" + subpath?.[0] === "/search" ? "pointer-events-none bg-black dark:bg-white text-white dark:text-black" : "")}>
        <svg class="size-full">
          <use href="/ui.svg#search"></use>
        </svg>
      </a>

      <a href="/rss.xml" target="_blank" aria-label={`Rss feed for ${SITE.TITLE}`} class={cn("hidden md:flex", "size-9 rounded-full p-2 items-center justify-center", "bg-transparent hover:bg-black/5 dark:hover:bg-white/20", "stroke-current hover:stroke-black hover:dark:stroke-white", "border border-black/10 dark:border-white/25", "transition-colors duration-300 ease-in-out")}>
        <svg class="size-full">
          <use href="/ui.svg#rss"></use>
        </svg>
      </a>

      <button id="header-theme-button" aria-label={`Toggle light and dark theme`} class={cn("hidden md:flex", "size-9 rounded-full p-2 items-center justify-center", "bg-transparent hover:bg-black/5 dark:hover:bg-white/20", "stroke-current hover:stroke-black hover:dark:stroke-white", "border border-black/10 dark:border-white/25", "transition-colors duration-300 ease-in-out")}>
        <svg class="size-full block dark:hidden">
          <use href="/ui.svg#sun"></use>
        </svg>
        <svg class="size-full hidden dark:block">
          <use href="/ui.svg#moon"></use>
        </svg>
      </button>

      <button id="header-drawer-button" aria-label={`Toggle drawer open and closed`} class={cn("flex md:hidden", "size-9 rounded-full p-2 items-center justify-center", "bg-transparent hover:bg-black/5 dark:hover:bg-white/20", "stroke-current hover:stroke-black hover:dark:stroke-white", "border border-black/10 dark:border-white/25", "transition-colors duration-300 ease-in-out")}>
        <svg id="drawer-open" class="size-full">
          <use href="/ui.svg#menu"></use>
        </svg>
        <svg id="drawer-close" class="size-full">
          <use href="/ui.svg#x"></use>
        </svg>
      </button>
    </div>
  </div>
  </Container>
</header>

<style>
  #header-drawer-button > #drawer-open {
    @apply block;
  }

  #header-drawer-button > #drawer-close {
    @apply hidden;
  }

  #header-drawer-button.open > #drawer-open {
    @apply hidden;
  }

  #header-drawer-button.open > #drawer-close {
    @apply block;
  }
</style>

<script is:inline>
  function toggleDrawer() {
    const drawer = document.getElementById("drawer")
    const drawerButton = document.getElementById("header-drawer-button")
    drawer?.classList.toggle("open")
    drawerButton?.classList.toggle("open")
  }

  function initializeDrawerButton() {
    const drawerButton = document.getElementById("header-drawer-button")
    drawerButton?.addEventListener("click", toggleDrawer)
  }

  document.addEventListener("astro:after-swap", initializeDrawerButton)
  initializeDrawerButton()
</script>


================================================
FILE: src/components/MeteorShower.astro
================================================
---
/**
 * Meteors.astro
 * This component creates meteors that are appended to the galaxy on interval.
 * Meteors are removed from the document after the animation is completed.
 * There are four (4) meteor shower containers, one for each diagonal direction.
 */
---

<div id="meteors">
  <!-- rotations defined in base.css & tailwind.config.mjs -->
  <div class="shower ur" />
  <div class="shower dr" /> 
  <div class="shower dl" />
  <div class="shower ul" />
</div>

<script>
  function createMeteor () {
  // create a meteor
  let meteor = document.createElement("div");
  meteor.setAttribute("class", "meteor");
  meteor.style.left = Math.round(Math.random() * window.innerWidth) + "px";
  meteor.style.top  = Math.round(Math.random() * window.innerHeight) + "px";
  
  // append the meteor to a random meteor shower (direction)
  const showers = document.querySelectorAll(".shower");
  if (showers.length > 0) {
    const random = Math.floor(Math.random() * showers.length);
    const shower = showers[random];
    shower.append(meteor);
  }

  // remove the meteor after the animation duration
  setTimeout(() => {
    meteor.remove();
  }, 3500);
}

// Create meteors on interval on two (2) seconds
setInterval(createMeteor, 1500);
</script>

================================================
FILE: src/components/Search.tsx
================================================
import type { CollectionEntry } from "astro:content"
import { createEffect, createSignal } from "solid-js"
import Fuse from "fuse.js"
import ArrowCard from "@components/ArrowCard"
import SearchBar from "@components/SearchBar"

type Props = {
  data: CollectionEntry<"blog">[]
}

export default function Search({ data }: Props) {
  const [query, setQuery] = createSignal("")
  const [results, setResults] = createSignal<CollectionEntry<"blog">[]>([])

  const fuse = new Fuse(data, {
    keys: ["slug", "data.title", "data.summary", "data.tags"],
    includeMatches: true,
    minMatchCharLength: 2,
    threshold: 0.4,
  })

  createEffect(() => {
    if (query().length < 2) {
      setResults([])
    } else {
      setResults(fuse.search(query()).map((result) => result.item))
    }
  })

  const onSearchInput = (e: Event) => {
    const target = e.target as HTMLInputElement
    setQuery(target.value)
  }

  return (
    <div class="flex flex-col">
      <SearchBar onSearchInput={onSearchInput} query={query} setQuery={setQuery} placeholderText="What are you looking for?" />

      {(query().length >= 2 && results().length >= 1) && (
        <div class="mt-12">
          <div class="text-sm uppercase mb-2">
            Found {results().length} results for {`'${query()}'`}
          </div>
          <ul class="flex flex-col gap-3">
            {results().map(result => (
              <li>
                <ArrowCard entry={result} pill={true} />
              </li>
            ))}
          </ul>
        </div>
      )}
    </div>
  )
}

================================================
FILE: src/components/SearchBar.tsx
================================================
type Props = {
    onSearchInput: (e: Event) => void;
    query: () => string;
    setQuery: (value: string) => void;
    placeholderText: string;
};

export default function SearchBar({ onSearchInput, query, setQuery, placeholderText }: Props) {
    return (<div class="relative">
        <svg class="absolute size-6 left-2 top-[0.45rem] stroke-neutral-400 dark:stroke-neutral-500 pointer-events-none">
            <use href={`/ui.svg#search`} />
        </svg>
        <input name="search" type="text" value={query()} onInput={onSearchInput} autocomplete="off" spellcheck={false} placeholder={placeholderText} class="w-full px-10 py-1.5 rounded outline-none placeholder-neutral-400 dark:placeholder-neutral-500 text-black dark:text-white bg-black/5 dark:bg-white/10 hover:bg-black/10 hover:dark:bg-white/15 focus:bg-black/10 focus:dark:bg-white/15 border border-black/10 dark:border-white/10 focus:border-black/40 focus:dark:border-white/40" />
        {query().length > 0 && (
            <button
                onClick={() => setQuery("")}
                class="absolute flex justify-center items-center h-full w-10 right-0 top-0 stroke-neutral-400 dark:stroke-neutral-500 hover:stroke-neutral-600 hover:dark:stroke-neutral-300"
            >
                <svg class="size-5">
                    <use href={`/ui.svg#x`} />
                </svg>
            </button>
        )}
    </div>)
}

================================================
FILE: src/components/SearchCollection.tsx
================================================
import type { CollectionEntry } from "astro:content"
import { createEffect, createSignal, For, onMount } from "solid-js"
import Fuse from "fuse.js"
import ArrowCard from "@components/ArrowCard"
import { cn } from "@lib/utils"
import SearchBar from "@components/SearchBar"

type Props = {
  entry_name: string
  tags: string[]
  data: CollectionEntry<"blog">[] | CollectionEntry<'projects'>[]
}

export default function SearchCollection({ entry_name, data, tags }: Props) {
  const coerced = data.map((entry) => entry as CollectionEntry<'blog'>);

  const [query, setQuery] = createSignal("");
  const [filter, setFilter] = createSignal(new Set<string>())
  const [collection, setCollection] = createSignal<CollectionEntry<'blog'>[]>([])
  const [descending, setDescending] = createSignal(false);

  const fuse = new Fuse(coerced, {
    keys: ["slug", "data.title", "data.summary", "data.tags"],
    includeMatches: true,
    minMatchCharLength: 2,
    threshold: 0.4,
  })

  createEffect(() => {
    const filtered = (query().length < 2
      ? coerced
      : fuse.search(query()).map((result) => result.item)
    ).filter((entry) =>
      Array.from(filter()).every((value) =>
        entry.data.tags.some((tag: string) =>
          tag.toLowerCase() === String(value).toLowerCase()
        )
      )
    );
    setCollection(descending() ? filtered.toReversed() : filtered)
  })

  function toggleDescending() {
    setDescending(!descending())
  }

  function toggleTag(tag: string) {
    setFilter((prev) =>
      new Set(prev.has(tag)
        ? [...prev].filter((t) => t !== tag)
        : [...prev, tag]
      )
    )
  }

  function clearFilters() {
    setFilter(new Set<string>());
  }

  const onSearchInput = (e: Event) => {
    const target = e.target as HTMLInputElement
    setQuery(target.value)
  }

  onMount(() => {
    const wrapper = document.getElementById("search-collection-wrapper");
    if (wrapper) {
      wrapper.style.minHeight = "unset";
    }
  })

  return (
    <div class="grid grid-cols-1 sm:grid-cols-3 gap-6">
      {/* Control Panel*/}
      <div class="col-span-3 sm:col-span-1">
        <div class="sticky top-24 mt-7">
          {/* Search Bar */}
          <SearchBar onSearchInput={onSearchInput} query={query} setQuery={setQuery} placeholderText={`Search ${entry_name}`} />
          {/* Tag Filters */}
          <div class="relative flex flex-row justify-between w-full"><p class="text-sm font-semibold uppercase my-4 text-black dark:text-white">Tags</p>
            {filter().size > 0 && (
              <button
                onClick={clearFilters}
                class="absolute flex justify-center items-center h-full w-10 right-0 top-0 stroke-neutral-400 dark:stroke-neutral-500 hover:stroke-neutral-600 hover:dark:stroke-neutral-300"
              >
                <svg class="size-5">
                  <use href={`/ui.svg#x`} />
                </svg>
              </button>
            )}</div>
          <ul class="flex flex-wrap sm:flex-col gap-1.5">
            <For each={tags}>
              {(tag) => (
                <li class="sm:w-full">
                  <button
                    onClick={() => toggleTag(tag)}
                    class={cn(
                      "w-full px-2 py-1 rounded",
                      "flex gap-2 items-center",
                      "bg-black/5 dark:bg-white/10",
                      "hover:bg-black/10 hover:dark:bg-white/15",
                      "transition-colors duration-300 ease-in-out",
                      filter().has(tag) && "text-black dark:text-white"
                    )}
                  >
                    <svg
                      class={cn(
                        "shrink-0 size-5 fill-black/50 dark:fill-white/50",
                        "transition-colors duration-300 ease-in-out",
                        filter().has(tag) && "fill-black dark:fill-white"
                      )}
                    >
                      <use
                        href={`/ui.svg#square`}
                        class={cn(!filter().has(tag) ? "block" : "hidden")}
                      />
                      <use
                        href={`/ui.svg#square-check`}
                        class={cn(filter().has(tag) ? "block" : "hidden")}
                      />
                    </svg>

                    <span class="truncate block min-w-0 pt-[2px]">
                      {tag}
                    </span>
                  </button>

                </li>
              )}
            </For>
          </ul>
        </div>
      </div>
      {/* Posts */}
      <div class="col-span-3 sm:col-span-2">
        <div class="flex flex-col">
          {/* Info Bar */}
          <div class='flex justify-between flex-row mb-2'>
            <div class="text-sm uppercase">
              SHOWING {collection().length} OF {data.length} {entry_name}
            </div>
            <button onClick={toggleDescending} class='flex flex-row gap-1 stroke-neutral-400 dark:stroke-neutral-500 hover:stroke-neutral-600 hover:dark:stroke-neutral-300 text-neutral-400 dark:text-neutral-500 hover:text-neutral-600 hover:dark:text-neutral-300'>
              <div class="text-sm uppercase">
                {descending() ? "DESCENDING" : "ASCENDING"}
              </div>
              <svg
                class="size-5 left-2 top-[0.45rem]"
              >
                <use href={`/ui.svg#sort-descending`} class={descending() ? "block" : "hidden"}></use>
                <use href={`/ui.svg#sort-ascending`} class={descending() ? "hidden" : "block"}></use>
              </svg>
            </button>
          </div>
          <ul class="flex flex-col gap-3">
            {collection().map((entry) => (
              <li>
                <ArrowCard entry={entry} />
              </li>
            ))}
          </ul>
        </div>
      </div>
    </div>
  )
}


================================================
FILE: src/components/StackCard.astro
================================================
---
type Props = {
  text: string
  icon: string
  href: string
}

const { text, icon, href } = Astro.props
---

<a href={href} target="_blank" class="w-fit px-3 py-2 group rounded border flex gap-2 items-center border-neutral-200 dark:border-neutral-700 hover:bg-neutral-100 hover:dark:bg-neutral-800 blend">
  <svg height={20} width={20}>
    <use href={`/stack.svg#${icon}`}></use>
  </svg>
  <span class="text-sm capitalize text-neutral-500 dark:text-neutral-400 group-hover:text-black group-hover:dark:text-white blend">
    {text}
  </span>
</a>

================================================
FILE: src/components/TwinklingStars.astro
================================================
---
/**
 * TwinkleStars.astro
 * This component creates twinkling stars that are appended to the galaxy on interval.
 * Twinkle stars are removed from the document after the animation is completed.
 * The svg below is just a template for the script to clone and append to the galaxy.
 */
---

<svg 
  id="twinkle-star" 
  class="template" 
  width="149" 
  height="149" 
  viewBox="0 0 149 149" 
  fill="none" 
  xmlns="http://www.w3.org/2000/svg"
  class="absolute left-full animate-twinkle"
>
  <circle cx="74" cy="74" r="11" fill="white"/>
  <rect y="141.421" width="200" height="10" transform="rotate(-45 0 141.421)" fill="url(#paint0_linear_4_2)"/>
  <rect x="7.07107" width="200" height="10" transform="rotate(45 7.07107 0)" fill="url(#paint1_linear_4_2)"/>
  <defs>
    <linearGradient id="paint0_linear_4_2" x1="0" y1="146.421" x2="200" y2="146.421" gradientUnits="userSpaceOnUse">
      <stop stop-color="#1E1E1E"/>
      <stop offset="0.445" stop-color="white"/>
      <stop offset="0.58721" stop-color="white"/>
      <stop offset="1" stop-color="#1E1E1E"/>
    </linearGradient>
    <linearGradient id="paint1_linear_4_2" x1="7.07107" y1="5" x2="207.071" y2="5" gradientUnits="userSpaceOnUse">
      <stop stop-color="#1E1E1E"/>
      <stop offset="0.42" stop-color="white"/>
      <stop offset="0.555" stop-color="white"/>
      <stop offset="1" stop-color="#1E1E1E"/>
    </linearGradient>
  </defs>
</svg>

<script is:inline>
  // Generate a twinkle star and append it to the galaxy, remove it after animation.
  function generateTwinkleStar() {
    const twinkleStarTemplate = document.getElementById("twinkle-star")
    if (!twinkleStarTemplate) { return; }
    // Clone the twinkle star template and set its attributes.
    const twinkleStar = twinkleStarTemplate.cloneNode(true);
    twinkleStar.style.position = "absolute";
    twinkleStar.style.left = Math.floor(Math.random() * window.innerWidth) + "px";
    twinkleStar.style.top = Math.floor(Math.random() * (window.innerHeight/3)) + "px";
    twinkleStar.style.width = window.innerWidth < 768 ? Math.floor(Math.random() * (15 - 7.5 + 1) + 7.5) : Math.floor(Math.random() * (30 - 15 + 1) + 15) + "px";
    twinkleStar.style.height = twinkleStar.style.width;
    twinkleStar.classList.add("twinkle");
    document.getElementById("galaxy").appendChild(twinkleStar);

    // Remove the twinkle star after the animation is completed.
    setTimeout(() => {
      twinkleStar.remove();
    }, 2500);
  }

  setInterval(generateTwinkleStar, 5000);
</script>
  

================================================
FILE: src/consts.ts
================================================
import type { Site, Page, Links, Socials } from "@types"

// Global
export const SITE: Site = {
  TITLE: "Astro Sphere",
  DESCRIPTION: "Welcome to Astro Sphere, a portfolio and blog for designers and developers.",
  AUTHOR: "Mark Horn",
}

// Work Page
export const WORK: Page = {
  TITLE: "Work",
  DESCRIPTION: "Places I have worked.",
}

// Blog Page
export const BLOG: Page = {
  TITLE: "Blog",
  DESCRIPTION: "Writing on topics I am passionate about.",
}

// Projects Page 
export const PROJECTS: Page = {
  TITLE: "Projects",
  DESCRIPTION: "Recent projects I have worked on.",
}

// Search Page
export const SEARCH: Page = {
  TITLE: "Search",
  DESCRIPTION: "Search all posts and projects by keyword.",
}

// Links
export const LINKS: Links = [
  { 
    TEXT: "Home", 
    HREF: "/", 
  },
  { 
    TEXT: "Work", 
    HREF: "/work", 
  },
  { 
    TEXT: "Blog", 
    HREF: "/blog", 
  },
  { 
    TEXT: "Projects", 
    HREF: "/projects", 
  },
]

// Socials
export const SOCIALS: Socials = [
  { 
    NAME: "Email",
    ICON: "email", 
    TEXT: "markhorn.dev@gmail.com",
    HREF: "mailto:markhorn.dev@gmail.com",
  },
  { 
    NAME: "Github",
    ICON: "github",
    TEXT: "markhorn-dev",
    HREF: "https://github.com/markhorn-dev/astro-sphere"
  },
  { 
    NAME: "LinkedIn",
    ICON: "linkedin",
    TEXT: "markhorn-dev",
    HREF: "https://www.linkedin.com/in/markhorn-dev/",
  },
  { 
    NAME: "Twitter",
    ICON: "twitter-x",
    TEXT: "markhorn_dev",
    HREF: "https://twitter.com/markhorn_dev",
  },
]



================================================
FILE: src/content/blog/01-astro-sphere-file-structure/index.md
================================================
---
title: "Astro Sphere: File Structure"
summary: "You'll find these directories and files in the project. What do they do?"
date: "Mar 17 2024"
draft: false
tags:
- Tutorial
- Astro
- Astro Sphere
---

A one line summary of what each file and directory is for:
```js
/
├── public/ // Files publicly available to the browser
│   ├── fonts/ // The default fonts for Astro Sphere
│   │   └── atkinson-bold.woff  // default font weight 700
│   │   └── atkinson-regular.woff // default font weight 400
│   ├── js/ // Javascript that will be imported into <head>
│   │   └── animate.js // function for animating page elements
│   │   └── bg.js // function for generating the background
│   │   └── scroll.js // scroll handler for the header styles
│   │   └── theme.js // controls the light and dark theme
│   └── brand.svg //the icon that displays in header and footer
│   └── favicon.svg //the icon that displays in the browser
│   └── ui.svg // an svg sprite for all ui icons on the website
│   └── social.svg // an svg sprite for all social media icons
│   └── open-graph.jpg // the default image for open-graph
│   └── robots.txt // for web crawlers and bots to index the website
├── src/ // Everything that will be built for the website
│   ├── components/ // All astro and SolidJs components
│   ├── content/ // Contains all static markdown to be compiled
│   │   |  blog/ // Contains all blog post markdown
│   │   |  projects/ // Contains all projects markdown
│   │   |  work/ // Contains all work page markdown
│   │   |  legal/ // Contains all legal docs markdown
│   │   └── config.ts // Contains the collection config for Astro
│   ├── layouts/ // Reused layouts across the website
│   └── pages/ // All of the pages on the website
│   └── styles/ // CSS and global tailwind styles
│   └── lib/ // Global helper functions
│   └── consts.ts // Page metadata, general configuration
│   └── types.ts // Types for consts.ts
└── .gitignore // Files and directories to be ignored by Git
└── .eslintignore // Files and directories to be ignored by ESLint
└── eslintrc.cjs // ESLint configuration
└── astro.config.mjs // Astro configuration
└── tailwind.config.mjs // Tailwind configuration
└── tsconfig.json // Typescript configuration
└── package.json // All the installed packages
```

================================================
FILE: src/content/blog/02-astro-sphere-getting-started/index.md
================================================
---
title: "Astro Sphere: Getting Started"
summary: "You've downloaded and installed the project. Let's hit the ground running."
date: "Mar 16 2024"
draft: false
tags:
- Tutorial
- Astro
- Astro Sphere
---

Astro Sphere is designed to be configurable. This article will cover the basics on
configuring the site and make it personal.

### First let's change the url

```js
//astro.config.mjs

export default defineConfig({
  site: "https://astro-sphere.vercel.app", // your domain here
  integrations: [mdx(), sitemap(), solidJs(), tailwind({ applyBaseStyles: false })],
})
```

### Next, Let's configure the Site

```js
// src/consts.ts

export const SITE: Site = {
  TITLE: "Astro Sphere",
  DESCRIPTION: "Welcome to Astro Sphere, a portfolio and blog for designers and developers.",
  AUTHOR: "Mark Horn",
}
```

| Field       | Type   | Description                                                            |
| :---------- | :----- | :--------------------------------------------------------------------- |
| TITLE       | String | The title of the website. Displayed in header and footer. Used in SEO. |
| DESCRIPTION | String | The description of the index page of the website. Used in SEO.         |
| AUTHOR      | String | Your name.                                                             |

### Change the branding

The browser icon is located in `/public/favicon.svg`

The header and footer branding icon is located in `/public/brand.svg` as a sprite with id="brand"

### The rest of the consts file

Each page has a metadata entry that is useful for SEO.

```js
export const WORK: Page = {
  TITLE: "Work",
  DESCRIPTION: "Places I have worked.",
}
```

The links that are displayed in the header and drawer

```js
export const LINKS: Links = [
  { HREF: "/", TEXT: "Home" },
  { HREF: "/work", TEXT: "Work" },
  { HREF: "/blog", TEXT: "Blog" },
  { HREF: "/projects", TEXT: "Projects" },
]
```

The social media links

```js
export const SOCIALS: Socials = [
  { 
    NAME: "Github",
    ICON: "github",
    TEXT: "markhorn-dev",
    HREF: "https://github.com/markhorn-dev/astro-sphere"
  },
]
```

| Field | Type | Required | Description |
| :---- | :--- | :------- | :---------- |
| NAME  | string | yes | Accessible name |
| ICON  | string | yes | Refers to the symbol id in `public/social.svg` |
| TEXT  | string | yes | Shorthand profile name |
| HREF  | string | yes | The link to the social media profile |

================================================
FILE: src/content/blog/03-astro-sphere-add-new-post-or-projects/index.md
================================================
---
title: "Astro Sphere: Adding a new post or project."
summary: "Adding a new article (blog post or project) is pretty easy."
date: "Mar 14 2024"
draft: false
tags:
- Tutorial
- Astro
- Astro Sphere
---
### Basics

Create a folder in the respective collection you wish to create content. The name of the folder will be the slug in which your content will be found.

```text
creating the following

/content/blog/my-new-post/index.md

will be published to

https://yourdomain.com/blog/my-new-post

```

### Frontmatter

Front matter is in yaml if you are familiar with the format. All posts and projects require frontmatter at the top of the document to be imported. All frontmatter must be inside triple dashes, similar to Astro format. See example below.

### Blog Collection

| Field   | Type    | Req? | Description                                                   |
| :------ | :------ | :--- | :------------------------------------------------------------ |
| title   | string  | yes  | Title of the post. Used in SEO.                               |
| summary | string  | yes  | Short description of the post. Used in SEO.                   |
| date    | string  | yes  | Any string date that javascript can convert. Used in sorting  |
| tags    | array   | yes  | Post topic. Array of strings. Used in filtering.              |
| draft   | boolean | no   | Hides the post from collections. Unpublished entry.           |

Example blog post frontmatter

```yaml
---
title: "Astro Sphere: Adding a new post or project."
summary: "Adding a new article (blog post or project) is pretty easy."
date: "Mar 18 2024"
draft: false
tags:
- Tutorial
- Astro
- Astro Sphere
---
```

### Projects Collection (extends Blog Collection)

| Field   | Type    | Req? | Description                                                   |
| :------ | :------ | :--- | :------------------------------------------------------------ |
| title   | string  | yes  | Title of the post. Used in SEO.                               |
| summary | string  | yes  | Short description of the post. Used in SEO.                   |
| date    | string  | yes  | Any string date that javascript can convert. Used in sorting  |
| tags    | array   | yes  | Post topic. Array of strings. Used in filtering.              |
| draft   | boolean | no   | Hides the post from collections. Unpublished entry.           |
| demoUrl | string  | no   | A link to the deployed project, if applicable.                |
| repoUrl | string  | no   | A link to the repository, if applicable.                      |

Example project frontmatter

```yaml
---
title: "Astro Sphere"
summary: "Astro Sphere, a portfolio and blog for designers and developers."
date: "Mar 18 2024"
draft: false
tags:
- Astro
- Typescript
- Javascript
- Tailwind
- SolidJS
demoUrl: https://astro-sphere.vercel.app
repoUrl: https://github.com/markhorn-dev/astro-sphere
---
```

### Write your content
You've made it this far, all that is left to do is write your content beneath the frontmatter. Writing markdown will be covered in the next article.

================================================
FILE: src/content/blog/04-astro-sphere-writing-markdown/index.md
================================================
---
title: "Astro Sphere: Writing Markdown"
summary: "Basic Markdown syntax that can be used when writing Markdown content in Astro Sphere."
date: "Mar 13 2024"
draft: false
tags:
- Tutorial
- Astro
- Astro Sphere
- Markdown
---

### Headings

```text
# H1

## H2

### H3

#### H4

##### H5

###### H6

```

# H1

## H2

### H3

#### H4

##### H5

###### H6

### Paragraph

Xerum, quo qui aut unt expliquam qui dolut labo. Aque venitatiusda cum, voluptionse latur sitiae dolessi aut parist aut dollo enim qui voluptate ma dolestendit peritin re plis aut quas inctum laceat est volestemque commosa as cus endigna tectur, offic to cor sequas etum rerum idem sintibus eiur? Quianimin porecus evelectur, cum que nis nust voloribus ratem aut omnimi, sitatur? Quiatem. Nam, omnis sum am facea corem alique molestrunt et eos evelece arcillit ut aut eos eos nus, sin conecerem erum fuga. Ri oditatquam, ad quibus unda veliamenimin cusam et facea ipsamus es exerum sitate dolores editium rerore eost, temped molorro ratiae volorro te reribus dolorer sperchicium faceata tiustia prat.

Itatur? Quiatae cullecum rem ent aut odis in re eossequodi nonsequ idebis ne sapicia is sinveli squiatum, core et que aut hariosam ex eat.

### Images

Relative image in the /public folder

```markdown
![blog placeholder](/open-graph.jpg)
```

![blog placeholder](/open-graph.jpg)

Relative Image in the same folder as the markdown

```markdown
![Test Relative Image](./spongebob.png)
```

![Test Relative Image](./spongebob.png)

## Blockquotes

The blockquote element represents content that is quoted from another source, optionally with a citation which must be within a `footer` or `cite` element, and optionally with in-line changes such as annotations and abbreviations.

### Blockquote without attribution

#### Syntax

```markdown
> Tiam, ad mint andaepu dandae nostion secatur sequo quae.  
> **Note** that you can use _Markdown syntax_ within a blockquote.
```

#### Output

> Tiam, ad mint andaepu dandae nostion secatur sequo quae.  
> **Note** that you can use _Markdown syntax_ within a blockquote.

### Blockquote with attribution

#### Syntax

```markdown
> Don't communicate by sharing memory, share memory by communicating.<br>
> — <cite>Rob Pike[^1]</cite>
```

#### Output

> Don't communicate by sharing memory, share memory by communicating.<br>
> — <cite>Rob Pike[^1]</cite>

[^1]: The above quote is excerpted from Rob Pike's [talk](https://www.youtube.com/watch?v=PAAkCSZUG1c) during Gopherfest, November 18, 2015.

## Tables

#### Syntax

```markdown
| Italics   | Bold     | Code   |
| --------- | -------- | ------ |
| _italics_ | **bold** | `code` |
```

#### Output

| Italics   | Bold     | Code   |
| --------- | -------- | ------ |
| _italics_ | **bold** | `code` |

## Code Blocks

#### Syntax

we can use 3 backticks ``` in new line and write snippet and close with 3 backticks on new line and to highlight language specific syntac, write one word of language name after first 3 backticks, for eg. html, javascript, css, markdown, typescript, txt, bash

````markdown
```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Example HTML5 Document</title>
  </head>
  <body>
    <p>Test</p>
  </body>
</html>
```
````

Output

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Example HTML5 Document</title>
  </head>
  <body>
    <p>Test</p>
  </body>
</html>
```

## List Types

### Ordered List

#### Syntax

```markdown
1. First item
2. Second item
3. Third item
```

#### Output

1. First item
2. Second item
3. Third item

### Unordered List

#### Syntax

```markdown
- List item
- Another item
- And another item
```

#### Output

- List item
- Another item
- And another item

### Nested list

#### Syntax

```markdown
- Fruit
  - Apple
  - Orange
  - Banana
- Dairy
  - Milk
  - Cheese
```

#### Output

- Fruit
  - Apple
  - Orange
  - Banana
- Dairy
  - Milk
  - Cheese

## Other Elements — abbr, sub, sup, kbd, mark

#### Syntax

```markdown
<abbr title="Graphics Interchange Format">GIF</abbr> is a bitmap image format.

H<sub>2</sub>O

X<sup>n</sup> + Y<sup>n</sup> = Z<sup>n</sup>

Press <kbd><kbd>CTRL</kbd>+<kbd>ALT</kbd>+<kbd>Delete</kbd></kbd> to end the session.

Most <mark>salamanders</mark> are nocturnal, and hunt for insects, worms, and other small creatures.
```

#### Output

<abbr title="Graphics Interchange Format">GIF</abbr> is a bitmap image format.

H<sub>2</sub>O

X<sup>n</sup> + Y<sup>n</sup> = Z<sup>n</sup>

Press <kbd><kbd>CTRL</kbd>+<kbd>ALT</kbd>+<kbd>Delete</kbd></kbd> to end the session.

Most <mark>salamanders</mark> are nocturnal, and hunt for insects, worms, and other small creatures.


================================================
FILE: src/content/blog/05-astro-sphere-writing-mdx/MyComponent.astro
================================================
---
type Props = {
  name: string
}
const { name } = Astro.props
---

<div class="border p-4 bg-yellow-100 text-black">
  <div>
    Hello, 
    <span class="font-semibold">
      {name}!!!
    </span>
  </div>
  <slot/>
</div>

================================================
FILE: src/content/blog/05-astro-sphere-writing-mdx/index.mdx
================================================
---
title: "Astro Sphere: Writing MDX"
summary: "Lorem ipsum dolor sit amet"
date: "Mar 12 2024"
draft: false
tags:
- Tutorial
- Astro
- Astro Sphere
- Markdown
- MDX
---

MDX is a special flavor of Markdown that supports embedded JavaScript & JSX syntax. This unlocks the ability to [mix JavaScript and UI Components into your Markdown content](https://docs.astro.build/en/guides/markdown-content/#mdx-features) for things like interactive charts or alerts.

If you have existing content authored in MDX, this integration will hopefully make migrating to Astro a breeze.

## An astro component with props

```
// Imported from relative path (same dir as markdown file)
import MyComponent from "./MyComponent.astro"

<MyComponent name="You">
  Welcome to MDX
</MyComponent>
```

import MyComponent from "./MyComponent.astro"

<MyComponent name="You">
  Welcome to MDX
</MyComponent>



## An interactive Solid Js component

```
// Imported from components directory (src/components)
import MyComponent from "@components/Counter"

// Don't forget the astro client:load directive
<Counter client:load /> 
```

import Counter from "@components/Counter"

<Counter client:load />

<br/>
<br/>
<br/>

================================================
FILE: src/content/blog/06-astro-sphere-social-links/index.md
================================================
---
title: "Astro Sphere: Social media links"
summary: "A quick tutorial on how to change, add or remove social media links"
date: "Mar 11 2024"
draft: false
tags:
- Tutorial
- Astro
- Astro Sphere
---

Astro Sphere comes preconfigured with social media links for Email, Github, Linked In and Twitter (X), but it's very easy to add more.

### Edit `consts.ts`

```js
// consts.ts

export const SOCIALS: Socials = [
  { 
    NAME: "Github",
    ICON: "github",
    TEXT: "markhorn-dev",
    HREF: "https://github.com/markhorn-dev/astro-sphere"
  },
]
```

| Field | Type | Required | Description |
| :---- | :--- | :------- | :---------- |
| NAME  | string | yes | Accessible name |
| ICON  | string | yes | Refers to the symbol id in `public/social.svg` |
| TEXT  | string | yes | Shorthand profile name |
| HREF  | string | yes | The link to the social media profile |

### Edit /public/social.svg

Simply add your own symbols to the svg sprite.

It is recommended that all styles be removed from new symbols added, or they may not show up correctly or conflict with Tailwind's classes.

The id should match the icon field as specified in your `consts.ts` file.

```html
<!-- public/social.svg -->

<svg xmlns="http://www.w3.org/2000/svg">
  <defs>
    <symbol id="github" viewBox="0 0 496 512">
      <path d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3 .3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5 .3-6.2 2.3zm44.2-1.7c-2.9 .7-4.9 2.6-4.6 4.9 .3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3 .7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3 .3 2.9 2.3 3.9 1.6 1 3.6 .7 4.3-.7 .7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3 .7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3 .7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z"/>
    </symbol>
</defs>
</svg>
```


================================================
FILE: src/content/config.ts
================================================
import { defineCollection, z } from "astro:content"

const work = defineCollection({
  type: "content",
  schema: z.object({
    company: z.string(),
    role: z.string(),
    dateStart: z.coerce.date(),
    dateEnd: z.union([z.coerce.date(), z.string()]),
  }),
})

const blog = defineCollection({
  type: "content",
  schema: z.object({
    title: z.string(),
    summary: z.string(),
    date: z.coerce.date(),
    tags: z.array(z.string()),
    draft: z.boolean().optional(),
  }),
})

const projects = defineCollection({
  type: "content",
  schema: z.object({
    title: z.string(),
    summary: z.string(),
    date: z.coerce.date(),
    tags: z.array(z.string()),
    draft: z.boolean().optional(),
    demoUrl: z.string().optional(),
    repoUrl: z.string().optional(),
  }),
})

const legal = defineCollection({
  type: "content",
  schema: z.object({
    title: z.string(),
    date: z.coerce.date(),
  }),
})

export const collections = { work, blog, projects, legal }


================================================
FILE: src/content/legal/privacy.md
================================================
---
title: "Privacy Policy"
date: "03/07/2024"
---

This Privacy Policy governs the manner in which [Your Company Name] collects, uses, maintains, and discloses information collected from users (each, a "User") of the [Your Website URL] website ("Site"). This privacy policy applies to the Site and all products and services offered by [Your Company Name].

#### Personal identification information
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras sit amet massa ut neque consequat congue. Sed id ipsum vitae sem imperdiet suscipit. Nulla facilisi. Morbi quis nibh at nunc pulvinar rhoncus. Proin porttitor dapibus dolor, id fermentum urna eleifend et. In feugiat pretium erat nec vestibulum.

#### Non-personal identification information
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras sit amet massa ut neque consequat congue. Sed id ipsum vitae sem imperdiet suscipit. Nulla facilisi. Morbi quis nibh at nunc pulvinar rhoncus. Proin porttitor dapibus dolor, id fermentum urna eleifend et. In feugiat pretium erat nec vestibulum.

#### Web browser cookies
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras sit amet massa ut neque consequat congue. Sed id ipsum vitae sem imperdiet suscipit. Nulla facilisi. Morbi quis nibh at nunc pulvinar rhoncus. Proin porttitor dapibus dolor, id fermentum urna eleifend et. In feugiat pretium erat nec vestibulum.

#### How we use collected information
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras sit amet massa ut neque consequat congue. Sed id ipsum vitae sem imperdiet suscipit. Nulla facilisi. Morbi quis nibh at nunc pulvinar rhoncus. Proin porttitor dapibus dolor, id fermentum urna eleifend et. In feugiat pretium erat nec vestibulum.

#### How we protect your information
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras sit amet massa ut neque consequat congue. Sed id ipsum vitae sem imperdiet suscipit. Nulla facilisi. Morbi quis nibh at nunc pulvinar rhoncus. Proin porttitor dapibus dolor, id fermentum urna eleifend et. In feugiat pretium erat nec vestibulum.

#### Sharing your personal information
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras sit amet massa ut neque consequat congue. Sed id ipsum vitae sem imperdiet suscipit. Nulla facilisi. Morbi quis nibh at nunc pulvinar rhoncus. Proin porttitor dapibus dolor, id fermentum urna eleifend et. In feugiat pretium erat nec vestibulum.

#### Changes to this privacy policy
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras sit amet massa ut neque consequat congue. Sed id ipsum vitae sem imperdiet suscipit. Nulla facilisi. Morbi quis nibh at nunc pulvinar rhoncus. Proin porttitor dapibus dolor, id fermentum urna eleifend et. In feugiat pretium erat nec vestibulum.



================================================
FILE: src/content/legal/terms.md
================================================
---
title: "Terms of Use"
date: "03/07/2024"
---

Please read these Terms of Use ("Terms", "Terms of Use") carefully before using the [Your Website URL] website (the "Service") operated by [Your Company Name] ("us", "we", or "our").

#### Agreement to Terms
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras sit amet massa ut neque consequat congue. Sed id ipsum vitae sem imperdiet suscipit. Nulla facilisi. Morbi quis nibh at nunc pulvinar rhoncus. Proin porttitor dapibus dolor, id fermentum urna eleifend et. In feugiat pretium erat nec vestibulum.

#### Intellectual Property Rights
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras sit amet massa ut neque consequat congue. Sed id ipsum vitae sem imperdiet suscipit. Nulla facilisi. Morbi quis nibh at nunc pulvinar rhoncus. Proin porttitor dapibus dolor, id fermentum urna eleifend et. In feugiat pretium erat nec vestibulum.

#### User Representations
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras sit amet massa ut neque consequat congue. Sed id ipsum vitae sem imperdiet suscipit. Nulla facilisi. Morbi quis nibh at nunc pulvinar rhoncus. Proin porttitor dapibus dolor, id fermentum urna eleifend et. In feugiat pretium erat nec vestibulum.

#### Links to Other Websites
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras sit amet massa ut neque consequat congue. Sed id ipsum vitae sem imperdiet suscipit. Nulla facilisi. Morbi quis nibh at nunc pulvinar rhoncus. Proin porttitor dapibus dolor, id fermentum urna eleifend et. In feugiat pretium erat nec vestibulum.

#### Termination
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras sit amet massa ut neque consequat congue. Sed id ipsum vitae sem imperdiet suscipit. Nulla facilisi. Morbi quis nibh at nunc pulvinar rhoncus. Proin porttitor dapibus dolor, id fermentum urna eleifend et. In feugiat pretium erat nec vestibulum.

#### Governing Law
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras sit amet massa ut neque consequat congue. Sed id ipsum vitae sem imperdiet suscipit. Nulla facilisi. Morbi quis nibh at nunc pulvinar rhoncus. Proin porttitor dapibus dolor, id fermentum urna eleifend et. In feugiat pretium erat nec vestibulum.

#### Changes to These Terms of Use
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras sit amet massa ut neque consequat congue. Sed id ipsum vitae sem imperdiet suscipit. Nulla facilisi. Morbi quis nibh at nunc pulvinar rhoncus. Proin porttitor dapibus dolor, id fermentum urna eleifend et. In feugiat pretium erat nec vestibulum.

================================================
FILE: src/content/projects/project-1/index.md
================================================
---
title: "Project One"
summary: "Lorem ipsum dolor sit amet"
date: "Mar 18 2022"
draft: false
tags:
- Astro
- Javascript
- Typescript
- Tailwind
- SolidJs
demoUrl: https://astro-sphere-demo.vercel.app
repoUrl: https://github.com/markhorn-dev/astro-sphere
---

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Vitae ultricies leo integer malesuada nunc vel risus commodo viverra. Adipiscing enim eu turpis egestas pretium. Euismod elementum nisi quis eleifend quam adipiscing. In hac habitasse platea dictumst vestibulum. Sagittis purus sit amet volutpat. Netus et malesuada fames ac turpis egestas. Eget magna fermentum iaculis eu non diam phasellus vestibulum lorem. Varius sit amet mattis vulputate enim. Habitasse platea dictumst quisque sagittis. Integer quis auctor elit sed vulputate mi. Dictumst quisque sagittis purus sit amet.


================================================
FILE: src/content/projects/project-2/index.md
================================================
---
title: "Project Two"
summary: "Lorem ipsum dolor sit amet"
date: "Mar 17 2022"
draft: false
tags:
- Svelte
- Sveltekit
- Typescript
- Tailwind
---

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Vitae ultricies leo integer malesuada nunc vel risus commodo viverra. Adipiscing enim eu turpis egestas pretium. Euismod elementum nisi quis eleifend quam adipiscing. In hac habitasse platea dictumst vestibulum. Sagittis purus sit amet volutpat. Netus et malesuada fames ac turpis egestas. Eget magna fermentum iaculis eu non diam phasellus vestibulum lorem. Varius sit amet mattis vulputate enim. Habitasse platea dictumst quisque sagittis. Integer quis auctor elit sed vulputate mi. Dictumst quisque sagittis purus sit amet.

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Vitae ultricies leo integer malesuada nunc vel risus commodo viverra. Adipiscing enim eu turpis egestas pretium. Euismod elementum nisi quis eleifend quam adipiscing. In hac habitasse platea dictumst vestibulum. Sagittis purus sit amet volutpat. Netus et malesuada fames ac turpis egestas. Eget magna fermentum iaculis eu non diam phasellus vestibulum lorem. Varius sit amet mattis vulputate enim. Habitasse platea dictumst quisque sagittis. Integer quis auctor elit sed vulputate mi. Dictumst quisque sagittis purus sit amet.

================================================
FILE: src/content/projects/project-3/index.md
================================================
---
title: "Project Three"
summary: "Lorem ipsum dolor sit amet"
date: "Mar 16 2022"
draft: false
tags:
- Vue
- Javascript
- Tailwind
---

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Vitae ultricies leo integer malesuada nunc vel risus commodo viverra. Adipiscing enim eu turpis egestas pretium. Euismod elementum nisi quis eleifend quam adipiscing. In hac habitasse platea dictumst vestibulum. Sagittis purus sit amet volutpat. Netus et malesuada fames ac turpis egestas. Eget magna fermentum iaculis eu non diam phasellus vestibulum lorem. Varius sit amet mattis vulputate enim. Habitasse platea dictumst quisque sagittis. Integer quis auctor elit sed vulputate mi. Dictumst quisque sagittis purus sit amet.

================================================
FILE: src/content/projects/project-4/index.md
================================================
---
title: "Project Four"
summary: "Lorem ipsum dolor sit amet"
date: "Mar 15 2022"
draft: false
tags:
- React
- Javascript
- StyleX
---

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Vitae ultricies leo integer malesuada nunc vel risus commodo viverra. Adipiscing enim eu turpis egestas pretium. 

Euismod elementum nisi quis eleifend quam adipiscing. In hac habitasse platea dictumst vestibulum. Sagittis purus sit amet volutpat. Netus et malesuada fames ac turpis egestas. 

Eget magna fermentum iaculis eu non diam phasellus vestibulum lorem. 
Varius sit amet mattis vulputate enim. Habitasse platea dictumst quisque sagittis. Integer quis auctor elit sed vulputate mi. Dictumst quisque sagittis purus sit amet.

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Vitae ultricies leo integer malesuada nunc vel risus commodo viverra. Adipiscing enim eu turpis egestas pretium. 

Euismod elementum nisi quis eleifend quam adipiscing. In hac habitasse platea dictumst vestibulum. Sagittis purus sit amet volutpat. Netus et malesuada fames ac turpis egestas. 

Eget magna fermentum iaculis eu non diam phasellus vestibulum lorem. 
Varius sit amet mattis vulputate enim. Habitasse platea dictumst quisque sagittis. Integer quis auctor elit sed vulputate mi. Dictumst quisque sagittis purus sit amet.

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Vitae ultricies leo integer malesuada nunc vel risus commodo viverra. Adipiscing enim eu turpis egestas pretium. 

Euismod elementum nisi quis eleifend quam adipiscing. In hac habitasse platea dictumst vestibulum. Sagittis purus sit amet volutpat. Netus et malesuada fames ac turpis egestas. 

Eget magna fermentum iaculis eu non diam phasellus vestibulum lorem. 
Varius sit amet mattis vulputate enim. Habitasse platea dictumst quisque sagittis. Integer quis auctor elit sed vulputate mi. Dictumst quisque sagittis purus sit amet.

================================================
FILE: src/content/work/apple.md
================================================
---
company: "Apple"
role: "Software Engineer"
dateStart: "01/01/2020"
dateEnd: "11/27/2022"
---

Voluptatem est quaerat voluptas praesentium ipsa dolorem dignissimos nulla ratione distinctio quae maiores eligendi nostrum? Quibusdam, debitis voluptatum, lorem ipsum dolor. Sit amet consectetur adipisicing elit. Iure illo neque tempora.

- Sit amet consectetur adipisicing elit. Iure illo neque tempora.
- Quibusdam, debitis voluptatum, lorem ipsum


================================================
FILE: src/content/work/facebook.md
================================================
---
company: "Facebook"
role: "Intern"
dateStart: "07/01/2019"
dateEnd: "12/31/2019"
---

Iure illo neque tempora, voluptatem est quaerat voluptas praesentium ipsa dolorem dignissimos nulla ratione distinctio quae maiores eligendi nostrum? Quibusdam, debitis voluptatum, lorem ipsum dolor. Sit amet consectetur adipisicing elit.

- Sit amet consectetur adipisicing elit.


================================================
FILE: src/content/work/google.md
================================================
---
company: "Google"
role: "Staff Software Engineer"
dateStart: "11/27/2022"
dateEnd: "Now"
---

Sit amet consectetur adipisicing elit. Iure illo neque tempora, voluptatem est quaerat voluptas praesentium ipsa dolorem dignissimos nulla ratione distinctio quae maiores eligendi nostrum? Quibusdam, debitis voluptatum, lorem ipsum dolor.

- Aadipisicing elit. Iure illo neque tempora, voluptatem est.
- dolorem dignissimos nulla ratione.
- Quibusdam, debitis voluptatum, lorem ipsum dolor.


================================================
FILE: src/content/work/mcdonalds.md
================================================
---
company: "McDonalds"
role: "French Fryer"
dateStart: "03/16/2018"
dateEnd: "07/01/2019"
---

Lorem ipsum dolor, sit amet consectetur adipisicing elit. Iure illo neque tempora, voluptatem est quaerat voluptas praesentium ipsa dolorem dignissimos nulla ratione distinctio quae maiores eligendi nostrum? Quibusdam, debitis voluptatum.

- Quibusdam, debitis voluptatum.
- amet consectetur adipisicing elit. Iure illo neque tempora.


================================================
FILE: src/env.d.ts
================================================
/// <reference path="../.astro/types.d.ts" />
/// <reference types="astro/client" />

================================================
FILE: src/layouts/ArticleBottomLayout.astro
================================================
---
import { type CollectionEntry, getCollection } from "astro:content"

type Props = {
  entry: CollectionEntry<"blog"> | CollectionEntry<"projects">
}

// Get the requested entry
const { entry } = Astro.props
const { collection } = entry
const { Content } = await entry.render()

// Get the next and prev entries (modulo to wrap index)
const items = (await getCollection(collection))
  .filter(post => !post.data.draft)
  .sort((a, b) => b.data.date.getTime() - a.data.date.getTime())
const index = items.findIndex(x => x.slug === entry.slug)
const prev = items[(index - 1 + items.length) % items.length]
const next = items[(index + 1) % items.length]
---

<div>
  <article>
    <Content/>
  </article>
  <div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
    <a href={`/${prev.collection}/${prev.slug}`} class="group p-4 gap-3 flex items-center border rounded-lg hover:bg-black/5 hover:dark:bg-white/10 border-black/15 dark:border-white/20 blend">
      <div class="order-2 w-full h-full group-hover:text-black group-hover:dark:text-white blend">
        <div class="flex flex-wrap gap-2">
          <div class="text-sm uppercase">
            Prev
          </div>
        </div>
        <div class="font-semibold mt-3 text-black dark:text-white">
          {prev.data.title}
        </div>
      </div>
      <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="order-1 stroke-current group-hover:stroke-black group-hover:dark:stroke-white rotate-180">
        <line x1="5" y1="12" x2="19" y2="12" class="scale-x-0 group-hover:scale-x-100 translate-x-4 group-hover:translate-x-1 transition-all duration-300 ease-in-out" />
        <polyline points="12 5 19 12 12 19" class="translate-x-0 group-hover:translate-x-1 transition-all duration-300 ease-in-out" />
      </svg>
    </a>
    <a href={`/${next.collection}/${next.slug}`} class="group p-4 gap-3 flex items-center border rounded-lg hover:bg-black/5 hover:dark:bg-white/10 border-black/15 dark:border-white/20 transition-colors duration-300 ease-in-out">
      <div class="w-full h-full text-right group-hover:text-black group-hover:dark:text-white blend">
        <div class="text-sm uppercase">
          Next
        </div>
        <div class="font-semibold mt-3 text-black dark:text-white">
          {next.data.title}
        </div>
      </div>
      <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="stroke-current group-hover:stroke-black group-hover:dark:stroke-white">
        <line x1="5" y1="12" x2="19" y2="12" class="scale-x-0 group-hover:scale-x-100 translate-x-4 group-hover:translate-x-1 transition-all duration-300 ease-in-out" />
        <polyline points="12 5 19 12 12 19" class="translate-x-0 group-hover:translate-x-1 transition-all duration-300 ease-in-out" />
      </svg>
    </a>
  </div>
</div>


================================================
FILE: src/layouts/ArticleTopLayout.astro
================================================
---
import type { CollectionEntry } from "astro:content"
import { formatDate, readingTime } from "@lib/utils"

type Props = {
  entry: CollectionEntry<"projects"> | CollectionEntry<"blog">
}

const { entry } = Astro.props
const { collection, data, body } = entry
const { title, summary, date } = data

const demoUrl = collection === "projects" ? data.demoUrl : null
const repoUrl = collection === "projects" ? data.repoUrl : null
---

<div>
  <a href={`/${collection}`} class="group w-fit p-1.5 gap-1.5 text-sm flex items-center border rounded hover:bg-black/5 hover:dark:bg-white/10 border-black/15 dark:border-white/20 transition-colors duration-300 ease-in-out">
    <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="stroke-current group-hover:stroke-black group-hover:dark:stroke-white">
      <line x1="19" y1="12" x2="5" y2="12" class="scale-x-0 group-hover:scale-x-100 translate-x-3 group-hover:translate-x-0 transition-all duration-300 ease-in-out" />
      <polyline points="12 19 5 12 12 5" class="translate-x-1 group-hover:translate-x-0 transition-all duration-300 ease-in-out" />
    </svg>
    <div class="w-full group-hover:text-black group-hover:dark:text-white transition-colors duration-300 ease-in-out">
      Back to {collection}
    </div>
  </a>
  <div class="flex flex-wrap text-sm uppercase mt-12 gap-3 opacity-75">
    <div class="flex items-center gap-2">
      <svg class="size-5 stroke-current">
        <use href="/ui.svg#calendar"/>
      </svg>
      {formatDate(date)}
    </div>
    <div class="flex items-center gap-2">
      <svg class="size-5 stroke-current">
        <use href="/ui.svg#book-open"/>
      </svg>
      {readingTime(body)}
    </div>
  </div>
  <h1 class="text-3xl font-semibold text-black dark:text-white mt-2">
    {title}
  </h1>
  <div class="mt-1">
    {summary}
  </div>
  {(demoUrl || repoUrl) && 
  <div class="mt-4 flex flex-wrap gap-2">
    {demoUrl && 
      <a href={demoUrl} target="_blank" class="group flex gap-2 items-center px-3 py-1.5 truncate rounded text-xs md:text-sm lg:text-base border border-black/25 dark:border-white/25 hover:bg-black/5 hover:dark:bg-white/15 blend">
        <svg class="size-4">
          <use href="/ui.svg#globe" class="fill-current group-hover:fill-black group-hover:dark:fill-white blend"/>
        </svg>
        <span class="text-current group-hover:text-black group-hover:dark:text-white blend">
          See Demo
        </span>
      </a>
    }
    {repoUrl && 
      <a href={repoUrl} target="_blank" class="group flex gap-2 items-center px-3 py-1.5 truncate rounded text-xs md:text-sm lg:text-base border border-black/25 dark:border-white/25 hover:bg-black/5 hover:dark:bg-white/15 blend">
        <svg class="size-4">
          <use href="/ui.svg#link" class="fill-current group-hover:fill-black group-hover:dark:fill-white blend"/>
        </svg>
        <span class="text-current group-hover:text-black group-hover:dark:text-white blend">
          See Repository
        </span>
      </a>
    }
  </div> 
  }
</div>


================================================
FILE: src/layouts/BottomLayout.astro
================================================
---
import Container from "@components/Container.astro"
---

<div class="flex-1 py-5">
  <Container size="md">
    <slot/>
  </Container>
</div>

================================================
FILE: src/layouts/PageLayout.astro
================================================
---
import "@styles/global.css"
import BaseHead from "@components/BaseHead.astro"
import Header from "@components/Header.astro"
import Footer from "@components/Footer.astro"
import Drawer from "@components/Drawer.astro"
const { title, description } = Astro.props
import { SITE } from "@consts"
---

<!doctype html>
<html lang="en">
  <head>
    <BaseHead title={`${title} | ${SITE.TITLE}`} description={description} />
  </head>
  <body>
    <Header />
    <Drawer />
    <main>
      <slot />
    </main>
    <Footer />
  </body>
</html>


================================================
FILE: src/layouts/TopLayout.astro
================================================
---
import Container from "@components/Container.astro"
---

<div class="pt-36 pb-5">
  <Container size="md">
    <slot/>
  </Container>
</div>

================================================
FILE: src/lib/utils.ts
================================================
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}

export function formatDate(date: Date) {
  return Intl.DateTimeFormat("en-US", {
    month: "short",
    day: "2-digit",
    year: "numeric"
  }).format(date)
}

export function readingTime(html: string) {
  const textOnly = html.replace(/<[^>]+>/g, "")
  const wordCount = textOnly.split(/\s+/).length
  const readingTimeMinutes = ((wordCount / 200) + 1).toFixed()
  return `${readingTimeMinutes} min read`
}


export function truncateText(str: string, maxLength: number): string {
  const ellipsis = '…';

  if (str.length <= maxLength) return str;

  const trimmed = str.trimEnd();
  if (trimmed.length <= maxLength) return trimmed;

  const cutoff = maxLength - ellipsis.length;
  let sliced = str.slice(0, cutoff).trimEnd();

  return sliced + ellipsis;
}

================================================
FILE: src/pages/blog/[...slug].astro
================================================
---
import { type CollectionEntry, getCollection } from "astro:content"
import PageLayout from "@layouts/PageLayout.astro"
import TopLayout from "@layouts/TopLayout.astro"
import BottomLayout from "@layouts/BottomLayout.astro"
import ArticleTopLayout from "@layouts/ArticleTopLayout.astro"
import ArticleBottomLayout from "@layouts/ArticleBottomLayout.astro"

// Create the static blog pages
export async function getStaticPaths() {
	const posts = await getCollection("blog")
	return posts.map((post) => ({
		params: { slug: post.slug },
		props: post,
	}))
}

// Get the requested post
type Props = CollectionEntry<"blog">
const post = Astro.props
const { title, summary } = post.data
---

<PageLayout title={title} description={summary}>
  <TopLayout>
    <div class="animate">
      <ArticleTopLayout entry={post}/>
    </div>
  </TopLayout>
  <BottomLayout>
    <div class="animate">
      <ArticleBottomLayout entry={post} />
    </div>
  </BottomLayout>
</PageLayout>

================================================
FILE: src/pages/blog/index.astro
================================================
---
import { getCollection } from "astro:content";
import PageLayout from "@layouts/PageLayout.astro";
import TopLayout from "@layouts/TopLayout.astro";
import BottomLayout from "@layouts/BottomLayout.astro";
import SearchCollection from "@components/SearchCollection";
import { BLOG } from "@consts";

const posts = (await getCollection("blog"))
  .filter((post) => !post.data.draft)
  .sort((a, b) => b.data.date.getTime() - a.data.date.getTime());

const tags = [...new Set(posts.flatMap((post) => post.data.tags))].sort(
  (a, b) => a.localeCompare(b),
);

const estimated_initial_size =
  28 + posts.length * 158 + (posts.length - 1) * 12;
---

<PageLayout title={BLOG.TITLE} description={BLOG.DESCRIPTION}>
  <TopLayout>
    <div class="animate">
      <h1 class="text-3xl font-semibold text-black dark:text-white mt-2">
        {BLOG.TITLE}
      </h1>
      <div class="mt-1">
        {BLOG.DESCRIPTION}
      </div>
    </div>
  </TopLayout>
  <BottomLayout>
    <div
      id="search-collection-wrapper"
      class="animate"
      style={{ minHeight: `${estimated_initial_size}px` }}
    >
      <SearchCollection
        client:load
        entry_name={"posts"}
        tags={tags}
        data={posts}
      />
    </div>
  </BottomLayout>
</PageLayout>


================================================
FILE: src/pages/index.astro
================================================
---
import { getCollection } from "astro:content"
import PageLayout from "@layouts/PageLayout.astro"
import ArrowCard from "@components/ArrowCard"
import StackCard from "@components/StackCard.astro"
import { SITE, SOCIALS } from "@consts"
import TwinklingStars from "@components/TwinklingStars.astro"
import MeteorShower from "@components/MeteorShower.astro"

const posts = (await getCollection("blog"))
  .filter(post => !post.data.draft)
  .sort((a, b) => b.data.date.getTime() - a.data.date.getTime())
  .slice(0,3)

const projects = (await getCollection("projects"))
  .filter(project => !project.data.draft)
  .sort((a, b) => b.data.date.getTime() - a.data.date.getTime())
  .slice(0,3)

const stack = [
  { 
    text: "Astro", 
    icon: "astro", 
    href: "https://astro.build" 
  },
  { 
    text: "Javascript", 
    icon: "javascript", 
    href: "https://www.javascript.com" 
  },
  { 
    text: "Typescript", 
    icon: "typescript", 
    href: "https://www.typescriptlang.org" 
  },
  { 
    text: "Tailwind", 
    icon: "tailwind", 
    href: "https://tailwindcss.com" 
  },
]
---

<PageLayout title="Home" description={SITE.DESCRIPTION}>

  <!-- Light Mode: Particles -->
  <div class='absolute inset-0 block dark:hidden'>
    <div id='particles1' class='fixed inset-0'></div>
    <div id='particles2' class='fixed inset-0'></div>
    <div id='particles3' class='fixed inset-0'></div>
  </div>

  <!-- Dark Theme: Stars -->
  <div class='absolute inset-0 bg-black hidden dark:block'>
    <div id='stars1' class='fixed inset-0'></div>
    <div id='stars2' class='fixed inset-0'></div>
    <div id='stars3' class='fixed inset-0'></div>
  </div>

  <!-- Dark Theme: Twinkling Stars / Metors -->
  <div id="galaxy" class="fixed inset-0">
    <div class="hidden dark:block">
      <TwinklingStars/>
      <MeteorShower/>
    </div>
  </div>

  <script is:inline src="/js/bg.js"></script>
  
  <!-- HERO -->
  <section class="relative h-screen w-full">
    <div id="planetcont" class='animate absolute inset-0 top-1/4 overflow-hidden'>
      <div id="crescent" class='absolute top-0 left-1/2 -translate-x-1/2 w-[250vw] min-h-[100vh] aspect-square rounded-full p-[1px] bg-gradient-to-b from-black/25 dark:from-white/75 from-0% to-transparent to-5%'>
        <div id="planet" class='w-full h-full bg-white dark:bg-black rounded-full p-[1px] overflow-hidden flex justify-center'>
          <div id="blur" class='w-full h-20 rounded-full bg-neutral-900/25 dark:bg-white/25 blur-3xl'/>
        </div>
      </div>
    </div>
    <div class="animate absolute h-full w-full flex items-center justify-center">
      <div class='relative w-full h-full flex items-center justify-center'>
        <div class='p-5 text-center'>
          <p class='animated text-lg md:text-xl lg:text-2xl font-semibold opacity-75'>
            Hello, I am ...
          </p>
          <p class='animated text-2xl md:text-3xl lg:text-4xl font-bold uppercase text-black dark:text-white'>
            Astro Sphere
          </p>
          <p class='animated text-sm md:text-base lg:text-lg opacity-75'>
            Currently designing products for humans.
          </p>
          <div id="ctaButtons" class='animated flex flex-wrap gap-4 justify-center mt-5'>
            <a href='/blog' class='py-2 px-4 rounded truncate text-xs md:text-sm lg:text-base bg-black dark:bg-white text-white dark:text-black hover:opacity-75 blend'>
              Read my blog
            </a>
            <a href='/work' class='py-2 px-4 truncate rounded text-xs md:text-sm lg:text-base border border-black/25 dark:border-white/25 hover:bg-black/5 hover:dark:bg-white/15 blend'>
              View my work
            </a>
          </div>
        </div>
      </div>
    </div>
  </section>

  <div class="relative bg-white dark:bg-black">
    <div class="mx-auto max-w-screen-sm p-5 space-y-24 pb-16">

      <!-- About Section -->
      <section class="animate">
        <article>
          <p>I am a <b><i>software engineer</i></b>, <b><i>ui/ux designer</i></b>, <b><i>product planner</i></b>, <b><i>mentor</i></b>, <b><i>student</i></b>, <b><i>minimalist</i></b>, <b><i>eternal optimist</i></b>, <b><i>crypto enthusiast</i></b> and <b><i>sarcasm connoisseur</i></b>.</p>
          <p>I love to both build and break things. I am motivated by challenging projects with self-guided research and dynamic problem solving. My true passion is crafting creative front end designs with unique takes on color, typography and motion.</p>
          <p>During my career</p>
          <p>I have built products ranging from marketing and ecommerce websites to complex enterprise apps with focus on delivering fast, elegant code along with delightful user interfaces.</p>
          <p>Now</p>
          <p>I currently work as a software engineer at StreamlineFS, where I do product planning, design and development.</p>
        </article>
      </section>

      <!-- Blog Preview Section -->
      <section class="animate">
        <div class="space-y-4">
          <div class="flex justify-between">
            <p class="font-semibold text-black dark:text-white">
              Recent posts
            </p>
            <a href="/blog" class="w-fit col-span-3 group flex gap-1 items-center underline decoration-[.5px] decoration-black/25 dark:decoration-white/50 hover:decoration-black dark:hover:decoration-white text-black dark:text-white underline-offset-2 blend">
              <span class="text-black/75 dark:text-white/75 group-hover:text-black group-hover:dark:text-white blend">
                All posts
              </span>
            </a>
          </div>
          <ul class="space-y-4">
            {posts.map((post) => (
              <li>
                <ArrowCard entry={post} />
              </li>
            ))}
          </ul>
        </div>
      </section>

      <!-- Tech Stack Section -->
      <section class="animate">
        <div class="space-y-4">
          <p class="font-semibold text-black dark:text-white">
            Website build with
          </p>
          <div class="flex flex-wrap items-center gap-2 mt-5">
            {stack.map(item => (
              <StackCard 
                text={item.text}
                icon={item.icon}
                href={item.href}
              />
            ))}
          </div>
          <div>
            Performing reactivity and statefulness, special guest
            <a href="https://www.solidjs.com/" target="_blank" class="w-fit group underline decoration-[.5px] decoration-black/25 dark:decoration-white/50 hover:decoration-black dark:hover:decoration-white text-black dark:text-white underline-offset-2 blend">
              <span class="text-black/75 dark:text-white/75 group-hover:text-black group-hover:dark:text-white blend">
                SolidJS
              </span>
            </a>
          </div>
        </div>
      </section>

      <!-- Project Preview Section -->
      <section class="animate">
        <div class="space-y-4">
          <div class="flex justify-between">
            <p class="font-semibold text-black dark:text-white">
              Recent projects
            </p>
            <a href="/projects" class="w-fit col-span-3 group flex gap-1 items-center underline decoration-[.5px] decoration-black/25 dark:decoration-white/50 hover:decoration-black dark:hover:decoration-white text-black dark:text-white underline-offset-2 blend">
              <span class="text-black/75 dark:text-white/75 group-hover:text-black group-hover:dark:text-white blend">
                All projects
              </span>
            </a>
          </div>
          <ul class="space-y-4">
            {projects.map((project) => (
              <li>
                <ArrowCard entry={project} />
              </li>
            ))}
          </ul>
        </div>
      </section>

      <!-- Contact Section -->
      <section class="animate">
        <div>
          <p class="font-semibold text-black dark:text-white">
            Let's Connect
          </p>
          <p>
            Reach out to me via email or on social media.
          </p>
          <div class="grid grid-cols-4 gap-y-2 mt-4 auto-cols-min">
            {SOCIALS.map(social => (
              <div class="col-span-1 flex items-center gap-1">
                <span class="whitespace-nowrap truncate">
                  {social.NAME}
                </span>
              </div>
              <div class="col-span-3 truncate">
                <a href={social.HREF} target="_blank" class="w-fit col-span-3 group flex gap-1 items-center underline decoration-[.5px] decoration-black/25 dark:decoration-white/50 hover:decoration-black dark:hover:decoration-white text-black dark:text-white underline-offset-2 blend">
                  <span class="text-black/75 dark:text-white/75 group-hover:text-black group-hover:dark:text-white blend">
                    {social.TEXT}
                  </span>
                </a>
              </div>
            ))}
        </div>
      </section>
    </div>
  </div>
</PageLayout>


================================================
FILE: src/pages/legal/[...slug].astro
================================================
---
import { type CollectionEntry, getCollection } from "astro:content"
import PageLayout from "@layouts/PageLayout.astro"
import TopLayout from "@layouts/TopLayout.astro"
import BottomLayout from "@layouts/BottomLayout.astro"
import { formatDate } from "@lib/utils"
import { SITE } from "@consts"

// Create the static pages for legal docs
export async function getStaticPaths() {
	const docs = await getCollection("legal")
	return docs.map((doc) => ({
		params: { slug: doc.slug },
		props: doc,
	}))
}

// Get the requested legal doc
type Props = CollectionEntry<"legal">;
const doc = Astro.props
const { title, date } = doc.data
const { Content } = await doc.render();

---

<PageLayout title={title} description={`${title} for ${SITE.TITLE}`}>
  <TopLayout>
    <div class="animate">
      <div class="page-heading">
        {title}
      </div>
      <p class="font-normal opacity-75">
        Last updated: {formatDate(date)}
      </p>
    </div>
  </TopLayout>
  <BottomLayout>
    <article class="animate">
      <Content/>
    </article>
  </BottomLayout>
</PageLayout>

================================================
FILE: src/pages/projects/[...slug].astro
================================================
---
import { type CollectionEntry, getCollection } from "astro:content"
import PageLayout from "@layouts/PageLayout.astro"
import TopLayout from "@layouts/TopLayout.astro"
import BottomLayout from "@layouts/BottomLayout.astro"
import ArticleTopLayout from "@layouts/ArticleTopLayout.astro"
import ArticleBottomLayout from "@layouts/ArticleBottomLayout.astro"

// Create the static projects pages
export async function getStaticPaths() {
	const projects = await getCollection("projects")
	return projects.map((project) => ({
		params: { slug: project.slug },
		props: project,
	}))
}

// Get the requested project
type Props = CollectionEntry<"projects">
const project = Astro.props
const { title, summary } = project.data
---

<PageLayout title={title} description={summary}>
  <TopLayout>
    <div class="animate">
      <ArticleTopLayout entry={project} />
    </div>
  </TopLayout>
  <BottomLayout>
    <div class="animate">
      <ArticleBottomLayout entry={project} />
    </div>
  </BottomLayout>
</PageLayout>

================================================
FILE: src/pages/projects/index.astro
================================================
---
import { getCollection } from "astro:content";
import PageLayout from "@layouts/PageLayout.astro";
import TopLayout from "@layouts/TopLayout.astro";
import BottomLayout from "@layouts/BottomLayout.astro";
import SearchCollection from "@components/SearchCollection";
import { PROJECTS } from "@consts";
import type { CollectionEntry } from "astro:content";

const projects = (await getCollection("projects"))
  .filter((project) => !project.data.draft)
  .sort((a, b) => b.data.date.getTime() - a.data.date.getTime());

const tags = [
  ...new Set(projects.flatMap((project) => project.data.tags)),
].sort((a, b) => a.localeCompare(b));

const estimated_initial_size =
  28 + projects.length * 158 + (projects.length - 1) * 12;
---

<PageLayout title={PROJECTS.TITLE} description={PROJECTS.DESCRIPTION}>
  <TopLayout>
    <div class="animate">
      <h1 class="text-3xl font-semibold text-black dark:text-white mt-2">
        {PROJECTS.TITLE}
      </h1>
      <div class="mt-1">
        {PROJECTS.DESCRIPTION}
      </div>
    </div>
  </TopLayout>
  <BottomLayout>
    <div
      id="search-collection-wrapper"
      class="animate"
      style={{ minHeight: `${estimated_initial_size}px` }}
    >
      <SearchCollection
        client:load
        entry_name={"projects"}
        tags={tags}
        data={projects}
      />
    </div>
  </BottomLayout>
</PageLayout>


================================================
FILE: src/pages/robots.txt.ts
================================================
import type { APIRoute } from "astro"

const robotsTxt = `
User-agent: *
Allow: /

Sitemap: ${new URL("sitemap-index.xml", import.meta.env.SITE).href}
`.trim()

export const GET: APIRoute = () => {
  return new Response(robotsTxt, {
    headers: {
      "Content-Type": "text/plain; charset=utf-8",
    },
  })
}


================================================
FILE: src/pages/rss.xml.ts
================================================
import rss from "@astrojs/rss"
import { getCollection } from "astro:content"
import { SITE } from "@consts"

type Context = {
  site: string
}

export async function GET(context: Context) {
	const posts = await getCollection("blog")
  const projects = await getCollection("projects")

  const items = [...posts, ...projects]

  items.sort((a, b) => new Date(b.data.date).getTime() - new Date(a.data.date).getTime())

  return rss({
    title: SITE.TITLE,
    description: SITE.DESCRIPTION,
    site: context.site,
    items: items.map((item) => ({
      title: item.data.title,
      description: item.data.summary,
      pubDate: item.data.date,
      link: item.slug.startsWith("blog")
        ? `/blog/${item.slug}/`
        : `/projects/${item.slug}/`,
    })),
  })
}


================================================
FILE: src/pages/search/index.astro
================================================
---
import { type CollectionEntry, getCollection } from "astro:content";
import PageLayout from "@layouts/PageLayout.astro";
import TopLayout from "@layouts/TopLayout.astro";
import BottomLayout from "@layouts/BottomLayout.astro";
import Search from "@components/Search";
import { SEARCH } from "@consts";

const posts = (await getCollection("blog")).filter((post) => !post.data.draft);

const projects = (await getCollection("projects")).filter(
  (post) => !post.data.draft,
);

const data = [...posts, ...projects] as CollectionEntry<"blog">[];
---

<PageLayout title={SEARCH.TITLE} description={SEARCH.DESCRIPTION}>
  <TopLayout>
    <div class="animate page-heading">
      {SEARCH.TITLE}
    </div>
  </TopLayout>
  <BottomLayout>
    <div class="animate">
      <Search client:load data={data} />
    </div>
  </BottomLayout>
</PageLayout>


================================================
FILE: src/pages/work/index.astro
================================================
---
import { getCollection } from "astro:content"
import PageLayout from "@layouts/PageLayout.astro"
import TopLayout from "@layouts/TopLayout.astro"
import BottomLayout from "@layouts/BottomLayout.astro"
import { WORK } from "@consts"

const collection = await getCollection("work")

collection.sort((a, b) => new Date(b.data.dateStart).getTime() - new Date(a.data.dateStart).getTime())

const work = await Promise.all(
  collection.map(async (item) => {
    const { Content } = await item.render()
    return { ...item, Content }
  })
)

function formatWorkDate(input: Date | string) {
  if (typeof input === "string") return input

  const month = input.toLocaleDateString("en-US", {
    month: "short",
  })

  const year = new Date(input).getFullYear()
  return `${month} ${year}`
}
---

<PageLayout title={WORK.TITLE} description={WORK.DESCRIPTION}>
  <TopLayout>
    <div class="animate page-heading">
      {WORK.TITLE}
    </div>
  </TopLayout>
  <BottomLayout>
    <ul>
      { 
        work.map((entry) => (
          <li class="animate border-b border-black/10 dark:border-white/25 mt-4 py-8 first-of-type:mt-0 first-of-type:pt-0 last-of-type:border-none">
            <div class="text-sm uppercase mb-4">
              {formatWorkDate(entry.data.dateStart)} - {formatWorkDate(entry.data.dateEnd)}
            </div>
            <div class="text-black dark:text-white font-semibold">
              {entry.data.company}
            </div>
            <div class="text-sm font-semibold">
              {entry.data.role}
            </div>
            <article class="prose dark:prose-invert">
              <entry.Content />
            </article>
          </li>
        ))
      }
    </ul>
  </BottomLayout>
</PageLayout>


================================================
FILE: src/styles/global.css
================================================
@tailwind base;
@tailwind components;
@tailwind utilities;

:root {
  --copy-btn-margin: 10px;
}

@layer base {
  @font-face {
    font-family: "Atkinson";
    src: url("/fonts/atkinson-regular.woff") format("woff");
    font-weight: 400;
    font-style: normal;
    font-display: swap;
  }

  @font-face {
    font-family: "Atkinson";
    src: url("/fonts/atkinson-bold.woff") format("woff");
    font-weight: 700;
    font-style: normal;
    font-display: swap;
  }
}

html {
  overflow-y: scroll;
  color-scheme: light;
  background-color: white;
  font-family: "Atkinson", sans-serif;

}

html.dark {
  color-scheme: dark;
  background-color: black;
}

html,
body {
  @apply h-full w-full antialiased;
  @apply bg-white dark:bg-black;
  @apply text-black/75 dark:text-white/75;
}

body {
  @apply relative flex flex-col;
}

main {
  @apply flex flex-col flex-1 bg-white dark:bg-black;
}

header {
  @apply border-b;
  @apply transition-all duration-300 ease-in-out;
}

header:not(.scrolled) {
  @apply bg-transparent border-transparent;
}

header.scrolled {
  @apply bg-white/75 dark:bg-black/50;
  @apply border-black/10 dark:border-white/25;
  @apply backdrop-blur-sm saturate-200;
}

article {
  @apply prose dark:prose-invert max-w-full pb-12;
}

.page-heading {
  @apply font-semibold text-black dark:text-white;
}

.blend {
  @apply transition-all duration-300 ease-in-out;
}

/** Light theme particles on home page */
@keyframes animateParticle {
  from {
    transform: translateY(0px);
  }

  to {
    transform: translateY(-2000px);
  }
}

/** styles for public /animation.js */
.animate {
  opacity: 0;
  transform: translateY(50px);
  transition: opacity 1s ease, transform 1s ease;
}

.animate.show {
  opacity: 1;
  transform: translateY(0);
}

article img {
  padding-top: 20px;
  padding-bottom: 20px;
  display: block;
  margin: 0 auto;
}

/**
 * TWINKLE STARS
 */

#twinkle-star.template {
  @apply absolute -left-full;
  /* hide offscreen */
}

#twinkle-star.twinkle {
  @apply animate-twinkle;
  /* defined in tailwind.config */
}


/**
 * Meteors
 */

#meteors .shower {
  @apply absolute inset-0 top-0;
  ;
  @apply left-1/2 -translate-x-1/2;
  @apply w-screen aspect-square;
}

#meteors .meteor {
  @apply animate-meteor;
  /* defined in tailwind.config */
  @apply absolute top-1/2 left-1/2 w-px h-[75vh];
  @apply bg-gradient-to-b from-white to-transparent;
}

#meteors .shower.ur {
  @apply rotate-45;
}

#meteors .shower.dr {
  @apply rotate-135;
}

#meteors .shower.dl {
  @apply rotate-225;
}

#meteors .shower.ul {
  @apply rotate-315;
}

.copy-cnt {
  @apply absolute w-full;
  top: var(--copy-btn-margin);
}

.copy-btn {
  @apply w-[30px] fixed;
  left: calc(100% - var(--copy-btn-margin));
  transform: translateX(-100%);
}

.copy-svg {
  @apply w-full aspect-square text-white opacity-70 hover:opacity-90;
}

/* Card transition */
/* .fade-move-enter-active,
.fade-move-exit-active {
  transition: all 300ms ease;
}

.fade-move-enter-from,
.fade-move-exit-to {
  opacity: 0;
  transform: translateY(10px);
} */

================================================
FILE: src/types.ts
================================================
export type Page = {
  TITLE: string
  DESCRIPTION: string
}

export interface Site extends Page {
  AUTHOR: string
}

export type Links = {
  TEXT: string
  HREF: string
}[]

export type Socials = {
  NAME: string
  ICON: string
  TEXT: string
  HREF: string
}[]

================================================
FILE: tailwind.config.mjs
================================================
import defaultTheme from "tailwindcss/defaultTheme"

/** @type {import('tailwindcss').Config} */
export default {
  darkMode: ["class"],
  content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"],
  theme: {
    extend: {
      fontFamily: {
        "sans": ["Atkinson", ...defaultTheme.fontFamily.sans],
      },
      typography: {
        DEFAULT: {
          css: {
            maxWidth: "full",
          },
        },
      },
      rotate: {
        "45": "45deg",
        "135": "135deg",
        "225": "225deg",
        "315": "315deg",
      },
      animation: {
        twinkle: "twinkle 2s ease-in-out forwards",
        meteor: "meteor 3s ease-in-out forwards",
      },
      keyframes: {
        twinkle: {
          "0%": { 
            opacity: 0, 
            transform: "rotate(0deg)" 
          },
          "50%": { 
            opacity: 1,
            transform: "rotate(180deg)" 
          },
          "100%": { 
            opacity: 0, 
            transform: "rotate(360deg)" 
          },
        },
        meteor: {
          "0%": { 
            opacity: 0, 
            transform: "translateY(200%)" 
          },
          "50%": { 
            opacity: 1  
          },
          "100%": { 
            opacity: 0, 
            transform: "translateY(0)" 
          },
        },
      },
    },
  },
  plugins: [require("@tailwindcss/typography")],
}


================================================
FILE: tsconfig.json
================================================
{
  "extends": "astro/tsconfigs/strict",
  "compilerOptions": {
    "strictNullChecks": true,
    "baseUrl": ".",
    "paths": {
      "@*": [
        "src/*"
      ]
    },
    "jsx": "preserve",
    "jsxImportSource": "solid-js"
  },
  "include": [
    "src/**/*.ts",
    "src/**/*.tsx",
    "src/**/*.astro"
  ],
  "exclude": [
    "dist",
    "node_modules"
  ]
}
Download .txt
gitextract_txjs_ovg/

├── .github/
│   └── workflows/
│       └── stale.yaml
├── .gitignore
├── .vscode/
│   ├── extensions.json
│   ├── launch.json
│   └── settings.json
├── LICENSE
├── README.md
├── astro.config.mjs
├── package.json
├── public/
│   ├── js/
│   │   ├── animate.js
│   │   ├── bg.js
│   │   ├── copy.js
│   │   ├── scroll.js
│   │   └── theme.js
│   └── robots.txt
├── src/
│   ├── components/
│   │   ├── ArrowCard.tsx
│   │   ├── BaseHead.astro
│   │   ├── Container.astro
│   │   ├── Counter.tsx
│   │   ├── Drawer.astro
│   │   ├── Footer.astro
│   │   ├── Header.astro
│   │   ├── MeteorShower.astro
│   │   ├── Search.tsx
│   │   ├── SearchBar.tsx
│   │   ├── SearchCollection.tsx
│   │   ├── StackCard.astro
│   │   └── TwinklingStars.astro
│   ├── consts.ts
│   ├── content/
│   │   ├── blog/
│   │   │   ├── 01-astro-sphere-file-structure/
│   │   │   │   └── index.md
│   │   │   ├── 02-astro-sphere-getting-started/
│   │   │   │   └── index.md
│   │   │   ├── 03-astro-sphere-add-new-post-or-projects/
│   │   │   │   └── index.md
│   │   │   ├── 04-astro-sphere-writing-markdown/
│   │   │   │   └── index.md
│   │   │   ├── 05-astro-sphere-writing-mdx/
│   │   │   │   ├── MyComponent.astro
│   │   │   │   └── index.mdx
│   │   │   └── 06-astro-sphere-social-links/
│   │   │       └── index.md
│   │   ├── config.ts
│   │   ├── legal/
│   │   │   ├── privacy.md
│   │   │   └── terms.md
│   │   ├── projects/
│   │   │   ├── project-1/
│   │   │   │   └── index.md
│   │   │   ├── project-2/
│   │   │   │   └── index.md
│   │   │   ├── project-3/
│   │   │   │   └── index.md
│   │   │   └── project-4/
│   │   │       └── index.md
│   │   └── work/
│   │       ├── apple.md
│   │       ├── facebook.md
│   │       ├── google.md
│   │       └── mcdonalds.md
│   ├── env.d.ts
│   ├── layouts/
│   │   ├── ArticleBottomLayout.astro
│   │   ├── ArticleTopLayout.astro
│   │   ├── BottomLayout.astro
│   │   ├── PageLayout.astro
│   │   └── TopLayout.astro
│   ├── lib/
│   │   └── utils.ts
│   ├── pages/
│   │   ├── blog/
│   │   │   ├── [...slug].astro
│   │   │   └── index.astro
│   │   ├── index.astro
│   │   ├── legal/
│   │   │   └── [...slug].astro
│   │   ├── projects/
│   │   │   ├── [...slug].astro
│   │   │   └── index.astro
│   │   ├── robots.txt.ts
│   │   ├── rss.xml.ts
│   │   ├── search/
│   │   │   └── index.astro
│   │   └── work/
│   │       └── index.astro
│   ├── styles/
│   │   └── global.css
│   └── types.ts
├── tailwind.config.mjs
└── tsconfig.json
Download .txt
SYMBOL INDEX (37 symbols across 14 files)

FILE: public/js/animate.js
  function animate (line 1) | function animate() {

FILE: public/js/bg.js
  function generateParticles (line 2) | function generateParticles(n) {
  function generateStars (line 10) | function generateStars(n) {
  function getRandom (line 18) | function getRandom(max) {
  function initBG (line 22) | function initBG() {

FILE: public/js/copy.js
  function copyCode (line 31) | function copyCode(event) {
  function getChildByTagName (line 43) | function getChildByTagName(element, tagName) {

FILE: public/js/scroll.js
  function onScroll (line 1) | function onScroll() {

FILE: public/js/theme.js
  function changeTheme (line 1) | function changeTheme() {
  function preloadTheme (line 31) | function preloadTheme() {
  function initializeThemeButtons (line 54) | function initializeThemeButtons() {

FILE: src/components/ArrowCard.tsx
  type Props (line 4) | type Props = {
  function ArrowCard (line 9) | function ArrowCard({ entry, pill }: Props) {

FILE: src/components/Counter.tsx
  function CounterButton (line 3) | function CounterButton() {

FILE: src/components/Search.tsx
  type Props (line 7) | type Props = {
  function Search (line 11) | function Search({ data }: Props) {

FILE: src/components/SearchBar.tsx
  type Props (line 1) | type Props = {
  function SearchBar (line 8) | function SearchBar({ onSearchInput, query, setQuery, placeholderText }: ...

FILE: src/components/SearchCollection.tsx
  type Props (line 8) | type Props = {
  function SearchCollection (line 14) | function SearchCollection({ entry_name, data, tags }: Props) {

FILE: src/consts.ts
  constant SITE (line 4) | const SITE: Site = {
  constant WORK (line 11) | const WORK: Page = {
  constant BLOG (line 17) | const BLOG: Page = {
  constant PROJECTS (line 23) | const PROJECTS: Page = {
  constant SEARCH (line 29) | const SEARCH: Page = {
  constant LINKS (line 35) | const LINKS: Links = [
  constant SOCIALS (line 55) | const SOCIALS: Socials = [

FILE: src/lib/utils.ts
  function cn (line 4) | function cn(...inputs: ClassValue[]) {
  function formatDate (line 8) | function formatDate(date: Date) {
  function readingTime (line 16) | function readingTime(html: string) {
  function truncateText (line 24) | function truncateText(str: string, maxLength: number): string {

FILE: src/pages/rss.xml.ts
  type Context (line 5) | type Context = {
  function GET (line 9) | async function GET(context: Context) {

FILE: src/types.ts
  type Page (line 1) | type Page = {
  type Site (line 6) | interface Site extends Page {
  type Links (line 10) | type Links = {
  type Socials (line 15) | type Socials = {
Condensed preview — 68 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (116K chars).
[
  {
    "path": ".github/workflows/stale.yaml",
    "chars": 725,
    "preview": "name: Close inactive issues\non:\n  workflow_dispatch:\n  schedule:\n    - cron: \"0 0 * * *\"\n\njobs:\n  close-issues:\n    runs"
  },
  {
    "path": ".gitignore",
    "chars": 229,
    "preview": "# build output\ndist/\n\n# generated types\n.astro/\n\n# dependencies\nnode_modules/\n\n# logs\nnpm-debug.log*\nyarn-debug.log*\nyar"
  },
  {
    "path": ".vscode/extensions.json",
    "chars": 111,
    "preview": "{\n  \"recommendations\": [\"astro-build.astro-vscode\", \"unifiedjs.vscode-mdx\"],\n  \"unwantedRecommendations\": []\n}\n"
  },
  {
    "path": ".vscode/launch.json",
    "chars": 207,
    "preview": "{\n  \"version\": \"0.2.0\",\n  \"configurations\": [\n    {\n      \"command\": \"./node_modules/.bin/astro dev\",\n      \"name\": \"Dev"
  },
  {
    "path": ".vscode/settings.json",
    "chars": 80,
    "preview": "{\n  \"[astro]\": {\n    \"editor.defaultFormatter\": \"astro-build.astro-vscode\"\n  }\n}"
  },
  {
    "path": "LICENSE",
    "chars": 1066,
    "preview": "MIT License\n\nCopyright (c) 2024 Mark Horn\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\n"
  },
  {
    "path": "README.md",
    "chars": 3048,
    "preview": "![Astro Sphere Lighthouse Score](_astrosphere.jpg)\n\nAstro Sphere is a static, minimalist, lightweight, lightning fast po"
  },
  {
    "path": "astro.config.mjs",
    "chars": 391,
    "preview": "import { defineConfig } from \"astro/config\"\nimport mdx from \"@astrojs/mdx\"\nimport sitemap from \"@astrojs/sitemap\"\nimport"
  },
  {
    "path": "package.json",
    "chars": 850,
    "preview": "{\n  \"name\": \"astro-sphere\",\n  \"type\": \"module\",\n  \"version\": \"1.0.0\",\n  \"scripts\": {\n    \"dev\": \"astro dev\",\n    \"dev:ne"
  },
  {
    "path": "public/js/animate.js",
    "chars": 331,
    "preview": "function animate() {\n  const animateElements = document.querySelectorAll('.animate')\n\n  animateElements.forEach((element"
  },
  {
    "path": "public/js/bg.js",
    "chars": 2435,
    "preview": "\n  function generateParticles(n) {\n    let value = `${getRandom(2560)}px ${getRandom(2560)}px #000`;\n    for (let i = 2;"
  },
  {
    "path": "public/js/copy.js",
    "chars": 1418,
    "preview": "const codeBlocks = document.querySelectorAll('pre:has(code)');\n\n//add copy btn to every code block on the dom\ncodeBlocks"
  },
  {
    "path": "public/js/scroll.js",
    "chars": 242,
    "preview": "function onScroll() {\n  const header = document.getElementById(\"header\")\n  if (window.scrollY > 0) {\n    header.classLis"
  },
  {
    "path": "public/js/theme.js",
    "chars": 1744,
    "preview": "function changeTheme() {\n  const element = document.documentElement\n  const theme = element.classList.contains(\"dark\") ?"
  },
  {
    "path": "public/robots.txt",
    "chars": 72,
    "preview": "User-agent: *\nAllow: /\n\nSitemap: http://localhost:4321/sitemap-index.xml"
  },
  {
    "path": "src/components/ArrowCard.tsx",
    "chars": 2186,
    "preview": "import { formatDate, truncateText } from \"@lib/utils\"\nimport type { CollectionEntry } from \"astro:content\"\n\ntype Props ="
  },
  {
    "path": "src/components/BaseHead.astro",
    "chars": 2256,
    "preview": "---\nimport { ViewTransitions } from \"astro:transitions\"\n\ninterface Props {\n  title: string\n  description: string\n  image"
  },
  {
    "path": "src/components/Container.astro",
    "chars": 393,
    "preview": "---\nimport { cn } from \"@lib/utils\"\n\ntype Props = {\n  size: \"sm\" | \"md\" | \"lg\" | \"xl\" | \"2xl\"\n}\n\nconst { size } = Astro."
  },
  {
    "path": "src/components/Counter.tsx",
    "chars": 525,
    "preview": "import { createSignal } from \"solid-js\"\n\nfunction CounterButton() {\n  const [count, setCount] = createSignal(0)\n\n  const"
  },
  {
    "path": "src/components/Drawer.astro",
    "chars": 2557,
    "preview": "---\nimport { SITE, LINKS } from \"@consts\"\nimport { cn } from \"@lib/utils\"\nconst { pathname } = Astro.url\nconst subpath ="
  },
  {
    "path": "src/components/Footer.astro",
    "chars": 4617,
    "preview": "---\nimport { SITE, SOCIALS } from \"@consts\"\nimport Container from \"@components/Container.astro\"\n---\n\n<footer class=\"rela"
  },
  {
    "path": "src/components/Header.astro",
    "chars": 4667,
    "preview": "---\nimport { SITE, LINKS } from \"@consts\"\nimport { cn } from \"@lib/utils\"\nconst { pathname } = Astro.url\nconst subpath ="
  },
  {
    "path": "src/components/MeteorShower.astro",
    "chars": 1251,
    "preview": "---\n/**\n * Meteors.astro\n * This component creates meteors that are appended to the galaxy on interval.\n * Meteors are r"
  },
  {
    "path": "src/components/Search.tsx",
    "chars": 1551,
    "preview": "import type { CollectionEntry } from \"astro:content\"\nimport { createEffect, createSignal } from \"solid-js\"\nimport Fuse f"
  },
  {
    "path": "src/components/SearchBar.tsx",
    "chars": 1402,
    "preview": "type Props = {\n    onSearchInput: (e: Event) => void;\n    query: () => string;\n    setQuery: (value: string) => void;\n  "
  },
  {
    "path": "src/components/SearchCollection.tsx",
    "chars": 5895,
    "preview": "import type { CollectionEntry } from \"astro:content\"\nimport { createEffect, createSignal, For, onMount } from \"solid-js\""
  },
  {
    "path": "src/components/StackCard.astro",
    "chars": 551,
    "preview": "---\ntype Props = {\n  text: string\n  icon: string\n  href: string\n}\n\nconst { text, icon, href } = Astro.props\n---\n\n<a href"
  },
  {
    "path": "src/components/TwinklingStars.astro",
    "chars": 2528,
    "preview": "---\n/**\n * TwinkleStars.astro\n * This component creates twinkling stars that are appended to the galaxy on interval.\n * "
  },
  {
    "path": "src/consts.ts",
    "chars": 1527,
    "preview": "import type { Site, Page, Links, Socials } from \"@types\"\n\n// Global\nexport const SITE: Site = {\n  TITLE: \"Astro Sphere\","
  },
  {
    "path": "src/content/blog/01-astro-sphere-file-structure/index.md",
    "chars": 2289,
    "preview": "---\ntitle: \"Astro Sphere: File Structure\"\nsummary: \"You'll find these directories and files in the project. What do they"
  },
  {
    "path": "src/content/blog/02-astro-sphere-getting-started/index.md",
    "chars": 2431,
    "preview": "---\ntitle: \"Astro Sphere: Getting Started\"\nsummary: \"You've downloaded and installed the project. Let's hit the ground r"
  },
  {
    "path": "src/content/blog/03-astro-sphere-add-new-post-or-projects/index.md",
    "chars": 3078,
    "preview": "---\ntitle: \"Astro Sphere: Adding a new post or project.\"\nsummary: \"Adding a new article (blog post or project) is pretty"
  },
  {
    "path": "src/content/blog/04-astro-sphere-writing-markdown/index.md",
    "chars": 4718,
    "preview": "---\ntitle: \"Astro Sphere: Writing Markdown\"\nsummary: \"Basic Markdown syntax that can be used when writing Markdown conte"
  },
  {
    "path": "src/content/blog/05-astro-sphere-writing-mdx/MyComponent.astro",
    "chars": 226,
    "preview": "---\ntype Props = {\n  name: string\n}\nconst { name } = Astro.props\n---\n\n<div class=\"border p-4 bg-yellow-100 text-black\">\n"
  },
  {
    "path": "src/content/blog/05-astro-sphere-writing-mdx/index.mdx",
    "chars": 1193,
    "preview": "---\ntitle: \"Astro Sphere: Writing MDX\"\nsummary: \"Lorem ipsum dolor sit amet\"\ndate: \"Mar 12 2024\"\ndraft: false\ntags:\n- Tu"
  },
  {
    "path": "src/content/blog/06-astro-sphere-social-links/index.md",
    "chars": 2663,
    "preview": "---\ntitle: \"Astro Sphere: Social media links\"\nsummary: \"A quick tutorial on how to change, add or remove social media li"
  },
  {
    "path": "src/content/config.ts",
    "chars": 981,
    "preview": "import { defineCollection, z } from \"astro:content\"\n\nconst work = defineCollection({\n  type: \"content\",\n  schema: z.obje"
  },
  {
    "path": "src/content/legal/privacy.md",
    "chars": 2775,
    "preview": "---\ntitle: \"Privacy Policy\"\ndate: \"03/07/2024\"\n---\n\nThis Privacy Policy governs the manner in which [Your Company Name] "
  },
  {
    "path": "src/content/legal/terms.md",
    "chars": 2572,
    "preview": "---\ntitle: \"Terms of Use\"\ndate: \"03/07/2024\"\n---\n\nPlease read these Terms of Use (\"Terms\", \"Terms of Use\") carefully bef"
  },
  {
    "path": "src/content/projects/project-1/index.md",
    "chars": 913,
    "preview": "---\ntitle: \"Project One\"\nsummary: \"Lorem ipsum dolor sit amet\"\ndate: \"Mar 18 2022\"\ndraft: false\ntags:\n- Astro\n- Javascri"
  },
  {
    "path": "src/content/projects/project-2/index.md",
    "chars": 1454,
    "preview": "---\ntitle: \"Project Two\"\nsummary: \"Lorem ipsum dolor sit amet\"\ndate: \"Mar 17 2022\"\ndraft: false\ntags:\n- Svelte\n- Sveltek"
  },
  {
    "path": "src/content/projects/project-3/index.md",
    "chars": 789,
    "preview": "---\ntitle: \"Project Three\"\nsummary: \"Lorem ipsum dolor sit amet\"\ndate: \"Mar 16 2022\"\ndraft: false\ntags:\n- Vue\n- Javascri"
  },
  {
    "path": "src/content/projects/project-4/index.md",
    "chars": 2107,
    "preview": "---\ntitle: \"Project Four\"\nsummary: \"Lorem ipsum dolor sit amet\"\ndate: \"Mar 15 2022\"\ndraft: false\ntags:\n- React\n- Javascr"
  },
  {
    "path": "src/content/work/apple.md",
    "chars": 449,
    "preview": "---\ncompany: \"Apple\"\nrole: \"Software Engineer\"\ndateStart: \"01/01/2020\"\ndateEnd: \"11/27/2022\"\n---\n\nVoluptatem est quaerat"
  },
  {
    "path": "src/content/work/facebook.md",
    "chars": 371,
    "preview": "---\ncompany: \"Facebook\"\nrole: \"Intern\"\ndateStart: \"07/01/2019\"\ndateEnd: \"12/31/2019\"\n---\n\nIure illo neque tempora, volup"
  },
  {
    "path": "src/content/work/google.md",
    "chars": 489,
    "preview": "---\ncompany: \"Google\"\nrole: \"Staff Software Engineer\"\ndateStart: \"11/27/2022\"\ndateEnd: \"Now\"\n---\n\nSit amet consectetur a"
  },
  {
    "path": "src/content/work/mcdonalds.md",
    "chars": 432,
    "preview": "---\ncompany: \"McDonalds\"\nrole: \"French Fryer\"\ndateStart: \"03/16/2018\"\ndateEnd: \"07/01/2019\"\n---\n\nLorem ipsum dolor, sit "
  },
  {
    "path": "src/env.d.ts",
    "chars": 84,
    "preview": "/// <reference path=\"../.astro/types.d.ts\" />\n/// <reference types=\"astro/client\" />"
  },
  {
    "path": "src/layouts/ArticleBottomLayout.astro",
    "chars": 3016,
    "preview": "---\nimport { type CollectionEntry, getCollection } from \"astro:content\"\n\ntype Props = {\n  entry: CollectionEntry<\"blog\">"
  },
  {
    "path": "src/layouts/ArticleTopLayout.astro",
    "chars": 3146,
    "preview": "---\nimport type { CollectionEntry } from \"astro:content\"\nimport { formatDate, readingTime } from \"@lib/utils\"\n\ntype Prop"
  },
  {
    "path": "src/layouts/BottomLayout.astro",
    "chars": 144,
    "preview": "---\nimport Container from \"@components/Container.astro\"\n---\n\n<div class=\"flex-1 py-5\">\n  <Container size=\"md\">\n    <slot"
  },
  {
    "path": "src/layouts/PageLayout.astro",
    "chars": 539,
    "preview": "---\nimport \"@styles/global.css\"\nimport BaseHead from \"@components/BaseHead.astro\"\nimport Header from \"@components/Header"
  },
  {
    "path": "src/layouts/TopLayout.astro",
    "chars": 143,
    "preview": "---\nimport Container from \"@components/Container.astro\"\n---\n\n<div class=\"pt-36 pb-5\">\n  <Container size=\"md\">\n    <slot/"
  },
  {
    "path": "src/lib/utils.ts",
    "chars": 927,
    "preview": "import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: Cla"
  },
  {
    "path": "src/pages/blog/[...slug].astro",
    "chars": 973,
    "preview": "---\nimport { type CollectionEntry, getCollection } from \"astro:content\"\nimport PageLayout from \"@layouts/PageLayout.astr"
  },
  {
    "path": "src/pages/blog/index.astro",
    "chars": 1267,
    "preview": "---\nimport { getCollection } from \"astro:content\";\nimport PageLayout from \"@layouts/PageLayout.astro\";\nimport TopLayout "
  },
  {
    "path": "src/pages/index.astro",
    "chars": 9050,
    "preview": "---\nimport { getCollection } from \"astro:content\"\nimport PageLayout from \"@layouts/PageLayout.astro\"\nimport ArrowCard fr"
  },
  {
    "path": "src/pages/legal/[...slug].astro",
    "chars": 1080,
    "preview": "---\nimport { type CollectionEntry, getCollection } from \"astro:content\"\nimport PageLayout from \"@layouts/PageLayout.astr"
  },
  {
    "path": "src/pages/projects/[...slug].astro",
    "chars": 1016,
    "preview": "---\nimport { type CollectionEntry, getCollection } from \"astro:content\"\nimport PageLayout from \"@layouts/PageLayout.astr"
  },
  {
    "path": "src/pages/projects/index.astro",
    "chars": 1375,
    "preview": "---\nimport { getCollection } from \"astro:content\";\nimport PageLayout from \"@layouts/PageLayout.astro\";\nimport TopLayout "
  },
  {
    "path": "src/pages/robots.txt.ts",
    "chars": 313,
    "preview": "import type { APIRoute } from \"astro\"\n\nconst robotsTxt = `\nUser-agent: *\nAllow: /\n\nSitemap: ${new URL(\"sitemap-index.xml"
  },
  {
    "path": "src/pages/rss.xml.ts",
    "chars": 773,
    "preview": "import rss from \"@astrojs/rss\"\nimport { getCollection } from \"astro:content\"\nimport { SITE } from \"@consts\"\n\ntype Contex"
  },
  {
    "path": "src/pages/search/index.astro",
    "chars": 847,
    "preview": "---\nimport { type CollectionEntry, getCollection } from \"astro:content\";\nimport PageLayout from \"@layouts/PageLayout.ast"
  },
  {
    "path": "src/pages/work/index.astro",
    "chars": 1735,
    "preview": "---\nimport { getCollection } from \"astro:content\"\nimport PageLayout from \"@layouts/PageLayout.astro\"\nimport TopLayout fr"
  },
  {
    "path": "src/styles/global.css",
    "chars": 3048,
    "preview": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n:root {\n  --copy-btn-margin: 10px;\n}\n\n@layer base {\n  @font-"
  },
  {
    "path": "src/types.ts",
    "chars": 263,
    "preview": "export type Page = {\n  TITLE: string\n  DESCRIPTION: string\n}\n\nexport interface Site extends Page {\n  AUTHOR: string\n}\n\ne"
  },
  {
    "path": "tailwind.config.mjs",
    "chars": 1399,
    "preview": "import defaultTheme from \"tailwindcss/defaultTheme\"\n\n/** @type {import('tailwindcss').Config} */\nexport default {\n  dark"
  },
  {
    "path": "tsconfig.json",
    "chars": 367,
    "preview": "{\n  \"extends\": \"astro/tsconfigs/strict\",\n  \"compilerOptions\": {\n    \"strictNullChecks\": true,\n    \"baseUrl\": \".\",\n    \"p"
  }
]

About this extraction

This page contains the full source code of the markhorn-dev/astro-sphere GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 68 files (103.8 KB), approximately 30.1k tokens, and a symbol index with 37 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!