mirror of
https://github.com/BradNut/personal-website-sveltekit
synced 2025-09-08 23:20:18 +00:00
Merge pull request #1 from BradNut/pagination-articles
Pagination articles
This commit is contained in:
commit
09baf97ed6
20 changed files with 481 additions and 392 deletions
|
|
@ -22,6 +22,7 @@
|
||||||
"@playwright/test": "^1.28.1",
|
"@playwright/test": "^1.28.1",
|
||||||
"@rgossiaux/svelte-headlessui": "^1.0.2",
|
"@rgossiaux/svelte-headlessui": "^1.0.2",
|
||||||
"@sveltejs/adapter-auto": "^1.0.0",
|
"@sveltejs/adapter-auto": "^1.0.0",
|
||||||
|
"@sveltejs/adapter-static": "^2.0.0",
|
||||||
"@sveltejs/adapter-vercel": "^1.0.5",
|
"@sveltejs/adapter-vercel": "^1.0.5",
|
||||||
"@sveltejs/kit": "^1.0.0",
|
"@sveltejs/kit": "^1.0.0",
|
||||||
"@types/postcss-preset-env": "^8.0.0",
|
"@types/postcss-preset-env": "^8.0.0",
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ specifiers:
|
||||||
'@playwright/test': ^1.28.1
|
'@playwright/test': ^1.28.1
|
||||||
'@rgossiaux/svelte-headlessui': ^1.0.2
|
'@rgossiaux/svelte-headlessui': ^1.0.2
|
||||||
'@sveltejs/adapter-auto': ^1.0.0
|
'@sveltejs/adapter-auto': ^1.0.0
|
||||||
|
'@sveltejs/adapter-static': ^2.0.0
|
||||||
'@sveltejs/adapter-vercel': ^1.0.5
|
'@sveltejs/adapter-vercel': ^1.0.5
|
||||||
'@sveltejs/kit': ^1.0.0
|
'@sveltejs/kit': ^1.0.0
|
||||||
'@types/postcss-preset-env': ^8.0.0
|
'@types/postcss-preset-env': ^8.0.0
|
||||||
|
|
@ -48,6 +49,7 @@ devDependencies:
|
||||||
'@playwright/test': 1.29.2
|
'@playwright/test': 1.29.2
|
||||||
'@rgossiaux/svelte-headlessui': 1.0.2_svelte@3.55.1
|
'@rgossiaux/svelte-headlessui': 1.0.2_svelte@3.55.1
|
||||||
'@sveltejs/adapter-auto': 1.0.2_@sveltejs+kit@1.2.2
|
'@sveltejs/adapter-auto': 1.0.2_@sveltejs+kit@1.2.2
|
||||||
|
'@sveltejs/adapter-static': 2.0.0_@sveltejs+kit@1.2.2
|
||||||
'@sveltejs/adapter-vercel': 1.0.5_@sveltejs+kit@1.2.2
|
'@sveltejs/adapter-vercel': 1.0.5_@sveltejs+kit@1.2.2
|
||||||
'@sveltejs/kit': 1.2.2_svelte@3.55.1+vite@4.0.4
|
'@sveltejs/kit': 1.2.2_svelte@3.55.1+vite@4.0.4
|
||||||
'@types/postcss-preset-env': 8.0.0_postcss@8.4.21
|
'@types/postcss-preset-env': 8.0.0_postcss@8.4.21
|
||||||
|
|
@ -696,6 +698,14 @@ packages:
|
||||||
import-meta-resolve: 2.2.1
|
import-meta-resolve: 2.2.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@sveltejs/adapter-static/2.0.0_@sveltejs+kit@1.2.2:
|
||||||
|
resolution: {integrity: sha512-M9F8EyCIdVcpZu0zvS7U7v+rc7elNNDUkMHLsOkJYlfQ9rDdif3fqteMqddcrq1lFPEGH1ErdTjpsXdb3k1e8w==}
|
||||||
|
peerDependencies:
|
||||||
|
'@sveltejs/kit': ^1.5.0
|
||||||
|
dependencies:
|
||||||
|
'@sveltejs/kit': 1.2.2_svelte@3.55.1+vite@4.0.4
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@sveltejs/adapter-vercel/1.0.5_@sveltejs+kit@1.2.2:
|
/@sveltejs/adapter-vercel/1.0.5_@sveltejs+kit@1.2.2:
|
||||||
resolution: {integrity: sha512-TnFw+2ZwUuzguIeh013h0eiFmHylBuL1aV3jcUWbJmBLRo6YA0Ti8X8VXgFP4izP7XGuLKdpUIMg7HaZe21LbA==}
|
resolution: {integrity: sha512-TnFw+2ZwUuzguIeh013h0eiFmHylBuL1aV3jcUWbJmBLRo6YA0Ti8X8VXgFP4izP7XGuLKdpUIMg7HaZe21LbA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
export let image = defaultMetadata.defaultImage;
|
export let image = defaultMetadata.defaultImage;
|
||||||
export let location: string = '';
|
export let location: string = '';
|
||||||
|
|
||||||
const seo = {
|
$: seo = {
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
image: `${siteUrl}${image}`,
|
image: `${siteUrl}${image}`,
|
||||||
|
|
@ -23,7 +23,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>{`${seo.title} - ${defaultMetadata.defaultTitle}`}</title>
|
<title>{`${seo.title} | ${defaultMetadata.defaultTitle}`}</title>
|
||||||
<link rel="icon" type="image/gif" href="/b_shell_nut_favicon.gif" />
|
<link rel="icon" type="image/gif" href="/b_shell_nut_favicon.gif" />
|
||||||
<meta name="description" content={seo.description} />
|
<meta name="description" content={seo.description} />
|
||||||
<meta name="og:type" content="website" />
|
<meta name="og:type" content="website" />
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
<a class:active={$page.url.pathname === '/portfolio'} href="/portfolio">Portfolio</a>
|
<a class:active={$page.url.pathname === '/portfolio'} href="/portfolio">Portfolio</a>
|
||||||
<a class:active={$page.url.pathname === '/uses'} href="/uses">Uses</a>
|
<a class:active={$page.url.pathname === '/uses'} href="/uses">Uses</a>
|
||||||
<a class:active={$page.url.pathname === '/privacy'} href="/privacy">Privacy</a>
|
<a class:active={$page.url.pathname === '/privacy'} href="/privacy">Privacy</a>
|
||||||
<a class:active={$page.url.pathname === '/articles'} href="/articles">Favorite Articles</a>
|
<a class:active={$page.url.pathname === '/articles/1'} href="/articles">Favorite Articles</a>
|
||||||
</nav>
|
</nav>
|
||||||
<!-- <p className="center"> -->
|
<!-- <p className="center"> -->
|
||||||
<p>
|
<p>
|
||||||
|
|
|
||||||
78
src/lib/components/pagination/index.svelte
Normal file
78
src/lib/components/pagination/index.svelte
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
<script lang="ts">
|
||||||
|
export let additionalClasses: string;
|
||||||
|
export let pageSize: number;
|
||||||
|
export let totalCount: number;
|
||||||
|
export let currentPage: number;
|
||||||
|
export let skip: number;
|
||||||
|
export let base: string;
|
||||||
|
|
||||||
|
// make some variables
|
||||||
|
$: totalPages = Math.ceil(totalCount / pageSize);
|
||||||
|
$: prevPage = currentPage - 1;
|
||||||
|
$: nextPage = currentPage + 1;
|
||||||
|
$: hasNextPage = nextPage <= totalPages;
|
||||||
|
$: hasPrevPage = prevPage >= 1;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class={`paginationStyles ${additionalClasses}`}>
|
||||||
|
<a
|
||||||
|
title={`${!hasPrevPage ? 'No ' : ''}Prev Page`}
|
||||||
|
aria-disabled={!hasPrevPage}
|
||||||
|
href={`${base}/${prevPage}`}
|
||||||
|
>
|
||||||
|
← <span class="word">Prev</span>
|
||||||
|
</a>
|
||||||
|
{#each { length: totalPages } as _, i}
|
||||||
|
<a
|
||||||
|
aria-current={currentPage === i + 1}
|
||||||
|
href={`${base}/${i + 1}`}
|
||||||
|
>
|
||||||
|
{i + 1}
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
<a
|
||||||
|
title={`${!hasNextPage ? 'No ' : ''}Next Page`}
|
||||||
|
aria-disabled={!hasNextPage}
|
||||||
|
href={`${base}/${nextPage}`}
|
||||||
|
>
|
||||||
|
<span class="word">Next</span> →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="postcss">
|
||||||
|
a {
|
||||||
|
&[aria-current="true"] {
|
||||||
|
color: var(--shellYellow)
|
||||||
|
}
|
||||||
|
|
||||||
|
&[aria-disabled="true"] {
|
||||||
|
pointer-events: none;
|
||||||
|
color: var(--lightGrey);
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.paginationStyles {
|
||||||
|
display: flex;
|
||||||
|
align-content: center;
|
||||||
|
align-items: center;
|
||||||
|
justify-items: center;
|
||||||
|
border: 1px solid var(--grey);
|
||||||
|
margin: 3rem 0;
|
||||||
|
border-radius: 5px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
padding: 1rem;
|
||||||
|
flex: 1;
|
||||||
|
border-right: 1px solid var(--grey);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
@media (max-width: 800px) {
|
||||||
|
.word {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -6,6 +6,7 @@ const state = () => {
|
||||||
const { subscribe, set, update } = writable<Article[]>([]);
|
const { subscribe, set, update } = writable<Article[]>([]);
|
||||||
|
|
||||||
function addAll(articles: Article[]) {
|
function addAll(articles: Article[]) {
|
||||||
|
// console.log(typeof articles);
|
||||||
for (const article of articles) {
|
for (const article of articles) {
|
||||||
add(article);
|
add(article);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
export type PageQuery {
|
export type PageQuery = {
|
||||||
sort: string;
|
sort: string;
|
||||||
perPage: number;
|
perPage: number;
|
||||||
since: number;
|
since: number;
|
||||||
}
|
page: number;
|
||||||
|
tags: string;
|
||||||
|
content: 'metadata' | 'full';
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
export const prerender = true;
|
|
||||||
import { type ServerLoad } from '@sveltejs/kit';
|
|
||||||
|
|
||||||
export const load: ServerLoad = async ({ isDataRequest, cookies, params, setHeaders }) => {
|
|
||||||
const queryParams = {
|
|
||||||
// ids: `${params?.id}`,
|
|
||||||
// fields:
|
|
||||||
// 'id,name,price,min_age,min_players,max_players,thumb_url,playtime,min_playtime,max_playtime,min_age,description,year_published,url,image_url'
|
|
||||||
};
|
|
||||||
|
|
||||||
const initialRequest = !isDataRequest;
|
|
||||||
console.log(`Is initialRequest: ${initialRequest}`);
|
|
||||||
|
|
||||||
const cacheValue = `${initialRequest ? +new Date() : cookies.get('articles-cache')}`;
|
|
||||||
console.log(`Cache Value: ${cacheValue}`);
|
|
||||||
|
|
||||||
if (initialRequest) {
|
|
||||||
cookies.set('articles-cache', cacheValue, { path: '/', httpOnly: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
articlesCacheBust: cacheValue
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { PageData } from "./$types";
|
|
||||||
import SEO from '$root/lib/components/SEO.svelte';
|
import SEO from '$root/lib/components/SEO.svelte';
|
||||||
|
|
||||||
export let data: PageData;
|
// export let data: PageData;
|
||||||
const userNames = {
|
const userNames = {
|
||||||
github: 'BradNut',
|
github: 'BradNut',
|
||||||
linkedIn: 'bradley-shellnut',
|
linkedIn: 'bradley-shellnut',
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export const prerender = true;
|
|
||||||
|
|
@ -3,7 +3,9 @@ import {
|
||||||
WALLABAG_CLIENT_SECRET,
|
WALLABAG_CLIENT_SECRET,
|
||||||
WALLABAG_USERNAME,
|
WALLABAG_USERNAME,
|
||||||
WALLABAG_PASSWORD,
|
WALLABAG_PASSWORD,
|
||||||
WALLABAG_URL
|
WALLABAG_URL,
|
||||||
|
WALLABAG_MAX_ARTICLES,
|
||||||
|
PAGE_SIZE
|
||||||
} from '$env/static/private';
|
} from '$env/static/private';
|
||||||
import intersect from 'just-intersect';
|
import intersect from 'just-intersect';
|
||||||
import type { Article, WallabagArticle } from '$root/lib/types/article';
|
import type { Article, WallabagArticle } from '$root/lib/types/article';
|
||||||
|
|
@ -39,8 +41,11 @@ export async function fetchArticlesApi(
|
||||||
|
|
||||||
const pageQuery: PageQuery = {
|
const pageQuery: PageQuery = {
|
||||||
sort: 'updated',
|
sort: 'updated',
|
||||||
perPage: 500,
|
perPage: +PAGE_SIZE,
|
||||||
since: 0
|
since: 0,
|
||||||
|
page: +queryParams?.page || 1,
|
||||||
|
tags: 'programming',
|
||||||
|
content: 'metadata'
|
||||||
};
|
};
|
||||||
const entriesQueryParams = new URLSearchParams(pageQuery);
|
const entriesQueryParams = new URLSearchParams(pageQuery);
|
||||||
console.log(`Entries params: ${entriesQueryParams}`);
|
console.log(`Entries params: ${entriesQueryParams}`);
|
||||||
|
|
@ -63,17 +68,17 @@ export async function fetchArticlesApi(
|
||||||
throw new Error(pageResponse.statusText);
|
throw new Error(pageResponse.statusText);
|
||||||
}
|
}
|
||||||
|
|
||||||
const entries = await pageResponse.json();
|
const { _embedded, page, pages, total, limit } = await pageResponse.json();
|
||||||
const articles: Article[] = [];
|
const articles: Article[] = [];
|
||||||
|
|
||||||
// do {
|
// do {
|
||||||
// nbEntries += entries._embedded.items.length;
|
// nbEntries += entries._embedded.items.length;
|
||||||
console.log(`number of articles fetched: ${entries._embedded.items.length}`);
|
console.log(`number of articles fetched: ${_embedded.items.length}`);
|
||||||
entries._embedded.items.forEach((article: WallabagArticle) => {
|
_embedded.items.forEach((article: WallabagArticle) => {
|
||||||
if (articles?.length === 30) {
|
// if (articles?.length === +WALLABAG_MAX_ARTICLES) {
|
||||||
console.log('Reached 30 articles');
|
// console.log('Reached 30 articles');
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
const rawTags = article?.tags?.map((tag) => tag.slug);
|
const rawTags = article?.tags?.map((tag) => tag.slug);
|
||||||
if (intersect(rawTags, Object.values(ArticleTag))?.length > 0) {
|
if (intersect(rawTags, Object.values(ArticleTag))?.length > 0) {
|
||||||
const tags = rawTags.map((rawTag) => rawTag as unknown as ArticleTag);
|
const tags = rawTags.map((rawTag) => rawTag as unknown as ArticleTag);
|
||||||
|
|
@ -105,5 +110,5 @@ export async function fetchArticlesApi(
|
||||||
// entries = await response.json();
|
// entries = await response.json();
|
||||||
// } while (entries._links.next);
|
// } while (entries._links.next);
|
||||||
|
|
||||||
return { articles };
|
return { articles, currentPage: page, totalPages: pages, limit, totalArticles: total };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,25 +2,27 @@ import { json } from '@sveltejs/kit';
|
||||||
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, setHeaders, request, params }: RequestEvent) => {
|
export const GET: RequestHandler = async ({ url, setHeaders }: RequestEvent) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetchArticlesApi('get', `search`, {});
|
const response = await fetchArticlesApi('get', `fetchArticles`, {
|
||||||
|
page: url?.searchParams?.get('page') || '1'
|
||||||
|
});
|
||||||
|
|
||||||
if (response?.articles) {
|
if (response?.articles) {
|
||||||
setHeaders({
|
setHeaders({
|
||||||
'cache-control': 'max-age=604800'
|
'cache-control': 'max-age=60'
|
||||||
});
|
});
|
||||||
|
|
||||||
const articlesResponse = response.articles;
|
// const articlesResponse = response.articles;
|
||||||
console.log(`Found articles ${articlesResponse.length}`);
|
// console.log(`Found articles ${articlesResponse?.articles?.length}`);
|
||||||
const articles = [];
|
// const articles = [];
|
||||||
|
|
||||||
for (const article of articlesResponse) {
|
// for (const article of articlesResponse) {
|
||||||
const { tags, title, url, hashed_url, reading_time, preview_picture } = article;
|
// const { tags, title, url, hashed_url, reading_time, preview_picture } = article;
|
||||||
articles.push({ tags, title, url, hashed_url, reading_time, preview_picture });
|
// articles.push({ tags, title, url, hashed_url, reading_time, preview_picture });
|
||||||
}
|
// }
|
||||||
|
|
||||||
return json(articles);
|
return json(response);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|
|
||||||
|
|
@ -1,120 +1 @@
|
||||||
<script lang="ts">
|
<h1>Articles</h1>
|
||||||
import { page } from "$app/stores";
|
|
||||||
import { Article } from "$lib/types/article";
|
|
||||||
import { ArticleTag } from "$lib/types/articleTag";
|
|
||||||
import SEO from "$root/lib/components/SEO.svelte";
|
|
||||||
|
|
||||||
$: ({ articles } = $page.data);
|
|
||||||
|
|
||||||
const currentPage: number = 1;
|
|
||||||
// const articles = data?.articles?.nodes;
|
|
||||||
// const maxArticles = parseInt(process.env.GATSBY_WALLABAG_MAX_ARTICLES);
|
|
||||||
// const totalCount =
|
|
||||||
// data.articles.totalCount > maxArticles ? maxArticles : articles.length;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<SEO title={`Tech Articles - Page ${currentPage}`} />
|
|
||||||
|
|
||||||
<div class="pageStyles">
|
|
||||||
<h1 style="margin-bottom: 2rem">Favorite Tech Articles</h1>
|
|
||||||
<!-- <Pagination
|
|
||||||
clazz="top-pagination"
|
|
||||||
pageSize={parseInt(process.env.GATSBY_PAGE_SIZE)}
|
|
||||||
totalCount={totalCount}
|
|
||||||
currentPage={pageContext.currentPage || 1}
|
|
||||||
skip={pageContext.skip}
|
|
||||||
base="/articles"
|
|
||||||
/> -->
|
|
||||||
<div class="articlesStyles">
|
|
||||||
{#each articles as article (article.hashed_url)}
|
|
||||||
<div class="articleStyles card">
|
|
||||||
<section>
|
|
||||||
<h3>
|
|
||||||
<a
|
|
||||||
target="_blank"
|
|
||||||
aria-label={`Link to ${article.title}`}
|
|
||||||
href={article.url}
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
{article.title}
|
|
||||||
</a>
|
|
||||||
</h3>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<p>Reading time: {article.reading_time} minutes</p>
|
|
||||||
<div class="tagStyles">
|
|
||||||
<p>Tags:</p>
|
|
||||||
{#each article.tags as tag}
|
|
||||||
<p>{tag}</p>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
<!-- <Pagination
|
|
||||||
clazz="bottom-pagination"
|
|
||||||
pageSize={parseInt(process.env.GATSBY_PAGE_SIZE)}
|
|
||||||
totalCount={totalCount}
|
|
||||||
currentPage={pageContext.currentPage || 1}
|
|
||||||
skip={pageContext.skip}
|
|
||||||
base="/articles"
|
|
||||||
/> -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style lang="postcss">
|
|
||||||
.pageStyles {
|
|
||||||
.bottom-pagination {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 650px) {
|
|
||||||
.bottom-pagination {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.articlesStyles {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, minmax(250px, 1fr));
|
|
||||||
min-height: 800px;
|
|
||||||
|
|
||||||
@media (max-width: 1000px) {
|
|
||||||
grid-template-columns: repeat(2, minmax(250px, 1fr));
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 650px) {
|
|
||||||
grid-template-columns: minmax(250px, 1fr);
|
|
||||||
}
|
|
||||||
|
|
||||||
gap: 2.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.articleStyles {
|
|
||||||
display: grid;
|
|
||||||
grid-template-rows: 1fr auto;
|
|
||||||
align-items: start;
|
|
||||||
|
|
||||||
/* p {
|
|
||||||
margin: 0.4rem 0.25rem;
|
|
||||||
} */
|
|
||||||
}
|
|
||||||
|
|
||||||
.tagStyles {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,17 +1,6 @@
|
||||||
import { getCurrentCookieValue } from '$lib/util/cookieUtils';
|
import { redirect } from '@sveltejs/kit';
|
||||||
import type { Article } from '$root/lib/types/article';
|
import type { PageServerLoad } from './$types';
|
||||||
import type { PageLoad } from './$types';
|
|
||||||
|
|
||||||
export const load: PageLoad = async ({ fetch, parent, url, setHeaders }) => {
|
export const load = async () => {
|
||||||
const parentData = await parent();
|
throw redirect(302, '/articles/1');
|
||||||
|
|
||||||
const cacheBust = getCurrentCookieValue('articles-cache') || parentData.articlesCacheBust;
|
|
||||||
// const search = url.searchParams.get('search') || '';
|
|
||||||
|
|
||||||
const resp = await fetch(`/api/articles?cache=${cacheBust}`);
|
|
||||||
const articles: Article[] = await resp.json();
|
|
||||||
|
|
||||||
return {
|
|
||||||
articles
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
24
src/routes/articles/[page]/+page.server.ts
Normal file
24
src/routes/articles/[page]/+page.server.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import type { PageServerLoad } from './$types';
|
||||||
|
import type { Article } from '$root/lib/types/article';
|
||||||
|
|
||||||
|
export type ArticlePageLoad = {
|
||||||
|
articles: Article[];
|
||||||
|
currentPage: number;
|
||||||
|
totalPages: number;
|
||||||
|
limit: number;
|
||||||
|
totalArticles: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async ({ fetch, params }) => {
|
||||||
|
const { page } = params;
|
||||||
|
const resp = await fetch(`/api/articles?page=${page}`);
|
||||||
|
const { articles, currentPage, totalPages, limit, totalArticles }: ArticlePageLoad =
|
||||||
|
await resp.json();
|
||||||
|
return {
|
||||||
|
articles,
|
||||||
|
currentPage,
|
||||||
|
totalPages,
|
||||||
|
limit,
|
||||||
|
totalArticles
|
||||||
|
};
|
||||||
|
};
|
||||||
123
src/routes/articles/[page]/+page.svelte
Normal file
123
src/routes/articles/[page]/+page.svelte
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import Pagination from "$lib/components/pagination/index.svelte";
|
||||||
|
import SEO from "$root/lib/components/SEO.svelte";
|
||||||
|
import type { Article } from "$root/lib/types/article";
|
||||||
|
import type { PageData } from "./$types";
|
||||||
|
|
||||||
|
export let data: PageData;
|
||||||
|
let articles: Article[];
|
||||||
|
let currentPage: number;
|
||||||
|
let totalPages: number;
|
||||||
|
let totalArticles: number;
|
||||||
|
let limit: number;
|
||||||
|
$: ({ articles, currentPage, totalPages, totalArticles, limit } = data);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SEO title={`Tech Articles - Page ${currentPage}`} />
|
||||||
|
|
||||||
|
<div class="pageStyles">
|
||||||
|
<h1 style="margin-bottom: 2rem">Favorite Tech Articles</h1>
|
||||||
|
<Pagination
|
||||||
|
additionalClasses="top-pagination"
|
||||||
|
pageSize={limit}
|
||||||
|
totalCount={totalArticles}
|
||||||
|
currentPage={currentPage || 1}
|
||||||
|
skip={currentPage}
|
||||||
|
base="/articles"
|
||||||
|
/>
|
||||||
|
<div class="articlesStyles">
|
||||||
|
{#each articles as article (article.hashed_url)}
|
||||||
|
<div class="articleStyles card">
|
||||||
|
<section>
|
||||||
|
<h3>
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
aria-label={`Link to ${article.title}`}
|
||||||
|
href={article.url.href}
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
{article.title}
|
||||||
|
</a>
|
||||||
|
</h3>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<p>Reading time: {article.reading_time} minutes</p>
|
||||||
|
<div class="tagStyles">
|
||||||
|
<p>Tags:</p>
|
||||||
|
{#each article.tags as tag}
|
||||||
|
<p>{tag}</p>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
<Pagination
|
||||||
|
additionalClasses="bottom-pagination"
|
||||||
|
pageSize={limit}
|
||||||
|
totalCount={totalPages}
|
||||||
|
currentPage={currentPage || 1}
|
||||||
|
skip={currentPage}
|
||||||
|
base="/articles"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="postcss">
|
||||||
|
.pageStyles {
|
||||||
|
.bottom-pagination {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 650px) {
|
||||||
|
.bottom-pagination {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.articlesStyles {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(250px, 1fr));
|
||||||
|
min-height: 800px;
|
||||||
|
|
||||||
|
@media (max-width: 1000px) {
|
||||||
|
grid-template-columns: repeat(2, minmax(250px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 650px) {
|
||||||
|
grid-template-columns: minmax(250px, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
gap: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.articleStyles {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: 1fr auto;
|
||||||
|
align-items: start;
|
||||||
|
a {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* p {
|
||||||
|
margin: 0.4rem 0.25rem;
|
||||||
|
} */
|
||||||
|
}
|
||||||
|
|
||||||
|
.tagStyles {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export const prerender = true;
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export const prerender = true;
|
|
||||||
|
|
@ -5,216 +5,217 @@
|
||||||
<SEO title="Privacy Blog" />
|
<SEO title="Privacy Blog" />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<h1>Privacy</h1>
|
||||||
<h1>Privacy</h1>
|
<p>
|
||||||
<p>
|
Long story short, I believe everyone should know who has your personal
|
||||||
Long story short, I believe everyone should know who has your personal
|
data, how it is being collected/stored, and what it is being used for.
|
||||||
data, how it is being collected/stored, and what it is being used for.
|
</p>
|
||||||
</p>
|
<p>
|
||||||
<p>
|
However, it is ultimately up to each person to determine how much data
|
||||||
However, it is ultimately up to each person to determine how much data
|
they are willing to give to any business/entity.
|
||||||
they are willing to give to any business/entity.
|
</p>
|
||||||
</p>
|
<p>
|
||||||
<p>
|
For the sake of transparency I am using{' '}
|
||||||
For the sake of transparency I am using{' '}
|
<a
|
||||||
|
target="_blank"
|
||||||
|
aria-label="Umami Analytics FAQ"
|
||||||
|
href="https://umami.is/docs/faq"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
Umami Analytics
|
||||||
|
</a>{' '}
|
||||||
|
to anonymously track visits to my site. You can completely block this
|
||||||
|
if you want by either using an AdBlocker like{' '}
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
aria-label="uBlock Origin"
|
||||||
|
href="https://ublockorigin.com/"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
uBlock Origin
|
||||||
|
</a>{' '}
|
||||||
|
and/or set your browser to send "Do Not Track" requests as I honor
|
||||||
|
them.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 2.5rem;">
|
||||||
|
<h2>Useful Resources</h2>
|
||||||
|
<p>
|
||||||
|
Here are a few sites/lists of privacy oriented software for you to
|
||||||
|
check out:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
aria-label="Umami Analytics FAQ"
|
aria-label="Privacy Respecting Software"
|
||||||
href="https://umami.is/docs/faq"
|
href="https://github.com/Lissy93/personal-security-checklist/blob/master/5_Privacy_Respecting_Software.md"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
Umami Analytics
|
Privacy Respecting Software
|
||||||
</a>{' '}
|
</a>
|
||||||
to anonymously track visits to my site. You can completely block this
|
</li>
|
||||||
if you want by either using an AdBlocker like{' '}
|
<li>
|
||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
aria-label="uBlock Origin"
|
aria-label="Privacy Guides"
|
||||||
href="https://ublockorigin.com/"
|
href="https://www.privacyguides.org/"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
uBlock Origin
|
Privacy Guides
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
aria-label="Ethical Alternatives"
|
||||||
|
href="https://ethical.net/resources/"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
Ethical Alternatives
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3>Privacy Centric Paid Services I use:</h3>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
Article Saving:{' '}
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
aria-label="Wallabag Article Saver"
|
||||||
|
href="https://github.com/wallabag/wallabag"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
Wallabag
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Anonymous Email Forwarding:{' '}
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
aria-label="AnonAddy Email Forwarding"
|
||||||
|
href="https://anonaddy.com/"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
AnonAddy
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Email:{' '}
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
aria-label="ProtonMail"
|
||||||
|
href="https://protonmail.com"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
ProtonMail
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Notes:{' '}
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
aria-label="Standard Notes"
|
||||||
|
href="https://standardnotes.org/"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
Standard Notes
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
VPN:{' '}
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
aria-label="Mullvad VPN"
|
||||||
|
href="https://mullvad.net/en/"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
Mullvad VPN
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3>NAS Servers for Self Hosting:</h3>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
aria-label="Synology NAS"
|
||||||
|
href="https://synology.com/"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
Synology
|
||||||
|
</a>
|
||||||
|
: An easy, not cheap, local solution for Google Services like Drive,
|
||||||
|
Drive, Photos, Calendar, other services using Docker, etc. (Yes I
|
||||||
|
should use{' '}
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
aria-label="NextCloud Local Hosting Service"
|
||||||
|
href="https://nextcloud.com/"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
NextCloud
|
||||||
|
</a>
|
||||||
|
...maybe eventually)
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Mac Mini: Used as a{' '}
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
aria-label="Plex"
|
||||||
|
href="https://www.plex.tv/"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
Plex Server
|
||||||
</a>{' '}
|
</a>{' '}
|
||||||
and/or set your browser to send "Do Not Track" requests as I honor
|
for my Movies/TV/Music until I build a dedicated NAS and maybe
|
||||||
them.
|
switch to{' '}
|
||||||
</p>
|
<a
|
||||||
</div>
|
target="_blank"
|
||||||
<div style="margin-top: 2.5rem;">
|
aria-label="JellyFin"
|
||||||
<h2>Useful Resources</h2>
|
href="https://jellyfin.org/"
|
||||||
<p>
|
rel="noreferrer"
|
||||||
Here are a few sites/lists of privacy oriented software for you to
|
>
|
||||||
check out:
|
JellyFin
|
||||||
</p>
|
</a>{' '}
|
||||||
<ul>
|
once it is more mature.
|
||||||
<li>
|
</li>
|
||||||
<a
|
</ul>
|
||||||
target="_blank"
|
</div>
|
||||||
aria-label="Privacy Respecting Software"
|
<div>
|
||||||
href="https://github.com/Lissy93/personal-security-checklist/blob/master/5_Privacy_Respecting_Software.md"
|
<h3>Software Deployed:</h3>
|
||||||
rel="noreferrer"
|
<ul>
|
||||||
>
|
<li>
|
||||||
Privacy Respecting Software
|
<a
|
||||||
</a>
|
target="_blank"
|
||||||
</li>
|
aria-label="Traefik Cloud Proxy"
|
||||||
<li>
|
href="https://github.com/traefik/traefik"
|
||||||
<a
|
rel="noreferrer"
|
||||||
target="_blank"
|
>
|
||||||
aria-label="Privacy Guides"
|
Traefik
|
||||||
href="https://www.privacyguides.org/"
|
</a>
|
||||||
rel="noreferrer"
|
: A HTTP reverse proxy and load balancer.
|
||||||
>
|
</li>
|
||||||
Privacy Guides
|
<li>
|
||||||
</a>
|
<a
|
||||||
</li>
|
target="_blank"
|
||||||
<li>
|
aria-label="Syncthing File Synchronization"
|
||||||
<a
|
href="https://github.com/syncthing/syncthing"
|
||||||
target="_blank"
|
rel="noreferrer"
|
||||||
aria-label="Ethical Alternatives"
|
>
|
||||||
href="https://ethical.net/resources/"
|
Syncthing
|
||||||
rel="noreferrer"
|
</a>
|
||||||
>
|
: An open source file synchronization program.
|
||||||
Ethical Alternatives
|
</li>
|
||||||
</a>
|
</ul>
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3>Privacy Centric Paid Services I use:</h3>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
Article Saving:{' '}
|
|
||||||
<a
|
|
||||||
target="_blank"
|
|
||||||
aria-label="Wallabag Article Saver"
|
|
||||||
href="https://github.com/wallabag/wallabag"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
Wallabag
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Anonymous Email Forwarding:{' '}
|
|
||||||
<a
|
|
||||||
target="_blank"
|
|
||||||
aria-label="AnonAddy Email Forwarding"
|
|
||||||
href="https://anonaddy.com/"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
AnonAddy
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Email:{' '}
|
|
||||||
<a
|
|
||||||
target="_blank"
|
|
||||||
aria-label="ProtonMail"
|
|
||||||
href="https://protonmail.com"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
ProtonMail
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Notes:{' '}
|
|
||||||
<a
|
|
||||||
target="_blank"
|
|
||||||
aria-label="Standard Notes"
|
|
||||||
href="https://standardnotes.org/"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
Standard Notes
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
VPN:{' '}
|
|
||||||
<a
|
|
||||||
target="_blank"
|
|
||||||
aria-label="Mullvad VPN"
|
|
||||||
href="https://mullvad.net/en/"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
Mullvad VPN
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3>NAS Servers for Self Hosting:</h3>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
target="_blank"
|
|
||||||
aria-label="Synology NAS"
|
|
||||||
href="https://synology.com/"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
Synology
|
|
||||||
</a>
|
|
||||||
: An easy, not cheap, local solution for Google Services like Drive,
|
|
||||||
Drive, Photos, Calendar, other services using Docker, etc. (Yes I
|
|
||||||
should use{' '}
|
|
||||||
<a
|
|
||||||
target="_blank"
|
|
||||||
aria-label="NextCloud Local Hosting Service"
|
|
||||||
href="https://nextcloud.com/"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
NextCloud
|
|
||||||
</a>
|
|
||||||
...maybe eventually)
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Mac Mini: Used as a{' '}
|
|
||||||
<a
|
|
||||||
target="_blank"
|
|
||||||
aria-label="Plex"
|
|
||||||
href="https://www.plex.tv/"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
Plex Server
|
|
||||||
</a>{' '}
|
|
||||||
for my Movies/TV/Music until I build a dedicated NAS and maybe
|
|
||||||
switch to{' '}
|
|
||||||
<a
|
|
||||||
target="_blank"
|
|
||||||
aria-label="JellyFin"
|
|
||||||
href="https://jellyfin.org/"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
JellyFin
|
|
||||||
</a>{' '}
|
|
||||||
once it is more mature.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3>Software Deployed:</h3>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
target="_blank"
|
|
||||||
aria-label="Traefik Cloud Proxy"
|
|
||||||
href="https://github.com/traefik/traefik"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
Traefik
|
|
||||||
</a>
|
|
||||||
: A HTTP reverse proxy and load balancer.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
target="_blank"
|
|
||||||
aria-label="Syncthing File Synchronization"
|
|
||||||
href="https://github.com/syncthing/syncthing"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
Syncthing
|
|
||||||
</a>
|
|
||||||
: An open source file synchronization program.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="postcss">
|
<style lang="postcss">
|
||||||
|
div {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export const prerender = true;
|
|
||||||
Loading…
Reference in a new issue