Merge branch 'master' of github.com:BradNut/personal-website-sveltekit into redis

This commit is contained in:
Bradley Shellnut 2023-02-15 16:59:47 -08:00
commit e9ee47de56
22 changed files with 368 additions and 30 deletions

View file

@ -1,13 +1,15 @@
WALLABAG_MAX_ARTICLES=30 WALLABAG_MAX_ARTICLES=30
WALLABAG_CLIENT_ID= WALLABAG_MAX_PAGES=5
WALLABAG_CLIENT_SECRET= NODE_VERSION=18.12.1
WALLABAG_USERNAME= WALLABAG_CLIENT_ID=''
WALLABAG_PASSWORD= WALLABAG_CLIENT_SECRET=''
WALLABAG_URL="https://app.wallabag.it" WALLABAG_USERNAME=''
PUBLIC_SITE_URL= WALLABAG_PASSWORD=''
WALLABAG_URL=""
PUBLIC_SITE_URL=""
PUBLIC_UMAMI_DO_NOT_TRACK=true PUBLIC_UMAMI_DO_NOT_TRACK=true
PUBLIC_UMAMI_URL= PUBLIC_UMAMI_URL=""
PUBLIC_UMAMI_ID= PUBLIC_UMAMI_ID=""
PAGE_SIZE=6 PAGE_SIZE=6
USE_REDIS_CACHE=true USE_REDIS_CACHE=true
REDIS_URI=redis://{username}:{password}@{redisURL}:{redisPort} REDIS_URI=

View file

@ -1,5 +1,6 @@
{ {
"cSpell.words": [ "cSpell.words": [
"Bandcamp",
"bradleyshellnut", "bradleyshellnut",
"iconify", "iconify",
"Mullvad", "Mullvad",

View file

@ -15,6 +15,7 @@
}, },
"devDependencies": { "devDependencies": {
"@iconify-icons/material-symbols": "^1.2.27", "@iconify-icons/material-symbols": "^1.2.27",
"@iconify-icons/mdi": "^1.2.41",
"@iconify-icons/radix-icons": "^1.2.8", "@iconify-icons/radix-icons": "^1.2.8",
"@iconify-icons/simple-icons": "^1.2.42", "@iconify-icons/simple-icons": "^1.2.42",
"@leveluptuts/svelte-side-menu": "^1.0.5", "@leveluptuts/svelte-side-menu": "^1.0.5",

View file

@ -2,6 +2,7 @@ lockfileVersion: 5.4
specifiers: specifiers:
'@iconify-icons/material-symbols': ^1.2.27 '@iconify-icons/material-symbols': ^1.2.27
'@iconify-icons/mdi': ^1.2.41
'@iconify-icons/radix-icons': ^1.2.8 '@iconify-icons/radix-icons': ^1.2.8
'@iconify-icons/simple-icons': ^1.2.42 '@iconify-icons/simple-icons': ^1.2.42
'@leveluptuts/svelte-side-menu': ^1.0.5 '@leveluptuts/svelte-side-menu': ^1.0.5
@ -46,6 +47,7 @@ dependencies:
devDependencies: devDependencies:
'@iconify-icons/material-symbols': 1.2.27 '@iconify-icons/material-symbols': 1.2.27
'@iconify-icons/mdi': 1.2.41
'@iconify-icons/radix-icons': 1.2.8 '@iconify-icons/radix-icons': 1.2.8
'@iconify-icons/simple-icons': 1.2.42 '@iconify-icons/simple-icons': 1.2.42
'@leveluptuts/svelte-side-menu': 1.0.5 '@leveluptuts/svelte-side-menu': 1.0.5
@ -569,6 +571,12 @@ packages:
'@iconify/types': 2.0.0 '@iconify/types': 2.0.0
dev: true dev: true
/@iconify-icons/mdi/1.2.41:
resolution: {integrity: sha512-duqTSmY0H+e/LdSZD5B8PxnJfdfh6qdLVnrI6klHGSSykz23d1KdvoPpfFpgF8mWWDm4UlHIO+rrvsqMLEb3NQ==}
dependencies:
'@iconify/types': 2.0.0
dev: true
/@iconify-icons/radix-icons/1.2.8: /@iconify-icons/radix-icons/1.2.8:
resolution: {integrity: sha512-bZqRIbeqe6yNSLPgcQOyOl86C2P/apaY9Dq/BddWxitN8olbTp2MLuDJenNF+wxbQGgKkQFfm3vb6Z+4Nbhk+g==} resolution: {integrity: sha512-bZqRIbeqe6yNSLPgcQOyOl86C2P/apaY9Dq/BddWxitN8olbTp2MLuDJenNF+wxbQGgKkQFfm3vb6Z+4Nbhk+g==}
dependencies: dependencies:

View file

@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import { PUBLIC_UMAMI_DO_NOT_TRACK, PUBLIC_UMAMI_URL, PUBLIC_UMAMI_ID } from '$env/static/public'; import { PUBLIC_UMAMI_DO_NOT_TRACK, PUBLIC_UMAMI_URL, PUBLIC_UMAMI_ID } from '$env/static/public';
const analyticsURL = `https://${PUBLIC_UMAMI_URL}/umami.js`;
</script> </script>
<svelte:head> <svelte:head>
@ -9,6 +8,6 @@
defer defer
data-website-id={PUBLIC_UMAMI_ID} data-website-id={PUBLIC_UMAMI_ID}
data-do-not-track={PUBLIC_UMAMI_DO_NOT_TRACK} data-do-not-track={PUBLIC_UMAMI_DO_NOT_TRACK}
src={analyticsURL} src={PUBLIC_UMAMI_URL}
></script> ></script>
</svelte:head> </svelte:head>

View file

@ -0,0 +1,105 @@
<script lang="ts">
import OpenInNew from '@iconify-icons/mdi/open-in-new';
import type { Article } from "$root/lib/types/article";
export let articles: Article[];
export let totalArticles: number;
export let compact: boolean = false;
</script>
<div>
<h2>Favorite Articles</h2>
<div style="display: grid;">
{#each articles as article}
<article class="articleStyles card">
<section>
<h3>
<a
target="_blank"
aria-label={`Link to ${article.title}`}
href={article.url.toString()}
rel="noreferrer"
>
{#if compact}
{article.title.substring(0, 50).trim()}&#8230;
{:else}
{article.title}
{/if}
<iconify-icon icon={OpenInNew} width="24" height="24" role="img" title="Open Article Externally" />
</a>{' '}
</h3>
</section>
<section>
<p>Reading time: {article.reading_time} minutes</p>
<div class="tagsStyles">
<p>Tags:</p>
{#each article.tags as tag}
<p>{tag}</p>
{/each}
</div>
</section>
</article>
{/each}
</div>
<div class="moreArticlesStyles">
<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>
<style lang="postcss">
.articleStyles {
display: grid;
grid-template-rows: repeat(1fr, 3);
gap: 0.5rem;
margin: 1.5rem 0;
a {
overflow-wrap: anywhere;
}
p {
margin: 0.4rem 0.25rem;
}
}
.tagsStyles {
display: flex;
flex-wrap: wrap;
flex-direction: row;
justify-content: left;
align-items: center;
p + p {
background-color: var(--linkHover);
color: var(--buttonTextColor);
padding: 0.25rem 0.5rem;
margin: 0.5rem;
border-radius: 2px;
font-size: 1.2rem;
}
}
.moreArticlesStyles {
margin: 1.7rem;
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
gap: 1rem;
a {
font-size: 2rem;
svg {
text-decoration: none;
}
}
@media (max-width: 1000px) {
font-size: 1.5rem;
}
}
</style>

View file

@ -0,0 +1,90 @@
<script lang="ts">
import type { Album } from "$root/lib/types/album";
export let albums: Album[];
const displayAlbums =
albums?.length > 6 ? albums.slice(0, 6) : albums;
</script>
<div>
<h2>Currently listening to:</h2>
<div class="albumsStyles">
{#each displayAlbums as album}
<div>
<figure>
<a
title={`Link to ${album.title} by ${album.artist}`}
target="_blank"
href={album.url}
rel="noreferrer"
>
<img
src={`https://images.weserv.nl/?url=${encodeURIComponent(
album.artwork
)}&w=230&h=230`}
alt={`Album art for ${album.title}`}
/>
</a>
</figure>
<a
target="_blank"
href={album.url}
rel="noreferrer"
>
<h3>{album.title.length > 20 ? `${album.title.slice(0, 20)}...` : album.title}</h3>
<h3>by {album.artist}</h3>
</a>
</div>
{/each}
</div>
</div>
<style lang="postcss">
.albumsStyles {
display: grid;
grid-template-columns: repeat(3, minmax(auto, 1fr));
gap: 1rem;
@media (max-width: 1000px) {
grid-template-columns: repeat(2, minmax(150px, 1fr));
img {
width: 230px;
height: 100%;
object-fit: cover;
}
}
@media (max-width: 575px) {
height: 500px;
overflow-x: hidden;
overflow-y: auto;
::-webkit-scrollbar {
width: 12px;
}
scrollbar-width: thin;
scrollbar-color: var(--lightGrey) var(--darkGrey);
::-webkit-scrollbar-track {
background: var(--darkGrey);
}
::-webkit-scrollbar-thumb {
background-color: var(--lightGrey);
border-radius: 6px;
border: 3px solid var(--darkGrey);
}
grid-template-columns: minmax(230px, 1fr);
}
}
.albumStyles {
display: grid;
justify-content: center;
text-align: center;
@media (max-width: 550px) {
grid-template-columns: 0.75fr 0.75fr;
font-size: 1rem;
align-items: center;
}
}
</style>

6
src/lib/types/album.ts Normal file
View file

@ -0,0 +1,6 @@
export type Album = {
url: string;
artwork: string;
title: string;
artist: string;
};

View file

@ -0,0 +1,40 @@
// import * as htmlparser2 from 'htmlparser2';
import scrapeIt from 'scrape-it';
import type { Album } from '../types/album';
export async function fetchBandcampAlbums() {
try {
const { data } = await scrapeIt('https://bandcamp.com/royvalentine', {
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 || [];
console.log(`Albums ${JSON.stringify(albums)}`);
if (albums && albums?.length > 0) {
return albums;
} else {
return [];
}
} catch (error) {
console.log(error);
}
}

View file

@ -0,0 +1,16 @@
import type { PageServerLoad } from './lib/$types';
import { PAGE_SIZE } from '$env/static/private';
import { fetchBandcampAlbums } from '$root/lib/util/fetchBandcampAlbums';
export const load: PageServerLoad = async ({ fetch, setHeaders }) => {
const albums = async () => await fetchBandcampAlbums();
const articles = async () => await fetch(`/api/articles?page=1&limit=3`);
setHeaders({
'cache-control': 'max-age=43200'
});
return {
albums: albums(),
articlesData: (await articles()).json()
};
};

View file

@ -1,7 +1,21 @@
<script lang="ts"> <script lang="ts">
import SEO from '$root/lib/components/SEO.svelte'; import type { PageData } from './$types';
import Bandcamp from '$lib/components/bandcamp/index.svelte';
import Articles from '$lib/components/articles/index.svelte';
import SEO from '$lib/components/SEO.svelte';
import type { Album } from '$lib/types/album';
import type { Article } from '$lib/types/article';
import type { ArticlePageLoad } from './articles/[page]/+page.server';
export let data: PageData;
let albums: Album[];
let articlesData: ArticlePageLoad;
let articles: Article[];
let totalArticles: number;
$: ({ albums, articlesData } = data);
$: ({ articles, totalArticles } = articlesData);
$: console.log(`All data: ${JSON.stringify(articlesData)}`);
// export let data: PageData;
const userNames = { const userNames = {
github: 'BradNut', github: 'BradNut',
linkedIn: 'bradley-shellnut', linkedIn: 'bradley-shellnut',
@ -57,8 +71,8 @@
</p> </p>
</div> </div>
<div class="social-info"> <div class="social-info">
<!-- <Bandcamp /> --> <Bandcamp {albums} />
<!-- <Articles /> --> <Articles {articles} {totalArticles} compact={true} />
</div> </div>
</div> </div>

View file

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { Image, Picture } from 'svelte-lazy-loader'; import { Picture } from 'svelte-lazy-loader';
import Graphql from '@iconify-icons/simple-icons/graphql'; import Graphql from '@iconify-icons/simple-icons/graphql';
import Nextdotjs from '@iconify-icons/simple-icons/next-dot-js'; import Nextdotjs from '@iconify-icons/simple-icons/next-dot-js';
import Prisma from '@iconify-icons/simple-icons/prisma'; import Prisma from '@iconify-icons/simple-icons/prisma';

View file

@ -0,0 +1 @@
export const prerender = true;

View file

@ -4,8 +4,9 @@ import {
WALLABAG_USERNAME, WALLABAG_USERNAME,
WALLABAG_PASSWORD, WALLABAG_PASSWORD,
WALLABAG_URL, WALLABAG_URL,
WALLABAG_MAX_ARTICLES, WALLABAG_MAX_PAGES,
PAGE_SIZE, PAGE_SIZE,
WALLABAG_MAX_ARTICLES,
USE_REDIS_CACHE USE_REDIS_CACHE
} from '$env/static/private'; } from '$env/static/private';
import intersect from 'just-intersect'; import intersect from 'just-intersect';
@ -61,6 +62,29 @@ export async function fetchArticlesApi(
const auth = await authResponse.json(); const auth = await authResponse.json();
const pageQuery: PageQuery = {
sort: 'updated',
perPage: +queryParams?.limit || +PAGE_SIZE,
since: 0,
page: +queryParams?.page || 1,
tags: 'programming',
content: 'metadata'
};
const entriesQueryParams = new URLSearchParams({
...pageQuery,
perPage: `${pageQuery.perPage}`,
since: `${pageQuery.since}`,
page: `${pageQuery.page}`
});
console.log(`Entries params: ${entriesQueryParams}`);
if (lastFetched) {
pageQuery.since = Math.round(lastFetched / 1000);
}
lastFetched = new Date();
const nbEntries = 0;
const pageResponse = await fetch(`${WALLABAG_URL}/api/entries.json?${entriesQueryParams}`, { const pageResponse = await fetch(`${WALLABAG_URL}/api/entries.json?${entriesQueryParams}`, {
method: 'GET', method: 'GET',
headers: { headers: {
@ -92,7 +116,7 @@ export async function fetchArticlesApi(
articles.push({ articles.push({
tags, tags,
title: article.title, title: article.title,
url: article.url, url: new URL(article.url),
hashed_url: article.hashed_url, hashed_url: article.hashed_url,
reading_time: article.reading_time, reading_time: article.reading_time,
preview_picture: article.preview_picture, preview_picture: article.preview_picture,
@ -103,12 +127,12 @@ export async function fetchArticlesApi(
} }
}); });
const responseData = { return {
articles, articles,
currentPage: page, currentPage: page,
totalPages: pages, totalPages: pages > +WALLABAG_MAX_PAGES ? +WALLABAG_MAX_PAGES : pages,
limit, limit,
totalArticles: total, totalArticles: total > +WALLABAG_MAX_ARTICLES ? +WALLABAG_MAX_ARTICLES : total,
cacheControl cacheControl
}; };

View file

@ -1,17 +1,37 @@
import { json } from '@sveltejs/kit'; import { json, error } from '@sveltejs/kit';
import { WALLABAG_MAX_PAGES } from '$env/static/private';
import type { RequestHandler, RequestEvent } from './$types'; import type { RequestHandler, RequestEvent } from './$types';
import { fetchArticlesApi } from '$root/routes/api'; import { fetchArticlesApi } from '$root/routes/api';
export const GET: RequestHandler = async ({ url }: RequestEvent) => { export const GET: RequestHandler = async ({ url }: RequestEvent) => {
try { try {
const page = url?.searchParams?.get('page') || '1';
if (+page > +WALLABAG_MAX_PAGES) {
throw new Error('Page does not exist');
}
const response = await fetchArticlesApi('get', `fetchArticles`, { const response = await fetchArticlesApi('get', `fetchArticles`, {
page: url?.searchParams?.get('page') || '1' page,
limit: url?.searchParams?.get('limit') || '6'
}); });
if (response?.articles) { if (response?.articles) {
if (response?.cacheControl) {
if (!response.cacheControl.includes('no-cache')) {
setHeaders({
'cache-control': response?.cacheControl
});
} else {
setHeaders({
'cache-control': 'max-age=43200'
});
}
}
console.log(`API response ${JSON.stringify(response)}`);
return json(response); return json(response);
} }
} catch (error) { } catch (e) {
console.error(error); console.error(e);
throw error(404, 'Page does not exist');
} }
}; };

View file

@ -1,5 +1,7 @@
import { error } from '@sveltejs/kit';
import type { PageServerLoad } from './$types'; import type { PageServerLoad } from './$types';
import type { Article } from '$root/lib/types/article'; import { WALLABAG_MAX_PAGES } from '$env/static/private';
import type { Article } from '$lib/types/article';
export type ArticlePageLoad = { export type ArticlePageLoad = {
articles: Article[]; articles: Article[];
@ -12,6 +14,11 @@ export type ArticlePageLoad = {
export const load: PageServerLoad = async ({ fetch, params, setHeaders }) => { export const load: PageServerLoad = async ({ fetch, params, setHeaders }) => {
const { page } = params; const { page } = params;
if (+page > +WALLABAG_MAX_PAGES) {
throw error(404, {
message: 'Not found'
});
}
const resp = await fetch(`/api/articles?page=${page}`); const resp = await fetch(`/api/articles?page=${page}`);
const { articles, currentPage, totalPages, limit, totalArticles, cacheControl }: ArticlePageLoad = const { articles, currentPage, totalPages, limit, totalArticles, cacheControl }: ArticlePageLoad =
await resp.json(); await resp.json();

View file

@ -37,7 +37,7 @@
<a <a
target="_blank" target="_blank"
aria-label={`Link to ${article.title}`} aria-label={`Link to ${article.title}`}
href={article.url.href} href={article.url.toString()}
rel="noreferrer" rel="noreferrer"
> >
{article.title} {article.title}

View file

@ -7,7 +7,6 @@
TabPanels, TabPanels,
} from "@rgossiaux/svelte-headlessui"; } from "@rgossiaux/svelte-headlessui";
import { Image, Picture } from "svelte-lazy-loader"; import { Image, Picture } from "svelte-lazy-loader";
import SEO from "$root/lib/components/SEO.svelte";
import personalSite from "$lib/assets/images/Bradley_Shellnut_New_Site.png?format=webp;avif;png&metadata"; import personalSite from "$lib/assets/images/Bradley_Shellnut_New_Site.png?format=webp;avif;png&metadata";
import personalSiteBlurred from "$lib/assets/images/Bradley_Shellnut_New_Site.png?w=100&png&blur=10"; import personalSiteBlurred from "$lib/assets/images/Bradley_Shellnut_New_Site.png?w=100&png&blur=10";
import weddingWebsite from "$lib/assets/images/Wedding_Website.png?format=webp;avif;png&metadata"; import weddingWebsite from "$lib/assets/images/Wedding_Website.png?format=webp;avif;png&metadata";
@ -18,7 +17,9 @@
import shellnutArchitectWebsiteBlurred from "$lib/assets/images/Mark_Shellnut_Architect.png?w=100&png&blur=10"; import shellnutArchitectWebsiteBlurred from "$lib/assets/images/Mark_Shellnut_Architect.png?w=100&png&blur=10";
</script> </script>
<SEO title="Portfolio" /> <svelte:head>
<title>Portfolio | Bradley Shellnut</title>
</svelte:head>
<h1>Portfolio!</h1> <h1>Portfolio!</h1>
<div class="portfolioTabStyles"> <div class="portfolioTabStyles">

View file

@ -0,0 +1 @@
export const prerender = true;

View file

@ -0,0 +1 @@
export const prerender = true;

View file

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { Image, Picture } from "svelte-lazy-loader"; import { Picture } from "svelte-lazy-loader";
import SEO from "$root/lib/components/SEO.svelte"; import SEO from "$root/lib/components/SEO.svelte";
import desktop from '$lib/assets/images/Desktop_so_clean.jpg?format=webp;avif;jpg&metadata'; import desktop from '$lib/assets/images/Desktop_so_clean.jpg?format=webp;avif;jpg&metadata';
import desktopBlurred from '$lib/assets/images/Desktop_so_clean.jpg?w=100&jpg&blur=10'; import desktopBlurred from '$lib/assets/images/Desktop_so_clean.jpg?w=100&jpg&blur=10';

1
src/routes/uses/+page.ts Normal file
View file

@ -0,0 +1 @@
export const prerender = true;