├── assets
├── icon.png
└── fonts
│ ├── Sedan-Italic.ttf
│ └── Sedan-Regular.ttf
├── postcss.config.js
├── newtab
├── index.html
└── index.tsx
├── utils
├── queryclient.ts
└── readwise.ts
├── storage.ts
├── style.css
├── tsconfig.json
├── README.md
├── .gitignore
├── components
└── Providers.tsx
├── themes.ts
├── .prettierrc.mjs
├── background
└── index.ts
├── tailwind.config.js
├── .github
└── workflows
│ └── submit.yml
├── package.json
└── options
└── index.tsx
/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/djyde/wisetab/HEAD/assets/icon.png
--------------------------------------------------------------------------------
/assets/fonts/Sedan-Italic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/djyde/wisetab/HEAD/assets/fonts/Sedan-Italic.ttf
--------------------------------------------------------------------------------
/assets/fonts/Sedan-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/djyde/wisetab/HEAD/assets/fonts/Sedan-Regular.ttf
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/newtab/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | __plasmo_static_index_title__
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/utils/queryclient.ts:
--------------------------------------------------------------------------------
1 | import { QueryClient } from "@tanstack/react-query";
2 |
3 | export const queryClient = new QueryClient({
4 | defaultOptions: {
5 | queries: {
6 | refetchOnWindowFocus: false,
7 | },
8 | },
9 | });
--------------------------------------------------------------------------------
/storage.ts:
--------------------------------------------------------------------------------
1 | import { Storage } from "@plasmohq/storage";
2 |
3 | export const storage = new Storage({
4 | area: 'local'
5 | })
6 |
7 | export enum StorageKey {
8 | ReadwiseToken = 'readwise_token',
9 | DailyReview = 'daily_review',
10 | ReviewCursor = 'review_cursor',
11 | }
--------------------------------------------------------------------------------
/style.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 |
6 | @layer base {
7 | @font-face {
8 | font-family: "Sedan";
9 | font-style: normal;
10 | font-weight: 400;
11 | font-display: swap;
12 | src: url(data-base64:~assets/fonts/Sedan-Regular.ttf);
13 | }
14 | }
15 |
16 |
17 | body {
18 | font-size: 16px;
19 | @apply antialiased;
20 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "plasmo/templates/tsconfig.base",
3 | "exclude": [
4 | "node_modules"
5 | ],
6 | "include": [
7 | ".plasmo/index.d.ts",
8 | "./**/*.ts",
9 | "./**/*.tsx"
10 | ],
11 | "compilerOptions": {
12 | "strictNullChecks": true,
13 | "paths": {
14 | "~*": [
15 | "./*"
16 | ]
17 | },
18 | "baseUrl": "."
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Wisetab
2 |
3 | Readwise daily review on your browser's new tab page.
4 |
5 | 
6 |
7 | ## Getting Started
8 |
9 | First, run the development server:
10 |
11 | ```bash
12 | pnpm dev
13 | # or
14 | npm run dev
15 | ```
16 |
17 | ## Making production build
18 |
19 | Run the following:
20 |
21 | ```bash
22 | pnpm build
23 | # or
24 | npm run build
25 | ```
26 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
3 |
4 | # dependencies
5 | /node_modules
6 | /.pnp
7 | .pnp.js
8 |
9 | # testing
10 | /coverage
11 |
12 | # misc
13 | .DS_Store
14 | *.pem
15 |
16 | # debug
17 | npm-debug.log*
18 | yarn-debug.log*
19 | yarn-error.log*
20 | .pnpm-debug.log*
21 |
22 | # local env files
23 | .env*.local
24 |
25 | out/
26 | build/
27 | dist/
28 |
29 | # plasmo
30 | .plasmo
31 |
32 | # typescript
33 | .tsbuildinfo
34 |
--------------------------------------------------------------------------------
/components/Providers.tsx:
--------------------------------------------------------------------------------
1 | import { QueryClientProvider } from "@tanstack/react-query";
2 | import { queryClient } from "~utils/queryclient";
3 | import '../style.css'
4 | import { useEffect } from "react";
5 | import { themeChange } from "theme-change";
6 |
7 | export function Providers(props: {
8 | children: React.ReactNode
9 | }) {
10 | useEffect(() => {
11 | themeChange(false)
12 | }, [])
13 |
14 | return (
15 |
16 | {props.children}
17 |
18 | )
19 | }
--------------------------------------------------------------------------------
/themes.ts:
--------------------------------------------------------------------------------
1 | export const themes = [
2 | "light",
3 | "dark",
4 | "cupcake",
5 | "bumblebee",
6 | "emerald",
7 | "corporate",
8 | "synthwave",
9 | "retro",
10 | "cyberpunk",
11 | "valentine",
12 | "halloween",
13 | "garden",
14 | "forest",
15 | "aqua",
16 | "lofi",
17 | "pastel",
18 | "fantasy",
19 | "wireframe",
20 | "black",
21 | "luxury",
22 | "dracula",
23 | "cmyk",
24 | "autumn",
25 | "business",
26 | "acid",
27 | "lemonade",
28 | "night",
29 | "coffee",
30 | "winter",
31 | "dim",
32 | "nord",
33 | "sunset",
34 | ]
--------------------------------------------------------------------------------
/.prettierrc.mjs:
--------------------------------------------------------------------------------
1 | /**
2 | * @type {import('prettier').Options}
3 | */
4 | export default {
5 | printWidth: 80,
6 | tabWidth: 2,
7 | useTabs: false,
8 | semi: false,
9 | singleQuote: false,
10 | trailingComma: "none",
11 | bracketSpacing: true,
12 | bracketSameLine: true,
13 | plugins: ["@ianvs/prettier-plugin-sort-imports"],
14 | importOrder: [
15 | "", // Node.js built-in modules
16 | "", // Imports not matched by other special words or groups.
17 | "", // Empty line
18 | "^@plasmo/(.*)$",
19 | "",
20 | "^@plasmohq/(.*)$",
21 | "",
22 | "^~(.*)$",
23 | "",
24 | "^[./]"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/background/index.ts:
--------------------------------------------------------------------------------
1 | import { StorageKey, storage } from "~storage"
2 | import { getDailyReviewList } from "~utils/readwise"
3 |
4 | async function refreshDailyReview() {
5 | const dailyReviewList = await getDailyReviewList()
6 | // Save the daily review list to storage
7 | await storage.set(StorageKey.DailyReview, dailyReviewList)
8 | }
9 |
10 | const alarmName = 'refreshDailyReview'
11 |
12 | chrome.runtime.onInstalled.addListener(async () => {
13 | console.log('set alarm')
14 | await chrome.alarms.create(alarmName, { periodInMinutes: 60 })
15 | })
16 |
17 |
18 | chrome.alarms.onAlarm.addListener(async (alarm) => {
19 | if (alarm.name === alarmName) {
20 | await refreshDailyReview()
21 | }
22 | })
23 |
24 | storage.watch({
25 | [StorageKey.ReadwiseToken]: async () => {
26 | await refreshDailyReview()
27 | }
28 | })
29 |
--------------------------------------------------------------------------------
/utils/readwise.ts:
--------------------------------------------------------------------------------
1 | import { StorageKey, storage } from "~storage"
2 |
3 | export type DailyReview = {
4 | review_id: number,
5 | review_url: string,
6 | review_completed: boolean,
7 | highlights: {
8 | text: string,
9 | title: string,
10 | author: string,
11 | url: string,
12 | source_type: string,
13 | note: string,
14 | highlighted_at: string,
15 | image_url: string
16 | id: number
17 | }[]
18 | }
19 |
20 | export async function getDailyReviewList() {
21 | const token = await storage.get(StorageKey.ReadwiseToken)
22 |
23 | if (!token) {
24 | return null
25 | }
26 |
27 | const response = await fetch("https://readwise.io/api/v2/review/", {
28 | method: "GET",
29 | headers: {
30 | Authorization: `Token ${token}`
31 | }
32 | })
33 |
34 | if (response.status === 401) {
35 | return null
36 | }
37 |
38 | const json = await response.json()
39 | return json as DailyReview
40 | }
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: [
4 | "./newtab/**/*.tsx",
5 | "./options/**/*.tsx",
6 | ],
7 | daisyui: {
8 | themes: [
9 | "light",
10 | "dark",
11 | "cupcake",
12 | "bumblebee",
13 | "emerald",
14 | "corporate",
15 | "synthwave",
16 | "retro",
17 | "cyberpunk",
18 | "valentine",
19 | "halloween",
20 | "garden",
21 | "forest",
22 | "aqua",
23 | "lofi",
24 | "pastel",
25 | "fantasy",
26 | "wireframe",
27 | "black",
28 | "luxury",
29 | "dracula",
30 | "cmyk",
31 | "autumn",
32 | "business",
33 | "acid",
34 | "lemonade",
35 | "night",
36 | "coffee",
37 | "winter",
38 | "dim",
39 | "nord",
40 | "sunset",
41 | ]
42 | },
43 | theme: {
44 | fontFamily: {
45 | "serif-eng": ["Sedan", "serif"],
46 | },
47 | extend: {},
48 | },
49 | plugins: [
50 | require("daisyui")
51 | ],
52 | }
53 |
54 |
--------------------------------------------------------------------------------
/.github/workflows/submit.yml:
--------------------------------------------------------------------------------
1 | name: "Submit to Web Store"
2 | on:
3 | workflow_dispatch:
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/checkout@v3
10 | - name: Cache pnpm modules
11 | uses: actions/cache@v3
12 | with:
13 | path: ~/.pnpm-store
14 | key: ${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
15 | restore-keys: |
16 | ${{ runner.os }}-
17 | - uses: pnpm/action-setup@v2.2.4
18 | with:
19 | version: latest
20 | run_install: true
21 | - name: Use Node.js 16.x
22 | uses: actions/setup-node@v3.4.1
23 | with:
24 | node-version: 16.x
25 | cache: "pnpm"
26 | - name: Build the extension
27 | run: pnpm build
28 | - name: Package the extension into a zip artifact
29 | run: pnpm package
30 | - name: Browser Platform Publish
31 | uses: PlasmoHQ/bpp@v3
32 | with:
33 | keys: ${{ secrets.SUBMIT_KEYS }}
34 | artifact: build/chrome-mv3-prod.zip
35 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "wisetab",
3 | "displayName": "Wisetab",
4 | "version": "0.0.5",
5 | "description": "Show Readwise highlight in your new tab page",
6 | "author": "Randy Lu",
7 | "scripts": {
8 | "dev": "plasmo dev",
9 | "build": "plasmo build",
10 | "package": "plasmo package"
11 | },
12 | "dependencies": {
13 | "@plasmohq/storage": "^1.10.0",
14 | "@tanstack/react-query": "^5.29.2",
15 | "autoprefixer": "^10.4.19",
16 | "classnames": "^2.5.1",
17 | "daisyui": "^4.10.2",
18 | "dompurify": "^3.1.0",
19 | "markdown-it": "^14.1.0",
20 | "plasmo": "0.85.2",
21 | "postcss": "^8.4.38",
22 | "react": "18.2.0",
23 | "react-dom": "18.2.0",
24 | "react-hook-form": "^7.51.3",
25 | "react-icons": "^5.1.0",
26 | "tailwindcss": "^3.4.3",
27 | "theme-change": "^2.5.0"
28 | },
29 | "devDependencies": {
30 | "@ianvs/prettier-plugin-sort-imports": "4.1.1",
31 | "@types/chrome": "0.0.258",
32 | "@types/node": "20.11.5",
33 | "@types/react": "18.2.48",
34 | "@types/react-dom": "18.2.18",
35 | "prettier": "3.2.4",
36 | "typescript": "5.3.3"
37 | },
38 | "manifest": {
39 | "permissions": [
40 | "alarms"
41 | ],
42 | "host_permissions": []
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/options/index.tsx:
--------------------------------------------------------------------------------
1 | import { useForm } from "react-hook-form";
2 | import { Providers } from "~components/Providers";
3 | import { StorageKey, storage } from "~storage";
4 |
5 | export default function Options() {
6 |
7 |
8 | const form = useForm({
9 | defaultValues: async () => {
10 | return {
11 | readwiseToken: await storage.get(StorageKey.ReadwiseToken)
12 | }
13 | }
14 | })
15 |
16 | return (
17 |
18 |
19 |
20 |
Options
21 |
39 |
40 |
41 |
42 |
43 | )
44 | }
--------------------------------------------------------------------------------
/newtab/index.tsx:
--------------------------------------------------------------------------------
1 | import { StorageKey, storage } from '~storage'
2 | import '../style.css'
3 | import { useStorage } from '@plasmohq/storage/hook'
4 | import type { DailyReview } from '~utils/readwise'
5 | import { useEffect } from 'react'
6 | import { QueryClientProvider, useMutation, useQuery } from '@tanstack/react-query'
7 | import { queryClient } from '~utils/queryclient'
8 | import cn from 'classnames'
9 | import { Providers } from '~components/Providers'
10 | import { LuSettings } from 'react-icons/lu'
11 | import { themes } from '~themes'
12 | import { themeChange } from 'theme-change'
13 | import MarkdownIt from 'markdown-it';
14 | import DOMPurify from 'dompurify';
15 |
16 | function NewTab() {
17 | const md = new MarkdownIt();
18 | md.renderer.rules.link_open = function(tokens, idx, options, env, self) {
19 | const aIndex = tokens[idx].attrIndex('class');
20 | if (aIndex < 0) {
21 | tokens[idx].attrPush(['class', 'link link-primary']);
22 | } else {
23 | tokens[idx].attrs[aIndex][1] += ' link link-primary';
24 | }
25 | return self.renderToken(tokens, idx, options);
26 | };
27 |
28 | const review = useQuery({
29 | queryKey: ['dailyReview'],
30 | queryFn: async () => {
31 | const result = await storage.get(StorageKey.DailyReview)
32 | return result
33 | }
34 | })
35 |
36 | if (!review.isPending && !review.data) {
37 | return (
38 | <>
39 |
40 |
41 | Please first
44 |
45 |
46 | >
47 | )
48 | }
49 |
50 | const highlightCount = review.data?.highlights.length || 0
51 |
52 | // random index base on highlight count
53 | const random = Math.floor(Math.random() * highlightCount)
54 |
55 | const currentReview = review.data?.highlights[random]
56 |
57 | const renderTextWithLinks = (text) => {
58 | const renderedText = md.render(text);
59 | const sanitizedText = DOMPurify.sanitize(renderedText);
60 | return sanitizedText;
61 | }
62 |
63 | return (
64 |
65 |
89 | {currentReview && (
90 | <>
91 |
200
94 | })
95 | }>
96 |
97 |

98 |
99 |
100 |
101 |
102 |
103 | {currentReview.note}
104 |
105 |
106 |
107 |
108 |
111 |
112 |
113 | {currentReview.author} / {currentReview.title}
114 |
115 |
116 | >
117 | )}
118 |
119 | )
120 | }
121 |
122 | export default function Page() {
123 |
124 | return (
125 |
126 |
127 |
128 | )
129 | }
--------------------------------------------------------------------------------