mirror of
https://github.com/BradNut/personal-website-sveltekit
synced 2025-09-08 23:20:18 +00:00
Adding API to get articles from Wallabag, getting articles and caching if first load of application, and rendering the articles on the page.
This commit is contained in:
parent
2585c0d67c
commit
4a9ad4b468
11 changed files with 252 additions and 69 deletions
|
|
@ -31,6 +31,7 @@
|
||||||
background: var(--footerBackground);
|
background: var(--footerBackground);
|
||||||
place-content: center;
|
place-content: center;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
|
margin-top: 5rem;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
|
|
||||||
@media (max-width: 800px) {
|
@media (max-width: 800px) {
|
||||||
|
|
|
||||||
11
src/lib/types/article.ts
Normal file
11
src/lib/types/article.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
export type Article {
|
||||||
|
tags: string[];
|
||||||
|
title: string;
|
||||||
|
url: URL;
|
||||||
|
hashed_url: string;
|
||||||
|
reading_time: number;
|
||||||
|
preview_picture: string;
|
||||||
|
created_at: Date;
|
||||||
|
updated_at: Date;
|
||||||
|
archived_at: Date | null;
|
||||||
|
}
|
||||||
5
src/lib/types/pageQuery.ts
Normal file
5
src/lib/types/pageQuery.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
export type PageQuery {
|
||||||
|
sort: string;
|
||||||
|
perPage: number;
|
||||||
|
since: number;
|
||||||
|
}
|
||||||
17
src/lib/util/cookieUtils.ts
Normal file
17
src/lib/util/cookieUtils.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
export function getCookieLookup() {
|
||||||
|
if (typeof document !== 'object') {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return document.cookie.split('; ').reduce((lookup, v) => {
|
||||||
|
const parts = v.split('=');
|
||||||
|
lookup[parts[0]] = parts[1];
|
||||||
|
|
||||||
|
return lookup;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getCurrentCookieValue = (name) => {
|
||||||
|
const cookies = getCookieLookup();
|
||||||
|
return cookies[name] ?? '';
|
||||||
|
};
|
||||||
25
src/routes/+layout.server.ts
Normal file
25
src/routes/+layout.server.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { error, type ServerLoad } from '@sveltejs/kit';
|
||||||
|
import { fetchArticlesApi } from './api';
|
||||||
|
// import type { PageServerLoad } from './$types';
|
||||||
|
|
||||||
|
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
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -61,7 +61,7 @@
|
||||||
max-width: 850px;
|
max-width: 850px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 2rem 0rem;
|
padding: 2rem 0rem;
|
||||||
max-width: 80vw;
|
/* max-width: 80vw; */
|
||||||
|
|
||||||
@media (min-width: 1600px) {
|
@media (min-width: 1600px) {
|
||||||
max-width: 70vw;
|
max-width: 70vw;
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ import {
|
||||||
WALLABAG_PASSWORD,
|
WALLABAG_PASSWORD,
|
||||||
WALLABAG_URL
|
WALLABAG_URL
|
||||||
} from '$env/static/private';
|
} from '$env/static/private';
|
||||||
|
import type { Article } from '$root/lib/types/article';
|
||||||
|
import type { PageQuery } from '$root/lib/types/pageQuery';
|
||||||
import { URLSearchParams } from 'url';
|
import { URLSearchParams } from 'url';
|
||||||
|
|
||||||
const base: string = WALLABAG_URL;
|
const base: string = WALLABAG_URL;
|
||||||
|
|
@ -15,7 +17,7 @@ export async function fetchArticlesApi(
|
||||||
queryParams: Record<string, string>,
|
queryParams: Record<string, string>,
|
||||||
data?: Record<string, unknown>
|
data?: Record<string, unknown>
|
||||||
) {
|
) {
|
||||||
let lastFetched = null;
|
let lastFetched: Date | null = null;
|
||||||
|
|
||||||
const authBody = {
|
const authBody = {
|
||||||
grant_type: 'password',
|
grant_type: 'password',
|
||||||
|
|
@ -33,10 +35,13 @@ export async function fetchArticlesApi(
|
||||||
|
|
||||||
const auth = await authResponse.json();
|
const auth = await authResponse.json();
|
||||||
|
|
||||||
const pageQuery = {
|
const pageQuery: PageQuery = {
|
||||||
sort: 'updated',
|
sort: 'updated',
|
||||||
perPage: 500
|
perPage: 6,
|
||||||
|
since: 0
|
||||||
};
|
};
|
||||||
|
const entriesQueryParams = new URLSearchParams(pageQuery);
|
||||||
|
console.log(`Entries params: ${entriesQueryParams}`);
|
||||||
|
|
||||||
if (lastFetched) {
|
if (lastFetched) {
|
||||||
pageQuery.since = Math.round(lastFetched / 1000);
|
pageQuery.since = Math.round(lastFetched / 1000);
|
||||||
|
|
@ -44,45 +49,43 @@ export async function fetchArticlesApi(
|
||||||
|
|
||||||
lastFetched = new Date();
|
lastFetched = new Date();
|
||||||
|
|
||||||
let nbEntries = 0;
|
const nbEntries = 0;
|
||||||
const pageResponse = await fetch(
|
const pageResponse = await fetch(`${WALLABAG_URL}/api/entries.json?${entriesQueryParams}`, {
|
||||||
`${WALLABAG_URL}/api/entries.json?${new URLSearchParams(pageQuery)}`,
|
method: 'GET',
|
||||||
{
|
headers: {
|
||||||
method: 'GET',
|
Authorization: `Bearer ${auth.access_token}`
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${auth.access_token}`
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
|
|
||||||
if (!pageResponse.ok) {
|
if (!pageResponse.ok) {
|
||||||
throw new Error(pageResponse.statusText);
|
throw new Error(pageResponse.statusText);
|
||||||
}
|
}
|
||||||
|
|
||||||
let entries = await pageResponse.json();
|
const entries = await pageResponse.json();
|
||||||
const articles = [];
|
const articles: Article[] = [];
|
||||||
|
|
||||||
do {
|
// do {
|
||||||
nbEntries += entries._embedded.items.length;
|
// nbEntries += entries._embedded.items.length;
|
||||||
console.log(`number of articles fetched: ${nbEntries}`);
|
console.log(`number of articles fetched: ${entries._embedded.items.length}`);
|
||||||
entries._embedded.items.forEach((article) => {
|
entries._embedded.items.forEach((article: Article) => {
|
||||||
article.created_at = new Date(article.created_at);
|
article.created_at = new Date(article.created_at);
|
||||||
article.updated_at = new Date(article.updated_at);
|
article.updated_at = new Date(article.updated_at);
|
||||||
article.archived_at = article.archived_at ? new Date(article.archived_at) : null;
|
article.archived_at = article.archived_at ? new Date(article.archived_at) : null;
|
||||||
articles.push(article);
|
articles.push(article);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!entries._links.next) {
|
// if (!entries._links.next) {
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
const response = await fetch(entries._links.next.href, {
|
// console.log(`Links next ${JSON.stringify(entries._links.next)}`);
|
||||||
method: 'GET',
|
// const response = await fetch(entries._links.next.href, {
|
||||||
headers: {
|
// method: 'GET',
|
||||||
Authorization: `Bearer ${auth.access_token}`
|
// headers: {
|
||||||
}
|
// Authorization: `Bearer ${auth.access_token}`
|
||||||
});
|
// }
|
||||||
entries = await response.json();
|
// });
|
||||||
} while (entries._links.next);
|
// entries = await response.json();
|
||||||
|
// } while (entries._links.next);
|
||||||
|
|
||||||
return { articles };
|
return { articles };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
28
src/routes/api/articles/+server.ts
Normal file
28
src/routes/api/articles/+server.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { json } from '@sveltejs/kit';
|
||||||
|
import type { RequestHandler, RequestEvent } from './$types';
|
||||||
|
import { fetchArticlesApi } from '$root/routes/api';
|
||||||
|
|
||||||
|
export const GET: RequestHandler = async ({ url, setHeaders, request, params }: RequestEvent) => {
|
||||||
|
try {
|
||||||
|
const response = await fetchArticlesApi('get', `search`, {});
|
||||||
|
|
||||||
|
if (response?.articles) {
|
||||||
|
setHeaders({
|
||||||
|
'cache-control': 'max-age=604800'
|
||||||
|
});
|
||||||
|
|
||||||
|
const articlesResponse = response.articles;
|
||||||
|
console.log(`Found articles ${articlesResponse.length}`);
|
||||||
|
const articles = [];
|
||||||
|
|
||||||
|
for (const article of articlesResponse) {
|
||||||
|
const { tags, title, url, hashed_url, reading_time, preview_picture } = article;
|
||||||
|
articles.push({ tags, title, url, hashed_url, reading_time, preview_picture });
|
||||||
|
}
|
||||||
|
|
||||||
|
return json(articles);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
import { error } from '@sveltejs/kit';
|
|
||||||
import { fetchArticlesApi } from '../api';
|
|
||||||
import type { PageServerLoad } from './$types';
|
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ 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'
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetchArticlesApi('get', `search`, queryParams);
|
|
||||||
|
|
||||||
if (response?.articles) {
|
|
||||||
// const gameResponse = await response.json();
|
|
||||||
|
|
||||||
setHeaders({
|
|
||||||
'Cache-Control': 'max-age=3600'
|
|
||||||
});
|
|
||||||
|
|
||||||
const articles = response.articles;
|
|
||||||
console.log(`Found articles ${articles.length}`);
|
|
||||||
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error(500, 'error');
|
|
||||||
};
|
|
||||||
|
|
@ -1,10 +1,118 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { PageData } from "./$types";
|
import { page } from "$app/stores";
|
||||||
import SEO from "$root/lib/components/SEO.svelte";
|
import SEO from "$root/lib/components/SEO.svelte";
|
||||||
|
|
||||||
export let data: PageData;
|
$: ({ articles } = $page.data);
|
||||||
|
|
||||||
const currentPage: number = 1;
|
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>
|
</script>
|
||||||
|
|
||||||
<SEO title={`Tech Articles - Page ${currentPage}`} />
|
<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}
|
||||||
|
<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.label}</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>
|
||||||
17
src/routes/articles/+page.ts
Normal file
17
src/routes/articles/+page.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { getCurrentCookieValue } from '$lib/util/cookieUtils';
|
||||||
|
import type { Article } from '$root/lib/types/article';
|
||||||
|
import type { PageLoad } from './$types';
|
||||||
|
|
||||||
|
export const load: PageLoad = async ({ fetch, parent, url, setHeaders }) => {
|
||||||
|
const parentData = await parent();
|
||||||
|
|
||||||
|
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
|
||||||
|
};
|
||||||
|
};
|
||||||
Loading…
Reference in a new issue