aboutsummaryrefslogtreecommitdiffstats
path: root/packages
diff options
context:
space:
mode:
authorMohamedBassem <me@mbassem.com>2024-03-13 21:43:44 +0000
committerMohamed Bassem <me@mbassem.com>2024-03-14 16:40:45 +0000
commit04572a8e5081b1e4871e273cde9dbaaa44c52fe0 (patch)
tree8e993acb732a50d1306d4d6953df96c165c57f57 /packages
parent2df08ed08c065e8b91bc8df0266bd4bcbb062be4 (diff)
downloadkarakeep-04572a8e5081b1e4871e273cde9dbaaa44c52fe0.tar.zst
structure: Create apps dir and copy tooling dir from t3-turbo repo
Diffstat (limited to 'packages')
-rw-r--r--packages/browser-extension/.eslintrc.cjs3
-rw-r--r--packages/browser-extension/.gitignore24
-rw-r--r--packages/browser-extension/index.html13
-rw-r--r--packages/browser-extension/manifest.json19
-rw-r--r--packages/browser-extension/package.json44
-rw-r--r--packages/browser-extension/postcss.config.js6
-rw-r--r--packages/browser-extension/public/logo-128.pngbin2362 -> 0 bytes
-rw-r--r--packages/browser-extension/public/logo-16.pngbin287 -> 0 bytes
-rw-r--r--packages/browser-extension/public/logo-48.pngbin780 -> 0 bytes
-rw-r--r--packages/browser-extension/public/logo.pngbin412 -> 0 bytes
-rw-r--r--packages/browser-extension/src/BookmarkDeletedPage.tsx3
-rw-r--r--packages/browser-extension/src/BookmarkSavedPage.tsx62
-rw-r--r--packages/browser-extension/src/Layout.tsx51
-rw-r--r--packages/browser-extension/src/Logo.tsx10
-rw-r--r--packages/browser-extension/src/NotConfiguredPage.tsx47
-rw-r--r--packages/browser-extension/src/OptionsPage.tsx60
-rw-r--r--packages/browser-extension/src/SavePage.tsx60
-rw-r--r--packages/browser-extension/src/SignInPage.tsx89
-rw-r--r--packages/browser-extension/src/Spinner.tsx18
-rw-r--r--packages/browser-extension/src/assets/react.svg1
-rw-r--r--packages/browser-extension/src/index.css72
-rw-r--r--packages/browser-extension/src/main.tsx40
-rw-r--r--packages/browser-extension/src/utils/providers.tsx46
-rw-r--r--packages/browser-extension/src/utils/settings.ts22
-rw-r--r--packages/browser-extension/src/utils/trpc.ts4
-rw-r--r--packages/browser-extension/src/vite-env.d.ts1
-rw-r--r--packages/browser-extension/tailwind.config.js10
-rw-r--r--packages/browser-extension/tsconfig.json25
-rw-r--r--packages/browser-extension/tsconfig.node.json11
-rw-r--r--packages/browser-extension/vite.config.ts9
-rw-r--r--packages/db/package.json13
-rw-r--r--packages/db/tsconfig.json7
-rw-r--r--packages/mobile/.eslintrc.js4
-rw-r--r--packages/mobile/.gitignore39
-rw-r--r--packages/mobile/.npmrc1
-rw-r--r--packages/mobile/app.json57
-rw-r--r--packages/mobile/app/+not-found.tsx6
-rw-r--r--packages/mobile/app/_layout.tsx53
-rw-r--r--packages/mobile/app/dashboard/(tabs)/_layout.tsx38
-rw-r--r--packages/mobile/app/dashboard/(tabs)/index.tsx31
-rw-r--r--packages/mobile/app/dashboard/(tabs)/lists.tsx67
-rw-r--r--packages/mobile/app/dashboard/(tabs)/search.tsx35
-rw-r--r--packages/mobile/app/dashboard/(tabs)/settings.tsx41
-rw-r--r--packages/mobile/app/dashboard/_layout.tsx38
-rw-r--r--packages/mobile/app/dashboard/add-link.tsx57
-rw-r--r--packages/mobile/app/dashboard/add-note.tsx53
-rw-r--r--packages/mobile/app/dashboard/archive.tsx11
-rw-r--r--packages/mobile/app/dashboard/favourites.tsx11
-rw-r--r--packages/mobile/app/dashboard/lists/[slug].tsx31
-rw-r--r--packages/mobile/app/error.tsx9
-rw-r--r--packages/mobile/app/index.tsx20
-rw-r--r--packages/mobile/app/sharing.tsx99
-rw-r--r--packages/mobile/app/signin.tsx101
-rw-r--r--packages/mobile/assets/blur.jpegbin178818 -> 0 bytes
-rw-r--r--packages/mobile/assets/icon.pngbin2362 -> 0 bytes
-rw-r--r--packages/mobile/assets/splash.pngbin117993 -> 0 bytes
-rw-r--r--packages/mobile/babel.config.js9
-rw-r--r--packages/mobile/components/Logo.tsx11
-rw-r--r--packages/mobile/components/bookmarks/BookmarkCard.tsx243
-rw-r--r--packages/mobile/components/bookmarks/BookmarkList.tsx61
-rw-r--r--packages/mobile/components/ui/ActionButton.tsx21
-rw-r--r--packages/mobile/components/ui/Button.tsx81
-rw-r--r--packages/mobile/components/ui/Divider.tsx28
-rw-r--r--packages/mobile/components/ui/FullPageSpinner.tsx9
-rw-r--r--packages/mobile/components/ui/Input.tsx28
-rw-r--r--packages/mobile/components/ui/Skeleton.tsx38
-rw-r--r--packages/mobile/components/ui/Toast.tsx183
-rw-r--r--packages/mobile/eas.json19
-rw-r--r--packages/mobile/globals.css80
-rw-r--r--packages/mobile/lib/last-shared-intent.ts15
-rw-r--r--packages/mobile/lib/providers.tsx54
-rw-r--r--packages/mobile/lib/session.ts20
-rw-r--r--packages/mobile/lib/settings.ts29
-rw-r--r--packages/mobile/lib/storage-state.ts51
-rw-r--r--packages/mobile/lib/trpc.ts4
-rw-r--r--packages/mobile/lib/utils.ts6
-rw-r--r--packages/mobile/metro.config.js8
-rw-r--r--packages/mobile/nativewind-env.d.ts1
-rw-r--r--packages/mobile/package.json53
-rw-r--r--packages/mobile/tailwind.config.js71
-rw-r--r--packages/mobile/tsconfig.json10
-rw-r--r--packages/shared/package.json15
-rw-r--r--packages/shared/tsconfig.json (renamed from packages/workers/tsconfig.json)8
-rw-r--r--packages/trpc/package.json19
-rw-r--r--packages/trpc/tsconfig.json9
-rw-r--r--packages/web/README.md36
-rw-r--r--packages/web/app/api/auth/[...nextauth]/route.tsx3
-rw-r--r--packages/web/app/api/trpc/[trpc]/route.ts36
-rw-r--r--packages/web/app/dashboard/admin/page.tsx203
-rw-r--r--packages/web/app/dashboard/archive/page.tsx9
-rw-r--r--packages/web/app/dashboard/bookmarks/layout.tsx23
-rw-r--r--packages/web/app/dashboard/bookmarks/loading.tsx11
-rw-r--r--packages/web/app/dashboard/bookmarks/page.tsx5
-rw-r--r--packages/web/app/dashboard/error.tsx9
-rw-r--r--packages/web/app/dashboard/favourites/page.tsx14
-rw-r--r--packages/web/app/dashboard/layout.tsx24
-rw-r--r--packages/web/app/dashboard/lists/[listId]/page.tsx44
-rw-r--r--packages/web/app/dashboard/lists/page.tsx14
-rw-r--r--packages/web/app/dashboard/not-found.tsx7
-rw-r--r--packages/web/app/dashboard/preview/[bookmarkId]/page.tsx14
-rw-r--r--packages/web/app/dashboard/search/page.tsx41
-rw-r--r--packages/web/app/dashboard/settings/page.tsx9
-rw-r--r--packages/web/app/dashboard/tags/[tagName]/page.tsx55
-rw-r--r--packages/web/app/dashboard/tags/page.tsx56
-rw-r--r--packages/web/app/favicon.icobin15406 -> 0 bytes
-rw-r--r--packages/web/app/globals.css76
-rw-r--r--packages/web/app/layout.tsx51
-rw-r--r--packages/web/app/page.tsx12
-rw-r--r--packages/web/app/signin/page.tsx25
-rw-r--r--packages/web/components.json17
-rw-r--r--packages/web/components/dashboard/bookmarks/AddLinkButton.tsx102
-rw-r--r--packages/web/components/dashboard/bookmarks/AddToListModal.tsx168
-rw-r--r--packages/web/components/dashboard/bookmarks/BookmarkCardSkeleton.tsx30
-rw-r--r--packages/web/components/dashboard/bookmarks/BookmarkOptions.tsx185
-rw-r--r--packages/web/components/dashboard/bookmarks/BookmarkPreview.tsx101
-rw-r--r--packages/web/components/dashboard/bookmarks/BookmarkedTextEditor.tsx109
-rw-r--r--packages/web/components/dashboard/bookmarks/BookmarkedTextViewer.tsx20
-rw-r--r--packages/web/components/dashboard/bookmarks/Bookmarks.tsx32
-rw-r--r--packages/web/components/dashboard/bookmarks/BookmarksGrid.tsx64
-rw-r--r--packages/web/components/dashboard/bookmarks/LinkCard.tsx114
-rw-r--r--packages/web/components/dashboard/bookmarks/TagList.tsx39
-rw-r--r--packages/web/components/dashboard/bookmarks/TagModal.tsx207
-rw-r--r--packages/web/components/dashboard/bookmarks/TextCard.tsx94
-rw-r--r--packages/web/components/dashboard/bookmarks/TopNav.tsx43
-rw-r--r--packages/web/components/dashboard/lists/AllListsView.tsx66
-rw-r--r--packages/web/components/dashboard/lists/DeleteListButton.tsx77
-rw-r--r--packages/web/components/dashboard/lists/ListView.tsx25
-rw-r--r--packages/web/components/dashboard/search/SearchInput.tsx25
-rw-r--r--packages/web/components/dashboard/settings/AddApiKey.tsx167
-rw-r--r--packages/web/components/dashboard/settings/ApiKeySettings.tsx49
-rw-r--r--packages/web/components/dashboard/settings/DeleteApiKey.tsx74
-rw-r--r--packages/web/components/dashboard/sidebar/AllLists.tsx60
-rw-r--r--packages/web/components/dashboard/sidebar/ModileSidebar.tsx24
-rw-r--r--packages/web/components/dashboard/sidebar/ModileSidebarItem.tsx27
-rw-r--r--packages/web/components/dashboard/sidebar/NewListModal.tsx170
-rw-r--r--packages/web/components/dashboard/sidebar/Sidebar.tsx66
-rw-r--r--packages/web/components/dashboard/sidebar/SidebarItem.tsx33
-rw-r--r--packages/web/components/dashboard/sidebar/SidebarProfileOptions.tsx35
-rw-r--r--packages/web/components/signin/CredentialsForm.tsx222
-rw-r--r--packages/web/components/signin/SignInForm.tsx37
-rw-r--r--packages/web/components/signin/SignInProviderButton.tsx21
-rw-r--r--packages/web/components/ui/action-button.tsx25
-rw-r--r--packages/web/components/ui/back-button.tsx9
-rw-r--r--packages/web/components/ui/badge.tsx36
-rw-r--r--packages/web/components/ui/button.tsx56
-rw-r--r--packages/web/components/ui/card.tsx86
-rw-r--r--packages/web/components/ui/dialog.tsx122
-rw-r--r--packages/web/components/ui/dropdown-menu.tsx200
-rw-r--r--packages/web/components/ui/form.tsx177
-rw-r--r--packages/web/components/ui/imageCard.tsx70
-rw-r--r--packages/web/components/ui/input.tsx25
-rw-r--r--packages/web/components/ui/label.tsx26
-rw-r--r--packages/web/components/ui/popover.tsx31
-rw-r--r--packages/web/components/ui/scroll-area.tsx48
-rw-r--r--packages/web/components/ui/select.tsx160
-rw-r--r--packages/web/components/ui/separator.tsx31
-rw-r--r--packages/web/components/ui/skeleton.tsx15
-rw-r--r--packages/web/components/ui/spinner.tsx20
-rw-r--r--packages/web/components/ui/table.tsx117
-rw-r--r--packages/web/components/ui/tabs.tsx55
-rw-r--r--packages/web/components/ui/textarea.tsx24
-rw-r--r--packages/web/components/ui/toast.tsx127
-rw-r--r--packages/web/components/ui/toaster.tsx35
-rw-r--r--packages/web/components/ui/use-toast.ts189
-rw-r--r--packages/web/lib/bookmarkUtils.tsx22
-rw-r--r--packages/web/lib/hooks/bookmark-search.ts73
-rw-r--r--packages/web/lib/providers.tsx75
-rw-r--r--packages/web/lib/trpc.tsx5
-rw-r--r--packages/web/lib/utils.ts6
-rw-r--r--packages/web/next.config.mjs43
-rw-r--r--packages/web/package.json74
-rw-r--r--packages/web/postcss.config.js6
-rw-r--r--packages/web/public/blur.avifbin52746 -> 0 bytes
-rw-r--r--packages/web/public/icons/logo-128.pngbin2362 -> 0 bytes
-rw-r--r--packages/web/public/icons/logo-16.pngbin287 -> 0 bytes
-rw-r--r--packages/web/public/icons/logo-48.pngbin780 -> 0 bytes
-rw-r--r--packages/web/public/manifest.json25
-rw-r--r--packages/web/server/api/client.ts16
-rw-r--r--packages/web/server/auth.ts96
-rw-r--r--packages/web/tailwind.config.ts89
-rw-r--r--packages/web/tsconfig.json28
-rw-r--r--packages/web/vitest.config.ts14
-rw-r--r--packages/workers/crawler.ts201
-rw-r--r--packages/workers/index.ts16
-rw-r--r--packages/workers/openai.ts263
-rw-r--r--packages/workers/package.json45
-rw-r--r--packages/workers/search.ts116
187 files changed, 49 insertions, 8884 deletions
diff --git a/packages/browser-extension/.eslintrc.cjs b/packages/browser-extension/.eslintrc.cjs
deleted file mode 100644
index 450106a4..00000000
--- a/packages/browser-extension/.eslintrc.cjs
+++ /dev/null
@@ -1,3 +0,0 @@
-module.exports = {
- ignorePatterns: ["dist/"],
-};
diff --git a/packages/browser-extension/.gitignore b/packages/browser-extension/.gitignore
deleted file mode 100644
index a547bf36..00000000
--- a/packages/browser-extension/.gitignore
+++ /dev/null
@@ -1,24 +0,0 @@
-# Logs
-logs
-*.log
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-pnpm-debug.log*
-lerna-debug.log*
-
-node_modules
-dist
-dist-ssr
-*.local
-
-# Editor directories and files
-.vscode/*
-!.vscode/extensions.json
-.idea
-.DS_Store
-*.suo
-*.ntvs*
-*.njsproj
-*.sln
-*.sw?
diff --git a/packages/browser-extension/index.html b/packages/browser-extension/index.html
deleted file mode 100644
index e4b78eae..00000000
--- a/packages/browser-extension/index.html
+++ /dev/null
@@ -1,13 +0,0 @@
-<!doctype html>
-<html lang="en">
- <head>
- <meta charset="UTF-8" />
- <link rel="icon" type="image/svg+xml" href="/vite.svg" />
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
- <title>Vite + React + TS</title>
- </head>
- <body>
- <div id="root"></div>
- <script type="module" src="/src/main.tsx"></script>
- </body>
-</html>
diff --git a/packages/browser-extension/manifest.json b/packages/browser-extension/manifest.json
deleted file mode 100644
index d3a27e7b..00000000
--- a/packages/browser-extension/manifest.json
+++ /dev/null
@@ -1,19 +0,0 @@
-{
- "manifest_version": 3,
- "name": "Hoarder",
- "description": "An extension to bookmark links to hoarder.app",
- "version": "1.1",
- "icons": {
- "16": "public/logo-16.png",
- "48": "public/logo-48.png",
- "128": "public/logo-128.png"
- },
- "action": {
- "default_popup": "index.html"
- },
- "options_ui": {
- "page": "index.html#options",
- "open_in_tab": false
- },
- "permissions": ["storage", "tabs"]
-}
diff --git a/packages/browser-extension/package.json b/packages/browser-extension/package.json
deleted file mode 100644
index b205a257..00000000
--- a/packages/browser-extension/package.json
+++ /dev/null
@@ -1,44 +0,0 @@
-{
- "name": "browser-extension",
- "private": true,
- "version": "0.0.0",
- "type": "module",
- "scripts": {
- "dev": "vite",
- "build": "tsc && vite build",
- "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
- "preview": "vite preview"
- },
- "dependencies": {
- "@hoarder/trpc": "0.1.0",
- "@tanstack/react-query": "^5.24.8",
- "@trpc/client": "11.0.0-next-beta.308",
- "@trpc/next": "11.0.0-next-beta.308",
- "@trpc/react-query": "11.0.0-next-beta.308",
- "@trpc/server": "11.0.0-next-beta.308",
- "@types/chrome": "^0.0.260",
- "lucide-react": "^0.330.0",
- "react": "^18.2.0",
- "react-dom": "^18.2.0",
- "react-router-dom": "^6.22.0",
- "use-chrome-storage": "^1.2.2",
- "superjson": "^2.2.1",
- "zod": "^3.22.4"
- },
- "devDependencies": {
- "@crxjs/vite-plugin": "^1.0.14",
- "@types/react": "^18.2.55",
- "@types/react-dom": "^18.2.19",
- "@typescript-eslint/eslint-plugin": "^6.21.0",
- "@typescript-eslint/parser": "^6.21.0",
- "@vitejs/plugin-react-swc": "^3.5.0",
- "autoprefixer": "^10.4.17",
- "eslint": "^8.56.0",
- "eslint-plugin-react-hooks": "^4.6.0",
- "eslint-plugin-react-refresh": "^0.4.5",
- "postcss": "^8.4.35",
- "tailwindcss": "^3.4.1",
- "typescript": "^5.2.2",
- "vite": "^5.1.0"
- }
-}
diff --git a/packages/browser-extension/postcss.config.js b/packages/browser-extension/postcss.config.js
deleted file mode 100644
index 2aa7205d..00000000
--- a/packages/browser-extension/postcss.config.js
+++ /dev/null
@@ -1,6 +0,0 @@
-export default {
- plugins: {
- tailwindcss: {},
- autoprefixer: {},
- },
-};
diff --git a/packages/browser-extension/public/logo-128.png b/packages/browser-extension/public/logo-128.png
deleted file mode 100644
index 71ead90c..00000000
--- a/packages/browser-extension/public/logo-128.png
+++ /dev/null
Binary files differ
diff --git a/packages/browser-extension/public/logo-16.png b/packages/browser-extension/public/logo-16.png
deleted file mode 100644
index dd864d44..00000000
--- a/packages/browser-extension/public/logo-16.png
+++ /dev/null
Binary files differ
diff --git a/packages/browser-extension/public/logo-48.png b/packages/browser-extension/public/logo-48.png
deleted file mode 100644
index 7ba1cd49..00000000
--- a/packages/browser-extension/public/logo-48.png
+++ /dev/null
Binary files differ
diff --git a/packages/browser-extension/public/logo.png b/packages/browser-extension/public/logo.png
deleted file mode 100644
index ebe0a6a3..00000000
--- a/packages/browser-extension/public/logo.png
+++ /dev/null
Binary files differ
diff --git a/packages/browser-extension/src/BookmarkDeletedPage.tsx b/packages/browser-extension/src/BookmarkDeletedPage.tsx
deleted file mode 100644
index 23e1d9da..00000000
--- a/packages/browser-extension/src/BookmarkDeletedPage.tsx
+++ /dev/null
@@ -1,3 +0,0 @@
-export default function BookmarkDeletedPage() {
- return <p className="text-lg">Bookmark Deleted!</p>;
-}
diff --git a/packages/browser-extension/src/BookmarkSavedPage.tsx b/packages/browser-extension/src/BookmarkSavedPage.tsx
deleted file mode 100644
index f25a83ba..00000000
--- a/packages/browser-extension/src/BookmarkSavedPage.tsx
+++ /dev/null
@@ -1,62 +0,0 @@
-import { Link, useNavigate, useParams } from "react-router-dom";
-import { api } from "./utils/trpc";
-import usePluginSettings from "./utils/settings";
-import { ArrowUpRightFromSquare, Trash } from "lucide-react";
-import Spinner from "./Spinner";
-import { useState } from "react";
-
-export default function BookmarkSavedPage() {
- const { bookmarkId } = useParams();
- const navigate = useNavigate();
- const [error, setError] = useState("");
-
- const { mutate: deleteBookmark, isPending } =
- api.bookmarks.deleteBookmark.useMutation({
- onSuccess: () => {
- navigate("/bookmarkdeleted");
- },
- onError: (e) => {
- setError(e.message);
- },
- });
-
- const { settings } = usePluginSettings();
-
- if (!bookmarkId) {
- return <div>NOT FOUND</div>;
- }
-
- return (
- <div className="flex flex-col gap-2">
- {error && <p className="text-red-500">{error}</p>}
- <div className="flex items-center justify-between gap-2">
- <p className="text-lg">Bookmarked!</p>
- <div className="flex gap-2">
- <Link
- className="flex gap-2 rounded-md p-3 text-black hover:text-black"
- target="_blank"
- to={`${settings.address}/dashboard/preview/${bookmarkId}`}
- >
- <ArrowUpRightFromSquare className="my-auto" size="20" />
- <p className="my-auto">Open</p>
- </Link>
- <button
- onClick={() => deleteBookmark({ bookmarkId: bookmarkId })}
- className="flex gap-2 bg-transparent text-red-500 hover:text-red-500"
- >
- {!isPending ? (
- <>
- <Trash className="my-auto" size="20" />
- <p className="my-auto">Delete</p>
- </>
- ) : (
- <span className="m-auto">
- <Spinner />
- </span>
- )}
- </button>
- </div>
- </div>
- </div>
- );
-}
diff --git a/packages/browser-extension/src/Layout.tsx b/packages/browser-extension/src/Layout.tsx
deleted file mode 100644
index f8279a18..00000000
--- a/packages/browser-extension/src/Layout.tsx
+++ /dev/null
@@ -1,51 +0,0 @@
-import { Outlet } from "react-router-dom";
-import { Home, RefreshCw, Settings, X } from "lucide-react";
-import { useNavigate } from "react-router-dom";
-import usePluginSettings from "./utils/settings";
-
-export default function Layout() {
- const navigate = useNavigate();
- const { settings, isPending: isInit } = usePluginSettings();
- if (!isInit) {
- return <div className="p-4">Loading ... </div>;
- }
-
- if (!settings.apiKey || !settings.address) {
- navigate("/notconfigured");
- return;
- }
-
- return (
- <div className="flex flex-col space-y-2">
- <div className="rounded-md bg-yellow-100 p-4">
- <Outlet />
- </div>
- <hr />
- <div className="flex justify-between space-x-3">
- <div className="my-auto">
- <a
- className="flex gap-2 text-black"
- target="_blank"
- href={`${settings.address}/dashboard/bookmarks`}
- >
- <Home />
- <span className="text-md my-auto">Bookmarks</span>
- </a>
- </div>
- <div className="flex space-x-3">
- {process.env.NODE_ENV == "development" && (
- <button onClick={() => navigate(0)}>
- <RefreshCw className="w-4" />
- </button>
- )}
- <button onClick={() => navigate("/options")}>
- <Settings className="w-4" />
- </button>
- <button onClick={() => window.close()}>
- <X className="w-4" />
- </button>
- </div>
- </div>
- </div>
- );
-}
diff --git a/packages/browser-extension/src/Logo.tsx b/packages/browser-extension/src/Logo.tsx
deleted file mode 100644
index 6b29e68c..00000000
--- a/packages/browser-extension/src/Logo.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import { PackageOpen } from "lucide-react";
-
-export default function Logo() {
- return (
- <span className="mx-auto flex gap-2">
- <PackageOpen size="40" className="my-auto" />
- <p className="text-4xl">Hoarder</p>
- </span>
- );
-}
diff --git a/packages/browser-extension/src/NotConfiguredPage.tsx b/packages/browser-extension/src/NotConfiguredPage.tsx
deleted file mode 100644
index fc5c8f47..00000000
--- a/packages/browser-extension/src/NotConfiguredPage.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-import { useEffect, useState } from "react";
-import { useNavigate } from "react-router-dom";
-import usePluginSettings from "./utils/settings";
-import Logo from "./Logo";
-
-export default function NotConfiguredPage() {
- const navigate = useNavigate();
-
- const { settings, setSettings } = usePluginSettings();
-
- const [error, setError] = useState("");
- const [serverAddress, setServerAddress] = useState(settings.address);
- useEffect(() => {
- setServerAddress(settings.address);
- }, [settings.address]);
-
- const onSave = () => {
- if (serverAddress == "") {
- setError("Server address is required");
- return;
- }
- setSettings((s) => ({ ...s, address: serverAddress }));
- navigate("/signin");
- };
-
- return (
- <div className="flex flex-col space-y-2">
- <Logo />
- <span className="pt-3">
- To use the plugin, you need to configure it first.
- </span>
- <p className="text-red-500">{error}</p>
- <div className="flex gap-2">
- <label className="my-auto">Server Address</label>
- <input
- name="address"
- value={serverAddress}
- className="h-8 flex-1 rounded-lg border border-gray-300 p-2"
- onChange={(e) => setServerAddress(e.target.value)}
- />
- </div>
- <button className="bg-black text-white" onClick={onSave}>
- Configure
- </button>
- </div>
- );
-}
diff --git a/packages/browser-extension/src/OptionsPage.tsx b/packages/browser-extension/src/OptionsPage.tsx
deleted file mode 100644
index 6407b3cc..00000000
--- a/packages/browser-extension/src/OptionsPage.tsx
+++ /dev/null
@@ -1,60 +0,0 @@
-import React, { useEffect } from "react";
-import usePluginSettings from "./utils/settings";
-import { api } from "./utils/trpc";
-import Spinner from "./Spinner";
-import { useNavigate } from "react-router-dom";
-import Logo from "./Logo";
-
-export default function OptionsPage() {
- const navigate = useNavigate();
- const { settings, setSettings } = usePluginSettings();
-
- const { data: whoami, error: whoAmIError } = api.users.whoami.useQuery(
- undefined,
- {
- enabled: settings.address != "",
- },
- );
-
- const invalidateWhoami = api.useUtils().users.whoami.refetch;
-
- useEffect(() => {
- invalidateWhoami();
- }, [settings, invalidateWhoami]);
-
- let loggedInMessage: React.ReactNode;
- if (whoAmIError) {
- if (whoAmIError.data?.code == "UNAUTHORIZED") {
- loggedInMessage = <span>Not logged in</span>;
- } else {
- loggedInMessage = (
- <span>Something went wrong: {whoAmIError.message}</span>
- );
- }
- } else if (whoami) {
- loggedInMessage = <span>{whoami.email}</span>;
- } else {
- loggedInMessage = <Spinner />;
- }
-
- const onLogout = () => {
- setSettings((s) => ({ ...s, apiKey: "" }));
- invalidateWhoami();
- navigate("/notconfigured");
- };
-
- return (
- <div className="flex flex-col space-y-2">
- <Logo />
- <span className="text-lg">Settings</span>
- <hr />
- <div className="flex gap-2">
- <span className="my-auto">Logged in as:</span>
- {loggedInMessage}
- </div>
- <button className="rounded-lg border border-gray-200" onClick={onLogout}>
- Logout
- </button>
- </div>
- );
-}
diff --git a/packages/browser-extension/src/SavePage.tsx b/packages/browser-extension/src/SavePage.tsx
deleted file mode 100644
index 638af149..00000000
--- a/packages/browser-extension/src/SavePage.tsx
+++ /dev/null
@@ -1,60 +0,0 @@
-import { useEffect, useState } from "react";
-import Spinner from "./Spinner";
-import { api } from "./utils/trpc";
-import { Navigate } from "react-router-dom";
-
-export default function SavePage() {
- const [error, setError] = useState<string | undefined>(undefined);
-
- const {
- data,
- mutate: createBookmark,
- status,
- } = api.bookmarks.createBookmark.useMutation({
- onError: (e) => {
- setError("Something went wrong: " + e.message);
- },
- });
-
- useEffect(() => {
- async function runSave() {
- let currentUrl;
- const [currentTab] = await chrome.tabs.query({
- active: true,
- lastFocusedWindow: true,
- });
- if (currentTab?.url) {
- currentUrl = currentTab.url;
- } else {
- setError("Couldn't find the URL of the current tab");
- return;
- }
-
- createBookmark({
- type: "link",
- url: currentUrl,
- });
- }
- runSave();
- }, [createBookmark]);
-
- switch (status) {
- case "error": {
- return <div className="text-red-500">{error}</div>;
- }
- case "success": {
- return <Navigate to={`/bookmark/${data.id}`} />;
- }
- case "pending": {
- return (
- <div className="flex justify-between text-lg">
- <span>Saving Bookmark </span>
- <Spinner />
- </div>
- );
- }
- case "idle": {
- return <div />;
- }
- }
-}
diff --git a/packages/browser-extension/src/SignInPage.tsx b/packages/browser-extension/src/SignInPage.tsx
deleted file mode 100644
index 6db7c348..00000000
--- a/packages/browser-extension/src/SignInPage.tsx
+++ /dev/null
@@ -1,89 +0,0 @@
-import { useState } from "react";
-import { api } from "./utils/trpc";
-import usePluginSettings from "./utils/settings";
-import { useNavigate } from "react-router-dom";
-import Logo from "./Logo";
-
-export default function SignInPage() {
- const navigate = useNavigate();
- const { setSettings } = usePluginSettings();
-
- const {
- mutate: login,
- error,
- isPending,
- } = api.apiKeys.exchange.useMutation({
- onSuccess: (resp) => {
- setSettings((s) => ({ ...s, apiKey: resp.key }));
- navigate("/options");
- },
- onError: () => {},
- });
-
- const [formData, setFormData] = useState<{
- email: string;
- password: string;
- }>({
- email: "",
- password: "",
- });
-
- const onSubmit = (e: React.FormEvent) => {
- e.preventDefault();
- const randStr = (Math.random() + 1).toString(36).substring(5);
- login({ ...formData, keyName: `Browser extension: (${randStr})` });
- };
-
- let errorMessage = "";
- if (error) {
- if (error.data?.code == "UNAUTHORIZED") {
- errorMessage = "Wrong username or password";
- } else {
- errorMessage = error.message;
- }
- }
-
- return (
- <div className="flex flex-col space-y-2">
- <Logo />
- <p className="text-lg">Login</p>
- <p className="text-red-500">{errorMessage}</p>
- <form className="flex flex-col gap-y-2" onSubmit={onSubmit}>
- <div className="flex flex-col gap-y-1">
- <label className="my-auto font-bold">Email</label>
- <input
- value={formData.email}
- onChange={(e) =>
- setFormData((f) => ({ ...f, email: e.target.value }))
- }
- type="text"
- name="email"
- className="h-8 flex-1 rounded-lg border border-gray-300 p-2"
- />
- </div>
- <div className="flex flex-col gap-y-1">
- <label className="my-auto font-bold">Password</label>
- <input
- value={formData.password}
- onChange={(e) =>
- setFormData((f) => ({
- ...f,
- password: e.target.value,
- }))
- }
- type="password"
- name="password"
- className="h-8 flex-1 rounded-lg border border-gray-300 p-2"
- />
- </div>
- <button
- className="bg-black text-white"
- type="submit"
- disabled={isPending}
- >
- Login
- </button>
- </form>
- </div>
- );
-}
diff --git a/packages/browser-extension/src/Spinner.tsx b/packages/browser-extension/src/Spinner.tsx
deleted file mode 100644
index 9fd8839b..00000000
--- a/packages/browser-extension/src/Spinner.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-export default function Spinner() {
- return (
- <svg
- xmlns="http://www.w3.org/2000/svg"
- width="24"
- height="24"
- viewBox="0 0 24 24"
- fill="none"
- stroke="currentColor"
- strokeWidth="2"
- strokeLinecap="round"
- strokeLinejoin="round"
- className="animate-spin"
- >
- <path d="M21 12a9 9 0 1 1-6.219-8.56" />
- </svg>
- );
-}
diff --git a/packages/browser-extension/src/assets/react.svg b/packages/browser-extension/src/assets/react.svg
deleted file mode 100644
index 6c87de9b..00000000
--- a/packages/browser-extension/src/assets/react.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg> \ No newline at end of file
diff --git a/packages/browser-extension/src/index.css b/packages/browser-extension/src/index.css
deleted file mode 100644
index e7d4bb2f..00000000
--- a/packages/browser-extension/src/index.css
+++ /dev/null
@@ -1,72 +0,0 @@
-@tailwind base;
-@tailwind components;
-@tailwind utilities;
-
-:root {
- font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
- line-height: 1.5;
- font-weight: 400;
-
- color-scheme: light dark;
- color: rgba(255, 255, 255, 0.87);
- background-color: #242424;
-
- font-synthesis: none;
- text-rendering: optimizeLegibility;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
-}
-
-a {
- font-weight: 500;
- color: #646cff;
- text-decoration: inherit;
-}
-a:hover {
- color: #535bf2;
-}
-
-body {
- margin: 0;
- display: flex;
- place-items: center;
- min-width: 320px;
- min-height: 100vh;
-}
-
-h1 {
- font-size: 3.2em;
- line-height: 1.1;
-}
-
-button {
- border-radius: 8px;
- border: 1px solid transparent;
- padding: 0.6em 1.2em;
- font-size: 1em;
- font-weight: 500;
- font-family: inherit;
- background-color: #1a1a1a;
- cursor: pointer;
- transition: border-color 0.25s;
-}
-button:hover {
- border-color: #646cff;
-}
-button:focus,
-button:focus-visible {
- outline: 4px auto -webkit-focus-ring-color;
-}
-
-@media (prefers-color-scheme: light) {
- :root {
- color: #213547;
- background-color: #ffffff;
- }
- a:hover {
- color: #747bff;
- }
- button {
- background-color: #f9f9f9;
- }
-}
diff --git a/packages/browser-extension/src/main.tsx b/packages/browser-extension/src/main.tsx
deleted file mode 100644
index 085a5a69..00000000
--- a/packages/browser-extension/src/main.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-import ReactDOM from "react-dom/client";
-import "./index.css";
-import OptionsPage from "./OptionsPage.tsx";
-import NotConfiguredPage from "./NotConfiguredPage.tsx";
-import { Providers } from "./utils/providers.tsx";
-import BookmarkSavedPage from "./BookmarkSavedPage.tsx";
-import { HashRouter, Routes, Route } from "react-router-dom";
-import Layout from "./Layout.tsx";
-import SavePage from "./SavePage.tsx";
-import BookmarkDeletedPage from "./BookmarkDeletedPage.tsx";
-import SignInPage from "./SignInPage.tsx";
-
-function App() {
- return (
- <div className="w-96 p-4">
- <Providers>
- <HashRouter>
- <Routes>
- <Route element={<Layout />}>
- <Route path="/" element={<SavePage />} />
- <Route
- path="/bookmark/:bookmarkId"
- element={<BookmarkSavedPage />}
- />
- <Route
- path="/bookmarkdeleted"
- element={<BookmarkDeletedPage />}
- />
- </Route>
- <Route path="/notconfigured" element={<NotConfiguredPage />} />
- <Route path="/options" element={<OptionsPage />} />
- <Route path="/signin" element={<SignInPage />} />
- </Routes>
- </HashRouter>
- </Providers>
- </div>
- );
-}
-
-ReactDOM.createRoot(document.getElementById("root")!).render(<App />);
diff --git a/packages/browser-extension/src/utils/providers.tsx b/packages/browser-extension/src/utils/providers.tsx
deleted file mode 100644
index d20f2512..00000000
--- a/packages/browser-extension/src/utils/providers.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
-import { httpBatchLink } from "@trpc/client";
-import { useEffect, useState } from "react";
-import { api } from "./trpc";
-import usePluginSettings, { getPluginSettings } from "./settings";
-import superjson from "superjson";
-
-function getTRPCClient(address: string) {
- return api.createClient({
- links: [
- httpBatchLink({
- url: `${address}/api/trpc`,
- async headers() {
- const settings = await getPluginSettings();
- return {
- Authorization: `Bearer ${settings.apiKey}`,
- };
- },
- transformer: superjson,
- }),
- ],
- });
-}
-
-export function Providers({ children }: { children: React.ReactNode }) {
- const { settings } = usePluginSettings();
- const [queryClient] = useState(() => new QueryClient());
-
- const [trpcClient, setTrpcClient] = useState<
- ReturnType<typeof getTRPCClient>
- >(getTRPCClient(settings.address));
-
- useEffect(() => {
- setTrpcClient(getTRPCClient(settings.address));
- }, [settings.address]);
-
- return (
- <api.Provider
- key={settings.address}
- client={trpcClient}
- queryClient={queryClient}
- >
- <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
- </api.Provider>
- );
-}
diff --git a/packages/browser-extension/src/utils/settings.ts b/packages/browser-extension/src/utils/settings.ts
deleted file mode 100644
index 37f474c0..00000000
--- a/packages/browser-extension/src/utils/settings.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { useChromeStorageSync } from "use-chrome-storage";
-
-export type Settings = {
- apiKey: string;
- address: string;
-};
-
-export default function usePluginSettings() {
- const [settings, setSettings, _1, _2, isInit] = useChromeStorageSync(
- "settings",
- {
- apiKey: "",
- address: "",
- } as Settings,
- );
-
- return { settings, setSettings, isPending: isInit };
-}
-
-export async function getPluginSettings() {
- return (await chrome.storage.sync.get("settings")).settings as Settings;
-}
diff --git a/packages/browser-extension/src/utils/trpc.ts b/packages/browser-extension/src/utils/trpc.ts
deleted file mode 100644
index da21a55a..00000000
--- a/packages/browser-extension/src/utils/trpc.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-import { createTRPCReact } from "@trpc/react-query";
-import type { AppRouter } from "@hoarder/trpc/routers/_app";
-
-export const api = createTRPCReact<AppRouter>();
diff --git a/packages/browser-extension/src/vite-env.d.ts b/packages/browser-extension/src/vite-env.d.ts
deleted file mode 100644
index 11f02fe2..00000000
--- a/packages/browser-extension/src/vite-env.d.ts
+++ /dev/null
@@ -1 +0,0 @@
-/// <reference types="vite/client" />
diff --git a/packages/browser-extension/tailwind.config.js b/packages/browser-extension/tailwind.config.js
deleted file mode 100644
index 1c0c7c87..00000000
--- a/packages/browser-extension/tailwind.config.js
+++ /dev/null
@@ -1,10 +0,0 @@
-/** @type {import('tailwindcss').Config} */
-const tailwindConfig = {
- content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
- theme: {
- extend: {},
- },
- plugins: [],
-};
-
-export default tailwindConfig;
diff --git a/packages/browser-extension/tsconfig.json b/packages/browser-extension/tsconfig.json
deleted file mode 100644
index a7fc6fbf..00000000
--- a/packages/browser-extension/tsconfig.json
+++ /dev/null
@@ -1,25 +0,0 @@
-{
- "compilerOptions": {
- "target": "ES2020",
- "useDefineForClassFields": true,
- "lib": ["ES2020", "DOM", "DOM.Iterable"],
- "module": "ESNext",
- "skipLibCheck": true,
-
- /* Bundler mode */
- "moduleResolution": "bundler",
- "allowImportingTsExtensions": true,
- "resolveJsonModule": true,
- "isolatedModules": true,
- "noEmit": true,
- "jsx": "react-jsx",
-
- /* Linting */
- "strict": true,
- "noUnusedLocals": true,
- "noUnusedParameters": true,
- "noFallthroughCasesInSwitch": true
- },
- "include": ["src"],
- "references": [{ "path": "./tsconfig.node.json" }]
-}
diff --git a/packages/browser-extension/tsconfig.node.json b/packages/browser-extension/tsconfig.node.json
deleted file mode 100644
index 97ede7ee..00000000
--- a/packages/browser-extension/tsconfig.node.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "compilerOptions": {
- "composite": true,
- "skipLibCheck": true,
- "module": "ESNext",
- "moduleResolution": "bundler",
- "allowSyntheticDefaultImports": true,
- "strict": true
- },
- "include": ["vite.config.ts"]
-}
diff --git a/packages/browser-extension/vite.config.ts b/packages/browser-extension/vite.config.ts
deleted file mode 100644
index 29c6bc6e..00000000
--- a/packages/browser-extension/vite.config.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { defineConfig } from "vite";
-import react from "@vitejs/plugin-react-swc";
-import { crx } from "@crxjs/vite-plugin";
-import manifest from "./manifest.json";
-
-// https://vitejs.dev/config/
-export default defineConfig({
- plugins: [react(), crx({ manifest })],
-});
diff --git a/packages/db/package.json b/packages/db/package.json
index 20dee1e9..aebbf8fe 100644
--- a/packages/db/package.json
+++ b/packages/db/package.json
@@ -4,6 +4,7 @@
"version": "0.1.0",
"private": true,
"main": "index.ts",
+ "type": "module",
"scripts": {
"typecheck": "tsc --noEmit",
"migrate": "tsx migrate.ts",
@@ -18,8 +19,18 @@
"tsx": "^4.7.1"
},
"devDependencies": {
+ "@hoarder/eslint-config": "workspace:^0.2.0",
+ "@hoarder/prettier-config": "workspace:^0.1.0",
+ "@hoarder/tsconfig": "workspace:^0.1.0",
"@tsconfig/node21": "^21.0.1",
"@types/better-sqlite3": "^7.6.9",
"drizzle-kit": "^0.20.14"
- }
+ },
+ "eslintConfig": {
+ "root": true,
+ "extends": [
+ "@hoarder/eslint-config/base"
+ ]
+ },
+ "prettier": "@hoarder/prettier-config"
}
diff --git a/packages/db/tsconfig.json b/packages/db/tsconfig.json
index cf49c407..59982ea1 100644
--- a/packages/db/tsconfig.json
+++ b/packages/db/tsconfig.json
@@ -1,11 +1,10 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
- "extends": "@tsconfig/node21/tsconfig.json",
+ "extends": "@hoarder/tsconfig/node.json",
"include": ["**/*.ts"],
"exclude": ["node_modules"],
"compilerOptions": {
- "module": "ESNext",
- "moduleResolution": "node",
- "esModuleInterop": true
+ "baseUrl" : ".",
+ "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
}
}
diff --git a/packages/mobile/.eslintrc.js b/packages/mobile/.eslintrc.js
deleted file mode 100644
index 53beac49..00000000
--- a/packages/mobile/.eslintrc.js
+++ /dev/null
@@ -1,4 +0,0 @@
-module.exports = {
- root: true,
- extends: ["universe/native"],
-};
diff --git a/packages/mobile/.gitignore b/packages/mobile/.gitignore
deleted file mode 100644
index 2920e5a8..00000000
--- a/packages/mobile/.gitignore
+++ /dev/null
@@ -1,39 +0,0 @@
-# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
-
-# dependencies
-node_modules/
-
-# Expo
-.expo/
-dist/
-web-build/
-
-# Native
-*.orig.*
-*.jks
-*.p8
-*.p12
-*.key
-*.mobileprovision
-
-# Metro
-.metro-health-check*
-
-# debug
-npm-debug.*
-yarn-debug.*
-yarn-error.*
-
-# macOS
-.DS_Store
-*.pem
-
-# local env files
-.env*.local
-
-# typescript
-*.tsbuildinfo
-
-#build files
-ios/
-android/
diff --git a/packages/mobile/.npmrc b/packages/mobile/.npmrc
deleted file mode 100644
index d67f3748..00000000
--- a/packages/mobile/.npmrc
+++ /dev/null
@@ -1 +0,0 @@
-node-linker=hoisted
diff --git a/packages/mobile/app.json b/packages/mobile/app.json
deleted file mode 100644
index e16baa37..00000000
--- a/packages/mobile/app.json
+++ /dev/null
@@ -1,57 +0,0 @@
-{
- "expo": {
- "name": "Hoarder App",
- "slug": "hoarder",
- "scheme": "hoarder",
- "version": "1.2.0",
- "orientation": "portrait",
- "icon": "./assets/icon.png",
- "userInterfaceStyle": "light",
- "splash": {
- "image": "./assets/splash.png",
- "resizeMode": "contain",
- "backgroundColor": "#ffffff"
- },
- "assetBundlePatterns": [
- "**/*"
- ],
- "ios": {
- "supportsTablet": true,
- "bundleIdentifier": "app.hoarder.hoardermobile",
- "config": {
- "usesNonExemptEncryption": false
- }
- },
- "android": {
- "adaptiveIcon": {
- "foregroundImage": "./assets/icon.png",
- "backgroundColor": "#ffffff"
- },
- "package": "app.hoarder.hoardermobile"
- },
- "plugins": [
- "expo-router",
- [
- "expo-share-intent",
- {
- "iosActivationRules": {
- "NSExtensionActivationSupportsWebURLWithMaxCount": 1,
- "NSExtensionActivationSupportsWebPageWithMaxCount": 0,
- "NSExtensionActivationSupportsImageWithMaxCount": 0,
- "NSExtensionActivationSupportsMovieWithMaxCount": 0,
- "NSExtensionActivationSupportsText": true
- }
- }
- ],
- "expo-secure-store"
- ],
- "extra": {
- "router": {
- "origin": false
- },
- "eas": {
- "projectId": "d6d14643-ad43-4cd3-902a-92c5944d5e45"
- }
- }
- }
-}
diff --git a/packages/mobile/app/+not-found.tsx b/packages/mobile/app/+not-found.tsx
deleted file mode 100644
index 466505b6..00000000
--- a/packages/mobile/app/+not-found.tsx
+++ /dev/null
@@ -1,6 +0,0 @@
-import { View } from "react-native";
-
-// This is kinda important given that the sharing modal always resolve to an unknown route
-export default function NotFound() {
- return <View />;
-}
diff --git a/packages/mobile/app/_layout.tsx b/packages/mobile/app/_layout.tsx
deleted file mode 100644
index 6304ced5..00000000
--- a/packages/mobile/app/_layout.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-import "@/globals.css";
-import "expo-dev-client";
-
-import { useRouter } from "expo-router";
-import { Stack } from "expo-router/stack";
-import { useShareIntent } from "expo-share-intent";
-import { StatusBar } from "expo-status-bar";
-import { useEffect } from "react";
-import { View } from "react-native";
-
-import { useLastSharedIntent } from "@/lib/last-shared-intent";
-import { Providers } from "@/lib/providers";
-
-export default function RootLayout() {
- const router = useRouter();
- const { hasShareIntent, shareIntent, resetShareIntent } = useShareIntent();
-
- const lastSharedIntent = useLastSharedIntent();
-
- useEffect(() => {
- const intentJson = JSON.stringify(shareIntent);
- if (hasShareIntent && !lastSharedIntent.isPreviouslyShared(intentJson)) {
- // TODO: Remove once https://github.com/achorein/expo-share-intent/issues/14 is fixed
- lastSharedIntent.setIntent(intentJson);
- router.replace({
- pathname: "sharing",
- params: { shareIntent: intentJson },
- });
- resetShareIntent();
- }
- }, [hasShareIntent]);
-
- return (
- <Providers>
- <View className="h-full w-full bg-white">
- <Stack
- screenOptions={{
- headerShown: false,
- }}
- >
- <Stack.Screen name="index" />
- <Stack.Screen
- name="sharing"
- options={{
- presentation: "modal",
- }}
- />
- </Stack>
- <StatusBar style="auto" />
- </View>
- </Providers>
- );
-}
diff --git a/packages/mobile/app/dashboard/(tabs)/_layout.tsx b/packages/mobile/app/dashboard/(tabs)/_layout.tsx
deleted file mode 100644
index 5b2d810a..00000000
--- a/packages/mobile/app/dashboard/(tabs)/_layout.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import { Tabs } from "expo-router";
-import { ClipboardList, Home, Search, Settings } from "lucide-react-native";
-import React from "react";
-
-export default function TabLayout() {
- return (
- <Tabs screenOptions={{ tabBarActiveTintColor: "blue" }}>
- <Tabs.Screen
- name="index"
- options={{
- title: "Home",
- tabBarIcon: ({ color }) => <Home color={color} />,
- }}
- />
- <Tabs.Screen
- name="search"
- options={{
- title: "Search",
- tabBarIcon: ({ color }) => <Search color={color} />,
- }}
- />
- <Tabs.Screen
- name="lists"
- options={{
- title: "Lists",
- tabBarIcon: ({ color }) => <ClipboardList color={color} />,
- }}
- />
- <Tabs.Screen
- name="settings"
- options={{
- title: "Settings",
- tabBarIcon: ({ color }) => <Settings color={color} />,
- }}
- />
- </Tabs>
- );
-}
diff --git a/packages/mobile/app/dashboard/(tabs)/index.tsx b/packages/mobile/app/dashboard/(tabs)/index.tsx
deleted file mode 100644
index b2349525..00000000
--- a/packages/mobile/app/dashboard/(tabs)/index.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import { Link, Stack } from "expo-router";
-import { SquarePen, Link as LinkIcon } from "lucide-react-native";
-import { View } from "react-native";
-
-import BookmarkList from "@/components/bookmarks/BookmarkList";
-
-function HeaderRight() {
- return (
- <View className="flex flex-row">
- <Link href="dashboard/add-link" className="mt-2 px-2">
- <LinkIcon />
- </Link>
- <Link href="dashboard/add-note" className="mt-2 px-2">
- <SquarePen />
- </Link>
- </View>
- );
-}
-
-export default function Home() {
- return (
- <>
- <Stack.Screen
- options={{
- headerRight: () => <HeaderRight />,
- }}
- />
- <BookmarkList archived={false} />
- </>
- );
-}
diff --git a/packages/mobile/app/dashboard/(tabs)/lists.tsx b/packages/mobile/app/dashboard/(tabs)/lists.tsx
deleted file mode 100644
index b534ddda..00000000
--- a/packages/mobile/app/dashboard/(tabs)/lists.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-import { Link } from "expo-router";
-import { useEffect, useState } from "react";
-import { FlatList, View } from "react-native";
-
-import { api } from "@/lib/trpc";
-
-export default function Lists() {
- const [refreshing, setRefreshing] = useState(false);
- const { data: lists, isPending } = api.lists.list.useQuery();
- const apiUtils = api.useUtils();
-
- useEffect(() => {
- setRefreshing(isPending);
- }, [isPending]);
-
- if (!lists) {
- // Add spinner
- return <View />;
- }
-
- const onRefresh = () => {
- apiUtils.lists.list.invalidate();
- };
-
- const links = [
- {
- id: "fav",
- logo: "⭐️",
- name: "Favourites",
- href: "/dashboard/favourites",
- },
- {
- id: "arch",
- logo: "🗄️",
- name: "Archive",
- href: "/dashboard/archive",
- },
- ];
-
- links.push(
- ...lists.lists.map((l) => ({
- id: l.id,
- logo: l.icon,
- name: l.name,
- href: `/dashboard/lists/${l.id}`,
- })),
- );
-
- return (
- <FlatList
- contentContainerStyle={{
- gap: 10,
- marginTop: 10,
- }}
- renderItem={(l) => (
- <View className="mx-2 block rounded-xl border border-gray-100 bg-white px-4 py-2">
- <Link key={l.item.id} href={l.item.href} className="text-lg">
- {l.item.logo} {l.item.name}
- </Link>
- </View>
- )}
- data={links}
- refreshing={refreshing}
- onRefresh={onRefresh}
- />
- );
-}
diff --git a/packages/mobile/app/dashboard/(tabs)/search.tsx b/packages/mobile/app/dashboard/(tabs)/search.tsx
deleted file mode 100644
index 980cab36..00000000
--- a/packages/mobile/app/dashboard/(tabs)/search.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import { keepPreviousData } from "@tanstack/react-query";
-import { useState } from "react";
-import { View } from "react-native";
-import { useDebounce } from "use-debounce";
-
-import BookmarkList from "@/components/bookmarks/BookmarkList";
-import { Divider } from "@/components/ui/Divider";
-import { Input } from "@/components/ui/Input";
-import { api } from "@/lib/trpc";
-
-export default function Search() {
- const [search, setSearch] = useState("");
-
- const [query] = useDebounce(search, 200);
-
- const { data } = api.bookmarks.searchBookmarks.useQuery(
- { text: query },
- { placeholderData: keepPreviousData },
- );
-
- return (
- <View>
- <Input
- placeholder="Search"
- className="mx-4 mt-4 bg-white"
- value={search}
- onChangeText={setSearch}
- autoFocus
- autoCapitalize="none"
- />
- <Divider orientation="horizontal" className="mb-1 mt-4 w-full" />
- {data && <BookmarkList ids={data.bookmarks.map((b) => b.id)} />}
- </View>
- );
-}
diff --git a/packages/mobile/app/dashboard/(tabs)/settings.tsx b/packages/mobile/app/dashboard/(tabs)/settings.tsx
deleted file mode 100644
index 9f86d5ec..00000000
--- a/packages/mobile/app/dashboard/(tabs)/settings.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import { useRouter } from "expo-router";
-import { useEffect } from "react";
-import { Text, View } from "react-native";
-
-import Logo from "@/components/Logo";
-import { Button } from "@/components/ui/Button";
-import { useSession } from "@/lib/session";
-import { api } from "@/lib/trpc";
-
-export default function Dashboard() {
- const router = useRouter();
-
- const { isLoggedIn, logout } = useSession();
-
- useEffect(() => {
- if (isLoggedIn !== undefined && !isLoggedIn) {
- router.replace("signin");
- }
- }, [isLoggedIn]);
-
- const { data, error, isLoading } = api.users.whoami.useQuery();
-
- useEffect(() => {
- if (error?.data?.code === "UNAUTHORIZED") {
- logout();
- }
- }, [error]);
-
- return (
- <View className="flex h-full w-full items-center gap-4 p-4">
- <Logo />
- <View className="w-full rounded-lg bg-white px-4 py-2">
- <Text className="text-lg">
- {isLoading ? "Loading ..." : data?.email}
- </Text>
- </View>
-
- <Button className="w-full" label="Log Out" onPress={logout} />
- </View>
- );
-}
diff --git a/packages/mobile/app/dashboard/_layout.tsx b/packages/mobile/app/dashboard/_layout.tsx
deleted file mode 100644
index ff2384d2..00000000
--- a/packages/mobile/app/dashboard/_layout.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import { Stack } from "expo-router/stack";
-
-export default function Dashboard() {
- return (
- <Stack>
- <Stack.Screen
- name="(tabs)"
- options={{ headerShown: false, title: "Home" }}
- />
- <Stack.Screen
- name="favourites"
- options={{
- title: "⭐️ Favourites",
- }}
- />
- <Stack.Screen
- name="archive"
- options={{
- title: "🗄️ Archive",
- }}
- />
- <Stack.Screen
- name="add-link"
- options={{
- title: "New link",
- presentation: "modal",
- }}
- />
- <Stack.Screen
- name="add-note"
- options={{
- title: "New Note",
- presentation: "modal",
- }}
- />
- </Stack>
- );
-}
diff --git a/packages/mobile/app/dashboard/add-link.tsx b/packages/mobile/app/dashboard/add-link.tsx
deleted file mode 100644
index 69a9c7a2..00000000
--- a/packages/mobile/app/dashboard/add-link.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-import { useRouter } from "expo-router";
-import { useState } from "react";
-import { View, Text } from "react-native";
-
-import { Button } from "@/components/ui/Button";
-import { Input } from "@/components/ui/Input";
-import { api } from "@/lib/trpc";
-
-export default function AddNote() {
- const [text, setText] = useState("");
- const [error, setError] = useState<string | undefined>();
- const router = useRouter();
- const invalidateAllBookmarks =
- api.useUtils().bookmarks.getBookmarks.invalidate;
-
- const { mutate } = api.bookmarks.createBookmark.useMutation({
- onSuccess: () => {
- invalidateAllBookmarks();
- if (router.canGoBack()) {
- router.replace("../");
- } else {
- router.replace("dashboard");
- }
- },
- onError: (e) => {
- let message;
- if (e.data?.code === "BAD_REQUEST") {
- const error = JSON.parse(e.message)[0];
- message = error.message;
- } else {
- message = `Something went wrong: ${e.message}`;
- }
- setError(message);
- },
- });
-
- return (
- <View className="flex gap-2 p-4">
- {error && (
- <Text className="w-full text-center text-red-500">{error}</Text>
- )}
- <Input
- className="bg-white"
- value={text}
- onChangeText={setText}
- placeholder="Link"
- autoCapitalize="none"
- inputMode="url"
- autoFocus
- />
- <Button
- onPress={() => mutate({ type: "link", url: text })}
- label="Add Link"
- />
- </View>
- );
-}
diff --git a/packages/mobile/app/dashboard/add-note.tsx b/packages/mobile/app/dashboard/add-note.tsx
deleted file mode 100644
index cf775a15..00000000
--- a/packages/mobile/app/dashboard/add-note.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-import { useRouter } from "expo-router";
-import { useState } from "react";
-import { View, Text } from "react-native";
-
-import { Button } from "@/components/ui/Button";
-import { Input } from "@/components/ui/Input";
-import { api } from "@/lib/trpc";
-
-export default function AddNote() {
- const [text, setText] = useState("");
- const [error, setError] = useState<string | undefined>();
- const router = useRouter();
- const invalidateAllBookmarks =
- api.useUtils().bookmarks.getBookmarks.invalidate;
-
- const { mutate } = api.bookmarks.createBookmark.useMutation({
- onSuccess: () => {
- invalidateAllBookmarks();
- if (router.canGoBack()) {
- router.replace("../");
- } else {
- router.replace("dashboard");
- }
- },
- onError: (e) => {
- let message;
- if (e.data?.code === "BAD_REQUEST") {
- const error = JSON.parse(e.message)[0];
- message = error.message;
- } else {
- message = `Something went wrong: ${e.message}`;
- }
- setError(message);
- },
- });
-
- return (
- <View className="flex gap-2 p-4">
- {error && (
- <Text className="w-full text-center text-red-500">{error}</Text>
- )}
- <Input
- className="bg-white"
- value={text}
- onChangeText={setText}
- multiline
- placeholder="What's on your mind?"
- autoFocus
- />
- <Button onPress={() => mutate({ type: "text", text })} label="Add Note" />
- </View>
- );
-}
diff --git a/packages/mobile/app/dashboard/archive.tsx b/packages/mobile/app/dashboard/archive.tsx
deleted file mode 100644
index d75cfe22..00000000
--- a/packages/mobile/app/dashboard/archive.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import { View } from "react-native";
-
-import BookmarkList from "@/components/bookmarks/BookmarkList";
-
-export default function Archive() {
- return (
- <View>
- <BookmarkList archived />
- </View>
- );
-}
diff --git a/packages/mobile/app/dashboard/favourites.tsx b/packages/mobile/app/dashboard/favourites.tsx
deleted file mode 100644
index 90374f18..00000000
--- a/packages/mobile/app/dashboard/favourites.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import { View } from "react-native";
-
-import BookmarkList from "@/components/bookmarks/BookmarkList";
-
-export default function Favourites() {
- return (
- <View>
- <BookmarkList archived={false} favourited />
- </View>
- );
-}
diff --git a/packages/mobile/app/dashboard/lists/[slug].tsx b/packages/mobile/app/dashboard/lists/[slug].tsx
deleted file mode 100644
index 54744874..00000000
--- a/packages/mobile/app/dashboard/lists/[slug].tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import { useLocalSearchParams, Stack } from "expo-router";
-import { View } from "react-native";
-
-import BookmarkList from "@/components/bookmarks/BookmarkList";
-import FullPageSpinner from "@/components/ui/FullPageSpinner";
-import { api } from "@/lib/trpc";
-
-export default function ListView() {
- const { slug } = useLocalSearchParams();
- if (typeof slug !== "string") {
- throw new Error("Unexpected param type");
- }
- const { data: list } = api.lists.get.useQuery({ listId: slug });
-
- if (!list) {
- return <FullPageSpinner />;
- }
-
- return (
- <>
- <Stack.Screen
- options={{
- headerTitle: `${list.icon} ${list.name}`,
- }}
- />
- <View>
- <BookmarkList archived={false} ids={list.bookmarks} />
- </View>
- </>
- );
-}
diff --git a/packages/mobile/app/error.tsx b/packages/mobile/app/error.tsx
deleted file mode 100644
index 2ca227a4..00000000
--- a/packages/mobile/app/error.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-import { View, Text } from "react-native";
-
-export default function ErrorPage() {
- return (
- <View className="flex-1 items-center justify-center gap-4">
- <Text className="text-4xl">Error!</Text>
- </View>
- );
-}
diff --git a/packages/mobile/app/index.tsx b/packages/mobile/app/index.tsx
deleted file mode 100644
index 5ce20cda..00000000
--- a/packages/mobile/app/index.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import { useRouter } from "expo-router";
-import { useEffect } from "react";
-import { View } from "react-native";
-
-import { useSession } from "@/lib/session";
-
-export default function App() {
- const router = useRouter();
- const { isLoggedIn } = useSession();
- useEffect(() => {
- if (isLoggedIn === undefined) {
- // Wait until it's loaded
- } else if (isLoggedIn) {
- router.replace("dashboard");
- } else {
- router.replace("signin");
- }
- }, [isLoggedIn]);
- return <View />;
-}
diff --git a/packages/mobile/app/sharing.tsx b/packages/mobile/app/sharing.tsx
deleted file mode 100644
index 64bbd933..00000000
--- a/packages/mobile/app/sharing.tsx
+++ /dev/null
@@ -1,99 +0,0 @@
-import { Link, useLocalSearchParams, useRouter } from "expo-router";
-import { ShareIntent, useShareIntent } from "expo-share-intent";
-import { useEffect, useMemo, useState } from "react";
-import { View, Text } from "react-native";
-import { z } from "zod";
-
-import { api } from "@/lib/trpc";
-
-type Mode =
- | { type: "idle" }
- | { type: "success"; bookmarkId: string }
- | { type: "error" };
-
-function SaveBookmark({ setMode }: { setMode: (mode: Mode) => void }) {
- // Desperate attempt to fix sharing duplication
- const { hasShareIntent, resetShareIntent } = useShareIntent();
-
- const params = useLocalSearchParams();
-
- const shareIntent = useMemo(() => {
- if (params && params.shareIntent) {
- if (typeof params.shareIntent === "string") {
- return JSON.parse(params.shareIntent) as ShareIntent;
- }
- }
- return null;
- }, [params]);
-
- const invalidateAllBookmarks =
- api.useUtils().bookmarks.getBookmarks.invalidate;
-
- useEffect(() => {
- if (!isPending && shareIntent?.text) {
- const val = z.string().url();
- if (val.safeParse(shareIntent.text).success) {
- // This is a URL, else treated as text
- mutate({ type: "link", url: shareIntent.text });
- } else {
- mutate({ type: "text", text: shareIntent.text });
- }
- }
- if (hasShareIntent) {
- resetShareIntent();
- }
- }, []);
-
- const { mutate, isPending } = api.bookmarks.createBookmark.useMutation({
- onSuccess: (d) => {
- invalidateAllBookmarks();
- setMode({ type: "success", bookmarkId: d.id });
- },
- onError: () => {
- setMode({ type: "error" });
- },
- });
-
- return <Text className="text-4xl">Hoarding ...</Text>;
-}
-
-export default function Sharing() {
- const router = useRouter();
- const [mode, setMode] = useState<Mode>({ type: "idle" });
-
- let comp;
- switch (mode.type) {
- case "idle": {
- comp = <SaveBookmark setMode={setMode} />;
- break;
- }
- case "success": {
- comp = <Text className="text-4xl">Hoarded!</Text>;
- break;
- }
- case "error": {
- comp = <Text className="text-4xl">Error!</Text>;
- break;
- }
- }
-
- // Auto dismiss the modal after saving.
- useEffect(() => {
- if (mode.type === "idle") {
- return;
- }
-
- const timeoutId = setTimeout(() => {
- router.replace("dashboard");
- }, 2000);
-
- return () => clearTimeout(timeoutId);
- }, [mode.type]);
-
- return (
- <View className="flex-1 items-center justify-center gap-4">
- {comp}
- <Link href="dashboard">Dismiss</Link>
- </View>
- );
-}
diff --git a/packages/mobile/app/signin.tsx b/packages/mobile/app/signin.tsx
deleted file mode 100644
index a89b0087..00000000
--- a/packages/mobile/app/signin.tsx
+++ /dev/null
@@ -1,101 +0,0 @@
-import { useRouter } from "expo-router";
-import { useEffect, useState } from "react";
-import { View, Text } from "react-native";
-
-import Logo from "@/components/Logo";
-import { Button } from "@/components/ui/Button";
-import { Input } from "@/components/ui/Input";
-import useAppSettings from "@/lib/settings";
-import { api } from "@/lib/trpc";
-
-export default function Signin() {
- const router = useRouter();
-
- const { settings, setSettings } = useAppSettings();
-
- const [error, setError] = useState<string | undefined>();
-
- const { mutate: login, isPending } = api.apiKeys.exchange.useMutation({
- onSuccess: (resp) => {
- setSettings({ ...settings, apiKey: resp.key });
- router.replace("dashboard");
- },
- onError: (e) => {
- if (e.data?.code === "UNAUTHORIZED") {
- setError("Wrong username or password");
- } else {
- setError(`${e.message}`);
- }
- },
- });
-
- const [formData, setFormData] = useState<{
- email: string;
- password: string;
- }>({
- email: "",
- password: "",
- });
-
- useEffect(() => {
- if (settings.apiKey) {
- router.navigate("dashboard");
- }
- }, [settings]);
-
- const onSignin = () => {
- const randStr = (Math.random() + 1).toString(36).substring(5);
- login({ ...formData, keyName: `Mobile App: (${randStr})` });
- };
-
- return (
- <View className="flex h-full flex-col justify-center gap-2 px-4">
- <View className="items-center">
- <Logo />
- </View>
- {error && (
- <Text className="w-full text-center text-red-500">{error}</Text>
- )}
- <View className="gap-2">
- <Text className="font-bold">Server Address</Text>
- <Input
- className="w-full"
- placeholder="Server Address"
- value={settings.address}
- autoCapitalize="none"
- keyboardType="url"
- onEndEditing={(e) =>
- setSettings({ ...settings, address: e.nativeEvent.text })
- }
- />
- </View>
- <View className="gap-2">
- <Text className="font-bold">Email</Text>
- <Input
- className="w-full"
- placeholder="Email"
- keyboardType="email-address"
- autoCapitalize="none"
- value={formData.email}
- onChangeText={(e) => setFormData((s) => ({ ...s, email: e }))}
- />
- </View>
- <View className="gap-2">
- <Text className="font-bold">Password</Text>
- <Input
- className="w-full"
- placeholder="Password"
- secureTextEntry
- value={formData.password}
- onChangeText={(e) => setFormData((s) => ({ ...s, password: e }))}
- />
- </View>
- <Button
- className="w-full"
- label="Sign In"
- onPress={onSignin}
- disabled={isPending}
- />
- </View>
- );
-}
diff --git a/packages/mobile/assets/blur.jpeg b/packages/mobile/assets/blur.jpeg
deleted file mode 100644
index 387ce697..00000000
--- a/packages/mobile/assets/blur.jpeg
+++ /dev/null
Binary files differ
diff --git a/packages/mobile/assets/icon.png b/packages/mobile/assets/icon.png
deleted file mode 100644
index 71ead90c..00000000
--- a/packages/mobile/assets/icon.png
+++ /dev/null
Binary files differ
diff --git a/packages/mobile/assets/splash.png b/packages/mobile/assets/splash.png
deleted file mode 100644
index 3759c518..00000000
--- a/packages/mobile/assets/splash.png
+++ /dev/null
Binary files differ
diff --git a/packages/mobile/babel.config.js b/packages/mobile/babel.config.js
deleted file mode 100644
index f3c649bb..00000000
--- a/packages/mobile/babel.config.js
+++ /dev/null
@@ -1,9 +0,0 @@
-module.exports = function (api) {
- api.cache(true);
- return {
- presets: [
- ["babel-preset-expo", { jsxImportSource: "nativewind" }],
- "nativewind/babel",
- ],
- };
-};
diff --git a/packages/mobile/components/Logo.tsx b/packages/mobile/components/Logo.tsx
deleted file mode 100644
index 57f7a5c3..00000000
--- a/packages/mobile/components/Logo.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import { PackageOpen } from "lucide-react-native";
-import { View, Text } from "react-native";
-
-export default function Logo() {
- return (
- <View className="flex flex-row items-center justify-center gap-2 ">
- <PackageOpen color="black" size={70} />
- <Text className="text-5xl">Hoarder</Text>
- </View>
- );
-}
diff --git a/packages/mobile/components/bookmarks/BookmarkCard.tsx b/packages/mobile/components/bookmarks/BookmarkCard.tsx
deleted file mode 100644
index 25947790..00000000
--- a/packages/mobile/components/bookmarks/BookmarkCard.tsx
+++ /dev/null
@@ -1,243 +0,0 @@
-import { ZBookmark } from "@hoarder/trpc/types/bookmarks";
-import * as WebBrowser from "expo-web-browser";
-import { Star, Archive, Trash, ArchiveRestore } from "lucide-react-native";
-import { View, Text, Image, ScrollView, Pressable } from "react-native";
-import Markdown from "react-native-markdown-display";
-
-import { ActionButton } from "../ui/ActionButton";
-import { Divider } from "../ui/Divider";
-import { Skeleton } from "../ui/Skeleton";
-import { useToast } from "../ui/Toast";
-
-import { api } from "@/lib/trpc";
-
-const MAX_LOADING_MSEC = 30 * 1000;
-
-export function isBookmarkStillCrawling(bookmark: ZBookmark) {
- return (
- bookmark.content.type === "link" &&
- !bookmark.content.crawledAt &&
- Date.now().valueOf() - bookmark.createdAt.valueOf() < MAX_LOADING_MSEC
- );
-}
-
-export function isBookmarkStillTagging(bookmark: ZBookmark) {
- return (
- bookmark.taggingStatus === "pending" &&
- Date.now().valueOf() - bookmark.createdAt.valueOf() < MAX_LOADING_MSEC
- );
-}
-
-export function isBookmarkStillLoading(bookmark: ZBookmark) {
- return isBookmarkStillTagging(bookmark) || isBookmarkStillCrawling(bookmark);
-}
-
-function ActionBar({ bookmark }: { bookmark: ZBookmark }) {
- const { toast } = useToast();
- const apiUtils = api.useUtils();
-
- const { mutate: deleteBookmark, isPending: isDeletionPending } =
- api.bookmarks.deleteBookmark.useMutation({
- onSuccess: () => {
- apiUtils.bookmarks.getBookmarks.invalidate();
- },
- onError: () => {
- toast({
- message: "Something went wrong",
- variant: "destructive",
- showProgress: false,
- });
- },
- });
- const {
- mutate: updateBookmark,
- variables,
- isPending: isUpdatePending,
- } = api.bookmarks.updateBookmark.useMutation({
- onSuccess: () => {
- apiUtils.bookmarks.getBookmarks.invalidate();
- apiUtils.bookmarks.getBookmark.invalidate({ bookmarkId: bookmark.id });
- },
- onError: () => {
- toast({
- message: "Something went wrong",
- variant: "destructive",
- showProgress: false,
- });
- },
- });
-
- return (
- <View className="flex flex-row gap-4">
- <Pressable
- onPress={() =>
- updateBookmark({
- bookmarkId: bookmark.id,
- favourited: !bookmark.favourited,
- })
- }
- >
- {(variables ? variables.favourited : bookmark.favourited) ? (
- <Star fill="#ebb434" color="#ebb434" />
- ) : (
- <Star color="gray" />
- )}
- </Pressable>
- <ActionButton
- loading={isUpdatePending}
- onPress={() =>
- updateBookmark({
- bookmarkId: bookmark.id,
- archived: !bookmark.archived,
- })
- }
- >
- {bookmark.archived ? (
- <ArchiveRestore color="gray" />
- ) : (
- <Archive color="gray" />
- )}
- </ActionButton>
- <ActionButton
- loading={isDeletionPending}
- onPress={() =>
- deleteBookmark({
- bookmarkId: bookmark.id,
- })
- }
- >
- <Trash color="gray" />
- </ActionButton>
- </View>
- );
-}
-
-function TagList({ bookmark }: { bookmark: ZBookmark }) {
- const tags = bookmark.tags;
-
- if (isBookmarkStillTagging(bookmark)) {
- return (
- <>
- <Skeleton className="h-4 w-full" />
- <Skeleton className="h-4 w-full" />
- </>
- );
- }
-
- return (
- <ScrollView horizontal showsHorizontalScrollIndicator={false}>
- <View className="flex flex-row gap-2">
- {tags.map((t) => (
- <View
- key={t.id}
- className="rounded-full border border-gray-200 px-2.5 py-0.5 text-xs font-semibold"
- >
- <Text>{t.name}</Text>
- </View>
- ))}
- </View>
- </ScrollView>
- );
-}
-
-function LinkCard({ bookmark }: { bookmark: ZBookmark }) {
- if (bookmark.content.type !== "link") {
- throw new Error("Wrong content type rendered");
- }
-
- const url = bookmark.content.url;
- const parsedUrl = new URL(url);
-
- const imageComp = bookmark.content.imageUrl ? (
- <Image
- source={{ uri: bookmark.content.imageUrl }}
- className="h-56 min-h-56 w-full rounded-t-lg object-cover"
- />
- ) : (
- <Image
- source={require("@/assets/blur.jpeg")}
- className="h-56 w-full rounded-t-lg"
- />
- );
-
- return (
- <View className="flex gap-2">
- {imageComp}
- <View className="flex gap-2 p-2">
- <Text
- className="line-clamp-2 text-xl font-bold"
- onPress={() => WebBrowser.openBrowserAsync(url)}
- >
- {bookmark.content.title || parsedUrl.host}
- </Text>
- <TagList bookmark={bookmark} />
- <Divider orientation="vertical" className="mt-2 h-0.5 w-full" />
- <View className="mt-2 flex flex-row justify-between px-2 pb-2">
- <Text className="my-auto line-clamp-1">{parsedUrl.host}</Text>
- <ActionBar bookmark={bookmark} />
- </View>
- </View>
- </View>
- );
-}
-
-function TextCard({ bookmark }: { bookmark: ZBookmark }) {
- if (bookmark.content.type !== "text") {
- throw new Error("Wrong content type rendered");
- }
- return (
- <View className="flex max-h-96 gap-2 p-2">
- <View className="max-h-56 overflow-hidden p-2">
- <Markdown>{bookmark.content.text}</Markdown>
- </View>
- <TagList bookmark={bookmark} />
- <Divider orientation="vertical" className="mt-2 h-0.5 w-full" />
- <View className="flex flex-row justify-between p-2">
- <View />
- <ActionBar bookmark={bookmark} />
- </View>
- </View>
- );
-}
-
-export default function BookmarkCard({
- bookmark: initialData,
-}: {
- bookmark: ZBookmark;
-}) {
- const { data: bookmark } = api.bookmarks.getBookmark.useQuery(
- {
- bookmarkId: initialData.id,
- },
- {
- initialData,
- refetchInterval: (query) => {
- const data = query.state.data;
- if (!data) {
- return false;
- }
- // If the link is not crawled or not tagged
- if (isBookmarkStillLoading(data)) {
- return 1000;
- }
- return false;
- },
- },
- );
-
- let comp;
- switch (bookmark.content.type) {
- case "link":
- comp = <LinkCard bookmark={bookmark} />;
- break;
- case "text":
- comp = <TextCard bookmark={bookmark} />;
- break;
- }
-
- return (
- <View className="w-96 rounded-lg border border-gray-300 bg-white shadow-sm">
- {comp}
- </View>
- );
-}
diff --git a/packages/mobile/components/bookmarks/BookmarkList.tsx b/packages/mobile/components/bookmarks/BookmarkList.tsx
deleted file mode 100644
index 8e408709..00000000
--- a/packages/mobile/components/bookmarks/BookmarkList.tsx
+++ /dev/null
@@ -1,61 +0,0 @@
-import { useEffect, useState } from "react";
-import { Text, View } from "react-native";
-import Animated, { LinearTransition } from "react-native-reanimated";
-
-import BookmarkCard from "./BookmarkCard";
-import FullPageSpinner from "../ui/FullPageSpinner";
-
-import { api } from "@/lib/trpc";
-
-export default function BookmarkList({
- favourited,
- archived,
- ids,
-}: {
- favourited?: boolean;
- archived?: boolean;
- ids?: string[];
-}) {
- const apiUtils = api.useUtils();
- const [refreshing, setRefreshing] = useState(false);
- const { data, isPending, isPlaceholderData } =
- api.bookmarks.getBookmarks.useQuery({
- favourited,
- archived,
- ids,
- });
-
- useEffect(() => {
- setRefreshing(isPending || isPlaceholderData);
- }, [isPending, isPlaceholderData]);
-
- if (isPending || !data) {
- return <FullPageSpinner />;
- }
-
- const onRefresh = () => {
- apiUtils.bookmarks.getBookmarks.invalidate();
- apiUtils.bookmarks.getBookmark.invalidate();
- };
-
- return (
- <Animated.FlatList
- itemLayoutAnimation={LinearTransition}
- contentContainerStyle={{
- gap: 15,
- marginVertical: 15,
- alignItems: "center",
- }}
- renderItem={(b) => <BookmarkCard bookmark={b.item} />}
- ListEmptyComponent={
- <View className="h-full items-center justify-center">
- <Text className="text-xl">No Bookmarks</Text>
- </View>
- }
- data={data.bookmarks}
- refreshing={refreshing}
- onRefresh={onRefresh}
- keyExtractor={(b) => b.id}
- />
- );
-}
diff --git a/packages/mobile/components/ui/ActionButton.tsx b/packages/mobile/components/ui/ActionButton.tsx
deleted file mode 100644
index c51eb332..00000000
--- a/packages/mobile/components/ui/ActionButton.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import { ActivityIndicator, Pressable, PressableProps } from "react-native";
-
-export function ActionButton({
- children,
- loading,
- disabled,
- ...props
-}: PressableProps & {
- loading: boolean;
-}) {
- if (disabled !== undefined) {
- disabled ||= loading;
- } else if (loading) {
- disabled = true;
- }
- return (
- <Pressable {...props} disabled={disabled}>
- {loading ? <ActivityIndicator /> : children}
- </Pressable>
- );
-}
diff --git a/packages/mobile/components/ui/Button.tsx b/packages/mobile/components/ui/Button.tsx
deleted file mode 100644
index 4c3cbc69..00000000
--- a/packages/mobile/components/ui/Button.tsx
+++ /dev/null
@@ -1,81 +0,0 @@
-import { type VariantProps, cva } from "class-variance-authority";
-import { Text, TouchableOpacity } from "react-native";
-
-import { cn } from "@/lib/utils";
-
-const buttonVariants = cva(
- "flex flex-row items-center justify-center rounded-md",
- {
- variants: {
- variant: {
- default: "bg-primary",
- secondary: "bg-secondary",
- destructive: "bg-destructive",
- ghost: "bg-slate-700",
- link: "text-primary underline-offset-4",
- },
- size: {
- default: "h-10 px-4",
- sm: "h-8 px-2",
- lg: "h-12 px-8",
- },
- },
- defaultVariants: {
- variant: "default",
- size: "default",
- },
- },
-);
-
-const buttonTextVariants = cva("text-center font-medium", {
- variants: {
- variant: {
- default: "text-primary-foreground",
- secondary: "text-secondary-foreground",
- destructive: "text-destructive-foreground",
- ghost: "text-primary-foreground",
- link: "text-primary-foreground underline",
- },
- size: {
- default: "text-base",
- sm: "text-sm",
- lg: "text-xl",
- },
- },
- defaultVariants: {
- variant: "default",
- size: "default",
- },
-});
-
-interface ButtonProps
- extends React.ComponentPropsWithoutRef<typeof TouchableOpacity>,
- VariantProps<typeof buttonVariants> {
- label: string;
- labelClasses?: string;
-}
-function Button({
- label,
- labelClasses,
- className,
- variant,
- size,
- ...props
-}: ButtonProps) {
- return (
- <TouchableOpacity
- className={cn(buttonVariants({ variant, size, className }))}
- {...props}
- >
- <Text
- className={cn(
- buttonTextVariants({ variant, size, className: labelClasses }),
- )}
- >
- {label}
- </Text>
- </TouchableOpacity>
- );
-}
-
-export { Button, buttonVariants, buttonTextVariants };
diff --git a/packages/mobile/components/ui/Divider.tsx b/packages/mobile/components/ui/Divider.tsx
deleted file mode 100644
index 1da0a71e..00000000
--- a/packages/mobile/components/ui/Divider.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import { View } from "react-native";
-
-import { cn } from "@/lib/utils";
-
-function Divider({
- color = "#DFE4EA",
- className,
- orientation,
- ...props
-}: {
- color?: string;
- orientation: "horizontal" | "vertical";
-} & React.ComponentPropsWithoutRef<typeof View>) {
- const dividerStyles = [{ backgroundColor: color }];
-
- return (
- <View
- className={cn(
- orientation === "horizontal" ? "h-0.5" : "w-0.5",
- className,
- )}
- style={dividerStyles}
- {...props}
- />
- );
-}
-
-export { Divider };
diff --git a/packages/mobile/components/ui/FullPageSpinner.tsx b/packages/mobile/components/ui/FullPageSpinner.tsx
deleted file mode 100644
index 01187f11..00000000
--- a/packages/mobile/components/ui/FullPageSpinner.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-import { View, ActivityIndicator } from "react-native";
-
-export default function FullPageSpinner() {
- return (
- <View className="h-full w-full items-center justify-center">
- <ActivityIndicator />
- </View>
- );
-}
diff --git a/packages/mobile/components/ui/Input.tsx b/packages/mobile/components/ui/Input.tsx
deleted file mode 100644
index 2fcb2764..00000000
--- a/packages/mobile/components/ui/Input.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import { forwardRef } from "react";
-import { Text, TextInput, View } from "react-native";
-
-import { cn } from "@/lib/utils";
-
-export interface InputProps
- extends React.ComponentPropsWithoutRef<typeof TextInput> {
- label?: string;
- labelClasses?: string;
- inputClasses?: string;
-}
-
-const Input = forwardRef<React.ElementRef<typeof TextInput>, InputProps>(
- ({ className, label, labelClasses, inputClasses, ...props }, ref) => (
- <View className={cn("flex flex-col gap-1.5", className)}>
- {label && <Text className={cn("text-base", labelClasses)}>{label}</Text>}
- <TextInput
- className={cn(
- inputClasses,
- "border-input rounded-lg border px-4 py-2.5",
- )}
- {...props}
- />
- </View>
- ),
-);
-
-export { Input };
diff --git a/packages/mobile/components/ui/Skeleton.tsx b/packages/mobile/components/ui/Skeleton.tsx
deleted file mode 100644
index 68b22e1e..00000000
--- a/packages/mobile/components/ui/Skeleton.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import { useEffect, useRef } from "react";
-import { Animated, type View } from "react-native";
-
-import { cn } from "@/lib/utils";
-
-function Skeleton({
- className,
- ...props
-}: { className?: string } & React.ComponentPropsWithoutRef<typeof View>) {
- const fadeAnim = useRef(new Animated.Value(0.5)).current;
-
- useEffect(() => {
- Animated.loop(
- Animated.sequence([
- Animated.timing(fadeAnim, {
- toValue: 1,
- duration: 1000,
- useNativeDriver: true,
- }),
- Animated.timing(fadeAnim, {
- toValue: 0.5,
- duration: 1000,
- useNativeDriver: true,
- }),
- ]),
- ).start();
- }, [fadeAnim]);
-
- return (
- <Animated.View
- className={cn("bg-muted rounded-md", className)}
- style={[{ opacity: fadeAnim }]}
- {...props}
- />
- );
-}
-
-export { Skeleton };
diff --git a/packages/mobile/components/ui/Toast.tsx b/packages/mobile/components/ui/Toast.tsx
deleted file mode 100644
index fb319f84..00000000
--- a/packages/mobile/components/ui/Toast.tsx
+++ /dev/null
@@ -1,183 +0,0 @@
-import { createContext, useContext, useEffect, useRef, useState } from "react";
-import { Animated, Text, View } from "react-native";
-
-import { cn } from "@/lib/utils";
-
-const toastVariants = {
- default: "bg-foreground",
- destructive: "bg-destructive",
- success: "bg-green-500",
- info: "bg-blue-500",
-};
-
-interface ToastProps {
- id: number;
- message: string;
- onHide: (id: number) => void;
- variant?: keyof typeof toastVariants;
- duration?: number;
- showProgress?: boolean;
-}
-function Toast({
- id,
- message,
- onHide,
- variant = "default",
- duration = 3000,
- showProgress = true,
-}: ToastProps) {
- const opacity = useRef(new Animated.Value(0)).current;
- const progress = useRef(new Animated.Value(0)).current;
-
- useEffect(() => {
- Animated.sequence([
- Animated.timing(opacity, {
- toValue: 1,
- duration: 500,
- useNativeDriver: true,
- }),
- Animated.timing(progress, {
- toValue: 1,
- duration: duration - 1000,
- useNativeDriver: false,
- }),
- Animated.timing(opacity, {
- toValue: 0,
- duration: 500,
- useNativeDriver: true,
- }),
- ]).start(() => onHide(id));
- }, [duration]);
-
- return (
- <Animated.View
- className={`
- ${toastVariants[variant]}
- m-2 mb-1 transform rounded-lg p-4 shadow-md transition-all
- `}
- style={{
- opacity,
- transform: [
- {
- translateY: opacity.interpolate({
- inputRange: [0, 1],
- outputRange: [-20, 0],
- }),
- },
- ],
- }}
- >
- <Text className="text-background text-left font-semibold">{message}</Text>
- {showProgress && (
- <View className="mt-2 rounded">
- <Animated.View
- className="h-2 rounded bg-white opacity-30 dark:bg-black"
- style={{
- width: progress.interpolate({
- inputRange: [0, 1],
- outputRange: ["0%", "100%"],
- }),
- }}
- />
- </View>
- )}
- </Animated.View>
- );
-}
-
-type ToastVariant = keyof typeof toastVariants;
-
-interface ToastMessage {
- id: number;
- text: string;
- variant: ToastVariant;
- duration?: number;
- position?: string;
- showProgress?: boolean;
-}
-interface ToastContextProps {
- toast: (t: {
- message: string;
- variant?: keyof typeof toastVariants;
- duration?: number;
- position?: "top" | "bottom";
- showProgress?: boolean;
- }) => void;
- removeToast: (id: number) => void;
-}
-const ToastContext = createContext<ToastContextProps | undefined>(undefined);
-
-// TODO: refactor to pass position to Toast instead of ToastProvider
-function ToastProvider({
- children,
- position = "top",
-}: {
- children: React.ReactNode;
- position?: "top" | "bottom";
-}) {
- const [messages, setMessages] = useState<ToastMessage[]>([]);
-
- const toast: ToastContextProps["toast"] = ({
- message,
- variant = "default",
- duration = 3000,
- position = "top",
- showProgress = true,
- }: {
- message: string;
- variant?: ToastVariant;
- duration?: number;
- position?: "top" | "bottom";
- showProgress?: boolean;
- }) => {
- setMessages((prev) => [
- ...prev,
- {
- id: Date.now(),
- text: message,
- variant,
- duration,
- position,
- showProgress,
- },
- ]);
- };
-
- const removeToast = (id: number) => {
- setMessages((prev) => prev.filter((message) => message.id !== id));
- };
-
- return (
- <ToastContext.Provider value={{ toast, removeToast }}>
- {children}
- <View
- className={cn("absolute left-0 right-0", {
- "top-[45px]": position === "top",
- "bottom-0": position === "bottom",
- })}
- >
- {messages.map((message) => (
- <Toast
- key={message.id}
- id={message.id}
- message={message.text}
- variant={message.variant}
- duration={message.duration}
- showProgress={message.showProgress}
- onHide={removeToast}
- />
- ))}
- </View>
- </ToastContext.Provider>
- );
-}
-
-function useToast() {
- const context = useContext(ToastContext);
- if (!context) {
- throw new Error("useToast must be used within ToastProvider");
- }
- return context;
-}
-
-export { ToastProvider, ToastVariant, Toast, toastVariants, useToast };
diff --git a/packages/mobile/eas.json b/packages/mobile/eas.json
deleted file mode 100644
index 0897755d..00000000
--- a/packages/mobile/eas.json
+++ /dev/null
@@ -1,19 +0,0 @@
-{
- "cli": {
- "version": ">= 7.5.0",
- "promptToConfigurePushNotifications": false
- },
- "build": {
- "development": {
- "developmentClient": true,
- "distribution": "internal"
- },
- "preview": {
- "distribution": "internal"
- },
- "production": {}
- },
- "submit": {
- "production": {}
- }
-}
diff --git a/packages/mobile/globals.css b/packages/mobile/globals.css
deleted file mode 100644
index de1cf559..00000000
--- a/packages/mobile/globals.css
+++ /dev/null
@@ -1,80 +0,0 @@
-@tailwind base;
-@tailwind components;
-@tailwind utilities;
-
-@layer base {
- :root {
- --background: 0 0% 100%;
- --foreground: 222.2 47.4% 11.2%;
-
- --muted: 210 40% 96.1%;
- --muted-foreground: 215.4 16.3% 46.9%;
-
- --popover: 0 0% 100%;
- --popover-foreground: 222.2 47.4% 11.2%;
-
- --border: 214.3 31.8% 91.4%;
- --input: 214.3 31.8% 91.4%;
-
- --card: 0 0% 100%;
- --card-foreground: 222.2 47.4% 11.2%;
-
- --primary: 222.2 47.4% 11.2%;
- --primary-foreground: 210 40% 98%;
-
- --secondary: 210 40% 96.1%;
- --secondary-foreground: 222.2 47.4% 11.2%;
-
- --accent: 210 40% 96.1%;
- --accent-foreground: 222.2 47.4% 11.2%;
-
- --destructive: 0 100% 50%;
- --destructive-foreground: 210 40% 98%;
-
- --ring: 215 20.2% 65.1%;
-
- --radius: 0.5rem;
- }
-
- .dark:root {
- --background: 224 71% 4%;
- --foreground: 213 31% 91%;
-
- --muted: 223 47% 11%;
- --muted-foreground: 215.4 16.3% 56.9%;
-
- --accent: 216 34% 17%;
- --accent-foreground: 210 40% 98%;
-
- --popover: 224 71% 4%;
- --popover-foreground: 215 20.2% 65.1%;
-
- --border: 216 34% 17%;
- --input: 216 34% 17%;
-
- --card: 224 71% 4%;
- --card-foreground: 213 31% 91%;
-
- --primary: 210 40% 98%;
- --primary-foreground: 222.2 47.4% 1.2%;
-
- --secondary: 222.2 47.4% 11.2%;
- --secondary-foreground: 210 40% 98%;
-
- --destructive: 0 63% 31%;
- --destructive-foreground: 210 40% 98%;
-
- --ring: 216 34% 17%;
-
- --radius: 0.5rem;
- }
-}
-
-@layer base {
- * {
- @apply border-border;
- }
- body {
- @apply bg-background text-foreground;
- }
-}
diff --git a/packages/mobile/lib/last-shared-intent.ts b/packages/mobile/lib/last-shared-intent.ts
deleted file mode 100644
index 951bcf74..00000000
--- a/packages/mobile/lib/last-shared-intent.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { create } from "zustand";
-
-interface LastSharedIntent {
- lastIntent: string;
- setIntent: (intent: string) => void;
- isPreviouslyShared: (intent: string) => boolean;
-}
-
-export const useLastSharedIntent = create<LastSharedIntent>((set, get) => ({
- lastIntent: "",
- setIntent: (intent: string) => set({ lastIntent: intent }),
- isPreviouslyShared: (intent: string) => {
- return get().lastIntent === intent;
- },
-}));
diff --git a/packages/mobile/lib/providers.tsx b/packages/mobile/lib/providers.tsx
deleted file mode 100644
index 1717afb2..00000000
--- a/packages/mobile/lib/providers.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
-import { httpBatchLink } from "@trpc/client";
-import { useEffect, useState } from "react";
-import superjson from "superjson";
-
-import useAppSettings, { getAppSettings } from "./settings";
-import { api } from "./trpc";
-
-import { ToastProvider } from "@/components/ui/Toast";
-
-function getTRPCClient(address: string) {
- return api.createClient({
- links: [
- httpBatchLink({
- url: `${address}/api/trpc`,
- async headers() {
- const settings = await getAppSettings();
- return {
- Authorization:
- settings && settings.apiKey
- ? `Bearer ${settings.apiKey}`
- : undefined,
- };
- },
- transformer: superjson,
- }),
- ],
- });
-}
-
-export function Providers({ children }: { children: React.ReactNode }) {
- const { settings } = useAppSettings();
- const [queryClient] = useState(() => new QueryClient());
-
- const [trpcClient, setTrpcClient] = useState<
- ReturnType<typeof getTRPCClient>
- >(getTRPCClient(settings.address));
-
- useEffect(() => {
- setTrpcClient(getTRPCClient(settings.address));
- }, [settings.address]);
-
- return (
- <api.Provider
- key={settings.address}
- client={trpcClient}
- queryClient={queryClient}
- >
- <QueryClientProvider client={queryClient}>
- <ToastProvider>{children}</ToastProvider>
- </QueryClientProvider>
- </api.Provider>
- );
-}
diff --git a/packages/mobile/lib/session.ts b/packages/mobile/lib/session.ts
deleted file mode 100644
index e2ab245b..00000000
--- a/packages/mobile/lib/session.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { useCallback, useMemo } from "react";
-
-import useAppSettings from "./settings";
-
-export function useSession() {
- const { settings, isLoading, setSettings } = useAppSettings();
- const isLoggedIn = useMemo(() => {
- return isLoading ? undefined : !!settings.apiKey;
- }, [isLoading, settings]);
-
- const logout = useCallback(() => {
- setSettings({ ...settings, apiKey: undefined });
- }, [settings]);
-
- return {
- isLoggedIn,
- isLoading,
- logout,
- };
-}
diff --git a/packages/mobile/lib/settings.ts b/packages/mobile/lib/settings.ts
deleted file mode 100644
index 21f40528..00000000
--- a/packages/mobile/lib/settings.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import * as SecureStore from "expo-secure-store";
-
-import { useStorageState } from "./storage-state";
-
-const SETTING_NAME = "settings";
-
-export type Settings = {
- apiKey?: string;
- address: string;
-};
-
-export default function useAppSettings() {
- let [[isLoading, settings], setSettings] =
- useStorageState<Settings>(SETTING_NAME);
-
- settings ||= {
- address: "https://demo.hoarder.app",
- };
-
- return { settings, setSettings, isLoading };
-}
-
-export async function getAppSettings() {
- const val = await SecureStore.getItemAsync(SETTING_NAME);
- if (!val) {
- return null;
- }
- return JSON.parse(val) as Settings;
-}
diff --git a/packages/mobile/lib/storage-state.ts b/packages/mobile/lib/storage-state.ts
deleted file mode 100644
index 4988f0e0..00000000
--- a/packages/mobile/lib/storage-state.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import * as SecureStore from "expo-secure-store";
-import * as React from "react";
-
-type UseStateHook<T> = [[boolean, T | null], (value: T | null) => void];
-
-function useAsyncState<T>(
- initialValue: [boolean, T | null] = [true, null],
-): UseStateHook<T> {
- return React.useReducer(
- (
- state: [boolean, T | null],
- action: T | null = null,
- ): [boolean, T | null] => [false, action],
- initialValue,
- ) as UseStateHook<T>;
-}
-
-export async function setStorageItemAsync(key: string, value: string | null) {
- if (value == null) {
- await SecureStore.deleteItemAsync(key);
- } else {
- await SecureStore.setItemAsync(key, value);
- }
-}
-
-export function useStorageState<T>(key: string): UseStateHook<T> {
- // Public
- const [state, setState] = useAsyncState<T>();
-
- // Get
- React.useEffect(() => {
- SecureStore.getItemAsync(key).then((value) => {
- if (!value) {
- setState(null);
- return null;
- }
- setState(JSON.parse(value));
- });
- }, [key]);
-
- // Set
- const setValue = React.useCallback(
- (value: T | null) => {
- setState(value);
- setStorageItemAsync(key, JSON.stringify(value));
- },
- [key],
- );
-
- return [state, setValue];
-}
diff --git a/packages/mobile/lib/trpc.ts b/packages/mobile/lib/trpc.ts
deleted file mode 100644
index 6b428bd9..00000000
--- a/packages/mobile/lib/trpc.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-import type { AppRouter } from "@hoarder/trpc/routers/_app";
-import { createTRPCReact } from "@trpc/react-query";
-
-export const api = createTRPCReact<AppRouter>();
diff --git a/packages/mobile/lib/utils.ts b/packages/mobile/lib/utils.ts
deleted file mode 100644
index 365058ce..00000000
--- a/packages/mobile/lib/utils.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { type ClassValue, clsx } from "clsx";
-import { twMerge } from "tailwind-merge";
-
-export function cn(...inputs: ClassValue[]) {
- return twMerge(clsx(inputs));
-}
diff --git a/packages/mobile/metro.config.js b/packages/mobile/metro.config.js
deleted file mode 100644
index 6b2b0477..00000000
--- a/packages/mobile/metro.config.js
+++ /dev/null
@@ -1,8 +0,0 @@
-const { getDefaultConfig } = require("expo/metro-config");
-const { withNativeWind } = require("nativewind/metro");
-
-/** @type {import('expo/metro-config').MetroConfig} */
-// eslint-disable-next-line no-undef
-const config = getDefaultConfig(__dirname);
-
-module.exports = withNativeWind(config, { input: "./globals.css" });
diff --git a/packages/mobile/nativewind-env.d.ts b/packages/mobile/nativewind-env.d.ts
deleted file mode 100644
index a13e3136..00000000
--- a/packages/mobile/nativewind-env.d.ts
+++ /dev/null
@@ -1 +0,0 @@
-/// <reference types="nativewind/types" />
diff --git a/packages/mobile/package.json b/packages/mobile/package.json
deleted file mode 100644
index 1298b8db..00000000
--- a/packages/mobile/package.json
+++ /dev/null
@@ -1,53 +0,0 @@
-{
- "name": "hoarder-mobile",
- "version": "1.0.0",
- "main": "expo-router/entry",
- "scripts": {
- "start": "expo start",
- "android": "expo run:android",
- "ios": "expo run:ios",
- "web": "expo start --web",
- "lint": "eslint ."
- },
- "dependencies": {
- "@hoarder/trpc": "0.1.0",
- "@tanstack/react-query": "^5.24.8",
- "class-variance-authority": "^0.7.0",
- "clsx": "^2.1.0",
- "expo": "~50.0.11",
- "expo-config-plugin-ios-share-extension": "^0.0.4",
- "expo-constants": "~15.4.5",
- "expo-dev-client": "^3.3.9",
- "expo-image": "^1.10.6",
- "expo-linking": "~6.2.2",
- "expo-router": "~3.4.8",
- "expo-secure-store": "^12.8.1",
- "expo-share-intent": "^1.0.1",
- "expo-status-bar": "~1.11.1",
- "expo-web-browser": "^12.8.2",
- "lucide-react-native": "^0.354.0",
- "nativewind": "^4.0.1",
- "react": "18.2.0",
- "react-native": "0.73.4",
- "react-native-markdown-display": "^7.0.2",
- "react-native-reanimated": "^3.8.0",
- "react-native-safe-area-context": "4.8.2",
- "react-native-screens": "~3.29.0",
- "react-native-svg": "^15.1.0",
- "tailwind-merge": "^2.2.1",
- "use-debounce": "^10.0.0",
- "zod": "^3.22.4",
- "zustand": "^4.5.1"
- },
- "devDependencies": {
- "@babel/core": "^7.20.0",
- "@types/react": "~18.2.45",
- "ajv": "latest",
- "eslint": "^8.57.0",
- "eslint-config-universe": "^12.0.0",
- "prettier": "^3.2.5",
- "tailwindcss": "3.3.2",
- "typescript": "^5.1.3"
- },
- "private": true
-}
diff --git a/packages/mobile/tailwind.config.js b/packages/mobile/tailwind.config.js
deleted file mode 100644
index b49f9598..00000000
--- a/packages/mobile/tailwind.config.js
+++ /dev/null
@@ -1,71 +0,0 @@
-const { hairlineWidth } = require("nativewind/theme");
-
-/** @type {import('tailwindcss').Config} */
-module.exports = {
- content: ["./app/**/*.{js,jsx,ts,tsx}", "./components/**/*.{js,jsx,ts,tsx}"],
- plugins: [],
- presets: [require("nativewind/preset")],
- theme: {
- container: {
- center: true,
- padding: "2rem",
- screens: {
- "2xl": "1400px",
- },
- },
- extend: {
- colors: {
- border: "hsl(var(--border))",
- input: "hsl(var(--input))",
- ring: "hsl(var(--ring))",
- background: "hsl(var(--background))",
- foreground: "hsl(var(--foreground))",
- primary: {
- DEFAULT: "hsl(var(--primary))",
- foreground: "hsl(var(--primary-foreground))",
- },
- secondary: {
- DEFAULT: "hsl(var(--secondary))",
- foreground: "hsl(var(--secondary-foreground))",
- },
- destructive: {
- DEFAULT: "hsl(var(--destructive))",
- foreground: "hsl(var(--destructive-foreground))",
- },
- muted: {
- DEFAULT: "hsl(var(--muted))",
- foreground: "hsl(var(--muted-foreground))",
- },
- accent: {
- DEFAULT: "hsl(var(--accent))",
- foreground: "hsl(var(--accent-foreground))",
- },
- popover: {
- DEFAULT: "hsl(var(--popover))",
- foreground: "hsl(var(--popover-foreground))",
- },
- card: {
- DEFAULT: "hsl(var(--card))",
- foreground: "hsl(var(--card-foreground))",
- },
- },
- borderWidth: {
- hairline: hairlineWidth(),
- },
- keyframes: {
- "accordion-down": {
- from: { height: "0" },
- to: { height: "var(--radix-accordion-content-height)" },
- },
- "accordion-up": {
- from: { height: "var(--radix-accordion-content-height)" },
- to: { height: "0" },
- },
- },
- animation: {
- "accordion-down": "accordion-down 0.2s ease-out",
- "accordion-up": "accordion-up 0.2s ease-out",
- },
- },
- },
-};
diff --git a/packages/mobile/tsconfig.json b/packages/mobile/tsconfig.json
deleted file mode 100644
index 84d97cb0..00000000
--- a/packages/mobile/tsconfig.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "extends": "expo/tsconfig.base",
- "compilerOptions": {
- "strict": true,
- "baseUrl": ".",
- "paths": {
- "@/*": ["./*"]
- }
- }
-}
diff --git a/packages/shared/package.json b/packages/shared/package.json
index 0b3a8078..dca8925c 100644
--- a/packages/shared/package.json
+++ b/packages/shared/package.json
@@ -3,10 +3,23 @@
"name": "@hoarder/shared",
"version": "0.1.0",
"private": true,
+ "type": "module",
"dependencies": {
"meilisearch": "^0.37.0",
"winston": "^3.11.0",
"zod": "^3.22.4"
},
- "main": "index.ts"
+ "devDependencies": {
+ "@hoarder/eslint-config": "workspace:^0.2.0",
+ "@hoarder/prettier-config": "workspace:^0.1.0",
+ "@hoarder/tsconfig": "workspace:^0.1.0"
+ },
+ "main": "index.ts",
+ "eslintConfig": {
+ "root": true,
+ "extends": [
+ "@hoarder/eslint-config/base"
+ ]
+ },
+ "prettier": "@hoarder/prettier-config"
}
diff --git a/packages/workers/tsconfig.json b/packages/shared/tsconfig.json
index cf49c407..71bf61e7 100644
--- a/packages/workers/tsconfig.json
+++ b/packages/shared/tsconfig.json
@@ -1,11 +1,9 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
- "extends": "@tsconfig/node21/tsconfig.json",
+ "extends": "@hoarder/tsconfig/node.json",
"include": ["**/*.ts"],
"exclude": ["node_modules"],
"compilerOptions": {
- "module": "ESNext",
- "moduleResolution": "node",
- "esModuleInterop": true
- }
+ "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
+ },
}
diff --git a/packages/trpc/package.json b/packages/trpc/package.json
index ac4fb6fe..631f6ece 100644
--- a/packages/trpc/package.json
+++ b/packages/trpc/package.json
@@ -3,6 +3,7 @@
"name": "@hoarder/trpc",
"version": "0.1.0",
"private": true,
+ "type": "module",
"scripts": {
"typecheck": "tsc --noEmit",
"test": "vitest"
@@ -10,20 +11,28 @@
"dependencies": {
"@hoarder/db": "workspace:*",
"@hoarder/shared": "workspace:*",
- "@trpc/server": "11.0.0-next-beta.304",
+ "@trpc/server": "11.0.0-next-beta.308",
"bcryptjs": "^2.4.3",
"drizzle-orm": "^0.29.4",
"superjson": "^2.2.1",
"zod": "^3.22.4"
},
"devDependencies": {
+ "@hoarder/eslint-config": "workspace:^0.2.0",
+ "@hoarder/prettier-config": "workspace:^0.1.0",
+ "@hoarder/tsconfig": "workspace:^0.1.0",
"@types/bcryptjs": "^2.4.6",
"@tsconfig/node21": "^21.0.1",
"@types/bcrypt": "^5.0.2",
- "aws-sdk": "^2.1570.0",
- "mock-aws-s3": "^4.0.2",
"nock": "^13.5.4",
"vite-tsconfig-paths": "^4.3.1",
"vitest": "^1.3.1"
- }
-}
+ },
+ "eslintConfig": {
+ "root": true,
+ "extends": [
+ "@hoarder/eslint-config/base"
+ ]
+ },
+ "prettier": "@hoarder/prettier-config"
+} \ No newline at end of file
diff --git a/packages/trpc/tsconfig.json b/packages/trpc/tsconfig.json
index bf020b01..80329662 100644
--- a/packages/trpc/tsconfig.json
+++ b/packages/trpc/tsconfig.json
@@ -1,13 +1,10 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
- "extends": "@tsconfig/node21/tsconfig.json",
+ "extends": "@hoarder/tsconfig/node.json",
"include": ["**/*.ts"],
"exclude": ["node_modules"],
"compilerOptions": {
- "module": "ESNext",
- "moduleResolution": "node",
- "baseUrl": "./",
- "esModuleInterop": true
- }
+ "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
+ },
}
diff --git a/packages/web/README.md b/packages/web/README.md
deleted file mode 100644
index c4033664..00000000
--- a/packages/web/README.md
+++ /dev/null
@@ -1,36 +0,0 @@
-This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
-
-## Getting Started
-
-First, run the development server:
-
-```bash
-npm run dev
-# or
-yarn dev
-# or
-pnpm dev
-# or
-bun dev
-```
-
-Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
-
-You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
-
-This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
-
-## Learn More
-
-To learn more about Next.js, take a look at the following resources:
-
-- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
-- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
-
-You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
-
-## Deploy on Vercel
-
-The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
-
-Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
diff --git a/packages/web/app/api/auth/[...nextauth]/route.tsx b/packages/web/app/api/auth/[...nextauth]/route.tsx
deleted file mode 100644
index 2f7f1cb0..00000000
--- a/packages/web/app/api/auth/[...nextauth]/route.tsx
+++ /dev/null
@@ -1,3 +0,0 @@
-import { authHandler } from "@/server/auth";
-
-export { authHandler as GET, authHandler as POST };
diff --git a/packages/web/app/api/trpc/[trpc]/route.ts b/packages/web/app/api/trpc/[trpc]/route.ts
deleted file mode 100644
index b6753101..00000000
--- a/packages/web/app/api/trpc/[trpc]/route.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
-import { appRouter } from "@hoarder/trpc/routers/_app";
-import { createContext } from "@/server/api/client";
-import { authenticateApiKey } from "@hoarder/trpc/auth";
-import { db } from "@hoarder/db";
-
-const handler = (req: Request) =>
- fetchRequestHandler({
- endpoint: "/api/trpc",
- req,
- router: appRouter,
- onError: ({ path, error }) => {
- if (process.env.NODE_ENV === "development") {
- console.error(`❌ tRPC failed on ${path}`);
- }
- console.error(error);
- },
-
- createContext: async (opts) => {
- // TODO: This is a hack until we offer a proper REST API instead of the trpc based one.
- // Check if the request has an Authorization token, if it does, assume that API key authentication is requested.
- const authorizationHeader = opts.req.headers.get("Authorization");
- if (authorizationHeader && authorizationHeader.startsWith("Bearer ")) {
- const token = authorizationHeader.split(" ")[1];
- try {
- const user = await authenticateApiKey(token);
- return { user, db };
- } catch (e) {
- // Fallthrough to cookie-based auth
- }
- }
-
- return createContext();
- },
- });
-export { handler as GET, handler as POST };
diff --git a/packages/web/app/dashboard/admin/page.tsx b/packages/web/app/dashboard/admin/page.tsx
deleted file mode 100644
index 6babdd79..00000000
--- a/packages/web/app/dashboard/admin/page.tsx
+++ /dev/null
@@ -1,203 +0,0 @@
-"use client";
-
-import { ActionButton } from "@/components/ui/action-button";
-import LoadingSpinner from "@/components/ui/spinner";
-import {
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
-} from "@/components/ui/table";
-import { toast } from "@/components/ui/use-toast";
-import { api } from "@/lib/trpc";
-import { keepPreviousData } from "@tanstack/react-query";
-import { Trash } from "lucide-react";
-import { useSession } from "next-auth/react";
-import { useRouter } from "next/navigation";
-
-function ActionsSection() {
- const { mutate: recrawlLinks, isPending: isRecrawlPending } =
- api.admin.recrawlAllLinks.useMutation({
- onSuccess: () => {
- toast({
- description: "Recrawl enqueued",
- });
- },
- onError: (e) => {
- toast({
- variant: "destructive",
- description: e.message,
- });
- },
- });
-
- const { mutate: reindexBookmarks, isPending: isReindexPending } =
- api.admin.reindexAllBookmarks.useMutation({
- onSuccess: () => {
- toast({
- description: "Reindex enqueued",
- });
- },
- onError: (e) => {
- toast({
- variant: "destructive",
- description: e.message,
- });
- },
- });
-
- return (
- <>
- <p className="text-xl">Actions</p>
- <ActionButton
- className="w-1/2"
- variant="destructive"
- loading={isRecrawlPending}
- onClick={() => recrawlLinks()}
- >
- Recrawl All Links
- </ActionButton>
- <ActionButton
- className="w-1/2"
- variant="destructive"
- loading={isReindexPending}
- onClick={() => reindexBookmarks()}
- >
- Reindex All Bookmarks
- </ActionButton>
- </>
- );
-}
-
-function ServerStatsSection() {
- const { data: serverStats } = api.admin.stats.useQuery(undefined, {
- refetchInterval: 1000,
- placeholderData: keepPreviousData,
- });
-
- if (!serverStats) {
- return <LoadingSpinner />;
- }
-
- return (
- <>
- <p className="text-xl">Server Stats</p>
- <Table className="w-1/2">
- <TableBody>
- <TableRow>
- <TableCell className="w-2/3">Num Users</TableCell>
- <TableCell>{serverStats.numUsers}</TableCell>
- </TableRow>
- <TableRow>
- <TableCell>Num Bookmarks</TableCell>
- <TableCell>{serverStats.numBookmarks}</TableCell>
- </TableRow>
- </TableBody>
- </Table>
- <hr />
- <p className="text-xl">Background Jobs</p>
- <Table className="w-1/2">
- <TableBody>
- <TableRow>
- <TableCell className="w-2/3">Pending Crawling Jobs</TableCell>
- <TableCell>{serverStats.pendingCrawls}</TableCell>
- </TableRow>
- <TableRow>
- <TableCell>Pending Indexing Jobs</TableCell>
- <TableCell>{serverStats.pendingIndexing}</TableCell>
- </TableRow>
- <TableRow>
- <TableCell>Pending OpenAI Jobs</TableCell>
- <TableCell>{serverStats.pendingOpenai}</TableCell>
- </TableRow>
- </TableBody>
- </Table>
- </>
- );
-}
-
-function UsersSection() {
- const { data: session } = useSession();
- const invalidateUserList = api.useUtils().users.list.invalidate;
- const { data: users } = api.users.list.useQuery();
- const { mutate: deleteUser, isPending: isDeletionPending } =
- api.users.delete.useMutation({
- onSuccess: () => {
- toast({
- description: "User deleted",
- });
- invalidateUserList();
- },
- onError: (e) => {
- toast({
- variant: "destructive",
- description: `Something went wrong: ${e.message}`,
- });
- },
- });
-
- if (!users) {
- return <LoadingSpinner />;
- }
-
- return (
- <>
- <p className="text-xl">Users</p>
- <Table>
- <TableHeader>
- <TableHead>Name</TableHead>
- <TableHead>Email</TableHead>
- <TableHead>Role</TableHead>
- <TableHead>Action</TableHead>
- </TableHeader>
- <TableBody>
- {users.users.map((u) => (
- <TableRow key={u.id}>
- <TableCell>{u.name}</TableCell>
- <TableCell>{u.email}</TableCell>
- <TableCell>{u.role}</TableCell>
- <TableCell>
- <ActionButton
- variant="destructive"
- onClick={() => deleteUser({ userId: u.id })}
- loading={isDeletionPending}
- disabled={session!.user.id == u.id}
- >
- <Trash />
- </ActionButton>
- </TableCell>
- </TableRow>
- ))}
- </TableBody>
- </Table>
- </>
- );
-}
-
-export default function AdminPage() {
- const router = useRouter();
- const { data: session, status } = useSession();
-
- if (status == "loading") {
- return <LoadingSpinner />;
- }
-
- if (!session || session.user.role != "admin") {
- router.push("/");
- return;
- }
-
- return (
- <div className="m-4 flex flex-col gap-5 rounded-md border bg-white p-4">
- <p className="text-2xl">Admin</p>
- <hr />
- <ServerStatsSection />
- <hr />
- <UsersSection />
- <hr />
- <ActionsSection />
- </div>
- );
-}
diff --git a/packages/web/app/dashboard/archive/page.tsx b/packages/web/app/dashboard/archive/page.tsx
deleted file mode 100644
index 69559185..00000000
--- a/packages/web/app/dashboard/archive/page.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-import Bookmarks from "@/components/dashboard/bookmarks/Bookmarks";
-
-export default async function ArchivedBookmarkPage() {
- return (
- <div className="continer mt-4">
- <Bookmarks title="🗄️ Archive" archived={true} showDivider={true} />
- </div>
- );
-}
diff --git a/packages/web/app/dashboard/bookmarks/layout.tsx b/packages/web/app/dashboard/bookmarks/layout.tsx
deleted file mode 100644
index 71ee143b..00000000
--- a/packages/web/app/dashboard/bookmarks/layout.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import React from "react";
-import TopNav from "@/components/dashboard/bookmarks/TopNav";
-import type { Metadata } from "next";
-
-export const metadata: Metadata = {
- title: "Hoarder - Bookmarks",
-};
-
-export default function BookmarksLayout({
- children,
-}: Readonly<{
- children: React.ReactNode;
-}>) {
- return (
- <div className="flex h-full flex-col">
- <div>
- <TopNav />
- </div>
- <hr />
- <div className="my-4 flex-1 pb-4">{children}</div>
- </div>
- );
-}
diff --git a/packages/web/app/dashboard/bookmarks/loading.tsx b/packages/web/app/dashboard/bookmarks/loading.tsx
deleted file mode 100644
index 4e56c3c4..00000000
--- a/packages/web/app/dashboard/bookmarks/loading.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import Spinner from "@/components/ui/spinner";
-
-export default function Loading() {
- return (
- <div className="flex size-full">
- <div className="m-auto">
- <Spinner />
- </div>
- </div>
- );
-}
diff --git a/packages/web/app/dashboard/bookmarks/page.tsx b/packages/web/app/dashboard/bookmarks/page.tsx
deleted file mode 100644
index c9391d85..00000000
--- a/packages/web/app/dashboard/bookmarks/page.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import Bookmarks from "@/components/dashboard/bookmarks/Bookmarks";
-
-export default async function BookmarksPage() {
- return <Bookmarks title="Bookmarks" archived={false} />;
-}
diff --git a/packages/web/app/dashboard/error.tsx b/packages/web/app/dashboard/error.tsx
deleted file mode 100644
index 556e59a3..00000000
--- a/packages/web/app/dashboard/error.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-"use client";
-
-export default function Error() {
- return (
- <div className="flex size-full">
- <div className="m-auto text-3xl">Something went wrong</div>
- </div>
- );
-}
diff --git a/packages/web/app/dashboard/favourites/page.tsx b/packages/web/app/dashboard/favourites/page.tsx
deleted file mode 100644
index de17461d..00000000
--- a/packages/web/app/dashboard/favourites/page.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import Bookmarks from "@/components/dashboard/bookmarks/Bookmarks";
-
-export default async function FavouritesBookmarkPage() {
- return (
- <div className="continer mt-4">
- <Bookmarks
- title="⭐️ Favourites"
- archived={false}
- favourited={true}
- showDivider={true}
- />
- </div>
- );
-}
diff --git a/packages/web/app/dashboard/layout.tsx b/packages/web/app/dashboard/layout.tsx
deleted file mode 100644
index 31d592fb..00000000
--- a/packages/web/app/dashboard/layout.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import { Separator } from "@/components/ui/separator";
-import MobileSidebar from "@/components/dashboard/sidebar/ModileSidebar";
-import Sidebar from "@/components/dashboard/sidebar/Sidebar";
-
-export default async function Dashboard({
- children,
-}: Readonly<{
- children: React.ReactNode;
-}>) {
- return (
- <div className="flex min-h-screen w-screen flex-col sm:h-screen sm:flex-row">
- <div className="hidden flex-none sm:flex">
- <Sidebar />
- </div>
- <main className="flex-1 bg-gray-100 sm:overflow-y-auto">
- <div className="block w-full sm:hidden">
- <MobileSidebar />
- <Separator />
- </div>
- {children}
- </main>
- </div>
- );
-}
diff --git a/packages/web/app/dashboard/lists/[listId]/page.tsx b/packages/web/app/dashboard/lists/[listId]/page.tsx
deleted file mode 100644
index 006fd3ad..00000000
--- a/packages/web/app/dashboard/lists/[listId]/page.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-import { api } from "@/server/api/client";
-import { getServerAuthSession } from "@/server/auth";
-import { TRPCError } from "@trpc/server";
-import { notFound, redirect } from "next/navigation";
-import ListView from "@/components/dashboard/lists/ListView";
-import DeleteListButton from "@/components/dashboard/lists/DeleteListButton";
-
-export default async function ListPage({
- params,
-}: {
- params: { listId: string };
-}) {
- const session = await getServerAuthSession();
- if (!session) {
- redirect("/");
- }
-
- let list;
- try {
- list = await api.lists.get({ listId: params.listId });
- } catch (e) {
- if (e instanceof TRPCError) {
- if (e.code == "NOT_FOUND") {
- notFound();
- }
- }
- throw e;
- }
-
- const bookmarks = await api.bookmarks.getBookmarks({ ids: list.bookmarks });
-
- return (
- <div className="container flex flex-col gap-3">
- <div className="flex justify-between">
- <span className="pt-4 text-2xl">
- {list.icon} {list.name}
- </span>
- <DeleteListButton list={list} />
- </div>
- <hr />
- <ListView list={list} bookmarks={bookmarks.bookmarks} />
- </div>
- );
-}
diff --git a/packages/web/app/dashboard/lists/page.tsx b/packages/web/app/dashboard/lists/page.tsx
deleted file mode 100644
index 88eeda47..00000000
--- a/packages/web/app/dashboard/lists/page.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import { api } from "@/server/api/client";
-import AllListsView from "@/components/dashboard/lists/AllListsView";
-
-export default async function ListsPage() {
- const lists = await api.lists.list();
-
- return (
- <div className="container mt-4 flex flex-col gap-3">
- <p className="text-2xl">📋 All Lists</p>
- <hr />
- <AllListsView initialData={lists.lists} />
- </div>
- );
-}
diff --git a/packages/web/app/dashboard/not-found.tsx b/packages/web/app/dashboard/not-found.tsx
deleted file mode 100644
index 64df220c..00000000
--- a/packages/web/app/dashboard/not-found.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-export default function NotFound() {
- return (
- <div className="flex size-full">
- <div className="m-auto text-3xl">Not Found :(</div>
- </div>
- );
-}
diff --git a/packages/web/app/dashboard/preview/[bookmarkId]/page.tsx b/packages/web/app/dashboard/preview/[bookmarkId]/page.tsx
deleted file mode 100644
index 707d2b69..00000000
--- a/packages/web/app/dashboard/preview/[bookmarkId]/page.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import { api } from "@/server/api/client";
-import BookmarkPreview from "@/components/dashboard/bookmarks/BookmarkPreview";
-
-export default async function BookmarkPreviewPage({
- params,
-}: {
- params: { bookmarkId: string };
-}) {
- const bookmark = await api.bookmarks.getBookmark({
- bookmarkId: params.bookmarkId,
- });
-
- return <BookmarkPreview initialData={bookmark} />;
-}
diff --git a/packages/web/app/dashboard/search/page.tsx b/packages/web/app/dashboard/search/page.tsx
deleted file mode 100644
index 602f6aa0..00000000
--- a/packages/web/app/dashboard/search/page.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-"use client";
-
-import BookmarksGrid from "@/components/dashboard/bookmarks/BookmarksGrid";
-import Loading from "../bookmarks/loading";
-import { Suspense, useRef } from "react";
-import { SearchInput } from "@/components/dashboard/search/SearchInput";
-import { useBookmarkSearch } from "@/lib/hooks/bookmark-search";
-
-function SearchComp() {
- const { data, isPending, isPlaceholderData } = useBookmarkSearch();
-
- const inputRef: React.MutableRefObject<HTMLInputElement | null> =
- useRef<HTMLInputElement | null>(null);
-
- return (
- <div className="container flex flex-col gap-3 p-4">
- <SearchInput
- ref={inputRef}
- autoFocus={true}
- loading={isPending || isPlaceholderData}
- />
- <hr />
- {data ? (
- <BookmarksGrid
- query={{ ids: data.bookmarks.map((b) => b.id) }}
- bookmarks={data.bookmarks}
- />
- ) : (
- <Loading />
- )}
- </div>
- );
-}
-
-export default function SearchPage() {
- return (
- <Suspense>
- <SearchComp />
- </Suspense>
- );
-}
diff --git a/packages/web/app/dashboard/settings/page.tsx b/packages/web/app/dashboard/settings/page.tsx
deleted file mode 100644
index 38091e6c..00000000
--- a/packages/web/app/dashboard/settings/page.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-import ApiKeySettings from "@/components/dashboard/settings/ApiKeySettings";
-export default async function Settings() {
- return (
- <div className="m-4 flex flex-col space-y-2 rounded-md border bg-white p-4">
- <p className="text-2xl">Settings</p>
- <ApiKeySettings />
- </div>
- );
-}
diff --git a/packages/web/app/dashboard/tags/[tagName]/page.tsx b/packages/web/app/dashboard/tags/[tagName]/page.tsx
deleted file mode 100644
index c978b86a..00000000
--- a/packages/web/app/dashboard/tags/[tagName]/page.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-import { getServerAuthSession } from "@/server/auth";
-import { db } from "@hoarder/db";
-import { notFound, redirect } from "next/navigation";
-import BookmarksGrid from "@/components/dashboard/bookmarks/BookmarksGrid";
-import { api } from "@/server/api/client";
-import { bookmarkTags, tagsOnBookmarks } from "@hoarder/db/schema";
-import { and, eq } from "drizzle-orm";
-
-export default async function TagPage({
- params,
-}: {
- params: { tagName: string };
-}) {
- const session = await getServerAuthSession();
- if (!session) {
- redirect("/");
- }
- const tagName = decodeURIComponent(params.tagName);
- const tag = await db.query.bookmarkTags.findFirst({
- where: and(
- eq(bookmarkTags.userId, session.user.id),
- eq(bookmarkTags.name, tagName),
- ),
- columns: {
- id: true,
- },
- });
-
- if (!tag) {
- // TODO: Better error message when the tag is not there
- notFound();
- }
-
- const bookmarkIds = await db.query.tagsOnBookmarks.findMany({
- where: eq(tagsOnBookmarks.tagId, tag.id),
- columns: {
- bookmarkId: true,
- },
- });
-
- const query = {
- ids: bookmarkIds.map((b) => b.bookmarkId),
- archived: false,
- };
-
- const bookmarks = await api.bookmarks.getBookmarks(query);
-
- return (
- <div className="container flex flex-col gap-3">
- <span className="pt-4 text-2xl">{tagName}</span>
- <hr />
- <BookmarksGrid query={query} bookmarks={bookmarks.bookmarks} />
- </div>
- );
-}
diff --git a/packages/web/app/dashboard/tags/page.tsx b/packages/web/app/dashboard/tags/page.tsx
deleted file mode 100644
index 44c164e1..00000000
--- a/packages/web/app/dashboard/tags/page.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import { Separator } from "@/components/ui/separator";
-import { getServerAuthSession } from "@/server/auth";
-import { db } from "@hoarder/db";
-import { bookmarkTags, tagsOnBookmarks } from "@hoarder/db/schema";
-import { count, eq } from "drizzle-orm";
-import Link from "next/link";
-import { redirect } from "next/navigation";
-
-function TagPill({ name, count }: { name: string; count: number }) {
- return (
- <Link
- className="text-foreground hover:bg-foreground hover:text-background flex gap-2 rounded-md border border-gray-200 bg-white px-2 py-1"
- href={`/dashboard/tags/${name}`}
- >
- {name} <Separator orientation="vertical" /> {count}
- </Link>
- );
-}
-
-export default async function TagsPage() {
- const session = await getServerAuthSession();
- if (!session) {
- redirect("/");
- }
-
- let tags = await db
- .select({
- id: tagsOnBookmarks.tagId,
- name: bookmarkTags.name,
- count: count(),
- })
- .from(tagsOnBookmarks)
- .where(eq(bookmarkTags.userId, session.user.id))
- .groupBy(tagsOnBookmarks.tagId)
- .innerJoin(bookmarkTags, eq(bookmarkTags.id, tagsOnBookmarks.tagId));
-
- // Sort tags by usage desc
- tags = tags.sort((a, b) => b.count - a.count);
-
- let tagPill;
- if (tags.length) {
- tagPill = tags.map((t) => (
- <TagPill key={t.id} name={t.name} count={t.count} />
- ));
- } else {
- tagPill = "No Tags";
- }
-
- return (
- <div className="container mt-2 space-y-3">
- <span className="text-2xl">All Tags</span>
- <hr />
- <div className="flex flex-wrap gap-3">{tagPill}</div>
- </div>
- );
-}
diff --git a/packages/web/app/favicon.ico b/packages/web/app/favicon.ico
deleted file mode 100644
index 750e3c04..00000000
--- a/packages/web/app/favicon.ico
+++ /dev/null
Binary files differ
diff --git a/packages/web/app/globals.css b/packages/web/app/globals.css
deleted file mode 100644
index 8abdb15c..00000000
--- a/packages/web/app/globals.css
+++ /dev/null
@@ -1,76 +0,0 @@
-@tailwind base;
-@tailwind components;
-@tailwind utilities;
-
-@layer base {
- :root {
- --background: 0 0% 100%;
- --foreground: 222.2 84% 4.9%;
-
- --card: 0 0% 100%;
- --card-foreground: 222.2 84% 4.9%;
-
- --popover: 0 0% 100%;
- --popover-foreground: 222.2 84% 4.9%;
-
- --primary: 222.2 47.4% 11.2%;
- --primary-foreground: 210 40% 98%;
-
- --secondary: 210 40% 96.1%;
- --secondary-foreground: 222.2 47.4% 11.2%;
-
- --muted: 210 40% 96.1%;
- --muted-foreground: 215.4 16.3% 46.9%;
-
- --accent: 210 40% 96.1%;
- --accent-foreground: 222.2 47.4% 11.2%;
-
- --destructive: 0 84.2% 60.2%;
- --destructive-foreground: 210 40% 98%;
-
- --border: 214.3 31.8% 91.4%;
- --input: 214.3 31.8% 91.4%;
- --ring: 222.2 84% 4.9%;
-
- --radius: 0.5rem;
- }
-
- .dark {
- --background: 222.2 84% 4.9%;
- --foreground: 210 40% 98%;
-
- --card: 222.2 84% 4.9%;
- --card-foreground: 210 40% 98%;
-
- --popover: 222.2 84% 4.9%;
- --popover-foreground: 210 40% 98%;
-
- --primary: 210 40% 98%;
- --primary-foreground: 222.2 47.4% 11.2%;
-
- --secondary: 217.2 32.6% 17.5%;
- --secondary-foreground: 210 40% 98%;
-
- --muted: 217.2 32.6% 17.5%;
- --muted-foreground: 215 20.2% 65.1%;
-
- --accent: 217.2 32.6% 17.5%;
- --accent-foreground: 210 40% 98%;
-
- --destructive: 0 62.8% 30.6%;
- --destructive-foreground: 210 40% 98%;
-
- --border: 217.2 32.6% 17.5%;
- --input: 217.2 32.6% 17.5%;
- --ring: 212.7 26.8% 83.9%;
- }
-}
-
-@layer base {
- * {
- @apply border-border;
- }
- body {
- @apply bg-background text-foreground;
- }
-}
diff --git a/packages/web/app/layout.tsx b/packages/web/app/layout.tsx
deleted file mode 100644
index b1790a1f..00000000
--- a/packages/web/app/layout.tsx
+++ /dev/null
@@ -1,51 +0,0 @@
-import type { Metadata } from "next";
-import { Inter } from "next/font/google";
-import "./globals.css";
-import React from "react";
-import { Toaster } from "@/components/ui/toaster";
-import Providers from "@/lib/providers";
-import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
-import { getServerAuthSession } from "@/server/auth";
-import type { Viewport } from "next";
-
-const inter = Inter({ subsets: ["latin"] });
-
-export const metadata: Metadata = {
- title: "Hoarder",
- applicationName: "Hoarder",
- description: "Your AI powered second brain",
- manifest: "/manifest.json",
- appleWebApp: {
- capable: true,
- title: "Hoarder",
- },
- formatDetection: {
- telephone: false,
- },
-};
-
-export const viewport: Viewport = {
- width: "device-width",
- initialScale: 1,
- maximumScale: 1,
- userScalable: false,
-};
-
-export default async function RootLayout({
- children,
-}: Readonly<{
- children: React.ReactNode;
-}>) {
- const session = await getServerAuthSession();
- return (
- <html lang="en">
- <body className={inter.className}>
- <Providers session={session}>
- {children}
- <ReactQueryDevtools initialIsOpen={false} />
- </Providers>
- <Toaster />
- </body>
- </html>
- );
-}
diff --git a/packages/web/app/page.tsx b/packages/web/app/page.tsx
deleted file mode 100644
index f467b64b..00000000
--- a/packages/web/app/page.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import { getServerAuthSession } from "@/server/auth";
-import { redirect } from "next/navigation";
-
-export default async function Home() {
- // TODO: Home currently just redirects between pages until we build a proper landing page
- const session = await getServerAuthSession();
- if (!session) {
- redirect("/signin");
- }
-
- redirect("/dashboard/bookmarks");
-}
diff --git a/packages/web/app/signin/page.tsx b/packages/web/app/signin/page.tsx
deleted file mode 100644
index fed71b62..00000000
--- a/packages/web/app/signin/page.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import { PackageOpen } from "lucide-react";
-import SignInForm from "@/components/signin/SignInForm";
-import { redirect } from "next/dist/client/components/navigation";
-import { getServerAuthSession } from "@/server/auth";
-
-export default async function SignInPage() {
- const session = await getServerAuthSession();
- if (session) {
- redirect("/");
- }
-
- return (
- <div className="grid min-h-screen grid-rows-6 justify-center">
- <div className="row-span-2 flex w-96 items-center justify-center space-x-2">
- <span>
- <PackageOpen size="60" className="" />
- </span>
- <p className="text-6xl">Hoarder</p>
- </div>
- <div className="row-span-4 px-3">
- <SignInForm />
- </div>
- </div>
- );
-}
diff --git a/packages/web/components.json b/packages/web/components.json
deleted file mode 100644
index fa674c93..00000000
--- a/packages/web/components.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
- "$schema": "https://ui.shadcn.com/schema.json",
- "style": "default",
- "rsc": true,
- "tsx": true,
- "tailwind": {
- "config": "tailwind.config.ts",
- "css": "app/globals.css",
- "baseColor": "slate",
- "cssVariables": true,
- "prefix": ""
- },
- "aliases": {
- "components": "@/components",
- "utils": "@/lib/utils"
- }
-}
diff --git a/packages/web/components/dashboard/bookmarks/AddLinkButton.tsx b/packages/web/components/dashboard/bookmarks/AddLinkButton.tsx
deleted file mode 100644
index 5973f909..00000000
--- a/packages/web/components/dashboard/bookmarks/AddLinkButton.tsx
+++ /dev/null
@@ -1,102 +0,0 @@
-import { Form, FormControl, FormField, FormItem } from "@/components/ui/form";
-import { Input } from "@/components/ui/input";
-import { useForm, SubmitErrorHandler } from "react-hook-form";
-import { z } from "zod";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { toast } from "@/components/ui/use-toast";
-import { api } from "@/lib/trpc";
-import { ActionButton } from "@/components/ui/action-button";
-import { Button } from "@/components/ui/button";
-import {
- Dialog,
- DialogClose,
- DialogContent,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from "@/components/ui/dialog";
-import { useState } from "react";
-
-export function AddLinkButton({ children }: { children: React.ReactNode }) {
- const [isOpen, setOpen] = useState(false);
-
- const formSchema = z.object({
- url: z.string().url({ message: "The link must be a valid URL" }),
- });
- const form = useForm<z.infer<typeof formSchema>>({
- resolver: zodResolver(formSchema),
- defaultValues: {
- url: "",
- },
- });
-
- const invalidateBookmarksCache = api.useUtils().bookmarks.invalidate;
- const createBookmarkMutator = api.bookmarks.createBookmark.useMutation({
- onSuccess: () => {
- invalidateBookmarksCache();
- form.reset();
- setOpen(false);
- },
- onError: () => {
- toast({ description: "Something went wrong", variant: "destructive" });
- },
- });
-
- const onError: SubmitErrorHandler<z.infer<typeof formSchema>> = (errors) => {
- toast({
- description: Object.values(errors)
- .map((v) => v.message)
- .join("\n"),
- variant: "destructive",
- });
- };
-
- return (
- <Dialog open={isOpen} onOpenChange={setOpen}>
- <DialogTrigger asChild>{children}</DialogTrigger>
- <DialogContent>
- <Form {...form}>
- <DialogHeader>
- <DialogTitle>Add Link</DialogTitle>
- </DialogHeader>
- <form
- className="flex flex-col gap-4"
- onSubmit={form.handleSubmit(
- (value) =>
- createBookmarkMutator.mutate({ url: value.url, type: "link" }),
- onError,
- )}
- >
- <FormField
- control={form.control}
- name="url"
- render={({ field }) => {
- return (
- <FormItem className="flex-1">
- <FormControl>
- <Input type="text" placeholder="Link" {...field} />
- </FormControl>
- </FormItem>
- );
- }}
- />
- <DialogFooter className="flex-shrink gap-1 sm:justify-end">
- <DialogClose asChild>
- <Button type="button" variant="secondary">
- Close
- </Button>
- </DialogClose>
- <ActionButton
- type="submit"
- loading={createBookmarkMutator.isPending}
- >
- Add
- </ActionButton>
- </DialogFooter>
- </form>
- </Form>
- </DialogContent>
- </Dialog>
- );
-}
diff --git a/packages/web/components/dashboard/bookmarks/AddToListModal.tsx b/packages/web/components/dashboard/bookmarks/AddToListModal.tsx
deleted file mode 100644
index c9fd5da0..00000000
--- a/packages/web/components/dashboard/bookmarks/AddToListModal.tsx
+++ /dev/null
@@ -1,168 +0,0 @@
-import { ActionButton } from "@/components/ui/action-button";
-import { Button } from "@/components/ui/button";
-import {
- Dialog,
- DialogClose,
- DialogContent,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from "@/components/ui/dialog";
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormMessage,
-} from "@/components/ui/form";
-
-import { toast } from "@/components/ui/use-toast";
-import { api } from "@/lib/trpc";
-import { useState } from "react";
-
-import {
- Select,
- SelectContent,
- SelectGroup,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select";
-import LoadingSpinner from "@/components/ui/spinner";
-import { z } from "zod";
-import { useForm } from "react-hook-form";
-import { zodResolver } from "@hookform/resolvers/zod";
-
-export default function AddToListModal({
- bookmarkId,
- open,
- setOpen,
-}: {
- bookmarkId: string;
- open: boolean;
- setOpen: (open: boolean) => void;
-}) {
- const formSchema = z.object({
- listId: z.string({
- required_error: "Please select a list",
- }),
- });
- const form = useForm<z.infer<typeof formSchema>>({
- resolver: zodResolver(formSchema),
- });
-
- const { data: lists, isPending: isFetchingListsPending } =
- api.lists.list.useQuery();
-
- const listInvalidationFunction = api.useUtils().lists.get.invalidate;
- const bookmarksInvalidationFunction =
- api.useUtils().bookmarks.getBookmarks.invalidate;
-
- const { mutate: addToList, isPending: isAddingToListPending } =
- api.lists.addToList.useMutation({
- onSuccess: (_resp, req) => {
- toast({
- description: "List has been updated!",
- });
- listInvalidationFunction({ listId: req.listId });
- bookmarksInvalidationFunction();
- },
- onError: (e) => {
- if (e.data?.code == "BAD_REQUEST") {
- toast({
- variant: "destructive",
- description: e.message,
- });
- } else {
- toast({
- variant: "destructive",
- title: "Something went wrong",
- });
- }
- },
- });
-
- const isPending = isFetchingListsPending || isAddingToListPending;
-
- return (
- <Dialog open={open} onOpenChange={setOpen}>
- <DialogContent>
- <Form {...form}>
- <form
- onSubmit={form.handleSubmit((value) => {
- addToList({
- bookmarkId: bookmarkId,
- listId: value.listId,
- });
- })}
- >
- <DialogHeader>
- <DialogTitle>Add to List</DialogTitle>
- </DialogHeader>
-
- <div className="py-4">
- {lists ? (
- <FormField
- control={form.control}
- name="listId"
- render={({ field }) => {
- return (
- <FormItem>
- <FormControl>
- <Select onValueChange={field.onChange}>
- <SelectTrigger className="w-full">
- <SelectValue placeholder="Select a list" />
- </SelectTrigger>
- <SelectContent>
- <SelectGroup>
- {lists &&
- lists.lists.map((l) => (
- <SelectItem key={l.id} value={l.id}>
- {l.icon} {l.name}
- </SelectItem>
- ))}
- </SelectGroup>
- </SelectContent>
- </Select>
- </FormControl>
- <FormMessage />
- </FormItem>
- );
- }}
- />
- ) : (
- <LoadingSpinner />
- )}
- </div>
- <DialogFooter className="sm:justify-end">
- <DialogClose asChild>
- <Button type="button" variant="secondary">
- Close
- </Button>
- </DialogClose>
- <ActionButton
- type="submit"
- loading={isAddingToListPending}
- disabled={isPending}
- >
- Add
- </ActionButton>
- </DialogFooter>
- </form>
- </Form>
- </DialogContent>
- </Dialog>
- );
-}
-
-export function useAddToListModal(bookmarkId: string) {
- const [open, setOpen] = useState(false);
-
- return {
- open,
- setOpen,
- content: (
- <AddToListModal bookmarkId={bookmarkId} open={open} setOpen={setOpen} />
- ),
- };
-}
diff --git a/packages/web/components/dashboard/bookmarks/BookmarkCardSkeleton.tsx b/packages/web/components/dashboard/bookmarks/BookmarkCardSkeleton.tsx
deleted file mode 100644
index 1f5fa433..00000000
--- a/packages/web/components/dashboard/bookmarks/BookmarkCardSkeleton.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import {
- ImageCard,
- ImageCardBody,
- ImageCardContent,
- ImageCardFooter,
- ImageCardTitle,
- ImageCardBanner,
-} from "@/components/ui/imageCard";
-import { Skeleton } from "@/components/ui/skeleton";
-
-export default function BookmarkCardSkeleton() {
- return (
- <ImageCard
- className={
- "border-grey-100 border bg-gray-50 duration-300 ease-in hover:border-blue-300 hover:transition-all"
- }
- >
- <ImageCardBanner src="/blur.avif" />
- <ImageCardContent>
- <ImageCardTitle></ImageCardTitle>
- <ImageCardBody className="space-y-2">
- <Skeleton className="h-4 w-full" />
- <Skeleton className="h-4 w-full" />
- <Skeleton className="h-4 w-full" />
- </ImageCardBody>
- <ImageCardFooter></ImageCardFooter>
- </ImageCardContent>
- </ImageCard>
- );
-}
diff --git a/packages/web/components/dashboard/bookmarks/BookmarkOptions.tsx b/packages/web/components/dashboard/bookmarks/BookmarkOptions.tsx
deleted file mode 100644
index 4f08ebee..00000000
--- a/packages/web/components/dashboard/bookmarks/BookmarkOptions.tsx
+++ /dev/null
@@ -1,185 +0,0 @@
-"use client";
-
-import { useToast } from "@/components/ui/use-toast";
-import { api } from "@/lib/trpc";
-import { ZBookmark, ZBookmarkedLink } from "@hoarder/trpc/types/bookmarks";
-import { Button } from "@/components/ui/button";
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu";
-import {
- Archive,
- Link,
- List,
- MoreHorizontal,
- Pencil,
- RotateCw,
- Star,
- Tags,
- Trash2,
-} from "lucide-react";
-import { useTagModel } from "./TagModal";
-import { useState } from "react";
-import { BookmarkedTextEditor } from "./BookmarkedTextEditor";
-import { useAddToListModal } from "./AddToListModal";
-
-export default function BookmarkOptions({ bookmark }: { bookmark: ZBookmark }) {
- const { toast } = useToast();
- const linkId = bookmark.id;
-
- const { setOpen: setTagModalIsOpen, content: tagModal } =
- useTagModel(bookmark);
- const { setOpen: setAddToListModalOpen, content: addToListModal } =
- useAddToListModal(bookmark.id);
-
- const [isTextEditorOpen, setTextEditorOpen] = useState(false);
-
- const invalidateAllBookmarksCache =
- api.useUtils().bookmarks.getBookmarks.invalidate;
-
- const invalidateBookmarkCache =
- api.useUtils().bookmarks.getBookmark.invalidate;
-
- const onError = () => {
- toast({
- variant: "destructive",
- title: "Something went wrong",
- description: "There was a problem with your request.",
- });
- };
- const deleteBookmarkMutator = api.bookmarks.deleteBookmark.useMutation({
- onSuccess: () => {
- toast({
- description: "The bookmark has been deleted!",
- });
- },
- onError,
- onSettled: () => {
- invalidateAllBookmarksCache();
- },
- });
-
- const updateBookmarkMutator = api.bookmarks.updateBookmark.useMutation({
- onSuccess: () => {
- toast({
- description: "The bookmark has been updated!",
- });
- },
- onError,
- onSettled: () => {
- invalidateBookmarkCache({ bookmarkId: bookmark.id });
- invalidateAllBookmarksCache();
- },
- });
-
- const crawlBookmarkMutator = api.bookmarks.recrawlBookmark.useMutation({
- onSuccess: () => {
- toast({
- description: "Re-fetch has been enqueued!",
- });
- },
- onError,
- onSettled: () => {
- invalidateBookmarkCache({ bookmarkId: bookmark.id });
- },
- });
-
- return (
- <>
- {tagModal}
- {addToListModal}
- <BookmarkedTextEditor
- bookmark={bookmark}
- open={isTextEditorOpen}
- setOpen={setTextEditorOpen}
- />
- <DropdownMenu>
- <DropdownMenuTrigger asChild>
- <Button
- variant="ghost"
- className="px-1 focus-visible:ring-0 focus-visible:ring-offset-0"
- >
- <MoreHorizontal />
- </Button>
- </DropdownMenuTrigger>
- <DropdownMenuContent className="w-fit">
- {bookmark.content.type === "text" && (
- <DropdownMenuItem onClick={() => setTextEditorOpen(true)}>
- <Pencil className="mr-2 size-4" />
- <span>Edit</span>
- </DropdownMenuItem>
- )}
- <DropdownMenuItem
- onClick={() =>
- updateBookmarkMutator.mutate({
- bookmarkId: linkId,
- favourited: !bookmark.favourited,
- })
- }
- >
- <Star className="mr-2 size-4" />
- <span>{bookmark.favourited ? "Un-favourite" : "Favourite"}</span>
- </DropdownMenuItem>
- <DropdownMenuItem
- onClick={() =>
- updateBookmarkMutator.mutate({
- bookmarkId: linkId,
- archived: !bookmark.archived,
- })
- }
- >
- <Archive className="mr-2 size-4" />
- <span>{bookmark.archived ? "Un-archive" : "Archive"}</span>
- </DropdownMenuItem>
- {bookmark.content.type === "link" && (
- <DropdownMenuItem
- onClick={() => {
- navigator.clipboard.writeText(
- (bookmark.content as ZBookmarkedLink).url,
- );
- toast({
- description: "Link was added to your clipboard!",
- });
- }}
- >
- <Link className="mr-2 size-4" />
- <span>Copy Link</span>
- </DropdownMenuItem>
- )}
- <DropdownMenuItem onClick={() => setTagModalIsOpen(true)}>
- <Tags className="mr-2 size-4" />
- <span>Edit Tags</span>
- </DropdownMenuItem>
-
- <DropdownMenuItem onClick={() => setAddToListModalOpen(true)}>
- <List className="mr-2 size-4" />
- <span>Add to List</span>
- </DropdownMenuItem>
-
- {bookmark.content.type === "link" && (
- <DropdownMenuItem
- onClick={() =>
- crawlBookmarkMutator.mutate({ bookmarkId: bookmark.id })
- }
- >
- <RotateCw className="mr-2 size-4" />
- <span>Refresh</span>
- </DropdownMenuItem>
- )}
- <DropdownMenuItem
- className="text-destructive"
- onClick={() =>
- deleteBookmarkMutator.mutate({ bookmarkId: bookmark.id })
- }
- >
- <Trash2 className="mr-2 size-4" />
- <span>Delete</span>
- </DropdownMenuItem>
- </DropdownMenuContent>
- </DropdownMenu>
- </>
- );
-}
diff --git a/packages/web/components/dashboard/bookmarks/BookmarkPreview.tsx b/packages/web/components/dashboard/bookmarks/BookmarkPreview.tsx
deleted file mode 100644
index 2a8ae1b1..00000000
--- a/packages/web/components/dashboard/bookmarks/BookmarkPreview.tsx
+++ /dev/null
@@ -1,101 +0,0 @@
-"use client";
-
-import { BackButton } from "@/components/ui/back-button";
-import { Skeleton } from "@/components/ui/skeleton";
-import { isBookmarkStillCrawling } from "@/lib/bookmarkUtils";
-import { api } from "@/lib/trpc";
-import { ZBookmark } from "@hoarder/trpc/types/bookmarks";
-import { ArrowLeftCircle, CalendarDays, ExternalLink } from "lucide-react";
-import Link from "next/link";
-import Markdown from "react-markdown";
-
-export default function BookmarkPreview({
- initialData,
-}: {
- initialData: ZBookmark;
-}) {
- const { data: bookmark } = api.bookmarks.getBookmark.useQuery(
- {
- bookmarkId: initialData.id,
- },
- {
- initialData,
- refetchInterval: (query) => {
- const data = query.state.data;
- if (!data) {
- return false;
- }
- // If the link is not crawled or not tagged
- if (isBookmarkStillCrawling(data)) {
- return 1000;
- }
- return false;
- },
- },
- );
-
- const linkHeader = bookmark.content.type == "link" && (
- <div className="flex flex-col space-y-2">
- <p className="text-center text-3xl">
- {bookmark.content.title || bookmark.content.url}
- </p>
- <Link href={bookmark.content.url} className="mx-auto flex gap-2">
- <span className="my-auto">View Original</span>
- <ExternalLink />
- </Link>
- </div>
- );
-
- let content;
- switch (bookmark.content.type) {
- case "link": {
- if (!bookmark.content.htmlContent) {
- content = (
- <div className="text-red-500">Failed to fetch link content ...</div>
- );
- } else {
- content = (
- <div
- dangerouslySetInnerHTML={{
- __html: bookmark.content.htmlContent || "",
- }}
- className="prose"
- />
- );
- }
- break;
- }
- case "text": {
- content = <Markdown className="prose">{bookmark.content.text}</Markdown>;
- break;
- }
- }
-
- return (
- <div className="bg-background m-4 min-h-screen space-y-4 rounded-md border p-4">
- <div className="flex justify-between">
- <BackButton className="ghost" variant="ghost">
- <ArrowLeftCircle />
- </BackButton>
- <div className="my-auto">
- <span className="my-auto flex gap-2">
- <CalendarDays /> {bookmark.createdAt.toLocaleString()}
- </span>
- </div>
- </div>
- <hr />
- {linkHeader}
- <div className="mx-auto flex h-full border-x p-2 px-4 lg:w-2/3">
- {isBookmarkStillCrawling(bookmark) ? (
- <div className="flex w-full flex-col gap-2">
- <Skeleton className="h-4" />
- <Skeleton className="h-4" />
- <Skeleton className="h-4" />
- </div>
- ) : (
- content
- )}
- </div>
- </div>
- );
-}
diff --git a/packages/web/components/dashboard/bookmarks/BookmarkedTextEditor.tsx b/packages/web/components/dashboard/bookmarks/BookmarkedTextEditor.tsx
deleted file mode 100644
index a5b58f1a..00000000
--- a/packages/web/components/dashboard/bookmarks/BookmarkedTextEditor.tsx
+++ /dev/null
@@ -1,109 +0,0 @@
-import { ZBookmark } from "@hoarder/trpc/types/bookmarks";
-import {
- Dialog,
- DialogClose,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from "@/components/ui/dialog";
-import { ActionButton } from "@/components/ui/action-button";
-import { Button } from "@/components/ui/button";
-import { Textarea } from "@/components/ui/textarea";
-import { api } from "@/lib/trpc";
-import { useState } from "react";
-import { toast } from "@/components/ui/use-toast";
-
-export function BookmarkedTextEditor({
- bookmark,
- open,
- setOpen,
-}: {
- bookmark?: ZBookmark;
- open: boolean;
- setOpen: (open: boolean) => void;
-}) {
- const isNewBookmark = bookmark === undefined;
- const [noteText, setNoteText] = useState(
- bookmark && bookmark.content.type == "text" ? bookmark.content.text : "",
- );
-
- const invalidateAllBookmarksCache =
- api.useUtils().bookmarks.getBookmarks.invalidate;
- const invalidateOneBookmarksCache =
- api.useUtils().bookmarks.getBookmark.invalidate;
-
- const { mutate: createBookmarkMutator, isPending: isCreationPending } =
- api.bookmarks.createBookmark.useMutation({
- onSuccess: () => {
- invalidateAllBookmarksCache();
- toast({
- description: "Note created!",
- });
- setOpen(false);
- setNoteText("");
- },
- onError: () => {
- toast({ description: "Something went wrong", variant: "destructive" });
- },
- });
- const { mutate: updateBookmarkMutator, isPending: isUpdatePending } =
- api.bookmarks.updateBookmarkText.useMutation({
- onSuccess: () => {
- invalidateOneBookmarksCache({
- bookmarkId: bookmark!.id,
- });
- toast({
- description: "Note updated!",
- });
- setOpen(false);
- },
- onError: () => {
- toast({ description: "Something went wrong", variant: "destructive" });
- },
- });
- const isPending = isCreationPending || isUpdatePending;
-
- const onSave = () => {
- if (isNewBookmark) {
- createBookmarkMutator({
- type: "text",
- text: noteText,
- });
- } else {
- updateBookmarkMutator({
- bookmarkId: bookmark.id,
- text: noteText,
- });
- }
- };
-
- return (
- <Dialog open={open} onOpenChange={setOpen}>
- <DialogContent>
- <DialogHeader>
- <DialogTitle>{isNewBookmark ? "New Note" : "Edit Note"}</DialogTitle>
- <DialogDescription>
- Write your note with markdown support
- </DialogDescription>
- </DialogHeader>
- <Textarea
- value={noteText}
- onChange={(e) => setNoteText(e.target.value)}
- className="h-52 grow"
- />
- <DialogFooter className="flex-shrink gap-1 sm:justify-end">
- <DialogClose asChild>
- <Button type="button" variant="secondary">
- Close
- </Button>
- </DialogClose>
- <ActionButton type="button" loading={isPending} onClick={onSave}>
- Save
- </ActionButton>
- </DialogFooter>
- </DialogContent>
- </Dialog>
- );
-}
diff --git a/packages/web/components/dashboard/bookmarks/BookmarkedTextViewer.tsx b/packages/web/components/dashboard/bookmarks/BookmarkedTextViewer.tsx
deleted file mode 100644
index 8a620341..00000000
--- a/packages/web/components/dashboard/bookmarks/BookmarkedTextViewer.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import { Dialog, DialogContent } from "@/components/ui/dialog";
-import Markdown from "react-markdown";
-
-export function BookmarkedTextViewer({
- content,
- open,
- setOpen,
-}: {
- content: string;
- open: boolean;
- setOpen: (open: boolean) => void;
-}) {
- return (
- <Dialog open={open} onOpenChange={setOpen}>
- <DialogContent className="max-h-[75%] overflow-auto">
- <Markdown className="prose">{content}</Markdown>
- </DialogContent>
- </Dialog>
- );
-}
diff --git a/packages/web/components/dashboard/bookmarks/Bookmarks.tsx b/packages/web/components/dashboard/bookmarks/Bookmarks.tsx
deleted file mode 100644
index 1ad3670c..00000000
--- a/packages/web/components/dashboard/bookmarks/Bookmarks.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import { redirect } from "next/navigation";
-import BookmarksGrid from "./BookmarksGrid";
-import { ZGetBookmarksRequest } from "@hoarder/trpc/types/bookmarks";
-import { api } from "@/server/api/client";
-import { getServerAuthSession } from "@/server/auth";
-
-export default async function Bookmarks({
- favourited,
- archived,
- title,
- showDivider,
-}: ZGetBookmarksRequest & { title: string; showDivider?: boolean }) {
- const session = await getServerAuthSession();
- if (!session) {
- redirect("/");
- }
-
- const query = {
- favourited,
- archived,
- };
-
- const bookmarks = await api.bookmarks.getBookmarks(query);
-
- return (
- <div className="container flex flex-col gap-3">
- <div className="text-2xl">{title}</div>
- {showDivider && <hr />}
- <BookmarksGrid query={query} bookmarks={bookmarks.bookmarks} />
- </div>
- );
-}
diff --git a/packages/web/components/dashboard/bookmarks/BookmarksGrid.tsx b/packages/web/components/dashboard/bookmarks/BookmarksGrid.tsx
deleted file mode 100644
index 4d5b6b0a..00000000
--- a/packages/web/components/dashboard/bookmarks/BookmarksGrid.tsx
+++ /dev/null
@@ -1,64 +0,0 @@
-"use client";
-
-import LinkCard from "./LinkCard";
-import { ZBookmark, ZGetBookmarksRequest } from "@hoarder/trpc/types/bookmarks";
-import { api } from "@/lib/trpc";
-import TextCard from "./TextCard";
-import { Slot } from "@radix-ui/react-slot";
-import Masonry from "react-masonry-css";
-import resolveConfig from "tailwindcss/resolveConfig";
-import tailwindConfig from "@/tailwind.config";
-import { useMemo } from "react";
-
-function getBreakpointConfig() {
- const fullConfig = resolveConfig(tailwindConfig);
-
- const breakpointColumnsObj: { [key: number]: number; default: number } = {
- default: 3,
- };
- breakpointColumnsObj[parseInt(fullConfig.theme.screens.lg)] = 2;
- breakpointColumnsObj[parseInt(fullConfig.theme.screens.md)] = 1;
- breakpointColumnsObj[parseInt(fullConfig.theme.screens.sm)] = 1;
- return breakpointColumnsObj;
-}
-
-function renderBookmark(bookmark: ZBookmark) {
- let comp;
- switch (bookmark.content.type) {
- case "link":
- comp = <LinkCard bookmark={bookmark} />;
- break;
- case "text":
- comp = <TextCard bookmark={bookmark} />;
- break;
- }
- return (
- <Slot
- key={bookmark.id}
- className="border-grey-100 mb-4 border bg-gray-50 duration-300 ease-in hover:border-blue-300 hover:transition-all"
- >
- {comp}
- </Slot>
- );
-}
-
-export default function BookmarksGrid({
- query,
- bookmarks: initialBookmarks,
-}: {
- query: ZGetBookmarksRequest;
- bookmarks: ZBookmark[];
-}) {
- const { data } = api.bookmarks.getBookmarks.useQuery(query, {
- initialData: { bookmarks: initialBookmarks },
- });
- const breakpointConfig = useMemo(() => getBreakpointConfig(), []);
- if (data.bookmarks.length == 0) {
- return <p>No bookmarks</p>;
- }
- return (
- <Masonry className="flex gap-4" breakpointCols={breakpointConfig}>
- {data.bookmarks.map((b) => renderBookmark(b))}
- </Masonry>
- );
-}
diff --git a/packages/web/components/dashboard/bookmarks/LinkCard.tsx b/packages/web/components/dashboard/bookmarks/LinkCard.tsx
deleted file mode 100644
index 50f30e47..00000000
--- a/packages/web/components/dashboard/bookmarks/LinkCard.tsx
+++ /dev/null
@@ -1,114 +0,0 @@
-"use client";
-
-import {
- ImageCard,
- ImageCardBanner,
- ImageCardBody,
- ImageCardContent,
- ImageCardFooter,
- ImageCardTitle,
-} from "@/components/ui/imageCard";
-import { ZBookmark } from "@hoarder/trpc/types/bookmarks";
-import Link from "next/link";
-import BookmarkOptions from "./BookmarkOptions";
-import { api } from "@/lib/trpc";
-import { Maximize2, Star } from "lucide-react";
-import TagList from "./TagList";
-import {
- isBookmarkStillCrawling,
- isBookmarkStillLoading,
- isBookmarkStillTagging,
-} from "@/lib/bookmarkUtils";
-
-export default function LinkCard({
- bookmark: initialData,
- className,
-}: {
- bookmark: ZBookmark;
- className?: string;
-}) {
- const { data: bookmark } = api.bookmarks.getBookmark.useQuery(
- {
- bookmarkId: initialData.id,
- },
- {
- initialData,
- refetchInterval: (query) => {
- const data = query.state.data;
- if (!data) {
- return false;
- }
- // If the link is not crawled or not tagged
- if (isBookmarkStillLoading(data)) {
- return 1000;
- }
- return false;
- },
- },
- );
- const link = bookmark.content;
- if (link.type != "link") {
- throw new Error("Unexpected bookmark type");
- }
- const parsedUrl = new URL(link.url);
-
- // A dummy white pixel for when there's no image.
- // TODO: Better handling for cards with no images
- const image =
- link.imageUrl ??
- "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAA1JREFUGFdj+P///38ACfsD/QVDRcoAAAAASUVORK5CYII=";
-
- return (
- <ImageCard className={className}>
- <Link href={link.url}>
- <ImageCardBanner
- src={isBookmarkStillCrawling(bookmark) ? "/blur.avif" : image}
- />
- </Link>
- <ImageCardContent>
- <ImageCardTitle>
- <Link className="line-clamp-2" href={link.url} target="_blank">
- {link?.title ?? parsedUrl.host}
- </Link>
- </ImageCardTitle>
- {/* There's a hack here. Every tag has the full hight of the container itself. That why, when we enable flex-wrap,
- the overflowed don't show up. */}
- <ImageCardBody className="flex h-full flex-wrap space-x-1 overflow-hidden">
- <TagList
- bookmark={bookmark}
- loading={isBookmarkStillTagging(bookmark)}
- />
- </ImageCardBody>
- <ImageCardFooter>
- <div className="mt-1 flex justify-between text-gray-500">
- <div className="my-auto">
- <Link
- className="line-clamp-1 hover:text-black"
- href={link.url}
- target="_blank"
- >
- {parsedUrl.host}
- </Link>
- </div>
- <div className="flex">
- {bookmark.favourited && (
- <Star
- className="m-1 size-8 rounded p-1"
- color="#ebb434"
- fill="#ebb434"
- />
- )}
- <Link
- className="my-auto block px-2"
- href={`/dashboard/preview/${bookmark.id}`}
- >
- <Maximize2 size="20" />
- </Link>
- <BookmarkOptions bookmark={bookmark} />
- </div>
- </div>
- </ImageCardFooter>
- </ImageCardContent>
- </ImageCard>
- );
-}
diff --git a/packages/web/components/dashboard/bookmarks/TagList.tsx b/packages/web/components/dashboard/bookmarks/TagList.tsx
deleted file mode 100644
index 6c9d2d22..00000000
--- a/packages/web/components/dashboard/bookmarks/TagList.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import { badgeVariants } from "@/components/ui/badge";
-import Link from "next/link";
-import { Skeleton } from "@/components/ui/skeleton";
-import { ZBookmark } from "@hoarder/trpc/types/bookmarks";
-import { cn } from "@/lib/utils";
-
-export default function TagList({
- bookmark,
- loading,
-}: {
- bookmark: ZBookmark;
- loading?: boolean;
-}) {
- if (loading) {
- return (
- <div className="flex w-full flex-col justify-end space-y-2 p-2">
- <Skeleton className="h-4 w-full" />
- <Skeleton className="h-4 w-full" />
- </div>
- );
- }
- return (
- <>
- {bookmark.tags.map((t) => (
- <div key={t.id} className="flex h-full flex-col justify-end">
- <Link
- className={cn(
- badgeVariants({ variant: "outline" }),
- "hover:bg-foreground hover:text-secondary text-nowrap",
- )}
- href={`/dashboard/tags/${t.name}`}
- >
- {t.name}
- </Link>
- </div>
- ))}
- </>
- );
-}
diff --git a/packages/web/components/dashboard/bookmarks/TagModal.tsx b/packages/web/components/dashboard/bookmarks/TagModal.tsx
deleted file mode 100644
index 8c09d00e..00000000
--- a/packages/web/components/dashboard/bookmarks/TagModal.tsx
+++ /dev/null
@@ -1,207 +0,0 @@
-import { ActionButton } from "@/components/ui/action-button";
-import { Button } from "@/components/ui/button";
-import {
- Dialog,
- DialogClose,
- DialogContent,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from "@/components/ui/dialog";
-import { Input } from "@/components/ui/input";
-import { toast } from "@/components/ui/use-toast";
-import { api } from "@/lib/trpc";
-import { ZBookmark } from "@hoarder/trpc/types/bookmarks";
-import { ZAttachedByEnum } from "@hoarder/trpc/types/tags";
-import { cn } from "@/lib/utils";
-import { Sparkles, X } from "lucide-react";
-import { useState, KeyboardEvent, useEffect } from "react";
-
-type EditableTag = { attachedBy: ZAttachedByEnum; id?: string; name: string };
-
-function TagAddInput({ addTag }: { addTag: (tag: string) => void }) {
- const onKeyUp = (e: KeyboardEvent<HTMLInputElement>) => {
- if (e.key === "Enter") {
- addTag(e.currentTarget.value);
- e.currentTarget.value = "";
- }
- };
- return (
- <Input
- onKeyUp={onKeyUp}
- className="h-8 w-full border-none focus-visible:ring-0 focus-visible:ring-offset-0"
- />
- );
-}
-
-function TagPill({
- tag,
- deleteCB,
-}: {
- tag: { attachedBy: ZAttachedByEnum; id?: string; name: string };
- deleteCB: () => void;
-}) {
- const isAttachedByAI = tag.attachedBy == "ai";
- return (
- <div
- className={cn(
- "flex min-h-8 space-x-1 rounded px-2",
- isAttachedByAI
- ? "bg-gradient-to-tr from-purple-500 to-purple-400 text-white"
- : "bg-gray-200",
- )}
- >
- {isAttachedByAI && <Sparkles className="m-auto size-4" />}
- <p className="m-auto">{tag.name}</p>
- <button className="m-auto size-4" onClick={deleteCB}>
- <X className="size-4" />
- </button>
- </div>
- );
-}
-
-function TagEditor({
- tags,
- setTags,
-}: {
- tags: Map<string, EditableTag>;
- setTags: (
- cb: (m: Map<string, EditableTag>) => Map<string, EditableTag>,
- ) => void;
-}) {
- return (
- <div className="mt-4 flex flex-wrap gap-2 rounded border p-2">
- {[...tags.values()].map((t) => (
- <TagPill
- key={t.name}
- tag={t}
- deleteCB={() =>
- setTags((m) => {
- const newMap = new Map(m);
- newMap.delete(t.name);
- return newMap;
- })
- }
- />
- ))}
- <div className="flex-1">
- <TagAddInput
- addTag={(val) => {
- setTags((m) => {
- if (m.has(val)) {
- // Tag already exists
- // Do nothing
- return m;
- }
- const newMap = new Map(m);
- newMap.set(val, { attachedBy: "human", name: val });
- return newMap;
- });
- }}
- />
- </div>
- </div>
- );
-}
-
-export default function TagModal({
- bookmark,
- open,
- setOpen,
-}: {
- bookmark: ZBookmark;
- open: boolean;
- setOpen: (open: boolean) => void;
-}) {
- const [tags, setTags] = useState<Map<string, EditableTag>>(new Map());
- useEffect(() => {
- const m = new Map<string, EditableTag>();
- for (const t of bookmark.tags) {
- m.set(t.name, { attachedBy: t.attachedBy, id: t.id, name: t.name });
- }
- setTags(m);
- }, [bookmark.tags]);
-
- const bookmarkInvalidationFunction =
- api.useUtils().bookmarks.getBookmark.invalidate;
-
- const { mutate, isPending } = api.bookmarks.updateTags.useMutation({
- onSuccess: () => {
- toast({
- description: "Tags has been updated!",
- });
- bookmarkInvalidationFunction({ bookmarkId: bookmark.id });
- },
- onError: () => {
- toast({
- variant: "destructive",
- title: "Something went wrong",
- description: "There was a problem with your request.",
- });
- },
- });
-
- const onSaveButton = () => {
- const exitingTags = new Set(bookmark.tags.map((t) => t.name));
-
- const attach = [];
- const detach = [];
- for (const t of tags.values()) {
- if (!exitingTags.has(t.name)) {
- attach.push({ tag: t.name });
- }
- }
- for (const t of bookmark.tags) {
- if (!tags.has(t.name)) {
- detach.push({ tagId: t.id });
- }
- }
- mutate({
- bookmarkId: bookmark.id,
- attach,
- detach,
- });
- };
-
- return (
- <Dialog open={open} onOpenChange={setOpen}>
- <DialogContent>
- <DialogHeader>
- <DialogTitle>Edit Tags</DialogTitle>
- </DialogHeader>
- <TagEditor tags={tags} setTags={setTags} />
- <DialogFooter className="sm:justify-end">
- <DialogClose asChild>
- <Button type="button" variant="secondary">
- Close
- </Button>
- </DialogClose>
- <ActionButton
- type="button"
- loading={isPending}
- onClick={onSaveButton}
- >
- Save
- </ActionButton>
- </DialogFooter>
- </DialogContent>
- </Dialog>
- );
-}
-
-export function useTagModel(bookmark: ZBookmark) {
- const [open, setOpen] = useState(false);
-
- return {
- open,
- setOpen,
- content: (
- <TagModal
- key={bookmark.id}
- bookmark={bookmark}
- open={open}
- setOpen={setOpen}
- />
- ),
- };
-}
diff --git a/packages/web/components/dashboard/bookmarks/TextCard.tsx b/packages/web/components/dashboard/bookmarks/TextCard.tsx
deleted file mode 100644
index 2565e69d..00000000
--- a/packages/web/components/dashboard/bookmarks/TextCard.tsx
+++ /dev/null
@@ -1,94 +0,0 @@
-"use client";
-
-import { ZBookmark } from "@hoarder/trpc/types/bookmarks";
-import BookmarkOptions from "./BookmarkOptions";
-import { api } from "@/lib/trpc";
-import { Maximize2, Star } from "lucide-react";
-import { cn } from "@/lib/utils";
-import TagList from "./TagList";
-import Markdown from "react-markdown";
-import { useState } from "react";
-import { BookmarkedTextViewer } from "./BookmarkedTextViewer";
-import Link from "next/link";
-import { isBookmarkStillTagging } from "@/lib/bookmarkUtils";
-
-export default function TextCard({
- bookmark: initialData,
- className,
-}: {
- bookmark: ZBookmark;
- className?: string;
-}) {
- const { data: bookmark } = api.bookmarks.getBookmark.useQuery(
- {
- bookmarkId: initialData.id,
- },
- {
- initialData,
- refetchInterval: (query) => {
- const data = query.state.data;
- if (!data) {
- return false;
- }
- if (isBookmarkStillTagging(data)) {
- return 1000;
- }
- return false;
- },
- },
- );
- const [previewModalOpen, setPreviewModalOpen] = useState(false);
- const bookmarkedText = bookmark.content;
- if (bookmarkedText.type != "text") {
- throw new Error("Unexpected bookmark type");
- }
-
- return (
- <>
- <BookmarkedTextViewer
- content={bookmarkedText.text}
- open={previewModalOpen}
- setOpen={setPreviewModalOpen}
- />
- <div
- className={cn(
- className,
- cn(
- "flex h-min max-h-96 flex-col gap-y-1 overflow-hidden rounded-lg p-2 shadow-md",
- ),
- )}
- >
- <Markdown className="prose grow overflow-hidden">
- {bookmarkedText.text}
- </Markdown>
- <div className="mt-4 flex flex-none flex-wrap gap-1 overflow-hidden">
- <TagList
- bookmark={bookmark}
- loading={isBookmarkStillTagging(bookmark)}
- />
- </div>
- <div className="flex w-full justify-between">
- <div />
- <div className="flex gap-0 text-gray-500">
- <div>
- {bookmark.favourited && (
- <Star
- className="my-1 size-8 rounded p-1"
- color="#ebb434"
- fill="#ebb434"
- />
- )}
- </div>
- <Link
- className="my-auto block px-2"
- href={`/dashboard/preview/${bookmark.id}`}
- >
- <Maximize2 size="20" />
- </Link>
- <BookmarkOptions bookmark={bookmark} />
- </div>
- </div>
- </div>
- </>
- );
-}
diff --git a/packages/web/components/dashboard/bookmarks/TopNav.tsx b/packages/web/components/dashboard/bookmarks/TopNav.tsx
deleted file mode 100644
index 6c0f18e5..00000000
--- a/packages/web/components/dashboard/bookmarks/TopNav.tsx
+++ /dev/null
@@ -1,43 +0,0 @@
-"use client";
-
-import { Link, NotebookPen } from "lucide-react";
-import { Button } from "@/components/ui/button";
-import { BookmarkedTextEditor } from "./BookmarkedTextEditor";
-import { useState } from "react";
-import { AddLinkButton } from "./AddLinkButton";
-import { SearchInput } from "../search/SearchInput";
-
-function AddText() {
- const [isEditorOpen, setEditorOpen] = useState(false);
-
- return (
- <div className="flex">
- <BookmarkedTextEditor open={isEditorOpen} setOpen={setEditorOpen} />
- <Button className="m-auto" onClick={() => setEditorOpen(true)}>
- <NotebookPen />
- </Button>
- </div>
- );
-}
-
-function AddLink() {
- return (
- <div className="flex">
- <AddLinkButton>
- <Button className="m-auto">
- <Link />
- </Button>
- </AddLinkButton>
- </div>
- );
-}
-
-export default function TopNav() {
- return (
- <div className="container flex gap-2 py-4">
- <SearchInput />
- <AddLink />
- <AddText />
- </div>
- );
-}
diff --git a/packages/web/components/dashboard/lists/AllListsView.tsx b/packages/web/components/dashboard/lists/AllListsView.tsx
deleted file mode 100644
index 81f31cde..00000000
--- a/packages/web/components/dashboard/lists/AllListsView.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-"use client";
-
-import { Button } from "@/components/ui/button";
-import { api } from "@/lib/trpc";
-import { ZBookmarkList } from "@hoarder/trpc/types/lists";
-import { keepPreviousData } from "@tanstack/react-query";
-import { Plus } from "lucide-react";
-import Link from "next/link";
-import { useNewListModal } from "@/components/dashboard/sidebar/NewListModal";
-
-function ListItem({
- name,
- icon,
- path,
-}: {
- name: string;
- icon: string;
- path: string;
-}) {
- return (
- <Link href={path}>
- <div className="bg-background rounded-md border border-gray-200 px-4 py-2 text-lg">
- <p className="text-nowrap">
- {icon} {name}
- </p>
- </div>
- </Link>
- );
-}
-
-export default function AllListsView({
- initialData,
-}: {
- initialData: ZBookmarkList[];
-}) {
- const { setOpen: setIsNewListModalOpen } = useNewListModal();
- let { data: lists } = api.lists.list.useQuery(undefined, {
- initialData: { lists: initialData },
- placeholderData: keepPreviousData,
- });
-
- // TODO: This seems to be a bug in react query
- lists ||= { lists: initialData };
-
- return (
- <div className="flex flex-col flex-wrap gap-2 md:flex-row">
- <Button
- className="my-auto flex h-full"
- onClick={() => setIsNewListModalOpen(true)}
- >
- <Plus />
- <span className="my-auto">New List</span>
- </Button>
- <ListItem name="Favourites" icon="⭐️" path={`/dashboard/favourites`} />
- <ListItem name="Archive" icon="🗄️" path={`/dashboard/archive`} />
- {lists.lists.map((l) => (
- <ListItem
- key={l.id}
- name={l.name}
- icon={l.icon}
- path={`/dashboard/lists/${l.id}`}
- />
- ))}
- </div>
- );
-}
diff --git a/packages/web/components/dashboard/lists/DeleteListButton.tsx b/packages/web/components/dashboard/lists/DeleteListButton.tsx
deleted file mode 100644
index 5303b217..00000000
--- a/packages/web/components/dashboard/lists/DeleteListButton.tsx
+++ /dev/null
@@ -1,77 +0,0 @@
-"use client";
-
-import { Button } from "@/components/ui/button";
-import {
- Dialog,
- DialogClose,
- DialogContent,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from "@/components/ui/dialog";
-import { Trash } from "lucide-react";
-import { useRouter } from "next/navigation";
-import { toast } from "@/components/ui/use-toast";
-import { api } from "@/lib/trpc";
-import { ActionButton } from "@/components/ui/action-button";
-import { useState } from "react";
-import { ZBookmarkList } from "@hoarder/trpc/types/lists";
-
-export default function DeleteListButton({ list }: { list: ZBookmarkList }) {
- const [isDialogOpen, setDialogOpen] = useState(false);
-
- const router = useRouter();
-
- const listsInvalidationFunction = api.useUtils().lists.list.invalidate;
- const { mutate: deleteList, isPending } = api.lists.delete.useMutation({
- onSuccess: () => {
- listsInvalidationFunction();
- toast({
- description: `List "${list.icon} ${list.name}" is deleted!`,
- });
- router.push("/");
- },
- onError: () => {
- toast({
- variant: "destructive",
- description: `Something went wrong`,
- });
- },
- });
- return (
- <Dialog open={isDialogOpen} onOpenChange={setDialogOpen}>
- <DialogTrigger asChild>
- <Button className="mt-auto flex gap-2" variant="destructive">
- <Trash className="size-5" />
- <span className="hidden md:block">Delete List</span>
- </Button>
- </DialogTrigger>
- <DialogContent>
- <DialogHeader>
- <DialogTitle>
- Delete {list.icon} {list.name}?
- </DialogTitle>
- </DialogHeader>
- <span>
- Are you sure you want to delete {list.icon} {list.name}?
- </span>
- <DialogFooter className="sm:justify-end">
- <DialogClose asChild>
- <Button type="button" variant="secondary">
- Close
- </Button>
- </DialogClose>
- <ActionButton
- type="button"
- variant="destructive"
- loading={isPending}
- onClick={() => deleteList({ listId: list.id })}
- >
- Delete
- </ActionButton>
- </DialogFooter>
- </DialogContent>
- </Dialog>
- );
-}
diff --git a/packages/web/components/dashboard/lists/ListView.tsx b/packages/web/components/dashboard/lists/ListView.tsx
deleted file mode 100644
index 2d48d9e3..00000000
--- a/packages/web/components/dashboard/lists/ListView.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-"use client";
-
-import BookmarksGrid from "@/components/dashboard/bookmarks/BookmarksGrid";
-import { ZBookmark } from "@hoarder/trpc/types/bookmarks";
-import { ZBookmarkListWithBookmarks } from "@hoarder/trpc/types/lists";
-import { api } from "@/lib/trpc";
-
-export default function ListView({
- bookmarks,
- list: initialData,
-}: {
- list: ZBookmarkListWithBookmarks;
- bookmarks: ZBookmark[];
-}) {
- const { data } = api.lists.get.useQuery(
- { listId: initialData.id },
- {
- initialData,
- },
- );
-
- return (
- <BookmarksGrid query={{ ids: data.bookmarks }} bookmarks={bookmarks} />
- );
-}
diff --git a/packages/web/components/dashboard/search/SearchInput.tsx b/packages/web/components/dashboard/search/SearchInput.tsx
deleted file mode 100644
index 73d14c90..00000000
--- a/packages/web/components/dashboard/search/SearchInput.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import { Input } from "@/components/ui/input";
-import { useDoBookmarkSearch } from "@/lib/hooks/bookmark-search";
-import { cn } from "@/lib/utils";
-import React from "react";
-
-const SearchInput = React.forwardRef<
- HTMLInputElement,
- React.HTMLAttributes<HTMLInputElement> & { loading?: boolean }
->(({ className, loading = false, ...props }, ref) => {
- const { debounceSearch, searchQuery } = useDoBookmarkSearch();
-
- return (
- <Input
- ref={ref}
- placeholder="Search"
- defaultValue={searchQuery}
- onChange={(e) => debounceSearch(e.target.value)}
- className={cn(loading ? "animate-pulse-border" : undefined, className)}
- {...props}
- />
- );
-});
-SearchInput.displayName = "SearchInput";
-
-export { SearchInput };
diff --git a/packages/web/components/dashboard/settings/AddApiKey.tsx b/packages/web/components/dashboard/settings/AddApiKey.tsx
deleted file mode 100644
index a4fd9c25..00000000
--- a/packages/web/components/dashboard/settings/AddApiKey.tsx
+++ /dev/null
@@ -1,167 +0,0 @@
-"use client";
-
-import { Button } from "@/components/ui/button";
-import { Input } from "@/components/ui/input";
-import {
- Form,
- FormControl,
- FormDescription,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from "@/components/ui/form";
-
-import {
- Dialog,
- DialogClose,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from "@/components/ui/dialog";
-import { z } from "zod";
-import { useRouter } from "next/navigation";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { useForm, SubmitErrorHandler } from "react-hook-form";
-import { toast } from "@/components/ui/use-toast";
-import { api } from "@/lib/trpc";
-import { useState } from "react";
-import { Check, Copy } from "lucide-react";
-import { ActionButton } from "@/components/ui/action-button";
-
-function ApiKeySuccess({ apiKey }: { apiKey: string }) {
- const [isCopied, setCopied] = useState(false);
-
- const onCopy = () => {
- navigator.clipboard.writeText(apiKey);
- setCopied(true);
- setTimeout(() => setCopied(false), 2000);
- };
-
- return (
- <div>
- <div className="py-4">
- Note: please copy the key and store it somewhere safe. Once you close
- the dialog, you won&apos;t be able to access it again.
- </div>
- <div className="flex space-x-2 pt-2">
- <Input value={apiKey} readOnly />
- <Button onClick={onCopy}>
- {!isCopied ? (
- <Copy className="size-4" />
- ) : (
- <Check className="size-4" />
- )}
- </Button>
- </div>
- </div>
- );
-}
-
-function AddApiKeyForm({ onSuccess }: { onSuccess: (key: string) => void }) {
- const formSchema = z.object({
- name: z.string(),
- });
- const router = useRouter();
- const mutator = api.apiKeys.create.useMutation({
- onSuccess: (resp) => {
- onSuccess(resp.key);
- router.refresh();
- },
- onError: () => {
- toast({ description: "Something went wrong", variant: "destructive" });
- },
- });
-
- const form = useForm<z.infer<typeof formSchema>>({
- resolver: zodResolver(formSchema),
- });
-
- async function onSubmit(value: z.infer<typeof formSchema>) {
- mutator.mutate({ name: value.name });
- }
-
- const onError: SubmitErrorHandler<z.infer<typeof formSchema>> = (errors) => {
- toast({
- description: Object.values(errors)
- .map((v) => v.message)
- .join("\n"),
- variant: "destructive",
- });
- };
-
- return (
- <Form {...form}>
- <form
- onSubmit={form.handleSubmit(onSubmit, onError)}
- className="flex w-full space-x-3 space-y-8 pt-4"
- >
- <FormField
- control={form.control}
- name="name"
- render={({ field }) => {
- return (
- <FormItem className="flex-1">
- <FormLabel>Name</FormLabel>
- <FormControl>
- <Input type="text" placeholder="Name" {...field} />
- </FormControl>
- <FormDescription>
- Give your API key a unique name
- </FormDescription>
- <FormMessage />
- </FormItem>
- );
- }}
- />
- <ActionButton
- className="h-full"
- type="submit"
- loading={mutator.isPending}
- >
- Create
- </ActionButton>
- </form>
- </Form>
- );
-}
-
-export default function AddApiKey() {
- const [key, setKey] = useState<string | undefined>(undefined);
- const [dialogOpen, setDialogOpen] = useState<boolean>(false);
- return (
- <Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
- <DialogTrigger asChild>
- <Button>New API Key</Button>
- </DialogTrigger>
- <DialogContent>
- <DialogHeader>
- <DialogTitle>
- {key ? "Key was successfully created" : "Create API key"}
- </DialogTitle>
- <DialogDescription>
- {key ? (
- <ApiKeySuccess apiKey={key} />
- ) : (
- <AddApiKeyForm onSuccess={setKey} />
- )}
- </DialogDescription>
- </DialogHeader>
- <DialogFooter className="sm:justify-end">
- <DialogClose asChild>
- <Button
- type="button"
- variant="outline"
- onClick={() => setKey(undefined)}
- >
- Close
- </Button>
- </DialogClose>
- </DialogFooter>
- </DialogContent>
- </Dialog>
- );
-}
diff --git a/packages/web/components/dashboard/settings/ApiKeySettings.tsx b/packages/web/components/dashboard/settings/ApiKeySettings.tsx
deleted file mode 100644
index 1598f25f..00000000
--- a/packages/web/components/dashboard/settings/ApiKeySettings.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-import {
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
-} from "@/components/ui/table";
-import { api } from "@/server/api/client";
-import DeleteApiKey from "./DeleteApiKey";
-import AddApiKey from "./AddApiKey";
-
-export default async function ApiKeys() {
- const keys = await api.apiKeys.list();
- return (
- <div className="pt-4">
- <span className="text-xl">API Keys</span>
- <hr className="my-2" />
- <div className="flex flex-col space-y-3">
- <div className="flex flex-1 justify-end">
- <AddApiKey />
- </div>
- <Table>
- <TableHeader>
- <TableRow>
- <TableHead>Name</TableHead>
- <TableHead>Key</TableHead>
- <TableHead>Created At</TableHead>
- <TableHead>Action</TableHead>
- </TableRow>
- </TableHeader>
- <TableBody>
- {keys.keys.map((k) => (
- <TableRow key={k.id}>
- <TableCell>{k.name}</TableCell>
- <TableCell>**_{k.keyId}_**</TableCell>
- <TableCell>{k.createdAt.toLocaleString()}</TableCell>
- <TableCell>
- <DeleteApiKey name={k.name} id={k.id} />
- </TableCell>
- </TableRow>
- ))}
- <TableRow></TableRow>
- </TableBody>
- </Table>
- </div>
- </div>
- );
-}
diff --git a/packages/web/components/dashboard/settings/DeleteApiKey.tsx b/packages/web/components/dashboard/settings/DeleteApiKey.tsx
deleted file mode 100644
index 566136af..00000000
--- a/packages/web/components/dashboard/settings/DeleteApiKey.tsx
+++ /dev/null
@@ -1,74 +0,0 @@
-"use client";
-
-import { Button } from "@/components/ui/button";
-import { Trash } from "lucide-react";
-
-import {
- Dialog,
- DialogClose,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from "@/components/ui/dialog";
-import { useRouter } from "next/navigation";
-import { toast } from "@/components/ui/use-toast";
-import { api } from "@/lib/trpc";
-import { ActionButton } from "@/components/ui/action-button";
-import { useState } from "react";
-
-export default function DeleteApiKey({
- name,
- id,
-}: {
- name: string;
- id: string;
-}) {
- const [isDialogOpen, setDialogOpen] = useState(false);
- const router = useRouter();
- const mutator = api.apiKeys.revoke.useMutation({
- onSuccess: () => {
- toast({
- description: "Key was successfully deleted",
- });
- setDialogOpen(false);
- router.refresh();
- },
- });
-
- return (
- <Dialog open={isDialogOpen} onOpenChange={setDialogOpen}>
- <DialogTrigger asChild>
- <Button variant="destructive">
- <Trash className="size-5" />
- </Button>
- </DialogTrigger>
- <DialogContent>
- <DialogHeader>
- <DialogTitle>Delete API Key</DialogTitle>
- <DialogDescription>
- Are you sure you want to delete the API key &quot;{name}&quot;? Any
- service using this API key will lose access.
- </DialogDescription>
- </DialogHeader>
- <DialogFooter className="sm:justify-end">
- <DialogClose asChild>
- <Button type="button" variant="secondary">
- Close
- </Button>
- </DialogClose>
- <ActionButton
- type="button"
- variant="destructive"
- loading={mutator.isPending}
- onClick={() => mutator.mutate({ id })}
- >
- Delete
- </ActionButton>
- </DialogFooter>
- </DialogContent>
- </Dialog>
- );
-}
diff --git a/packages/web/components/dashboard/sidebar/AllLists.tsx b/packages/web/components/dashboard/sidebar/AllLists.tsx
deleted file mode 100644
index a77252d0..00000000
--- a/packages/web/components/dashboard/sidebar/AllLists.tsx
+++ /dev/null
@@ -1,60 +0,0 @@
-"use client";
-
-import { api } from "@/lib/trpc";
-import SidebarItem from "./SidebarItem";
-import NewListModal, { useNewListModal } from "./NewListModal";
-import { Plus } from "lucide-react";
-import Link from "next/link";
-import { ZBookmarkList } from "@hoarder/trpc/types/lists";
-
-export default function AllLists({
- initialData,
-}: {
- initialData: { lists: ZBookmarkList[] };
-}) {
- let { data: lists } = api.lists.list.useQuery(undefined, {
- initialData,
- });
- // TODO: This seems to be a bug in react query
- lists ||= initialData;
- const { setOpen } = useNewListModal();
-
- return (
- <ul className="max-h-full gap-y-2 overflow-auto text-sm font-medium">
- <NewListModal />
- <li className="flex justify-between pb-2 font-bold">
- <p>Lists</p>
- <Link href="#" onClick={() => setOpen(true)}>
- <Plus />
- </Link>
- </li>
- <SidebarItem
- logo={<span className="text-lg">📋</span>}
- name="All Lists"
- path={`/dashboard/lists`}
- className="py-0.5"
- />
- <SidebarItem
- logo={<span className="text-lg">⭐️</span>}
- name="Favourties"
- path={`/dashboard/favourites`}
- className="py-0.5"
- />
- <SidebarItem
- logo={<span className="text-lg">🗄️</span>}
- name="Archive"
- path={`/dashboard/archive`}
- className="py-0.5"
- />
- {lists.lists.map((l) => (
- <SidebarItem
- key={l.id}
- logo={<span className="text-lg"> {l.icon}</span>}
- name={l.name}
- path={`/dashboard/lists/${l.id}`}
- className="py-0.5"
- />
- ))}
- </ul>
- );
-}
diff --git a/packages/web/components/dashboard/sidebar/ModileSidebar.tsx b/packages/web/components/dashboard/sidebar/ModileSidebar.tsx
deleted file mode 100644
index 4bd6a347..00000000
--- a/packages/web/components/dashboard/sidebar/ModileSidebar.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import MobileSidebarItem from "./ModileSidebarItem";
-import {
- Tag,
- PackageOpen,
- Settings,
- Search,
- ClipboardList,
-} from "lucide-react";
-import SidebarProfileOptions from "./SidebarProfileOptions";
-
-export default async function MobileSidebar() {
- return (
- <aside className="w-full">
- <ul className="flex justify-between space-x-2 border-b-black bg-gray-100 px-5 py-2 pt-5">
- <MobileSidebarItem logo={<PackageOpen />} path="/dashboard/bookmarks" />
- <MobileSidebarItem logo={<Search />} path="/dashboard/search" />
- <MobileSidebarItem logo={<ClipboardList />} path="/dashboard/lists" />
- <MobileSidebarItem logo={<Tag />} path="/dashboard/tags" />
- <MobileSidebarItem logo={<Settings />} path="/dashboard/settings" />
- <SidebarProfileOptions />
- </ul>
- </aside>
- );
-}
diff --git a/packages/web/components/dashboard/sidebar/ModileSidebarItem.tsx b/packages/web/components/dashboard/sidebar/ModileSidebarItem.tsx
deleted file mode 100644
index 9389d2e4..00000000
--- a/packages/web/components/dashboard/sidebar/ModileSidebarItem.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-"use client";
-
-import { cn } from "@/lib/utils";
-import Link from "next/link";
-import { usePathname } from "next/navigation";
-
-export default function MobileSidebarItem({
- logo,
- path,
-}: {
- logo: React.ReactNode;
- path: string;
-}) {
- const currentPath = usePathname();
- return (
- <li
- className={cn(
- "flex w-full rounded-lg hover:bg-gray-50",
- path == currentPath ? "bg-gray-50" : "",
- )}
- >
- <Link href={path} className="mx-auto px-3 py-2">
- {logo}
- </Link>
- </li>
- );
-}
diff --git a/packages/web/components/dashboard/sidebar/NewListModal.tsx b/packages/web/components/dashboard/sidebar/NewListModal.tsx
deleted file mode 100644
index f51616ed..00000000
--- a/packages/web/components/dashboard/sidebar/NewListModal.tsx
+++ /dev/null
@@ -1,170 +0,0 @@
-"use client";
-
-import data from "@emoji-mart/data";
-import Picker from "@emoji-mart/react";
-
-import {
- Popover,
- PopoverContent,
- PopoverTrigger,
-} from "@/components/ui/popover";
-
-import { ActionButton } from "@/components/ui/action-button";
-import { Button } from "@/components/ui/button";
-import {
- Dialog,
- DialogClose,
- DialogContent,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from "@/components/ui/dialog";
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormMessage,
-} from "@/components/ui/form";
-
-import { toast } from "@/components/ui/use-toast";
-import { api } from "@/lib/trpc";
-
-import { z } from "zod";
-import { useForm } from "react-hook-form";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { Input } from "@/components/ui/input";
-
-import { create } from "zustand";
-
-export const useNewListModal = create<{
- open: boolean;
- setOpen: (v: boolean) => void;
-}>((set) => ({
- open: false,
- setOpen: (open: boolean) => set(() => ({ open })),
-}));
-
-export default function NewListModal() {
- const { open, setOpen } = useNewListModal();
-
- const formSchema = z.object({
- name: z.string(),
- icon: z.string(),
- });
- const form = useForm<z.infer<typeof formSchema>>({
- resolver: zodResolver(formSchema),
- defaultValues: {
- name: "",
- icon: "💡",
- },
- });
-
- const listsInvalidationFunction = api.useUtils().lists.list.invalidate;
-
- const { mutate: createList, isPending } = api.lists.create.useMutation({
- onSuccess: () => {
- toast({
- description: "List has been created!",
- });
- listsInvalidationFunction();
- setOpen(false);
- },
- onError: (e) => {
- if (e.data?.code == "BAD_REQUEST") {
- toast({
- variant: "destructive",
- description: e.message,
- });
- } else {
- toast({
- variant: "destructive",
- title: "Something went wrong",
- });
- }
- },
- });
-
- return (
- <Dialog
- open={open}
- onOpenChange={(s) => {
- form.reset();
- setOpen(s);
- }}
- >
- <DialogContent>
- <Form {...form}>
- <form
- onSubmit={form.handleSubmit((value) => {
- createList(value);
- })}
- >
- <DialogHeader>
- <DialogTitle>New List</DialogTitle>
- </DialogHeader>
- <div className="flex w-full gap-2 py-4">
- <FormField
- control={form.control}
- name="icon"
- render={({ field }) => {
- return (
- <FormItem>
- <FormControl>
- <Popover>
- <PopoverTrigger className="border-input h-full rounded border px-2 text-2xl">
- {field.value}
- </PopoverTrigger>
- <PopoverContent>
- <Picker
- data={data}
- onEmojiSelect={(e: { native: string }) =>
- field.onChange(e.native)
- }
- />
- </PopoverContent>
- </Popover>
- </FormControl>
- <FormMessage />
- </FormItem>
- );
- }}
- />
-
- <FormField
- control={form.control}
- name="name"
- render={({ field }) => {
- return (
- <FormItem className="grow">
- <FormControl>
- <Input
- type="text"
- className="w-full"
- placeholder="List Name"
- {...field}
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- );
- }}
- />
- </div>
- <DialogFooter className="sm:justify-end">
- <DialogClose asChild>
- <Button type="button" variant="secondary">
- Close
- </Button>
- </DialogClose>
- <ActionButton type="submit" loading={isPending}>
- Create
- </ActionButton>
- </DialogFooter>
- </form>
- </Form>
- </DialogContent>
- </Dialog>
- );
-}
diff --git a/packages/web/components/dashboard/sidebar/Sidebar.tsx b/packages/web/components/dashboard/sidebar/Sidebar.tsx
deleted file mode 100644
index a5c1d7a5..00000000
--- a/packages/web/components/dashboard/sidebar/Sidebar.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-import { Tag, Home, PackageOpen, Settings, Search, Shield } from "lucide-react";
-import { redirect } from "next/navigation";
-import SidebarItem from "./SidebarItem";
-import { getServerAuthSession } from "@/server/auth";
-import Link from "next/link";
-import SidebarProfileOptions from "./SidebarProfileOptions";
-import { Separator } from "@/components/ui/separator";
-import AllLists from "./AllLists";
-import serverConfig from "@hoarder/shared/config";
-import { api } from "@/server/api/client";
-
-export default async function Sidebar() {
- const session = await getServerAuthSession();
- if (!session) {
- redirect("/");
- }
-
- const lists = await api.lists.list();
-
- return (
- <aside className="flex h-screen w-60 flex-col gap-5 border-r p-4">
- <Link href={"/dashboard/bookmarks"}>
- <div className="flex items-center rounded-lg px-1 text-slate-900">
- <PackageOpen />
- <span className="ml-2 text-base font-semibold">Hoarder</span>
- </div>
- </Link>
- <hr />
- <div>
- <ul className="space-y-2 text-sm font-medium">
- <SidebarItem
- logo={<Home />}
- name="Home"
- path="/dashboard/bookmarks"
- />
- {serverConfig.meilisearch && (
- <SidebarItem
- logo={<Search />}
- name="Search"
- path="/dashboard/search"
- />
- )}
- <SidebarItem logo={<Tag />} name="Tags" path="/dashboard/tags" />
- <SidebarItem
- logo={<Settings />}
- name="Settings"
- path="/dashboard/settings"
- />
- {session.user.role == "admin" && (
- <SidebarItem
- logo={<Shield />}
- name="Admin"
- path="/dashboard/admin"
- />
- )}
- </ul>
- </div>
- <Separator />
- <AllLists initialData={lists} />
- <div className="mt-auto flex justify-between justify-self-end">
- <div className="my-auto"> {session.user.name} </div>
- <SidebarProfileOptions />
- </div>
- </aside>
- );
-}
diff --git a/packages/web/components/dashboard/sidebar/SidebarItem.tsx b/packages/web/components/dashboard/sidebar/SidebarItem.tsx
deleted file mode 100644
index 856bdffd..00000000
--- a/packages/web/components/dashboard/sidebar/SidebarItem.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-"use client";
-
-import { cn } from "@/lib/utils";
-import Link from "next/link";
-import { usePathname } from "next/navigation";
-
-export default function SidebarItem({
- name,
- logo,
- path,
- className,
-}: {
- name: string;
- logo: React.ReactNode;
- path: string;
- className?: string;
-}) {
- const currentPath = usePathname();
- return (
- <li
- className={cn(
- "rounded-lg px-3 py-2 hover:bg-slate-100",
- path == currentPath ? "bg-gray-50" : "",
- className,
- )}
- >
- <Link href={path} className="flex w-full gap-x-2">
- {logo}
- <span className="my-auto"> {name} </span>
- </Link>
- </li>
- );
-}
diff --git a/packages/web/components/dashboard/sidebar/SidebarProfileOptions.tsx b/packages/web/components/dashboard/sidebar/SidebarProfileOptions.tsx
deleted file mode 100644
index f931b63e..00000000
--- a/packages/web/components/dashboard/sidebar/SidebarProfileOptions.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-"use client";
-
-import { Button } from "@/components/ui/button";
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu";
-import { LogOut, MoreHorizontal } from "lucide-react";
-import { signOut } from "next-auth/react";
-
-export default function SidebarProfileOptions() {
- return (
- <DropdownMenu>
- <DropdownMenuTrigger asChild>
- <Button variant="ghost">
- <MoreHorizontal />
- </Button>
- </DropdownMenuTrigger>
- <DropdownMenuContent className="w-fit">
- <DropdownMenuItem
- onClick={() =>
- signOut({
- callbackUrl: "/",
- })
- }
- >
- <LogOut className="mr-2 size-4" />
- <span>Sign Out</span>
- </DropdownMenuItem>
- </DropdownMenuContent>
- </DropdownMenu>
- );
-}
diff --git a/packages/web/components/signin/CredentialsForm.tsx b/packages/web/components/signin/CredentialsForm.tsx
deleted file mode 100644
index 5296e163..00000000
--- a/packages/web/components/signin/CredentialsForm.tsx
+++ /dev/null
@@ -1,222 +0,0 @@
-"use client";
-import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { useForm } from "react-hook-form";
-import { z } from "zod";
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from "@/components/ui/form";
-import { Input } from "@/components/ui/input";
-import { ActionButton } from "@/components/ui/action-button";
-import { zSignUpSchema } from "@hoarder/trpc/types/users";
-import { signIn } from "next-auth/react";
-import { useState } from "react";
-import { api } from "@/lib/trpc";
-import { useRouter } from "next/navigation";
-import { TRPCClientError } from "@trpc/client";
-
-const signInSchema = z.object({
- email: z.string().email(),
- password: z.string(),
-});
-
-function SignIn() {
- const [signinError, setSigninError] = useState(false);
- const router = useRouter();
- const form = useForm<z.infer<typeof signInSchema>>({
- resolver: zodResolver(signInSchema),
- });
-
- return (
- <Form {...form}>
- <form
- onSubmit={form.handleSubmit(async (value) => {
- const resp = await signIn("credentials", {
- redirect: false,
- email: value.email,
- password: value.password,
- });
- if (!resp || !resp?.ok) {
- setSigninError(true);
- return;
- }
- router.replace("/");
- })}
- >
- <div className="flex w-full flex-col space-y-2">
- {signinError && (
- <p className="w-full text-center text-red-500">
- Incorrect username or password
- </p>
- )}
- <FormField
- control={form.control}
- name="email"
- render={({ field }) => {
- return (
- <FormItem>
- <FormLabel>Email</FormLabel>
- <FormControl>
- <Input type="text" placeholder="Email" {...field} />
- </FormControl>
- <FormMessage />
- </FormItem>
- );
- }}
- />
- <FormField
- control={form.control}
- name="password"
- render={({ field }) => {
- return (
- <FormItem>
- <FormLabel>Password</FormLabel>
- <FormControl>
- <Input type="password" placeholder="Password" {...field} />
- </FormControl>
- <FormMessage />
- </FormItem>
- );
- }}
- />
- <ActionButton type="submit" loading={form.formState.isSubmitting}>
- Sign In
- </ActionButton>
- </div>
- </form>
- </Form>
- );
-}
-
-function SignUp() {
- const form = useForm<z.infer<typeof zSignUpSchema>>({
- resolver: zodResolver(zSignUpSchema),
- });
- const [errorMessage, setErrorMessage] = useState("");
-
- const router = useRouter();
-
- const createUserMutation = api.users.create.useMutation();
-
- return (
- <Form {...form}>
- <form
- onSubmit={form.handleSubmit(async (value) => {
- try {
- await createUserMutation.mutateAsync(value);
- } catch (e) {
- if (e instanceof TRPCClientError) {
- setErrorMessage(e.message);
- }
- return;
- }
- const resp = await signIn("credentials", {
- redirect: false,
- email: value.email,
- password: value.password,
- });
- if (!resp || !resp.ok) {
- setErrorMessage("Hit an unexpected error while signing in");
- return;
- }
- router.replace("/");
- })}
- >
- <div className="flex w-full flex-col space-y-2">
- {errorMessage && (
- <p className="w-full text-center text-red-500">{errorMessage}</p>
- )}
- <FormField
- control={form.control}
- name="name"
- render={({ field }) => {
- return (
- <FormItem>
- <FormLabel>Name</FormLabel>
- <FormControl>
- <Input type="text" placeholder="Name" {...field} />
- </FormControl>
- <FormMessage />
- </FormItem>
- );
- }}
- />
- <FormField
- control={form.control}
- name="email"
- render={({ field }) => {
- return (
- <FormItem>
- <FormLabel>Email</FormLabel>
- <FormControl>
- <Input type="text" placeholder="Email" {...field} />
- </FormControl>
- <FormMessage />
- </FormItem>
- );
- }}
- />
- <FormField
- control={form.control}
- name="password"
- render={({ field }) => {
- return (
- <FormItem>
- <FormLabel>Password</FormLabel>
- <FormControl>
- <Input type="password" placeholder="Password" {...field} />
- </FormControl>
- <FormMessage />
- </FormItem>
- );
- }}
- />
- <FormField
- control={form.control}
- name="confirmPassword"
- render={({ field }) => {
- return (
- <FormItem>
- <FormLabel>Confirm Password</FormLabel>
- <FormControl>
- <Input
- type="password"
- placeholder="Confirm Password"
- {...field}
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- );
- }}
- />
- <ActionButton type="submit" loading={form.formState.isSubmitting}>
- Sign Up
- </ActionButton>
- </div>
- </form>
- </Form>
- );
-}
-
-export default function CredentialsForm() {
- return (
- <Tabs defaultValue="signin" className="w-full">
- <TabsList className="grid w-full grid-cols-2">
- <TabsTrigger value="signin">Sign In</TabsTrigger>
- <TabsTrigger value="signup">Sign Up</TabsTrigger>
- </TabsList>
- <TabsContent value="signin">
- <SignIn />
- </TabsContent>
- <TabsContent value="signup">
- <SignUp />
- </TabsContent>
- </Tabs>
- );
-}
diff --git a/packages/web/components/signin/SignInForm.tsx b/packages/web/components/signin/SignInForm.tsx
deleted file mode 100644
index 7c8f8936..00000000
--- a/packages/web/components/signin/SignInForm.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import { getProviders } from "next-auth/react";
-import SignInProviderButton from "./SignInProviderButton";
-import CredentialsForm from "./CredentialsForm";
-
-export default async function SignInForm() {
- const providers = await getProviders();
- let providerValues;
- if (providers) {
- providerValues = Object.values(providers).filter(
- // Credentials are handled manually by the sign in form
- (p) => p.id != "credentials",
- );
- }
-
- return (
- <div className="flex flex-col items-center space-y-2">
- <CredentialsForm />
-
- {providerValues && providerValues.length > 0 && (
- <>
- <div className="flex w-full items-center">
- <div className="flex-1 grow border-t-2 border-gray-200"></div>
- <span className="bg-white px-3 text-gray-500">Or</span>
- <div className="flex-1 grow border-t-2 border-gray-200"></div>
- </div>
- <div className="space-y-2">
- {providerValues.map((provider) => (
- <div key={provider.id}>
- <SignInProviderButton provider={provider} />
- </div>
- ))}
- </div>
- </>
- )}
- </div>
- );
-}
diff --git a/packages/web/components/signin/SignInProviderButton.tsx b/packages/web/components/signin/SignInProviderButton.tsx
deleted file mode 100644
index 0831236c..00000000
--- a/packages/web/components/signin/SignInProviderButton.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-"use client";
-import { Button } from "@/components/ui/button";
-import { ClientSafeProvider, signIn } from "next-auth/react";
-
-export default function SignInProviderButton({
- provider,
-}: {
- provider: ClientSafeProvider;
-}) {
- return (
- <Button
- onClick={() =>
- signIn(provider.id, {
- callbackUrl: "/",
- })
- }
- >
- Sign in with {provider.name}
- </Button>
- );
-}
diff --git a/packages/web/components/ui/action-button.tsx b/packages/web/components/ui/action-button.tsx
deleted file mode 100644
index 42e16f65..00000000
--- a/packages/web/components/ui/action-button.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import { Button, ButtonProps } from "./button";
-import LoadingSpinner from "./spinner";
-
-export function ActionButton({
- children,
- loading,
- spinner,
- disabled,
- ...props
-}: ButtonProps & {
- loading: boolean;
- spinner?: React.ReactNode;
-}) {
- spinner ||= <LoadingSpinner />;
- if (disabled !== undefined) {
- disabled ||= loading;
- } else if (loading) {
- disabled = true;
- }
- return (
- <Button {...props} disabled={disabled}>
- {loading ? spinner : children}
- </Button>
- );
-}
diff --git a/packages/web/components/ui/back-button.tsx b/packages/web/components/ui/back-button.tsx
deleted file mode 100644
index 685930df..00000000
--- a/packages/web/components/ui/back-button.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-"use client";
-
-import { useRouter } from "next/navigation";
-import { Button, ButtonProps } from "./button";
-
-export function BackButton({ ...props }: ButtonProps) {
- const router = useRouter();
- return <Button {...props} onClick={() => router.back()} />;
-}
diff --git a/packages/web/components/ui/badge.tsx b/packages/web/components/ui/badge.tsx
deleted file mode 100644
index c30daca1..00000000
--- a/packages/web/components/ui/badge.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import * as React from "react";
-import { cva, type VariantProps } from "class-variance-authority";
-
-import { cn } from "@/lib/utils";
-
-const badgeVariants = cva(
- "focus:ring-ring inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2",
- {
- variants: {
- variant: {
- default:
- "bg-primary text-primary-foreground hover:bg-primary/80 border-transparent",
- secondary:
- "bg-secondary text-secondary-foreground hover:bg-secondary/80 border-transparent",
- destructive:
- "bg-destructive text-destructive-foreground hover:bg-destructive/80 border-transparent",
- outline: "text-foreground",
- },
- },
- defaultVariants: {
- variant: "default",
- },
- },
-);
-
-export interface BadgeProps
- extends React.HTMLAttributes<HTMLDivElement>,
- VariantProps<typeof badgeVariants> {}
-
-function Badge({ className, variant, ...props }: BadgeProps) {
- return (
- <div className={cn(badgeVariants({ variant }), className)} {...props} />
- );
-}
-
-export { Badge, badgeVariants };
diff --git a/packages/web/components/ui/button.tsx b/packages/web/components/ui/button.tsx
deleted file mode 100644
index 79b45fa0..00000000
--- a/packages/web/components/ui/button.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import * as React from "react";
-import { Slot } from "@radix-ui/react-slot";
-import { cva, type VariantProps } from "class-variance-authority";
-
-import { cn } from "@/lib/utils";
-
-const buttonVariants = cva(
- "ring-offset-background focus-visible:ring-ring inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
- {
- variants: {
- variant: {
- default: "bg-primary text-primary-foreground hover:bg-primary/90",
- destructive:
- "bg-destructive text-destructive-foreground hover:bg-destructive/90",
- outline:
- "border-input bg-background hover:bg-accent hover:text-accent-foreground border",
- secondary:
- "bg-secondary text-secondary-foreground hover:bg-secondary/80",
- ghost: "hover:bg-accent hover:text-accent-foreground",
- link: "text-primary underline-offset-4 hover:underline",
- },
- size: {
- default: "h-10 px-4 py-2",
- sm: "h-9 rounded-md px-3",
- lg: "h-11 rounded-md px-8",
- icon: "size-10",
- },
- },
- defaultVariants: {
- variant: "default",
- size: "default",
- },
- },
-);
-
-export interface ButtonProps
- extends React.ButtonHTMLAttributes<HTMLButtonElement>,
- VariantProps<typeof buttonVariants> {
- asChild?: boolean;
-}
-
-const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
- ({ className, variant, size, asChild = false, ...props }, ref) => {
- const Comp = asChild ? Slot : "button";
- return (
- <Comp
- className={cn(buttonVariants({ variant, size, className }))}
- ref={ref}
- {...props}
- />
- );
- },
-);
-Button.displayName = "Button";
-
-export { Button, buttonVariants };
diff --git a/packages/web/components/ui/card.tsx b/packages/web/components/ui/card.tsx
deleted file mode 100644
index f4e57996..00000000
--- a/packages/web/components/ui/card.tsx
+++ /dev/null
@@ -1,86 +0,0 @@
-import * as React from "react";
-
-import { cn } from "@/lib/utils";
-
-const Card = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes<HTMLDivElement>
->(({ className, ...props }, ref) => (
- <div
- ref={ref}
- className={cn(
- "bg-card text-card-foreground rounded-lg border shadow-sm",
- className,
- )}
- {...props}
- />
-));
-Card.displayName = "Card";
-
-const CardHeader = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes<HTMLDivElement>
->(({ className, ...props }, ref) => (
- <div
- ref={ref}
- className={cn("flex flex-col space-y-1.5 p-6", className)}
- {...props}
- />
-));
-CardHeader.displayName = "CardHeader";
-
-const CardTitle = React.forwardRef<
- HTMLParagraphElement,
- React.HTMLAttributes<HTMLHeadingElement>
->(({ className, ...props }, ref) => (
- <h3
- ref={ref}
- className={cn(
- "text-2xl font-semibold leading-none tracking-tight",
- className,
- )}
- {...props}
- />
-));
-CardTitle.displayName = "CardTitle";
-
-const CardDescription = React.forwardRef<
- HTMLParagraphElement,
- React.HTMLAttributes<HTMLParagraphElement>
->(({ className, ...props }, ref) => (
- <p
- ref={ref}
- className={cn("text-muted-foreground text-sm", className)}
- {...props}
- />
-));
-CardDescription.displayName = "CardDescription";
-
-const CardContent = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes<HTMLDivElement>
->(({ className, ...props }, ref) => (
- <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
-));
-CardContent.displayName = "CardContent";
-
-const CardFooter = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes<HTMLDivElement>
->(({ className, ...props }, ref) => (
- <div
- ref={ref}
- className={cn("flex items-center p-6 pt-0", className)}
- {...props}
- />
-));
-CardFooter.displayName = "CardFooter";
-
-export {
- Card,
- CardHeader,
- CardFooter,
- CardTitle,
- CardDescription,
- CardContent,
-};
diff --git a/packages/web/components/ui/dialog.tsx b/packages/web/components/ui/dialog.tsx
deleted file mode 100644
index 8fe3fe35..00000000
--- a/packages/web/components/ui/dialog.tsx
+++ /dev/null
@@ -1,122 +0,0 @@
-"use client";
-
-import * as React from "react";
-import * as DialogPrimitive from "@radix-ui/react-dialog";
-import { X } from "lucide-react";
-
-import { cn } from "@/lib/utils";
-
-const Dialog = DialogPrimitive.Root;
-
-const DialogTrigger = DialogPrimitive.Trigger;
-
-const DialogPortal = DialogPrimitive.Portal;
-
-const DialogClose = DialogPrimitive.Close;
-
-const DialogOverlay = React.forwardRef<
- React.ElementRef<typeof DialogPrimitive.Overlay>,
- React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
->(({ className, ...props }, ref) => (
- <DialogPrimitive.Overlay
- ref={ref}
- className={cn(
- "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80",
- className,
- )}
- {...props}
- />
-));
-DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
-
-const DialogContent = React.forwardRef<
- React.ElementRef<typeof DialogPrimitive.Content>,
- React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
->(({ className, children, ...props }, ref) => (
- <DialogPortal>
- <DialogOverlay />
- <DialogPrimitive.Content
- ref={ref}
- className={cn(
- "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg duration-200 sm:rounded-lg",
- className,
- )}
- {...props}
- >
- {children}
- <DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none">
- <X className="size-4" />
- <span className="sr-only">Close</span>
- </DialogPrimitive.Close>
- </DialogPrimitive.Content>
- </DialogPortal>
-));
-DialogContent.displayName = DialogPrimitive.Content.displayName;
-
-const DialogHeader = ({
- className,
- ...props
-}: React.HTMLAttributes<HTMLDivElement>) => (
- <div
- className={cn(
- "flex flex-col space-y-1.5 text-center sm:text-left",
- className,
- )}
- {...props}
- />
-);
-DialogHeader.displayName = "DialogHeader";
-
-const DialogFooter = ({
- className,
- ...props
-}: React.HTMLAttributes<HTMLDivElement>) => (
- <div
- className={cn(
- "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
- className,
- )}
- {...props}
- />
-);
-DialogFooter.displayName = "DialogFooter";
-
-const DialogTitle = React.forwardRef<
- React.ElementRef<typeof DialogPrimitive.Title>,
- React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
->(({ className, ...props }, ref) => (
- <DialogPrimitive.Title
- ref={ref}
- className={cn(
- "text-lg font-semibold leading-none tracking-tight",
- className,
- )}
- {...props}
- />
-));
-DialogTitle.displayName = DialogPrimitive.Title.displayName;
-
-const DialogDescription = React.forwardRef<
- React.ElementRef<typeof DialogPrimitive.Description>,
- React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
->(({ className, ...props }, ref) => (
- <DialogPrimitive.Description
- ref={ref}
- className={cn("text-muted-foreground text-sm", className)}
- {...props}
- />
-));
-DialogDescription.displayName = DialogPrimitive.Description.displayName;
-
-export {
- Dialog,
- DialogPortal,
- DialogOverlay,
- DialogClose,
- DialogTrigger,
- DialogContent,
- DialogHeader,
- DialogFooter,
- DialogTitle,
- DialogDescription,
-};
diff --git a/packages/web/components/ui/dropdown-menu.tsx b/packages/web/components/ui/dropdown-menu.tsx
deleted file mode 100644
index 3a9a2ff7..00000000
--- a/packages/web/components/ui/dropdown-menu.tsx
+++ /dev/null
@@ -1,200 +0,0 @@
-"use client";
-
-import * as React from "react";
-import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
-import { Check, ChevronRight, Circle } from "lucide-react";
-
-import { cn } from "@/lib/utils";
-
-const DropdownMenu = DropdownMenuPrimitive.Root;
-
-const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
-
-const DropdownMenuGroup = DropdownMenuPrimitive.Group;
-
-const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
-
-const DropdownMenuSub = DropdownMenuPrimitive.Sub;
-
-const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
-
-const DropdownMenuSubTrigger = React.forwardRef<
- React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
- inset?: boolean;
- }
->(({ className, inset, children, ...props }, ref) => (
- <DropdownMenuPrimitive.SubTrigger
- ref={ref}
- className={cn(
- "focus:bg-accent data-[state=open]:bg-accent flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none",
- inset && "pl-8",
- className,
- )}
- {...props}
- >
- {children}
- <ChevronRight className="ml-auto size-4" />
- </DropdownMenuPrimitive.SubTrigger>
-));
-DropdownMenuSubTrigger.displayName =
- DropdownMenuPrimitive.SubTrigger.displayName;
-
-const DropdownMenuSubContent = React.forwardRef<
- React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
->(({ className, ...props }, ref) => (
- <DropdownMenuPrimitive.SubContent
- ref={ref}
- className={cn(
- "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-lg",
- className,
- )}
- {...props}
- />
-));
-DropdownMenuSubContent.displayName =
- DropdownMenuPrimitive.SubContent.displayName;
-
-const DropdownMenuContent = React.forwardRef<
- React.ElementRef<typeof DropdownMenuPrimitive.Content>,
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
->(({ className, sideOffset = 4, ...props }, ref) => (
- <DropdownMenuPrimitive.Portal>
- <DropdownMenuPrimitive.Content
- ref={ref}
- sideOffset={sideOffset}
- className={cn(
- "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-md",
- className,
- )}
- {...props}
- />
- </DropdownMenuPrimitive.Portal>
-));
-DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
-
-const DropdownMenuItem = React.forwardRef<
- React.ElementRef<typeof DropdownMenuPrimitive.Item>,
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
- inset?: boolean;
- }
->(({ className, inset, ...props }, ref) => (
- <DropdownMenuPrimitive.Item
- ref={ref}
- className={cn(
- "focus:bg-accent focus:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
- inset && "pl-8",
- className,
- )}
- {...props}
- />
-));
-DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
-
-const DropdownMenuCheckboxItem = React.forwardRef<
- React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
->(({ className, children, checked, ...props }, ref) => (
- <DropdownMenuPrimitive.CheckboxItem
- ref={ref}
- className={cn(
- "focus:bg-accent focus:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
- className,
- )}
- checked={checked}
- {...props}
- >
- <span className="absolute left-2 flex size-3.5 items-center justify-center">
- <DropdownMenuPrimitive.ItemIndicator>
- <Check className="size-4" />
- </DropdownMenuPrimitive.ItemIndicator>
- </span>
- {children}
- </DropdownMenuPrimitive.CheckboxItem>
-));
-DropdownMenuCheckboxItem.displayName =
- DropdownMenuPrimitive.CheckboxItem.displayName;
-
-const DropdownMenuRadioItem = React.forwardRef<
- React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
->(({ className, children, ...props }, ref) => (
- <DropdownMenuPrimitive.RadioItem
- ref={ref}
- className={cn(
- "focus:bg-accent focus:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
- className,
- )}
- {...props}
- >
- <span className="absolute left-2 flex size-3.5 items-center justify-center">
- <DropdownMenuPrimitive.ItemIndicator>
- <Circle className="size-2 fill-current" />
- </DropdownMenuPrimitive.ItemIndicator>
- </span>
- {children}
- </DropdownMenuPrimitive.RadioItem>
-));
-DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
-
-const DropdownMenuLabel = React.forwardRef<
- React.ElementRef<typeof DropdownMenuPrimitive.Label>,
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
- inset?: boolean;
- }
->(({ className, inset, ...props }, ref) => (
- <DropdownMenuPrimitive.Label
- ref={ref}
- className={cn(
- "px-2 py-1.5 text-sm font-semibold",
- inset && "pl-8",
- className,
- )}
- {...props}
- />
-));
-DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
-
-const DropdownMenuSeparator = React.forwardRef<
- React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
->(({ className, ...props }, ref) => (
- <DropdownMenuPrimitive.Separator
- ref={ref}
- className={cn("bg-muted -mx-1 my-1 h-px", className)}
- {...props}
- />
-));
-DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
-
-const DropdownMenuShortcut = ({
- className,
- ...props
-}: React.HTMLAttributes<HTMLSpanElement>) => {
- return (
- <span
- className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
- {...props}
- />
- );
-};
-DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
-
-export {
- DropdownMenu,
- DropdownMenuTrigger,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuCheckboxItem,
- DropdownMenuRadioItem,
- DropdownMenuLabel,
- DropdownMenuSeparator,
- DropdownMenuShortcut,
- DropdownMenuGroup,
- DropdownMenuPortal,
- DropdownMenuSub,
- DropdownMenuSubContent,
- DropdownMenuSubTrigger,
- DropdownMenuRadioGroup,
-};
diff --git a/packages/web/components/ui/form.tsx b/packages/web/components/ui/form.tsx
deleted file mode 100644
index e62e10e9..00000000
--- a/packages/web/components/ui/form.tsx
+++ /dev/null
@@ -1,177 +0,0 @@
-import * as React from "react";
-import * as LabelPrimitive from "@radix-ui/react-label";
-import { Slot } from "@radix-ui/react-slot";
-import {
- Controller,
- ControllerProps,
- FieldPath,
- FieldValues,
- FormProvider,
- useFormContext,
-} from "react-hook-form";
-
-import { cn } from "@/lib/utils";
-import { Label } from "@/components/ui/label";
-
-const Form = FormProvider;
-
-type FormFieldContextValue<
- TFieldValues extends FieldValues = FieldValues,
- TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
-> = {
- name: TName;
-};
-
-const FormFieldContext = React.createContext<FormFieldContextValue>(
- {} as FormFieldContextValue,
-);
-
-const FormField = <
- TFieldValues extends FieldValues = FieldValues,
- TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
->({
- ...props
-}: ControllerProps<TFieldValues, TName>) => {
- return (
- <FormFieldContext.Provider value={{ name: props.name }}>
- <Controller {...props} />
- </FormFieldContext.Provider>
- );
-};
-
-const useFormField = () => {
- const fieldContext = React.useContext(FormFieldContext);
- const itemContext = React.useContext(FormItemContext);
- const { getFieldState, formState } = useFormContext();
-
- const fieldState = getFieldState(fieldContext.name, formState);
-
- if (!fieldContext) {
- throw new Error("useFormField should be used within <FormField>");
- }
-
- const { id } = itemContext;
-
- return {
- id,
- name: fieldContext.name,
- formItemId: `${id}-form-item`,
- formDescriptionId: `${id}-form-item-description`,
- formMessageId: `${id}-form-item-message`,
- ...fieldState,
- };
-};
-
-type FormItemContextValue = {
- id: string;
-};
-
-const FormItemContext = React.createContext<FormItemContextValue>(
- {} as FormItemContextValue,
-);
-
-const FormItem = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes<HTMLDivElement>
->(({ className, ...props }, ref) => {
- const id = React.useId();
-
- return (
- <FormItemContext.Provider value={{ id }}>
- <div ref={ref} className={cn("space-y-2", className)} {...props} />
- </FormItemContext.Provider>
- );
-});
-FormItem.displayName = "FormItem";
-
-const FormLabel = React.forwardRef<
- React.ElementRef<typeof LabelPrimitive.Root>,
- React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
->(({ className, ...props }, ref) => {
- const { error, formItemId } = useFormField();
-
- return (
- <Label
- ref={ref}
- className={cn(error && "text-destructive", className)}
- htmlFor={formItemId}
- {...props}
- />
- );
-});
-FormLabel.displayName = "FormLabel";
-
-const FormControl = React.forwardRef<
- React.ElementRef<typeof Slot>,
- React.ComponentPropsWithoutRef<typeof Slot>
->(({ ...props }, ref) => {
- const { error, formItemId, formDescriptionId, formMessageId } =
- useFormField();
-
- return (
- <Slot
- ref={ref}
- id={formItemId}
- aria-describedby={
- !error
- ? `${formDescriptionId}`
- : `${formDescriptionId} ${formMessageId}`
- }
- aria-invalid={!!error}
- {...props}
- />
- );
-});
-FormControl.displayName = "FormControl";
-
-const FormDescription = React.forwardRef<
- HTMLParagraphElement,
- React.HTMLAttributes<HTMLParagraphElement>
->(({ className, ...props }, ref) => {
- const { formDescriptionId } = useFormField();
-
- return (
- <p
- ref={ref}
- id={formDescriptionId}
- className={cn("text-muted-foreground text-sm", className)}
- {...props}
- />
- );
-});
-FormDescription.displayName = "FormDescription";
-
-const FormMessage = React.forwardRef<
- HTMLParagraphElement,
- React.HTMLAttributes<HTMLParagraphElement>
->(({ className, children, ...props }, ref) => {
- const { error, formMessageId } = useFormField();
- const body = error ? String(error?.message) : children;
-
- if (!body) {
- return null;
- }
-
- return (
- <p
- ref={ref}
- id={formMessageId}
- className={cn("text-destructive text-sm font-medium", className)}
- {...props}
- >
- {body}
- </p>
- );
-});
-FormMessage.displayName = "FormMessage";
-
-export {
- useFormField,
- Form,
- FormItem,
- FormLabel,
- FormControl,
- FormDescription,
- FormMessage,
- FormField,
-};
diff --git a/packages/web/components/ui/imageCard.tsx b/packages/web/components/ui/imageCard.tsx
deleted file mode 100644
index f10ebdb5..00000000
--- a/packages/web/components/ui/imageCard.tsx
+++ /dev/null
@@ -1,70 +0,0 @@
-import * as React from "react";
-
-import { cn } from "@/lib/utils";
-
-export function ImageCard({
- className,
- ...props
-}: React.HTMLAttributes<HTMLDivElement>) {
- return (
- <div
- className={cn("h-96 overflow-hidden rounded-lg shadow-md", className)}
- {...props}
- />
- );
-}
-
-export function ImageCardBanner({
- className,
- ...props
-}: React.ImgHTMLAttributes<HTMLImageElement>) {
- return (
- // eslint-disable-next-line @next/next/no-img-element
- <img
- className={cn("h-56 min-h-56 w-full object-cover", className)}
- alt="card banner"
- {...props}
- />
- );
-}
-
-export function ImageCardContent({
- className,
- ...props
-}: React.HTMLAttributes<HTMLDivElement>) {
- return (
- <div
- className={cn(
- "flex h-40 min-h-40 flex-col justify-between p-2",
- className,
- )}
- {...props}
- />
- );
-}
-
-export function ImageCardTitle({
- className,
- ...props
-}: React.HTMLAttributes<HTMLDivElement>) {
- return (
- <div
- className={cn("order-first flex-none text-lg font-bold", className)}
- {...props}
- />
- );
-}
-
-export function ImageCardBody({
- className,
- ...props
-}: React.HTMLAttributes<HTMLDivElement>) {
- return <div className={cn("order-1", className)} {...props} />;
-}
-
-export function ImageCardFooter({
- className,
- ...props
-}: React.HTMLAttributes<HTMLDivElement>) {
- return <div className={cn("order-last", className)} {...props} />;
-}
diff --git a/packages/web/components/ui/input.tsx b/packages/web/components/ui/input.tsx
deleted file mode 100644
index 21aac7ad..00000000
--- a/packages/web/components/ui/input.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import * as React from "react";
-
-import { cn } from "@/lib/utils";
-
-export interface InputProps
- extends React.InputHTMLAttributes<HTMLInputElement> {}
-
-const Input = React.forwardRef<HTMLInputElement, InputProps>(
- ({ className, type, ...props }, ref) => {
- return (
- <input
- type={type}
- className={cn(
- "border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
- className,
- )}
- ref={ref}
- {...props}
- />
- );
- },
-);
-Input.displayName = "Input";
-
-export { Input };
diff --git a/packages/web/components/ui/label.tsx b/packages/web/components/ui/label.tsx
deleted file mode 100644
index 84f8b0c7..00000000
--- a/packages/web/components/ui/label.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-"use client";
-
-import * as React from "react";
-import * as LabelPrimitive from "@radix-ui/react-label";
-import { cva, type VariantProps } from "class-variance-authority";
-
-import { cn } from "@/lib/utils";
-
-const labelVariants = cva(
- "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
-);
-
-const Label = React.forwardRef<
- React.ElementRef<typeof LabelPrimitive.Root>,
- React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
- VariantProps<typeof labelVariants>
->(({ className, ...props }, ref) => (
- <LabelPrimitive.Root
- ref={ref}
- className={cn(labelVariants(), className)}
- {...props}
- />
-));
-Label.displayName = LabelPrimitive.Root.displayName;
-
-export { Label };
diff --git a/packages/web/components/ui/popover.tsx b/packages/web/components/ui/popover.tsx
deleted file mode 100644
index a361ba7d..00000000
--- a/packages/web/components/ui/popover.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-"use client";
-
-import * as React from "react";
-import * as PopoverPrimitive from "@radix-ui/react-popover";
-
-import { cn } from "@/lib/utils";
-
-const Popover = PopoverPrimitive.Root;
-
-const PopoverTrigger = PopoverPrimitive.Trigger;
-
-const PopoverContent = React.forwardRef<
- React.ElementRef<typeof PopoverPrimitive.Content>,
- React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
->(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
- <PopoverPrimitive.Portal>
- <PopoverPrimitive.Content
- ref={ref}
- align={align}
- sideOffset={sideOffset}
- className={cn(
- "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 rounded-md border p-4 shadow-md outline-none",
- className,
- )}
- {...props}
- />
- </PopoverPrimitive.Portal>
-));
-PopoverContent.displayName = PopoverPrimitive.Content.displayName;
-
-export { Popover, PopoverTrigger, PopoverContent };
diff --git a/packages/web/components/ui/scroll-area.tsx b/packages/web/components/ui/scroll-area.tsx
deleted file mode 100644
index 32cb6022..00000000
--- a/packages/web/components/ui/scroll-area.tsx
+++ /dev/null
@@ -1,48 +0,0 @@
-"use client";
-
-import * as React from "react";
-import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
-
-import { cn } from "@/lib/utils";
-
-const ScrollArea = React.forwardRef<
- React.ElementRef<typeof ScrollAreaPrimitive.Root>,
- React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
->(({ className, children, ...props }, ref) => (
- <ScrollAreaPrimitive.Root
- ref={ref}
- className={cn("relative overflow-hidden", className)}
- {...props}
- >
- <ScrollAreaPrimitive.Viewport className="size-full rounded-[inherit]">
- {children}
- </ScrollAreaPrimitive.Viewport>
- <ScrollBar />
- <ScrollAreaPrimitive.Corner />
- </ScrollAreaPrimitive.Root>
-));
-ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
-
-const ScrollBar = React.forwardRef<
- React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
- React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
->(({ className, orientation = "vertical", ...props }, ref) => (
- <ScrollAreaPrimitive.ScrollAreaScrollbar
- ref={ref}
- orientation={orientation}
- className={cn(
- "flex touch-none select-none transition-colors",
- orientation === "vertical" &&
- "h-full w-2.5 border-l border-l-transparent p-[1px]",
- orientation === "horizontal" &&
- "h-2.5 flex-col border-t border-t-transparent p-[1px]",
- className,
- )}
- {...props}
- >
- <ScrollAreaPrimitive.ScrollAreaThumb className="bg-border relative flex-1 rounded-full" />
- </ScrollAreaPrimitive.ScrollAreaScrollbar>
-));
-ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
-
-export { ScrollArea, ScrollBar };
diff --git a/packages/web/components/ui/select.tsx b/packages/web/components/ui/select.tsx
deleted file mode 100644
index efd4ff1e..00000000
--- a/packages/web/components/ui/select.tsx
+++ /dev/null
@@ -1,160 +0,0 @@
-"use client";
-
-import * as React from "react";
-import * as SelectPrimitive from "@radix-ui/react-select";
-import { Check, ChevronDown, ChevronUp } from "lucide-react";
-
-import { cn } from "@/lib/utils";
-
-const Select = SelectPrimitive.Root;
-
-const SelectGroup = SelectPrimitive.Group;
-
-const SelectValue = SelectPrimitive.Value;
-
-const SelectTrigger = React.forwardRef<
- React.ElementRef<typeof SelectPrimitive.Trigger>,
- React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
->(({ className, children, ...props }, ref) => (
- <SelectPrimitive.Trigger
- ref={ref}
- className={cn(
- "border-input bg-background ring-offset-background placeholder:text-muted-foreground focus:ring-ring flex h-10 w-full items-center justify-between rounded-md border px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
- className,
- )}
- {...props}
- >
- {children}
- <SelectPrimitive.Icon asChild>
- <ChevronDown className="size-4 opacity-50" />
- </SelectPrimitive.Icon>
- </SelectPrimitive.Trigger>
-));
-SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
-
-const SelectScrollUpButton = React.forwardRef<
- React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
- React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
->(({ className, ...props }, ref) => (
- <SelectPrimitive.ScrollUpButton
- ref={ref}
- className={cn(
- "flex cursor-default items-center justify-center py-1",
- className,
- )}
- {...props}
- >
- <ChevronUp className="size-4" />
- </SelectPrimitive.ScrollUpButton>
-));
-SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
-
-const SelectScrollDownButton = React.forwardRef<
- React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
- React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
->(({ className, ...props }, ref) => (
- <SelectPrimitive.ScrollDownButton
- ref={ref}
- className={cn(
- "flex cursor-default items-center justify-center py-1",
- className,
- )}
- {...props}
- >
- <ChevronDown className="size-4" />
- </SelectPrimitive.ScrollDownButton>
-));
-SelectScrollDownButton.displayName =
- SelectPrimitive.ScrollDownButton.displayName;
-
-const SelectContent = React.forwardRef<
- React.ElementRef<typeof SelectPrimitive.Content>,
- React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
->(({ className, children, position = "popper", ...props }, ref) => (
- <SelectPrimitive.Portal>
- <SelectPrimitive.Content
- ref={ref}
- className={cn(
- "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border shadow-md",
- position === "popper" &&
- "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
- className,
- )}
- position={position}
- {...props}
- >
- <SelectScrollUpButton />
- <SelectPrimitive.Viewport
- className={cn(
- "p-1",
- position === "popper" &&
- "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
- )}
- >
- {children}
- </SelectPrimitive.Viewport>
- <SelectScrollDownButton />
- </SelectPrimitive.Content>
- </SelectPrimitive.Portal>
-));
-SelectContent.displayName = SelectPrimitive.Content.displayName;
-
-const SelectLabel = React.forwardRef<
- React.ElementRef<typeof SelectPrimitive.Label>,
- React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
->(({ className, ...props }, ref) => (
- <SelectPrimitive.Label
- ref={ref}
- className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
- {...props}
- />
-));
-SelectLabel.displayName = SelectPrimitive.Label.displayName;
-
-const SelectItem = React.forwardRef<
- React.ElementRef<typeof SelectPrimitive.Item>,
- React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
->(({ className, children, ...props }, ref) => (
- <SelectPrimitive.Item
- ref={ref}
- className={cn(
- "focus:bg-accent focus:text-accent-foreground relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
- className,
- )}
- {...props}
- >
- <span className="absolute left-2 flex size-3.5 items-center justify-center">
- <SelectPrimitive.ItemIndicator>
- <Check className="size-4" />
- </SelectPrimitive.ItemIndicator>
- </span>
-
- <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
- </SelectPrimitive.Item>
-));
-SelectItem.displayName = SelectPrimitive.Item.displayName;
-
-const SelectSeparator = React.forwardRef<
- React.ElementRef<typeof SelectPrimitive.Separator>,
- React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
->(({ className, ...props }, ref) => (
- <SelectPrimitive.Separator
- ref={ref}
- className={cn("bg-muted -mx-1 my-1 h-px", className)}
- {...props}
- />
-));
-SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
-
-export {
- Select,
- SelectGroup,
- SelectValue,
- SelectTrigger,
- SelectContent,
- SelectLabel,
- SelectItem,
- SelectSeparator,
- SelectScrollUpButton,
- SelectScrollDownButton,
-};
diff --git a/packages/web/components/ui/separator.tsx b/packages/web/components/ui/separator.tsx
deleted file mode 100644
index 3b9f2b84..00000000
--- a/packages/web/components/ui/separator.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-"use client";
-
-import * as React from "react";
-import * as SeparatorPrimitive from "@radix-ui/react-separator";
-
-import { cn } from "@/lib/utils";
-
-const Separator = React.forwardRef<
- React.ElementRef<typeof SeparatorPrimitive.Root>,
- React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
->(
- (
- { className, orientation = "horizontal", decorative = true, ...props },
- ref,
- ) => (
- <SeparatorPrimitive.Root
- ref={ref}
- decorative={decorative}
- orientation={orientation}
- className={cn(
- "bg-border shrink-0",
- orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
- className,
- )}
- {...props}
- />
- ),
-);
-Separator.displayName = SeparatorPrimitive.Root.displayName;
-
-export { Separator };
diff --git a/packages/web/components/ui/skeleton.tsx b/packages/web/components/ui/skeleton.tsx
deleted file mode 100644
index 5fab2023..00000000
--- a/packages/web/components/ui/skeleton.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import { cn } from "@/lib/utils";
-
-function Skeleton({
- className,
- ...props
-}: React.HTMLAttributes<HTMLDivElement>) {
- return (
- <div
- className={cn("bg-muted animate-pulse rounded-md", className)}
- {...props}
- />
- );
-}
-
-export { Skeleton };
diff --git a/packages/web/components/ui/spinner.tsx b/packages/web/components/ui/spinner.tsx
deleted file mode 100644
index adcd2807..00000000
--- a/packages/web/components/ui/spinner.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import { cn } from "@/lib/utils";
-
-export default function LoadingSpinner({ className }: { className?: string }) {
- return (
- <svg
- xmlns="http://www.w3.org/2000/svg"
- width="24"
- height="24"
- viewBox="0 0 24 24"
- fill="none"
- stroke="currentColor"
- strokeWidth="2"
- strokeLinecap="round"
- strokeLinejoin="round"
- className={cn("animate-spin", className)}
- >
- <path d="M21 12a9 9 0 1 1-6.219-8.56" />
- </svg>
- );
-}
diff --git a/packages/web/components/ui/table.tsx b/packages/web/components/ui/table.tsx
deleted file mode 100644
index 0fa9288e..00000000
--- a/packages/web/components/ui/table.tsx
+++ /dev/null
@@ -1,117 +0,0 @@
-import * as React from "react";
-
-import { cn } from "@/lib/utils";
-
-const Table = React.forwardRef<
- HTMLTableElement,
- React.HTMLAttributes<HTMLTableElement>
->(({ className, ...props }, ref) => (
- <div className="relative w-full overflow-auto">
- <table
- ref={ref}
- className={cn("w-full caption-bottom text-sm", className)}
- {...props}
- />
- </div>
-));
-Table.displayName = "Table";
-
-const TableHeader = React.forwardRef<
- HTMLTableSectionElement,
- React.HTMLAttributes<HTMLTableSectionElement>
->(({ className, ...props }, ref) => (
- <thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
-));
-TableHeader.displayName = "TableHeader";
-
-const TableBody = React.forwardRef<
- HTMLTableSectionElement,
- React.HTMLAttributes<HTMLTableSectionElement>
->(({ className, ...props }, ref) => (
- <tbody
- ref={ref}
- className={cn("[&_tr:last-child]:border-0", className)}
- {...props}
- />
-));
-TableBody.displayName = "TableBody";
-
-const TableFooter = React.forwardRef<
- HTMLTableSectionElement,
- React.HTMLAttributes<HTMLTableSectionElement>
->(({ className, ...props }, ref) => (
- <tfoot
- ref={ref}
- className={cn(
- "bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
- className,
- )}
- {...props}
- />
-));
-TableFooter.displayName = "TableFooter";
-
-const TableRow = React.forwardRef<
- HTMLTableRowElement,
- React.HTMLAttributes<HTMLTableRowElement>
->(({ className, ...props }, ref) => (
- <tr
- ref={ref}
- className={cn(
- "hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
- className,
- )}
- {...props}
- />
-));
-TableRow.displayName = "TableRow";
-
-const TableHead = React.forwardRef<
- HTMLTableCellElement,
- React.ThHTMLAttributes<HTMLTableCellElement>
->(({ className, ...props }, ref) => (
- <th
- ref={ref}
- className={cn(
- "text-muted-foreground h-12 px-4 text-left align-middle font-medium [&:has([role=checkbox])]:pr-0",
- className,
- )}
- {...props}
- />
-));
-TableHead.displayName = "TableHead";
-
-const TableCell = React.forwardRef<
- HTMLTableCellElement,
- React.TdHTMLAttributes<HTMLTableCellElement>
->(({ className, ...props }, ref) => (
- <td
- ref={ref}
- className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
- {...props}
- />
-));
-TableCell.displayName = "TableCell";
-
-const TableCaption = React.forwardRef<
- HTMLTableCaptionElement,
- React.HTMLAttributes<HTMLTableCaptionElement>
->(({ className, ...props }, ref) => (
- <caption
- ref={ref}
- className={cn("text-muted-foreground mt-4 text-sm", className)}
- {...props}
- />
-));
-TableCaption.displayName = "TableCaption";
-
-export {
- Table,
- TableHeader,
- TableBody,
- TableFooter,
- TableHead,
- TableRow,
- TableCell,
- TableCaption,
-};
diff --git a/packages/web/components/ui/tabs.tsx b/packages/web/components/ui/tabs.tsx
deleted file mode 100644
index 990017db..00000000
--- a/packages/web/components/ui/tabs.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-"use client";
-
-import * as React from "react";
-import * as TabsPrimitive from "@radix-ui/react-tabs";
-
-import { cn } from "@/lib/utils";
-
-const Tabs = TabsPrimitive.Root;
-
-const TabsList = React.forwardRef<
- React.ElementRef<typeof TabsPrimitive.List>,
- React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
->(({ className, ...props }, ref) => (
- <TabsPrimitive.List
- ref={ref}
- className={cn(
- "bg-muted text-muted-foreground inline-flex h-10 items-center justify-center rounded-md p-1",
- className,
- )}
- {...props}
- />
-));
-TabsList.displayName = TabsPrimitive.List.displayName;
-
-const TabsTrigger = React.forwardRef<
- React.ElementRef<typeof TabsPrimitive.Trigger>,
- React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
->(({ className, ...props }, ref) => (
- <TabsPrimitive.Trigger
- ref={ref}
- className={cn(
- "ring-offset-background focus-visible:ring-ring data-[state=active]:bg-background data-[state=active]:text-foreground inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm",
- className,
- )}
- {...props}
- />
-));
-TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
-
-const TabsContent = React.forwardRef<
- React.ElementRef<typeof TabsPrimitive.Content>,
- React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
->(({ className, ...props }, ref) => (
- <TabsPrimitive.Content
- ref={ref}
- className={cn(
- "ring-offset-background focus-visible:ring-ring mt-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2",
- className,
- )}
- {...props}
- />
-));
-TabsContent.displayName = TabsPrimitive.Content.displayName;
-
-export { Tabs, TabsList, TabsTrigger, TabsContent };
diff --git a/packages/web/components/ui/textarea.tsx b/packages/web/components/ui/textarea.tsx
deleted file mode 100644
index a0de3371..00000000
--- a/packages/web/components/ui/textarea.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import * as React from "react";
-
-import { cn } from "@/lib/utils";
-
-export interface TextareaProps
- extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
-
-const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
- ({ className, ...props }, ref) => {
- return (
- <textarea
- className={cn(
- "border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex min-h-[80px] w-full rounded-md border px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
- className,
- )}
- ref={ref}
- {...props}
- />
- );
- },
-);
-Textarea.displayName = "Textarea";
-
-export { Textarea };
diff --git a/packages/web/components/ui/toast.tsx b/packages/web/components/ui/toast.tsx
deleted file mode 100644
index 0d162dca..00000000
--- a/packages/web/components/ui/toast.tsx
+++ /dev/null
@@ -1,127 +0,0 @@
-import * as React from "react";
-import * as ToastPrimitives from "@radix-ui/react-toast";
-import { cva, type VariantProps } from "class-variance-authority";
-import { X } from "lucide-react";
-
-import { cn } from "@/lib/utils";
-
-const ToastProvider = ToastPrimitives.Provider;
-
-const ToastViewport = React.forwardRef<
- React.ElementRef<typeof ToastPrimitives.Viewport>,
- React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
->(({ className, ...props }, ref) => (
- <ToastPrimitives.Viewport
- ref={ref}
- className={cn(
- "fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
- className,
- )}
- {...props}
- />
-));
-ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
-
-const toastVariants = cva(
- "data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none",
- {
- variants: {
- variant: {
- default: "bg-background text-foreground border",
- destructive:
- "destructive border-destructive bg-destructive text-destructive-foreground group",
- },
- },
- defaultVariants: {
- variant: "default",
- },
- },
-);
-
-const Toast = React.forwardRef<
- React.ElementRef<typeof ToastPrimitives.Root>,
- React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
- VariantProps<typeof toastVariants>
->(({ className, variant, ...props }, ref) => {
- return (
- <ToastPrimitives.Root
- ref={ref}
- className={cn(toastVariants({ variant }), className)}
- {...props}
- />
- );
-});
-Toast.displayName = ToastPrimitives.Root.displayName;
-
-const ToastAction = React.forwardRef<
- React.ElementRef<typeof ToastPrimitives.Action>,
- React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
->(({ className, ...props }, ref) => (
- <ToastPrimitives.Action
- ref={ref}
- className={cn(
- "ring-offset-background hover:bg-secondary focus:ring-ring group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
- className,
- )}
- {...props}
- />
-));
-ToastAction.displayName = ToastPrimitives.Action.displayName;
-
-const ToastClose = React.forwardRef<
- React.ElementRef<typeof ToastPrimitives.Close>,
- React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
->(({ className, ...props }, ref) => (
- <ToastPrimitives.Close
- ref={ref}
- className={cn(
- "text-foreground/50 hover:text-foreground absolute right-2 top-2 rounded-md p-1 opacity-0 transition-opacity focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
- className,
- )}
- toast-close=""
- {...props}
- >
- <X className="size-4" />
- </ToastPrimitives.Close>
-));
-ToastClose.displayName = ToastPrimitives.Close.displayName;
-
-const ToastTitle = React.forwardRef<
- React.ElementRef<typeof ToastPrimitives.Title>,
- React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
->(({ className, ...props }, ref) => (
- <ToastPrimitives.Title
- ref={ref}
- className={cn("text-sm font-semibold", className)}
- {...props}
- />
-));
-ToastTitle.displayName = ToastPrimitives.Title.displayName;
-
-const ToastDescription = React.forwardRef<
- React.ElementRef<typeof ToastPrimitives.Description>,
- React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
->(({ className, ...props }, ref) => (
- <ToastPrimitives.Description
- ref={ref}
- className={cn("text-sm opacity-90", className)}
- {...props}
- />
-));
-ToastDescription.displayName = ToastPrimitives.Description.displayName;
-
-type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
-
-type ToastActionElement = React.ReactElement<typeof ToastAction>;
-
-export {
- type ToastProps,
- type ToastActionElement,
- ToastProvider,
- ToastViewport,
- Toast,
- ToastTitle,
- ToastDescription,
- ToastClose,
- ToastAction,
-};
diff --git a/packages/web/components/ui/toaster.tsx b/packages/web/components/ui/toaster.tsx
deleted file mode 100644
index 7d82ed55..00000000
--- a/packages/web/components/ui/toaster.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-"use client";
-
-import {
- Toast,
- ToastClose,
- ToastDescription,
- ToastProvider,
- ToastTitle,
- ToastViewport,
-} from "@/components/ui/toast";
-import { useToast } from "@/components/ui/use-toast";
-
-export function Toaster() {
- const { toasts } = useToast();
-
- return (
- <ToastProvider>
- {toasts.map(function ({ id, title, description, action, ...props }) {
- return (
- <Toast key={id} {...props}>
- <div className="grid gap-1">
- {title && <ToastTitle>{title}</ToastTitle>}
- {description && (
- <ToastDescription>{description}</ToastDescription>
- )}
- </div>
- {action}
- <ToastClose />
- </Toast>
- );
- })}
- <ToastViewport />
- </ToastProvider>
- );
-}
diff --git a/packages/web/components/ui/use-toast.ts b/packages/web/components/ui/use-toast.ts
deleted file mode 100644
index 5491e140..00000000
--- a/packages/web/components/ui/use-toast.ts
+++ /dev/null
@@ -1,189 +0,0 @@
-// Inspired by react-hot-toast library
-import * as React from "react";
-
-import type { ToastActionElement, ToastProps } from "@/components/ui/toast";
-
-const TOAST_LIMIT = 1;
-const TOAST_REMOVE_DELAY = 1000000;
-
-type ToasterToast = ToastProps & {
- id: string;
- title?: React.ReactNode;
- description?: React.ReactNode;
- action?: ToastActionElement;
-};
-
-const actionTypes = {
- ADD_TOAST: "ADD_TOAST",
- UPDATE_TOAST: "UPDATE_TOAST",
- DISMISS_TOAST: "DISMISS_TOAST",
- REMOVE_TOAST: "REMOVE_TOAST",
-} as const;
-
-let count = 0;
-
-function genId() {
- count = (count + 1) % Number.MAX_SAFE_INTEGER;
- return count.toString();
-}
-
-type ActionType = typeof actionTypes;
-
-type Action =
- | {
- type: ActionType["ADD_TOAST"];
- toast: ToasterToast;
- }
- | {
- type: ActionType["UPDATE_TOAST"];
- toast: Partial<ToasterToast>;
- }
- | {
- type: ActionType["DISMISS_TOAST"];
- toastId?: ToasterToast["id"];
- }
- | {
- type: ActionType["REMOVE_TOAST"];
- toastId?: ToasterToast["id"];
- };
-
-interface State {
- toasts: ToasterToast[];
-}
-
-const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
-
-const addToRemoveQueue = (toastId: string) => {
- if (toastTimeouts.has(toastId)) {
- return;
- }
-
- const timeout = setTimeout(() => {
- toastTimeouts.delete(toastId);
- dispatch({
- type: "REMOVE_TOAST",
- toastId: toastId,
- });
- }, TOAST_REMOVE_DELAY);
-
- toastTimeouts.set(toastId, timeout);
-};
-
-export const reducer = (state: State, action: Action): State => {
- switch (action.type) {
- case "ADD_TOAST":
- return {
- ...state,
- toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
- };
-
- case "UPDATE_TOAST":
- return {
- ...state,
- toasts: state.toasts.map((t) =>
- t.id === action.toast.id ? { ...t, ...action.toast } : t,
- ),
- };
-
- case "DISMISS_TOAST": {
- const { toastId } = action;
-
- // ! Side effects ! - This could be extracted into a dismissToast() action,
- // but I'll keep it here for simplicity
- if (toastId) {
- addToRemoveQueue(toastId);
- } else {
- state.toasts.forEach((toast) => {
- addToRemoveQueue(toast.id);
- });
- }
-
- return {
- ...state,
- toasts: state.toasts.map((t) =>
- t.id === toastId || toastId === undefined
- ? {
- ...t,
- open: false,
- }
- : t,
- ),
- };
- }
- case "REMOVE_TOAST":
- if (action.toastId === undefined) {
- return {
- ...state,
- toasts: [],
- };
- }
- return {
- ...state,
- toasts: state.toasts.filter((t) => t.id !== action.toastId),
- };
- }
-};
-
-const listeners: Array<(_state: State) => void> = [];
-
-let memoryState: State = { toasts: [] };
-
-function dispatch(action: Action) {
- memoryState = reducer(memoryState, action);
- listeners.forEach((listener) => {
- listener(memoryState);
- });
-}
-
-type Toast = Omit<ToasterToast, "id">;
-
-function toast({ ...props }: Toast) {
- const id = genId();
-
- const update = (props: ToasterToast) =>
- dispatch({
- type: "UPDATE_TOAST",
- toast: { ...props, id },
- });
- const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
-
- dispatch({
- type: "ADD_TOAST",
- toast: {
- ...props,
- id,
- open: true,
- onOpenChange: (open) => {
- if (!open) dismiss();
- },
- },
- });
-
- return {
- id: id,
- dismiss,
- update,
- };
-}
-
-function useToast() {
- const [state, setState] = React.useState<State>(memoryState);
-
- React.useEffect(() => {
- listeners.push(setState);
- return () => {
- const index = listeners.indexOf(setState);
- if (index > -1) {
- listeners.splice(index, 1);
- }
- };
- }, [state]);
-
- return {
- ...state,
- toast,
- dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
- };
-}
-
-export { useToast, toast };
diff --git a/packages/web/lib/bookmarkUtils.tsx b/packages/web/lib/bookmarkUtils.tsx
deleted file mode 100644
index a2828c29..00000000
--- a/packages/web/lib/bookmarkUtils.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import { ZBookmark } from "@hoarder/trpc/types/bookmarks";
-
-const MAX_LOADING_MSEC = 30 * 1000;
-
-export function isBookmarkStillCrawling(bookmark: ZBookmark) {
- return (
- bookmark.content.type == "link" &&
- !bookmark.content.crawledAt &&
- Date.now().valueOf() - bookmark.createdAt.valueOf() < MAX_LOADING_MSEC
- );
-}
-
-export function isBookmarkStillTagging(bookmark: ZBookmark) {
- return (
- bookmark.taggingStatus == "pending" &&
- Date.now().valueOf() - bookmark.createdAt.valueOf() < MAX_LOADING_MSEC
- );
-}
-
-export function isBookmarkStillLoading(bookmark: ZBookmark) {
- return isBookmarkStillTagging(bookmark) || isBookmarkStillCrawling(bookmark);
-}
diff --git a/packages/web/lib/hooks/bookmark-search.ts b/packages/web/lib/hooks/bookmark-search.ts
deleted file mode 100644
index 738e1bd8..00000000
--- a/packages/web/lib/hooks/bookmark-search.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-import { useEffect, useState } from "react";
-import { api } from "@/lib/trpc";
-import { useRouter, useSearchParams } from "next/navigation";
-import { keepPreviousData } from "@tanstack/react-query";
-
-function useSearchQuery() {
- const searchParams = useSearchParams();
- const searchQuery = searchParams.get("q") || "";
- return { searchQuery };
-}
-
-export function useDoBookmarkSearch() {
- const router = useRouter();
- const { searchQuery } = useSearchQuery();
- const [timeoutId, setTimeoutId] = useState<NodeJS.Timeout | undefined>();
-
- useEffect(() => {
- return () => {
- if (!timeoutId) {
- return;
- }
- clearTimeout(timeoutId);
- };
- }, [timeoutId]);
-
- const doSearch = (val: string) => {
- setTimeoutId(undefined);
- router.replace(`/dashboard/search?q=${val}`);
- };
-
- const debounceSearch = (val: string) => {
- if (timeoutId) {
- clearTimeout(timeoutId);
- }
- const id = setTimeout(() => {
- doSearch(val);
- }, 200);
- setTimeoutId(id);
- };
-
- return {
- doSearch,
- debounceSearch,
- searchQuery,
- };
-}
-
-export function useBookmarkSearch() {
- const { searchQuery } = useSearchQuery();
-
- const { data, isPending, isPlaceholderData, error } =
- api.bookmarks.searchBookmarks.useQuery(
- {
- text: searchQuery,
- },
- {
- placeholderData: keepPreviousData,
- gcTime: 0,
- },
- );
-
- if (error) {
- throw error;
- }
-
- return {
- searchQuery,
- error,
- data,
- isPending,
- isPlaceholderData,
- };
-}
diff --git a/packages/web/lib/providers.tsx b/packages/web/lib/providers.tsx
deleted file mode 100644
index 5c4649b5..00000000
--- a/packages/web/lib/providers.tsx
+++ /dev/null
@@ -1,75 +0,0 @@
-"use client";
-
-import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
-import React, { useState } from "react";
-import { api } from "./trpc";
-import { loggerLink } from "@trpc/client";
-import { httpBatchLink } from "@trpc/client";
-import superjson from "superjson";
-import { SessionProvider } from "next-auth/react";
-import { Session } from "next-auth";
-
-function makeQueryClient() {
- return new QueryClient({
- defaultOptions: {
- queries: {
- // With SSR, we usually want to set some default staleTime
- // above 0 to avoid refetching immediately on the client
- staleTime: 60 * 1000,
- },
- },
- });
-}
-
-let browserQueryClient: QueryClient | undefined = undefined;
-
-function getQueryClient() {
- if (typeof window === "undefined") {
- // Server: always make a new query client
- return makeQueryClient();
- } else {
- // Browser: make a new query client if we don't already have one
- // This is very important so we don't re-make a new client if React
- // supsends during the initial render. This may not be needed if we
- // have a suspense boundary BELOW the creation of the query client
- if (!browserQueryClient) browserQueryClient = makeQueryClient();
- return browserQueryClient;
- }
-}
-
-export default function Providers({
- children,
- session,
-}: {
- children: React.ReactNode;
- session: Session | null;
-}) {
- const queryClient = getQueryClient();
-
- const [trpcClient] = useState(() =>
- api.createClient({
- links: [
- loggerLink({
- enabled: (op) =>
- process.env.NODE_ENV === "development" ||
- (op.direction === "down" && op.result instanceof Error),
- }),
- httpBatchLink({
- // TODO: Change this to be a full URL exposed as a client side setting
- url: `/api/trpc`,
- transformer: superjson,
- }),
- ],
- }),
- );
-
- return (
- <SessionProvider session={session}>
- <api.Provider client={trpcClient} queryClient={queryClient}>
- <QueryClientProvider client={queryClient}>
- {children}
- </QueryClientProvider>
- </api.Provider>
- </SessionProvider>
- );
-}
diff --git a/packages/web/lib/trpc.tsx b/packages/web/lib/trpc.tsx
deleted file mode 100644
index 79a2a9fe..00000000
--- a/packages/web/lib/trpc.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-"use client";
-import type { AppRouter } from "@hoarder/trpc/routers/_app";
-import { createTRPCReact } from "@trpc/react-query";
-
-export const api = createTRPCReact<AppRouter>();
diff --git a/packages/web/lib/utils.ts b/packages/web/lib/utils.ts
deleted file mode 100644
index 365058ce..00000000
--- a/packages/web/lib/utils.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { type ClassValue, clsx } from "clsx";
-import { twMerge } from "tailwind-merge";
-
-export function cn(...inputs: ClassValue[]) {
- return twMerge(clsx(inputs));
-}
diff --git a/packages/web/next.config.mjs b/packages/web/next.config.mjs
deleted file mode 100644
index bda43a58..00000000
--- a/packages/web/next.config.mjs
+++ /dev/null
@@ -1,43 +0,0 @@
-import pwa from "next-pwa";
-
-const withPWA = pwa({
- dest: "public",
- disable: process.env.NODE_ENV != "production",
-});
-
-/** @type {import('next').NextConfig} */
-const nextConfig = withPWA({
- output: "standalone",
- async headers() {
- return [
- {
- // Routes this applies to
- source: "/api/(.*)",
- // Headers
- headers: [
- // Allow for specific domains to have access or * for all
- {
- key: "Access-Control-Allow-Origin",
- value: "chrome-extension://olmdabfolepgfmjhmikngmfekcdgjinp",
- },
- // Allows for specific methods accepted
- {
- key: "Access-Control-Allow-Methods",
- value: "GET, POST, PUT, DELETE, OPTIONS",
- },
- // Allows for specific headers accepted (These are a few standard ones)
- {
- key: "Access-Control-Allow-Headers",
- value: "Content-Type, Authorization",
- },
- {
- key: "Access-Control-Allow-Credentials",
- value: "true",
- },
- ],
- },
- ];
- },
-});
-
-export default nextConfig;
diff --git a/packages/web/package.json b/packages/web/package.json
deleted file mode 100644
index e0c9d407..00000000
--- a/packages/web/package.json
+++ /dev/null
@@ -1,74 +0,0 @@
-{
- "$schema": "https://json.schemastore.org/package.json",
- "name": "@hoarder/web",
- "version": "0.1.0",
- "private": true,
- "scripts": {
- "dev": "next dev",
- "build": "next build",
- "start": "next start",
- "lint": "next lint",
- "test": "vitest",
- "typecheck": "tsc --noEmit"
- },
- "dependencies": {
- "@auth/drizzle-adapter": "^0.8.0",
- "@emoji-mart/data": "^1.1.2",
- "@emoji-mart/react": "^1.1.1",
- "@hoarder/db": "0.1.0",
- "@hoarder/shared": "0.1.0",
- "@hoarder/trpc": "0.1.0",
- "@hookform/resolvers": "^3.3.4",
- "@next/eslint-plugin-next": "^14.1.1",
- "@radix-ui/react-dialog": "^1.0.5",
- "@radix-ui/react-dropdown-menu": "^2.0.6",
- "@radix-ui/react-label": "^2.0.2",
- "@radix-ui/react-popover": "^1.0.7",
- "@radix-ui/react-scroll-area": "^1.0.5",
- "@radix-ui/react-select": "^2.0.0",
- "@radix-ui/react-separator": "^1.0.3",
- "@radix-ui/react-slot": "^1.0.2",
- "@radix-ui/react-tabs": "^1.0.4",
- "@radix-ui/react-toast": "^1.1.5",
- "@tanstack/react-query": "^5.24.6",
- "@tanstack/react-query-devtools": "^5.21.0",
- "@trpc/client": "11.0.0-next-beta.304",
- "@trpc/next": "11.0.0-next-beta.304",
- "@trpc/react-query": "^11.0.0-next-beta.304",
- "@trpc/server": "11.0.0-next-beta.304",
- "better-sqlite3": "^9.4.3",
- "class-variance-authority": "^0.7.0",
- "clsx": "^2.1.0",
- "drizzle-orm": "^0.29.4",
- "install": "^0.13.0",
- "lucide-react": "^0.322.0",
- "meilisearch": "^0.37.0",
- "next": "14.1.1",
- "next-auth": "^4.24.5",
- "next-pwa": "^5.6.0",
- "prettier": "^3.2.5",
- "react": "^18",
- "react-dom": "^18",
- "react-hook-form": "^7.50.1",
- "react-markdown": "^9.0.1",
- "react-masonry-css": "^1.0.16",
- "server-only": "^0.0.1",
- "superjson": "^2.2.1",
- "tailwind-merge": "^2.2.1",
- "tailwindcss-animate": "^1.0.7",
- "zod": "^3.22.4",
- "zustand": "^4.5.1"
- },
- "devDependencies": {
- "@tailwindcss/typography": "^0.5.10",
- "@types/emoji-mart": "^3.0.14",
- "@types/react": "^18",
- "@types/react-dom": "^18",
- "autoprefixer": "^10.0.1",
- "postcss": "^8",
- "tailwindcss": "^3.3.0",
- "ts-node": "^10.9.2",
- "vite-tsconfig-paths": "^4.3.1",
- "vitest": "^1.3.1"
- }
-}
diff --git a/packages/web/postcss.config.js b/packages/web/postcss.config.js
deleted file mode 100644
index 12a703d9..00000000
--- a/packages/web/postcss.config.js
+++ /dev/null
@@ -1,6 +0,0 @@
-module.exports = {
- plugins: {
- tailwindcss: {},
- autoprefixer: {},
- },
-};
diff --git a/packages/web/public/blur.avif b/packages/web/public/blur.avif
deleted file mode 100644
index cbc6cd37..00000000
--- a/packages/web/public/blur.avif
+++ /dev/null
Binary files differ
diff --git a/packages/web/public/icons/logo-128.png b/packages/web/public/icons/logo-128.png
deleted file mode 100644
index 71ead90c..00000000
--- a/packages/web/public/icons/logo-128.png
+++ /dev/null
Binary files differ
diff --git a/packages/web/public/icons/logo-16.png b/packages/web/public/icons/logo-16.png
deleted file mode 100644
index dd864d44..00000000
--- a/packages/web/public/icons/logo-16.png
+++ /dev/null
Binary files differ
diff --git a/packages/web/public/icons/logo-48.png b/packages/web/public/icons/logo-48.png
deleted file mode 100644
index 7ba1cd49..00000000
--- a/packages/web/public/icons/logo-48.png
+++ /dev/null
Binary files differ
diff --git a/packages/web/public/manifest.json b/packages/web/public/manifest.json
deleted file mode 100644
index b42343f6..00000000
--- a/packages/web/public/manifest.json
+++ /dev/null
@@ -1,25 +0,0 @@
-{
- "name": "Hoarder",
- "short_name": "Hoarder",
- "icons": [
- {
- "src": "/icons/logo-16.png",
- "sizes": "16x16",
- "type": "image/png",
- "purpose": "any maskable"
- },
- {
- "src": "/icons/logo-48.png",
- "sizes": "48x48",
- "type": "image/png"
- },
- {
- "src": "/icons/logo-128.png",
- "sizes": "128x128",
- "type": "image/png"
- }
- ],
- "start_url": "/",
- "display": "standalone",
- "orientation": "portrait"
-}
diff --git a/packages/web/server/api/client.ts b/packages/web/server/api/client.ts
deleted file mode 100644
index 88ea7a0e..00000000
--- a/packages/web/server/api/client.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { appRouter } from "@hoarder/trpc/routers/_app";
-import { getServerAuthSession } from "@/server/auth";
-import { Context, createCallerFactory } from "@hoarder/trpc";
-import { db } from "@hoarder/db";
-
-export const createContext = async (database?: typeof db): Promise<Context> => {
- const session = await getServerAuthSession();
- return {
- user: session?.user ?? null,
- db: database ?? db,
- };
-};
-
-const createCaller = createCallerFactory(appRouter);
-
-export const api = createCaller(createContext);
diff --git a/packages/web/server/auth.ts b/packages/web/server/auth.ts
deleted file mode 100644
index 950443b9..00000000
--- a/packages/web/server/auth.ts
+++ /dev/null
@@ -1,96 +0,0 @@
-import NextAuth, { NextAuthOptions, getServerSession } from "next-auth";
-import type { Adapter } from "next-auth/adapters";
-import AuthentikProvider from "next-auth/providers/authentik";
-import serverConfig from "@hoarder/shared/config";
-import { validatePassword } from "@hoarder/trpc/auth";
-import { db } from "@hoarder/db";
-import { DefaultSession } from "next-auth";
-import CredentialsProvider from "next-auth/providers/credentials";
-import { DrizzleAdapter } from "@auth/drizzle-adapter";
-
-import { Provider } from "next-auth/providers/index";
-
-declare module "next-auth/jwt" {
- export interface JWT {
- user: {
- id: string;
- role: "admin" | "user";
- } & DefaultSession["user"];
- }
-}
-
-declare module "next-auth" {
- /**
- * Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context
- */
- export interface Session {
- user: {
- id: string;
- role: "admin" | "user";
- } & DefaultSession["user"];
- }
-
- export interface DefaultUser {
- role: "admin" | "user" | null;
- }
-}
-
-const providers: Provider[] = [
- CredentialsProvider({
- // The name to display on the sign in form (e.g. "Sign in with...")
- name: "Credentials",
- credentials: {
- email: { label: "Email", type: "email", placeholder: "Email" },
- password: { label: "Password", type: "password" },
- },
- async authorize(credentials) {
- if (!credentials) {
- return null;
- }
-
- try {
- return await validatePassword(
- credentials?.email,
- credentials?.password,
- );
- } catch (e) {
- return null;
- }
- },
- }),
-];
-
-if (serverConfig.auth.authentik) {
- providers.push(AuthentikProvider(serverConfig.auth.authentik));
-}
-
-export const authOptions: NextAuthOptions = {
- // https://github.com/nextauthjs/next-auth/issues/9493
- adapter: DrizzleAdapter(db) as Adapter,
- providers: providers,
- session: {
- strategy: "jwt",
- },
- callbacks: {
- async jwt({ token, user }) {
- if (user) {
- token.user = {
- id: user.id,
- name: user.name,
- email: user.email,
- image: user.image,
- role: user.role || "user",
- };
- }
- return token;
- },
- async session({ session, token }) {
- session.user = { ...token.user };
- return session;
- },
- },
-};
-
-export const authHandler = NextAuth(authOptions);
-
-export const getServerAuthSession = () => getServerSession(authOptions);
diff --git a/packages/web/tailwind.config.ts b/packages/web/tailwind.config.ts
deleted file mode 100644
index 521ba51c..00000000
--- a/packages/web/tailwind.config.ts
+++ /dev/null
@@ -1,89 +0,0 @@
-import type { Config } from "tailwindcss";
-
-const config = {
- darkMode: ["class"],
- content: [
- "./pages/**/*.{ts,tsx}",
- "./components/**/*.{ts,tsx}",
- "./app/**/*.{ts,tsx}",
- "./src/**/*.{ts,tsx}",
- ],
- prefix: "",
- theme: {
- container: {
- center: true,
- padding: "2rem",
- screens: {
- "2xl": "1400px",
- },
- },
- extend: {
- colors: {
- border: "hsl(var(--border))",
- input: "hsl(var(--input))",
- ring: "hsl(var(--ring))",
- background: "hsl(var(--background))",
- foreground: "hsl(var(--foreground))",
- primary: {
- DEFAULT: "hsl(var(--primary))",
- foreground: "hsl(var(--primary-foreground))",
- },
- secondary: {
- DEFAULT: "hsl(var(--secondary))",
- foreground: "hsl(var(--secondary-foreground))",
- },
- destructive: {
- DEFAULT: "hsl(var(--destructive))",
- foreground: "hsl(var(--destructive-foreground))",
- },
- muted: {
- DEFAULT: "hsl(var(--muted))",
- foreground: "hsl(var(--muted-foreground))",
- },
- accent: {
- DEFAULT: "hsl(var(--accent))",
- foreground: "hsl(var(--accent-foreground))",
- },
- popover: {
- DEFAULT: "hsl(var(--popover))",
- foreground: "hsl(var(--popover-foreground))",
- },
- card: {
- DEFAULT: "hsl(var(--card))",
- foreground: "hsl(var(--card-foreground))",
- },
- },
- borderRadius: {
- lg: "var(--radius)",
- md: "calc(var(--radius) - 2px)",
- sm: "calc(var(--radius) - 4px)",
- },
- keyframes: {
- "accordion-down": {
- from: { height: "0" },
- to: { height: "var(--radix-accordion-content-height)" },
- },
- "accordion-up": {
- from: { height: "var(--radix-accordion-content-height)" },
- to: { height: "0" },
- },
- "pulse-border": {
- "0%, 100%": {
- "box-shadow": "0 0 0 0 gray",
- },
- "50%": {
- "box-shadow": "0 0 0 2px gray",
- },
- },
- },
- animation: {
- "accordion-down": "accordion-down 0.2s ease-out",
- "accordion-up": "accordion-up 0.2s ease-out",
- "pulse-border": "pulse-border 1s ease-in-out infinite",
- },
- },
- },
- plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography")],
-} satisfies Config;
-
-export default config;
diff --git a/packages/web/tsconfig.json b/packages/web/tsconfig.json
deleted file mode 100644
index ecbd5643..00000000
--- a/packages/web/tsconfig.json
+++ /dev/null
@@ -1,28 +0,0 @@
-{
- "$schema": "https://json.schemastore.org/tsconfig",
- "compilerOptions": {
- "lib": ["dom", "dom.iterable", "esnext"],
- "allowJs": true,
- "skipLibCheck": true,
- "strict": true,
- "noEmit": true,
- "esModuleInterop": true,
- "module": "esnext",
- "moduleResolution": "bundler",
- "resolveJsonModule": true,
- "isolatedModules": true,
- "jsx": "preserve",
- "incremental": true,
- "target": "ES6",
- "plugins": [
- {
- "name": "next"
- }
- ],
- "paths": {
- "@/*": ["./*"]
- }
- },
- "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
- "exclude": ["node_modules"]
-}
diff --git a/packages/web/vitest.config.ts b/packages/web/vitest.config.ts
deleted file mode 100644
index c3d02f71..00000000
--- a/packages/web/vitest.config.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-/// <reference types="vitest" />
-
-import { defineConfig } from "vitest/config";
-import tsconfigPaths from "vite-tsconfig-paths";
-
-// https://vitejs.dev/config/
-export default defineConfig({
- plugins: [tsconfigPaths()],
- test: {
- alias: {
- "@/*": "./*",
- },
- },
-});
diff --git a/packages/workers/crawler.ts b/packages/workers/crawler.ts
deleted file mode 100644
index 5db2da7b..00000000
--- a/packages/workers/crawler.ts
+++ /dev/null
@@ -1,201 +0,0 @@
-import logger from "@hoarder/shared/logger";
-import {
- LinkCrawlerQueue,
- OpenAIQueue,
- SearchIndexingQueue,
- ZCrawlLinkRequest,
- queueConnectionDetails,
- zCrawlLinkRequestSchema,
-} from "@hoarder/shared/queues";
-import DOMPurify from "dompurify";
-import { JSDOM } from "jsdom";
-
-import { Worker } from "bullmq";
-import { Job } from "bullmq";
-
-import { db } from "@hoarder/db";
-
-import { Browser } from "puppeteer";
-import puppeteer from "puppeteer-extra";
-import StealthPlugin from "puppeteer-extra-plugin-stealth";
-import AdblockerPlugin from "puppeteer-extra-plugin-adblocker";
-
-import metascraper from "metascraper";
-
-import metascraperDescription from "metascraper-description";
-import metascraperImage from "metascraper-image";
-import metascraperLogo from "metascraper-logo-favicon";
-import metascraperTitle from "metascraper-title";
-import metascraperUrl from "metascraper-url";
-import metascraperTwitter from "metascraper-twitter";
-import metascraperReadability from "metascraper-readability";
-import { Mutex } from "async-mutex";
-import assert from "assert";
-import serverConfig from "@hoarder/shared/config";
-import { bookmarkLinks } from "@hoarder/db/schema";
-import { eq } from "drizzle-orm";
-import { Readability } from "@mozilla/readability";
-
-const metascraperParser = metascraper([
- metascraperReadability(),
- metascraperTitle(),
- metascraperDescription(),
- metascraperTwitter(),
- metascraperImage(),
- metascraperLogo(),
- metascraperUrl(),
-]);
-
-let browser: Browser | undefined;
-// Guards the interactions with the browser instance.
-// This is needed given that most of the browser APIs are async.
-const browserMutex = new Mutex();
-
-async function launchBrowser() {
- browser = undefined;
- await browserMutex.runExclusive(async () => {
- browser = await puppeteer.launch({
- headless: serverConfig.crawler.headlessBrowser,
- executablePath: serverConfig.crawler.browserExecutablePath,
- userDataDir: serverConfig.crawler.browserUserDataDir,
- });
- browser.on("disconnected", async () => {
- logger.info(
- "The puppeteer browser got disconnected. Will attempt to launch it again.",
- );
- await launchBrowser();
- });
- });
-}
-
-export class CrawlerWorker {
- static async build() {
- puppeteer.use(StealthPlugin());
- puppeteer.use(
- AdblockerPlugin({
- blockTrackersAndAnnoyances: true,
- }),
- );
- await launchBrowser();
-
- logger.info("Starting crawler worker ...");
- const worker = new Worker<ZCrawlLinkRequest, void>(
- LinkCrawlerQueue.name,
- runCrawler,
- {
- connection: queueConnectionDetails,
- autorun: false,
- },
- );
-
- worker.on("completed", (job) => {
- const jobId = job?.id || "unknown";
- logger.info(`[Crawler][${jobId}] Completed successfully`);
- });
-
- worker.on("failed", (job, error) => {
- const jobId = job?.id || "unknown";
- logger.error(`[Crawler][${jobId}] Crawling job failed: ${error}`);
- });
-
- return worker;
- }
-}
-
-async function getBookmarkUrl(bookmarkId: string) {
- const bookmark = await db.query.bookmarkLinks.findFirst({
- where: eq(bookmarkLinks.id, bookmarkId),
- });
-
- if (!bookmark) {
- throw new Error("The bookmark either doesn't exist or not a link");
- }
- return bookmark.url;
-}
-
-async function crawlPage(url: string) {
- assert(browser);
- const context = await browser.createBrowserContext();
-
- try {
- const page = await context.newPage();
-
- await page.goto(url, {
- timeout: 10000, // 10 seconds
- });
-
- // Wait until there's at most two connections for 2 seconds
- // Attempt to wait only for 5 seconds
- await Promise.race([
- page.waitForNetworkIdle({
- idleTime: 1000, // 1 sec
- concurrency: 2,
- }),
- new Promise((f) => setTimeout(f, 5000)),
- ]);
-
- const htmlContent = await page.content();
- return htmlContent;
- } finally {
- await context.close();
- }
-}
-
-async function runCrawler(job: Job<ZCrawlLinkRequest, void>) {
- const jobId = job.id || "unknown";
-
- const request = zCrawlLinkRequestSchema.safeParse(job.data);
- if (!request.success) {
- logger.error(
- `[Crawler][${jobId}] Got malformed job request: ${request.error.toString()}`,
- );
- return;
- }
-
- const { bookmarkId } = request.data;
- const url = await getBookmarkUrl(bookmarkId);
-
- logger.info(
- `[Crawler][${jobId}] Will crawl "${url}" for link with id "${bookmarkId}"`,
- );
- // TODO(IMPORTANT): Run security validations on the input URL (e.g. deny localhost, etc)
-
- const htmlContent = await crawlPage(url);
-
- const meta = await metascraperParser({
- url,
- html: htmlContent,
- });
-
- const window = new JSDOM("").window;
- const purify = DOMPurify(window);
- const purifiedHTML = purify.sanitize(htmlContent);
- const purifiedDOM = new JSDOM(purifiedHTML, { url });
- const readableContent = new Readability(purifiedDOM.window.document).parse();
-
- // TODO(important): Restrict the size of content to store
-
- await db
- .update(bookmarkLinks)
- .set({
- title: meta.title,
- description: meta.description,
- imageUrl: meta.image,
- favicon: meta.logo,
- content: readableContent?.textContent,
- htmlContent: readableContent?.content,
- crawledAt: new Date(),
- })
- .where(eq(bookmarkLinks.id, bookmarkId));
-
- // Enqueue openai job
- OpenAIQueue.add("openai", {
- bookmarkId,
- });
-
- // Update the search index
- SearchIndexingQueue.add("search_indexing", {
- bookmarkId,
- type: "index",
- });
-}
diff --git a/packages/workers/index.ts b/packages/workers/index.ts
deleted file mode 100644
index 295eeaef..00000000
--- a/packages/workers/index.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import "dotenv/config";
-import { CrawlerWorker } from "./crawler";
-import { OpenAiWorker } from "./openai";
-import { SearchIndexingWorker } from "./search";
-
-async function main() {
- const [crawler, openai, search] = [
- await CrawlerWorker.build(),
- await OpenAiWorker.build(),
- await SearchIndexingWorker.build(),
- ];
-
- await Promise.all([crawler.run(), openai.run(), search.run()]);
-}
-
-main();
diff --git a/packages/workers/openai.ts b/packages/workers/openai.ts
deleted file mode 100644
index 1ec22d32..00000000
--- a/packages/workers/openai.ts
+++ /dev/null
@@ -1,263 +0,0 @@
-import { db } from "@hoarder/db";
-import logger from "@hoarder/shared/logger";
-import serverConfig from "@hoarder/shared/config";
-import {
- OpenAIQueue,
- SearchIndexingQueue,
- ZOpenAIRequest,
- queueConnectionDetails,
- zOpenAIRequestSchema,
-} from "@hoarder/shared/queues";
-import { Job } from "bullmq";
-import OpenAI from "openai";
-import { z } from "zod";
-import { Worker } from "bullmq";
-import { bookmarkTags, bookmarks, tagsOnBookmarks } from "@hoarder/db/schema";
-import { and, eq, inArray } from "drizzle-orm";
-
-const openAIResponseSchema = z.object({
- tags: z.array(z.string()),
-});
-
-async function attemptMarkTaggingStatus(
- jobData: object | undefined,
- status: "success" | "failure",
-) {
- if (!jobData) {
- return;
- }
- try {
- const request = zOpenAIRequestSchema.parse(jobData);
- await db
- .update(bookmarks)
- .set({
- taggingStatus: status,
- })
- .where(eq(bookmarks.id, request.bookmarkId));
- } catch (e) {
- console.log(`Something went wrong when marking the tagging status: ${e}`);
- }
-}
-
-export class OpenAiWorker {
- static async build() {
- logger.info("Starting openai worker ...");
- const worker = new Worker<ZOpenAIRequest, void>(
- OpenAIQueue.name,
- runOpenAI,
- {
- connection: queueConnectionDetails,
- autorun: false,
- },
- );
-
- worker.on("completed", async (job) => {
- const jobId = job?.id || "unknown";
- logger.info(`[openai][${jobId}] Completed successfully`);
- await attemptMarkTaggingStatus(job?.data, "success");
- });
-
- worker.on("failed", async (job, error) => {
- const jobId = job?.id || "unknown";
- logger.error(`[openai][${jobId}] openai job failed: ${error}`);
- await attemptMarkTaggingStatus(job?.data, "failure");
- });
-
- return worker;
- }
-}
-
-const PROMPT_BASE = `
-I'm building a read-it-later app and I need your help with automatic tagging.
-Please analyze the text after the sentence "CONTENT START HERE:" and suggest relevant tags that describe its key themes, topics, and main ideas.
-Aim for a variety of tags, including broad categories, specific keywords, and potential sub-genres. If it's a famous website
-you may also include a tag for the website. Tags should be lowercases and don't contain spaces. If the tag is not generic enough, don't
-include it. Aim for 3-5 tags. If there are no good tags, don't emit any. The content can include text for cookie consent and privacy policy, ignore those while tagging.
-You must respond in JSON with the key "tags" and the value is list of tags.
-CONTENT START HERE:
-`;
-
-function buildPrompt(
- bookmark: NonNullable<Awaited<ReturnType<typeof fetchBookmark>>>,
-) {
- if (bookmark.link) {
- if (!bookmark.link.description && !bookmark.link.content) {
- throw new Error(
- `No content found for link "${bookmark.id}". Skipping ...`,
- );
- }
-
- let content = bookmark.link.content;
- if (content) {
- let words = content.split(" ");
- if (words.length > 2000) {
- words = words.slice(2000);
- content = words.join(" ");
- }
- }
- return `
-${PROMPT_BASE}
-URL: ${bookmark.link.url}
-Title: ${bookmark.link.title || ""}
-Description: ${bookmark.link.description || ""}
-Content: ${content || ""}
- `;
- }
-
- if (bookmark.text) {
- // TODO: Ensure that the content doesn't exceed the context length of openai
- return `
-${PROMPT_BASE}
-${bookmark.text.text}
- `;
- }
-
- throw new Error("Unknown bookmark type");
-}
-
-async function fetchBookmark(linkId: string) {
- return await db.query.bookmarks.findFirst({
- where: eq(bookmarks.id, linkId),
- with: {
- link: true,
- text: true,
- },
- });
-}
-
-async function inferTags(
- jobId: string,
- bookmark: NonNullable<Awaited<ReturnType<typeof fetchBookmark>>>,
- openai: OpenAI,
-) {
- const chatCompletion = await openai.chat.completions.create({
- messages: [{ role: "system", content: buildPrompt(bookmark) }],
- model: "gpt-3.5-turbo-0125",
- response_format: { type: "json_object" },
- });
-
- const response = chatCompletion.choices[0].message.content;
- if (!response) {
- throw new Error(`[openai][${jobId}] Got no message content from OpenAI`);
- }
-
- try {
- let tags = openAIResponseSchema.parse(JSON.parse(response)).tags;
- logger.info(
- `[openai][${jobId}] Inferring tag for bookmark "${bookmark.id}" used ${chatCompletion.usage?.total_tokens} tokens and inferred: ${tags}`,
- );
-
- // Sometimes the tags contain the hashtag symbol, let's strip them out if they do.
- tags = tags.map((t) => {
- if (t.startsWith("#")) {
- return t.slice(1);
- }
- return t;
- });
-
- return tags;
- } catch (e) {
- throw new Error(
- `[openai][${jobId}] Failed to parse JSON response from OpenAI: ${e}`,
- );
- }
-}
-
-async function connectTags(
- bookmarkId: string,
- newTags: string[],
- userId: string,
-) {
- if (newTags.length == 0) {
- return;
- }
-
- await db.transaction(async (tx) => {
- // Create tags that didn't exist previously
- await tx
- .insert(bookmarkTags)
- .values(
- newTags.map((t) => ({
- name: t,
- userId,
- })),
- )
- .onConflictDoNothing();
-
- const newTagIds = (
- await tx.query.bookmarkTags.findMany({
- where: and(
- eq(bookmarkTags.userId, userId),
- inArray(bookmarkTags.name, newTags),
- ),
- columns: {
- id: true,
- },
- })
- ).map((r) => r.id);
-
- // Delete old AI tags
- await tx
- .delete(tagsOnBookmarks)
- .where(
- and(
- eq(tagsOnBookmarks.attachedBy, "ai"),
- eq(tagsOnBookmarks.bookmarkId, bookmarkId),
- ),
- );
-
- // Attach new ones
- await tx
- .insert(tagsOnBookmarks)
- .values(
- newTagIds.map((tagId) => ({
- tagId,
- bookmarkId,
- attachedBy: "ai" as const,
- })),
- )
- .onConflictDoNothing();
- });
-}
-
-async function runOpenAI(job: Job<ZOpenAIRequest, void>) {
- const jobId = job.id || "unknown";
-
- const { openAI } = serverConfig;
-
- if (!openAI.apiKey) {
- logger.debug(
- `[openai][${jobId}] OpenAI is not configured, nothing to do now`,
- );
- return;
- }
-
- const openai = new OpenAI({
- apiKey: openAI.apiKey,
- });
-
- const request = zOpenAIRequestSchema.safeParse(job.data);
- if (!request.success) {
- throw new Error(
- `[openai][${jobId}] Got malformed job request: ${request.error.toString()}`,
- );
- }
-
- const { bookmarkId } = request.data;
- const bookmark = await fetchBookmark(bookmarkId);
- if (!bookmark) {
- throw new Error(
- `[openai][${jobId}] bookmark with id ${bookmarkId} was not found`,
- );
- }
-
- const tags = await inferTags(jobId, bookmark, openai);
-
- await connectTags(bookmarkId, tags, bookmark.userId);
-
- // Update the search index
- SearchIndexingQueue.add("search_indexing", {
- bookmarkId,
- type: "index",
- });
-}
diff --git a/packages/workers/package.json b/packages/workers/package.json
deleted file mode 100644
index f2fc164c..00000000
--- a/packages/workers/package.json
+++ /dev/null
@@ -1,45 +0,0 @@
-{
- "$schema": "https://json.schemastore.org/package.json",
- "name": "@hoarder/workers",
- "version": "0.1.0",
- "private": true,
- "dependencies": {
- "@hoarder/db": "workspace:*",
- "@hoarder/shared": "workspace:*",
- "@mozilla/readability": "^0.5.0",
- "@tsconfig/node21": "^21.0.1",
- "async-mutex": "^0.4.1",
- "bullmq": "^5.1.9",
- "dompurify": "^3.0.9",
- "dotenv": "^16.4.1",
- "drizzle-orm": "^0.29.4",
- "jsdom": "^24.0.0",
- "metascraper": "^5.43.4",
- "metascraper-description": "^5.43.4",
- "metascraper-image": "^5.43.4",
- "metascraper-logo": "^5.43.4",
- "metascraper-logo-favicon": "^5.43.4",
- "metascraper-readability": "^5.43.4",
- "metascraper-title": "^5.43.4",
- "metascraper-twitter": "^5.43.4",
- "metascraper-url": "^5.43.4",
- "openai": "^4.26.1",
- "puppeteer": "^22.0.0",
- "puppeteer-extra": "^3.3.6",
- "puppeteer-extra-plugin-adblocker": "^2.13.6",
- "puppeteer-extra-plugin-stealth": "^2.11.2",
- "tsx": "^4.7.1",
- "typescript": "^5",
- "zod": "^3.22.4"
- },
- "devDependencies": {
- "@types/dompurify": "^3.0.5",
- "@types/jsdom": "^21.1.6",
- "@types/metascraper": "^5.14.3"
- },
- "scripts": {
- "start": "tsx watch index.ts",
- "start:prod": "tsx index.ts",
- "typecheck": "tsc --noEmit"
- }
-}
diff --git a/packages/workers/search.ts b/packages/workers/search.ts
deleted file mode 100644
index 618e7c89..00000000
--- a/packages/workers/search.ts
+++ /dev/null
@@ -1,116 +0,0 @@
-import { db } from "@hoarder/db";
-import logger from "@hoarder/shared/logger";
-import { getSearchIdxClient } from "@hoarder/shared/search";
-import {
- SearchIndexingQueue,
- ZSearchIndexingRequest,
- queueConnectionDetails,
- zSearchIndexingRequestSchema,
-} from "@hoarder/shared/queues";
-import { Job } from "bullmq";
-import { Worker } from "bullmq";
-import { bookmarks } from "@hoarder/db/schema";
-import { eq } from "drizzle-orm";
-
-export class SearchIndexingWorker {
- static async build() {
- logger.info("Starting search indexing worker ...");
- const worker = new Worker<ZSearchIndexingRequest, void>(
- SearchIndexingQueue.name,
- runSearchIndexing,
- {
- connection: queueConnectionDetails,
- autorun: false,
- },
- );
-
- worker.on("completed", (job) => {
- const jobId = job?.id || "unknown";
- logger.info(`[search][${jobId}] Completed successfully`);
- });
-
- worker.on("failed", (job, error) => {
- const jobId = job?.id || "unknown";
- logger.error(`[search][${jobId}] openai job failed: ${error}`);
- });
-
- return worker;
- }
-}
-
-async function runIndex(
- searchClient: NonNullable<Awaited<ReturnType<typeof getSearchIdxClient>>>,
- bookmarkId: string,
-) {
- const bookmark = await db.query.bookmarks.findFirst({
- where: eq(bookmarks.id, bookmarkId),
- with: {
- link: true,
- text: true,
- tagsOnBookmarks: {
- with: {
- tag: true,
- },
- },
- },
- });
-
- if (!bookmark) {
- throw new Error(`Bookmark ${bookmarkId} not found`);
- }
-
- searchClient.addDocuments([
- {
- id: bookmark.id,
- userId: bookmark.userId,
- ...(bookmark.link
- ? {
- url: bookmark.link.url,
- title: bookmark.link.title,
- description: bookmark.link.description,
- content: bookmark.link.content,
- }
- : undefined),
- ...(bookmark.text ? { content: bookmark.text.text } : undefined),
- tags: bookmark.tagsOnBookmarks.map((t) => t.tag.name),
- },
- ]);
-}
-
-async function runDelete(
- searchClient: NonNullable<Awaited<ReturnType<typeof getSearchIdxClient>>>,
- bookmarkId: string,
-) {
- await searchClient.deleteDocument(bookmarkId);
-}
-
-async function runSearchIndexing(job: Job<ZSearchIndexingRequest, void>) {
- const jobId = job.id || "unknown";
-
- const request = zSearchIndexingRequestSchema.safeParse(job.data);
- if (!request.success) {
- throw new Error(
- `[search][${jobId}] Got malformed job request: ${request.error.toString()}`,
- );
- }
-
- const searchClient = await getSearchIdxClient();
- if (!searchClient) {
- logger.debug(
- `[search][${jobId}] Search is not configured, nothing to do now`,
- );
- return;
- }
-
- const bookmarkId = request.data.bookmarkId;
- switch (request.data.type) {
- case "index": {
- await runIndex(searchClient, bookmarkId);
- break;
- }
- case "delete": {
- await runDelete(searchClient, bookmarkId);
- break;
- }
- }
-}