init new urara

This commit is contained in:
matthieu42morin 2024-04-28 13:44:18 +02:00
parent 15bc3ce30c
commit e1c965efb4
193 changed files with 3525 additions and 3632 deletions

View File

@ -1,19 +0,0 @@
Dockerfile
.dockerignore
.git
.gitignore
.gitattributes
README.md
.npmrc
.prettierrc
.eslintrc.cjs
.graphqlrc
.editorconfig
.svelte-kit
.vscode
node_modules
build
package
**/.env
**/dist
*.local

View File

@ -1,14 +0,0 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true
indent_style = tab
indent_size = 4
charset = utf-8
trim_trailing_whitespace = false
quote_type = single
[*.{yml, md}]
indent_style = space
indent_size = 2

View File

@ -1,13 +0,0 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
*.local
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock

View File

@ -1,30 +0,0 @@
module.exports = {
root: true,
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:svelte/recommended',
'prettier'
],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
parserOptions: {
sourceType: 'module',
ecmaVersion: 2020,
extraFileExtensions: ['.svelte']
},
env: {
browser: true,
es2017: true,
node: true
},
overrides: [
{
files: ['*.svelte'],
parser: 'svelte-eslint-parser',
parserOptions: {
parser: '@typescript-eslint/parser'
}
}
]
};

37
.eslintrc.json Normal file
View File

@ -0,0 +1,37 @@
{
"root": true,
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:svelte/recommended",
"prettier"
],
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint",
"svelte"
],
"parserOptions": {
"sourceType": "module",
"ecmaVersion": 2020,
"extraFileExtensions": [
".svelte"
]
},
"env": {
"browser": true,
"es2017": true,
"node": true
},
"overrides": [
{
"files": [
"*.svelte"
],
"parser": "svelte-eslint-parser",
"parserOptions": {
"parser": "@typescript-eslint/parser"
}
}
]
}

2
.gitattributes vendored Normal file
View File

@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto

View File

@ -1,27 +0,0 @@
name: Playwright Tests
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- name: Install dependencies
run: npm install -g pnpm && pnpm install
- name: Install Playwright Browsers
run: pnpm exec playwright install --with-deps
- name: Run Playwright tests
run: pnpm exec playwright test
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30

35
.github/CONTRIBUTING.md vendored Normal file
View File

@ -0,0 +1,35 @@
# Contributing
Thanks for ur interest in contributing to Urara! Please take a moment to read this document before submitting a pull request.
## Pull requests
pls ask before u start working on any important new feature.
for minor features and bug fixes: I will accept them as long as I think the code quality is good enough.
### Commit message
This is not mandatory at this time, but pls use [gitmoji](https://gitmoji.dev) and [Conventional Commits](https://www.conventionalcommits.org) whenever possible.
### Check the code
Run this command to check the code:
```bash
pnpm check
```
In general, expect to see output like this:
```text
svelte-check found 0 errors, 0 warnings, and 0 hints
```
### Format the code
run this command to format the code:
```bash
pnpm format
```

14
.github/workflows/gh-pages.yml vendored Normal file
View File

@ -0,0 +1,14 @@
name: Deploy to GitHub Pages
on:
push:
branches: main
workflow_dispatch:
jobs:
deploy:
uses: importantimport/.github/.github/workflows/pnpm-gh-pages.yml@main
permissions:
contents: write
with:
publish_dir: build

6
.gitignore vendored
View File

@ -1,3 +1,9 @@
# temp file
src/routes/**/+page.svelte.md
src/routes/**/+page.md
src/static
urara.js
+ .turbo
+ build/**
+ dist/**

2
.npmrc Normal file
View File

@ -0,0 +1,2 @@
engine-strict=true
strict-peer-dependencies=false

1
.nvmrc Normal file
View File

@ -0,0 +1 @@
v21.6.1

View File

@ -1,9 +0,0 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"pluginSearchDirs": ["."],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}

27
.prettierrc.json Normal file
View File

@ -0,0 +1,27 @@
{
"printWidth": 128,
"useTabs": false,
"tabWidth": 2,
"semi": false,
"singleQuote": true,
"endOfLine": "lf",
"arrowParens": "avoid",
"trailingComma": "none",
"bracketSpacing": true,
"bracketSameLine": true,
"htmlWhitespaceSensitivity": "ignore",
"plugins": [
"prettier-plugin-svelte"
],
"pluginSearchDirs": [
"."
],
"overrides": [
{
"files": "*.svelte",
"options": {
"parser": "svelte"
}
}
]
}

View File

@ -1,3 +1,3 @@
{
"recommendations": ["svelte.svelte-vscode", "orta.vscode-twoslash-queries"]
"recommendations": ["svelte.svelte-vscode", "dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]
}

118
.vscode/settings.json vendored
View File

@ -1,112 +1,10 @@
{
"editor.formatOnSave": true,
"svelte.plugin.typescript.format.enable": true, // enable Svelte formatter for TypeScript in Svelte files
"svelte.plugin.svelte.format.enable": true, // enable Svelte formatter for HTML and CSS in Svelte files
"[svelte]": {
"editor.defaultFormatter": "svelte.svelte-vscode" // enable Prettier formatter for JavaScript files
},
"editor.fontFamily": "Fira Code",
"editor.fontLigatures": true,
"markdownlint.config": {
"MD033": {
"allowed_elements": ["a"]
}
},
"prettier.documentSelectors": ["**/*.svelte"],
"tailwindCSS.classAttributes": [
"class",
"accent",
"active",
"aspectRatio",
"background",
"bgBackdrop",
"bgDark",
"bgDrawer",
"bgLight",
"blur",
"border",
"button",
"buttonClasses",
"buttonTextFirst",
"buttonTextLast",
"buttonTextNext",
"buttonTextPrevious",
"caretClosed",
"caretOpen",
"color",
"controlSeparator",
"controlVariant",
"cursor",
"display",
"element",
"fill",
"fillDark",
"fillLight",
"flex",
"gap",
"gridColumns",
"height",
"hover",
"inactive",
"indent",
"justify",
"meter",
"padding",
"regionAnchor",
"regionBackdrop",
"regionBody",
"regionCaption",
"regionCaret",
"regionCell",
"regionChildren",
"regionCone",
"regionContent",
"regionControl",
"regionDefault",
"regionDrawer",
"regionFoot",
"regionFootCell",
"regionHead",
"regionHeadCell",
"regionHeader",
"regionIcon",
"regionInterface",
"regionInterfaceText",
"regionLabel",
"regionLead",
"regionLegend",
"regionList",
"regionListItem",
"regionNavigation",
"regionPage",
"regionPanel",
"regionRowHeadline",
"regionRowMain",
"regionSummary",
"regionSymbol",
"regionTab",
"regionTrail",
"ring",
"rounded",
"select",
"shadow",
"slotDefault",
"slotFooter",
"slotHeader",
"slotLead",
"slotMessage",
"slotMeta",
"slotPageContent",
"slotPageFooter",
"slotPageHeader",
"slotSidebarLeft",
"slotSidebarRight",
"slotTrail",
"spacing",
"text",
"track",
"width",
"zIndex"
]
"editor.formatOnSave": true,
"files.eol": "\n",
"typescript.tsdk": "node_modules\\typescript\\lib",
"css.lint.unknownAtRules": "ignore",
"svelte.plugin.css.diagnostics.enable": false,
"[html]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}

View File

@ -1,29 +0,0 @@
FROM node:18-alpine AS builder
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN npm install -g pnpm && \
echo "Installing pnpm..."
RUN pnpm install --frozen-lockfile && \
echo "Installing deps..."
COPY . .
RUN pnpm run build && \
echo "Building..." && \
pnpm prune --production
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/build build/
COPY --from=builder /app/node_modules node_modules/
COPY package.json .
EXPOSE 3000
ENV NODE_ENV=production
CMD [ "node", "build" ]

20
LICENSE
View File

@ -1,20 +0,0 @@
Copyright (c) 2023-2024 Matt Morin
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.

135
README.md
View File

@ -1,30 +1,123 @@
# Hello world, this is my personal site
<br />
<div align="center">
<a href="https://github.com/importantimport/urara">
<img src="https://github.com/importantimport/urara/raw/main/urara/hello-world/urara.webp" alt="urara" /></a>
</div>
<br />
Featuring a blog, projects, current social accounts, skills and so on and so on, look at [Technical Features](#technical-features)
<p align="center">
<a href="https://fff.js.org"><img src="https://img.shields.io/badge/%F0%9F%8C%9F%20F%20F%20F-1.0-yellow?style=flat" alt="fff" /></a>
<img src="https://img.shields.io/github/languages/top/importantimport/urara?color=%23ff3e00" alt="Language" />
<a href="https://github.com/importantimport/urara/blob/main/COPYING"><img src="https://img.shields.io/github/license/importantimport/urara?color=%23fff" alt="License" /></a>
<img src="https://app.fossa.com/api/projects/git%2Bgithub.com%2Fimportantimport%2Furara.svg?type=shield" alt="FOSSA Status" />
</p>
<p align="center">
<a href="https://urara-demo.netlify.app">🚀 Demo</a>
/
<a href="https://urara-docs.netlify.app">📝 Documentation</a>
/
<a href="https://github.com/importantimport/urara/discussions">💬 Discussions</a>
</p>
<p align="center">
<span>English</span>
|
<a href="https://github.com/importantimport/urara/blob/main/README.zh.md">正體中文</a>
</p>
## Stack info
## 🎉 Try it now!
I focused on researching the best possible modern solutions to frontend and worked on my previous knowledge of svelte.
### Local
Javascript, Typescript
Framework: Sveltekit
CSS: Tailwindcss, postcss
[MDsveX](https://mdsvex.pngwn.io/) for markdown text file processing with plugins
Dockerfile, node-adapter for custom deploy
AWS S3 for static assets
AWS lambda for automation
```bash
npx degit importantimport/urara my-blog && cd my-blog # create a new project in my-blog
pnpm i # if u don't have pnpm installed, run: npm i -g pnpm
```
## Technical Features
### Remote
- A possibility of great .md file processing with ability to use svelte components in .md
- Rss feed
- Sitemap, robots, Manifest, Workers
- Some playwright testing
[![Open in StackBlitz](https://img.shields.io/badge/-Open%20in%20StackBlitz-1374ef?style=for-the-badge&logo=Amp)](https://stackblitz.com/github/importantimport/urara) [![Use this template](https://img.shields.io/badge/-Use%20this%20Template-181717?style=for-the-badge&logo=GitHub)](https://github.com/importantimport/urara/generate) [![Deploy with Vercel](https://img.shields.io/badge/-Deploy%20with%20Vercel-1374ef?style=for-the-badge&logo=Vercel)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fimportantimport%2Furara&env=URARA_SITE_URL&envDescription=Site%20URL.&envLink=https%3A%2F%2Fexample.com&project-name=urara-blog&repository-name=urara-blog) [![Deploy to Netlify](https://img.shields.io/badge/-Deploy%20to%20Netlify-15847d?style=for-the-badge&logo=Netlify&logoColor=white)](https://app.netlify.com/start/deploy?repository=https%3A%2F%2Fgithub.com%2Fimportantimport%2Furara#URARA_SITE_URL=https://example.com&CUSTOM_LOGO=https://github.com/importantimport/urara/raw/main/urara/assets/any@512.png)
[CSP from rodnylab](https://rodneylab.com/sveltekit-content-security-policy/)
## ⚡️ Usage
## Credits
### Developing
This project is using [Skeleton Labs UI / Component / utils Library](https://www.skeleton.dev/) for sveltekit.
This project used some logic of gitpod.io sveltekit blog with MIT License, however they have shortly pulled their site off of github, their source or license now unreachable.
I have learned some svelte tricks used here from [Matt Croat](https://matia.xyz) alias [https://joyofcode.xyz/]
Start a development server:
```bash
pnpm dev
```
### Building
Create a production version of ur blog:
```bash
pnpm build
```
u can preview the built app with `pnpm preview`.
### Documentation
For full documentation, visit [urara-docs.netlify.app](https://urara-docs.netlify.app).
### Give this project a star
tyvm! ur ⭐ will give me more motivation to improve this project.
## ✨ Features
- Out of the box **Atom feed** (WebSub), **Sitemap**, **PWA** (Web app manifest & ServiceWorker) support.
- Present beautiful interface designs and animations with daisyUI, of course.
- Good [IndieWeb](https://indieweb.org/) Compatibility - Multi-kind posts with [microformats2](https://microformats.org/) markup content, Showcasing [Webmentions](https://indieweb.org/Webmention) via [webmentions.io](https://webmentions.io) API.
- Don't worry about the article and image directories - just put them under a folder and they'll be [copied automatically at build time](https://github.com/importantimport/urara/blob/main/urara.ts).
- [Comment Components](https://github.com/importantimport/urara/tree/main/src/lib/components/comments): Webmentions, Giscus, Utterances... u can use more than one.
## 📦️ Pre-packed
### TailwindCSS & PostCSS Plugins
- [daisyUI](https://github.com/saadeghi/daisyui) - The most popular, free and open-source Tailwind CSS component library.
- [Tailwind CSS Typography](https://github.com/tailwindlabs/tailwindcss-typography) - Beautiful typographic defaults for HTML you don't control.
- [Autoprefixer](https://github.com/postcss/autoprefixer) - Parse CSS and add vendor prefixes to rules by Can I Use.
- [CSSNANO](https://github.com/cssnano/cssnano) - A modular minifier, built on top of the PostCSS ecosystem.
### Markdown preprocessor & Syntax highlighter
- [MDsveX](https://github.com/pngwn/MDsveX) - A markdown preprocessor for Svelte.
- [Shiki Twoslash](https://github.com/shikijs/twoslash) - A beautiful Syntax Highlighter.
### Vite Plugins
- [UnoCSS](https://github.com/unocss/unocss) - The instant on-demand atomic CSS engine.
- [VitePWA](https://github.com/antfu/vite-plugin-pwa) - Zero-config PWA for Vite.
## 🚀 Sites
- [./kwaa.dev](https://kwaa.dev) - [kwaa/blog](https://github.com/kwaa/blog)
- [Seviche.cc](https://seviche.cc) - [Sevichecc/Urara-Blog](https://github.com/Sevichecc/Urara-Blog)
- [./khatta.sh](https://blog.shameerkashif.me) - [hash3liZer/khatta](https://github.com/hash3liZer/khatta)
and more...
- [urara-blog - Discussions](https://github.com/importantimport/urara/discussions/2)
- [urara-blog - Topics](https://github.com/topics/urara-blog)
are u using Urara? add the `urara-blog` topic on ur repo!
## 👥 Contributing
If u're interested in contributing to Urara, pls read [contributing docs](.github/CONTRIBUTING.md) before submitting a pull request.
## 📝 License
This work is free, it comes without any warranty. You can redistribute it and/or modify it under the
terms of the Do What The Fuck You Want To Public License, Version 2,
as published by Sam Hocevar. See the [COPYING](https://github.com/importantimport/urara/blob/main/COPYING) file for more details.
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fimportantimport%2Furara.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fimportantimport%2Furara?ref=badge_large)
special thanks / inspired from:
- [@michaeloliverx - Generate Posts List](https://github.com/pngwn/MDsveX/issues/294#issuecomment-907029639)
- icon made by [Kpouri](https://github.com/kpouri)

125
README.zh.md Normal file
View File

@ -0,0 +1,125 @@
<br />
<div align="center">
<a href="https://github.com/importantimport/urara">
<img src="https://github.com/importantimport/urara/raw/main/urara/hello-world/urara.webp" alt="urara" /></a>
</div>
<br />
<p align="center">
<a href="https://fff.js.org"><img src="https://img.shields.io/badge/%F0%9F%8C%9F%20F%20F%20F-1.0-yellow?style=flat" alt="fff" /></a>
<img src="https://img.shields.io/github/languages/top/importantimport/urara?color=%23ff3e00" alt="Language" />
<a href="https://github.com/importantimport/urara/blob/main/COPYING"><img src="https://img.shields.io/github/license/importantimport/urara?color=%23fff" alt="License" /></a>
<img src="https://app.fossa.com/api/projects/git%2Bgithub.com%2Fimportantimport%2Furara.svg?type=shield" alt="FOSSA Status" />
</p>
<p align="center">
<a href="https://urara-demo.netlify.app">🚀 演示</a>
/
<a href="https://urara-docs.netlify.app">📝 文檔</a>
/
<a href="https://github.com/importantimport/urara/discussions">💬 討論</a>
</p>
<p align="center">
<a href="https://github.com/importantimport/urara">English</a>
|
<span>正體中文</span>
</p>
## 🎉 現在就試試!
### 本地
```bash
npx degit importantimport/urara my-blog && cd my-blog # 在當前目錄創建一個名為 my-blog 的新項目
pnpm i # 如果你沒有安裝 pnpm運行npm i -g pnpm
```
### 遠端
[![Open in StackBlitz](https://img.shields.io/badge/-Open%20in%20StackBlitz-1374ef?style=for-the-badge&logo=Amp)](https://stackblitz.com/github/importantimport/urara) [![Use this template](https://img.shields.io/badge/-Use%20this%20Template-181717?style=for-the-badge&logo=GitHub)](https://github.com/importantimport/urara/generate) [![Deploy with Vercel](https://img.shields.io/badge/-Deploy%20with%20Vercel-1374ef?style=for-the-badge&logo=Vercel)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fimportantimport%2Furara&env=URARA_SITE_URL&envDescription=Site%20URL.&envLink=https%3A%2F%2Fexample.com&project-name=urara-blog&repository-name=urara-blog) [![Deploy to Netlify](https://img.shields.io/badge/-Deploy%20to%20Netlify-15847d?style=for-the-badge&logo=Netlify&logoColor=white)](https://app.netlify.com/start/deploy?repository=https%3A%2F%2Fgithub.com%2Fimportantimport%2Furara#URARA_SITE_URL=https://example.com&CUSTOM_LOGO=https://github.com/importantimport/urara/raw/main/urara/assets/any@512.png)
## ⚡️ 用法
### 開發
啟動開發服務器:
```bash
pnpm dev
```
### 構建
創建你的博客的產品版本:
```bash
pnpm build
```
你還可以使用 `pnpm preview` 預覽構建的網站。
### 文檔
如需完整文檔,請訪問 [urara-docs.netlify.app](https://urara-docs.netlify.app)。
### 給這個項目一個 star
非常感謝!你的 ⭐ 會給我更多的動力來改進這個項目。
## ✨ 特徵
- 開箱即用的 **Atom feed** (WebSub), **Sitemap**, **PWA** (Web app manifest & ServiceWorker) 支持。
- 使用 daisyUI 呈現精美的界面設計和動畫效果,當然。
- 良好的 [IndieWeb](https://indieweb.org/) 兼容性 - 帶有 [microformats2](https://microformats.org/) 標記內容的多種帖子,通過 [webmentions.io](https://webmentions.io) API 展示 [Webmentions](https://indieweb.org/Webmention)。
- 不用擔心文章和圖像目錄 - 只需將它們放在一個文件夾下,它們就會[在構建時自動複製](https://github.com/importantimport/urara/blob/main/urara.ts)。
- [評論組件](https://github.com/importantimport/urara/tree/main/src/lib/components/comments) Webmentions、 Giscus、 Utterances... 你可以使用不止一個。
## 📦️ 預捆綁
### TailwindCSS & PostCSS 插件
- [daisyUI](https://github.com/saadeghi/daisyui) - The most popular, free and open-source Tailwind CSS component library.
- [Tailwind CSS Typography](https://github.com/tailwindlabs/tailwindcss-typography) - Beautiful typographic defaults for HTML you don't control.
- [Autoprefixer](https://github.com/postcss/autoprefixer) - Parse CSS and add vendor prefixes to rules by Can I Use.
- [CSSNANO](https://github.com/cssnano/cssnano) - A modular minifier, built on top of the PostCSS ecosystem.
### Markdown 預處理器和語法高亮
- [MDsveX](https://github.com/pngwn/MDsveX) - A markdown preprocessor for Svelte.
- [Shiki Twoslash](https://github.com/shikijs/twoslash) - A beautiful Syntax Highlighter.
### Vite 插件
- [UnoCSS](https://github.com/unocss/unocss) - The instant on-demand atomic CSS engine.
- [VitePWA](https://github.com/antfu/vite-plugin-pwa) - Zero-config PWA for Vite.
## 🚀 網站
- [./kwaa.dev](https://kwaa.dev) - [kwaa/blog](https://github.com/kwaa/blog)
- [Seviche.cc](https://seviche.cc) - [Sevichecc/Urara-Blog](https://github.com/Sevichecc/Urara-Blog)
和更多...
- [urara-blog - Discussions](https://github.com/importantimport/urara/discussions/2)
- [urara-blog - Topics](https://github.com/topics/urara-blog)
你在用 Urara 嗎?在你的 repo 上添加 `urara-blog` 主題!
## 👥 貢獻
如果您有興趣為 Urara 做出貢獻,請在提交拉取請求之前閱讀[貢獻文檔](.github/CONTRIBUTING.md)。
## 📝 License
這項工作是免費的,它沒有任何保證。你可以在以下條款下重新發布和/或修改它:
Do What The Fuck You Want To Public License, Version 2,
as published by Sam Hocevar.
有關詳細信息,請參閱 [COPYING](COPYING) 文件。
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fimportantimport%2Furara.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fimportantimport%2Furara?ref=badge_large)
特別感謝 / 靈感來自於:
- [@michaeloliverx - Generate Posts List](https://github.com/pngwn/MDsveX/issues/294#issuecomment-907029639)
- [Kpouri](https://github.com/kpouri) 製作的圖標

View File

@ -1,73 +1,119 @@
import { defineMDSveXConfig as defineConfig } from 'mdsvex';
import remarkExternalLinks from 'remark-external-links';
import remarkSetImagePath from './src/lib/utils/remark-set-image-path.js';
import remarkLinkWithImageAsOnlyChild from './src/lib/utils/remark-link-with-image-as-only-child.js';
import rehypeImgSize from 'rehype-img-size';
// rehype plugins
import rehypeSlug from 'rehype-slug'
import rehypeAutolinkHeadings from 'rehype-autolink-headings'
import rehypeExternalLinks from 'rehype-external-links'
import remarkUnwrapImages from 'remark-unwrap-images';
import remarkToc from 'remark-toc';
import rehypeSlug from 'rehype-slug';
// urara remark plugins
import { parse, join } from 'node:path'
import { visit } from 'unist-util-visit'
import { toString } from 'mdast-util-to-string'
import Slugger from 'github-slugger'
import remarkFFF from 'remark-fff'
import remarkFootnotes from 'remark-footnotes'
// import { highlightCode } from './src/lib/utils/highlighter.js';
// highlighter
import { escapeSvelte } from 'mdsvex'
import { lex, parse as parseFence } from 'fenceparser'
import { renderCodeToHTML, runTwoSlash, createShikiHighlighter } from 'shiki-twoslash'
/** @type {import('mdsvex').MdsvexOptions} */
const config = defineConfig({
extensions: ['.svelte.md', '.md', '.svx'],
smartypants: {
dashes: 'oldschool'
},
/* Wait for skeleton to implement Prismjs, for now use <CodeBlock /> in .md files */
// layout: {
// blog: './src/lib/components/blog/_blog-layout.svelte',
// project: './src/lib/components/projects/_project-layout.svelte',
// _: './src/lib/components/fallback/_layout.svelte'
// },
/* Plugins */
rehypePlugins: [
[rehypeSlug],
[rehypeImgSize]
// [
// /** Custom rehype plugin to add loading="lazy" to all images */
// () => {
// return (tree) => {
// visit(tree, 'element', (node) => {
// if (node.tagName === 'img') {
// node.properties.loading = 'lazy';
// }
// });
// };
// }
// ]
],
remarkPlugins: [
[remarkToc, { maxDepth: 3, tight: true }],
[
(remarkExternalLinks,
{
target: '_blank'
})
],
[remarkUnwrapImages],
remarkSetImagePath,
remarkLinkWithImageAsOnlyChild
// [
// headings,
// {
// behavior: 'append',
// linkProperties: {},
// content: function (node) {
// return [
// h('span.icon.icon-link header-anchor', {
// ariaLabel: toString(node) + ' permalink'
// })
// ];
// }
// }
// ],
const remarkUraraFm =
() =>
(tree, { data, filename }) => {
const filepath = filename ? filename.split('/src/routes')[1] : 'unknown'
const { dir, name } = parse(filepath)
if (!data.fm) data.fm = {}
// Generate slug & path
data.fm.slug = filepath
data.fm.path = join(dir, `/${name}`.replace('/+page', '').replace('.svelte', ''))
// Generate ToC
if (data.fm.toc !== false) {
const [slugs, toc] = [new Slugger(), []]
visit(tree, 'heading', node => {
toc.push({
depth: node.depth,
title: toString(node),
slug: slugs.slug(toString(node), false)
})
})
if (toc.length > 0) data.fm.toc = toc
else data.fm.toc = false
}
}
// remarkHeadingsPermaLinks,
// getHeadings
]
});
// Better type definitions needed
const remarkUraraSpoiler = () => tree =>
visit(tree, 'paragraph', node => {
const { children } = node
const text = children[0].value
const re = /\|\|(.{1,}?)\|\|/g
if (re.test(children[0].value)) {
children[0].type = 'html'
children[0].value = text.replace(re, (_match, p1) => `<span class="spoiler">${p1}</span>`)
}
return node
})
export default config;
/** @type {import("mdsvex").MdsvexOptions} */
export default {
extensions: ['.svelte.md', '.md'],
smartypants: {
dashes: 'oldschool'
},
layout: {
_: './src/lib/components/post_layout.svelte'
},
highlight: {
highlighter: async (code, lang, meta) => {
let fence, twoslash
try {
fence = parseFence(lex([lang, meta].filter(Boolean).join(' ')))
} catch (error) {
throw new Error(`Could not parse the codefence for this code sample \n${code}`)
}
if (fence?.twoslash === true) twoslash = runTwoSlash(code, lang)
return `{@html \`${escapeSvelte(
renderCodeToHTML(
code,
lang,
fence ?? {},
{ themeName: 'material-default' },
await createShikiHighlighter({ theme: 'material-default' }),
twoslash
)
)}\` }`
}
},
remarkPlugins: [
[
remarkFFF,
{
presets: [],
target: 'mdsvex',
autofill: {
provider: 'fs',
path: path => path.replace('/src/routes/', '/urara/')
},
strict: {
media: {
type: 'string',
array: false
}
}
}
],
remarkUraraFm,
remarkUraraSpoiler,
[remarkFootnotes, { inlineNotes: true }]
],
rehypePlugins: [
rehypeSlug,
[rehypeAutolinkHeadings, { behavior: 'wrap' }],
[
rehypeExternalLinks,
{
rel: ['nofollow', 'noopener', 'noreferrer', 'external'],
target: '_blank'
}
]
]
}

7
netlify.toml Normal file
View File

@ -0,0 +1,7 @@
[build]
command = "npx pnpm i --store=node_modules/.pnpm-store && npx pnpm build"
publish = "build"
[build.environment]
NPM_FLAGS = "--version"
[functions]
node_bundler = "esbuild"

View File

@ -1,83 +1,86 @@
{
"name": "its-personal~portfolio",
"description": "Uses pnpm, svelte, mdsvex",
"version": "1.0.0",
"main": "index.js",
"private": true,
"homepage": "mattmor.in",
"author": "Matthieu Morin",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://git.mattmor.in/Madmin/its-personal/"
},
"packageManager": "pnpm@8.6.6",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"test": "vitest",
"lint": "prettier --plugin-search-dir . --check . && eslint .",
"format": "prettier --plugin-search-dir . --write .",
"test-ct": "playwright test -c playwright-ct.config.ts"
},
"devDependencies": {
"@playwright/test": "^1.41.2",
"@skeletonlabs/skeleton": "2.0.0",
"@skeletonlabs/tw-plugin": "0.1.0",
"@sveltejs/kit": "^2.5.0",
"@tailwindcss/forms": "0.5.6",
"@tailwindcss/typography": "0.5.9",
"@types/js-cookie": "^3.0.6",
"@types/node": "20.5.7",
"@types/prismjs": "^1.26.3",
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0",
"autoprefixer": "10.4.15",
"emoji-regex": "^10.3.0",
"eslint": "^8.56.0",
"eslint-config-prettier": "^8.10.0",
"eslint-plugin-svelte": "^2.35.1",
"js-cookie": "^3.0.5",
"postcss": "8.4.29",
"prettier": "^2.8.8",
"prettier-plugin-svelte": "^2.10.1",
"rehype-autolink-headings": "^7.1.0",
"rehype-img-size": "^1.0.1",
"rehype-slug": "^6.0.0",
"remark-external-links": "^9.0.1",
"remark-toc": "^9.0.0",
"remark-unwrap-images": "^4.0.0",
"sass": "^1.71.0",
"shiki": "^1.1.6",
"svelte": "^4.2.11",
"svelte-check": "^3.6.4",
"tailwindcss": "3.3.3",
"tslib": "^2.6.2",
"typescript": "^5.3.3",
"unist-util-visit": "^5.0.0",
"vite": "^5.1.3",
"vite-plugin-tailwind-purgecss": "0.2.0",
"vitest": "^0.34.6"
},
"dependencies": {
"@floating-ui/dom": "1.5.1",
"@fortawesome/fontawesome-free": "^6.5.1",
"@sentry/sveltekit": "^7.102.0",
"@sveltejs/adapter-node": "^4.0.1",
"@sveltejs/vite-plugin-svelte": "^3.0.2",
"@threlte/core": "^6.1.1",
"@threlte/extras": "^8.7.5",
"@yushijinhun/three-minifier-rollup": "^0.4.0",
"highlight.js": "11.8.0",
"latest": "^0.2.0",
"linkedom": "^0.15.6",
"mdsvex": "^0.11.0",
"prismjs": "^1.29.0",
"rss": "^1.2.2",
"svelte-preprocess": "^5.1.3"
},
"type": "module"
"name": "urara",
"type": "module",
"version": "0.0.1",
"license": "WTFPL",
"repository": "importantimport/urara",
"homepage": "https://github.com/importantimport/urara",
"bugs": "https://github.com/importantimport/urara/issues",
"author": "藍+85CD",
"packageManager": "pnpm@8.14.1",
"scripts": {
"clean": "node urara.js clean",
"tsc": "tsc -p tsconfig.node.json",
"tsc:watch": "tsc -w -p tsconfig.node.json",
"urara:build": "node urara.js build",
"urara:watch": "node urara.js watch",
"kit:dev": "cross-env NODE_OPTIONS=--max_old_space_size=7680 vite dev",
"kit:build": "cross-env NODE_OPTIONS=--max_old_space_size=7680 vite build",
"dev:parallel": "run-p -r tsc:watch urara:watch \"kit:dev {@} \" --",
"dev": "run-s tsc \"dev:parallel {@} \" --",
"build": "run-s tsc urara:build kit:build clean",
"preview": "vite preview",
"start": "cross-env ADAPTER=node pnpm build && node build",
"check": "svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --check --plugin-search-dir=. . && eslint --ignore-path .gitignore .",
"format": "prettier --write --plugin-search-dir=. ."
},
"devDependencies": {
"@iconify-json/heroicons-outline": "^1.1.10",
"@iconify-json/heroicons-solid": "^1.1.11",
"@sveltejs/adapter-auto": "^3.1.1",
"@sveltejs/adapter-netlify": "^4.1.0",
"@sveltejs/adapter-node": "^4.0.1",
"@sveltejs/adapter-static": "^3.0.1",
"@sveltejs/adapter-vercel": "^5.1.0",
"@sveltejs/kit": "^2.5.0",
"@sveltejs/vite-plugin-svelte": "^3.0.2",
"@tailwindcss/typography": "^0.5.10",
"@types/node": "^20.11.17",
"@types/unist": "^3.0.2",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"@unocss/extractor-svelte": "^0.55.7",
"@vite-pwa/sveltekit": "^0.1.3",
"chalk": "^5.3.0",
"chokidar": "^3.6.0",
"cross-env": "^7.0.3",
"daisyui": "^4.6.2",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.35.1",
"fenceparser": "^2.2.0",
"fff-flavored-frontmatter": "1.0.0-alpha.1",
"github-slugger": "^2.0.0",
"mdast-util-to-string": "^3.2.0",
"mdsvex": "^0.11.0",
"npm-run-all": "^4.1.5",
"postcss": "^8.4.35",
"postcss-lightningcss": "^0.7.0",
"prettier": "^3.2.5",
"prettier-plugin-svelte": "^3.1.2",
"rehype-autolink-headings": "^6.1.1",
"rehype-external-links": "^2.1.0",
"rehype-slug": "^5.1.0",
"remark": "^14.0.3",
"remark-fff": "1.0.0-alpha.1",
"remark-footnotes": "~2.0.0",
"shiki-twoslash": "^3.1.2",
"svelte": "^4.2.10",
"svelte-check": "^3.6.4",
"svelte-eslint-parser": "^0.33.1",
"svelte-preprocess": "^5.1.3",
"sveltekit-embed": "^0.0.14",
"tailwindcss": "^3.4.1",
"tslib": "^2.6.2",
"typescript": "^5.3.3",
"unist-util-visit": "^4.1.2",
"unocss": "^0.58.5",
"vite": "^5.1.1",
"vite-imagetools": "^4.0.19",
"vite-plugin-pwa": "^0.17.5",
"workbox-build": "^7.0.0",
"workbox-window": "^7.0.0"
}
}

View File

@ -1,77 +0,0 @@
import { defineConfig, devices } from '@playwright/test';
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './tests',
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://127.0.0.1:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry'
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] }
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] }
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] }
},
/* Test against mobile viewports. */
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] }
},
{
name: 'Mobile Safari',
use: { ...devices['iPhone 12'] }
}
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
]
/* Run your local dev server before starting the tests */
// webServer: {
// command: 'npm run start',
// url: 'http://127.0.0.1:3000',
// reuseExistingServer: !process.env.CI,
// },
});

View File

@ -1,6 +0,0 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

101
src/app.d.ts vendored
View File

@ -1,9 +1,94 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
// and what to do when importing types
declare namespace App {
// interface Locals {}
// interface PageData {}
// interface Error {}
// interface Platform {}
/// <reference types="@sveltejs/kit" />
import type { FFFBase, FFFMedia, FFFMention } from 'fff-flavored-frontmatter'
interface ImportMetaEnv extends Readonly<Record<string, string>> {
readonly URARA_SITE_PROTOCOL?: 'http://' | 'https://'
readonly URARA_SITE_DOMAIN?: string
}
interface ImportMeta {
glob<Module = { [key: string]: unknown }>(pattern: string): Record<string, Module>
readonly env: ImportMetaEnv
}
declare global {
namespace Urara {
namespace Post {
type Frontmatter = Omit<FFFBase, 'flags'> &
Pick<FFFMention, 'in_reply_to'> &
Pick<FFFMedia, 'alt'> & {
/**
* post type.
* @remarks auto-generated
*/
type: 'article' | 'note' | 'photo' | 'reply' | 'audio' | 'video' | 'like' | 'repost' | 'bookmark'
/**
* post layout.
*/
layout?: 'article' | 'note' | 'photo' | 'reply'
/**
* post path.
* @remarks auto-generated
*/
path: string
/**
* post slug.
* @remarks auto-generated
*/
slug: string
/**
* table of contents.
* @remarks auto-generated, article-only, set to `false` to disable
*/
toc?: false | Toc[]
/**
* the created date of the post.
* @remarks auto-generated or set manually
*/
created: string
/**
* the updated date of the post.
* @remarks auto-generated or set manually
*/
updated: string
/**
* the published date of the post.
*/
published?: string
/**
* the featured image for article, or image for "photo" / "multi-photo" posts.
* @remarks currently only supports string
*/
image?: string
/** enable some advanced features.
* @property hidden - deprecated, transfer to `unlisted`
* @property unlisted - hide this post from the homepage and feed.
* @property bridgy-fed - add a link to Bridgy Fed in the post. https://fed.brid.gy/
* @property bridgy-{target} - add a link to Bridgy in the post. https://brid.gy/publish/{target}
*/
flags?: string[]
}
type Toc = {
depth: number
title?: string
slug?: string
children?: Toc[]
}
interface Module {
default: {
render: () => {
html: string
head: string
css: {
code: string
}
}
}
metadata: Frontmatter
}
}
type Post = Post.Frontmatter & { html?: string }
type Page = { title?: string; path: string }
}
}

View File

@ -1,22 +1,16 @@
<!DOCTYPE html>
<html lang="en" class="dark">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<link rel="icon" href="/images/profile-pic.png" />
<!-- <link
rel="icon"
href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='48' height='48' viewBox='0 0 18 16'><text x='0' y='14'>🚀</text></svg>"
/> -->
<link rel="preload" href="/fonts/Quicksand.ttf" as="font" type="font/ttf" crossorigin />
<!-- Dropin replacement for FontAwesome-->
<!-- <link
href="https://cdn.jsdelivr.net/npm/ficons@1.1.52/dist/ficons/font.css"
rel="stylesheet"
/> -->
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover" data-theme="wintry">
<div style="display: contents" class="h-full overflow-hidden">%sveltekit.body%</div>
</body>
<!doctype html>
<html lang="en">
<head prefix="og: https://ogp.me/ns#">
<meta charset="utf-8" />
<meta name="generator" content="gh:importantimport/urara" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="manifest" crossorigin="use-credentials" href="/manifest.webmanifest" />
<link rel="alternate" type="application/feed+json" href="/feed.json" />
<link rel="alternate" type="application/atom+xml" href="/atom.xml" />
<link rel="sitemap" type="application/xml" href="/sitemap.xml" />
%sveltekit.head%
</head>
<body itemscope itemtype="https://schema.org/WebPage">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

176
src/app.pcss Normal file
View File

@ -0,0 +1,176 @@
/* tailwind */
@tailwind base;
@tailwind components;
@tailwind utilities;
/* global */
html {
@apply !bg-base-200 scroll-smooth overflow-x-hidden overflow-y-scroll;
}
::selection {
@apply bg-primary/20;
}
/* .urara-prose */
.urara-prose {
@apply !max-w-none;
}
/* .urara-prose heading */
.urara-prose > :is(h1, h2, h3, h4, h5) > a {
@apply no-underline font-bold;
}
.urara-prose > :is(h1, h2, h3, h4, h5) > a::after {
@apply pl-2 text-base-200 transition-all content-['#'];
}
.urara-prose > :is(h1, h2, h3, h4, h5):hover > a::after {
@apply text-primary;
}
/* .urara-prose table */
.urara-prose div > table > thead {
@apply border-0;
}
.urara-prose div > table > thead > tr > th {
@apply !relative;
}
/* .urara-prose a */
.urara-prose :is(p, li) > a {
@apply bg-[length:100%_0.2em] hover:bg-[length:100%_100%] bg-[position:0_88%] bg-gradient-to-t from-secondary/50 to-secondary/40 bg-no-repeat transition-all ease-in-out !no-underline;
}
/* .urara-prose misc */
.urara-prose > p img {
@apply w-full;
}
.urara-prose :is(p, li) > code {
@apply bg-base-200 px-2;
}
.urara-prose li > input {
@apply checkbox checkbox-xs me-2 -my-0.5;
}
.urara-prose kbd {
@apply kbd;
}
.urara-prose hr {
@apply border-none divider;
}
/* footer a */
footer a {
@apply !no-underline hover:text-primary hover:!underline transition-all;
}
.spoiler {
@apply blur-sm hover:blur-none active:blur-none transition-all select-all;
}
/* .prose pre */
.prose pre {
@apply mockup-code !bg-neutral min-w-0;
}
.prose pre:not(.shiki) {
@apply bg-neutral text-neutral-content;
}
.prose pre:not(.shiki)::before {
@apply sticky -left-5 -ml-5;
}
/* .urara-prose pre */
@media (max-width: 768px) {
.urara-prose > pre {
@apply -mx-8 rounded-none;
}
}
.urara-prose > pre {
@apply pb-0;
}
.urara-prose > pre > div.code-container {
@apply pb-5 overflow-x-auto;
}
/* shiki */
pre.shiki {
@apply px-0;
}
pre.shiki::before {
@apply sticky;
}
pre.shiki > div.code-title {
@apply absolute -mt-10 ml-20 pt-1.5 pl-1.5 opacity-50;
}
pre.shiki .language-id {
@apply hidden;
}
pre.shiki > .code-container {
@apply overflow-auto;
}
:is(pre.shiki[text='true'], pre.shiki[svelte='true']) > div.code-container {
@apply mx-5;
}
pre.shiki:not([text='true'], [svelte='true']) > .code-container > code > div.line > span:first-child {
@apply pl-5;
}
pre.shiki:not([text='true'], [svelte='true']) > .code-container > code > div.line > span:last-child {
@apply pr-5;
}
pre.shiki div.dim {
@apply opacity-50 transition-opacity;
}
pre.shiki:hover div.dim {
@apply opacity-100;
}
pre.shiki div.highlight::before {
@apply bg-warning/20 absolute content-[''] w-full h-6;
}
pre.twoslash data-lsp {
@apply border-b border-dashed border-transparent transition-all;
}
pre.twoslash:hover data-lsp {
@apply border-neutral-content/30;
}
pre.twoslash data-lsp:hover::before {
/* https://daisyui.com/blog/how-to-update-daisyui-4/#3-all--focus-colors-are-removed */
@apply content-[attr(lsp)] absolute rounded translate-y-5 bg-[color-mix(in_oklab,oklch(var(--n)),black_7%)] text-neutral-content font-mono whitespace-pre-wrap transition-all px-2 py-1 z-50;
}
/* your code here */

View File

@ -1,53 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@tailwind variants;
html,
body {
@apply h-full overflow-hidden;
}
:root [data-theme='wintry'] {
}
.gradient-heading {
@apply bg-clip-text text-transparent box-decoration-clone;
/* Direction */
@apply bg-gradient-to-br;
/* Color Stops */
@apply from-primary-500 via-tertiary-500 to-secondary-500;
}
/* gold-nouveau theme */
@font-face {
font-family: 'Quicksand';
src: url('/fonts/Quicksand.ttf');
font-display: swap;
}
/*
@font-face {
font-family: 'Magilio';
src: url('/fonts/MagilioRegular.ttf') format('truetype');
font-weight: 900;
font-display: swap;
}
@font-face {
font-family: 'Satoshi-Variable';
src: url('/fonts/satoshi/Satoshi-Variable.woff2') format('woff2'),
url('/fonts/satoshi/Satoshi-Variable.woff') format('woff'),
url('/fonts/satoshi/Satoshi-Variable.ttf') format('truetype');
font-weight: 300 900;
font-display: swap;
font-style: normal;
}
@font-face {
font-family: 'Satoshi-VariableItalic';
src: url('/fonts/satoshi/Satoshi-VariableItalic.woff2') format('woff2'),
url('/fonts/satoshi/Satoshi-VariableItalic.woff') format('woff'),
url('/fonts/satoshi/Satoshi-VariableItalic.ttf') format('truetype');
font-weight: 300 900;
font-display: swap;
font-style: italic;
} */

View File

@ -1,32 +0,0 @@
import type { Post } from '$lib/types/post';
import type { MarkdownMetadata } from '$content/types';
import type { MdsvexImport } from './types';
import { parseReadContent } from '$content/utils';
import { error } from '@sveltejs/kit';
export function listPosts() {
const posts = import.meta.glob<Post>('./blog/*.md', {
eager: true,
import: 'metadata'
});
return parseReadContent(posts);
}
export async function getPostMetadata(slug: string) {
const { post } = await getPost(slug);
return post;
}
export async function getPost(slug: string) {
try {
const data: MdsvexImport<Post & MarkdownMetadata> = await import(`./blog/${slug}.md`);
return {
post: { ...data.metadata, slug },
Component: data.default
};
} catch {
throw error(404, `Unable to find blog post "${slug}"`);
}
}

View File

@ -1,64 +0,0 @@
<script lang="ts">
import { onMount } from 'svelte';
import { tweened } from 'svelte/motion';
import { quadInOut } from 'svelte/easing';
import { T } from '@threlte/core';
import { Align, Grid, OrbitControls } from '@threlte/extras';
import type { Contributions } from '$lib/types/contributions';
let contributions: Contributions[] = [];
onMount(async () => {
const response = await fetch('/api/matthieu42morin/2023');
contributions = await response.json();
console.log(contributions);
});
const colorMap = ['#0e0e0e', '#00442a', '#006d35', '#00a648', '#00d35c'];
// function to normalize the height of the cubes
function normalize(count: number, base = 4, offset = 6) {
switch (true) {
case count === 0:
return base;
case count > 40:
return count;
default:
return count * (base * offset);
}
}
// tweened value to animate the Z scale of the cubes
const scaleZ = tweened(0, { duration: 2000, easing: quadInOut });
onMount(() => {
$scaleZ = 1;
});
</script>
<T.PerspectiveCamera makeDefault position={[10, 100, 600]} fov={50}>
<OrbitControls enableDamping autoRotate />
</T.PerspectiveCamera>
<T.AmbientLight color="#fff" intensity={0.4} />
<T.DirectionalLight position={[0, 200, 200]} intensity={2} color="#fff" />
<T.DirectionalLight position={[0, 200, -200]} intensity={2} color="#fff" />
<Align auto>
<Grid infiniteGrid sectionColor="#4a4b4a" sectionSize={40} cellSize={40} fadeDistance={800} />
{#if Array.isArray(contributions) && contributions.length > 0}
{#each contributions as row, i}
{#each row as contribution, j}
{#if contribution !== null}
{@const z = normalize(contribution.level)}
<T.Group position={[0, 0, 12 * i]} scale.z={$scaleZ}>
<T.Mesh position={[12 * j, z / 2, 0]}>
<T.BoxGeometry args={[10, z, 10]} />
<T.MeshStandardMaterial color={colorMap[contribution.level]} />
</T.Mesh>
</T.Group>
{/if}
{/each}
{/each}
{/if}
</Align>

View File

@ -1,32 +0,0 @@
---
title: First post
excerpt: First post
date: 2021-01-01
tags:
- first
- post
published: true
image: Feature.jpg
---
## Svelte
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)
```

View File

@ -1,12 +0,0 @@
---
title: w post
excerpt: w post
date: 2021-01-01
tags:
- first
- post
published: true
image: Feature.jpg
---
## Svelte

View File

@ -1,27 +0,0 @@
---
title: Second post
excerpt: Second post
date: 2023-01-01
tags:
- first
- post
published: true
image: Feature.jpg
---
## Switching to Self-Hosted Git
You can check it out on git.mattmor.in, it's a simple encrypted gitea instance running on AWS
### Media
Media inside the **Svelte** folder is server from the `static` folder.
### Bye Bye Github with your fancy features
I am ditching the societal value of having a contributions table on my profile, you should view it on git.mattmor.in
--- If my contributions in 3d do not work, Github made breaking changes to their frontend and I can't scrape it anymore.

View File

@ -1,36 +0,0 @@
import type { MdsvexImport } from '$content/types';
import type { MarkdownMetadata } from '$content/types';
import type { Project } from '$lib/types/projects';
import { parseReadContent } from './utils';
import { error } from '@sveltejs/kit';
/**
* Gets all the projects metadata
*/
export function listProjects() {
const projects = import.meta.glob<Project>('./projects/*.md', {
eager: true,
import: 'metadata'
});
return parseReadContent(projects);
}
export async function getProjectMetadata(slug: string) {
const { post } = await getProject(slug);
return post;
}
export async function getProject(slug: string) {
try {
const data: MdsvexImport<Project & MarkdownMetadata> = await import(
`./projects/${slug}.md`
);
return {
post: { ...data.metadata, slug },
Component: data.default
};
} catch {
throw error(404, `Unable to find project "${slug}"`);
}
}

View File

@ -1,15 +0,0 @@
---
title: Erant
excerpt: A SaaS helping SMEs in the tourism sector with virtualization, customer experience & analytics. It got into the republic finale of Soutěž & Podnikej.
date: 2021-01-01
published: true
image: Feature.jpg
---
## Svelte
Media inside the **Svelte** folder is served from the `static` folder.
```python
```

View File

@ -1,27 +0,0 @@
---
title: Seedling
excerpt: An Iot Project, where we built a sensor system for plant care and accompanying app that alerted the user to what their plant needs. We made PCBs, industrial designs, 3D printed cases as clueless students.
date: 2021-01-01
published: true
image: Feature.jpg
---
This is a recollection of my first "startup" journey.
In 2020, after the onset of Covid, I remembered a presentation I heard from the founder of "Soutěž & Podnikej" Martin Vítek, on a meeting of the Prague Highschool Assembly.
It was an invitation to join their great program guiding highschoolers to launch an idea to financial fruition. The program, now sadly no longer operating, was inspirational to me as entrepreneurship and tech startups for me was a way to bring about a revolution, to solve a problem.
The journey that I embarked on, however, was not something I thought I signed up for.
I have not yet till that point in my life understood the deep rabbit hole of truly understanding a problem, the markets and people's needs, technical difficulties and the state of current technological developments, having the methodologies, networks of contacts and partners and capital at my disposal.
However what the program gave me was at least a brief outline of where to begin and how to progress. I began with a problem. As I was engaged in discussions and student organizations centered on ecology, especially thanks to being a part of many MUN and EYP conferences, I remembered a paper of the UN FAO called [2050: A third more mouths to feed](https://www.fao.org/newsroom/detail/2050-A-third-more-mouths-to-feed/), basically amongst the array of possibilities - water scarcity, food demand, population will increase
[Ben Einsteins LinkedIn post](https://www.linkedin.com/pulse/heres-why-juiceros-press-so-expensive-ben-einstein/) sums up the problem of Hardware startups
## Svelte
Media inside the **Svelte** folder is served from the `static` folder.
```python
```

View File

@ -1,139 +0,0 @@
export type SkillLevel = 'A' | 'B' | 'C'; // A: Proficient, B: Experienced, C: limited Experience
export interface Skill {
title: string;
level: SkillLevel;
}
export interface SkillSubCategory {
title: string;
level: number;
skills: Skill[];
}
export interface SkillCategory {
title: string;
level: number;
subCategories: SkillSubCategory[];
}
// prettier-ignore
const skillCategories: SkillCategory[] = [
{title:'Software Development', level: 70, subcategories: [
{title:'Programming Languages', level: 75, skills: [
{ title: 'JavaScript/TypeScript', level: 'A' },
{ title: 'Python', level: 'B' },
{ title: 'Rust', level: 'C' },
{ title: 'Bash', level: 'B' },
{ title: 'SQL', level: 'B' },
{ title: 'LaTeX', level: 'B' },
]},
{title:'Web Frameworks', level: 70, skills: [
{ title: 'Svelte(Kit)', level: 'A' },
{ title: 'React', level: 'C'},
]},
{title:'Configuration and Performance', level: 70, skills: [
{ title: 'SEO', level: 'B'},
{ title: 'Performance', level: 'B'},
{ title: 'Obfuscation', level: 'B'},
]},
{title:'Databases', level: 70, skills: [
{ title: 'PostgreSQL', level: 'A' },
{ title: 'MariaDB', level: 'B' },
{ title: 'MongoDB', level: 'C' }
]},
{title:'Testing & Validation', level: 50, skills: [
{ title: 'ajv', level: 'A' },
{ title: 'Playwright', level: 'B'},
{ title: 'SEO, performance optimizations', level: 'B'},
]},
]},
{title:'DevOps', level: 70, subcategories: [
{title:'Infrastructure & Configuration Management', level: 80, skills: [
{ title: 'Terraform & tooling', level: 'A'},
{ title: 'Ansible', level: 'A'},
{ title: 'Docker, Docker-Compose', level: 'A' },
{ title: 'Kubernetes', level: 'C'},
]},
{ title: 'Version Control & CI/CD', level: 90, skills: [
{ title: 'Git', level: 'A' },
{ title: 'GitHub Ecosystem', level: 'A' },
{ title: 'Gitea', level: 'A' },
{ title: 'Gitlab Ecosystem', level: 'B' }
]},
{ title: 'Monitoring & Observability ', level: 90, skills: [
{ title: 'Grafana', level: 'B' },
{ title: 'Prometheus', level: 'B' },
]},
{title:'Vercel', level: 100, skills: []},
]},
{title:'Cloud Computing', level: 70, subcategories: [
{title:'AWS', level: 80, skills: [
{ title: 'EC2', level: 'A' },
{ title: 'RDS', level: 'A'},
{ title: 'S3', level: 'A'},
{ title: 'CloudFormation', level: 'C'},
]},
{ title: 'Azure (C)', skills: [
{ title: 'Azure OpenAI API', level: 'B' },
{ title: 'AKS', level: 'B' }
]},
{ title: 'Hashicorp', skills: [
{ title: 'Vault', level: 'B' },
{ title: 'Consul', level: 'C' },
]},
{title:'Vercel', level: 100, skills: []},
{title:'DigitalOcean', level: 100, skills: []},
]},
{title:'System Administration', level: 75, subcategories: [
{title:'Operating Systems', level: 80, skills: [
{ title: 'Debian / Ubuntu', level: 'A' },
{ title: 'Nix(OS)', level: 'B' },
{ title: 'Arch Linux', level: 'B' },
{ title: 'Alpine', level: 'B' },
{ title: 'MicroSuck Winbloats', level: 'B' },
]},
{ title: 'Version Control & CI/CD', level: 90, skills: [
{ title: 'Git', level: 'A' },
{ title: 'GitHub & Gitea Ecosystem', level: 'A' },
{ title: 'Gitlab Ecosystem', level: 'B' }
]},
{ title: 'Monitoring & Observability ', level: 90, skills: [
{ title: 'Grafana', level: 'B' },
{ title: 'Prometheus', level: 'B' },
]},
{ title: 'Secrets Man. & Cryptography', level: 70, skills: [
{ title: 'Hashicorp Vault', level: 'A' },
{ title: 'Sops', level: 'B' },
{ title: 'AWS Secrets Manager', level: 'B' },
]}
]},
{title:"Some fun geek skillz", level: 70, subcategories: [
{title:'mini hardware', level: 80, skills: [
{ title: 'Raspberry Pi', level: 'A' },
{ title: 'ESP8266, ESP32', level: 'A' },
{ title: 'Arduino', level: 'A' },
{ title: 'MQTT', level: 'B' },
{ title: 'Wifi & BLE', level: 'B' }
]},
{ title: '3D printing', level: 90, skills: [
{ title: 'knowledge, theory', level: 'A' },
{ title: 'materials - PLA, PETG', level: 'A' },
{ title: 'Autodesk Inventor', level: 'B' },
{ title: 'OctoPrint and Klipper', level: 'B' },
{ title: 'Ultimaker Cura', level: 'B' },
]},
]},
{title:'Languages', level: 70, skills: [
{ title: 'English', level: 'A' },
{ title: 'Czech', level: 'A' },
{ title: 'French', level: 'B' },
{ title: 'German', level: 'C' }
]},
{title:'Design', level: 70, skills: [
{ title: 'Figma', level: 'A' },
{ title: 'UI/UX', level: 'B' },
{ title: 'LaTeX', level: 'C' },
{ title: 'Wireframing & Prototyping', level: 'B' },
{ title: 'Myriads of image manipulation and generation tools', level: 'B' },
]}
];
export default skillCategories;

View File

@ -1,16 +0,0 @@
export interface MarkdownHeading {
title: string;
slug: string;
level: number;
children: MarkdownHeading[];
}
export interface MarkdownMetadata {
headings: MarkdownHeading[];
}
export interface MdsvexImport<T extends MarkdownMetadata = MarkdownMetadata> {
// Technically not correct but needed to make language-tools happy
default: ConstructorOfATypedSvelteComponent;
metadata: T;
}

View File

@ -1,94 +0,0 @@
import type { create_ssr_component } from 'svelte/internal';
/**
* 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);
}
/**
* 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.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
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;
}
/**
* 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) {
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
}))
.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));
// }
// type DateStyle = Intl.DateTimeFormatOptions['dateStyle'];
/**
* 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) => {
try {
const d = new Date(date);
return `${d.toLocaleString('default', {
month: 'long'
})} ${d.getDate()}, ${d.getFullYear()}`;
} catch (e) {
return '';
}
};
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

@ -1,83 +0,0 @@
// https://gist.github.com/acoyfellow/d8e86979c66ebea25e1643594e38be73, Rodney Lab
import {
PUBLIC_DOMAIN,
PUBLIC_SENTRY_KEY,
PUBLIC_SENTRY_PROJECT_ID,
PUBLIC_SENTRY_ORG_ID,
PUBLIC_WORKER_URL
} from '$env/static/public';
export const rootDomain = PUBLIC_DOMAIN; // or your server IP for dev
const directives = {
'base-uri': ["'self'"],
'child-src': ["'self'", 'blob:'],
// 'connect-src': ["'self'", 'ws://localhost:*'],
'connect-src': [
"'self'",
'ws://localhost:*',
'https://*.sentry.io',
'https://hcaptcha.com',
'https://*.hcaptcha.com',
'https://*.cartocdn.com',
PUBLIC_DOMAIN,
PUBLIC_WORKER_URL
],
'img-src': ["'self'", 'data:', 'https://images.unsplash.com'],
'font-src': ["'self'", 'data:'],
'form-action': ["'self'"],
'frame-ancestors': ["'self'"],
'frame-src': [
"'self'",
// "https://*.stripe.com",
// "https://*.facebook.com",
// "https://*.facebook.net",
'https://hcaptcha.com',
'https://*.hcaptcha.com',
'https://www.openstreetmap.org',
'https://*.cartocdn.com'
],
'manifest-src': ["'self'"],
'media-src': ["'self'", 'data:'],
'object-src': ["'none'"],
// 'style-src': ["'self'", "'unsafe-inline'"],
'style-src': ["'self'", "'unsafe-inline'", 'https://hcaptcha.com', 'https://*.hcaptcha.com'],
'default-src': [
"'self'",
rootDomain,
`ws://${rootDomain}`,
// 'https://*.google.com',
// 'https://*.googleapis.com',
// 'https://*.firebase.com',
// 'https://*.gstatic.com',
// 'https://*.cloudfunctions.net',
// 'https://*.algolia.net',
// 'https://*.facebook.com',
// 'https://*.facebook.net',
// 'https://*.stripe.com',
'https://*.sentry.io'
],
'script-src': [
"'self'",
"'unsafe-inline'",
// 'https://*.stripe.com',
// 'https://*.facebook.com',
// 'https://*.facebook.net',
'https://hcaptcha.com',
'https://*.hcaptcha.com',
'https://*.sentry.io',
// 'https://polyfill.io',
'https://*.cartocdn.com'
],
'worker-src': ["'self'", 'blob:'],
//report-to can throw "Content-Security-Policy: Couldnt process unknown directive report-to", leave it for older browsers.
'report-to': ["'csp-endpoint'"],
'report-uri': [
`https://${PUBLIC_SENTRY_ORG_ID}.ingest.us.sentry.io/api/${PUBLIC_SENTRY_PROJECT_ID}/security/?sentry_key=${PUBLIC_SENTRY_KEY}`
]
};
export const csp = Object.entries(directives)
.map(([key, arr]) => key + ' ' + arr.join(' '))
.join('; ');

View File

@ -1,26 +0,0 @@
import { handleErrorWithSentry, replayIntegration } from '@sentry/sveltekit';
import * as Sentry from '@sentry/sveltekit';
import {
PUBLIC_SENTRY_KEY,
PUBLIC_SENTRY_PROJECT_ID,
PUBLIC_SENTRY_ORG_ID
} from '$env/static/public';
Sentry.init({
dsn: `https://${PUBLIC_SENTRY_KEY}@${PUBLIC_SENTRY_ORG_ID}.ingest.us.sentry.io/${PUBLIC_SENTRY_PROJECT_ID}`,
tracesSampleRate: 1.0,
// This sets the sample rate to be 10%. You may want this to be 100% while
// in development and sample at a lower rate in production
replaysSessionSampleRate: 0.1,
// If the entire session is not sampled, use the below sample rate to sample
// sessions when an error occurs.
replaysOnErrorSampleRate: 1.0,
// If you don't want to use Session Replay, just remove the line below:
integrations: [replayIntegration()]
});
// If you have a custom error handler, pass it to `handleErrorWithSentry`
export const handleError = handleErrorWithSentry();

View File

@ -1,52 +1,7 @@
import type { Handle } from '@sveltejs/kit';
import { sequence } from '@sveltejs/kit/hooks';
import type { Handle } from '@sveltejs/kit'
import { site } from '$lib/config/site'
import { handleErrorWithSentry, sentryHandle } from '@sentry/sveltekit';
import * as Sentry from '@sentry/sveltekit';
import {
PUBLIC_SENTRY_KEY,
PUBLIC_SENTRY_PROJECT_ID,
PUBLIC_SENTRY_ORG_ID
} from '$env/static/public';
import { csp, rootDomain } from './cspDirectives';
Sentry.init({
dsn: `https://${PUBLIC_SENTRY_KEY}@${PUBLIC_SENTRY_ORG_ID}.ingest.us.sentry.io/${PUBLIC_SENTRY_PROJECT_ID}`,
tracesSampleRate: 1.0
});
export const cspHandle: Handle = async ({ event, resolve }) => {
if (!csp) {
throw new Error('csp is undefined');
}
const response = await resolve(event);
// Permission fullscreen necessary for maps fullscreen
const headers = {
'X-Frame-Options': 'SAMEORIGIN',
'Referrer-Policy': 'no-referrer',
'Permissions-Policy': `accelerometer=(), autoplay=(), camera=(), document-domain=(self, 'js-profiling'), encrypted-media=(), fullscreen=(self ${rootDomain}), gyroscope=(), interest-cohort=(), magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), sync-xhr=(), usb=(), xr-spatial-tracking=(), geolocation=()`,
'X-Content-Type-Options': 'nosniff',
// 'Content-Security-Policy-Report-Only': csp,
'Content-Security-Policy': csp,
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains; preload',
'Expect-CT': `max-age=86400, report-uri="https://${PUBLIC_SENTRY_ORG_ID}.ingest.us.sentry.io/api/${PUBLIC_SENTRY_PROJECT_ID}/security/?sentry_key=${PUBLIC_SENTRY_KEY}"`,
'Report-To': `{group: "csp-endpoint", "max_age": 10886400, "endpoints": [{"url": "https://${PUBLIC_SENTRY_ORG_ID}.ingest.us.sentry.io/api/${PUBLIC_SENTRY_PROJECT_ID}/security/?sentry_key=${PUBLIC_SENTRY_KEY}"}]}`
};
Object.entries(headers).forEach(([key, value]) => {
response.headers.set(key, value);
});
return response;
};
// If you have custom handlers, make sure to place them after `sentryHandle()` in the `sequence` function.
export const handle: Handle = sequence(sentryHandle(), cspHandle);
// If you have a custom error handler, pass it to `handleErrorWithSentry`
export const handleError = handleErrorWithSentry();
// https://gist.github.com/acoyfellow/d8e86979c66ebea25e1643594e38be73
// https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
// https://scotthelme.co.uk/content-security-policy-an-introduction/
// scanner: https://securityheaders.com/
export const handle: Handle = async ({ event, resolve }) =>
await resolve(event, {
transformPageChunk: ({ html }) => html.replace('<html lang="en">', `<html lang="${site.lang ?? 'en'}">`)
})

View File

@ -1,7 +0,0 @@
import { describe, it, expect } from 'vitest';
describe('sum test', () => {
it('adds 1 + 2 to equal 3', () => {
expect(1 + 2).toBe(3);
});
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 246 KiB

View File

@ -1,143 +0,0 @@
/**
* atom-dark theme for `prism.js`
* Based on Atom's `atom-dark` theme: https://github.com/atom/atom-dark-syntax
* @author Joe Gibson (@gibsjose)
*/
code[class*="language-"],
pre[class*="language-"] {
color: #c5c8c6;
text-shadow: 0 1px rgba(0, 0, 0, 0.3);
font-family: Inconsolata, Monaco, Consolas, 'Courier New', Courier, monospace;
direction: ltr;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: 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: #1d1f21;
}
/* Inline code */
:not(pre) > code[class*="language-"] {
padding: .1em;
border-radius: .3em;
}
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: #7C7C7C;
}
.token.punctuation {
color: #c5c8c6;
}
.namespace {
opacity: .7;
}
.token.property,
.token.keyword,
.token.tag {
color: #96CBFE;
}
.token.class-name {
color: #FFFFB6;
text-decoration: underline;
}
.token.boolean,
.token.constant {
color: #99CC99;
}
.token.symbol,
.token.deleted {
color: #f92672;
}
.token.number {
color: #FF73FD;
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.inserted {
color: #A8FF60;
}
.token.variable {
color: #C6C5FE;
}
.token.operator {
color: #EDEDED;
}
.token.entity {
color: #FFFFB6;
cursor: help;
}
.token.url {
color: #96CBFE;
}
.language-css .token.string,
.style .token.string {
color: #87C38A;
}
.token.atrule,
.token.attr-value {
color: #F9EE98;
}
.token.function {
color: #DAD085;
}
.token.regex {
color: #E9C062;
}
.token.important {
color: #fd971f;
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}

View File

@ -1,124 +0,0 @@
/**
* 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

@ -1,18 +0,0 @@
<script lang="ts">
import skills from '$content/skills';
</script>
<div
class="card flex flex-col items-center justify-center mx-auto w-3/4 bg-surface-50 bg-opacity-50 p-8 m-8"
id="skills"
>
<h2 class="h2 m-2">My skillset</h2>
{#each skills as skill}
<div class="text-lg font-bold m-2">{skill.title}</div>
<div class="flex flex-wrap justify-center space-x-2 m-2">
{#each skill.list as s}
<div class="chip variant-outline-primary">{s}</div>
{/each}
</div>
{/each}
</div>

View File

@ -1,25 +0,0 @@
<script lang="ts">
import MatrixLogo from './logos/MatrixLogo.svelte';
import GiteaLogo from './logos/GiteaLogo.svelte';
import { socialLinks } from '$lib/config';
</script>
<div class="flex flex-rows-auto gap-1 max-h-28">
{#each socialLinks as link}
<a
class="logo-item"
href={link.href}
target="_blank"
rel={link.title === 'Mastodon' ? 'me' : 'noreferrer'}
aria-label={link.title}
>
{#if link.title === 'Gitea'}
<GiteaLogo />
{:else if link.title === 'Matrix'}
<MatrixLogo />
{:else}
<i class={link.icon + ' text-3xl md:text-5xl'} />
{/if}
</a>
{/each}
</div>

View File

@ -1,29 +0,0 @@
<script lang="ts">
import { twMerge } from 'tailwind-merge';
export let src: string;
export let type: string = 'video/mp4';
export let trackSrc: string = '';
export let srclang: string = 'en';
export let label: string = 'english_captions';
let videoClass = twMerge($$props.class);
</script>
<video {...$$restProps} class={videoClass}>
<source {src} {type} />
<slot />
<track src={trackSrc} kind="captions" {srclang} {label} />
Your browser does not support the video tag.
</video>
<!--
@component
[Go to docs](https://flowbite-svelte.com/)
## Props
@prop export let src: string;
@prop export let type: string = 'video/mp4';
@prop export let trackSrc: string = '';
@prop export let srclang: string = 'en';
@prop export let label: string = 'english_captions';
-->

View File

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

View File

@ -1,40 +0,0 @@
<script lang="ts">
import type { Tag } from '$lib/types/post';
export let selected: Tag | null = null;
let className = '';
export { className as class };
import { page } from '$app/stores';
import { goto } from '$app/navigation';
let options: Tag[] = ['DevOps', 'Philosophy', 'Updates'];
const clickHandler = (value: Tag) => {
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}">
<h3 class="h3 mb-2 md:mb-3">Sort by category</h3>
<ul class="flex flex-wrap justify-center gap-2">
{#each options as option}
<li>
<button
class="chip {option === selected
? 'variant-filled-primary'
: 'variant-soft-primary'}"
on:click={() => clickHandler(option)}
>
{option}
</button>
</li>
{/each}
</ul>
</section>

View File

@ -1,63 +0,0 @@
<script lang="ts">
import { formatDate } from '$content/utils';
import '$lib/assets/prism-nord.css';
export let imagesDirectoryName: string;
export let excerpt: string = '';
export let date: string = '';
export let slug: string = '';
export let title: string;
export let image: string;
export let tags: string[] = [];
export let type: 'blog' | 'projects';
</script>
<svelte:head>
<title>{title}</title>
<meta name="description" content={excerpt} />
<meta property="og:title" content={title} />
<meta property="og:type" content="article" />
<meta property="og:description" content={excerpt} />
<meta property="og:url" content="https://mattmor.in/{slug}" />
<meta property="og:image" content={image} />
<meta property="og:type" content="article:published_time" />
<meta property="article:published_time" content={date} />
{#each tags as tag (tag)}
<meta property="article:tag" content={tag} />
{/each}
</svelte:head>
<article class="flex justify-center mt-4 mb-8">
<div class=" w-full md:w-[50rem] leading-[177.7%]">
<header>
<img
src="/images/{imagesDirectoryName}/{slug}/{image}"
alt={`${title}`}
class=" bg-black/50 w-full aspect-[21/9] max-h-[540px] rounded-t-lg"
/>
</header>
<div class="flex-auto flex justify-between items-center py-4 px-2 bg-surface-900">
{#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>
<div class="space-y-4">
<h2 class="h2" data-toc-ignore>{title}</h2>
<div class="max-w-none text-token">
<slot />
</div>
</div>
<hr class="opacity-50" />
<footer class="p-4 flex justify-start items-center space-x-4" />
</div>
</article>

View File

@ -1,101 +0,0 @@
<script lang="ts">
import { isAnExternalLink } from '$lib/utils/helpers';
import type { Post } from '$lib/types/post';
import { onMount, onDestroy } from 'svelte';
export let isMostRecent: boolean = false;
export let type: Post['type'] = 'blog' | 'projects';
export let post: Post;
// export let published: boolean;
// export let headlineOrder: 'h3' | '' = '';
// export let badge: string = '';
// export let textWidth: string = '';
//window width
let iteration = 0;
let interval;
onMount(() => {
const interval = setInterval(() => {
console.log(window.innerWidth);
iteration++;
if (iteration === 50) {
clearInterval(interval);
}
}, 1000);
});
const generateURL = (href?: string, slug?: string) => {
if (href) return href;
return `/${type}/${slug}`;
};
$: href = generateURL(post['href'], post.slug);
$: 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'
});
onDestroy(() => {
clearInterval(interval);
});
</script>
<a
{href}
{target}
class="card bg-gradient-to-br variant-glass-primary card-hover overflow-hidden flex flex-col space-y-4"
>
<!-- Blog in long cols, projects in wide rows -->
<div class="flex {type === 'blog' ? 'flex-col' : 'flex-row'} justify-between w-full h-full">
<header>
<img
src={`/images/${type}/${post.slug}/${post.image}`}
class="bg-black/200 w-full aspect-[3/2]"
alt="Post preview"
/>
</header>
<section class="p-4 space-y-4">
<h2 class="h2 text-ellipsis overflow-hidden" data-toc-ignore>{post.title}</h2>
<article class="text-ellipsis break-words overflow-hidden max-h-[128px] max-w-4">
<p>
<!-- cspell:disable -->
{post.excerpt}
<!-- cspell:enable -->
</p>
</article>
</section>
<section>
<hr class="opacity-30 bg-tertiary-500" />
<footer class="p-4 flex justify-between">
<div class="flex flex-wrap gap-2">
{#if post.tags && post.tags.length > 0}
<small>tags: </small>
{#each post.tags as tag}
<a
data-sveltekit-preload-data="hover"
href="/blog?{new URLSearchParams({ tag }).toString()}"
class="chip variant-glass-secondary"
>
<p class="text-md text-token">{tag}</p>
</a>
{/each}
{/if}
</div>
<div class="mt-auto">
<small>
{#if post.date}
<span class="text-sm ml-4">
{displayDate}
</span>
{/if}
</small>
</div>
</footer>
</section>
</div>
</a>

View File

@ -0,0 +1,40 @@
<script lang="ts">
import { onMount } from 'svelte'
import { site } from '$lib/config/site'
import type { GiscusConfig } from '$lib/types/post'
export let config: GiscusConfig
onMount(() => {
const giscus = document.createElement('script')
Object.entries({
src: config.src ?? 'https://giscus.app/client.js',
'data-repo': config.repo,
'data-repo-id': config.repoID,
'data-category': config.category ?? '',
'data-category-id': config.categoryID,
'data-mapping': 'pathname',
'data-reactions-enabled': config.reactionsEnabled === false ? '0' : '1',
'data-input-position': config.inputPosition ?? 'bottom',
'data-theme': config.theme ?? 'preferred_color_scheme',
'data-lang': config.lang ?? site.lang ?? 'en',
'data-loading': config.loading ?? '',
crossorigin: 'anonymous',
async: ''
}).forEach(([key, value]) => giscus.setAttribute(key, value))
setTimeout(() => {
const observer = new MutationObserver(() => {
document.getElementById('giscus-loading')!.remove()
observer.disconnect()
})
observer.observe(document.getElementById('giscus')!, {
childList: true
})
document.getElementById('giscus-container')!.appendChild(giscus)
}, 1000)
})
</script>
<div id="giscus-container">
<button id="giscus-loading" class="btn btn-lg flex mx-auto my-4 btn-ghost btn-circle loading" />
<div id="giscus" class="giscus" />
</div>

View File

@ -0,0 +1,63 @@
<script lang="ts">
import { onMount, onDestroy } from 'svelte'
import type { Remark42Config } from '$lib/types/post'
export let post: Urara.Post
export let config: Remark42Config
let remark42Instance: any
onMount(() => {
const [c, s] = [document.createElement('script'), document.createElement('script')]
c.id = 'remark42_config'
c.type = 'application/javascript'
c.innerHTML = `
var remark_config = {
host: '${config.host}',
site_id: '${config.site_id || 'remark'}',
url: '${post.path}',
components: [${config.components || "'embed'"}],
max_shown_comments: ${config.max_shown_comments || 15},
max_last_comments: ${config.max_last_comments || 15},
theme: '${config.theme || 'light'}',
page_title: '${config.page_title || post.title}',
locale: '${config.locale || 'en'}',
show_email_subscription: ${config.show_email_subscription || true},
show_rss_subscription: ${config.show_rss_subscription || true},
simple_view: ${config.simple_view || false},
no_footer: ${config.no_footer || false},
}`
s.id = 'remark42_script'
s.type = 'application/javascript'
s.innerHTML = `!function(e,n){for(var o=0;o<e.length;o++){var r=n.createElement("script"),c=".js",d=n.head||n.body;"noModule"in r?(r.type="module",c=".mjs"):r.async=!0,r.defer=!0,r.src='${config.host}/web/'+e[o]+c,d.appendChild(r)}}(remark_config.components||["embed"],document);`
document.head.appendChild(c)
document.head.appendChild(s)
const opt = {
...config,
url: post.path
}
const checkRemark42 = () => {
if ((window as any).REMARK42) {
remark42Instance = (window as any).REMARK42.createInstance({
node: document.getElementById('remark42') as HTMLElement,
...opt
})
} else {
setTimeout(checkRemark42, 100)
}
}
checkRemark42()
})
onDestroy(() => {
if (remark42Instance && typeof remark42Instance.destroy === 'function') {
remark42Instance.destroy()
}
})
</script>
<div id="remark42" />

View File

@ -0,0 +1,33 @@
<script lang="ts">
import { onMount } from 'svelte'
import type { UtterancesConfig } from '$lib/types/post'
export let config: UtterancesConfig
onMount(() => {
const utterances = document.createElement('script')
Object.entries({
src: config.src ?? 'https://utteranc.es/client.js',
repo: config.repo,
'issue-term': 'pathname',
label: config.label ?? '',
theme: config.theme ?? 'preferred-color-scheme',
crossorigin: 'anonymous',
async: ''
}).forEach(([key, value]) => utterances.setAttribute(key, value))
setTimeout(() => {
const observer = new MutationObserver(() => {
document.getElementById('utterances-loading')!.remove()
observer.disconnect()
})
observer.observe(document.getElementById('utterances')!, {
childList: true
})
document.getElementById('utterances-container')!.appendChild(utterances)
}, 1000)
})
</script>
<div id="utterances-container">
<button id="utterances-loading" class="btn btn-lg flex mx-auto my-4 btn-ghost btn-circle loading" />
<div id="utterances" class="utterances" />
</div>

View File

@ -0,0 +1,204 @@
<script lang="ts">
import { onMount } from 'svelte'
import { site } from '$lib/config/site'
import type { WebmentionConfig } from '$lib/types/post'
export let config: WebmentionConfig
export let post: Urara.Post
interface WebmentionFeed {
type: 'feed'
name: 'Webmentions'
children: WebmentionEntry[]
}
interface WebmentionEntry {
url: string
author?: {
name?: string
photo?: string
url?: string
}
content?: {
html?: string
text?: string
}
rsvp?: string
published?: string
'wm-received': string
'wm-source': string
'wm-target': string
'wm-id': number
'wm-property': 'in-reply-to' | 'like-of' | 'repost-of' | 'bookmark-of' | 'mention-of' | 'rsvp'
'wm-private': boolean
}
let [page, loaded, end, mentions, sortDirUp]: [number, boolean, boolean, WebmentionEntry[], boolean] = [
0,
false,
false,
[],
config?.sortDir === 'up' ? true : false
]
const load = async () =>
await fetch(
`https://webmention.io/api/mentions.jf2?page=${page}&per-page=${config?.perPage ?? '20'}&sort-by=${
config?.sortBy ?? 'created'
}&sort-dir=${sortDirUp ? 'up' : 'down'}${
config?.property && config.property.forEach(wmProperty => `&wm-property=${wmProperty}`)
}&target[]=${site.protocol + site.domain + post.path}&target[]=${site.protocol + site.domain + post.path}/`
)
.then(res => res.json())
.then((feed: WebmentionFeed) => {
if (feed.children.length < 10) end = true
feed = {
...feed,
children: feed.children.filter(
(entry: WebmentionEntry) => !config.blockList?.includes(new URL(entry['wm-source']).origin)
)
}
if (feed.children.length > 0) mentions = [...mentions, ...feed.children]
page++
loaded = true
})
const reset = async () => {
page = 0
loaded = false
end = false
mentions = []
await load()
}
onMount(() => load())
</script>
<div class="flex flex-col gap-8">
<div class="flex">
<p class="flex-1 m-auto italic opacity-50">
{`sort-by=${config?.sortBy ?? 'created'}&sort-dir=${sortDirUp ? 'up' : 'down'}`}
</p>
<button
class="btn btn-ghost btn-sm float-right"
on:click={() => {
sortDirUp = !sortDirUp
reset()
}}>
{#if sortDirUp === true}
<span class="i-heroicons-outline-sort-ascending" />
{:else}
<span class="i-heroicons-outline-sort-descending" />
{/if}
</button>
</div>
{#key mentions}
{#each mentions as mention}
{@const [wmProperty, borderColor, textColor, tooltipColor] = {
'in-reply-to': ['💬 replied', 'border-primary/50', 'text-primary', 'tooltip-primary'],
'like-of': ['❤️ liked', 'border-secondary/50', 'text-secondary', 'tooltip-secondary'],
'repost-of': ['🔄 reposted', 'border-accent/50', 'text-accent', 'tooltip-accent'],
'bookmark-of': ['⭐️ bookmarked', 'border-neutral/50', 'text-neutral', 'tooltip-neutral'],
'mention-of': ['💬 mentioned', 'border-base-300/50', 'text-base-content', 'tooltip-base-content'],
rsvp: [
`📅 RSVPed ${
mention.rsvp &&
{
yes: '✅',
no: '❌',
interested: '💡',
maybe: '💭'
}[mention.rsvp]
}`,
'border-warning/50',
'text-warning',
'tooltip-warning'
]
}[mention['wm-property']]}
{#if mention.url !== null}
<div class="{borderColor} border-2 rounded-box p-4">
<div class="flex bg-base-200 rounded-btn">
{#if mention?.author?.photo}
<img
class="w-12 h-12 flex-none rounded-btn"
src={mention.author.photo}
alt={mention.author?.name ?? new URL(mention.url).host}
loading="lazy"
decoding="async" />
{/if}
<div class="flex-1 px-4 py-2 m-auto">
<p>
{#if mention?.author?.url}
<a class="font-semibold {textColor} hover:underline" href={mention.author.url}>
{mention.author?.name ?? new URL(mention.url).host}
</a>
{:else}
{mention?.author?.name ?? new URL(mention.url).host}
{/if}
<a class="{textColor} hover:underline" href={mention['wm-source']}>
{wmProperty}
</a>
this post on
<span
class="tooltip tooltip-bottom xl:tooltip-right {tooltipColor}"
data-tip={new Date(mention.published ?? mention['wm-received']).toLocaleString()}>
{mention.published ? mention.published.slice(0, 10) : mention['wm-received'].slice(0, 10)}
</span>
</p>
</div>
</div>
{#if mention.content}
<div class="prose max-w-none break-words mt-4">
<p>{@html mention.content?.html ?? mention.content?.text}</p>
</div>
{/if}
</div>
{/if}
{/each}
{/key}
{#if loaded === true}
{#if end !== true}
<button
on:click={() => {
loaded = false
load()
}}
class="btn btn-primary btn-block">
LOAD
</button>
{:else if config?.form !== true}
<div class="divider mt-0 -mb-2">END</div>
{/if}
{:else}
<button id="webmention-loading" class="btn btn-lg btn-block flex btn-ghost loading" />
{/if}
{#if config?.form === true}
<form id="webmention-form" method="post" action="https://webmention.io/{config.username}/webmention">
<input type="hidden" name="target" value={site.protocol + site.domain + post.path} />
<div class="label gap-4">
<span class="label-text">send webmentions here:</span>
{#if config?.commentParade === true}
<span class="label-text-alt text-right">
or <a
class="hover:!text-primary"
href="https://quill.p3k.io/?dontask=1&me=https://commentpara.de/&reply={encodeURI(
site.protocol + site.domain + post.path
)}">
comment anonymously
</a>
</span>
{/if}
</div>
<div class="flex gap-2">
<div class="flex-1">
<input
class="input input-bordered focus:input-primary w-full"
type="text"
id="reply-url"
name="source"
placeholder="https://example.com/my-post" />
</div>
<button class="btn btn-primary flex-none mt-auto" type="submit" id="webmention-submit">Send</button>
</div>
</form>
{/if}
</div>

View File

@ -0,0 +1,35 @@
<script lang="ts">
export let title: string | undefined = undefined
export let description: string | undefined = undefined
export let status: 'info' | 'success' | 'warning' | 'error' | undefined = undefined
</script>
<div
class:alert-info={status === 'info'}
class:alert-success={status === 'success'}
class:alert-warning={status === 'warning'}
class:alert-error={status === 'error'}
class="alert flex-col shadow-inner my-4">
<div class="mr-auto">
{#if status === 'success'}
<span class="i-heroicons-outline-check-circle" />
{:else if status === 'warning'}
<span class="i-heroicons-outline-exclamation-circle" />
{:else if status === 'error'}
<span class="i-heroicons-outline-x-circle" />
{:else}
<span class="i-heroicons-outline-information-circle" />
{/if}
<div>
<div class:font-bold={description}>{title}</div>
{#if description}
<div class="text-xs">{description}</div>
{/if}
</div>
</div>
{#if $$slots.default}
<div class="block w-full">
<slot />
</div>
{/if}
</div>

View File

@ -0,0 +1,46 @@
<script lang="ts">
import { site } from '$lib/config/site'
import { footer as footerConfig } from '$lib/config/general'
let className: string | undefined = undefined
export { className as class }
export let sticky: boolean = false
export let rounded: boolean = false
</script>
<footer
id="footer"
class="footer footer-center bg-base-300 text-base-content shadow-inner p-8 {rounded
? 'rounded-box'
: 'md:rounded-box'} {sticky ? 'sticky bottom-0 z-0 md:static' : ''} {className ?? ''}">
<div class="prose">
<p>
{#if footerConfig.nav}
{#each footerConfig.nav as { text, link }, i}
<a href={link} rel="noopener noreferrer external" target="_blank">{text}</a>
{#if i + 1 < footerConfig.nav.length}
<span class="mr-1">·</span>
{/if}
{/each}
<br />
{/if}
Copyright © {footerConfig.since && footerConfig.since !== new Date().toJSON().substring(0, 4)
? `${footerConfig.since} - ${new Date().toJSON().substring(0, 4)}`
: new Date().toJSON().substring(0, 4)}
{site.author.name}
<br />
Powered by
<a
rel="noopener noreferrer external"
target="_blank"
class="tooltip tooltip-secondary hover:text-secondary"
data-tip="🌸 [δ] - Based on MDsveX & SvelteKit 🌸"
href="https://github.com/importantimport/urara">
Urara
</a>
{#if footerConfig.html}
<br />
{@html footerConfig.html}
{/if}
</p>
</div>
</footer>

View File

@ -0,0 +1,39 @@
<script lang="ts">
import { dev } from '$app/environment'
import { head } from '$lib/config/general'
import { site } from '$lib/config/site'
import OpenGraph from '$lib/components/head_opengraph.svelte'
export let post: Urara.Post | undefined = undefined
export let page: Urara.Page | undefined = undefined
</script>
<svelte:head>
<meta name="author" content={site.author?.name} />
{#if post}
<link rel="canonical" href={site.protocol + site.domain + post.path} />
{#if post.type === 'article'}
<title>{post.title} | {site.title}</title>
{:else if post.type === 'note'}
<title>{post.summary ?? post.path.slice(1)} | {site.title}</title>
{/if}
{#if post.tags}<meta name="keywords" content={post.tags.join(', ')} />{/if}
{#if post.summary}<meta name="description" content={post.summary} />{/if}
{:else}
<meta name="description" content={site.description} />
<meta name="keywords" content={site.keywords?.join(', ')} />
{#if page}
<title>{page.title ?? page.path.slice(1)} | {site.title}</title>
<link rel="canonical" href={site.protocol + site.domain + page.path} />
{:else}
<title>{site.subtitle ? `${site.title} - ${site.subtitle}` : site.title}</title>
<link rel="canonical" href={site.protocol + site.domain} />
{/if}
{/if}
{#if head.custom}
{#each head.custom({ dev, post, page }) as tag}
{@html tag}
{/each}
{/if}
</svelte:head>
<OpenGraph {post} {page} />

View File

@ -0,0 +1,15 @@
<script lang="ts">
import { favicon, any } from '$lib/config/icon'
</script>
<svelte:head>
{#if favicon}
<link rel="shortcut icon" href={favicon.src} sizes={favicon.sizes} type={favicon.type} />
{/if}
{#if any['180']}
<link rel="apple-touch-icon" href={any['180'].src} sizes={any['180'].sizes} type={any['180'].type} />
{/if}
{#if any['192']}
<link rel="icon" href={any['192'].src} sizes={any['192'].sizes} type={any['192'].type} />
{/if}
</svelte:head>

View File

@ -0,0 +1,45 @@
<script lang="ts">
import { site } from '$lib/config/site'
import { any, maskable } from '$lib/config/icon'
export let post: Urara.Post | undefined = undefined
export let page: Urara.Page | undefined = undefined
</script>
<svelte:head>
<meta property="og:site_name" content={site.title} />
<meta property="og:locale" content={site.lang} />
{#if post}
<meta property="og:type" content="article" />
<meta property="og:title" content={post.title ?? post.summary ?? post.path.slice(1)} />
{#if post.summary}
<meta property="og:description" content={post.summary} />
{/if}
{#if post.image}
<meta property="og:image" content={(post.image.startsWith('http') ? '' : site.protocol + site.domain) + post.image} />
<meta name="twitter:card" content="summary_large_image" />
{:else}
<meta property="og:image" content={maskable['512'].src ?? any['512'].src ?? any['192'].src} />
<meta name="twitter:card" content="summary" />
{/if}
{#if post.tags}
{#each post.tags as tag}
<meta property="article:tag" content={tag} />
{/each}
{/if}
<meta property="og:url" content={site.protocol + site.domain + post.path} />
<meta property="article:author" content={site.author.name} />
<meta property="article:published_time" content={post.published ?? post.created} />
<meta property="article:modified_time" content={post.updated ?? post.published ?? post.created} />
{:else}
<meta property="og:type" content="website" />
<meta property="og:image" content={maskable['512'].src ?? any['512'].src ?? any['192'].src} />
<meta property="og:description" content={site.description} />
{#if page}
<meta property="og:title" content={page.title ?? page.path.slice(1)} />
<meta property="og:url" content={site.protocol + site.domain + page.path} />
{:else}
<meta property="og:title" content={site.title} />
<meta property="og:url" content={site.protocol + site.domain} />
{/if}
{/if}
</svelte:head>

View File

@ -0,0 +1,19 @@
<script lang="ts">
import { head } from '$lib/config/general'
import { post } from '$lib/config/post'
import Icon from '$lib/components/head_icon.svelte'
</script>
<svelte:head>
{#if head.me}
{#each head.me as href}
<link rel="me" {href} />
{/each}
{/if}
{#if post.comment?.webmention?.username}
<link rel="webmention" href="https://webmention.io/{post.comment.webmention.username}/webmention" />
<link rel="pingback" href="https://webmention.io/{post.comment.webmention.username}/xmlrpc" />
{/if}
</svelte:head>
<Icon />

View File

@ -0,0 +1,138 @@
<script lang="ts">
import { browser, dev } from '$app/environment'
import { fly } from 'svelte/transition'
import { site } from '$lib/config/site'
import { theme } from '$lib/config/general'
import { title as storedTitle } from '$lib/stores/title'
import { header as headerConfig } from '$lib/config/general'
import { hslToHex } from '$lib/utils/color'
import Nav from '$lib/components/header_nav.svelte'
import Search from '$lib/components/header_search.svelte'
export let path: string
let title: string
let currentTheme: string
let currentThemeColor: string
let search: boolean = false
let pin: boolean = true
let percent: number
let [scrollY, lastY] = [0, 0]
storedTitle.subscribe(storedTitle => (title = storedTitle as string))
$: if (browser && currentTheme) {
document.documentElement.setAttribute('data-theme', currentTheme)
currentThemeColor = hslToHex(
...(getComputedStyle(document.documentElement)
.getPropertyValue('--b1')
.slice(dev ? 1 : 0)
.replaceAll('%', '')
.split(' ')
.map(Number) as [number, number, number])
)
}
$: if (scrollY) {
pin = lastY - scrollY > 0 || scrollY === 0 ? true : false
lastY = scrollY
if (browser)
percent =
Math.round((scrollY / (document.documentElement.scrollHeight - document.documentElement.clientHeight)) * 10000) / 100
}
if (browser)
currentTheme =
localStorage.getItem('theme') ??
(window.matchMedia('(prefers-color-scheme: dark)').matches ? theme?.[1].name : theme[0].name ?? theme[0].name)
</script>
<svelte:head>
<meta name="theme-color" content={currentThemeColor} />
</svelte:head>
<svelte:window bind:scrollY />
<header
id="header"
class:-translate-y-32={!pin && scrollY > 0}
class="fixed z-50 w-full transition-all duration-500 ease-in-out border-b-2 border-transparent max-h-[4.125rem] {scrollY >
32 && 'backdrop-blur !border-base-content/10 bg-base-100/30 md:bg-base-200/30'}">
{#if !search}
<div in:fly={{ x: -50, duration: 300, delay: 300 }} out:fly={{ x: -50, duration: 300 }} class="navbar">
<div class="navbar-start">
{#if headerConfig.nav}
<Nav {path} {title} {pin} {scrollY} nav={headerConfig.nav} />
{/if}
<a href="/" class="btn btn-ghost normal-case text-lg">{site.title}</a>
</div>
<div class="navbar-end">
{#if headerConfig.search}
<button aria-label="search" on:click={() => (search = !search)} tabindex="0" class="btn btn-square btn-ghost">
<span class="i-heroicons-outline-search" />
</button>
{/if}
<div id="change-theme" class="dropdown dropdown-end">
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<!-- reference: https://github.com/saadeghi/daisyui/issues/1285 -->
<div tabindex="0" class="btn btn-square btn-ghost">
<span class="i-heroicons-outline-color-swatch" />
</div>
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<!-- reference: https://github.com/saadeghi/daisyui/issues/1285 -->
<ul
tabindex="0"
class="flex flex-nowrap shadow-2xl menu dropdown-content bg-base-100 text-base-content rounded-box w-52 p-2 gap-2 overflow-y-auto max-h-[21.5rem]"
class:hidden={!pin}>
{#each theme as { name, text }}
<button
data-theme={name}
on:click={() => {
currentTheme = name
localStorage.setItem('theme', name)
}}
class:border-2={currentTheme === name}
class:border-primary={currentTheme === name}
class="btn btn-ghost w-full hover:bg-primary group rounded-lg flex bg-base-100 p-2 transition-all">
<p class="flex-1 text-left text-base-content group-hover:text-primary-content transition-color">
{text ?? name}
</p>
<div class="grid grid-cols-4 gap-0.5 m-auto">
{#each ['bg-primary', 'bg-secondary', 'bg-accent', 'bg-neutral'] as bg}
<div class={`${bg} w-1 h-4 rounded-btn`} />
{/each}
</div>
</button>
{/each}
</ul>
</div>
</div>
</div>
{:else}
<div in:fly={{ x: 50, duration: 300, delay: 300 }} out:fly={{ x: 50, duration: 300 }} class="navbar">
<Search />
<button on:click={() => (search = !search)} tabindex="0" class="btn btn-square btn-ghost">
<span class="i-heroicons-outline-x" />
</button>
</div>
{/if}
</header>
<button
id="totop"
on:click={() => window.scrollTo(0, 0)}
class:translate-y-24={!pin || scrollY === 0}
aria-label="scroll to top"
class="fixed grid group btn btn-circle btn-lg border-none backdrop-blur bottom-6 right-6 z-50 duration-500 ease-in-out {percent >
95
? 'btn-accent shadow-lg'
: 'btn-ghost bg-base-100/30 md:bg-base-200/30'}"
class:opacity-100={scrollY}>
<!-- https://daisyui.com/blog/how-to-update-daisyui-4/#3-all--focus-colors-are-removed -->
<div
class="radial-progress text-accent transition-all duration-500 ease-in-out group-hover:text-[color-mix(in_oklab,oklch(var(--a)),black_7%)] col-start-1 row-start-1"
style={`--size:4rem; --thickness: 0.25rem; --value:${percent};`} />
<div
class:border-transparent={percent > 95}
class="border-4 border-base-content/10 group-hover:border-transparent col-start-1 row-start-1 rounded-full w-full h-full p-4 grid duration-500 ease-in-out">
<span class="i-heroicons-solid-chevron-up !w-6 !h-6" />
</div>
</button>

View File

@ -0,0 +1,75 @@
<script lang="ts">
export let nav: { text: string; link?: string; children?: { text: string; link: string }[] }[]
export let path: string
export let title: string
export let scrollY: number
export let pin: boolean
</script>
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<!-- reference: https://github.com/saadeghi/daisyui/issues/1285 -->
<div class="dropdown lg:hidden">
<label for="navbar-dropdown" tabindex="0" class="btn btn-square btn-ghost">
<span class="i-heroicons-outline-menu-alt-1" />
</label>
<ul
id="navbar-dropdown"
tabindex="0"
class:hidden={!pin}
class="menu menu-compact dropdown-content bg-base-100 text-base-content shadow-lg rounded-box min-w-max max-w-52 p-2
">
{#each nav as { text, link, children }}
{#if link && !children}
<li>
<a class:font-bold={link === path} href={link}>{text}</a>
</li>
{:else if children}
<li tabindex="0">
<span class:font-bold={children.some(({ link }) => link === path)} class="justify-between gap-1 max-w-[13rem]">
{text}
<span class="i-heroicons-solid-chevron-right mr-2" />
</span>
<ul class="bg-base-100 text-base-content shadow-lg p-2">
{#each children as { text, link }}
<li>
<a class:font-bold={link === path} href={link}>{text}</a>
</li>
{/each}
</ul>
</li>
{/if}
{/each}
</ul>
</div>
<div class:swap-active={scrollY > 32 && title} class="swap order-last hidden lg:inline-grid">
<button
on:click={() => window.scrollTo(0, 0)}
class:hidden={scrollY < 32 || !title}
class="swap-on btn btn-ghost text-base font-normal normal-case transition-all duration-200">
{title}
</button>
<ul class:hidden={scrollY > 64 && title} class="swap-off menu menu-horizontal p-0">
{#each nav as { text, link, children }}
{#if link && !children}
<li>
<a class="!rounded-btn" class:font-bold={link === path} href={link}>{text}</a>
</li>
{:else if children}
<li>
<span class:font-bold={children.some(({ link }) => link === path)} class="!rounded-btn gap-1">
{text}
<span class="i-heroicons-solid-chevron-down -mr-1" />
</span>
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<ul tabindex="0" class="menu rounded-box bg-base-100 text-base-content shadow-lg p-2">
{#each children as { text, link }}
<li>
<a class:font-bold={link === path} href={link}>{text}</a>
</li>
{/each}
</ul>
</li>
{/if}
{/each}
</ul>
</div>

View File

@ -0,0 +1,21 @@
<script lang="ts">
import { site } from '$lib/config/site'
import { header as headerConfig } from '$lib/config/general'
</script>
<form
action={headerConfig?.search?.provider === 'duckduckgo' ? '//duckduckgo.com/' : '//google.com/search'}
method="get"
class="flex-1">
<input
type="text"
name="q"
class="input input-ghost input-bordered xl:bg-base-100 xl:text-base-content transition-all w-full h-12" />
<input
type="hidden"
name={headerConfig?.search?.provider === 'duckduckgo' ? 'sites' : 'sitesearch'}
value={site.protocol + site.domain} />
<button type="submit" class="btn btn-square btn-ghost ml-2">
<span class="i-heroicons-outline-search" />
</button>
</form>

View File

@ -1,105 +0,0 @@
<script lang="ts">
import QuickLinks from '$lib/components/home/QuickLinks.svelte';
</script>
<section class="grid grid-cols-1 lg:grid-cols-2 mx-4">
<!-- Text and links-->
<div class="order-1 max-w-3/4 space-y-8">
<h1 class="h1">Hello, I&#39;m Matt.</h1>
<p class="text-xl opacity-75" data-svelte-h="svelte-169iyno">
A dev with an array of skills from from frontend and devops to design. I have a
strong passion for innovation and change in tech, automation and solving 👾 problems
.
<br />From wearing a lot of 🤠 hats in past projects and startups I became a
generalist, now I am actively deepening my knowledge in software:
<span
class=" text-4xl bg-gradient-to-r from-primary-800 via-secondary-900 to-tertiary-900 dark:from-primary-400 dark:via-secondary-400 dark:to-tertiary-400 text-transparent bg-clip-text"
>DevOps, CyberSec and AI.</span
>
</p>
<QuickLinks />
</div>
<!-- Logo and buttons-->
<div class="order-2 hidden lg:block">
<figure class="items-center">
<img
class="rounded-10 w-64 overflow-hidden"
src="/images/profile-pic.png"
alt="Profile"
/>
<section class="img-bg w-64">
<img
class="rounded-10 w-64 overflow-hidden"
src="/images/profile-pic.png"
alt="Profile"
/>
</section>
</figure>
<div class="flex justify-center space-x-2 m-4">
<a
class="btn variant-ghost-primary"
href="https://git.mattmor.in"
target="_blank"
rel="personal"
>
Look at my code
</a>
<a
class="btn variant-ghost-primary"
href="/CV_Matthieu_Morin.pdf"
target="_blank"
rel="cv"
>
Get my CV
</a>
</div>
</div>
<div class="order-2 flex justify-center lg:hidden space-x-2 m-4">
<a
class="btn variant-ghost-primary"
href="https://git.mattmor.in"
target="_blank"
rel="personal-mob"
>
Look at my code
</a>
<a
class="btn variant-ghost-primary"
href="/CV_Matthieu_Morin.pdf"
target="_blank"
rel="cv-mob"
>
Download my CV
</a>
</div>
</section>
<style lang="postcss">
figure {
@apply flex relative flex-col;
}
.img-bg {
@apply absolute z-[-1] rounded-full blur-[100px] transition-all;
animation: pulse 5s cubic-bezier(0, 30, 150, 1) infinite, glow 5s linear infinite;
}
@keyframes glow {
0% {
@apply bg-primary-400/50;
}
33% {
@apply bg-secondary-400/50;
}
66% {
@apply bg-tertiary-400/50;
}
100% {
@apply bg-primary-400/50;
}
}
@keyframes pulse {
50% {
transform: scale(1.5);
}
}
</style>

View File

@ -1,26 +0,0 @@
<section class="grid grid-cols-1 md:grid-cols-3 gap-4 lg:gap-8" data-svelte-h="svelte-1u9sn7t">
<div class="card variant-ringed-hollow p-4 md:p-8 space-y-4">
<i class="fa-solid fa-screwdriver-wrench text-4xl text-primary-500" />
<h3 class="h3">Development</h3>
<p class="opacity-75">
I possess a wide range of skills that enable me to develop visually appealing and
interactive user interfaces for web applications.
</p>
</div>
<div class="card variant-ringed-hollow p-4 md:p-8 space-y-4">
<i class="fa-solid fa-palette text-4xl text-primary-500" />
<h3 class="h3">Design</h3>
<p class="opacity-75">
Over the years, I have honed my ability to create visually appealing interfaces that
are both user-friendly and intuitive.
</p>
</div>
<div class="card variant-ringed-hollow p-4 md:p-8 space-y-4">
<i class="fa-solid fa-users text-4xl text-primary-500" />
<h3 class="h3">User Experience</h3>
<p class="opacity-75">
I understand the importance of creating a seamless UX for end-users. Which includes
a solid understanding user behavior.
</p>
</div>
</section>

View File

@ -1,28 +0,0 @@
<script lang="ts">
import * as conf from '$lib/config';
import GiteaLogo from '$lib/components/logos/GiteaLogo.svelte';
</script>
<div class="flex space-x-4">
<a
class="btn btn-icon variant-soft-primary hover:variant-filled-primary"
href={conf.socialLinks[2].href}
target="_blank"
rel="noreferrer"
title="Gitea - private github"
><GiteaLogo clazz="w-6" />
</a><a
class="btn btn-icon variant-soft-primary hover:variant-filled-primary"
href={conf.socialLinks[0].href}
target="_blank"
rel="noreferrer"
title="LinkedIn"
><i class="fa-brands fa-linkedin" />
</a><a
class="btn btn-icon variant-soft-primary hover:variant-filled-primary"
href="mailto:{conf.email}"
target=""
rel="noreferrer"
title="Email"
><i class="fa-solid fa-envelope" />
</a>
</div>

View File

@ -0,0 +1,56 @@
<script lang="ts">
import { site } from '$lib/config/site'
</script>
<div
class="h-card flex flex-col gap-4 sticky top-24 card card-body p-4 items-right xl:border-2 xl:py-8 border-base-content/10 xl:ml-auto xl:mr-8 xl:max-w-xs">
<a href={site.protocol + site.domain} class="hidden u-url u-uid">{site.author.name}</a>
<figure class="relative mx-auto group">
{#if site.author.avatar}
<img
class="u-photo rounded-full shadow-xl hover:shadow-2xl transition-shadow z-10 w-24 h-24 md:w-32 md:h-32"
src={site.author.avatar}
alt={site.author.name} />
{/if}
{#if site.author.status}
<div
class="absolute z-20 rounded-full w-8 h-8 md:w-10 md:h-10 bottom-0 right-0 bg-base-100 shadow-xl text-lg md:text-xl text-center py-0.5 md:py-1.5">
{site.author.status}
</div>
{/if}
</figure>
<div class="text-center flex flex-col gap-2">
<h2 class="text-2xl font-bold mt-0 mb-2 p-name">{site.author.name}</h2>
<p class="opacity-75 p-note">{@html site.author.bio}</p>
{#if site.author.metadata}
<div class="flex gap-1 flex-wrap justify-center">
{#each site.author.metadata as { text, icon, link, rel }}
{#if link}
<a
href={link}
rel={rel ?? 'me noopener noreferrer external'}
class:btn-square={!text}
class="btn btn-sm btn-ghost normal-case gap-2 u-url"
target="_blank">
{#if icon}
<span class="{icon} !w-5 !h-5">{icon}</span>
{/if}
{#if text}
{text}
{/if}
</a>
{:else}
<button class:btn-square={!text} class="btn btn-sm btn-ghost normal-case gap-2">
{#if icon}
<span class="{icon} !w-5 !h-5">{icon}</span>
{/if}
{#if text}
{text}
{/if}
</button>
{/if}
{/each}
</div>
{/if}
</div>
</div>

View File

@ -1,16 +0,0 @@
<script lang="ts">
export let clazz = 'w-8 md:w-14 ';
</script>
<svg class={clazz} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"
><path
d="M395.9 484.2l-126.9-61c-12.5-6-17.9-21.2-11.8-33.8l61-126.9c6-12.5 21.2-17.9 33.8-11.8 17.2 8.3 27.1 13 27.1 13l-.1-109.2 16.7-.1.1 117.1s57.4 24.2 83.1 40.1c3.7 2.3 10.2 6.8 12.9 14.4 2.1 6.1 2 13.1-1 19.3l-61 126.9c-6.2 12.7-21.4 18.1-33.9 12z"
fill="#fff"
/><g fill="#609926"
><path
d="M622.7 149.8c-4.1-4.1-9.6-4-9.6-4s-117.2 6.6-177.9 8c-13.3.3-26.5.6-39.6.7v117.2c-5.5-2.6-11.1-5.3-16.6-7.9 0-36.4-.1-109.2-.1-109.2-29 .4-89.2-2.2-89.2-2.2s-141.4-7.1-156.8-8.5c-9.8-.6-22.5-2.1-39 1.5-8.7 1.8-33.5 7.4-53.8 26.9C-4.9 212.4 6.6 276.2 8 285.8c1.7 11.7 6.9 44.2 31.7 72.5 45.8 56.1 144.4 54.8 144.4 54.8s12.1 28.9 30.6 55.5c25 33.1 50.7 58.9 75.7 62 63 0 188.9-.1 188.9-.1s12 .1 28.3-10.3c14-8.5 26.5-23.4 26.5-23.4S547 483 565 451.5c5.5-9.7 10.1-19.1 14.1-28 0 0 55.2-117.1 55.2-231.1-1.1-34.5-9.6-40.6-11.6-42.6zM125.6 353.9c-25.9-8.5-36.9-18.7-36.9-18.7S69.6 321.8 60 295.4c-16.5-44.2-1.4-71.2-1.4-71.2s8.4-22.5 38.5-30c13.8-3.7 31-3.1 31-3.1s7.1 59.4 15.7 94.2c7.2 29.2 24.8 77.7 24.8 77.7s-26.1-3.1-43-9.1zm300.3 107.6s-6.1 14.5-19.6 15.4c-5.8.4-10.3-1.2-10.3-1.2s-.3-.1-5.3-2.1l-112.9-55s-10.9-5.7-12.8-15.6c-2.2-8.1 2.7-18.1 2.7-18.1L322 273s4.8-9.7 12.2-13c.6-.3 2.3-1 4.5-1.5 8.1-2.1 18 2.8 18 2.8L467.4 315s12.6 5.7 15.3 16.2c1.9 7.4-.5 14-1.8 17.2-6.3 15.4-55 113.1-55 113.1z"
/><path
d="M326.8 380.1c-8.2.1-15.4 5.8-17.3 13.8-1.9 8 2 16.3 9.1 20 7.7 4 17.5 1.8 22.7-5.4 5.1-7.1 4.3-16.9-1.8-23.1l24-49.1c1.5.1 3.7.2 6.2-.5 4.1-.9 7.1-3.6 7.1-3.6 4.2 1.8 8.6 3.8 13.2 6.1 4.8 2.4 9.3 4.9 13.4 7.3.9.5 1.8 1.1 2.8 1.9 1.6 1.3 3.4 3.1 4.7 5.5 1.9 5.5-1.9 14.9-1.9 14.9-2.3 7.6-18.4 40.6-18.4 40.6-8.1-.2-15.3 5-17.7 12.5-2.6 8.1 1.1 17.3 8.9 21.3 7.8 4 17.4 1.7 22.5-5.3 5-6.8 4.6-16.3-1.1-22.6 1.9-3.7 3.7-7.4 5.6-11.3 5-10.4 13.5-30.4 13.5-30.4.9-1.7 5.7-10.3 2.7-21.3-2.5-11.4-12.6-16.7-12.6-16.7-12.2-7.9-29.2-15.2-29.2-15.2s0-4.1-1.1-7.1c-1.1-3.1-2.8-5.1-3.9-6.3 4.7-9.7 9.4-19.3 14.1-29-4.1-2-8.1-4-12.2-6.1-4.8 9.8-9.7 19.7-14.5 29.5-6.7-.1-12.9 3.5-16.1 9.4-3.4 6.3-2.7 14.1 1.9 19.8l-24.6 50.4z"
/></g
></svg
>

View File

@ -1,17 +0,0 @@
<script lang="ts">
import { modeCurrent } from '@skeletonlabs/skeleton';
let clazz = 'w-6 md:w-12';
let fillStyle = 'fill: #000000';
$: {
fillStyle = $modeCurrent ? 'fill: #000000' : 'fill: #ffffff';
}
</script>
<svg class={clazz} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<path style={fillStyle} d="M 30,2.0000001 V 30 h -1 -2 v 2 h 5 V -3.3333334e-8 L 27,0 v 2 z" />
<path
style={fillStyle}
d="M 9.9515939,10.594002 V 12.138 h 0.043994 c 0.3845141,-0.563728 0.8932271,-1.031728 1.4869981,-1.368 0.580003,-0.322998 1.244999,-0.485 1.993002,-0.485 0.72,0 1.376999,0.139993 1.971998,0.42 0.595,0.279004 1.047001,0.771001 1.355002,1.477001 0.338003,-0.500001 0.795999,-0.941 1.376999,-1.323001 0.579999,-0.382998 1.265998,-0.574 2.059998,-0.574 0.602003,0 1.160002,0.074 1.674002,0.220006 0.514,0.148006 0.953998,0.382998 1.321999,0.706998 0.36601,0.322999 0.653001,0.746 0.859,1.268002 0.205001,0.521998 0.307994,1.15 0.307994,1.887001 v 7.632997 h -3.127 v -6.463997 c 0,-0.383002 -0.01512,-0.743002 -0.04399,-1.082003 -0.02079,-0.3072 -0.103219,-0.607113 -0.242003,-0.881998 -0.133153,-0.25081 -0.335962,-0.457777 -0.584001,-0.596002 -0.257008,-0.146003 -0.605998,-0.220006 -1.046997,-0.220006 -0.440002,0 -0.796003,0.085 -1.068,0.253002 -0.272013,0.170003 -0.485001,0.390002 -0.639001,0.662003 -0.159119,0.287282 -0.263585,0.601602 -0.307994,0.926997 -0.05197,0.346923 -0.07801,0.697217 -0.07801,1.048002 v 6.353999 h -3.128005 v -6.398 c 0,-0.338003 -0.0072,-0.673001 -0.02116,-1.004001 -0.01134,-0.313663 -0.07487,-0.623229 -0.187994,-0.915999 -0.107943,-0.276623 -0.300435,-0.512126 -0.550001,-0.673001 -0.25799,-0.168 -0.636,-0.253002 -1.134999,-0.253002 -0.198123,0.0083 -0.394383,0.04195 -0.584002,0.100006 -0.258368,0.07446 -0.498455,0.201827 -0.704999,0.373985 -0.227981,0.183987 -0.421999,0.449 -0.583997,0.794003 -0.161008,0.345978 -0.242003,0.797998 -0.242003,1.356998 v 6.618999 H 6.99942 V 10.590001 Z"
/>
<path style={fillStyle} d="M 2,2.0000001 V 30 h 3 v 2 H 0 V 9.2650922e-8 L 5,0 v 2 z" />
</svg>

View File

@ -1,37 +0,0 @@
<script lang="ts">
import { getDrawerStore, Drawer } from '@skeletonlabs/skeleton';
const drawerStore = getDrawerStore();
import { NavRoutes } from '$lib/config';
import { page } from '$app/stores';
import { browser } from '$app/environment';
// what is my url?
$: classesDrawer = $drawerStore.id === 'mobile-nav' ? 'md:hidden' : '';
</script>
<Drawer class={classesDrawer}>
{#if $drawerStore.id === 'demo'}
<!-- Doc Sidebar -->
{:else if $drawerStore.id === 'mobile-nav'}
<!-- Drawer nav only -->
<nav
class="z-50 flex flex-col gap-2 border border-surface-100-800-token bg-surface-50/50 dark:bg-surface-900/50 backdrop-blur-lg rounded-bl-container-token rounded-br-container-token p-2 shadow-xl"
>
{#each NavRoutes as route}
<a
href={route.href}
class="btn md:btn-sm hover:variant-soft-primary {route.href ===
$page.url.pathname
? 'variant-filled-primary'
: ''}">{route.title}</a
>
{/each}
</nav>
{:else}
<!-- Fallback Error -->
<div class="w-full h-full flex justify-center items-center">
<div class="text-center space-y-2">
<p>Invalid <code class="code">$drawerStore.id</code> provided.</p>
</div>
</div>
{/if}
</Drawer>

View File

@ -1,25 +0,0 @@
<script lang="ts">
import GiteaLogo from '$lib/components/logos/GiteaLogo.svelte';
import SocialsCloud from '$lib/components/SocialsCloud.svelte';
const year = new Date().getFullYear();
</script>
<footer
class="page-footer bg-surface-50 dark:bg-surface-900 border-t border-surface-500/10 text-xs mt-4 md:text-base"
>
<SocialsCloud />
<hr class="opacity-20" />
<div class="w-full max-w-7xl mx-auto p-4 md:py-8 flex items-center justify-center">
<!-- <div class="container px-5 py-8 mx-auto flex items-center sm:flex-row flex-col"> -->
<a class="items-center md:justify-start justify-center" href="/">
<p class="sm:pl-4 text-base sm:py-2 sm:mt-0 mt-4 text-center">
All content, unless otherwise stated,
<br>by Matthieu Morin, is under © copyright {year},
<br>and all of it licensed under <a class="anchor font-bold" href="https://creativecommons.org/licenses/by-sa/4.0/deed.en">CC BY-SA 4.0</a>.
<br>This site coded by me is <a class="font-bold anchor" href="https://git.mattmor.in/Madmin/its-personal/src/branch/master/LICENSE">MIT Licensed</a>.
</p>
</a>
</div>
</footer>

View File

@ -1,63 +0,0 @@
<script lang="ts">
import { LightSwitch, AppBar, Avatar, getDrawerStore, Drawer } from '@skeletonlabs/skeleton';
import type { DrawerSettings } from '@skeletonlabs/skeleton';
const drawerStore = getDrawerStore();
// Components
import { getImageLink } from '$lib/images';
import { page } from '$app/stores';
import { NavRoutes } from '$lib/config';
function triggerStyled() {
const drawerSettings: DrawerSettings = {
position: 'top',
id: 'mobile-nav',
// Provide your property overrides:
bgDrawer: 'overflow-y-auto ',
bgBackdrop:
'z-50 bg-gradient-to-tr from-primary-500/50 via-secondary-500/50 to-tertiary-500/50',
width: 'w-full h-auto ',
rounded: 'rounded-bl-container-token rounded-br-container-token'
// Metadata
// meta: ''
};
drawerStore.open(drawerSettings);
}
// Local
const imgPlaceholder = getImageLink({ id: 'linky', w: 128, h: 128 });
</script>
<section
class="!max-w-7xl mx-auto grid grid-cols-[1fr_auto_auto]
md:grid-cols-[48px_1fr_48px] md:place-items-center items-center gap-4 p-4"
>
<a href="/" title="Return to Homepage">
<Avatar src={'/images/profile-pic.png'} width="w-16" rounded={'rounded-full'} />
</a>
<section id="mobile-nav" class="hidden md:block">
<nav
class="flex flex-col md:flex-row gap-2 border md:border-0 border-surface-100-800-token bg-surface-50/50 dark:bg-surface-900/50 backdrop-blur-lg rounded-bl-container-token rounded-br-container-token md:rounded-token p-2 shadow-xl"
>
{#each NavRoutes as route}
<a
href={route.href}
class="btn md:btn-sm hover:variant-soft-primary {route.href ===
$page.url.pathname
? 'variant-filled-primary'
: ''}"
>
{route.title}
</a>
{/each}
</nav>
</section>
<section class="block md:hidden" id="mobile-nav">
<button class="btn variant-filled-primary" on:click={triggerStyled}>
<i class="fa-solid fa-bars" /> <span>Menu</span>
</button>
</section>
<LightSwitch />
</section>

View File

@ -0,0 +1,12 @@
<script lang="ts">
export let post: Urara.Post
const actions = import.meta.glob<any>('/src/lib/components/actions/*.svelte', { eager: true, import: 'default' })
</script>
<div class="sticky top-24 hidden xl:flex flex-col gap-4 w-fit h-[calc(100vh-12rem)] ml-auto mr-8 my-8 justify-center">
{#if Object.keys(actions).length}
{#each Object.values(actions) as action}
<svelte:component this={action} {post} />
{/each}
{/if}
</div>

View File

@ -0,0 +1,130 @@
<script lang="ts">
import { browser } from '$app/environment'
import { post as postConfig } from '$lib/config/post'
import { posts as storedPosts } from '$lib/stores/posts'
import { title as storedTitle } from '$lib/stores/title'
import Reply from '$lib/components/post_reply.svelte'
import Status from '$lib/components/post_status.svelte'
import Image from '$lib/components/prose/img.svelte'
import Pagination from '$lib/components/post_pagination.svelte'
import Comment from '$lib/components/post_comment.svelte'
export let post: Urara.Post
export let preview: boolean = false
export let loading: 'eager' | 'lazy' = 'lazy'
export let decoding: 'async' | 'sync' | 'auto' = 'async'
// pagination
let index: number
let prev: Urara.Post | undefined = undefined
let next: Urara.Post | undefined = undefined
if (browser && !preview)
storedPosts.subscribe((storedPosts: Urara.Post[]) => {
index = storedPosts.findIndex(storedPost => storedPost.path === post.path)
prev = storedPosts
.slice(0, index)
.reverse()
.find(post => !post.flags?.includes('unlisted'))
next = storedPosts.slice(index + 1).find(post => !post.flags?.includes('unlisted'))
storedTitle.set(post.title ?? post.path.slice(1))
})
</script>
<svelte:element
this={post.type === 'article' ? 'article' : 'div'}
itemscope
itemtype="https://schema.org/BlogPosting"
itemprop="blogPost"
class:md:mb-8={!preview}
class:lg:mb-16={!preview}
class:group={preview}
class:image-full={preview && post.type === 'article' && post.image}
class:before:!rounded-none={preview && post.image}
class="h-entry card bg-base-100 rounded-none md:rounded-box md:shadow-xl overflow-hidden z-10">
{#if !preview && postConfig.bridgy}
<div id="bridgy" class="hidden">
{#each post.flags?.some( flag => flag.startsWith('bridgy') ) ? post.flags.flatMap( flag => (flag.startsWith('bridgy') ? flag.slice(7) : []) ) : [...(postConfig.bridgy.post ?? []), ...(postConfig.bridgy[post.type] ?? [])] as target}
{#if target === 'fed'}
<a href="https://fed.brid.gy/">fed</a>
{:else}
<a href="https://brid.gy/publish/{target}">{target}</a>
{/if}
{/each}
</div>
{/if}
{#if post.in_reply_to}
<Reply in_reply_to={post.in_reply_to} class="mt-4 mx-4" />
{/if}
{#if post.image && preview}
<figure class="!block">
<Image
class={post.type === 'article'
? 'u-featured object-center h-full w-full absolute group-hover:scale-105 transition-transform duration-500 ease-in-out'
: 'u-photo rounded-xl md:rounded-b-none -mb-6 md:-mb-2'}
src={post.image}
alt={post.alt ?? post.image}
{loading}
{decoding} />
</figure>
{/if}
<div
class={`card-body gap-0 ${
preview && post.type === 'article' && post.image ? 'md:col-start-1 md:row-start-1 md:text-neutral-content md:z-20' : ''
}`}>
<div class="flex flex-col gap-2">
{#if post.image && !preview}
<figure
class={`md:order-last rounded-box shadow-xl mb-4 ${
post.type === 'article' ? 'flex-col gap-2 -mx-4 -mt-8 md:mt-0' : 'flex-col -mx-8'
}`}>
<Image
class={`${post.type === 'article' ? 'u-featured' : 'u-photo'}`}
src={post.image}
alt={post.alt ?? post.image}
{loading}
{decoding} />
</figure>
{/if}
<Status {post} {preview} />
{#if post.title}
{#if preview}
<h2
itemprop="name headline"
class="card-title text-3xl mr-auto bg-[length:100%_0%] bg-[position:0_88%] underline decoration-4 decoration-transparent group-hover:decoration-primary hover:bg-[length:100%_100%] hover:text-primary-content bg-gradient-to-t from-primary to-primary bg-no-repeat transition-all ease-in-out duration-300">
<a itemprop="url" class="u-url p-name" href={post.path}>{post.title ?? post.path.slice(1)}</a>
</h2>
{:else}
<h1 itemprop="name headline" class="card-title text-3xl mb-8 p-name">{post.title ?? post.path.slice(1)}</h1>
{/if}
{/if}
{#if post.summary}
<p itemprop="description" class:hidden={!preview || post.type !== 'article'} class="p-summary mb-auto">
{post.summary}
</p>
{/if}
</div>
<main itemprop="articleBody" class:mt-4={post.type !== 'article'} class="urara-prose prose e-content">
{#if !preview}
<slot />
{:else if post.html}
{@html post.html}
{/if}
</main>
{#if !preview && post.tags}
<div class="divider mt-4 mb-0" />
<div>
{#each post.tags as tag}
<a href="/?tags={tag}" class="btn btn-sm btn-ghost normal-case mt-2 mr-2 p-category">
#{tag}
</a>
{/each}
</div>
{/if}
</div>
{#if !preview}
{#if (prev || next) && !post.flags?.includes('pagination-disabled') && !post.flags?.includes('unlisted')}
<Pagination {next} {prev} />
{/if}
{#if browser && postConfig.comment && !post.flags?.includes('comment-disabled')}
<Comment {post} config={postConfig.comment} />
{/if}
{/if}
</svelte:element>

View File

@ -0,0 +1,46 @@
<script lang="ts">
import type { CommentConfig } from '$lib/types/post'
import { toSnake } from '$lib/utils/case'
export let post: Urara.Post
export let config: CommentConfig
const comments = import.meta.glob<any>('/src/lib/components/comments/*.svelte', { eager: true, import: 'default' })
let currentComment: string | undefined = undefined
let currentConfig: unknown | undefined = undefined
currentComment = localStorage.getItem('comment') ?? toSnake(config.use[0])
// @ts-ignore No index signature with a parameter of type 'string' was found on type 'CommentConfig'. ts(7053)
$: if (currentComment) currentConfig = config[currentComment]
</script>
{#if config?.use.length > 0}
<div id="post-comment" class="card card-body">
{#if config.use.length > 1}
<div
class="tabs w-full mb-8"
class:tabs-boxed={config?.['style'] === 'boxed'}
class:tab-bordered={config?.['style'] === 'bordered'}
class:tab-lifted={config?.['style'] === 'lifted'}>
{#each config.use as name}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<span
on:click={() => {
currentComment = toSnake(name)
localStorage.setItem('comment', toSnake(name))
}}
class="flex-1 tab transition-all"
class:tab-active={currentComment === toSnake(name)}>
{name}
</span>
{/each}
</div>
{/if}
{#if currentComment}
{#key currentComment}
<svelte:component
this={comments[`/src/lib/components/comments/${currentComment}.svelte`]}
{post}
config={currentConfig} />
{/key}
{/if}
</div>
{/if}

View File

@ -0,0 +1,39 @@
<script lang="ts">
import { fly } from 'svelte/transition'
import { browser } from '$app/environment'
import Card from '$lib/components/post_card.svelte'
import Head from '$lib/components/head.svelte'
import Toc from '$lib/components/post_toc.svelte'
import Action from '$lib/components/post_action.svelte'
import Footer from '$lib/components/footer.svelte'
export let post: Urara.Post
</script>
<Head {post} />
<div class="flex flex-col flex-nowrap justify-center xl:flex-row xl:flex-wrap">
<div
in:fly={{ x: 25, duration: 300, delay: 500 }}
out:fly={{ x: 25, duration: 300 }}
class="flex-1 w-full order-first ease-out transform mx-auto xl:mr-0 xl:ml-0">
{#if browser}
<Action {post} />
{/if}
</div>
<div
in:fly={{ x: -25, duration: 300, delay: 500 }}
out:fly={{ x: -25, duration: 300 }}
class="flex-1 w-full xl:order-last ease-out transform mx-auto xl:ml-0 xl:mr-0">
{#if browser && post.toc}
<div class="h-full hidden xl:block">
<Toc toc={post.toc} />
</div>
{/if}
</div>
<div class="flex-none w-full max-w-screen-md mx-auto xl:mx-0">
<Card {post}>
<slot />
</Card>
<Footer sticky={true} />
</div>
</div>

View File

@ -0,0 +1,32 @@
<script lang="ts" context="module">
import Image from '$lib/components/prose/img.svelte'
import table from '$lib/components/prose/table.svelte'
export { Image as img, table }
</script>
<script lang="ts">
import { typeOfPost } from '$lib/utils/posts'
import Container from '$lib/components/post_container.svelte'
// auto-generated
export let path
export let slug
export let toc
// common
export let created
export let updated
export let published
export let summary
export let tags
export let flags
// specify
export let title
export let image
export let in_reply_to
// post
let fm = { path, slug, toc, created, updated, published, summary, tags, flags, title, image, in_reply_to }
let post = { type: typeOfPost(fm), ...fm }
</script>
<Container {post}>
<slot />
</Container>

View File

@ -0,0 +1,59 @@
<script lang="ts">
import Image from '$lib/components/prose/img.svelte'
export let prev: Urara.Post | undefined = undefined
export let next: Urara.Post | undefined = undefined
</script>
<nav class="flex flex-col md:flex-row flex-warp justify-evenly">
{#if prev}
<div
class:image-full={prev['image']}
class:md:rounded-r-box={next && !next['image']}
class="flex-1 card group rounded-none before:!rounded-none overflow-hidden">
{#if prev['image']}
<figure class="!block">
<Image
class="object-center h-full w-full absolute group-hover:scale-105 transition-transform duration-500 ease-in-out"
src={prev['image']}
alt={prev['alt'] ?? prev['image']} />
</figure>
{/if}
<div class="card-body">
<span class="i-heroicons-outline-chevron-left opacity-50 group-hover:opacity-100 mr-auto" />
<a
rel="prev"
href={prev.path}
class="card-title block text-left mb-0 mr-auto bg-[length:100%_0%] bg-[position:0_88%] underline decoration-3 decoration-transparent group-hover:decoration-primary hover:bg-[length:100%_100%] hover:text-primary-content bg-gradient-to-t from-primary to-primary bg-no-repeat transition-all ease-in-out duration-300">
{prev['title'] ?? prev['summary'] ?? prev.path.slice(1)}
</a>
</div>
</div>
{#if next && !next['image'] && !prev['image']}
<div class="flex-0 divider mx-4 md:divider-horizontal md:mx-0 md:my-4" />
{/if}
{/if}
{#if next}
<div
class:image-full={next['image']}
class:md:rounded-l-box={prev && !prev['image']}
class="flex-1 card group rounded-none before:!rounded-none overflow-hidden">
{#if next['image']}
<figure class="!block">
<Image
class="object-center h-full w-full absolute group-hover:scale-105 transition-transform duration-500 ease-in-out"
src={next['image']}
alt={next['alt'] ?? next['image']} />
</figure>
{/if}
<div class="card-body">
<a
rel="next"
href={next.path}
class="card-title block text-right mb-0 ml-auto bg-[length:100%_0%] bg-[position:0_88%] underline decoration-3 decoration-transparent group-hover:decoration-primary hover:bg-[length:100%_100%] hover:text-primary-content bg-gradient-to-t from-primary to-primary bg-no-repeat transition-all ease-in-out duration-300">
{next['title'] ?? next['summary'] ?? next.path.slice(1)}
</a>
<span class="i-heroicons-outline-chevron-right opacity-50 group-hover:opacity-100 ml-auto" />
</div>
</div>
{/if}
</nav>

View File

@ -0,0 +1,17 @@
<script lang="ts">
let className = ''
export { className as class }
export let in_reply_to: Urara.Post['in_reply_to']
</script>
<div class="flex flex-wrap gap-2 rounded-box outline outline-neutral/10 p-4 {className}">
<span class="flex-none font-bold uppercase opacity-30">Reply to:&nbsp;</span>
<a
href={in_reply_to}
rel="noopener noreferrer external"
target="_blank"
class="ml-auto flex-none flex rounded-badge bg-base-200 hover:bg-base-300 transition-all gap-2 px-4 u-in-reply-to">
<span class="i-heroicons-outline-reply my-auto !w-4 !h-4" />
{in_reply_to}
</a>
</div>

View File

@ -0,0 +1,35 @@
<script lang="ts">
import { date } from '$lib/config/general'
import { site } from '$lib/config/site'
export let post: Urara.Post
export let preview: boolean = false
const stringPublished = new Date(post.published ?? post.created).toLocaleString(date.locales, date.options)
const stringUpdated = new Date(post.updated ?? post.published ?? post.created).toLocaleString(date.locales, date.options)
const jsonPublished = new Date(post.published ?? post.created).toJSON()
const jsonUpdated = new Date(post.updated ?? post.published ?? post.created).toJSON()
</script>
<div class:md:mb-4={!preview && post.type !== 'article'} class="flex font-semibold gap-1.5">
<a
class:hidden={preview}
rel="author"
class="opacity-75 hover:opacity-100 hover:text-primary duration-500 ease-in-out p-author h-card"
href={site.protocol + site.domain}>
{site.author.name}
</a>
<span class:hidden={preview} class="opacity-50">/</span>
<a href={post.path} class="u-url u-uid swap group/time">
<time
class="group-hover/time:opacity-0 font-semibold opacity-75 duration-500 ease-in-out mr-auto dt-published"
datetime={jsonPublished}
itemprop="datePublished">
{stringPublished}
</time>
<time
class="opacity-0 group-hover/time:opacity-100 font-semibold text-primary duration-500 ease-in-out mr-auto dt-updated"
datetime={jsonUpdated}
itemprop="dateModified">
{stringUpdated}
</time>
</a>
</div>

View File

@ -0,0 +1,80 @@
<script lang="ts" context="module">
export const prerender = true
</script>
<script lang="ts">
import { onMount, onDestroy } from 'svelte'
export let toc: Urara.Post.Toc[]
let intersecting: string[] = []
let intersectingArticle: boolean = true
let bordered: string[] = []
onMount(() => {
if (window.screen.availWidth >= 1280) {
const headingsObserver = new IntersectionObserver(
headings =>
headings.forEach(heading =>
heading.isIntersecting
? intersecting.push(heading.target.id)
: (intersecting = intersecting.filter(h => h !== heading.target.id))
),
{ rootMargin: '-64px 0px 0px 0px' }
)
const articleObserver = new IntersectionObserver(article => (intersectingArticle = article[0].isIntersecting))
Array.from(document.querySelectorAll('main h2, main h3, main h4, main h5, main h6')).forEach(element =>
headingsObserver.observe(element)
)
articleObserver.observe(document.getElementsByTagName('main')[0])
}
})
onDestroy(() => {
// @ts-ignore: Cannot find name 'headingsObserver'
if (typeof headingsObserver !== 'undefined') headingsObserver.disconnect()
// @ts-ignore: Cannot find name 'articleObserver'
if (typeof articleObserver !== 'undefined') articleObserver.disconnect()
})
$: if (intersecting.length > 0) bordered = intersecting
$: if (intersectingArticle === false) bordered = []
$: if (bordered)
toc.forEach(heading =>
bordered.includes(heading.slug!)
? document.getElementById(`toc-link-${heading.slug}`)?.classList.add('!border-accent')
: document.getElementById(`toc-link-${heading.slug}`)?.classList.remove('!border-accent')
)
</script>
<aside class="sticky top-16 py-8">
<nav
id="post-toc"
aria-label="TableOfContent"
dir="rtl"
class="max-h-[calc(100vh-12rem)] overflow-y-hidden hover:overflow-y-auto">
<ul dir="ltr" id="toc-list-root">
{#each toc as { depth, title, slug }}
<li id={`toc-item-${slug}`} class="flex flex-col">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<span
dir="ltr"
on:click={() =>
// @ts-ignore Object is possibly 'null'. ts(2531)
document.getElementById(slug).scrollIntoView({ behavior: 'smooth' })}
id={`toc-link-${slug}`}
class="cursor-pointer border-l-4 border-transparent transition-all hover:border-primary hover:bg-base-content hover:bg-opacity-10 active:bg-primary active:text-primary-content active:font-bold pr-4 {depth <=
2
? 'py-3'
: 'py-2'}"
class:pl-4={depth <= 2}
class:pl-8={depth === 3}
class:pl-12={depth === 4}
class:pl-16={depth === 5}
class:pl-20={depth === 6}>
{title}
</span>
</li>
{/each}
</ul>
</nav>
</aside>

View File

@ -1,10 +0,0 @@
<script lang="ts">
import ProjectContentLayout from '$lib/components/blog/PostLayout.svelte';
import type { Post } from '$lib/types/post';
export let post: Post;
</script>
<ProjectContentLayout {...post} imagesDirectoryName="projects">
<slot />
</ProjectLayout>

View File

@ -0,0 +1,37 @@
<script lang="ts">
/* @see {@link https://github.com/sveltejs/kit/issues/241#issuecomment-1363621896} */
type Image = {
src: string
w: number
h: number
}
const sources = import.meta.glob<Image[]>(['/src/static/**/*.{jpg,jpeg,png,webp,avif}', '!/src/static/assets'], {
query: {
format: 'avif',
quality: '80',
width: '736',
source: ''
},
import: 'default',
eager: true
})
let className: string | undefined = undefined
export { className as class }
export let src: string
export let alt: string = src
export let loading: 'eager' | 'lazy' = 'lazy'
export let decoding: 'async' | 'sync' | 'auto' = 'async'
let source: Image[] | undefined = sources[`/src/static${src}`]
</script>
{#if source}
<picture>
<source srcset={source.map(({ src, w }) => `${src} ${w}w`).join(', ')} type="image/avif" />
<img {src} {alt} class={className ?? 'rounded-lg my-2'} {loading} {decoding} />
</picture>
{:else}
<img {src} {alt} class={className ?? 'rounded-lg my-2'} {loading} {decoding} />
{/if}

View File

@ -0,0 +1,5 @@
<div class="overflow-x-auto mb-4">
<table class="table w-full">
<slot />
</table>
</div>

View File

@ -0,0 +1,13 @@
<script lang="ts">
import { fly } from 'svelte/transition'
export let path: string = ''
</script>
{#key path}
<div
class="bg-base-100 md:bg-base-200 min-h-screen pt-16 md:pb-8 lg:pb-16"
in:fly={{ y: 100, duration: 300, delay: 300 }}
out:fly={{ y: -100, duration: 300 }}>
<slot />
</div>
{/key}

View File

@ -1,30 +0,0 @@
import { dev } from '$app/environment';
export const title = "Matt's Portfolio";
export const description =
'I code, I think, I write. My thoughts go into the world of Free & Open Source Software, AI and philosophy of mind, Climate Change, Cybersecurity.';
export const url = dev ? 'http://localhost:5174' : 'https://mattmor.in';
export const author = 'Matt Morin';
export const backgroundColor = '#111827';
export const themeColor = '#3b82f6';
export const logo = '/Logo.png';
export const keywords = 'Dev, FOSS, Nix, Philosopher, DevOps, Climate';
export const ogLanguage = 'en_US';
export const siteLanguage = 'en-US';
// prettier-ignore
export const socialLinks = [
{ title: 'LinkedIn', href: 'https://linkedin.com/in/mattmor-in', icon: 'fa-brands fa-linkedin'},
{ title: 'Matrix', href: '', icon: './MatrixLogo' },
{ title: 'Gitea', href: 'https://git.mattmor.in', icon: './GiteaLogo' },
{ title: 'Mastodon', href: 'https://mastodon.social/@matt_mor', icon: 'fa-brands fa-mastodon'},
{ title: 'RSS feed', href: '/blog/feed', icon: 'fa-regular fa-square-rss' },
{ title: 'email', href: 'matt.b.morin@protonmail.com', icon: 'fa-regular mail'}
];
// Routes
export const NavRoutes = [
{ title: 'Home', href: '/' },
{ title: 'Blog', href: '/blog' },
{ title: 'Projects', href: '/projects' }
];

92
src/lib/config/general.ts Normal file
View File

@ -0,0 +1,92 @@
import type { ThemeConfig, HeadConfig, HeaderConfig, FooterConfig, DateConfig, FeedConfig } from '$lib/types/general'
export const theme: ThemeConfig = [
{
name: 'cmyk',
text: '🖨 Light'
},
{
name: 'dracula',
text: '🧛 Dark'
},
{
name: 'valentine',
text: '🌸 Valentine'
},
{
name: 'aqua',
text: '💦 Aqua'
},
{
name: 'synthwave',
text: '🌃 Synthwave'
},
{
name: 'night',
text: '🌃 Night'
},
{
name: 'lofi',
text: '🎶 Lo-Fi'
},
{
name: 'lemonade',
text: '🍋 Lemonade'
},
{
name: 'cupcake',
text: '🧁 Cupcake'
},
{
name: 'garden',
text: '🏡 Garden'
},
{
name: 'retro',
text: '🌇 Retro'
},
{
name: 'black',
text: '🖤 Black'
}
]
export const head: HeadConfig = {}
export const header: HeaderConfig = {
nav: [
{
text: 'Get Started',
link: '/hello-world'
},
{
text: 'Elements',
link: '/hello-world/elements'
}
]
}
export const footer: FooterConfig = {
nav: [
{
text: 'Feed',
link: '/atom.xml'
},
{
text: 'Sitemap',
link: '/sitemap.xml'
}
]
}
export const date: DateConfig = {
locales: 'en-US',
options: {
year: '2-digit',
weekday: 'long',
month: 'short',
day: 'numeric'
}
}
export const feed: FeedConfig = {}

39
src/lib/config/icon.ts Normal file
View File

@ -0,0 +1,39 @@
import type { Icon } from '$lib/types/icon'
import { site } from '$lib/config/site'
export const favicon: Icon = {
src: site.protocol + site.domain + '/favicon.png',
sizes: '48x48',
type: 'image/png'
}
export const any: { [key: number]: Icon } = {
180: {
src: site.protocol + site.domain + '/assets/any@180.png',
sizes: '180x180',
type: 'image/png'
},
192: {
src: site.protocol + site.domain + '/assets/any@192.png',
sizes: '192x192',
type: 'image/png'
},
512: {
src: site.protocol + site.domain + '/assets/any@512.png',
sizes: '512x512',
type: 'image/png'
}
}
export const maskable: { [key: number]: Icon } = {
192: {
src: site.protocol + site.domain + '/assets/maskable@192.png',
sizes: '192x192',
type: 'image/png'
},
512: {
src: site.protocol + site.domain + '/assets/maskable@512.png',
sizes: '512x512',
type: 'image/png'
}
}

3
src/lib/config/post.ts Normal file
View File

@ -0,0 +1,3 @@
import type { PostConfig } from '$lib/types/post'
export const post: PostConfig = {}

17
src/lib/config/site.ts Normal file
View File

@ -0,0 +1,17 @@
import type { SiteConfig } from '$lib/types/site'
export const site: SiteConfig = {
protocol: import.meta.env.URARA_SITE_PROTOCOL ?? import.meta.env.DEV ? 'http://' : 'https://',
domain: import.meta.env.URARA_SITE_DOMAIN ?? 'urara-demo.netlify.app',
title: 'Urara',
subtitle: 'Sweet & Powerful SvelteKit Blog Template',
lang: 'en-US',
description: 'Powered by SvelteKit/Urara',
author: {
avatar: '/assets/maskable@512.png',
name: 'John Doe',
status: '🌸',
bio: 'lorem ipsum dolor sit amet, consectetur adipiscing elit.'
},
themeColor: '#3D4451'
}

4
src/lib/stores/posts.ts Normal file
View File

@ -0,0 +1,4 @@
import type { Writable } from 'svelte/store'
import { writable } from 'svelte/store'
export const posts: Writable<Urara.Post[]> = writable([])
export const tags: Writable<string[]> = writable([])

2
src/lib/stores/title.ts Normal file
View File

@ -0,0 +1,2 @@
import { writable } from 'svelte/store'
export const title = writable({})

View File

@ -1,6 +0,0 @@
type Contribution = {
date: string;
level: number;
};
export type Contributions = Array<Contribution | null>;

Some files were not shown because too many files have changed in this diff Show More