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
+ build/**
+ dist/**
dist
dist-ssr
build
static
.svelte-kit
.netlify
.vercel
.DS_Store
node_modules
/build
/.svelte-kit
/package
/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/**
static/**
build/**
node_modules/**
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
*.local
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
.netlify/**
.vercel_build_output/**
package-lock.json
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"]
}

118
.vscode/settings.json vendored
View File

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

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
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
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)
## 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 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/]
<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,121 +1,128 @@
// rehype plugins
import rehypeSlug from 'rehype-slug'
import rehypeAutolinkHeadings from 'rehype-autolink-headings'
import rehypeExternalLinks from 'rehype-external-links'
// urara remark plugins
import { parse, join } from 'node:path'
import { visit } from 'unist-util-visit'
import { toString } from 'mdast-util-to-string'
import Slugger from 'github-slugger'
import remarkFFF from 'remark-fff'
import remarkFootnotes from 'remark-footnotes'
import { defineMDSveXConfig as defineConfig } from 'mdsvex';
import rehypeExternalLinks from 'rehype-external-links';
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
import readingTime from 'mdsvex-reading-time';
import remarkUnwrapImages from 'remark-unwrap-images';
import remarkToc from 'remark-toc';
import rehypeSlug from 'rehype-slug';
import { visit } from 'unist-util-visit';
import { toString } from 'mdast-util-to-string';
import Slugger from 'github-slugger';
import remarkFFF from 'remark-fff';
import remarkFootnotes from 'remark-footnotes';
// import { 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
import { escapeSvelte } from 'mdsvex'
import { lex, parse as parseFence } from 'fenceparser'
import { renderCodeToHTML, runTwoSlash, createShikiHighlighter } from 'shiki-twoslash'
import readingTime from 'mdsvex-reading-time'
import { escapeSvelte } from 'mdsvex';
import { lex, parse as parseFence } from 'fenceparser';
import { renderCodeToHTML, runTwoSlash, createShikiHighlighter } from 'shiki-twoslash';
const remarkUraraFm =
() =>
(tree, { data, filename }) => {
const filepath = filename ? filename.split('/src/routes')[1] : 'unknown'
const { dir, name } = parse(filepath)
if (!data.fm) data.fm = {}
// Generate slug & path
data.fm.slug = filepath
data.fm.path = join(dir, `/${name}`.replace('/+page', '').replace('.svelte', ''))
// Generate ToC
if (data.fm.toc !== false) {
const [slugs, toc] = [new Slugger(), []]
visit(tree, 'heading', node => {
toc.push({
depth: node.depth,
title: toString(node),
slug: slugs.slug(toString(node), false)
})
})
if (toc.length > 0) data.fm.toc = toc
else data.fm.toc = false
}
}
/** @type {import('mdsvex').MdsvexOptions} */
const config = defineConfig({
extensions: ['.svelte.md', '.md', '.svx'],
smartypants: {
dashes: 'oldschool'
},
// layout: {
// _: './src/lib/components/blog/Post.svelte'
// },
highlight: {
highlighter: async (code, lang, meta) => {
let fence, twoslash;
try {
fence = parseFence(lex([lang, meta].filter(Boolean).join(' ')));
} catch (error) {
throw new Error(`Could not parse the codefence for this code sample \n${code}`);
}
if (fence?.twoslash === true) twoslash = runTwoSlash(code, lang);
return `{@html \`${escapeSvelte(
renderCodeToHTML(
code,
lang,
fence ?? {},
{ themeName: 'github-dark-dimmed' },
await createShikiHighlighter({ theme: 'github-dark-dimmed' }),
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: [
[remarkToc, { maxDepth: 3, tight: true }],
[
remarkFFF,
{
presets: [],
target: 'mdsvex',
autofill: {
provider: 'fs',
path: (path) => path.replace('/src/routes/', '/urara/')
},
strict: {
media: {
type: 'string',
array: false
}
}
}
],
[readingTime, { wpm: 200 }],
[remarkFootnotes, { inlineNotes: true }]
// [
// headings,
// {
// behavior: 'append',
// linkProperties: {},
// content: function (node) {
// return [
// h('span.icon.icon-link header-anchor', {
// ariaLabel: toString(node) + ' permalink'
// })
// ];
// }
// }
// ],
// 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
})
// remarkHeadingsPermaLinks,
// getHeadings
]
});
/** @type {import("mdsvex").MdsvexOptions} */
export default {
extensions: ['.svelte.md', '.md'],
smartypants: {
dashes: 'oldschool'
},
layout: {
_: './src/lib/components/blog/post_layout.svelte'
},
highlight: {
highlighter: async (code, lang, meta) => {
let fence, twoslash
try {
fence = parseFence(lex([lang, meta].filter(Boolean).join(' ')))
} catch (error) {
throw new Error(`Could not parse the codefence for this code sample \n${code}`)
}
if (fence?.twoslash === true) twoslash = runTwoSlash(code, lang)
return `{@html \`${escapeSvelte(
renderCodeToHTML(
code,
lang,
fence ?? {},
{ themeName: 'github-dark-dimmed' },
await createShikiHighlighter({ theme: 'github-dark-dimmed' }),
twoslash
)
)}\` }`
}
},
remarkPlugins: [
[
remarkFFF,
{
presets: [],
target: 'mdsvex',
autofill: {
provider: 'fs',
path: path => path.replace('/src/routes/', '/urara/')
},
strict: {
media: {
type: 'string',
array: false
}
}
}
],
[readingTime, { wpm: 200 }],
remarkUraraFm,
remarkUraraSpoiler,
[remarkFootnotes, { inlineNotes: true }]
],
rehypePlugins: [
rehypeSlug,
[rehypeAutolinkHeadings, { behavior: 'wrap' }],
[
rehypeExternalLinks,
{
rel: ['nofollow', 'noopener', 'noreferrer', 'external'],
target: '_blank'
}
]
]
}
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",
"type": "module",
"version": "2.0.0",
"license": "WTFPL",
"repository": "Madmin/its-personal",
"homepage": "https://git.mattmor.in/Madmin/its-personal",
"bugs": "https://git.mattmor.in/Madmin/its-personal/issues",
"author": "Madmin",
"packageManager": "pnpm@8.14.1",
"scripts": {
"clean": "node urara.js clean",
"tsc": "tsc -p tsconfig.node.json",
"tsc:watch": "tsc -w -p tsconfig.node.json",
"urara:build": "node urara.js build",
"urara:watch": "node urara.js watch",
"kit:dev": "cross-env NODE_OPTIONS=--max_old_space_size=7680 vite dev",
"kit:build": "cross-env NODE_OPTIONS=--max_old_space_size=7680 vite build",
"dev:parallel": "run-p -r tsc:watch urara:watch \"kit:dev {@} \" --",
"dev": "run-s tsc \"dev:parallel {@} \" --",
"build": "run-s tsc urara:build kit:build clean",
"preview": "vite preview",
"start": "cross-env ADAPTER=node pnpm build && node build",
"check": "svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --check --plugin-search-dir=. . && eslint --ignore-path .gitignore .",
"format": "prettier --write --plugin-search-dir=. ."
},
"devDependencies": {
"@iconify-json/heroicons-outline": "^1.1.10",
"@iconify-json/heroicons-solid": "^1.1.11",
"@iconify-json/simple-icons": "^1.1.100",
"@sveltejs/adapter-auto": "^3.1.1",
"@sveltejs/adapter-netlify": "^4.1.0",
"@sveltejs/adapter-node": "^4.0.1",
"@sveltejs/adapter-static": "^3.0.1",
"@sveltejs/adapter-vercel": "^5.1.0",
"@sveltejs/kit": "^2.5.0",
"@sveltejs/vite-plugin-svelte": "^3.0.2",
"@tailwindcss/typography": "^0.5.10",
"@types/node": "^20.11.17",
"@types/unist": "^3.0.2",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"@unocss/extractor-svelte": "^0.55.7",
"@vite-pwa/sveltekit": "^0.1.3",
"autoprefixer": "^10.4.19",
"chalk": "^5.3.0",
"chokidar": "^3.6.0",
"cross-env": "^7.0.3",
"daisyui": "^4.6.2",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.35.1",
"fenceparser": "^2.2.0",
"fff-flavored-frontmatter": "1.0.0-alpha.1",
"github-slugger": "^2.0.0",
"mdast-util-to-string": "^3.2.0",
"mdsvex": "^0.11.0",
"mdsvex-reading-time": "^1.0.4",
"npm-run-all": "^4.1.5",
"postcss": "^8.4.35",
"postcss-lightningcss": "^0.7.0",
"prettier": "^3.2.5",
"prettier-plugin-svelte": "^3.1.2",
"reading-time": "^1.5.0",
"rehype-autolink-headings": "^6.1.1",
"rehype-external-links": "^2.1.0",
"rehype-slug": "^5.1.0",
"remark": "^14.0.3",
"remark-fff": "1.0.0-alpha.1",
"remark-footnotes": "~2.0.0",
"shiki-twoslash": "^3.1.2",
"svelte": "^4.2.10",
"svelte-check": "^3.6.4",
"svelte-eslint-parser": "^0.33.1",
"svelte-preprocess": "^5.1.3",
"sveltekit-embed": "^0.0.14",
"tailwindcss": "^3.4.1",
"tslib": "^2.6.2",
"typescript": "^5.3.3",
"unist-util-visit": "^4.1.2",
"unocss": "^0.58.5",
"vite": "^5.1.1",
"vite-imagetools": "^4.0.19",
"vite-plugin-pwa": "^0.17.5",
"workbox-build": "^7.0.0",
"workbox-window": "^7.0.0"
},
"dependencies": {
"@fortawesome/fontawesome-free": "^6.5.2",
"@sentry/sveltekit": "^7.112.2",
"@sveltejs/adapter-cloudflare": "^4.4.0",
"@yushijinhun/three-minifier-rollup": "^0.4.0",
"vite-plugin-tailwind-purgecss": "^0.3.3",
"vitest": "^1.5.2"
}
"name": "its-personal~portfolio",
"description": "Uses pnpm, svelte, mdsvex",
"version": "1.0.0",
"main": "index.js",
"private": true,
"homepage": "mattmor.in",
"author": "Matthieu Morin",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://git.mattmor.in/Madmin/its-personal/"
},
"packageManager": "pnpm@8.6.6",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"test": "vitest",
"lint": "prettier --plugin-search-dir . --check . && eslint .",
"format": "prettier --plugin-search-dir . --write .",
"generate:images": "node ./generate-responsive-image-data.js",
"test-ct": "playwright test -c playwright-ct.config.ts"
},
"devDependencies": {
"@bitmachina/highlighter": "1.0.0-alpha.5",
"@fontsource/cooper-hewitt": "^5.0.11",
"@fontsource/fira-mono": "^5.0.13",
"@kitbook/mdsvex-shiki-twoslash": "1.0.0-beta.31",
"@playwright/test": "^1.41.2",
"@skeletonlabs/skeleton": "2.0.0",
"@skeletonlabs/tw-plugin": "0.1.0",
"@sveltejs/kit": "^2.5.0",
"@tailwindcss/forms": "0.5.6",
"@tailwindcss/typography": "0.5.9",
"@types/js-cookie": "^3.0.6",
"@types/node": "20.5.7",
"@types/object-hash": "^3.0.6",
"@types/prismjs": "^1.26.3",
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0",
"autoprefixer": "10.4.15",
"cloudinary": "^2.2.0",
"emoji-regex": "^10.3.0",
"eslint": "^8.56.0",
"eslint-config-prettier": "^8.10.0",
"eslint-plugin-svelte": "^2.35.1",
"fenceparser": "^2.2.0",
"github-slugger": "^2.0.0",
"js-cookie": "^3.0.5",
"mdast-util-to-string": "^4.0.0",
"mdsvex-reading-time": "^1.0.4",
"object-hash": "^3.0.0",
"postcss": "8.4.29",
"prettier": "^2.8.8",
"prettier-plugin-svelte": "^2.10.1",
"reading-time": "^1.5.0",
"rehype-autolink-headings": "^7.1.0",
"rehype-external-links": "^3.0.0",
"rehype-img-size": "^1.0.1",
"rehype-slug": "^6.0.0",
"remark-fff": "^1.2.1",
"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",
"svelte": "^4.2.11",
"svelte-check": "^3.6.4",
"tailwind-merge": "^2.2.2",
"tailwindcss": "3.3.3",
"tslib": "^2.6.2",
"typescript": "^5.3.3",
"unist-util-visit": "^5.0.0",
"vite": "^5.1.3",
"vite-imagetools": "^7.0.1",
"vite-plugin-tailwind-purgecss": "0.2.0",
"vitest": "^0.34.6"
},
"dependencies": {
"@floating-ui/dom": "1.5.1",
"@fortawesome/fontawesome-free": "^6.5.1",
"@sentry/sveltekit": "^7.102.0",
"@sveltejs/adapter-node": "^4.0.1",
"@sveltejs/vite-plugin-svelte": "^3.0.2",
"@threlte/core": "^6.1.1",
"@threlte/extras": "^8.7.5",
"@yushijinhun/three-minifier-rollup": "^0.4.0",
"front-matter": "^4.0.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 = {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

101
src/app.d.ts vendored
View File

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

View File

@ -1,30 +1,22 @@
<!doctype html>
<html lang="en">
<head prefix="og: https://ogp.me/ns#">
<meta charset="utf-8" />
<meta name="generator" content="gh:importantimport/urara" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="apple-touch-icon" sizes="180x180" href="%sveltekit.assets%/assets/apple-touch-icon.png" />
<link rel="icon" href="%sveltekit.assets%/assets/maskable@192.png" />
<link rel="manifest" crossorigin="use-credentials" href="/manifest.webmanifest" />
<link rel="alternate" type="application/feed+json" href="/feed.json" />
<link rel="alternate" type="application/atom+xml" href="/atom.xml" />
<link rel="sitemap" type="application/xml" href="/sitemap.xml" />
%sveltekit.head%
</head>
<body itemscope itemtype="https://schema.org/WebPage" data-sveltekit-prefetch>
<script>
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>
<!DOCTYPE html>
<html lang="en" class="dark">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<link rel="icon" href="/images/profile-pic.png" />
<!-- <link
rel="icon"
href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='48' height='48' viewBox='0 0 18 16'><text x='0' y='14'>🚀</text></svg>"
/> -->
<link rel="preload" href="/fonts/Quicksand.ttf" as="font" type="font/ttf" crossorigin />
<!-- Dropin replacement for FontAwesome-->
<!-- <link
href="https://cdn.jsdelivr.net/npm/ficons@1.1.52/dist/ficons/font.css"
rel="stylesheet"
/> -->
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover" data-theme="wintry">
<div style="display: contents" class="h-full overflow-hidden">%sveltekit.body%</div>
</body>
</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 {
title: string
level: Level
title: string;
level: Level;
}
export interface SubCategory {
title: string
level: number
skills: Skill[]
title: string;
level: number;
skills: Skill[];
}
export interface Category {
icon: string
title: string
level: number
subCategories: SubCategory[]
icon: string;
title: string;
level: number;
subCategories: SubCategory[];
}
// prettier-ignore
const list: Category[] = [
@ -42,7 +42,7 @@ const list: Category[] = [
{ title:'Testing & Validation', level: 50, skills: [
{ title: 'ajv', level: 'A' },
{ title: 'Playwright', level: 'B'},
{ title: 'node', level: 'B'},
{ title: 'SEO, performance optimizations', level: 'B'},
]},
]},
{ icon: '🔁🔁', title:'DevOps', level: 70, subCategories: [
@ -80,7 +80,7 @@ const list: Category[] = [
{ title:'Vercel', 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: 'Debian / Ubuntu', level: 'A' },
{ title: 'Nix(OS)', level: 'B' },
@ -103,7 +103,7 @@ const list: Category[] = [
{ 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: 'Raspberry Pi', level: 'A' },
{ title: 'ESP8266, ESP32', level: 'A' },
@ -119,13 +119,13 @@ const list: Category[] = [
{ title: 'Ultimaker Cura', level: 'B' },
]},
]},
{ icon: '💬', title:'Languages', level: 70, subCategories: [
{ icon: '🤐💬', title:'Languages', level: 70, subCategories: [
{ title: 'English', level: 'A' },
{ title: 'Czech', level: 'A' },
{ title: 'French', level: 'B' },
{ title: 'German', level: 'C' }
]},
{ icon: '🎨', title:'Design', level: 70, subCategories: [
{ icon: '🎨✏️📐', title:'Design', level: 70, subCategories: [
{ title: 'UI/UX', level: 'B' },
{ title: 'LaTeX', level: 'C' },
{ title: 'Wireframing, Prototyping, Diagramming', level: 'B', skills: [
@ -143,4 +143,4 @@ const list: Category[] = [
]}
];
export default list
export default list;

View File

@ -1,73 +1,83 @@
// 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 = {
'base-uri': ["'self'"],
'child-src': ["'self'", 'blob:'],
'connect-src': [
"'self'",
'ws://localhost:*',
'https://*.sentry.io',
'https://hcaptcha.com',
'https://*.hcaptcha.com',
'https://*.cartocdn.com',
'https://*.mattmor.in/**'
],
'img-src': ["'self'", 'data:', 'https://images.unsplash.com', `${site.protocol}${site.domain}`],
'font-src': ["'self'", 'data:'],
'form-action': ["'self'"],
'frame-ancestors': ["'self'"],
'frame-src': [
"'self'",
// "https://*.stripe.com",
// "https://*.facebook.com",
// "https://*.facebook.net",
'https://hcaptcha.com',
'https://*.hcaptcha.com',
'https://www.openstreetmap.org',
'https://*.cartocdn.com'
],
'manifest-src': ["'self'"],
'media-src': ["'self'", 'data:'],
'object-src': ["'none'"],
// 'style-src': ["'self'", "'unsafe-inline'"],
'style-src': ["'self'", "'unsafe-inline'", 'https://hcaptcha.com', 'https://*.hcaptcha.com'],
'default-src': [
"'self'",
site.domain,
`ws://${site.domain}`,
// 'https://*.google.com',
// 'https://*.googleapis.com',
// 'https://*.firebase.com',
// 'https://*.gstatic.com',
// 'https://*.cloudfunctions.net',
// 'https://*.algolia.net',
// 'https://*.facebook.com',
// 'https://*.facebook.net',
// 'https://*.stripe.com',
'https://*.sentry.io'
],
'script-src': [
"'self'",
"'unsafe-inline'",
// 'https://*.stripe.com',
// 'https://*.facebook.com',
// 'https://*.facebook.net',
'https://hcaptcha.com',
'https://*.hcaptcha.com',
'https://*.sentry.io',
// 'https://polyfill.io',
'https://*.cartocdn.com'
],
'worker-src': ["'self'", 'blob:'],
//report-to can throw "Content-Security-Policy: Couldnt process unknown directive report-to", leave it for older browsers.
'report-to': ["'csp-endpoint'"],
'report-uri': [
`https://${PUBLIC_SENTRY_ORG_ID}.ingest.us.sentry.io/api/${PUBLIC_SENTRY_PROJECT_ID}/security/?sentry_key=${PUBLIC_SENTRY_KEY}`
]
}
'base-uri': ["'self'"],
'child-src': ["'self'", 'blob:'],
// 'connect-src': ["'self'", 'ws://localhost:*'],
'connect-src': [
"'self'",
'ws://localhost:*',
'https://*.sentry.io',
'https://hcaptcha.com',
'https://*.hcaptcha.com',
'https://*.cartocdn.com',
PUBLIC_DOMAIN,
PUBLIC_WORKER_URL
],
'img-src': ["'self'", 'data:', 'https://images.unsplash.com'],
'font-src': ["'self'", 'data:'],
'form-action': ["'self'"],
'frame-ancestors': ["'self'"],
'frame-src': [
"'self'",
// "https://*.stripe.com",
// "https://*.facebook.com",
// "https://*.facebook.net",
'https://hcaptcha.com',
'https://*.hcaptcha.com',
'https://www.openstreetmap.org',
'https://*.cartocdn.com'
],
'manifest-src': ["'self'"],
'media-src': ["'self'", 'data:'],
'object-src': ["'none'"],
// 'style-src': ["'self'", "'unsafe-inline'"],
'style-src': ["'self'", "'unsafe-inline'", 'https://hcaptcha.com', 'https://*.hcaptcha.com'],
'default-src': [
"'self'",
rootDomain,
`ws://${rootDomain}`,
// 'https://*.google.com',
// 'https://*.googleapis.com',
// 'https://*.firebase.com',
// 'https://*.gstatic.com',
// 'https://*.cloudfunctions.net',
// 'https://*.algolia.net',
// 'https://*.facebook.com',
// 'https://*.facebook.net',
// 'https://*.stripe.com',
'https://*.sentry.io'
],
'script-src': [
"'self'",
"'unsafe-inline'",
// 'https://*.stripe.com',
// 'https://*.facebook.com',
// 'https://*.facebook.net',
'https://hcaptcha.com',
'https://*.hcaptcha.com',
'https://*.sentry.io',
// 'https://polyfill.io',
'https://*.cartocdn.com'
],
'worker-src': ["'self'", 'blob:'],
//report-to can throw "Content-Security-Policy: Couldnt process unknown directive report-to", leave it for older browsers.
'report-to': ["'csp-endpoint'"],
'report-uri': [
`https://${PUBLIC_SENTRY_ORG_ID}.ingest.us.sentry.io/api/${PUBLIC_SENTRY_PROJECT_ID}/security/?sentry_key=${PUBLIC_SENTRY_KEY}`
]
};
export const csp = Object.entries(directives)
.map(([key, arr]) => key + ' ' + arr.join(' '))
.join('; ')
.map(([key, arr]) => key + ' ' + arr.join(' '))
.join('; ');

View File

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

View File

@ -1,53 +1,51 @@
import type { Handle } from '@sveltejs/kit'
import { sequence } from '@sveltejs/kit/hooks'
import { site } from '$lib/config/site'
import type { Handle } from '@sveltejs/kit';
import { sequence } from '@sveltejs/kit/hooks';
import { handleErrorWithSentry, sentryHandle } from '@sentry/sveltekit'
import * as Sentry from '@sentry/sveltekit'
import { PUBLIC_SENTRY_KEY, PUBLIC_SENTRY_PROJECT_ID, PUBLIC_SENTRY_ORG_ID } from '$env/static/public'
import { handleErrorWithSentry, sentryHandle } from '@sentry/sveltekit';
import * as Sentry from '@sentry/sveltekit';
import {
PUBLIC_SENTRY_KEY,
PUBLIC_SENTRY_PROJECT_ID,
PUBLIC_SENTRY_ORG_ID
} from '$env/static/public';
import { csp } from './cspDirectives'
import { csp, rootDomain } from './cspDirectives';
Sentry.init({
dsn: `https://${PUBLIC_SENTRY_KEY}@${PUBLIC_SENTRY_ORG_ID}.ingest.us.sentry.io/${PUBLIC_SENTRY_PROJECT_ID}`,
tracesSampleRate: 1.0
})
dsn: `https://${PUBLIC_SENTRY_KEY}@${PUBLIC_SENTRY_ORG_ID}.ingest.us.sentry.io/${PUBLIC_SENTRY_PROJECT_ID}`,
tracesSampleRate: 1.0
});
export const cspHandle: Handle = async ({ event, resolve }) => {
if (!csp) {
throw new Error('csp is undefined')
}
const response = await resolve(event)
if (!csp) {
throw new Error('csp is undefined');
}
const response = await resolve(event);
// Permission fullscreen necessary for maps fullscreen
const headers = {
'X-Frame-Options': 'SAMEORIGIN',
'Referrer-Policy': 'no-referrer',
'Permissions-Policy': `accelerometer=(), autoplay=(), camera=(), document-domain=(self, 'js-profiling'), encrypted-media=(), fullscreen=(self ${site.domain}), gyroscope=(), interest-cohort=(), magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), sync-xhr=(), usb=(), xr-spatial-tracking=(), geolocation=()`,
'X-Content-Type-Options': 'nosniff',
// 'Content-Security-Policy-Report-Only': csp,
'Content-Security-Policy': csp,
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains; preload',
'Expect-CT': `max-age=86400, report-uri="https://${PUBLIC_SENTRY_ORG_ID}.ingest.us.sentry.io/api/${PUBLIC_SENTRY_PROJECT_ID}/security/?sentry_key=${PUBLIC_SENTRY_KEY}"`,
'Report-To': `{group: "csp-endpoint", "max_age": 10886400, "endpoints": [{"url": "https://${PUBLIC_SENTRY_ORG_ID}.ingest.us.sentry.io/api/${PUBLIC_SENTRY_PROJECT_ID}/security/?sentry_key=${PUBLIC_SENTRY_KEY}"}]}`
}
// Permission fullscreen necessary for maps fullscreen
const headers = {
'X-Frame-Options': 'SAMEORIGIN',
'Referrer-Policy': 'no-referrer',
'Permissions-Policy': `accelerometer=(), autoplay=(), camera=(), document-domain=(self, 'js-profiling'), encrypted-media=(), fullscreen=(self ${rootDomain}), gyroscope=(), interest-cohort=(), magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), sync-xhr=(), usb=(), xr-spatial-tracking=(), geolocation=()`,
'X-Content-Type-Options': 'nosniff',
// 'Content-Security-Policy-Report-Only': csp,
'Content-Security-Policy': csp,
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains; preload',
'Expect-CT': `max-age=86400, report-uri="https://${PUBLIC_SENTRY_ORG_ID}.ingest.us.sentry.io/api/${PUBLIC_SENTRY_PROJECT_ID}/security/?sentry_key=${PUBLIC_SENTRY_KEY}"`,
'Report-To': `{group: "csp-endpoint", "max_age": 10886400, "endpoints": [{"url": "https://${PUBLIC_SENTRY_ORG_ID}.ingest.us.sentry.io/api/${PUBLIC_SENTRY_PROJECT_ID}/security/?sentry_key=${PUBLIC_SENTRY_KEY}"}]}`
};
Object.entries(headers).forEach(([key, value]) => {
response.headers.set(key, value)
})
return response
}
export const langHandle: Handle = async ({ event, resolve }) =>
await resolve(event, {
transformPageChunk: ({ html }) => html.replace('<html lang="en">', `<html lang="${site.lang ?? 'en'}">`)
})
Object.entries(headers).forEach(([key, value]) => {
response.headers.set(key, value);
});
return response;
};
// If you have custom handlers, make sure to place them after `sentryHandle()` in the `sequence` function.
export const handle: Handle = sequence(sentryHandle(), cspHandle, langHandle)
export const handle: Handle = sequence(sentryHandle(), cspHandle);
// 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://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
// 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">
import Envelope from '$lib/components/Envelope.svelte'
export let mail: string;
import socials from '$lib/socialsObjects';
export let mail: string = socials[0].href;
export let clazz: string = '';
export let w: number = 24;
export let h: number = 24;
export let iconClazz: string = '';
export let h: number = 16;
export let w: number = 16;
</script>
<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>
<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%" />
<foreignObject x="0" y="0" width="100%" height="100%">
<div class="email-icon-wrapper">
<Envelope />
<i class={`${iconClazz} `} />
</div>
</foreignObject>
</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">
import QuickLinks from '$lib/components/home/QuickLinks.svelte';
import { social } from '$lib/config/site';
</script>
<section class="grid grid-cols-1 lg:grid-cols-2 items-center mx-4">
<!-- Text and links-->
<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">
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.
<br />Matt's main professional exploits are:
<br /><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"
<br /><span
class=" text-4xl bg-gradient-to-r from-primary-800 via-secondary-900 to-tertiary-900 dark:from-primary-400 dark:via-secondary-400 dark:to-tertiary-400 text-transparent bg-clip-text"
>DevOps, Web, AI & IoT</span
>
</p>
@ -26,49 +25,51 @@
</div>
<!-- Logo and buttons-->
<div class="order-2 hidden lg:block">
<figure class="items-center flex flex-relative flex-col">
<div class="relative">
<div class="img-bg"></div>
<img
class="mask mask-squircle w-64 overflow-hidden"
src="/assets/maskable@512.png"
alt="Profile"
/>
</div>
</figure>
<figure class="items-center">
<img
class="rounded-10 w-64 overflow-hidden"
src="/images/profile-pic.png"
alt="Profile"
/>
<section class="img-bg w-64">
<img
class="rounded-10 w-64 overflow-hidden"
src="/images/profile-pic.png"
alt="Profile"
/>
</section>
</figure>
<div class="flex justify-center space-x-2 m-4">
<a
class="btn btn-accent z-10"
href={social.Gitea}
class="btn variant-ghost-primary"
href="https://git.mattmor.in"
target="_blank"
rel="personal"
>
Look at my code
</a>
<a
class="btn btn-accent z-10"
href="https://git.mattmor.in/Madmin/Academic-cv/src/branch/master/matt_morin_cv_academic.pdf"
class="btn variant-ghost-primary"
href="/CV_Matthieu_Morin.pdf"
target="_blank"
rel="cv"
>
Download my CV
Get my CV
</a>
</div>
</div>
<div class="order-2 flex justify-center lg:hidden space-x-2 m-4">
<a
class="btn btn-primary"
href={social.Gitea}
class="btn variant-ghost-primary"
href="https://git.mattmor.in"
target="_blank"
rel="personal-mob"
>
Look at my code
</a>
<a
class="btn btn-primary"
href="https://git.mattmor.in/Madmin/Academic-cv/src/branch/master/matt_morin_cv_academic.pdf"
class="btn variant-ghost-primary"
href="/CV_Matthieu_Morin.pdf"
target="_blank"
rel="cv-mob"
>
@ -78,29 +79,30 @@
</section>
<style lang="postcss">
.img-bg {
@apply absolute inset-0 z-0 rounded-full blur-[100px] transition-all;
animation:
pulse 5s cubic-bezier(0, 0.3, 1, 1) infinite,
glow 5s linear infinite;
}
@keyframes glow {
0% {
@apply bg-primary;
}
33% {
@apply bg-secondary;
}
66% {
@apply bg-accent;
}
100% {
@apply bg-primary;
}
}
@keyframes pulse {
50% {
transform: scale(1.5);
}
}
figure {
@apply flex relative flex-col;
}
.img-bg {
@apply absolute z-[-1] rounded-full blur-[100px] transition-all;
animation: pulse 5s cubic-bezier(0, 30, 150, 1) infinite, glow 5s linear infinite;
}
@keyframes glow {
0% {
@apply bg-primary-400/50;
}
33% {
@apply bg-secondary-400/50;
}
66% {
@apply bg-tertiary-400/50;
}
100% {
@apply bg-primary-400/50;
}
}
@keyframes pulse {
50% {
transform: scale(1.5);
}
}
</style>

View File

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

View File

@ -1,28 +1,27 @@
<script lang="ts">
import socials from '$lib/config/socialsObjects';
import ObfuscatedEmail from '$lib/components/ObfuscatedEmail.svelte';
import GiteaLogo from '$lib/components/logos/GiteaLogo.svelte';
import socials from '$lib/socialsObjects';
import ObfuscatedEmail from '../ObfuscatedEmail.svelte';
</script>
<div class="flex space-x-4 justify-center md:justify-start">
<div class="flex space-x-4">
<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}
target="_blank"
rel="noreferrer"
title="Gitea - private github"
><i class={socials[3].icon + ' text-3xl md:text-3xl text-base-content'} />
><GiteaLogo clazz="w-6" />
</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}
target="_blank"
rel="noreferrer"
title={socials[1].title}
><i class={socials[1].icon + ' text-3xl md:text-3xl text-base-content'} />
><i class={socials[1].icon} />
</a><ObfuscatedEmail
mail={socials[0].href}
clazz="btn btn-circle btn-outline btn-secondary btn-lg hover:btn-secondary"
h={8}
w={8}
clazz="btn btn-icon variant-soft-primary hover:variant-filled-primary h-[43px]"
iconClazz={socials[0].icon}
/>
</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">
import GiteaLogo from '$lib/assets/logos/GiteaLogo.svelte';
import SocialsCloud from '$lib/components/main/SocialsCloud.svelte';
import GiteaLogo from '$lib/components/logos/GiteaLogo.svelte';
import SocialsCloud from '$lib/components/SocialsCloud.svelte';
const year = new Date().getFullYear();
</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">
import { AccordionItem, ProgressBar } from '@skeletonlabs/skeleton';
import type { Skill } from '$lib/config/skills';
import list from '$lib/config/skills';
import type { Skill } from '$content/skills';
import list from '$content/skills';
function sortSkills(skills: Skill[]): Skill[] {
return skills.sort((a, b) => {
@ -13,21 +14,22 @@
</script>
{#each list as category}
<div class="collapse collapse-arrow outline outline-2 outline-secondary ">
<input type="radio" name="skillAccordion" />
<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 align-bottom emoji">{category.icon}</i>
<h2 class="text-3xl align-middle">{category.title}</h2>
<AccordionItem>
<svelte:fragment slot="lead">
<i class="text-3xl">{category.icon}</i>
</svelte:fragment>
<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.
<ProgressBar class="min-w-[100px] h-2" value={category.level} max={100} /> -->
</div>
<div class="collapse-content">
</svelte:fragment>
<svelte:fragment slot="content">
<div class="flex flex-col justify-center m-2 space-y-8">
{#if category.subCategories}
{#each category.subCategories as subCategory (subCategory.title)}
<div class="flex flex-col space-y-2">
<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} /> -->
</div>
@ -35,11 +37,11 @@
{#if subCategory.skills}
{#each sortSkills(subCategory.skills) as skill (skill.title)}
<span
class="mt-2 badge badge-md md:badge-lg {skill.level === 'A'
? 'badge-primary'
class="chip {skill.level === 'A'
? 'variant-filled-primary'
: skill.level === 'B'
? 'badge-accent'
: 'badge-primary badge-ghost'}"
? 'variant-outline-primary'
: 'variant-outline-tertiary'}"
>
{skill.title}
</span>
@ -50,14 +52,6 @@
{/each}
{/if}
</div>
</div>
</div>
</svelte:fragment>
</AccordionItem>
{/each}
<style>
.emoji {
position: relative;
top: +0.2em;
line-height: 2;
}
</style>

View File

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

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