Blog layouts and test posts

This commit is contained in:
matthieu42morin 2023-11-11 16:33:14 +01:00
parent 889825672d
commit 434c214908
11 changed files with 549 additions and 46 deletions

View File

@ -1,10 +1,9 @@
import type { BlogPost } from '$lib/types/blog'; import type { BlogPost } from '$lib/types/blog';
import type { MarkdownMetadata } from '$lib/contents/types'; import type { MarkdownMetadata } from '$content/types';
import type { MdsvexImport } from './types'; import type { MdsvexImport } from './types';
import { parseReadContent } from '../../content/utils'; import { parseReadContent } from '$content/utils';
import { error } from '@sveltejs/kit'; import { error } from '@sveltejs/kit';
export function listBlogPosts() { export function listBlogPosts() {
const posts = import.meta.glob<BlogPost>('./blog/*.md', { const posts = import.meta.glob<BlogPost>('./blog/*.md', {
eager: true, eager: true,

View File

@ -7,9 +7,27 @@ tags:
- post - post
published: true published: true
image: Feature.jpg image: Feature.jpg
--- ---
## Svelte ## Svelte
Media inside the **Svelte** folder is served from the `static` folder. Media inside the **Svelte** folder is served from the `static` folder.
```python
input_text = ''' "yahooapis.com",
"hotmail.com",
"gfx.ms",
"afx.ms",
"live.com",
'''
# and so on...
lines = input_text.split('\n')
formatted_lines = ['* ' + line.strip()[1:-2] + ' * block' for line in lines if line]
output_text = '\n'.join(formatted_lines)
print(output_text)
```

184
src/content/utils.ts Normal file
View File

@ -0,0 +1,184 @@
import type { create_ssr_component } from 'svelte/internal';
type DateStyle = Intl.DateTimeFormatOptions['dateStyle'];
/**
* Sorts an array of objects by their `date` property in descending order.
* @param a - The first object to compare.
* @param b - The second object to compare.
* @returns A number that represents the difference between the parsed dates of the two objects.
*/
export function dateSort<T extends { date?: string }>(a: T, b: T): number {
return Date.parse(b.date) - Date.parse(a.date);
}
export function renderMdsvexComponent(component: any): string {
if (typeof component['render'] != 'function') {
throw new Error(
"Unable to render something that isn't a mdsvex component",
);
}
return (component as ReturnType<typeof create_ssr_component>).render().html;
}
export function mdPathToSlug(path: string) {
return path.split('/').at(-1).slice(0, -3);
}
export function parseReadContent<T extends { date?: string }>(
data: Record<string, T>,
): T[] {
return Object.entries(data)
.map(([file, data]) => ({
slug: mdPathToSlug(file),
...data,
}))
.sort(dateSort);
}
// Old utils
// /**
// * Formats a date string using the specified date style and locale.
// * @param date - The date string to format.
// * @param dateStyle - The style to use when formatting the date. Defaults to 'medium'.
// * @param locales - The locale to use when formatting the date. Defaults to 'en'.
// * @returns The formatted date string.
// */
// // export function formatDate(date: string, dateStyle: DateStyle = 'medium', locales = 'en') {
// // const formatter = new Intl.DateTimeFormat(locales, { dateStyle });
// // return formatter.format(new Date(date));
// // }
// /**
// * Renders an mdsvex component and returns the resulting HTML string.
// * @param component - The mdsvex component to render.
// * @returns The HTML string that was generated by rendering the component.
// * @throws An error if the `render` property of the component is not a function.
// */
// export function renderMdsvexComponent(component: ReturnType<typeof create_ssr_component>): string {
// if (typeof component['render'] !== 'function') {
// throw new Error("Unable to render something that isn't a mdsvex component");
// }
// return component.render().html;
// }
// /**
// * Converts an md file path to a slug by removing the last segment of the path and the `.md` extension.
// * @param path - The path of the md file.
// * @returns The slug of the md file.
// */
// export function mdPathToSlug(path: string): string {
// return (path.split('/').at(-1) || '')?.slice(0, -3) || '';
// }
// /**
// * Parses an object of data that has string keys and values of type `T` that have an optional `date` property.
// * @param data - The object of data to parse.
// * @param dateSort - A function that sorts an array of objects by their `date` property in descending order.
// * @param mdPathToSlug - A function that converts an md file path to a slug.
// * @returns An array of objects that have a `slug` property and the properties of the original data objects.
// */
// export function parseReadContent<T extends { date?: string }>(data: Record<string, T>): T[] {
// return Object.entries(data)
// .map(
// ([file, data]) =>
// ({
// slug: mdPathToSlug(file),
// ...data
// } as { slug: string } & T & { date: string })
// )
// .sort(dateSort);
// }
import { readable } from 'svelte/store';
export const isEurope = () => {
const offset = new Date().getTimezoneOffset();
return offset <= 0 && offset >= -180;
};
export const stringToBeautifiedFragment = (str: string = '') =>
(str || '')
.toLocaleLowerCase()
.replace(/\s/g, '-')
.replace(/\?/g, '')
.replace(/,/g, '');
export const showHideOverflowY = (bool: boolean) => {
const html = document.querySelector('html');
if (bool) {
html.classList.add('overflow-y-hidden');
} else {
html.classList.remove('overflow-y-hidden');
}
};
export const formatDate = (date) => {
try {
const d = new Date(date);
return `${d.toLocaleString('default', {
month: 'long',
})} ${d.getDate()}, ${d.getFullYear()}`;
} catch (e) {
return '';
}
};
export const scrollToElement = async (
element: HTMLElement,
selector: string,
) => {
const firstElement: HTMLElement = element.querySelector(selector);
if (!firstElement) {
return;
}
firstElement.scrollIntoView({
behavior: 'smooth',
});
};
export const removeTrailingSlash = (site: string) => {
return site.replace(/\/$/, '');
};
export const useMediaQuery = (mediaQueryString: string) => {
const matches = readable<boolean>(null, (set) => {
if (typeof globalThis['window'] === 'undefined') return;
const match = window.matchMedia(mediaQueryString);
set(match.matches);
const element = (event: MediaQueryListEvent) => set(event.matches);
match.addEventListener('change', element);
return () => {
match.removeEventListener('change', element);
};
});
return matches;
};
export const scrollIntoView = (selector: string) => {
const scrollToElement = document.querySelector(selector);
if (!scrollToElement) return;
const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
scrollToElement.scrollIntoView({
block: 'nearest',
inline: 'start',
behavior: mediaQuery.matches ? 'auto' : 'smooth',
});
};
export const getVariantFromStatus = (status: string) => {
if (status === 'soon' || status === 'Early Access') {
return 'pink';
} else {
return 'orange';
}
};

View File

@ -0,0 +1,124 @@
/**
* Nord Theme Originally by Arctic Ice Studio
* https://nordtheme.com
*
* Ported for PrismJS by Zane Hitchcoxc (@zwhitchcox) and Gabriel Ramos (@gabrieluizramos)
*/
code[class*="language-"],
pre[class*="language-"] {
color: #f8f8f2;
background: none;
font-family: "Fira Code", Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
/* Code blocks */
pre[class*="language-"] {
padding: 1em;
margin: .5em 0;
overflow: auto;
border-radius: 0.3em;
}
:not(pre) > code[class*="language-"],
pre[class*="language-"] {
background: #2E3440;
}
/* Inline code */
:not(pre) > code[class*="language-"] {
padding: .1em;
border-radius: .3em;
white-space: normal;
}
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: #636f88;
}
.token.punctuation {
color: #81A1C1;
}
.namespace {
opacity: .7;
}
.token.property,
.token.tag,
.token.constant,
.token.symbol,
.token.deleted {
color: #81A1C1;
}
.token.number {
color: #B48EAD;
}
.token.boolean {
color: #81A1C1;
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.inserted {
color: #A3BE8C;
}
.token.operator,
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string,
.token.variable {
color: #81A1C1;
}
.token.atrule,
.token.attr-value,
.token.function,
.token.class-name {
color: #88C0D0;
}
.token.keyword {
color: #81A1C1;
}
.token.regex,
.token.important {
color: #EBCB8B;
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}

View File

@ -0,0 +1,11 @@
<script lang="ts">
import PostContentLayout from './PostLayout.svelte';
import type { BlogPost } from '$lib/types/blog';
export let post: BlogPost;
</script>
<PostContentLayout {...post} imagesDirectoryName="blog">
<slot />
</PostContentLayout>

View File

@ -0,0 +1,38 @@
<script lang="ts">
import type { BlogTag } from '$lib/types/blog';
export let selected: BlogTag;
let className = '';
export { className as class };
import { page } from '$app/stores';
import { goto } from '$app/navigation';
let options: BlogTag[] = ['Projects', 'Blog', 'Updates'];
const clickHandler = (value: BlogTag) => {
if (value === selected) {
goto(`/blog`, { keepFocus: true, noScroll: true });
selected = '';
return;
}
let query = new URLSearchParams($page.url.searchParams.toString());
query.set('tag', value);
goto(`?${query.toString()}`, { keepFocus: true, noScroll: true });
selected = value;
};
</script>
<section class="flex justify-center flex-col items-center {className}">
<p class="text-semibold mb-2 md:mb-4">Sort by category</p>
<ul class="flex flex-wrap justify-center gap-2">
{#each options as option}
<li>
<button
class="btn btn-md variant-filled-primary"
on:click={() => clickHandler(option)}
>
{option}
</button>
</li>
{/each}
</ul>
</section>

View File

@ -1,48 +1,52 @@
<script lang="ts"> <script lang="ts">
import { formatDate } from '$src/content/utils'; import { formatDate } from '$content/utils';
import '$lib/assets/prism-nord.css'; import '$lib/assets/prism-nord.css';
export let baseUrl: string;
export let imagesDirectoryName: string; export let imagesDirectoryName: string;
export let date: string = ''; export let date: string = '';
export let slug: string = ''; export let slug: string = '';
export let title: string; export let title: string;
export let image: string; export let image: string;
export let teaserImage: string;
export let tags: string[] = []; export let tags: string[] = [];
</script> </script>
<article> <article>
<div class="flex justify-center mt-4 mb-8"> <div class="flex justify-center mt-4 mb-8">
<div class="w-full lg:w-[50rem] leading-[177.7%]"> <div class="w-full lg:w-[50rem] leading-[177.7%]">
<img <header>
src="/images/{imagesDirectoryName}/{slug}/{teaserImage || image}" <img
alt={`${title}`} src="/images/{imagesDirectoryName}/{slug}/{image}"
class="max-h-[540px] rounded-tl-2xl rounded-tr-[1.3rem]" alt={`${title}`}
/> class=" bg-black/50 w-full aspect-[21/9] max-h-[540px] rounded-tr-[1.3rem]"
<div />
class="prose prose-img:rounded-tl-2xl prose-img:rounded-tr-[1.3rem] max-w-none text-base" </header>
> <div class="p-4 space-y-4">
<p class="{tags && tags.length > 0 ? '!mb-2' : '!mb-2'} mt-8 text-base"> <h3 class="h3" data-toc-ignore>{title}</h3>
{formatDate(date)} <div class="prose max-w-none text-base">
</p> <slot />
{#if tags && tags.length > 0} </div>
<div class="flex mb-2 items-center gap-2">
{#each tags as tag}
<a
data-sveltekit-preload-data="hover"
href="/blog?{new URLSearchParams({ tag }).toString()}"
>
<span class="chip variant-ghost-surface">{tag}</span>
</a>
{/each}
</div>
{/if}
<slot />
</div> </div>
</div> </div>
</div> </div>
<hr class="opacity-50" />
<footer class="p-4 flex justify-start items-center space-x-4">
<div class="flex-auto flex justify-between items-center">
{#if tags && tags.length > 0}
<div class="flex mb-2 items-center gap-2">
tags: {#each tags as tag}
<a
data-sveltekit-preload-data="hover"
href="/blog?{new URLSearchParams({ tag }).toString()}"
>
<span class="chip variant-ghost-surface">{tag}</span>
</a>
{/each}
</div>
{/if}
<small>On {formatDate(date)}</small>
</div>
</footer>
</article> </article>
<style lang="postcss"> <style lang="postcss">

View File

@ -3,10 +3,8 @@
import type { BlogPost } from '$lib/types/blog'; import type { BlogPost } from '$lib/types/blog';
export let post: BlogPost; export let post: BlogPost;
export let isMostRecent: boolean = false;
export let type: 'blog'; export let type: 'blog';
export let layout: 'row' | 'column' = 'column'; export let published: boolean;
export let published: boolean = true;
export let headlineOrder: 'h3' | '' = ''; export let headlineOrder: 'h3' | '' = '';
export let badge: string = ''; export let badge: string = '';
export let textWidth: string = ''; export let textWidth: string = '';
@ -19,6 +17,12 @@
$: href = generateURL(post['href'], post.slug); $: href = generateURL(post['href'], post.slug);
$: target = post && post['href'] && isAnExternalLink(post['href']) ? '_blank' : undefined; $: target = post && post['href'] && isAnExternalLink(post['href']) ? '_blank' : undefined;
const displayDate = new Date(Date.parse(post.date)).toLocaleDateString(undefined, {
year: 'numeric',
month: 'short',
day: 'numeric'
});
</script> </script>
<a <a
@ -67,11 +71,7 @@
<small> <small>
{#if post.date} {#if post.date}
<span class="date text-p-small ml-macro"> <span class="date text-p-small ml-macro">
{new Date(Date.parse(post.date)).toLocaleDateString(undefined, { {displayDate}
year: 'numeric',
month: 'short',
day: 'numeric'
})}
</span> </span>
{/if} {/if}
</small> </small>

View File

@ -1,16 +1,18 @@
import type { MarkdownMetadata } from '$lib/contents/types'; import type { MarkdownMetadata } from '../../../src/content/types';
export type BlogTag = 'Projects' | 'Blog' | 'Updates' | ''; export type BlogTag = 'Projects' | 'Blog' | 'Updates' | '';
export interface BlogPost extends MarkdownMetadata { export interface BlogPost extends MarkdownMetadata {
title: string; author?: string;
subtitle?: string;
tags?: BlogTag[];
modified?: string;
date?: string; date?: string;
excerpt: string; excerpt: string;
image: string; image: string;
slug?: string; slug?: string;
href?: string; href?: string;
published: boolean; tags?: BlogTag[];
subtitle?: string;
teaserImage: string;
title: string;
isNotAnActualPost?: boolean;
type?: string;
} }

123
src/lib/utils/helpers.ts Normal file
View File

@ -0,0 +1,123 @@
import { readable } from 'svelte/store';
/**
* Determines if the current timezone is between 0 and +3 hours UTC.
* @returns {boolean} True if the timezone is between 0 and +3 hours UTC, false otherwise.
*/
export const isEurope = () => {
const offset = new Date().getTimezoneOffset();
return offset <= 0 && offset >= -180; // Returns true if the timezone is between 0 and +3 hours UTC, false otherwise
};
/**
* Takes a string and returns a beautified version of it.
* @param str The input string to be beautified.
* @returns The beautified string.
*/
export const stringToBeautifiedFragment = (str = '') =>
(str || '').toLocaleLowerCase().replace(/\s/g, '-').replace(/\?/g, '').replace(/,/g, '');
/**
* Toggles the 'overflow-y-hidden' class on the 'html' element of the document.
* @param bool A boolean value indicating whether to show or hide the overflow-y scrollbar.
*/
export const showHideOverflowY = (bool: boolean) => {
const html = document.querySelector('html');
if (html) {
if (bool) {
html.classList.add('overflow-y-hidden');
} else {
html.classList.remove('overflow-y-hidden');
}
}
};
/**
* Formats a date string into a human-readable format.
* @param date - The date string to format.
* @returns A string representing the formatted date, or an empty string if the input is invalid.
*/
export const formatDate = (date: string) => {
try {
const d = new Date(date);
return `${d.toLocaleString('default', {
month: 'long'
})} ${d.getDate()}, ${d.getFullYear()}`;
} catch (e) {
return '';
}
};
/**
* Scrolls to the first element that matches the given selector within the provided element.
* @param element The element to search within.
* @param selector The selector to match against.
*/
export const scrollToElement = async (element: HTMLElement, selector: string) => {
const firstElement: HTMLElement | null = element.querySelector(selector);
if (!firstElement) {
return;
}
firstElement.scrollIntoView({
behavior: 'smooth'
});
};
/**
* Checks if a given URL is an external link.
* @param href - The URL to check.
* @returns True if the URL is an external link, false otherwise.
*/
export const isAnExternalLink = (href: string) => href.startsWith('http');
/**
* Checks if the user agent is running on a Mac or iPad.
* @returns {boolean} Returns true if the user agent is running on a Mac or iPad, false otherwise.
*/
export const isMac = () =>
navigator.userAgent.includes('Macintosh') || navigator.userAgent.includes('iPad');
/**
* Removes the trailing slash from a given string.
* @param site - The string to remove the trailing slash from.
* @returns The string without the trailing slash.
*/
export const removeTrailingSlash = (site: string) => {
return site.replace(/\/$/, '');
};
/**
* Returns a readable store that tracks whether the media query string matches the current viewport.
* @param mediaQueryString - The media query string to match against the viewport.
* @returns A readable store that tracks whether the media query string matches the current viewport.
*/
export const useMediaQuery = (mediaQueryString: string) => {
const matches = readable<boolean | undefined>(undefined, (set) => {
if (typeof globalThis['window'] === 'undefined') return;
const match = window.matchMedia(mediaQueryString);
set(match.matches);
const element = (event: MediaQueryListEvent) => set(event.matches);
match.addEventListener('change', element);
return () => {
match.removeEventListener('change', element);
};
});
return matches;
};
/**
* Scrolls the page to the nearest element matching the given selector.
* @param selector - The CSS selector of the element to scroll to.
*/
export const scrollIntoView = (selector: string) => {
const scrollToElement = document.querySelector(selector);
if (!scrollToElement) return;
const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
scrollToElement.scrollIntoView({
block: 'nearest',
inline: 'start',
behavior: mediaQuery.matches ? 'auto' : 'smooth'
});
};

View File

@ -43,7 +43,7 @@
</section> --> </section> -->
<script lang="ts"> <script lang="ts">
import PostPreview from '$lib/components/blog/BlogLayout.svelte'; import PostPreview from '$lib/components/blog/PostPreview.svelte';
import type { PageData } from './$types'; import type { PageData } from './$types';
import type { BlogTag } from '$lib/types/blog'; import type { BlogTag } from '$lib/types/blog';
import { page } from '$app/stores'; import { page } from '$app/stores';