Compare commits

..

19 Commits

Author SHA1 Message Date
matthieu42morin 2acd807a49 quickcards, mainpage refactor, content fill 2024-04-28 01:52:16 +02:00
matthieu42morin dc948269b9 Revert "custom shiki highlighter from dominikg"
This reverts commit 9b976986d7.
2024-04-28 01:42:44 +02:00
matthieu42morin 9b976986d7 custom shiki highlighter from dominikg 2024-04-28 01:39:24 +02:00
matthieu42morin c7c2ea60a2 blog server, page functions 2024-04-28 01:30:30 +02:00
matthieu42morin a89f69c69c blogs pages, projs - unfinished 2024-04-28 01:29:37 +02:00
matthieu42morin 35c507e55b delete old/unused components 2024-04-28 01:28:36 +02:00
matthieu42morin 82df5cdaa2 main layout 2slash 2024-04-28 01:24:52 +02:00
matthieu42morin 31fdc9a654 tags refactor to component 2024-04-28 01:24:07 +02:00
matthieu42morin 8c425f89c8 Individual blogPosts - Server + page + Layout 2024-04-28 01:23:33 +02:00
matthieu42morin 74987b8408 New API for posts ! best way to do this. 2024-04-28 01:21:34 +02:00
matthieu42morin dda41c8414 SEO revamped from RodneyLabs - TS support 2024-04-28 01:20:13 +02:00
matthieu42morin 6ce876ab03 Hero 2024-04-28 01:19:43 +02:00
matthieu42morin 0170618b67 Main - Footer, Header - responsive drawer 2024-04-28 01:19:28 +02:00
matthieu42morin 34f70f0412 blog utils & helpers 2024-04-28 01:18:49 +02:00
matthieu42morin f691a6b377 Layouts for feed and /blog 2024-04-28 01:18:02 +02:00
matthieu42morin fd3084fef7 cloudinary serverside images 2024-04-28 01:15:59 +02:00
matthieu42morin e49c5001c2 skills 2024-04-28 01:15:26 +02:00
matthieu42morin de6c8eb073 Obfuscated mail and socials 2024-04-28 01:14:47 +02:00
matthieu42morin 13c52582c4 2slash highlighter 2024-04-28 01:13:28 +02:00
238 changed files with 2740 additions and 4702 deletions

View File

@ -1,19 +0,0 @@
# General
NODE_ENV=development
URARA_SITE_DOMAIN=localhost
URARA_SITE_PROTOCOL=http://
PUBLIC_SITE_URL=localhost:5173
# Sentry
PUBLIC_SENTRY_ORG=mattmor
PUBLIC_SENTRY_PROJECT=itspersonal
PUBLIC_SENTRY_KEY=cc0a2e656e0cbbcade519f24627044df
PUBLIC_SENTRY_PROJECT_ID=4506781187899392
PUBLIC_SENTRY_ORG_ID=o4505828687478784
SENTRY_AUTH_TOKEN="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"
SENTRT_LOG_LEVEL=debug
# Cloudinary
PUBLIC_CLOUDINARY_NAME=dbex8wss6
CLOUDINARY_API_KEY=998888477457534
CLOUDINARY_API_SECRET=E0-FptJ1rFiyxe9Z9F6SEr9CJns

14
.editorconfig Normal file
View File

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

View File

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

2
.gitattributes vendored
View File

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

View File

@ -1,32 +0,0 @@
name: 'Publish to Netlify'
on:
push:
branches: [ prod, deploy ]
pull_request:
branches: [ prod, deploy ]
jobs:
publish:
timeout-minutes: 10
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
name: Checkout
- name: Build
run: |
pnpm --version && node --version
pnpm ci
pnpm run build
- name: Deploy to Netlify
uses: nwtgck/actions-netlify@v3.0
with:
publish-dir: './dist'
production-branch: master
github-token: ${{ secrets.GITHUB_TOKEN }}
deploy-message: "Deploy from GitHub Actions"
enable-pull-request-comment: false
enable-commit-comment: true
overwrites-pull-request-comment: true
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
timeout-minutes: 1

View File

@ -1,21 +0,0 @@
name: Vercel Preview Deployment
env:
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
on:
push:
branches-ignore:
- main
jobs:
Deploy-Preview:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install Vercel CLI
run: npm install --global vercel@latest
- name: Pull Vercel Environment Information
run: vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }}
- name: Build Project Artifacts
run: vercel build --token=${{ secrets.VERCEL_TOKEN }}
- name: Deploy Project Artifacts to Vercel
run: vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }}

View File

@ -1,21 +0,0 @@
name: Vercel Production Deployment
env:
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
on:
push:
branches:
- main
jobs:
Deploy-Production:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install Vercel CLI
run: npm install --global vercel@latest
- name: Pull Vercel Environment Information
run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
- name: Build Project Artifacts
run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
- name: Deploy Project Artifacts to Vercel
run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}

View File

@ -1,35 +0,0 @@
# 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
```

View File

@ -1,32 +0,0 @@
name: 'Publish to Netlify'
on:
push:
branches: [ prod, deploy ]
pull_request:
branches: [ prod, deploy ]
jobs:
publish:
timeout-minutes: 10
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
name: Checkout
- name: Build
run: |
pnpm --version && node --version
pnpm ci
pnpm run build
- name: Deploy to Netlify
uses: nwtgck/actions-netlify@v3.0
with:
publish-dir: './build'
production-branch: deploy
github-token: ${{ secrets.GH_TOKEN }}
deploy-message: "Deploy from Gitea Actions"
enable-pull-request-comment: false
enable-commit-comment: true
overwrites-pull-request-comment: true
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
timeout-minutes: 5

13
.gitignore vendored
View File

@ -1,23 +1,12 @@
# temp file
src/routes/**/+page.svelte.md
src/routes/**/+page.md
src/static
urara.js
# build
+ .turbo + .turbo
+ build/** + build/**
+ dist/** + dist/**
dist dist
dist-ssr dist-ssr
build
static
.svelte-kit
.netlify
.vercel
.DS_Store .DS_Store
node_modules node_modules
/build
/.svelte-kit /.svelte-kit
/package /package
/lambda/ /lambda/

2
.npmrc
View File

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

1
.nvmrc
View File

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

View File

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

9
.prettierrc Normal file
View File

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

View File

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

View File

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

114
.vscode/settings.json vendored
View File

@ -1,10 +1,112 @@
{ {
"editor.formatOnSave": true, "editor.formatOnSave": true,
"files.eol": "\n", "svelte.plugin.typescript.format.enable": true, // enable Svelte formatter for TypeScript in Svelte files
"typescript.tsdk": "node_modules\\typescript\\lib", "svelte.plugin.svelte.format.enable": true, // enable Svelte formatter for HTML and CSS in Svelte files
"css.lint.unknownAtRules": "ignore", "[svelte]": {
"svelte.plugin.css.diagnostics.enable": false, "editor.defaultFormatter": "svelte.svelte-vscode" // enable Prettier formatter for JavaScript files
"[html]": { },
"editor.defaultFormatter": "esbenp.prettier-vscode"
"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"
]
} }

29
Dockerfile Normal file
View File

@ -0,0 +1,29 @@
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" ]

View File

@ -1,4 +1,4 @@
Copyright (c) 2023-2024 Matt Morin & importantimport Copyright (c) 2023-2024 Matt Morin
Permission is hereby granted, free of charge, to any person obtaining Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the a copy of this software and associated documentation files (the

125
README.md
View File

@ -1,6 +1,5 @@
# Hello world, this is my personal site # Hello world, this is my personal site
I recently switched to Urara, an incredibly well made template, because although my site was beautiful and worked, it was an immense hassle to get it working and I couldn't fix some issues with mdsvex. I found Urara while looking for code highlighting and it has the same and more features than my completely own work. You should see my previous site at a backup branch. It should be able to be run in dev, but typography from tailwindcss is not working with articles...
Featuring a blog, projects, current social accounts, skills and so on and so on, look at [Technical Features](#technical-features) Featuring a blog, projects, current social accounts, skills and so on and so on, look at [Technical Features](#technical-features)
## Stack info ## Stack info
@ -29,127 +28,3 @@ AWS lambda for automation
This project is using [Skeleton Labs UI / Component / utils Library](https://www.skeleton.dev/) for sveltekit. 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. 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/] I have learned some svelte tricks used here from [Matt Croat](https://matia.xyz) alias [https://joyofcode.xyz/]
<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">🚀 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>
## 🎉 Try it now!
### Local
```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
```
### Remote
[![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=PUBLIC_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#PUBLIC_SITE_URL=https://example.com&CUSTOM_LOGO=https://github.com/importantimport/urara/raw/main/urara/assets/any@512.png)
## ⚡️ Usage
### Developing
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)

View File

@ -1,77 +1,44 @@
// rehype plugins import { defineMDSveXConfig as defineConfig } from 'mdsvex';
import rehypeSlug from 'rehype-slug' import rehypeExternalLinks from 'rehype-external-links';
import rehypeAutolinkHeadings from 'rehype-autolink-headings' import rehypeAutolinkHeadings from 'rehype-autolink-headings';
import rehypeExternalLinks from 'rehype-external-links' import readingTime from 'mdsvex-reading-time';
import remarkUnwrapImages from 'remark-unwrap-images';
// urara remark plugins import remarkToc from 'remark-toc';
import { parse, join } from 'node:path' import rehypeSlug from 'rehype-slug';
import { visit } from 'unist-util-visit' import { visit } from 'unist-util-visit';
import { toString } from 'mdast-util-to-string' import { toString } from 'mdast-util-to-string';
import Slugger from 'github-slugger' import Slugger from 'github-slugger';
import remarkFFF from 'remark-fff' import remarkFFF from 'remark-fff';
import remarkFootnotes from 'remark-footnotes' import remarkFootnotes from 'remark-footnotes';
// import { shikiTwoslashHighlighter } from '@kitbook/mdsvex-shiki-twoslash';
// import createShikiHighlighter from './src/lib/utils/shiki-highlighter.js';
// const shiki = await createShikiHighlighter({
// theme: 'github-dark-dimmed',
// showLineNumbers: (n) => true
// });
// highlighter // highlighter
import { escapeSvelte } from 'mdsvex' import { escapeSvelte } from 'mdsvex';
import { lex, parse as parseFence } from 'fenceparser' import { lex, parse as parseFence } from 'fenceparser';
import { renderCodeToHTML, runTwoSlash, createShikiHighlighter } from 'shiki-twoslash' import { renderCodeToHTML, runTwoSlash, createShikiHighlighter } from 'shiki-twoslash';
import readingTime from 'mdsvex-reading-time'
const remarkUraraFm = /** @type {import('mdsvex').MdsvexOptions} */
() => const config = defineConfig({
(tree, { data, filename }) => { extensions: ['.svelte.md', '.md', '.svx'],
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
}
}
// 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
})
/** @type {import("mdsvex").MdsvexOptions} */
export default {
extensions: ['.svelte.md', '.md'],
smartypants: { smartypants: {
dashes: 'oldschool' dashes: 'oldschool'
}, },
layout: { // layout: {
_: './src/lib/components/blog/post_layout.svelte' // _: './src/lib/components/blog/Post.svelte'
}, // },
highlight: { highlight: {
highlighter: async (code, lang, meta) => { highlighter: async (code, lang, meta) => {
let fence, twoslash let fence, twoslash;
try { try {
fence = parseFence(lex([lang, meta].filter(Boolean).join(' '))) fence = parseFence(lex([lang, meta].filter(Boolean).join(' ')));
} catch (error) { } catch (error) {
throw new Error(`Could not parse the codefence for this code sample \n${code}`) throw new Error(`Could not parse the codefence for this code sample \n${code}`);
} }
if (fence?.twoslash === true) twoslash = runTwoSlash(code, lang) if (fence?.twoslash === true) twoslash = runTwoSlash(code, lang);
return `{@html \`${escapeSvelte( return `{@html \`${escapeSvelte(
renderCodeToHTML( renderCodeToHTML(
code, code,
@ -81,10 +48,44 @@ export default {
await createShikiHighlighter({ theme: 'github-dark-dimmed' }), await createShikiHighlighter({ theme: 'github-dark-dimmed' }),
twoslash twoslash
) )
)}\` }` )}\` }`;
} }
}, },
// highlight: {
// highlighter: shiki
// },
/* 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],
[rehypeAutolinkHeadings, { behavior: 'wrap' }],
[
rehypeExternalLinks,
{
rel: ['nofollow', 'noopener', 'noreferrer', 'external'],
target: '_blank'
}
]
// [
// /** 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: [ remarkPlugins: [
[remarkToc, { maxDepth: 3, tight: true }],
[ [
remarkFFF, remarkFFF,
{ {
@ -92,7 +93,7 @@ export default {
target: 'mdsvex', target: 'mdsvex',
autofill: { autofill: {
provider: 'fs', provider: 'fs',
path: path => path.replace('/src/routes/', '/urara/') path: (path) => path.replace('/src/routes/', '/urara/')
}, },
strict: { strict: {
media: { media: {
@ -103,19 +104,25 @@ export default {
} }
], ],
[readingTime, { wpm: 200 }], [readingTime, { wpm: 200 }],
remarkUraraFm,
remarkUraraSpoiler,
[remarkFootnotes, { inlineNotes: true }] [remarkFootnotes, { inlineNotes: true }]
], // [
rehypePlugins: [ // headings,
rehypeSlug, // {
[rehypeAutolinkHeadings, { behavior: 'wrap' }], // behavior: 'append',
[ // linkProperties: {},
rehypeExternalLinks, // content: function (node) {
{ // return [
rel: ['nofollow', 'noopener', 'noreferrer', 'external'], // h('span.icon.icon-link header-anchor', {
target: '_blank' // ariaLabel: toString(node) + ' permalink'
} // })
// ];
// }
// }
// ],
// remarkHeadingsPermaLinks,
// getHeadings
] ]
] });
}
export default config;

View File

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

View File

@ -1,98 +1,104 @@
{ {
"name": "its-personal", "name": "its-personal~portfolio",
"type": "module", "description": "Uses pnpm, svelte, mdsvex",
"version": "2.0.0", "version": "1.0.0",
"license": "WTFPL", "main": "index.js",
"repository": "Madmin/its-personal", "private": true,
"homepage": "https://git.mattmor.in/Madmin/its-personal", "homepage": "mattmor.in",
"bugs": "https://git.mattmor.in/Madmin/its-personal/issues", "author": "Matthieu Morin",
"author": "Madmin", "license": "MIT",
"packageManager": "pnpm@8.14.1", "repository": {
"type": "git",
"url": "https://git.mattmor.in/Madmin/its-personal/"
},
"packageManager": "pnpm@8.6.6",
"scripts": { "scripts": {
"clean": "node urara.js clean", "dev": "vite dev",
"tsc": "tsc -p tsconfig.node.json", "build": "vite build",
"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", "preview": "vite preview",
"start": "cross-env ADAPTER=node pnpm build && node build", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check": "svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch", "test": "vitest",
"lint": "prettier --check --plugin-search-dir=. . && eslint --ignore-path .gitignore .", "lint": "prettier --plugin-search-dir . --check . && eslint .",
"format": "prettier --write --plugin-search-dir=. ." "format": "prettier --plugin-search-dir . --write .",
"generate:images": "node ./generate-responsive-image-data.js",
"test-ct": "playwright test -c playwright-ct.config.ts"
}, },
"devDependencies": { "devDependencies": {
"@iconify-json/heroicons-outline": "^1.1.10", "@bitmachina/highlighter": "1.0.0-alpha.5",
"@iconify-json/heroicons-solid": "^1.1.11", "@fontsource/cooper-hewitt": "^5.0.11",
"@iconify-json/simple-icons": "^1.1.100", "@fontsource/fira-mono": "^5.0.13",
"@sveltejs/adapter-auto": "^3.1.1", "@kitbook/mdsvex-shiki-twoslash": "1.0.0-beta.31",
"@sveltejs/adapter-netlify": "^4.1.0", "@playwright/test": "^1.41.2",
"@sveltejs/adapter-node": "^4.0.1", "@skeletonlabs/skeleton": "2.0.0",
"@sveltejs/adapter-static": "^3.0.1", "@skeletonlabs/tw-plugin": "0.1.0",
"@sveltejs/adapter-vercel": "^5.1.0",
"@sveltejs/kit": "^2.5.0", "@sveltejs/kit": "^2.5.0",
"@sveltejs/vite-plugin-svelte": "^3.0.2", "@tailwindcss/forms": "0.5.6",
"@tailwindcss/typography": "^0.5.10", "@tailwindcss/typography": "0.5.9",
"@types/node": "^20.11.17", "@types/js-cookie": "^3.0.6",
"@types/unist": "^3.0.2", "@types/node": "20.5.7",
"@typescript-eslint/eslint-plugin": "^6.21.0", "@types/object-hash": "^3.0.6",
"@typescript-eslint/parser": "^6.21.0", "@types/prismjs": "^1.26.3",
"@unocss/extractor-svelte": "^0.55.7", "@typescript-eslint/eslint-plugin": "^5.62.0",
"@vite-pwa/sveltekit": "^0.1.3", "@typescript-eslint/parser": "^5.62.0",
"autoprefixer": "^10.4.19", "autoprefixer": "10.4.15",
"chalk": "^5.3.0", "cloudinary": "^2.2.0",
"chokidar": "^3.6.0", "emoji-regex": "^10.3.0",
"cross-env": "^7.0.3",
"daisyui": "^4.6.2",
"eslint": "^8.56.0", "eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^8.10.0",
"eslint-plugin-svelte": "^2.35.1", "eslint-plugin-svelte": "^2.35.1",
"fenceparser": "^2.2.0", "fenceparser": "^2.2.0",
"fff-flavored-frontmatter": "1.0.0-alpha.1",
"github-slugger": "^2.0.0", "github-slugger": "^2.0.0",
"mdast-util-to-string": "^3.2.0", "js-cookie": "^3.0.5",
"mdsvex": "^0.11.0", "mdast-util-to-string": "^4.0.0",
"mdsvex-reading-time": "^1.0.4", "mdsvex-reading-time": "^1.0.4",
"npm-run-all": "^4.1.5", "object-hash": "^3.0.0",
"postcss": "^8.4.35", "postcss": "8.4.29",
"postcss-lightningcss": "^0.7.0", "prettier": "^2.8.8",
"prettier": "^3.2.5", "prettier-plugin-svelte": "^2.10.1",
"prettier-plugin-svelte": "^3.1.2",
"reading-time": "^1.5.0", "reading-time": "^1.5.0",
"rehype-autolink-headings": "^6.1.1", "rehype-autolink-headings": "^7.1.0",
"rehype-external-links": "^2.1.0", "rehype-external-links": "^3.0.0",
"rehype-slug": "^5.1.0", "rehype-img-size": "^1.0.1",
"remark": "^14.0.3", "rehype-slug": "^6.0.0",
"remark-fff": "1.0.0-alpha.1", "remark-fff": "^1.2.1",
"remark-footnotes": "~2.0.0", "remark-footnotes": "^4.0.1",
"remark-toc": "^9.0.0",
"remark-unwrap-images": "^4.0.0",
"sass": "^1.71.0",
"shiki": "^1.3.0",
"shiki-themes": "^0.2.7",
"shiki-twoslash": "^3.1.2", "shiki-twoslash": "^3.1.2",
"svelte": "^4.2.10", "svelte": "^4.2.11",
"svelte-check": "^3.6.4", "svelte-check": "^3.6.4",
"svelte-eslint-parser": "^0.33.1", "tailwind-merge": "^2.2.2",
"svelte-preprocess": "^5.1.3", "tailwindcss": "3.3.3",
"sveltekit-embed": "^0.0.14",
"tailwindcss": "^3.4.1",
"tslib": "^2.6.2", "tslib": "^2.6.2",
"typescript": "^5.3.3", "typescript": "^5.3.3",
"unist-util-visit": "^4.1.2", "unist-util-visit": "^5.0.0",
"unocss": "^0.58.5", "vite": "^5.1.3",
"vite": "^5.1.1", "vite-imagetools": "^7.0.1",
"vite-imagetools": "^4.0.19", "vite-plugin-tailwind-purgecss": "0.2.0",
"vite-plugin-pwa": "^0.17.5", "vitest": "^0.34.6"
"workbox-build": "^7.0.0",
"workbox-window": "^7.0.0"
}, },
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free": "^6.5.2", "@floating-ui/dom": "1.5.1",
"@sentry/sveltekit": "^7.112.2", "@fortawesome/fontawesome-free": "^6.5.1",
"@sveltejs/adapter-cloudflare": "^4.4.0", "@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", "@yushijinhun/three-minifier-rollup": "^0.4.0",
"vite-plugin-tailwind-purgecss": "^0.3.3", "front-matter": "^4.0.2",
"vitest": "^1.5.2" "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",
"vanilla-lazyload": "^19.1.3"
},
"type": "module"
} }

77
playwright.config.ts Normal file
View File

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

View File

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

101
src/app.d.ts vendored
View File

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

View File

@ -1,30 +1,22 @@
<!doctype html> <!DOCTYPE html>
<html lang="en"> <html lang="en" class="dark">
<head prefix="og: https://ogp.me/ns#"> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="generator" content="gh:importantimport/urara" /> <meta name="viewport" content="width=device-width,initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <link rel="icon" href="/images/profile-pic.png" />
<link rel="apple-touch-icon" sizes="180x180" href="%sveltekit.assets%/assets/apple-touch-icon.png" /> <!-- <link
<link rel="icon" href="%sveltekit.assets%/assets/maskable@192.png" /> rel="icon"
<link rel="manifest" crossorigin="use-credentials" href="/manifest.webmanifest" /> 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="alternate" type="application/feed+json" href="/feed.json" /> /> -->
<link rel="alternate" type="application/atom+xml" href="/atom.xml" /> <link rel="preload" href="/fonts/Quicksand.ttf" as="font" type="font/ttf" crossorigin />
<link rel="sitemap" type="application/xml" href="/sitemap.xml" /> <!-- Dropin replacement for FontAwesome-->
<!-- <link
href="https://cdn.jsdelivr.net/npm/ficons@1.1.52/dist/ficons/font.css"
rel="stylesheet"
/> -->
%sveltekit.head% %sveltekit.head%
</head> </head>
<body itemscope itemtype="https://schema.org/WebPage" data-sveltekit-prefetch> <body data-sveltekit-preload-data="hover" data-theme="wintry">
<script> <div style="display: contents" class="h-full overflow-hidden">%sveltekit.body%</div>
const themeLocalStorageKey = 'theme'
if (
!!localStorage.getItem(themeLocalStorageKey)
? localStorage.getItem(themeLocalStorageKey) === 'dark'
: window.matchMedia('(prefers-color-scheme: dark)').matches
) {
document.body.classList.add('dark')
} else {
document.body.classList.add('light')
}
</script>
<div style="display: contents">%sveltekit.body%</div>
</body> </body>
</html> </html>

View File

@ -1,262 +0,0 @@
/* tailwind */
@tailwind base;
@tailwind components;
@tailwind utilities;
/* global */
html {
@apply !bg-base-200 scroll-smooth overflow-x-hidden overflow-y-scroll;
}
/* ############ FONTS ############ */
/* ############ HEADINGS ############ */
h1 {
@apply text-6xl font-black;
}
h2 {
@apply text-5xl font-extrabold;
}
h3 {
@apply text-4xl font-bold;
}
h4 {
@apply text-3xl font-semibold;
}
h5 {
@apply text-2xl font-medium;
}
h6 {
@apply text-xl font-normal;
}
/* ############ HEADINGS ############ */
@font-face {
font-family: 'Orbitron-Variable';
src:
url('/assets/fonts/Orbitron-VariableFont_wght.woff') format('woff'),
url('/assets/fonts/Orbitron-VariableFont_wght.ttf') format('truetype');
font-weight: 300 900;
font-display: swap;
font-style: normal;
}
@font-face {
font-family: 'RobotoMono-Italic-Variable';
src:
url('/assets/fonts/RobotoMono-Italic-VariableFont_wght.woff') format('woff'),
url('/assets/fonts/RobotoMono-Italic-VariableFont_wght.ttf') format('truetype');
font-weight: 300 900;
font-display: swap;
font-style: normal;
}
@font-face {
font-family: 'RobotoMono-VariableFont';
src:
url('/assets/fonts/RobotoMono-VariableFont_wght.woff') format('woff'),
url('/assets/fonts/RobotoMono-VariableFont_wght.ttf') format('truetype');
font-weight: 300 900;
font-display: swap;
font-style: italic;
}
/* ############ FONTS ############ */
/* ############ FONTS CONFIG ############ */
/* Normal text */
body {
@apply font-['RobotoMono-VariableFont'];
}
/* Italic text outside prose */
em {
@apply font-['RobotoMono-Italic-Variable'];
}
/* Prose text */
.urara-prose {
@apply font-['RobotoMono-VariableFont'];
}
/* Italic text */
.urara-prose em {
@apply font-['RobotoMono-Italic-Variable'];
}
/* ############ FONTS CONFIG ############ */
::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 */

53
src/app.postcss Normal file
View File

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

View File

@ -1,19 +1,19 @@
export type Level = 'A' | 'B' | 'C' // A: Proficient, B: Experienced, C: limited Experience export type Level = 'A' | 'B' | 'C'; // A: Proficient, B: Experienced, C: limited Experience
export interface Skill { export interface Skill {
title: string title: string;
level: Level level: Level;
} }
export interface SubCategory { export interface SubCategory {
title: string title: string;
level: number level: number;
skills: Skill[] skills: Skill[];
} }
export interface Category { export interface Category {
icon: string icon: string;
title: string title: string;
level: number level: number;
subCategories: SubCategory[] subCategories: SubCategory[];
} }
// prettier-ignore // prettier-ignore
const list: Category[] = [ const list: Category[] = [
@ -42,7 +42,7 @@ const list: Category[] = [
{ title:'Testing & Validation', level: 50, skills: [ { title:'Testing & Validation', level: 50, skills: [
{ title: 'ajv', level: 'A' }, { title: 'ajv', level: 'A' },
{ title: 'Playwright', level: 'B'}, { title: 'Playwright', level: 'B'},
{ title: 'node', level: 'B'}, { title: 'SEO, performance optimizations', level: 'B'},
]}, ]},
]}, ]},
{ icon: '🔁🔁', title:'DevOps', level: 70, subCategories: [ { icon: '🔁🔁', title:'DevOps', level: 70, subCategories: [
@ -80,7 +80,7 @@ const list: Category[] = [
{ title:'Vercel', level: 100, skills: []}, { title:'Vercel', level: 100, skills: []},
{ title:'DigitalOcean', level: 100, skills: []}, { title:'DigitalOcean', level: 100, skills: []},
]}, ]},
{ icon: '💻⚙️', title:'System Administration', level: 75, subCategories: [ { icon: '🔧💻🔒⚙️', title:'System Administration', level: 75, subCategories: [
{ title:'Operating Systems', level: 80, skills: [ { title:'Operating Systems', level: 80, skills: [
{ title: 'Debian / Ubuntu', level: 'A' }, { title: 'Debian / Ubuntu', level: 'A' },
{ title: 'Nix(OS)', level: 'B' }, { title: 'Nix(OS)', level: 'B' },
@ -103,7 +103,7 @@ const list: Category[] = [
{ title: 'AWS Secrets Manager', level: 'B' }, { title: 'AWS Secrets Manager', level: 'B' },
]} ]}
]}, ]},
{ icon: '🔧', title:"Some fun geek skillz", level: 70, subCategories: [ { icon: '', title:"Some fun geek skillz", level: 70, subCategories: [
{ title:'mini hardware', level: 80, skills: [ { title:'mini hardware', level: 80, skills: [
{ title: 'Raspberry Pi', level: 'A' }, { title: 'Raspberry Pi', level: 'A' },
{ title: 'ESP8266, ESP32', level: 'A' }, { title: 'ESP8266, ESP32', level: 'A' },
@ -119,13 +119,13 @@ const list: Category[] = [
{ title: 'Ultimaker Cura', level: 'B' }, { title: 'Ultimaker Cura', level: 'B' },
]}, ]},
]}, ]},
{ icon: '💬', title:'Languages', level: 70, subCategories: [ { icon: '🤐💬', title:'Languages', level: 70, subCategories: [
{ title: 'English', level: 'A' }, { title: 'English', level: 'A' },
{ title: 'Czech', level: 'A' }, { title: 'Czech', level: 'A' },
{ title: 'French', level: 'B' }, { title: 'French', level: 'B' },
{ title: 'German', level: 'C' } { title: 'German', level: 'C' }
]}, ]},
{ icon: '🎨', title:'Design', level: 70, subCategories: [ { icon: '🎨✏️📐', title:'Design', level: 70, subCategories: [
{ title: 'UI/UX', level: 'B' }, { title: 'UI/UX', level: 'B' },
{ title: 'LaTeX', level: 'C' }, { title: 'LaTeX', level: 'C' },
{ title: 'Wireframing, Prototyping, Diagramming', level: 'B', skills: [ { title: 'Wireframing, Prototyping, Diagramming', level: 'B', skills: [
@ -143,4 +143,4 @@ const list: Category[] = [
]} ]}
]; ];
export default list export default list;

View File

@ -1,10 +1,19 @@
// https://gist.github.com/acoyfellow/d8e86979c66ebea25e1643594e38be73, Rodney Lab // https://gist.github.com/acoyfellow/d8e86979c66ebea25e1643594e38be73, Rodney Lab
import { site } from '$lib/config/site'
import { PUBLIC_SENTRY_KEY, PUBLIC_SENTRY_PROJECT_ID, PUBLIC_SENTRY_ORG_ID } from '$env/static/public' 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 = { const directives = {
'base-uri': ["'self'"], 'base-uri': ["'self'"],
'child-src': ["'self'", 'blob:'], 'child-src': ["'self'", 'blob:'],
// 'connect-src': ["'self'", 'ws://localhost:*'],
'connect-src': [ 'connect-src': [
"'self'", "'self'",
'ws://localhost:*', 'ws://localhost:*',
@ -12,9 +21,10 @@ const directives = {
'https://hcaptcha.com', 'https://hcaptcha.com',
'https://*.hcaptcha.com', 'https://*.hcaptcha.com',
'https://*.cartocdn.com', 'https://*.cartocdn.com',
'https://*.mattmor.in/**' PUBLIC_DOMAIN,
PUBLIC_WORKER_URL
], ],
'img-src': ["'self'", 'data:', 'https://images.unsplash.com', `${site.protocol}${site.domain}`], 'img-src': ["'self'", 'data:', 'https://images.unsplash.com'],
'font-src': ["'self'", 'data:'], 'font-src': ["'self'", 'data:'],
'form-action': ["'self'"], 'form-action': ["'self'"],
'frame-ancestors': ["'self'"], 'frame-ancestors': ["'self'"],
@ -35,8 +45,8 @@ const directives = {
'style-src': ["'self'", "'unsafe-inline'", 'https://hcaptcha.com', 'https://*.hcaptcha.com'], 'style-src': ["'self'", "'unsafe-inline'", 'https://hcaptcha.com', 'https://*.hcaptcha.com'],
'default-src': [ 'default-src': [
"'self'", "'self'",
site.domain, rootDomain,
`ws://${site.domain}`, `ws://${rootDomain}`,
// 'https://*.google.com', // 'https://*.google.com',
// 'https://*.googleapis.com', // 'https://*.googleapis.com',
// 'https://*.firebase.com', // 'https://*.firebase.com',
@ -66,8 +76,8 @@ const directives = {
'report-uri': [ 'report-uri': [
`https://${PUBLIC_SENTRY_ORG_ID}.ingest.us.sentry.io/api/${PUBLIC_SENTRY_PROJECT_ID}/security/?sentry_key=${PUBLIC_SENTRY_KEY}` `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) export const csp = Object.entries(directives)
.map(([key, arr]) => key + ' ' + arr.join(' ')) .map(([key, arr]) => key + ' ' + arr.join(' '))
.join('; ') .join('; ');

View File

@ -1,6 +1,10 @@
import { handleErrorWithSentry, replayIntegration } from '@sentry/sveltekit' import { handleErrorWithSentry, replayIntegration } from '@sentry/sveltekit';
import * as Sentry 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 {
PUBLIC_SENTRY_KEY,
PUBLIC_SENTRY_PROJECT_ID,
PUBLIC_SENTRY_ORG_ID
} from '$env/static/public';
Sentry.init({ Sentry.init({
dsn: `https://${PUBLIC_SENTRY_KEY}@${PUBLIC_SENTRY_ORG_ID}.ingest.us.sentry.io/${PUBLIC_SENTRY_PROJECT_ID}`, dsn: `https://${PUBLIC_SENTRY_KEY}@${PUBLIC_SENTRY_ORG_ID}.ingest.us.sentry.io/${PUBLIC_SENTRY_PROJECT_ID}`,
@ -16,7 +20,7 @@ Sentry.init({
// If you don't want to use Session Replay, just remove the line below: // If you don't want to use Session Replay, just remove the line below:
integrations: [replayIntegration()] integrations: [replayIntegration()]
}) });
// If you have a custom error handler, pass it to `handleErrorWithSentry` // If you have a custom error handler, pass it to `handleErrorWithSentry`
export const handleError = handleErrorWithSentry() export const handleError = handleErrorWithSentry();

View File

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

7
src/index.test.ts Normal file
View File

@ -0,0 +1,7 @@
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.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

View File

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

View File

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

View File

@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<!--! Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. -->
<path class="fill-current text-base-content"
d="M48 64C21.5 64 0 85.5 0 112c0 15.1 7.1 29.3 19.2 38.4L236.8 313.6c11.4 8.5 27 8.5 38.4 0L492.8 150.4c12.1-9.1 19.2-23.3 19.2-38.4c0-26.5-21.5-48-48-48H48zM0 176V384c0 35.3 28.7 64 64 64H448c35.3 0 64-28.7 64-64V176L294.4 339.2c-22.8 17.1-54 17.1-76.8 0L0 176z" />
</svg>

Before

Width:  |  Height:  |  Size: 602 B

View File

@ -1,23 +1,24 @@
<script lang="ts"> <script lang="ts">
import Envelope from '$lib/components/Envelope.svelte' import socials from '$lib/socialsObjects';
export let mail: string; export let mail: string = socials[0].href;
export let clazz: string = ''; export let clazz: string = '';
export let w: number = 24; export let iconClazz: string = '';
export let h: number = 24; export let h: number = 16;
export let w: number = 16;
</script> </script>
<div class={` ${clazz}`}> <div class={` ${clazz}`}>
<svg xmlns="http://www.w3.org/2000/svg" lang="en-GB" aria-labelledby="title" class="w-{w} h-{h}"> <svg xmlns="http://www.w3.org/2000/svg" lang="en-GB" aria-labelledby="title">
<title id="title">Send me a mail!</title> <title id="title">Send me a mail!</title>
<defs /> <defs />
<a href="mailto:{mail}" target="_blank" rel="noreferrer" aria-label="Send me a mail!"> <a href="mailto:{mail}" target="" rel="noreferrer" aria-label="Send me a mail!">
<rect class="fill-current text-transparent" width="100%" height="100%" /> <rect class="fill-current text-transparent" width="100%" height="100%" />
<foreignObject x="0" y="0" width="100%" height="100%"> <foreignObject x="0" y="0" width="100%" height="100%">
<div class="email-icon-wrapper"> <div class="email-icon-wrapper">
<Envelope /> <i class={`${iconClazz} `} />
</div> </div>
</foreignObject> </foreignObject>
</a> </a>

View File

@ -0,0 +1,53 @@
<script lang="ts">
import { website } from '$lib/config';
import type { Tag } from '$lib/types/post';
export let article = false;
export let datePublished: string | null = null;
export let lastUpdated: string | null = null;
export let featuredImage: string;
export let featuredImageAlt: string;
export let squareImage: string;
export let metadescription: string;
export let ogLanguage: string;
export let pageTitle: string;
export let siteTitle: string;
export let url: string;
export let tags: Tag[];
</script>
<svelte:head>
<meta property="og:site_name" content={siteTitle} />
<meta property="og:locale" content={ogLanguage} />
<meta property="og:url" content={url} />
<meta property="og:type" content={article ? 'article' : 'website'} />
<meta property="og:title" content={pageTitle} />
{#if metadescription}
<meta property="og:description" content={metadescription} />
{:else}
<meta name="og:description" content={website.description} />
{/if}
{#if featuredImage}
<meta property="og:image" content={featuredImage} />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="627" />
<meta property="og:image:alt" content={featuredImageAlt} />
{/if}
{#if squareImage}
<meta property="og:image" content={squareImage} />
<meta property="og:image:width" content="400" />
<meta property="og:image:height" content="400" />
<meta property="og:image:alt" content={featuredImageAlt} />
{/if}
{#if article}
<!-- <meta property="article:publisher" content={facebookPage} />
<meta property="article:author" content={facebookAuthorPage} /> -->
<meta property="article:published_time" content={datePublished} />
<meta property="article:modified_time" content={lastUpdated} />
{/if}
{#if tags && tags.length > 0}
{#each tags as tag (tag)}
<meta property="article:tag" content={tag} />
{/each}
{/if}
</svelte:head>

View File

@ -0,0 +1,207 @@
<script lang="ts">
import hash from 'object-hash';
export let article = false;
export let author;
export let breadcrumbs: { name: string; slug: string }[] = [];
export let datePublished;
export let entity;
export let lastUpdated;
export let featuredImage;
export let metadescription;
export let siteLanguage;
export let siteTitle;
export let siteTitleAlt;
export let siteUrl = '';
export let title;
export let url;
export let githubPage;
export let linkedinProfile;
export let telegramUsername;
export let twitterUsername;
/**
* @type {{ url: string; faviconWidth: number; faviconHeight: number } | null}
*/
export let entityMeta = null;
const entityHash = hash({ author }, { algorithm: 'md5' });
const schemaOrgEntity =
entityMeta !== null
? {
'@type': ['Person', 'Organization'],
'@id': `${siteUrl}/#/schema/person/${entityHash}`,
name: author,
image: {
'@type': 'ImageObject',
'@id': `${siteUrl}/#personlogo`,
inLanguage: siteLanguage,
url: entityMeta.url,
width: entityMeta.faviconWidth,
height: entityMeta.faviconHeight,
caption: author
},
logo: {
'@id': `${siteUrl}/#personlogo`
},
sameAs: [
`https://twitter.com/${twitterUsername}`,
`https://github.com/${githubPage}`,
`https://t.me/${telegramUsername}`,
`https://linkedin.com/in/${linkedinProfile}`
]
}
: null;
const schemaOrgWebsite = {
'@type': 'WebSite',
'@id': `${siteUrl}/#website`,
url: siteUrl,
name: siteTitle,
description: siteTitleAlt,
publisher: {
'@id': `${siteUrl}/#/schema/person/${entityHash}`
},
potentialAction: [
{
'@type': 'SearchAction',
target: `${siteUrl}/?s={search_term_string}`,
'query-input': 'required name=search_term_string'
}
],
inLanguage: siteLanguage
};
const schemaOrgImageObject = {
'@type': 'ImageObject',
'@id': `${url}#primaryimage`,
inLanguage: siteLanguage,
url: featuredImage.url,
contentUrl: featuredImage.url,
width: featuredImage.width,
height: featuredImage.height,
caption: featuredImage.caption
};
const schemaOrgBreadcrumbList = {
'@type': 'BreadcrumbList',
'@id': `${url}#breadcrumb`,
itemListElement: breadcrumbs.map((element, index) => ({
'@type': 'ListItem',
position: index + 1,
item: {
'@type': 'WebPage',
'@id': `${siteUrl}/${element.slug}`,
url: `${siteUrl}/${element.slug}`,
name: element.name
}
}))
};
const schemaOrgWebPage = {
'@type': 'WebPage',
'@id': `${url}#webpage`,
url,
name: title,
isPartOf: {
'@id': `${siteUrl}/#website`
},
primaryImageOfPage: {
'@id': `${url}#primaryimage`
},
datePublished,
dateModified: lastUpdated,
author: {
'@id': `${siteUrl}/#/schema/person/${entityHash}`
},
description: metadescription,
breadcrumb: {
'@id': `${url}#breadcrumb`
},
inLanguage: siteLanguage,
potentialAction: [
{
'@type': 'ReadAction',
target: [url]
}
]
};
let schemaOrgArticle = null;
if (article) {
schemaOrgArticle = {
'@type': 'Article',
'@id': `${url}#article`,
isPartOf: {
'@id': `${url}#webpage`
},
author: {
'@id': `${siteUrl}/#/schema/person/${entityHash}`
},
headline: title,
datePublished,
dateModified: lastUpdated,
mainEntityOfPage: {
'@id': `${url}#webpage`
},
publisher: {
'@id': `${siteUrl}/#/schema/person/${entityHash}`
},
image: {
'@id': `${url}#primaryimage`
},
articleSection: ['blog'],
inLanguage: siteLanguage
};
}
const schemaOrgPublisher = {
'@type': ['Person', 'Organization'],
'@id': `${siteUrl}/#/schema/person/${entityHash}`,
name: entity,
image: {
'@type': 'ImageObject',
'@id': `${siteUrl}/#personlogo`,
inLanguage: siteLanguage,
url: `${siteUrl}/assets/rodneylab-logo.png`,
contentUrl: `${siteUrl}/assets/rodneylab-logo.png`,
width: 512,
height: 512,
caption: entity
},
logo: {
'@id': `${siteUrl}/#personlogo`
},
sameAs: [
`https://twitter.com/${twitterUsername}`,
`https://github.com/${githubPage}`,
`https://t.me/${telegramUsername}`,
`https://linkedin.com/in/${linkedinProfile}`
]
};
const schemaOrgArray = [
schemaOrgEntity,
schemaOrgWebsite,
schemaOrgImageObject,
schemaOrgWebPage,
schemaOrgBreadcrumbList,
...(article ? [schemaOrgArticle] : []),
schemaOrgPublisher
];
const schemaOrgObject = {
'@context': 'https://schema.org',
'@graph': schemaOrgArray
};
let jsonLdString = JSON.stringify(schemaOrgObject);
let jsonLdScript = `
<script type="application/ld+json">
${jsonLdString}
${'<'}/script>
`;
</script>
<svelte:head>
{@html jsonLdScript}
</svelte:head>

View File

@ -0,0 +1,32 @@
<script>
export let article = false;
export let author;
export let twitterUsername;
export let image;
export let timeToRead = 0;
/*
* When there is an equivalent og tag present, Twitter takes that so check OpenGraph before
* adding additional tags, unless you want to override OpenGraph.
*/
</script>
<svelte:head>
<meta name="twitter:card" content="summary_large_image" />
{#if image}
<meta name="twitter:image" content={image.url} />
{/if}
{#if twitterUsername}
<meta name="twitter:creator" content={`@${twitterUsername}`} />
<meta name="twitter:site" content={`@${twitterUsername}`} />
{/if}
<meta name="twitter:label1" content="Written by" />
<meta name="twitter:data1" content={author} />
{#if article && timeToRead > 0}
<meta name="twitter:label2" content="Est. reading time" />
<meta
name="twitter:data2"
content={timeToRead !== 1 ? `${timeToRead} minutes` : '1 minute'}
/>
{/if}
</svelte:head>

View File

@ -0,0 +1,106 @@
<script lang="ts">
import defaultFeaturedImage from '$lib/assets/home/home.jpg';
import defaultOgImage from '$lib/assets/home/home-open-graph.jpg';
import defaultOgSquareImage from '$lib/assets/home/home-open-graph-square.jpg';
import defaultTwitterImage from '$lib/assets/home/home-twitter.jpg';
import { website } from '$lib/config';
import { VERTICAL_LINE_ENTITY } from '$lib/constants';
import OpenGraph from './OG.svelte';
import SchemaOrg from './SchemaOrg.svelte';
import Twitter from './Twitter.svelte';
import type { Tag } from '$lib/types/post';
const {
author,
entity,
ogLanguage,
siteLanguage,
siteShortTitle,
siteTitle,
siteUrl,
githubPage,
linkedinProfile,
telegramUsername,
twitterUsername
} = website;
export let article = false;
export let breadcrumbs: { name: string; slug: string }[] = [];
export let entityMeta = null;
export let lastUpdated;
export let datePublished;
export let metadescription: string;
export let tags: Tag[];
export let slug: string;
export let timeToRead = 0;
export let title: string;
const defaultAlt =
'picture of a person with long, curly hair, wearing a red had taking a picture with an analogue camera';
// imported props with fallback defaults
export let featuredImage: string;
export let featuredImageAlt: string | undefined = defaultAlt;
export let ogImage: string;
export let ogSquareImage: string;
export let twitterImage: string;
const url = `${siteUrl}/${slug}`;
const pageTitle = `${siteTitle} ${VERTICAL_LINE_ENTITY} ${title}`;
const openGraphProps = {
article,
datePublished,
lastUpdated,
image: ogImage,
squareImage: ogSquareImage,
metadescription,
ogLanguage,
pageTitle,
siteTitle,
url,
tags,
...(article ? { datePublished, lastUpdated } : {})
};
const schemaOrgProps = {
article,
author,
breadcrumbs,
datePublished,
entity,
lastUpdated,
entityMeta,
featuredImage,
metadescription,
siteLanguage,
siteTitle,
siteTitleAlt: siteShortTitle,
siteUrl,
title: pageTitle,
url,
githubPage,
linkedinProfile,
twitterUsername,
telegramUsername
};
const twitterProps = {
article,
author,
twitterUsername,
image: twitterImage,
timeToRead
};
</script>
<svelte:head>
<title>{pageTitle}</title>
<meta name="description" content={metadescription} />
<meta
name="robots"
content="index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1"
/>
<link rel="canonical" href={url} />
</svelte:head>
<Twitter {...twitterProps} />
<OpenGraph {...openGraphProps} />
<SchemaOrg {...schemaOrgProps} />

View File

@ -0,0 +1,36 @@
<script lang="ts">
import MatrixLogo from '$lib/components/logos/MatrixLogo.svelte';
import GiteaLogo from '$lib/components/logos/GiteaLogo.svelte';
import socials from '$lib/socialsObjects';
import ObfuscatedEmail from '$lib/components/ObfuscatedEmail.svelte';
</script>
<div class="flex flex-rows-auto gap-1 max-h-28">
{#each socials as link}
{#if link.title === 'Email'}
<ObfuscatedEmail
mail={socials[0].href}
clazz="logo-item w-[43px]"
h={48}
w={48}
iconClazz={socials[0].icon + ' text-3xl md:text-5xl'}
/>
{:else}
<a
class="logo-item"
href={link.title === 'Email' ? `mailto:${link.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>
{/if}
{/each}
</div>

View File

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

View File

@ -1,3 +0,0 @@
<a href="#post-comment" class="btn btn-lg btn-circle btn-ghost bg-base-100 shadow-lg hover:shadow-xl">
<span class="i-heroicons-outline-chat-alt-2" />
</a>

View File

@ -1,12 +0,0 @@
<script lang="ts">
import { site } from '$lib/config/site'
export let post: Urara.Post
</script>
<a
href={`https://www.addtoany.com/share#url=${site.protocol + site.domain + post.path}&title=${encodeURI(
post.title ?? post.path.slice(1)
)}`}
class="btn btn-lg btn-circle btn-ghost bg-base-100 shadow-lg hover:shadow-xl">
<span class="i-heroicons-outline-share" />
</a>

View File

@ -0,0 +1,3 @@
<article class="text-token prose prose-slate mx-auto dark:prose-invert lg:prose-lg">
<slot />
</article>

View File

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

View File

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

View File

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

View File

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

View File

@ -1,32 +0,0 @@
<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/blog/post_container.svelte'
// auto-generated
export let path
export let slug
export let toc
// common
export let created
export let updated
export let published
export let summary
export let tags
export let flags
// specify
export let title
export let image
export let in_reply_to
// post
let fm = { path, slug, toc, created, updated, published, summary, tags, flags, title, image, in_reply_to }
let post = { type: typeOfPost(fm), ...fm }
</script>
<Container {post}>
<slot />
</Container>

View File

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

View File

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

View File

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

View File

@ -1,81 +0,0 @@
<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'}"
role={`toc-link-${slug}`}
class:pl-4={depth <= 2}
class:pl-8={depth === 3}
class:pl-12={depth === 4}
class:pl-16={depth === 5}
class:pl-20={depth === 6}>
{title}
</span>
</li>
{/each}
</ul>
</nav>
</aside>

View File

@ -1,32 +0,0 @@
<!-- <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/blog/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> -->
<article class="text-token prose prose-slate mx-auto dark:prose-invert lg:prose-lg">
<slot />
</article>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,75 +0,0 @@
<script lang="ts">
import { onMount } from 'svelte'
export let user: string
export let repo: string
let info: {
html_url: string
description: string
homepage?: string
owner: { avatar_url: string }
stargazers_count: any
license?: { key?: any }
}
onMount(async () => {
info = await fetch(`https://api.github.com/repos/${user}/${repo}`).then(res => res.json())
})
</script>
<div class="card bg-base-100 !bg-base-200 my-4 ">
<div class="p-6">
{#if info}
<div class="flex">
<div class="flex-initial pr-4">
<div class="card-title mb-6 !text-3xl font-medium">
<a rel="noopener noreferrer external" target="_blank" class="no-underline" href={info.html_url}>
{user}/<span class="font-semibold">{repo}</span>
</a>
</div>
<p class="prose">
{info.description}
<br />
<a rel="noopener noreferrer external" target="_blank" href={info.homepage}>{info.homepage}</a>
</p>
</div>
<img class="w-20 h-20 mt-0 ml-auto mb-auto rounded-xl flex-initial" alt="owner_avatar" src={info.owner.avatar_url} />
</div>
<div class="card-actions -ml-2">
<button class="btn btn-sm btn-ghost">
<svg
aria-hidden="true"
viewBox="0 0 16 16"
version="1.1"
data-view-component="true"
class="inline-block w-4 h-4 mr-2 fill-current">
<path
fill-rule="evenodd"
d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25zm0 2.445L6.615 5.5a.75.75 0 01-.564.41l-3.097.45 2.24 2.184a.75.75 0 01.216.664l-.528 3.084 2.769-1.456a.75.75 0 01.698 0l2.77 1.456-.53-3.084a.75.75 0 01.216-.664l2.24-2.183-3.096-.45a.75.75 0 01-.564-.41L8 2.694v.001z" />
</svg>
{info.stargazers_count}
</button>
{#if info.license}
<a class="btn btn-sm btn-ghost" href="https://choosealicense.com/licenses/{info.license.key}">
<svg
aria-hidden="true"
viewBox="0 0 16 16"
version="1.1"
data-view-component="true"
class="inline-block w-4 h-4 mr-2 fill-current">
<path
fill-rule="evenodd"
d="M8.75.75a.75.75 0 00-1.5 0V2h-.984c-.305 0-.604.08-.869.23l-1.288.737A.25.25 0 013.984 3H1.75a.75.75 0 000 1.5h.428L.066 9.192a.75.75 0 00.154.838l.53-.53-.53.53v.001l.002.002.002.002.006.006.016.015.045.04a3.514 3.514 0 00.686.45A4.492 4.492 0 003 11c.88 0 1.556-.22 2.023-.454a3.515 3.515 0 00.686-.45l.045-.04.016-.015.006-.006.002-.002.001-.002L5.25 9.5l.53.53a.75.75 0 00.154-.838L3.822 4.5h.162c.305 0 .604-.08.869-.23l1.289-.737a.25.25 0 01.124-.033h.984V13h-2.5a.75.75 0 000 1.5h6.5a.75.75 0 000-1.5h-2.5V3.5h.984a.25.25 0 01.124.033l1.29.736c.264.152.563.231.868.231h.162l-2.112 4.692a.75.75 0 00.154.838l.53-.53-.53.53v.001l.002.002.002.002.006.006.016.015.045.04a3.517 3.517 0 00.686.45A4.492 4.492 0 0013 11c.88 0 1.556-.22 2.023-.454a3.512 3.512 0 00.686-.45l.045-.04.01-.01.006-.005.006-.006.002-.002.001-.002-.529-.531.53.53a.75.75 0 00.154-.838L13.823 4.5h.427a.75.75 0 000-1.5h-2.234a.25.25 0 01-.124-.033l-1.29-.736A1.75 1.75 0 009.735 2H8.75V.75zM1.695 9.227c.285.135.718.273 1.305.273s1.02-.138 1.305-.273L3 6.327l-1.305 2.9zm10 0c.285.135.718.273 1.305.273s1.02-.138 1.305-.273L13 6.327l-1.305 2.9z" />
</svg>
{info.license.key}
</a>
{/if}
<button class="btn btn-sm btn-circle btn-ghost ml-auto">
<span class="i-simple-icons-github" />
</button>
</div>
{/if}
</div>
</div>

View File

@ -1,44 +0,0 @@
<script lang="ts">
import type { Project } from '$lib/config/projects'
import Footer from '$lib/components/main/footer.svelte'
export let item: unknown
let project = item as unknown as Project
let tags = project.tags
</script>
{#if project.id === 'footer'}
<Footer rounded={true} class="max-w-4xl mx-auto p-4 md:p-8" />
{:else}
<a
id={project.id}
href={project.link}
class="card mx-auto max-w-4xl bg-base-100 shadow-xl transition-shadow mb-7 h-card u-url hover:shadow-2xl">
<div class="absolute text-5xl font-bold opacity-5 rotate-6 leading-tight top-2 right-0">
{project.feature}
</div>
<div class="card-body p-4">
<div class="flex flex-col md:flex-row items-start gap-4">
<div class="mb-auto aspect-video w-full max-w-full shrink-0 md:max-w-xs">
<img class="rounded-md " src={project.img} alt={project.description} />
</div>
<div class="card-title flex-1 flex-col items-start gap-4">
<div>
<h2 class="p-name text-left text-2xl mb-2">{project.name}</h2>
<div class="mb-3 text-base font-normal">
{#if tags}
{#each tags as tag}
<span class="btn btn-sm btn-ghost normal-case border-dotted border-base-content/20 border-2 my-1 mr-1">
{tag}
</span>
{/each}
{/if}
</div>
</div>
<p class="text-left text-base font-normal opacity-70">
{@html project.description}
</p>
</div>
</div>
</div>
</a>
{/if}

View File

@ -1,12 +1,11 @@
<script lang="ts"> <script lang="ts">
import QuickLinks from '$lib/components/home/QuickLinks.svelte'; import QuickLinks from '$lib/components/home/QuickLinks.svelte';
import { social } from '$lib/config/site';
</script> </script>
<section class="grid grid-cols-1 lg:grid-cols-2 items-center mx-4"> <section class="grid grid-cols-1 lg:grid-cols-2 items-center mx-4">
<!-- Text and links--> <!-- Text and links-->
<div class="order-1 max-w-3/4 space-y-8"> <div class="order-1 max-w-3/4 space-y-8">
<h1 class="text-4xl">Hello, I&#39;m Matt.</h1> <h1 class="h1">Hello, I&#39;m Matt.</h1>
<p class="text-xl opacity-75"> <p class="text-xl opacity-75">
A 🧠 that consumes ⚡️ and produces 👾 bug$... No sorry, produces code that works 100% A 🧠 that consumes ⚡️ and produces 👾 bug$... No sorry, produces code that works 100%
@ -17,8 +16,8 @@
moments ~~~ <br />although the 👾 bugs produce different emotions sometimes. moments ~~~ <br />although the 👾 bugs produce different emotions sometimes.
<br />Matt's main professional exploits are: <br />Matt's main professional exploits are:
<br /><br /><span <br /><span
class=" text-4xl bg-gradient-to-r from-primary via-secondary to-accent dark:from-primary dark:via-secondary dark:to-accent text-transparent bg-clip-text" 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, Web, AI & IoT</span >DevOps, Web, AI & IoT</span
> >
</p> </p>
@ -26,49 +25,51 @@
</div> </div>
<!-- Logo and buttons--> <!-- Logo and buttons-->
<div class="order-2 hidden lg:block"> <div class="order-2 hidden lg:block">
<figure class="items-center flex flex-relative flex-col"> <figure class="items-center">
<div class="relative">
<div class="img-bg"></div>
<img <img
class="mask mask-squircle w-64 overflow-hidden" class="rounded-10 w-64 overflow-hidden"
src="/assets/maskable@512.png" src="/images/profile-pic.png"
alt="Profile" alt="Profile"
/> />
<section class="img-bg w-64">
</div> <img
class="rounded-10 w-64 overflow-hidden"
src="/images/profile-pic.png"
alt="Profile"
/>
</section>
</figure> </figure>
<div class="flex justify-center space-x-2 m-4"> <div class="flex justify-center space-x-2 m-4">
<a <a
class="btn btn-accent z-10" class="btn variant-ghost-primary"
href={social.Gitea} href="https://git.mattmor.in"
target="_blank" target="_blank"
rel="personal" rel="personal"
> >
Look at my code Look at my code
</a> </a>
<a <a
class="btn btn-accent z-10" class="btn variant-ghost-primary"
href="https://git.mattmor.in/Madmin/Academic-cv/src/branch/master/matt_morin_cv_academic.pdf" href="/CV_Matthieu_Morin.pdf"
target="_blank" target="_blank"
rel="cv" rel="cv"
> >
Download my CV Get my CV
</a> </a>
</div> </div>
</div> </div>
<div class="order-2 flex justify-center lg:hidden space-x-2 m-4"> <div class="order-2 flex justify-center lg:hidden space-x-2 m-4">
<a <a
class="btn btn-primary" class="btn variant-ghost-primary"
href={social.Gitea} href="https://git.mattmor.in"
target="_blank" target="_blank"
rel="personal-mob" rel="personal-mob"
> >
Look at my code Look at my code
</a> </a>
<a <a
class="btn btn-primary" class="btn variant-ghost-primary"
href="https://git.mattmor.in/Madmin/Academic-cv/src/branch/master/matt_morin_cv_academic.pdf" href="/CV_Matthieu_Morin.pdf"
target="_blank" target="_blank"
rel="cv-mob" rel="cv-mob"
> >
@ -78,24 +79,25 @@
</section> </section>
<style lang="postcss"> <style lang="postcss">
figure {
@apply flex relative flex-col;
}
.img-bg { .img-bg {
@apply absolute inset-0 z-0 rounded-full blur-[100px] transition-all; @apply absolute z-[-1] rounded-full blur-[100px] transition-all;
animation: animation: pulse 5s cubic-bezier(0, 30, 150, 1) infinite, glow 5s linear infinite;
pulse 5s cubic-bezier(0, 0.3, 1, 1) infinite,
glow 5s linear infinite;
} }
@keyframes glow { @keyframes glow {
0% { 0% {
@apply bg-primary; @apply bg-primary-400/50;
} }
33% { 33% {
@apply bg-secondary; @apply bg-secondary-400/50;
} }
66% { 66% {
@apply bg-accent; @apply bg-tertiary-400/50;
} }
100% { 100% {
@apply bg-primary; @apply bg-primary-400/50;
} }
} }
@keyframes pulse { @keyframes pulse {

View File

@ -1,31 +1,26 @@
<script lang="ts"> <section class="grid grid-cols-1 md:grid-cols-3 gap-4 lg:gap-8" data-svelte-h="svelte-1u9sn7t">
const quickCards = [ <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" />
icon: 'fa-solid fa-screwdriver-wrench', <h3 class="h3">Development</h3>
title: 'Development', <p class="opacity-75">
text: 'I hone my problem solving and coding skills all the time. I try to learn the underlying mechanics and use abstractions where relevant.' I hone my problem solving and coding skills all the time. I try to learn the underlying
}, mechanics and use abstractions where relevant.
{ </p>
icon: 'fa-solid fa-palette', </div>
title: 'Creative solutions', <div class="card variant-ringed-hollow p-4 md:p-8 space-y-4">
text: 'I try to create better, novel ways to approach problems and give my best to present them in a clear and concise manner.' <i class="fa-solid fa-palette text-4xl text-primary-500" />
}, <h3 class="h3">Creativity & Presentation</h3>
{ <p class="opacity-75">
icon: 'fa-solid fa-users', I try to create better, novel ways to approach problems and give my best to present them
title: 'Teamwork', in a clear and concise manner.
text: 'From my experience with a wide range of roles I understand the how to communication with stakeholders across an organization.' </p>
} </div>
] <div class="card variant-ringed-hollow p-4 md:p-8 space-y-4">
</script> <i class="fa-solid fa-users text-4xl text-primary-500" />
<h3 class="h3">Teamwork & Stakeholders</h3>
<section id="quick-links" class="grid grid-cols-1 md:grid-cols-3 lg:gap-4 gap-4"> <p class="opacity-75">
{#each quickCards as card} From my experience with a wide range of roles I understand the how to communication with
<div class="card card-compact card-bordered pt-4 px-5 bg-base-300 text-base-content"> stakeholders across an organization.
<div class="flex flex-row gap-4 mx-2"><i class="{card.icon} text-4xl text-accent mx-1 mt-2" />
<h3 class="text-2xl card-title mt-2">{card.title}</h3></div>
<p class="opacity-75 card-body justify-start !text-base prose">
{card.text}
</p> </p>
</div> </div>
{/each}
</section> </section>

View File

@ -1,28 +1,27 @@
<script lang="ts"> <script lang="ts">
import socials from '$lib/config/socialsObjects'; import GiteaLogo from '$lib/components/logos/GiteaLogo.svelte';
import ObfuscatedEmail from '$lib/components/ObfuscatedEmail.svelte'; import socials from '$lib/socialsObjects';
import ObfuscatedEmail from '../ObfuscatedEmail.svelte';
</script> </script>
<div class="flex space-x-4 justify-center md:justify-start"> <div class="flex space-x-4">
<a <a
class="btn btn-circle btn-outline btn-secondary btn-lg hover:btn-secondary" class="btn btn-icon variant-soft-primary hover:variant-filled-primary"
href={socials[3].href} href={socials[3].href}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
title="Gitea - private github" title="Gitea - private github"
><i class={socials[3].icon + ' text-3xl md:text-3xl text-base-content'} /> ><GiteaLogo clazz="w-6" />
</a><a </a><a
class="btn btn-circle btn-outline btn-secondary btn-lg hover:btn-secondary" class="btn btn-icon variant-soft-primary hover:variant-filled-primary"
href={socials[1].href} href={socials[1].href}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
title={socials[1].title} title={socials[1].title}
><i class={socials[1].icon + ' text-3xl md:text-3xl text-base-content'} /> ><i class={socials[1].icon} />
</a><ObfuscatedEmail </a><ObfuscatedEmail
mail={socials[0].href} mail={socials[0].href}
clazz="btn btn-circle btn-outline btn-secondary btn-lg hover:btn-secondary" clazz="btn btn-icon variant-soft-primary hover:variant-filled-primary h-[43px]"
h={8} iconClazz={socials[0].icon}
w={8}
/> />
</div> </div>

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import GiteaLogo from '$lib/assets/logos/GiteaLogo.svelte'; import GiteaLogo from '$lib/components/logos/GiteaLogo.svelte';
import SocialsCloud from '$lib/components/main/SocialsCloud.svelte'; import SocialsCloud from '$lib/components/SocialsCloud.svelte';
const year = new Date().getFullYear(); const year = new Date().getFullYear();
</script> </script>

View File

@ -1,29 +0,0 @@
<script lang="ts">
import socials from '$lib/config/socialsObjects';
import ObfuscatedEmail from '$lib/components/ObfuscatedEmail.svelte';
</script>
<div class="flex md:flex-row flex-wrap gap-2 max-h-24">
{#each socials as link}
<div class="card card-compact card-bordered p-6 bg-base-200 aspect-square flex items-center justify-center ">
{#if link.title === 'Email'}
<ObfuscatedEmail
mail={socials[0].href}
clazz="w-full h-full flex items-center justify-center"
h={12}
w={12}
/>
{:else}
<a
class="w-full h-full flex items-center justify-center"
href={link.href}
target="_blank"
rel={link.title === 'Mastodon' ? 'me' : 'noreferrer'}
aria-label={link.title}
>
<i class={link.icon + ' text-3xl md:text-5xl'} />
</a>
{/if}
</div>
{/each}
</div>

View File

@ -1,81 +0,0 @@
<script lang="ts">
import SocialsCloud from '$lib/components/main/SocialsCloud.svelte';
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 flex">
<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 />
<span class="sm:pl-4 text-base sm:py-2 sm:mt-0 mt-4 text-center">
All content by me, the Author, unless otherwise stated, is permissively licensed under
<a
rel="noopener noreferrer external"
target="_blank"
class="tooltip tooltip-secondary link-primary font-bold hover:text-secondary"
href="https://creativecommons.org/licenses/by-sa/4.0/deed.en">CC BY-SA 4.0</a
>.
</span>
The code of this site is
<a
rel="noopener noreferrer external"
target="_blank"
class="tooltip tooltip-secondary link-primary font-bold hover:text-secondary"
data-tip="🌸 See the license [δ] - Based on MDsveX & SvelteKit 🌸"
href="https://git.mattmor.in/Madmin/its-personal/src/branch/master/LICENSE">
MIT licensed, Powered by my code & Urara
</a>
{#if footerConfig.html}
<br />
{@html footerConfig.html}
{/if}
</p>
</div>
<SocialsCloud />
</footer>
<!-- <footer
class="page-footer bg-surface-50 dark:bg-surface-900 border-t border-surface-500/10 text-xs mt-4 md:text-base"
>
<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 © ,
<br />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
>.
</p>
</a>
</div>
</footer> -->

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,8 @@
<script lang="ts"> <script lang="ts">
import { AccordionItem, ProgressBar } from '@skeletonlabs/skeleton';
import type { Skill } from '$lib/config/skills'; import type { Skill } from '$content/skills';
import list from '$lib/config/skills'; import list from '$content/skills';
function sortSkills(skills: Skill[]): Skill[] { function sortSkills(skills: Skill[]): Skill[] {
return skills.sort((a, b) => { return skills.sort((a, b) => {
@ -13,21 +14,22 @@
</script> </script>
{#each list as category} {#each list as category}
<div class="collapse collapse-arrow outline outline-2 outline-secondary "> <AccordionItem>
<input type="radio" name="skillAccordion" /> <svelte:fragment slot="lead">
<div class=" flex flex-col md:flex-row collapse-title font-semibold space-x-4 space-y-4 align-middle justify-center items-center"> <i class="text-3xl">{category.icon}</i>
<i class="text-3xl align-bottom emoji">{category.icon}</i> </svelte:fragment>
<h2 class="text-3xl align-middle">{category.title}</h2> <svelte:fragment slot="summary">
<h2 class="h2 font-bold m-2">{category.title}</h2>
<!-- Progresses are stupidly subjective and I don't know how to grade this so no progress bars for now. <!-- Progresses are stupidly subjective and I don't know how to grade this so no progress bars for now.
<ProgressBar class="min-w-[100px] h-2" value={category.level} max={100} /> --> <ProgressBar class="min-w-[100px] h-2" value={category.level} max={100} /> -->
</div> </svelte:fragment>
<div class="collapse-content"> <svelte:fragment slot="content">
<div class="flex flex-col justify-center m-2 space-y-8"> <div class="flex flex-col justify-center m-2 space-y-8">
{#if category.subCategories} {#if category.subCategories}
{#each category.subCategories as subCategory (subCategory.title)} {#each category.subCategories as subCategory (subCategory.title)}
<div class="flex flex-col space-y-2"> <div class="flex flex-col space-y-2">
<div class="flex flex-row justify-center items-center"> <div class="flex flex-row justify-center items-center">
<h3 class="text-2xl font-semibold m-2">{subCategory.title}</h3> <p class="text-xl font-medium m-2">{subCategory.title}</p>
<!-- <ProgressBar value={subCategory.level} max={100} /> --> <!-- <ProgressBar value={subCategory.level} max={100} /> -->
</div> </div>
@ -35,11 +37,11 @@
{#if subCategory.skills} {#if subCategory.skills}
{#each sortSkills(subCategory.skills) as skill (skill.title)} {#each sortSkills(subCategory.skills) as skill (skill.title)}
<span <span
class="mt-2 badge badge-md md:badge-lg {skill.level === 'A' class="chip {skill.level === 'A'
? 'badge-primary' ? 'variant-filled-primary'
: skill.level === 'B' : skill.level === 'B'
? 'badge-accent' ? 'variant-outline-primary'
: 'badge-primary badge-ghost'}" : 'variant-outline-tertiary'}"
> >
{skill.title} {skill.title}
</span> </span>
@ -50,14 +52,6 @@
{/each} {/each}
{/if} {/if}
</div> </div>
</div> </svelte:fragment>
</div> </AccordionItem>
{/each} {/each}
<style>
.emoji {
position: relative;
top: +0.2em;
line-height: 2;
}
</style>

View File

@ -1,25 +1,27 @@
<script lang="ts"> <script lang="ts">
import { Accordion } from '@skeletonlabs/skeleton';
import IndividualSkills from './IndividualSkills.svelte'; import IndividualSkills from './IndividualSkills.svelte';
</script> </script>
<section>
<section <Accordion
class="card flex flex-col items-center justify-center mx-auto w-full bg-base-300 p-8 mt-8 space-y-4" class="card flex flex-col items-center justify-center mx-auto w-3/4 bg-surface-50 bg-opacity-50 p-8 m-8 space-y-4"
autocollapse
id="skills" id="skills"
> >
<h2 class="text-4xl m-2">My skillset</h2> <h2 class="h2 m-2">My skillset</h2>
<p class="text-center"> <p class="text-center">
Below is a list of tools, frameworks, languages and skills <br />I use or have used to Below is a list of tools, frameworks, languages and skills <br />I use or have used to
varying degrees and a subjective rating varying degrees and a subjective rating
</p> </p>
<span class="h6 m-2">based on my proficiency:</span> <span class="h6 m-2">based on my proficiency:</span>
<div class="flex flex-wrap justify-center space-x-2 m-2"> <div class="flex flex-wrap justify-center space-x-2 m-2">
<span class="badge badge-primary badge-lg">Proficient</span> <span class="chip variant-filled-primary text-lg">Proficient</span>
<span class="badge badge-accent badge-lg">Experienced</span> <span class="chip variant-outline-primary text-lg">Experienced</span>
<span class="badge-ghost badge-lg">Limited Experience</span> <span class="chip variant-outline-tertiary text-lg">Limited Experience</span>
</div> </div>
<div id="categoryContainer" class="flex flex-col items-center justify-center mx-auto space-y-4"> <div class="flex flex-col items-center justify-center mx-auto space-x-2 space-y-2">
<IndividualSkills /> <IndividualSkills />
</div> </div>
</Accordion>
</section> </section>

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