Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add readability.js API #11

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 45 additions & 15 deletions components/Page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { animate as framer, AnimationControls, motion } from 'framer-motion';
import { Interweave, MatcherInterface, MatchResponse } from 'interweave';
import { FC, useEffect, useRef } from 'react';
import language from '../constants/language';

import { Book, Highlight } from '../lib/readwise/types';
import language from '../constants/language';
import { Highlight } from '../lib/readwise/types';

interface Props {
pageIndex: number;
Expand All @@ -12,6 +13,7 @@ interface Props {
pan: boolean;
bookmark: Highlight;
token: string;
article: any;
}

const Page: FC<Props> = ({
Expand All @@ -21,22 +23,52 @@ const Page: FC<Props> = ({
custom,
pan,
bookmark,
article,
}) => {
const pageRef = useRef<HTMLDivElement>();
const highlightRef = useRef<HTMLParagraphElement>(null);

useEffect(() => {
let topPos =
highlightRef.current.offsetTop + highlightRef.current.clientHeight - 8;
console.log(topPos);
const matcher: MatcherInterface<any> = {
inverseName: 'noMark',
propName: 'mark',
match(string): MatchResponse<any> {
Comment on lines +31 to +34
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test this out

const result = string.match(bookmark.text);

highlightRef.current.className = 'bg-moss/20';
if (!result) {
return null;
}

return {
index: result.index!,
length: result[0].length,
match: result[0],
className: 'highlight',
valid: true,
ref: highlightRef,
};
},
createElement(children, props) {
return <span {...props}>{children}</span>;
},
asTag() {
return 'span';
},
};

useEffect(() => {
if (highlightRef.current) {
{
let topPos =
highlightRef.current.offsetTop + highlightRef.current.clientHeight;

if (pan) {
framer(pageRef.current.scrollTop, topPos, {
onUpdate: (top) =>
pageRef.current.scrollTo({ top, behavior: 'smooth' }),
});
highlightRef.current.className = 'bg-moss/20';
if (pan) {
framer(pageRef.current.scrollTop, topPos, {
onUpdate: (top) =>
pageRef.current.scrollTo({ top, behavior: 'smooth' }),
});
}
}
}
}, [pan]);

Expand All @@ -53,9 +85,7 @@ const Page: FC<Props> = ({
{language.article.highlight.eyebrow(pageIndex + 1)}
</span>
<div className="relative h-full w-full text-sm mt-2 gap-2 flex flex-col">
<p>
<span ref={highlightRef}>{bookmark.text}</span>
</p>
<Interweave content={article} matchers={[matcher]} />;
</div>
</motion.div>
);
Expand Down
4 changes: 4 additions & 0 deletions constants/language.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ const language = {
</a>
</>
),
coverImage: {
alt: (title: string) =>
`A featured image related to the link titled ${title}`,
},
readwiseLink: 'View on Readwise',
highlight: {
eyebrow: (index: number) => `${index}`,
Expand Down
1 change: 1 addition & 0 deletions constants/values.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export const THEME_COLOR = '#295122';

export const READWISE_TOKEN_LOCALSTORAGE_KEY = 'g:readwise_token';
export const READWISE_API_BASE_URL = 'https://readwise.io/api/';
163 changes: 79 additions & 84 deletions lib/readwise/index.ts
Original file line number Diff line number Diff line change
@@ -1,80 +1,74 @@
import { READWISE_TOKEN_LOCALSTORAGE_KEY } from './../../constants/values';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
import { useEffect } from 'react';
import { useLocalstorage } from 'rooks';
import useSWR, { mutate } from 'swr';

import { Book, Highlight, RawBook, RawHighlight } from './types';

interface FetchBookmarksRequest {
name?: string;
state?: string;
count?: number;
id?: number | string;
token?: string;
}

interface FetchBookmarksResponse {
list: Record<string, RawHighlight>;
}

interface FetchBooksRequest {
name?: string;
state?: string;
count?: number;
token?: string;
}

interface FetchBooksResponse {
list: Record<string, RawBook>;
}

const BASE_URL = 'https://readwise.io/api/';

export const request = async (
url,
method: 'POST' | 'GET',
import {
READWISE_API_BASE_URL,
READWISE_TOKEN_LOCALSTORAGE_KEY,
} from '../../constants/values';
import {
Book,
FetchBookmarksRequest,
FetchBookmarksResponse,
FetchBooksRequest,
FetchBooksResponse,
Highlight,
} from './types';

/**
* Fetches data from the Readwie API.
* @param url The Readwise API endpoint to hit. Omit the leading slash.
* @param method The method to use for the request.
* @param token The Readwise user access token to use for the request.
* @param options Additional options to pass to the request.
* @returns The response body as a JSON object.
*/
const request = async <T>(
url: string,
method: 'POST' | 'GET' | 'DELETE',
token: string,
options?: object
) => {
return fetch(BASE_URL + url, {
options?: RequestInit
): Promise<T> => {
const response = await fetch(READWISE_API_BASE_URL + url, {
method: method,
headers: {
Authorization: `Token ${token}`,
'content-type': 'application/json',
...options?.headers,
},
...options,
});
};

export const auth = async (token: string) =>
await fetch('https://readwise.io/api/v2/auth', {
method: 'GET',
headers: {
Authorization: `Token ${token}`,
'content-type': 'application/json',
},
});
const json = await response.json();

return json;
};

/**
* Verify the that saved Readwise token is valid.
* @param token The user's Readwise access token.
* @returns unknown
*/
// TODO: Figure out what the response is and type the return value accoringly.
export const verifyAuth = async (token: string) =>
await request<any>('v2/auth', 'GET', token);

/**
* Fetch all highlights from a given book.
* @param args.id The book ID to fetch highlights from.
* @param args.token The user's Readwise access token.
* @returns `Highlight[]` – The user's highlights.
*/
export const fetchHighlights = async ({
name,
state,
count,
id,
token,
}: FetchBookmarksRequest = {}): Promise<Array<Highlight>> => {
const response = await fetch(
BASE_URL + `v2/highlights?page_size=1000&book_id=${id}`,
{
method: 'GET',
headers: {
Authorization: `Token ${token}`,
'content-type': 'application/json',
},
}
}: FetchBookmarksRequest): Promise<Highlight[]> => {
const result = await request<any>(
`v2/highlights?page_size=1000&book_id=${id}`,
'GET',
token
);

const result = await response.json();
const results = result.results as FetchBookmarksResponse;

const bookmarks: Array<Highlight> = Object.values(results).map((item) => ({
Expand All @@ -94,15 +88,26 @@ export const fetchHighlights = async ({
return bookmarks;
};

/**
* React hook to fetch the user's Readwise bookmarks.
* @returns An object with the data and loading state.
*/
export function useHighlights() {
const { value: token } = useLocalstorage(READWISE_TOKEN_LOCALSTORAGE_KEY);
const { value: token }: { value: string } = useLocalstorage(
READWISE_TOKEN_LOCALSTORAGE_KEY
);

const router = useRouter();
const id = router.query.id as string;

const { data, error, isValidating } = useSWR<Array<Highlight>, any>(
id ? 'v2/highlights' : null,
() => fetchHighlights({ id, token: token as string })
() => fetchHighlights({ id, token }),
{
revalidateIfStale: false,
revalidateOnFocus: false,
revalidateOnReconnect: false,
}
);

useEffect(() => {
Expand All @@ -125,27 +130,23 @@ export function useHighlights() {
};
}

export async function fetchBooks({ token }: FetchBooksRequest = {}): Promise<
Array<Book>
> {
const response = await request(
export async function fetchBooks({
token,
}: FetchBooksRequest): Promise<Array<Book>> {
const { results } = await request<FetchBooksResponse>(
'v2/books?category=articles&page_size=500',
'GET',
token
);

const result = await response.json();

const results = result.results as FetchBooksResponse;

const books: Array<Book> = Object.values(results).map((item) => ({
id: item.id,
title: item.title,
author: item.author,
category: item.category,
source: item.source,
num_highlights: item.num_highlights,
last_highlight_at: item.last_highlighted_at,
last_highlight_at: item.last_highlight_at,
updated: item.updated,
cover_image_url: item.cover_image_url,
highlights_url: item.highlights_url,
Expand All @@ -158,14 +159,14 @@ export async function fetchBooks({ token }: FetchBooksRequest = {}): Promise<
}

export function useBooks(token: string) {
// const { value: token } = useLocalstorage('g:readwise_token');

// useEffect(() => {
// mutate('v2/books');
// }, [token]);

const { data, error, isValidating } = useSWR('v2/books', () =>
fetchBooks({ token: token })
const { data, error, isValidating } = useSWR(
'v2/books',
() => fetchBooks({ token: token }),
{
revalidateIfStale: false,
revalidateOnFocus: false,
revalidateOnReconnect: false,
}
);

useEffect(() => {
Expand All @@ -184,9 +185,3 @@ export function useBooks(token: string) {
error: error,
};
}

export const CmsClient: {
fetchHighlights(): Promise<Array<Highlight>>;
} = {
fetchHighlights,
};
Loading