Merging some some logic from other projects for sveltekit #2

Merged
Madmin merged 4 commits from dfhsfd into master 2024-04-04 00:16:59 +00:00
92 changed files with 4365 additions and 1264 deletions

View File

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

View File

@ -6,7 +6,7 @@ node_modules
.env .env
.env.* .env.*
!.env.example !.env.example
*.local
# Ignore files for PNPM, NPM and YARN # Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml pnpm-lock.yaml
package-lock.json package-lock.json

27
.github/workflows/playwright.yml vendored Normal file
View File

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

33
.gitignore vendored
View File

@ -1,19 +1,41 @@
+ .turbo + .turbo
+ build/** + build/**
+ dist/** + dist/**
dist
dist-ssr
.DS_Store .DS_Store
node_modules node_modules
/build /build
/.svelte-kit /.svelte-kit
/package /package
/lambda/
# Sentry Config File
.sentryclirc
.env .env
.env.* .env.*
!.env.example !.env.example
*.local
vite.config.js.timestamp-* vite.config.js.timestamp-*
vite.config.ts.timestamp-* vite.config.ts.timestamp-*
.vercel .vercel
.output .output
.netlify
cypress/screenshots
cypress/videos
.idea
.stars-cache
# pm
.yarn .yarn
yarn-error.log
yarn.lock
pnpm-lock.yaml
# Logs # Logs
logs logs
@ -24,14 +46,10 @@ yarn-error.log*
pnpm-debug.log* pnpm-debug.log*
lerna-debug.log* lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files # Editor directories and files
!.vscode/extensions.json !.vscode/extensions.json
.vscode/* .vscode/*
.editorconfig
.idea .idea
*.suo *.suo
*.ntvs* *.ntvs*
@ -61,3 +79,8 @@ public/api/vendor
/playwright-report/ /playwright-report/
/blob-report/ /blob-report/
/playwright/.cache/ /playwright/.cache/
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/

View File

@ -6,6 +6,7 @@ node_modules
.env .env
.env.* .env.*
!.env.example !.env.example
*.local
# Ignore files for PNPM, NPM and YARN # Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml pnpm-lock.yaml

29
Dockerfile Normal file
View File

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

60
csp-directives.js Normal file
View File

@ -0,0 +1,60 @@
import { SENTRY_KEY } from '$env/static/private';
const rootDomain = process.env.VITE_DOMAIN; // or your server IP for dev
/** @type {import('@sveltejs/kit').CspDirectives} */
const cspDirectives = {
'base-uri': ['self'],
'child-src': ['self'],
'connect-src': ['self', 'ws://localhost:*'],
// 'connect-src': ['self', 'ws://localhost:*', 'https://hcaptcha.com', 'https://*.hcaptcha.com'],
'img-src': ['self', 'data:'],
'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',
],
'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 ? [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',
// 'https://*.stripe.com',
// 'https://*.facebook.com',
// 'https://*.facebook.net',
// 'https://hcaptcha.com',
// 'https://*.hcaptcha.com',
'https://*.sentry.io'
// 'https://polyfill.io',
],
'worker-src': ['self'],
// remove report-to & report-uri if you do not want to use Sentry reporting
'report-to': ["'csp-endpoint'"],
'report-uri': [
'https://o4505828687478784.ingest.sentry.io/api/4506781187899392/security/?sentry_key=cc0a2e656e0cbbcade519f24627044df'
]
};
export default cspDirectives;

View File

@ -2,11 +2,7 @@ import { defineMDSveXConfig as defineConfig } from 'mdsvex';
import remarkExternalLinks from 'remark-external-links'; import remarkExternalLinks from 'remark-external-links';
import remarkSetImagePath from './src/lib/utils/remark-set-image-path.js'; 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 remarkLinkWithImageAsOnlyChild from './src/lib/utils/remark-link-with-image-as-only-child.js';
import { toString } from 'mdast-util-to-string';
import rehypeWrap from 'rehype-wrap-all';
import rehypeImgSize from 'rehype-img-size'; import rehypeImgSize from 'rehype-img-size';
import { h } from 'hastscript';
import { visit } from 'unist-util-visit';
import remarkUnwrapImages from 'remark-unwrap-images'; import remarkUnwrapImages from 'remark-unwrap-images';
import remarkToc from 'remark-toc'; import remarkToc from 'remark-toc';
@ -28,10 +24,8 @@ const config = defineConfig({
// }, // },
/* Plugins */ /* Plugins */
rehypePlugins: [ rehypePlugins: [
[rehypeSlug] [rehypeSlug],
[rehypeImgSize]
// [rehypeWrap, { selector: 'table', wrapper: 'div.overflow-auto' }],
// [rehypeImgSize, { dir: './static' }],
// [ // [
// /** Custom rehype plugin to add loading="lazy" to all images */ // /** Custom rehype plugin to add loading="lazy" to all images */
// () => { // () => {
@ -53,7 +47,9 @@ const config = defineConfig({
target: '_blank' target: '_blank'
}) })
], ],
[remarkUnwrapImages] [remarkUnwrapImages],
remarkSetImagePath,
remarkLinkWithImageAsOnlyChild
// [ // [
// headings, // headings,
// { // {
@ -68,8 +64,7 @@ const config = defineConfig({
// } // }
// } // }
// ], // ],
// remarkSetImagePath,
// remarkLinkWithImageAsOnlyChild,
// remarkHeadingsPermaLinks, // remarkHeadingsPermaLinks,
// getHeadings // getHeadings
] ]

View File

@ -1,50 +0,0 @@
import { MdsvexOptions, defineMDSveXConfig as defineConfig } from 'mdsvex';
import headings from 'rehype-autolink-headings';
import remarkExternalLinks from 'remark-external-links';
import slug from 'rehype-slug';
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 { toString } from 'mdast-util-to-string';
import rehypeWrap from 'rehype-wrap-all';
import rehypeImgSize from 'rehype-img-size';
import { h } from 'hastscript';
import { visit } from 'unist-util-visit';
import remarkToc from 'remark-toc';
// import { highlightCode } from './src/lib/utils/highlighter.js';
const config: MdsvexOptions = defineConfig({
extensions: ['.svelte.md', '.md', '.svx'],
smartypants: {
dashes: 'oldschool'
}
// Wait for skeleton to implement Prismjs, for now use <CodeBlock /> in .md files
// highlight: {},
// layout: {
// blog: './src/lib/components/blog/_blog-layout.svelte',
// project: './src/lib/components/projects/_project-layout.svelte',
// _: './src/lib/components/fallback/_layout.svelte'
// },
// rehypePlugins: [
// [rehypeWrap, { selector: 'table', wrapper: 'div.overflow-auto' }],
// [rehypeImgSize, { dir: './static' }],
// [slug],
// [
// headings,
// {
// behavior: 'prepend',
// headingProperties: {},
// content: '<i class="fa-regular fa-link"></i>'
// }
// ]
// ],
// remarkPlugins: [
// [remarkToc, { maxDepth: 3, tight: true }],
// [remarkExternalLinks, { target: '_blank', rel: 'noreferrer' }],
// remarkSetImagePath,
// remarkLinkWithImageAsOnlyChild,
// remarkHeadingsPermaLinks,
// getHeadings
// ]
});
export default config;

View File

@ -24,11 +24,10 @@
"test-ct": "playwright test -c playwright-ct.config.ts" "test-ct": "playwright test -c playwright-ct.config.ts"
}, },
"devDependencies": { "devDependencies": {
"@playwright/experimental-ct-svelte": "^1.41.2", "@playwright/test": "^1.41.2",
"@skeletonlabs/skeleton": "2.0.0", "@skeletonlabs/skeleton": "2.0.0",
"@skeletonlabs/tw-plugin": "0.1.0", "@skeletonlabs/tw-plugin": "0.1.0",
"@sveltejs/adapter-cloudflare": "^2.3.4", "@sveltejs/kit": "^2.5.0",
"@sveltejs/kit": "^1.30.3",
"@tailwindcss/forms": "0.5.6", "@tailwindcss/forms": "0.5.6",
"@tailwindcss/typography": "0.5.9", "@tailwindcss/typography": "0.5.9",
"@types/js-cookie": "^3.0.6", "@types/js-cookie": "^3.0.6",
@ -41,43 +40,42 @@
"eslint": "^8.56.0", "eslint": "^8.56.0",
"eslint-config-prettier": "^8.10.0", "eslint-config-prettier": "^8.10.0",
"eslint-plugin-svelte": "^2.35.1", "eslint-plugin-svelte": "^2.35.1",
"ficons": "^1.1.54",
"hastscript": "^8.0.0",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"mdast-util-to-string": "^4.0.0",
"mdsvex": "^0.11.0",
"postcss": "8.4.29", "postcss": "8.4.29",
"prettier": "^2.8.8", "prettier": "^2.8.8",
"prettier-plugin-svelte": "^2.10.1", "prettier-plugin-svelte": "^2.10.1",
"rehype-autolink-headings": "^7.1.0",
"rehype-img-size": "^1.0.1", "rehype-img-size": "^1.0.1",
"rehype-wrap-all": "^1.1.0", "rehype-slug": "^6.0.0",
"remark-external-links": "^9.0.1", "remark-external-links": "^9.0.1",
"sass": "^1.70.0", "remark-toc": "^9.0.0",
"svelte": "^4.2.10", "remark-unwrap-images": "^4.0.0",
"sass": "^1.71.0",
"shiki": "^1.1.6",
"svelte": "^4.2.11",
"svelte-check": "^3.6.4", "svelte-check": "^3.6.4",
"tailwindcss": "3.3.3", "tailwindcss": "3.3.3",
"tslib": "^2.6.2", "tslib": "^2.6.2",
"typescript": "^5.3.3", "typescript": "^5.3.3",
"unist-util-visit": "^5.0.0", "unist-util-visit": "^5.0.0",
"vite": "^4.5.2", "vite": "^5.1.3",
"vite-plugin-tailwind-purgecss": "0.1.3", "vite-plugin-tailwind-purgecss": "0.2.0",
"vitest": "^0.34.6" "vitest": "^0.34.6"
}, },
"dependencies": { "dependencies": {
"@floating-ui/dom": "1.5.1", "@floating-ui/dom": "1.5.1",
"@fortawesome/fontawesome-free": "^6.5.1", "@fortawesome/fontawesome-free": "^6.5.1",
"@sentry/sveltekit": "^7.102.0",
"@sveltejs/adapter-node": "^4.0.1", "@sveltejs/adapter-node": "^4.0.1",
"@sveltejs/vite-plugin-svelte": "^3.0.2",
"@threlte/core": "^6.1.1", "@threlte/core": "^6.1.1",
"@threlte/extras": "^8.7.4", "@threlte/extras": "^8.7.5",
"@yushijinhun/three-minifier-rollup": "^0.4.0", "@yushijinhun/three-minifier-rollup": "^0.4.0",
"highlight.js": "11.8.0", "highlight.js": "11.8.0",
"latest": "^0.2.0", "latest": "^0.2.0",
"linkedom": "^0.15.6", "linkedom": "^0.15.6",
"mdsvex": "^0.11.0",
"prismjs": "^1.29.0", "prismjs": "^1.29.0",
"rehype-autolink-headings": "^7.1.0",
"rehype-slug": "^6.0.0",
"remark-toc": "^9.0.0",
"remark-unwrap-images": "^4.0.0",
"rss": "^1.2.2", "rss": "^1.2.2",
"svelte-preprocess": "^5.1.3" "svelte-preprocess": "^5.1.3"
}, },

View File

@ -1,46 +0,0 @@
import { defineConfig, devices } from '@playwright/experimental-ct-svelte';
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './',
/* The base directory, relative to the config file, for snapshot files created with toMatchSnapshot and toHaveScreenshot. */
snapshotDir: './__snapshots__',
/* Maximum time one test can run for. */
timeout: 10 * 1000,
/* 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: {
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
/* Port to use for Playwright component endpoint. */
ctPort: 3100,
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
],
});

77
playwright.config.ts Normal file
View File

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

View File

@ -1,12 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Testing Page</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="./index.ts"></script>
</body>
</html>

View File

@ -1,2 +0,0 @@
// Import styles, initialize component theme here.
// import '../src/common.css';

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,11 @@
import type { BlogPost } from '$lib/types/blog'; import type { Post } from '$lib/types/post';
import type { MarkdownMetadata } from '$content/types'; import type { MarkdownMetadata } from '$content/types';
import type { MdsvexImport } from './types'; import type { MdsvexImport } from './types';
import { parseReadContent } from '$content/utils'; import { parseReadContent } from '$content/utils';
import { error } from '@sveltejs/kit'; import { error } from '@sveltejs/kit';
export function listBlogPosts() { export function listPosts() {
const posts = import.meta.glob<BlogPost>('./blog/*.md', { const posts = import.meta.glob<Post>('./blog/*.md', {
eager: true, eager: true,
import: 'metadata' import: 'metadata'
}); });
@ -13,14 +13,14 @@ export function listBlogPosts() {
return parseReadContent(posts); return parseReadContent(posts);
} }
export async function getBlogPostMetadata(slug: string) { export async function getPostMetadata(slug: string) {
const { post } = await getBlogPost(slug); const { post } = await getPost(slug);
return post; return post;
} }
export async function getBlogPost(slug: string) { export async function getPost(slug: string) {
try { try {
const data: MdsvexImport<BlogPost & MarkdownMetadata> = await import(`./blog/${slug}.md`); const data: MdsvexImport<Post & MarkdownMetadata> = await import(`./blog/${slug}.md`);
return { return {
post: { ...data.metadata, slug }, post: { ...data.metadata, slug },

View File

@ -0,0 +1,12 @@
tldr:
I started by containerizing my SvelteKit and Strapi apps, then Pushed these to Docker Hub and AWS ECR,
leveraging 1 free private image on docker hub and free-tier 500mb limit on AWS ECR, thereby minimizing costs and exploring each option.
1. Conteinerization
a. Sveltekit
Keeping in mind that dev, build, test and lint, etc. scripts are handled by turborepo, we use these when containerizing.
Step 1.
b. Strapi

View File

@ -0,0 +1,7 @@
choosing a package manager
I tried to abstain from the community battles between pms and looked at the performance of the top 3 used - npm, yarn and pnpm. https://pnpm.io/benchmarks
I've got experience with each and would like to use pnpm as it is the much faster than NPM,
however Strapi doesn't officialy support it so I ended up choosing yarn as in most cases it is a bit faster than npm, has some better handling with monorepos and I personally like it.
I used Turborepo with npm and there were some problems with setting dependencies in workspaces vs the root of monorepo and private packages, which required workarounds.
In terms of CI performance can have significant consequences.

View File

@ -0,0 +1,15 @@
AWS set up
A thing I learned using linux is user management, specifically don't do everything as root :D.
When using AWS first I didn't care about IAM roles, but I think I learned the same lesson...
Anyways I used this process, when configuring accounts:
When starting login as root user and access IAM dashboard,
Set up Multi-factor Auth as root user (prefferably with Yubikey or some OTP in a secure device)
Later on, like in linux, the root account access can be minimized for a better decentralized privilege system, even when being the only admin.
Go to services then IAM Identity center
Select identity source -> In my case stick with the default Identity Center directory, bigger organizations or ones using other sources can connect to external ones.
In Multi-account permissions select permission sets and create a permission set.
Then go to AWS accounts, select the account you wish and assign the permission set to them.

View File

@ -0,0 +1,62 @@
Namecheap
Saw mattmor.in and just bought it, no questions asked.
I immediately created a cloudflare account, because of their great certificate management, security checks, analytics and mostly DNS.
Setting up AWS EC2 & RDS instances
EC2 setup
I've got a great opportunity to set up a "free-tier" instance, of course the limitations are quite interesting - 750 hrs/m and some compute limiting factors.
Let's use this opportunity, it is a better deal than GCP with 300$/3mos and my experience with GCP while dealing with Auth API was pretty bad.
Made SSH keys
got an Elastic IP and connected it with my EC2 instance to be accessible publicly on a stable IP.
added DNS records to cloudflare
Log in via SSH, update, upgrade everything, restart.
Then I configured fail2ban with some custom responses and enabled it, I also configured the seemingly redundant ufw and allowed ssh, https
And with that I am ready to roll onto installing necessary software like nginx, jenkins, docker for now.
RDS setup
I've already configured Strapi for my backend, I selected PostgreSQL, because while theoretically the default SQLite db would be sufficient and very easy to manage.
I know it's scalability is limited and is not suitable for multiple user access, which I might need in other projects with the same tech stack.
The main reason for choosing PostgreSQL is I can showcase it and learn with it on a project using AWS.
I've worked mainly with MariaDB before, and used PostgreSQL only on some ancient Wordpress projects back in the day.
As I already set up a free-tier EC2 instance, I can also setup AWS RDS with Aurora(PostgreSQL compatible) or PostgreSQL also on free-tier, which is awesome!
Let's choose PostgreSQL, because that's simpler and more than sufficient.
The Free Tier is limited to the same time frame for RDS...
If I really wanted to make this completely free I could use AWS Lambda, which is also free up to 1 million requests/month :D, to turn off both the EC2 and RDS instances at night. Gotta think when do recruiters go to sleep...
Now seriously, to actualy set this up:
Select:
burstable class db.t4g.micro
General Purpose SSD (gp3) 20GiB
I disabled autoscaling as it's not possible I would use more than this in this use-case.
Strapi recommends PostgreSQL v14.0 at the time of writing
AWS offers 14.5 above, let's choose 14.9 R1, should be backwards compatible.
Template -> Free tier
Security:
As my OpSec dictates I choose passwords with high entropy so you can't hack me.
Backup & Maintenance:
Setup Backup and Amazon auto-maintenance windows, I am not a maniac to not backup.
Connectivity:
I will not connect this to my EC2 instance, because I will be testing this on localhost.
I need public access and in this case I do not think accessing by a private network is necessary.
I will make a new VPC security group, then we can access it via an internet gateway.
https://docs.aws.amazon.com/images/AmazonRDS/latest/UserGuide/images/GS-VPC-network.png

View File

@ -8,7 +8,6 @@ tags:
published: true published: true
image: Feature.jpg image: Feature.jpg
--- ---
## Svelte ## Svelte
Media inside the **Svelte** folder is served from the `static` folder. Media inside the **Svelte** folder is served from the `static` folder.

View File

@ -0,0 +1,18 @@
install Grafana, Prometheus, ELK Stack, and Jenkins on a single server and use them to monitor other EC2 instances or cloud resources. There are trade-offs:
Pros:
Simplified Management: All tools in one place.
Lower Costs: Fewer servers to maintain.
Cons:
Resource Contention: These tools can be resource-intensive.
Single Point of Failure: If the server goes down, all tools are affected.
Security Risks: Multiple services on one server can increase the attack surface.
Recommendations:
Use containerization (Docker) for easier management and isolation.
Set up a robust backup and recovery strategy.
Ensure adequate resource allocation and scaling capabilities.

View File

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

View File

@ -0,0 +1,76 @@
The goal of this project is to showcase my skillset both as a portfolio site and showcase how I used devops and web development practices to build it.
The tech stack and feature set is meant to:
legitimize the goal of the portfolio site, which is mentioned above
Use modern DevOps and web development practices that I know of so I can build it as fast as possible.
minimize costs and be potentially scalable and feature-upgradeable.
be secure and be build using my evolving OpSec and limited cybersecurity knowledge.
show proficiency in using new tools.
As such the current cloud stack is:
1. AWS EC2 t3.micro
For: Jenkins
Why: CI/CD pipeline, free tier, 1GB RAM sufficient for Jenkins.
2. AWS EC2 t3.micro
For: docker-compose SvelteKit app
Why: Frontend, free tier, 1GB RAM sufficient for SvelteKit.
3. Amazon ECS Anywhere
For: docker-compose Strapi
Why: Container orchestration, 2200 free hours, scalable.
4. AWS Lambda
For: Automated tasks
Why: 1 million free requests, event-driven architecture.
5. Amazon RDS
For: Database
Why: 750 free hours, managed service, 20GB storage.
6. Amazon S3
For: File storage, backups
Why: 5GB free storage, durable.
7. Amazon CloudWatch
For: Monitoring
Why: 10 free custom metrics and alarms.
8. AWS Secrets Manager
For: Secrets
Why: Secure, but consider alternatives due to cost.
9. Amazon API Gateway
For: APIs
Why: 1 million free API calls, secure.
10. Terraform and Ansible
For: IaC and Configuration
Why: Version control, automation.
11. Documentation
For: READMEs
Why: Clarity, onboarding, and best practices.
12. GitHub and AWS ECR
For: Code and container repositories
Why: Version control, Docker Hub limitations.
Cost Analysis
EC2 t3.micro: Free tier
ECS Anywhere: Free tier (2200 hours)
Lambda: Free tier (1 million requests)
RDS: Free tier (750 hours)
S3: Free tier (5GB)
CloudWatch: Free tier (10 metrics)
Secrets Manager: $0.40/secret, consider alternatives
API Gateway: Free tier (1 million calls)

View File

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

View File

@ -0,0 +1,74 @@
Hard skills in SW:
HTML, CSS, JS, TS, Svelte, Sveltekit, Vite,
WebDev (meta)frameworks: Svelte (and sveltekit), a theoretical knowledge of reactjs (and next.js) and vue (and nuxt)
WebDev langs: HTML, CSS, tailwindcss, postcss (basics), JS, TS (basics), multiple ui libraries and billions of packages :D
WebDev linting: eslint
WebDev testing: Playwright
Languages: JS, Python, Micropython, C and C++ (both basics with microcontrollers), I really want to code in OstraJava and Brainf*ck
OS:
Linux [Ubuntu, Debian (Rpi OS, Kali - basics), QubesOS, Arch (Manjaro)], WSL,
Windows :D, advanced as a power user of XP,Vista,7,8,10,11 in my life, caused deep trauma. Haven't used win servers and don't plan to.
Terminal:
Process Monitoring
Performance Monitoring
Networking Tools
Text Manipulation
Scripting:
Bash
Power Shell (for user tasks)
Editors:
Nano, Emacs, VS Code, Notepad :D and Jupyter notebook
Version control: Git
VCS Hosting: Github, Gitlab
CI/CD:
Jenkins In progress
Gitlab CI In progress
Infrastructure Provisioning:
Terraform: In progress
Cloud Providers:
AWS - Preffered
DigitalOcean
Google Cloud (I did auth, api, company set up and some bots with spreadsheet)
CDN: Cloudinary
Networking, Security and Protocols:
FTP / SFTP
SSL / TLS
HTTP / HTTPS
DNS
SSH (putty and linux)
Serverless: AWS Lambda (0), Cloudflare, Vercel
Monorepo (with pipelines): all yarn, npm, pnpm with turborepo
Containerization: docker (dockerfile, docker-compose, ran many apps with it), DockerHub, AWS RCD
Container Orchestration: Docker swarm(basics)
K8s/K3s: In progress...
Orchestration: Terraform (0)
GitOps: In progress ArgoCD
Application monitoring: New Relic, Prometheus, Grafana, Elk stack
Logs Management:

View File

@ -0,0 +1,56 @@
## Turborepo [see 1st source]
I will use turborepo as I have experience with it from previous projects, I prefer the centralized, more orderly handling of repositories.
Because our CMS and webapp are workspaces that need to be configured differently, we need to extend the root config of Turborepo to each app individually by creating a turbo.json in each workspace.
For Strapi this will be:
$root/apps/cms/turbo.json
```
{
"extends": ["//"],
"pipeline": {
"build": {
// custom configuration for the build task in this workspace
},
// new tasks only available in this workspace
"special-task": {},
}
}
```
For Sveltekit Webapp:
$root/apps/web/turbo.json
```
{
"extends": ["//"],
"pipeline": {
"build": {
"outputs": [".svelte-kit/**"]
}
}
}
```
Key notes about this setup:
Docker:
Base Layer: Installs container dependencies and Turborepo globally.
Pruned Layer: Copies project files and runs turbo prune to exclude unnecessary dependencies.
Installer Layer: Copies pruned workspace and runs yarn to install dependencies.
Runner Layer: Starts the app.
## Docker
Alpine is more lightweight than Ubuntu, I will stick to the inspiration blog post [see 2nd source], which has a similar setup.
The blog post creates intermediate images 'base', 'pruned' etc. that can be used in subsequent stages.
To push images to docker hub:
```docker push mando42/portfolio:tagname```
Learned
Used sources:
1. https://turbo.build/repo/docs
2. https://dev.to/moofoo/creating-a-development-dockerfile-and-docker-composeyml-for-yarn-122-monorepos-using-turborepo-896

View File

@ -1,4 +1,6 @@
import type { MarkdownMetadata, MdsvexImport } from './types'; import type { MdsvexImport } from '$content/types';
import type { MarkdownMetadata } from '$content/types';
import type { Project } from '$lib/types/projects';
import { parseReadContent } from './utils'; import { parseReadContent } from './utils';
import { error } from '@sveltejs/kit'; import { error } from '@sveltejs/kit';
@ -13,10 +15,16 @@ export function listProjects() {
return parseReadContent(projects); return parseReadContent(projects);
} }
export async function getProjectMetadata(slug: string) {
const { post } = await getProject(slug);
return post;
}
export async function getProject(slug: string) { export async function getProject(slug: string) {
try { try {
const data: MdsvexImport<Project> = await import(`./projects/${slug}.md`); const data: MdsvexImport<Project & MarkdownMetadata> = await import(
`./projects/${slug}.md`
);
return { return {
post: { ...data.metadata, slug }, post: { ...data.metadata, slug },

View File

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

View File

@ -1,33 +0,0 @@
---
title: First Project
excerpt: This is the first project
date: 2021-01-01
tags:
- first
- post
published: true
image: Feature.jpg
---
## Svelte
Media inside the **Svelte** folder is served from the `static` folder.
```python
input_text = ''' "yahooapis.com",
"hotmail.com",
"gfx.ms",
"afx.ms",
"live.com",
'''
# and so on...
lines = input_text.split('\n')
formatted_lines = ['* ' + line.strip()[1:-2] + ' * block' for line in lines if line]
output_text = '\n'.join(formatted_lines)
print(output_text)
```

View File

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

View File

@ -1,22 +1,86 @@
type Skill = { type SkillLevel = 'A' | 'B' | 'C'; // A: Proficient, B: Experienced, C: limited Experience
type Skill = { title: string; level: SkillLevel };
type SkillList = {
title: string; title: string;
list: string[]; skill: Skill[];
}; };
// prettier-ignore // prettier-ignore
const skills: Skill[] = [ const skills: SkillList[] = [
{title:'Langs', list: ['JavaScript/TypeScript', 'Python', 'a bit of C/C++', 'Bash', 'SQL', '...']}, {title:'Langs', skill: [
{title:'Frontend', list: ['Svelte', 'SvelteKit']}, { title: 'JavaScript/TypeScript', level: 'A' },
{title:'Devops', list: ['Git', 'Terraform', 'Ansible', 'Docker', 'Docker Compose', 'K8s', 'Grafana']}, { title: 'Python', level: 'B' },
{title:'CI/CD', list: ['GitHub Actions', 'Gitea Actors', 'Gitlab', 'SFTP', ]}, { title: 'C/C++', level: 'C' },
{title:'Linux', list: ['Debian', 'Ubuntu', 'Arch Linux', 'Alpine', 'Raspbian']}, { title: 'Bash, sh', level: 'B' },
{title:'SysAdmin', list: ['Systemd', 'nginx', 'User acc management', 'basic networking tools, ...']}, { title: 'SQL', level: 'B' },
{title:'Databases', list: ['PostgreSQL', 'MariaDB', 'SQLite' ]}, ]},
{title:'Cloud', list: ['AWS', 'Azure', 'Cloudflare', 'DigitalOcean', 'Vercel', 'Hetzner Cloud']}, {title:'Frontend', skill: [
{title:'IoT', list: ['Raspberry Pi', 'Arduino', 'ESP32', 'ESP8266', '...sensors', 'MQTT', 'LoRa', 'BLE', 'NFC', 'WiFi', ]}, { title: 'Svelte', level: 'A' },
{title:'other tools', list: ['HC Vault / AWS Secrets Manager', 'AWS Lambda']}, { title: 'SvelteKit', level: 'A' }
{title:'Languages', list: ['English', 'Czech', 'French', 'German']}, ]},
{title:'design', list: ['Figma', 'UI/UX', 'Wireframing', 'Prototyping', 'Adobe InDesign, Illustrator, Inkscape, ...',]} {title:'IaC', skill: [
{ title: 'Docker', level: 'A' },
{ title: 'Terraform', level: 'B' },
{ title: 'Ansible', level: 'B' },
{ title: 'Kubernetes', level: 'B' },
{ title: 'Grafana', level: 'B' }
]},
{title:'CI/CD', skill: [
{ title: 'Git', level: 'A' },
{ title: 'GitHub Ecosystem', level: 'A' },
{ title: 'Gitea', level: 'A' },
{ title: 'Gitlab Ecosystem', level: 'B' }
]},
{title:'Linux', skill: [
{ title: 'Debian', level: 'A' },
{ title: 'Ubuntu', level: 'A' },
{ title: 'Arch Linux', level: 'B' },
{ title: 'Alpine', level: 'B' }
]},
{title:'SysAdmin', skill: [
{ title: 'Systemd', level: 'B' },
{ title: 'nginx', level: 'B' },
{ title: 'Management', level: 'B' },
{ title: 'Security + networking', level: 'B' }
]},
{title:'Databases', skill: [
{ title: 'PostgreSQL', level: 'A' },
{ title: 'MariaDB', level: 'B' },
{ title: 'MongoDB', level: 'C' }
]},
{title:'Cloud', skill: [
{ title: 'AWS', level: 'B' },
{ title: 'Azure', level: 'C' },
{ title: 'Cloudflare', level: 'A' },
{ title: 'DigitalOcean', level: 'A' },
{ title: 'Vercel', level: 'A' }
]},
{title:'IoT', skill: [
{ title: 'Raspberry Pi', level: 'A' },
{ title: 'ESPs, microcontrollers', level: 'A' },
{ title: 'MQTT', level: 'B' },
{ title: 'BLE', level: 'B' }
]},
{title:'other tools', skill: [
{ title: 'HC Vault / AWS Secrets Manager / Azure KV', level: 'A' },
{ title: 'AWS Lambda', level: 'A' },
{ title: 'Twilio Products', level: 'B' }
]},
{title:'Languages', skill: [
{ title: 'English', level: 'A' },
{ title: 'Czech', level: 'A' },
{ title: 'French', level: 'B' },
{ title: 'German', level: 'B' }
]},
{title:'design', skill: [
{ title: 'Figma', level: 'A' },
{ title: 'UI/UX', level: 'B' },
{ title: 'Wireframing', level: 'B' },
{ title: 'Prototyping', level: 'B' },
{ title: 'Adobe Products, Inkscape, ...', level: 'B' }
]}
]; ];
export default skills; export default skills;

83
src/cspDirectives.ts Normal file
View File

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

26
src/hooks.client.ts Normal file
View File

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

52
src/hooks.server.ts Normal file
View File

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

View File

@ -1,16 +0,0 @@
// import type { Handle } from "@sveltejs/kit";
// import * as cookie from "cookie";
import { sequence } from "@sveltejs/kit/hooks";
import { basename } from "path";
const handleHeaders = async ({ event, resolve }:{event:any, resolve:any}) => {
const response = await resolve(event);
// Avoid clickjacking attacks, see https://cheatsheetseries.owasp.org/cheatsheets/Clickjacking_Defense_Cheat_Sheet.html
response.headers.set(
"Content-Security-Policy",
"frame-ancestors *.mattmor.in *;"
);
return response;
};
export const handle = sequence(handleHeaders);

View File

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

View File

@ -14,7 +14,9 @@
<!-- <div class="container px-5 py-8 mx-auto flex items-center sm:flex-row flex-col"> --> <!-- <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="/"> <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"> <p class="sm:pl-4 text-base sm:py-2 sm:mt-0 mt-4">
© {year} Matthieu Morin All content, unless otherwise stated, by Matthieu Morin, is under © copyright {year}, and all of it licensed under CC BY-SA 4.0.
This site coded by me is <a src="">MIT Licensed</a> .
</p> </p>
</a> </a>
</div> </div>

View File

@ -0,0 +1,84 @@
<script lang="ts">
import { LightSwitch, AppBar, Avatar, getDrawerStore, Drawer } from '@skeletonlabs/skeleton';
import type { DrawerSettings } from '@skeletonlabs/skeleton';
const drawerStore = getDrawerStore();
// Components
import { getImageLink } from '$lib/images';
function triggerStyled() {
const drawerSettings: DrawerSettings = {
id: 'example-3',
// Provide your property overrides:
bgDrawer: 'bg-purple-900 text-white',
bgBackdrop: 'bg-gradient-to-tr from-indigo-500/50 via-purple-500/50 to-pink-500/50',
width: 'w-[280px] md:w-[480px]',
padding: 'p-4',
rounded: 'rounded-xl'
};
drawerStore.open(drawerSettings);
}
// Local
const imgPlaceholder = getImageLink({ id: 'linky', w: 128, h: 128 });
// function trigger(position: 'left' | 'top' | 'right' | 'bottom'): void {
// const s: DrawerSettings = { id: 'demo', position };
// drawerStore.open(s);
// }
// function triggerStyled(): void {
// const drawerSettings: DrawerSettings = {
// id: 'demo',
// // Property Overrides
// position: 'top',
// bgDrawer: 'bg-purple-900 text-white',
// bgBackdrop: 'bg-gradient-to-tr from-indigo-500/50 via-purple-500/50 to-pink-500/50',
// width: 'w-full md:w-[480px]',
// padding: 'p-4',
// rounded: 'rounded-xl',
// // Metadata
// meta: 'Styled Drawer'
// };
// drawerStore.open(drawerSettings);
// }
// function triggerMetadata(): void {
// const drawerSettings: DrawerSettings = {
// id: 'demo',
// position: 'top',
// // Metadata
// meta: 'Metadata Received!'
// };
// drawerStore.open(drawerSettings);
// }
// class="!max-w-7xl mx-auto grid grid-cols-[1fr_auto_auto]
// md:grid-cols-[48px_1fr_48px] md:place-items-center items-center gap-4 p-4"
</script>
<AppBar
gridColumns="grid-cols-[1fr_auto_auto]"
slotDefault="place-self-center"
slotTrail="place-content-end"
>
<a href="/" title="Return to Homepage" data-svelte-h="svelte-1hw8p15">
<Avatar src={'/images/profile-pic.png'} width="w-16" rounded={'rounded-full'} />
</a>
<section id="demo" class="hidden md:block">
<nav
class="flex flex-col md:flex-row gap-2 border md:border-0 border-surface-100-800-token bg-surface-50/50 dark:bg-surface-900/50 backdrop-blur-lg rounded-bl-container-token rounded-br-container-token md:rounded-token p-2 shadow-xl"
>
<a href="/" class="btn md:btn-sm hover:variant-soft-primary !variant-filled-primary"
>Home
</a>
<a href="/blog" class="btn md:btn-sm hover:variant-soft-primary">Blog</a>
<a href="/projects" class="btn md:btn-sm hover:variant-soft-primary">Projects </a>
</nav>
</section>
<section class="block md:hidden">
<button class="btn variant-filled-primary" on:click={triggerStyled}>
<i class="fa-solid fa-bars" /> <span>Menu</span>
</button>
</section>
<LightSwitch />
</AppBar>

View File

@ -0,0 +1,45 @@
<script lang="ts">
import { AppBar, LightSwitch } from '@skeletonlabs/skeleton';
import * as config from '$lib/config';
</script>
<AppBar>
<!-- Left-side Header -->
<svelte:fragment slot="lead">
<a href="/">
<div class="items-center align-middle flex gap-2">
<img src="/images/Logo.png" alt="Logo" class="w-8 h-8" />
<strong class="text-3xl uppercase">{config.title}</strong>
</div>
</a>
</svelte:fragment>
<!-- Right-side Header -->
<svelte:fragment slot="trail">
<!-- Links -->
<a href="/blog">Blog</a>
<a href="/projects">Projects</a>
<a href="/about">About</a>
<!-- Social -->
<section class="hidden sm:inline-flex space-x-1">
<a
class="btn-icon hover:variant-soft-primary"
href="https://github.com/matthieu42morin"
target="_blank"
rel="noreferrer"
>
<i class="fa-brands fa-github text-lg" />
</a>
<a
class="btn-icon hover:variant-soft-primary"
href="https://discord.gg/EXqV7W8MtY"
target="_blank"
rel="noreferrer"
>
<i class="fa-brands fa-linkedin text-lg" />
</a>
</section>
<LightSwitch />
</svelte:fragment>
</AppBar>

View File

@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import skills from '$content/skills'; import skills from '$content/skills';
import { error } from '@sveltejs/kit';
</script> </script>
<div <div
@ -7,11 +8,22 @@
id="skills" id="skills"
> >
<h2 class="h2 m-2">My skillset</h2> <h2 class="h2 m-2">My skillset</h2>
{#each skills as skill} {#each skills as skillList}
<div class="text-lg font-bold m-2">{skill.title}</div> <div class="text-lg font-bold m-2">{skillList.title}</div>
<div class="flex flex-wrap justify-center space-x-2 m-2"> <div class="flex flex-wrap justify-center space-x-2 m-2">
{#each skill.list as s} {#each skillList.skill as skill}
<div class="chip variant-outline-primary">{s}</div> {#if skill.level === 'A'}
<span>Proficient:</span>
<div class="chip variant-filled-primary">{skill.title}</div>
{:else if skill.level === 'B'}
<span>Experienced:</span>
<div class="chip variant-outline-primary">{skill.title}</div>
{:else if skill.level === 'C'}
<span>Limited Experience:</span>
<div class="chip variant-outline-tertiary">{skill.title}</div>
{:else}
<!--Error No input-->
{/if}
{/each} {/each}
</div> </div>
{/each} {/each}

View File

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

View File

@ -1,14 +1,14 @@
<script lang="ts"> <script lang="ts">
import type { BlogTag } from '$lib/types/blog'; import type { Tag } from '$lib/types/post';
export let selected: BlogTag; export let selected: Tag | null = null;
let className = ''; let className = '';
export { className as class }; export { className as class };
import { page } from '$app/stores'; import { page } from '$app/stores';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
let options: BlogTag[] = ['DevOps', 'AI', 'Updates']; let options: Tag[] = ['DevOps', 'Philosophy', 'Updates'];
const clickHandler = (value: BlogTag) => { const clickHandler = (value: Tag) => {
if (value === selected) { if (value === selected) {
goto(`/blog`, { keepFocus: true, noScroll: true }); goto(`/blog`, { keepFocus: true, noScroll: true });
selected = ''; selected = '';

View File

@ -0,0 +1,60 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import Prism from 'prismjs';
const dispatch = createEventDispatcher();
export let language = 'plaintext';
export let code = '';
let displayCode = Prism.highlight(code, Prism.languages[language], language);
function onCopyClick() {
navigator.clipboard.writeText(code);
dispatch('copy');
}
</script>
<div class="codeblock">
<header>
<span>{language}</span>
<button on:click={onCopyClick}>Copy</button>
</header>
<pre><code class="language-{language}">{@html displayCode}</code></pre>
</div>
<style>
/* Add your CSS styling here */
</style>
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import { storeHighlightJs } from './stores.js'; // I assume this is where you will store the Prism instance
import { clipboard } from '../../actions/Clipboard/clipboard.js';
export let language = 'plaintext';
export let code = '';
// ... [Maintain other props and classes from Skeleton's CodeBlock component here] ...
let displayCode: string = code;
// Language aliasing
function languageFormatter(lang: string): string {
const map = {
js: 'javascript',
ts: 'typescript',
shell: 'terminal'
};
return map[lang] || lang;
};
function onCopyClick() {
navigator.clipboard.writeText(code);
dispatch('copy');
}
// Highlight code using PrismJS
$: if ($storeHighlightJs !== undefined) {
displayCode = $storeHighlightJs.highlight(code, { language }).value.trim();
}

View File

@ -0,0 +1,63 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import Prism from 'prismjs';
// Event Dispatcher
const dispatch = createEventDispatcher();
// Props
export let lang: string = '';
export let code: string | null = null;
export let title: string | null = null;
export let rawCode: string | null = null;
export let lineNumbers = false;
// Props (styles)
export let background: string = 'bg-neutral-900/90';
export let blur: string = '';
export let text: string = 'text-sm';
export let color: string = 'text-white';
export let rounded: string = 'rounded-container-token';
export let shadow: string = 'shadow';
export let button: string = 'btn btn-sm variant-soft !text-white';
export let buttonLabel = 'Copy';
export let buttonCopied = '👍';
let displayCode: string = Prism.highlight(code, Prism.languages[language], language);
let copyState = false;
function languageFormatter(lang: string): string {
const map = {
js: 'javascript',
ts: 'typescript',
shell: 'terminal'
};
return map[lang] || lang;
}
function onCopyClick() {
copyState = true;
setTimeout(() => {
copyState = false;
}, 2000);
dispatch('copy');
}
$: if (lineNumbers) {
displayCode = displayCode.replace(/^/gm, '<span class="line"></span>\t');
}
</script>
{#if language && code}
<div class="codeblock" data-testid="codeblock">
<!-- Header -->
<header>
<span>{languageFormatter(language)}</span>
<button on:click={onCopyClick}>
{!copyState ? buttonLabel : buttonCopied}
</button>
</header>
<!-- Pre/Code -->
<pre><code class="language-{language}">{@html displayCode}</code></pre>
</div>
{/if}

View File

@ -0,0 +1,81 @@
<script lang="ts">
import HeadingLinkIcon from './svgs/heading-link.svelte';
export let lang: string = '';
export let code: string | null = null;
export let title: string | null = null;
export let rawCode: string | null = null;
let copiedSuccessfully = false;
const displayLanguageMap = {
yaml: 'yml',
shell: 'bash'
};
const mapDisplayLanguage = (str: string) => {
return displayLanguageMap[str.toLowerCase()] || str;
};
let copyCode = async () => {
try {
const copiedCode = rawCode;
await navigator.clipboard.writeText(copiedCode);
} catch (e) {}
copiedSuccessfully = true;
};
$: if (copiedSuccessfully) {
setTimeout(() => {
copiedSuccessfully = false;
}, 1000);
}
$: tag = title ?? mapDisplayLanguage(lang);
</script>
<div class="my-8 overflow-y-auto rounded-xl">
<div class="sticky bg-sand-dark dark:bg-light-black top-0 left-0 z-10 py-1 flex items-center">
{#if tag}
{#if title}
<span class="ml-4">{tag}</span>
{:else}
<div class="ml-4 flex items-center">
<div class="flex items-center gap-1">
<img class="dark:hidden" src="/svg/docs/language.svg" alt="language icon" />
<img
class="hidden dark:block"
src="/svg/docs/language-dark.svg"
alt="language icon"
/>
<span class="font-semibold text-important">language:&nbsp;</span>
</div>
<span class="text-important">{tag}</span>
</div>
{/if}
{/if}
<div class="flex-1" />
<button
on:click={copyCode}
class="mr-4 group px-2 py-1 transition-all duration-200 delay-[50] text-[12px] dark:stroke-[#999795] dark:hover:stroke-divider-light hover:stroke-black stroke-[#565252]"
><div
class={copiedSuccessfully
? 'hidden opacity-0'
: 'opacity-100 flex items-center gap-1 underline group-hover:text-black underline-offset-[0.25em] dark:group-hover:text-important group-hover:decoration-transparent transition-all duration-200 delay-[50]'}
>
<HeadingLinkIcon /><span>copy code</span>
</div>
<div
class="transition-opacity z-10 duration-300 rounded-md ease-out
{copiedSuccessfully ? 'opacity-100' : 'hidden opacity-0'}"
aria-hidden="true"
>
copied successfully
</div></button
>
</div>
<div>
<!-- don't format this or it'll make the codeblock weird -->
<!-- prettier-ignore -->
<pre class="language-{lang} !m-0 !rounded-none"><code class="language-{lang}">{@html code}</code></pre>
</div>
</div>

View File

@ -9,6 +9,7 @@
export let title: string; export let title: string;
export let image: string; export let image: string;
export let tags: string[] = []; export let tags: string[] = [];
export let type: 'blog' | 'projects';
</script> </script>
<svelte:head> <svelte:head>
@ -27,15 +28,15 @@
</svelte:head> </svelte:head>
<article class="flex justify-center mt-4 mb-8"> <article class="flex justify-center mt-4 mb-8">
<div class=" w-full lg:w-[50rem] leading-[177.7%]"> <div class=" w-full md:w-[50rem] leading-[177.7%]">
<header> <header>
<img <img
src="/images/{imagesDirectoryName}/{slug}/{image}" src="/images/{imagesDirectoryName}/{slug}/{image}"
alt={`${title}`} alt={`${title}`}
class=" bg-black/50 w-full aspect-[21/9] max-h-[540px] rounded-t-[1.3rem]" class=" bg-black/50 w-full aspect-[21/9] max-h-[540px] rounded-t-lg"
/> />
</header> </header>
<div class="flex-auto flex justify-between items-center py-4 px-2"> <div class="flex-auto flex justify-between items-center py-4 px-2 bg-surface-900">
{#if tags && tags.length > 0} {#if tags && tags.length > 0}
<div class="flex mb-2 items-center gap-2"> <div class="flex mb-2 items-center gap-2">
tags: {#each tags as tag} tags: {#each tags as tag}
@ -56,13 +57,7 @@
<slot /> <slot />
</div> </div>
</div> </div>
</div>
<hr class="opacity-50" /> <hr class="opacity-50" />
<footer class="p-4 flex justify-start items-center space-x-4" /> <footer class="p-4 flex justify-start items-center space-x-4" />
</div>
</article> </article>
<style lang="postcss">
.prose :global(nav.toc) {
@apply hidden;
}
</style>

View File

@ -1,15 +1,28 @@
<script lang="ts"> <script lang="ts">
import { isAnExternalLink } from '$lib/utils/helpers'; import { isAnExternalLink } from '$lib/utils/helpers';
import type { BlogPost } from '$lib/types/blog'; import type { Post } from '$lib/types/post';
import { onMount } from 'svelte';
export let isMostRecent: boolean = false; export let isMostRecent: boolean = false;
export let type: 'blog' | 'projects'; export let type: Post['type'] = 'blog' | 'projects';
export let post: BlogPost; export let post: Post;
// export let published: boolean; // export let published: boolean;
// export let headlineOrder: 'h3' | '' = ''; // export let headlineOrder: 'h3' | '' = '';
// export let badge: string = ''; // export let badge: string = '';
// export let textWidth: string = ''; // export let textWidth: string = '';
//window width
let iteration = 0;
const interval = setInterval(() => {
console.log(window.innerWidth);
iteration++;
if (iteration === 50) {
clearInterval(interval);
}
}, 1000);
const generateURL = (href?: string, slug?: string) => { const generateURL = (href?: string, slug?: string) => {
if (href) return href; if (href) return href;
return `/${type}/${slug}`; return `/${type}/${slug}`;
@ -29,21 +42,21 @@
<a <a
{href} {href}
{target} {target}
data-sveltekit-preload-data="hover"
class="card bg-gradient-to-br variant-glass-primary card-hover overflow-hidden flex flex-col space-y-4" class="card bg-gradient-to-br variant-glass-primary card-hover overflow-hidden flex flex-col space-y-4"
data-analytics={`{"context":"grid","variant":"preview"}`} data-analytics={`{"context":"grid","variant":"preview"}`}
> >
<div class="flex flex-col justify-between auto-rows-auto w-full h-full text-token"> <!-- Blog in long cols, projects in wide rows -->
<div class="flex {type === 'blog' ? 'flex-col' : 'flex-row'} justify-between w-full h-full">
<header> <header>
<img <img
src="/images/blog/{post.slug}/{post.image}" src={`/images/${type}/${post.slug}/${post.image}`}
class="bg-black/200 w-full aspect-[3/2]" class="bg-black/200 w-full aspect-[3/2]"
alt="Post" alt="Post preview image"
/> />
</header> </header>
<section class="p-4 space-y-4"> <section class="p-4 space-y-4">
<h2 class="h2" data-toc-ignore>{post.title}</h2> <h2 class="h2 text-ellipsis overflow-hidden" data-toc-ignore>{post.title}</h2>
<article> <article class="text-ellipsis overflow-hidden max-h-[128px] max-w-4">
<p> <p>
<!-- cspell:disable --> <!-- cspell:disable -->
{post.excerpt} {post.excerpt}
@ -81,6 +94,3 @@
</section> </section>
</div> </div>
</a> </a>
<style lang="postcss">
</style>

View File

@ -10,6 +10,7 @@
><path ><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" 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 /><path
class="w-"
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" 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 /></g
></svg ></svg

View File

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

View File

@ -11,7 +11,7 @@ export const github = 'https://github.com/matthieu42morin';
// prettier-ignore // prettier-ignore
export const socialLinks = [ export const socialLinks = [
{ title: 'LinkedIn', href: 'https://www.linkedin.com/in/matthieu-morin-7524731a9/', icon: 'fa-brands fa-linkedin'}, { title: 'LinkedIn', href: 'https://linkedin.com/in/mattmor-in', icon: 'fa-brands fa-linkedin'},
{ title: 'Matrix', href: '', icon: './MatrixLogo' }, { title: 'Matrix', href: '', icon: './MatrixLogo' },
{ title: 'Gitea', href: 'https://git.mattmor.in', icon: './GiteaLogo' }, { title: 'Gitea', href: 'https://git.mattmor.in', icon: './GiteaLogo' },
{ title: 'Mastodon', href: 'https://mastodon.social/@matt_mor', icon: 'fa-brands fa-mastodon'}, { title: 'Mastodon', href: 'https://mastodon.social/@matt_mor', icon: 'fa-brands fa-mastodon'},

6
src/lib/constants.ts Normal file
View File

@ -0,0 +1,6 @@
export const cookies = {
NECESSARY: 'mattmor-necessary',
ANALYTICAL: 'mattmor-analytical',
TARGETING: 'mattmor-targeting',
VISITED: 'mattmor-marketing-website-visited'
};

29
src/lib/images.ts Normal file
View File

@ -0,0 +1,29 @@
type ImageLinkArgs = {
/** Image ID */
id: ImageKey;
/** Image Height */
h: number;
/** Image Width */
w: number;
/** Adds the fit=max query param */
max?: boolean;
};
export function getImageLink({ id, h, w, max }: ImageLinkArgs): string {
const path = images[id].raw;
return `${path}&w=${w}&h=${h}&auto=format&fit=${max ? 'max' : 'crop'}`;
}
type ImageKey = keyof typeof images;
export const images = {
oboci: {
raw: '/images/services/oboci.jpg'
},
linky: {
raw: 'https://images.unsplash.com/photo-1510111652602-195fc654aa83?ixid=M3w0Njc5ODF8MHwxfGFsbHx8fHx8fHx8fDE2ODc5NzY0Nzl8&ixlib=rb-4.0.3&amp;w=48&amp;h=48&amp;auto=format&amp;fit=crop'
},
'classic-linky': {
raw: 'https://images.unsplash.com/photo-1617296537916-291a105cd2f4?ixid=M3w0Njc5ODF8MHwxfGFsbHx8fHx8fHx8fDE2ODc5NzY1MTl8&ixlib=rb-4.0.3'
}
};

View File

@ -1 +0,0 @@
export type categories = 'sveltekit' | 'svelte';

View File

@ -1,18 +1,17 @@
import type { MarkdownMetadata } from '$content/types'; import type { MarkdownMetadata } from '$content/types';
export type BlogTag = 'DevOps' | 'AI' | 'Updates' | ''; export type Tag = 'DevOps' | 'Philosophy' | 'Updates' | '';
export interface BlogPost extends MarkdownMetadata { export interface Post extends MarkdownMetadata {
author?: string; type?: 'Blog' | 'projects' | string;
date?: string; date?: string;
excerpt: string; excerpt: string;
image: string; image: string;
slug?: string; slug?: string;
href?: string; href?: string;
tags?: BlogTag[]; tags?: Tag[];
subtitle?: string; subtitle?: string;
teaserImage: string; teaserImage: string;
title: string; title: string;
isNotAnActualPost?: boolean; isNotAnActualPost?: boolean;
type?: string;
} }

View File

@ -1,12 +0,0 @@
import type { MarkdownMetadata } from '$content/types';
export interface Project extends MarkdownMetadata {
title: string;
excerpt: string;
slug: string;
image: string;
date: string;
pageTitle: string;
pageDescription: string;
keywords: string;
}

View File

@ -0,0 +1,45 @@
import Prism from 'prismjs';
import 'prismjs/components/prism-docker.min.js';
import 'prismjs/components/prism-bash.min.js';
import 'prismjs/components/prism-yaml.min.js';
import 'prismjs/components/prism-javascript.min.js';
import 'prismjs/components/prism-json.min.js';
import 'prismjs/components/prism-markdown.min.js';
import 'prismjs/components/prism-sql.min.js';
import 'prismjs/components/prism-toml.min.js';
import 'prismjs/components/prism-promql.min.js';
import 'prismjs/components/prism-go.min.js';
import 'prismjs/components/prism-typescript.min.js';
import 'prismjs/components/prism-python.min.js';
import { escapeSvelte } from 'mdsvex';
const langMap = {
sh: 'bash',
Dockerfile: 'dockerfile',
YAML: 'yaml'
};
/**
*
* @param {string} code the code that gets parsed
* @param {string} lang the language the code is written in
* @param {string} meta meta information for the code fence
* @returns {string}
*/
export function highlightCode(code, lang, meta) {
let title = null;
const _lang = langMap[lang] || lang || '';
if (meta) {
title = meta.match(/title="?(.*?)"/)?.[1];
}
const highlighted = _lang
? escapeSvelte(Prism.highlight(code, Prism.languages[_lang], _lang))
: code;
return `<CodeFence code={${JSON.stringify(highlighted)}}
rawCode={${JSON.stringify(code)}}
lang={"${_lang}"}
${title ? `title={"${title}"}` : ''}
/>`;
}

View File

@ -0,0 +1,34 @@
import { escapeSvelte } from 'mdsvex';
import { getHighlighter } from 'shiki';
/**
* @param code {string} - code to highlight
* @param lang {string} - code language
* @param meta {string} - code meta
* @returns {Promise<string>} - highlighted html
*/
async function highlighter(code, lang, meta) {
const shikiHighlighter = await getHighlighter({
theme: 'github-dark'
});
const html = escapeSvelte(
shikiHighlighter.codeToHtml(code, {
lang
})
);
return `{@html \`${html}\` }`;
}
// /**
// * Returns code with curly braces and backticks replaced by HTML entity equivalents
// * @param {string} html - highlighted HTML
// * @returns {string} - escaped HTML
// */
// function escapeHtml(code) {
// return code.replace(
// /[{}`]/g,
// (character) => ({ '{': '&lbrace;', '}': '&rbrace;', '`': '&grave;' }[character]),
// );
// }
export default highlighter;

View File

@ -0,0 +1,159 @@
import Prism from 'prismjs';
import 'prismjs/components/prism-docker.min.js';
import 'prismjs/components/prism-bash.min.js';
import 'prismjs/components/prism-yaml.min.js';
import 'prismjs/components/prism-javascript.min.js';
import 'prismjs/components/prism-json.min.js';
import 'prismjs/components/prism-markdown.min.js';
import 'prismjs/components/prism-sql.min.js';
import 'prismjs/components/prism-toml.min.js';
import 'prismjs/components/prism-promql.min.js';
import 'prismjs/components/prism-go.min.js';
import 'prismjs/components/prism-typescript.min.js';
import 'prismjs/components/prism-python.min.js';
import { escapeSvelte } from 'mdsvex';
export function highlightCode(code, lang) {
if (!Prism.languages[lang]) {
// Fallback to plaintext if the language isn't supported
return code;
}
const highlighted = escapeSvelte(Prism.highlight(code, Prism.languages[lang], lang));
return `<CodeBlock code={${JSON.stringify(highlighted)}} language={"${lang}"} />`;
}
// const langMap = {
// sh: 'bash',
// Dockerfile: 'dockerfile',
// YAML: 'yaml',
// };
// /**
// *
// * @param {string} code the code that gets parsed
// * @param {string} lang the language the code is written in
// * @param {string} meta meta information for the code fence
// * @returns {string}
// */
// export function highlightCode(code, lang, meta) {
// let title = null;
// const _lang = langMap[lang] || lang || '';
// if (meta) {
// title = meta.match(/title="?(.*?)"/)?.[1];
// }
// const highlighted = _lang
// ? escapeSvelte(Prism.highlight(code, Prism.languages[_lang], _lang))
// : code;
// return `<CodeFence code={${JSON.stringify(highlighted)}}
// rawCode={${JSON.stringify(code)}}
// lang={"${_lang}"}
// ${title ? `title={"${title}"}` : ''}
// />`;
// }
// import hljs from 'highlight.js/lib/core';
// // Import each language module you require
// import xml from 'highlight.js/lib/languages/xml'; // for HTML
// import css from 'highlight.js/lib/languages/css';
// import json from 'highlight.js/lib/languages/json';
// import javascript from 'highlight.js/lib/languages/javascript';
// import typescript from 'highlight.js/lib/languages/typescript';
// import shell from 'highlight.js/lib/languages/shell';
// import python from 'highlight.js/lib/languages/python';
// // The GOAT language
// import brainfuck from 'highlight.js/lib/languages/brainfuck'
// hljs.registerLanguage('xml', xml); // for HTML
// hljs.registerLanguage('css', css);
// hljs.registerLanguage('json', json);
// hljs.registerLanguage('javascript', javascript);
// hljs.registerLanguage('typescript', typescript);
// hljs.registerLanguage('shell', shell);
// hljs.registerLanguage('python', python);
// hljs.registerLanguage('brainfuck', brainfuck);
// // // Define the highlighter function
// // export function highlightCode(code: string, language: string): HighlightResult {
// // let result: string;
// // try {
// // // Use the highlight function from the hljs API
// // const highlightedCode = hljs.highlight(code, { language: `${lang}`}).value;
// // result = hljs.highlight(language, code).value;
// // } catch (error) {
// // console.error('Error in highlighting:', error);
// // result = code;
// // }
// // return result
// // return `{@html \`${html}\` }`;
// // }
// // const html = escapeSvelte(highlighter.codeToHtml(code, { lang }))
// // const highlighted = _lang
// // highlighter: async (code, lang = 'text') => {
// // // Enable debugMode or safeMode depending on the environment
// // if (dev) {
// // hljs.debugMode();
// // } else if (browser) {
// // hljs.safeMode();
// // }
// // // Tests
// // console.assert(highlightCode('console.log("Hello, world!");', 'javascript') !== 'console.log("Hello, world!");', 'JavaScript code is not highlighted correctly');
// // console.assert(highlightCode('print("Hello, world!")', 'python') !== 'print("Hello, world!")', 'Python code is not highlighted correctly');
// // const highlighter = await hljs.registerLanguage(lang, require(`highlight.js/lib/languages/{lang}`));
// // const highlightedHTML = escapeSvelte(highlighter.codeToHtml(code, { lang }))
// // return `{@html \`${highlightedHTML}\` }`
// // }
// /**
// (property) HighlightOptions.highlighter?: Highlighter | undefined
// highlighter - A custom highlight function for syntax highlighting. Two arguments are passed, both strings: the code to highlight and the language (if one is provided). It must return a string that will be injected into the document (or a promise that resolves to a string).
// example:
// highlighter(code, lang = "") {
// return `<pre class="${lang}"><code>${code}</code></pre>`;
// }
// Can be an async function.
// */
// // Define the custom highlighter function
// /**
// * Highlights code using the hljs library and returns a CodeBlock component.
// * - This is a test of the custom highlighter function, it doesn't work because it's out of scope of svelte
// * @async
// * @param {string} code - The code to be highlighted.
// * @param {string} lang - The language of the code to be highlighted.
// * @returns {Promise<string>} - A string representation of a CodeBlock component.
// */
// const customHighlighter = async (code = '', lang = '') => {
// let highlightedCode = '';
// // Enable debugMode or safeMode depending on the environment
// // if (process.env.NODE_ENV === 'development') {
// // hljs.debugMode();
// // } else if (process.env.NODE_ENV === 'production') {
// // hljs.safeMode();
// // }
// try {
// // Use the highlight function from the hljs API
// highlightedCode = hljs.highlight(lang || '', code).value;
// } catch (error) {
// console.error('Error in highlighting:', error);
// highlightedCode = code;
// };
// // Return the CodeBlock component
// return `<CodeBlock language="${lang}" code={\`${highlightedCode}\`} />`;
// };
// Define the HighlightOptions
// export const highlightOptions = {
// highlighter: highlightCode,
// alias: {
// yavascript: 'javascript'
// }
// };

View File

@ -1,16 +1,27 @@
<!-- This page handles any error encountered by the site. --> <!-- Sentry implementation test to error on any site -->
<script> <script>
import { SENTRY_ORG } from '$env/static/public';
import { page } from '$app/stores'; import { page } from '$app/stores';
let sth = import.meta.env.SENTRY_ORG;
</script> </script>
{#if $page.status === 404} {#if $page.status === 404}
<div class="flex flex-col items-center"> <div class="flex flex-col items-center m-4">
<div <div
class="relative mb-[3.33vh] flex h-96 w-96 items-center justify-center rounded-full bg-404" class="relative mb-[3.33vh] flex h-24 w-24 md:h-64 md:w-64 items-center justify-center bg-surface-800"
> >
<h1 class="h1 absolute text-h1 leading-[5rem]">404</h1> <img
class="object-fill"
src="/animations/travolta_confused.webp"
alt="Travolta Confused gif"
/>
<h1 class="h1 absolute leading-[5rem] text-white">404</h1>
</div> </div>
<h2 class="h4 mb-x-small">You just hit a route that doesn't exist</h2> <h2 class="h4 mb-2">
? My God, what are you doing here, this page doesn't exist and so on and so on.
</h2>
<span>{sth}</span>
</div> </div>
{:else} {:else}
<h2>{$page.status}</h2> <h2>{$page.status}</h2>
@ -19,8 +30,10 @@
<p class="subhead">{$page.error.message}</p> <p class="subhead">{$page.error.message}</p>
{/if} {/if}
<p><strong>Sorry!</strong> Maybe try one of these links?</p> <p><strong>Sorry!</strong> A grave error has occured. Maybe try one of these links?</p>
<ul> <ul>
<li><a href="/">Home</a></li> <li><a href="/">Home</a></li>
<li><a href="/blog">Blog</a></li>
<li><a href="/projects">Projects</a></li>
</ul> </ul>
{/if} {/if}

View File

@ -109,7 +109,7 @@
target="_blank" target="_blank"
rel="cv" rel="cv"
> >
Download my CV Get my CV
</a> </a>
</div> </div>
</div> </div>

View File

@ -0,0 +1,3 @@
<p>There is nothing to see yet</p>
In the beginning the universe was created.
This has made a lot of people very angry and has widely been regarded as a bad move

View File

@ -1 +0,0 @@
<p>There is nothing to see yet</p>

View File

@ -0,0 +1,56 @@
// // import type { Post } from '$lib/types/post';
// // import { json } from '@sveltejs/kit';
// // async function getPosts(): Promise<Post[]> {
// // const posts: Post[] = [];
// // const paths = import.meta.glob('$content/*.md', { eager: true });
// // for (const [path, file] of Object.entries(paths)) {
// // const slug = path.split('/').pop()?.replace('.md', '');
// // if (slug && file !== null && typeof file === 'object' && 'metadata' in file) {
// // const metadata = file.metadata as Omit<Post, 'slug'>;
// // const post: Post = { ...metadata, slug };
// // if (post.published) {
// // posts.push(post);
// // }
// // }
// // }
// // return posts.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
// // }
// // export async function GET() {
// // const posts = await getPosts();
// // return json(posts);
// // }
// import { json } from '@sveltejs/kit';
// import type { BlogPost } from '$lib/types/blog';
// async function getPosts() {
// let posts: BlogPost[] = [];
// const paths = import.meta.glob('/src/content/*.md', { eager: true });
// for (const path in paths) {
// const file = paths[path];
// const slug = path.split('/').at(-1)?.replace('.md', '');
// if (file && typeof file === 'object' && 'metadata' in file && slug) {
// const metadata = file.metadata as Omit<BlogPost, 'slug'>;
// const post = { ...metadata, slug } satisfies BlogPost;
// post.published && posts.push(post);
// }
// }
// posts = posts.sort(
// (first, second) => new Date(second.date).getTime() - new Date(first.date).getTime()
// );
// return posts;
// }
// export async function GET() {
// const posts = await getPosts();
// return json(posts);
// }

View File

@ -1,7 +1,7 @@
import { listBlogPosts } from '$content/blog'; import { listPosts } from '$content/blog';
export const load = async () => { export const load = async () => {
return { return {
posts: listBlogPosts(), posts: listPosts()
}; };
}; };

View File

@ -1,59 +1,14 @@
<!-- <script lang="ts">
import CategoryFilter from './../../lib/components/blog/CategoryFilter.svelte';
import { formatDate } from '$src/content/utils.js';
import { BlogPost } from '$lib/components/blog';
import * as config from '$lib/config';
export let data;
</script>
<svelte:head>
<title>{config.title}</title>
</svelte:head>
<section>
<ul class="posts">
{#each data.posts as post}
<li class="post">
<a href={post.slug} class="title"></a>
<p class="date">{formatDate(post.date, 'full', 'en')}</p>
<p class="description"></p>
</li>
{/each}
</ul>
<div class="flex w-3/4">
<BlogPost />
<div>Some shit</div>
</div>
</section>
<section class="flex max-w-[600px]">
{#each data.posts as post}
<li class="posts">
<a href={post.slug}>
<BlogPost>
<svelte:fragment slot="header">{post.title}</svelte:fragment>
<p class="date" />
<svelte:fragment slot="footer"
>{formatDate(post.date)}, {post.description}</svelte:fragment
>
</BlogPost>
</a>
</li>
{/each}
</section> -->
<script lang="ts"> <script lang="ts">
import PostPreview from '$lib/components/blog/PostPreview.svelte'; import PostPreview from '$lib/components/blog/PostPreview.svelte';
import CategoryFilter from '$lib/components/blog/CategoryFilter.svelte'; import CategoryFilter from '$lib/components/blog/CategoryFilter.svelte';
import type { PageData } from './$types'; import type { PageData } from './$types';
import type { BlogTag } from '$lib/types/blog'; import type { Tag } from '$lib/types/post';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
export let data: PageData; export let data: PageData;
let filter: BlogTag | null = null; let filter: Tag | null = null;
$: posts = data.posts.filter((post) => (filter ? post.tags?.includes(filter) : true)); $: posts = data.posts.filter((post) => (filter ? post.tags?.includes(filter) : true));
@ -61,7 +16,7 @@
const tagParam = $page.url.searchParams.get('tag'); const tagParam = $page.url.searchParams.get('tag');
if (!filter && typeof tagParam == 'string') { if (!filter && typeof tagParam == 'string') {
filter = tagParam as BlogTag; filter = tagParam as Tag;
} }
}); });

View File

@ -1,4 +1,4 @@
import { listBlogPosts } from '$content/blog'; import { listPosts } from '$content/blog';
import { error } from '@sveltejs/kit'; import { error } from '@sveltejs/kit';
function shuffle<T>(array: T[]) { function shuffle<T>(array: T[]) {
@ -9,7 +9,7 @@ function shuffle<T>(array: T[]) {
} }
export async function load({ params }) { export async function load({ params }) {
const posts = listBlogPosts(); const posts = listPosts();
const currentPost = posts.find((post) => post.slug == params.slug); const currentPost = posts.find((post) => post.slug == params.slug);
if (!currentPost) { if (!currentPost) {
@ -22,6 +22,6 @@ export async function load({ params }) {
featuredPosts: posts featuredPosts: posts
.filter((post) => post.slug != params.slug) .filter((post) => post.slug != params.slug)
.filter((p) => p.tags?.some((t) => currentPost.tags?.includes(t))) .filter((p) => p.tags?.some((t) => currentPost.tags?.includes(t)))
.slice(0, 3), .slice(0, 3)
}; };
} }

View File

@ -1,8 +1,8 @@
import { getBlogPost, listBlogPosts } from '$content/blog.js'; import { getPost, listPosts } from '$content/blog';
import type { PageLoad } from './$types'; import type { PageLoad } from './$types';
export const entries = async () => { export const entries = async () => {
const posts = await listBlogPosts(); const posts = await listPosts();
return posts return posts
.filter((post) => post.slug !== undefined) .filter((post) => post.slug !== undefined)
.map((post) => ({ slug: post.slug as string })); .map((post) => ({ slug: post.slug as string }));
@ -10,5 +10,5 @@ export const entries = async () => {
export const load: PageLoad = async ({ params, parent }) => { export const load: PageLoad = async ({ params, parent }) => {
await parent(); await parent();
return await getBlogPost(params.slug); return await getPost(params.slug);
}; };

View File

@ -0,0 +1,24 @@
// import { error } from '@sveltejs/kit';
// import type { BlogPost } from '$lib/types/blog';
// /** @type {import('./$types').PageLoad} */
// export async function load({ params }) {
// try {
// const post: BlogPost = await import(`$content/${params.slug}.md`);
// console.log(post.title);
// console.log(post.slug);
// return {
// content: post.default,
// meta: post.metadata
// };
// } catch (e) {
// throw error(404, `Could not find ${params.slug}`);
// }
// }
// // export async function load({ params }) {
// // const post = await import(`../../../content/${params.slug}.md`);
// // console.log(post.default);
// // console.log(post.metadata);
// // }

View File

@ -0,0 +1,7 @@
export const prerender = true;
export async function load({ url }) {
return {
url: url.pathname
};
}

7
src/routes/blog/bpage.ts Normal file
View File

@ -0,0 +1,7 @@
import type { Post } from '$lib/types/post';
export async function load({ fetch }) {
const response = await fetch('api/posts');
const posts: Post[] = await response.json();
return { posts };
}

View File

@ -0,0 +1,5 @@
// import { redirect } from '@sveltejs/kit';
// export async function load() {
// throw redirect(301, '/blog/rss.xml');
// }

View File

@ -1,9 +1,9 @@
import { listBlogPosts } from '$content/blog'; import { listPosts } from '$content/blog';
import RSS from 'rss'; import RSS from 'rss';
import type { BlogPost } from '$lib/types/blog'; import type { Post } from '$lib/types/post';
export const GET = async () => { export const GET = async () => {
const posts = listBlogPosts(); const posts = listPosts();
/* /*
The RSS feed is a JavaScript object that contains information about the blog feed. The RSS feed is a JavaScript object that contains information about the blog feed.
@ -29,7 +29,7 @@ export const GET = async () => {
// This creates an RSS feed. It does so by iterating over all posts and // This creates an RSS feed. It does so by iterating over all posts and
// adding each post to the feed. // adding each post to the feed.
posts.forEach((post: BlogPost) => { posts.forEach((post: Post) => {
feed.item({ feed.item({
title: post.title, title: post.title,
description: post.excerpt, description: post.excerpt,

View File

@ -1,11 +1,11 @@
import * as config from '$lib/config'; import * as config from '$lib/config';
import type { BlogPost } from '$lib/types'; import type { Post } from '$lib/types';
export const prerender = true; export const prerender = true;
export async function GET({ fetch }) { export async function GET({ fetch }) {
const response = await fetch('api/posts'); const response = await fetch('api/posts');
const posts: BlogPost[] = await response.json(); const posts: Post[] = await response.json();
const headers = { 'Content-Type': 'application/xml' }; const headers = { 'Content-Type': 'application/xml' };

View File

@ -0,0 +1,58 @@
<script lang="ts">
let Name = '';
let Email = '';
let Message = '';
let PrefferedMethod = '';
let ClientBudget = '';
// if name !=== ''
</script>
<label class="label">
<span>Name</span>
<input
class="input"
type="text"
placeholder="Input your name and or name of your company"
bind:value={Name}
/>
</label>
<label class="label">
<span>E-mail</span>
<input
class="input"
type="text"
placeholder="Please provide your work email, where we can reach you easily"
bind:value={Email}
/>
</label>
<label class="label">
<span>Message</span>
<textarea
class="textarea"
rows="4"
placeholder="Please tell us your project specifications, vision of where you see your project going and how do you want us to help."
bind:value={Message}
/>
</label>
<label>
<span>Preffered Method of contact </span>
</label>
<label class="label">
<span>Budget for project</span>
<select class="select" bind:value={ClientBudget} required>
<option value="">-- Please choose --</option>
<option value="0-5000">0-5000</option>
<option value="5000-10000">1000-5000</option>
<option value="10000-20000">10000-20000</option>
<option value="20000-50000">20000-50000</option>
<option value="50000+">50000+</option>
</select>
<p>
These ranges help us understand the scale of your project. Every project is unique and we'll
work with you to determine the exact price.
</p>
</label>

View File

@ -14,7 +14,7 @@
> >
{#each data.projects as post} {#each data.projects as post}
<div class="flex justify-center min-w-[20rem] max-w-sm"> <div class="flex justify-center min-w-[20rem] max-w-sm">
<PostPreview {post} type="blog" isMostRecent /> <PostPreview {post} type="projects" />
</div> </div>
{/each} {/each}
</div> </div>

View File

@ -5,6 +5,6 @@
export let data: PageData; export let data: PageData;
</script> </script>
<ProjectsContentLayout projects={data.projects} {...data.post}> <ProjectsContentLayout post={data.post} {...data.post}>
<svelte:component this={data.Component} /> <svelte:component this={data.Component} />
</ProjectsContentLayout> </ProjectsContentLayout>

View File

@ -0,0 +1,99 @@
<!--
This is just a very simple page with a button to throw an example error.
Feel free to delete this file and the entire sentry route.
-->
<script>
import * as Sentry from '@sentry/sveltekit';
function getSentryData() {
Sentry.startSpan(
{
name: 'Example Frontend Span',
op: 'test'
},
async () => {
const res = await fetch('/sentry-example');
if (!res.ok) {
throw new Error('Sentry Example Frontend Error');
}
}
);
}
const SentryOrg = import.meta.env.SENTRY_ORG;
const SentryProj = import.meta.env.SENTRY_PROJECT;
</script>
<div>
<head>
<title>Sentry Onboarding</title>
<meta name="description" content="Test Sentry for your SvelteKit app!" />
</head>
<main>
<h1>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 44">
<path
fill="currentColor"
d="M124.32,28.28,109.56,9.22h-3.68V34.77h3.73V15.19l15.18,19.58h3.26V9.22h-3.73ZM87.15,23.54h13.23V20.22H87.14V12.53h14.93V9.21H83.34V34.77h18.92V31.45H87.14ZM71.59,20.3h0C66.44,19.06,65,18.08,65,15.7c0-2.14,1.89-3.59,4.71-3.59a12.06,12.06,0,0,1,7.07,2.55l2-2.83a14.1,14.1,0,0,0-9-3c-5.06,0-8.59,3-8.59,7.27,0,4.6,3,6.19,8.46,7.52C74.51,24.74,76,25.78,76,28.11s-2,3.77-5.09,3.77a12.34,12.34,0,0,1-8.3-3.26l-2.25,2.69a15.94,15.94,0,0,0,10.42,3.85c5.48,0,9-2.95,9-7.51C79.75,23.79,77.47,21.72,71.59,20.3ZM195.7,9.22l-7.69,12-7.64-12h-4.46L186,24.67V34.78h3.84V24.55L200,9.22Zm-64.63,3.46h8.37v22.1h3.84V12.68h8.37V9.22H131.08ZM169.41,24.8c3.86-1.07,6-3.77,6-7.63,0-4.91-3.59-8-9.38-8H154.67V34.76h3.8V25.58h6.45l6.48,9.2h4.44l-7-9.82Zm-10.95-2.5V12.6h7.17c3.74,0,5.88,1.77,5.88,4.84s-2.29,4.86-5.84,4.86Z M29,2.26a4.67,4.67,0,0,0-8,0L14.42,13.53A32.21,32.21,0,0,1,32.17,40.19H27.55A27.68,27.68,0,0,0,12.09,17.47L6,28a15.92,15.92,0,0,1,9.23,12.17H4.62A.76.76,0,0,1,4,39.06l2.94-5a10.74,10.74,0,0,0-3.36-1.9l-2.91,5a4.54,4.54,0,0,0,1.69,6.24A4.66,4.66,0,0,0,4.62,44H19.15a19.4,19.4,0,0,0-8-17.31l2.31-4A23.87,23.87,0,0,1,23.76,44H36.07a35.88,35.88,0,0,0-16.41-31.8l4.67-8a.77.77,0,0,1,1.05-.27c.53.29,20.29,34.77,20.66,35.17a.76.76,0,0,1-.68,1.13H40.6q.09,1.91,0,3.81h4.78A4.59,4.59,0,0,0,50,39.43a4.49,4.49,0,0,0-.62-2.28Z"
/>
</svg>
</h1>
<p>
Get Started with this <strong>simple Example:</strong>
</p>
<p>1. Send us a sample error:</p>
<button type="button" on:click={getSentryData}> Throw error! </button>
<p>
2. Look for the error on the
<a href="https://{SentryOrg}.sentry.io/issues/?project=4506781187899392">Issues Page</a
>.
</p>
<p style="margin-top: 24px;">
For more information, take a look at the
<a href="https://docs.sentry.io/platforms/javascript/guides/sveltekit/">
Sentry SvelteKit Documentation
</a>
</p>
</main>
</div>
<style>
main {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
h1 {
font-size: 4rem;
margin: 14px 0;
}
svg {
height: 1em;
}
button {
padding: 12px;
cursor: pointer;
background-color: rgb(54, 45, 89);
border-radius: 4px;
border: none;
color: white;
font-size: 1em;
margin: 1em;
transition: all 0.25s ease-in-out;
}
button:hover {
background-color: #8c5393;
box-shadow: 4px;
box-shadow: 0px 0px 15px 2px rgba(140, 83, 147, 0.5);
}
button:active {
background-color: #c73852;
}
</style>

View File

@ -0,0 +1,6 @@
// This is just a very simple API route that throws an example error.
// Feel free to delete this file and the entire sentry route.
export const GET = async () => {
throw new Error("Sentry Example API Route Error");
};

View File

@ -1,5 +1,5 @@
import { removeTrailingSlash } from '$lib/utils/helpers'; import { removeTrailingSlash } from '$lib/utils/helpers';
import { listBlogPosts } from '$content/blog'; import { listPosts } from '$content/blog';
// prettier-ignore // prettier-ignore
const sitemap = (pages: string[]) => `<?xml version="1.0" encoding="UTF-8" ?> const sitemap = (pages: string[]) => `<?xml version="1.0" encoding="UTF-8" ?>
@ -44,8 +44,8 @@ export const GET = async () => {
.replace('/+page', ''); .replace('/+page', '');
}); });
const blogPosts = listBlogPosts().map((post) => `https://www.mattmor.in/blog/${post.slug}`); const Posts = listPosts().map((post) => `https://www.mattmor.in/blog/${post.slug}`);
const renderedSitemap = sitemap([...staticPages, ...blogPosts]); const renderedSitemap = sitemap([...staticPages, ...Posts]);
return new Response(renderedSitemap, { return new Response(renderedSitemap, {
headers: { headers: {

Binary file not shown.

After

Width:  |  Height:  |  Size: 734 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 950 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 950 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 712 KiB

After

Width:  |  Height:  |  Size: 372 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 950 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 950 KiB

View File

@ -1,14 +1,11 @@
import adapter from '@sveltejs/adapter-cloudflare'; import adapter from '@sveltejs/adapter-node';
import { vitePreprocess } from '@sveltejs/kit/vite';
import preprocess from 'svelte-preprocess'; import preprocess from 'svelte-preprocess';
// import { mdsvexGlobalComponents } from './src/lib/utils/mdsvexGlobalComponents.js/index.js'; // config extensions
import { mdsvex } from 'mdsvex'; import { mdsvex } from 'mdsvex';
import mdsvexConfig from './mdsvex.config.js'; import mdsvexConfig from './mdsvex.config.js';
/* Not using
// import { mdsvexGlobalComponents } from './src/lib/utils/mdsvex-global-components.js';
import cspDirectives from './csp-directives.js';
/** @type {import('@sveltejs/kit').Config} */ /** @type {import('@sveltejs/kit').Config} */
const config = { const config = {
@ -16,10 +13,7 @@ const config = {
// Consult https://kit.svelte.dev/docs/integrations#preprocessors // Consult https://kit.svelte.dev/docs/integrations#preprocessors
// for more information about preprocessors // for more information about preprocessors
preprocess: [ preprocess: [
preprocess({ preprocess({ postcss: true }),
postcss: true
}),
vitePreprocess(),
// No neeed rn unless using mdsvex highlighter with svelte components // No neeed rn unless using mdsvex highlighter with svelte components
//mdsvexGlobalComponents({ //mdsvexGlobalComponents({
@ -38,8 +32,12 @@ const config = {
// If your environment is not supported or you settled on a specific environment, switch out the adapter. // If your environment is not supported or you settled on a specific environment, switch out the adapter.
// See https://kit.svelte.dev/docs/adapters for more information about adapters. // See https://kit.svelte.dev/docs/adapters for more information about adapters.
adapter: adapter({}), adapter: adapter({
// sometimes problems with out: 'build',
precompress: false
}),
// Aliases need tsconfig explicit inclusion
alias: { alias: {
$lib: './src/lib', $lib: './src/lib',
$root: './', $root: './',
@ -47,15 +45,14 @@ const config = {
$routes: './src/routes', $routes: './src/routes',
$content: './src/content' $content: './src/content'
}, },
// TODO: FIX Banning external malware scripts for security and privacy of users, threw errors, paths: {
// csp: { assets: 'https://',
// directives: { }
// 'script-src': ['self'] // protect against XSS and others, using cloudflare endpoint
// }, csp: {
// reportOnly: { mode: 'auto',
// 'script-src': ['self'] directives: cspDirectives
// } },
// },
csrf: { csrf: {
checkOrigin: process.env.NODE_ENV === 'development' ? false : true checkOrigin: process.env.NODE_ENV === 'development' ? false : true
}, },

View File

@ -0,0 +1,437 @@
import { test, expect, type Page } from '@playwright/test';
test.beforeEach(async ({ page }) => {
await page.goto('https://demo.playwright.dev/todomvc');
});
const TODO_ITEMS = [
'buy some cheese',
'feed the cat',
'book a doctors appointment'
];
test.describe('New Todo', () => {
test('should allow me to add todo items', async ({ page }) => {
// create a new todo locator
const newTodo = page.getByPlaceholder('What needs to be done?');
// Create 1st todo.
await newTodo.fill(TODO_ITEMS[0]);
await newTodo.press('Enter');
// Make sure the list only has one todo item.
await expect(page.getByTestId('todo-title')).toHaveText([
TODO_ITEMS[0]
]);
// Create 2nd todo.
await newTodo.fill(TODO_ITEMS[1]);
await newTodo.press('Enter');
// Make sure the list now has two todo items.
await expect(page.getByTestId('todo-title')).toHaveText([
TODO_ITEMS[0],
TODO_ITEMS[1]
]);
await checkNumberOfTodosInLocalStorage(page, 2);
});
test('should clear text input field when an item is added', async ({ page }) => {
// create a new todo locator
const newTodo = page.getByPlaceholder('What needs to be done?');
// Create one todo item.
await newTodo.fill(TODO_ITEMS[0]);
await newTodo.press('Enter');
// Check that input is empty.
await expect(newTodo).toBeEmpty();
await checkNumberOfTodosInLocalStorage(page, 1);
});
test('should append new items to the bottom of the list', async ({ page }) => {
// Create 3 items.
await createDefaultTodos(page);
// create a todo count locator
const todoCount = page.getByTestId('todo-count')
// Check test using different methods.
await expect(page.getByText('3 items left')).toBeVisible();
await expect(todoCount).toHaveText('3 items left');
await expect(todoCount).toContainText('3');
await expect(todoCount).toHaveText(/3/);
// Check all items in one call.
await expect(page.getByTestId('todo-title')).toHaveText(TODO_ITEMS);
await checkNumberOfTodosInLocalStorage(page, 3);
});
});
test.describe('Mark all as completed', () => {
test.beforeEach(async ({ page }) => {
await createDefaultTodos(page);
await checkNumberOfTodosInLocalStorage(page, 3);
});
test.afterEach(async ({ page }) => {
await checkNumberOfTodosInLocalStorage(page, 3);
});
test('should allow me to mark all items as completed', async ({ page }) => {
// Complete all todos.
await page.getByLabel('Mark all as complete').check();
// Ensure all todos have 'completed' class.
await expect(page.getByTestId('todo-item')).toHaveClass(['completed', 'completed', 'completed']);
await checkNumberOfCompletedTodosInLocalStorage(page, 3);
});
test('should allow me to clear the complete state of all items', async ({ page }) => {
const toggleAll = page.getByLabel('Mark all as complete');
// Check and then immediately uncheck.
await toggleAll.check();
await toggleAll.uncheck();
// Should be no completed classes.
await expect(page.getByTestId('todo-item')).toHaveClass(['', '', '']);
});
test('complete all checkbox should update state when items are completed / cleared', async ({ page }) => {
const toggleAll = page.getByLabel('Mark all as complete');
await toggleAll.check();
await expect(toggleAll).toBeChecked();
await checkNumberOfCompletedTodosInLocalStorage(page, 3);
// Uncheck first todo.
const firstTodo = page.getByTestId('todo-item').nth(0);
await firstTodo.getByRole('checkbox').uncheck();
// Reuse toggleAll locator and make sure its not checked.
await expect(toggleAll).not.toBeChecked();
await firstTodo.getByRole('checkbox').check();
await checkNumberOfCompletedTodosInLocalStorage(page, 3);
// Assert the toggle all is checked again.
await expect(toggleAll).toBeChecked();
});
});
test.describe('Item', () => {
test('should allow me to mark items as complete', async ({ page }) => {
// create a new todo locator
const newTodo = page.getByPlaceholder('What needs to be done?');
// Create two items.
for (const item of TODO_ITEMS.slice(0, 2)) {
await newTodo.fill(item);
await newTodo.press('Enter');
}
// Check first item.
const firstTodo = page.getByTestId('todo-item').nth(0);
await firstTodo.getByRole('checkbox').check();
await expect(firstTodo).toHaveClass('completed');
// Check second item.
const secondTodo = page.getByTestId('todo-item').nth(1);
await expect(secondTodo).not.toHaveClass('completed');
await secondTodo.getByRole('checkbox').check();
// Assert completed class.
await expect(firstTodo).toHaveClass('completed');
await expect(secondTodo).toHaveClass('completed');
});
test('should allow me to un-mark items as complete', async ({ page }) => {
// create a new todo locator
const newTodo = page.getByPlaceholder('What needs to be done?');
// Create two items.
for (const item of TODO_ITEMS.slice(0, 2)) {
await newTodo.fill(item);
await newTodo.press('Enter');
}
const firstTodo = page.getByTestId('todo-item').nth(0);
const secondTodo = page.getByTestId('todo-item').nth(1);
const firstTodoCheckbox = firstTodo.getByRole('checkbox');
await firstTodoCheckbox.check();
await expect(firstTodo).toHaveClass('completed');
await expect(secondTodo).not.toHaveClass('completed');
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
await firstTodoCheckbox.uncheck();
await expect(firstTodo).not.toHaveClass('completed');
await expect(secondTodo).not.toHaveClass('completed');
await checkNumberOfCompletedTodosInLocalStorage(page, 0);
});
test('should allow me to edit an item', async ({ page }) => {
await createDefaultTodos(page);
const todoItems = page.getByTestId('todo-item');
const secondTodo = todoItems.nth(1);
await secondTodo.dblclick();
await expect(secondTodo.getByRole('textbox', { name: 'Edit' })).toHaveValue(TODO_ITEMS[1]);
await secondTodo.getByRole('textbox', { name: 'Edit' }).fill('buy some sausages');
await secondTodo.getByRole('textbox', { name: 'Edit' }).press('Enter');
// Explicitly assert the new text value.
await expect(todoItems).toHaveText([
TODO_ITEMS[0],
'buy some sausages',
TODO_ITEMS[2]
]);
await checkTodosInLocalStorage(page, 'buy some sausages');
});
});
test.describe('Editing', () => {
test.beforeEach(async ({ page }) => {
await createDefaultTodos(page);
await checkNumberOfTodosInLocalStorage(page, 3);
});
test('should hide other controls when editing', async ({ page }) => {
const todoItem = page.getByTestId('todo-item').nth(1);
await todoItem.dblclick();
await expect(todoItem.getByRole('checkbox')).not.toBeVisible();
await expect(todoItem.locator('label', {
hasText: TODO_ITEMS[1],
})).not.toBeVisible();
await checkNumberOfTodosInLocalStorage(page, 3);
});
test('should save edits on blur', async ({ page }) => {
const todoItems = page.getByTestId('todo-item');
await todoItems.nth(1).dblclick();
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages');
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).dispatchEvent('blur');
await expect(todoItems).toHaveText([
TODO_ITEMS[0],
'buy some sausages',
TODO_ITEMS[2],
]);
await checkTodosInLocalStorage(page, 'buy some sausages');
});
test('should trim entered text', async ({ page }) => {
const todoItems = page.getByTestId('todo-item');
await todoItems.nth(1).dblclick();
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(' buy some sausages ');
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter');
await expect(todoItems).toHaveText([
TODO_ITEMS[0],
'buy some sausages',
TODO_ITEMS[2],
]);
await checkTodosInLocalStorage(page, 'buy some sausages');
});
test('should remove the item if an empty text string was entered', async ({ page }) => {
const todoItems = page.getByTestId('todo-item');
await todoItems.nth(1).dblclick();
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('');
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter');
await expect(todoItems).toHaveText([
TODO_ITEMS[0],
TODO_ITEMS[2],
]);
});
test('should cancel edits on escape', async ({ page }) => {
const todoItems = page.getByTestId('todo-item');
await todoItems.nth(1).dblclick();
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages');
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Escape');
await expect(todoItems).toHaveText(TODO_ITEMS);
});
});
test.describe('Counter', () => {
test('should display the current number of todo items', async ({ page }) => {
// create a new todo locator
const newTodo = page.getByPlaceholder('What needs to be done?');
// create a todo count locator
const todoCount = page.getByTestId('todo-count')
await newTodo.fill(TODO_ITEMS[0]);
await newTodo.press('Enter');
await expect(todoCount).toContainText('1');
await newTodo.fill(TODO_ITEMS[1]);
await newTodo.press('Enter');
await expect(todoCount).toContainText('2');
await checkNumberOfTodosInLocalStorage(page, 2);
});
});
test.describe('Clear completed button', () => {
test.beforeEach(async ({ page }) => {
await createDefaultTodos(page);
});
test('should display the correct text', async ({ page }) => {
await page.locator('.todo-list li .toggle').first().check();
await expect(page.getByRole('button', { name: 'Clear completed' })).toBeVisible();
});
test('should remove completed items when clicked', async ({ page }) => {
const todoItems = page.getByTestId('todo-item');
await todoItems.nth(1).getByRole('checkbox').check();
await page.getByRole('button', { name: 'Clear completed' }).click();
await expect(todoItems).toHaveCount(2);
await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]);
});
test('should be hidden when there are no items that are completed', async ({ page }) => {
await page.locator('.todo-list li .toggle').first().check();
await page.getByRole('button', { name: 'Clear completed' }).click();
await expect(page.getByRole('button', { name: 'Clear completed' })).toBeHidden();
});
});
test.describe('Persistence', () => {
test('should persist its data', async ({ page }) => {
// create a new todo locator
const newTodo = page.getByPlaceholder('What needs to be done?');
for (const item of TODO_ITEMS.slice(0, 2)) {
await newTodo.fill(item);
await newTodo.press('Enter');
}
const todoItems = page.getByTestId('todo-item');
const firstTodoCheck = todoItems.nth(0).getByRole('checkbox');
await firstTodoCheck.check();
await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]);
await expect(firstTodoCheck).toBeChecked();
await expect(todoItems).toHaveClass(['completed', '']);
// Ensure there is 1 completed item.
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
// Now reload.
await page.reload();
await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]);
await expect(firstTodoCheck).toBeChecked();
await expect(todoItems).toHaveClass(['completed', '']);
});
});
test.describe('Routing', () => {
test.beforeEach(async ({ page }) => {
await createDefaultTodos(page);
// make sure the app had a chance to save updated todos in storage
// before navigating to a new view, otherwise the items can get lost :(
// in some frameworks like Durandal
await checkTodosInLocalStorage(page, TODO_ITEMS[0]);
});
test('should allow me to display active items', async ({ page }) => {
const todoItem = page.getByTestId('todo-item');
await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check();
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
await page.getByRole('link', { name: 'Active' }).click();
await expect(todoItem).toHaveCount(2);
await expect(todoItem).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]);
});
test('should respect the back button', async ({ page }) => {
const todoItem = page.getByTestId('todo-item');
await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check();
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
await test.step('Showing all items', async () => {
await page.getByRole('link', { name: 'All' }).click();
await expect(todoItem).toHaveCount(3);
});
await test.step('Showing active items', async () => {
await page.getByRole('link', { name: 'Active' }).click();
});
await test.step('Showing completed items', async () => {
await page.getByRole('link', { name: 'Completed' }).click();
});
await expect(todoItem).toHaveCount(1);
await page.goBack();
await expect(todoItem).toHaveCount(2);
await page.goBack();
await expect(todoItem).toHaveCount(3);
});
test('should allow me to display completed items', async ({ page }) => {
await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check();
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
await page.getByRole('link', { name: 'Completed' }).click();
await expect(page.getByTestId('todo-item')).toHaveCount(1);
});
test('should allow me to display all items', async ({ page }) => {
await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check();
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
await page.getByRole('link', { name: 'Active' }).click();
await page.getByRole('link', { name: 'Completed' }).click();
await page.getByRole('link', { name: 'All' }).click();
await expect(page.getByTestId('todo-item')).toHaveCount(3);
});
test('should highlight the currently applied filter', async ({ page }) => {
await expect(page.getByRole('link', { name: 'All' })).toHaveClass('selected');
//create locators for active and completed links
const activeLink = page.getByRole('link', { name: 'Active' });
const completedLink = page.getByRole('link', { name: 'Completed' });
await activeLink.click();
// Page change - active items.
await expect(activeLink).toHaveClass('selected');
await completedLink.click();
// Page change - completed items.
await expect(completedLink).toHaveClass('selected');
});
});
async function createDefaultTodos(page: Page) {
// create a new todo locator
const newTodo = page.getByPlaceholder('What needs to be done?');
for (const item of TODO_ITEMS) {
await newTodo.fill(item);
await newTodo.press('Enter');
}
}
async function checkNumberOfTodosInLocalStorage(page: Page, expected: number) {
return await page.waitForFunction(e => {
return JSON.parse(localStorage['react-todos']).length === e;
}, expected);
}
async function checkNumberOfCompletedTodosInLocalStorage(page: Page, expected: number) {
return await page.waitForFunction(e => {
return JSON.parse(localStorage['react-todos']).filter((todo: any) => todo.completed).length === e;
}, expected);
}
async function checkTodosInLocalStorage(page: Page, title: string) {
return await page.waitForFunction(t => {
return JSON.parse(localStorage['react-todos']).map((todo: any) => todo.title).includes(t);
}, title);
}

51
tests/example.spec.ts Normal file
View File

@ -0,0 +1,51 @@
import { test, expect } from '@playwright/test';
test('has title', async ({ page }) => {
await page.goto('/');
// Expect a title "to contain" a substring.
await expect(page).toHaveTitle(/Playwright/);
});
test('get started link', async ({ page }) => {
await page.goto('/');
// Click the Look at my code link.
await page.getByRole('link', { name: 'Look at my code' }).click();
// Click the get cv link.
await page.getByRole('link', { name: 'Get my CV' }).click();
// Expects page to have a heading with the name of Installation.
await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible();
});
// Blog tests
test('Blog tests', async ({ page }) => {
await page.goto('/blog');
await page.click('a[href="/about"]');
expect(page.url()).toBe('http://localhost:513/about');
});
beforeAll(async () => {
browser = await chromium.launch();
});
afterAll(async () => {
await browser.close();
});
beforeEach(async () => {
page = await browser.newPage();
await page.goto('http://localhost:5000/skills'); // replace with the URL of your skills page
});
afterEach(async () => {
await page.close();
});
it('should display the correct skill levels', async () => {
const skills = await page.$$eval('.chip', (elements) => elements.map((el) => el.textContent));
for (const skill of skills) {
expect(skill).toMatch(/^(Proficient|Experienced|Limited Experience):/);
}
});

View File

@ -16,7 +16,18 @@
"resolveJsonModule": true, "resolveJsonModule": true,
"sourceMap": true "sourceMap": true
}, },
"include": ["./scripts/**/*", "./test/*.js", "./*.js", "mdsvex.config.ts", "svelte.config.ts"], "include": [
"./scripts/**/*",
"./test/*.js",
"./*.js",
"./src/**/*.d.ts",
"./src/**/*.js",
"./src/**/*.svelte",
"./src/**/*.ts",
".svelte-kit/ambient.d.ts",
".svelte-kit/types/**/$types.d.ts",
"csp-directives.ts"
],
"exclude": ["node_modules/*"] "exclude": ["node_modules/*"]
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
// //

View File

@ -1,3 +1,4 @@
import { sentrySvelteKit } from '@sentry/sveltekit';
import { purgeCss } from 'vite-plugin-tailwind-purgecss'; import { purgeCss } from 'vite-plugin-tailwind-purgecss';
import { sveltekit } from '@sveltejs/kit/vite'; import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vitest/config'; import { defineConfig } from 'vitest/config';
@ -11,9 +12,15 @@ export default defineConfig({
}, },
build: { build: {
// to resolve https://github.com/vitejs/vite/issues/6985 // to resolve https://github.com/vitejs/vite/issues/6985
target: 'esnext', target: 'esnext'
}, },
plugins: [ plugins: [
sentrySvelteKit({
sourceMapsUploadOptions: {
org: process.env.SENTRY_ORG,
project: process.env.SENTRY_PROJECT
}
}),
sveltekit(), sveltekit(),
purgeCss({ purgeCss({
safelist: { safelist: {