Compare commits
63 Commits
old-skelet
...
master
Author | SHA1 | Date |
---|---|---|
matthieu42morin | 3094b13a59 | |
matthieu42morin | 49a499c506 | |
matthieu42morin | 8c98adb5d0 | |
matthieu42morin | 96f282d53a | |
matthieu42morin | e7b8dc721a | |
matthieu42morin | 62726a6a40 | |
matthieu42morin | 02d4ca7898 | |
matthieu42morin | 910b62cd1b | |
matthieu42morin | b687c154b0 | |
matthieu42morin | f90270bc24 | |
matthieu42morin | b0b463bea6 | |
matthieu42morin | 4e64c1b2f1 | |
matthieu42morin | 6e29c4f9e5 | |
matthieu42morin | 25f2766ec2 | |
matthieu42morin | 608045ef6c | |
matthieu42morin | 0f4f34cf79 | |
matthieu42morin | 8ec3f31973 | |
matthieu42morin | 4125839132 | |
matthieu42morin | 8905f4233d | |
matthieu42morin | 68311ecd7c | |
matthieu42morin | 37880d297b | |
matthieu42morin | c7d48b3d96 | |
matthieu42morin | 6e4aca2559 | |
matthieu42morin | 05826f9343 | |
matthieu42morin | b3ea1e4f4c | |
matthieu42morin | 53b4751ccc | |
matthieu42morin | 43534bfefd | |
matthieu42morin | 06e41a886b | |
matthieu42morin | 000948bc8a | |
matthieu42morin | b219773768 | |
matthieu42morin | 9ff7e8c734 | |
matthieu42morin | a0d0641de3 | |
matthieu42morin | db5ffe3ea8 | |
matthieu42morin | 94d5a3e3e3 | |
matthieu42morin | 7e45c838df | |
matthieu42morin | be63e9952a | |
matthieu42morin | 4c674edd98 | |
matthieu42morin | 7f6f93027f | |
matthieu42morin | a57f126d6c | |
matthieu42morin | 5d9d187000 | |
matthieu42morin | 4199070dfd | |
matthieu42morin | a902adaf43 | |
matthieu42morin | 3d19886432 | |
matthieu42morin | 6568468a41 | |
matthieu42morin | e3b53e3cce | |
matthieu42morin | 851cf69ecf | |
matthieu42morin | 3519242c06 | |
matthieu42morin | e3a74fe6e3 | |
matthieu42morin | 9b67ce3b28 | |
matthieu42morin | b16c3a04b1 | |
matthieu42morin | d0c5f79ae5 | |
matthieu42morin | c6be61d1e8 | |
matthieu42morin | 38a1e787c6 | |
matthieu42morin | 2a567b6c9a | |
matthieu42morin | e6034bce7e | |
matthieu42morin | 84156796be | |
matthieu42morin | ab4427d719 | |
matthieu42morin | c4693a458c | |
matthieu42morin | 71dbcf2abd | |
matthieu42morin | 5c6dcf7bae | |
matthieu42morin | a9ce9291e5 | |
matthieu42morin | e2794dae17 | |
matthieu42morin | e1c965efb4 |
|
@ -0,0 +1,19 @@
|
|||
# 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
|
|
@ -1,14 +0,0 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = false
|
||||
quote_type = single
|
||||
|
||||
[*.{yml, md}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"root": true,
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:svelte/recommended",
|
||||
"prettier"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": [
|
||||
"@typescript-eslint",
|
||||
"svelte"
|
||||
],
|
||||
"parserOptions": {
|
||||
"sourceType": "module",
|
||||
"ecmaVersion": 2020,
|
||||
"extraFileExtensions": [
|
||||
".svelte"
|
||||
]
|
||||
},
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2017": true,
|
||||
"node": true
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"*.svelte"
|
||||
],
|
||||
"parser": "svelte-eslint-parser",
|
||||
"parserOptions": {
|
||||
"parser": "@typescript-eslint/parser"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
|
@ -0,0 +1,32 @@
|
|||
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
|
|
@ -0,0 +1,21 @@
|
|||
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 }}
|
|
@ -0,0 +1,21 @@
|
|||
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 }}
|
|
@ -0,0 +1,35 @@
|
|||
# Contributing
|
||||
|
||||
Thanks for ur interest in contributing to Urara! Please take a moment to read this document before submitting a pull request.
|
||||
|
||||
## Pull requests
|
||||
|
||||
pls ask before u start working on any important new feature.
|
||||
|
||||
for minor features and bug fixes: I will accept them as long as I think the code quality is good enough.
|
||||
|
||||
### Commit message
|
||||
|
||||
This is not mandatory at this time, but pls use [gitmoji](https://gitmoji.dev) and [Conventional Commits](https://www.conventionalcommits.org) whenever possible.
|
||||
|
||||
### Check the code
|
||||
|
||||
Run this command to check the code:
|
||||
|
||||
```bash
|
||||
pnpm check
|
||||
```
|
||||
|
||||
In general, expect to see output like this:
|
||||
|
||||
```text
|
||||
svelte-check found 0 errors, 0 warnings, and 0 hints
|
||||
```
|
||||
|
||||
### Format the code
|
||||
|
||||
run this command to format the code:
|
||||
|
||||
```bash
|
||||
pnpm format
|
||||
```
|
|
@ -0,0 +1,32 @@
|
|||
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
|
|
@ -1,27 +0,0 @@
|
|||
name: Playwright Tests
|
||||
on:
|
||||
push:
|
||||
branches: [ main, master ]
|
||||
pull_request:
|
||||
branches: [ main, master ]
|
||||
jobs:
|
||||
test:
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
- name: Install dependencies
|
||||
run: npm install -g pnpm && pnpm install
|
||||
- name: Install Playwright Browsers
|
||||
run: pnpm exec playwright install --with-deps
|
||||
- name: Run Playwright tests
|
||||
run: pnpm exec playwright test
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
|
@ -1,12 +1,23 @@
|
|||
# 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/
|
||||
|
|
|
@ -1,14 +1,7 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
*.local
|
||||
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
.svelte-kit/**
|
||||
static/**
|
||||
build/**
|
||||
node_modules/**
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
.netlify/**
|
||||
.vercel_build_output/**
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"pluginSearchDirs": ["."],
|
||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"recommendations": ["svelte.svelte-vscode", "orta.vscode-twoslash-queries"]
|
||||
"recommendations": ["svelte.svelte-vscode", "dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]
|
||||
}
|
||||
|
|
|
@ -1,112 +1,10 @@
|
|||
{
|
||||
"editor.formatOnSave": true,
|
||||
"svelte.plugin.typescript.format.enable": true, // enable Svelte formatter for TypeScript in Svelte files
|
||||
"svelte.plugin.svelte.format.enable": true, // enable Svelte formatter for HTML and CSS in Svelte files
|
||||
"[svelte]": {
|
||||
"editor.defaultFormatter": "svelte.svelte-vscode" // enable Prettier formatter for JavaScript files
|
||||
},
|
||||
|
||||
"editor.fontFamily": "Fira Code",
|
||||
"editor.fontLigatures": true,
|
||||
"markdownlint.config": {
|
||||
"MD033": {
|
||||
"allowed_elements": ["a"]
|
||||
}
|
||||
},
|
||||
"prettier.documentSelectors": ["**/*.svelte"],
|
||||
"tailwindCSS.classAttributes": [
|
||||
"class",
|
||||
"accent",
|
||||
"active",
|
||||
"aspectRatio",
|
||||
"background",
|
||||
"bgBackdrop",
|
||||
"bgDark",
|
||||
"bgDrawer",
|
||||
"bgLight",
|
||||
"blur",
|
||||
"border",
|
||||
"button",
|
||||
"buttonClasses",
|
||||
"buttonTextFirst",
|
||||
"buttonTextLast",
|
||||
"buttonTextNext",
|
||||
"buttonTextPrevious",
|
||||
"caretClosed",
|
||||
"caretOpen",
|
||||
"color",
|
||||
"controlSeparator",
|
||||
"controlVariant",
|
||||
"cursor",
|
||||
"display",
|
||||
"element",
|
||||
"fill",
|
||||
"fillDark",
|
||||
"fillLight",
|
||||
"flex",
|
||||
"gap",
|
||||
"gridColumns",
|
||||
"height",
|
||||
"hover",
|
||||
"inactive",
|
||||
"indent",
|
||||
"justify",
|
||||
"meter",
|
||||
"padding",
|
||||
"regionAnchor",
|
||||
"regionBackdrop",
|
||||
"regionBody",
|
||||
"regionCaption",
|
||||
"regionCaret",
|
||||
"regionCell",
|
||||
"regionChildren",
|
||||
"regionCone",
|
||||
"regionContent",
|
||||
"regionControl",
|
||||
"regionDefault",
|
||||
"regionDrawer",
|
||||
"regionFoot",
|
||||
"regionFootCell",
|
||||
"regionHead",
|
||||
"regionHeadCell",
|
||||
"regionHeader",
|
||||
"regionIcon",
|
||||
"regionInterface",
|
||||
"regionInterfaceText",
|
||||
"regionLabel",
|
||||
"regionLead",
|
||||
"regionLegend",
|
||||
"regionList",
|
||||
"regionListItem",
|
||||
"regionNavigation",
|
||||
"regionPage",
|
||||
"regionPanel",
|
||||
"regionRowHeadline",
|
||||
"regionRowMain",
|
||||
"regionSummary",
|
||||
"regionSymbol",
|
||||
"regionTab",
|
||||
"regionTrail",
|
||||
"ring",
|
||||
"rounded",
|
||||
"select",
|
||||
"shadow",
|
||||
"slotDefault",
|
||||
"slotFooter",
|
||||
"slotHeader",
|
||||
"slotLead",
|
||||
"slotMessage",
|
||||
"slotMeta",
|
||||
"slotPageContent",
|
||||
"slotPageFooter",
|
||||
"slotPageHeader",
|
||||
"slotSidebarLeft",
|
||||
"slotSidebarRight",
|
||||
"slotTrail",
|
||||
"spacing",
|
||||
"text",
|
||||
"track",
|
||||
"width",
|
||||
"zIndex"
|
||||
]
|
||||
"editor.formatOnSave": true,
|
||||
"files.eol": "\n",
|
||||
"typescript.tsdk": "node_modules\\typescript\\lib",
|
||||
"css.lint.unknownAtRules": "ignore",
|
||||
"svelte.plugin.css.diagnostics.enable": false,
|
||||
"[html]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
}
|
||||
}
|
||||
|
|
29
Dockerfile
29
Dockerfile
|
@ -1,29 +0,0 @@
|
|||
FROM node:18-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
|
||||
RUN npm install -g pnpm && \
|
||||
echo "Installing pnpm..."
|
||||
RUN pnpm install --frozen-lockfile && \
|
||||
echo "Installing deps..."
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN pnpm run build && \
|
||||
echo "Building..." && \
|
||||
pnpm prune --production
|
||||
|
||||
|
||||
FROM node:18-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /app/build build/
|
||||
COPY --from=builder /app/node_modules node_modules/
|
||||
COPY package.json .
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
ENV NODE_ENV=production
|
||||
CMD [ "node", "build" ]
|
2
LICENSE
2
LICENSE
|
@ -1,4 +1,4 @@
|
|||
Copyright (c) 2023-2024 Matt Morin
|
||||
Copyright (c) 2023-2024 Matt Morin & importantimport
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
|
|
127
README.md
127
README.md
|
@ -1,5 +1,6 @@
|
|||
# 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
|
||||
|
@ -25,6 +26,130 @@ AWS lambda for automation
|
|||
|
||||
## Credits
|
||||
|
||||
This project is using [Skeleton Labs UI / Component / utils Library](https://www.skeleton.dev/) for sveltekit.
|
||||
This project is using [Skeleton Labs UI / Component / utils Library](https://www.skeleton.dev/) for sveltekit.
|
||||
This project used some logic of gitpod.io sveltekit blog with MIT License, however they have shortly pulled their site off of github, their source or license now unreachable.
|
||||
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)
|
||||
|
|
184
mdsvex.config.js
184
mdsvex.config.js
|
@ -1,73 +1,121 @@
|
|||
import { defineMDSveXConfig as defineConfig } from 'mdsvex';
|
||||
import remarkExternalLinks from 'remark-external-links';
|
||||
import remarkSetImagePath from './src/lib/utils/remark-set-image-path.js';
|
||||
import remarkLinkWithImageAsOnlyChild from './src/lib/utils/remark-link-with-image-as-only-child.js';
|
||||
import rehypeImgSize from 'rehype-img-size';
|
||||
// rehype plugins
|
||||
import rehypeSlug from 'rehype-slug'
|
||||
import rehypeAutolinkHeadings from 'rehype-autolink-headings'
|
||||
import rehypeExternalLinks from 'rehype-external-links'
|
||||
|
||||
import remarkUnwrapImages from 'remark-unwrap-images';
|
||||
import remarkToc from 'remark-toc';
|
||||
import rehypeSlug from 'rehype-slug';
|
||||
// urara remark plugins
|
||||
import { parse, join } from 'node:path'
|
||||
import { visit } from 'unist-util-visit'
|
||||
import { toString } from 'mdast-util-to-string'
|
||||
import Slugger from 'github-slugger'
|
||||
import remarkFFF from 'remark-fff'
|
||||
import remarkFootnotes from 'remark-footnotes'
|
||||
|
||||
// import { highlightCode } from './src/lib/utils/highlighter.js';
|
||||
// highlighter
|
||||
import { escapeSvelte } from 'mdsvex'
|
||||
import { lex, parse as parseFence } from 'fenceparser'
|
||||
import { renderCodeToHTML, runTwoSlash, createShikiHighlighter } from 'shiki-twoslash'
|
||||
import readingTime from 'mdsvex-reading-time'
|
||||
|
||||
/** @type {import('mdsvex').MdsvexOptions} */
|
||||
const config = defineConfig({
|
||||
extensions: ['.svelte.md', '.md', '.svx'],
|
||||
smartypants: {
|
||||
dashes: 'oldschool'
|
||||
},
|
||||
/* Wait for skeleton to implement Prismjs, for now use <CodeBlock /> in .md files */
|
||||
// layout: {
|
||||
// blog: './src/lib/components/blog/_blog-layout.svelte',
|
||||
// project: './src/lib/components/projects/_project-layout.svelte',
|
||||
// _: './src/lib/components/fallback/_layout.svelte'
|
||||
// },
|
||||
/* Plugins */
|
||||
rehypePlugins: [
|
||||
[rehypeSlug],
|
||||
[rehypeImgSize]
|
||||
// [
|
||||
// /** Custom rehype plugin to add loading="lazy" to all images */
|
||||
// () => {
|
||||
// return (tree) => {
|
||||
// visit(tree, 'element', (node) => {
|
||||
// if (node.tagName === 'img') {
|
||||
// node.properties.loading = 'lazy';
|
||||
// }
|
||||
// });
|
||||
// };
|
||||
// }
|
||||
// ]
|
||||
],
|
||||
remarkPlugins: [
|
||||
[remarkToc, { maxDepth: 3, tight: true }],
|
||||
[
|
||||
(remarkExternalLinks,
|
||||
{
|
||||
target: '_blank'
|
||||
})
|
||||
],
|
||||
[remarkUnwrapImages],
|
||||
remarkSetImagePath,
|
||||
remarkLinkWithImageAsOnlyChild
|
||||
// [
|
||||
// headings,
|
||||
// {
|
||||
// behavior: 'append',
|
||||
// linkProperties: {},
|
||||
// content: function (node) {
|
||||
// return [
|
||||
// h('span.icon.icon-link header-anchor', {
|
||||
// ariaLabel: toString(node) + ' permalink'
|
||||
// })
|
||||
// ];
|
||||
// }
|
||||
// }
|
||||
// ],
|
||||
const remarkUraraFm =
|
||||
() =>
|
||||
(tree, { data, filename }) => {
|
||||
const filepath = filename ? filename.split('/src/routes')[1] : 'unknown'
|
||||
const { dir, name } = parse(filepath)
|
||||
if (!data.fm) data.fm = {}
|
||||
// Generate slug & path
|
||||
data.fm.slug = filepath
|
||||
data.fm.path = join(dir, `/${name}`.replace('/+page', '').replace('.svelte', ''))
|
||||
// Generate ToC
|
||||
if (data.fm.toc !== false) {
|
||||
const [slugs, toc] = [new Slugger(), []]
|
||||
visit(tree, 'heading', node => {
|
||||
toc.push({
|
||||
depth: node.depth,
|
||||
title: toString(node),
|
||||
slug: slugs.slug(toString(node), false)
|
||||
})
|
||||
})
|
||||
if (toc.length > 0) data.fm.toc = toc
|
||||
else data.fm.toc = false
|
||||
}
|
||||
}
|
||||
|
||||
// remarkHeadingsPermaLinks,
|
||||
// getHeadings
|
||||
]
|
||||
});
|
||||
// Better type definitions needed
|
||||
const remarkUraraSpoiler = () => tree =>
|
||||
visit(tree, 'paragraph', node => {
|
||||
const { children } = node
|
||||
const text = children[0].value
|
||||
const re = /\|\|(.{1,}?)\|\|/g
|
||||
if (re.test(children[0].value)) {
|
||||
children[0].type = 'html'
|
||||
children[0].value = text.replace(re, (_match, p1) => `<span class="spoiler">${p1}</span>`)
|
||||
}
|
||||
return node
|
||||
})
|
||||
|
||||
export default config;
|
||||
/** @type {import("mdsvex").MdsvexOptions} */
|
||||
export default {
|
||||
extensions: ['.svelte.md', '.md'],
|
||||
smartypants: {
|
||||
dashes: 'oldschool'
|
||||
},
|
||||
layout: {
|
||||
_: './src/lib/components/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'
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
[build]
|
||||
command = "npx pnpm i --store=node_modules/.pnpm-store && npx pnpm build"
|
||||
publish = "build"
|
||||
[build.environment]
|
||||
NPM_FLAGS = "--version"
|
||||
[functions]
|
||||
node_bundler = "esbuild"
|
177
package.json
177
package.json
|
@ -1,83 +1,98 @@
|
|||
{
|
||||
"name": "its-personal~portfolio",
|
||||
"description": "Uses pnpm, svelte, mdsvex",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"private": true,
|
||||
"homepage": "mattmor.in",
|
||||
"author": "Matthieu Morin",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.mattmor.in/Madmin/its-personal/"
|
||||
},
|
||||
"packageManager": "pnpm@8.6.6",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"test": "vitest",
|
||||
"lint": "prettier --plugin-search-dir . --check . && eslint .",
|
||||
"format": "prettier --plugin-search-dir . --write .",
|
||||
"test-ct": "playwright test -c playwright-ct.config.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.41.2",
|
||||
"@skeletonlabs/skeleton": "2.0.0",
|
||||
"@skeletonlabs/tw-plugin": "0.1.0",
|
||||
"@sveltejs/kit": "^2.5.0",
|
||||
"@tailwindcss/forms": "0.5.6",
|
||||
"@tailwindcss/typography": "0.5.9",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/node": "20.5.7",
|
||||
"@types/prismjs": "^1.26.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
||||
"@typescript-eslint/parser": "^5.62.0",
|
||||
"autoprefixer": "10.4.15",
|
||||
"emoji-regex": "^10.3.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^8.10.0",
|
||||
"eslint-plugin-svelte": "^2.35.1",
|
||||
"js-cookie": "^3.0.5",
|
||||
"postcss": "8.4.29",
|
||||
"prettier": "^2.8.8",
|
||||
"prettier-plugin-svelte": "^2.10.1",
|
||||
"rehype-autolink-headings": "^7.1.0",
|
||||
"rehype-img-size": "^1.0.1",
|
||||
"rehype-slug": "^6.0.0",
|
||||
"remark-external-links": "^9.0.1",
|
||||
"remark-toc": "^9.0.0",
|
||||
"remark-unwrap-images": "^4.0.0",
|
||||
"sass": "^1.71.0",
|
||||
"shiki": "^1.1.6",
|
||||
"svelte": "^4.2.11",
|
||||
"svelte-check": "^3.6.4",
|
||||
"tailwindcss": "3.3.3",
|
||||
"tslib": "^2.6.2",
|
||||
"typescript": "^5.3.3",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"vite": "^5.1.3",
|
||||
"vite-plugin-tailwind-purgecss": "0.2.0",
|
||||
"vitest": "^0.34.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "1.5.1",
|
||||
"@fortawesome/fontawesome-free": "^6.5.1",
|
||||
"@sentry/sveltekit": "^7.102.0",
|
||||
"@sveltejs/adapter-node": "^4.0.1",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.2",
|
||||
"@threlte/core": "^6.1.1",
|
||||
"@threlte/extras": "^8.7.5",
|
||||
"@yushijinhun/three-minifier-rollup": "^0.4.0",
|
||||
"highlight.js": "11.8.0",
|
||||
"latest": "^0.2.0",
|
||||
"linkedom": "^0.15.6",
|
||||
"mdsvex": "^0.11.0",
|
||||
"prismjs": "^1.29.0",
|
||||
"rss": "^1.2.2",
|
||||
"svelte-preprocess": "^5.1.3"
|
||||
},
|
||||
"type": "module"
|
||||
"name": "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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,77 +0,0 @@
|
|||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// require('dotenv').config();
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: './tests',
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: 'html',
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
// baseURL: 'http://127.0.0.1:3000',
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on-first-retry'
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] }
|
||||
},
|
||||
|
||||
{
|
||||
name: 'firefox',
|
||||
use: { ...devices['Desktop Firefox'] }
|
||||
},
|
||||
|
||||
{
|
||||
name: 'webkit',
|
||||
use: { ...devices['Desktop Safari'] }
|
||||
},
|
||||
|
||||
/* Test against mobile viewports. */
|
||||
{
|
||||
name: 'Mobile Chrome',
|
||||
use: { ...devices['Pixel 5'] }
|
||||
},
|
||||
{
|
||||
name: 'Mobile Safari',
|
||||
use: { ...devices['iPhone 12'] }
|
||||
}
|
||||
|
||||
/* Test against branded browsers. */
|
||||
// {
|
||||
// name: 'Microsoft Edge',
|
||||
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
||||
// },
|
||||
// {
|
||||
// name: 'Google Chrome',
|
||||
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
||||
// },
|
||||
]
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
// webServer: {
|
||||
// command: 'npm run start',
|
||||
// url: 'http://127.0.0.1:3000',
|
||||
// reuseExistingServer: !process.env.CI,
|
||||
// },
|
||||
});
|
|
@ -1,6 +1,6 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,94 @@
|
|||
// See https://kit.svelte.dev/docs/types#app
|
||||
// for information about these interfaces
|
||||
// and what to do when importing types
|
||||
declare namespace App {
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface Error {}
|
||||
// interface Platform {}
|
||||
/// <reference types="@sveltejs/kit" />
|
||||
|
||||
import type { FFFBase, FFFMedia, FFFMention } from 'fff-flavored-frontmatter'
|
||||
|
||||
interface ImportMetaEnv extends Readonly<Record<string, string>> {
|
||||
readonly URARA_SITE_PROTOCOL?: 'http://' | 'https://'
|
||||
readonly URARA_SITE_DOMAIN?: string
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
glob<Module = { [key: string]: unknown }>(pattern: string): Record<string, Module>
|
||||
readonly env: ImportMetaEnv
|
||||
}
|
||||
|
||||
declare global {
|
||||
namespace Urara {
|
||||
namespace Post {
|
||||
type Frontmatter = Omit<FFFBase, 'flags'> &
|
||||
Pick<FFFMention, 'in_reply_to'> &
|
||||
Pick<FFFMedia, 'alt'> & {
|
||||
/**
|
||||
* post type.
|
||||
* @remarks auto-generated
|
||||
*/
|
||||
type: 'article' | 'note' | 'photo' | 'reply' | 'audio' | 'video' | 'like' | 'repost' | 'bookmark'
|
||||
/**
|
||||
* post layout.
|
||||
*/
|
||||
layout?: 'article' | 'note' | 'photo' | 'reply'
|
||||
/**
|
||||
* post path.
|
||||
* @remarks auto-generated
|
||||
*/
|
||||
path: string
|
||||
/**
|
||||
* post slug.
|
||||
* @remarks auto-generated
|
||||
*/
|
||||
slug: string
|
||||
/**
|
||||
* table of contents.
|
||||
* @remarks auto-generated, article-only, set to `false` to disable
|
||||
*/
|
||||
toc?: false | Toc[]
|
||||
/**
|
||||
* the created date of the post.
|
||||
* @remarks auto-generated or set manually
|
||||
*/
|
||||
created: string
|
||||
/**
|
||||
* the updated date of the post.
|
||||
* @remarks auto-generated or set manually
|
||||
*/
|
||||
updated: string
|
||||
/**
|
||||
* the published date of the post.
|
||||
*/
|
||||
published?: string
|
||||
/**
|
||||
* the featured image for article, or image for "photo" / "multi-photo" posts.
|
||||
* @remarks currently only supports string
|
||||
*/
|
||||
image?: string
|
||||
/** enable some advanced features.
|
||||
* @property hidden - deprecated, transfer to `unlisted`
|
||||
* @property unlisted - hide this post from the homepage and feed.
|
||||
* @property bridgy-fed - add a link to Bridgy Fed in the post. https://fed.brid.gy/
|
||||
* @property bridgy-{target} - add a link to Bridgy in the post. https://brid.gy/publish/{target}
|
||||
*/
|
||||
flags?: string[]
|
||||
}
|
||||
type Toc = {
|
||||
depth: number
|
||||
title?: string
|
||||
slug?: string
|
||||
children?: Toc[]
|
||||
}
|
||||
interface Module {
|
||||
default: {
|
||||
render: () => {
|
||||
html: string
|
||||
head: string
|
||||
css: {
|
||||
code: string
|
||||
}
|
||||
}
|
||||
}
|
||||
metadata: Frontmatter
|
||||
}
|
||||
}
|
||||
type Post = Post.Frontmatter & { html?: string }
|
||||
type Page = { title?: string; path: string }
|
||||
}
|
||||
}
|
||||
|
|
50
src/app.html
50
src/app.html
|
@ -1,22 +1,30 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" class="dark">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||
<link rel="icon" href="/images/profile-pic.png" />
|
||||
<!-- <link
|
||||
rel="icon"
|
||||
href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='48' height='48' viewBox='0 0 18 16'><text x='0' y='14'>🚀</text></svg>"
|
||||
/> -->
|
||||
<link rel="preload" href="/fonts/Quicksand.ttf" as="font" type="font/ttf" crossorigin />
|
||||
<!-- Dropin replacement for FontAwesome-->
|
||||
<!-- <link
|
||||
href="https://cdn.jsdelivr.net/npm/ficons@1.1.52/dist/ficons/font.css"
|
||||
rel="stylesheet"
|
||||
/> -->
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover" data-theme="wintry">
|
||||
<div style="display: contents" class="h-full overflow-hidden">%sveltekit.body%</div>
|
||||
</body>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head prefix="og: https://ogp.me/ns#">
|
||||
<meta charset="utf-8" />
|
||||
<meta name="generator" content="gh:importantimport/urara" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="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>
|
||||
</html>
|
||||
|
|
|
@ -0,0 +1,262 @@
|
|||
/* 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 */
|
|
@ -1,53 +0,0 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@tailwind variants;
|
||||
|
||||
html,
|
||||
body {
|
||||
@apply h-full overflow-hidden;
|
||||
}
|
||||
|
||||
:root [data-theme='wintry'] {
|
||||
}
|
||||
|
||||
.gradient-heading {
|
||||
@apply bg-clip-text text-transparent box-decoration-clone;
|
||||
/* Direction */
|
||||
@apply bg-gradient-to-br;
|
||||
/* Color Stops */
|
||||
@apply from-primary-500 via-tertiary-500 to-secondary-500;
|
||||
}
|
||||
/* gold-nouveau theme */
|
||||
@font-face {
|
||||
font-family: 'Quicksand';
|
||||
src: url('/fonts/Quicksand.ttf');
|
||||
font-display: swap;
|
||||
}
|
||||
/*
|
||||
@font-face {
|
||||
font-family: 'Magilio';
|
||||
src: url('/fonts/MagilioRegular.ttf') format('truetype');
|
||||
font-weight: 900;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Satoshi-Variable';
|
||||
src: url('/fonts/satoshi/Satoshi-Variable.woff2') format('woff2'),
|
||||
url('/fonts/satoshi/Satoshi-Variable.woff') format('woff'),
|
||||
url('/fonts/satoshi/Satoshi-Variable.ttf') format('truetype');
|
||||
font-weight: 300 900;
|
||||
font-display: swap;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Satoshi-VariableItalic';
|
||||
src: url('/fonts/satoshi/Satoshi-VariableItalic.woff2') format('woff2'),
|
||||
url('/fonts/satoshi/Satoshi-VariableItalic.woff') format('woff'),
|
||||
url('/fonts/satoshi/Satoshi-VariableItalic.ttf') format('truetype');
|
||||
font-weight: 300 900;
|
||||
font-display: swap;
|
||||
font-style: italic;
|
||||
} */
|
|
@ -1,32 +0,0 @@
|
|||
import type { Post } from '$lib/types/post';
|
||||
import type { MarkdownMetadata } from '$content/types';
|
||||
import type { MdsvexImport } from './types';
|
||||
import { parseReadContent } from '$content/utils';
|
||||
import { error } from '@sveltejs/kit';
|
||||
|
||||
export function listPosts() {
|
||||
const posts = import.meta.glob<Post>('./blog/*.md', {
|
||||
eager: true,
|
||||
import: 'metadata'
|
||||
});
|
||||
|
||||
return parseReadContent(posts);
|
||||
}
|
||||
|
||||
export async function getPostMetadata(slug: string) {
|
||||
const { post } = await getPost(slug);
|
||||
return post;
|
||||
}
|
||||
|
||||
export async function getPost(slug: string) {
|
||||
try {
|
||||
const data: MdsvexImport<Post & MarkdownMetadata> = await import(`./blog/${slug}.md`);
|
||||
|
||||
return {
|
||||
post: { ...data.metadata, slug },
|
||||
Component: data.default
|
||||
};
|
||||
} catch {
|
||||
throw error(404, `Unable to find blog post "${slug}"`);
|
||||
}
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
import type { MdsvexImport } from '$content/types';
|
||||
import type { MarkdownMetadata } from '$content/types';
|
||||
import type { Project } from '$lib/types/projects';
|
||||
import { parseReadContent } from './utils';
|
||||
import { error } from '@sveltejs/kit';
|
||||
|
||||
/**
|
||||
* Gets all the projects metadata
|
||||
*/
|
||||
export function listProjects() {
|
||||
const projects = import.meta.glob<Project>('./projects/*.md', {
|
||||
eager: true,
|
||||
import: 'metadata'
|
||||
});
|
||||
|
||||
return parseReadContent(projects);
|
||||
}
|
||||
export async function getProjectMetadata(slug: string) {
|
||||
const { post } = await getProject(slug);
|
||||
return post;
|
||||
}
|
||||
|
||||
export async function getProject(slug: string) {
|
||||
try {
|
||||
const data: MdsvexImport<Project & MarkdownMetadata> = await import(
|
||||
`./projects/${slug}.md`
|
||||
);
|
||||
|
||||
return {
|
||||
post: { ...data.metadata, slug },
|
||||
Component: data.default
|
||||
};
|
||||
} catch {
|
||||
throw error(404, `Unable to find project "${slug}"`);
|
||||
}
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
export interface MarkdownHeading {
|
||||
title: string;
|
||||
slug: string;
|
||||
level: number;
|
||||
children: MarkdownHeading[];
|
||||
}
|
||||
|
||||
export interface MarkdownMetadata {
|
||||
headings: MarkdownHeading[];
|
||||
}
|
||||
|
||||
export interface MdsvexImport<T extends MarkdownMetadata = MarkdownMetadata> {
|
||||
// Technically not correct but needed to make language-tools happy
|
||||
default: ConstructorOfATypedSvelteComponent;
|
||||
metadata: T;
|
||||
}
|
|
@ -1,94 +0,0 @@
|
|||
import type { create_ssr_component } from 'svelte/internal';
|
||||
|
||||
/**
|
||||
* Sorts an array of objects by their `date` property in descending order.
|
||||
* @param a - The first object to compare.
|
||||
* @param b - The second object to compare.
|
||||
* @returns A number that represents the difference between the parsed dates of the two objects.
|
||||
*/
|
||||
export function dateSort<T extends { date?: string }>(a: T, b: T): number {
|
||||
return Date.parse(b.date) - Date.parse(a.date);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders an mdsvex component and returns the resulting HTML string.
|
||||
* @param component - The mdsvex component to render.
|
||||
* @returns The HTML string that was generated by rendering the component.
|
||||
* @throws An error if the `render` property of the component is not a function.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function renderMdsvexComponent(component: any): string {
|
||||
if (typeof component['render'] != 'function') {
|
||||
throw new Error("Unable to render something that isn't a mdsvex component");
|
||||
}
|
||||
|
||||
return (component as ReturnType<typeof create_ssr_component>).render().html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an md file path to a slug by removing the last segment of the path and the `.md` extension.
|
||||
* @param path - The path of the md file.
|
||||
* @returns The slug of the md file.
|
||||
*/
|
||||
export function mdPathToSlug(path: string) {
|
||||
return path.split('/').at(-1).slice(0, -3);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an object of data that has string keys and values of type `T` that have an optional `date` property.
|
||||
* @param data - The object of data to parse.
|
||||
* @param dateSort - A function that sorts an array of objects by their `date` property in descending order.
|
||||
* @param mdPathToSlug - A function that converts an md file path to a slug.
|
||||
* @returns An array of objects that have a `slug` property and the properties of the original data objects.
|
||||
*/
|
||||
export function parseReadContent<T extends { date?: string }>(data: Record<string, T>): T[] {
|
||||
return Object.entries(data)
|
||||
.map(([file, data]) => ({
|
||||
slug: mdPathToSlug(file),
|
||||
...data
|
||||
}))
|
||||
.sort(dateSort);
|
||||
}
|
||||
|
||||
// Old utils
|
||||
// /**
|
||||
// * Formats a date string using the specified date style and locale.
|
||||
// * @param date - The date string to format.
|
||||
// * @param dateStyle - The style to use when formatting the date. Defaults to 'medium'.
|
||||
// * @param locales - The locale to use when formatting the date. Defaults to 'en'.
|
||||
// * @returns The formatted date string.
|
||||
// */
|
||||
// export function formatDate(date: string, dateStyle: DateStyle = 'medium', locales = 'en') {
|
||||
// const formatter = new Intl.DateTimeFormat(locales, { dateStyle });
|
||||
// return formatter.format(new Date(date));
|
||||
// }
|
||||
// type DateStyle = Intl.DateTimeFormatOptions['dateStyle'];
|
||||
/**
|
||||
* Formats a date string into a human-readable format.
|
||||
* @param date - The date string to format.
|
||||
* @returns A string representing the formatted date, or an empty string if the input is invalid.
|
||||
*/
|
||||
export const formatDate = (date) => {
|
||||
try {
|
||||
const d = new Date(date);
|
||||
return `${d.toLocaleString('default', {
|
||||
month: 'long'
|
||||
})} ${d.getDate()}, ${d.getFullYear()}`;
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
export const scrollIntoView = (selector: string) => {
|
||||
const scrollToElement = document.querySelector(selector);
|
||||
|
||||
if (!scrollToElement) return;
|
||||
|
||||
const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
|
||||
|
||||
scrollToElement.scrollIntoView({
|
||||
block: 'nearest',
|
||||
inline: 'start',
|
||||
behavior: mediaQuery.matches ? 'auto' : 'smooth'
|
||||
});
|
||||
};
|
|
@ -1,83 +1,73 @@
|
|||
// https://gist.github.com/acoyfellow/d8e86979c66ebea25e1643594e38be73, Rodney Lab
|
||||
|
||||
import {
|
||||
PUBLIC_DOMAIN,
|
||||
PUBLIC_SENTRY_KEY,
|
||||
PUBLIC_SENTRY_PROJECT_ID,
|
||||
PUBLIC_SENTRY_ORG_ID,
|
||||
PUBLIC_WORKER_URL
|
||||
} from '$env/static/public';
|
||||
|
||||
export const rootDomain = PUBLIC_DOMAIN; // or your server IP for dev
|
||||
import { site } from '$lib/config/site'
|
||||
import { PUBLIC_SENTRY_KEY, PUBLIC_SENTRY_PROJECT_ID, PUBLIC_SENTRY_ORG_ID } from '$env/static/public'
|
||||
|
||||
const directives = {
|
||||
'base-uri': ["'self'"],
|
||||
'child-src': ["'self'", 'blob:'],
|
||||
// 'connect-src': ["'self'", 'ws://localhost:*'],
|
||||
'connect-src': [
|
||||
"'self'",
|
||||
'ws://localhost:*',
|
||||
'https://*.sentry.io',
|
||||
'https://hcaptcha.com',
|
||||
'https://*.hcaptcha.com',
|
||||
'https://*.cartocdn.com',
|
||||
PUBLIC_DOMAIN,
|
||||
PUBLIC_WORKER_URL
|
||||
],
|
||||
'img-src': ["'self'", 'data:', 'https://images.unsplash.com'],
|
||||
'font-src': ["'self'", 'data:'],
|
||||
'form-action': ["'self'"],
|
||||
'frame-ancestors': ["'self'"],
|
||||
'frame-src': [
|
||||
"'self'",
|
||||
// "https://*.stripe.com",
|
||||
// "https://*.facebook.com",
|
||||
// "https://*.facebook.net",
|
||||
'https://hcaptcha.com',
|
||||
'https://*.hcaptcha.com',
|
||||
'https://www.openstreetmap.org',
|
||||
'https://*.cartocdn.com'
|
||||
],
|
||||
'manifest-src': ["'self'"],
|
||||
'media-src': ["'self'", 'data:'],
|
||||
'object-src': ["'none'"],
|
||||
// 'style-src': ["'self'", "'unsafe-inline'"],
|
||||
'style-src': ["'self'", "'unsafe-inline'", 'https://hcaptcha.com', 'https://*.hcaptcha.com'],
|
||||
'default-src': [
|
||||
"'self'",
|
||||
rootDomain,
|
||||
`ws://${rootDomain}`,
|
||||
// 'https://*.google.com',
|
||||
// 'https://*.googleapis.com',
|
||||
// 'https://*.firebase.com',
|
||||
// 'https://*.gstatic.com',
|
||||
// 'https://*.cloudfunctions.net',
|
||||
// 'https://*.algolia.net',
|
||||
// 'https://*.facebook.com',
|
||||
// 'https://*.facebook.net',
|
||||
// 'https://*.stripe.com',
|
||||
'https://*.sentry.io'
|
||||
],
|
||||
'script-src': [
|
||||
"'self'",
|
||||
"'unsafe-inline'",
|
||||
// 'https://*.stripe.com',
|
||||
// 'https://*.facebook.com',
|
||||
// 'https://*.facebook.net',
|
||||
'https://hcaptcha.com',
|
||||
'https://*.hcaptcha.com',
|
||||
'https://*.sentry.io',
|
||||
// 'https://polyfill.io',
|
||||
'https://*.cartocdn.com'
|
||||
],
|
||||
'worker-src': ["'self'", 'blob:'],
|
||||
//report-to can throw "Content-Security-Policy: Couldn’t process unknown directive ‘report-to’", leave it for older browsers.
|
||||
'report-to': ["'csp-endpoint'"],
|
||||
'report-uri': [
|
||||
`https://${PUBLIC_SENTRY_ORG_ID}.ingest.us.sentry.io/api/${PUBLIC_SENTRY_PROJECT_ID}/security/?sentry_key=${PUBLIC_SENTRY_KEY}`
|
||||
]
|
||||
};
|
||||
'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: Couldn’t process unknown directive ‘report-to’", leave it for older browsers.
|
||||
'report-to': ["'csp-endpoint'"],
|
||||
'report-uri': [
|
||||
`https://${PUBLIC_SENTRY_ORG_ID}.ingest.us.sentry.io/api/${PUBLIC_SENTRY_PROJECT_ID}/security/?sentry_key=${PUBLIC_SENTRY_KEY}`
|
||||
]
|
||||
}
|
||||
|
||||
export const csp = Object.entries(directives)
|
||||
.map(([key, arr]) => key + ' ' + arr.join(' '))
|
||||
.join('; ');
|
||||
.map(([key, arr]) => key + ' ' + arr.join(' '))
|
||||
.join('; ')
|
||||
|
|
|
@ -1,26 +1,22 @@
|
|||
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()
|
||||
|
|
|
@ -1,51 +1,53 @@
|
|||
import type { Handle } from '@sveltejs/kit';
|
||||
import { sequence } from '@sveltejs/kit/hooks';
|
||||
import type { Handle } from '@sveltejs/kit'
|
||||
import { sequence } from '@sveltejs/kit/hooks'
|
||||
import { site } from '$lib/config/site'
|
||||
|
||||
import { handleErrorWithSentry, sentryHandle } from '@sentry/sveltekit';
|
||||
import * as Sentry from '@sentry/sveltekit';
|
||||
import {
|
||||
PUBLIC_SENTRY_KEY,
|
||||
PUBLIC_SENTRY_PROJECT_ID,
|
||||
PUBLIC_SENTRY_ORG_ID
|
||||
} from '$env/static/public';
|
||||
import { handleErrorWithSentry, sentryHandle } from '@sentry/sveltekit'
|
||||
import * as Sentry from '@sentry/sveltekit'
|
||||
import { PUBLIC_SENTRY_KEY, PUBLIC_SENTRY_PROJECT_ID, PUBLIC_SENTRY_ORG_ID } from '$env/static/public'
|
||||
|
||||
import { csp, rootDomain } from './cspDirectives';
|
||||
import { csp } 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 ${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}"}]}`
|
||||
};
|
||||
// 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}"}]}`
|
||||
}
|
||||
|
||||
Object.entries(headers).forEach(([key, value]) => {
|
||||
response.headers.set(key, value);
|
||||
});
|
||||
return response;
|
||||
};
|
||||
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'}">`)
|
||||
})
|
||||
|
||||
// If you have custom handlers, make sure to place them after `sentryHandle()` in the `sequence` function.
|
||||
export const handle: Handle = sequence(sentryHandle(), cspHandle);
|
||||
export const handle: Handle = sequence(sentryHandle(), cspHandle, langHandle)
|
||||
|
||||
// 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/
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('sum test', () => {
|
||||
it('adds 1 + 2 to equal 3', () => {
|
||||
expect(1 + 2).toBe(3);
|
||||
});
|
||||
});
|
Binary file not shown.
Before Width: | Height: | Size: 41 KiB |
Binary file not shown.
Before Width: | Height: | Size: 170 KiB |
Binary file not shown.
Before Width: | Height: | Size: 78 KiB |
Binary file not shown.
Before Width: | Height: | Size: 246 KiB |
|
@ -1,143 +0,0 @@
|
|||
/**
|
||||
* atom-dark theme for `prism.js`
|
||||
* Based on Atom's `atom-dark` theme: https://github.com/atom/atom-dark-syntax
|
||||
* @author Joe Gibson (@gibsjose)
|
||||
*/
|
||||
|
||||
code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
color: #c5c8c6;
|
||||
text-shadow: 0 1px rgba(0, 0, 0, 0.3);
|
||||
font-family: Inconsolata, Monaco, Consolas, 'Courier New', Courier, monospace;
|
||||
direction: ltr;
|
||||
text-align: left;
|
||||
white-space: pre;
|
||||
word-spacing: normal;
|
||||
word-break: normal;
|
||||
line-height: 1.5;
|
||||
|
||||
-moz-tab-size: 4;
|
||||
-o-tab-size: 4;
|
||||
tab-size: 4;
|
||||
|
||||
-webkit-hyphens: none;
|
||||
-moz-hyphens: none;
|
||||
-ms-hyphens: none;
|
||||
hyphens: none;
|
||||
}
|
||||
|
||||
/* Code blocks */
|
||||
pre[class*="language-"] {
|
||||
padding: 1em;
|
||||
margin: .5em 0;
|
||||
overflow: auto;
|
||||
border-radius: 0.3em;
|
||||
}
|
||||
|
||||
:not(pre) > code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
background: #1d1f21;
|
||||
}
|
||||
|
||||
/* Inline code */
|
||||
:not(pre) > code[class*="language-"] {
|
||||
padding: .1em;
|
||||
border-radius: .3em;
|
||||
}
|
||||
|
||||
.token.comment,
|
||||
.token.prolog,
|
||||
.token.doctype,
|
||||
.token.cdata {
|
||||
color: #7C7C7C;
|
||||
}
|
||||
|
||||
.token.punctuation {
|
||||
color: #c5c8c6;
|
||||
}
|
||||
|
||||
.namespace {
|
||||
opacity: .7;
|
||||
}
|
||||
|
||||
.token.property,
|
||||
.token.keyword,
|
||||
.token.tag {
|
||||
color: #96CBFE;
|
||||
}
|
||||
|
||||
.token.class-name {
|
||||
color: #FFFFB6;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.token.boolean,
|
||||
.token.constant {
|
||||
color: #99CC99;
|
||||
}
|
||||
|
||||
.token.symbol,
|
||||
.token.deleted {
|
||||
color: #f92672;
|
||||
}
|
||||
|
||||
.token.number {
|
||||
color: #FF73FD;
|
||||
}
|
||||
|
||||
.token.selector,
|
||||
.token.attr-name,
|
||||
.token.string,
|
||||
.token.char,
|
||||
.token.builtin,
|
||||
.token.inserted {
|
||||
color: #A8FF60;
|
||||
}
|
||||
|
||||
.token.variable {
|
||||
color: #C6C5FE;
|
||||
}
|
||||
|
||||
.token.operator {
|
||||
color: #EDEDED;
|
||||
}
|
||||
|
||||
.token.entity {
|
||||
color: #FFFFB6;
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.token.url {
|
||||
color: #96CBFE;
|
||||
}
|
||||
|
||||
.language-css .token.string,
|
||||
.style .token.string {
|
||||
color: #87C38A;
|
||||
}
|
||||
|
||||
.token.atrule,
|
||||
.token.attr-value {
|
||||
color: #F9EE98;
|
||||
}
|
||||
|
||||
.token.function {
|
||||
color: #DAD085;
|
||||
}
|
||||
|
||||
.token.regex {
|
||||
color: #E9C062;
|
||||
}
|
||||
|
||||
.token.important {
|
||||
color: #fd971f;
|
||||
}
|
||||
|
||||
.token.important,
|
||||
.token.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.token.italic {
|
||||
font-style: italic;
|
||||
}
|
|
@ -1,124 +0,0 @@
|
|||
/**
|
||||
* Nord Theme Originally by Arctic Ice Studio
|
||||
* https://nordtheme.com
|
||||
*
|
||||
* Ported for PrismJS by Zane Hitchcoxc (@zwhitchcox) and Gabriel Ramos (@gabrieluizramos)
|
||||
*/
|
||||
|
||||
code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
color: #f8f8f2;
|
||||
background: none;
|
||||
font-family: "Fira Code", Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
|
||||
text-align: left;
|
||||
white-space: pre;
|
||||
word-spacing: normal;
|
||||
word-break: normal;
|
||||
word-wrap: normal;
|
||||
line-height: 1.5;
|
||||
-moz-tab-size: 4;
|
||||
-o-tab-size: 4;
|
||||
tab-size: 4;
|
||||
-webkit-hyphens: none;
|
||||
-moz-hyphens: none;
|
||||
-ms-hyphens: none;
|
||||
hyphens: none;
|
||||
}
|
||||
|
||||
/* Code blocks */
|
||||
pre[class*="language-"] {
|
||||
padding: 1em;
|
||||
margin: .5em 0;
|
||||
overflow: auto;
|
||||
border-radius: 0.3em;
|
||||
}
|
||||
|
||||
:not(pre) > code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
background: #2E3440;
|
||||
}
|
||||
|
||||
/* Inline code */
|
||||
:not(pre) > code[class*="language-"] {
|
||||
padding: .1em;
|
||||
border-radius: .3em;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.token.comment,
|
||||
.token.prolog,
|
||||
.token.doctype,
|
||||
.token.cdata {
|
||||
color: #636f88;
|
||||
}
|
||||
|
||||
.token.punctuation {
|
||||
color: #81A1C1;
|
||||
}
|
||||
|
||||
.namespace {
|
||||
opacity: .7;
|
||||
}
|
||||
|
||||
.token.property,
|
||||
.token.tag,
|
||||
.token.constant,
|
||||
.token.symbol,
|
||||
.token.deleted {
|
||||
color: #81A1C1;
|
||||
}
|
||||
|
||||
.token.number {
|
||||
color: #B48EAD;
|
||||
}
|
||||
|
||||
.token.boolean {
|
||||
color: #81A1C1;
|
||||
}
|
||||
|
||||
.token.selector,
|
||||
.token.attr-name,
|
||||
.token.string,
|
||||
.token.char,
|
||||
.token.builtin,
|
||||
.token.inserted {
|
||||
color: #A3BE8C;
|
||||
}
|
||||
|
||||
.token.operator,
|
||||
.token.entity,
|
||||
.token.url,
|
||||
.language-css .token.string,
|
||||
.style .token.string,
|
||||
.token.variable {
|
||||
color: #81A1C1;
|
||||
}
|
||||
|
||||
.token.atrule,
|
||||
.token.attr-value,
|
||||
.token.function,
|
||||
.token.class-name {
|
||||
color: #88C0D0;
|
||||
}
|
||||
|
||||
.token.keyword {
|
||||
color: #81A1C1;
|
||||
}
|
||||
|
||||
.token.regex,
|
||||
.token.important {
|
||||
color: #EBCB8B;
|
||||
}
|
||||
|
||||
.token.important,
|
||||
.token.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.token.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.token.entity {
|
||||
cursor: help;
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
<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>
|
After Width: | Height: | Size: 602 B |
|
@ -0,0 +1,31 @@
|
|||
<script lang="ts">
|
||||
import Envelope from '$lib/components/Envelope.svelte'
|
||||
export let mail: string;
|
||||
export let clazz: string = '';
|
||||
export let w: number = 24;
|
||||
export let h: number = 24;
|
||||
</script>
|
||||
|
||||
<div class={` ${clazz}`}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" lang="en-GB" aria-labelledby="title" class="w-{w} h-{h}">
|
||||
<title id="title">Send me a mail!</title>
|
||||
|
||||
<defs />
|
||||
|
||||
<a href="mailto:{mail}" target="_blank" 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 />
|
||||
</div>
|
||||
</foreignObject>
|
||||
</a>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
.email-icon-wrapper {
|
||||
@apply flex items-center justify-center w-full h-full;
|
||||
}
|
||||
</style>
|
|
@ -1,18 +0,0 @@
|
|||
<script lang="ts">
|
||||
import skills from '$content/skills';
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="card flex flex-col items-center justify-center mx-auto w-3/4 bg-surface-50 bg-opacity-50 p-8 m-8"
|
||||
id="skills"
|
||||
>
|
||||
<h2 class="h2 m-2">My skillset</h2>
|
||||
{#each skills as skill}
|
||||
<div class="text-lg font-bold m-2">{skill.title}</div>
|
||||
<div class="flex flex-wrap justify-center space-x-2 m-2">
|
||||
{#each skill.list as s}
|
||||
<div class="chip variant-outline-primary">{s}</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
|
@ -1,25 +0,0 @@
|
|||
<script lang="ts">
|
||||
import MatrixLogo from './logos/MatrixLogo.svelte';
|
||||
import GiteaLogo from './logos/GiteaLogo.svelte';
|
||||
import { socialLinks } from '$lib/config';
|
||||
</script>
|
||||
|
||||
<div class="flex flex-rows-auto gap-1 max-h-28">
|
||||
{#each socialLinks as link}
|
||||
<a
|
||||
class="logo-item"
|
||||
href={link.href}
|
||||
target="_blank"
|
||||
rel={link.title === 'Mastodon' ? 'me' : 'noreferrer'}
|
||||
aria-label={link.title}
|
||||
>
|
||||
{#if link.title === 'Gitea'}
|
||||
<GiteaLogo />
|
||||
{:else if link.title === 'Matrix'}
|
||||
<MatrixLogo />
|
||||
{:else}
|
||||
<i class={link.icon + ' text-3xl md:text-5xl'} />
|
||||
{/if}
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
|
@ -1,29 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export let src: string;
|
||||
export let type: string = 'video/mp4';
|
||||
export let trackSrc: string = '';
|
||||
export let srclang: string = 'en';
|
||||
export let label: string = 'english_captions';
|
||||
|
||||
let videoClass = twMerge($$props.class);
|
||||
</script>
|
||||
|
||||
<video {...$$restProps} class={videoClass}>
|
||||
<source {src} {type} />
|
||||
<slot />
|
||||
<track src={trackSrc} kind="captions" {srclang} {label} />
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
|
||||
<!--
|
||||
@component
|
||||
[Go to docs](https://flowbite-svelte.com/)
|
||||
## Props
|
||||
@prop export let src: string;
|
||||
@prop export let type: string = 'video/mp4';
|
||||
@prop export let trackSrc: string = '';
|
||||
@prop export let srclang: string = 'en';
|
||||
@prop export let label: string = 'english_captions';
|
||||
-->
|
|
@ -0,0 +1,3 @@
|
|||
<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>
|
|
@ -0,0 +1,12 @@
|
|||
<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>
|
|
@ -1,11 +0,0 @@
|
|||
<script lang="ts">
|
||||
import PostLayout from './PostLayout.svelte';
|
||||
|
||||
import type { Post } from '$lib/types/post';
|
||||
|
||||
export let post: Post;
|
||||
</script>
|
||||
|
||||
<PostLayout {...post} imagesDirectoryName="blog">
|
||||
<slot />
|
||||
</PostLayout>
|
|
@ -1,63 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { formatDate } from '$content/utils';
|
||||
import '$lib/assets/prism-nord.css';
|
||||
|
||||
export let imagesDirectoryName: string;
|
||||
export let excerpt: string = '';
|
||||
export let date: string = '';
|
||||
export let slug: string = '';
|
||||
export let title: string;
|
||||
export let image: string;
|
||||
export let tags: string[] = [];
|
||||
export let type: 'blog' | 'projects';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{title}</title>
|
||||
<meta name="description" content={excerpt} />
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:type" content="article" />
|
||||
<meta property="og:description" content={excerpt} />
|
||||
<meta property="og:url" content="https://mattmor.in/{slug}" />
|
||||
<meta property="og:image" content={image} />
|
||||
<meta property="og:type" content="article:published_time" />
|
||||
<meta property="article:published_time" content={date} />
|
||||
{#each tags as tag (tag)}
|
||||
<meta property="article:tag" content={tag} />
|
||||
{/each}
|
||||
</svelte:head>
|
||||
|
||||
<article class="flex justify-center mt-4 mb-8">
|
||||
<div class=" w-full md:w-[50rem] leading-[177.7%]">
|
||||
<header>
|
||||
<img
|
||||
src="/images/{imagesDirectoryName}/{slug}/{image}"
|
||||
alt={`${title}`}
|
||||
class=" bg-black/50 w-full aspect-[21/9] max-h-[540px] rounded-t-lg"
|
||||
/>
|
||||
</header>
|
||||
<div class="flex-auto flex justify-between items-center py-4 px-2 bg-surface-900">
|
||||
{#if tags && tags.length > 0}
|
||||
<div class="flex mb-2 items-center gap-2">
|
||||
tags: {#each tags as tag}
|
||||
<a
|
||||
data-sveltekit-preload-data="hover"
|
||||
href="/blog?{new URLSearchParams({ tag }).toString()}"
|
||||
>
|
||||
<span class="chip variant-ghost-surface">{tag}</span>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<small>On {formatDate(date)}</small>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<h2 class="h2" data-toc-ignore>{title}</h2>
|
||||
<div class="max-w-none text-token">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
<hr class="opacity-50" />
|
||||
<footer class="p-4 flex justify-start items-center space-x-4" />
|
||||
</div>
|
||||
</article>
|
|
@ -0,0 +1,12 @@
|
|||
<script lang="ts">
|
||||
export let post: Urara.Post
|
||||
const actions = import.meta.glob<any>('/src/lib/components/actions/*.svelte', { eager: true, import: 'default' })
|
||||
</script>
|
||||
|
||||
<div class="sticky top-24 hidden xl:flex flex-col gap-4 w-fit h-[calc(100vh-12rem)] ml-auto mr-8 my-8 justify-center">
|
||||
{#if Object.keys(actions).length}
|
||||
{#each Object.values(actions) as action}
|
||||
<svelte:component this={action} {post} />
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
|
@ -0,0 +1,130 @@
|
|||
<script lang="ts">
|
||||
import { browser } from '$app/environment'
|
||||
import { post as postConfig } from '$lib/config/post'
|
||||
import { posts as storedPosts } from '$lib/stores/posts'
|
||||
import { title as storedTitle } from '$lib/stores/title'
|
||||
import Reply from '$lib/components/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>
|
|
@ -0,0 +1,46 @@
|
|||
<script lang="ts">
|
||||
import type { CommentConfig } from '$lib/types/post'
|
||||
import { toSnake } from '$lib/utils/case'
|
||||
export let post: Urara.Post
|
||||
export let config: CommentConfig
|
||||
const comments = import.meta.glob<any>('/src/lib/components/comments/*.svelte', { eager: true, import: 'default' })
|
||||
let currentComment: string | undefined = undefined
|
||||
let currentConfig: unknown | undefined = undefined
|
||||
currentComment = localStorage.getItem('comment') ?? toSnake(config.use[0])
|
||||
// @ts-ignore No index signature with a parameter of type 'string' was found on type 'CommentConfig'. ts(7053)
|
||||
$: if (currentComment) currentConfig = config[currentComment]
|
||||
</script>
|
||||
|
||||
{#if config?.use.length > 0}
|
||||
<div id="post-comment" class="card card-body">
|
||||
{#if config.use.length > 1}
|
||||
<div
|
||||
class="tabs w-full mb-8"
|
||||
class:tabs-boxed={config?.['style'] === 'boxed'}
|
||||
class:tab-bordered={config?.['style'] === 'bordered'}
|
||||
class:tab-lifted={config?.['style'] === 'lifted'}>
|
||||
{#each config.use as name}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<span
|
||||
on:click={() => {
|
||||
currentComment = toSnake(name)
|
||||
localStorage.setItem('comment', toSnake(name))
|
||||
}}
|
||||
class="flex-1 tab transition-all"
|
||||
class:tab-active={currentComment === toSnake(name)}>
|
||||
{name}
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{#if currentComment}
|
||||
{#key currentComment}
|
||||
<svelte:component
|
||||
this={comments[`/src/lib/components/comments/${currentComment}.svelte`]}
|
||||
{post}
|
||||
config={currentConfig} />
|
||||
{/key}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
|
@ -0,0 +1,47 @@
|
|||
<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>
|
|
@ -0,0 +1,32 @@
|
|||
<script lang="ts" context="module">
|
||||
import Image from '$lib/components/prose/img.svelte'
|
||||
import table from '$lib/components/prose/table.svelte'
|
||||
export { Image as img, table }
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { typeOfPost } from '$lib/utils/posts'
|
||||
import Container from '$lib/components/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>
|
|
@ -0,0 +1,59 @@
|
|||
<script lang="ts">
|
||||
import Image from '$lib/components/prose/img.svelte'
|
||||
export let prev: Urara.Post | undefined = undefined
|
||||
export let next: Urara.Post | undefined = undefined
|
||||
</script>
|
||||
|
||||
<nav class="flex flex-col md:flex-row flex-warp justify-evenly">
|
||||
{#if prev}
|
||||
<div
|
||||
class:image-full={prev['image']}
|
||||
class:md:rounded-r-box={next && !next['image']}
|
||||
class="flex-1 card group rounded-none before:!rounded-none overflow-hidden">
|
||||
{#if prev['image']}
|
||||
<figure class="!block">
|
||||
<Image
|
||||
class="object-center h-full w-full absolute group-hover:scale-105 transition-transform duration-500 ease-in-out"
|
||||
src={prev['image']}
|
||||
alt={prev['alt'] ?? prev['image']} />
|
||||
</figure>
|
||||
{/if}
|
||||
<div class="card-body">
|
||||
<span class="i-heroicons-outline-chevron-left opacity-50 group-hover:opacity-100 mr-auto" />
|
||||
<a
|
||||
rel="prev"
|
||||
href={prev.path}
|
||||
class="card-title block text-left mb-0 mr-auto bg-[length:100%_0%] bg-[position:0_88%] underline decoration-3 decoration-transparent group-hover:decoration-primary hover:bg-[length:100%_100%] hover:text-primary-content bg-gradient-to-t from-primary to-primary bg-no-repeat transition-all ease-in-out duration-300">
|
||||
{prev['title'] ?? prev['summary'] ?? prev.path.slice(1)}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{#if next && !next['image'] && !prev['image']}
|
||||
<div class="flex-0 divider mx-4 md:divider-horizontal md:mx-0 md:my-4" />
|
||||
{/if}
|
||||
{/if}
|
||||
{#if next}
|
||||
<div
|
||||
class:image-full={next['image']}
|
||||
class:md:rounded-l-box={prev && !prev['image']}
|
||||
class="flex-1 card group rounded-none before:!rounded-none overflow-hidden">
|
||||
{#if next['image']}
|
||||
<figure class="!block">
|
||||
<Image
|
||||
class="object-center h-full w-full absolute group-hover:scale-105 transition-transform duration-500 ease-in-out"
|
||||
src={next['image']}
|
||||
alt={next['alt'] ?? next['image']} />
|
||||
</figure>
|
||||
{/if}
|
||||
<div class="card-body">
|
||||
<a
|
||||
rel="next"
|
||||
href={next.path}
|
||||
class="card-title block text-right mb-0 ml-auto bg-[length:100%_0%] bg-[position:0_88%] underline decoration-3 decoration-transparent group-hover:decoration-primary hover:bg-[length:100%_100%] hover:text-primary-content bg-gradient-to-t from-primary to-primary bg-no-repeat transition-all ease-in-out duration-300">
|
||||
{next['title'] ?? next['summary'] ?? next.path.slice(1)}
|
||||
</a>
|
||||
<span class="i-heroicons-outline-chevron-right opacity-50 group-hover:opacity-100 ml-auto" />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</nav>
|
|
@ -0,0 +1,17 @@
|
|||
<script lang="ts">
|
||||
let className = ''
|
||||
export { className as class }
|
||||
export let in_reply_to: Urara.Post['in_reply_to']
|
||||
</script>
|
||||
|
||||
<div class="flex flex-wrap gap-2 rounded-box outline outline-neutral/10 p-4 {className}">
|
||||
<span class="flex-none font-bold uppercase opacity-30">Reply to: </span>
|
||||
<a
|
||||
href={in_reply_to}
|
||||
rel="noopener noreferrer external"
|
||||
target="_blank"
|
||||
class="ml-auto flex-none flex rounded-badge bg-base-200 hover:bg-base-300 transition-all gap-2 px-4 u-in-reply-to">
|
||||
<span class="i-heroicons-outline-reply my-auto !w-4 !h-4" />
|
||||
{in_reply_to}
|
||||
</a>
|
||||
</div>
|
|
@ -0,0 +1,35 @@
|
|||
<script lang="ts">
|
||||
import { date } from '$lib/config/general'
|
||||
import { site } from '$lib/config/site'
|
||||
export let post: Urara.Post
|
||||
export let preview: boolean = false
|
||||
const stringPublished = new Date(post.published ?? post.created).toLocaleString(date.locales, date.options)
|
||||
const stringUpdated = new Date(post.updated ?? post.published ?? post.created).toLocaleString(date.locales, date.options)
|
||||
const jsonPublished = new Date(post.published ?? post.created).toJSON()
|
||||
const jsonUpdated = new Date(post.updated ?? post.published ?? post.created).toJSON()
|
||||
</script>
|
||||
|
||||
<div class:md:mb-4={!preview && post.type !== 'article'} class="flex font-semibold gap-1.5">
|
||||
<a
|
||||
class:hidden={preview}
|
||||
rel="author"
|
||||
class="opacity-75 hover:opacity-100 hover:text-primary duration-500 ease-in-out p-author h-card"
|
||||
href={site.protocol + site.domain}>
|
||||
{site.author.name}
|
||||
</a>
|
||||
<span class:hidden={preview} class="opacity-50">/</span>
|
||||
<a href={post.path} class="u-url u-uid swap group/time">
|
||||
<time
|
||||
class="group-hover/time:opacity-0 font-semibold opacity-75 duration-500 ease-in-out mr-auto dt-published"
|
||||
datetime={jsonPublished}
|
||||
itemprop="datePublished">
|
||||
{stringPublished}
|
||||
</time>
|
||||
<time
|
||||
class="opacity-0 group-hover/time:opacity-100 font-semibold text-primary duration-500 ease-in-out mr-auto dt-updated"
|
||||
datetime={jsonUpdated}
|
||||
itemprop="dateModified">
|
||||
{stringUpdated}
|
||||
</time>
|
||||
</a>
|
||||
</div>
|
|
@ -0,0 +1,81 @@
|
|||
<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>
|
|
@ -1,12 +1,11 @@
|
|||
<script lang="ts">
|
||||
import type { Tag } from '$lib/types/post';
|
||||
export let selected: Tag | null = null;
|
||||
let className = '';
|
||||
export { className as class };
|
||||
export let className = '';
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
let options: Tag[] = ['DevOps', 'Philosophy', 'Updates'];
|
||||
const tags: Tag[] = ['Blog', 'Projects', 'Updates'];
|
||||
|
||||
const clickHandler = (value: Tag) => {
|
||||
if (value === selected) {
|
||||
|
@ -24,7 +23,7 @@
|
|||
<section class="flex justify-center flex-col items-center {className}">
|
||||
<h3 class="h3 mb-2 md:mb-3">Sort by category</h3>
|
||||
<ul class="flex flex-wrap justify-center gap-2">
|
||||
{#each options as option}
|
||||
{#each tags as option}
|
||||
<li>
|
||||
<button
|
||||
class="chip {option === selected
|
|
@ -0,0 +1,75 @@
|
|||
<script lang="ts">
|
||||
import Preview from '$lib/components/blog/Preview.svelte';
|
||||
import CategoryFilter from '$lib/components/blog/CategoryFilter.svelte';
|
||||
import type { Post, Tag } from '$lib/types/post';
|
||||
import { H_ELLIPSIS_ENTITY } from '$lib/constants';
|
||||
import { page } from '$app/stores';
|
||||
import { onMount } from 'svelte';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
export let posts: Post[] = [];
|
||||
|
||||
let filter: Tag | null = null;
|
||||
export const displayAmount = 8;
|
||||
|
||||
$: showPosts = displayAmount;
|
||||
$: postCount = posts.length;
|
||||
$: posts = data.posts.filter((post: Post) => (filter ? post.tags?.includes(filter) : true));
|
||||
$: displayPosts = posts.slice(displayAmount);
|
||||
|
||||
const handleClick = () => {
|
||||
showPosts += displayAmount;
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
const tagParam = $page.url.searchParams.get('tag');
|
||||
|
||||
if (!filter && typeof tagParam == 'string') {
|
||||
filter = tagParam as Tag;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<section role="feed">
|
||||
<div class="space-y-8">
|
||||
<header class="flex flex-col justify-center items-center">
|
||||
<h1 class="h1 m-4">Blog</h1>
|
||||
<CategoryFilter className="mb-2 md:mb-4" bind:selected={filter} />
|
||||
</header>
|
||||
<div
|
||||
class="grid m-auto max-w-7xl w-full gap-6 grid-cols-none justify-center md:grid-cols-2 lg:grid-cols-3"
|
||||
>
|
||||
{#each posts.slice(0, displayAmount) as post, index}
|
||||
<article
|
||||
class="flex justify-center min-w-[20rem] max-w-sm"
|
||||
aria-posinset={index + 1}
|
||||
aria-setsize={postCount}
|
||||
>
|
||||
<Preview {post} type="blog" />
|
||||
</article>
|
||||
{:else}
|
||||
<p>No posts yet!</p>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if posts.slice(displayAmount).length > 0}
|
||||
<div>
|
||||
<h2 class="mb-4 text-center">Previous posts</h2>
|
||||
<div
|
||||
class="previous grid m-auto max-w-7xl w-full gap-6 grid-cols-none justify-center md:grid-cols-2 lg:grid-cols-3"
|
||||
>
|
||||
{#each displayPosts as post}
|
||||
<div class="flex justify-center min-w-[20rem] max-w-sm">
|
||||
<Preview {post} type="blog" />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if showPosts < postCount}
|
||||
<button type="submit" on:click={handleClick}>See more {H_ELLIPSIS_ENTITY}</button>
|
||||
{/if}
|
||||
</section>
|
|
@ -0,0 +1,32 @@
|
|||
<!-- <script lang="ts" context="module">
|
||||
import Image from '$lib/components/prose/img.svelte'
|
||||
import table from '$lib/components/prose/table.svelte'
|
||||
export { Image as img, table }
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { typeOfPost } from '$lib/utils/posts'
|
||||
import Container from '$lib/components/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>
|
|
@ -0,0 +1,69 @@
|
|||
<script lang="ts">
|
||||
import SEO from '$lib/components/SEO/index.svelte';
|
||||
import '$lib/assets/prism-nord.css';
|
||||
import type { Post } from '$lib/types/post';
|
||||
import Tags from '$lib/components/blog/Tags.svelte';
|
||||
|
||||
export let post: Post;
|
||||
|
||||
const {
|
||||
postTitle,
|
||||
datePublished,
|
||||
featuredImage,
|
||||
featuredImageAlt,
|
||||
lastUpdated,
|
||||
seoMetaDescription: metadescription,
|
||||
slug,
|
||||
timeToRead,
|
||||
ogImage,
|
||||
ogSquareImage,
|
||||
twitterImage
|
||||
} = post || {};
|
||||
|
||||
const breadcrumbs = [
|
||||
{ name: 'Home', slug: '' },
|
||||
{ name: 'type', slug: post.type },
|
||||
{ name: postTitle, slug }
|
||||
];
|
||||
</script>
|
||||
|
||||
<SEO
|
||||
article
|
||||
{breadcrumbs}
|
||||
{slug}
|
||||
{postTitle}
|
||||
{datePublished}
|
||||
{lastUpdated}
|
||||
{metadescription}
|
||||
{timeToRead}
|
||||
{featuredImage}
|
||||
{ogImage}
|
||||
{ogSquareImage}
|
||||
{twitterImage}
|
||||
/>
|
||||
|
||||
<section class="flex justify-center mt-4 mb-8">
|
||||
<div
|
||||
class=" w-full md:w-[50rem] leading-[177.7%] bg-white/50 dark:bg-black/50 m-2 rounded-t-lg mx-auto"
|
||||
>
|
||||
<header>
|
||||
<img
|
||||
src={featuredImage}
|
||||
alt={featuredImageAlt}
|
||||
class=" w-full aspect-[21/9] max-h-[540px] rounded-t-lg"
|
||||
/>
|
||||
</header>
|
||||
<Tags {post} />
|
||||
<div class="space-y-4 m-8">
|
||||
<h2 class="h2" data-toc-ignore>{post.title}</h2>
|
||||
<div class="max-w-none md:w-[720px]">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
<hr class="opacity-50" />
|
||||
<footer class="p-4 flex justify-start items-center space-x-4" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style lang="postcss">
|
||||
</style>
|
|
@ -1,10 +1,10 @@
|
|||
<script lang="ts">
|
||||
import { isAnExternalLink } from '$lib/utils/helpers';
|
||||
import { isAnExternalLink, generateURL } from '$lib/utils/helpers';
|
||||
import type { Post } from '$lib/types/post';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { formatDate } from '$lib/utils/blog';
|
||||
|
||||
export let isMostRecent: boolean = false;
|
||||
export let type: Post['type'] = 'blog' | 'projects';
|
||||
export let type: Post['type'];
|
||||
export let post: Post;
|
||||
// export let published: boolean;
|
||||
// export let headlineOrder: 'h3' | '' = '';
|
||||
|
@ -13,7 +13,7 @@
|
|||
|
||||
//window width
|
||||
let iteration = 0;
|
||||
let interval;
|
||||
let interval: string | number | NodeJS.Timeout | undefined;
|
||||
|
||||
onMount(() => {
|
||||
const interval = setInterval(() => {
|
||||
|
@ -26,23 +26,13 @@
|
|||
}, 1000);
|
||||
});
|
||||
|
||||
const generateURL = (href?: string, slug?: string) => {
|
||||
if (href) return href;
|
||||
return `/${type}/${slug}`;
|
||||
};
|
||||
|
||||
$: href = generateURL(post['href'], post.slug);
|
||||
$: href = generateURL(post['href'], post.type, post.slug);
|
||||
|
||||
$: target = post && post['href'] && isAnExternalLink(post['href']) ? '_blank' : undefined;
|
||||
|
||||
const displayDate = new Date(Date.parse(post.date ?? '')).toLocaleDateString(undefined, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
});
|
||||
onDestroy(() => {
|
||||
clearInterval(interval);
|
||||
});
|
||||
clearInterval(interval);
|
||||
});
|
||||
</script>
|
||||
|
||||
<a
|
||||
|
@ -53,10 +43,15 @@
|
|||
<!-- Blog in long cols, projects in wide rows -->
|
||||
<div class="flex {type === 'blog' ? 'flex-col' : 'flex-row'} justify-between w-full h-full">
|
||||
<header>
|
||||
<img
|
||||
<!-- <img
|
||||
src={`/images/${type}/${post.slug}/${post.image}`}
|
||||
class="bg-black/200 w-full aspect-[3/2]"
|
||||
alt="Post preview"
|
||||
/> -->
|
||||
<img
|
||||
src={post.featuredImage}
|
||||
alt={post.title}
|
||||
class="bg-black/200 h-[448px] w-[672px] aspect-[3/2]"
|
||||
/>
|
||||
</header>
|
||||
<section class="p-4 space-y-4">
|
||||
|
@ -88,9 +83,9 @@
|
|||
</div>
|
||||
<div class="mt-auto">
|
||||
<small>
|
||||
{#if post.date}
|
||||
{#if post.datePublished}
|
||||
<span class="text-sm ml-4">
|
||||
{displayDate}
|
||||
{formatDate(post.datePublished)}
|
||||
</span>
|
||||
{/if}
|
||||
</small>
|
|
@ -0,0 +1,22 @@
|
|||
<script lang="ts">
|
||||
import type { Post } from '$lib/types/post';
|
||||
import { formatDate } from '$lib/utils/blog';
|
||||
|
||||
export let post: Post;
|
||||
</script>
|
||||
|
||||
<section class="flex-auto flex justify-between items-center py-4 px-2 m-8">
|
||||
{#if post.tags && post.tags.length > 0}
|
||||
<div class="flex mb-2 items-center gap-2">
|
||||
tags: {#each post.tags as tag}
|
||||
<a
|
||||
data-sveltekit-preload-data="hover"
|
||||
href="/{post.type}{new URLSearchParams({ tag }).toString()}"
|
||||
>
|
||||
<span class="chip variant-ghost-surface">{tag}</span>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<small>On {formatDate(post.datePublished)}</small>
|
||||
</section>
|
|
@ -0,0 +1,40 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte'
|
||||
import { site } from '$lib/config/site'
|
||||
import type { GiscusConfig } from '$lib/types/post'
|
||||
export let config: GiscusConfig
|
||||
|
||||
onMount(() => {
|
||||
const giscus = document.createElement('script')
|
||||
Object.entries({
|
||||
src: config.src ?? 'https://giscus.app/client.js',
|
||||
'data-repo': config.repo,
|
||||
'data-repo-id': config.repoID,
|
||||
'data-category': config.category ?? '',
|
||||
'data-category-id': config.categoryID,
|
||||
'data-mapping': 'pathname',
|
||||
'data-reactions-enabled': config.reactionsEnabled === false ? '0' : '1',
|
||||
'data-input-position': config.inputPosition ?? 'bottom',
|
||||
'data-theme': config.theme ?? 'preferred_color_scheme',
|
||||
'data-lang': config.lang ?? site.lang ?? 'en',
|
||||
'data-loading': config.loading ?? '',
|
||||
crossorigin: 'anonymous',
|
||||
async: ''
|
||||
}).forEach(([key, value]) => giscus.setAttribute(key, value))
|
||||
setTimeout(() => {
|
||||
const observer = new MutationObserver(() => {
|
||||
document.getElementById('giscus-loading')!.remove()
|
||||
observer.disconnect()
|
||||
})
|
||||
observer.observe(document.getElementById('giscus')!, {
|
||||
childList: true
|
||||
})
|
||||
document.getElementById('giscus-container')!.appendChild(giscus)
|
||||
}, 1000)
|
||||
})
|
||||
</script>
|
||||
|
||||
<div id="giscus-container">
|
||||
<button id="giscus-loading" class="btn btn-lg flex mx-auto my-4 btn-ghost btn-circle loading" />
|
||||
<div id="giscus" class="giscus" />
|
||||
</div>
|
|
@ -0,0 +1,63 @@
|
|||
<script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte'
|
||||
import type { Remark42Config } from '$lib/types/post'
|
||||
export let post: Urara.Post
|
||||
export let config: Remark42Config
|
||||
|
||||
let remark42Instance: any
|
||||
|
||||
onMount(() => {
|
||||
const [c, s] = [document.createElement('script'), document.createElement('script')]
|
||||
|
||||
c.id = 'remark42_config'
|
||||
c.type = 'application/javascript'
|
||||
c.innerHTML = `
|
||||
var remark_config = {
|
||||
host: '${config.host}',
|
||||
site_id: '${config.site_id || 'remark'}',
|
||||
url: '${post.path}',
|
||||
components: [${config.components || "'embed'"}],
|
||||
max_shown_comments: ${config.max_shown_comments || 15},
|
||||
max_last_comments: ${config.max_last_comments || 15},
|
||||
theme: '${config.theme || 'light'}',
|
||||
page_title: '${config.page_title || post.title}',
|
||||
locale: '${config.locale || 'en'}',
|
||||
show_email_subscription: ${config.show_email_subscription || true},
|
||||
show_rss_subscription: ${config.show_rss_subscription || true},
|
||||
simple_view: ${config.simple_view || false},
|
||||
no_footer: ${config.no_footer || false},
|
||||
}`
|
||||
|
||||
s.id = 'remark42_script'
|
||||
s.type = 'application/javascript'
|
||||
s.innerHTML = `!function(e,n){for(var o=0;o<e.length;o++){var r=n.createElement("script"),c=".js",d=n.head||n.body;"noModule"in r?(r.type="module",c=".mjs"):r.async=!0,r.defer=!0,r.src='${config.host}/web/'+e[o]+c,d.appendChild(r)}}(remark_config.components||["embed"],document);`
|
||||
document.head.appendChild(c)
|
||||
document.head.appendChild(s)
|
||||
|
||||
const opt = {
|
||||
...config,
|
||||
url: post.path
|
||||
}
|
||||
|
||||
const checkRemark42 = () => {
|
||||
if ((window as any).REMARK42) {
|
||||
remark42Instance = (window as any).REMARK42.createInstance({
|
||||
node: document.getElementById('remark42') as HTMLElement,
|
||||
...opt
|
||||
})
|
||||
} else {
|
||||
setTimeout(checkRemark42, 100)
|
||||
}
|
||||
}
|
||||
|
||||
checkRemark42()
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
if (remark42Instance && typeof remark42Instance.destroy === 'function') {
|
||||
remark42Instance.destroy()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div id="remark42" />
|
|
@ -0,0 +1,33 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte'
|
||||
import type { UtterancesConfig } from '$lib/types/post'
|
||||
export let config: UtterancesConfig
|
||||
|
||||
onMount(() => {
|
||||
const utterances = document.createElement('script')
|
||||
Object.entries({
|
||||
src: config.src ?? 'https://utteranc.es/client.js',
|
||||
repo: config.repo,
|
||||
'issue-term': 'pathname',
|
||||
label: config.label ?? '',
|
||||
theme: config.theme ?? 'preferred-color-scheme',
|
||||
crossorigin: 'anonymous',
|
||||
async: ''
|
||||
}).forEach(([key, value]) => utterances.setAttribute(key, value))
|
||||
setTimeout(() => {
|
||||
const observer = new MutationObserver(() => {
|
||||
document.getElementById('utterances-loading')!.remove()
|
||||
observer.disconnect()
|
||||
})
|
||||
observer.observe(document.getElementById('utterances')!, {
|
||||
childList: true
|
||||
})
|
||||
document.getElementById('utterances-container')!.appendChild(utterances)
|
||||
}, 1000)
|
||||
})
|
||||
</script>
|
||||
|
||||
<div id="utterances-container">
|
||||
<button id="utterances-loading" class="btn btn-lg flex mx-auto my-4 btn-ghost btn-circle loading" />
|
||||
<div id="utterances" class="utterances" />
|
||||
</div>
|
|
@ -0,0 +1,204 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte'
|
||||
import { site } from '$lib/config/site'
|
||||
import type { WebmentionConfig } from '$lib/types/post'
|
||||
export let config: WebmentionConfig
|
||||
export let post: Urara.Post
|
||||
|
||||
interface WebmentionFeed {
|
||||
type: 'feed'
|
||||
name: 'Webmentions'
|
||||
children: WebmentionEntry[]
|
||||
}
|
||||
|
||||
interface WebmentionEntry {
|
||||
url: string
|
||||
author?: {
|
||||
name?: string
|
||||
photo?: string
|
||||
url?: string
|
||||
}
|
||||
content?: {
|
||||
html?: string
|
||||
text?: string
|
||||
}
|
||||
rsvp?: string
|
||||
published?: string
|
||||
'wm-received': string
|
||||
'wm-source': string
|
||||
'wm-target': string
|
||||
'wm-id': number
|
||||
'wm-property': 'in-reply-to' | 'like-of' | 'repost-of' | 'bookmark-of' | 'mention-of' | 'rsvp'
|
||||
'wm-private': boolean
|
||||
}
|
||||
|
||||
let [page, loaded, end, mentions, sortDirUp]: [number, boolean, boolean, WebmentionEntry[], boolean] = [
|
||||
0,
|
||||
false,
|
||||
false,
|
||||
[],
|
||||
config?.sortDir === 'up' ? true : false
|
||||
]
|
||||
|
||||
const load = async () =>
|
||||
await fetch(
|
||||
`https://webmention.io/api/mentions.jf2?page=${page}&per-page=${config?.perPage ?? '20'}&sort-by=${
|
||||
config?.sortBy ?? 'created'
|
||||
}&sort-dir=${sortDirUp ? 'up' : 'down'}${
|
||||
config?.property && config.property.forEach(wmProperty => `&wm-property=${wmProperty}`)
|
||||
}&target[]=${site.protocol + site.domain + post.path}&target[]=${site.protocol + site.domain + post.path}/`
|
||||
)
|
||||
.then(res => res.json())
|
||||
.then((feed: WebmentionFeed) => {
|
||||
if (feed.children.length < 10) end = true
|
||||
feed = {
|
||||
...feed,
|
||||
children: feed.children.filter(
|
||||
(entry: WebmentionEntry) => !config.blockList?.includes(new URL(entry['wm-source']).origin)
|
||||
)
|
||||
}
|
||||
if (feed.children.length > 0) mentions = [...mentions, ...feed.children]
|
||||
page++
|
||||
loaded = true
|
||||
})
|
||||
|
||||
const reset = async () => {
|
||||
page = 0
|
||||
loaded = false
|
||||
end = false
|
||||
mentions = []
|
||||
await load()
|
||||
}
|
||||
|
||||
onMount(() => load())
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-8">
|
||||
<div class="flex">
|
||||
<p class="flex-1 m-auto italic opacity-50">
|
||||
{`sort-by=${config?.sortBy ?? 'created'}&sort-dir=${sortDirUp ? 'up' : 'down'}`}
|
||||
</p>
|
||||
<button
|
||||
class="btn btn-ghost btn-sm float-right"
|
||||
on:click={() => {
|
||||
sortDirUp = !sortDirUp
|
||||
reset()
|
||||
}}>
|
||||
{#if sortDirUp === true}
|
||||
<span class="i-heroicons-outline-sort-ascending" />
|
||||
{:else}
|
||||
<span class="i-heroicons-outline-sort-descending" />
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
{#key mentions}
|
||||
{#each mentions as mention}
|
||||
{@const [wmProperty, borderColor, textColor, tooltipColor] = {
|
||||
'in-reply-to': ['💬 replied', 'border-primary/50', 'text-primary', 'tooltip-primary'],
|
||||
'like-of': ['❤️ liked', 'border-secondary/50', 'text-secondary', 'tooltip-secondary'],
|
||||
'repost-of': ['🔄 reposted', 'border-accent/50', 'text-accent', 'tooltip-accent'],
|
||||
'bookmark-of': ['⭐️ bookmarked', 'border-neutral/50', 'text-neutral', 'tooltip-neutral'],
|
||||
'mention-of': ['💬 mentioned', 'border-base-300/50', 'text-base-content', 'tooltip-base-content'],
|
||||
rsvp: [
|
||||
`📅 RSVPed ${
|
||||
mention.rsvp &&
|
||||
{
|
||||
yes: '✅',
|
||||
no: '❌',
|
||||
interested: '💡',
|
||||
maybe: '💭'
|
||||
}[mention.rsvp]
|
||||
}`,
|
||||
'border-warning/50',
|
||||
'text-warning',
|
||||
'tooltip-warning'
|
||||
]
|
||||
}[mention['wm-property']]}
|
||||
{#if mention.url !== null}
|
||||
<div class="{borderColor} border-2 rounded-box p-4">
|
||||
<div class="flex bg-base-200 rounded-btn">
|
||||
{#if mention?.author?.photo}
|
||||
<img
|
||||
class="w-12 h-12 flex-none rounded-btn"
|
||||
src={mention.author.photo}
|
||||
alt={mention.author?.name ?? new URL(mention.url).host}
|
||||
loading="lazy"
|
||||
decoding="async" />
|
||||
{/if}
|
||||
<div class="flex-1 px-4 py-2 m-auto">
|
||||
<p>
|
||||
{#if mention?.author?.url}
|
||||
<a class="font-semibold {textColor} hover:underline" href={mention.author.url}>
|
||||
{mention.author?.name ?? new URL(mention.url).host}
|
||||
</a>
|
||||
{:else}
|
||||
{mention?.author?.name ?? new URL(mention.url).host}
|
||||
{/if}
|
||||
<a class="{textColor} hover:underline" href={mention['wm-source']}>
|
||||
{wmProperty}
|
||||
</a>
|
||||
this post on
|
||||
<span
|
||||
class="tooltip tooltip-bottom xl:tooltip-right {tooltipColor}"
|
||||
data-tip={new Date(mention.published ?? mention['wm-received']).toLocaleString()}>
|
||||
{mention.published ? mention.published.slice(0, 10) : mention['wm-received'].slice(0, 10)}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{#if mention.content}
|
||||
<div class="prose max-w-none break-words mt-4">
|
||||
<p>{@html mention.content?.html ?? mention.content?.text}</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
{/key}
|
||||
{#if loaded === true}
|
||||
{#if end !== true}
|
||||
<button
|
||||
on:click={() => {
|
||||
loaded = false
|
||||
load()
|
||||
}}
|
||||
class="btn btn-primary btn-block">
|
||||
LOAD
|
||||
</button>
|
||||
{:else if config?.form !== true}
|
||||
<div class="divider mt-0 -mb-2">END</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<button id="webmention-loading" class="btn btn-lg btn-block flex btn-ghost loading" />
|
||||
{/if}
|
||||
{#if config?.form === true}
|
||||
<form id="webmention-form" method="post" action="https://webmention.io/{config.username}/webmention">
|
||||
<input type="hidden" name="target" value={site.protocol + site.domain + post.path} />
|
||||
<div class="label gap-4">
|
||||
<span class="label-text">send webmentions here:</span>
|
||||
{#if config?.commentParade === true}
|
||||
<span class="label-text-alt text-right">
|
||||
or <a
|
||||
class="hover:!text-primary"
|
||||
href="https://quill.p3k.io/?dontask=1&me=https://commentpara.de/&reply={encodeURI(
|
||||
site.protocol + site.domain + post.path
|
||||
)}">
|
||||
comment anonymously
|
||||
</a>
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<div class="flex-1">
|
||||
<input
|
||||
class="input input-bordered focus:input-primary w-full"
|
||||
type="text"
|
||||
id="reply-url"
|
||||
name="source"
|
||||
placeholder="https://example.com/my-post" />
|
||||
</div>
|
||||
<button class="btn btn-primary flex-none mt-auto" type="submit" id="webmention-submit">Send</button>
|
||||
</div>
|
||||
</form>
|
||||
{/if}
|
||||
</div>
|
|
@ -0,0 +1,35 @@
|
|||
<script lang="ts">
|
||||
export let title: string | undefined = undefined
|
||||
export let description: string | undefined = undefined
|
||||
export let status: 'info' | 'success' | 'warning' | 'error' | undefined = undefined
|
||||
</script>
|
||||
|
||||
<div
|
||||
class:alert-info={status === 'info'}
|
||||
class:alert-success={status === 'success'}
|
||||
class:alert-warning={status === 'warning'}
|
||||
class:alert-error={status === 'error'}
|
||||
class="alert flex-col shadow-inner my-4">
|
||||
<div class="mr-auto">
|
||||
{#if status === 'success'}
|
||||
<span class="i-heroicons-outline-check-circle" />
|
||||
{:else if status === 'warning'}
|
||||
<span class="i-heroicons-outline-exclamation-circle" />
|
||||
{:else if status === 'error'}
|
||||
<span class="i-heroicons-outline-x-circle" />
|
||||
{:else}
|
||||
<span class="i-heroicons-outline-information-circle" />
|
||||
{/if}
|
||||
<div>
|
||||
<div class:font-bold={description}>{title}</div>
|
||||
{#if description}
|
||||
<div class="text-xs">{description}</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if $$slots.default}
|
||||
<div class="block w-full">
|
||||
<slot />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
|
@ -0,0 +1,75 @@
|
|||
<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>
|
|
@ -0,0 +1,44 @@
|
|||
<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}
|
|
@ -1,105 +1,106 @@
|
|||
<script lang="ts">
|
||||
import QuickLinks from '$lib/components/home/QuickLinks.svelte';
|
||||
import QuickLinks from '$lib/components/home/QuickLinks.svelte';
|
||||
import { social } from '$lib/config/site';
|
||||
</script>
|
||||
<section class="grid grid-cols-1 lg:grid-cols-2 mx-4">
|
||||
<!-- Text and links-->
|
||||
<div class="order-1 max-w-3/4 space-y-8">
|
||||
<h1 class="h1">Hello, I'm Matt.</h1>
|
||||
|
||||
<p class="text-xl opacity-75" data-svelte-h="svelte-169iyno">
|
||||
A dev with an array of skills from from frontend and devops to design. I have a
|
||||
strong passion for innovation and change in tech, automation and solving 👾 problems
|
||||
.
|
||||
<br />From wearing a lot of 🤠 hats in past projects and startups I became a
|
||||
generalist, now I am actively deepening my knowledge in software:
|
||||
<span
|
||||
class=" text-4xl bg-gradient-to-r from-primary-800 via-secondary-900 to-tertiary-900 dark:from-primary-400 dark:via-secondary-400 dark:to-tertiary-400 text-transparent bg-clip-text"
|
||||
>DevOps, CyberSec and AI.</span
|
||||
>
|
||||
</p>
|
||||
<QuickLinks />
|
||||
|
||||
</div>
|
||||
<!-- Logo and buttons-->
|
||||
<div class="order-2 hidden lg:block">
|
||||
<figure class="items-center">
|
||||
<img
|
||||
class="rounded-10 w-64 overflow-hidden"
|
||||
src="/images/profile-pic.png"
|
||||
alt="Profile"
|
||||
/>
|
||||
<section class="img-bg w-64">
|
||||
<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'm Matt.</h1>
|
||||
|
||||
<p class="text-xl opacity-75">
|
||||
A 🧠 that consumes ⚡️ and produces 👾 bug$... No sorry, produces code that works 100%
|
||||
of the time. Really.
|
||||
|
||||
<br /> Usually, this creature you are reading about exhibits "passion" for innovation
|
||||
and change. It gets dopamine and reward stimuli from solving problems ~~~ the "Ahaa"
|
||||
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"
|
||||
>DevOps, Web, AI & IoT</span
|
||||
>
|
||||
</p>
|
||||
<QuickLinks />
|
||||
</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="rounded-10 w-64 overflow-hidden"
|
||||
src="/images/profile-pic.png"
|
||||
class="mask mask-squircle w-64 overflow-hidden"
|
||||
src="/assets/maskable@512.png"
|
||||
alt="Profile"
|
||||
/>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</figure>
|
||||
<div class="flex justify-center space-x-2 m-4">
|
||||
<a
|
||||
class="btn variant-ghost-primary"
|
||||
href="https://git.mattmor.in"
|
||||
target="_blank"
|
||||
rel="personal"
|
||||
>
|
||||
Look at my code
|
||||
</a>
|
||||
<a
|
||||
class="btn variant-ghost-primary"
|
||||
href="/CV_Matthieu_Morin.pdf"
|
||||
target="_blank"
|
||||
rel="cv"
|
||||
>
|
||||
Get my CV
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="order-2 flex justify-center lg:hidden space-x-2 m-4">
|
||||
|
||||
<div class="flex justify-center space-x-2 m-4">
|
||||
<a
|
||||
class="btn variant-ghost-primary"
|
||||
href="https://git.mattmor.in"
|
||||
class="btn btn-accent z-10"
|
||||
href={social.Gitea}
|
||||
target="_blank"
|
||||
rel="personal-mob"
|
||||
rel="personal"
|
||||
>
|
||||
Look at my code
|
||||
</a>
|
||||
<a
|
||||
class="btn variant-ghost-primary"
|
||||
href="/CV_Matthieu_Morin.pdf"
|
||||
class="btn btn-accent z-10"
|
||||
href="https://git.mattmor.in/Madmin/Academic-cv/src/branch/master/matt_morin_cv_academic.pdf"
|
||||
target="_blank"
|
||||
rel="cv-mob"
|
||||
rel="cv"
|
||||
>
|
||||
Download my CV
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="order-2 flex justify-center lg:hidden space-x-2 m-4">
|
||||
<a
|
||||
class="btn btn-primary"
|
||||
href={social.Gitea}
|
||||
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"
|
||||
target="_blank"
|
||||
rel="cv-mob"
|
||||
>
|
||||
Download my CV
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style lang="postcss">
|
||||
figure {
|
||||
@apply flex relative flex-col;
|
||||
}
|
||||
.img-bg {
|
||||
@apply absolute z-[-1] rounded-full blur-[100px] transition-all;
|
||||
animation: pulse 5s cubic-bezier(0, 30, 150, 1) infinite, glow 5s linear infinite;
|
||||
}
|
||||
@keyframes glow {
|
||||
0% {
|
||||
@apply bg-primary-400/50;
|
||||
}
|
||||
33% {
|
||||
@apply bg-secondary-400/50;
|
||||
}
|
||||
66% {
|
||||
@apply bg-tertiary-400/50;
|
||||
}
|
||||
100% {
|
||||
@apply bg-primary-400/50;
|
||||
}
|
||||
}
|
||||
@keyframes pulse {
|
||||
50% {
|
||||
transform: scale(1.5);
|
||||
}
|
||||
}
|
||||
.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);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,26 +1,31 @@
|
|||
<section class="grid grid-cols-1 md:grid-cols-3 gap-4 lg:gap-8" data-svelte-h="svelte-1u9sn7t">
|
||||
<div class="card variant-ringed-hollow p-4 md:p-8 space-y-4">
|
||||
<i class="fa-solid fa-screwdriver-wrench text-4xl text-primary-500" />
|
||||
<h3 class="h3">Development</h3>
|
||||
<p class="opacity-75">
|
||||
I possess a wide range of skills that enable me to develop visually appealing and
|
||||
interactive user interfaces for web applications.
|
||||
</p>
|
||||
</div>
|
||||
<div class="card variant-ringed-hollow p-4 md:p-8 space-y-4">
|
||||
<i class="fa-solid fa-palette text-4xl text-primary-500" />
|
||||
<h3 class="h3">Design</h3>
|
||||
<p class="opacity-75">
|
||||
Over the years, I have honed my ability to create visually appealing interfaces that
|
||||
are both user-friendly and intuitive.
|
||||
</p>
|
||||
</div>
|
||||
<div class="card variant-ringed-hollow p-4 md:p-8 space-y-4">
|
||||
<i class="fa-solid fa-users text-4xl text-primary-500" />
|
||||
<h3 class="h3">User Experience</h3>
|
||||
<p class="opacity-75">
|
||||
I understand the importance of creating a seamless UX for end-users. Which includes
|
||||
a solid understanding user behavior.
|
||||
</p>
|
||||
<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>
|
||||
|
|
|
@ -1,28 +1,28 @@
|
|||
<script lang="ts">
|
||||
import * as conf from '$lib/config';
|
||||
import GiteaLogo from '$lib/components/logos/GiteaLogo.svelte';
|
||||
import socials from '$lib/config/socialsObjects';
|
||||
import ObfuscatedEmail from '$lib/components/ObfuscatedEmail.svelte';
|
||||
|
||||
</script>
|
||||
<div class="flex space-x-4">
|
||||
<a
|
||||
class="btn btn-icon variant-soft-primary hover:variant-filled-primary"
|
||||
href={conf.socialLinks[2].href}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
title="Gitea - private github"
|
||||
><GiteaLogo clazz="w-6" />
|
||||
</a><a
|
||||
class="btn btn-icon variant-soft-primary hover:variant-filled-primary"
|
||||
href={conf.socialLinks[0].href}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
title="LinkedIn"
|
||||
><i class="fa-brands fa-linkedin" />
|
||||
</a><a
|
||||
class="btn btn-icon variant-soft-primary hover:variant-filled-primary"
|
||||
href="mailto:{conf.email}"
|
||||
target=""
|
||||
rel="noreferrer"
|
||||
title="Email"
|
||||
><i class="fa-solid fa-envelope" />
|
||||
</a>
|
||||
|
||||
<div class="flex space-x-4 justify-center md:justify-start">
|
||||
<a
|
||||
class="btn btn-circle btn-outline btn-secondary btn-lg hover:btn-secondary"
|
||||
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'} />
|
||||
</a><a
|
||||
class="btn btn-circle btn-outline btn-secondary btn-lg hover:btn-secondary"
|
||||
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'} />
|
||||
</a><ObfuscatedEmail
|
||||
mail={socials[0].href}
|
||||
clazz="btn btn-circle btn-outline btn-secondary btn-lg hover:btn-secondary"
|
||||
h={8}
|
||||
w={8}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
<script lang="ts">
|
||||
import { site } from '$lib/config/site'
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="h-card flex flex-col gap-4 sticky top-24 card card-body p-4 items-right xl:border-2 xl:py-8 border-base-content/10 xl:ml-auto xl:mr-8 xl:max-w-xs">
|
||||
<a href={site.protocol + site.domain} class="hidden u-url u-uid">{site.author.name}</a>
|
||||
<figure class="relative mx-auto group">
|
||||
{#if site.author.avatar}
|
||||
<img
|
||||
class="u-photo rounded-full shadow-xl hover:shadow-2xl transition-shadow z-10 w-24 h-24 md:w-32 md:h-32"
|
||||
src={site.author.avatar}
|
||||
alt={site.author.name} />
|
||||
{/if}
|
||||
{#if site.author.status}
|
||||
<div
|
||||
class="absolute z-20 rounded-full w-8 h-8 md:w-10 md:h-10 bottom-0 right-0 bg-base-100 shadow-xl text-lg md:text-xl text-center py-0.5 md:py-1.5">
|
||||
{site.author.status}
|
||||
</div>
|
||||
{/if}
|
||||
</figure>
|
||||
<div class="text-center flex flex-col gap-2">
|
||||
<h2 class="text-2xl font-bold mt-0 mb-2 p-name">{site.author.name}</h2>
|
||||
<p class="opacity-75 p-note">{@html site.author.bio}</p>
|
||||
{#if site.author.metadata}
|
||||
<div class="flex gap-1 flex-wrap justify-center">
|
||||
{#each site.author.metadata as { text, icon, link, rel }}
|
||||
{#if link}
|
||||
<a
|
||||
href={link}
|
||||
rel={rel ?? 'me noopener noreferrer external'}
|
||||
class:btn-square={!text}
|
||||
class="btn btn-sm btn-ghost normal-case gap-2 u-url"
|
||||
target="_blank">
|
||||
{#if icon}
|
||||
<span class="{icon} !w-5 !h-5">{icon}</span>
|
||||
{/if}
|
||||
{#if text}
|
||||
{text}
|
||||
{/if}
|
||||
</a>
|
||||
{:else}
|
||||
<button class:btn-square={!text} class="btn btn-sm btn-ghost normal-case gap-2">
|
||||
{#if icon}
|
||||
<span class="{icon} !w-5 !h-5">{icon}</span>
|
||||
{/if}
|
||||
{#if text}
|
||||
{text}
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
|
@ -1,16 +0,0 @@
|
|||
<script lang="ts">
|
||||
export let clazz = 'w-8 md:w-14 ';
|
||||
</script>
|
||||
|
||||
<svg class={clazz} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"
|
||||
><path
|
||||
d="M395.9 484.2l-126.9-61c-12.5-6-17.9-21.2-11.8-33.8l61-126.9c6-12.5 21.2-17.9 33.8-11.8 17.2 8.3 27.1 13 27.1 13l-.1-109.2 16.7-.1.1 117.1s57.4 24.2 83.1 40.1c3.7 2.3 10.2 6.8 12.9 14.4 2.1 6.1 2 13.1-1 19.3l-61 126.9c-6.2 12.7-21.4 18.1-33.9 12z"
|
||||
fill="#fff"
|
||||
/><g fill="#609926"
|
||||
><path
|
||||
d="M622.7 149.8c-4.1-4.1-9.6-4-9.6-4s-117.2 6.6-177.9 8c-13.3.3-26.5.6-39.6.7v117.2c-5.5-2.6-11.1-5.3-16.6-7.9 0-36.4-.1-109.2-.1-109.2-29 .4-89.2-2.2-89.2-2.2s-141.4-7.1-156.8-8.5c-9.8-.6-22.5-2.1-39 1.5-8.7 1.8-33.5 7.4-53.8 26.9C-4.9 212.4 6.6 276.2 8 285.8c1.7 11.7 6.9 44.2 31.7 72.5 45.8 56.1 144.4 54.8 144.4 54.8s12.1 28.9 30.6 55.5c25 33.1 50.7 58.9 75.7 62 63 0 188.9-.1 188.9-.1s12 .1 28.3-10.3c14-8.5 26.5-23.4 26.5-23.4S547 483 565 451.5c5.5-9.7 10.1-19.1 14.1-28 0 0 55.2-117.1 55.2-231.1-1.1-34.5-9.6-40.6-11.6-42.6zM125.6 353.9c-25.9-8.5-36.9-18.7-36.9-18.7S69.6 321.8 60 295.4c-16.5-44.2-1.4-71.2-1.4-71.2s8.4-22.5 38.5-30c13.8-3.7 31-3.1 31-3.1s7.1 59.4 15.7 94.2c7.2 29.2 24.8 77.7 24.8 77.7s-26.1-3.1-43-9.1zm300.3 107.6s-6.1 14.5-19.6 15.4c-5.8.4-10.3-1.2-10.3-1.2s-.3-.1-5.3-2.1l-112.9-55s-10.9-5.7-12.8-15.6c-2.2-8.1 2.7-18.1 2.7-18.1L322 273s4.8-9.7 12.2-13c.6-.3 2.3-1 4.5-1.5 8.1-2.1 18 2.8 18 2.8L467.4 315s12.6 5.7 15.3 16.2c1.9 7.4-.5 14-1.8 17.2-6.3 15.4-55 113.1-55 113.1z"
|
||||
/><path
|
||||
d="M326.8 380.1c-8.2.1-15.4 5.8-17.3 13.8-1.9 8 2 16.3 9.1 20 7.7 4 17.5 1.8 22.7-5.4 5.1-7.1 4.3-16.9-1.8-23.1l24-49.1c1.5.1 3.7.2 6.2-.5 4.1-.9 7.1-3.6 7.1-3.6 4.2 1.8 8.6 3.8 13.2 6.1 4.8 2.4 9.3 4.9 13.4 7.3.9.5 1.8 1.1 2.8 1.9 1.6 1.3 3.4 3.1 4.7 5.5 1.9 5.5-1.9 14.9-1.9 14.9-2.3 7.6-18.4 40.6-18.4 40.6-8.1-.2-15.3 5-17.7 12.5-2.6 8.1 1.1 17.3 8.9 21.3 7.8 4 17.4 1.7 22.5-5.3 5-6.8 4.6-16.3-1.1-22.6 1.9-3.7 3.7-7.4 5.6-11.3 5-10.4 13.5-30.4 13.5-30.4.9-1.7 5.7-10.3 2.7-21.3-2.5-11.4-12.6-16.7-12.6-16.7-12.2-7.9-29.2-15.2-29.2-15.2s0-4.1-1.1-7.1c-1.1-3.1-2.8-5.1-3.9-6.3 4.7-9.7 9.4-19.3 14.1-29-4.1-2-8.1-4-12.2-6.1-4.8 9.8-9.7 19.7-14.5 29.5-6.7-.1-12.9 3.5-16.1 9.4-3.4 6.3-2.7 14.1 1.9 19.8l-24.6 50.4z"
|
||||
/></g
|
||||
></svg
|
||||
>
|
|
@ -1,17 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { modeCurrent } from '@skeletonlabs/skeleton';
|
||||
let clazz = 'w-6 md:w-12';
|
||||
let fillStyle = 'fill: #000000';
|
||||
$: {
|
||||
fillStyle = $modeCurrent ? 'fill: #000000' : 'fill: #ffffff';
|
||||
}
|
||||
</script>
|
||||
|
||||
<svg class={clazz} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
||||
<path style={fillStyle} d="M 30,2.0000001 V 30 h -1 -2 v 2 h 5 V -3.3333334e-8 L 27,0 v 2 z" />
|
||||
<path
|
||||
style={fillStyle}
|
||||
d="M 9.9515939,10.594002 V 12.138 h 0.043994 c 0.3845141,-0.563728 0.8932271,-1.031728 1.4869981,-1.368 0.580003,-0.322998 1.244999,-0.485 1.993002,-0.485 0.72,0 1.376999,0.139993 1.971998,0.42 0.595,0.279004 1.047001,0.771001 1.355002,1.477001 0.338003,-0.500001 0.795999,-0.941 1.376999,-1.323001 0.579999,-0.382998 1.265998,-0.574 2.059998,-0.574 0.602003,0 1.160002,0.074 1.674002,0.220006 0.514,0.148006 0.953998,0.382998 1.321999,0.706998 0.36601,0.322999 0.653001,0.746 0.859,1.268002 0.205001,0.521998 0.307994,1.15 0.307994,1.887001 v 7.632997 h -3.127 v -6.463997 c 0,-0.383002 -0.01512,-0.743002 -0.04399,-1.082003 -0.02079,-0.3072 -0.103219,-0.607113 -0.242003,-0.881998 -0.133153,-0.25081 -0.335962,-0.457777 -0.584001,-0.596002 -0.257008,-0.146003 -0.605998,-0.220006 -1.046997,-0.220006 -0.440002,0 -0.796003,0.085 -1.068,0.253002 -0.272013,0.170003 -0.485001,0.390002 -0.639001,0.662003 -0.159119,0.287282 -0.263585,0.601602 -0.307994,0.926997 -0.05197,0.346923 -0.07801,0.697217 -0.07801,1.048002 v 6.353999 h -3.128005 v -6.398 c 0,-0.338003 -0.0072,-0.673001 -0.02116,-1.004001 -0.01134,-0.313663 -0.07487,-0.623229 -0.187994,-0.915999 -0.107943,-0.276623 -0.300435,-0.512126 -0.550001,-0.673001 -0.25799,-0.168 -0.636,-0.253002 -1.134999,-0.253002 -0.198123,0.0083 -0.394383,0.04195 -0.584002,0.100006 -0.258368,0.07446 -0.498455,0.201827 -0.704999,0.373985 -0.227981,0.183987 -0.421999,0.449 -0.583997,0.794003 -0.161008,0.345978 -0.242003,0.797998 -0.242003,1.356998 v 6.618999 H 6.99942 V 10.590001 Z"
|
||||
/>
|
||||
<path style={fillStyle} d="M 2,2.0000001 V 30 h 3 v 2 H 0 V 9.2650922e-8 L 5,0 v 2 z" />
|
||||
</svg>
|
|
@ -3,8 +3,6 @@
|
|||
const drawerStore = getDrawerStore();
|
||||
import { NavRoutes } from '$lib/config';
|
||||
import { page } from '$app/stores';
|
||||
import { browser } from '$app/environment';
|
||||
// what is my url?
|
||||
$: classesDrawer = $drawerStore.id === 'mobile-nav' ? 'md:hidden' : '';
|
||||
</script>
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="ts">
|
||||
import GiteaLogo from '$lib/components/logos/GiteaLogo.svelte';
|
||||
import SocialsCloud from '$lib/components/SocialsCloud.svelte';
|
||||
import GiteaLogo from '$lib/assets/logos/GiteaLogo.svelte';
|
||||
import SocialsCloud from '$lib/components/main/SocialsCloud.svelte';
|
||||
|
||||
const year = new Date().getFullYear();
|
||||
</script>
|
||||
|
@ -14,11 +14,19 @@
|
|||
<!-- <div class="container px-5 py-8 mx-auto flex items-center sm:flex-row flex-col"> -->
|
||||
<a class="items-center md:justify-start justify-center" href="/">
|
||||
<p class="sm:pl-4 text-base sm:py-2 sm:mt-0 mt-4 text-center">
|
||||
All content, unless otherwise stated,
|
||||
<br>by Matthieu Morin, is under © copyright {year},
|
||||
<br>and all of it licensed under <a class="anchor font-bold" href="https://creativecommons.org/licenses/by-sa/4.0/deed.en">CC BY-SA 4.0</a>.
|
||||
<br>This site coded by me is <a class="font-bold anchor" href="https://git.mattmor.in/Madmin/its-personal/src/branch/master/LICENSE">MIT Licensed</a>.
|
||||
|
||||
All content on this website, unless otherwise stated,
|
||||
<br />by Matthieu Morin, is under copyright © {year},
|
||||
<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
|
||||
>.
|
||||
<br />This site, coded by me, is
|
||||
<a
|
||||
class="font-bold anchor"
|
||||
href="https://git.mattmor.in/Madmin/its-personal/src/branch/master/LICENSE"
|
||||
>MIT Licensed</a
|
||||
>.
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
<script lang="ts">
|
||||
import { LightSwitch, AppBar, Avatar, getDrawerStore, Drawer } from '@skeletonlabs/skeleton';
|
||||
import { LightSwitch, Avatar, getDrawerStore } from '@skeletonlabs/skeleton';
|
||||
import type { DrawerSettings } from '@skeletonlabs/skeleton';
|
||||
const drawerStore = getDrawerStore();
|
||||
|
||||
// Components
|
||||
import { getImageLink } from '$lib/images';
|
||||
import { page } from '$app/stores';
|
||||
import { NavRoutes } from '$lib/config';
|
||||
|
||||
|
@ -23,9 +22,6 @@
|
|||
};
|
||||
drawerStore.open(drawerSettings);
|
||||
}
|
||||
|
||||
// Local
|
||||
const imgPlaceholder = getImageLink({ id: 'linky', w: 128, h: 128 });
|
||||
</script>
|
||||
|
||||
<section
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
<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>
|
|
@ -0,0 +1,81 @@
|
|||
<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> -->
|
|
@ -0,0 +1,39 @@
|
|||
<script lang="ts">
|
||||
import { dev } from '$app/environment'
|
||||
import { head } from '$lib/config/general'
|
||||
import { site } from '$lib/config/site'
|
||||
import OpenGraph from '$lib/components/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} />
|
|
@ -0,0 +1,15 @@
|
|||
<script lang="ts">
|
||||
import { favicon, any } from '$lib/config/icon'
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
{#if favicon}
|
||||
<link rel="shortcut icon" href={favicon.src} sizes={favicon.sizes} type={favicon.type} />
|
||||
{/if}
|
||||
{#if any['180']}
|
||||
<link rel="apple-touch-icon" href={any['180'].src} sizes={any['180'].sizes} type={any['180'].type} />
|
||||
{/if}
|
||||
{#if any['192']}
|
||||
<link rel="icon" href={any['192'].src} sizes={any['192'].sizes} type={any['192'].type} />
|
||||
{/if}
|
||||
</svelte:head>
|
|
@ -0,0 +1,45 @@
|
|||
<script lang="ts">
|
||||
import { site } from '$lib/config/site'
|
||||
import { any, maskable } from '$lib/config/icon'
|
||||
export let post: Urara.Post | undefined = undefined
|
||||
export let page: Urara.Page | undefined = undefined
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<meta property="og:site_name" content={site.title} />
|
||||
<meta property="og:locale" content={site.lang} />
|
||||
{#if post}
|
||||
<meta property="og:type" content="article" />
|
||||
<meta property="og:title" content={post.title ?? post.summary ?? post.path.slice(1)} />
|
||||
{#if post.summary}
|
||||
<meta property="og:description" content={post.summary} />
|
||||
{/if}
|
||||
{#if post.image}
|
||||
<meta property="og:image" content={(post.image.startsWith('http') ? '' : site.protocol + site.domain) + post.image} />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
{:else}
|
||||
<meta property="og:image" content={maskable['512'].src ?? any['512'].src ?? any['192'].src} />
|
||||
<meta name="twitter:card" content="summary" />
|
||||
{/if}
|
||||
{#if post.tags}
|
||||
{#each post.tags as tag}
|
||||
<meta property="article:tag" content={tag} />
|
||||
{/each}
|
||||
{/if}
|
||||
<meta property="og:url" content={site.protocol + site.domain + post.path} />
|
||||
<meta property="article:author" content={site.author.name} />
|
||||
<meta property="article:published_time" content={post.published ?? post.created} />
|
||||
<meta property="article:modified_time" content={post.updated ?? post.published ?? post.created} />
|
||||
{:else}
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:image" content={maskable['512'].src ?? any['512'].src ?? any['192'].src} />
|
||||
<meta property="og:description" content={site.description} />
|
||||
{#if page}
|
||||
<meta property="og:title" content={page.title ?? page.path.slice(1)} />
|
||||
<meta property="og:url" content={site.protocol + site.domain + page.path} />
|
||||
{:else}
|
||||
<meta property="og:title" content={site.title} />
|
||||
<meta property="og:url" content={site.protocol + site.domain} />
|
||||
{/if}
|
||||
{/if}
|
||||
</svelte:head>
|
|
@ -0,0 +1,21 @@
|
|||
<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 />
|
|
@ -0,0 +1,138 @@
|
|||
<script lang="ts">
|
||||
import { browser, dev } from '$app/environment'
|
||||
import { fly } from 'svelte/transition'
|
||||
import { site } from '$lib/config/site'
|
||||
import { theme } from '$lib/config/general'
|
||||
import { title as storedTitle } from '$lib/stores/title'
|
||||
import { header as headerConfig } from '$lib/config/general'
|
||||
import { hslToHex } from '$lib/utils/color'
|
||||
import Nav from '$lib/components/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>
|
|
@ -0,0 +1,75 @@
|
|||
<script lang="ts">
|
||||
export let nav: { text: string; link?: string; children?: { text: string; link: string }[] }[]
|
||||
export let path: string
|
||||
export let title: string
|
||||
export let scrollY: number
|
||||
export let pin: boolean
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||
<!-- reference: https://github.com/saadeghi/daisyui/issues/1285 -->
|
||||
<div class="dropdown lg:hidden">
|
||||
<label for="navbar-dropdown" tabindex="0" class="btn btn-square btn-ghost">
|
||||
<span class="i-heroicons-outline-menu-alt-1" />
|
||||
</label>
|
||||
<ul
|
||||
id="navbar-dropdown"
|
||||
tabindex="0"
|
||||
class:hidden={!pin}
|
||||
class="menu menu-compact dropdown-content bg-base-100 text-base-content shadow-lg rounded-box min-w-max max-w-52 p-2
|
||||
">
|
||||
{#each nav as { text, link, children }}
|
||||
{#if link && !children}
|
||||
<li>
|
||||
<a class:font-bold={link === path} href={link}>{text}</a>
|
||||
</li>
|
||||
{:else if children}
|
||||
<li tabindex="0">
|
||||
<span class:font-bold={children.some(({ link }) => link === path)} class="justify-between gap-1 max-w-[13rem]">
|
||||
{text}
|
||||
<span class="i-heroicons-solid-chevron-right mr-2" />
|
||||
</span>
|
||||
<ul class="bg-base-100 text-base-content shadow-lg p-2">
|
||||
{#each children as { text, link }}
|
||||
<li>
|
||||
<a class:font-bold={link === path} href={link}>{text}</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</li>
|
||||
{/if}
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
<div class:swap-active={scrollY > 32 && title} class="swap order-last hidden lg:inline-grid">
|
||||
<button
|
||||
on:click={() => window.scrollTo(0, 0)}
|
||||
class:hidden={scrollY < 32 || !title}
|
||||
class="swap-on btn btn-ghost text-base font-normal normal-case transition-all duration-200">
|
||||
{title}
|
||||
</button>
|
||||
<ul class:hidden={scrollY > 64 && title} class="swap-off menu menu-horizontal p-0">
|
||||
{#each nav as { text, link, children }}
|
||||
{#if link && !children}
|
||||
<li>
|
||||
<a class="!rounded-btn" class:font-bold={link === path} href={link}>{text}</a>
|
||||
</li>
|
||||
{:else if children}
|
||||
<li>
|
||||
<span class:font-bold={children.some(({ link }) => link === path)} class="!rounded-btn gap-1">
|
||||
{text}
|
||||
<span class="i-heroicons-solid-chevron-down -mr-1" />
|
||||
</span>
|
||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||
<ul tabindex="0" class="menu rounded-box bg-base-100 text-base-content shadow-lg p-2">
|
||||
{#each children as { text, link }}
|
||||
<li>
|
||||
<a class:font-bold={link === path} href={link}>{text}</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</li>
|
||||
{/if}
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
|
@ -0,0 +1,21 @@
|
|||
<script lang="ts">
|
||||
import { site } from '$lib/config/site'
|
||||
import { header as headerConfig } from '$lib/config/general'
|
||||
</script>
|
||||
|
||||
<form
|
||||
action={headerConfig?.search?.provider === 'duckduckgo' ? '//duckduckgo.com/' : '//google.com/search'}
|
||||
method="get"
|
||||
class="flex-1">
|
||||
<input
|
||||
type="text"
|
||||
name="q"
|
||||
class="input input-ghost input-bordered xl:bg-base-100 xl:text-base-content transition-all w-full h-12" />
|
||||
<input
|
||||
type="hidden"
|
||||
name={headerConfig?.search?.provider === 'duckduckgo' ? 'sites' : 'sitesearch'}
|
||||
value={site.protocol + site.domain} />
|
||||
<button type="submit" class="btn btn-square btn-ghost ml-2">
|
||||
<span class="i-heroicons-outline-search" />
|
||||
</button>
|
||||
</form>
|
|
@ -1,10 +0,0 @@
|
|||
<script lang="ts">
|
||||
import ProjectContentLayout from '$lib/components/blog/PostLayout.svelte';
|
||||
import type { Post } from '$lib/types/post';
|
||||
|
||||
export let post: Post;
|
||||
</script>
|
||||
|
||||
<ProjectContentLayout {...post} imagesDirectoryName="projects">
|
||||
<slot />
|
||||
</ProjectLayout>
|
|
@ -0,0 +1,37 @@
|
|||
<script lang="ts">
|
||||
/* @see {@link https://github.com/sveltejs/kit/issues/241#issuecomment-1363621896} */
|
||||
|
||||
type Image = {
|
||||
src: string
|
||||
w: number
|
||||
h: number
|
||||
}
|
||||
|
||||
const sources = import.meta.glob<Image[]>(['/src/static/**/*.{jpg,jpeg,png,webp,avif}', '!/src/static/assets'], {
|
||||
query: {
|
||||
format: 'avif',
|
||||
quality: '80',
|
||||
width: '736',
|
||||
source: ''
|
||||
},
|
||||
import: 'default',
|
||||
eager: true
|
||||
})
|
||||
|
||||
let className: string | undefined = undefined
|
||||
export { className as class }
|
||||
export let src: string
|
||||
export let alt: string = src
|
||||
export let loading: 'eager' | 'lazy' = 'lazy'
|
||||
export let decoding: 'async' | 'sync' | 'auto' = 'async'
|
||||
let source: Image[] | undefined = sources[`/src/static${src}`]
|
||||
</script>
|
||||
|
||||
{#if source}
|
||||
<picture>
|
||||
<source srcset={source.map(({ src, w }) => `${src} ${w}w`).join(', ')} type="image/avif" />
|
||||
<img {src} {alt} class={className ?? 'rounded-lg my-2'} {loading} {decoding} />
|
||||
</picture>
|
||||
{:else}
|
||||
<img {src} {alt} class={className ?? 'rounded-lg my-2'} {loading} {decoding} />
|
||||
{/if}
|
|
@ -0,0 +1,5 @@
|
|||
<div class="overflow-x-auto mb-4">
|
||||
<table class="table w-full">
|
||||
<slot />
|
||||
</table>
|
||||
</div>
|
|
@ -0,0 +1,63 @@
|
|||
<script lang="ts">
|
||||
|
||||
import type { Skill } from '$lib/config/skills';
|
||||
import list from '$lib/config/skills';
|
||||
|
||||
function sortSkills(skills: Skill[]): Skill[] {
|
||||
return skills.sort((a, b) => {
|
||||
if (a.level < b.level) return -1;
|
||||
if (a.level > b.level) return 1;
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
</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>
|
||||
<!-- 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">
|
||||
<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>
|
||||
<!-- <ProgressBar value={subCategory.level} max={100} /> -->
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap justify-center space-x-2 m-2">
|
||||
{#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'
|
||||
: skill.level === 'B'
|
||||
? 'badge-accent'
|
||||
: 'badge-primary badge-ghost'}"
|
||||
>
|
||||
{skill.title}
|
||||
</span>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<style>
|
||||
.emoji {
|
||||
position: relative;
|
||||
top: +0.2em;
|
||||
line-height: 2;
|
||||
}
|
||||
</style>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue