Initial commit
This commit is contained in:
65
src/components/BaseHead.astro
Normal file
65
src/components/BaseHead.astro
Normal file
@@ -0,0 +1,65 @@
|
||||
---
|
||||
import '@fontsource/fira-sans'
|
||||
import { SITE } from '$/config'
|
||||
import '../styles/global.css'
|
||||
|
||||
export type Props = {
|
||||
title: string
|
||||
description: string
|
||||
permalink: string
|
||||
image: string
|
||||
}
|
||||
|
||||
const { title = SITE.title , description, permalink, image } = Astro.props as Props
|
||||
---
|
||||
<!-- Use Google Fonts, if you don't wanna prefer a self-hosted version -->
|
||||
<!-- <link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@300;400;500;600;700&display=swap" rel="stylesheet"> -->
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<title>{title}</title>
|
||||
<meta name="title" content={title}/>
|
||||
{description &&
|
||||
<meta name="description" content={description}/>
|
||||
}
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||||
<link rel="shortcut icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||
<link rel="manifest" href="/site.webmanifest">
|
||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
|
||||
|
||||
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="msapplication-TileColor" content="#da532c" />
|
||||
<meta name="msapplication-config" content="/browserconfig.xml"/>
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
|
||||
<!-- Open Graph Tags (Facebook) -->
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:title" content={title} />
|
||||
{permalink &&
|
||||
<meta property="og:url" content={permalink} />
|
||||
}
|
||||
{description &&
|
||||
<meta property="og:description" content={description} />
|
||||
}
|
||||
{image &&
|
||||
<meta property="og:image" content={image} />
|
||||
}
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta property="twitter:card" content="summary_large_image" />
|
||||
<meta property="twitter:title" content={title} />
|
||||
{permalink &&
|
||||
<meta property="twitter:url" content={permalink} />
|
||||
}
|
||||
{description &&
|
||||
<meta property="twitter:description" content={description} />
|
||||
}
|
||||
{image &&
|
||||
<meta property="twitter:image" content={image} />
|
||||
}
|
||||
|
||||
<script src="https://identity.netlify.com/v1/netlify-identity-widget.js"></script>
|
||||
7
src/components/BaseLayout.astro
Normal file
7
src/components/BaseLayout.astro
Normal file
@@ -0,0 +1,7 @@
|
||||
<body class="font-sans antialiased min-h-screen bg-gray-100 dark:bg-gray-800">
|
||||
<div class="transition-colors">
|
||||
<main class="mx-auto max-w-4xl px-4 md:px-0">
|
||||
<slot />
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
17
src/components/Footer.astro
Normal file
17
src/components/Footer.astro
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
import { SITE } from '$/config'
|
||||
import ModeLabel from './ModeLabel.svelte'
|
||||
import NetlifyIdentity from './NetlifyIdentity.svelte'
|
||||
---
|
||||
<footer class="footer">
|
||||
<nav class="nav">
|
||||
<div>2021 © Copyright notice | <a href={ SITE.githubUrl } title={`${ SITE.name }'s Github URL'`}>{ SITE.name }</a>
|
||||
<ModeLabel client:load/> theme on <a href="https://astro.build/">Astro</a></div>
|
||||
<NetlifyIdentity client:load/>
|
||||
</nav>
|
||||
</footer>
|
||||
<style>
|
||||
.footer {
|
||||
@apply py-6 border-t
|
||||
}
|
||||
</style>
|
||||
69
src/components/Header.astro
Normal file
69
src/components/Header.astro
Normal file
@@ -0,0 +1,69 @@
|
||||
---
|
||||
import { SITE } from '$/config'
|
||||
import SvgIcon from './SvgIcon.astro'
|
||||
import ModeSwitcherBtn from './ModeSwitcherBtn.svelte'
|
||||
import SearchBtn from './SearchBtn.svelte'
|
||||
|
||||
---
|
||||
|
||||
<header class="header">
|
||||
<div class="header__logo">
|
||||
<a href="/" class="avatar">
|
||||
<img class="header__logo-img" src="/assets/logo.svg" alt="Astro logo" />
|
||||
</a>
|
||||
</div>
|
||||
<div class="header__meta flex-1">
|
||||
<h3 class="header__title dark:text-theme-dark-secondary">
|
||||
<a href="">{ SITE.name }</a>
|
||||
</h3>
|
||||
<div class="header__meta-more flex">
|
||||
<p class="header__desc">
|
||||
{ SITE.description }
|
||||
</p>
|
||||
<nav class="header__nav flex">
|
||||
<ul class="header__ref-list">
|
||||
<li>
|
||||
<SearchBtn client:visible />
|
||||
</li>
|
||||
<li>
|
||||
<a href={ SITE.githubUrl } title={`${ SITE.name }'s Github URL'`}>
|
||||
<SvgIcon>
|
||||
<path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"></path>
|
||||
</SvgIcon>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/rss.xml" title="RSS">
|
||||
<SvgIcon>
|
||||
<path d="M4 11a9 9 0 0 1 9 9"></path>
|
||||
<path d="M4 4a16 16 0 0 1 16 16"></path>
|
||||
<circle cx="5" cy="19" r="1"></circle>
|
||||
</SvgIcon>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<ModeSwitcherBtn client:visible />
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<style>
|
||||
.header {
|
||||
@apply flex gap-4 border-b py-3 /* border-gray-200 dark:border-gray-700 - check styles/global.css */
|
||||
}
|
||||
.header__logo-img {
|
||||
@apply w-16 h-16 rounded-full overflow-hidden
|
||||
}
|
||||
.header__title {
|
||||
@apply text-4xl font-extrabold md:text-5xl text-theme-secondary dark:text-theme-dark-secondary
|
||||
}
|
||||
.header__desc {
|
||||
@apply text-xl flex-1 dark:text-gray-200
|
||||
}
|
||||
.header__ref-list {
|
||||
@apply flex gap-3 text-gray-400
|
||||
}
|
||||
</style>
|
||||
10
src/components/Intro.astro
Normal file
10
src/components/Intro.astro
Normal file
@@ -0,0 +1,10 @@
|
||||
<img src="/assets/yay.svg" alt="Yay!" />
|
||||
|
||||
<style>
|
||||
img {
|
||||
@apply mx-auto w-2/3 mt-6
|
||||
}
|
||||
h1 {
|
||||
@apply w-full justify-center text-center text-3xl font-bold text-purple-600 py-10
|
||||
}
|
||||
</style>
|
||||
28
src/components/MainLayout.astro
Normal file
28
src/components/MainLayout.astro
Normal file
@@ -0,0 +1,28 @@
|
||||
---
|
||||
import BaseLayout from './BaseLayout.astro';
|
||||
import Header from './Header.astro';
|
||||
import Footer from './Footer.astro';
|
||||
import Nav from './Nav.astro';
|
||||
import Portal from './Portal.astro';
|
||||
import SearchModal from './SearchModal.svelte'
|
||||
|
||||
---
|
||||
<BaseLayout>
|
||||
<br class="my-4"/>
|
||||
<Header/>
|
||||
<Nav/>
|
||||
<div class="content">
|
||||
<slot />
|
||||
</div>
|
||||
<br class="my-4"/>
|
||||
<Footer/>
|
||||
<Portal>
|
||||
<SearchModal client:load/>
|
||||
</Portal>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
.content {
|
||||
min-height: 580px
|
||||
}
|
||||
</style>
|
||||
48
src/components/MediaPreview.astro
Normal file
48
src/components/MediaPreview.astro
Normal file
@@ -0,0 +1,48 @@
|
||||
---
|
||||
import { getMonthName } from '$/utils'
|
||||
const { post } = Astro.props
|
||||
---
|
||||
<div class="post-preview">
|
||||
<div class="sm:w-20 md:w-32">
|
||||
<div class="post-preview__date">
|
||||
<span class="post-preview__date__day">{ new Date(post.date).getDate() }</span>
|
||||
<span class="post-preview__date__month-n-year">{ `${getMonthName(post.date)} ${new Date(post.date).getFullYear()}` }</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="flex flex-col mb-2">
|
||||
<h4 class="post-preview__title">
|
||||
<a href={post.url} title={post.title} target="_blank">{post.title}</a>
|
||||
</h4>
|
||||
<div>
|
||||
<strong>{post.host}</strong>
|
||||
{
|
||||
post.participants.length > 0 && ` <em>with</em> ${post.participants.join(', ')}`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<p class="post-preview__desc">
|
||||
{post.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
.post-preview {
|
||||
@apply flex gap-6
|
||||
}
|
||||
.post-preview__date {
|
||||
@apply flex flex-col w-full text-center
|
||||
}
|
||||
.post-preview__date__day {
|
||||
@apply text-6xl font-semibold text-gray-500 dark:text-gray-300
|
||||
}
|
||||
.post-preview__date__month-n-year {
|
||||
@apply text-gray-400
|
||||
}
|
||||
.post-preview__title {
|
||||
@apply text-2xl font-semibold text-theme-primary dark:text-theme-dark-primary hover:underline
|
||||
}
|
||||
.post-preview__desc {
|
||||
@apply text-lg leading-6 dark:text-white line-clamp-2
|
||||
}
|
||||
</style>
|
||||
14
src/components/MediaPreviewList.astro
Normal file
14
src/components/MediaPreviewList.astro
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
import MediaPreview from './MediaPreview.astro'
|
||||
const { posts } = Astro.props
|
||||
---
|
||||
<section class="media-preview__list">
|
||||
{posts.map((post) => (
|
||||
<MediaPreview post={post}/>
|
||||
))}
|
||||
</section>
|
||||
<style>
|
||||
.media-preview__list {
|
||||
@apply flex flex-col gap-12
|
||||
}
|
||||
</style>
|
||||
7
src/components/ModeLabel.svelte
Normal file
7
src/components/ModeLabel.svelte
Normal file
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import ModeSensitive from './ModeSensitive.svelte'
|
||||
</script>
|
||||
<ModeSensitive>
|
||||
<span slot="dark">(dark)</span>
|
||||
<span slot="light">(light)</span>
|
||||
</ModeSensitive>
|
||||
8
src/components/ModeSensitive.svelte
Normal file
8
src/components/ModeSensitive.svelte
Normal file
@@ -0,0 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { theme } from '../store/theme'
|
||||
</script>
|
||||
{#if $theme === 'dark'}
|
||||
<slot name="dark"/>
|
||||
{:else}
|
||||
<slot name="light"/>
|
||||
{/if}
|
||||
35
src/components/ModeSwitcher.svelte
Normal file
35
src/components/ModeSwitcher.svelte
Normal file
@@ -0,0 +1,35 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte'
|
||||
import { theme } from '../store/theme'
|
||||
|
||||
type ThemeType = 'dark' | 'light'
|
||||
|
||||
const THEME_DARK: ThemeType = 'dark'
|
||||
const THEME_LIGHT: ThemeType = 'light'
|
||||
let currTheme: ThemeType = THEME_DARK
|
||||
|
||||
|
||||
function toggleTheme() {
|
||||
window.document.documentElement.classList.toggle(THEME_DARK)
|
||||
currTheme = localStorage.getItem('theme') === THEME_DARK ? THEME_LIGHT : THEME_DARK
|
||||
// Update Storage
|
||||
localStorage.setItem('theme', currTheme)
|
||||
// Update Store
|
||||
theme.set(currTheme)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (localStorage.getItem('theme') === THEME_DARK || (!('theme' in localStorage) && window.matchMedia(`(prefers-color-scheme: ${THEME_DARK})`).matches)) {
|
||||
window.document.documentElement.classList.add(THEME_DARK)
|
||||
currTheme = THEME_DARK
|
||||
} else {
|
||||
window.document.documentElement.classList.remove(THEME_DARK)
|
||||
currTheme = THEME_LIGHT
|
||||
}
|
||||
// Update Store
|
||||
theme.set(currTheme)
|
||||
})
|
||||
</script>
|
||||
<button on:click={toggleTheme}>
|
||||
<slot theme={currTheme}/>
|
||||
</button>
|
||||
21
src/components/ModeSwitcherBtn.svelte
Normal file
21
src/components/ModeSwitcherBtn.svelte
Normal file
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import ModeSwitcher from './ModeSwitcher.svelte'
|
||||
import SvgIcon from './SvgIcon.svelte'
|
||||
</script>
|
||||
<ModeSwitcher let:theme>
|
||||
<SvgIcon>
|
||||
{#if theme === 'dark'}
|
||||
<circle cx="12" cy="12" r="5"></circle>
|
||||
<line x1="12" y1="1" x2="12" y2="3"></line>
|
||||
<line x1="12" y1="21" x2="12" y2="23"></line>
|
||||
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
|
||||
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
|
||||
<line x1="1" y1="12" x2="3" y2="12"></line>
|
||||
<line x1="21" y1="12" x2="23" y2="12"></line>
|
||||
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
|
||||
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
|
||||
{:else}
|
||||
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"></path>
|
||||
{/if}
|
||||
</SvgIcon>
|
||||
</ModeSwitcher>
|
||||
18
src/components/Nav.astro
Normal file
18
src/components/Nav.astro
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
import { toTitleCase } from '$/utils'
|
||||
import { NAV_ITEMS } from '$/config'
|
||||
---
|
||||
<nav class="nav py-3">
|
||||
<ul class="nav-list dark:text-theme-dark-secondary">
|
||||
{
|
||||
Object.keys(NAV_ITEMS).map(navItemKey => <li>
|
||||
<a class="hover:underline" href={NAV_ITEMS[navItemKey].path} title={NAV_ITEMS[navItemKey].title}>{toTitleCase(NAV_ITEMS[navItemKey].title)}</a>
|
||||
</li>)
|
||||
}
|
||||
</ul>
|
||||
</nav>
|
||||
<style>
|
||||
.nav-list {
|
||||
@apply inline-flex list-none gap-8 text-xl font-semibold text-theme-secondary dark:text-theme-dark-secondary py-2 flex-wrap
|
||||
}
|
||||
</style>
|
||||
16
src/components/NetlifyIdentity.svelte
Normal file
16
src/components/NetlifyIdentity.svelte
Normal file
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { onMount} from 'svelte'
|
||||
|
||||
onMount(() => {
|
||||
if (window.netlifyIdentity) {
|
||||
window.netlifyIdentity.on('init', (user) => {
|
||||
if (!user) {
|
||||
window.netlifyIdentity.on('login', () => {
|
||||
document.location.href = '/admin/';
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
15
src/components/Paginator.astro
Normal file
15
src/components/Paginator.astro
Normal file
@@ -0,0 +1,15 @@
|
||||
---
|
||||
const { page } = Astro.props
|
||||
---
|
||||
<div class="page__actions">
|
||||
{page.url.prev && <a class="action__go-to-x" href={page.url.prev} title="Go to Previous">← Prev</a>}
|
||||
{page.url.next && <a class="action__go-to-x" href={page.url.next} title="Go to Next">Next →</a>}
|
||||
</div>
|
||||
<style>
|
||||
.page__actions {
|
||||
@apply flex justify-center md:justify-end py-6 gap-2
|
||||
}
|
||||
.action__go-to-x {
|
||||
@apply text-base uppercase text-gray-500 dark:text-gray-400 hover:underline
|
||||
}
|
||||
</style>
|
||||
3
src/components/Portal.astro
Normal file
3
src/components/Portal.astro
Normal file
@@ -0,0 +1,3 @@
|
||||
<div class="portal-root">
|
||||
<slot/>
|
||||
</div>
|
||||
40
src/components/PostDraftPreview.astro
Normal file
40
src/components/PostDraftPreview.astro
Normal file
@@ -0,0 +1,40 @@
|
||||
---
|
||||
import { getMonthName, getSlugFromPathname } from '$/utils'
|
||||
const { frontmatter: post, file } = Astro.props.post
|
||||
---
|
||||
<div class="post-draft-preview">
|
||||
<div class="sm:w-20 md:w-32">
|
||||
<div class="post-draft-preview__date">
|
||||
<span class="post-draft-preview__date__day">{ new Date(post.date).getDate() }</span>
|
||||
<span class="post-draft-preview__date__month-n-year">{ `${getMonthName(post.date)} ${new Date(post.date).getFullYear()}` }</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h4 class="post-draft-preview__title">
|
||||
<a href={`/drafts/${getSlugFromPathname(file)}`} title={post.title}>{post.title}</a>
|
||||
</h4>
|
||||
<p class="post-draft-preview__desc">
|
||||
{post.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
.post-draft-preview {
|
||||
@apply flex gap-6
|
||||
}
|
||||
.post-draft-preview__date {
|
||||
@apply flex flex-col w-full text-center
|
||||
}
|
||||
.post-draft-preview__date__day {
|
||||
@apply text-6xl font-semibold text-gray-500 dark:text-gray-300
|
||||
}
|
||||
.post-draft-preview__date__month-n-year {
|
||||
@apply text-gray-400
|
||||
}
|
||||
.post-draft-preview__title {
|
||||
@apply text-2xl font-semibold text-theme-primary dark:text-theme-dark-primary hover:underline mb-2
|
||||
}
|
||||
.post-draft-preview__desc {
|
||||
@apply text-lg leading-6 dark:text-white line-clamp-2
|
||||
}
|
||||
</style>
|
||||
14
src/components/PostDraftPreviewList.astro
Normal file
14
src/components/PostDraftPreviewList.astro
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
import PostDraftPreview from './PostDraftPreview.astro'
|
||||
const { posts } = Astro.props
|
||||
---
|
||||
<section class="post-draft-preview__list">
|
||||
{posts.map((post) => (
|
||||
<PostDraftPreview post={post}/>
|
||||
))}
|
||||
</section>
|
||||
<style>
|
||||
.post-draft-preview__list {
|
||||
@apply flex flex-col gap-12
|
||||
}
|
||||
</style>
|
||||
40
src/components/PostPreview.astro
Normal file
40
src/components/PostPreview.astro
Normal file
@@ -0,0 +1,40 @@
|
||||
---
|
||||
import { getMonthName } from '$/utils'
|
||||
const { frontmatter: post, url } = Astro.props.post
|
||||
---
|
||||
<div class="post-preview">
|
||||
<div class="sm:w-20 md:w-32">
|
||||
<div class="post-preview__date">
|
||||
<span class="post-preview__date__day">{ new Date(post.date).getDate() }</span>
|
||||
<span class="post-preview__date__month-n-year">{ `${getMonthName(post.date)} ${new Date(post.date).getFullYear()}` }</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h4 class="post-preview__title">
|
||||
<a href={url} title={post.title}>{post.title}</a>
|
||||
</h4>
|
||||
<p class="post-preview__desc">
|
||||
{post.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
.post-preview {
|
||||
@apply flex gap-6;
|
||||
}
|
||||
.post-preview__date {
|
||||
@apply flex flex-col w-full text-center;
|
||||
}
|
||||
.post-preview__date__day {
|
||||
@apply text-6xl font-semibold text-gray-500 dark:text-gray-300;
|
||||
}
|
||||
.post-preview__date__month-n-year {
|
||||
@apply text-gray-400;
|
||||
}
|
||||
.post-preview__title {
|
||||
@apply text-2xl font-semibold text-theme-primary dark:text-theme-dark-primary hover:underline mb-2;
|
||||
}
|
||||
.post-preview__desc {
|
||||
@apply text-lg leading-6 line-clamp-2 dark:text-white;
|
||||
}
|
||||
</style>
|
||||
15
src/components/PostPreviewList.astro
Normal file
15
src/components/PostPreviewList.astro
Normal file
@@ -0,0 +1,15 @@
|
||||
---
|
||||
import PostPreview from './PostPreview.astro'
|
||||
const { posts } = Astro.props
|
||||
const sortedPosts = posts.sort((a, b) => new Date(b.date) - new Date(a.date));
|
||||
---
|
||||
<section class="post-preview__list">
|
||||
{sortedPosts.map((post) => (
|
||||
<PostPreview post={post}/>
|
||||
))}
|
||||
</section>
|
||||
<style>
|
||||
.post-preview__list {
|
||||
@apply flex flex-col gap-12
|
||||
}
|
||||
</style>
|
||||
46
src/components/PostSearchPreview.svelte
Normal file
46
src/components/PostSearchPreview.svelte
Normal file
@@ -0,0 +1,46 @@
|
||||
<script lang="ts">
|
||||
type Props = {
|
||||
slug: string
|
||||
title: string
|
||||
description: string
|
||||
category: string,
|
||||
tags: Array<string>
|
||||
}
|
||||
export let post: Props
|
||||
export let isLast: boolean = false
|
||||
</script>
|
||||
<div class="post-preview hover:bg-theme-primary">
|
||||
<div class="flex-1">
|
||||
<h4 class="post-preview__title">
|
||||
<a href={`/${post.category}/${post.slug}`} title={post.title}>{post.title} →</a>
|
||||
</h4>
|
||||
<p class="post-preview__desc">
|
||||
{post.description}
|
||||
</p>
|
||||
<ul class="tag-list">
|
||||
{#each post.tags as tag}
|
||||
<a class="tag" href={`/tags/${tag}`} title={tag}>{tag}</a>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{#if !isLast}
|
||||
<hr class="my-4 text-theme-dark-secondary"/>
|
||||
{/if}
|
||||
<style lang="postcss">
|
||||
.post-preview {
|
||||
@apply flex gap-6 text-left;
|
||||
}
|
||||
.post-preview__title {
|
||||
@apply text-lg leading-tight font-semibold text-white mb-2;
|
||||
}
|
||||
.post-preview__desc {
|
||||
@apply text-base text-theme-dark-primary leading-5 line-clamp-2;
|
||||
}
|
||||
.tag-list {
|
||||
@apply list-none py-2 flex flex-wrap gap-2;
|
||||
}
|
||||
.tag {
|
||||
@apply inline-block text-xs px-4 py-1 rounded-full text-theme-primary bg-theme-dark-primary;
|
||||
}
|
||||
</style>
|
||||
11
src/components/Prose.astro
Normal file
11
src/components/Prose.astro
Normal file
@@ -0,0 +1,11 @@
|
||||
<article class="prose dark:prose-dark">
|
||||
<slot />
|
||||
</article>
|
||||
|
||||
<style>
|
||||
.prose {
|
||||
@apply max-w-none
|
||||
/* Size Modifiers: https://github.com/tailwindlabs/tailwindcss-typography#size-modifiers */
|
||||
/* Color Themes: https://github.com/tailwindlabs/tailwindcss-typography#color-modifiers */
|
||||
}
|
||||
</style>
|
||||
96
src/components/Search.svelte
Normal file
96
src/components/Search.svelte
Normal file
@@ -0,0 +1,96 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte'
|
||||
import SearchIcon from './SearchIcon.svelte'
|
||||
import PostSearchPreview from './PostSearchPreview.svelte'
|
||||
|
||||
let searchInput
|
||||
let searchableDocs
|
||||
let searchIndex
|
||||
|
||||
let searchQuery = ''
|
||||
let searchResults = []
|
||||
|
||||
onMount(async() => {
|
||||
const lunr = (await import('lunr')).default
|
||||
const resp = await fetch('/search-index.json')
|
||||
searchableDocs = await resp.json()
|
||||
// Initialize indexing
|
||||
searchIndex = lunr(function(){
|
||||
// the match key...
|
||||
this.ref('slug')
|
||||
|
||||
// indexable properties
|
||||
this.field('title')
|
||||
this.field('description')
|
||||
this.field('tags')
|
||||
|
||||
// Omit, if you don't want to search on `body`
|
||||
this.field('body')
|
||||
|
||||
// Index every document
|
||||
searchableDocs.forEach(doc => {
|
||||
this.add(doc)
|
||||
}, this)
|
||||
})
|
||||
searchInput.focus()
|
||||
})
|
||||
|
||||
$: {
|
||||
if(searchQuery && searchQuery.length >= 3) {
|
||||
const matches = searchIndex.search(searchQuery)
|
||||
searchResults = []
|
||||
matches.map(match => {
|
||||
searchableDocs.filter(doc => {
|
||||
if(match.ref === doc.slug) {
|
||||
searchResults.push(doc)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<div class="search">
|
||||
<div class="search__ctrl">
|
||||
<label for="search"><SearchIcon found={searchResults.length > 0} /></label>
|
||||
<input type="text" name="search" bind:this={searchInput} placeholder="What are you looking for?" bind:value={searchQuery} />
|
||||
</div>
|
||||
<div class="search__results">
|
||||
{#if searchResults.length}
|
||||
{#each searchResults as post, i }
|
||||
<PostSearchPreview post={post} isLast={ i === searchResults.length - 1 } />
|
||||
{/each}
|
||||
{:else}
|
||||
<div class="search__results--none">
|
||||
{#if searchQuery.length}
|
||||
No matching items found!
|
||||
{:else}
|
||||
Search something and let me find it for you! :-)
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="note"><small>click anywhere outside to close</small></div>
|
||||
</div>
|
||||
<style>
|
||||
.search {
|
||||
@apply w-full relative bg-theme-primary p-8 rounded-md shadow-lg;
|
||||
}
|
||||
input {
|
||||
@apply w-full px-4 py-2 pl-10 text-xl font-semibold text-gray-600 border-0 shadow-inner rounded-md bg-gray-100 placeholder-theme-dark-secondary;
|
||||
}
|
||||
.search__ctrl {
|
||||
@apply pb-4 relative;
|
||||
}
|
||||
.search__ctrl label {
|
||||
@apply text-theme-primary absolute top-2 left-2;
|
||||
}
|
||||
.search__results {
|
||||
@apply w-96 h-64 py-4 overflow-y-auto;
|
||||
}
|
||||
.search__results--none {
|
||||
@apply text-center text-theme-dark-primary;
|
||||
}
|
||||
.note {
|
||||
@apply w-full text-center text-white;
|
||||
}
|
||||
</style>
|
||||
11
src/components/SearchBtn.svelte
Normal file
11
src/components/SearchBtn.svelte
Normal file
@@ -0,0 +1,11 @@
|
||||
<script lang="ts">
|
||||
import SearchIcon from './SearchIcon.svelte'
|
||||
import { isSearchVisible } from '../store/search'
|
||||
|
||||
function showSearchDialog() {
|
||||
isSearchVisible.set(true)
|
||||
}
|
||||
</script>
|
||||
<button on:click={showSearchDialog}>
|
||||
<SearchIcon />
|
||||
</button>
|
||||
18
src/components/SearchIcon.svelte
Normal file
18
src/components/SearchIcon.svelte
Normal file
@@ -0,0 +1,18 @@
|
||||
<script lang="ts">
|
||||
import SvgIcon from './SvgIcon.svelte'
|
||||
export let found:boolean = false
|
||||
</script>
|
||||
<SvgIcon>
|
||||
{#if found}
|
||||
<path
|
||||
d="M7.66542 10.2366L9.19751 8.951L10.4831 10.4831L13.5473 7.91194L14.8328 9.44402L10.2366 13.3007L7.66542 10.2366Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
{/if}
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M16.2071 4.89344C19.0923 7.77862 19.3131 12.3193 16.8693 15.4578C16.8846 15.4713 16.8996 15.4854 16.9143 15.5L21.1569 19.7427C21.5474 20.1332 21.5474 20.7664 21.1569 21.1569C20.7664 21.5474 20.1332 21.5474 19.7427 21.1569L15.5 16.9143C15.4854 16.8996 15.4713 16.8846 15.4578 16.8693C12.3193 19.3131 7.77862 19.0923 4.89344 16.2071C1.76924 13.083 1.76924 8.01763 4.89344 4.89344C8.01763 1.76924 13.083 1.76924 16.2071 4.89344ZM14.7929 14.7929C17.1361 12.4498 17.1361 8.6508 14.7929 6.30765C12.4498 3.96451 8.6508 3.96451 6.30765 6.30765C3.96451 8.6508 3.96451 12.4498 6.30765 14.7929C8.6508 17.1361 12.4498 17.1361 14.7929 14.7929Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</SvgIcon>
|
||||
32
src/components/SearchModal.svelte
Normal file
32
src/components/SearchModal.svelte
Normal file
@@ -0,0 +1,32 @@
|
||||
<script lang="ts">
|
||||
import { fade, fly } from 'svelte/transition'
|
||||
import { isSearchVisible } from '../store/search'
|
||||
import Search from './Search.svelte'
|
||||
|
||||
const dismissModal = () => isSearchVisible.set(false)
|
||||
const handleEsc = (event) => {
|
||||
if (event.key === 'Escape') {
|
||||
dismissModal()
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
{#if $isSearchVisible}
|
||||
<div class="modal__backdrop" on:click={dismissModal} on:keydown={handleEsc} transition:fade></div>
|
||||
<div class="modal">
|
||||
<div class="modal__cnt" transition:fly="{{ y: 200, duration: 300 }}">
|
||||
<Search />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<style>
|
||||
.modal {
|
||||
@apply absolute top-0 left-0 w-full h-full grid justify-center content-center pointer-events-none;
|
||||
}
|
||||
.modal__backdrop {
|
||||
@apply absolute top-0 left-0 w-full h-screen opacity-50 bg-gradient-to-tr from-fuchsia-600 to-fuchsia-900 z-0;
|
||||
}
|
||||
.modal__cnt {
|
||||
@apply w-full z-10 pointer-events-auto;
|
||||
}
|
||||
</style>
|
||||
3
src/components/SvgIcon.astro
Normal file
3
src/components/SvgIcon.astro
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<slot />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 202 B |
3
src/components/SvgIcon.svelte
Normal file
3
src/components/SvgIcon.svelte
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<slot/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 201 B |
Reference in New Issue
Block a user