feat: toggle theme for code highlighting in article content

This commit is contained in:
Sergey Yarkov 2022-07-31 05:26:09 +03:00
parent 2ea0c75258
commit c3ac7c6c14
10 changed files with 115 additions and 59 deletions

View File

@ -5,14 +5,13 @@
* and body markup to prevent FOUC (Flash of unstyled content). * and body markup to prevent FOUC (Flash of unstyled content).
*/ */
const getTheme = () => { window.theme = (() => {
const local = localStorage.getItem('theme'); const local = localStorage.getItem('theme');
if (local && (local === 'dark' || local === 'light')) return local; if (local && (local === 'dark' || local === 'light')) return local;
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches; const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
return isDark ? 'dark' : 'light'; return isDark ? 'dark' : 'light';
}; })();
window.theme = getTheme();
document.documentElement.setAttribute('data-theme', window.theme); document.documentElement.setAttribute('data-theme', window.theme);
/* Sync with OS theme */ /* Sync with OS theme */

View File

@ -0,0 +1,48 @@
/**
* This script will load the properly code highlithing theme based on user theme
* It is important to insert it after detecting a user theme
* and assigning the `window.theme` property
*/
if (window.theme) {
/* the {theme} in the path will be dynamically changed */
const PATH = `assets/css/prism-one-{theme}.min.css`;
const $head = document.getElementsByTagName('head')[0];
const getPath = (theme) => PATH.replace(`{theme}`, theme);
const createLink = (theme, media) => {
const $link = document.createElement('link');
$link.rel = 'stylesheet';
$link.type = 'text/css';
$link.href = getPath(theme);
$link.media = media;
return $link;
};
/* init links to load them first */
const $light = createLink('light', 'none');
const $dark = createLink('dark', 'none');
const toggleMedia = (theme) => {
switch (theme) {
case 'light':
$light.media = 'all';
$dark.media = 'none';
break;
case 'dark':
$light.media = 'none';
$dark.media = 'all';
break;
default:
break;
}
};
toggleMedia(window.theme);
$head.appendChild($light);
$head.appendChild($dark);
window.addEventListener('onthemetoggled', ({ detail: newTheme }) => toggleMedia(newTheme));
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', ({ matches: isDark }) => {
return isDark ? toggleMedia('dark') : toggleMedia('light');
});
}

View File

@ -1,12 +1,12 @@
import type { Component } from 'solid-js'; import { Component } from 'solid-js';
import { createSignal, createEffect, For } from 'solid-js'; import { createSignal, createEffect, For } from 'solid-js';
import { search, selectedTags } from '@stores/searchStore'; import { search, selectedTags } from '@stores/searchStore';
import { findArticlesBySearch, findArticlesByTags, sortArtcilesByDate } from '@utils/articles';
import { ArticleItem } from '@components/ArticleItem'; import { ArticleItem } from '@components/ArticleItem';
import * as utils from '@utils/articles'; import SearchInput from '@components/SearchInput';
import TagsList from '@components/TagsList';
import './ArticlesList.scss'; import './ArticlesList.scss';
import SearchInput from '../SearchInput';
import TagsList from '../TagsList';
type ArticlesListProps = { type ArticlesListProps = {
data: { data: {
@ -39,7 +39,7 @@ const ArticlesList: Component<ArticlesListProps> = (props) => {
* Find articles by title * Find articles by title
*/ */
if (search()) { if (search()) {
filtered = utils.findArticlesBySearch(search(), filtered); filtered = findArticlesBySearch(search(), filtered);
setArticles(filtered); setArticles(filtered);
} }
@ -47,7 +47,7 @@ const ArticlesList: Component<ArticlesListProps> = (props) => {
* Find articles by selected tags * Find articles by selected tags
*/ */
if (selectedTags().length !== 0) { if (selectedTags().length !== 0) {
filtered = utils.findArticlesByTags(selectedTags(), filtered); filtered = findArticlesByTags(selectedTags(), filtered);
setArticles(filtered); setArticles(filtered);
return; return;
@ -78,7 +78,7 @@ const ArticlesList: Component<ArticlesListProps> = (props) => {
return ( return (
<div class="articles-list__wrapper"> <div class="articles-list__wrapper">
<h2>{year}</h2> <h2>{year}</h2>
<For each={utils.sortArtcilesByDate(articles()[year])} children={(article) => <ArticleItem data={article} />} /> <For each={sortArtcilesByDate(articles()[year])} children={(article) => <ArticleItem data={article} />} />
</div> </div>
); );
}} }}

View File

@ -1,17 +1,18 @@
--- ---
import { SITE_METADATA } from '@root/config'; import { SITE_METADATA } from '@root/config';
export interface Props { export interface Props {
title?: string | undefined; title?: string | undefined;
description?: string | undefined; description?: string | undefined;
} }
const { title = SITE_METADATA.title, description } = Astro.props as Props; const { title = SITE_METADATA.title, description } = Astro.props as Props;
--- ---
<link rel='preconnect' href='https://fonts.googleapis.com' /> <link rel='preconnect' href='https://fonts.googleapis.com' />
<link rel='preconnect' href='https://fonts.gstatic.com' crossorigin /> <link rel='preconnect' href='https://fonts.gstatic.com' crossorigin />
<link href='https://fonts.googleapis.com/css2?family=Rubik:ital,wght@0,400;0,500;0,600;0,700;1,400&display=swap' rel='stylesheet' /> <link href='https://fonts.googleapis.com/css2?family=Rubik:ital,wght@0,400;0,500;0,600;0,700;1,400&display=swap'
rel='stylesheet' />
<meta charset='utf-8' /> <meta charset='utf-8' />
<meta name='viewport' content='width=device-width, initial-scale=1' /> <meta name='viewport' content='width=device-width, initial-scale=1' />
@ -21,4 +22,4 @@ const { title = SITE_METADATA.title, description } = Astro.props as Props;
<meta name='title' content={title} /> <meta name='title' content={title} />
<meta name='description' content={description || SITE_METADATA.description} /> <meta name='description' content={description || SITE_METADATA.description} />
<script is:inline src='assets/js/detect-theme.js' /> <script is:inline src='assets/js/detect-theme.js' />
<slot></slot> <slot></slot>

View File

@ -1,13 +1,13 @@
--- ---
import './BaseLayout.scss' import './BaseLayout.scss';
import Header from '@components/Header' import Header from '@components/Header';
import Footer from '@components/Footer' import Footer from '@components/Footer';
--- ---
<body> <body>
<Header /> <Header />
<main class='site-content'> <main class='site-content'>
<slot></slot> <slot></slot>
</main> </main>
<Footer /> <Footer />
</body> </body>

View File

@ -25,7 +25,7 @@ const SearchInput: Component<SearchInputProps> = (props) => {
* Update state and set url param * Update state and set url param
*/ */
const onSearch: JSX.EventHandler<HTMLInputElement, InputEvent> = (e) => { const onSearch: JSX.EventHandler<HTMLInputElement, InputEvent> = (e) => {
const value = e.currentTarget.value; const value = e.currentTarget.value.trim();
if (value.length === 0) { if (value.length === 0) {
resetSearch(); resetSearch();

View File

@ -1,6 +1,5 @@
import type { Component, JSX } from 'solid-js'; import type { Component, JSX } from 'solid-js';
import { SITE_SETTINGS } from '@root/config';
import { createSignal, onMount, Switch, Match } from 'solid-js'; import { createSignal, onMount, Switch, Match } from 'solid-js';
import './ThemeSwitcher.scss'; import './ThemeSwitcher.scss';
@ -23,14 +22,17 @@ const SunIcon: Component = () => (
); );
const ThemeSwitcher: Component = () => { const ThemeSwitcher: Component = () => {
const [theme, setTheme] = createSignal<ThemeModeType>(SITE_SETTINGS.defaultTheme); const [theme, setTheme] = createSignal<ThemeModeType>('light');
const [isReady, setIsReady] = createSignal<boolean>(false); const [isReady, setIsReady] = createSignal<boolean>(false);
const toggleTheme: JSX.EventHandler<HTMLButtonElement, MouseEvent> = () => { const toggleTheme: JSX.EventHandler<HTMLButtonElement, MouseEvent> = () => {
if (!window.theme) return; if (!window.theme) return;
const newTheme = theme() === 'light' ? 'dark' : 'light'; const newTheme = theme() === 'light' ? 'dark' : 'light';
const toggleEvent = new CustomEvent('onthemetoggled', { detail: newTheme });
setTheme(newTheme); setTheme(newTheme);
window.dispatchEvent(toggleEvent);
localStorage.setItem('theme', newTheme); localStorage.setItem('theme', newTheme);
document.documentElement.setAttribute('data-theme', newTheme); document.documentElement.setAttribute('data-theme', newTheme);
}; };
@ -39,7 +41,7 @@ const ThemeSwitcher: Component = () => {
setIsReady(true); setIsReady(true);
if (window.theme) { if (window.theme) {
setTheme(window.theme); setTheme(window.theme);
/* Update theme state on changing OS the,e */ /* Update theme state on changing OS theme */
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', ({ matches: isDark }) => { window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', ({ matches: isDark }) => {
const theme = isDark ? 'dark' : 'light'; const theme = isDark ? 'dark' : 'light';
setTheme(theme); setTheme(theme);

View File

@ -1,35 +1,36 @@
--- ---
import type { Props as BaseHeadProps } from '@root/components/BaseHead.astro'; import type { Props as BaseHeadProps } from '@root/components/BaseHead.astro';
import BaseHead from '@components/BaseHead.astro'; import BaseHead from '@components/BaseHead.astro';
import BaseLayout from '@components/BaseLayout'; import BaseLayout from '@components/BaseLayout';
import ArticleContent from '@components/ArticleContent'; import ArticleContent from '@components/ArticleContent';
import ArticlePost from '@components/ArticlePost'; import ArticlePost from '@components/ArticlePost';
import '@styles/global.scss'; import '@styles/global.scss';
export interface Props extends BaseHeadProps { export interface Props extends BaseHeadProps {
lang?: string | undefined; lang?: string | undefined;
content: ArticleType; content: ArticleType;
} }
const { lang = 'ru', content } = Astro.props as Props; const { lang = 'ru', content } = Astro.props as Props;
--- ---
<html lang={lang}> <html lang={lang}>
<head>
<BaseHead title={content.title}>
<link rel='stylesheet' href='assets/css/prism-one-dark.min.css' media='(prefers-color-scheme: dark)' />
<link rel='stylesheet' href='assets/css/prism-one-light.min.css' media='(prefers-color-scheme: light)' />
</BaseHead>
</head>
<BaseLayout> <head>
<div class='container'> <BaseHead title={content.title}>
<ArticlePost data={content}> <script is:inline defer src='assets/js/load-code-highlight.js' />
<ArticleContent> </BaseHead>
<slot></slot> </head>
</ArticleContent>
</ArticlePost> <BaseLayout>
</div> <div class='container'>
</BaseLayout> <ArticlePost data={content}>
</html> <ArticleContent>
<slot></slot>
</ArticleContent>
</ArticlePost>
</div>
</BaseLayout>
</html>

View File

@ -116,7 +116,7 @@
--font-color-grayer: var(--color-gray-200); --font-color-grayer: var(--color-gray-200);
/* Border */ /* Border */
--border-color: var(--color-gray-700); --border-color: var(--color-gray-600);
--border-color-hover: var(--color-gray-600); --border-color-hover: var(--color-gray-600);
/* Elements */ /* Elements */

5
src/types/main.d.ts vendored
View File

@ -3,6 +3,11 @@ export {};
declare global { declare global {
interface Window { interface Window {
theme?: ThemeModeType | undefined; theme?: ThemeModeType | undefined;
addEventListener<K extends keyof CustomEventMap>(type: K, listener: (this: Window, ev: CustomEventMap[K]) => void): void;
}
interface CustomEventMap {
onthemetoggled: CustomEvent<ThemeModeType>;
} }
type ArticleType = { type ArticleType = {