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_CLIENT_ID=
WALLABAG_CLIENT_SECRET=
WALLABAG_USERNAME=
WALLABAG_PASSWORD=
WALLABAG_URL="https://app.wallabag.it"
PUBLIC_SITE_URL=
WALLABAG_MAX_PAGES=5
NODE_VERSION=18.12.1
WALLABAG_CLIENT_ID=''
WALLABAG_CLIENT_SECRET=''
WALLABAG_USERNAME=''
WALLABAG_PASSWORD=''
WALLABAG_URL=""
PUBLIC_SITE_URL=""
PUBLIC_UMAMI_DO_NOT_TRACK=true
PUBLIC_UMAMI_URL=
PUBLIC_UMAMI_ID=
PUBLIC_UMAMI_URL=""
PUBLIC_UMAMI_ID=""
PAGE_SIZE=6
USE_REDIS_CACHE=true
REDIS_URI=redis://{username}:{password}@{redisURL}:{redisPort}
REDIS_URI=

View file

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

View file

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

View file

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

View file

@ -1,6 +1,5 @@
<script lang="ts">
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>
<svelte:head>
@ -9,6 +8,6 @@
defer
data-website-id={PUBLIC_UMAMI_ID}
data-do-not-track={PUBLIC_UMAMI_DO_NOT_TRACK}
src={analyticsURL}
src={PUBLIC_UMAMI_URL}
></script>
</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">
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;
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)}`);
const userNames = {
github: 'BradNut',
linkedIn: 'bradley-shellnut',
@ -57,8 +71,8 @@
</p>
</div>
<div class="social-info">
<!-- <Bandcamp /> -->
<!-- <Articles /> -->
<Bandcamp {albums} />
<Articles {articles} {totalArticles} compact={true} />
</div>
</div>

View file

@ -1,5 +1,5 @@
<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 Nextdotjs from '@iconify-icons/simple-icons/next-dot-js';
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_PASSWORD,
WALLABAG_URL,
WALLABAG_MAX_ARTICLES,
WALLABAG_MAX_PAGES,
PAGE_SIZE,
WALLABAG_MAX_ARTICLES,
USE_REDIS_CACHE
} from '$env/static/private';
import intersect from 'just-intersect';
@ -61,6 +62,29 @@ export async function fetchArticlesApi(
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}`, {
method: 'GET',
headers: {
@ -92,7 +116,7 @@ export async function fetchArticlesApi(
articles.push({
tags,
title: article.title,
url: article.url,
url: new URL(article.url),
hashed_url: article.hashed_url,
reading_time: article.reading_time,
preview_picture: article.preview_picture,
@ -103,12 +127,12 @@ export async function fetchArticlesApi(
}
});
const responseData = {
return {
articles,
currentPage: page,
totalPages: pages,
totalPages: pages > +WALLABAG_MAX_PAGES ? +WALLABAG_MAX_PAGES : pages,
limit,
totalArticles: total,
totalArticles: total > +WALLABAG_MAX_ARTICLES ? +WALLABAG_MAX_ARTICLES : total,
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 { fetchArticlesApi } from '$root/routes/api';
export const GET: RequestHandler = async ({ url }: RequestEvent) => {
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`, {
page: url?.searchParams?.get('page') || '1'
page,
limit: url?.searchParams?.get('limit') || '6'
});
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);
}
} catch (error) {
console.error(error);
} catch (e) {
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 { Article } from '$root/lib/types/article';
import { WALLABAG_MAX_PAGES } from '$env/static/private';
import type { Article } from '$lib/types/article';
export type ArticlePageLoad = {
articles: Article[];
@ -12,6 +14,11 @@ export type ArticlePageLoad = {
export const load: PageServerLoad = async ({ fetch, params, setHeaders }) => {
const { page } = params;
if (+page > +WALLABAG_MAX_PAGES) {
throw error(404, {
message: 'Not found'
});
}
const resp = await fetch(`/api/articles?page=${page}`);
const { articles, currentPage, totalPages, limit, totalArticles, cacheControl }: ArticlePageLoad =
await resp.json();

View file

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

View file

@ -7,7 +7,6 @@
TabPanels,
} from "@rgossiaux/svelte-headlessui";
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 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";
@ -18,7 +17,9 @@
import shellnutArchitectWebsiteBlurred from "$lib/assets/images/Mark_Shellnut_Architect.png?w=100&png&blur=10";
</script>
<SEO title="Portfolio" />
<svelte:head>
<title>Portfolio | Bradley Shellnut</title>
</svelte:head>
<h1>Portfolio!</h1>
<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">
import { Image, Picture } from "svelte-lazy-loader";
import { Picture } from "svelte-lazy-loader";
import SEO from "$root/lib/components/SEO.svelte";
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';

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

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