init new urara
This commit is contained in:
parent
15bc3ce30c
commit
e1c965efb4
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
|
@ -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
|
|
@ -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
|
||||
```
|
|
@ -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
|
|
@ -1,3 +1,9 @@
|
|||
# temp file
|
||||
src/routes/**/+page.svelte.md
|
||||
src/routes/**/+page.md
|
||||
src/static
|
||||
urara.js
|
||||
|
||||
+ .turbo
|
||||
+ build/**
|
||||
+ dist/**
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"pluginSearchDirs": ["."],
|
||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"recommendations": ["svelte.svelte-vscode", "orta.vscode-twoslash-queries"]
|
||||
"recommendations": ["svelte.svelte-vscode", "dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
29
Dockerfile
29
Dockerfile
|
@ -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
20
LICENSE
|
@ -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
135
README.md
|
@ -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)
|
||||
|
|
|
@ -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) 製作的圖標
|
182
mdsvex.config.js
182
mdsvex.config.js
|
@ -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'
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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"
|
165
package.json
165
package.json
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
// },
|
||||
});
|
|
@ -1,6 +0,0 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
36
src/app.html
36
src/app.html
|
@ -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>
|
||||
|
|
|
@ -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 */
|
|
@ -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;
|
||||
} */
|
|
@ -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}"`);
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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)
|
||||
|
||||
```
|
|
@ -1,12 +0,0 @@
|
|||
---
|
||||
title: w post
|
||||
excerpt: w post
|
||||
date: 2021-01-01
|
||||
tags:
|
||||
- first
|
||||
- post
|
||||
published: true
|
||||
image: Feature.jpg
|
||||
---
|
||||
|
||||
## Svelte
|
|
@ -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.
|
|
@ -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}"`);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
||||
```
|
|
@ -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
|
||||
|
||||
```
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
|
@ -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'
|
||||
});
|
||||
};
|
|
@ -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: Couldn’t 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('; ');
|
|
@ -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();
|
|
@ -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'}">`)
|
||||
})
|
||||
|
|
|
@ -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 |
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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';
|
||||
-->
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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" />
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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} />
|
|
@ -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>
|
|
@ -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>
|
|
@ -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 />
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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'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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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
|
||||
>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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: </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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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}
|
|
@ -0,0 +1,5 @@
|
|||
<div class="overflow-x-auto mb-4">
|
||||
<table class="table w-full">
|
||||
<slot />
|
||||
</table>
|
||||
</div>
|
|
@ -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}
|
|
@ -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' }
|
||||
];
|
|
@ -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 = {}
|
|
@ -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'
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
import type { PostConfig } from '$lib/types/post'
|
||||
|
||||
export const post: PostConfig = {}
|
|
@ -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'
|
||||
}
|
|
@ -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([])
|
|
@ -0,0 +1,2 @@
|
|||
import { writable } from 'svelte/store'
|
||||
export const title = writable({})
|
|
@ -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
Loading…
Reference in New Issue