mirror of
https://github.com/BradNut/personal-website-sveltekit
synced 2025-09-08 23:20:18 +00:00
Merge pull request #30 from BradNut/development
Development Svelte 5 Merge
This commit is contained in:
commit
7ae5b22e97
60 changed files with 2634 additions and 3197 deletions
|
|
@ -1,13 +0,0 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
/** @type { import("eslint").Linter.Config } */
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:svelte/recommended',
|
||||
'prettier'
|
||||
],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['@typescript-eslint'],
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 2020,
|
||||
extraFileExtensions: ['.svelte']
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
es2017: true,
|
||||
node: true
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.svelte'],
|
||||
parser: 'svelte-eslint-parser',
|
||||
parserOptions: {
|
||||
parser: '@typescript-eslint/parser'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
11
.gitignore
vendored
11
.gitignore
vendored
|
|
@ -5,11 +5,14 @@ node_modules
|
|||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
*.xdp*
|
||||
!.env.example
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
|
||||
.fleet
|
||||
.vercel
|
||||
.output
|
||||
.idea
|
||||
.vercel
|
||||
.fleet
|
||||
|
||||
# Sentry Config File
|
||||
.sentryclirc
|
||||
|
|
|
|||
1
.node-version
Normal file
1
.node-version
Normal file
|
|
@ -0,0 +1 @@
|
|||
22
|
||||
2
.nvmrc
2
.nvmrc
|
|
@ -1 +1 @@
|
|||
v22
|
||||
22
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
11
.prettierrc
11
.prettierrc
|
|
@ -1,11 +0,0 @@
|
|||
{
|
||||
"useTabs": true,
|
||||
"tabWidth": 2,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"bracketSpacing": true,
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"pluginSearchDirs": ["."],
|
||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||
}
|
||||
65
biome.json
Normal file
65
biome.json
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
||||
"vcs": {
|
||||
"enabled": false,
|
||||
"clientKind": "git",
|
||||
"useIgnoreFile": false
|
||||
},
|
||||
"files": { "ignoreUnknown": false, "ignore": [] },
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"formatWithErrors": false,
|
||||
"indentStyle": "space",
|
||||
"indentWidth": 2,
|
||||
"lineEnding": "lf",
|
||||
"lineWidth": 150,
|
||||
"attributePosition": "auto",
|
||||
"ignore": [
|
||||
"**/.DS_Store",
|
||||
"**/node_modules",
|
||||
"./build",
|
||||
"./.svelte-kit",
|
||||
"./package",
|
||||
"**/.env",
|
||||
"**/.env.*",
|
||||
"**/pnpm-lock.yaml",
|
||||
"**/package-lock.json",
|
||||
"**/yarn.lock",
|
||||
"**/paraglide/**"
|
||||
]
|
||||
},
|
||||
"organizeImports": { "enabled": true },
|
||||
"linter": { "enabled": true, "rules": { "recommended": true } },
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"jsxQuoteStyle": "single",
|
||||
"quoteProperties": "asNeeded",
|
||||
"trailingCommas": "all",
|
||||
"indentStyle": "space",
|
||||
"lineEnding": "lf",
|
||||
"lineWidth": 150,
|
||||
"semicolons": "always",
|
||||
"arrowParentheses": "always",
|
||||
"bracketSpacing": true,
|
||||
"bracketSameLine": false,
|
||||
"quoteStyle": "single",
|
||||
"attributePosition": "auto"
|
||||
},
|
||||
"parser": {
|
||||
"unsafeParameterDecoratorsEnabled": true
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"include": ["*.svelte"],
|
||||
"linter": {
|
||||
"rules": {
|
||||
"style": {
|
||||
"useConst": "off",
|
||||
"useImportType": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
11
docker-compose.yaml
Normal file
11
docker-compose.yaml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
version: "3.8"
|
||||
services:
|
||||
redis:
|
||||
image: redis:latest
|
||||
container_name: personal_website_redis
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
volumes:
|
||||
redis_data:
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
import { defineMDSveXConfig as defineConfig } from 'mdsvex';
|
||||
import relativeImages from 'mdsvex-relative-images';
|
||||
|
||||
const config = defineConfig({
|
||||
extensions: ['.svelte.md', '.md', '.svx'],
|
||||
|
||||
smartypants: {
|
||||
dashes: 'oldschool'
|
||||
},
|
||||
remarkPlugins: [relativeImages],
|
||||
rehypePlugins: []
|
||||
});
|
||||
|
||||
export default config;
|
||||
57
package.json
57
package.json
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "personal-website-sveltekit",
|
||||
"version": "0.0.1",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "NODE_OPTIONS=\"--inspect\" vite dev --host",
|
||||
|
|
@ -10,71 +10,54 @@
|
|||
"test:ui": "svelte-kit sync && playwright test --ui",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "prettier --plugin-search-dir . --check . && eslint .",
|
||||
"format": "prettier --plugin-search-dir . --write .",
|
||||
"lint": "biome lint --error-on-warnings .",
|
||||
"format": "biome format --write .",
|
||||
"test:integration": "playwright test",
|
||||
"test:unit": "vitest"
|
||||
},
|
||||
"packageManager": "pnpm@9.11.0",
|
||||
"devDependencies": {
|
||||
"@iconify-icons/material-symbols": "^1.2.58",
|
||||
"@iconify-icons/mdi": "^1.2.48",
|
||||
"@iconify-icons/radix-icons": "^1.2.9",
|
||||
"@iconify-icons/simple-icons": "^1.2.74",
|
||||
"@melt-ui/pp": "^0.3.2",
|
||||
"@playwright/test": "^1.49.0",
|
||||
"@sveltejs/adapter-auto": "^3.3.1",
|
||||
"@sveltejs/adapter-node": "^5.2.9",
|
||||
"@sveltejs/adapter-static": "^3.0.6",
|
||||
"@sveltejs/enhanced-img": "^0.2.1",
|
||||
"@sveltejs/kit": "^2.9.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.1.2",
|
||||
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
||||
"@typescript-eslint/parser": "^7.18.0",
|
||||
"@biomejs/biome": "^1.9.4",
|
||||
"@playwright/test": "^1.49.1",
|
||||
"@sveltejs/adapter-node": "^5.2.11",
|
||||
"@sveltejs/enhanced-img": "^0.4.4",
|
||||
"@sveltejs/kit": "^2.13.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
||||
"@zerodevx/svelte-img": "^2.1.2",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-svelte": "^2.46.0",
|
||||
"iconify-icon": "^2.1.0",
|
||||
"just-intersect": "^4.3.0",
|
||||
"mdsvex": "^0.11.2",
|
||||
"mdsvex-relative-images": "^1.0.3",
|
||||
"postcss": "^8.4.49",
|
||||
"postcss-import": "^16.1.0",
|
||||
"postcss-load-config": "^5.1.0",
|
||||
"postcss-preset-env": "^9.6.0",
|
||||
"prettier": "^3.4.1",
|
||||
"prettier-plugin-svelte": "^3.3.2",
|
||||
"sass": "^1.81.0",
|
||||
"satori": "^0.10.14",
|
||||
"satori-html": "^0.3.2",
|
||||
"scrape-it": "^6.1.3",
|
||||
"sharp": "^0.33.5",
|
||||
"svelte": "^4.2.19",
|
||||
"svelte-check": "^3.8.6",
|
||||
"svelte-meta-tags": "^3.1.4",
|
||||
"svelte-preprocess": "^5.1.4",
|
||||
"svelte": "^5.14.5",
|
||||
"svelte-check": "^4.1.0",
|
||||
"svelte-meta-tags": "^4.0.4",
|
||||
"svelte-preprocess": "^6.0.3",
|
||||
"svelte-sequential-preprocessor": "^2.0.2",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.7.2",
|
||||
"vanilla-lazyload": "^19.1.3",
|
||||
"vite": "^5.4.11",
|
||||
"vite": "^6.0.4",
|
||||
"vite-imagetools": "^7.0.5",
|
||||
"vitest": "^1.6.0"
|
||||
"vitest": "^2.1.8"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@melt-ui/svelte": "^0.76.3",
|
||||
"@resvg/resvg-js": "^2.6.2",
|
||||
"@sveltejs/adapter-vercel": "^5.5.0",
|
||||
"@sveltejs/adapter-vercel": "^5.5.2",
|
||||
"@types/nprogress": "^0.2.3",
|
||||
"@unpic/svelte": "^0.0.57",
|
||||
"@vercel/og": "^0.6.4",
|
||||
"bits-ui": "^0.21.16",
|
||||
"bits-ui": "1.0.0-next.66",
|
||||
"flexsearch": "^0.7.43",
|
||||
"ioredis": "^5.4.1",
|
||||
"lucide-svelte": "^0.378.0",
|
||||
"lucide-svelte": "^0.468.0",
|
||||
"nprogress": "^0.2.0",
|
||||
"svelte-local-storage-store": "^0.6.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
3112
pnpm-lock.yaml
3112
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
|
@ -5,14 +5,18 @@ import {
|
|||
WALLABAG_PASSWORD,
|
||||
WALLABAG_URL,
|
||||
PAGE_SIZE,
|
||||
USE_REDIS_CACHE
|
||||
} from '$env/static/private';
|
||||
import intersect from 'just-intersect';
|
||||
import type { Article, ArticlePageLoad, WallabagArticle } from '$lib/types/article';
|
||||
import { ArticleTag } from '$lib/types/articleTag';
|
||||
import type { PageQuery } from '$lib/types/pageQuery';
|
||||
import { URLSearchParams } from 'url';
|
||||
import { redis } from '$lib/server/redis';
|
||||
USE_REDIS_CACHE,
|
||||
} from "$env/static/private";
|
||||
import intersect from "just-intersect";
|
||||
import type {
|
||||
Article,
|
||||
ArticlePageLoad,
|
||||
WallabagArticle,
|
||||
} from "$lib/types/article";
|
||||
import { ArticleTag } from "$lib/types/articleTag";
|
||||
import type { PageQuery } from "$lib/types/pageQuery";
|
||||
import { URLSearchParams } from "node:url";
|
||||
import { redis } from "$lib/server/redis";
|
||||
|
||||
const base: string = WALLABAG_URL;
|
||||
|
||||
|
|
@ -29,64 +33,77 @@ export async function fetchArticlesApi(
|
|||
}
|
||||
|
||||
const pageQuery: PageQuery = {
|
||||
sort: 'updated',
|
||||
sort: "updated",
|
||||
perPage,
|
||||
since: 0,
|
||||
page: Number(queryParams?.page) || 1,
|
||||
tags: 'programming',
|
||||
content: 'metadata'
|
||||
tags: "programming",
|
||||
content: "metadata",
|
||||
};
|
||||
const entriesQueryParams = new URLSearchParams({
|
||||
...pageQuery,
|
||||
perPage: `${pageQuery.perPage}`,
|
||||
since: `${pageQuery.since}`,
|
||||
page: `${pageQuery.page}`
|
||||
page: `${pageQuery.page}`,
|
||||
});
|
||||
|
||||
if (USE_REDIS_CACHE) {
|
||||
console.log('Using redis cache');
|
||||
const cached = await redis.get(entriesQueryParams.toString());
|
||||
|
||||
if (cached) {
|
||||
console.log("Cache hit!");
|
||||
const response = JSON.parse(cached);
|
||||
const ttl = await redis.ttl(entriesQueryParams.toString());
|
||||
|
||||
console.log(`Response ${JSON.stringify(response)}`);
|
||||
console.log(`Returning cached response with ttl of ${ttl} seconds`);
|
||||
return { ...response, cacheControl: `max-age=${ttl}` };
|
||||
}
|
||||
}
|
||||
|
||||
const authBody = {
|
||||
grant_type: 'password',
|
||||
grant_type: "password",
|
||||
client_id: WALLABAG_CLIENT_ID,
|
||||
client_secret: WALLABAG_CLIENT_SECRET,
|
||||
username: WALLABAG_USERNAME,
|
||||
password: WALLABAG_PASSWORD
|
||||
password: WALLABAG_PASSWORD,
|
||||
};
|
||||
|
||||
const authResponse = await fetch(`${base}/oauth/v2/token`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams(authBody)
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams(authBody),
|
||||
});
|
||||
|
||||
const auth = await authResponse.json();
|
||||
|
||||
const pageResponse = await fetch(`${WALLABAG_URL}/api/entries.json?${entriesQueryParams}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${auth.access_token}`
|
||||
const pageResponse = await fetch(
|
||||
`${WALLABAG_URL}/api/entries.json?${entriesQueryParams}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${auth.access_token}`,
|
||||
},
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
if (!pageResponse.ok) {
|
||||
throw new Error(pageResponse.statusText);
|
||||
}
|
||||
|
||||
const cacheControl = pageResponse.headers.get('cache-control') || 'no-cache';
|
||||
const cacheControl = pageResponse.headers.get("cache-control") || "no-cache";
|
||||
|
||||
const { _embedded: favoriteArticles, page, pages, total, limit } = await pageResponse.json();
|
||||
const {
|
||||
_embedded: favoriteArticles,
|
||||
page,
|
||||
pages,
|
||||
total,
|
||||
limit,
|
||||
} = await pageResponse.json();
|
||||
const articles: Article[] = [];
|
||||
|
||||
favoriteArticles.items.forEach((article: WallabagArticle) => {
|
||||
for (const article of favoriteArticles.items as WallabagArticle[]) {
|
||||
const rawTags = article?.tags?.map((tag) => tag.slug);
|
||||
if (intersect(rawTags, Object.values(ArticleTag))?.length > 0) {
|
||||
const tags = rawTags.map((rawTag) => rawTag as unknown as ArticleTag);
|
||||
|
|
@ -94,16 +111,16 @@ export async function fetchArticlesApi(
|
|||
tags,
|
||||
title: article.title,
|
||||
url: new URL(article.url),
|
||||
domain_name: article.domain_name?.replace('www.', '') ?? '',
|
||||
domain_name: article.domain_name?.replace("www.", "") ?? "",
|
||||
hashed_url: article.hashed_url,
|
||||
reading_time: article.reading_time,
|
||||
preview_picture: article.preview_picture,
|
||||
created_at: new Date(article.created_at),
|
||||
updated_at: new Date(article.updated_at),
|
||||
archived_at: article.archived_at ? new Date(article.archived_at) : null
|
||||
archived_at: article.archived_at ? new Date(article.archived_at) : null,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const responseData: ArticlePageLoad = {
|
||||
articles,
|
||||
|
|
@ -111,11 +128,16 @@ export async function fetchArticlesApi(
|
|||
totalPages: pages,
|
||||
limit,
|
||||
totalArticles: total,
|
||||
cacheControl
|
||||
cacheControl,
|
||||
};
|
||||
|
||||
if (USE_REDIS_CACHE) {
|
||||
redis.set(entriesQueryParams.toString(), JSON.stringify(responseData), 'EX', 43200);
|
||||
redis.set(
|
||||
entriesQueryParams.toString(),
|
||||
JSON.stringify(responseData),
|
||||
"EX",
|
||||
43200
|
||||
);
|
||||
}
|
||||
|
||||
return responseData;
|
||||
|
|
|
|||
|
|
@ -1,14 +1,17 @@
|
|||
<script lang="ts">
|
||||
import type { Article } from "$lib/types/article";
|
||||
import ExternalLink from './ExternalLink.svelte';
|
||||
import type { Article } from '$lib/types/article';
|
||||
import { ArrowRight } from 'lucide-svelte';
|
||||
import ExternalLink from './ExternalLink.svelte';
|
||||
|
||||
export let articles: Article[];
|
||||
export let totalArticles: number;
|
||||
export let compact: boolean = false;
|
||||
export let classes: string[] = [];
|
||||
const {
|
||||
articles,
|
||||
totalArticles,
|
||||
compact = false,
|
||||
classes = [],
|
||||
}: { articles: Article[]; totalArticles: number; compact: boolean; classes?: string[] } = $props();
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<section class="articles">
|
||||
<h2>Favorite Articles</h2>
|
||||
<div class={classes.join(' ')}>
|
||||
{#each articles as article (article.hashed_url)}
|
||||
|
|
@ -16,16 +19,19 @@
|
|||
<section>
|
||||
<h3>
|
||||
<ExternalLink
|
||||
ariaLabel={`Link to ${article.title}`}
|
||||
href={article.url.toString()}
|
||||
showIcon
|
||||
>
|
||||
{#if compact}
|
||||
{article.title.substring(0, 50).trim()}
|
||||
{:else}
|
||||
{article.title}
|
||||
{/if}
|
||||
</ExternalLink>
|
||||
textData={{
|
||||
text: compact ? article.title.substring(0, 50).trim() : article.title,
|
||||
location: 'left',
|
||||
showIcon: true,
|
||||
}}
|
||||
linkData={{
|
||||
href: article.url.toString(),
|
||||
ariaLabel: `Link to ${article.title}`,
|
||||
title: `Link to ${article.title}`,
|
||||
target: '_blank',
|
||||
}}
|
||||
iconData={{ iconClass: 'center' }}
|
||||
/>
|
||||
</h3>
|
||||
<p>{article.domain_name}</p>
|
||||
</section>
|
||||
|
|
@ -41,13 +47,8 @@
|
|||
</article>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="moreArticles">
|
||||
<a href="/articles">{`${totalArticles} more articles`}</a>
|
||||
<a href="/articles" aria-label={`${totalArticles} more articles`}>
|
||||
<iconify-icon icon="material-symbols:arrow-right-alt-rounded"></iconify-icon>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<a class="moreArticles" href="/articles">{`${totalArticles} more articles`} <ArrowRight /></a>
|
||||
</section>
|
||||
|
||||
|
||||
<style lang="postcss">
|
||||
|
|
@ -59,6 +60,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
.articles {
|
||||
display: grid;
|
||||
place-content: center;
|
||||
}
|
||||
|
||||
.columns {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(250px, 1fr));
|
||||
|
|
@ -93,19 +99,15 @@
|
|||
}
|
||||
|
||||
.moreArticles {
|
||||
margin: 1.7rem;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
place-items: center;
|
||||
place-content: center;
|
||||
gap: 1rem;
|
||||
|
||||
& a {
|
||||
font-size: 2rem;
|
||||
}
|
||||
font-size: var(--h2);
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
font-size: 1.5rem;
|
||||
font-size: var(--h3);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
import type { Album } from "$lib/types/album";
|
||||
import LazyImage from './LazyImage.svelte';
|
||||
|
||||
export let albums: Album[];
|
||||
const { albums }: { albums: Album[] } = $props();
|
||||
const displayAlbums =
|
||||
albums?.length > 6 ? albums.slice(0, 6) : albums;
|
||||
|
||||
|
|
@ -10,15 +10,9 @@
|
|||
album.src = {
|
||||
img: { src: `${album.artwork}`, w: 230, h: 230 },
|
||||
sources: {
|
||||
avif: [
|
||||
{ src: `${album.artwork}`, w: 230, h: 230 },
|
||||
],
|
||||
webp: [
|
||||
{ src: `${album.artwork}`, w: 230, h: 230 },
|
||||
],
|
||||
jpg: [
|
||||
{ src: `${album.artwork}`, w: 230, h: 230 },
|
||||
]
|
||||
avif: `${album.artwork}`,
|
||||
webp: `${album.artwork}`,
|
||||
jpg: `${album.artwork}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
129
src/lib/components/ContactHub.svelte
Normal file
129
src/lib/components/ContactHub.svelte
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
<script lang="ts">
|
||||
import { Mail } from "lucide-svelte";
|
||||
import {
|
||||
blueSkyIcon,
|
||||
gitHubIcon,
|
||||
linkedInIcon,
|
||||
xIcon,
|
||||
} from "../util/logoIcons.svelte";
|
||||
import ExternalLink from '$lib/components/ExternalLink.svelte';
|
||||
|
||||
interface Props {
|
||||
showBlueSky?: boolean;
|
||||
showEmail?: boolean;
|
||||
showGithub?: boolean;
|
||||
showLinkedIn?: boolean;
|
||||
showX?: boolean;
|
||||
userNames: Record<string, string>;
|
||||
showText?: boolean;
|
||||
justify?: boolean;
|
||||
}
|
||||
|
||||
let {
|
||||
showBlueSky = false,
|
||||
showEmail = false,
|
||||
showGithub = false,
|
||||
showLinkedIn = false,
|
||||
showX = false,
|
||||
userNames,
|
||||
showText = false,
|
||||
justify = false,
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
{#if showText}
|
||||
<h3>Contact Information</h3>
|
||||
{/if}
|
||||
<div class:justifyCenter={justify}>
|
||||
{#if showX && userNames?.x}
|
||||
<ExternalLink
|
||||
linkData={{ href: `https://www.x.com/${userNames.x}`, ariaLabel: 'Contact through X', title: 'Contact through X', target: '_blank', clazz: "hub-icon x-contact" }}
|
||||
iconData={{ type: 'svg', icon: xIcon, iconClass: 'center' }}
|
||||
textData={{ showIcon: true }}
|
||||
/>
|
||||
{/if}
|
||||
{#if showBlueSky && userNames?.blueSky}
|
||||
<ExternalLink
|
||||
linkData={{ href: `https://bsky.app/profile/${userNames.blueSky}`, ariaLabel: 'Contact through Bluesky', title: 'Contact through Bluesky', target: '_blank', clazz: "hub-icon bluesky-contact" }}
|
||||
iconData={{ type: 'svg', icon: blueSkyIcon, iconClass: 'center' }}
|
||||
textData={{ showIcon: true }}
|
||||
/>
|
||||
{/if}
|
||||
{#if showLinkedIn && userNames?.linkedIn}
|
||||
<ExternalLink
|
||||
linkData={{ href: `https://www.linkedin.com/in/${userNames.linkedIn}`, ariaLabel: 'Contact through LinkedIn', title: 'Contact through LinkedIn', target: '_blank', clazz: "hub-icon linkedIn-contact" }}
|
||||
iconData={{ type: 'svg', icon: linkedInIcon, iconClass: 'center' }}
|
||||
textData={{ showIcon: true }}
|
||||
/>
|
||||
{/if}
|
||||
{#if showGithub && userNames?.github}
|
||||
<ExternalLink
|
||||
linkData={{ href: `https://www.github.com/${userNames.github}`, ariaLabel: 'Contact through Github', title: 'Contact through Github', target: '_blank', clazz: "hub-icon github-contact" }}
|
||||
iconData={{ type: 'svg', icon: gitHubIcon, iconClass: 'center' }}
|
||||
textData={{ showIcon: true }}
|
||||
/>
|
||||
{/if}
|
||||
{#if showEmail && userNames?.email}
|
||||
<ExternalLink
|
||||
linkData={{ href: `mailto:${userNames.email}`, ariaLabel: 'Contact by email', title: 'Contact by email', target: '_blank', clazz: "hub-icon email-contact" }}
|
||||
iconData={{ type: 'icon', icon: Mail, iconClass: 'center' }}
|
||||
textData={{ showIcon: true }}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
div {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 2rem;
|
||||
align-items: center;
|
||||
|
||||
&.justifyCenter {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.hub-icon) {
|
||||
transition: transform 0.2s cubic-bezier(0.65, 0, 0.35, 1);
|
||||
&:hover {
|
||||
transition: transform 0.4s cubic-bezier(0.65, 0, 0.35, 1);
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
}
|
||||
|
||||
:global(.x-contact) {
|
||||
color: var(--textColor);
|
||||
&:hover {
|
||||
color: var(--xColor);
|
||||
}
|
||||
}
|
||||
|
||||
:global(.linkedIn-contact) {
|
||||
color: var(--textColor);
|
||||
&:hover {
|
||||
color: var(--linkedInColor);
|
||||
}
|
||||
}
|
||||
|
||||
:global(.github-contact) {
|
||||
color: var(--textColor);
|
||||
&:hover {
|
||||
color: var(--githubColor);
|
||||
}
|
||||
}
|
||||
|
||||
:global(.bluesky-contact) {
|
||||
color: var(--textColor);
|
||||
&:hover {
|
||||
color: var(--blueskyColor);
|
||||
}
|
||||
}
|
||||
|
||||
:global(.email-contact) {
|
||||
color: var(--textColor);
|
||||
&:hover {
|
||||
color: var(--linkHover);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,37 +1,113 @@
|
|||
<script lang="ts">
|
||||
import OpenInNew from '@iconify-icons/mdi/open-in-new';
|
||||
import type { IconifyIcon } from 'iconify-icon';
|
||||
export let rel = 'noreferrer';
|
||||
export let target = '_blank';
|
||||
export let href: string;
|
||||
export let ariaLabel: string;
|
||||
export let showIcon: boolean = false;
|
||||
export let clazz = "";
|
||||
export let icon: IconifyIcon = OpenInNew;
|
||||
import { ExternalLink } from "lucide-svelte";
|
||||
import type {
|
||||
ExternalLinkType,
|
||||
LinkIconType,
|
||||
} from "$lib/types/externalLinkTypes";
|
||||
|
||||
const { iconData, linkData, textData }: ExternalLinkType = $props();
|
||||
|
||||
let textLocationClass = "";
|
||||
if (textData?.location === "top") {
|
||||
textLocationClass = "text-top";
|
||||
} else if (textData?.location === "bottom") {
|
||||
textLocationClass = "text-bottom";
|
||||
} else if (textData?.location === "left") {
|
||||
textLocationClass = "text-left";
|
||||
} else if (textData?.location === "right") {
|
||||
textLocationClass = "text-right";
|
||||
} else {
|
||||
textLocationClass = "text-left";
|
||||
}
|
||||
|
||||
const linkDecoration =
|
||||
linkData?.textDecoration && linkData?.textDecoration === "none"
|
||||
? `text-decoration-${linkData?.textDecoration}`
|
||||
: "text-decoration-underline";
|
||||
const linkClass =
|
||||
`${linkData?.clazz} ${textLocationClass} ${linkDecoration}`.trim();
|
||||
</script>
|
||||
|
||||
{#snippet externalLink({ iconData, linkData, textData }: ExternalLinkType)}
|
||||
<a
|
||||
class={linkClass}
|
||||
aria-label={`Open ${linkData?.ariaLabel ?? linkData?.title ?? linkData?.href} externally`}
|
||||
title={linkData?.title ?? `Open ${linkData?.ariaLabel} externally`}
|
||||
href={linkData.href}
|
||||
rel={linkData?.rel ?? "noreferrer"}
|
||||
target={linkData?.target ?? "_blank"}
|
||||
>
|
||||
{#if textData?.location === "top" || (textData?.location === "left" && textData?.text)}
|
||||
{textData?.text}
|
||||
{/if}
|
||||
{#if textData?.showIcon}
|
||||
{@render linkIcon?.(iconData ?? {})}
|
||||
{/if}
|
||||
{#if textData?.location === "bottom" || (textData?.location === "right" && textData?.text)}
|
||||
{textData?.text}
|
||||
{/if}
|
||||
</a>
|
||||
{/snippet}
|
||||
|
||||
<a class:show-icon={showIcon} class={clazz} aria-label={`Open ${ariaLabel} externally`} title={`Open ${ariaLabel} externally`} {href} {rel} {target}>
|
||||
<slot />
|
||||
{#if showIcon}
|
||||
<iconify-icon {icon} width="24" height="24" role="img" title={`Open ${ariaLabel} Externally`} />
|
||||
{#snippet linkIcon({ type, icon, iconClass }: LinkIconType)}
|
||||
{#if type === "svg" && icon}
|
||||
<svg
|
||||
style="width: 2.5rem; height: 2.5rem;"
|
||||
class={iconClass ?? ""}
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
{@render icon?.()}
|
||||
</svg>
|
||||
{:else if type === "icon" && icon}
|
||||
{@const Icon = icon}
|
||||
<Icon />
|
||||
{:else}
|
||||
{@const Icon = ExternalLink}
|
||||
<Icon />
|
||||
{/if}
|
||||
</a>
|
||||
{/snippet}
|
||||
|
||||
{@render externalLink({ iconData, linkData, textData })}
|
||||
|
||||
<style lang="postcss">
|
||||
a {
|
||||
margin: 1rem 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 0;
|
||||
font-size: var(--bodyTextSize);
|
||||
}
|
||||
|
||||
.show-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
.text-top {
|
||||
padding-bottom: 0.3rem;
|
||||
}
|
||||
|
||||
.text-bottom {
|
||||
padding-top: 0.3rem;
|
||||
}
|
||||
|
||||
.text-left,
|
||||
.text-right {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
place-items: baseline;
|
||||
place-content: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.text-decoration-none {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.text-decoration-underline {
|
||||
text-decoration: underline;
|
||||
text-decoration-color: var(--shellYellow);
|
||||
}
|
||||
|
||||
.show-icon {
|
||||
&:hover {
|
||||
color: var(--shellYellow);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -3,14 +3,24 @@
|
|||
import Img from '@zerodevx/svelte-img';
|
||||
import type { ExternalImageSource } from '../types/album';
|
||||
|
||||
export let clazz = "";
|
||||
export let src: Record<string, any> | ExternalImageSource[] | undefined;
|
||||
export let alt: string;
|
||||
export let style = "";
|
||||
export let loading: "lazy" | "eager" = "lazy";
|
||||
interface Props {
|
||||
clazz?: string;
|
||||
src: Record<string, any> | ExternalImageSource[] | undefined;
|
||||
alt: string;
|
||||
style?: string;
|
||||
loading?: "lazy" | "eager";
|
||||
}
|
||||
|
||||
let ref: any;
|
||||
let loaded: boolean;
|
||||
let {
|
||||
clazz = "",
|
||||
src,
|
||||
alt,
|
||||
style = "",
|
||||
loading = "lazy"
|
||||
}: Props = $props();
|
||||
|
||||
let ref: any = $state();
|
||||
let loaded: boolean = $state();
|
||||
|
||||
onMount(() => {
|
||||
if (ref.complete) {
|
||||
|
|
@ -21,7 +31,7 @@
|
|||
|
||||
<div class="wrap">
|
||||
<Img class={clazz} {style} {src} {alt} {loading} bind:ref on:load={() => (loaded = true)} />
|
||||
<div class:blur={loaded} class:loaded />
|
||||
<div class:blur={loaded} class:loaded></div>
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
|
|
|
|||
|
|
@ -1,10 +1,21 @@
|
|||
<script lang="ts">
|
||||
export let rel = '';
|
||||
export let target = '';
|
||||
export let href: string;
|
||||
export let ariaLabel: string;
|
||||
interface Props {
|
||||
rel?: string;
|
||||
target?: string;
|
||||
href: string;
|
||||
ariaLabel: string;
|
||||
children?: import('svelte').Snippet;
|
||||
}
|
||||
|
||||
let {
|
||||
rel = '',
|
||||
target = '',
|
||||
href,
|
||||
ariaLabel,
|
||||
children
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<a aria-label={ariaLabel} {href} {rel} {target}>
|
||||
<slot />
|
||||
{@render children?.()}
|
||||
</a>
|
||||
|
|
@ -3,32 +3,44 @@
|
|||
import { Pagination } from "bits-ui";
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-svelte';
|
||||
|
||||
export let additionalClasses: string;
|
||||
export let pageSize: number;
|
||||
export let totalCount: number;
|
||||
export let currentPage: number;
|
||||
export let base: string;
|
||||
interface Props {
|
||||
additionalClasses: string;
|
||||
pageSize: number;
|
||||
totalCount: number;
|
||||
currentPage: number;
|
||||
base: string;
|
||||
}
|
||||
|
||||
let {
|
||||
additionalClasses,
|
||||
pageSize,
|
||||
totalCount,
|
||||
currentPage,
|
||||
base
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<Pagination.Root let:pages count={totalCount} perPage={pageSize} page={currentPage || 1} class={`${additionalClasses}`}
|
||||
<Pagination.Root count={totalCount} perPage={pageSize} page={currentPage || 1} class={`${additionalClasses}`}
|
||||
onPageChange={(page) => goto(`${base}/${page}`)}>
|
||||
<Pagination.PrevButton>
|
||||
<ChevronLeft />
|
||||
</Pagination.PrevButton>
|
||||
{#each pages as page (page.key)}
|
||||
{#if page.type === "ellipsis"}
|
||||
<div class="ellipsis text-[15px] font-medium text-foreground-alt">...</div>
|
||||
{:else}
|
||||
<Pagination.Page {page}>
|
||||
<a href={`${base}/${page.value}`}>
|
||||
{page.value}
|
||||
</a>
|
||||
</Pagination.Page>
|
||||
{/if}
|
||||
{/each}
|
||||
<Pagination.NextButton>
|
||||
<ChevronRight />
|
||||
</Pagination.NextButton>
|
||||
{#snippet children({ pages })}
|
||||
<Pagination.PrevButton>
|
||||
<ChevronLeft />
|
||||
</Pagination.PrevButton>
|
||||
{#each pages as page (page.key)}
|
||||
{#if page.type === "ellipsis"}
|
||||
<div class="ellipsis text-[15px] font-medium text-foreground-alt">...</div>
|
||||
{:else}
|
||||
<Pagination.Page {page}>
|
||||
<a href={`${base}/${page.value}`}>
|
||||
{page.value}
|
||||
</a>
|
||||
</Pagination.Page>
|
||||
{/if}
|
||||
{/each}
|
||||
<Pagination.NextButton>
|
||||
<ChevronRight />
|
||||
</Pagination.NextButton>
|
||||
{/snippet}
|
||||
</Pagination.Root>
|
||||
|
||||
<style lang="postcss">
|
||||
|
|
|
|||
|
|
@ -1,21 +1,41 @@
|
|||
<script lang="ts">
|
||||
import LazyImage from './LazyImage.svelte';
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { Picture } from 'vite-imagetools';
|
||||
import { ExternalLinkType } from '../types/externalLinkType';
|
||||
|
||||
export let name: string;
|
||||
export let src: Record<string, any>;
|
||||
export let alt: string;
|
||||
export let style = "";
|
||||
export let loading: "lazy" | "eager" = "lazy";
|
||||
const {
|
||||
links,
|
||||
details,
|
||||
portfolioDetails,
|
||||
externalLinks,
|
||||
name,
|
||||
src,
|
||||
alt,
|
||||
style,
|
||||
fetchpriority = 'auto',
|
||||
loading = 'lazy',
|
||||
}: {
|
||||
links: Snippet<ExternalLinkType[]>;
|
||||
details: Snippet<string>;
|
||||
portfolioDetails: string;
|
||||
externalLinks: ExternalLinkType[];
|
||||
name: string;
|
||||
src: string | Picture;
|
||||
alt: string;
|
||||
style: string;
|
||||
fetchpriority?: 'high' | 'low' | 'auto';
|
||||
loading?: 'lazy' | 'eager';
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<div class="portfolio">
|
||||
<div class="portfolio-picture">
|
||||
<h2>{name}</h2>
|
||||
<LazyImage {style} {src} {alt} {loading} />
|
||||
<slot name="portfolio-links" />
|
||||
<enhanced:img {src} {style} {alt} {fetchpriority} {loading} />
|
||||
{@render links(externalLinks)}
|
||||
</div>
|
||||
<div class="portfolio-details">
|
||||
<slot name="portfolio-details" />
|
||||
{@render details()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -9,17 +9,26 @@
|
|||
siteUrl,
|
||||
};
|
||||
|
||||
export let title = defaultMetadata.defaultTitle;
|
||||
export let description = defaultMetadata.defaultDescription;
|
||||
export let image = defaultMetadata.defaultImage;
|
||||
export let location: string = '';
|
||||
interface Props {
|
||||
title?: any;
|
||||
description?: any;
|
||||
image?: any;
|
||||
location?: string;
|
||||
}
|
||||
|
||||
$: seo = {
|
||||
let {
|
||||
title = defaultMetadata.defaultTitle,
|
||||
description = defaultMetadata.defaultDescription,
|
||||
image = defaultMetadata.defaultImage,
|
||||
location = ''
|
||||
}: Props = $props();
|
||||
|
||||
let seo = $derived({
|
||||
title,
|
||||
description,
|
||||
image: `${siteUrl}${image}`,
|
||||
url: `${siteUrl}${location || ``}`,
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
<script lang="ts">
|
||||
export let name: string;
|
||||
interface Props {
|
||||
name: string;
|
||||
}
|
||||
|
||||
let { name }: Props = $props();
|
||||
</script>
|
||||
|
||||
<span>{name}</span>
|
||||
|
|
|
|||
|
|
@ -1,130 +0,0 @@
|
|||
<script lang="ts">
|
||||
import email from '@iconify-icons/material-symbols/mail';
|
||||
import github from '@iconify-icons/radix-icons/github-logo';
|
||||
import linkedin from '@iconify-icons/radix-icons/linkedin-logo';
|
||||
import twitter from '@iconify-icons/radix-icons/twitter-logo';
|
||||
|
||||
export let showGithub: boolean = false;
|
||||
export let showTwitter: boolean = false;
|
||||
export let showLinkedIn: boolean = false;
|
||||
export let showEmail: boolean = false;
|
||||
export let userNames: Record<string, string>;
|
||||
export let showText: boolean = false;
|
||||
export let justify: boolean = false;
|
||||
</script>
|
||||
|
||||
{#if showText}
|
||||
<h3>Contact Information</h3>
|
||||
{/if}
|
||||
<div class:justifyCenter={justify}>
|
||||
{#if showTwitter && userNames?.twitter}
|
||||
<span>
|
||||
<a
|
||||
href={`https://www.twitter.com/${userNames.twitter}`}
|
||||
target="_blank"
|
||||
title="Contact through Twitter"
|
||||
aria-label="Contact through Twitter"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<iconify-icon icon={twitter} class="twitter-contact" width="24" height="24" />
|
||||
</a>
|
||||
</span>
|
||||
{/if}
|
||||
{#if showLinkedIn && userNames?.linkedIn}
|
||||
<span>
|
||||
<a
|
||||
href={`https://www.linkedin.com/in/${userNames.linkedIn}`}
|
||||
target="_blank"
|
||||
title="Contact through LinkedIn"
|
||||
aria-label="Contact through LinkedIn"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<iconify-icon icon={linkedin} class="linkedin-contact" width="24" height="24" />
|
||||
</a>
|
||||
</span>
|
||||
{/if}
|
||||
{#if showGithub && userNames?.github}
|
||||
<span>
|
||||
<a
|
||||
href={`https://www.github.com/${userNames.github}`}
|
||||
target="_blank"
|
||||
title="View Github"
|
||||
aria-label="View Github"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<iconify-icon icon={github} class="github-contact" width="24" height="24" />
|
||||
</a>
|
||||
</span>
|
||||
{/if}
|
||||
{#if showEmail && userNames?.email}
|
||||
<span>
|
||||
<a
|
||||
href={`mailto:${userNames.email}`}
|
||||
target="_blank"
|
||||
title="Contact by email"
|
||||
aria-label="Contact by email"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<iconify-icon icon={email} class="email-contact" width="24" height="24" />
|
||||
</a>
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
div {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
|
||||
&.justifyCenter {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
--twitterColor: #1da1f2;
|
||||
--linkedInColor: #0a66c2;
|
||||
--githubColor: #72757e;
|
||||
--emailColor: var(--linkHover);
|
||||
}
|
||||
|
||||
iconify-icon {
|
||||
transition: transform 0.2s cubic-bezier(0.65, 0, 0.35, 1);
|
||||
&:hover {
|
||||
transition: transform 0.4s cubic-bezier(0.65, 0, 0.35, 1);
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
font-size: 4rem;
|
||||
margin: 1.5rem;
|
||||
|
||||
@media (max-width: 550px) {
|
||||
font-size: 3.55rem;
|
||||
}
|
||||
}
|
||||
.twitter-contact {
|
||||
color: var(--textColor);
|
||||
&:hover {
|
||||
color: var(--twitterColor);
|
||||
}
|
||||
}
|
||||
.linkedin-contact {
|
||||
color: var(--textColor);
|
||||
&:hover {
|
||||
color: var(--linkedInColor);
|
||||
}
|
||||
}
|
||||
.github-contact {
|
||||
color: var(--textColor);
|
||||
&:hover {
|
||||
color: var(--githubColor);
|
||||
}
|
||||
}
|
||||
.email-contact {
|
||||
color: var(--textColor);
|
||||
&:hover {
|
||||
color: var(--linkHover);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import ContactHub from '$lib/components/contactHub/index.svelte';
|
||||
import ContactHub from '$lib/components/ContactHub.svelte';
|
||||
const userNames = {
|
||||
github: 'BradNut',
|
||||
linkedIn: 'bradley-shellnut',
|
||||
email: 'bradleyshellnut@pm.me',
|
||||
email: 'bradleyshellnut[at]pm.me',
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
@ -27,6 +27,7 @@
|
|||
<style lang="postcss">
|
||||
footer {
|
||||
display: grid;
|
||||
gap: 2rem;
|
||||
grid-template-rows: repeat(3, 1fr);
|
||||
background: var(--footerBackground);
|
||||
place-content: center;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,23 @@
|
|||
<script lang="ts">
|
||||
export let header: string;
|
||||
export let page: string;
|
||||
export let image: string;
|
||||
export let content: string;
|
||||
export let width = 1200;
|
||||
export let height = 630;
|
||||
export let url: string;
|
||||
interface Props {
|
||||
header: string;
|
||||
page: string;
|
||||
image: string;
|
||||
content: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
url: string;
|
||||
}
|
||||
|
||||
let {
|
||||
header,
|
||||
page,
|
||||
image,
|
||||
content,
|
||||
width = 1200,
|
||||
height = 630,
|
||||
url
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="social-card" style={`width: ${width}px; height: ${height}px;`}>
|
||||
|
|
|
|||
|
|
@ -1,42 +0,0 @@
|
|||
import meta from '$lib/assets/images/portfolio/Bradley_Shellnut_New_Site.png?metadata';
|
||||
import formatMeta from '$lib/assets/images/portfolio/Bradley_Shellnut_New_Site.png?format=webp;avif;png&metadata';
|
||||
import placeholder from '$lib/assets/images/portfolio/Bradley_Shellnut_New_Site.png?w=100&png&blur=10';
|
||||
|
||||
type ImageMeta = {
|
||||
format: string;
|
||||
src: string;
|
||||
type: string;
|
||||
width: string;
|
||||
height: string;
|
||||
};
|
||||
|
||||
type Sources = {
|
||||
srcset: URL;
|
||||
type: string;
|
||||
width: string;
|
||||
height: string;
|
||||
};
|
||||
|
||||
const { height, src, width } = meta as ImageMeta;
|
||||
|
||||
const sources: Sources[] = [];
|
||||
const imageFormatsMetadata: ImageMeta[] = JSON.parse(`${formatMeta}`);
|
||||
for (const metadata of imageFormatsMetadata) {
|
||||
sources.push({
|
||||
srcset: new URL(metadata.src),
|
||||
type: `image/${metadata.format}`,
|
||||
width: metadata.width,
|
||||
height: metadata.height
|
||||
});
|
||||
}
|
||||
|
||||
const data = {
|
||||
alt: 'Home Page of bradleyshellnut.com',
|
||||
width,
|
||||
height,
|
||||
src,
|
||||
sources,
|
||||
placeholder
|
||||
};
|
||||
|
||||
export { data as default };
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
---
|
||||
portfolioName: 'Old Personal Website'
|
||||
portfolioSubHeading: 'My first personal website.'
|
||||
portfolioImage: 'Old_Website_Bradley_Shellnut.png'
|
||||
portfolioImageAlt: 'Home Page of the old bradleyshellnut.com website'
|
||||
---
|
||||
|
||||
<script>
|
||||
import ExternalLink from '$lib/components/ExternalLink.svelte';
|
||||
</script>
|
||||
|
||||
{portfolioSubHeading}
|
||||
|
||||
This was my first real personal website hosted on DigitalOcean.
|
||||
|
||||
Tech stack:
|
||||
|
||||
- React
|
||||
- Redux
|
||||
- ReactStrap for CSS grid management
|
||||
- React Router for routing links in the page
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
---
|
||||
portfolioName: 'Personal Website'
|
||||
portfolioSubHeading: 'My personal website re-written using SvelteKit.'
|
||||
portfolioImage: 'Bradley_Shellnut_New_Site.png'
|
||||
portfolioImageAlt: 'Home Page of bradleyshellnut.com'
|
||||
---
|
||||
|
||||
<script>
|
||||
import ExternalLink from '$lib/components/ExternalLink.svelte';
|
||||
</script>
|
||||
|
||||
{portfolioSubHeading}
|
||||
|
||||
Tech Stack:
|
||||
|
||||
- <ExternalLink href="https://kit.svelte.dev/" showIcon>SvelteKit</ExternalLink>
|
||||
- <ExternalLink showIcon href="https://www.melt-ui.com/">Melt UI</ExternalLink> for the headless-ui components.
|
||||
- TypeScript
|
||||
- Deployed on Vercel
|
||||
- Icons in the [/about](/about) page and the Bee, Shell, and Nut icons are all made by <ExternalLink showIcon href="https://www.flaticon.com/authors/freepik" ariaLabel="Freepik">"Freepik"</ExternalLink> from <ExternalLink showIcon href="https://www.flaticon.com" ariaLabel="Flaticon">flaticon.com</ExternalLink>
|
||||
|
||||
Previous version of my website was written using React and Gatsby which you can view <ExternalLink href="https://wonderful-austin-9f17d2.netlify.app/" ariaLabel="React and Gatsby Personal Site version" showIcon>here</ExternalLink>.
|
||||
|
||||
Each iteration brings better code and my previous React version was improved after the suggestions on <ExternalLink showIcon href="https://syntax.fm/show/444/syntax-highlight#t=33:19" ariaLabel="Syntax.fm Podcast Number 444">Show 444</ExternalLink> of the <ExternalLink href="https://syntax.fm/show/444/syntax-highlight#t=33:19" ariaLabel="Syntax.fm Podcast" showIcon>Syntax Podcast</ExternalLink>.
|
||||
|
||||
You can view the previous archived version of the site before those changes <ExternalLink href="https://web.archive.org/web/20210224002046/https://bradleyshellnut.com/" ariaLabel="Archive before Syntax Podcast" showIcon>here</ExternalLink>.
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
---
|
||||
portfolioName: 'Wedding Website'
|
||||
portfolioSubHeading: 'An application that allows viewing of wedding details and provides the ability to RSVP to the wedding.'
|
||||
portfolioImage: 'Wedding_Website.png'
|
||||
portfolioImageAlt: 'Wedding Website'
|
||||
---
|
||||
|
||||
<script>
|
||||
import ExternalLink from '$lib/components/ExternalLink.svelte';
|
||||
</script>
|
||||
|
||||
{portfolioSubHeading}
|
||||
|
||||
The app was initially created for my wedding but what is linked here is a public demo of the application.
|
||||
|
||||
Tech stack:
|
||||
|
||||
- Next.js 13
|
||||
- React 18
|
||||
- <ExternalLink href="https://radix-ui.com" ariaLabel="Radix UI" showIcon>Radix UI</ExternalLink>
|
||||
- MongoDB
|
||||
- Styled Components
|
||||
- Next Iron Session
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
---
|
||||
portfolioName: 'Mark Shellnut Architect'
|
||||
portfolioSubHeading: 'Company website for Mark Shellnut Architect.'
|
||||
portfolioImage: 'Mark_Shellnut_Architect.png'
|
||||
portfolioImageAlt: "Picture of Mark Shellnut Architect's Website"
|
||||
---
|
||||
|
||||
<script>
|
||||
import ExternalLink from '$lib/components/ExternalLink.svelte';
|
||||
</script>
|
||||
|
||||
{portfolioSubHeading}
|
||||
|
||||
Tech stack:
|
||||
|
||||
- React 18
|
||||
- Gatsby 5
|
||||
- <ExternalLink href="https://radix-ui.com" ariaLabel="Radix UI" showIcon>Radix UI</ExternalLink>
|
||||
- Styled Components
|
||||
- GraphQL
|
||||
- Lambda Functions
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
<script>
|
||||
import ExternalLink from '$lib/components/ExternalLink.svelte';
|
||||
</script>
|
||||
|
||||
## Development
|
||||
|
||||
My development setup has been documented here: <ExternalLink ariaLabel="Bradley Shellnut Computer Setup" href="https://github.com/BradNut/computer-setup-info/blob/master/linux/Environment-Setup.md" showIcon>Computer Setup Info</ExternalLink>.
|
||||
|
||||
And documentation for my terminal and coding setup can be found here: <ExternalLink ariaLabel="Bradley Shellnut Dotfiles" href="https://github.com/BradNut/dotfiles" showIcon>Dotfiles</ExternalLink>.
|
||||
|
||||
### Terminal & Shell Setup
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
Linux default terminal running ZSH and <ExternalLink ariaLabel="Starship Prompt" href="https://starship.rs" showIcon>Starship</ExternalLink>.
|
||||
</li>
|
||||
<li>
|
||||
Mac: <ExternalLink ariaLabel="iTerm 2 Terminal" href="https://iterm2.com/" showIcon>iTerm2</ExternalLink>
|
||||
</li>
|
||||
<li>
|
||||
<ExternalLink ariaLable="Oh My Zsh" href="https://github.com/robbyrussell/oh-my-zsh" showIcon>Oh My Zsh</ExternalLink>
|
||||
<ul>
|
||||
<li>
|
||||
<ExternalLink ariaLael="Z plugin" href="https://github.com/agkozak/zsh-z" showIcon>Z</ExternalLink>
|
||||
</li>
|
||||
<li>
|
||||
<ExternalLink ariaLabel="Autosuggestion plugin" href="https://github.com/zsh-users/zsh-autosuggestions" showIcon>zsh-autosuggestions</ExternalLink>
|
||||
</li>
|
||||
<li>
|
||||
<ExternalLink ariaLabel="Syntax highlighting plugin" href="https://github.com/zsh-users/zsh-syntax-highlighting" showIcon>zsh-syntax-highlighting</ExternalLink>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
### Useful System Packages
|
||||
|
||||
- <ExternalLink ariaLabel="Linux Brew" href="https://docs.brew.sh/Homebrew-on-Linux" showIcon>Linux Brew</ExternalLink>
|
||||
- <ExternalLink ariaLabel="Homebrew Link" href="https://brew.sh/" showIcon>Homebrew</ExternalLink>
|
||||
- <ExternalLink ariaLabel="TLDR Man Pages" href="https://tldr.sh/" showIcon>TLDR Man Pages</ExternalLink>
|
||||
- <ExternalLink ariaLabel="Trash-CLI" href="https://www.npmjs.com/package/trash-cli" showIcon>Trash-CLI</ExternalLink>
|
||||
|
||||
### Software
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<ExternalLink ariaLabel="VS Codium Website" href="https://vscodium.com/" showIcon>VSCodium</ExternalLink>
|
||||
<ul>
|
||||
<li>
|
||||
My extensions list: <ExternalLink ariaLabel="VS Code Extensions" href="https://github.com/BradNut/dotfiles/blob/master/vs-code-extensions-i-use.md" showIcon>VSCode Extensions List</ExternalLink>
|
||||
</li>
|
||||
</ul>
|
||||
<li>
|
||||
<ExternalLink ariaLabel="Sublime Text 3 Website" href="https://www.sublimetext.com/3"
|
||||
showIcon>Sublime Text 3</ExternalLink>
|
||||
<ul>
|
||||
<li>My Packages List: <ExternalLink ariaLabel="VS Code Extensions" href="https://github.com/BradNut/dotfiles/blob/master/sublime-text-extensions-i-use.md" showIcon>Sublime Text Packages List</ExternalLink>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<ExternalLink ariaLabel="IntelliJ IDEA" href="https://jetbrains.com/idea/" showIcon>IntelliJ Ultimate</ExternalLink>
|
||||
<ul>
|
||||
<li>My Plugins List: <ExternalLink ariaLabel="IntelliJ Plugins" href="https://github.com/BradNut/dotfiles/blob/master/intellij-plugins.md" showIcon>IntelliJ Plugins</ExternalLink>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
### Useful Applications
|
||||
|
||||
- <ExternalLink ariaLabel="Postman Website" href="https://postman.com/" showIcon>Postman</ExternalLink>
|
||||
|
||||
### Browsers
|
||||
|
||||
- <ExternalLink ariaLabel="Brave Browser" href="https://brave.com/" showIcon>Brave Browser</ExternalLink>
|
||||
- <ExternalLink ariaLabel="Firefox" href="https://www.mozilla.org/en-US/firefox/new/" showIcon>Firefox</ExternalLink>
|
||||
|
||||
<style lang="postcss">
|
||||
ul {
|
||||
list-style-type: square;
|
||||
padding-inline-start: 4rem;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
## Hardware & Accessories
|
||||
|
||||
- MacBook Pro 15-inch for work.
|
||||
- Personal desktop running [PopOS](https://pop.system76.com/ 'PopOS Linux Distro').
|
||||
- Dell XPS 13 running PopOS.
|
||||
- Phone 📱: Pixel 6 running [GrapheneOS](https://grapheneos.org/).
|
||||
- Keyboard ⌨️: [Ducky Keyboard](https://www.duckychannel.com.tw/en/Ducky-One2-RGB-TKL 'Ducky One 2 RGB TKL Keyboard')
|
||||
- Mouse 🖱️: [Logitech G502 Gaming Mouse](https://www.amazon.com/Logitech-G502-Performance-Gaming-Mouse/dp/B07GBZ4Q68 'Logitech G502 Gaming Mouse')
|
||||
- Chair 🪑: [SecretLab Omega 2020 Fabric](https://secretlabus.myshopify.com/collections/omega-series#omega_2020-stealth 'Secretlab Omega 2020 Fabric')
|
||||
- Monitor 🖥️: [Samsung Odyssey G7 Gaming Monitor](https://www.samsung.com/us/computing/monitors/gaming/32--odyssey-g7-gaming-monitor-lc32g75tqsnxza/ 'Samsung Odyssey G7 Gaming Monitor')
|
||||
|
||||
<style lang="postcss">
|
||||
ul {
|
||||
list-style-type: square;
|
||||
padding-inline-start: 4rem;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
<script>
|
||||
import ExternalLink from '$lib/components/ExternalLink.svelte';
|
||||
</script>
|
||||
|
||||
## Privacy Hardware and Software
|
||||
|
||||
For the past few years I've been working towards moving my data to self-hosted systems and systems I trust to hold my data.
|
||||
|
||||
If you want to see more info about this head to my <a href="/privacy">Privacy</a> page.
|
||||
|
||||
### Hardware Authentication
|
||||
|
||||
- <ExternalLink ariaLabel="YubiKey 5C" href="https://www.yubico.com/product/yubikey-5c/" showIcon>YubiKey 5C</ExternalLink>
|
||||
|
||||
### NAS Servers
|
||||
|
||||
- <ExternalLink ariaLabel="Synology NAS" href="https://synology.com/" showIcon>Synology DS918+</ExternalLink>
|
||||
|
||||
<style lang="postcss">
|
||||
ul {
|
||||
list-style-type: square;
|
||||
padding-inline-start: 4rem;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -10,6 +10,7 @@ import type { SvelteComponent } from 'svelte';
|
|||
|
||||
const fontData = read(firaSansSemiBold).arrayBuffer();
|
||||
|
||||
|
||||
export async function componentToPng(component: SvelteComponent,
|
||||
props: Record<string, string | undefined>,
|
||||
height: number, width: number) {
|
||||
|
|
|
|||
10
src/lib/types/externalLinkType.ts
Normal file
10
src/lib/types/externalLinkType.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import type { Icon as IconType } from 'lucide-svelte';
|
||||
import type { Snippet } from "svelte";
|
||||
|
||||
export type ExternalLinkType = {
|
||||
ariaLabel: string;
|
||||
href: string;
|
||||
icon?: Snippet | typeof IconType;
|
||||
showIcon: boolean;
|
||||
text: string;
|
||||
};
|
||||
30
src/lib/types/externalLinkTypes.ts
Normal file
30
src/lib/types/externalLinkTypes.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import type { Snippet } from 'svelte';
|
||||
import type { Icon as IconType } from 'lucide-svelte';
|
||||
|
||||
export interface LinkTextType {
|
||||
text?: string;
|
||||
showIcon: boolean;
|
||||
location?: 'top' | 'bottom' | 'left' | 'right';
|
||||
}
|
||||
|
||||
export interface LinkDataType {
|
||||
rel?: string;
|
||||
target?: string;
|
||||
href: string;
|
||||
title?: string;
|
||||
ariaLabel: string;
|
||||
clazz?: string | undefined;
|
||||
textDecoration?: 'none' | 'underline' | 'line-through';
|
||||
}
|
||||
|
||||
export interface ExternalLinkType {
|
||||
iconData?: LinkIconType;
|
||||
linkData: LinkDataType;
|
||||
textData?: LinkTextType;
|
||||
}
|
||||
|
||||
export interface LinkIconType {
|
||||
type?: 'icon' | 'svg';
|
||||
icon?: Snippet | typeof IconType;
|
||||
iconClass?: string | undefined;
|
||||
}
|
||||
|
|
@ -14,7 +14,7 @@ export async function fetchBandcampAlbums() {
|
|||
console.log(`Cache hit!`);
|
||||
const ttl = await redis.ttl('bandcampAlbums');
|
||||
|
||||
return response;
|
||||
return { ...response, cacheControl: `max-age=${ttl}` };
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
53
src/lib/util/logoIcons.svelte
Normal file
53
src/lib/util/logoIcons.svelte
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
<script module>
|
||||
import { ExternalLink } from 'lucide-svelte';
|
||||
export { blueSkyIcon, dockerIcon, drizzleIcon, honoIcon, gitHubIcon, linkedInIcon, lucideIcon, nextDotJsIcon, reactIcon, svelteIcon, typescriptIcon, xIcon };
|
||||
</script>
|
||||
|
||||
{#snippet gitHubIcon()}
|
||||
<path fill="currentColor" d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/>
|
||||
{/snippet}
|
||||
|
||||
{#snippet drizzleIcon()}
|
||||
<path fill="currentColor" d="M5.353 11.823a1.036 1.036 0 0 0-.395-1.422 1.063 1.063 0 0 0-1.437.399L.138 16.702a1.035 1.035 0 0 0 .395 1.422 1.063 1.063 0 0 0 1.437-.398l3.383-5.903Zm11.216 0a1.036 1.036 0 0 0-.394-1.422 1.064 1.064 0 0 0-1.438.399l-3.382 5.902a1.036 1.036 0 0 0 .394 1.422c.506.283 1.15.104 1.438-.398l3.382-5.903Zm7.293-4.525a1.036 1.036 0 0 0-.395-1.422 1.062 1.062 0 0 0-1.437.399l-3.383 5.902a1.036 1.036 0 0 0 .395 1.422 1.063 1.063 0 0 0 1.437-.399l3.383-5.902Zm-11.219 0a1.035 1.035 0 0 0-.394-1.422 1.064 1.064 0 0 0-1.438.398l-3.382 5.903a1.036 1.036 0 0 0 .394 1.422c.506.282 1.15.104 1.438-.399l3.382-5.902Z"/>
|
||||
{/snippet}
|
||||
|
||||
{#snippet svelteIcon()}
|
||||
<path fill="currentColor" d="M10.354 21.125a4.44 4.44 0 0 1-4.765-1.767 4.109 4.109 0 0 1-.703-3.107 3.898 3.898 0 0 1 .134-.522l.105-.321.287.21a7.21 7.21 0 0 0 2.186 1.092l.208.063-.02.208a1.253 1.253 0 0 0 .226.83 1.337 1.337 0 0 0 1.435.533 1.231 1.231 0 0 0 .343-.15l5.59-3.562a1.164 1.164 0 0 0 .524-.778 1.242 1.242 0 0 0-.211-.937 1.338 1.338 0 0 0-1.435-.533 1.23 1.23 0 0 0-.343.15l-2.133 1.36a4.078 4.078 0 0 1-1.135.499 4.44 4.44 0 0 1-4.765-1.766 4.108 4.108 0 0 1-.702-3.108 3.855 3.855 0 0 1 1.742-2.582l5.589-3.563a4.072 4.072 0 0 1 1.135-.499 4.44 4.44 0 0 1 4.765 1.767 4.109 4.109 0 0 1 .703 3.107 3.943 3.943 0 0 1-.134.522l-.105.321-.286-.21a7.204 7.204 0 0 0-2.187-1.093l-.208-.063.02-.207a1.255 1.255 0 0 0-.226-.831 1.337 1.337 0 0 0-1.435-.532 1.231 1.231 0 0 0-.343.15L8.62 9.368a1.162 1.162 0 0 0-.524.778 1.24 1.24 0 0 0 .211.937 1.338 1.338 0 0 0 1.435.533 1.235 1.235 0 0 0 .344-.151l2.132-1.36a4.067 4.067 0 0 1 1.135-.498 4.44 4.44 0 0 1 4.765 1.766 4.108 4.108 0 0 1 .702 3.108 3.857 3.857 0 0 1-1.742 2.583l-5.589 3.562a4.072 4.072 0 0 1-1.135.499m10.358-17.95C18.484-.015 14.082-.96 10.9 1.068L5.31 4.63a6.412 6.412 0 0 0-2.896 4.295 6.753 6.753 0 0 0 .666 4.336 6.43 6.43 0 0 0-.96 2.396 6.833 6.833 0 0 0 1.168 5.167c2.229 3.19 6.63 4.135 9.812 2.108l5.59-3.562a6.41 6.41 0 0 0 2.896-4.295 6.756 6.756 0 0 0-.665-4.336 6.429 6.429 0 0 0 .958-2.396 6.831 6.831 0 0 0-1.167-5.168Z"/>
|
||||
{/snippet}
|
||||
|
||||
{#snippet typescriptIcon()}
|
||||
<path fill="currentColor" d="M1.125 0C.502 0 0 .502 0 1.125v21.75C0 23.498.502 24 1.125 24h21.75c.623 0 1.125-.502 1.125-1.125V1.125C24 .502 23.498 0 22.875 0zm17.363 9.75c.612 0 1.154.037 1.627.111a6.38 6.38 0 0 1 1.306.34v2.458a3.95 3.95 0 0 0-.643-.361 5.093 5.093 0 0 0-.717-.26 5.453 5.453 0 0 0-1.426-.2c-.3 0-.573.028-.819.086a2.1 2.1 0 0 0-.623.242c-.17.104-.3.229-.393.374a.888.888 0 0 0-.14.49c0 .196.053.373.156.529.104.156.252.304.443.444s.423.276.696.41c.273.135.582.274.926.416.47.197.892.407 1.266.628.374.222.695.473.963.753.268.279.472.598.614.957.142.359.214.776.214 1.253 0 .657-.125 1.21-.373 1.656a3.033 3.033 0 0 1-1.012 1.085 4.38 4.38 0 0 1-1.487.596c-.566.12-1.163.18-1.79.18a9.916 9.916 0 0 1-1.84-.164 5.544 5.544 0 0 1-1.512-.493v-2.63a5.033 5.033 0 0 0 3.237 1.2c.333 0 .624-.03.872-.09.249-.06.456-.144.623-.25.166-.108.29-.234.373-.38a1.023 1.023 0 0 0-.074-1.089 2.12 2.12 0 0 0-.537-.5 5.597 5.597 0 0 0-.807-.444 27.72 27.72 0 0 0-1.007-.436c-.918-.383-1.602-.852-2.053-1.405-.45-.553-.676-1.222-.676-2.005 0-.614.123-1.141.369-1.582.246-.441.58-.804 1.004-1.089a4.494 4.494 0 0 1 1.47-.629 7.536 7.536 0 0 1 1.77-.201zm-15.113.188h9.563v2.166H9.506v9.646H6.789v-9.646H3.375z"/>
|
||||
{/snippet}
|
||||
|
||||
{#snippet reactIcon()}
|
||||
<path fill="currentColor" d="M14.23 12.004a2.236 2.236 0 0 1-2.235 2.236 2.236 2.236 0 0 1-2.236-2.236 2.236 2.236 0 0 1 2.235-2.236 2.236 2.236 0 0 1 2.236 2.236zm2.648-10.69c-1.346 0-3.107.96-4.888 2.622-1.78-1.653-3.542-2.602-4.887-2.602-.41 0-.783.093-1.106.278-1.375.793-1.683 3.264-.973 6.365C1.98 8.917 0 10.42 0 12.004c0 1.59 1.99 3.097 5.043 4.03-.704 3.113-.39 5.588.988 6.38.32.187.69.275 1.102.275 1.345 0 3.107-.96 4.888-2.624 1.78 1.654 3.542 2.603 4.887 2.603.41 0 .783-.09 1.106-.275 1.374-.792 1.683-3.263.973-6.365C22.02 15.096 24 13.59 24 12.004c0-1.59-1.99-3.097-5.043-4.032.704-3.11.39-5.587-.988-6.38-.318-.184-.688-.277-1.092-.278zm-.005 1.09v.006c.225 0 .406.044.558.127.666.382.955 1.835.73 3.704-.054.46-.142.945-.25 1.44-.96-.236-2.006-.417-3.107-.534-.66-.905-1.345-1.727-2.035-2.447 1.592-1.48 3.087-2.292 4.105-2.295zm-9.77.02c1.012 0 2.514.808 4.11 2.28-.686.72-1.37 1.537-2.02 2.442-1.107.117-2.154.298-3.113.538-.112-.49-.195-.964-.254-1.42-.23-1.868.054-3.32.714-3.707.19-.09.4-.127.563-.132zm4.882 3.05c.455.468.91.992 1.36 1.564-.44-.02-.89-.034-1.345-.034-.46 0-.915.01-1.36.034.44-.572.895-1.096 1.345-1.565zM12 8.1c.74 0 1.477.034 2.202.093.406.582.802 1.203 1.183 1.86.372.64.71 1.29 1.018 1.946-.308.655-.646 1.31-1.013 1.95-.38.66-.773 1.288-1.18 1.87-.728.063-1.466.098-2.21.098-.74 0-1.477-.035-2.202-.093-.406-.582-.802-1.204-1.183-1.86-.372-.64-.71-1.29-1.018-1.946.303-.657.646-1.313 1.013-1.954.38-.66.773-1.286 1.18-1.868.728-.064 1.466-.098 2.21-.098zm-3.635.254c-.24.377-.48.763-.704 1.16-.225.39-.435.782-.635 1.174-.265-.656-.49-1.31-.676-1.947.64-.15 1.315-.283 2.015-.386zm7.26 0c.695.103 1.365.23 2.006.387-.18.632-.405 1.282-.66 1.933-.2-.39-.41-.783-.64-1.174-.225-.392-.465-.774-.705-1.146zm3.063.675c.484.15.944.317 1.375.498 1.732.74 2.852 1.708 2.852 2.476-.005.768-1.125 1.74-2.857 2.475-.42.18-.88.342-1.355.493-.28-.958-.646-1.956-1.1-2.98.45-1.017.81-2.01 1.085-2.964zm-13.395.004c.278.96.645 1.957 1.1 2.98-.45 1.017-.812 2.01-1.086 2.964-.484-.15-.944-.318-1.37-.5-1.732-.737-2.852-1.706-2.852-2.474 0-.768 1.12-1.742 2.852-2.476.42-.18.88-.342 1.356-.494zm11.678 4.28c.265.657.49 1.312.676 1.948-.64.157-1.316.29-2.016.39.24-.375.48-.762.705-1.158.225-.39.435-.788.636-1.18zm-9.945.02c.2.392.41.783.64 1.175.23.39.465.772.705 1.143-.695-.102-1.365-.23-2.006-.386.18-.63.406-1.282.66-1.933zM17.92 16.32c.112.493.2.968.254 1.423.23 1.868-.054 3.32-.714 3.708-.147.09-.338.128-.563.128-1.012 0-2.514-.807-4.11-2.28.686-.72 1.37-1.536 2.02-2.44 1.107-.118 2.154-.3 3.113-.54zm-11.83.01c.96.234 2.006.415 3.107.532.66.905 1.345 1.727 2.035 2.446-1.595 1.483-3.092 2.295-4.11 2.295-.22-.005-.406-.05-.553-.132-.666-.38-.955-1.834-.73-3.703.054-.46.142-.944.25-1.438zm4.56.64c.44.02.89.034 1.345.034.46 0 .915-.01 1.36-.034-.44.572-.895 1.095-1.345 1.565-.455-.47-.91-.993-1.36-1.565z"/>
|
||||
{/snippet}
|
||||
|
||||
{#snippet dockerIcon()}
|
||||
<path fill="currentColor" d="M13.983 11.078h2.119a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.119a.185.185 0 00-.185.185v1.888c0 .102.083.185.185.185m-2.954-5.43h2.118a.186.186 0 00.186-.186V3.574a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m0 2.716h2.118a.187.187 0 00.186-.186V6.29a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.887c0 .102.082.185.185.186m-2.93 0h2.12a.186.186 0 00.184-.186V6.29a.185.185 0 00-.185-.185H8.1a.185.185 0 00-.185.185v1.887c0 .102.083.185.185.186m-2.964 0h2.119a.186.186 0 00.185-.186V6.29a.185.185 0 00-.185-.185H5.136a.186.186 0 00-.186.185v1.887c0 .102.084.185.186.186m5.893 2.715h2.118a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m-2.93 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.083.185.185.185m-2.964 0h2.119a.185.185 0 00.185-.185V9.006a.185.185 0 00-.184-.186h-2.12a.186.186 0 00-.186.186v1.887c0 .102.084.185.186.185m-2.92 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.082.185.185.185M23.763 9.89c-.065-.051-.672-.51-1.954-.51-.338.001-.676.03-1.01.087-.248-1.7-1.653-2.53-1.716-2.566l-.344-.199-.226.327c-.284.438-.49.922-.612 1.43-.23.97-.09 1.882.403 2.661-.595.332-1.55.413-1.744.42H.751a.751.751 0 00-.75.748 11.376 11.376 0 00.692 4.062c.545 1.428 1.355 2.48 2.41 3.124 1.18.723 3.1 1.137 5.275 1.137.983.003 1.963-.086 2.93-.266a12.248 12.248 0 003.823-1.389c.98-.567 1.86-1.288 2.61-2.136 1.252-1.418 1.998-2.997 2.553-4.4h.221c1.372 0 2.215-.549 2.68-1.009.309-.293.55-.65.707-1.046l.098-.288Z"/>
|
||||
{/snippet}
|
||||
|
||||
{#snippet honoIcon()}
|
||||
<path fill="currentColor" d="M12.445.002a45.529 45.529 0 0 0-5.252 8.146 8.595 8.595 0 0 1-.555-.53 27.796 27.796 0 0 0-1.205-1.542 8.762 8.762 0 0 0-1.251 2.12 20.743 20.743 0 0 0-1.448 5.88 8.867 8.867 0 0 0 .338 3.468c1.312 3.48 3.794 5.593 7.445 6.337 3.055.438 5.755-.333 8.097-2.312 2.677-2.59 3.359-5.634 2.047-9.132a33.287 33.287 0 0 0-2.988-5.59A91.34 91.34 0 0 0 12.615.053a.216.216 0 0 0-.17-.051Zm-.336 3.906a50.93 50.93 0 0 1 4.794 6.552c.448.767.817 1.57 1.108 2.41.606 2.386-.044 4.354-1.951 5.904-1.845 1.298-3.87 1.683-6.072 1.156-2.376-.737-3.75-2.335-4.121-4.794a5.107 5.107 0 0 1 .242-2.266c.358-.908.79-1.774 1.3-2.601l1.446-2.121a397.33 397.33 0 0 0 3.254-4.24Z"/>
|
||||
{/snippet}
|
||||
|
||||
{#snippet linkedInIcon()}
|
||||
<path fill="currentColor" d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/>
|
||||
{/snippet}
|
||||
|
||||
{#snippet xIcon()}
|
||||
<path fill="currentColor" d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z"/>
|
||||
{/snippet}
|
||||
|
||||
{#snippet blueSkyIcon()}
|
||||
<path fill="currentColor" d="M12 10.8c-1.087-2.114-4.046-6.053-6.798-7.995C2.566.944 1.561 1.266.902 1.565.139 1.908 0 3.08 0 3.768c0 .69.378 5.65.624 6.479.815 2.736 3.713 3.66 6.383 3.364.136-.02.275-.039.415-.056-.138.022-.276.04-.415.056-3.912.58-7.387 2.005-2.83 7.078 5.013 5.19 6.87-1.113 7.823-4.308.953 3.195 2.05 9.271 7.733 4.308 4.267-4.308 1.172-6.498-2.74-7.078a8.741 8.741 0 0 1-.415-.056c.14.017.279.036.415.056 2.67.297 5.568-.628 6.383-3.364.246-.828.624-5.79.624-6.478 0-.69-.139-1.861-.902-2.206-.659-.298-1.664-.62-4.3 1.24C16.046 4.748 13.087 8.687 12 10.8Z"/>
|
||||
{/snippet}
|
||||
|
||||
{#snippet nextDotJsIcon()}
|
||||
<path fill="currentColor" d="M18.665 21.978C16.758 23.255 14.465 24 12 24 5.377 24 0 18.623 0 12S5.377 0 12 0s12 5.377 12 12c0 3.583-1.574 6.801-4.067 9.001L9.219 7.2H7.2v9.596h1.615V9.251l9.85 12.727Zm-3.332-8.533 1.6 2.061V7.2h-1.6v6.245Z"/>
|
||||
{/snippet}
|
||||
|
||||
{#snippet lucideIcon(icon)}
|
||||
{@const Icon = icon}
|
||||
<Icon />
|
||||
{/snippet}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { MetaTags } from 'svelte-meta-tags';
|
||||
import NProgress from "nprogress";
|
||||
import 'iconify-icon';
|
||||
import { browser } from "$app/environment";
|
||||
import { navigating, page } from "$app/stores";
|
||||
import { PUBLIC_SITE_URL } from '$env/static/public';
|
||||
|
|
@ -10,6 +9,11 @@
|
|||
import Header from '$lib/components/header/index.svelte';
|
||||
import Footer from '$lib/components/footer/index.svelte';
|
||||
import Analytics from '$lib/components/analytics/index.svelte';
|
||||
interface Props {
|
||||
children?: import('svelte').Snippet;
|
||||
}
|
||||
|
||||
let { children }: Props = $props();
|
||||
|
||||
NProgress.configure({
|
||||
// Full list: https://github.com/rstacruz/nprogress#configuration
|
||||
|
|
@ -17,17 +21,16 @@
|
|||
});
|
||||
|
||||
const dev = process.env.NODE_ENV !== 'production';
|
||||
const siteUrl = PUBLIC_SITE_URL || 'https://bradleyshellnut.com/';
|
||||
|
||||
$: {
|
||||
$effect(() => {
|
||||
if (browser && $navigating) {
|
||||
NProgress.start();
|
||||
} else {
|
||||
NProgress.done();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$: metaTags = {
|
||||
let metaTags = $derived({
|
||||
titleTemplate: '%s | Bradley Shellnut',
|
||||
additionalMetaTags: [
|
||||
{
|
||||
|
|
@ -36,7 +39,7 @@
|
|||
}
|
||||
],
|
||||
...$page.data.metaTagsChild
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if !dev}
|
||||
|
|
@ -48,7 +51,7 @@
|
|||
<div class="wrapper">
|
||||
<Header />
|
||||
<main>
|
||||
<slot />
|
||||
{@render children?.()}
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,60 +1,60 @@
|
|||
import type { MetaTagsProps } from 'svelte-meta-tags';
|
||||
import { PUBLIC_SITE_URL } from '$env/static/public';
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { fetchBandcampAlbums } from '$lib/util/fetchBandcampAlbums';
|
||||
import type { Album } from '$lib/types/album';
|
||||
import type { ArticlePageLoad } from '$lib/types/article';
|
||||
import { fetchBandcampAlbums } from '$lib/util/fetchBandcampAlbums';
|
||||
import type { MetaTagsProps } from 'svelte-meta-tags';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async ({ fetch, setHeaders, url }) => {
|
||||
let baseUrl;
|
||||
if (url.origin.includes('prerender')) {
|
||||
baseUrl = PUBLIC_SITE_URL || 'https://bradleyshellnut.com';
|
||||
} else {
|
||||
baseUrl = new URL(url.origin).href || PUBLIC_SITE_URL || 'https://bradleyshellnut.com';
|
||||
}
|
||||
const currentPageUrl = new URL(url.pathname, url.origin).href;
|
||||
let baseUrl;
|
||||
if (url.origin.includes('prerender')) {
|
||||
baseUrl = PUBLIC_SITE_URL || 'https://bradleyshellnut.com';
|
||||
} else {
|
||||
baseUrl = new URL(url.origin).href || PUBLIC_SITE_URL || 'https://bradleyshellnut.com';
|
||||
}
|
||||
const currentPageUrl = new URL(url.pathname, url.origin).href;
|
||||
|
||||
const metaTags: MetaTagsProps = Object.freeze({
|
||||
title: 'Home',
|
||||
description: "My name is Bradley Shellnut and I'm a Full Stack Software Engineer.",
|
||||
openGraph: {
|
||||
title: 'Home',
|
||||
description: "My name is Bradley Shellnut and I'm a Full Stack Software Engineer.",
|
||||
url: currentPageUrl,
|
||||
siteName: 'Bradley Shellnut Personal Website',
|
||||
type: 'website',
|
||||
locale: 'en_US',
|
||||
images: [
|
||||
{
|
||||
url: `${baseUrl}og?header=Home | bradleyshellnut.com&page=Hi I'm Bradley Shellnut.&content=I'm a full stack software engineer currently working on Java Spring, PostgreSQL, and React / Angular JS.`,
|
||||
alt: 'Bradley Shellnut Website Home Page',
|
||||
width: 1200,
|
||||
height: 630
|
||||
}
|
||||
]
|
||||
},
|
||||
twitter: {
|
||||
title: 'Home',
|
||||
description: 'Home page',
|
||||
card: 'summary_large_image',
|
||||
image: `${baseUrl}og?header=Home | bradleyshellnut.com&page=Hi I'm Bradley Shellnut.&content=I'm a full stack software engineer currently working on Java Spring, PostgreSQL, and React / Angular JS.`,
|
||||
imageAlt: 'Bradley Shellnut Website Logo'
|
||||
},
|
||||
url: currentPageUrl
|
||||
});
|
||||
const metaTags: MetaTagsProps = Object.freeze({
|
||||
title: 'Home',
|
||||
description: "My name is Bradley Shellnut and I'm a Full Stack Software Engineer.",
|
||||
openGraph: {
|
||||
title: 'Home',
|
||||
description: "My name is Bradley Shellnut and I'm a Full Stack Software Engineer.",
|
||||
url: currentPageUrl,
|
||||
siteName: 'Bradley Shellnut Personal Website',
|
||||
type: 'website',
|
||||
locale: 'en_US',
|
||||
images: [
|
||||
{
|
||||
url: `${baseUrl}og?header=Home | bradleyshellnut.com&page=Hi I'm Bradley Shellnut.&content=I'm a full stack software engineer currently working on Java Spring, PostgreSQL, and React / Angular JS.`,
|
||||
alt: 'Bradley Shellnut Website Home Page',
|
||||
width: 1200,
|
||||
height: 630,
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
title: 'Home',
|
||||
description: 'Home page',
|
||||
card: 'summary_large_image',
|
||||
image: `${baseUrl}og?header=Home | bradleyshellnut.com&page=Hi I'm Bradley Shellnut.&content=I'm a full stack software engineer currently working on Java Spring, PostgreSQL, and React / Angular JS.`,
|
||||
imageAlt: 'Bradley Shellnut Website Logo',
|
||||
},
|
||||
url: currentPageUrl,
|
||||
});
|
||||
|
||||
const [albums, articles]: [Album[], ArticlePageLoad] = await Promise.all([
|
||||
await fetchBandcampAlbums(),
|
||||
(await fetch(`/api/articles?page=1&limit=3`)).json()
|
||||
]);
|
||||
const [albums, articles]: [Album[], ArticlePageLoad] = await Promise.all([
|
||||
(await fetch('/api/bandcamp/albums')).json(),
|
||||
(await fetch('/api/articles?page=1&limit=3')).json(),
|
||||
]);
|
||||
|
||||
setHeaders({
|
||||
'cache-control': 'max-age=43200'
|
||||
});
|
||||
return {
|
||||
baseUrl,
|
||||
metaTagsChild: metaTags,
|
||||
albums,
|
||||
articlesData: articles
|
||||
};
|
||||
};
|
||||
setHeaders({
|
||||
'cache-control': 'max-age=43200',
|
||||
});
|
||||
return {
|
||||
baseUrl,
|
||||
metaTagsChild: metaTags,
|
||||
albums,
|
||||
articlesData: articles,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,31 +1,21 @@
|
|||
<script lang="ts">
|
||||
import type { PageData } from './$types';
|
||||
import Bandcamp from '$lib/components/Bandcamp.svelte';
|
||||
import Articles from '$lib/components/Articles.svelte';
|
||||
import type { Album } from '$lib/types/album';
|
||||
import type { Article, ArticlePageLoad } from '$lib/types/article';
|
||||
import Articles from '$lib/components/Articles.svelte';
|
||||
import Bandcamp from '$lib/components/Bandcamp.svelte';
|
||||
import type { Album } from '$lib/types/album';
|
||||
import type { Article, ArticlePageLoad } from '$lib/types/article';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
let albums: Album[];
|
||||
let articlesData: ArticlePageLoad;
|
||||
let articles: Article[];
|
||||
let totalArticles: number;
|
||||
const { data } = $props();
|
||||
let albums: Album[] = $derived(data.albums);
|
||||
let articlesData: ArticlePageLoad = $derived(data.articlesData);
|
||||
let articles: Article[] = $derived(articlesData.articles);
|
||||
let totalArticles: number = $derived(articlesData.totalArticles);
|
||||
|
||||
$: if (data) {
|
||||
albums = data.albums;
|
||||
articlesData = data.articlesData;
|
||||
}
|
||||
|
||||
$: if (articlesData) {
|
||||
articles = articlesData.articles;
|
||||
totalArticles = articlesData.totalArticles;
|
||||
}
|
||||
|
||||
const userNames = {
|
||||
github: 'BradNut',
|
||||
linkedIn: 'bradley-shellnut',
|
||||
email: 'bradleyshellnut@pm.me',
|
||||
};
|
||||
const userNames = {
|
||||
github: 'BradNut',
|
||||
linkedIn: 'bradley-shellnut',
|
||||
email: 'bradleyshellnut@pm.me',
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="home">
|
||||
|
|
|
|||
|
|
@ -1,22 +1,14 @@
|
|||
<script lang="ts">
|
||||
import Graphql from '@iconify-icons/simple-icons/graphql';
|
||||
import Nextdotjs from '@iconify-icons/simple-icons/nextdotjs';
|
||||
import Prisma from '@iconify-icons/simple-icons/prisma';
|
||||
import Gatsby from '@iconify-icons/simple-icons/gatsby';
|
||||
import Docker from '@iconify-icons/simple-icons/docker';
|
||||
import React from '@iconify-icons/simple-icons/react';
|
||||
import Remix from '@iconify-icons/simple-icons/remix';
|
||||
import Svelte from '@iconify-icons/simple-icons/svelte';
|
||||
import TypeScript from '@iconify-icons/simple-icons/typescript';
|
||||
import LazyImage from '$lib/components/LazyImage.svelte';
|
||||
import cruise from '$lib/assets/images/cruise.png?as=run:0';
|
||||
import tortie_derp from '$lib/assets/images/tortie_derp.jpg?as=run';
|
||||
import orange_derp from '$lib/assets/images/orange_derp.jpg?as=run';
|
||||
import cruise from '$lib/assets/images/cruise.png?enhanced';
|
||||
import orange_derp from '$lib/assets/images/orange_derp.jpg?enhanced';
|
||||
import tortie_derp from '$lib/assets/images/tortie_derp.jpg?enhanced';
|
||||
import turnip from '$lib/assets/images/turnip.svg';
|
||||
import type { Course } from '$lib/types/courses';
|
||||
import { dockerIcon, drizzleIcon, nextDotJsIcon, reactIcon, svelteIcon, typescriptIcon } from '$lib/util/logoIcons.svelte';
|
||||
import CourseCard from './CourseCard.svelte';
|
||||
import courseData from './course.json';
|
||||
import type { Course } from '$root/lib/types/courses';
|
||||
import TechListItem from './TechListItem.svelte';
|
||||
import courseData from './course.json';
|
||||
import ExternalLink from '$lib/components/ExternalLink.svelte';
|
||||
|
||||
const courses: Course[] = courseData.courses;
|
||||
</script>
|
||||
|
|
@ -49,68 +41,35 @@
|
|||
as:
|
||||
</p>
|
||||
<div class="tech-list">
|
||||
<TechListItem
|
||||
itemText="React"
|
||||
ariaLabel="React"
|
||||
href="https://reactjs.org/"
|
||||
clazz="center"
|
||||
icon={React}
|
||||
<ExternalLink
|
||||
iconData={{ type: 'svg', icon: svelteIcon, iconClass: 'center' }}
|
||||
linkData={{ href: 'https://svelte.dev', ariaLabel: 'Svelte', title: 'Svelte', target: '_blank', clazz: "tech-list-item", textDecoration: 'none' }}
|
||||
textData={{ text: "Svelte", showIcon: true, location: 'bottom' }}
|
||||
/>
|
||||
<TechListItem
|
||||
itemText="TypeScript"
|
||||
ariaLabel="TypeScript"
|
||||
href="https://www.typescriptlang.org/"
|
||||
clazz="center"
|
||||
icon={TypeScript}
|
||||
<ExternalLink
|
||||
iconData={{ type: 'svg', icon: typescriptIcon, iconClass: 'center' }}
|
||||
linkData={{ href: 'https://www.typescriptlang.org/', ariaLabel: 'TypeScript', title: 'TypeScript', target: '_blank', clazz: "tech-list-item", textDecoration: 'none' }}
|
||||
textData={{ text: "TypeScript", showIcon: true, location: 'bottom' }}
|
||||
/>
|
||||
<TechListItem
|
||||
itemText="Svelte"
|
||||
ariaLabel="Svelte"
|
||||
href="https://svelte.dev"
|
||||
clazz="center"
|
||||
icon={Svelte}
|
||||
<ExternalLink
|
||||
iconData={{ type: 'svg', icon: drizzleIcon, iconClass: 'center' }}
|
||||
linkData={{ href: 'https://orm.drizzle.team/', ariaLabel: 'Drizzle ORM', title: 'Drizzle ORM', target: '_blank', clazz: "tech-list-item", textDecoration: 'none' }}
|
||||
textData={{ text: "Drizzle ORM", showIcon: true, location: 'bottom' }}
|
||||
/>
|
||||
<TechListItem
|
||||
itemText="NextJS"
|
||||
ariaLabel="NextJS"
|
||||
href="https://nextjs.org/"
|
||||
clazz="center"
|
||||
icon={Nextdotjs}
|
||||
<ExternalLink
|
||||
iconData={{ type: 'svg', icon: reactIcon, iconClass: 'center' }}
|
||||
linkData={{ href: 'https://reactjs.org/', ariaLabel: 'React', title: 'React', target: '_blank', clazz: "tech-list-item", textDecoration: 'none' }}
|
||||
textData={{ text: "React", showIcon: true, location: 'bottom' }}
|
||||
/>
|
||||
<TechListItem
|
||||
itemText="Remix"
|
||||
ariaLabel="Remix"
|
||||
href="https://remix.run/"
|
||||
clazz="center"
|
||||
icon={Remix}
|
||||
<ExternalLink
|
||||
iconData={{ type: 'svg', icon: nextDotJsIcon, iconClass: 'center' }}
|
||||
linkData={{ href: 'https://nextjs.org/', ariaLabel: 'Next.js', title: 'Next.js', target: '_blank', clazz: "tech-list-item", textDecoration: 'none' }}
|
||||
textData={{ text: "Next.js", showIcon: true, location: 'bottom' }}
|
||||
/>
|
||||
<TechListItem
|
||||
itemText="GraphQL"
|
||||
ariaLabel="GraphQL"
|
||||
href="https://graphql.org/"
|
||||
clazz="center"
|
||||
icon={Graphql}
|
||||
/>
|
||||
<TechListItem
|
||||
itemText="Prisma"
|
||||
ariaLabel="Prisma"
|
||||
href="https://prisma.io/"
|
||||
clazz="center"
|
||||
icon={Prisma}
|
||||
/>
|
||||
<TechListItem
|
||||
itemText="GatsbyJS"
|
||||
ariaLabel="GatsbyJS"
|
||||
href="https://gatsbyjs.com/"
|
||||
clazz="center"
|
||||
icon={Gatsby}
|
||||
/>
|
||||
<TechListItem
|
||||
itemText="Docker"
|
||||
ariaLabel="Docker"
|
||||
href="https://docker.com/"
|
||||
clazz="center"
|
||||
icon={Docker}
|
||||
<ExternalLink
|
||||
iconData={{ type: 'svg', icon: dockerIcon, iconClass: 'center' }}
|
||||
linkData={{ href: 'https://www.docker.com/', ariaLabel: 'Docker', title: 'Docker', target: '_blank', clazz: "tech-list-item", textDecoration: 'none' }}
|
||||
textData={{ text: "Docker", showIcon: true, location: 'bottom' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -140,7 +99,7 @@
|
|||
justify-content: center;
|
||||
"
|
||||
>
|
||||
<LazyImage src={cruise} alt="Clip art of a cruise ship. Cruise icons created by C-mo Box - Flaticon" />
|
||||
<enhanced:img src={cruise} alt="Clip art of a cruise ship. Cruise icons created by C-mo Box - Flaticon" />
|
||||
<p class="center">Crusin'</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -148,11 +107,11 @@
|
|||
<p>Hanging out with these two cats, Turnip and Taco.</p>
|
||||
<div class="cat-pics">
|
||||
<figure>
|
||||
<LazyImage src={tortie_derp} alt="Turnip Cat" />
|
||||
<enhanced:img src={tortie_derp} alt="Tortie Cat" />
|
||||
<p class="center">Turnip <img class="icon" src={String(turnip)} width="25px" height="25px" alt="Turnip" /></p>
|
||||
</figure>
|
||||
<figure>
|
||||
<LazyImage src={orange_derp} alt="Taco Cat" />
|
||||
<enhanced:img src={orange_derp} alt="Tortie Cat" />
|
||||
<p class="center">Taco 🌮</p>
|
||||
</figure>
|
||||
</div>
|
||||
|
|
@ -185,7 +144,7 @@
|
|||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
place-content: center;
|
||||
gap: 1rem;
|
||||
gap: 2rem;
|
||||
margin-top: 1rem;
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,42 +1,45 @@
|
|||
export const prerender = true;
|
||||
|
||||
import type { MetaTagsProps } from 'svelte-meta-tags';
|
||||
import { PUBLIC_SITE_URL } from '$env/static/public';
|
||||
import type { PageLoad } from './$types';
|
||||
import { PUBLIC_SITE_URL } from "$env/static/public";
|
||||
import type { MetaTagsProps } from "svelte-meta-tags";
|
||||
import type { PageLoad } from "./$types";
|
||||
|
||||
export const load: PageLoad = async ({ url }) => {
|
||||
const baseUrl = new URL(url.origin).href || PUBLIC_SITE_URL || 'https://bradleyshellnut.com';
|
||||
const baseUrl =
|
||||
new URL(url.origin).href ||
|
||||
PUBLIC_SITE_URL ||
|
||||
"https://bradleyshellnut.com";
|
||||
const currentPageUrl = new URL(url.pathname, url.origin).href;
|
||||
const metaTags: MetaTagsProps = Object.freeze({
|
||||
title: 'About',
|
||||
description: 'About Bradley Shellnut',
|
||||
title: "About",
|
||||
description: "About Bradley Shellnut",
|
||||
openGraph: {
|
||||
title: 'About',
|
||||
description: 'About Bradley Shellnut',
|
||||
title: "About",
|
||||
description: "About Bradley Shellnut",
|
||||
url: currentPageUrl,
|
||||
siteName: 'Bradley Shellnut Personal Website',
|
||||
type: 'website',
|
||||
locale: 'en_US',
|
||||
siteName: "Bradley Shellnut Personal Website",
|
||||
type: "website",
|
||||
locale: "en_US",
|
||||
images: [
|
||||
{
|
||||
url: `${baseUrl}og?header=About | bradleyshellnut.com&page=Hey! My name is Bradley Shellnut.&content=I am a full stack software engineer who's interested in new tech and not afraid to discover new interests.`,
|
||||
alt: 'About Bradley Shellnut',
|
||||
alt: "About Bradley Shellnut",
|
||||
width: 1200,
|
||||
height: 630
|
||||
}
|
||||
]
|
||||
height: 630,
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
title: 'About',
|
||||
description: 'About page',
|
||||
card: 'summary_large_image',
|
||||
title: "About",
|
||||
description: "About page",
|
||||
card: "summary_large_image",
|
||||
image: `${baseUrl}og?header=About | bradleyshellnut.com&page=Hey! My name is Bradley Shellnut.&content=I am a full stack software engineer who's interested in new tech and not afraid to discover new interests.`,
|
||||
imageAlt: 'Bradley Shellnut Website Logo'
|
||||
imageAlt: "Bradley Shellnut Website Logo",
|
||||
},
|
||||
url: currentPageUrl
|
||||
url: currentPageUrl,
|
||||
});
|
||||
|
||||
return {
|
||||
metaTagsChild: metaTags
|
||||
}
|
||||
}
|
||||
metaTagsChild: metaTags,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,7 +3,11 @@
|
|||
import Tag from "$lib/components/Tag.svelte";
|
||||
import type { Course } from "$lib/types/courses";
|
||||
|
||||
export let course: Course;
|
||||
interface Props {
|
||||
course: Course;
|
||||
}
|
||||
|
||||
let { course }: Props = $props();
|
||||
const { externalLinks, tags } = course;
|
||||
</script>
|
||||
|
||||
|
|
@ -11,12 +15,9 @@
|
|||
<h3>
|
||||
{#each externalLinks as link}
|
||||
<ExternalLink
|
||||
ariaLabel={link.ariaLabel}
|
||||
href={link.href}
|
||||
showIcon={link.showIcon}
|
||||
>
|
||||
{link.text}
|
||||
</ExternalLink>
|
||||
linkData={{ href: link.href, ariaLabel: link.ariaLabel, title: link.ariaLabel, target: '_blank', clazz: "tech-list-item" }}
|
||||
textData={{ text: link.text, showIcon: link.showIcon, location: 'left' }}
|
||||
/>
|
||||
{/each}
|
||||
</h3>
|
||||
<div class="tags">
|
||||
|
|
|
|||
|
|
@ -1,23 +1,28 @@
|
|||
<script lang="ts">
|
||||
import type { IconifyIcon } from "iconify-icon/dist/iconify-icon.js";
|
||||
import ExternalLink from '$lib/components/ExternalLink.svelte';
|
||||
import { lucideIcon } from '$lib/util/logoIcons.svelte';
|
||||
import type { Snippet } from "svelte";
|
||||
import type { LinkTextType } from '$lib/types/externalLinkTypes';
|
||||
|
||||
export let ariaLabel: string;
|
||||
export let href: string;
|
||||
export let clazz = "";
|
||||
export let itemText: string;
|
||||
export let icon: IconifyIcon;
|
||||
interface Props {
|
||||
linkData: LinkTextType;
|
||||
ariaLabel: string;
|
||||
href: string;
|
||||
clazz?: string;
|
||||
textData?: LinkTextType;
|
||||
icon: Snippet;
|
||||
}
|
||||
|
||||
let { ariaLabel, href, clazz = '', textData, icon }: Props = $props();
|
||||
</script>
|
||||
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
aria-label={ariaLabel}
|
||||
{href}
|
||||
class={clazz}
|
||||
>
|
||||
<iconify-icon {icon} width="24" height="24" role="img" title={itemText} />
|
||||
<p>{itemText}</p>
|
||||
</a>
|
||||
<ExternalLink
|
||||
ariaLabel={ariaLabel}
|
||||
href={href}
|
||||
linkClass={clazz}
|
||||
icon={lucideIcon}
|
||||
textData={textData}
|
||||
/>
|
||||
|
||||
<style lang="postcss">
|
||||
a {
|
||||
|
|
@ -44,4 +49,8 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -10,11 +10,13 @@ export async function GET({ setHeaders, url }) {
|
|||
}
|
||||
|
||||
try {
|
||||
const response = await fetchArticlesApi('get', `fetchArticles`, {
|
||||
const response = await fetchArticlesApi('get', 'fetchArticles', {
|
||||
page,
|
||||
limit
|
||||
});
|
||||
|
||||
console.log(`JSON articles response: ${JSON.stringify(response)}`);
|
||||
|
||||
if (response?.articles) {
|
||||
if (response?.cacheControl) {
|
||||
if (!response.cacheControl.includes('no-cache')) {
|
||||
|
|
|
|||
67
src/routes/api/bandcamp/albums/+server.ts
Normal file
67
src/routes/api/bandcamp/albums/+server.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import { json, error } from '@sveltejs/kit';
|
||||
import { BANDCAMP_USERNAME, PAGE_SIZE, USE_REDIS_CACHE } from '$env/static/private';
|
||||
import { fetchArticlesApi } from '$lib/api';
|
||||
import { redis } from '$root/lib/server/redis';
|
||||
import type { Album, BandCampResults } from '$lib/types/album';
|
||||
import scrapeIt, { type ScrapeResult } from 'scrape-it';
|
||||
|
||||
export async function GET({ setHeaders, url }) {
|
||||
try {
|
||||
if (USE_REDIS_CACHE) {
|
||||
const cached: string | null = await redis.get('bandcampAlbums');
|
||||
|
||||
if (cached) {
|
||||
const response: Album[] = JSON.parse(cached);
|
||||
const ttl = await redis.ttl("bandcampAlbums");
|
||||
if (ttl) {
|
||||
setHeaders({
|
||||
"cache-control": `max-age=${ttl}`,
|
||||
});
|
||||
} else {
|
||||
setHeaders({
|
||||
"cache-control": "max-age=43200",
|
||||
});
|
||||
}
|
||||
return json(response);
|
||||
}
|
||||
}
|
||||
|
||||
const { data }: ScrapeResult<BandCampResults> = await scrapeIt(`https://bandcamp.com/${BANDCAMP_USERNAME}`, {
|
||||
collectionItems: {
|
||||
listItem: '.collection-item-container',
|
||||
data: {
|
||||
url: {
|
||||
selector: '.collection-title-details > a.item-link',
|
||||
attr: 'href',
|
||||
},
|
||||
artwork: {
|
||||
selector: 'div.collection-item-art-container a img',
|
||||
attr: 'src',
|
||||
},
|
||||
title: {
|
||||
selector: 'span.item-link-alt > div.collection-item-title',
|
||||
},
|
||||
artist: {
|
||||
selector: 'span.item-link-alt > div.collection-item-artist',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const albums: Album[] = data?.collectionItems || [];
|
||||
|
||||
if (albums && albums?.length > 0) {
|
||||
if (USE_REDIS_CACHE) {
|
||||
redis.set('bandcampAlbums', JSON.stringify(albums), 'EX', 43200);
|
||||
}
|
||||
setHeaders({
|
||||
"cache-control": "max-age=43200",
|
||||
});
|
||||
return json(albums);
|
||||
}
|
||||
return json([]);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return json([]);
|
||||
}
|
||||
}
|
||||
|
|
@ -4,15 +4,15 @@
|
|||
import Articles from '$lib/components/Articles.svelte';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
let articles: Article[];
|
||||
let currentPage: number;
|
||||
let totalArticles: number;
|
||||
let limit: number;
|
||||
|
||||
$: if (data) {
|
||||
({ articles, currentPage, totalArticles, limit } = data);
|
||||
interface Props {
|
||||
data: PageData;
|
||||
}
|
||||
|
||||
let { data }: Props = $props();
|
||||
let articles: Article[] = $state(data?.articles);
|
||||
let currentPage: number = $state(data?.currentPage);
|
||||
let totalArticles: number = $state(data?.totalArticles);
|
||||
let limit: number = $state(data?.limit);
|
||||
</script>
|
||||
|
||||
<h1 style:margin-bottom={"2rem"}>Favorite Tech Articles</h1>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import SocialImageCard from '$lib/components/socialImageCard.svelte';
|
||||
import { componentToPng } from '$root/lib/renderImage';
|
||||
import SocialImageCard from "$lib/components/socialImageCard.svelte";
|
||||
import { componentToPng } from "$root/lib/renderImage";
|
||||
|
||||
const height = 630;
|
||||
const width = 1200;
|
||||
|
|
@ -7,22 +7,27 @@ const width = 1200;
|
|||
/** @type {import('./$types').RequestHandler} */
|
||||
export async function GET({ url }) {
|
||||
try {
|
||||
const faviconImageName = 'b_shell_nut_favicon.png';
|
||||
const faviconImageName = "b_shell_nut_favicon.png";
|
||||
const image = `${new URL(url.origin).href}${faviconImageName}`;
|
||||
const header = url.searchParams.get('header') ?? undefined;
|
||||
const page = url.searchParams.get('page') ?? undefined;
|
||||
const content = url.searchParams.get('content') ?? '';
|
||||
const header = url.searchParams.get("header") ?? undefined;
|
||||
const page = url.searchParams.get("page") ?? undefined;
|
||||
const content = url.searchParams.get("content") ?? "";
|
||||
|
||||
// @ts-expect-error: Argument of type 'typeof SocialImageCard__SvelteComponent_' is not assignable to parameter of type 'SvelteComponent<any, any, any>'
|
||||
return componentToPng(SocialImageCard, {
|
||||
header,
|
||||
page,
|
||||
content,
|
||||
image,
|
||||
width: `${width}`,
|
||||
height: `${height}`,
|
||||
url: new URL(url.origin).href
|
||||
}, height, width);
|
||||
return componentToPng(
|
||||
SocialImageCard,
|
||||
{
|
||||
header,
|
||||
page,
|
||||
content,
|
||||
image,
|
||||
width: `${width}`,
|
||||
height: `${height}`,
|
||||
url: new URL(url.origin).href,
|
||||
},
|
||||
height,
|
||||
width
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,103 +1,266 @@
|
|||
<script lang="ts">
|
||||
import { createTabs, melt } from '@melt-ui/svelte';
|
||||
import GitHub from '@iconify-icons/simple-icons/github';
|
||||
import Portfolio from '$lib/components/Portfolio.svelte';
|
||||
import personalSite from "$lib/assets/images/portfolio/Bradley_Shellnut_New_Site.png?as=run";
|
||||
import weddingWebsite from "$lib/assets/images/portfolio/Wedding_Website.png?as=run";
|
||||
import oldSite from '$lib/assets/images/portfolio/Old_Website_Bradley_Shellnut.png?as=run';
|
||||
import shellnutArchitectWebsite from "$lib/assets/images/portfolio/Mark_Shellnut_Architect.png?as=run";
|
||||
import ExternalLink from "$lib/components/ExternalLink.svelte";
|
||||
import Portfolio from "./Portfolio.svelte";
|
||||
// @ts-expect-error: Cannot find module '$lib/content/uses/development.md' or its corresponding type declarations.ts(2307)
|
||||
import PersonalWebsiteSvelteKit from "$lib/content/portfolio/personal/personal-website-sveltekit.md";
|
||||
// import OldWebsite from "$lib/content/portfolio/personal/old-website.md";
|
||||
// @ts-expect-error: Cannot find module '$lib/content/uses/development.md' or its corresponding type declarations.ts(2307)
|
||||
import WeddingWebsite from '$lib/content/portfolio/personal/wedding-website.md';
|
||||
// import PersonalWebsiteSvelteKit from "$lib/content/portfolio/personal/personal-website-sveltekit.md";
|
||||
// @ts-expect-error: Cannot find module '$lib/content/uses/development.md' or its corresponding type declarations.ts(2307)
|
||||
import MarkShellnutArchitect from '$lib/content/portfolio/professional/mark-shellnut-architect.md';
|
||||
// import WeddingWebsite from "$lib/content/portfolio/personal/wedding-website.md";
|
||||
// @ts-expect-error: Cannot find module '$lib/content/uses/development.md' or its corresponding type declarations.ts(2307)
|
||||
import OldWebsite from '$lib/content/portfolio/personal/old-website.md';
|
||||
import ExternalLink from '$lib/components/ExternalLink.svelte';
|
||||
|
||||
const {
|
||||
elements: { root, list, content, trigger }
|
||||
} = createTabs({
|
||||
defaultValue: 'personal'
|
||||
});
|
||||
|
||||
const triggers = [
|
||||
{ id: 'personal', title: 'Personal Sites' },
|
||||
{ id: 'professional', title: 'Professional Sites'}
|
||||
];
|
||||
// import MarkShellnutArchitect from "$lib/content/portfolio/professional/mark-shellnut-architect.md";
|
||||
import type { ExternalLinkType } from "$lib/types/externalLinkType";
|
||||
import { Tabs } from "bits-ui";
|
||||
import personalSite from "../../lib/assets/images/portfolio/Bradley_Shellnut_New_Site.png?enhanced";
|
||||
import shellnutArchitectWebsite from "../../lib/assets/images/portfolio/Mark_Shellnut_Architect.png?enhanced";
|
||||
import oldSite from "../../lib/assets/images/portfolio/Old_Website_Bradley_Shellnut.png?enhanced";
|
||||
import weddingWebsite from "../../lib/assets/images/portfolio/Wedding_Website.png?enhanced";
|
||||
import { gitHubIcon } from "$root/lib/util/logoIcons.svelte";
|
||||
</script>
|
||||
|
||||
<h1>Portfolio!</h1>
|
||||
<div use:melt={$root} class="root tab-group">
|
||||
<div use:melt={$list} aria-label="tabs portfolios" class="list tab-list">
|
||||
{#each triggers as triggerItem}
|
||||
<button use:melt={$trigger(triggerItem.id)} class="trigger" type="button">
|
||||
<h2>{triggerItem.title}</h2>
|
||||
</button>
|
||||
{#snippet links(externalLinks: ExternalLinkType[])}
|
||||
<span>
|
||||
{#each externalLinks as link}
|
||||
{#if link.icon && link.showIcon}
|
||||
<ExternalLink
|
||||
linkData={{
|
||||
href: link.href,
|
||||
ariaLabel: link.ariaLabel,
|
||||
title: link.ariaLabel,
|
||||
target: "_blank",
|
||||
}}
|
||||
textData={{
|
||||
text: link.text,
|
||||
showIcon: link.showIcon,
|
||||
location: "left",
|
||||
}}
|
||||
iconData={{ type: "svg", icon: link.icon }}
|
||||
/>
|
||||
{:else}
|
||||
<ExternalLink
|
||||
linkData={{
|
||||
href: link.href,
|
||||
ariaLabel: link.ariaLabel,
|
||||
title: link.ariaLabel,
|
||||
target: "_blank",
|
||||
}}
|
||||
textData={{
|
||||
text: link.text,
|
||||
showIcon: link.showIcon,
|
||||
location: "left",
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
<div use:melt={$content('personal')} class="content">
|
||||
<Portfolio name="Personal Website"
|
||||
</span>
|
||||
{/snippet}
|
||||
|
||||
<h1>Portfolio!</h1>
|
||||
<Tabs.Root value="personal">
|
||||
<Tabs.List>
|
||||
<Tabs.Trigger value="personal"><span>Personal</span></Tabs.Trigger>
|
||||
<Tabs.Trigger value="professional"><span>Professional</span></Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
<Tabs.Content value="personal">
|
||||
<Portfolio
|
||||
name="Personal Website"
|
||||
style="max-height: 550px;"
|
||||
src={personalSite}
|
||||
loading="eager"
|
||||
alt="Picture of Bradley Shellnut's Personal Website">
|
||||
<span slot="portfolio-links">
|
||||
<ExternalLink ariaLabel="View GitHub repository for my personal website" href="https://github.com/BradNut/personal-website-sveltekit" icon={GitHub} showIcon>GitHub repository</ExternalLink>
|
||||
</span>
|
||||
<PersonalWebsiteSvelteKit slot="portfolio-details" />
|
||||
alt="Picture of Bradley Shellnut's Personal Website"
|
||||
{links}
|
||||
externalLinks={[
|
||||
{
|
||||
ariaLabel: "View GitHub repository for my personal website",
|
||||
href: "https://github.com/BradNut/personal-website-sveltekit",
|
||||
icon: gitHubIcon,
|
||||
showIcon: true,
|
||||
text: "GitHub repository",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<h2>My personal website re-written using SvelteKit.</h2>
|
||||
Tech Stack:
|
||||
<ul>
|
||||
<li>
|
||||
<ExternalLink
|
||||
linkData={{
|
||||
href: "https://kit.svelte.dev/",
|
||||
ariaLabel: "SvelteKit",
|
||||
}}
|
||||
textData={{ text: "SvelteKit", showIcon: true, location: "left" }}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<ExternalLink
|
||||
linkData={{ href: "https://bits-ui.com/", ariaLabel: "Bits-UI" }}
|
||||
textData={{ text: "Bits-UI", showIcon: true, location: "left" }}
|
||||
/> for the headless-ui components.
|
||||
</li>
|
||||
<li>
|
||||
<ExternalLink
|
||||
linkData={{
|
||||
href: "https://www.typescriptlang.org/",
|
||||
ariaLabel: "TypeScript",
|
||||
}}
|
||||
textData={{ text: "TypeScript", showIcon: true, location: "left" }}
|
||||
/>
|
||||
</li>
|
||||
<li>Deployed on a Coolify Self Hosted Box</li>
|
||||
<li>
|
||||
Icons in the <a href="/about">/about</a> page and the Bee, Shell, and
|
||||
Nut icons are all made by <ExternalLink
|
||||
linkData={{
|
||||
href: "https://www.flaticon.com/authors/freepik",
|
||||
ariaLabel: "Freepik",
|
||||
}}
|
||||
textData={{ text: "Freepik", showIcon: true, location: "left" }}
|
||||
/> from <ExternalLink
|
||||
textData={{ text: "Flaticon", showIcon: true, location: "left" }}
|
||||
linkData={{
|
||||
href: "https://www.flaticon.com/",
|
||||
ariaLabel: "Flaticon",
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
<p>The previous version of my website was written using React and Gatsby which
|
||||
you can view <ExternalLink
|
||||
linkData={{
|
||||
href: "https://wonderful-austin-9f17d2.netlify.app/",
|
||||
ariaLabel: "React and Gatsby Personal Site version",
|
||||
}}
|
||||
textData={{ text: "here.", showIcon: true, location: "left" }}
|
||||
/></p>
|
||||
<p>
|
||||
Each iteration brings better code and my previous React version was
|
||||
improved after the suggestions on <ExternalLink
|
||||
linkData={{
|
||||
href: "https://syntax.fm/show/444/syntax-highlight#t=33:19",
|
||||
ariaLabel: "Syntax.fm Podcast Number 444",
|
||||
}}
|
||||
textData={{ text: "Show 444", showIcon: true, location: "left" }}
|
||||
/> of the <ExternalLink
|
||||
linkData={{ href: "https://syntax.fm/", ariaLabel: "Syntax.fm" }}
|
||||
textData={{ text: "Syntax Pocast.", showIcon: true, location: "left" }}
|
||||
/></p>
|
||||
<p>You can view the previous archived version of the site before those
|
||||
changes <ExternalLink
|
||||
textData={{ text: "here.", showIcon: true, location: "left" }}
|
||||
linkData={{
|
||||
href: "https://web.archive.org/web/20210224002046/https://bradleyshellnut.com/",
|
||||
ariaLabel: "Archive before Syntax Podcast",
|
||||
}}
|
||||
/></p>
|
||||
</Portfolio>
|
||||
<Portfolio name="Wedding Website"
|
||||
<Portfolio
|
||||
name="Wedding Website"
|
||||
style="max-height: 550px;"
|
||||
src={weddingWebsite}
|
||||
alt="Picture of NextJS Wedding Website">
|
||||
<span slot="portfolio-links">
|
||||
<ExternalLink ariaLabel="View Wedding Website" href="https://weddingsite-six.vercel.app/" showIcon>View Site</ExternalLink>
|
||||
<ExternalLink ariaLabel="View GitHub repository for the wedding site" href="https://github.com/BradNut/weddingsite" icon={GitHub} showIcon>GitHub repository</ExternalLink>
|
||||
</span>
|
||||
<WeddingWebsite slot="portfolio-details" />
|
||||
alt="Picture of NextJS Wedding Website"
|
||||
{links}
|
||||
externalLinks={[
|
||||
{
|
||||
ariaLabel: "View GitHub repository for the wedding site",
|
||||
href: "https://github.com/BradNut/weddingsite",
|
||||
icon: gitHubIcon,
|
||||
showIcon: true,
|
||||
text: "GitHub repository",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<p>The app was initially created for my wedding but what is linked here is a public demo of the application.</p>
|
||||
<p>An application that allows viewing of wedding details and provides the ability to RSVP to the wedding.</p>
|
||||
<p>Tech stack:</p>
|
||||
<ul>
|
||||
<li>Next.js 13</li>
|
||||
<li>React 18</li>
|
||||
<li><ExternalLink
|
||||
linkData={{ href: "https://radix-ui.com/", ariaLabel: "Radix UI" }}
|
||||
textData={{ text: "Radix UI", showIcon: true, location: "left" }}
|
||||
/></li>
|
||||
<li>MongoDB</li>
|
||||
<li>Styled Components</li>
|
||||
<li>Next Iron Session</li>
|
||||
</ul>
|
||||
</Portfolio>
|
||||
<Portfolio name="Old Personal Website"
|
||||
<Portfolio
|
||||
name="Old Personal Website"
|
||||
style="max-height: 320px;"
|
||||
src={oldSite}
|
||||
alt="Home Page of the old bradleyshellnut.com website">
|
||||
<span slot="portfolio-links">
|
||||
<ExternalLink ariaLabel="Archive of bradleyshellnut.com" href="https://web.archive.org/web/20201205233507/https://bradleyshellnut.com/about" showIcon>Link to an archive snapshot</ExternalLink>
|
||||
</span>
|
||||
<OldWebsite slot="portfolio-details" />
|
||||
alt="Home Page of the old bradleyshellnut.com website"
|
||||
{links}
|
||||
externalLinks={[
|
||||
{
|
||||
ariaLabel: "Archive of bradleyshellnut.com",
|
||||
href: "https://web.archive.org/web/20201205233507/https://bradleyshellnut.com/about",
|
||||
icon: gitHubIcon,
|
||||
showIcon: true,
|
||||
text: "Link to an archive snapshot",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<p>My first personal website</p>
|
||||
<p>This was my first real personal website hosted on DigitalOcean.</p>
|
||||
<p>Tech stack:</p>
|
||||
<ul>
|
||||
<li>React</li>
|
||||
<li>Redux</li>
|
||||
<li>ReactStrap for CSS grid management</li>
|
||||
<li>React Router for routing links in the page</li>
|
||||
</ul>
|
||||
</Portfolio>
|
||||
</div>
|
||||
<div use:melt={$content('professional')} class="content">
|
||||
<Portfolio name="Mark Shellnut Architect"
|
||||
</Tabs.Content>
|
||||
<Tabs.Content value="professional">
|
||||
<Portfolio
|
||||
name="Mark Shellnut Architect"
|
||||
style="max-height: 550px;"
|
||||
src={shellnutArchitectWebsite}
|
||||
alt="Picture of Mark Shellnut Architect's Website">
|
||||
<span slot="portfolio-links">
|
||||
<ExternalLink ariaLabel="View markshellnutarchitect.com" href="https://markshellnutarchitect.com" showIcon>Link to Mark Shellnut's Website</ExternalLink>
|
||||
</span>
|
||||
<MarkShellnutArchitect slot="portfolio-details" />
|
||||
alt="Picture of Mark Shellnut Architect's Website"
|
||||
{links}
|
||||
externalLinks={[
|
||||
{
|
||||
ariaLabel: "View Mark Shellnut Architect",
|
||||
href: "https://markshellnutarchitect.com",
|
||||
showIcon: false,
|
||||
text: "Link to Mark Shellnut Architect",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<p>Company website for Mark Shellnut Architect.</p>
|
||||
<p>Tech stack:</p>
|
||||
<ul>
|
||||
<li>React 18</li>
|
||||
<li>Gatsby 5</li>
|
||||
<li>
|
||||
<ExternalLink
|
||||
linkData={{ href: "https://radix-ui.com/", ariaLabel: "Radix UI" }}
|
||||
textData={{ text: "Radix UI", showIcon: true, location: "left" }}
|
||||
/>
|
||||
</li>
|
||||
<li>Styled Components</li>
|
||||
<li>GraphQL</li>
|
||||
<li>Lambda Functions</li>
|
||||
</ul>
|
||||
</Portfolio>
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
</Tabs.Root>
|
||||
|
||||
<style lang="postcss">
|
||||
.root {
|
||||
:global([data-tabs-root]) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* overflow: hidden; */
|
||||
/* border-radius: var(--border-radius); */
|
||||
|
||||
&[data-orientation="vertical"] {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
@media(min-width: 1000px) {
|
||||
@media (min-width: 1000px) {
|
||||
max-width: 50vw;
|
||||
}
|
||||
}
|
||||
|
||||
.list {
|
||||
:global([data-tabs-list]) {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
grid-template-columns: auto auto;
|
||||
|
|
@ -108,21 +271,19 @@
|
|||
user-select: none;
|
||||
}
|
||||
|
||||
.trigger {
|
||||
&[data-state='active'] {
|
||||
h2 {
|
||||
border-bottom: 2px solid var(--shellYellow);
|
||||
}
|
||||
}
|
||||
:global([data-tabs-trigger]) {
|
||||
font-size: var(--h2);
|
||||
}
|
||||
|
||||
&[data-state='inactive'] {
|
||||
h2 {
|
||||
border-bottom: 2px solid var(--white);
|
||||
}
|
||||
:global([data-state="active"]) {
|
||||
span {
|
||||
border-bottom: 2px solid var(--shellYellow);
|
||||
}
|
||||
}
|
||||
|
||||
:global(img) {
|
||||
border-radius: 3px;
|
||||
:global([data-state="inactive"]) {
|
||||
span {
|
||||
border-bottom: 2px solid var(--white);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
|
|||
59
src/routes/portfolio/Portfolio.svelte
Normal file
59
src/routes/portfolio/Portfolio.svelte
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
<script lang="ts">
|
||||
import type { Picture } from 'vite-imagetools';
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { ExternalLinkType } from '$lib/types/externalLinkType';
|
||||
|
||||
const {
|
||||
links,
|
||||
externalLinks,
|
||||
name,
|
||||
src,
|
||||
alt,
|
||||
style,
|
||||
fetchPriority = 'auto',
|
||||
loading = 'lazy',
|
||||
children
|
||||
}: { links: Snippet<ExternalLinkType[]>, externalLinks: ExternalLinkType[], name: string; src: string | Picture; alt: string;
|
||||
style: string; fetchPriority?: 'high' | 'low' | 'auto'; loading?: 'lazy' | 'eager', children?: Snippet } = $props();
|
||||
</script>
|
||||
|
||||
<div class="portfolio">
|
||||
<div class="portfolio-picture">
|
||||
<h2>{name}</h2>
|
||||
<enhanced:img {src} {style} {alt} fetchpriority={fetchPriority} {loading} />
|
||||
{@render links(externalLinks)}
|
||||
</div>
|
||||
<div class="portfolio-details">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
:global(.portfolio-picture) {
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
:global(.portfolio) {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(200px, 500px) auto;
|
||||
margin: 1.5rem auto;
|
||||
|
||||
@media (width <= 1200px) {
|
||||
grid-template-columns: minmax(200px, 500px);
|
||||
place-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.portfolio-details) {
|
||||
margin: 0 1.5rem;
|
||||
|
||||
@media (max-width: 800px) {
|
||||
margin: 1rem 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.portfolio-details ul) {
|
||||
list-style-type: disc;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import ExternalLink from "$lib/components/ExternalLink.svelte";
|
||||
import ExternalLink from '$lib/components/ExternalLink.svelte';
|
||||
</script>
|
||||
|
||||
<div>
|
||||
|
|
@ -13,9 +13,7 @@
|
|||
they are willing to give to any business/entity.
|
||||
</p>
|
||||
<p>
|
||||
For the sake of transparency I am using <ExternalLink ariaLabel="Umami Analytics FAQ" href="https://umami.is/docs/faq" showIcon>Umami Analytics</ExternalLink> to anonymously track visits to my site. You can completely block this if you want by either using an AdBlocker like
|
||||
<ExternalLink ariaLabel="uBlock Origin" href="https://ublockorigin.com/" showIcon>uBlock Origin</ExternalLink> or setting your browser to send "Do Not Track" requests as I honor them.
|
||||
</p>
|
||||
For the sake of transparency I am using <ExternalLink textData={{ text: "Umami Analytics", showIcon: true, location: "left" }} linkData={{ href: "https://umami.is", ariaLabel: "Umami Analytics" }} /> to anonymously track visits to my site. You can completely block this if you want by either using an AdBlocker like <ExternalLink textData={{ text: "uBlock Origin", showIcon: true, location: "left" }} linkData={{ href: "https://ublockorigin.com/", ariaLabel: "uBlock Origin" }} />. Sending "Do Not Track" requests in your browser is supported but not overall it is not recommended to turn this on since it can be used to fingerprint you on the web.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Useful Resources</h2>
|
||||
|
|
@ -25,55 +23,48 @@
|
|||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<ExternalLink ariaLabel="Awesome Privacy" href="https://github.com/Lissy93/awesome-privacy" showIcon>Privacy Respecting Software List</ExternalLink>
|
||||
<ExternalLink linkData={{ href: "https://www.eff.org/privacybadger", ariaLabel: "Privacy Badger" }} textData={{ text: "Privacy Badger", showIcon: true, location: "left" }} />
|
||||
<ExternalLink textData={{ text: "Awesome Privacy", showIcon: true, location: "left" }} linkData={{ href: "https://github.com/Lissy93/awesome-privacy", ariaLabel: "Awesome Privacy" }} />
|
||||
</li>
|
||||
<li>
|
||||
<ExternalLink ariaLabel="Privacy Guides" href="https://www.privacyguides.org/" showIcon>Privacy Guides</ExternalLink>
|
||||
<ExternalLink linkData={{ href: "https://privacyguides.org/", ariaLabel: "Privacy Guides" }} textData={{ text: "Privacy Guides", showIcon: true, location: "left" }} />
|
||||
</li>
|
||||
<li>
|
||||
<ExternalLink ariaLabel="Ethical Alternatives" href="https://ethical.net/resources/" showIcon>Ethical Alternatives</ExternalLink>
|
||||
<ExternalLink linkData={{ href: "https://ethical.net/resources/", ariaLabel: "Ethical Alternatives" }} textData={{ text: "Ethical Alternatives", showIcon: true, location: "left" }} />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Privacy Centric Paid Services I use:</h3>
|
||||
<ul>
|
||||
<li>
|
||||
Article Saving: <ExternalLink ariaLabel="Wallabag Article Saver" href="https://wallabag.com/" showIcon>Wallabag</ExternalLink>
|
||||
</li>
|
||||
<li>
|
||||
Anonymous Email Forwarding:
|
||||
<ExternalLink ariaLabel="AnonAddy Email Forwarding" href="https://anonaddy.com/" showIcon>AnonAddy</ExternalLink>
|
||||
</li>
|
||||
<li>
|
||||
Email: <ExternalLink ariaLabel="ProtonMail" href="https://protonmail.com/" showIcon>ProtonMail</ExternalLink>
|
||||
</li>
|
||||
<li>
|
||||
Notes: <ExternalLink ariaLabel="Joplin" href="https://joplinapp.org/" showIcon>Joplin Notes</ExternalLink>
|
||||
</li>
|
||||
<li>
|
||||
VPN: <ExternalLink ariaLabel="Mullvad VPN" href="https://mullvad.net/" showIcon>Mullvad VPN</ExternalLink>
|
||||
</li>
|
||||
</ul>
|
||||
<h3>Privacy Centric Paid Services I use:</h3>
|
||||
<ul>
|
||||
<li>
|
||||
Article Saving: <ExternalLink textData={{ text: "Wallabag Article Saver", showIcon: true, location: "left" }} linkData={{ href: "https://wallabag.com/", ariaLabel: "Wallabag Article Saver" }} />
|
||||
</li>
|
||||
<li>
|
||||
Anonymous Email Forwarding: <ExternalLink textData={{ text: "SimpleLogin (Now owned by Proton AG)", showIcon: true, location: "left" }} linkData={{ href: "https://simplelogin.io", ariaLabel: "SimpleLogin" }} />
|
||||
</li>
|
||||
<li>
|
||||
Email: <ExternalLink textData={{ text: "ProtonMail", showIcon: true, location: "left" }} linkData={{ href: "https://protonmail.com/", ariaLabel: "ProtonMail" }} />
|
||||
</li>
|
||||
<li>
|
||||
Notes: <ExternalLink textData={{ text: "Joplin Notes", showIcon: true, location: "left" }} linkData={{ href: "https://joplinapp.org/", ariaLabel: "Joplin Notes" }} />
|
||||
</li>
|
||||
<li>
|
||||
VPN: <ExternalLink textData={{ text: "ProtonVPN", showIcon: true, location: "left" }} linkData={{ href: "https://protonvpn.com", ariaLabel: "ProtonVPN" }} />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3>NAS Servers for Self Hosting:</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<ExternalLink ariaLabel="Synology NAS" href="https://synology.com/" showIcon>Synology NAS</ExternalLink>: An easy, not cheap, local solution for Google Services like Drive, Photos, Calendar, other services using Docker, etc. (Yes I should use <ExternalLink ariaLabel="NextCloud Local Hosting Service" href="https://nextcloud.com/" showIcon>NextCloud Local Hosting Service</ExternalLink>...maybe eventually)
|
||||
</li>
|
||||
<li>
|
||||
Mac Mini: Used as a <ExternalLink ariaLabel="Plex" href="https://www.plex.tv/" showIcon>Plex Server</ExternalLink> for my Movies/TV/Music until I build a dedicated NAS and maybe switch to <ExternalLink ariaLabel="JellyFin" href="https://jellyfin.org/" showIcon>JellyFin</ExternalLink> soon.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Software Deployed:</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<ExternalLink ariaLabel="Syncthing File Synchronization" href="https://github.com/syncthing/syncthing" showIcon>Syncthing</ExternalLink>: An open source file synchronization program.
|
||||
</li>
|
||||
</ul>
|
||||
<h3>NAS Servers for Self Hosting:</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<ExternalLink textData={{ text: "Synology NAS", showIcon: true, location: "left" }} linkData={{ href: "https://synology.com/", ariaLabel: "Synology NAS" }} />
|
||||
: An easy, not cheap, local solution for Google Services like Drive, Photos, Calendar, other services using Docker, etc. (Yes I should use <ExternalLink textData={{ text: "NextCloud Local Hosting Service", showIcon: true, location: "left" }} linkData={{ href: "https://nextcloud.com/", ariaLabel: "NextCloud Local Hosting Service" }} />...maybe eventually)
|
||||
</li>
|
||||
<li>
|
||||
Custom NAS Server used as a <ExternalLink textData={{ text: "Plex Machine", showIcon: true, location: "left" }} linkData={{ href: "https://www.plex.tv/", ariaLabel: "Plex" }} />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
|
|
|
|||
|
|
@ -1,24 +1,39 @@
|
|||
<script lang="ts">
|
||||
import ExternalLink from '$lib/components/ExternalLink.svelte';
|
||||
import LazyImage from '$lib/components/LazyImage.svelte';
|
||||
import desktop from '$lib/assets/images/Desktop_so_clean.jpg?as=run';
|
||||
// @ts-expect-error: Cannot find module '$lib/content/uses/development.md' or its corresponding type declarations.ts(2307)
|
||||
import HardwareAccessories from '$lib/content/uses/hardware-accessories.md';
|
||||
// @ts-expect-error: Cannot find module '$lib/content/uses/development.md' or its corresponding type declarations.ts(2307)
|
||||
import Development from '$lib/content/uses/development.md';
|
||||
// @ts-expect-error: Cannot find module '$lib/content/uses/development.md' or its corresponding type declarations.ts(2307)
|
||||
import PrivacyHardwareSoftware from '$lib/content/uses/privacy-hardware-software.md';
|
||||
import ExternalLink from "$lib/components/ExternalLink.svelte";
|
||||
import desktop from "$lib/assets/images/Desktop_so_clean.jpg?enhanced";
|
||||
import Development from "./development.svelte";
|
||||
import PrivacyHardwareSoftware from './privacy-hardware-software.svelte';
|
||||
import HardwareAccessories from "./hardware-accessories.svelte";
|
||||
</script>
|
||||
|
||||
<div class="uses">
|
||||
<div>
|
||||
<h1>/Uses</h1>
|
||||
<p>
|
||||
Inspired by <ExternalLink ariaLabel="Wes Bos' Website" href="https://wesbox.com" showIcon>Wes Bos'</ExternalLink> <ExternalLink ariaLabel="Wes Bos' Uses Page" href="https://wesbos.com/uses" showIcon>/uses</ExternalLink> page and <ExternalLink ariaLabel="Uses list website" href="https://uses.tech/" showIcon>Uses.tech</ExternalLink>, this is a list of what I use everyday for my work and personal life.
|
||||
Inspired by <ExternalLink
|
||||
linkData={{ href: "https://wesbos.com", ariaLabel: "Wes Bos' Website" }}
|
||||
textData={{ text: "Wes Bos", showIcon: true, location: "left" }}
|
||||
/>
|
||||
<ExternalLink
|
||||
linkData={{
|
||||
href: "https://wesbos.com/uses",
|
||||
ariaLabel: "Wes Bos' Uses Page",
|
||||
}}
|
||||
textData={{ text: "/uses", showIcon: true, location: "left" }}
|
||||
/> page and <ExternalLink
|
||||
linkData={{ href: "https://uses.tech/", ariaLabel: "Uses.tech" }}
|
||||
textData={{ text: "Uses.tech", showIcon: true, location: "left" }}
|
||||
/>, this is a list of what I use everyday for my work and personal life.
|
||||
</p>
|
||||
</div>
|
||||
<div class="uses-image">
|
||||
<LazyImage clazz="uses-image" src={desktop} alt="Clean desk with Samsung monitor and Ducky Keyboard" loading="eager" />
|
||||
<enhanced:img
|
||||
src={desktop}
|
||||
alt="Clean desk with Samsung monitor and Ducky Keyboard"
|
||||
fetchpriority="high"
|
||||
loading="eager"
|
||||
sizes="(max-height: 480px) 480px, 500px"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<HardwareAccessories />
|
||||
|
|
@ -33,22 +48,14 @@
|
|||
gap: 2rem;
|
||||
}
|
||||
|
||||
.uses-image img {
|
||||
max-width: 640px;
|
||||
height: auto;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
:global(.uses-image) {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
aspect-ratio: 4 / 3;
|
||||
max-height: 480px;
|
||||
|
||||
@media(max-width: 800px) {
|
||||
max-height: 350px;
|
||||
}
|
||||
|
||||
@media(max-width: 500px) {
|
||||
max-height: 300px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
|
|||
205
src/routes/uses/development.svelte
Normal file
205
src/routes/uses/development.svelte
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
<script lang="ts">
|
||||
import ExternalLink from "$lib/components/ExternalLink.svelte";
|
||||
</script>
|
||||
|
||||
<section id="uses-development">
|
||||
<h2>Development</h2>
|
||||
|
||||
<p>
|
||||
My development setup has been documented here: <ExternalLink
|
||||
linkData={{
|
||||
href: "https://github.com/BradNut/computer-setup-info/blob/master/linux/Environment-Setup.md",
|
||||
ariaLabel: "Bradley Shellnut Computer Setup",
|
||||
}}
|
||||
textData={{
|
||||
text: "Computer Setup Info",
|
||||
showIcon: true,
|
||||
location: "left",
|
||||
}}
|
||||
/>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
And documentation for my terminal and coding setup can be found here: <ExternalLink
|
||||
linkData={{
|
||||
href: "https://github.com/BradNut/dotfiles",
|
||||
ariaLabel: "Dotfiles",
|
||||
}}
|
||||
textData={{ text: "Dotfiles", showIcon: true, location: "left" }}
|
||||
/>
|
||||
>.
|
||||
</p>
|
||||
|
||||
<h3>Terminal & Shell Setup</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
Linux: <ExternalLink
|
||||
linkData={{ href: "https://tabby.sh/", ariaLabel: "Tabby" }}
|
||||
textData={{ text: "Tabby terminal", showIcon: true, location: "left" }}
|
||||
/> running ZSH and <ExternalLink
|
||||
linkData={{ href: "https://starship.rs/", ariaLabel: "Starship" }}
|
||||
textData={{ text: "Starship", showIcon: true, location: "left" }}
|
||||
/> as my prompt >.
|
||||
</li>
|
||||
<li>
|
||||
Mac: <ExternalLink
|
||||
linkData={{ href: "https://iterm2.com/", ariaLabel: "iTerm2" }}
|
||||
textData={{ text: "iTerm2", showIcon: true, location: "left" }}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<ExternalLink
|
||||
linkData={{ href: "https://zimfw.sh/", ariaLabel: "ZimFW" }}
|
||||
textData={{ text: "ZimFW", showIcon: true, location: "left" }}
|
||||
/> as my Zsh framework.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3>Useful System Packages</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<ExternalLink
|
||||
linkData={{
|
||||
href: "https://docs.brew.sh/Homebrew-on-Linux",
|
||||
ariaLabel: "Linux Brew",
|
||||
}}
|
||||
textData={{ text: "Linux Brew", showIcon: true, location: "left" }}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<ExternalLink
|
||||
linkData={{ href: "https://brew.sh/", ariaLabel: "Homebrew" }}
|
||||
textData={{ text: "Homebrew", showIcon: true, location: "left" }}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<ExternalLink
|
||||
linkData={{ href: "https://tldr.sh/", ariaLabel: "TLDR Man Pages" }}
|
||||
textData={{ text: "TLDR Man Pages", showIcon: true, location: "left" }}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<ExternalLink
|
||||
linkData={{
|
||||
href: "https://www.npmjs.com/package/trash-cli",
|
||||
ariaLabel: "Trash-CLI",
|
||||
}}
|
||||
textData={{ text: "Trash-CLI", showIcon: true, location: "left" }}
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3>Software</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<ExternalLink
|
||||
linkData={{ href: "https://vscodium.com/", ariaLabel: "VSCodium" }}
|
||||
textData={{ text: "VSCodium", showIcon: true, location: "left" }}
|
||||
/>
|
||||
<ul>
|
||||
<li>
|
||||
My extensions list: <ExternalLink
|
||||
linkData={{
|
||||
href: "https://github.com/BradNut/dotfiles/blob/master/vs-code-extensions-i-use.md",
|
||||
ariaLabel: "VSCode Extensions List",
|
||||
}}
|
||||
textData={{
|
||||
text: "VSCode Extensions List",
|
||||
showIcon: true,
|
||||
location: "left",
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<ExternalLink
|
||||
linkData={{
|
||||
href: "https://www.sublimetext.com/3",
|
||||
ariaLabel: "Sublime Text 3",
|
||||
}}
|
||||
textData={{ text: "Sublime Text 3", showIcon: true, location: "left" }}
|
||||
/>
|
||||
<ul>
|
||||
<li>
|
||||
My Packages List: <ExternalLink
|
||||
linkData={{
|
||||
href: "https://github.com/BradNut/dotfiles/blob/master/sublime-text-extensions-i-use.md",
|
||||
ariaLabel: "Sublime Text Packages List",
|
||||
}}
|
||||
textData={{
|
||||
text: "Sublime Text Packages List",
|
||||
showIcon: true,
|
||||
location: "left",
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<ExternalLink
|
||||
linkData={{
|
||||
href: "https://jetbrains.com/idea/",
|
||||
ariaLabel: "IntelliJ IDEA",
|
||||
}}
|
||||
textData={{ text: "IntelliJ IDEA", showIcon: true, location: "left" }}
|
||||
/>
|
||||
<ul>
|
||||
<li>
|
||||
My Plugins List: <ExternalLink
|
||||
linkData={{
|
||||
href: "https://github.com/BradNut/dotfiles/blob/master/intellij-plugins.md",
|
||||
ariaLabel: "IntelliJ Plugins",
|
||||
}}
|
||||
textData={{
|
||||
text: "IntelliJ Plugins",
|
||||
showIcon: true,
|
||||
location: "left",
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3>Useful Applications</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<ExternalLink
|
||||
linkData={{ href: "https://www.usebruno.com/", ariaLabel: "Bruno" }}
|
||||
textData={{ text: "Bruno", showIcon: true, location: "left" }}
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3>Browsers</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<ExternalLink
|
||||
linkData={{ href: "https://brave.com/", ariaLabel: "Brave Browser" }}
|
||||
textData={{ text: "Brave Browser", showIcon: true, location: "left" }}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<ExternalLink
|
||||
linkData={{
|
||||
href: "https://www.mozilla.org/en-US/firefox/new/",
|
||||
ariaLabel: "Firefox",
|
||||
}}
|
||||
textData={{ text: "Firefox", showIcon: true, location: "left" }}
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<style land="postcss">
|
||||
ul {
|
||||
list-style-type: square;
|
||||
padding-inline-start: 4rem;
|
||||
}
|
||||
</style>
|
||||
67
src/routes/uses/hardware-accessories.svelte
Normal file
67
src/routes/uses/hardware-accessories.svelte
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
<script lang="ts">
|
||||
import ExternalLink from "$lib/components/ExternalLink.svelte";
|
||||
</script>
|
||||
|
||||
<section>
|
||||
<h2>Hardware & Accessories</h2>
|
||||
<ul>
|
||||
<li>MacBook Pro 15-inch for work.</li>
|
||||
<li>
|
||||
Personal desktop running <ExternalLink
|
||||
linkData={{
|
||||
href: "https://pop.system76.com/",
|
||||
ariaLabel: "PopOS Linux Distro",
|
||||
}}
|
||||
textData={{ text: "PopOS", showIcon: true, location: "left" }}
|
||||
/>.
|
||||
</li>
|
||||
<li>Dell XPS 13 running PopOS.</li>
|
||||
<li>
|
||||
Phone 📱: Pixel 6 running <ExternalLink
|
||||
linkData={{
|
||||
href: "https://grapheneos.org/",
|
||||
ariaLabel: "GrapheneOS",
|
||||
}}
|
||||
textData={{ text: "GrapheneOS", showIcon: true, location: "left" }}
|
||||
/>.
|
||||
</li>
|
||||
<li>
|
||||
Keyboard ⌨️: <ExternalLink
|
||||
linkData={{
|
||||
href: "https://www.duckychannel.com.tw/en/Ducky-One2-RGB-TKL",
|
||||
ariaLabel: "Ducky One 2 RGB TKL Keyboard"
|
||||
}}
|
||||
textData={{ text: "Ducky One 2 RGB TKL Keyboard", showIcon: true, location: "left" }}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
Mouse 🖱️: <ExternalLink
|
||||
linkData={{
|
||||
href: "https://www.amazon.com/Logitech-G502-Performance-Gaming-Mouse/dp/B07GBZ4Q68",
|
||||
ariaLabel: "Logitech G502 Gaming Mouse"
|
||||
}}
|
||||
textData={{ text: "Logitech G502 Gaming Mouse", showIcon: true, location: "left" }}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
Chair 🪑: <ExternalLink
|
||||
linkData={{
|
||||
href: "https://secretlabus.myshopify.com/collections/omega-series#omega_2020-stealth",
|
||||
ariaLabel: "Secretlab Omega 2020 Fabric"
|
||||
}}
|
||||
textData={{ text: "Secretlab Omega 2020 Fabric", showIcon: true, location: "left" }}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
Monitor 🖥️: <ExternalLink
|
||||
linkData={{ href: "https://www.samsung.com/us/computing/monitors/gaming/32--odyssey-g7-gaming-monitor-lc32g75tqsnxza/", ariaLabel: "Samsung Odyssey G7 Gaming Monitor" }} textData={{ text: "Samsung Odyssey G7 Gaming Monitor", showIcon: true, location: "left" }} />
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<style lang="postcss">
|
||||
ul {
|
||||
list-style-type: square;
|
||||
padding-inline-start: 4rem;
|
||||
}
|
||||
</style>
|
||||
50
src/routes/uses/privacy-hardware-software.svelte
Normal file
50
src/routes/uses/privacy-hardware-software.svelte
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
<script lang="ts">
|
||||
import ExternalLink from "$lib/components/ExternalLink.svelte";
|
||||
</script>
|
||||
|
||||
<section id="uses-privacy-hardware-software">
|
||||
<h2>Privacy Hardware and Software</h2>
|
||||
|
||||
<p>
|
||||
For the past few years I've been working towards moving my data to
|
||||
self-hosted systems and systems I trust to hold my data.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
If you want to see more info about this head to my <a href="/privacy"
|
||||
>Privacy</a
|
||||
> page.
|
||||
</p>
|
||||
|
||||
<h3>Hardware Authentication</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<ExternalLink
|
||||
linkData={{
|
||||
href: "https://www.yubico.com/product/yubikey-5c/",
|
||||
ariaLabel: "YubiKey 5C",
|
||||
}}
|
||||
textData={{ text: "YubiKey 5C", showIcon: true, location: "left" }}
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3>NAS Servers</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<ExternalLink
|
||||
linkData={{ href: "https://synology.com/", ariaLabel: "Synology NAS" }}
|
||||
textData={{ text: "Synology DS918+", showIcon: true, location: "left" }}
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<style lang="postcss">
|
||||
ul {
|
||||
list-style-type: square;
|
||||
padding-inline-start: 4rem;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -29,6 +29,12 @@
|
|||
--linkHover: var(--shellYellow);
|
||||
--lightHairLine: #c5c5c5;
|
||||
|
||||
--xColor: #000000;
|
||||
--linkedInColor: #0077b5;
|
||||
--githubColor: #000000;
|
||||
--blueskyColor: #0085ff;
|
||||
--emailColor: var(--linkHover);
|
||||
|
||||
/* Styles */
|
||||
--line: solid 1px var(--lineColor);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,17 +1,12 @@
|
|||
import adapter from '@sveltejs/adapter-node';
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||
import { preprocessMeltUI } from '@melt-ui/pp';
|
||||
import { mdsvex } from 'mdsvex';
|
||||
import mdsvexConfig from './mdsvex.config.js';
|
||||
import relativeImages from 'mdsvex-relative-images';
|
||||
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
|
||||
// for more information about preprocessors
|
||||
extensions: ['.svelte', ...mdsvexConfig.extensions],
|
||||
preprocess: [vitePreprocess(), mdsvex(mdsvexConfig), preprocessMeltUI()],
|
||||
extensions: ['.svelte'],
|
||||
preprocess: [vitePreprocess()],
|
||||
vitePlugin: {
|
||||
inspector: {
|
||||
toggleKeyCombo: 'control-alt-shift',
|
||||
|
|
|
|||
|
|
@ -1,21 +1,45 @@
|
|||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import type { UserConfig } from 'vite';
|
||||
import { imagetools } from '@zerodevx/svelte-img/vite';
|
||||
import { sveltekit } from "@sveltejs/kit/vite";
|
||||
import { defineConfig } from "vite";
|
||||
import { enhancedImages } from "@sveltejs/enhanced-img";
|
||||
import { imagetools } from "@zerodevx/svelte-img/vite";
|
||||
|
||||
const config: UserConfig = {
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
enhancedImages(),
|
||||
sveltekit(),
|
||||
imagetools({
|
||||
// By default, directives are `?width=480;1024;1920&format=avif;webp;jpg`
|
||||
// Now we change it to generate 5 variants instead - `avif/jpg` formats at `640/1280` + LQIP (Now as:run)
|
||||
profiles: {
|
||||
run: new URLSearchParams('?w=300;480;640;1024;1920&format=avif;webp;jpg&as=run:64')
|
||||
}
|
||||
})
|
||||
run: new URLSearchParams(
|
||||
"?w=300;480;640;1024;1920&format=avif;webp;jpg&as=run:64"
|
||||
),
|
||||
},
|
||||
}),
|
||||
],
|
||||
esbuild: {
|
||||
target: "es2022",
|
||||
},
|
||||
test: {
|
||||
include: ['src/**/*.{test,spec}.{js,ts}']
|
||||
}
|
||||
};
|
||||
include: ["src/**/*.{test,spec}.{js,ts}"],
|
||||
mockReset: true,
|
||||
},
|
||||
css: {
|
||||
devSourcemap: true,
|
||||
preprocessorOptions: {
|
||||
postcss: {
|
||||
additionalData: `
|
||||
@custom-media --below_small (width < 400px);
|
||||
@custom-media --below_med (width < 700px);
|
||||
@custom-media --below_large (width < 900px);
|
||||
@custom-media --below_xlarge (width < 1200px);
|
||||
|
||||
export default config;
|
||||
@custom-media --above_small (width > 400px);
|
||||
@custom-media --above_med (width > 700px);
|
||||
@custom-media --above_large (width > 900px);
|
||||
@custom-media --above_xlarge (width > 1200px);
|
||||
`,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue