diff options
218 files changed, 1739 insertions, 12467 deletions
diff --git a/.eslintrc.cjs b/.eslintrc.cjs deleted file mode 100644 index 728f09f1..00000000 --- a/.eslintrc.cjs +++ /dev/null @@ -1,35 +0,0 @@ -/** @type {import("eslint").Linter.Config} */ -const config = { - parser: "@typescript-eslint/parser", - plugins: ["@typescript-eslint", "eslint-plugin-next-on-pages"], - extends: [ - "next/core-web-vitals", - "plugin:@typescript-eslint/recommended-type-checked", - "plugin:@typescript-eslint/stylistic-type-checked", - "plugin:eslint-plugin-next-on-pages/recommended", - ], - rules: { - // These opinionated rules are enabled in stylistic-type-checked above. - // Feel free to reconfigure them to your own preference. - "@typescript-eslint/array-type": "off", - "@typescript-eslint/consistent-type-definitions": "off", - - "@typescript-eslint/consistent-type-imports": [ - "warn", - { - prefer: "type-imports", - fixStyle: "inline-type-imports", - }, - ], - "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }], - "@typescript-eslint/require-await": "off", - "@typescript-eslint/no-misused-promises": [ - "error", - { - checksVoidReturn: { attributes: false }, - }, - ], - }, -}; - -module.exports = config; @@ -1,52 +1,43 @@ -.next/ -.turbo -*.sqlite -*.lockb -.next +bun.lockb +.npmrc +.vars +.*.vars .wrangler -drizzle/ -dist/ -pnpm-lock.yaml +.million +# Dependencies node_modules - -# dependencies -/node_modules -/.pnp +.pnp .pnp.js -# testing -/coverage +# Local env files +.env +.env.local +.env.development.local +.env.test.local +.env.production.local -# database -/prisma/db.sqlite -/prisma/db.sqlite-journal +# Testing +coverage -# next.js -/.next/ -/out/ -next-env.d.ts +# Turbo +.turbo -# production -/build +# Vercel +.vercel + +# Build Outputs +.next/ +out/ +build +dist -# misc -.DS_Store -*.pem -# debug +# Debug npm-debug.log* yarn-debug.log* yarn-error.log* -.pnpm-debug.log* - -# local env files -# do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables -.env -.env*.local -# vercel -.vercel - -# typescript -*.tsbuildinfo +# Misc +.DS_Store +*.pem diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index e8be54ff..00000000 --- a/.gitmodules +++ /dev/null @@ -1,6 +0,0 @@ -[submodule "apps/rowser-rendering"] - path = apps/rowser-rendering - url = https://github.com/dhravya/markdowner -[submodule "apps/browser-rendering"] - path = apps/browser-rendering - url = https://github.com/dhravya/markdowner diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index f3c04e81..00000000 --- a/.prettierignore +++ /dev/null @@ -1 +0,0 @@ -**/.gitignore
\ No newline at end of file diff --git a/LICENSE b/LICENSE deleted file mode 100644 index c50f32c7..00000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2024 Dhravya Shah - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/README.md b/README.md deleted file mode 100644 index 0fbe3e85..00000000 --- a/README.md +++ /dev/null @@ -1,90 +0,0 @@ - - -# SuperMemory - -Interested in helping build the best second brain for everyone? Join the discord https://discord.gg/2X2XsKz5AU. Contributions welcome. - -## 👀 What is this? - -Build your own second brain with supermemory. It's a ChatGPT for your bookmarks. Import tweets or save websites and content using the [chrome extension](https://chromewebstore.google.com/detail/supermemory/afpgkkipfdpeaflnpoaffkcankadgjfc?hl=en-GB&authuser=0) (the extension on webstore is not updated, please use the one in the repo) - -Well, here's the thing - me and @yxshv save a _lot_ of content on the internet. - -Twitter bookmarks, websites, snippets, etc. - -But we never look back to it - to us, it's like throwing information in the void. - -Supermemory fixes this. - -## How do I use this? - -Just go to [supermemory.dhr.wtf](https://supermemory.dhr.wtf) and sign in with your google account. - -To use the chrome extension, - -1. Get the chrome ext (click on the button) -2. Click on the "Extension Auth" button so the extension knows who you are :) -  - -## 👨💻 The Stack - - - -Supermemory has three main modules, managed by [turborepo](https://turbo.build): - -#### `apps/web`: The main web UI. - -The database, auth etc logic is here - - - -Built with: - -- Nextjs 14 -- [Next Auth](https://next-auth.js.org/) -- [Drizzle ORM](https://drizzle.team/) -- [Cloudflare D1 database](https://developers.cloudflare.com/d1/get-started/) -- Cloudflare ratelimiter -- [TailwindCSS](https://tailwindcss.com) -- [shadcn-ui](https://ui.shadcn.com) -- And some other amazing open source projects like [Novel](https://novel.sh) and [vaul](https://vaul.emilkowal.ski/) -- Hosted on [Cloudflare Pages](https://pages.cloudflare.com/) - -#### `apps/extension`: Chrome extension - -The chrome extension is one of the most important part of the setup, but is not required.This is to easily add pages to your memory. - - - -You can also use it to import all your twitter bookmarks! - - -Built with: - -- [CRXJS](https://crxjs.dev/vite-plugin/getting-started/react/create-project) -- Vite -- [TailwindCSS](https://tailwindcss.com) -- [shadcn-ui](https://ui.shadcn.com) -- React - -#### `apps/cf-ai-backend`: This module handles the vector store and AI response generation - -This is where the magic happens! -Built with: - -- Cloudflare Workers -- [Cloudflare AI](https://ai.cloudflare.com) -- [Cloudflare Vectorize](https://developers.cloudflare.com/vectorize/) -- [Cloudflare Queues](https://developers.cloudflare.com/queues/) -- [Cloudflare Browser Rendering](https://developers.cloudflare.com/browser-rendering/) -- [Cloudflare KV](https://developers.cloudflare.com/kv) - -## Contribute or self host - -Supermemory is design to be set up easily locally and super duper easy to set up 💫 - -Please see the [SETUP-GUIDE.md](SETUP-GUIDE.md) for setup instructions. - -### Contributing - -Contributions are very welcome! A contribution can be as small as a ⭐ or even finding and creating issues. diff --git a/SETUP-GUIDE.md b/SETUP-GUIDE.md deleted file mode 100644 index 7d69b545..00000000 --- a/SETUP-GUIDE.md +++ /dev/null @@ -1,79 +0,0 @@ -# Setup guide - -## Prerequisites - -- [bun](https://bun.sh/) -- [turbo](https://turbo.build/repo/docs/installing) -- [wrangler](https://developers.cloudflare.com/workers/cli-wrangler/install-update) - -## Steps - -1. Clone the repo -2. Run `bun install` in the root directory -3. Create a `.dev.vars` file in `apps/web` with the following content: - -```bash -GOOGLE_CLIENT_ID="-" -GOOGLE_CLIENT_SECRET="-" -NEXTAUTH_SECRET='nextauthsecret' -DATABASE_URL='database.sqlite' -NEXTAUTH_URL='http://localhost:3000' -BACKEND_SECURITY_KEY='veryrandomsecuritykey' -``` - -4. Setup the database: - -First, edit the `wrangler.toml` file in `apps/web` to point the d1 database to your account. - -You can create a d1 database by running this command - -``` -wrangler d1 create DATABASE_NAME -``` - -And then replace these values - -``` -[[d1_databases]] -binding = "DATABASE" -database_name = "YOUR_DATABASE_NAME" -database_id = "YOUR_DB_ID" -``` - -Simply run this command in `apps/web` - -``` -wrangler d1 execute dev-d1-anycontext --local --file=db/prepare.sql -``` - -If it runs, you can set up the cloud database as well by removing the `--local` flag. - -5. You need to host your own worker for the `apps/cf-ai-backend` module. - -To do this, first edit the `.dev.vars` file in `apps/cf-ai-backend` with the following content: - -```bash -SECURITY_KEY="veryrandomsecuritykey" -// Why? to generate embeddings with 4000+ tokens -OPENAI_API_KEY="sk-" -``` - -6. Run this command to initialise vector database - > Note: You need to use the workers paid plan to use vectorize for now. - -``` -wrangler vectorize create --dimensions=1536 supermem-vector-1 --metric=cosine -``` - -7. Change the `wrangler.toml` file in `apps/cf-ai-backend` to point to your KV namespace - -8. Run `bun dev` in the root directory and Voila! You have your own supermemory instance running! - -> Note: You need to replace the url `https://cf-ai-backend.dhr.wtf` everywhere with your own url for the cf-ai-backend module. - -## Deploying - -To deploy the web app, run `bun deploy` in the `apps/web` directory. -To deploy the cf-ai-backend module, run `wrangler publish` in the `apps/cf-ai-backend` directory. - -To get the extension running, you need to build it first. Run `bun build` in the `apps/extension` directory and then load the extension in chrome. diff --git a/apps/cf-ai-backend/jest.config.js b/apps/cf-ai-backend/jest.config.js deleted file mode 100644 index 85c5da79..00000000 --- a/apps/cf-ai-backend/jest.config.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - testEnvironment: "miniflare", - testMatch: ["**/test/**/*.+(ts|tsx)", "**/src/**/(*.)+(spec|test).+(ts|tsx)"], - transform: { - "^.+\\.(ts|tsx)$": "esbuild-jest", - }, -}; diff --git a/apps/cf-ai-backend/tsconfig.json b/apps/cf-ai-backend/tsconfig.json index 9f6f8a73..2b75d5a0 100644 --- a/apps/cf-ai-backend/tsconfig.json +++ b/apps/cf-ai-backend/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { "lib": ["ES2020"], - "types": ["jest", "@cloudflare/workers-types"] + "types": ["@cloudflare/workers-types"] } } diff --git a/apps/cf-ai-backend/wrangler.toml b/apps/cf-ai-backend/wrangler.toml index 83e2d41a..db0ae945 100644 --- a/apps/cf-ai-backend/wrangler.toml +++ b/apps/cf-ai-backend/wrangler.toml @@ -1,5 +1,5 @@ name = "new-cf-ai-backend" -main = "src/server.ts" +main = "src/index.ts" compatibility_date = "2024-02-23" node_compat = true diff --git a/apps/extension/.eslintrc.cjs b/apps/extension/.eslintrc.cjs deleted file mode 100644 index 6e8698b7..00000000 --- a/apps/extension/.eslintrc.cjs +++ /dev/null @@ -1,18 +0,0 @@ -module.exports = { - root: true, - env: { browser: true, es2020: true }, - extends: [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended", - "plugin:react-hooks/recommended", - ], - ignorePatterns: ["dist", ".eslintrc.cjs"], - parser: "@typescript-eslint/parser", - plugins: ["react-refresh"], - rules: { - "react-refresh/only-export-components": [ - "warn", - { allowConstantExport: true }, - ], - }, -}; diff --git a/apps/extension/.gitignore b/apps/extension/.gitignore deleted file mode 100644 index 97d5fffe..00000000 --- a/apps/extension/.gitignore +++ /dev/null @@ -1,26 +0,0 @@ -# Logs -*.zip - -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/apps/extension/README.md b/apps/extension/README.md deleted file mode 100644 index bb156850..00000000 --- a/apps/extension/README.md +++ /dev/null @@ -1,30 +0,0 @@ -# React + TypeScript + Vite - -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. - -Currently, two official plugins are available: - -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh - -## Expanding the ESLint configuration - -If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: - -- Configure the top-level `parserOptions` property like this: - -```js -export default { - // other rules... - parserOptions: { - ecmaVersion: "latest", - sourceType: "module", - project: ["./tsconfig.json", "./tsconfig.node.json"], - tsconfigRootDir: __dirname, - }, -}; -``` - -- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` -- Optionally add `plugin:@typescript-eslint/stylistic-type-checked` -- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list diff --git a/apps/extension/components.json b/apps/extension/components.json deleted file mode 100644 index 6ceb01a1..00000000 --- a/apps/extension/components.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema.json", - "style": "default", - "rsc": false, - "tsx": true, - "tailwind": { - "config": "tailwind.config.js", - "css": "src/global.css", - "baseColor": "stone", - "cssVariables": false, - "prefix": "anycontext-" - }, - "aliases": { - "components": "src/components", - "utils": "src/lib/utils" - } -} diff --git a/apps/extension/icons/icon128.png b/apps/extension/icons/icon128.png Binary files differdeleted file mode 100644 index 9ae12886..00000000 --- a/apps/extension/icons/icon128.png +++ /dev/null diff --git a/apps/extension/icons/icon16.png b/apps/extension/icons/icon16.png Binary files differdeleted file mode 100644 index dedf07d2..00000000 --- a/apps/extension/icons/icon16.png +++ /dev/null diff --git a/apps/extension/icons/icon32.png b/apps/extension/icons/icon32.png Binary files differdeleted file mode 100644 index 4ceed7ba..00000000 --- a/apps/extension/icons/icon32.png +++ /dev/null diff --git a/apps/extension/icons/icon48.png b/apps/extension/icons/icon48.png Binary files differdeleted file mode 100644 index e8beaf4f..00000000 --- a/apps/extension/icons/icon48.png +++ /dev/null diff --git a/apps/extension/index.html b/apps/extension/index.html deleted file mode 100644 index e4b78eae..00000000 --- a/apps/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/apps/extension/manifest.json b/apps/extension/manifest.json deleted file mode 100644 index 5cf05298..00000000 --- a/apps/extension/manifest.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "manifest_version": 3, - "name": "SuperMemory", - "version": "2.0.0", - "action": { - "default_popup": "index.html" - }, - "icons": { - "16": "icons/icon16.png", - "32": "icons/icon32.png", - "48": "icons/icon48.png", - "128": "icons/icon128.png" - }, - "content_scripts": [ - { - "js": ["src/content.tsx"], - "matches": [ - "http://localhost:3000/*", - "https://opulent-funicular-94rx4v9w775f96q-3000.app.github.dev/*", - "https://anycontext.dhr.wtf/*", - "<all_urls>" - ] - } - ], - "permissions": [ - "activeTab", - "storage", - "http://localhost:3000/*", - "https://opulent-funicular-94rx4v9w775f96q-3000.app.github.dev/*", - "https://anycontext.dhr.wtf/*" - ], - "background": { - "service_worker": "src/background.ts" - } -} diff --git a/apps/extension/package.json b/apps/extension/package.json deleted file mode 100644 index a7f34ea6..00000000 --- a/apps/extension/package.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "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", - "package": "zip -r extension.zip dist/" - }, - "dependencies": { - "@radix-ui/react-dialog": "^1.0.5", - "@radix-ui/react-dropdown-menu": "^2.0.6", - "@radix-ui/react-popover": "^1.0.7", - "@radix-ui/react-tooltip": "^1.0.7", - "cmdk": "^1.0.0", - "react": "^18.2.0", - "react-dom": "^18.2.0" - }, - "devDependencies": { - "@types/node": "^20.11.22", - "@types/react": "^18.2.56", - "@types/react-dom": "^18.2.19", - "@typescript-eslint/eslint-plugin": "^7.0.2", - "@typescript-eslint/parser": "^7.0.2", - "@vitejs/plugin-react": "^4.2.1", - "eslint": "^8.56.0", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-react-refresh": "^0.4.5", - "typescript": "^5.2.2", - "vite": "^5.1.4" - } -} diff --git a/apps/extension/postcss.config.js b/apps/extension/postcss.config.js deleted file mode 100644 index 2aa7205d..00000000 --- a/apps/extension/postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -export default { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -}; diff --git a/apps/extension/public/vite.svg b/apps/extension/public/vite.svg deleted file mode 100644 index e7b8dfb1..00000000 --- a/apps/extension/public/vite.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="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
\ No newline at end of file diff --git a/apps/extension/src/App.tsx b/apps/extension/src/App.tsx deleted file mode 100644 index c29d98a2..00000000 --- a/apps/extension/src/App.tsx +++ /dev/null @@ -1,308 +0,0 @@ -import { useEffect, useState } from "react"; -import { z } from "zod"; -import { userObj } from "./types/zods"; -import { getEnv } from "./util"; - -const backendUrl = - getEnv() === "development" - ? "http://localhost:3000" - : "https://supermemory.dhr.wtf"; - -function App() { - const [userData, setUserData] = useState<z.infer<typeof userObj> | null>( - null, - ); - - const getUserData = () => { - chrome.runtime.sendMessage({ type: "getJwt" }, (response) => { - const jwt = response.jwt; - const loginButton = document.getElementById("login"); - - if (loginButton) { - if (jwt) { - fetch(`${backendUrl}/api/me`, { - headers: { - Authorization: `Bearer ${jwt}`, - }, - }) - .then((res) => res.json()) - .then((data) => { - const d = userObj.safeParse(data); - if (d.success) { - setUserData(d.data); - } else { - console.error(d.error); - } - }); - loginButton.style.display = "none"; - } - } - }); - }; - - useEffect(() => { - getUserData(); - }, []); - - // TODO: Implement getting bookmarks from Twitter API directly - // const [status, setStatus] = useState(''); - // const [bookmarks, setBookmarks] = useState<TweetData[]>([]); - - // const fetchBookmarks = (e: React.MouseEvent<HTMLButtonElement>) => { - // e.preventDefault(); - - // chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) { - // chrome.tabs.sendMessage(tabs[0].id!, { action: 'showProgressIndicator' }); - // }); - - // chrome.tabs.create( - // { url: 'https://twitter.com/i/bookmarks/all' }, - // function (tab) { - // chrome.tabs.onUpdated.addListener(function listener(tabId, info) { - // if (tabId === tab.id && info.status === 'complete') { - // chrome.tabs.onUpdated.removeListener(listener); - - // chrome.runtime.sendMessage( - // { action: 'getAuthData' }, - // function (response) { - // const authorizationHeader = response.authorizationHeader; - // const csrfToken = response.csrfToken; - // const cookies = response.cookies; - - // if (authorizationHeader && csrfToken && cookies) { - // fetchAllBookmarks(authorizationHeader, csrfToken, cookies) - // .then((bookmarks) => { - // console.log('Bookmarks data:', bookmarks); - // setBookmarks(bookmarks); - // chrome.tabs.sendMessage(tabId, { - // action: 'hideProgressIndicator', - // }); - // setStatus( - // `Fetched ${bookmarks.length} bookmarked tweets.`, - // ); - // }) - // .catch((error) => { - // console.error('Error:', error); - // chrome.tabs.sendMessage(tabId, { - // action: 'hideProgressIndicator', - // }); - // setStatus( - // 'Error fetching bookmarks. Please check the console for details.', - // ); - // }); - // } else { - // chrome.tabs.sendMessage(tabId, { - // action: 'hideProgressIndicator', - // }); - // setStatus('Missing authentication data'); - // } - // }, - // ); - // } - // }); - // }, - // ); - // }; - - return ( - <div className="p-8"> - <button - onClick={() => - chrome.tabs.create({ - url: `${backendUrl}/api/auth/signin`, - }) - } - id="login" - > - Log in - </button> - <div> - {userData && ( - <div className="flex items-center"> - <img - width={40} - className="rounded-full" - src={userData.data.user.image!} - alt="" - /> - <div> - <h3>{userData.data.user.name}</h3> - <p>{userData.data.user.email}</p> - </div> - {/* TODO: Implement getting bookmarks from API directly */} - {/* <button onClick={(e) => fetchBookmarks(e)}>Fetch Bookmarks</button> - <div>{status}</div> - - <div> - {bookmarks.map((bookmark) => ( - <div key={bookmark.tweet_id}> - <p>{bookmark.author}</p> - <p>{bookmark.date}</p> - <p>{bookmark.full_text}</p> - </div> - ))} - </div> */} - </div> - )} - </div> - </div> - ); -} - -// TODO: Implement getting bookmarks from Twitter API directly -// async function fetchAllBookmarks( -// authorizationHeader: string, -// csrfToken: string, -// cookies: string, -// ): Promise<TweetData[]> { -// const baseUrl = -// 'https://twitter.com/i/api/graphql/uJEL6XARgGmo2EAsO2Pfkg/Bookmarks'; -// const params = new URLSearchParams({ -// variables: JSON.stringify({ -// count: 100, -// includePromotedContent: true, -// }), -// features: JSON.stringify({ -// graphql_timeline_v2_bookmark_timeline: true, -// rweb_tipjar_consumption_enabled: false, -// responsive_web_graphql_exclude_directive_enabled: true, -// verified_phone_label_enabled: true, -// creator_subscriptions_tweet_preview_api_enabled: true, -// responsive_web_graphql_timeline_navigation_enabled: true, -// responsive_web_graphql_skip_user_profile_image_extensions_enabled: false, -// communities_web_enable_tweet_community_results_fetch: true, -// c9s_tweet_anatomy_moderator_badge_enabled: true, -// tweetypie_unmention_optimization_enabled: true, -// responsive_web_edit_tweet_api_enabled: true, -// graphql_is_translatable_rweb_tweet_is_translatable_enabled: true, -// view_counts_everywhere_api_enabled: true, -// longform_notetweets_consumption_enabled: true, -// responsive_web_twitter_article_tweet_consumption_enabled: true, -// tweet_awards_web_tipping_enabled: false, -// creator_subscriptions_quote_tweet_preview_enabled: false, -// freedom_of_speech_not_reach_fetch_enabled: true, -// standardized_nudges_misinfo: true, -// tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: -// true, -// tweet_with_visibility_results_prefer_gql_media_interstitial_enabled: -// false, -// rweb_video_timestamps_enabled: true, -// longform_notetweets_rich_text_read_enabled: true, -// longform_notetweets_inline_media_enabled: true, -// responsive_web_enhance_cards_enabled: false, -// }), -// }); - -// const requestUrl = `${baseUrl}?${params}`; - -// const headers = { -// Authorization: authorizationHeader, -// 'X-Csrf-Token': csrfToken, -// Cookie: cookies, -// }; - -// const bookmarks: TweetData[] = []; -// let nextCursor = null; -// let requestCount = 0; -// const maxRequestsPerWindow = 450; -// const windowDuration = 15 * 60 * 1000; // 15 minutes in milliseconds -// let windowStartTime = Date.now(); - -// do { -// if (nextCursor) { -// params.set( -// 'variables', -// JSON.stringify({ -// count: 100, -// cursor: nextCursor, -// includePromotedContent: true, -// }), -// ); -// } - -// // Check if the rate limit is exceeded -// if (requestCount >= maxRequestsPerWindow) { -// const elapsedTime = Date.now() - windowStartTime; -// if (elapsedTime < windowDuration) { -// const waitTime = windowDuration - elapsedTime; -// await new Promise((resolve) => setTimeout(resolve, waitTime)); -// } -// requestCount = 0; -// windowStartTime = Date.now(); -// } - -// try { -// const response = await fetch(requestUrl, { -// method: 'GET', -// headers: headers, -// }); - -// requestCount++; - -// if (!response.ok) { -// throw new Error(`HTTP error! status: ${response.status}`); -// } - -// const data = await response.json(); -// const timeline = data.data.bookmark_timeline_v2.timeline; - -// timeline.instructions.forEach( -// (instruction: { -// type: string; -// entries: { -// content: { -// entryType: string; -// itemContent: { -// tweet_results: { -// result: { -// legacy: { -// full_text: string; -// created_at: string; -// }; -// core: { -// user_results: { -// result: { -// legacy: { -// screen_name: string; -// }; -// }; -// }; -// }; -// rest_id: string; -// }; -// }; -// }; -// }; -// }[]; -// }) => { -// if (instruction.type === 'TimelineAddEntries') { -// instruction.entries.forEach((entry) => { -// if (entry.content.entryType === 'TimelineTimelineItem') { -// const tweet = entry.content.itemContent.tweet_results.result; -// const tweetData = { -// full_text: tweet.legacy.full_text, -// url: `https://twitter.com/${tweet.core.user_results.result.legacy.screen_name}/status/${tweet.rest_id}`, -// author: tweet.core.user_results.result.legacy.screen_name, -// date: tweet.legacy.created_at, -// tweet_id: tweet.rest_id, -// }; -// bookmarks.push(tweetData); -// } -// }); -// } -// }, -// ); - -// nextCursor = timeline.instructions.find( -// (instruction: { type: string }) => -// instruction.type === 'TimelineTerminateTimeline', -// )?.direction?.cursor; -// } catch (error) { -// console.error('Error fetching bookmarks:', error); -// throw error; -// } -// } while (nextCursor); - -// return bookmarks; -// } -export default App; diff --git a/apps/extension/src/SideBar.tsx b/apps/extension/src/SideBar.tsx deleted file mode 100644 index 385c0f22..00000000 --- a/apps/extension/src/SideBar.tsx +++ /dev/null @@ -1,361 +0,0 @@ -import { useState } from "react"; - -import "./ext.css"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "./components/ui/tooltip"; -import { FilterSpaces } from "./components/FilterCombobox"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogDescription, - DialogTrigger, - DialogFooter, - DialogClose, -} from "./components/ui/dialog"; -import { Space } from "./types/memory"; - -function sendUrlToAPI(spaces: number[]) { - // get the current URL - const url = window.location.href; - - const blacklist = ["localhost:3000", "anycontext.dhr.wtf"]; - // check if the URL is blacklisted - if (blacklist.some((blacklisted) => url.includes(blacklisted))) { - console.log("URL is blacklisted"); - return; - } else { - // const content = Entire page content, but cleaned up for the LLM. No ads, no scripts, no styles, just the text. if article, just the importnat info abou tit. - const content = document.documentElement.innerText; - chrome.runtime.sendMessage({ type: "urlChange", content, url, spaces }); - } -} - -function SideBar({ jwt }: { jwt: string }) { - // TODO: Implement getting bookmarks from Twitter API directly - // chrome.runtime.onMessage.addListener(function (request) { - // if (request.action === 'showProgressIndicator') { - // // TODO: SHOW PROGRESS INDICATOR - // // showProgressIndicator(); - // } else if (request.action === 'hideProgressIndicator') { - // // hideProgressIndicator(); - // } - // }); - - const [savedWebsites, setSavedWebsites] = useState<string[]>([]); - - const [isSendingData, setIsSendingData] = useState(false); - - const [loading, setLoading] = useState(false); - const [spaces, setSpaces] = useState<Space[]>(); - const [selectedSpaces, setSelectedSpaces] = useState<number[]>([]); - - const [isImportingTweets, setIsImportingTweets] = useState(false); - - const [log, setLog] = useState<string[]>([]); - - interface TweetData { - tweetText: string; - postUrl: string; - authorName: string; - handle: string; - time: string; - saveToUser: string; - } - - function sendBookmarkedTweetsToAPI(tweets: TweetData[], token: string) { - chrome.runtime.sendMessage({ - type: "sendBookmarkedTweets", - jwt: token, - tweets, - }); - } - - const fetchSpaces = async () => { - setLoading(true); - chrome.runtime.sendMessage({ type: "fetchSpaces" }, (resp) => { - console.log("response", resp); - setSpaces(resp); - setLoading(false); - }); - }; - - const fetchBookmarks = () => { - const tweets: TweetData[] = []; // Initialize an empty array to hold all tweet elements - - setIsImportingTweets(true); - console.log("Importing tweets"); - - const scrollInterval = 1000; - const scrollStep = 5000; // Pixels to scroll on each step - - let previousTweetCount = 0; - let unchangedCount = 0; - - const scrollToEndIntervalID = setInterval(() => { - window.scrollBy(0, scrollStep); - const currentTweetCount = tweets.length; - if (currentTweetCount === previousTweetCount) { - unchangedCount++; - if (unchangedCount >= 2) { - setLog([ - ...log, - "Scraping complete", - `Total tweets scraped: ${tweets.length}`, - "Downloading tweets as JSON...", - ]); - clearInterval(scrollToEndIntervalID); // Stop scrolling - observer.disconnect(); // Stop observing DOM changes - downloadTweetsAsJson(tweets); // Download the tweets list as a JSON file - } - } else { - unchangedCount = 0; // Reset counter if new tweets were added - } - previousTweetCount = currentTweetCount; // Update previous count for the next check - }, scrollInterval); - - function updateTweets() { - document - .querySelectorAll('article[data-testid="tweet"]') - .forEach((tweetElement) => { - const authorName = ( - tweetElement.querySelector( - '[data-testid="User-Name"]', - ) as HTMLElement - )?.innerText; - - const handle = ( - tweetElement.querySelector('[role="link"]') as HTMLLinkElement - ).href - .split("/") - .pop(); - - const tweetText = ( - tweetElement.querySelector( - '[data-testid="tweetText"]', - ) as HTMLElement - )?.innerText; - const time = ( - tweetElement.querySelector("time") as HTMLTimeElement - ).getAttribute("datetime"); - const postUrl = ( - tweetElement.querySelector( - ".css-175oi2r.r-18u37iz.r-1q142lx a", - ) as HTMLLinkElement - )?.href; - - const isTweetNew = !tweets.some((tweet) => tweet.postUrl === postUrl); - if (isTweetNew) { - tweets.push({ - authorName, - handle: handle ?? "", - tweetText, - time: time ?? "", - postUrl, - saveToUser: jwt, - }); - - setLog([...log, `Scraped tweet: ${tweets.length}`]); - } - }); - } - - // Initially populate the tweets array - updateTweets(); - - // Create a MutationObserver to observe changes in the DOM - const observer = new MutationObserver((mutations) => { - mutations.forEach((mutation) => { - if (mutation.addedNodes.length) { - updateTweets(); // Call updateTweets whenever new nodes are added to the DOM - } - }); - }); - - // Start observing the document body for child list changes - observer.observe(document.body, { childList: true, subtree: true }); - - function downloadTweetsAsJson(tweetsArray: TweetData[]) { - setLog([...log, "Saving the tweets to our database..."]); - sendBookmarkedTweetsToAPI(tweetsArray, jwt); - setIsImportingTweets(false); - } - }; - - return ( - <> - {isImportingTweets && ( - <div className="anycontext-overlay anycontext-fixed anycontext-font-sans anycontext-inset-0 anycontext-bg-black anycontext-bg-opacity-50"> - <div className="anycontext-flex anycontext-items-center anycontext-justify-center anycontext-h-screen"> - <div className="anycontext-flex anycontext-flex-col anycontext-items-center"> - <svg - xmlns="http://www.w3.org/2000/svg" - fill="none" - viewBox="0 0 24 24" - strokeWidth={2} - stroke="currentColor" - className="anycontext-w-10 anycontext-h-10 anycontext-animate-spin" - > - <path - strokeLinecap="round" - strokeLinejoin="round" - d="M17.593 3.322c1.1.128 1.907 1.077 1.907 2.185V21L12 17.25 4.5 21V5.507c0-1.108.806-2.057 1.907-2.185a48.507 48.507 0 0 1 11.186 0Z" - /> - </svg> - <p className="anycontext-mt-2">Importing your tweets...</p> - <div className="anycontext-mt-2"> - {log.map((message, index) => ( - <p key={index}>{message}</p> - ))} - </div> - </div> - </div> - </div> - )} - - <TooltipProvider> - <div className="anycontext-flex anycontext-group anycontext-flex-col anycontext-gap-2 anycontext-fixed anycontext-bottom-12 anycontext-right-0 anycontext-z-[99999] anycontext-font-sans"> - {window.location.href.includes("twitter.com") || - window.location.href.includes("x.com") ? ( - <Tooltip delayDuration={300}> - <TooltipTrigger className="anycontext-bg-transparent anycontext-border-none anycontext-m-0 anycontext-p-0"> - <button - onClick={() => { - if (window.location.href.endsWith("/i/bookmarks/all")) { - fetchBookmarks(); - } else { - window.location.href = - "https://twitter.com/i/bookmarks/all"; - - setTimeout(() => { - fetchBookmarks(); - }, 2500); - } - }} - className="anycontext-open-button disabled:anycontext-opacity-30 anycontext-bg-transparent - anycontext-border-none anycontext-m-0 anycontext-p-0" - > - <svg - xmlns="http://www.w3.org/2000/svg" - fill="none" - viewBox="0 0 24 24" - strokeWidth={1.5} - stroke="currentColor" - className="anycontext-w-6 anycontext-h-6" - > - <path - strokeLinecap="round" - strokeLinejoin="round" - d="M17.593 3.322c1.1.128 1.907 1.077 1.907 2.185V21L12 17.25 4.5 21V5.507c0-1.108.806-2.057 1.907-2.185a48.507 48.507 0 0 1 11.186 0Z" - /> - </svg> - </button> - </TooltipTrigger> - <TooltipContent className="anycontext-p-0" side="left"> - <p className="anycontext-p-0 anycontext-m-0"> - Import twitter bookmarks - </p> - </TooltipContent> - </Tooltip> - ) : ( - <></> - )} - <Dialog onOpenChange={(open) => open === true && fetchSpaces()}> - <Tooltip delayDuration={300}> - <TooltipTrigger - className="anycontext-bg-transparent - anycontext-border-none anycontext-m-0 anycontext-p-0 - " - > - <DialogTrigger asChild> - <button - disabled={savedWebsites.includes(window.location.href)} - className="anycontext-open-button disabled:anycontext-opacity-30 anycontext-bg-transparent - anycontext-border-none anycontext-m-0 anycontext-p-0" - > - {savedWebsites.includes(window.location.href) ? ( - <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="lucide lucide-file-check-2" - > - <path d="M4 22h14a2 2 0 0 0 2-2V7l-5-5H6a2 2 0 0 0-2 2v4" /> - <path d="M14 2v4a2 2 0 0 0 2 2h4" /> - <path d="m3 15 2 2 4-4" /> - </svg> - ) : ( - <svg - xmlns="http://www.w3.org/2000/svg" - viewBox="0 0 20 20" - fill="currentColor" - className={`anycontext-w-5 anycontext-h-5 ${isSendingData ? "anycontext-animate-spin" : ""}`} - > - <path d="M15.98 1.804a1 1 0 0 0-1.96 0l-.24 1.192a1 1 0 0 1-.784.785l-1.192.238a1 1 0 0 0 0 1.962l1.192.238a1 1 0 0 1 .785.785l.238 1.192a1 1 0 0 0 1.962 0l.238-1.192a1 1 0 0 1 .785-.785l1.192-.238a1 1 0 0 0 0-1.962l-1.192-.238a1 1 0 0 1-.785-.785l-.238-1.192ZM6.949 5.684a1 1 0 0 0-1.898 0l-.683 2.051a1 1 0 0 1-.633.633l-2.051.683a1 1 0 0 0 0 1.898l2.051.684a1 1 0 0 1 .633.632l.683 2.051a1 1 0 0 0 1.898 0l.683-2.051a1 1 0 0 1 .633-.633l2.051-.683a1 1 0 0 0 0-1.898l-2.051-.683a1 1 0 0 1-.633-.633L6.95 5.684ZM13.949 13.684a1 1 0 0 0-1.898 0l-.184.551a1 1 0 0 1-.632.633l-.551.183a1 1 0 0 0 0 1.898l.551.183a1 1 0 0 1 .633.633l.183.551a1 1 0 0 0 1.898 0l.184-.551a1 1 0 0 1 .632-.633l.551-.183a1 1 0 0 0 0-1.898l-.551-.184a1 1 0 0 1-.633-.632l-.183-.551Z" /> - </svg> - )} - </button> - </DialogTrigger> - </TooltipTrigger> - <TooltipContent className="anycontext-p-0" side="left"> - <p className="anycontext-p-0 anycontext-m-0"> - {savedWebsites.includes(window.location.href) - ? "Added to memory" - : "Add to memory"} - </p> - </TooltipContent> - </Tooltip> - <DialogContent> - <DialogHeader> - <DialogTitle>Add to Memory</DialogTitle> - <DialogDescription> - Add the current page to memory - </DialogDescription> - </DialogHeader> - - <FilterSpaces - loading={loading} - className="anycontext-mr-auto" - selectedSpaces={selectedSpaces} - setSelectedSpaces={setSelectedSpaces} - name={"Add to Spaces"} - spaces={spaces ?? []} - /> - <DialogFooter className="anycontext-w-full anycontext-text-sm"> - <DialogClose - onClick={() => { - sendUrlToAPI(selectedSpaces); - setIsSendingData(true); - setTimeout(() => { - setIsSendingData(false); - setSavedWebsites([ - ...savedWebsites, - window.location.href, - ]); - }, 1000); - }} - > - Add - </DialogClose> - <DialogClose>Cancel</DialogClose> - </DialogFooter> - </DialogContent> - </Dialog> - </div> - </TooltipProvider> - </> - ); -} - -export default SideBar; diff --git a/apps/extension/src/assets/Memories.tsx b/apps/extension/src/assets/Memories.tsx deleted file mode 100644 index 0c138b1e..00000000 --- a/apps/extension/src/assets/Memories.tsx +++ /dev/null @@ -1,69 +0,0 @@ -export const MemoryIcon: React.FC<React.SVGAttributes<SVGElement>> = ( - props, -) => ( - <svg - viewBox="0 0 89 53" - fill="none" - xmlns="http://www.w3.org/2000/svg" - {...props} - > - <rect - x="0.40697" - y="8.52821" - width="43.0286" - height="43.0286" - rx="5.5" - transform="rotate(-12 0.40697 8.52821)" - fill="var(--anycontext-icon-fill)" - stroke="var(--anycontext-icon-stroke)" - /> - <rect - x="20.8257" - y="9.19775" - width="43" - height="43" - rx="5.5" - fill="var(--anycontext-icon-fill)" - stroke="var(--anycontext-icon-stroke)" - /> - <rect - x="47.6965" - y="-0.612372" - width="43.0286" - height="43.0286" - rx="5.5" - transform="rotate(15 47.6965 -0.612372)" - fill="var(--anycontext-icon-fill)" - stroke="var(--anycontext-icon-stroke)" - /> - </svg> -); - -export const SpaceIcon: React.FC<React.SVGAttributes<SVGElement>> = (props) => ( - <svg - viewBox="0 0 34 30" - fill="none" - xmlns="http://www.w3.org/2000/svg" - {...props} - > - <rect - x="1.39502" - y="5.2229" - width="24" - height="24" - rx="5.5" - fill="var(--anycontext-icon-fill)" - stroke="var(--anycontext-icon-stroke)" - /> - <rect - x="11.2231" - y="-0.157702" - width="24" - height="24" - rx="5.5" - transform="rotate(20 11.2231 -0.157702)" - fill="var(--anycontext-icon-fill)" - stroke="var(--anycontext-icon-stroke)" - /> - </svg> -); diff --git a/apps/extension/src/assets/react.svg b/apps/extension/src/assets/react.svg deleted file mode 100644 index 6c87de9b..00000000 --- a/apps/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/apps/extension/src/background.ts b/apps/extension/src/background.ts deleted file mode 100644 index d2f8759e..00000000 --- a/apps/extension/src/background.ts +++ /dev/null @@ -1,161 +0,0 @@ -import { getEnv } from "./util"; -import { Space } from "./types/memory"; - -const backendUrl = - getEnv() === "development" - ? "http://localhost:3000" - : "https://supermemory.dhr.wtf"; - -interface TweetData { - tweetText: string; - postUrl: string; - authorName: string; - handle: string; - time: string; - saveToUser: string; -} - -// TODO: Implement getting bookmarks from Twitter API directly -// let authorizationHeader: string | null = null; -// let csrfToken: string | null = null; -// let cookies: string | null = null; - -// chrome.webRequest.onBeforeSendHeaders.addListener( -// (details) => { -// for (let i = 0; i < details.requestHeaders!.length; ++i) { -// const header = details.requestHeaders![i]; -// if (header.name.toLowerCase() === 'authorization') { -// authorizationHeader = header.value || null; -// } else if (header.name.toLowerCase() === 'x-csrf-token') { -// csrfToken = header.value || null; -// } else if (header.name.toLowerCase() === 'cookie') { -// cookies = header.value || null; -// } - -// console.log(header, authorizationHeader, csrfToken, cookies) -// } -// }, -// { urls: ['https://twitter.com/*', 'https://x.com/*'] }, -// ['requestHeaders'] -// ); - -chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { - if (request.type === "getJwt") { - chrome.storage.local.get(["jwt"], ({ jwt }) => { - sendResponse({ jwt }); - }); - - return true; - } else if (request.type === "urlChange") { - const content = request.content; - const url = request.url; - const spaces = request.spaces( - // eslint-disable-next-line no-unexpected-multiline - async () => { - chrome.storage.local.get(["jwt"], ({ jwt }) => { - if (!jwt) { - console.error("No JWT found"); - return; - } - fetch(`${backendUrl}/api/store`, { - method: "POST", - headers: { - Authorization: `Bearer ${jwt}`, - }, - body: JSON.stringify({ pageContent: content, url, spaces }), - }).then((ers) => console.log(ers.status)); - }); - }, - )(); - } else if (request.type === "fetchSpaces") { - chrome.storage.local.get(["jwt"], async ({ jwt }) => { - if (!jwt) { - console.error("No JWT found"); - return; - } - const resp = await fetch(`${backendUrl}/api/spaces`, { - headers: { - Authorization: `Bearer ${jwt}`, - }, - }); - - const data: { - message: "OK" | string; - data: Space[] | undefined; - } = await resp.json(); - - if (data.message === "OK" && data.data) { - sendResponse(data.data); - } - }); - - return true; - } else if (request.type === "queryApi") { - const input = request.input; - const jwt = request.jwt; - - (async () => { - await fetch(`${backendUrl}/api/ask`, { - method: "POST", - headers: { - Authorization: `Bearer ${jwt}`, - }, - body: JSON.stringify({ - query: input, - }), - }).then(async (response) => { - if (!response.body) { - throw new Error("No response body"); - } - if (!sender.tab?.id) { - throw new Error("No tab ID"); - } - const reader = response.body.getReader(); - // eslint-disable-next-line no-constant-condition - while (true) { - const { done, value } = await reader.read(); - if (done) break; - // For simplicity, we're sending chunks as they come. - // This might need to be adapted based on your data and needs. - const chunkAsString = new TextDecoder("utf-8") - .decode(value) - .replace("data: ", ""); - chrome.tabs.sendMessage(sender.tab.id, { - action: "streamData", - data: chunkAsString, - }); - } - // Notify the content script that the stream is complete. - chrome.tabs.sendMessage(sender.tab.id, { action: "streamEnd" }); - }); - // Indicate that sendResponse will be called asynchronously. - return true; - })(); - } - // TODO: Implement getting bookmarks from Twitter API directly - // else if (request.action === 'getAuthData') { - // sendResponse({ - // authorizationHeader: authorizationHeader, - // csrfToken: csrfToken, - // cookies: cookies - // }); - // } - else if (request.type === "sendBookmarkedTweets") { - const jwt = request.jwt; - const tweets = request.tweets as TweetData[]; - - (async () => { - await fetch(`${backendUrl}/api/vectorizeTweets`, { - method: "POST", - headers: { - Authorization: `Bearer ${jwt}`, - }, - body: JSON.stringify(tweets), - }).then(async (response) => { - return response.json(); - }); - })(); - - return true; - } -}); diff --git a/apps/extension/src/components/FilterCombobox.tsx b/apps/extension/src/components/FilterCombobox.tsx deleted file mode 100644 index 3c8779b6..00000000 --- a/apps/extension/src/components/FilterCombobox.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import * as React from "react"; -import { PlusCircleIcon, X } from "lucide-react"; -import { Space } from "../types/memory"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, -} from "./ui/dropdown-menu"; -import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu"; - -export interface Props extends React.ButtonHTMLAttributes<HTMLButtonElement> { - selectedSpaces: number[]; - setSelectedSpaces: ( - spaces: number[] | ((prev: number[]) => number[]), - ) => void; - name: string; - spaces: Space[]; - loading: boolean; -} - -export function FilterSpaces({ - loading, - selectedSpaces, - setSelectedSpaces, - spaces, -}: Props) { - console.log(selectedSpaces, spaces); - - const filteredSpaces = spaces.filter((space) => - selectedSpaces.includes(space.id), - ); - const leftSpaces = spaces.filter( - (space) => !selectedSpaces.includes(space.id), - ); - - if (loading) { - return "Loading..."; - } - - return ( - <div className="anycontext-flex anycontext-flex-wrap anycontext-gap-1 anycontext-text-sm anycontext-"> - {filteredSpaces.length < 1 && "Add to a space"} - {filteredSpaces.map((space) => ( - <SpaceItem - {...space} - key={space.id} - onRemove={() => - setSelectedSpaces((prev) => prev.filter((s) => s !== space.id)) - } - /> - ))} - {leftSpaces.length > 0 && ( - <DropdownMenu> - <DropdownMenuTrigger className="anycontext-rounded-full"> - <PlusCircleIcon - className="anycontext-w-5 anycontext-h-5 [--anycontext-icon-stroke:white] dark:[--anycontext-icon-stroke:black]" - stroke="var(--anycontext-icon-stroke)" - fill="currentColor" - /> - </DropdownMenuTrigger> - <DropdownMenuContent> - {leftSpaces.map((space) => ( - <> - {loading && "Loading..."} - <DropdownMenuItem - onClick={() => - setSelectedSpaces((prev) => [...prev, space.id]) - } - > - {space.name} - </DropdownMenuItem> - </> - ))} - </DropdownMenuContent> - </DropdownMenu> - )} - </div> - ); -} - -function SpaceItem({ name, onRemove }: Space & { onRemove: () => void }) { - return ( - <div className="anycontext-flex anycontext-justify-center anycontext-items-center anycontext-gap-2 anycontext-p-1 anycontext-pl-2 anycontext-pr-3 anycontext-rounded-full anycontext-bg-black/5 dark:anycontext-bg-white/5 anycontext-border-white/20 dark:anycontext-border-black/20 border"> - <button - onClick={onRemove} - className="anycontext-flex hover:anycontext-bg-transparent anycontext-justify-center anycontext-scale-110 anycontext-items-center focus-visible:anycontext-outline-none anycontext-rounded-full anycontext-w-3 anycontext-bg-black/5 dark:anycontext-bg-white/5 anycontext-h-3 anycontext-text-transparent hover:anycontext-text-black dark:hover:anycontext-text-white" - > - <X className="anycontext-w-3 anycontext-h-3" /> - </button> - {name} - </div> - ); -} diff --git a/apps/extension/src/components/ui/button.tsx b/apps/extension/src/components/ui/button.tsx deleted file mode 100644 index 6ca7d07a..00000000 --- a/apps/extension/src/components/ui/button.tsx +++ /dev/null @@ -1,58 +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( - "anycontext-inline-flex anycontext-items-center anycontext-justify-center anycontext-whitespace-nowrap anycontext-rounded-md anycontext-text-sm anycontext-font-medium anycontext-ring-offset-white anycontext-transition-colors focus-visible:anycontext-outline-none focus-visible:anycontext-ring-2 focus-visible:anycontext-ring-stone-950 focus-visible:anycontext-ring-offset-2 disabled:anycontext-pointer-events-none disabled:anycontext-opacity-50 dark:anycontext-ring-offset-stone-950 dark:focus-visible:anycontext-ring-stone-300", - { - variants: { - variant: { - default: - "anycontext-bg-stone-900 anycontext-text-stone-50 hover:anycontext-bg-stone-900/90 dark:anycontext-bg-stone-50 dark:anycontext-text-stone-900 dark:hover:anycontext-bg-stone-50/90", - destructive: - "anycontext-bg-red-500 anycontext-text-stone-50 hover:anycontext-bg-red-500/90 dark:anycontext-bg-red-900 dark:anycontext-text-stone-50 dark:hover:anycontext-bg-red-900/90", - outline: - "anycontext-border anycontext-border-stone-200 anycontext-bg-white hover:anycontext-bg-stone-100 hover:anycontext-text-stone-900 dark:anycontext-border-stone-800 dark:anycontext-bg-stone-950 dark:hover:anycontext-bg-stone-800 dark:hover:anycontext-text-stone-50", - secondary: - "anycontext-bg-stone-100 anycontext-text-stone-900 hover:anycontext-bg-stone-100/80 dark:anycontext-bg-stone-800 dark:anycontext-text-stone-50 dark:hover:anycontext-bg-stone-800/80", - ghost: - "hover:anycontext-bg-stone-100 hover:anycontext-text-stone-900 dark:hover:anycontext-bg-stone-800 dark:hover:anycontext-text-stone-50", - link: "anycontext-text-stone-900 anycontext-underline-offset-4 hover:anycontext-underline dark:anycontext-text-stone-50", - }, - size: { - default: "anycontext-h-10 anycontext-px-4 anycontext-py-2", - sm: "anycontext-h-9 anycontext-rounded-md anycontext-px-3", - lg: "anycontext-h-11 anycontext-rounded-md anycontext-px-8", - icon: "anycontext-h-10 anycontext-w-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 }; diff --git a/apps/extension/src/components/ui/command.tsx b/apps/extension/src/components/ui/command.tsx deleted file mode 100644 index 858b67f4..00000000 --- a/apps/extension/src/components/ui/command.tsx +++ /dev/null @@ -1,162 +0,0 @@ -import * as React from "react"; -import { type DialogProps } from "@radix-ui/react-dialog"; -import { Command as CommandPrimitive } from "cmdk"; -import { Search } from "lucide-react"; - -import { cn } from "../../lib/utils"; -import { Dialog, DialogContent } from "../../components/ui/dialog"; - -const Command = React.forwardRef< - React.ElementRef<typeof CommandPrimitive>, - React.ComponentPropsWithoutRef<typeof CommandPrimitive> ->(({ className, ...props }, ref) => ( - <CommandPrimitive - ref={ref} - className={cn( - "anycontext-flex anycontext-h-full anycontext-w-full anycontext-flex-col anycontext-overflow-hidden anycontext-rounded-md anycontext-bg-white anycontext-text-stone-950 dark:anycontext-bg-stone-950 dark:anycontext-text-stone-50", - className, - )} - {...props} - /> -)); -Command.displayName = CommandPrimitive.displayName; - -interface CommandDialogProps extends DialogProps {} - -const CommandDialog = ({ children, ...props }: CommandDialogProps) => { - return ( - <Dialog {...props}> - <DialogContent className="anycontext-overflow-hidden anycontext-p-0 anycontext-shadow-lg"> - <Command className="[&_[cmdk-group-heading]]:anycontext-px-2 [&_[cmdk-group-heading]]:anycontext-font-medium [&_[cmdk-group-heading]]:anycontext-text-stone-500 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:anycontext-pt-0 [&_[cmdk-group]]:anycontext-px-2 [&_[cmdk-input-wrapper]_svg]:anycontext-h-5 [&_[cmdk-input-wrapper]_svg]:anycontext-w-5 [&_[cmdk-input]]:anycontext-h-12 [&_[cmdk-item]]:anycontext-px-2 [&_[cmdk-item]]:anycontext-py-3 [&_[cmdk-item]_svg]:anycontext-h-5 [&_[cmdk-item]_svg]:anycontext-w-5 dark:[&_[cmdk-group-heading]]:anycontext-text-stone-400"> - {children} - </Command> - </DialogContent> - </Dialog> - ); -}; - -const CommandInput = React.forwardRef< - React.ElementRef<typeof CommandPrimitive.Input>, - React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input> ->(({ className, ...props }, ref) => ( - <div - className="anycontext-flex anycontext-items-center anycontext-border-b anycontext-px-3" - cmdk-input-wrapper="" - > - <Search className="anycontext-mr-2 anycontext-h-4 anycontext-w-4 anycontext-shrink-0 anycontext-opacity-50" /> - <CommandPrimitive.Input - ref={ref} - className={cn( - "anycontext-flex anycontext-h-11 anycontext-w-full anycontext-rounded-md anycontext-bg-transparent anycontext-py-3 anycontext-text-sm anycontext-outline-none placeholder:anycontext-text-stone-500 disabled:anycontext-cursor-not-allowed disabled:anycontext-opacity-50 dark:placeholder:anycontext-text-stone-400", - className, - )} - {...props} - /> - </div> -)); - -CommandInput.displayName = CommandPrimitive.Input.displayName; - -const CommandList = React.forwardRef< - React.ElementRef<typeof CommandPrimitive.List>, - React.ComponentPropsWithoutRef<typeof CommandPrimitive.List> ->(({ className, ...props }, ref) => ( - <CommandPrimitive.List - ref={ref} - className={cn( - "anycontext-max-h-[300px] anycontext-overflow-y-auto anycontext-overflow-x-hidden", - className, - )} - {...props} - /> -)); - -CommandList.displayName = CommandPrimitive.List.displayName; - -const CommandEmpty = React.forwardRef< - React.ElementRef<typeof CommandPrimitive.Empty>, - React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty> ->((props, ref) => ( - <CommandPrimitive.Empty - ref={ref} - className="anycontext-py-6 anycontext-text-center anycontext-text-sm" - {...props} - /> -)); - -CommandEmpty.displayName = CommandPrimitive.Empty.displayName; - -const CommandGroup = React.forwardRef< - React.ElementRef<typeof CommandPrimitive.Group>, - React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group> ->(({ className, ...props }, ref) => ( - <CommandPrimitive.Group - ref={ref} - className={cn( - "anycontext-overflow-hidden anycontext-p-1 anycontext-text-stone-950 [&_[cmdk-group-heading]]:anycontext-px-2 [&_[cmdk-group-heading]]:anycontext-py-1.5 [&_[cmdk-group-heading]]:anycontext-text-xs [&_[cmdk-group-heading]]:anycontext-font-medium [&_[cmdk-group-heading]]:anycontext-text-stone-500 dark:anycontext-text-stone-50 dark:[&_[cmdk-group-heading]]:anycontext-text-stone-400", - className, - )} - {...props} - /> -)); - -CommandGroup.displayName = CommandPrimitive.Group.displayName; - -const CommandSeparator = React.forwardRef< - React.ElementRef<typeof CommandPrimitive.Separator>, - React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator> ->(({ className, ...props }, ref) => ( - <CommandPrimitive.Separator - ref={ref} - className={cn( - "anycontext--mx-1 anycontext-h-px anycontext-bg-stone-200 dark:anycontext-bg-stone-800", - className, - )} - {...props} - /> -)); -CommandSeparator.displayName = CommandPrimitive.Separator.displayName; - -const CommandItem = React.forwardRef< - React.ElementRef<typeof CommandPrimitive.Item>, - React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item> ->(({ className, ...props }, ref) => ( - <CommandPrimitive.Item - ref={ref} - className={cn( - "anycontext-relative anycontext-flex anycontext-cursor-default anycontext-select-none anycontext-items-center anycontext-rounded-sm anycontext-px-2 anycontext-py-1.5 anycontext-text-sm anycontext-outline-none aria-selected:anycontext-bg-stone-100 aria-selected:anycontext-text-stone-900 data-[disabled]:anycontext-pointer-events-none data-[disabled]:anycontext-opacity-50 dark:aria-selected:anycontext-bg-stone-800 dark:aria-selected:anycontext-text-stone-50", - className, - )} - {...props} - /> -)); - -CommandItem.displayName = CommandPrimitive.Item.displayName; - -const CommandShortcut = ({ - className, - ...props -}: React.HTMLAttributes<HTMLSpanElement>) => { - return ( - <span - className={cn( - "anycontext-ml-auto anycontext-text-xs anycontext-tracking-widest anycontext-text-stone-500 dark:anycontext-text-stone-400", - className, - )} - {...props} - /> - ); -}; -CommandShortcut.displayName = "CommandShortcut"; - -export { - Command, - CommandDialog, - CommandInput, - CommandList, - CommandEmpty, - CommandGroup, - CommandItem, - CommandShortcut, - CommandSeparator, -}; diff --git a/apps/extension/src/components/ui/dialog.tsx b/apps/extension/src/components/ui/dialog.tsx deleted file mode 100644 index 583c335d..00000000 --- a/apps/extension/src/components/ui/dialog.tsx +++ /dev/null @@ -1,123 +0,0 @@ -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( - "anycontext-fixed anycontext-inset-0 anycontext-z-50 anycontext-bg-black/80 anycontext- data-[state=open]:anycontext-animate-in data-[state=closed]:anycontext-animate-out data-[state=closed]:anycontext-fade-out-0 data-[state=open]:anycontext-fade-in-0", - 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( - "anycontext-text-black dark:anycontext-text-white anycontext-fixed anycontext-left-[50%] anycontext-top-[50%] anycontext-z-50 anycontext-grid anycontext-w-full anycontext-max-w-lg anycontext-translate-x-[-50%] anycontext-translate-y-[-50%] anycontext-gap-4 anycontext-border anycontext-border-stone-200 anycontext-bg-white anycontext-p-6 anycontext-shadow-lg anycontext-duration-200 data-[state=open]:anycontext-animate-in data-[state=closed]:anycontext-animate-out data-[state=closed]:anycontext-fade-out-0 data-[state=open]:anycontext-fade-in-0 data-[state=closed]:anycontext-zoom-out-95 data-[state=open]:anycontext-zoom-in-95 data-[state=closed]:anycontext-slide-out-to-left-1/2 data-[state=closed]:anycontext-slide-out-to-top-[48%] data-[state=open]:anycontext-slide-in-from-left-1/2 data-[state=open]:anycontext-slide-in-from-top-[48%] sm:anycontext-rounded-lg dark:anycontext-border-stone-800 dark:anycontext-bg-stone-950", - className, - )} - {...props} - > - {children} - <DialogPrimitive.Close className="anycontext-absolute anycontext-right-4 anycontext-top-4 anycontext-rounded-sm anycontext-opacity-70 anycontext-ring-offset-white anycontext-transition-opacity hover:anycontext-opacity-100 focus:anycontext-outline-none focus:anycontext-ring-2 focus:anycontext-ring-stone-950 focus:anycontext-ring-offset-2 disabled:anycontext-pointer-events-none data-[state=open]:anycontext-bg-stone-100 data-[state=open]:anycontext-text-stone-500 dark:anycontext-ring-offset-stone-950 dark:focus:anycontext-ring-stone-300 dark:data-[state=open]:anycontext-bg-stone-800 dark:data-[state=open]:anycontext-text-stone-400"> - <X className="anycontext-h-4 anycontext-w-4" /> - <span className="anycontext-sr-only">Close</span> - </DialogPrimitive.Close> - </DialogPrimitive.Content> - </DialogPortal> -)); -DialogContent.displayName = DialogPrimitive.Content.displayName; - -const DialogHeader = ({ - className, - ...props -}: React.HTMLAttributes<HTMLDivElement>) => ( - <div - className={cn( - "anycontext-flex anycontext-flex-col anycontext-space-y-1.5 anycontext-text-center sm:anycontext-text-left", - className, - )} - {...props} - /> -); -DialogHeader.displayName = "DialogHeader"; - -const DialogFooter = ({ - className, - ...props -}: React.HTMLAttributes<HTMLDivElement>) => ( - <div - className={cn( - "anycontext-flex anycontext-flex-col-reverse sm:anycontext-flex-row sm:anycontext-justify-end sm:anycontext-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( - "anycontext-text-lg anycontext-font-semibold anycontext-leading-none anycontext-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( - "anycontext-text-sm anycontext-text-stone-500 dark:anycontext-text-stone-400", - className, - )} - {...props} - /> -)); -DialogDescription.displayName = DialogPrimitive.Description.displayName; - -export { - Dialog, - DialogPortal, - DialogOverlay, - DialogClose, - DialogTrigger, - DialogContent, - DialogHeader, - DialogFooter, - DialogTitle, - DialogDescription, -}; diff --git a/apps/extension/src/components/ui/dropdown-menu.tsx b/apps/extension/src/components/ui/dropdown-menu.tsx deleted file mode 100644 index fcc1edb2..00000000 --- a/apps/extension/src/components/ui/dropdown-menu.tsx +++ /dev/null @@ -1,204 +0,0 @@ -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( - "anycontext-flex anycontext-cursor-default anycontext-select-none anycontext-items-center anycontext-rounded-sm anycontext-px-2 anycontext-py-1.5 anycontext-text-sm anycontext-outline-none focus:anycontext-bg-stone-100 data-[state=open]:anycontext-bg-stone-100 dark:focus:anycontext-bg-stone-800 dark:data-[state=open]:anycontext-bg-stone-800", - inset && "anycontext-pl-8", - className, - )} - {...props} - > - {children} - <ChevronRight className="anycontext-ml-auto anycontext-h-4 anycontext-w-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( - "anycontext-z-50 anycontext-min-w-[8rem] anycontext-overflow-hidden anycontext-rounded-md anycontext-border anycontext-border-stone-200 anycontext-bg-white anycontext-p-1 anycontext-text-stone-950 anycontext-shadow-lg data-[state=open]:anycontext-animate-in data-[state=closed]:anycontext-animate-out data-[state=closed]:anycontext-fade-out-0 data-[state=open]:anycontext-fade-in-0 data-[state=closed]:anycontext-zoom-out-95 data-[state=open]:anycontext-zoom-in-95 data-[side=bottom]:anycontext-slide-in-from-top-2 data-[side=left]:anycontext-slide-in-from-right-2 data-[side=right]:anycontext-slide-in-from-left-2 data-[side=top]:anycontext-slide-in-from-bottom-2 dark:anycontext-border-stone-800 dark:anycontext-bg-stone-950 dark:anycontext-text-stone-50", - 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( - "anycontext-z-50 anycontext-min-w-[8rem] anycontext-overflow-hidden anycontext-rounded-md anycontext-border anycontext-border-stone-200 anycontext-bg-white anycontext-p-1 anycontext-text-stone-950 anycontext-shadow-md data-[state=open]:anycontext-animate-in data-[state=closed]:anycontext-animate-out data-[state=closed]:anycontext-fade-out-0 data-[state=open]:anycontext-fade-in-0 data-[state=closed]:anycontext-zoom-out-95 data-[state=open]:anycontext-zoom-in-95 data-[side=bottom]:anycontext-slide-in-from-top-2 data-[side=left]:anycontext-slide-in-from-right-2 data-[side=right]:anycontext-slide-in-from-left-2 data-[side=top]:anycontext-slide-in-from-bottom-2 dark:anycontext-border-stone-800 dark:anycontext-bg-stone-950 dark:anycontext-text-stone-50", - 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( - "anycontext-relative anycontext-flex anycontext-cursor-default anycontext-select-none anycontext-items-center anycontext-rounded-sm anycontext-px-2 anycontext-py-1.5 anycontext-text-sm anycontext-outline-none anycontext-transition-colors focus:anycontext-bg-stone-100 focus:anycontext-text-stone-900 data-[disabled]:anycontext-pointer-events-none data-[disabled]:anycontext-opacity-50 dark:focus:anycontext-bg-stone-800 dark:focus:anycontext-text-stone-50", - inset && "anycontext-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( - "anycontext-relative anycontext-flex anycontext-cursor-default anycontext-select-none anycontext-items-center anycontext-rounded-sm anycontext-py-1.5 anycontext-pl-8 anycontext-pr-2 anycontext-text-sm anycontext-outline-none anycontext-transition-colors focus:anycontext-bg-stone-100 focus:anycontext-text-stone-900 data-[disabled]:anycontext-pointer-events-none data-[disabled]:anycontext-opacity-50 dark:focus:anycontext-bg-stone-800 dark:focus:anycontext-text-stone-50", - className, - )} - checked={checked} - {...props} - > - <span className="anycontext-absolute anycontext-left-2 anycontext-flex anycontext-h-3.5 anycontext-w-3.5 anycontext-items-center anycontext-justify-center"> - <DropdownMenuPrimitive.ItemIndicator> - <Check className="anycontext-h-4 anycontext-w-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( - "anycontext-relative anycontext-flex anycontext-cursor-default anycontext-select-none anycontext-items-center anycontext-rounded-sm anycontext-py-1.5 anycontext-pl-8 anycontext-pr-2 anycontext-text-sm anycontext-outline-none anycontext-transition-colors focus:anycontext-bg-stone-100 focus:anycontext-text-stone-900 data-[disabled]:anycontext-pointer-events-none data-[disabled]:anycontext-opacity-50 dark:focus:anycontext-bg-stone-800 dark:focus:anycontext-text-stone-50", - className, - )} - {...props} - > - <span className="anycontext-absolute anycontext-left-2 anycontext-flex anycontext-h-3.5 anycontext-w-3.5 anycontext-items-center anycontext-justify-center"> - <DropdownMenuPrimitive.ItemIndicator> - <Circle className="anycontext-h-2 anycontext-w-2 anycontext-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( - "anycontext-px-2 anycontext-py-1.5 anycontext-text-sm anycontext-font-semibold", - inset && "anycontext-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( - "anycontext--mx-1 anycontext-my-1 anycontext-h-px anycontext-bg-stone-100 dark:anycontext-bg-stone-800", - className, - )} - {...props} - /> -)); -DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; - -const DropdownMenuShortcut = ({ - className, - ...props -}: React.HTMLAttributes<HTMLSpanElement>) => { - return ( - <span - className={cn( - "anycontext-ml-auto anycontext-text-xs anycontext-tracking-widest anycontext-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/apps/extension/src/components/ui/input.tsx b/apps/extension/src/components/ui/input.tsx deleted file mode 100644 index 4771795a..00000000 --- a/apps/extension/src/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( - "anycontext-flex anycontext-h-10 anycontext-w-full anycontext-rounded-md anycontext-border anycontext-border-stone-200 anycontext-bg-white anycontext-px-3 anycontext-py-2 anycontext-text-sm anycontext-ring-offset-white file:anycontext-border-0 file:anycontext-bg-transparent file:anycontext-text-sm file:anycontext-font-medium placeholder:anycontext-text-stone-500 focus-visible:anycontext-outline-none focus-visible:anycontext-ring-2 focus-visible:anycontext-ring-stone-950 focus-visible:anycontext-ring-offset-2 disabled:anycontext-cursor-not-allowed disabled:anycontext-opacity-50 dark:anycontext-border-stone-800 dark:anycontext-bg-stone-950 dark:anycontext-ring-offset-stone-950 dark:placeholder:anycontext-text-stone-400 dark:focus-visible:anycontext-ring-stone-300", - className, - )} - ref={ref} - {...props} - /> - ); - }, -); -Input.displayName = "Input"; - -export { Input }; diff --git a/apps/extension/src/components/ui/popover.tsx b/apps/extension/src/components/ui/popover.tsx deleted file mode 100644 index e1b9282d..00000000 --- a/apps/extension/src/components/ui/popover.tsx +++ /dev/null @@ -1,29 +0,0 @@ -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( - "anycontext-z-50 anycontext-w-72 anycontext-rounded-md anycontext-border anycontext-border-stone-200 anycontext-bg-white anycontext-p-4 anycontext-text-stone-950 anycontext-shadow-md anycontext-outline-none data-[state=open]:anycontext-animate-in data-[state=closed]:anycontext-animate-out data-[state=closed]:anycontext-fade-out-0 data-[state=open]:anycontext-fade-in-0 data-[state=closed]:anycontext-zoom-out-95 data-[state=open]:anycontext-zoom-in-95 data-[side=bottom]:anycontext-slide-in-from-top-2 data-[side=left]:anycontext-slide-in-from-right-2 data-[side=right]:anycontext-slide-in-from-left-2 data-[side=top]:anycontext-slide-in-from-bottom-2 dark:anycontext-border-stone-800 dark:anycontext-bg-stone-950 dark:anycontext-text-stone-50", - className, - )} - {...props} - /> - </PopoverPrimitive.Portal> -)); -PopoverContent.displayName = PopoverPrimitive.Content.displayName; - -export { Popover, PopoverTrigger, PopoverContent }; diff --git a/apps/extension/src/components/ui/tooltip.tsx b/apps/extension/src/components/ui/tooltip.tsx deleted file mode 100644 index 12185db5..00000000 --- a/apps/extension/src/components/ui/tooltip.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import * as React from "react"; -import * as TooltipPrimitive from "@radix-ui/react-tooltip"; - -import { cn } from "../../lib/utils"; - -const TooltipProvider = TooltipPrimitive.Provider; - -const Tooltip = TooltipPrimitive.Root; - -const TooltipTrigger = TooltipPrimitive.Trigger; - -const TooltipContent = React.forwardRef< - React.ElementRef<typeof TooltipPrimitive.Content>, - React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content> ->(({ className, sideOffset = 4, ...props }, ref) => ( - <TooltipPrimitive.Content - ref={ref} - sideOffset={sideOffset} - className={cn( - "anycontext-z-50 anycontext-overflow-hidden anycontext-rounded-md anycontext-border anycontext-border-stone-200 anycontext-bg-white anycontext-px-3 anycontext-py-1.5 anycontext-text-sm anycontext-text-stone-950 anycontext-shadow-md anycontext-animate-in anycontext-fade-in-0 anycontext-zoom-in-95 data-[state=closed]:anycontext-animate-out data-[state=closed]:anycontext-fade-out-0 data-[state=closed]:anycontext-zoom-out-95 data-[side=bottom]:anycontext-slide-in-from-top-2 data-[side=left]:anycontext-slide-in-from-right-2 data-[side=right]:anycontext-slide-in-from-left-2 data-[side=top]:anycontext-slide-in-from-bottom-2 dark:anycontext-border-stone-800 dark:anycontext-bg-stone-950 dark:anycontext-text-stone-50", - className, - )} - {...props} - /> -)); -TooltipContent.displayName = TooltipPrimitive.Content.displayName; - -export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; diff --git a/apps/extension/src/content.tsx b/apps/extension/src/content.tsx deleted file mode 100644 index 83d976cd..00000000 --- a/apps/extension/src/content.tsx +++ /dev/null @@ -1,45 +0,0 @@ -window.addEventListener("message", (event) => { - if (event.source !== window) { - return; - } - const { jwt } = event.data; - - if (jwt) { - if ( - !( - window.location.hostname === "localhost" || - window.location.hostname === "anycontext.dhr.wtf" || - window.location.hostname === "supermemory.dhr.wtf" - ) - ) { - console.log( - "JWT is only allowed to be used on localhost or anycontext.dhr.wtf", - ); - return; - } - - chrome.storage.local.set({ jwt }, () => {}); - } -}); - -const appContainer = document.createElement("div"); -appContainer.id = "anycontext-app-container"; - -// First in the body, above the content -document.body.insertBefore(appContainer, document.body.firstChild); - -appContainer.style.zIndex = "9999"; - -import ReactDOM from "react-dom/client"; -import SideBar from "./SideBar"; - -// get JWT from local storage -const jwt = chrome.storage.local.get("jwt").then((data) => { - return data.jwt; -}) as Promise<string>; - -jwt.then((token) => { - ReactDOM.createRoot( - document.getElementById("anycontext-app-container")!, - ).render(<SideBar jwt={token} />); -}); diff --git a/apps/extension/src/ext.css b/apps/extension/src/ext.css deleted file mode 100644 index bf7a4156..00000000 --- a/apps/extension/src/ext.css +++ /dev/null @@ -1,85 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -.anycontext-combobox-button { - padding: 0.5rem 1rem; - display: flex; - flex-direction: row; - justify-items: center; - align-items: center; - gap: 0.5rem; - @apply anycontext-rounded-md dark:anycontext-bg-white/5 anycontext-bg-black/5; -} - -.anycontext-overlay { - position: fixed; - top: 0; - left: 0; - min-height: 100vh; - width: 100%; - height: 100%; - background-color: rgba(0, 0, 0, 0.5); - z-index: 99998; -} - -.anycontext-sidebar { - position: fixed; - top: 0; - right: 0; - min-height: 100vh; - width: 100%; - max-width: 31%; /* Responsive width */ - z-index: 99999; - padding: 8px 16px; /* px-4 py-2 */ -} - -.anycontext-sidebar-content { - position: relative; - display: flex; - flex-direction: column; - height: 95vh; - background-color: white; - border-radius: 8px; /* rounded-lg */ - padding: 8px; /* px-2 */ - box-shadow: - 0 4px 6px -1px rgba(0, 0, 0, 0.1), - 0 2px 4px -1px rgba(0, 0, 0, 0.06); /* shadow-md */ -} - -.anycontext-close-button { - position: absolute; - right: 0; - padding: 8px; /* p-2 */ - border-radius: 4px; /* rounded-md */ - margin: 8px; /* m-2 */ -} - -.anycontext-close-button:hover { - background-color: rgba(114, 87, 255, 0.5); /* hover:bg-[#7257ff]/50 */ - color: white; /* hover:text-white */ -} - -.anycontext-open-button { - color: white; - background-color: #7257ff50; /* bg-indigo-600 */ - background-opacity: 75%; - cursor: pointer; - padding: 8px; /* px-4 py-2 */ - border-radius: 4px 0 0 4px; /* rounded-l-md */ - display: flex; - align-items: center; - justify-content: space-between; -} - -.anycontext-header { - margin: 16px; /* m-4 */ - font-weight: 600; /* font-semibold */ - font-size: 1.25rem; /* text-xl */ - color: black; -} - -.anycontext-icon { - height: 24px; /* h-6 */ - width: 24px; /* w-6 */ -} diff --git a/apps/extension/src/lib/utils.ts b/apps/extension/src/lib/utils.ts deleted file mode 100644 index 365058ce..00000000 --- a/apps/extension/src/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/apps/extension/src/main.tsx b/apps/extension/src/main.tsx deleted file mode 100644 index b5c00920..00000000 --- a/apps/extension/src/main.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from "react"; -import ReactDOM from "react-dom/client"; -import App from "./App.tsx"; - -ReactDOM.createRoot(document.getElementById("root")!).render( - <React.StrictMode> - <App /> - </React.StrictMode>, -); diff --git a/apps/extension/src/types/memory.ts b/apps/extension/src/types/memory.ts deleted file mode 100644 index 03ffb848..00000000 --- a/apps/extension/src/types/memory.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type Space = { - id: number; - name: string; -}; diff --git a/apps/extension/src/types/zods.ts b/apps/extension/src/types/zods.ts deleted file mode 100644 index 3316aa16..00000000 --- a/apps/extension/src/types/zods.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { z } from "zod"; - -export const userObj = z.object({ - message: z.string(), - data: z.object({ - session: z.object({ - sessionToken: z.string(), - userId: z.string(), - expires: z.string(), - }), - user: z.object({ - id: z.string(), - name: z.string(), - email: z.string().nullable().optional(), - emailVerified: z.string().nullable(), - image: z.string().nullable().optional(), - }), - }), -}); diff --git a/apps/extension/src/util.ts b/apps/extension/src/util.ts deleted file mode 100644 index d2ea35d3..00000000 --- a/apps/extension/src/util.ts +++ /dev/null @@ -1,13 +0,0 @@ -export const getEnv = () => { - // chrome.management.getSelf((self) => { - // if (self.installType === 'development') { - // return "development" - // } - // else { - // return "production" - // } - // }) - - // return null - return "production"; -}; diff --git a/apps/extension/src/vite-env.d.ts b/apps/extension/src/vite-env.d.ts deleted file mode 100644 index 11f02fe2..00000000 --- a/apps/extension/src/vite-env.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// <reference types="vite/client" /> diff --git a/apps/extension/tailwind.config.js b/apps/extension/tailwind.config.js deleted file mode 100644 index ed971842..00000000 --- a/apps/extension/tailwind.config.js +++ /dev/null @@ -1,42 +0,0 @@ -import tailwindcssAnimate from "tailwindcss-animate"; -/** @type {import('tailwindcss').Config} */ -export default { - //darkMode: "prefe", - content: [ - "./pages/**/*.{ts,tsx}", - "./components/**/*.{ts,tsx}", - "./app/**/*.{ts,tsx}", - "./src/**/*.{ts,tsx}", - "index.html", - ], - prefix: "anycontext-", - theme: { - container: { - center: true, - padding: "2rem", - screens: { - "2xl": "1400px", - }, - }, - extend: { - 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", - }, - }, - }, - plugins: [tailwindcssAnimate], - corePlugins: { - preflight: false, - }, -}; diff --git a/apps/extension/tsconfig.json b/apps/extension/tsconfig.json deleted file mode 100644 index 2fefaeb1..00000000 --- a/apps/extension/tsconfig.json +++ /dev/null @@ -1,26 +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" }], - "types": ["chrome"] -} diff --git a/apps/extension/tsconfig.node.json b/apps/extension/tsconfig.node.json deleted file mode 100644 index 97ede7ee..00000000 --- a/apps/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/apps/extension/vite.config.ts b/apps/extension/vite.config.ts deleted file mode 100644 index c2b53f80..00000000 --- a/apps/extension/vite.config.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Plugin, defineConfig } from "vite"; -import react from "@vitejs/plugin-react"; -import { crx } from "@crxjs/vite-plugin"; -import manifest from "./manifest.json"; -import path from "path"; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const viteManifestHackIssue846: Plugin & { - renderCrxManifest: (manifest: unknown, bundle: any) => void; -} = { - // Workaround from https://github.com/crxjs/chrome-extension-tools/issues/846#issuecomment-1861880919. - name: "manifestHackIssue846", - renderCrxManifest(_manifest, bundle) { - bundle["manifest.json"] = bundle[".vite/manifest.json"]; - bundle["manifest.json"].fileName = "manifest.json"; - delete bundle[".vite/manifest.json"]; - }, -}; - -export default defineConfig({ - plugins: [react(), crx({ manifest }), viteManifestHackIssue846], - resolve: { - alias: { - "@": path.resolve(__dirname, "./src"), - }, - }, -}); diff --git a/apps/web-v2/.eslintrc.json b/apps/web-v2/.eslintrc.json deleted file mode 100644 index abd7bea7..00000000 --- a/apps/web-v2/.eslintrc.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": [ - "next/core-web-vitals", - "plugin:eslint-plugin-next-on-pages/recommended" - ], - "plugins": ["eslint-plugin-next-on-pages"] -} diff --git a/apps/web-v2/.gitignore b/apps/web-v2/.gitignore deleted file mode 100644 index c213988d..00000000 --- a/apps/web-v2/.gitignore +++ /dev/null @@ -1,40 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js -.yarn/install-state.gz - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# local env files -.env*.local - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts - -# wrangler files -.wrangler -.dev.vars diff --git a/apps/web-v2/README.md b/apps/web-v2/README.md deleted file mode 100644 index 64c8f3bc..00000000 --- a/apps/web-v2/README.md +++ /dev/null @@ -1,70 +0,0 @@ -This is a [Next.js](https://nextjs.org/) project bootstrapped with [`c3`](https://developers.cloudflare.com/pages/get-started/c3). - -## 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. - -## Cloudflare integration - -Besides the `dev` script mentioned above `c3` has added a few extra scripts that allow you to integrate the application with the [Cloudflare Pages](https://pages.cloudflare.com/) environment, these are: - -- `pages:build` to build the application for Pages using the [`@cloudflare/next-on-pages`](https://github.com/cloudflare/next-on-pages) CLI -- `preview` to locally preview your Pages application using the [Wrangler](https://developers.cloudflare.com/workers/wrangler/) CLI -- `deploy` to deploy your Pages application using the [Wrangler](https://developers.cloudflare.com/workers/wrangler/) CLI - -> **Note:** while the `dev` script is optimal for local development you should preview your Pages application as well (periodically or before deployments) in order to make sure that it can properly work in the Pages environment (for more details see the [`@cloudflare/next-on-pages` recommended workflow](https://github.com/cloudflare/next-on-pages/blob/05b6256/internal-packages/next-dev/README.md#recommended-workflow)) - -### Bindings - -Cloudflare [Bindings](https://developers.cloudflare.com/pages/functions/bindings/) are what allows you to interact with resources available in the Cloudflare Platform. - -You can use bindings during development, when previewing locally your application and of course in the deployed application: - -- To use bindings in dev mode you need to define them in the `next.config.js` file under `setupDevBindings`, this mode uses the `next-dev` `@cloudflare/next-on-pages` submodule. For more details see its [documentation](https://github.com/cloudflare/next-on-pages/blob/05b6256/internal-packages/next-dev/README.md). - -- To use bindings in the preview mode you need to add them to the `pages:preview` script accordingly to the `wrangler pages dev` command. For more details see its [documentation](https://developers.cloudflare.com/workers/wrangler/commands/#dev-1) or the [Pages Bindings documentation](https://developers.cloudflare.com/pages/functions/bindings/). - -- To use bindings in the deployed application you will need to configure them in the Cloudflare [dashboard](https://dash.cloudflare.com/). For more details see the [Pages Bindings documentation](https://developers.cloudflare.com/pages/functions/bindings/). - -#### KV Example - -`c3` has added for you an example showing how you can use a KV binding. - -In order to enable the example: - -- Search for javascript/typescript lines containing the following comment: - ```ts - // KV Example: - ``` - and uncomment the commented lines below it. -- Do the same in the `wrangler.toml` file, where - the comment is: - ``` - # KV Example: - ``` -- If you're using TypeScript run the `cf-typegen` script to update the `env.d.ts` file: - ```bash - npm run cf-typegen - # or - yarn cf-typegen - # or - pnpm cf-typegen - # or - bun cf-typegen - ``` - -After doing this you can run the `dev` or `preview` script and visit the `/api/hello` route to see the example in action. - -Finally, if you also want to see the example work in the deployed application make sure to add a `MY_KV_NAMESPACE` binding to your Pages application in its [dashboard kv bindings settings section](https://dash.cloudflare.com/?to=/:account/pages/view/:pages-project/settings/functions#kv_namespace_bindings_section). After having configured it make sure to re-deploy your application. diff --git a/apps/web-v2/components.json b/apps/web-v2/components.json deleted file mode 100644 index 268ae38d..00000000 --- a/apps/web-v2/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": "src/app/globals.css", - "baseColor": "zinc", - "cssVariables": true, - "prefix": "" - }, - "aliases": { - "components": "@/components", - "utils": "@/lib/utils" - } -} diff --git a/apps/web-v2/env.d.ts b/apps/web-v2/env.d.ts deleted file mode 100644 index ac17de2c..00000000 --- a/apps/web-v2/env.d.ts +++ /dev/null @@ -1,47 +0,0 @@ -// Generated by Wrangler -// by running `wrangler types --env-interface CloudflareEnv env.d.ts` - -interface CloudflareEnv { - RATELIMITER: RateLimitBinding; -} - -export interface RateLimitBinding { - limit: LimitFunc; -} - -export interface LimitFunc { - (options: LimitOptions): Promise<RateLimitResult>; -} - -interface RateLimitResult { - success: boolean; -} - -export interface LimitOptions { - key: string; -} - -export interface RateLimitResponse { - key: string; - success: boolean; -} - -export interface RateLimitOptions { - continueOnRateLimit: boolean; -} - -export type RateLimitKeyFunc = { - (c: Context): string; -}; - -declare global { - namespace NodeJS { - interface ProcessEnv { - // [key: string]: string | undefined; - RATELIMITER: RateLimitBinding; - CLOUDFLARE_TURNSTILE_TOKEN: string; - } - } -} - -export {}; diff --git a/apps/web-v2/next.config.mjs b/apps/web-v2/next.config.mjs deleted file mode 100644 index b0c1476c..00000000 --- a/apps/web-v2/next.config.mjs +++ /dev/null @@ -1,13 +0,0 @@ -import { setupDevPlatform } from "@cloudflare/next-on-pages/next-dev"; - -// Here we use the @cloudflare/next-on-pages next-dev module to allow us to use bindings during local development -// (when running the application with `next dev`), for more information see: -// https://github.com/cloudflare/next-on-pages/blob/5712c57ea7/internal-packages/next-dev/README.md -if (process.env.NODE_ENV === "development") { - await setupDevPlatform(); -} - -/** @type {import('next').NextConfig} */ -const nextConfig = {}; - -export default nextConfig; diff --git a/apps/web-v2/package.json b/apps/web-v2/package.json deleted file mode 100644 index 69a39888..00000000 --- a/apps/web-v2/package.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "name": "web-v2", - "version": "0.1.0", - "private": true, - "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start", - "lint": "next lint", - "pages:build": "bunx @cloudflare/next-on-pages", - "preview": "bun pages:build && wrangler pages dev", - "deploy": "bun pages:build && wrangler pages deploy", - "cf-typegen": "wrangler types --env-interface CloudflareEnv env.d.ts" - }, - "dependencies": { - "@radix-ui/react-toast": "^1.1.5", - "class-variance-authority": "^0.7.0", - "clsx": "^2.1.1", - "lucide-react": "^0.378.0", - "next": "14.1.0", - "react": "^18", - "react-dom": "^18", - "tailwind-merge": "^2.3.0", - "tailwindcss-animate": "^1.0.7" - }, - "devDependencies": { - "@cloudflare/next-on-pages": "1", - "@cloudflare/workers-types": "^4.20240512.0", - "@types/node": "^20", - "@types/react": "^18", - "@types/react-dom": "^18", - "autoprefixer": "^10.0.1", - "eslint": "^8", - "eslint-config-next": "14.1.0", - "eslint-plugin-next-on-pages": "^1.11.3", - "postcss": "^8", - "tailwindcss": "^3.3.0", - "typescript": "^5", - "vercel": "^34.2.0", - "wrangler": "^3.57.0" - } -} diff --git a/apps/web-v2/postcss.config.js b/apps/web-v2/postcss.config.js deleted file mode 100644 index 12a703d9..00000000 --- a/apps/web-v2/postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -}; diff --git a/apps/web-v2/public/favicon.ico b/apps/web-v2/public/favicon.ico Binary files differdeleted file mode 100644 index dd86c6dd..00000000 --- a/apps/web-v2/public/favicon.ico +++ /dev/null diff --git a/apps/web-v2/public/og-image.png b/apps/web-v2/public/og-image.png Binary files differdeleted file mode 100644 index c5a61b17..00000000 --- a/apps/web-v2/public/og-image.png +++ /dev/null diff --git a/apps/web-v2/public/site.webmanifest b/apps/web-v2/public/site.webmanifest deleted file mode 100644 index c903e516..00000000 --- a/apps/web-v2/public/site.webmanifest +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "Supermemory - your second brain.", - "short_name": "Supermemory", - "icons": [ - { - "src": "/icons/android-chrome-192x192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "/icons/android-chrome-512x512.png", - "sizes": "512x512", - "type": "image/png" - } - ], - "theme_color": "#ffffff", - "background_color": "#ffffff", - "display": "standalone" -} diff --git a/apps/web-v2/src/app/(landing)/page.tsx b/apps/web-v2/src/app/(landing)/page.tsx deleted file mode 100644 index 5351c51b..00000000 --- a/apps/web-v2/src/app/(landing)/page.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import RotatingIcons from "./RotatingIcons"; -import Hero from "./Hero"; -import Navbar from "./Navbar"; -import Cta from "./Cta"; -import { Toaster } from "@/components/ui/toaster"; -import Features from "./Features"; -import Footer from "./footer"; -import { Metadata } from "next"; - -export const runtime = "edge"; - -export const metadata: Metadata = { - title: "Supermemory - Your personal second brain.", - description: - "Bring saved information from all over the internet into one place where you can connect it, and ask AI about it", - openGraph: { - images: [ - { - url: "https://supermemory.ai/og-image.png", - width: 1200, - height: 627, - alt: "Supermemory - Your personal second brain.", - }, - ], - }, - metadataBase: { - host: "https://supermemory.ai", - href: "/", - origin: "https://supermemory.ai", - password: "supermemory", - hash: "supermemory", - pathname: "/", - search: "", - username: "supermemoryai", - hostname: "supermemory.ai", - port: "", - protocol: "https:", - searchParams: new URLSearchParams(""), - toString: () => "https://supermemory.ai/", - toJSON: () => "https://supermemory.ai/", - }, - twitter: { - card: "summary_large_image", - site: "https://supermemory.ai", - creator: "https://supermemory.ai", - title: "Supermemory - Your personal second brain.", - description: - "Bring saved information from all over the internet into one place where you can connect it, and ask AI about it", - images: [ - { - url: "https://supermemory.ai/og-image.png", - width: 1200, - height: 627, - alt: "Supermemory - Your personal second brain.", - }, - ], - }, -}; - -export default function Home() { - return ( - <main className="dark flex min-h-screen flex-col items-center overflow-x-hidden px-2 md:px-0"> - <Navbar /> - - {/* Background gradients */} - <div className="absolute left-0 top-0 z-[-1] h-full w-full"> - <div className="overflow-x-hidden"> - <div - className="absolute left-0 h-32 w-[95%] overflow-x-hidden bg-[#369DFD] bg-opacity-70 blur-[337.4px]" - style={{ transform: "rotate(-30deg)" }} - /> - </div> - - {/* a blue gradient line that's slightly tilted with blur (a lotof blur)*/} - <div className="overflow-x-hidden"> - <div - className="absolute left-0 top-[100%] h-32 w-[90%] overflow-x-hidden bg-[#369DFD] bg-opacity-40 blur-[337.4px]" - style={{ transform: "rotate(-30deg)" }} - /> - </div> - - <div className="overflow-x-hidden"> - <div className="absolute right-0 top-[145%] h-40 w-[17%] overflow-x-hidden bg-[#369DFD] bg-opacity-20 blur-[110px]" /> - </div> - </div> - - {/* Hero section */} - <Hero /> - - {/* Features section */} - <Features /> - - <RotatingIcons /> - - <Cta /> - - <Toaster /> - - <Footer /> - </main> - ); -} diff --git a/apps/web-v2/src/app/globals.css b/apps/web-v2/src/app/globals.css deleted file mode 100644 index 67115e30..00000000 --- a/apps/web-v2/src/app/globals.css +++ /dev/null @@ -1,132 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -@layer base { - :root { - --foreground-rgb: 255, 255, 255; - --background-start-rgb: 0, 0, 0; - --background-end-rgb: 0, 0, 0; - --black-bg: #0f1114; - --soft-foreground: #ffffff; - --soft-foreground-text: #b2bcca; - - --background: 216, 14%, 7%; - --foreground: 240 10% 3.9%; - - --card: 0 0% 100%; - --card-foreground: 240 10% 3.9%; - - --popover: 0 0% 100%; - --popover-foreground: 240 10% 3.9%; - - --primary: 240 5.9% 10%; - --primary-foreground: 0 0% 98%; - - --secondary: 240 4.8% 95.9%; - --secondary-foreground: 240 5.9% 10%; - - --muted: 240 4.8% 95.9%; - --muted-foreground: 240 3.8% 46.1%; - - --accent: 240 4.8% 95.9%; - --accent-foreground: 240 5.9% 10%; - - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 0 0% 98%; - - --border: 240 5.9% 90%; - --input: 240 5.9% 90%; - --ring: 240 10% 3.9%; - - --radius: 0.5rem; - } - - .dark { - --background: 216, 14%, 7%; - --foreground: 0 0% 98%; - - --card: 240 10% 3.9%; - --card-foreground: 0 0% 98%; - - --popover: 240 10% 3.9%; - --popover-foreground: 0 0% 98%; - - --primary: 0 0% 98%; - --primary-foreground: 240 5.9% 10%; - - --secondary: 240 3.7% 15.9%; - --secondary-foreground: 0 0% 98%; - - --muted: 240 3.7% 15.9%; - --muted-foreground: 240 5% 64.9%; - - --accent: 240 3.7% 15.9%; - --accent-foreground: 0 0% 98%; - - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 0 0% 98%; - - --border: 240 3.7% 15.9%; - --input: 240 3.7% 15.9%; - --ring: 240 4.9% 83.9%; - } -} - -@layer base { - * { - @apply border-border; - } - body { - @apply bg-background text-foreground; - } -} - -html { - scroll-behavior: smooth; -} - -/* width */ -::-webkit-scrollbar { - width: 8px; -} - -/* Track */ -::-webkit-scrollbar-track { - background: transparent; -} - -/* Handle */ -::-webkit-scrollbar-thumb { - background: #131f2c; -} - -/* Handle on hover */ -::-webkit-scrollbar-thumb:hover { - background: #22303d; -} - -body { - color: rgb(var(--foreground-rgb)); - background: linear-gradient(to bottom, transparent, var(--black-bg)) - var(--black-bg); -} - -@layer utilities { - .text-balance { - text-wrap: balance; - } -} - -@keyframes rotate { - 0% { - transform: rotate(0deg) translateX(130px); /* Adjust radius */ - } - 100% { - transform: rotate(360deg) translateX(130px); /* Adjust radius */ - } -} - -.icon-container { - animation: rotate 10s linear infinite; -} diff --git a/apps/web-v2/src/app/layout.tsx b/apps/web-v2/src/app/layout.tsx deleted file mode 100644 index 4af34eee..00000000 --- a/apps/web-v2/src/app/layout.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import type { Metadata } from "next"; -import { Inter } from "next/font/google"; -import "./globals.css"; -import Script from "next/script"; - -const inter = Inter({ subsets: ["latin"] }); - -export const metadata: Metadata = { - title: "Supermemory - Your personal second brain.", - description: - "Bring saved information from all over the internet into one place where you can connect it, and ask AI about it", -}; - -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { - return ( - <html lang="en"> - <Script src="https://challenges.cloudflare.com/turnstile/v0/api.js" /> - <body className={inter.className}>{children}</body> - </html> - ); -} diff --git a/apps/web-v2/src/app/not-found.tsx b/apps/web-v2/src/app/not-found.tsx deleted file mode 100644 index 3409889a..00000000 --- a/apps/web-v2/src/app/not-found.tsx +++ /dev/null @@ -1,58 +0,0 @@ -export const runtime = "edge"; - -export default function NotFound() { - return ( - <> - <title>404: This page could not be found.</title> - <div style={styles.error}> - <div> - <style - dangerouslySetInnerHTML={{ - __html: `body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}`, - }} - /> - <h1 className="next-error-h1" style={styles.h1}> - 404 - </h1> - <div style={styles.desc}> - <h2 style={styles.h2}>This page could not be found.</h2> - </div> - </div> - </div> - </> - ); -} - -const styles = { - error: { - fontFamily: - 'system-ui,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"', - height: "100vh", - textAlign: "center", - display: "flex", - flexDirection: "column", - alignItems: "center", - justifyContent: "center", - }, - - desc: { - display: "inline-block", - }, - - h1: { - display: "inline-block", - margin: "0 20px 0 0", - padding: "0 23px 0 0", - fontSize: 24, - fontWeight: 500, - verticalAlign: "top", - lineHeight: "49px", - }, - - h2: { - fontSize: 14, - fontWeight: 400, - lineHeight: "49px", - margin: 0, - }, -} as const; diff --git a/apps/web-v2/src/components/ui/cardClick.tsx b/apps/web-v2/src/components/ui/cardClick.tsx deleted file mode 100644 index 10fb4a07..00000000 --- a/apps/web-v2/src/components/ui/cardClick.tsx +++ /dev/null @@ -1,78 +0,0 @@ -"use client"; - -import { cn } from "@/utils/cn"; -import { AnimatePresence, motion } from "framer-motion"; -import React from "react"; - -export const CardClick = ({ - tab, - handleClickIndex, - items, -}: { - tab: number; - handleClickIndex: (tab: number) => void; - items: { - title: string; - description: string; - svg: React.ReactNode; - }[]; -}) => { - return ( - <div className={cn("flex flex-col")}> - {items.map((item, idx) => ( - <div - key={idx} - className="group relative block h-full w-full cursor-pointer rounded-2xl p-2 transition-all hover:border" - onMouseDown={() => handleClickIndex(idx)} - > - <AnimatePresence> - {tab === idx && ( - <motion.span - className="absolute inset-0 -z-[1] block h-full w-full rounded-3xl [background:linear-gradient(#2E2E32,#2E2E32),linear-gradient(120deg,theme(colors.zinc.700),theme(colors.zinc.700/0),theme(colors.zinc.700))]" - layoutId="hoverBackground" - initial={{ opacity: 0 }} - animate={{ - opacity: 1, - transition: { duration: 0.15 }, - }} - exit={{ - opacity: 0, - transition: { duration: 0.15, delay: 0.2 }, - }} - /> - )} - </AnimatePresence> - <Card - title={item.title} - description={item.description} - svg={item.svg} - /> - </div> - ))} - </div> - ); -}; - -export const Card = ({ - title, - description, - svg, -}: { - title: string; - description: string; - svg: React.ReactNode; -}) => { - return ( - <div - className={`flex items-center rounded-2xl border border-transparent px-6 py-4 text-left`} - > - {svg} - <div> - <div className="font-inter-tight mb-1 text-lg font-semibold text-zinc-200"> - {title} - </div> - <div className="text-zinc-500">{description}</div> - </div> - </div> - ); -}; diff --git a/apps/web-v2/src/components/ui/toast.tsx b/apps/web-v2/src/components/ui/toast.tsx deleted file mode 100644 index 56f4ebfd..00000000 --- a/apps/web-v2/src/components/ui/toast.tsx +++ /dev/null @@ -1,129 +0,0 @@ -"use client"; - -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( - "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 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", - { - variants: { - variant: { - default: "border bg-background text-foreground", - destructive: - "destructive group border-destructive bg-destructive text-destructive-foreground", - }, - }, - 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="h-4 w-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/apps/web-v2/src/components/ui/toaster.tsx b/apps/web-v2/src/components/ui/toaster.tsx deleted file mode 100644 index 7d82ed55..00000000 --- a/apps/web-v2/src/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/apps/web-v2/src/components/ui/use-toast.ts b/apps/web-v2/src/components/ui/use-toast.ts deleted file mode 100644 index 6555e795..00000000 --- a/apps/web-v2/src/components/ui/use-toast.ts +++ /dev/null @@ -1,191 +0,0 @@ -"use client"; - -// 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/apps/web-v2/src/lib/utils.ts b/apps/web-v2/src/lib/utils.ts deleted file mode 100644 index 365058ce..00000000 --- a/apps/web-v2/src/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/apps/web-v2/src/utils/cn.ts b/apps/web-v2/src/utils/cn.ts deleted file mode 100644 index cec6ac9e..00000000 --- a/apps/web-v2/src/utils/cn.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { ClassValue, clsx } from "clsx"; -import { twMerge } from "tailwind-merge"; - -export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)); -} diff --git a/apps/web-v2/src/utils/icons.tsx b/apps/web-v2/src/utils/icons.tsx deleted file mode 100644 index da05d716..00000000 --- a/apps/web-v2/src/utils/icons.tsx +++ /dev/null @@ -1,277 +0,0 @@ -import * as React from "react"; -import type { SVGProps } from "react"; -export const Github = (props: SVGProps<SVGSVGElement>) => ( - <svg - viewBox="0 0 256 250" - width="1em" - height="1em" - fill="currentColor" - xmlns="http://www.w3.org/2000/svg" - preserveAspectRatio="xMidYMid" - {...props} - > - <path d="M128.001 0C57.317 0 0 57.307 0 128.001c0 56.554 36.676 104.535 87.535 121.46 6.397 1.185 8.746-2.777 8.746-6.158 0-3.052-.12-13.135-.174-23.83-35.61 7.742-43.124-15.103-43.124-15.103-5.823-14.795-14.213-18.73-14.213-18.73-11.613-7.944.876-7.78.876-7.78 12.853.902 19.621 13.19 19.621 13.19 11.417 19.568 29.945 13.911 37.249 10.64 1.149-8.272 4.466-13.92 8.127-17.116-28.431-3.236-58.318-14.212-58.318-63.258 0-13.975 5-25.394 13.188-34.358-1.329-3.224-5.71-16.242 1.24-33.874 0 0 10.749-3.44 35.21 13.121 10.21-2.836 21.16-4.258 32.038-4.307 10.878.049 21.837 1.47 32.066 4.307 24.431-16.56 35.165-13.12 35.165-13.12 6.967 17.63 2.584 30.65 1.255 33.873 8.207 8.964 13.173 20.383 13.173 34.358 0 49.163-29.944 59.988-58.447 63.157 4.591 3.972 8.682 11.762 8.682 23.704 0 17.126-.148 30.91-.148 35.126 0 3.407 2.304 7.398 8.792 6.14C219.37 232.5 256 184.537 256 128.002 256 57.307 198.691 0 128.001 0Zm-80.06 182.34c-.282.636-1.283.827-2.194.39-.929-.417-1.45-1.284-1.15-1.922.276-.655 1.279-.838 2.205-.399.93.418 1.46 1.293 1.139 1.931Zm6.296 5.618c-.61.566-1.804.303-2.614-.591-.837-.892-.994-2.086-.375-2.66.63-.566 1.787-.301 2.626.591.838.903 1 2.088.363 2.66Zm4.32 7.188c-.785.545-2.067.034-2.86-1.104-.784-1.138-.784-2.503.017-3.05.795-.547 2.058-.055 2.861 1.075.782 1.157.782 2.522-.019 3.08Zm7.304 8.325c-.701.774-2.196.566-3.29-.49-1.119-1.032-1.43-2.496-.726-3.27.71-.776 2.213-.558 3.315.49 1.11 1.03 1.45 2.505.701 3.27Zm9.442 2.81c-.31 1.003-1.75 1.459-3.199 1.033-1.448-.439-2.395-1.613-2.103-2.626.301-1.01 1.747-1.484 3.207-1.028 1.446.436 2.396 1.602 2.095 2.622Zm10.744 1.193c.036 1.055-1.193 1.93-2.715 1.95-1.53.034-2.769-.82-2.786-1.86 0-1.065 1.202-1.932 2.733-1.958 1.522-.03 2.768.818 2.768 1.868Zm10.555-.405c.182 1.03-.875 2.088-2.387 2.37-1.485.271-2.861-.365-3.05-1.386-.184-1.056.893-2.114 2.376-2.387 1.514-.263 2.868.356 3.061 1.403Z" /> - </svg> -); - -export const Twitter = (props: SVGProps<SVGSVGElement>) => ( - <svg - viewBox="0 0 256 209" - width="1em" - height="1em" - xmlns="http://www.w3.org/2000/svg" - preserveAspectRatio="xMidYMid" - {...props} - > - <path - d="M256 25.45c-9.42 4.177-19.542 7-30.166 8.27 10.845-6.5 19.172-16.793 23.093-29.057a105.183 105.183 0 0 1-33.351 12.745C205.995 7.201 192.346.822 177.239.822c-29.006 0-52.523 23.516-52.523 52.52 0 4.117.465 8.125 1.36 11.97-43.65-2.191-82.35-23.1-108.255-54.876-4.52 7.757-7.11 16.78-7.11 26.404 0 18.222 9.273 34.297 23.365 43.716a52.312 52.312 0 0 1-23.79-6.57c-.003.22-.003.44-.003.661 0 25.447 18.104 46.675 42.13 51.5a52.592 52.592 0 0 1-23.718.9c6.683 20.866 26.08 36.05 49.062 36.475-17.975 14.086-40.622 22.483-65.228 22.483-4.24 0-8.42-.249-12.529-.734 23.243 14.902 50.85 23.597 80.51 23.597 96.607 0 149.434-80.031 149.434-149.435 0-2.278-.05-4.543-.152-6.795A106.748 106.748 0 0 0 256 25.45" - fill="currentColor" - /> - </svg> -); - -export const Medium = (props: SVGProps<SVGSVGElement>) => ( - <svg - width="52" - height="52" - viewBox="0 0 52 52" - fill="none" - xmlns="http://www.w3.org/2000/svg" - {...props} - > - <path - d="M17.332 13C20.7798 13 24.0864 14.3696 26.5244 16.8076C28.9624 19.2456 30.332 22.5522 30.332 26C30.332 29.4478 28.9624 32.7544 26.5244 35.1924C24.0864 37.6304 20.7798 39 17.332 39C13.8842 39 10.5776 37.6304 8.13964 35.1924C5.70167 32.7544 4.33203 29.4478 4.33203 26C4.33203 22.5522 5.70167 19.2456 8.13964 16.8076C10.5776 14.3696 13.8842 13 17.332 13ZM36.832 15.1667C40.082 15.1667 42.2487 20.0178 42.2487 26C42.2487 31.9822 40.082 36.8333 36.832 36.8333C33.582 36.8333 31.4154 31.9822 31.4154 26C31.4154 20.0178 33.582 15.1667 36.832 15.1667ZM45.4987 16.25C46.322 16.25 47.0414 18.0418 47.4054 21.1163L47.5072 22.0762L47.5484 22.5853L47.6134 23.6557L47.635 24.2168L47.661 25.389L47.6654 26L47.661 26.611L47.635 27.7832L47.6134 28.3465L47.5484 29.4147L47.505 29.9238L47.4075 30.8837C47.0414 33.9603 46.3242 35.75 45.4987 35.75C44.6754 35.75 43.956 33.9582 43.592 30.8837L43.4902 29.9238C43.4753 29.7542 43.4616 29.5845 43.449 29.4147L43.384 28.3443C43.3756 28.1573 43.3684 27.9703 43.3624 27.7832L43.3364 26.611V25.389L43.3624 24.2168L43.384 23.6535L43.449 22.5853L43.4924 22.0762L43.5899 21.1163C43.956 18.0397 44.6732 16.25 45.4987 16.25Z" - fill="currentColor" - /> - </svg> -); - -export const Reddit = (props: SVGProps<SVGSVGElement>) => ( - <svg - xmlns="http://www.w3.org/2000/svg" - xmlnsXlink="http://www.w3.org/1999/xlink" - className="_1O4jTk-dZ-VIxsCuYB6OR8" - viewBox="0 0 216 216" - width="1em" - height="1em" - {...props} - > - <defs> - <radialGradient - id="snoo-radial-gragient" - cx={169.75} - cy={92.19} - r={50.98} - fx={169.75} - fy={92.19} - gradientTransform="matrix(1 0 0 .87 0 11.64)" - gradientUnits="userSpaceOnUse" - > - <stop offset={0} stopColor="#feffff" /> - <stop offset={0.4} stopColor="#feffff" /> - <stop offset={0.51} stopColor="#f9fcfc" /> - <stop offset={0.62} stopColor="#edf3f5" /> - <stop offset={0.7} stopColor="#dee9ec" /> - <stop offset={0.72} stopColor="#d8e4e8" /> - <stop offset={0.76} stopColor="#ccd8df" /> - <stop offset={0.8} stopColor="#c8d5dd" /> - <stop offset={0.83} stopColor="#ccd6de" /> - <stop offset={0.85} stopColor="#d8dbe2" /> - <stop offset={0.88} stopColor="#ede3e9" /> - <stop offset={0.9} stopColor="#ffebef" /> - </radialGradient> - <radialGradient - xlinkHref="#snoo-radial-gragient" - id="snoo-radial-gragient-2" - cx={47.31} - r={50.98} - fx={47.31} - /> - <radialGradient - xlinkHref="#snoo-radial-gragient" - id="snoo-radial-gragient-3" - cx={109.61} - cy={85.59} - r={153.78} - fx={109.61} - fy={85.59} - gradientTransform="matrix(1 0 0 .7 0 25.56)" - /> - <radialGradient - id="snoo-radial-gragient-4" - cx={-6.01} - cy={64.68} - r={12.85} - fx={-6.01} - fy={64.68} - gradientTransform="matrix(1.07 0 0 1.55 81.08 27.26)" - gradientUnits="userSpaceOnUse" - > - <stop offset={0} stopColor="#f60" /> - <stop offset={0.5} stopColor="#ff4500" /> - <stop offset={0.7} stopColor="#fc4301" /> - <stop offset={0.82} stopColor="#f43f07" /> - <stop offset={0.92} stopColor="#e53812" /> - <stop offset={1} stopColor="#d4301f" /> - </radialGradient> - <radialGradient - xlinkHref="#snoo-radial-gragient-4" - id="snoo-radial-gragient-5" - cx={-73.55} - cy={64.68} - r={12.85} - fx={-73.55} - fy={64.68} - gradientTransform="matrix(-1.07 0 0 1.55 62.87 27.26)" - /> - <radialGradient - id="snoo-radial-gragient-6" - cx={107.93} - cy={166.96} - r={45.3} - fx={107.93} - fy={166.96} - gradientTransform="matrix(1 0 0 .66 0 57.4)" - gradientUnits="userSpaceOnUse" - > - <stop offset={0} stopColor="#172e35" /> - <stop offset={0.29} stopColor="#0e1c21" /> - <stop offset={0.73} stopColor="#030708" /> - <stop offset={1} /> - </radialGradient> - <radialGradient - xlinkHref="#snoo-radial-gragient" - id="snoo-radial-gragient-7" - cx={147.88} - cy={32.94} - r={39.77} - fx={147.88} - fy={32.94} - gradientTransform="matrix(1 0 0 .98 0 .54)" - /> - <radialGradient - id="snoo-radial-gragient-8" - cx={131.31} - cy={73.08} - r={32.6} - fx={131.31} - fy={73.08} - gradientUnits="userSpaceOnUse" - > - <stop offset={0.48} stopColor="#7a9299" /> - <stop offset={0.67} stopColor="#172e35" /> - <stop offset={0.75} /> - <stop offset={0.82} stopColor="#172e35" /> - </radialGradient> - <style> - {"\n .snoo-cls-11{strokeWidth:0;fill:#ffc49c}\n "} - </style> - </defs> - <path - fill="#ff4500" - strokeWidth={0} - d="M108 0C48.35 0 0 48.35 0 108c0 29.82 12.09 56.82 31.63 76.37l-20.57 20.57C6.98 209.02 9.87 216 15.64 216H108c59.65 0 108-48.35 108-108S167.65 0 108 0Z" - /> - <circle - cx={169.22} - cy={106.98} - r={25.22} - fill="url(#snoo-radial-gragient)" - strokeWidth={0} - /> - <circle - cx={46.78} - cy={106.98} - r={25.22} - fill="url(#snoo-radial-gragient-2)" - strokeWidth={0} - /> - <ellipse - cx={108.06} - cy={128.64} - fill="url(#snoo-radial-gragient-3)" - strokeWidth={0} - rx={72} - ry={54} - /> - <path - fill="url(#snoo-radial-gragient-4)" - strokeWidth={0} - d="M86.78 123.48c-.42 9.08-6.49 12.38-13.56 12.38s-12.46-4.93-12.04-14.01c.42-9.08 6.49-15.02 13.56-15.02s12.46 7.58 12.04 16.66Z" - /> - <path - fill="url(#snoo-radial-gragient-5)" - strokeWidth={0} - d="M129.35 123.48c.42 9.08 6.49 12.38 13.56 12.38s12.46-4.93 12.04-14.01c-.42-9.08-6.49-15.02-13.56-15.02s-12.46 7.58-12.04 16.66Z" - /> - <ellipse - cx={79.63} - cy={116.37} - className="snoo-cls-11" - rx={2.8} - ry={3.05} - /> - <ellipse - cx={146.21} - cy={116.37} - className="snoo-cls-11" - rx={2.8} - ry={3.05} - /> - <path - fill="url(#snoo-radial-gragient-6)" - strokeWidth={0} - d="M108.06 142.92c-8.76 0-17.16.43-24.92 1.22-1.33.13-2.17 1.51-1.65 2.74 4.35 10.39 14.61 17.69 26.57 17.69s22.23-7.3 26.57-17.69c.52-1.23-.33-2.61-1.65-2.74-7.77-.79-16.16-1.22-24.92-1.22Z" - /> - <circle - cx={147.49} - cy={49.43} - r={17.87} - fill="url(#snoo-radial-gragient-7)" - strokeWidth={0} - /> - <path - fill="url(#snoo-radial-gragient-8)" - strokeWidth={0} - d="M107.8 76.92c-2.14 0-3.87-.89-3.87-2.27 0-16.01 13.03-29.04 29.04-29.04 2.14 0 3.87 1.73 3.87 3.87s-1.73 3.87-3.87 3.87c-11.74 0-21.29 9.55-21.29 21.29 0 1.38-1.73 2.27-3.87 2.27Z" - /> - <path - fill="#842123" - strokeWidth={0} - d="M62.82 122.65c.39-8.56 6.08-14.16 12.69-14.16 6.26 0 11.1 6.39 11.28 14.33.17-8.88-5.13-15.99-12.05-15.99s-13.14 6.05-13.56 15.2c-.42 9.15 4.97 13.83 12.04 13.83h.52c-6.44-.16-11.3-4.79-10.91-13.2Zm90.48 0c-.39-8.56-6.08-14.16-12.69-14.16-6.26 0-11.1 6.39-11.28 14.33-.17-8.88 5.13-15.99 12.05-15.99 7.07 0 13.14 6.05 13.56 15.2.42 9.15-4.97 13.83-12.04 13.83h-.52c6.44-.16 11.3-4.79 10.91-13.2Z" - /> - </svg> -); - -export const Notion = (props: SVGProps<SVGSVGElement>) => ( - <svg - xmlns="http://www.w3.org/2000/svg" - width="1em" - height="1em" - preserveAspectRatio="xMidYMid" - viewBox="0 0 256 268" - {...props} - > - <path - fill="#FFF" - d="M16.092 11.538 164.09.608c18.179-1.56 22.85-.508 34.28 7.801l47.243 33.282C253.406 47.414 256 48.975 256 55.207v182.527c0 11.439-4.155 18.205-18.696 19.24L65.44 267.378c-10.913.517-16.11-1.043-21.825-8.327L8.826 213.814C2.586 205.487 0 199.254 0 191.97V29.726c0-9.352 4.155-17.153 16.092-18.188Z" - /> - <path d="M164.09.608 16.092 11.538C4.155 12.573 0 20.374 0 29.726v162.245c0 7.284 2.585 13.516 8.826 21.843l34.789 45.237c5.715 7.284 10.912 8.844 21.825 8.327l171.864-10.404c14.532-1.035 18.696-7.801 18.696-19.24V55.207c0-5.911-2.336-7.614-9.21-12.66l-1.185-.856L198.37 8.409C186.94.1 182.27-.952 164.09.608ZM69.327 52.22c-14.033.945-17.216 1.159-25.186-5.323L23.876 30.778c-2.06-2.086-1.026-4.69 4.163-5.207l142.274-10.395c11.947-1.043 18.17 3.12 22.842 6.758l24.401 17.68c1.043.525 3.638 3.637.517 3.637L71.146 52.095l-1.819.125Zm-16.36 183.954V81.222c0-6.767 2.077-9.887 8.3-10.413L230.02 60.93c5.724-.517 8.31 3.12 8.31 9.879v153.917c0 6.767-1.044 12.49-10.387 13.008l-161.487 9.361c-9.343.517-13.489-2.594-13.489-10.921ZM212.377 89.53c1.034 4.681 0 9.362-4.681 9.897l-7.783 1.542v114.404c-6.758 3.637-12.981 5.715-18.18 5.715-8.308 0-10.386-2.604-16.609-10.396l-50.898-80.079v77.476l16.1 3.646s0 9.362-12.989 9.362l-35.814 2.077c-1.043-2.086 0-7.284 3.63-8.318l9.351-2.595V109.823l-12.98-1.052c-1.044-4.68 1.55-11.439 8.826-11.965l38.426-2.585 52.958 81.113v-71.76l-13.498-1.552c-1.043-5.733 3.111-9.896 8.3-10.404l35.84-2.087Z" /> - </svg> -); - -export const X = (props: SVGProps<SVGSVGElement>) => ( - <svg - xmlns="http://www.w3.org/2000/svg" - width="1em" - height="1em" - fill="none" - viewBox="0 0 1200 1227" - {...props} - > - <path - fill="#fff" - d="M714.163 519.284 1160.89 0h-105.86L667.137 450.887 357.328 0H0l468.492 681.821L0 1226.37h105.866l409.625-476.152 327.181 476.152H1200L714.137 519.284h.026ZM569.165 687.828l-47.468-67.894-377.686-540.24h162.604l304.797 435.991 47.468 67.894 396.2 566.721H892.476L569.165 687.854v-.026Z" - /> - </svg> -); diff --git a/apps/web-v2/tailwind.config.ts b/apps/web-v2/tailwind.config.ts deleted file mode 100644 index 139251ce..00000000 --- a/apps/web-v2/tailwind.config.ts +++ /dev/null @@ -1,88 +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: { - backgroundImage: { - "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", - "gradient-conic": - "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", - }, - colors: { - "black-bg": "var(--black-bg)", - "soft-foreground": "var(--foreground-bg)", - "soft-foreground-text": "var(--soft-foreground-text)", - 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" }, - }, - }, - animation: { - "accordion-down": "accordion-down 0.2s ease-out", - "accordion-up": "accordion-up 0.2s ease-out", - }, - }, - }, - plugins: [require("tailwindcss-animate")], -} satisfies Config; - -export default config; diff --git a/apps/web-v2/tsconfig.json b/apps/web-v2/tsconfig.json deleted file mode 100644 index 7b285893..00000000 --- a/apps/web-v2/tsconfig.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "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, - "plugins": [ - { - "name": "next" - } - ], - "paths": { - "@/*": ["./src/*"] - } - }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] -} diff --git a/apps/web-v2/wrangler.toml b/apps/web-v2/wrangler.toml deleted file mode 100644 index 5518fdd7..00000000 --- a/apps/web-v2/wrangler.toml +++ /dev/null @@ -1,94 +0,0 @@ -#:schema node_modules/wrangler/config-schema.json -name = "web-v2" -compatibility_date = "2024-05-12" -compatibility_flags = ["nodejs_compat"] -pages_build_output_dir = ".vercel/output/static" - -# [[unsafe.bindings]] -# name = "RATELIMITER" -# type = "ratelimit" -# namespace_id = "1011" - -# simple = { limit = 25, period = 10 } - -# Automatically place your workloads in an optimal location to minimize latency. -# If you are running back-end logic in a Pages Function, running it closer to your back-end infrastructure -# rather than the end user may result in better performance. -# Docs: https://developers.cloudflare.com/pages/functions/smart-placement/#smart-placement -# [placement] -# mode = "smart" - -# Variable bindings. These are arbitrary, plaintext strings (similar to environment variables) -# Docs: -# - https://developers.cloudflare.com/pages/functions/bindings/#environment-variables -# Note: Use secrets to store sensitive data. -# - https://developers.cloudflare.com/pages/functions/bindings/#secrets -# [vars] -# MY_VARIABLE = "production_value" - -# Bind the Workers AI model catalog. Run machine learning models, powered by serverless GPUs, on Cloudflare’s global network -# Docs: https://developers.cloudflare.com/pages/functions/bindings/#workers-ai -# [ai] -# binding = "AI" - -# Bind a D1 database. D1 is Cloudflare’s native serverless SQL database. -# Docs: https://developers.cloudflare.com/pages/functions/bindings/#d1-databases -# [[d1_databases]] -# binding = "MY_DB" -# database_name = "my-database" -# database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - -# Bind a Durable Object. Durable objects are a scale-to-zero compute primitive based on the actor model. -# Durable Objects can live for as long as needed. Use these when you need a long-running "server", such as in realtime apps. -# Docs: https://developers.cloudflare.com/workers/runtime-apis/durable-objects -# [[durable_objects.bindings]] -# name = "MY_DURABLE_OBJECT" -# class_name = "MyDurableObject" -# script_name = 'my-durable-object' - -# Bind a KV Namespace. Use KV as persistent storage for small key-value pairs. -# Docs: https://developers.cloudflare.com/pages/functions/bindings/#kv-namespaces -# KV Example: -# [[kv_namespaces]] -# binding = "MY_KV_NAMESPACE" -# id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" - -# Bind a Queue producer. Use this binding to schedule an arbitrary task that may be processed later by a Queue consumer. -# Docs: https://developers.cloudflare.com/pages/functions/bindings/#queue-producers -# [[queues.producers]] -# binding = "MY_QUEUE" -# queue = "my-queue" - -# Bind an R2 Bucket. Use R2 to store arbitrarily large blobs of data, such as files. -# Docs: https://developers.cloudflare.com/pages/functions/bindings/#r2-buckets -# [[r2_buckets]] -# binding = "MY_BUCKET" -# bucket_name = "my-bucket" - -# Bind another Worker service. Use this binding to call another Worker without network overhead. -# Docs: https://developers.cloudflare.com/pages/functions/bindings/#service-bindings -# [[services]] -# binding = "MY_SERVICE" -# service = "my-service" - -# To use different bindings for preview and production environments, follow the examples below. -# When using environment-specific overrides for bindings, ALL bindings must be specified on a per-environment basis. -# Docs: https://developers.cloudflare.com/pages/functions/wrangler-configuration#environment-specific-overrides - -######## PREVIEW environment config ######## - -# [env.preview.vars] -# API_KEY = "xyz789" - -# [[env.preview.kv_namespaces]] -# binding = "MY_KV_NAMESPACE" -# id = "<PREVIEW_NAMESPACE_ID>" - -######## PRODUCTION environment config ######## - -# [env.production.vars] -# API_KEY = "abc123" - -# [[env.production.kv_namespaces]] -# binding = "MY_KV_NAMESPACE" -# id = "<PRODUCTION_NAMESPACE_ID>" diff --git a/apps/web/.eslintrc.js b/apps/web/.eslintrc.js new file mode 100644 index 00000000..7d644a4c --- /dev/null +++ b/apps/web/.eslintrc.js @@ -0,0 +1,9 @@ +/** @type {import("eslint").Linter.Config} */ +module.exports = { + root: true, + extends: ["@repo/eslint-config/next.js"], + parser: "@typescript-eslint/parser", + parserOptions: { + project: true, + }, +}; diff --git a/apps/web/.eslintrc.json b/apps/web/.eslintrc.json deleted file mode 100644 index abd7bea7..00000000 --- a/apps/web/.eslintrc.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": [ - "next/core-web-vitals", - "plugin:eslint-plugin-next-on-pages/recommended" - ], - "plugins": ["eslint-plugin-next-on-pages"] -} diff --git a/apps/web/.gitignore b/apps/web/.gitignore deleted file mode 100644 index b7862632..00000000 --- a/apps/web/.gitignore +++ /dev/null @@ -1,41 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. -.next/ - -# dependencies -/node_modules -/.pnp -.pnp.js -.yarn/install-state.gz - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# local env files -.env*.local - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts - -# wrangler files -.wrangler -.dev.vars diff --git a/apps/web/README.md b/apps/web/README.md index 5164b159..3d7b63af 100644 --- a/apps/web/README.md +++ b/apps/web/README.md @@ -1,70 +1,28 @@ -This is a [Next.js](https://nextjs.org/) project bootstrapped with [`c3`](https://developers.cloudflare.com/pages/get-started/c3). - ## 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. -## Cloudflare integration - -Besides the `dev` script mentioned above `c3` has added a few extra scripts that allow you to integrate the application with the [Cloudflare Pages](https://pages.cloudflare.com/) environment, these are: - -- `pages:build` to build the application for Pages using the [`@cloudflare/next-on-pages`](https://github.com/cloudflare/next-on-pages) CLI -- `preview` to locally preview your Pages application using the [Wrangler](https://developers.cloudflare.com/workers/wrangler/) CLI -- `deploy` to deploy your Pages application using the [Wrangler](https://developers.cloudflare.com/workers/wrangler/) CLI - -> **Note:** while the `dev` script is optimal for local development you should preview your Pages application as well (periodically or before deployments) in order to make sure that it can properly work in the Pages environment (for more details see the [`@cloudflare/next-on-pages` recommended workflow](https://github.com/cloudflare/next-on-pages/blob/05b6256/internal-packages/next-dev/README.md#recommended-workflow)) - -### Bindings - -Cloudflare [Bindings](https://developers.cloudflare.com/pages/functions/bindings/) are what allows you to interact with resources available in the Cloudflare Platform. - -You can use bindings during development, when previewing locally your application and of course in the deployed application: - -- To use bindings in dev mode you need to define them in the `next.config.js` file under `setupDevBindings`, this mode uses the `next-dev` `@cloudflare/next-on-pages` submodule. For more details see its [documentation](https://github.com/cloudflare/next-on-pages/blob/05b6256/internal-packages/next-dev/README.md). +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. -- To use bindings in the preview mode you need to add them to the `pages:preview` script accordingly to the `wrangler pages dev` command. For more details see its [documentation](https://developers.cloudflare.com/workers/wrangler/commands/#dev-1) or the [Pages Bindings documentation](https://developers.cloudflare.com/pages/functions/bindings/). +To create [API routes](https://nextjs.org/docs/app/building-your-application/routing/router-handlers) add an `api/` directory to the `app/` directory with a `route.ts` file. For individual endpoints, create a subfolder in the `api` directory, like `api/hello/route.ts` would map to [http://localhost:3000/api/hello](http://localhost:3000/api/hello). -- To use bindings in the deployed application you will need to configure them in the Cloudflare [dashboard](https://dash.cloudflare.com/). For more details see the [Pages Bindings documentation](https://developers.cloudflare.com/pages/functions/bindings/). +## Learn More -#### KV Example +To learn more about Next.js, take a look at the following resources: -`c3` has added for you an example showing how you can use a KV binding. +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn/foundations/about-nextjs) - an interactive Next.js tutorial. -In order to enable the example: +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! -- Search for javascript/typescript lines containing the following comment: - ```ts - // KV Example: - ``` - and uncomment the commented lines below it. -- Do the same in the `wrangler.toml` file, where - the comment is: - ``` - # KV Example: - ``` -- If you're using TypeScript run the `build-cf-types` script to update the `env.d.ts` file: - ```bash - npm run build-cf-types - # or - yarn build-cf-types - # or - pnpm build-cf-types - # or - bun build-cf-types - ``` +## Deploy on Vercel -After doing this you can run the `dev` or `preview` script and visit the `/api/hello` route to see the example in action. +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_source=github.com&utm_medium=referral&utm_campaign=turborepo-readme) from the creators of Next.js. -Finally, if you also want to see the example work in the deployed application make sure to add a `MY_KV_NAMESPACE` binding to your Pages application in its [dashboard kv bindings settings section](https://dash.cloudflare.com/?to=/:account/pages/view/:pages-project/settings/functions#kv_namespace_bindings_section). After having configured it make sure to re-deploy your application. +Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. diff --git a/apps/web/app/(auth)/auth-buttons.tsx b/apps/web/app/(auth)/auth-buttons.tsx new file mode 100644 index 00000000..9da1f5a5 --- /dev/null +++ b/apps/web/app/(auth)/auth-buttons.tsx @@ -0,0 +1,15 @@ +"use client"; + +import { Button } from "@repo/ui/src/button"; +import React from "react"; +import { signIn } from "../helpers/server/auth"; + +function SignIn() { + return ( + <Button className="mt-4" onClick={async () => await signIn("google")}> + Login with Google + </Button> + ); +} + +export default SignIn; diff --git a/apps/web/app/(auth)/signin/page.tsx b/apps/web/app/(auth)/signin/page.tsx new file mode 100644 index 00000000..44d2b4f4 --- /dev/null +++ b/apps/web/app/(auth)/signin/page.tsx @@ -0,0 +1,8 @@ +import { getThemeToggler } from "../../helpers/lib/get-theme-button"; + +async function Signin() { + const SetThemeButton = getThemeToggler(); + return <SetThemeButton />; +} + +export default Signin; diff --git a/apps/web-v2/src/app/(landing)/Cta.tsx b/apps/web/app/(landing)/Cta.tsx index be99bf99..be99bf99 100644 --- a/apps/web-v2/src/app/(landing)/Cta.tsx +++ b/apps/web/app/(landing)/Cta.tsx diff --git a/apps/web-v2/src/app/(landing)/EmailInput.tsx b/apps/web/app/(landing)/EmailInput.tsx index 9fd175b7..7e7ee5e7 100644 --- a/apps/web-v2/src/app/(landing)/EmailInput.tsx +++ b/apps/web/app/(landing)/EmailInput.tsx @@ -2,7 +2,7 @@ import { FormEvent, useState } from "react"; import formSubmitAction from "./formSubmitAction"; -import { useToast } from "@/components/ui/use-toast"; +import { useToast } from "@repo/ui/src/shadcn/use-toast"; function EmailInput() { const [email, setEmail] = useState(""); diff --git a/apps/web-v2/src/app/(landing)/FeatureContent.tsx b/apps/web/app/(landing)/FeatureContent.tsx index 7de64d53..7de64d53 100644 --- a/apps/web-v2/src/app/(landing)/FeatureContent.tsx +++ b/apps/web/app/(landing)/FeatureContent.tsx diff --git a/apps/web-v2/src/app/(landing)/Features.tsx b/apps/web/app/(landing)/Features.tsx index f52f7523..dd60ce8f 100644 --- a/apps/web-v2/src/app/(landing)/Features.tsx +++ b/apps/web/app/(landing)/Features.tsx @@ -1,13 +1,11 @@ "use client"; import { useState, useRef, useEffect } from "react"; -import { Transition } from "@headlessui/react"; import Image from "next/image"; -import CarouselIllustration from "@/../public/images/carousel-illustration-01.png"; -import { X } from "@/utils/icons"; +import { X } from "@repo/ui/src/components/icons"; import { features } from "./FeatureContent"; -import { CardClick } from "@/components/ui/cardClick"; +import { CardClick } from "@repo/ui/src/components/cardClick"; export default function Features() { const [tab, setTab] = useState<number>(0); @@ -65,71 +63,62 @@ export default function Features() { <div className="relative lg:max-w-none"> <div className="relative flex flex-col" ref={tabs}> {/* Item 1 */} - <Transition - show={tab === 0} - enter="transition ease-in-out duration-700 transform order-first" - enterFrom="opacity-0 -translate-y-4" - enterTo="opacity-100 translate-y-0" - leave="transition ease-in-out duration-300 transform absolute" - leaveFrom="opacity-100 translate-y-0" - leaveTo="opacity-0 translate-y-4" - beforeEnter={() => heightFix()} - unmount={false} + <div + className="transition-all duration-700 transform order-first" + style={{ + height: tab === 0 ? "auto" : 0, + opacity: tab === 0 ? 1 : 0, + // transform: tab === 0 ? 'translateY(0)' : 'translateY(4rem)', + }} > <div> <Image className="mx-auto rounded-lg shadow-2xl lg:max-w-none" - src={CarouselIllustration} + src={"/images/carousel-illustration-01.png"} width={700} height={520} alt="Carousel 01" /> </div> - </Transition> + </div> {/* Item 2 */} - <Transition - show={tab === 1} - enter="transition ease-in-out duration-700 transform order-first" - enterFrom="opacity-0 -translate-y-4" - enterTo="opacity-100 translate-y-0" - leave="transition ease-in-out duration-300 transform absolute" - leaveFrom="opacity-100 translate-y-0" - leaveTo="opacity-0 translate-y-4" - beforeEnter={() => heightFix()} - unmount={false} + <div + className="transition-all duration-700 transform order-first" + style={{ + height: tab === 1 ? "auto" : 0, + opacity: tab === 1 ? 1 : 0, + transform: tab === 1 ? "translateY(0)" : "translateY(4rem)", + }} > <div> <Image className="mx-auto rounded-lg shadow-2xl lg:max-w-none" - src={CarouselIllustration} + src={"/images/carousel-illustration-01.png"} width={700} height={520} alt="Carousel 02" /> </div> - </Transition> + </div> {/* Item 3 */} - <Transition - show={tab === 2} - enter="transition ease-in-out duration-700 transform order-first" - enterFrom="opacity-0 -translate-y-4" - enterTo="opacity-100 translate-y-0" - leave="transition ease-in-out duration-300 transform absolute" - leaveFrom="opacity-100 translate-y-0" - leaveTo="opacity-0 translate-y-4" - beforeEnter={() => heightFix()} - unmount={false} + <div + className="transition-all duration-700 transform order-first" + style={{ + height: tab === 2 ? "auto" : 0, + opacity: tab === 2 ? 1 : 0, + transform: tab === 2 ? "translateY(0)" : "translateY(4rem)", + }} > <div> <Image className="mx-auto rounded-lg shadow-2xl lg:max-w-none" - src={CarouselIllustration} + src={"/images/carousel-illustration-01.png"} width={700} height={520} alt="Carousel 03" /> </div> - </Transition> + </div> </div> </div> </div> diff --git a/apps/web-v2/src/app/(landing)/Hero.tsx b/apps/web/app/(landing)/Hero.tsx index 6867355b..d571cc26 100644 --- a/apps/web-v2/src/app/(landing)/Hero.tsx +++ b/apps/web/app/(landing)/Hero.tsx @@ -1,7 +1,7 @@ "use client"; import React from "react"; import { motion } from "framer-motion"; -import { Twitter } from "@/utils/icons"; +import { Twitter } from "@repo/ui/src/components/icons"; import EmailInput from "./EmailInput"; import LinkArrow from "./linkArrow"; @@ -39,7 +39,7 @@ function Hero() { ...slap, transition: { ...slap.transition, delay: 0.2 }, }} - className="text-center text-4xl font-semibold tracking-normal text-white/95 md:text-5xl" + className="text-center text-4xl font-semibold tracking-tight text-white/95 md:text-5xl" > Build your own second brain with Supermemory </motion.h1> diff --git a/apps/web-v2/src/app/(landing)/Navbar.tsx b/apps/web/app/(landing)/Navbar.tsx index c7bc80d4..7d0e3225 100644 --- a/apps/web-v2/src/app/(landing)/Navbar.tsx +++ b/apps/web/app/(landing)/Navbar.tsx @@ -1,5 +1,5 @@ -import Logo from "@/../public/logo.svg"; -import { Github } from "@/utils/icons"; +import Logo from "../../public/logo.svg"; +import { Github } from "@repo/ui/src/components/icons"; import Image from "next/image"; import Link from "next/link"; import React from "react"; diff --git a/apps/web-v2/src/app/(landing)/RotatingIcons.tsx b/apps/web/app/(landing)/RotatingIcons.tsx index 27d4eaed..fafe9f0b 100644 --- a/apps/web-v2/src/app/(landing)/RotatingIcons.tsx +++ b/apps/web/app/(landing)/RotatingIcons.tsx @@ -1,7 +1,13 @@ "use client"; import { motion } from "framer-motion"; -import { Github, Medium, Notion, Reddit, Twitter } from "@/utils/icons"; +import { + Github, + Medium, + Notion, + Reddit, + Twitter, +} from "@repo/ui/src/components/icons"; import Image from "next/image"; const icons = [ diff --git a/apps/web-v2/src/app/(landing)/footer.tsx b/apps/web/app/(landing)/footer.tsx index 4ebfca0b..4ebfca0b 100644 --- a/apps/web-v2/src/app/(landing)/footer.tsx +++ b/apps/web/app/(landing)/footer.tsx diff --git a/apps/web-v2/src/app/(landing)/formSubmitAction.ts b/apps/web/app/(landing)/formSubmitAction.ts index fa96b943..9c2eefff 100644 --- a/apps/web-v2/src/app/(landing)/formSubmitAction.ts +++ b/apps/web/app/(landing)/formSubmitAction.ts @@ -9,6 +9,7 @@ const formSubmitAction = async (email: string, token: string) => { if (ip) { if (process.env.RATELIMITER) { + // @ts-ignore const { success } = await process.env.RATELIMITER.limit({ key: `waitlist-${ip}`, }); diff --git a/apps/web-v2/src/app/(landing)/linkArrow.tsx b/apps/web/app/(landing)/linkArrow.tsx index def37e91..def37e91 100644 --- a/apps/web-v2/src/app/(landing)/linkArrow.tsx +++ b/apps/web/app/(landing)/linkArrow.tsx diff --git a/apps/web/app/(landing)/page.tsx b/apps/web/app/(landing)/page.tsx new file mode 100644 index 00000000..36f0ce8d --- /dev/null +++ b/apps/web/app/(landing)/page.tsx @@ -0,0 +1,58 @@ +import RotatingIcons from "./RotatingIcons"; +import Hero from "./Hero"; +import Navbar from "./Navbar"; +import Cta from "./Cta"; +import { Toaster } from "@repo/ui/src/shadcn/toaster"; +import Features from "./Features"; +import Footer from "./footer"; +import { auth } from "../helpers/server/auth"; + +export const runtime = "edge"; + +export default async function Home() { + const user = await auth(); + + console.log(user); + + return ( + <main className="dark flex min-h-screen flex-col items-center overflow-x-hidden px-2 md:px-0"> + <Navbar /> + + {/* Background gradients */} + <div className="absolute left-0 top-0 z-[-1] h-full w-full"> + <div className="overflow-x-hidden"> + <div + className="absolute left-0 h-32 w-[95%] overflow-x-hidden bg-[#369DFD] bg-opacity-70 blur-[337.4px]" + style={{ transform: "rotate(-30deg)" }} + /> + </div> + + {/* a blue gradient line that's slightly tilted with blur (a lotof blur)*/} + <div className="overflow-x-hidden"> + <div + className="absolute left-0 top-[100%] h-32 w-[90%] overflow-x-hidden bg-[#369DFD] bg-opacity-40 blur-[337.4px]" + style={{ transform: "rotate(-30deg)" }} + /> + </div> + + <div className="overflow-x-hidden"> + <div className="absolute right-0 top-[145%] h-40 w-[17%] overflow-x-hidden bg-[#369DFD] bg-opacity-20 blur-[110px]" /> + </div> + </div> + + {/* Hero section */} + <Hero /> + + {/* Features section */} + <Features /> + + <RotatingIcons /> + + <Cta /> + + <Toaster /> + + <Footer /> + </main> + ); +} diff --git a/apps/web/app/api/[...nextauth]/route.ts b/apps/web/app/api/[...nextauth]/route.ts new file mode 100644 index 00000000..50807ab1 --- /dev/null +++ b/apps/web/app/api/[...nextauth]/route.ts @@ -0,0 +1,2 @@ +export { GET, POST } from "../../helpers/server/auth"; +export const runtime = "edge"; diff --git a/apps/web/app/api/ensureAuth.ts b/apps/web/app/api/ensureAuth.ts new file mode 100644 index 00000000..a1401a07 --- /dev/null +++ b/apps/web/app/api/ensureAuth.ts @@ -0,0 +1,33 @@ +import { NextRequest } from "next/server"; +import { db } from "../helpers/server/db"; +import { sessions, users } from "../helpers/server/db/schema"; +import { eq } from "drizzle-orm"; + +export async function ensureAuth(req: NextRequest) { + // A helper function to protect routes + + const token = + req.cookies.get("next-auth.session-token")?.value ?? + req.cookies.get("__Secure-authjs.session-token")?.value ?? + req.cookies.get("authjs.session-token")?.value ?? + req.headers.get("Authorization")?.replace("Bearer ", ""); + + if (!token) { + return undefined; + } + + const sessionData = await db + .select() + .from(sessions) + .innerJoin(users, eq(users.id, sessions.userId)) + .where(eq(sessions.sessionToken, token!)); + + if (!sessionData || sessionData.length < 0) { + return undefined; + } + + return { + user: sessionData[0]!.user, + session: sessionData[0]!, + }; +} diff --git a/apps/web-v2/src/app/api/hello/route.ts b/apps/web/app/api/hello/route.ts index 363d0704..363d0704 100644 --- a/apps/web-v2/src/app/api/hello/route.ts +++ b/apps/web/app/api/hello/route.ts diff --git a/apps/web/app/api/upload_image/route.ts b/apps/web/app/api/upload_image/route.ts new file mode 100644 index 00000000..0d93c5b0 --- /dev/null +++ b/apps/web/app/api/upload_image/route.ts @@ -0,0 +1,56 @@ +import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3"; +import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; + +import type { NextRequest } from "next/server"; +import { ensureAuth } from "../ensureAuth"; + +export const runtime = "edge"; + +export async function PUT(request: NextRequest) { + const d = await ensureAuth(request); + + if (!d) { + return new Response("Unauthorized", { status: 401 }); + } + + const reqUrl = new URL(request.url); + const filename = reqUrl.searchParams.get("filename"); + + if (!filename) { + return new Response("Missing filename", { status: 400 }); + } + + if ( + !process.env.R2_ENDPOINT || + !process.env.R2_ACCESS_ID || + !process.env.R2_SECRET_KEY || + !process.env.R2_BUCKET_NAME + ) { + return new Response( + "Missing one or more R2 env variables: R2_ENDPOINT, R2_ACCESS_ID, R2_SECRET_KEY, R2_BUCKET_NAME. To get them, go to the R2 console, create and paste keys in a `.dev.vars` file in the root of this project.", + { status: 500 }, + ); + } + + const s3 = new S3Client({ + region: "auto", + endpoint: process.env.R2_ENDPOINT, + credentials: { + accessKeyId: process.env.R2_ACCESS_ID, + secretAccessKey: process.env.R2_SECRET_KEY, + }, + }); + + const url = await getSignedUrl( + s3, + new PutObjectCommand({ Bucket: process.env.R2_BUCKET_NAME, Key: filename }), + { expiresIn: 3600 }, + ); + + return new Response(JSON.stringify({ url }), { + status: 200, + headers: { + "Content-Type": "application/json", + }, + }); +} diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css new file mode 100644 index 00000000..8eee6cbd --- /dev/null +++ b/apps/web/app/globals.css @@ -0,0 +1,50 @@ +:root { + --max-width: 1100px; + --border-radius: 12px; + --font-mono: ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono", + "Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro", + "Fira Mono", "Droid Sans Mono", "Courier New", monospace; + + --foreground-rgb: 255, 255, 255; + --background-start-rgb: 0, 0, 0; + --background-end-rgb: 0, 0, 0; + + --callout-rgb: 20, 20, 20; + --callout-border-rgb: 108, 108, 108; + --card-rgb: 100, 100, 100; + --card-border-rgb: 200, 200, 200; + + --glow-conic: conic-gradient( + from 180deg at 50% 50%, + #2a8af6 0deg, + #a853ba 180deg, + #e92a67 360deg + ); +} + +* { + box-sizing: border-box; + padding: 0; + margin: 0; +} + +html, +body { + max-width: 100vw; + overflow-x: hidden; +} + +body { + color: rgb(var(--foreground-rgb)); + background: linear-gradient( + to bottom, + transparent, + rgb(var(--background-end-rgb)) + ) + rgb(var(--background-start-rgb)); +} + +a { + color: inherit; + text-decoration: none; +} diff --git a/apps/web/app/helpers/lib/get-theme-button.tsx b/apps/web/app/helpers/lib/get-theme-button.tsx new file mode 100644 index 00000000..41720927 --- /dev/null +++ b/apps/web/app/helpers/lib/get-theme-button.tsx @@ -0,0 +1,11 @@ +// Theming that works perfectly with app router (no flicker, jumps etc!) + +import dynamic from "next/dynamic"; + +// Don't SSR the toggle since the value on the server will be different than the client +export const getThemeToggler = () => + dynamic(() => import("@repo/ui/src/shadcn/theme-toggle"), { + ssr: false, + // Make sure to code a placeholder so the UI doesn't jump when the component loads + loading: () => <div className="w-6 h-6" />, + }); diff --git a/apps/web/app/helpers/lib/handle-errors.ts b/apps/web/app/helpers/lib/handle-errors.ts new file mode 100644 index 00000000..42cae589 --- /dev/null +++ b/apps/web/app/helpers/lib/handle-errors.ts @@ -0,0 +1,25 @@ +import { isRedirectError } from "next/dist/client/components/redirect"; +import { toast } from "sonner"; +import { z } from "zod"; + +export function getErrorMessage(err: unknown) { + const unknownError = "Something went wrong, please try again later."; + + if (err instanceof z.ZodError) { + const errors = err.issues.map((issue) => { + return issue.message; + }); + return errors.join("\n"); + } else if (err instanceof Error) { + return err.message; + } else if (isRedirectError(err)) { + throw err; + } else { + return unknownError; + } +} + +export function showErrorToast(err: unknown) { + const errorMessage = getErrorMessage(err); + return toast.error(errorMessage); +} diff --git a/apps/web/src/server/auth.ts b/apps/web/app/helpers/server/auth.ts index 95edcf35..e2817cf0 100644 --- a/apps/web/src/server/auth.ts +++ b/apps/web/app/helpers/server/auth.ts @@ -1,15 +1,15 @@ -import { env } from "@/env"; -import NextAuth from "next-auth"; +import NextAuth, { NextAuthResult } from "next-auth"; import Google from "next-auth/providers/google"; import { DrizzleAdapter } from "@auth/drizzle-adapter"; import { db } from "./db"; export const { handlers: { GET, POST }, + signIn, + signOut, auth, } = NextAuth({ - secret: env.NEXTAUTH_SECRET, - trustHost: true, + secret: process.env.NEXTAUTH_SECRET ?? process.env.AUTH_SECRET, callbacks: { session: ({ session, token, user }) => ({ ...session, @@ -22,8 +22,8 @@ export const { adapter: DrizzleAdapter(db), providers: [ Google({ - clientId: env.GOOGLE_CLIENT_ID, - clientSecret: env.GOOGLE_CLIENT_SECRET, + clientId: process.env.GOOGLE_CLIENT_ID, + clientSecret: process.env.GOOGLE_CLIENT_SECRET, }), ], }); diff --git a/apps/web/src/server/db/index.ts b/apps/web/app/helpers/server/db/index.ts index 4d671bea..4d671bea 100644 --- a/apps/web/src/server/db/index.ts +++ b/apps/web/app/helpers/server/db/index.ts diff --git a/apps/web/src/server/db/schema.ts b/apps/web/app/helpers/server/db/schema.ts index cd2756f1..c4616eb2 100644 --- a/apps/web/src/server/db/schema.ts +++ b/apps/web/app/helpers/server/db/schema.ts @@ -6,7 +6,6 @@ import { sqliteTableCreator, text, integer, - unique, } from "drizzle-orm/sqlite-core"; export const createTable = sqliteTableCreator((name) => `${name}`); @@ -90,6 +89,7 @@ export const storedContent = createTable( url: text("url").notNull(), savedAt: int("savedAt", { mode: "timestamp" }).notNull(), baseUrl: text("baseUrl", { length: 255 }), + ogImage: text("ogImage", { length: 255 }), type: text("type", { enum: ["note", "page", "twitter-bookmark"] }).default( "page", ), diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx new file mode 100644 index 00000000..035b5827 --- /dev/null +++ b/apps/web/app/layout.tsx @@ -0,0 +1,71 @@ +import "@repo/tailwind-config/globals.css"; + +import type { Metadata } from "next"; +import { Inter } from "next/font/google"; +import { ThemeScript } from "next-app-theme/theme-script"; + +const inter = Inter({ subsets: ["latin"] }); + +export const metadata: Metadata = { + title: "Supermemory - Your personal second brain.", + description: + "Bring saved information from all over the internet into one place where you can connect it, and ask AI about it", + openGraph: { + images: [ + { + url: "https://supermemory.ai/og-image.png", + width: 1200, + height: 627, + alt: "Supermemory - Your personal second brain.", + }, + ], + }, + metadataBase: { + host: "https://supermemory.ai", + href: "/", + origin: "https://supermemory.ai", + password: "supermemory", + hash: "supermemory", + pathname: "/", + search: "", + username: "supermemoryai", + hostname: "supermemory.ai", + port: "", + protocol: "https:", + searchParams: new URLSearchParams(""), + toString: () => "https://supermemory.ai/", + toJSON: () => "https://supermemory.ai/", + }, + twitter: { + card: "summary_large_image", + site: "https://supermemory.ai", + creator: "https://supermemory.ai", + title: "Supermemory - Your personal second brain.", + description: + "Bring saved information from all over the internet into one place where you can connect it, and ask AI about it", + images: [ + { + url: "https://supermemory.ai/og-image.png", + width: 1200, + height: 627, + alt: "Supermemory - Your personal second brain.", + }, + ], + }, +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}): JSX.Element { + return ( + <html lang="en"> + <head> + <ThemeScript /> + </head> + {/* TODO: when lightmode support is added, remove the 'dark' class from the body tag */} + <body className={`${inter.className} dark`}>{children}</body> + </html> + ); +} diff --git a/apps/web/app/page-ref.tsx b/apps/web/app/page-ref.tsx new file mode 100644 index 00000000..2d4c9cc3 --- /dev/null +++ b/apps/web/app/page-ref.tsx @@ -0,0 +1,116 @@ +import { Button } from "@repo/ui/src/shadcn/button"; +import { auth, signIn, signOut } from "./helpers/server/auth"; +import { db } from "./helpers/server/db"; +import { sql } from "drizzle-orm"; +import { users } from "./helpers/server/db/schema"; +import { getThemeToggler } from "./helpers/lib/get-theme-button"; + +export const runtime = "edge"; + +export default async function Page() { + const usr = await auth(); + + const userCount = await db + .select({ + count: sql<number>`count(*)`.mapWith(Number), + }) + .from(users); + + const SetThemeButton = getThemeToggler(); + + return ( + <main className="flex flex-col items-center justify-center min-h-screen"> + <div className="flex max-w-2xl justify-between w-full"> + <SetThemeButton /> + + <div className="flex gap-2 items-center justify-center"> + {" "} + <svg + viewBox="0 0 256 116" + xmlns="http://www.w3.org/2000/svg" + width="45px" + height="45px" + preserveAspectRatio="xMidYMid" + > + <path + fill="#FFF" + d="m202.357 49.394-5.311-2.124C172.085 103.434 72.786 69.289 66.81 85.997c-.996 11.286 54.227 2.146 93.706 4.059 12.039.583 18.076 9.671 12.964 24.484l10.069.031c11.615-36.209 48.683-17.73 50.232-29.68-2.545-7.857-42.601 0-31.425-35.497Z" + /> + <path + fill="#F4811F" + d="M176.332 108.348c1.593-5.31 1.062-10.622-1.593-13.809-2.656-3.187-6.374-5.31-11.154-5.842L71.17 87.634c-.531 0-1.062-.53-1.593-.53-.531-.532-.531-1.063 0-1.594.531-1.062 1.062-1.594 2.124-1.594l92.946-1.062c11.154-.53 22.839-9.56 27.087-20.182l5.312-13.809c0-.532.531-1.063 0-1.594C191.203 20.182 166.772 0 138.091 0 111.535 0 88.697 16.995 80.73 40.896c-5.311-3.718-11.684-5.843-19.12-5.31-12.747 1.061-22.838 11.683-24.432 24.43-.531 3.187 0 6.374.532 9.56C16.996 70.107 0 87.103 0 108.348c0 2.124 0 3.718.531 5.842 0 1.063 1.062 1.594 1.594 1.594h170.489c1.062 0 2.125-.53 2.125-1.594l1.593-5.842Z" + /> + <path + fill="#FAAD3F" + d="M205.544 48.863h-2.656c-.531 0-1.062.53-1.593 1.062l-3.718 12.747c-1.593 5.31-1.062 10.623 1.594 13.809 2.655 3.187 6.373 5.31 11.153 5.843l19.652 1.062c.53 0 1.062.53 1.593.53.53.532.53 1.063 0 1.594-.531 1.063-1.062 1.594-2.125 1.594l-20.182 1.062c-11.154.53-22.838 9.56-27.087 20.182l-1.063 4.78c-.531.532 0 1.594 1.063 1.594h70.108c1.062 0 1.593-.531 1.593-1.593 1.062-4.25 2.124-9.03 2.124-13.81 0-27.618-22.838-50.456-50.456-50.456" + /> + </svg> + <span className="italic">Cloudflare Next Saas Starter</span> + </div> + + <div className="border border-black dark:border-white rounded-2xl p-2 flex items-center"> + Start by editing apps/web/page.tsx + </div> + </div> + + <div className="max-w-2xl text-start w-full mt-16"> + Welcome to Cloudflare Next Saas Starter. <br /> Built a full stack app + using production-ready tools and frameworks, host on Cloudflare + instantly. + <br /> + An opinionated, batteries-included framework with{" "} + <a + className="text-transparent bg-clip-text bg-gradient-to-r from-[#a93d64] to-[#275ba9]" + href="https://turbo.build" + > + Turborepo + </a>{" "} + and Nextjs. Fully Typesafe. Best practices followed by default. + <br /> <br /> + Here's what the stack includes: + <ul className="list-disc mt-4 prose dark:prose-invert"> + <li> + Authentication with <code>next-auth</code> + </li> + <li>Database using Cloudflare's D1 serverless databases</li> + <li>Drizzle ORM, already connected to your database and auth ⚡</li> + <li>Light/darkmode theming that works with server components (!)</li> + <li>Styling using TailwindCSS and ShadcnUI</li> + <li>Turborepo with a landing page and shared components</li> + <li>Cloudflare wrangler for quick functions on the edge</li> + <li> + ... best part: everything's already set up for you. Just code! + </li> + </ul> + <div className="mt-4 flex flex-col gap-2"> + <span>Number of users in database: {userCount[0]!.count}</span> + </div> + {usr?.user?.email ? ( + <> + <div className="mt-4 flex flex-col gap-2"> + <span>Hello {usr.user.name} 👋</span> + <span>{usr.user.email}</span> + </div> + <form + action={async () => { + "use server"; + await signOut(); + }} + > + <Button className="mt-4">Sign out</Button> + </form> + </> + ) : ( + <form + action={async () => { + "use server"; + await signIn("google"); + }} + > + <Button className="mt-4">Login with Google</Button> + </form> + )} + </div> + </main> + ); +} diff --git a/apps/web/app/upload/file-uploader.tsx b/apps/web/app/upload/file-uploader.tsx new file mode 100644 index 00000000..2404ce7c --- /dev/null +++ b/apps/web/app/upload/file-uploader.tsx @@ -0,0 +1,315 @@ +"use client"; + +import * as React from "react"; +import Image from "next/image"; +import { Cross2Icon, UploadIcon } from "@radix-ui/react-icons"; +import Dropzone, { + type DropzoneProps, + type FileRejection, +} from "react-dropzone"; +import { toast } from "sonner"; + +import { cn, formatBytes } from "@repo/ui/lib/utils"; +import { useControllableState } from "@repo/ui/hooks/use-controllable-state"; +import { Button } from "@repo/ui/src/button"; +import { Progress } from "@repo/ui/src/progress"; +import { ScrollArea } from "@repo/ui/src/scroll-area"; + +interface FileUploaderProps extends React.HTMLAttributes<HTMLDivElement> { + /** + * Value of the uploader. + * @type File[] + * @default undefined + * @example value={files} + */ + value?: File[]; + + /** + * Function to be called when the value changes. + * @type React.Dispatch<React.SetStateAction<File[]>> + * @default undefined + * @example onValueChange={(files) => setFiles(files)} + */ + onValueChange?: React.Dispatch<React.SetStateAction<File[]>>; + + /** + * Function to be called when files are uploaded. + * @type (files: File[]) => Promise<void> + * @default undefined + * @example onUpload={(files) => uploadFiles(files)} + */ + onUpload?: (files: File[]) => Promise<void>; + + /** + * Progress of the uploaded files. + * @type Record<string, number> | undefined + * @default undefined + * @example progresses={{ "file1.png": 50 }} + */ + progresses?: Record<string, number>; + + /** + * Accepted file types for the uploader. + * @type { [key: string]: string[]} + * @default + * ```ts + * { "image/*": [] } + * ``` + * @example accept={["image/png", "image/jpeg"]} + */ + accept?: DropzoneProps["accept"]; + + /** + * Maximum file size for the uploader. + * @type number | undefined + * @default 1024 * 1024 * 2 // 2MB + * @example maxSize={1024 * 1024 * 2} // 2MB + */ + maxSize?: DropzoneProps["maxSize"]; + + /** + * Maximum number of files for the uploader. + * @type number | undefined + * @default 1 + * @example maxFiles={5} + */ + maxFiles?: DropzoneProps["maxFiles"]; + + /** + * Whether the uploader should accept multiple files. + * @type boolean + * @default false + * @example multiple + */ + multiple?: boolean; + + /** + * Whether the uploader is disabled. + * @type boolean + * @default false + * @example disabled + */ + disabled?: boolean; +} + +export function FileUploader(props: FileUploaderProps) { + const { + value: valueProp, + onValueChange, + onUpload, + progresses, + accept = { "image/*": [] }, + maxSize = 1024 * 1024 * 2, + maxFiles = 1, + multiple = false, + disabled = false, + className, + ...dropzoneProps + } = props; + + const [files, setFiles] = useControllableState({ + prop: valueProp, + onChange: onValueChange, + }); + + const onDrop = React.useCallback( + (acceptedFiles: File[], rejectedFiles: FileRejection[]) => { + if (!multiple && maxFiles === 1 && acceptedFiles.length > 1) { + toast.error("Cannot upload more than 1 file at a time"); + return; + } + + if ((files?.length ?? 0) + acceptedFiles.length > maxFiles) { + toast.error(`Cannot upload more than ${maxFiles} files`); + return; + } + + const newFiles = acceptedFiles.map((file) => + Object.assign(file, { + preview: URL.createObjectURL(file), + }), + ); + + const updatedFiles = files ? [...files, ...newFiles] : newFiles; + + setFiles(updatedFiles); + + if (rejectedFiles.length > 0) { + rejectedFiles.forEach(({ file }) => { + toast.error(`File ${file.name} was rejected`); + }); + } + + if ( + onUpload && + updatedFiles.length > 0 && + updatedFiles.length <= maxFiles + ) { + const target = + updatedFiles.length > 0 ? `${updatedFiles.length} files` : `file`; + + toast.promise(onUpload(updatedFiles), { + loading: `Uploading ${target}...`, + success: () => { + setFiles([]); + return `${target} uploaded`; + }, + error: `Failed to upload ${target}`, + }); + } + }, + + [files, maxFiles, multiple, onUpload, setFiles], + ); + + function onRemove(index: number) { + if (!files) return; + const newFiles = files.filter((_, i) => i !== index); + setFiles(newFiles); + onValueChange?.(newFiles); + } + + // Revoke preview url when component unmounts + React.useEffect(() => { + return () => { + if (!files) return; + files.forEach((file) => { + if (isFileWithPreview(file)) { + URL.revokeObjectURL(file.preview); + } + }); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const isDisabled = disabled || (files?.length ?? 0) >= maxFiles; + + return ( + <div className="relative flex flex-col gap-6 overflow-hidden"> + <Dropzone + onDrop={onDrop} + accept={accept} + maxSize={maxSize} + maxFiles={maxFiles} + multiple={maxFiles > 1 || multiple} + disabled={isDisabled} + > + {({ getRootProps, getInputProps, isDragActive }) => ( + <div + {...getRootProps()} + className={cn( + "group relative grid h-52 w-full cursor-pointer place-items-center rounded-lg border-2 border-dashed border-muted-foreground/25 px-5 py-2.5 text-center transition hover:bg-muted/25", + "ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", + isDragActive && "border-muted-foreground/50", + isDisabled && "pointer-events-none opacity-60", + className, + )} + {...dropzoneProps} + > + <input {...getInputProps()} /> + {isDragActive ? ( + <div className="flex flex-col items-center justify-center gap-4 sm:px-5"> + <div className="rounded-full border border-dashed p-3"> + <UploadIcon + className="size-7 text-muted-foreground" + aria-hidden="true" + /> + </div> + <p className="font-medium text-muted-foreground"> + Drop the files here + </p> + </div> + ) : ( + <div className="flex flex-col items-center justify-center gap-4 sm:px-5"> + <div className="rounded-full border border-dashed p-3"> + <UploadIcon + className="size-7 text-muted-foreground" + aria-hidden="true" + /> + </div> + <div className="space-y-px"> + <p className="font-medium text-muted-foreground"> + Drag {`'n'`} drop files here, or click to select files + </p> + <p className="text-sm text-muted-foreground/70"> + You can upload + {maxFiles > 1 + ? ` ${maxFiles === Infinity ? "multiple" : maxFiles} + files (up to ${formatBytes(maxSize)} each)` + : ` a file with ${formatBytes(maxSize)}`} + </p> + </div> + </div> + )} + </div> + )} + </Dropzone> + {files?.length ? ( + <ScrollArea className="h-fit w-full px-3"> + <div className="max-h-48 space-y-4"> + {files?.map((file, index) => ( + <FileCard + key={index} + file={file} + onRemove={() => onRemove(index)} + progress={progresses?.[file.name]} + /> + ))} + </div> + </ScrollArea> + ) : null} + </div> + ); +} + +interface FileCardProps { + file: File; + onRemove: () => void; + progress?: number; +} + +function FileCard({ file, progress, onRemove }: FileCardProps) { + return ( + <div className="relative flex items-center space-x-4"> + <div className="flex flex-1 space-x-4"> + {isFileWithPreview(file) ? ( + <Image + src={file.preview} + alt={file.name} + width={48} + height={48} + loading="lazy" + className="aspect-square shrink-0 rounded-md object-cover" + /> + ) : null} + <div className="flex w-full flex-col gap-2"> + <div className="space-y-px"> + <p className="line-clamp-1 text-sm font-medium text-foreground/80"> + {file.name} + </p> + <p className="text-xs text-muted-foreground"> + {formatBytes(file.size)} + </p> + </div> + {progress ? <Progress value={progress} /> : null} + </div> + </div> + <div className="flex items-center gap-2"> + <Button + type="button" + variant="outline" + size="icon" + className="size-7" + onClick={onRemove} + > + <Cross2Icon className="size-4 " aria-hidden="true" /> + <span className="sr-only">Remove file</span> + </Button> + </div> + </div> + ); +} + +function isFileWithPreview(file: File): file is File & { preview: string } { + return "preview" in file && typeof file.preview === "string"; +} diff --git a/apps/web/app/upload/page.tsx b/apps/web/app/upload/page.tsx new file mode 100644 index 00000000..4899d695 --- /dev/null +++ b/apps/web/app/upload/page.tsx @@ -0,0 +1,99 @@ +"use client"; + +import * as React from "react"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; + +import { getErrorMessage } from "../helpers/lib/handle-errors"; +import { Button } from "@repo/ui/src/button"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@repo/ui/src/form"; +import { FileUploader } from "./file-uploader"; + +import { UploadedFilesCard } from "./uploaded-files-card"; +import { useUploadFile } from "@repo/ui/hooks/use-upload-file"; + +const schema = z.object({ + images: z.array(z.instanceof(File)), +}); + +type Schema = z.infer<typeof schema>; + +export default function ReactHookFormDemo() { + const [loading, setLoading] = React.useState(false); + const { uploadFiles, uploadedFiles, isUploading } = useUploadFile( + "imageUploader", + { defaultUploadedFiles: [] }, + ); + const form = useForm<Schema>({ + resolver: zodResolver(schema), + defaultValues: { + images: [], + }, + }); + + function onSubmit(input: Schema) { + setLoading(true); + + toast.promise(uploadFiles(input.images), { + loading: "Uploading images...", + success: () => { + form.reset(); + setLoading(false); + return "Images uploaded"; + }, + error: (err) => { + setLoading(false); + return getErrorMessage(err); + }, + }); + } + + return ( + <div className="flex flex-col w-full min-h-screen items-center justify-center"> + <Form {...form}> + <form + onSubmit={form.handleSubmit(onSubmit)} + className="flex max-w-2xl flex-col gap-6 w-full" + > + <FormField + control={form.control} + name="images" + render={({ field }) => ( + <div className="space-y-6"> + <FormItem className="w-full"> + <FormLabel>Images</FormLabel> + <FormControl> + <FileUploader + value={field.value} + onValueChange={field.onChange} + maxFiles={4} + maxSize={4 * 1024 * 1024} + onUpload={uploadFiles} + disabled={isUploading} + /> + </FormControl> + <FormMessage /> + </FormItem> + {uploadedFiles.length > 0 ? ( + <UploadedFilesCard uploadedFiles={uploadedFiles} /> + ) : null} + </div> + )} + /> + <Button className="w-fit" disabled={loading}> + Save + </Button> + </form> + </Form> + </div> + ); +} diff --git a/apps/web/app/upload/uploaded-files-card.tsx b/apps/web/app/upload/uploaded-files-card.tsx new file mode 100644 index 00000000..fc9b7d5b --- /dev/null +++ b/apps/web/app/upload/uploaded-files-card.tsx @@ -0,0 +1,93 @@ +import Image from "next/image"; +import type { UploadedFile } from "@repo/shared-types"; + +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@repo/ui/src/card"; +import { ScrollArea, ScrollBar } from "@repo/ui/src/scroll-area"; + +interface UploadedFilesCardProps { + uploadedFiles: UploadedFile[]; +} + +import { ImageIcon } from "@radix-ui/react-icons"; + +import { cn } from "@repo/ui/lib/utils"; + +interface EmptyCardProps extends React.ComponentPropsWithoutRef<typeof Card> { + title: string; + description?: string; + action?: React.ReactNode; + icon?: React.ComponentType<{ className?: string }>; + className?: string; +} + +function EmptyCard({ + title, + description, + icon: Icon = ImageIcon, + action, + className, + ...props +}: EmptyCardProps) { + return ( + <Card + className={cn( + "flex w-full flex-col items-center justify-center space-y-6 bg-transparent p-16", + className, + )} + {...props} + > + <div className="mr-4 shrink-0 rounded-full border border-dashed p-4"> + <Icon className="size-8 text-muted-foreground" aria-hidden="true" /> + </div> + <div className="flex flex-col items-center gap-1.5 text-center"> + <CardTitle>{title}</CardTitle> + {description ? <CardDescription>{description}</CardDescription> : null} + </div> + {action ? action : null} + </Card> + ); +} + +export function UploadedFilesCard({ uploadedFiles }: UploadedFilesCardProps) { + return ( + <Card> + <CardHeader> + <CardTitle>Uploaded files</CardTitle> + <CardDescription>View the uploaded files here</CardDescription> + </CardHeader> + <CardContent> + {uploadedFiles.length > 0 ? ( + <ScrollArea className="pb-4"> + <div className="flex w-max space-x-2.5"> + {uploadedFiles.map((file) => ( + <div key={file.key} className="relative aspect-video w-64"> + <Image + src={file.url} + alt={file.name} + fill + sizes="(min-width: 640px) 640px, 100vw" + loading="lazy" + className="rounded-md object-cover" + /> + </div> + ))} + </div> + <ScrollBar orientation="horizontal" /> + </ScrollArea> + ) : ( + <EmptyCard + title="No files uploaded" + description="Upload some files to see them here" + className="w-full" + /> + )} + </CardContent> + </Card> + ); +} diff --git a/apps/web/cf-env.d.ts b/apps/web/cf-env.d.ts new file mode 100644 index 00000000..98303f35 --- /dev/null +++ b/apps/web/cf-env.d.ts @@ -0,0 +1,7 @@ +declare global { + namespace NodeJS { + interface ProcessEnv extends CloudflareEnv {} + } +} + +export {}; diff --git a/apps/web/components.json b/apps/web/components.json deleted file mode 100644 index c9b7b380..00000000 --- a/apps/web/components.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema.json", - "style": "default", - "rsc": true, - "tsx": true, - "tailwind": { - "config": "tailwind.config.ts", - "css": "src/app/globals.css", - "baseColor": "gray", - "cssVariables": false - }, - "aliases": { - "utils": "@/lib/utils", - "components": "@/components" - } -} diff --git a/apps/web/db/wipe.sql b/apps/web/db/wipe.sql deleted file mode 100644 index 456a48ad..00000000 --- a/apps/web/db/wipe.sql +++ /dev/null @@ -1,7 +0,0 @@ -DELETE FROM `account`; -DELETE FROM `contentToSpace`; -DELETE FROM `session`; -DELETE FROM `space`; -DELETE FROM `storedContent`; -DELETE FROM `user`; -DELETE FROM `verificationToken`;
\ No newline at end of file diff --git a/apps/web/drizzle.config.ts b/apps/web/drizzle.config.ts index c4f5d5fe..ab071121 100644 --- a/apps/web/drizzle.config.ts +++ b/apps/web/drizzle.config.ts @@ -1,17 +1,12 @@ import { type Config } from "drizzle-kit"; -const localDb = { - url: process.env.LOCAL_DB_URL!, -}; - export default { - schema: "./src/server/db/schema.ts", - driver: process.env.LOCAL_DB_URL ? "better-sqlite" : "d1", - dbCredentials: process.env.LOCAL_DB_URL - ? localDb - : { - wranglerConfigPath: "./wrangler.toml", - dbName: "dev-d1-anycontext", - }, - out: "drizzle", + schema: "./app/helpers/server/db/schema.ts", + dialect: "sqlite", + driver: "d1", + dbCredentials: { + wranglerConfigPath: "./wrangler.toml", + dbName: "", + }, + out: "migrations", } satisfies Config; diff --git a/apps/web/env.d.ts b/apps/web/env.d.ts index 8868e855..d74dcdf3 100644 --- a/apps/web/env.d.ts +++ b/apps/web/env.d.ts @@ -1,13 +1,14 @@ -declare global { - namespace NodeJS { - interface ProcessEnv { - [key: string]: string | undefined; - DATABASE: D1Database; - VECTORIZE_INDEX: VectorizeIndex; - AI: any; - RATELIMITER: any; - } - } -} +// Generated by Wrangler on Sat May 25 2024 17:07:13 GMT-0500 (Central Daylight Time) +// by running `wrangler types --env-interface CloudflareEnv env.d.ts` -export {}; +interface CloudflareEnv { + GOOGLE_CLIENT_ID: string; + GOOGLE_CLIENT_SECRET: string; + NEXTAUTH_SECRET: string; + R2_ENDPOINT: string; + R2_ACCESS_ID: string; + R2_SECRET_KEY: string; + R2_BUCKET_NAME: string; + STORAGE: R2Bucket; + DATABASE: D1Database; +} diff --git a/apps/web/db/prepare.sql b/apps/web/migrations/000_setup.sql index 4c8c2af6..db7f9444 100644 --- a/apps/web/db/prepare.sql +++ b/apps/web/migrations/000_setup.sql @@ -47,6 +47,7 @@ CREATE TABLE `storedContent` ( `url` text NOT NULL, `savedAt` integer NOT NULL, `baseUrl` text(255), + `ogImage` text(255), `type` text DEFAULT 'page', `image` text(255), `user` text(255), diff --git a/apps/web/migrations/meta/0000_snapshot.json b/apps/web/migrations/meta/0000_snapshot.json new file mode 100644 index 00000000..29cc4323 --- /dev/null +++ b/apps/web/migrations/meta/0000_snapshot.json @@ -0,0 +1,492 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "409cec60-0c4b-4cda-8751-3e70768bbb6c", + "prevId": "00000000-0000-0000-0000-000000000000", + "tables": { + "account": { + "name": "account", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "text(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "providerAccountId": { + "name": "providerAccountId", + "type": "text(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_type": { + "name": "token_type", + "type": "text(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_state": { + "name": "session_state", + "type": "text(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oauth_token_secret": { + "name": "oauth_token_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oauth_token": { + "name": "oauth_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": ["userId"], + "isUnique": false + } + }, + "foreignKeys": { + "account_userId_user_id_fk": { + "name": "account_userId_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "contentToSpace": { + "name": "contentToSpace", + "columns": { + "contentId": { + "name": "contentId", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "spaceId": { + "name": "spaceId", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "contentToSpace_contentId_storedContent_id_fk": { + "name": "contentToSpace_contentId_storedContent_id_fk", + "tableFrom": "contentToSpace", + "tableTo": "storedContent", + "columnsFrom": ["contentId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "contentToSpace_spaceId_space_id_fk": { + "name": "contentToSpace_spaceId_space_id_fk", + "tableFrom": "contentToSpace", + "tableTo": "space", + "columnsFrom": ["spaceId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "contentToSpace_contentId_spaceId_pk": { + "columns": ["contentId", "spaceId"], + "name": "contentToSpace_contentId_spaceId_pk" + } + }, + "uniqueConstraints": {} + }, + "session": { + "name": "session", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "sessionToken": { + "name": "sessionToken", + "type": "text(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "session_userId_idx": { + "name": "session_userId_idx", + "columns": ["userId"], + "isUnique": false + } + }, + "foreignKeys": { + "session_userId_user_id_fk": { + "name": "session_userId_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "space": { + "name": "space", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'none'" + }, + "user": { + "name": "user", + "type": "text(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "space_name_unique": { + "name": "space_name_unique", + "columns": ["name"], + "isUnique": true + }, + "spaces_name_idx": { + "name": "spaces_name_idx", + "columns": ["name"], + "isUnique": false + }, + "spaces_user_idx": { + "name": "spaces_user_idx", + "columns": ["user"], + "isUnique": false + } + }, + "foreignKeys": { + "space_user_user_id_fk": { + "name": "space_user_user_id_fk", + "tableFrom": "space", + "tableTo": "user", + "columnsFrom": ["user"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "storedContent": { + "name": "storedContent", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "savedAt": { + "name": "savedAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "baseUrl": { + "name": "baseUrl", + "type": "text(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ogImage": { + "name": "ogImage", + "type": "text(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'page'" + }, + "image": { + "name": "image", + "type": "text(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user": { + "name": "user", + "type": "text(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "storedContent_url_idx": { + "name": "storedContent_url_idx", + "columns": ["url"], + "isUnique": false + }, + "storedContent_savedAt_idx": { + "name": "storedContent_savedAt_idx", + "columns": ["savedAt"], + "isUnique": false + }, + "storedContent_title_idx": { + "name": "storedContent_title_idx", + "columns": ["title"], + "isUnique": false + }, + "storedContent_user_idx": { + "name": "storedContent_user_idx", + "columns": ["user"], + "isUnique": false + } + }, + "foreignKeys": { + "storedContent_user_user_id_fk": { + "name": "storedContent_user_user_id_fk", + "tableFrom": "storedContent", + "tableTo": "user", + "columnsFrom": ["user"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "text(255)", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "emailVerified": { + "name": "emailVerified", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "image": { + "name": "image", + "type": "text(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "verificationToken": { + "name": "verificationToken", + "columns": { + "identifier": { + "name": "identifier", + "type": "text(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verificationToken_identifier_token_pk": { + "columns": ["identifier", "token"], + "name": "verificationToken_identifier_token_pk" + } + }, + "uniqueConstraints": {} + } + }, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + } +} diff --git a/apps/web/migrations/meta/_journal.json b/apps/web/migrations/meta/_journal.json new file mode 100644 index 00000000..a77d9616 --- /dev/null +++ b/apps/web/migrations/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "6", + "dialect": "sqlite", + "entries": [ + { + "idx": 0, + "version": "6", + "when": 1716677954608, + "tag": "0000_calm_monster_badoon", + "breakpoints": true + } + ] +} diff --git a/apps/web/next-env.d.ts b/apps/web/next-env.d.ts new file mode 100644 index 00000000..4f11a03d --- /dev/null +++ b/apps/web/next-env.d.ts @@ -0,0 +1,5 @@ +/// <reference types="next" /> +/// <reference types="next/image-types/global" /> + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs index 31faf8ff..51c492f4 100644 --- a/apps/web/next.config.mjs +++ b/apps/web/next.config.mjs @@ -1,20 +1,19 @@ +import MillionLint from "@million/lint"; import { setupDevPlatform } from "@cloudflare/next-on-pages/next-dev"; -// Here we use the @cloudflare/next-on-pages next-dev module to allow us to use bindings during local development -// (when running the application with `next dev`), for more information see: -// https://github.com/cloudflare/next-on-pages/blob/5712c57ea7/internal-packages/next-dev/README.md -if (process.env.NODE_ENV === "development") { - await setupDevPlatform(); -} - /** @type {import('next').NextConfig} */ const nextConfig = { - eslint: { - ignoreBuildErrors: true, - }, - typescript: { - ignoreBuildErrors: true, - }, + transpilePackages: ["@repo/ui"], }; +export default MillionLint.next({ + rsc: true, +})(nextConfig); -export default nextConfig; +// we only need to use the utility during development so we can check NODE_ENV +// (note: this check is recommended but completely optional) +if (process.env.NODE_ENV === "development") { + // `await`ing the call is not necessary but it helps making sure that the setup has succeeded. + // If you cannot use top level awaits you could use the following to avoid an unhandled rejection: + // `setupDevPlatform().catch(e => console.error(e));` + await setupDevPlatform(); +} diff --git a/apps/web/package.json b/apps/web/package.json index 8dccd4d6..70ad3b46 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,56 +1,36 @@ { "name": "web", - "version": "0.1.0", + "version": "1.0.0", "private": true, "scripts": { "dev": "next dev", "build": "next build", "start": "next start", - "lint": "next lint", + "lint": "eslint . --max-warnings 0", + "cf-typegen": "wrangler types --env-interface CloudflareEnv env.d.ts", "pages:build": "bunx @cloudflare/next-on-pages", - "preview": "bun pages:build && wrangler pages dev .vercel/output/static", - "deploy": "bun pages:build && wrangler pages deploy .vercel/output/static", - "build-cf-types": "wrangler types --env-interface CloudflareEnv env.d.ts", - "prepare-local-db": "drizzle-kit generate:sqlite --out db/prepare.sql && wrangler d1 execute dev-d1-anycontext --local --file=db/prepare.sql", - "schema-change": "drizzle-kit generate:sqlite --out db/schema-change.sql && wrangler d1 execute dev-d1-anycontext --local --file=db/schema-change.sql" + "preview": "bun pages:build && wrangler pages dev", + "deploy": "bun pages:build && wrangler pages deploy" }, "dependencies": { - "@formkit/auto-animate": "^0.8.1", - "@radix-ui/react-avatar": "^1.0.4", - "@radix-ui/react-dialog": "^1.0.5", - "@radix-ui/react-icons": "^1.3.0", - "@radix-ui/react-popover": "^1.0.7", - "@radix-ui/react-slot": "^1.0.2", - "cheerio": "^1.0.0-rc.12", - "class-variance-authority": "^0.7.0", - "clsx": "^2.1.0", - "cmdk": "latest", - "framer-motion": "^11.0.24", - "lucide-react": "^0.338.0", - "next": "14.2.0", - "novel": "0.1.22", - "react": "^18", - "react-dom": "^18", - "tailwind-merge": "^2.2.1", - "tailwindcss": "^3.4.3", - "tailwindcss-animate": "^1.0.7", - "tiptap-markdown": "^0.8.10", - "vaul": "^0.9.0" + "@million/lint": "^1.0.0-rc.11", + "@repo/ui": "*", + "million": "^3.1.6", + "next": "^14.1.1", + "react": "^18.2.0", + "react-dom": "^18.2.0" }, "devDependencies": { - "@cloudflare/next-on-pages": "1", - "@cloudflare/workers-types": "^4.20240329.0", - "@types/node": "^20", - "@types/react": "^18", - "@types/react-dom": "^18", - "autoprefixer": "^10.0.1", - "eslint": "^8", - "eslint-config-next": "14.1.0", - "eslint-plugin-next-on-pages": "^1.11.0", - "postcss": "^8", - "tailwind-scrollbar": "^3.1.0", - "typescript": "^5", - "vercel": "^33.6.2", - "wrangler": "^3.41.0" + "@next/eslint-plugin-next": "^14.1.1", + "@repo/eslint-config": "*", + "@repo/typescript-config": "*", + "@repo/tailwind-config": "*", + "@repo/shared-types": "*", + "@types/eslint": "^8.56.5", + "@types/node": "^20.11.24", + "@types/react": "^18.2.61", + "@types/react-dom": "^18.2.19", + "eslint": "^8.57.0", + "typescript": "^5.3.3" } } diff --git a/apps/web/postcss.config.js b/apps/web/postcss.config.js index 12a703d9..2e342dc3 100644 --- a/apps/web/postcss.config.js +++ b/apps/web/postcss.config.js @@ -1,6 +1 @@ -module.exports = { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -}; +module.exports = require("@repo/tailwind-config/postcss.config"); diff --git a/apps/web/public/android-chrome-192x192.png b/apps/web/public/android-chrome-192x192.png Binary files differdeleted file mode 100644 index 5e57b282..00000000 --- a/apps/web/public/android-chrome-192x192.png +++ /dev/null diff --git a/apps/web/public/android-chrome-512x512.png b/apps/web/public/android-chrome-512x512.png Binary files differdeleted file mode 100644 index 2553425e..00000000 --- a/apps/web/public/android-chrome-512x512.png +++ /dev/null diff --git a/apps/web/public/apple-touch-icon.png b/apps/web/public/apple-touch-icon.png Binary files differdeleted file mode 100644 index d55c6339..00000000 --- a/apps/web/public/apple-touch-icon.png +++ /dev/null diff --git a/apps/web/public/favicon-16x16.png b/apps/web/public/favicon-16x16.png Binary files differdeleted file mode 100644 index 67fbac93..00000000 --- a/apps/web/public/favicon-16x16.png +++ /dev/null diff --git a/apps/web/public/favicon-32x32.png b/apps/web/public/favicon-32x32.png Binary files differdeleted file mode 100644 index 2fc8f0b3..00000000 --- a/apps/web/public/favicon-32x32.png +++ /dev/null diff --git a/apps/web/public/favicon.ico b/apps/web/public/favicon.ico Binary files differindex 163c3627..dd86c6dd 100644 --- a/apps/web/public/favicon.ico +++ b/apps/web/public/favicon.ico diff --git a/apps/web-v2/public/icons/android-chrome-192x192.png b/apps/web/public/icons/android-chrome-192x192.png Binary files differindex 0b684f93..0b684f93 100644 --- a/apps/web-v2/public/icons/android-chrome-192x192.png +++ b/apps/web/public/icons/android-chrome-192x192.png diff --git a/apps/web-v2/public/icons/android-chrome-512x512.png b/apps/web/public/icons/android-chrome-512x512.png Binary files differindex b40756d0..b40756d0 100644 --- a/apps/web-v2/public/icons/android-chrome-512x512.png +++ b/apps/web/public/icons/android-chrome-512x512.png diff --git a/apps/web-v2/public/icons/apple-touch-icon.png b/apps/web/public/icons/apple-touch-icon.png Binary files differindex bb8cdb45..bb8cdb45 100644 --- a/apps/web-v2/public/icons/apple-touch-icon.png +++ b/apps/web/public/icons/apple-touch-icon.png diff --git a/apps/web/public/icons/black_without_bg.png b/apps/web/public/icons/black_without_bg.png Binary files differdeleted file mode 100644 index 1bd7582d..00000000 --- a/apps/web/public/icons/black_without_bg.png +++ /dev/null diff --git a/apps/web/public/icons/brain-icon.png b/apps/web/public/icons/brain-icon.png Binary files differdeleted file mode 100644 index cebe4095..00000000 --- a/apps/web/public/icons/brain-icon.png +++ /dev/null diff --git a/apps/web-v2/public/icons/favicon-16x16.png b/apps/web/public/icons/favicon-16x16.png Binary files differindex 12776519..12776519 100644 --- a/apps/web-v2/public/icons/favicon-16x16.png +++ b/apps/web/public/icons/favicon-16x16.png diff --git a/apps/web-v2/public/icons/favicon-32x32.png b/apps/web/public/icons/favicon-32x32.png Binary files differindex 9686aff9..9686aff9 100644 --- a/apps/web-v2/public/icons/favicon-32x32.png +++ b/apps/web/public/icons/favicon-32x32.png diff --git a/apps/web/public/icons/logo_bw_without_bg.png b/apps/web/public/icons/logo_bw_without_bg.png Binary files differdeleted file mode 100644 index 167b9aea..00000000 --- a/apps/web/public/icons/logo_bw_without_bg.png +++ /dev/null diff --git a/apps/web/public/icons/logo_without_bg.png b/apps/web/public/icons/logo_without_bg.png Binary files differdeleted file mode 100644 index 54313f60..00000000 --- a/apps/web/public/icons/logo_without_bg.png +++ /dev/null diff --git a/apps/web/public/icons/white_without_bg.png b/apps/web/public/icons/white_without_bg.png Binary files differdeleted file mode 100644 index 4f7d8d42..00000000 --- a/apps/web/public/icons/white_without_bg.png +++ /dev/null diff --git a/apps/web/public/icons/wordmark.png b/apps/web/public/icons/wordmark.png Binary files differdeleted file mode 100644 index 6fb4ee7b..00000000 --- a/apps/web/public/icons/wordmark.png +++ /dev/null diff --git a/apps/web-v2/public/images/carousel-illustration-01.png b/apps/web/public/images/carousel-illustration-01.png Binary files differindex 13ecb109..13ecb109 100644 --- a/apps/web-v2/public/images/carousel-illustration-01.png +++ b/apps/web/public/images/carousel-illustration-01.png diff --git a/apps/web-v2/public/images/feature-illustration.png b/apps/web/public/images/feature-illustration.png Binary files differindex 60d98263..60d98263 100644 --- a/apps/web-v2/public/images/feature-illustration.png +++ b/apps/web/public/images/feature-illustration.png diff --git a/apps/web-v2/public/landing-bg-1.svg b/apps/web/public/landing-bg-1.svg index 4154dade..4154dade 100644 --- a/apps/web-v2/public/landing-bg-1.svg +++ b/apps/web/public/landing-bg-1.svg diff --git a/apps/web-v2/public/landing-bg-2.svg b/apps/web/public/landing-bg-2.svg index 4d0020e1..4d0020e1 100644 --- a/apps/web-v2/public/landing-bg-2.svg +++ b/apps/web/public/landing-bg-2.svg diff --git a/apps/web-v2/public/landing-ui-2.png b/apps/web/public/landing-ui-2.png Binary files differindex 956da910..956da910 100644 --- a/apps/web-v2/public/landing-ui-2.png +++ b/apps/web/public/landing-ui-2.png diff --git a/apps/web-v2/public/landing-ui.svg b/apps/web/public/landing-ui.svg index 43e64a70..43e64a70 100644 --- a/apps/web-v2/public/landing-ui.svg +++ b/apps/web/public/landing-ui.svg diff --git a/apps/web/public/logo.png b/apps/web/public/logo.png Binary files differdeleted file mode 100644 index 3fad437e..00000000 --- a/apps/web/public/logo.png +++ /dev/null diff --git a/apps/web-v2/public/logo.svg b/apps/web/public/logo.svg index 6081634d..6081634d 100644 --- a/apps/web-v2/public/logo.svg +++ b/apps/web/public/logo.svg diff --git a/apps/web/public/note.svg b/apps/web/public/note.svg deleted file mode 100644 index ffb4c795..00000000 --- a/apps/web/public/note.svg +++ /dev/null @@ -1,5 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#6F6F6F" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-text w-10 h-10"> - <path d="M17 6.1H3"></path> - <path d="M21 12.1H3"> - </path><path d="M15.1 18H3"></path> -</svg> diff --git a/apps/web/public/og-image.png b/apps/web/public/og-image.png Binary files differindex 1050ff19..c5a61b17 100644 --- a/apps/web/public/og-image.png +++ b/apps/web/public/og-image.png diff --git a/apps/web/public/site.webmanifest b/apps/web/public/site.webmanifest index 8d302080..c903e516 100644 --- a/apps/web/public/site.webmanifest +++ b/apps/web/public/site.webmanifest @@ -1,14 +1,14 @@ { - "name": "Supermemory - Your second brain", - "short_name": "Save your memories forever, build your own second brain.", + "name": "Supermemory - your second brain.", + "short_name": "Supermemory", "icons": [ { - "src": "/android-chrome-192x192.png", + "src": "/icons/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" }, { - "src": "/android-chrome-512x512.png", + "src": "/icons/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" } diff --git a/apps/web/public/twitter.svg b/apps/web/public/twitter.svg deleted file mode 100644 index 0819971e..00000000 --- a/apps/web/public/twitter.svg +++ /dev/null @@ -1 +0,0 @@ -<svg viewBox="0 0 256 209" width="256" height="209" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid"><path d="M256 25.45c-9.42 4.177-19.542 7-30.166 8.27 10.845-6.5 19.172-16.793 23.093-29.057a105.183 105.183 0 0 1-33.351 12.745C205.995 7.201 192.346.822 177.239.822c-29.006 0-52.523 23.516-52.523 52.52 0 4.117.465 8.125 1.36 11.97-43.65-2.191-82.35-23.1-108.255-54.876-4.52 7.757-7.11 16.78-7.11 26.404 0 18.222 9.273 34.297 23.365 43.716a52.312 52.312 0 0 1-23.79-6.57c-.003.22-.003.44-.003.661 0 25.447 18.104 46.675 42.13 51.5a52.592 52.592 0 0 1-23.718.9c6.683 20.866 26.08 36.05 49.062 36.475-17.975 14.086-40.622 22.483-65.228 22.483-4.24 0-8.42-.249-12.529-.734 23.243 14.902 50.85 23.597 80.51 23.597 96.607 0 149.434-80.031 149.434-149.435 0-2.278-.05-4.543-.152-6.795A106.748 106.748 0 0 0 256 25.45" fill="#55acee"/></svg>
\ No newline at end of file diff --git a/apps/web/public/web.svg b/apps/web/public/web.svg deleted file mode 100644 index c59f016d..00000000 --- a/apps/web/public/web.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6"> - <path stroke-linecap="round" stroke-linejoin="round" d="M12 21a9.004 9.004 0 0 0 8.716-6.747M12 21a9.004 9.004 0 0 1-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 0 1 7.843 4.582M12 3a8.997 8.997 0 0 0-7.843 4.582m15.686 0A11.953 11.953 0 0 1 12 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0 1 21 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0 1 12 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 0 1 3 12c0-1.605.42-3.113 1.157-4.418" /> -</svg> diff --git a/apps/web/src/actions/db.ts b/apps/web/src/actions/db.ts deleted file mode 100644 index 59eb8976..00000000 --- a/apps/web/src/actions/db.ts +++ /dev/null @@ -1,676 +0,0 @@ -"use server"; -import { cookies, headers } from "next/headers"; -import { db } from "@/server/db"; -import { - contentToSpace, - sessions, - storedContent, - users, - space, - StoredContent, -} from "@/server/db/schema"; -import { SearchResult } from "@/contexts/MemoryContext"; -import { - like, - eq, - and, - sql, - exists, - asc, - notExists, - inArray, - notInArray, -} from "drizzle-orm"; -import { union } from "drizzle-orm/sqlite-core"; -import { env } from "@/env"; - -// @todo: (future) pagination not yet needed -export async function searchMemoriesAndSpaces( - query: string, - opts?: { - filter?: { memories?: boolean; spaces?: boolean }; - range?: { offset: number; limit: number }; - memoriesRelativeToSpace?: { - fromSpaces?: number[]; - notInSpaces?: number[]; - }; - }, -): Promise<SearchResult[]> { - const user = await getUser(); - - if (!user) { - return []; - } - - const defaultWhere = and( - eq(storedContent.user, user.id), - like(storedContent.title, `%${query}%`), - ); - const extraWheres = []; - - if (opts?.memoriesRelativeToSpace) { - if (opts.memoriesRelativeToSpace.fromSpaces) { - extraWheres.push( - exists( - db - .select() - .from(contentToSpace) - .where( - and( - eq(contentToSpace.contentId, storedContent.id), - inArray( - contentToSpace.spaceId, - opts.memoriesRelativeToSpace.fromSpaces, - ), - ), - ), - ), - ); - } - if (opts.memoriesRelativeToSpace.notInSpaces) { - extraWheres.push( - notExists( - db - .select() - .from(contentToSpace) - .where( - and( - eq(contentToSpace.contentId, storedContent.id), - inArray( - contentToSpace.spaceId, - opts.memoriesRelativeToSpace.notInSpaces, - ), - ), - ), - ), - ); - } - } - - try { - let searchMemoriesQuery = db - .select({ - type: sql<string>`'memory'`, - space: sql`NULL`, - memory: storedContent as any, - }) - .from(storedContent) - .where( - extraWheres.length == 2 - ? and(and(...extraWheres), defaultWhere) - : extraWheres.length == 1 - ? and(...extraWheres, defaultWhere) - : defaultWhere, - ) - .orderBy(asc(storedContent.savedAt)); - - let searchSpacesQuery = db - .select({ - type: sql<string>`'space'`, - space: space as any, - memory: sql`NULL`, - }) - .from(space) - .where(and(eq(space.user, user.id), like(space.name, `%${query}%`))) - .orderBy(asc(space.name)); - - let queries = []; - - console.log("adding"); - - [undefined, true].includes(opts?.filter?.memories) && - queries.push(searchMemoriesQuery); - [undefined, true].includes(opts?.filter?.spaces) && - queries.push(searchSpacesQuery); - - if (opts?.range) { - queries = queries.map((q) => - q.offset(opts.range!.offset).limit(opts.range!.limit), - ); - } else { - queries = queries.map((q) => q.all()); - } - - const data = await Promise.all(queries); - - console.log("resp", data); - - return data.reduce((acc, i) => [...acc, ...i]) as SearchResult[]; - } catch { - return []; - } -} - -export async function getMemoriesFromUrl(urls: string[]) { - const user = await getUser(); - - if (!user) { - return []; - } - - return urls.length > 0 - ? await db - .select() - .from(storedContent) - .where( - and( - inArray(storedContent.url, urls), - eq(storedContent.user, user.id), - ), - ) - .all() - : []; -} - -async function getUser() { - const token = - cookies().get("next-auth.session-token")?.value ?? - cookies().get("__Secure-authjs.session-token")?.value ?? - cookies().get("authjs.session-token")?.value ?? - headers().get("Authorization")?.replace("Bearer ", ""); - - if (!token) { - return null; - } - - const session = await db - .select() - .from(sessions) - .where(eq(sessions.sessionToken, token!)); - - if (!session || session.length === 0) { - return null; - } - - const [userData] = await db - .select() - .from(users) - .where(eq(users.id, session[0].userId)) - .limit(1); - - if (!userData) { - return null; - } - - return userData; -} - -export async function getSpace(id: number) { - const user = await getUser(); - - if (!user) { - return null; - } - - return ( - await db - .select() - .from(space) - .where(and(eq(space.id, id), eq(space.user, user.id))) - )[0]; -} - -export async function addSpace(name: string, memories: number[]) { - const user = await getUser(); - - if (!user) { - return null; - } - - const [addedSpace] = await db - .insert(space) - .values({ - name: name, - user: user.id, - }) - .returning(); - - const addedMemories = - memories.length > 0 - ? await db - .insert(contentToSpace) - .values( - memories.map((m) => ({ - contentId: m, - spaceId: addedSpace.id, - })), - ) - .returning() - : []; - - return { - space: addedSpace, - addedMemories, - }; -} - -export async function fetchContent(id: number) { - const user = await getUser(); - - if (!user) { - return null; - } - - const fetchedMemory = await db - .select() - .from(storedContent) - .where(and(eq(storedContent.id, id), eq(storedContent.user, user.id))); - - const memory = fetchedMemory.length > 0 ? fetchedMemory[0] : null; - - const spaces = memory - ? await db - .select() - .from(contentToSpace) - .where(eq(contentToSpace.contentId, memory.id)) - : []; - - return { - memory, - spaces: spaces.map((s) => s.spaceId), - }; -} - -export async function fetchContentForSpace( - spaceId: number, - range?: { - offset: number; - limit: number; - }, -) { - const user = await getUser(); - - if (!user) { - return null; - } - - const query = db - .select() - .from(storedContent) - .where( - exists( - db - .select() - .from(contentToSpace) - .where( - and( - and( - eq(contentToSpace.spaceId, spaceId), - eq(contentToSpace.contentId, storedContent.id), - ), - exists( - db - .select() - .from(space) - .where( - and( - eq(space.user, user.id), - eq(space.id, contentToSpace.spaceId), - ), - ), - ), - ), - ), - ), - ) - .orderBy(asc(storedContent.savedAt)); - - return range - ? await query.limit(range.limit).offset(range.offset) - : await query.all(); -} - -export async function fetchFreeMemories(range?: { - offset: number; - limit: number; -}) { - const user = await getUser(); - - if (!user) { - return []; - } - - try { - const query = db - .select() - .from(storedContent) - .where( - and( - notExists( - db - .select() - .from(contentToSpace) - .where(eq(contentToSpace.contentId, storedContent.id)), - ), - eq(storedContent.user, user.id), - ), - ) - .orderBy(asc(storedContent.savedAt)); - - return range - ? await query.limit(range.limit).offset(range.offset) - : await query.all(); - } catch { - return []; - } -} - -export async function updateSpaceTitle(id: number, title: string) { - const user = await getUser(); - - if (!user) { - return null; - } - - return ( - await db - .update(space) - .set({ name: title }) - .where(and(eq(space.id, id), eq(space.user, user.id))) - .returning() - )[0]; -} - -export async function addMemory( - content: typeof storedContent.$inferInsert, - spaces: number[], -) { - const user = await getUser(); - - if (!user) { - return null; - } - - if (!content.content || content.content.trim() === "") { - const resp = await fetch( - `https://cf-ai-backend.dhravya.workers.dev/getPageContent?url=${content.url}`, - { - headers: { - "X-Custom-Auth-Key": env.BACKEND_SECURITY_KEY, - }, - }, - ); - - const data = await resp.text(); - - console.log(data); - - content.content = data; - } - - if (!content.content || content.content == "") { - return null; - } - - let [addedMemory] = await db - .insert(storedContent) - .values({ - user: user.id, - ...content, - }) - .returning(); - - const addedToSpaces = - spaces.length > 0 - ? await db - .insert(contentToSpace) - .values( - spaces.map((s) => ({ - contentId: addedMemory.id, - spaceId: s, - })), - ) - .returning() - : []; - - if (content.type === "note") { - addedMemory = ( - await db - .update(storedContent) - .set({ - url: addedMemory.url + addedMemory.id, - }) - .where(eq(storedContent.id, addedMemory.id)) - .returning() - )[0]; - } - - console.log("adding with:", `${addedMemory.url}-${user.email}`); - // Add to vectorDB - const res = (await Promise.race([ - fetch("https://cf-ai-backend.dhravya.workers.dev/add", { - method: "POST", - headers: { - "X-Custom-Auth-Key": env.BACKEND_SECURITY_KEY, - }, - body: JSON.stringify({ - pageContent: addedMemory.content, - title: addedMemory.title, - url: addedMemory.url, - user: user.email, - }), - }), - new Promise((_, reject) => - setTimeout(() => reject(new Error("Request timed out")), 40000), - ), - ])) as Response; - - return { - memory: addedMemory, - addedToSpaces, - }; -} - -export async function addContentInSpaces(id: number, contents: number[]) { - const user = await getUser(); - - if (!user) { - return null; - } - - const data = - contents.length > 0 - ? await db - .insert(contentToSpace) - .values( - contents.map((i) => ({ - spaceId: id, - contentId: i, - })), - ) - .returning() - : []; - - return data; -} - -export async function updateMemory( - id: number, - { - title, - content, - spaces, - removedFromSpaces: removeSpaces, - }: { - title?: string; - content?: string; - spaces?: number[]; - removedFromSpaces?: number[]; - }, -) { - const user = await getUser(); - - if (!user) { - return null; - } - - let updatedMemory: StoredContent | null = null; - - if (title && content) { - const [prev] = await db - .select() - .from(storedContent) - .where(and(eq(storedContent.user, user.id), eq(storedContent.id, id))); - - if (!prev) { - return null; - } - - const newContent = { - ...(title ? { title } : {}), - ...(content ? { content } : {}), - }; - - const updated = { - ...newContent, - ...prev, - }; - - console.log("adding with:", `${updated.url}-${user.email}`); - // Add to vectorDB - const res = (await Promise.race([ - fetch("https://cf-ai-backend.dhravya.workers.dev/edit", { - method: "POST", - headers: { - "X-Custom-Auth-Key": env.BACKEND_SECURITY_KEY, - }, - body: JSON.stringify({ - pageContent: updated.content, - title: updated.title, - url: updated.url, - user: user.email, - uniqueUrl: updated.url, - }), - }), - new Promise((_, reject) => - setTimeout(() => reject(new Error("Request timed out")), 40000), - ), - ])) as Response; - - [updatedMemory] = await db - .update(storedContent) - .set(newContent) - .where(and(eq(storedContent.id, id), eq(storedContent.user, user.id))) - .returning(); - - console.log(updatedMemory, newContent); - } - - if (!updatedMemory) { - [updatedMemory] = await db - .select() - .from(storedContent) - .where(and(eq(storedContent.id, id), eq(storedContent.user, user.id))); - } - - const removedFromSpaces = removeSpaces - ? removeSpaces.length > 0 - ? await db - .delete(contentToSpace) - .where( - and( - inArray(contentToSpace.spaceId, removeSpaces), - eq(contentToSpace.contentId, id), - ), - ) - .returning() - : [] - : spaces - ? spaces.length > 0 - ? await db - .delete(contentToSpace) - .where( - and( - notInArray(contentToSpace.spaceId, spaces), - eq(contentToSpace.contentId, id), - ), - ) - .returning() - : await db - .delete(contentToSpace) - .where(eq(contentToSpace.contentId, id)) - : []; - - const addedToSpaces = - spaces && spaces.length > 0 - ? await db - .insert(contentToSpace) - .values( - spaces.map((s) => ({ - contentId: id, - spaceId: s, - })), - ) - .onConflictDoNothing() - .returning() - : []; - - const resultedSpaces = - ( - await db - .select() - .from(contentToSpace) - .where(eq(contentToSpace.contentId, id)) - .all() - ).map((i) => i.spaceId) ?? []; - - return { - memory: updatedMemory, - addedToSpaces, - removedFromSpaces, - resultedSpaces, - }; -} - -export async function deleteSpace(id: number) { - const user = await getUser(); - - if (!user) { - return null; - } - - await db.delete(contentToSpace).where(eq(contentToSpace.spaceId, id)); - - const [deleted] = await db - .delete(space) - .where(and(eq(space.user, user.id), eq(space.id, id))) - .returning(); - - return deleted; -} - -export async function deleteMemory(id: number) { - const user = await getUser(); - - if (!user) { - return null; - } - - await db.delete(contentToSpace).where(eq(contentToSpace.contentId, id)); - - const [deleted] = await db - .delete(storedContent) - .where(and(eq(storedContent.user, user.id), eq(storedContent.id, id))) - .returning(); - - if (deleted) { - console.log("adding with:", `${deleted.url}-${user.email}`); - const res = (await Promise.race([ - fetch(`https://cf-ai-backend.dhravya.workers.dev/delete`, { - method: "DELETE", - headers: { - "X-Custom-Auth-Key": env.BACKEND_SECURITY_KEY, - }, - body: JSON.stringify({ - websiteUrl: deleted.url, - user: user.email, - }), - }), - new Promise((_, reject) => - setTimeout(() => reject(new Error("Request timed out")), 40000), - ), - ])) as Response; - } - - return deleted; -} diff --git a/apps/web/src/app/MessagePoster.tsx b/apps/web/src/app/MessagePoster.tsx deleted file mode 100644 index 1abad1be..00000000 --- a/apps/web/src/app/MessagePoster.tsx +++ /dev/null @@ -1,24 +0,0 @@ -"use client"; - -import { useEffect } from "react"; - -function MessagePoster({ jwt }: { jwt: string }) { - useEffect(() => { - if (typeof window === "undefined") return; - window.postMessage({ jwt }, "*"); - }, [jwt]); - - return ( - <button - className="p-2" - onClick={() => { - if (typeof window === "undefined") return; - window.postMessage({ jwt }, "*"); - }} - > - Extension Auth - </button> - ); -} - -export default MessagePoster; diff --git a/apps/web/src/app/api/[...nextauth]/route.ts b/apps/web/src/app/api/[...nextauth]/route.ts deleted file mode 100644 index db7d1fb8..00000000 --- a/apps/web/src/app/api/[...nextauth]/route.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { GET, POST } from "@/server/auth"; -export const runtime = "edge"; diff --git a/apps/web/src/app/api/addTweetsToDb/route.ts b/apps/web/src/app/api/addTweetsToDb/route.ts deleted file mode 100644 index 7fe2edba..00000000 --- a/apps/web/src/app/api/addTweetsToDb/route.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { db } from "@/server/db"; -import { eq } from "drizzle-orm"; -import { sessions, storedContent, users } from "@/server/db/schema"; -import { type NextRequest, NextResponse } from "next/server"; - -export const runtime = "edge"; - -interface TweetData { - tweetText: string; - postUrl: string; - authorName: string; - handle: string; - time: string; - saveToUser: string; -} - -export async function POST(req: NextRequest) { - const token = - req.cookies.get("next-auth.session-token")?.value ?? - req.cookies.get("__Secure-authjs.session-token")?.value ?? - req.cookies.get("authjs.session-token")?.value ?? - req.headers.get("Authorization")?.replace("Bearer ", ""); - - if (!token) { - return new Response( - JSON.stringify({ message: "Invalid Key, TOKEN not found." }), - { status: 404 }, - ); - } - - const sessionData = await db - .select() - .from(sessions) - .where(eq(sessions.sessionToken, token!)); - - if (!sessionData || sessionData.length === 0) { - return new Response( - JSON.stringify({ message: "Invalid Key, session not found." }), - { status: 404 }, - ); - } - - const user = await db - .select() - .from(users) - .where(eq(users.id, sessionData[0].userId)) - .limit(1); - - if (!user || user.length === 0) { - return NextResponse.json( - { message: "Invalid Key, session not found." }, - { status: 404 }, - ); - } - - const session = { session: sessionData[0], user: user[0] }; - - const data = (await req.json()) as TweetData[]; - - for (const tweet of data) { - const { id } = ( - await db - .insert(storedContent) - .values({ - content: tweet.tweetText, - title: "Twitter Bookmark", - description: "", - url: tweet.postUrl, - baseUrl: "https://twitter.com", - image: "https://supermemory.dhr.wtf/twitter.svg", - savedAt: new Date(), - user: session.user.id, - type: "twitter-bookmark", - }) - .returning({ id: storedContent.id }) - )[0]; - - if (!id) { - return NextResponse.json( - { - message: "Error", - error: - "Something went wrong when inserting the tweet to storedContent", - }, - { status: 500 }, - ); - } - } - - return NextResponse.json({ message: "OK", data: "Success" }, { status: 200 }); -} diff --git a/apps/web/src/app/api/ask/route.ts b/apps/web/src/app/api/ask/route.ts deleted file mode 100644 index 17b24b3e..00000000 --- a/apps/web/src/app/api/ask/route.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { db } from "@/server/db"; -import { eq } from "drizzle-orm"; -import { sessions, users } from "@/server/db/schema"; -import { type NextRequest, NextResponse } from "next/server"; -import { env } from "@/env"; - -export const runtime = "edge"; - -export async function POST(req: NextRequest) { - const token = - req.cookies.get("next-auth.session-token")?.value ?? - req.cookies.get("__Secure-authjs.session-token")?.value ?? - req.cookies.get("authjs.session-token")?.value ?? - req.headers.get("Authorization")?.replace("Bearer ", ""); - - if (process.env.RATELIMITER) { - const { success } = await process.env.RATELIMITER.limit({ key: token }); - - if (!success) { - return new Response(JSON.stringify({ message: "Rate limit exceeded" }), { - status: 429, - }); - } - } - - const sessionData = await db - .select() - .from(sessions) - .where(eq(sessions.sessionToken, token!)); - - if (!sessionData || sessionData.length === 0) { - return new Response( - JSON.stringify({ message: "Invalid Key, session not found." }), - { status: 404 }, - ); - } - - const user = await db - .select() - .from(users) - .where(eq(users.id, sessionData[0].userId)) - .limit(1); - - if (!user || user.length === 0) { - return NextResponse.json( - { message: "Invalid Key, session not found." }, - { status: 404 }, - ); - } - - const body = (await req.json()) as { - query: string; - }; - - const resp = await fetch(`https://cf-ai-backend.dhravya.workers.dev/ask`, { - headers: { - "X-Custom-Auth-Key": env.BACKEND_SECURITY_KEY, - }, - method: "POST", - body: JSON.stringify({ - query: body.query, - }), - }); - - if (resp.status !== 200 || !resp.ok) { - const errorData = await resp.json(); - return new Response( - JSON.stringify({ message: "Error in CF function", error: errorData }), - { status: resp.status }, - ); - } - - // Stream the response back to the client - const { readable, writable } = new TransformStream(); - resp && resp.body!.pipeTo(writable); - - return new Response(readable, { status: 200 }); -} diff --git a/apps/web/src/app/api/chat/route.ts b/apps/web/src/app/api/chat/route.ts deleted file mode 100644 index c815070b..00000000 --- a/apps/web/src/app/api/chat/route.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { db } from "@/server/db"; -import { eq } from "drizzle-orm"; -import { sessions, users } from "@/server/db/schema"; -import { type NextRequest, NextResponse } from "next/server"; -import { env } from "@/env"; -import { ChatHistory } from "../../../../types/memory"; - -export const runtime = "edge"; - -export async function POST(req: NextRequest) { - const token = - req.cookies.get("next-auth.session-token")?.value ?? - req.cookies.get("__Secure-authjs.session-token")?.value ?? - req.cookies.get("authjs.session-token")?.value ?? - req.headers.get("Authorization")?.replace("Bearer ", ""); - - if (process.env.RATELIMITER) { - const { success } = await process.env.RATELIMITER.limit({ key: token }); - - if (!success) { - return new Response(JSON.stringify({ message: "Rate limit exceeded" }), { - status: 429, - }); - } - } - - const sessionData = await db - .select() - .from(sessions) - .where(eq(sessions.sessionToken, token!)); - - if (!sessionData || sessionData.length === 0) { - return new Response( - JSON.stringify({ message: "Invalid Key, session not found." }), - { status: 404 }, - ); - } - - const user = await db - .select() - .from(users) - .where(eq(users.id, sessionData[0].userId)) - .limit(1); - - if (!user || user.length === 0) { - return NextResponse.json( - { message: "Invalid Key, session not found." }, - { status: 404 }, - ); - } - - const session = { session: sessionData[0], user: user[0] }; - - const query = new URL(req.url).searchParams.get("q"); - const spaces = new URL(req.url).searchParams.get("spaces"); - - const sourcesOnly = - new URL(req.url).searchParams.get("sourcesOnly") ?? "false"; - - const chatHistory = (await req.json()) as { - chatHistory: ChatHistory[]; - }; - - console.log("CHathistory", chatHistory); - - if (!query) { - return new Response(JSON.stringify({ message: "Invalid query" }), { - status: 400, - }); - } - - try { - const resp = await fetch( - `https://cf-ai-backend.dhravya.workers.dev/chat?q=${query}&user=${session.user.email ?? session.user.name}&sourcesOnly=${sourcesOnly}&spaces=${spaces}`, - { - headers: { - "X-Custom-Auth-Key": env.BACKEND_SECURITY_KEY, - }, - method: "POST", - body: JSON.stringify({ - chatHistory: chatHistory.chatHistory ?? [], - }), - }, - ); - - console.log("sourcesOnly", sourcesOnly); - - if (sourcesOnly == "true") { - const data = await resp.json(); - console.log("data", data); - return new Response(JSON.stringify(data), { status: 200 }); - } - - if (resp.status !== 200 || !resp.ok) { - const errorData = await resp.json(); - console.log(errorData); - return new Response( - JSON.stringify({ message: "Error in CF function", error: errorData }), - { status: resp.status }, - ); - } - - // Stream the response back to the client - const { readable, writable } = new TransformStream(); - resp && resp.body!.pipeTo(writable); - - return new Response(readable, { status: 200 }); - } catch {} -} diff --git a/apps/web/src/app/api/getCount/route.ts b/apps/web/src/app/api/getCount/route.ts deleted file mode 100644 index 9fe54f78..00000000 --- a/apps/web/src/app/api/getCount/route.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { db } from "@/server/db"; -import { and, eq, ne, sql } from "drizzle-orm"; -import { sessions, storedContent, users } from "@/server/db/schema"; -import { type NextRequest, NextResponse } from "next/server"; - -export const runtime = "edge"; - -export async function GET(req: NextRequest) { - const token = - req.cookies.get("next-auth.session-token")?.value ?? - req.cookies.get("__Secure-authjs.session-token")?.value ?? - req.cookies.get("authjs.session-token")?.value ?? - req.headers.get("Authorization")?.replace("Bearer ", ""); - - if (!token) { - return new Response( - JSON.stringify({ message: "Invalid Key, session not found." }), - { status: 404 }, - ); - } - - const sessionData = await db - .select() - .from(sessions) - .where(eq(sessions.sessionToken, token!)); - - if (!sessionData || sessionData.length === 0) { - return new Response( - JSON.stringify({ message: "Invalid Key, session not found." }), - { status: 404 }, - ); - } - - const user = await db - .select() - .from(users) - .where(eq(users.id, sessionData[0].userId)) - .limit(1); - - if (!user || user.length === 0) { - return NextResponse.json( - { message: "Invalid Key, session not found." }, - { status: 404 }, - ); - } - - const session = { session: sessionData[0], user: user[0] }; - - const tweetsCount = await db - .select({ - count: sql<number>`count(*)`.mapWith(Number), - }) - .from(storedContent) - .where( - and( - eq(storedContent.user, session.user.id), - eq(storedContent.type, "twitter-bookmark"), - ), - ); - - const pageCount = await db - .select({ - count: sql<number>`count(*)`.mapWith(Number), - }) - .from(storedContent) - .where( - and( - eq(storedContent.user, session.user.id), - ne(storedContent.type, "twitter-bookmark"), - ), - ); - - return NextResponse.json({ - tweetsCount: tweetsCount[0].count, - tweetsLimit: 1000, - pageCount: pageCount[0].count, - pageLimit: 100, - user: session.user.email, - }); -} diff --git a/apps/web/src/app/api/me/route.ts b/apps/web/src/app/api/me/route.ts deleted file mode 100644 index 6d269872..00000000 --- a/apps/web/src/app/api/me/route.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { db } from "@/server/db"; -import { eq } from "drizzle-orm"; -import { sessions, users } from "@/server/db/schema"; -import { type NextRequest, NextResponse } from "next/server"; -import { env } from "@/env"; - -export const runtime = "edge"; - -export async function GET(req: NextRequest) { - const token = - req.cookies.get("next-auth.session-token")?.value ?? - req.cookies.get("__Secure-authjs.session-token")?.value ?? - req.cookies.get("authjs.session-token")?.value ?? - req.headers.get("Authorization")?.replace("Bearer ", ""); - - const session = await db - .select() - .from(sessions) - .where(eq(sessions.sessionToken, token!)); - - if (!session || session.length === 0) { - return new Response( - JSON.stringify({ message: "Invalid Key, session not found." }), - { status: 404 }, - ); - } - - const user = await db - .select() - .from(users) - .where(eq(users.id, session[0].userId)) - .limit(1); - - if (!user || user.length === 0) { - return NextResponse.json( - { message: "Invalid Key, session not found." }, - { status: 404 }, - ); - } - - return new Response( - JSON.stringify({ - message: "OK", - data: { session: session[0], user: user[0] }, - }), - { status: 200 }, - ); -} diff --git a/apps/web/src/app/api/query/route.ts b/apps/web/src/app/api/query/route.ts deleted file mode 100644 index 5806841e..00000000 --- a/apps/web/src/app/api/query/route.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { db } from "@/server/db"; -import { eq } from "drizzle-orm"; -import { sessions, users } from "@/server/db/schema"; -import { type NextRequest, NextResponse } from "next/server"; -import { env } from "@/env"; - -export const runtime = "edge"; - -export async function GET(req: NextRequest) { - const token = - req.cookies.get("next-auth.session-token")?.value ?? - req.cookies.get("__Secure-authjs.session-token")?.value ?? - req.cookies.get("authjs.session-token")?.value ?? - req.headers.get("Authorization")?.replace("Bearer ", ""); - - if (process.env.RATELIMITER) { - const { success } = await process.env.RATELIMITER.limit({ key: token }); - - if (!success) { - return new Response(JSON.stringify({ message: "Rate limit exceeded" }), { - status: 429, - }); - } - } - - const sessionData = await db - .select() - .from(sessions) - .where(eq(sessions.sessionToken, token!)); - - if (!sessionData || sessionData.length === 0) { - return new Response( - JSON.stringify({ message: "Invalid Key, session not found." }), - { status: 404 }, - ); - } - - const user = await db - .select() - .from(users) - .where(eq(users.id, sessionData[0].userId)) - .limit(1); - - if (!user || user.length === 0) { - return NextResponse.json( - { message: "Invalid Key, session not found." }, - { status: 404 }, - ); - } - - const session = { session: sessionData[0], user: user[0] }; - - const query = new URL(req.url).searchParams.get("q"); - const sourcesOnly = - new URL(req.url).searchParams.get("sourcesOnly") ?? "false"; - - if (!query) { - return new Response(JSON.stringify({ message: "Invalid query" }), { - status: 400, - }); - } - - const resp = await fetch( - `https://cf-ai-backend.dhravya.workers.dev/query?q=${query}&user=${session.user.email ?? session.user.name}&sourcesOnly=${sourcesOnly}`, - { - headers: { - "X-Custom-Auth-Key": env.BACKEND_SECURITY_KEY, - }, - }, - ); - - console.log(resp.status); - - if (resp.status !== 200 || !resp.ok) { - const errorData = await resp.json(); - console.log(errorData); - return new Response( - JSON.stringify({ message: "Error in CF function", error: errorData }), - { status: resp.status }, - ); - } - - // Stream the response back to the client - const { readable, writable } = new TransformStream(); - resp && resp.body!.pipeTo(writable); - - return new Response(readable, { status: 200 }); -} diff --git a/apps/web/src/app/api/spaces/route.ts b/apps/web/src/app/api/spaces/route.ts deleted file mode 100644 index d2685e9f..00000000 --- a/apps/web/src/app/api/spaces/route.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { db } from "@/server/db"; -import { sessions, space, users } from "@/server/db/schema"; -import { eq } from "drizzle-orm"; -import { NextRequest, NextResponse } from "next/server"; - -export const runtime = "edge"; - -export async function GET(req: NextRequest) { - const token = - req.cookies.get("next-auth.session-token")?.value ?? - req.cookies.get("__Secure-authjs.session-token")?.value ?? - req.cookies.get("authjs.session-token")?.value ?? - req.headers.get("Authorization")?.replace("Bearer ", ""); - - if (!token) { - return new Response( - JSON.stringify({ message: "Invalid Key, session not found." }), - { status: 404 }, - ); - } - - if (process.env.RATELIMITER) { - const { success } = await process.env.RATELIMITER.limit({ key: token }); - - if (!success) { - return new Response(JSON.stringify({ message: "Rate limit exceeded" }), { - status: 429, - }); - } - } - - const sessionData = await db - .select() - .from(sessions) - .where(eq(sessions.sessionToken, token!)); - - if (!sessionData || sessionData.length === 0) { - return new Response( - JSON.stringify({ message: "Invalid Key, session not found." }), - { status: 404 }, - ); - } - - const userData = await db - .select() - .from(users) - .where(eq(users.id, sessionData[0].userId)) - .limit(1); - - if (!userData || userData.length === 0) { - return NextResponse.json( - { message: "Invalid Key, session not found." }, - { status: 404 }, - ); - } - - const user = userData[0]; - - const spaces = await db - .select() - .from(space) - .where(eq(space.user, user.id)) - .all(); - - return NextResponse.json( - { - message: "OK", - data: spaces, - }, - { status: 200 }, - ); -} diff --git a/apps/web/src/app/api/store/route.ts b/apps/web/src/app/api/store/route.ts deleted file mode 100644 index 457eae2e..00000000 --- a/apps/web/src/app/api/store/route.ts +++ /dev/null @@ -1,165 +0,0 @@ -import { db } from "@/server/db"; -import { and, eq, sql, inArray } from "drizzle-orm"; -import { - contentToSpace, - sessions, - storedContent, - users, - space, -} from "@/server/db/schema"; -import { type NextRequest, NextResponse } from "next/server"; -import { env } from "@/env"; -import { getMetaData } from "@/server/helpers"; - -export const runtime = "edge"; - -export async function POST(req: NextRequest) { - const token = - req.cookies.get("next-auth.session-token")?.value ?? - req.cookies.get("__Secure-authjs.session-token")?.value ?? - req.cookies.get("authjs.session-token")?.value ?? - req.headers.get("Authorization")?.replace("Bearer ", ""); - - if (!token) { - return new Response( - JSON.stringify({ message: "Invalid Key, session not found." }), - { status: 404 }, - ); - } - - if (process.env.RATELIMITER) { - const { success } = await process.env.RATELIMITER.limit({ key: token }); - - if (!success) { - return new Response(JSON.stringify({ message: "Rate limit exceeded" }), { - status: 429, - }); - } - } - - const sessionData = await db - .select() - .from(sessions) - .where(eq(sessions.sessionToken, token!)); - - if (!sessionData || sessionData.length === 0) { - return new Response( - JSON.stringify({ message: "Invalid Key, session not found." }), - { status: 404 }, - ); - } - - const user = await db - .select() - .from(users) - .where(eq(users.id, sessionData[0].userId)) - .limit(1); - - if (!user || user.length === 0) { - return NextResponse.json( - { message: "Invalid Key, session not found." }, - { status: 404 }, - ); - } - - const session = { session: sessionData[0], user: user[0] }; - - const data = (await req.json()) as { - pageContent: string; - url: string; - spaces?: string[]; - }; - - const metadata = await getMetaData(data.url); - let storeToSpaces = data.spaces; - - if (!storeToSpaces) { - storeToSpaces = []; - } - - const count = await db - .select({ - count: sql<number>`count(*)`.mapWith(Number), - }) - .from(storedContent) - .where( - and( - eq(storedContent.user, session.user.id), - eq(storedContent.type, "page"), - ), - ); - - if (count[0].count > 100) { - return NextResponse.json( - { message: "Error", error: "Limit exceeded" }, - { status: 499 }, - ); - } - - const { id } = ( - await db - .insert(storedContent) - .values({ - content: data.pageContent, - title: metadata.title, - description: metadata.description, - url: data.url, - baseUrl: metadata.baseUrl, - image: metadata.image, - savedAt: new Date(), - user: session.user.id, - }) - .returning({ id: storedContent.id }) - )[0]; - - if (!id) { - return NextResponse.json( - { message: "Error", error: "Error in CF function" }, - { status: 500 }, - ); - } - - if (storeToSpaces.length > 0) { - const spaceData = await db - .select() - .from(space) - .where( - and( - inArray(space.name, storeToSpaces ?? []), - eq(space.user, session.user.id), - ), - ) - .all(); - - await Promise.all([ - spaceData.forEach(async (space) => { - await db - .insert(contentToSpace) - .values({ contentId: id, spaceId: space.id }); - }), - ]); - } - - const res = (await Promise.race([ - fetch("https://cf-ai-backend.dhravya.workers.dev/add", { - method: "POST", - headers: { - "X-Custom-Auth-Key": env.BACKEND_SECURITY_KEY, - }, - body: JSON.stringify({ ...data, user: session.user.email }), - }), - new Promise((_, reject) => - setTimeout(() => reject(new Error("Request timed out")), 40000), - ), - ])) as Response; - - if (res.status !== 200) { - console.log(res.status, res.statusText); - return NextResponse.json( - { message: "Error", error: "Error in CF function" }, - { status: 500 }, - ); - } - - return NextResponse.json({ message: "OK", data: "Success" }, { status: 200 }); -} diff --git a/apps/web/src/app/api/vectorizeTweets/route.ts b/apps/web/src/app/api/vectorizeTweets/route.ts deleted file mode 100644 index 63aa38f0..00000000 --- a/apps/web/src/app/api/vectorizeTweets/route.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { db } from "@/server/db"; -import { eq } from "drizzle-orm"; -import { sessions, storedContent, users } from "@/server/db/schema"; -import { type NextRequest, NextResponse } from "next/server"; -import { env } from "@/env"; - -export const runtime = "edge"; - -interface TweetData { - tweetText: string; - postUrl: string; - authorName: string; - handle: string; - time: string; - saveToUser: string; -} - -export async function POST(req: NextRequest) { - const token = - req.cookies.get("next-auth.session-token")?.value ?? - req.cookies.get("__Secure-authjs.session-token")?.value ?? - req.cookies.get("authjs.session-token")?.value ?? - req.headers.get("Authorization")?.replace("Bearer ", ""); - - if (!token) { - return new Response( - JSON.stringify({ message: "Invalid Key, session not found." }), - { status: 404 }, - ); - } - - const sessionData = await db - .select() - .from(sessions) - .where(eq(sessions.sessionToken, token!)); - - if (!sessionData || sessionData.length === 0) { - return new Response( - JSON.stringify({ message: "Invalid Key, session not found." }), - { status: 404 }, - ); - } - - const body = (await req.json()) as TweetData[]; - - console.log(body); - - const resp = await fetch( - `https://cf-ai-backend.dhravya.workers.dev/batchUploadTweets`, - { - headers: { - "X-Custom-Auth-Key": env.BACKEND_SECURITY_KEY, - }, - method: "POST", - body: JSON.stringify(body), - }, - ); - - return new Response(await resp.text(), { - status: resp.status, - headers: resp.headers, - }); -} diff --git a/apps/web/src/app/content.tsx b/apps/web/src/app/content.tsx deleted file mode 100644 index 5a68d902..00000000 --- a/apps/web/src/app/content.tsx +++ /dev/null @@ -1,18 +0,0 @@ -"use client"; -import SessionProviderWrapper from "@/components/dev/SessionProviderWrapper"; -import Main from "@/components/Main"; -import Sidebar from "@/components/Sidebar/index"; -import { useState } from "react"; - -export default function Content({ jwt }: { jwt: string }) { - const [selectedItem, setSelectedItem] = useState<string | null>(null); - - return ( - <SessionProviderWrapper> - <div className="flex w-screen"> - <Sidebar jwt={jwt} selectChange={setSelectedItem} /> - <Main sidebarOpen={selectedItem !== null} /> - </div> - </SessionProviderWrapper> - ); -} diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css deleted file mode 100644 index bed9278b..00000000 --- a/apps/web/src/app/globals.css +++ /dev/null @@ -1,140 +0,0 @@ -@import "@radix-ui/colors/gray"; -@import "@radix-ui/colors/gray-dark"; - -@tailwind base; -@tailwind components; -@tailwind utilities; - -:root { - --foreground-rgb: 0, 0, 0; - --background-start-rgb: 214, 219, 220; - --background-end-rgb: 255, 255, 255; -} - -@media (prefers-color-scheme: dark) { - :root { - --foreground-rgb: 255, 255, 255; - --background-start-rgb: 0, 0, 0; - --background-end-rgb: 0, 0, 0; - } -} - -body { - @apply text-rgray-11 max-h-screen overflow-y-hidden bg-white; - /* color: rgb(var(--foreground-rgb)); - background: linear-gradient( - to bottom, - transparent, - rgb(var(--background-end-rgb)) - ) - rgb(var(--background-start-rgb)); */ -} - -[vaul-drawer-wrapper] { - @apply bg-rgray-2; -} - -@layer utilities { - .text-balance { - text-wrap: balance; - } -} - -.sidebar { - height: 100vh; - height: 100dvh; - max-height: 100vh; - max-height: 100dvh; -} - -.DrawerContent { - padding-top: 5vh; - padding-top: 5dvh; -} - -.main-hidden { - padding-bottom: 20vh; - padding-bottom: 15dvh; -} - -.bottom-padding { - bottom: 20vh; - bottom: 20dvh; -} - -@media (min-width: 768px) { - .bottom-padding { - bottom: 0; - } -} - -.chat-answer code { - @apply bg-rgray-3 border-rgray-5 text-rgray-11 text-wrap rounded-md border p-1 text-sm; -} - -.novel-editor pre { - @apply bg-rgray-3 border-rgray-5 text-rgray-11 my-5 rounded-md border p-4 text-sm; -} - -.chat-answer h1 { - @apply text-rgray-11 my-5 text-xl font-medium; -} - -.chat-answer a { - @apply underline underline-offset-1 opacity-90 hover:opacity-100; -} - -.chat-answer img { - @apply my-5 rounded-md font-medium; -} - -.tippy-box { - @apply bg-rgray-3 text-rgray-11 border-rgray-5 rounded-md border py-0; -} - -.tippy-content #slash-command { - @apply text-rgray-11 border-none bg-transparent; -} - -#slash-command button { - @apply text-rgray-11 py-2; -} - -#slash-command button div:first-child { - @apply text-rgray-11 bg-rgray-4 border-rgray-5; -} - -#slash-command button.novel-bg-stone-100 { - @apply bg-rgray-1; -} - -.novel-editor [data-type="taskList"] > li { - @apply my-0; -} - -.novel-editor input[type="checkbox"] { - @apply accent-rgray-4 rounded-md; - - background: var(--gray-4) !important; - border: 1px solid var(--gray-10) !important; -} - -.novel-editor .is-empty::before { - content: "Press '/' for commands" !important; -} - -.novel-editor h1 { - @apply text-2xl; -} - -.novel-editor h2 { - @apply text-xl; -} - -.novel-editor h3 { - @apply text-lg; -} - -.novel-editor .drag-handle { - @apply hidden; -} diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx deleted file mode 100644 index e96df271..00000000 --- a/apps/web/src/app/layout.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import type { Metadata } from "next"; -import { Roboto, Inter } from "next/font/google"; -import "./globals.css"; - -const inter = Inter({ weight: ["300", "400", "500"], subsets: ["latin"] }); - -export const metadata: Metadata = { - title: "Supermemory - Your second brain", - description: "Save your memories forever, build your own second brain.", - openGraph: { - images: [ - { - url: "https://supermemory.dhr.wtf/og-image.png", - width: 1200, - height: 630, - }, - ], - siteName: "Supermemory", - title: "Supermemory - Your second brain", - description: "Save your memories forever, build your own second brain.", - }, - twitter: { - card: "summary_large_image", - site: "https://supermemory.dhr.wtf", - creator: "@dhravyashah", - description: "Save your memories forever, build your own second brain.", - images: [ - { - url: "https://supermemory.dhr.wtf/og-image.png", - width: 1200, - height: 630, - }, - ], - }, -}; - -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { - return ( - <html lang="en" className="dark"> - <head> - <meta - name="og:image" - content="https://supermemory.dhr.wtf/og-image.png" - /> - <script - async - src="https://u.dhr.wtf/script.js" - data-website-id="731dfc2e-b1c0-4696-a7b3-efd27b19dfdf" - ></script> - </head> - <body className={inter.className}> - <div - vaul-drawer-wrapper="" - className="min-w-screen overflow-x-hidden text-black" - > - {children} - </div> - </body> - </html> - ); -} diff --git a/apps/web/src/app/not-found.tsx b/apps/web/src/app/not-found.tsx deleted file mode 100644 index 3409889a..00000000 --- a/apps/web/src/app/not-found.tsx +++ /dev/null @@ -1,58 +0,0 @@ -export const runtime = "edge"; - -export default function NotFound() { - return ( - <> - <title>404: This page could not be found.</title> - <div style={styles.error}> - <div> - <style - dangerouslySetInnerHTML={{ - __html: `body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}`, - }} - /> - <h1 className="next-error-h1" style={styles.h1}> - 404 - </h1> - <div style={styles.desc}> - <h2 style={styles.h2}>This page could not be found.</h2> - </div> - </div> - </div> - </> - ); -} - -const styles = { - error: { - fontFamily: - 'system-ui,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"', - height: "100vh", - textAlign: "center", - display: "flex", - flexDirection: "column", - alignItems: "center", - justifyContent: "center", - }, - - desc: { - display: "inline-block", - }, - - h1: { - display: "inline-block", - margin: "0 20px 0 0", - padding: "0 23px 0 0", - fontSize: 24, - fontWeight: 500, - verticalAlign: "top", - lineHeight: "49px", - }, - - h2: { - fontSize: 14, - fontWeight: 400, - lineHeight: "49px", - margin: 0, - }, -} as const; diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx deleted file mode 100644 index 05cd1ab8..00000000 --- a/apps/web/src/app/page.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { db } from "@/server/db"; -import { - ChachedSpaceContent, - sessions, - space, - storedContent, - users, -} from "@/server/db/schema"; -import { and, eq, inArray, not } from "drizzle-orm"; -import { cookies, headers } from "next/headers"; -import { redirect } from "next/navigation"; -import { fetchContentForSpace, fetchFreeMemories } from "@/actions/db"; -import { MemoryProvider } from "@/contexts/MemoryContext"; -import Content from "./content"; -import Main from "@/components/Main"; -import { TailwindIndicator } from "@/components/dev/tailwindindicator"; - -export const runtime = "edge"; - -export default async function Home() { - const token = - cookies().get("next-auth.session-token")?.value ?? - cookies().get("__Secure-authjs.session-token")?.value ?? - cookies().get("authjs.session-token")?.value ?? - headers().get("Authorization")?.replace("Bearer ", ""); - - if (!token) { - return redirect("/api/auth/signin"); - } - - const session = await db - .select() - .from(sessions) - .where(eq(sessions.sessionToken, token!)); - - if (!session || session.length === 0) { - return redirect("/api/auth/signin"); - } - - const [userData] = await db - .select() - .from(users) - .where(eq(users.id, session[0].userId)) - .limit(1); - - if (!userData) { - return redirect("/api/auth/signin"); - } - - console.log(storedContent.user.name); - - const collectedSpaces = await db - .select() - .from(space) - .where(eq(space.user, userData.id)) - .all(); - - console.log(collectedSpaces); - - // Fetch only first 3 content of each spaces - let contents: ChachedSpaceContent[] = []; - - await Promise.all([ - collectedSpaces.forEach(async (space) => { - console.log("fetching "); - const data = ( - (await fetchContentForSpace(space.id, { - offset: 0, - limit: 3, - })) ?? [] - ).map((data) => ({ - ...data, - space: space.id, - })); - contents = [...contents, ...data]; - }), - ]); - - console.log("contents", contents); - - // freeMemories - const freeMemories = await fetchFreeMemories(); - console.log("free", freeMemories); - - return ( - <MemoryProvider - user={userData} - spaces={collectedSpaces} - freeMemories={freeMemories} - cachedMemories={contents} - > - <Content jwt={token} /> - <TailwindIndicator /> - {/* <MessagePoster jwt={token} /> */} - </MemoryProvider> - ); -} diff --git a/apps/web/src/app/privacy/page.tsx b/apps/web/src/app/privacy/page.tsx deleted file mode 100644 index 5e40cbe9..00000000 --- a/apps/web/src/app/privacy/page.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import React from "react"; -import Markdown from "react-markdown"; -import { policy } from "./privacy"; - -export const runtime = "edge"; - -function Page() { - return ( - <div> - <Markdown>{policy}</Markdown> - </div> - ); -} - -export default Page; diff --git a/apps/web/src/app/privacy/privacy.ts b/apps/web/src/app/privacy/privacy.ts deleted file mode 100644 index 2034f191..00000000 --- a/apps/web/src/app/privacy/privacy.ts +++ /dev/null @@ -1,49 +0,0 @@ -export const policy = ` -# Privacy Policy for AnyContext - -## Introduction - -This Privacy Policy provides detailed information on the handling, storage, and protection of your personal information by AnyContext, a browser extension developed and owned by Dhravya Shah in 2024. The extension is designed to enhance your browsing experience by providing contextual information based on the content of the web pages you visit. This policy outlines the types of data collected by AnyContext, how it is used, and the measures we take to protect your privacy. - -## Information Collection - -AnyContext collects the following types of information: - -1. **Web Browsing Data**: The extension has the capability to see all websites that users visit. However, AnyContext only stores data when the user actively clicks on the extension button while browsing. The browser history is not recorded, ensuring that your browsing activities remain private. - -2. **Current Page Data**: Upon activation (click) by the user, AnyContext stores data from the current HTML page. This data is used to provide relevant contextual information based on the content of the page you are viewing. - -3. **Personal Information**: When you interact with AnyContext, we may collect personal information including but not limited to your email address, session data, name, and profile picture. This information is collected to improve your user experience and to provide personalized services. - -## Data Storage and Security - -All collected data is securely stored in a SQLite database hosted on Cloudflare D1. We employ industry-standard security measures to protect your information from unauthorized access, alteration, disclosure, or destruction. Despite our efforts, no method of transmission over the Internet or method of electronic storage is 100% secure. Therefore, while we strive to use commercially acceptable means to protect your personal information, we cannot guarantee its absolute security. - -## Use of Information - -AnyContext uses the collected information for the following purposes: - -- To provide and improve the functionality of the extension. -- To offer personalized user experiences. -- To communicate with users regarding updates, support, and promotional offers, if consented. -- To ensure the security of our services and to detect, prevent, or address technical issues. - -## Sharing of Information - -AnyContext does not sell, trade, or rent users' personal identification information to others. We may share generic aggregated demographic information not linked to any personal identification information regarding visitors and users with our business partners, trusted affiliates, and advertisers for the purposes outlined above. - -## Your Privacy Rights - -You have the right to access, update, or delete your personal information that we hold. If you wish to exercise these rights, please contact us at the details provided below. - -## Changes to This Privacy Policy - -AnyContext reserves the right to update this privacy policy at any time. When we do, we will post a notification on our website and update the date at the top of this page. We encourage users to frequently check this page for any changes to stay informed about how we are protecting the personal information we collect. Your continued use of the service after the posting of changes to this policy will be deemed your acceptance of those changes. - -## Contact Us - -If you have any questions about this Privacy Policy, the practices of this site, or your dealings with this site, please contact us at: - -- Email: [email protected] - -This document was last updated on March 2, 2024.`; diff --git a/apps/web/src/assets/Bin.tsx b/apps/web/src/assets/Bin.tsx deleted file mode 100644 index d0793cef..00000000 --- a/apps/web/src/assets/Bin.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { cn } from "@/lib/utils"; -import { useEffect, useRef } from "react"; - -export const Bin: React.FC<React.HTMLAttributes<HTMLDivElement> & {}> = ({ - className, - ...props -}) => { - const icon = useRef<HTMLDivElement>(null); - - useEffect(() => { - let timeout: ReturnType<typeof setTimeout> | undefined; - - const observer = new MutationObserver(function (mutations) { - mutations.forEach(function (mutation) { - if ( - mutation.type === "attributes" && - mutation.attributeName === "data-open" && - (mutation.oldValue === "false" || mutation.oldValue === null) && - icon.current?.dataset["open"] === "true" - ) { - if (timeout) clearTimeout(timeout); - timeout = setTimeout(() => { - icon.current!.dataset["open"] = "false"; - }, 2000); - } - }); - }); - - observer.observe(icon.current!, { - attributes: true, //configure it to listen to attribute changes - }); - - return () => { - observer.disconnect(); - }; - }, []); - - return ( - <div - ref={icon} - data-open="false" - className={cn( - "relative z-[100] flex w-full origin-bottom flex-col items-center justify-center transition-transform delay-500 duration-500 data-[open='true']:-translate-y-2 data-[open='true']:scale-150 data-[open='true']:delay-0 [&[data-open='true']>[data-lid]]:rotate-[150deg] [&[data-open='true']>[data-lid]]:delay-0", - className, - )} - {...props} - > - <svg - data-lid - className="w-full origin-[90%_80%] transition-transform delay-500 duration-500 ease-in-out" - viewBox="0 0 24 7" - fill="none" - strokeWidth={1} - xmlns="http://www.w3.org/2000/svg" - > - <path - d="M3 6H21" - stroke="currentColor" - strokeWidth={1} - strokeLinecap="round" - strokeLinejoin="round" - /> - <path - d="M8 6V4C8 3 9 2 10 2H14C15 2 16 3 16 4V6" - stroke="currentColor" - strokeWidth={1} - strokeLinecap="round" - strokeLinejoin="round" - /> - </svg> - - <svg - className="-mt-[1px] w-full" - viewBox="0 0 24 19" - fill="none" - strokeWidth={1} - data-trash-bin - xmlns="http://www.w3.org/2000/svg" - > - <path - d="M19 1V15C19 16 18 17 17 17H7C6 17 5 16 5 15V1" - stroke="currentColor" - strokeWidth={1} - strokeLinecap="round" - strokeLinejoin="round" - /> - <path - d="M10 6V12" - stroke="currentColor" - strokeWidth={1} - strokeLinecap="round" - strokeLinejoin="round" - /> - <path - d="M14 6V12" - stroke="currentColor" - strokeWidth={1} - strokeLinecap="round" - strokeLinejoin="round" - /> - </svg> - </div> - ); -}; diff --git a/apps/web/src/assets/Memories.tsx b/apps/web/src/assets/Memories.tsx deleted file mode 100644 index cafcd54f..00000000 --- a/apps/web/src/assets/Memories.tsx +++ /dev/null @@ -1,69 +0,0 @@ -export const MemoryIcon: React.FC<React.SVGAttributes<SVGElement>> = ( - props, -) => ( - <svg - viewBox="0 0 89 53" - fill="none" - xmlns="http://www.w3.org/2000/svg" - {...props} - > - <rect - x="0.40697" - y="8.52821" - width="43.0286" - height="43.0286" - rx="5.5" - transform="rotate(-12 0.40697 8.52821)" - fill="white" - stroke="black" - /> - <rect - x="20.8257" - y="9.19775" - width="43" - height="43" - rx="5.5" - fill="white" - stroke="black" - /> - <rect - x="47.6965" - y="-0.612372" - width="43.0286" - height="43.0286" - rx="5.5" - transform="rotate(15 47.6965 -0.612372)" - fill="white" - stroke="black" - /> - </svg> -); - -export const SpaceIcon: React.FC<React.SVGAttributes<SVGElement>> = (props) => ( - <svg - viewBox="0 0 34 30" - fill="none" - xmlns="http://www.w3.org/2000/svg" - {...props} - > - <rect - x="1.39502" - y="5.2229" - width="24" - height="24" - rx="5.5" - fill="var(--gray-5)" - stroke="var(--gray-10)" - /> - <rect - x="11.2231" - y="-0.157702" - width="24" - height="24" - rx="5.5" - transform="rotate(20 11.2231 -0.157702)" - fill="var(--gray-5)" - stroke="var(--gray-10)" - /> - </svg> -); diff --git a/apps/web/src/assets/MemoryWithImages.tsx b/apps/web/src/assets/MemoryWithImages.tsx deleted file mode 100644 index 6f7ba90a..00000000 --- a/apps/web/src/assets/MemoryWithImages.tsx +++ /dev/null @@ -1,531 +0,0 @@ -import { svgId } from "@/lib/utils"; - -export const MemoryWithImage: React.FC< - { image: string; id: string } & React.SVGAttributes<SVGElement> -> = ({ image, id: _id, ...props }) => { - const id = "space-1-" + _id; - - return ( - <svg - viewBox="0 0 137 137" - fill="none" - xmlns="http://www.w3.org/2000/svg" - xmlnsXlink="http://www.w3.org/1999/xlink" - {...props} - > - <g filter={`url(#${svgId(id, "filter0_d_83_72")})`}> - <rect - x="33" - y="33" - width="72.207" - height="72.207" - rx="10" - fill="white" - /> - </g> - <rect - x="47.0637" - y="46.9976" - width="45" - height="45" - fill={`url(#${svgId(id, "pattern0")})`} - /> - <defs> - <filter - id={svgId(id, "filter0_d_83_72")} - x="21" - y="21" - width="96.207" - height="96.207" - filterUnits="userSpaceOnUse" - colorInterpolationFilters="sRGB" - > - <feFlood floodOpacity="0" result="BackgroundImageFix" /> - <feColorMatrix - in="SourceAlpha" - type="matrix" - values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" - result="hardAlpha" - /> - <feMorphology - radius="2" - operator="dilate" - in="SourceAlpha" - result="effect1_dropShadow_83_72" - /> - <feOffset /> - <feGaussianBlur stdDeviation="5" /> - <feComposite in2="hardAlpha" operator="out" /> - <feColorMatrix - type="matrix" - values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0" - /> - <feBlend - mode="normal" - in2="BackgroundImageFix" - result="effect1_dropShadow_83_72" - /> - <feBlend - mode="normal" - in="SourceGraphic" - in2="effect1_dropShadow_83_72" - result="shape" - /> - </filter> - <pattern - id={svgId(id, "pattern0")} - patternContentUnits="objectBoundingBox" - width="1" - height="1" - > - <use - xlinkHref={`#${svgId(id, "image0_83_72")}`} - transform="scale(0.00520833)" - /> - </pattern> - <image - id={svgId(id, "image0_83_72")} - width="192" - height="192" - xlinkHref={image} - /> - </defs> - </svg> - ); -}; - -export const MemoryWithImages2: React.FC< - { images: string[]; id: string } & React.SVGAttributes<SVGElement> -> = ({ images, id: _id, ...props }) => { - const id = "space-2-" + _id; - - return ( - <svg - viewBox="0 0 137 137" - fill="none" - xmlns="http://www.w3.org/2000/svg" - xmlnsXlink="http://www.w3.org/1999/xlink" - {...props} - > - <g clipPath={`url(#${svgId(id, "clip0_80_62")})`}> - <g filter={`url(#${svgId(id, "filter0_d_80_62")})`}> - <rect - x="7" - y="51.5427" - width="72.207" - height="72.207" - rx="10" - transform="rotate(-24.1922 7 51.5427)" - fill="#F4F3F2" - /> - </g> - <rect - x="26.2664" - y="58.9253" - width="45" - height="45" - transform="rotate(-24.2 26.2664 58.9253)" - fill={`url(#${svgId(id, "pattern0")})`} - /> - <g filter={`url(#${svgId(id, "filter1_d_80_62")})`}> - <rect - x="59.9409" - y="42.2124" - width="72.207" - height="72.207" - rx="10" - transform="rotate(10.2301 59.9409 42.2124)" - fill="#ffffff" - /> - </g> - <rect - x="71.2952" - y="58.4851" - width="45" - height="45" - transform="rotate(10.23 71.2952 58.4851)" - fill={`url(#${svgId(id, "pattern1")})`} - /> - </g> - <defs> - <filter - id={svgId(id, "filter0_d_80_62")} - x="-1.78271" - y="13.1697" - width="113.021" - height="113.021" - filterUnits="userSpaceOnUse" - colorInterpolationFilters="sRGB" - > - <feFlood floodOpacity="0" result="BackgroundImageFix" /> - <feColorMatrix - in="SourceAlpha" - type="matrix" - values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" - result="hardAlpha" - /> - <feMorphology - radius="2" - operator="dilate" - in="SourceAlpha" - result="effect1_dropShadow_80_62" - /> - <feOffset /> - <feGaussianBlur stdDeviation="5" /> - <feComposite in2="hardAlpha" operator="out" /> - <feColorMatrix - type="matrix" - values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0" - /> - <feBlend - mode="normal" - in2="BackgroundImageFix" - result="effect1_dropShadow_80_62" - /> - <feBlend - mode="normal" - in="SourceGraphic" - in2="effect1_dropShadow_80_62" - result="shape" - /> - </filter> - <pattern - id={svgId(id, "pattern0")} - patternContentUnits="objectBoundingBox" - width="1" - height="1" - > - <use - xlinkHref={`#${svgId(id, "image0_80_62")}`} - transform="scale(0.0111111)" - /> - </pattern> - <filter - id={svgId(id, "filter1_d_80_62")} - x="36.7322" - y="31.8276" - width="104.652" - height="104.653" - filterUnits="userSpaceOnUse" - colorInterpolationFilters="sRGB" - > - <feFlood floodOpacity="0" result="BackgroundImageFix" /> - <feColorMatrix - in="SourceAlpha" - type="matrix" - values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" - result="hardAlpha" - /> - <feMorphology - radius="2" - operator="dilate" - in="SourceAlpha" - result="effect1_dropShadow_80_62" - /> - <feOffset /> - <feGaussianBlur stdDeviation="5" /> - <feComposite in2="hardAlpha" operator="out" /> - <feColorMatrix - type="matrix" - values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0" - /> - <feBlend - mode="normal" - in2="BackgroundImageFix" - result="effect1_dropShadow_80_62" - /> - <feBlend - mode="normal" - in="SourceGraphic" - in2="effect1_dropShadow_80_62" - result="shape" - /> - </filter> - <pattern - id={svgId(id, "pattern1")} - patternContentUnits="objectBoundingBox" - width="1" - height="1" - > - <use - xlinkHref={`#${svgId(id, "image1_80_62")}`} - transform="scale(0.00520833)" - /> - </pattern> - <clipPath id={svgId(id, "clip0_80_62")}> - <rect width="137" height="137" fill="white" /> - </clipPath> - <image - id={svgId(id, "image0_80_62")} - width="90" - height="90" - xlinkHref={images[0]} - /> - <image - id={svgId(id, "image1_80_62")} - width="192" - height="192" - xlinkHref={images[1]} - /> - </defs> - </svg> - ); -}; - -export const MemoryWithImages3: React.FC< - { images: string[]; id: string } & React.SVGAttributes<SVGElement> -> = ({ images, id: _id, ...props }) => { - const id = "space-3-" + _id; - - return ( - <svg - viewBox="0 0 137 137" - fill="none" - xmlns="http://www.w3.org/2000/svg" - xmlnsXlink="http://www.w3.org/1999/xlink" - {...props} - > - <g clipPath={`url(#${svgId(id, "clip0_79_36")})`}> - <g filter={`url(#${svgId(id, "filter0_d_79_36")})`}> - <rect - x="53.5242" - y="12" - width="72.207" - height="72.207" - rx="10" - transform="rotate(14.9009 53.5242 12)" - fill="white" - className="shadow-md" - /> - </g> - <rect - x="63.3663" - y="27.3052" - width="45" - height="45" - transform="rotate(14.9 63.3663 27.3052)" - fill={`url(#${svgId(id, "pattern0")})`} - /> - <g filter={`url(#${svgId(id, "filter1_d_79_36")})`}> - <rect - x="7" - y="51.5427" - width="72.207" - height="72.207" - rx="10" - transform="rotate(-24.1922 7 51.5427)" - fill="#F4F3F2" - className="shadow-md" - /> - </g> - <rect - x="26.2664" - y="58.9253" - width="45" - height="45" - transform="rotate(-24.2 26.2664 58.9253)" - fill={`url(#${svgId(id, "pattern1")})`} - /> - <g filter={`url(#${svgId(id, "filter2_d_79_36")})`}> - <rect - x="59.9409" - y="42.2124" - width="72.207" - height="72.207" - rx="10" - transform="rotate(10.2301 59.9409 42.2124)" - fill="#E7E5E4" - className="shadow-md" - /> - </g> - <rect - x="71.2952" - y="58.4851" - width="45" - height="45" - transform="rotate(10.23 71.2952 58.4851)" - fill={`url(#${svgId(id, "pattern2")})`} - /> - </g> - <defs> - <filter - id={svgId(id, "filter0_d_79_36")} - x="25.189" - y="2.23267" - width="107.881" - height="107.881" - filterUnits="userSpaceOnUse" - colorInterpolationFilters="sRGB" - > - <feFlood floodOpacity="0" result="BackgroundImageFix" /> - <feColorMatrix - in="SourceAlpha" - type="matrix" - values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" - result="hardAlpha" - /> - <feMorphology - radius="2" - operator="dilate" - in="SourceAlpha" - result="effect1_dropShadow_79_36" - /> - <feOffset /> - <feGaussianBlur stdDeviation="5" /> - <feComposite in2="hardAlpha" operator="out" /> - <feColorMatrix - type="matrix" - values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0" - /> - <feBlend - mode="normal" - in2="BackgroundImageFix" - result="effect1_dropShadow_79_36" - /> - <feBlend - mode="normal" - in="SourceGraphic" - in2="effect1_dropShadow_79_36" - result="shape" - /> - </filter> - <pattern - id={svgId(id, "pattern0")} - patternContentUnits="objectBoundingBox" - width="1" - height="1" - > - <use - xlinkHref={`#${svgId(id, "image0_79_36")}`} - transform="scale(0.0111111)" - /> - </pattern> - <filter - id={svgId(id, "filter1_d_79_36")} - x="-1.78271" - y="13.1697" - width="113.021" - height="113.021" - filterUnits="userSpaceOnUse" - colorInterpolationFilters="sRGB" - > - <feFlood floodOpacity="0" result="BackgroundImageFix" /> - <feColorMatrix - in="SourceAlpha" - type="matrix" - values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" - result="hardAlpha" - /> - <feMorphology - radius="2" - operator="dilate" - in="SourceAlpha" - result="effect1_dropShadow_79_36" - /> - <feOffset /> - <feGaussianBlur stdDeviation="5" /> - <feComposite in2="hardAlpha" operator="out" /> - <feColorMatrix - type="matrix" - values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0" - /> - <feBlend - mode="normal" - in2="BackgroundImageFix" - result="effect1_dropShadow_79_36" - /> - <feBlend - mode="normal" - in="SourceGraphic" - in2="effect1_dropShadow_79_36" - result="shape" - /> - </filter> - <pattern - id={svgId(id, "pattern1")} - patternContentUnits="objectBoundingBox" - width="1" - height="1" - > - <use - xlinkHref={`#${svgId(id, "image1_79_36")}`} - transform="scale(0.0111111)" - /> - </pattern> - <filter - id={svgId(id, "filter2_d_79_36")} - x="36.7322" - y="31.8276" - width="104.652" - height="104.653" - filterUnits="userSpaceOnUse" - colorInterpolationFilters="sRGB" - > - <feFlood floodOpacity="0" result="BackgroundImageFix" /> - <feColorMatrix - in="SourceAlpha" - type="matrix" - values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" - result="hardAlpha" - /> - <feMorphology - radius="2" - operator="dilate" - in="SourceAlpha" - result="effect1_dropShadow_79_36" - /> - <feOffset /> - <feGaussianBlur stdDeviation="5" /> - <feComposite in2="hardAlpha" operator="out" /> - <feColorMatrix - type="matrix" - values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0" - /> - <feBlend - mode="normal" - in2="BackgroundImageFix" - result="effect1_dropShadow_79_36" - /> - <feBlend - mode="normal" - in="SourceGraphic" - in2="effect1_dropShadow_79_36" - result="shape" - /> - </filter> - <pattern - id={svgId(id, "pattern2")} - patternContentUnits="objectBoundingBox" - width="1" - height="1" - > - <use - xlinkHref={`#${svgId(id, "image2_79_36")}`} - transform="scale(0.00520833)" - /> - </pattern> - <clipPath id={svgId(id, "clip0_79_36")}> - <rect width="137" height="137" fill="white" /> - </clipPath> - <image - id={svgId(id, "image0_79_36")} - width="90" - height="90" - xlinkHref={images[0]} - /> - <image - id={svgId(id, "image1_79_36")} - width="90" - height="90" - xlinkHref={images[1]} - /> - <image - id={svgId(id, "image2_79_36")} - width="192" - height="192" - xlinkHref={images[2]} - /> - </defs> - </svg> - ); -}; diff --git a/apps/web/src/assets/Note.tsx b/apps/web/src/assets/Note.tsx deleted file mode 100644 index e69de29b..00000000 --- a/apps/web/src/assets/Note.tsx +++ /dev/null diff --git a/apps/web/src/components/ChatMessage.tsx b/apps/web/src/components/ChatMessage.tsx deleted file mode 100644 index 58ef9870..00000000 --- a/apps/web/src/components/ChatMessage.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import React, { useEffect } from "react"; -import { motion } from "framer-motion"; -import { ArrowUpRight, Globe, Text } from "lucide-react"; -import { convertRemToPixels } from "@/lib/utils"; -import { SpaceIcon } from "@/assets/Memories"; -import Markdown from "react-markdown"; -import { ChatHistory } from "../../types/memory"; - -export function ChatAnswer({ - children: message, - sources, - loading = false, -}: { - children: string; - sources?: ChatHistory["answer"]["sources"]; - loading?: boolean; -}) { - return ( - <div className="flex h-max w-full flex-col items-start gap-5"> - {loading ? ( - <MessageSkeleton /> - ) : ( - <div className="chat-answer h-full w-full text-lg text-white/60"> - <Markdown>{message}</Markdown> - </div> - )} - {!loading && sources && sources?.length > 0 && ( - <> - <h1 className="animate-fade-in text-rgray-12 text-md flex items-center justify-center gap-2 opacity-0 [animation-duration:1s]"> - <SpaceIcon className="h-6 w-6 -translate-y-[2px]" /> - Related Memories - </h1> - <div className="animate-fade-in -mt-3 flex items-center justify-start gap-1 opacity-0 [animation-duration:1s]"> - {sources?.map((source) => - source.isNote ? ( - <button className="bg-rgray-3 flex items-center justify-center gap-2 rounded-full py-1 pl-2 pr-3 text-sm"> - <Text className="h-4 w-4" /> - {source.source} - </button> - ) : ( - <a - className="bg-rgray-3 flex items-center justify-center gap-2 rounded-full py-1 pl-2 pr-3 text-sm" - key={source.source} - href={source.source} - target="_blank" - > - <Globe className="h-4 w-4" /> - {cleanUrl(source.source)} - </a> - ), - )} - </div> - </> - )} - </div> - ); -} - -export function ChatQuestion({ children }: { children: string }) { - return ( - <div - className={`text-rgray-12 h-max w-full text-left ${children.length > 200 ? "text-xl" : "text-2xl"}`} - > - {children} - </div> - ); -} - -export function ChatMessage({ - children, - isLast = false, - index, -}: { - children: React.ReactNode | React.ReactNode[]; - isLast?: boolean; - index: number; -}) { - const messageRef = React.useRef<HTMLDivElement>(null); - - useEffect(() => { - if (!isLast) return; - messageRef.current?.parentElement?.scrollTo({ - top: messageRef.current?.offsetTop, - behavior: "smooth", - }); - }, []); - - return ( - <motion.div - initial={{ opacity: 0, y: 20 }} - animate={{ opacity: 1, y: 0 }} - transition={{ - type: "tween", - duration: 0.5, - }} - ref={messageRef} - className={`${index === 0 ? "pt-16" : "pt-28"} flex h-max w-full resize-y flex-col items-start justify-start gap-5 transition-[height] ${isLast ? "min-h-screen pb-[40vh]" : "h-max"}`} - > - {children} - </motion.div> - ); -} - -function MessageSkeleton() { - return ( - <div className="animate-fade-in flex w-full flex-col items-start gap-3 opacity-0 [animation-delay:0.5s] [animation-duration:1s]"> - <div className="bg-rgray-5 h-6 w-full animate-pulse rounded-md text-lg"></div> - <div className="bg-rgray-5 h-6 w-full animate-pulse rounded-md text-lg"></div> - <div className="bg-rgray-5 h-6 w-full animate-pulse rounded-md text-lg"></div> - <div className="bg-rgray-5 h-6 w-full animate-pulse rounded-md text-lg"></div> - <div className="bg-rgray-5 h-6 w-[70%] animate-pulse rounded-md text-lg"></div> - </div> - ); -} - -function cleanUrl(url: string) { - if (url.startsWith("https://")) { - url = url.slice(8); - } else if (url.startsWith("http://")) { - url = url.slice(7); - } - - if (url.endsWith("/")) { - url = url.slice(0, -1); - } - - return url; -} diff --git a/apps/web/src/components/Main-2.tsx b/apps/web/src/components/Main-2.tsx deleted file mode 100644 index 1b602712..00000000 --- a/apps/web/src/components/Main-2.tsx +++ /dev/null @@ -1,709 +0,0 @@ -"use client"; -import { MemoryDrawer } from "./MemoryDrawer"; -import useViewport from "@/hooks/useViewport"; -import { AnimatePresence } from "framer-motion"; -import { cn } from "@/lib/utils"; - -import { Editor } from "novel"; -import { useAutoAnimate } from "@formkit/auto-animate/react"; -import { - MemoryWithImage, - MemoryWithImages3, - MemoryWithImages2, -} from "@/assets/MemoryWithImages"; -import { Input, InputWithIcon } from "./ui/input"; -import { - ArrowUpRight, - Edit3, - Loader, - Minus, - MoreHorizontal, - Plus, - Search, - Sparkles, - Text, - Trash2, -} from "lucide-react"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "./ui/dropdown-menu"; -import { useEffect, useMemo, useRef, useState } from "react"; -import { Variant, useAnimate, motion } from "framer-motion"; -import { SearchResult, useMemory } from "@/contexts/MemoryContext"; -import { SpaceIcon } from "@/assets/Memories"; -import { Dialog, DialogContent } from "./ui/dialog"; -import useTouchHold from "@/hooks/useTouchHold"; -import { DialogTrigger } from "@radix-ui/react-dialog"; -import { - AddExistingMemoryToSpace, - AddMemoryPage, - NoteAddPage, - SpaceAddPage, -} from "./Sidebar/AddMemoryDialog"; -import { ExpandedSpace } from "./Sidebar/ExpandedSpace"; -import { StoredContent, StoredSpace } from "@/server/db/schema"; -import { useDebounce } from "@/hooks/useDebounce"; -import { NoteEdit } from "./Sidebar/EditNoteDialog"; -import DeleteConfirmation from "./Sidebar/DeleteConfirmation"; - -import { ProfileDrawer } from "./ProfileDrawer"; - -function supportsDVH() { - try { - return CSS.supports("height: 100dvh"); - } catch { - return false; - } -} - -function pseudoRandomizeColorWithName(name: string) { - const colorsAvailable = [ - "99e9f2", - "a5d8ff", - "d0bfff", - "eebefa", - "fcc2d7", - "b2f2bb", - "96f2d7", - "ffec99", - "ffd8a8", - "ffc9c9", - ]; - - const colorIndex = - name - .split("") - .map((char) => char.charCodeAt(0)) - .reduce((acc, charCode) => acc + charCode, 0) % colorsAvailable.length; - - return colorsAvailable[colorIndex]; -} - -export default function Main({ sidebarOpen }: { sidebarOpen: boolean }) { - const { width } = useViewport(); - - const [parent, enableAnimations] = useAutoAnimate(); - const { spaces, deleteSpace, freeMemories, search } = useMemory(); - - const [isDropdownOpen, setIsDropdownOpen] = useState(false); - const [addMemoryState, setAddMemoryState] = useState< - "page" | "note" | "space" | "existing-memory" | null - >(null); - - const [expandedSpace, setExpandedSpace] = useState<number | null>(null); - - const [searchQuery, setSearcyQuery] = useState(""); - const [searchLoading, setSearchLoading] = useState(false); - const query = useDebounce(searchQuery, 500); - - const [searchResults, setSearchResults] = useState<SearchResult[]>([]); - - useEffect(() => { - const q = query.trim(); - if (q.length < 1) { - setSearchResults([]); - return; - } - - setSearchLoading(true); - - (async () => { - setSearchResults(await search(q)); - setSearchLoading(false); - })(); - }, [query]); - - // useEffect(() => { - // if (!isOpen) { - // setExpandedSpace(null); - // } - // }, [isOpen]); - - if (expandedSpace) { - return ( - <ExpandedSpace - spaceId={expandedSpace} - back={() => setExpandedSpace(null)} - // close={() => setExpandedSpace(null)} - /> - ); - } - - return ( - <> - <AnimatePresence mode="wait"> - <main - data-sidebar-open={sidebarOpen} - className={cn( - "sidebar relative flex w-full flex-col items-end gap-5 overflow-auto bg-[#FFF] px-5 pt-5 transition-[padding-left,padding-top,padding-right] delay-200 duration-200 md:items-center md:gap-10 md:px-72 [&[data-sidebar-open='true']]:pr-10 [&[data-sidebar-open='true']]:delay-0 md:[&[data-sidebar-open='true']]:pl-[calc(2.5rem+30vw)]", - )} - > - <div className="mt-16 w-full"> - <div className="flex justify-between gap-4"> - <h1 className="w-full text-3xl font-medium tracking-tight"> - Your Memories - </h1> - <div className="flex w-full"> - <AddMemoryModal type={addMemoryState}> - <DropdownMenu - open={isDropdownOpen} - onOpenChange={setIsDropdownOpen} - > - <DropdownMenuTrigger asChild> - <button className="focus-visible:ring-rgray-7 ml-auto flex items-center justify-center rounded-md px-3 py-2 transition hover:bg-stone-200 focus-visible:bg-white focus-visible:outline-none focus-visible:ring-2"> - <Plus className="mr-2 h-5 w-5" /> - Add - </button> - </DropdownMenuTrigger> - <DropdownMenuContent - onCloseAutoFocus={(e) => e.preventDefault()} - > - <DialogTrigger className="block w-full"> - <DropdownMenuItem - onClick={() => { - setAddMemoryState("page"); - }} - > - <Sparkles className="mr-2 h-4 w-4" /> - Page to Memory - </DropdownMenuItem> - </DialogTrigger> - <DialogTrigger className="block w-full"> - <DropdownMenuItem - onClick={() => { - setAddMemoryState("note"); - }} - > - <Text className="mr-2 h-4 w-4" /> - Note - </DropdownMenuItem> - </DialogTrigger> - <DialogTrigger className="block w-full"> - <DropdownMenuItem - onClick={() => { - setAddMemoryState("space"); - }} - > - <SpaceIcon className="mr-2 h-4 w-4" /> - Space - </DropdownMenuItem> - </DialogTrigger> - </DropdownMenuContent> - </DropdownMenu> - </AddMemoryModal> - </div> - </div> - <InputWithIcon - placeholder="Search" - icon={ - searchLoading ? ( - <Loader className="h-5 w-5 animate-spin opacity-50" /> - ) : ( - <Search className="h-5 w-5 opacity-50" /> - ) - } - className="mt-4 w-full text-black" - value={searchQuery} - onChange={(e) => setSearcyQuery(e.target.value)} - /> - </div> - <div - ref={parent} - className="grid w-full grid-flow-row grid-cols-3 gap-4 px-2 py-5" - > - {typeof window !== "undefined" ? ( - query.trim().length > 0 ? ( - <> - {searchResults.map(({ type, space, memory }, i) => ( - <> - {type === "memory" && ( - <MemoryItem - {...memory!} - key={i} - onDelete={() => { - setSearchResults((prev) => - prev.filter((i) => i.memory?.id !== memory.id), - ); - }} - /> - )} - {type === "space" && ( - <SpaceItem - {...space!} - key={i} - onDelete={() => { - setSearchResults((prev) => - prev.filter((i) => i.space?.id !== space.id), - ); - deleteSpace(space.id); - }} - /> - )} - </> - ))} - </> - ) : ( - <> - {spaces.map((space) => ( - <SpaceItem - onDelete={() => deleteSpace(space.id)} - key={space.id} - onClick={() => setExpandedSpace(space.id)} - {...space} - /> - ))} - {freeMemories.map((m) => ( - <MemoryItem {...m} key={m.id} /> - ))} - </> - ) - ) : ( - <> - {Array.from({ - length: spaces.length + freeMemories.length, - }).map((_, i) => ( - <div className="h-32 w-full animate-pulse rounded-2xl bg-stone-300/50"></div> - ))} - </> - )} - </div> - <div className="absolute right-10 top-10 z-[100] block md:hidden"> - {width <= 768 && <ProfileDrawer />} - </div> - </main> - {width <= 768 && <MemoryDrawer />} - </AnimatePresence> - </> - ); -} - -export function MemoryItem( - props: StoredContent & { - onDelete?: () => void; - removeFromSpace?: () => Promise<void>; - }, -) { - const { id, title, image, type, url, onDelete, removeFromSpace } = props; - - const { deleteMemory } = useMemory(); - - const name = title - ? title.length > 20 - ? title.slice(0, 20) + "..." - : title - : "Untitled Memory"; - - const [isDialogOpen, setIsDialogOpen] = useState(false); - - const [moreDropdownOpen, setMoreDropdownOpen] = useState(false); - - const touchEventProps = useTouchHold({ - onHold() { - setMoreDropdownOpen(true); - }, - }); - return ( - <Dialog - open={type === "note" ? isDialogOpen : false} - onOpenChange={setIsDialogOpen} - > - <DialogTrigger asChild> - <button - onClick={() => (type === "page" ? window.open(url) : null)} - data-space-text - className="relative flex h-min select-none flex-col items-center justify-center gap-2 text-center font-normal focus-visible:outline-none" - > - <div - {...touchEventProps} - className="has-[[data-space-text]:focus-visible]:ring-rgray-7 [&:has-[[data-space-text]:focus-visible]>[data-more-button]]:opacity-100 flex h-32 w-full items-center justify-center rounded-2xl border-2 border-black/20 p-2 pb-4 shadow-sm ring-transparent transition duration-150 ease-out hover:scale-105 has-[[data-space-text]:focus-visible]:outline-none has-[[data-space-text]:focus-visible]:ring-2 md:has-[[data-state='true']]:bg-transparent [&:hover>[data-more-button]]:opacity-100" - style={{ - backgroundColor: `#${pseudoRandomizeColorWithName(name)}`, - }} - > - {type === "page" ? ( - <PageMoreButton - isOpen={moreDropdownOpen} - setIsOpen={setMoreDropdownOpen} - removeFromSpace={removeFromSpace} - onDelete={() => { - deleteMemory(id); - onDelete?.(); - }} - url={url} - /> - ) : type === "note" ? ( - <NoteMoreButton - isOpen={moreDropdownOpen} - setIsOpen={setMoreDropdownOpen} - removeFromSpace={removeFromSpace} - onEdit={() => setIsDialogOpen(true)} - onDelete={() => { - deleteMemory(id); - onDelete?.(); - }} - /> - ) : null} - - <div className="flex h-24 w-24 items-center justify-center"> - {type === "page" ? ( - <img - className="h-16 w-16" - id={id.toString()} - src={image!} - onError={(e) => { - (e.target as HTMLImageElement).src = - "/icons/white_without_bg.png"; - }} - /> - ) : type === "note" ? ( - <Text - onClick={() => setIsDialogOpen(true)} - className="h-16 w-16" - /> - ) : ( - <></> - )} - </div> - </div> - {name} - </button> - </DialogTrigger> - <DialogContent className="w-max max-w-[auto]"> - <NoteEdit - onDelete={onDelete} - closeDialog={() => setIsDialogOpen(false)} - memory={props} - /> - </DialogContent> - </Dialog> - ); -} - -export function SpaceItem({ - name, - id, - onDelete, - onClick, -}: StoredSpace & { onDelete: () => void; onClick?: () => void }) { - const { cachedMemories } = useMemory(); - - const [itemRef, animateItem] = useAnimate(); - const { width } = useViewport(); - - const [moreDropdownOpen, setMoreDropdownOpen] = useState(false); - - const touchEventProps = useTouchHold({ - onHold() { - setMoreDropdownOpen(true); - }, - }); - - const spaceMemories = useMemo(() => { - return cachedMemories.filter((m) => m.space === id); - }, [cachedMemories]); - - const _name = name.length > 20 ? name.slice(0, 20) + "..." : name; - - return ( - <button - onClick={onClick} - data-space-text - className="relative flex h-min select-none flex-col items-center justify-center gap-2 text-center font-normal focus-visible:outline-none" - > - <motion.div - ref={itemRef} - {...touchEventProps} - className="has-[[data-space-text]:focus-visible]:ring-rgray-7 [&:has-[[data-space-text]:focus-visible]>[data-more-button]]:opacity-100 flex h-32 w-full items-center justify-center rounded-2xl border-2 border-black/20 p-2 pb-4 shadow-sm ring-transparent transition duration-150 ease-out hover:scale-105 has-[[data-space-text]:focus-visible]:outline-none has-[[data-space-text]:focus-visible]:ring-2 md:has-[[data-state='true']]:bg-transparent [&:hover>[data-more-button]]:opacity-100" - style={{ - backgroundColor: `#${pseudoRandomizeColorWithName(name)}`, - }} - > - <SpaceMoreButton - isOpen={moreDropdownOpen} - setIsOpen={setMoreDropdownOpen} - onEdit={onClick} - onDelete={onDelete} - /> - {spaceMemories.length > 2 ? ( - <MemoryWithImages3 - onClick={onClick} - className="h-24 w-24" - id={id.toString()} - images={ - spaceMemories - .map((c) => (c.type === "note" ? "/note.svg" : c.image)) - .reverse() as string[] - } - /> - ) : spaceMemories.length > 1 ? ( - <MemoryWithImages2 - onClick={onClick} - className="h-24 w-24" - id={id.toString()} - images={ - spaceMemories - .map((c) => (c.type === "note" ? "/note.svg" : c.image)) - .reverse() as string[] - } - /> - ) : spaceMemories.length === 1 ? ( - <MemoryWithImage - onClick={onClick} - className="h-24 w-24" - id={id.toString()} - image={ - spaceMemories[0].type === "note" - ? "/note.svg" - : spaceMemories[0].image! - } - /> - ) : ( - <div - onClick={onClick} - className="flex items-center justify-center gap-2" - > - <svg - xmlns="http://www.w3.org/2000/svg" - fill="none" - viewBox="0 0 24 24" - strokeWidth="1.5" - stroke="currentColor" - className="h-8 w-8" - > - <path - strokeLinecap="round" - strokeLinejoin="round" - d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" - /> - </svg> - <span className="text-stone-800/80">Empty Space</span> - </div> - )} - </motion.div> - - {_name} - </button> - ); -} - -export function SpaceMoreButton({ - onDelete, - isOpen, - setIsOpen, - onEdit, -}: { - onDelete?: () => void; - isOpen?: boolean; - onEdit?: () => void; - setIsOpen?: (open: boolean) => void; -}) { - return ( - <DeleteConfirmation onDelete={onDelete} trigger={false}> - <DropdownMenu open={isOpen} onOpenChange={setIsOpen}> - <DropdownMenuTrigger asChild> - <button - data-more-button - className="focus-visible:ring-rgray-7 absolute right-2 top-2 scale-0 rounded-md p-1 opacity-0 ring-transparent transition hover:bg-white focus-visible:bg-white focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 md:block md:scale-100 md:bg-transparent" - > - <MoreHorizontal className="h-5 w-5 text-black" /> - </button> - </DropdownMenuTrigger> - <DropdownMenuContent align="start"> - <DropdownMenuItem onClick={onEdit}> - <Edit3 className="mr-2 h-4 w-4" strokeWidth={1.5} /> - Edit - </DropdownMenuItem> - <DialogTrigger asChild> - <DropdownMenuItem className="focus:bg-red-100 focus:text-red-400"> - <Trash2 className="mr-2 h-4 w-4" strokeWidth={1.5} /> - Delete - </DropdownMenuItem> - </DialogTrigger> - </DropdownMenuContent> - </DropdownMenu> - </DeleteConfirmation> - ); -} - -export function PageMoreButton({ - onDelete, - isOpen, - setIsOpen, - url, - removeFromSpace, -}: { - onDelete?: () => void; - isOpen?: boolean; - url: string; - setIsOpen?: (open: boolean) => void; - removeFromSpace?: () => Promise<void>; -}) { - return ( - <DeleteConfirmation onDelete={onDelete} trigger={false}> - <DropdownMenu open={isOpen} onOpenChange={setIsOpen}> - <DropdownMenuTrigger asChild> - <button - data-more-button - className="focus-visible:ring-rgray-7 absolute right-2 top-2 scale-0 rounded-md p-1 opacity-0 ring-transparent transition hover:bg-white focus-visible:bg-white focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 md:block md:scale-100 md:bg-transparent" - > - <MoreHorizontal className="h-5 w-5 text-black" /> - </button> - </DropdownMenuTrigger> - <DropdownMenuContent align="start"> - <DropdownMenuItem onClick={() => window.open(url)}> - <ArrowUpRight - className="mr-2 h-4 w-4 scale-125" - strokeWidth={1.5} - /> - Open - </DropdownMenuItem> - {removeFromSpace && ( - <DropdownMenuItem onClick={removeFromSpace}> - <Minus className="mr-2 h-4 w-4" strokeWidth={1.5} /> - Remove from space - </DropdownMenuItem> - )} - <DialogTrigger asChild> - <DropdownMenuItem className="focus:bg-red-100 focus:text-red-400"> - <Trash2 className="mr-2 h-4 w-4" strokeWidth={1.5} /> - Delete - </DropdownMenuItem> - </DialogTrigger> - </DropdownMenuContent> - </DropdownMenu> - </DeleteConfirmation> - ); -} - -export function NoteMoreButton({ - onDelete, - isOpen, - setIsOpen, - onEdit, - removeFromSpace, -}: { - onDelete?: () => void; - isOpen?: boolean; - onEdit?: () => void; - setIsOpen?: (open: boolean) => void; - removeFromSpace?: () => Promise<void>; -}) { - return ( - <DeleteConfirmation onDelete={onDelete} trigger={false}> - <DropdownMenu open={isOpen} onOpenChange={setIsOpen}> - <DropdownMenuTrigger asChild> - <button - data-more-button - className="focus-visible:ring-rgray-7 absolute right-2 top-2 scale-0 rounded-md p-1 opacity-0 ring-transparent transition hover:bg-white focus-visible:bg-white focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 md:block md:scale-100 md:bg-transparent" - > - <MoreHorizontal className="h-5 w-5 text-black" /> - </button> - </DropdownMenuTrigger> - <DropdownMenuContent align="start"> - <DropdownMenuItem onClick={onEdit}> - <Edit3 className="mr-2 h-4 w-4" strokeWidth={1.5} /> - Edit - </DropdownMenuItem> - {removeFromSpace && ( - <DropdownMenuItem onClick={removeFromSpace}> - <Minus className="mr-2 h-4 w-4" strokeWidth={1.5} /> - Remove from space - </DropdownMenuItem> - )} - <DialogTrigger asChild> - <DropdownMenuItem className="focus:bg-red-100 focus:text-red-400"> - <Trash2 className="mr-2 h-4 w-4" strokeWidth={1.5} /> - Delete - </DropdownMenuItem> - </DialogTrigger> - </DropdownMenuContent> - </DropdownMenu> - </DeleteConfirmation> - ); -} - -export function AddMemoryModal({ - type, - children, - defaultSpaces, - onAdd, - data, -}: { - type: "page" | "note" | "space" | "existing-memory" | null; - children?: React.ReactNode | React.ReactNode[]; - defaultSpaces?: number[]; - data?: { - space?: { - title: string; - id: number; - }; - fromSpaces?: number[]; - notInSpaces?: number[]; - }; - onAdd?: (data?: StoredSpace | StoredContent | StoredContent[]) => void; -}) { - const [isDialogOpen, setIsDialogOpen] = useState(false); - - return ( - <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}> - {children} - <DialogContent - onOpenAutoFocus={(e) => { - e.preventDefault(); - const novel = document.querySelector('[contenteditable="true"]') as - | HTMLDivElement - | undefined; - if (novel) { - novel.autofocus = false; - novel.onfocus = () => { - ( - document.querySelector("[data-modal-autofocus]") as - | HTMLInputElement - | undefined - )?.focus(); - novel.onfocus = null; - }; - } - ( - document.querySelector("[data-modal-autofocus]") as - | HTMLInputElement - | undefined - )?.focus(); - }} - className="w-max max-w-[auto]" - > - {type === "page" ? ( - <AddMemoryPage - onAdd={onAdd} - defaultSpaces={defaultSpaces} - closeDialog={() => setIsDialogOpen(false)} - /> - ) : type === "note" ? ( - <NoteAddPage - onAdd={onAdd} - defaultSpaces={defaultSpaces} - closeDialog={() => setIsDialogOpen(false)} - /> - ) : type === "space" ? ( - <SpaceAddPage - onAdd={onAdd} - closeDialog={() => setIsDialogOpen(false)} - /> - ) : type === "existing-memory" ? ( - <AddExistingMemoryToSpace - onAdd={onAdd} - fromSpaces={data?.fromSpaces} - notInSpaces={data?.notInSpaces} - space={data!.space!} - closeDialog={() => setIsDialogOpen(false)} - /> - ) : ( - <></> - )} - </DialogContent> - </Dialog> - ); -} diff --git a/apps/web/src/components/Main.tsx b/apps/web/src/components/Main.tsx deleted file mode 100644 index 8ac52569..00000000 --- a/apps/web/src/components/Main.tsx +++ /dev/null @@ -1,533 +0,0 @@ -"use client"; -import { useCallback, useEffect, useRef, useState } from "react"; -import { FilterSpaces } from "./Sidebar/FilterCombobox"; -import { Textarea2 } from "./ui/textarea"; -import { ArrowRight, ArrowUp } from "lucide-react"; -import { MemoryDrawer } from "./MemoryDrawer"; -import useViewport from "@/hooks/useViewport"; -import { AnimatePresence, motion } from "framer-motion"; -import { cn, countLines, getIdsFromSource } from "@/lib/utils"; -import { ChatHistory } from "../../types/memory"; -import { ChatAnswer, ChatMessage, ChatQuestion } from "./ChatMessage"; -import { useRouter, useSearchParams } from "next/navigation"; -import { useMemory } from "@/contexts/MemoryContext"; - -import Image from "next/image"; -import { getMemoriesFromUrl } from "@/actions/db"; -import { ProfileDrawer } from "./ProfileDrawer"; - -function supportsDVH() { - try { - return CSS.supports("height: 100dvh"); - } catch { - return false; - } -} - -export default function Main({ sidebarOpen }: { sidebarOpen: boolean }) { - const searchParams = useSearchParams(); - - const [hide, setHide] = useState(false); - const [layout, setLayout] = useState<"chat" | "initial">("initial"); - const [value, setValue] = useState(""); - const { width } = useViewport(); - const [isAiLoading, setIsAiLoading] = useState(false); - - const { spaces } = useMemory(); - - // Variable to keep track of the chat history in this session - const [chatHistory, setChatHistory] = useState<ChatHistory[]>([]); - - const [toBeParsed, setToBeParsed] = useState(""); - - const textArea = useRef<HTMLDivElement>(null); - const main = useRef<HTMLDivElement>(null); - - const [selectedSpaces, setSelectedSpaces] = useState<number[]>([]); - - const [isStreaming, setIsStreaming] = useState(false); - - useEffect(() => { - const search = searchParams.get("q"); - if (search && search.trim().length > 0) { - setValue(search); - onSend(); - //router.push("/"); - } - }, []); - - useEffect(() => { - // function onResize() { - // if (!main.current || !window.visualViewport) return; - // if ( - // window.visualViewport.height < window.innerHeight + 20 && - // window.visualViewport.height > window.innerHeight - 20 - // ) { - // setHide(false); - // window.scrollTo(0, 0); - // } else { - // setHide(true); - // window.scrollTo(0, document.body.scrollHeight); - // } - // } - // window.visualViewport?.addEventListener("resize", onResize); - // return () => { - // window.visualViewport?.removeEventListener("resize", onResize); - // }; - }, []); - - useEffect(() => { - // Define a function to try parsing the accumulated data - const tryParseAccumulatedData = () => { - // Attempt to parse the "toBeParsed" state as JSON - try { - // Split the accumulated data by the known delimiter "\n\n" - const parts = toBeParsed.split("\n\n"); - let remainingData = ""; - - // Process each part to extract JSON objects - parts.forEach((part, index) => { - try { - const parsedPart = JSON.parse(part.replace("data: ", "")); // Try to parse the part as JSON - - // If the part is the last one and couldn't be parsed, keep it to accumulate more data - if (index === parts.length - 1 && !parsedPart) { - remainingData = part; - } else if (parsedPart && parsedPart.response) { - // Append to chat history in this way: - // If the last message was from the model, append to that message - // Otherwise, Start a new message from the model and append to that - if (chatHistory.length > 0) { - setChatHistory((prev: ChatHistory[]) => { - const lastMessage = prev[prev.length - 1]; - const newParts = [ - ...lastMessage.answer.parts, - { text: parsedPart.response }, - ]; - return [ - ...prev.slice(0, prev.length - 1), - { - ...lastMessage, - answer: { - parts: newParts, - sources: lastMessage.answer.sources, - }, - }, - ]; - }); - } else { - } - } - } catch (error) { - // If parsing fails and it's not the last part, it's a malformed JSON - if (index !== parts.length - 1) { - console.error("Malformed JSON part: ", part); - } else { - // If it's the last part, it may be incomplete, so keep it - remainingData = part; - } - } - }); - - // Update the toBeParsed state to only contain the unparsed remainder - if (remainingData !== toBeParsed) { - setToBeParsed(remainingData); - } - } catch (error) { - console.error("Error parsing accumulated data: ", error); - } - }; - - // Call the parsing function if there's data to be parsed - if (toBeParsed) { - tryParseAccumulatedData(); - } - }, [toBeParsed]); - - const modifyChatHistory = useCallback((old: ChatHistory[]) => { - const final: { role: "user" | "model"; parts: { text: string }[] }[] = []; - old.forEach((chat) => { - final.push({ - role: "user", - parts: [{ text: chat.question }], - }); - final.push({ - role: "model", - parts: chat.answer.parts.map((part) => ({ text: part.text })), - }); - }); - - return final; - }, []); - - const getSearchResults = async () => { - setIsAiLoading(true); - - const _value = value.trim(); - setValue(""); - - setChatHistory((prev) => [ - ...prev, - { - question: _value, - answer: { - parts: [], - sources: [], - }, - }, - ]); - - const sourcesResponse = await fetch( - `/api/chat?sourcesOnly=true&q=${_value}`, - { - method: "POST", - body: JSON.stringify({ - chatHistory: modifyChatHistory(chatHistory), - }), - }, - ); - - console.log("sources", sourcesResponse); - - const sourcesInJson = - getIdsFromSource( - ( - (await sourcesResponse.json()) as { - ids: string[]; - } - ).ids, - ) ?? []; - - const notesInSources = sourcesInJson.filter((urls) => - urls.startsWith("https://notes.supermemory.dhr.wtf/"), - ); - const nonNotes = sourcesInJson.filter((i) => !notesInSources.includes(i)); - - const fetchedTitles = await getMemoriesFromUrl(notesInSources); - - const sources = [ - ...nonNotes.map((n) => ({ isNote: false, source: n ?? "<unnamed>" })), - ...fetchedTitles.map((n) => ({ - isNote: true, - source: n.title ?? "<unnamed>", - })), - ]; - - setIsAiLoading(false); - setChatHistory((prev) => { - const lastMessage = prev[prev.length - 1]; - return [ - ...prev.slice(0, prev.length - 1), - { - ...lastMessage, - answer: { - parts: lastMessage.answer.parts, - sources, - }, - }, - ]; - }); - - const actualSelectedSpaces = selectedSpaces.map( - (space) => spaces.find((s) => s.id === space)?.name ?? "", - ); - - const response = await fetch( - `/api/chat?q=${_value}&spaces=${actualSelectedSpaces.join(",")}`, - { - method: "POST", - body: JSON.stringify({ - chatHistory: modifyChatHistory(chatHistory), - }), - }, - ); - - if (response.status !== 200) { - setIsAiLoading(false); - return; - } - - setIsStreaming(true); - - if (response.body) { - let reader = response.body?.getReader(); - let decoder = new TextDecoder("utf-8"); - let result = ""; - - // @ts-ignore - reader.read().then(function processText({ done, value }) { - if (done) { - setIsAiLoading(false); - setToBeParsed(""); - - return; - } - setToBeParsed((prev) => prev + decoder.decode(value)); - - return reader?.read().then(processText); - }); - } - }; - - const onSend = () => { - if (value.trim().length < 1) return; - setLayout("chat"); - getSearchResults(); - }; - - function onValueChange(e: React.ChangeEvent<HTMLTextAreaElement>) { - const value = e.target.value; - setValue(value); - const lines = countLines(e.target); - e.target.rows = Math.min(5, lines); - } - - return ( - <> - <AnimatePresence mode="wait"> - {layout === "chat" ? ( - <Chat - key="chat" - isLoading={isAiLoading} - chatHistory={chatHistory} - sidebarOpen={sidebarOpen} - askQuestion={onSend} - setValue={setValue} - value={value} - selectedSpaces={selectedSpaces} - setSelectedSpaces={setSelectedSpaces} - /> - ) : ( - <main - key="intial" - data-sidebar-open={sidebarOpen} - ref={main} - className={cn( - "sidebar relative flex w-full flex-col items-end justify-center gap-5 px-5 pt-5 transition-[padding-left,padding-top,padding-right] delay-200 duration-200 md:items-center md:gap-10 md:px-72 [&[data-sidebar-open='true']]:pr-10 [&[data-sidebar-open='true']]:delay-0 md:[&[data-sidebar-open='true']]:pl-[calc(2.5rem+30vw)]", - hide ? "" : "main-hidden", - )} - > - <Image - className="absolute right-10 top-10 hidden rounded-md md:block" - src="/icons/logo_bw_without_bg.png" - alt="Smort logo" - width={50} - height={50} - /> - <div className="absolute right-10 top-10 block md:hidden"> - {width <= 768 && <ProfileDrawer hide={hide} />} - </div> - <h1 className="text-rgray-11 mt-auto w-full text-center text-3xl font-bold tracking-tight md:mt-0"> - Ask your second brain - </h1> - - <FilterSpaces - name={"Filter"} - onClose={() => { - textArea.current?.querySelector("textarea")?.focus(); - }} - side="top" - align="start" - className="mr-auto bg-[#252525] md:hidden" - selectedSpaces={selectedSpaces} - setSelectedSpaces={setSelectedSpaces} - /> - <Textarea2 - ref={textArea} - className="bg-rgray-2 h-auto w-full flex-row items-start justify-center overflow-auto px-3 md:hidden md:items-center md:justify-center" - textAreaProps={{ - placeholder: "Ask your SuperMemory...", - className: - "overflow-auto h-auto p-3 md:resize-none text-lg w-auto resize-y text-rgray-11 w-full", - value, - rows: 1, - autoFocus: true, - onChange: onValueChange, - onKeyDown: (e) => { - if (e.key === "Enter" && !e.shiftKey) { - e.preventDefault(); - onSend(); - } - }, - }} - > - <div className="text-rgray-11/70 ml-auto mt-auto flex h-full w-min items-center justify-center pb-3 pr-2 md:hidden"> - <FilterSpaces - name={"Filter"} - onClose={() => { - textArea.current?.querySelector("textarea")?.focus(); - }} - className="hidden md:flex" - selectedSpaces={selectedSpaces} - setSelectedSpaces={setSelectedSpaces} - /> - <button - onClick={onSend} - disabled={value.trim().length < 1} - className="text-rgray-11/70 bg-rgray-3 focus-visible:ring-rgray-8 hover:bg-rgray-4 mt-auto flex items-center justify-center rounded-full p-2 ring-2 ring-transparent transition-[filter] focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50" - > - <ArrowUp className="h-5 w-5" /> - </button> - </div> - </Textarea2> - - <Textarea2 - ref={textArea} - exit={{ - opacity: 0, - y: 50, - }} - transition={{ - type: "tween", - duration: 0.2, - }} - textAreaProps={{ - placeholder: "Ask your second brain...", - className: - "h-auto overflow-auto md:h-full md:resize-none text-lg py-0 px-2 pt-2 md:py-0 md:p-5 resize-y text-rgray-11 w-full min-h-[1em]", - value, - autoFocus: true, - onChange: (e) => setValue(e.target.value), - onKeyDown: (e) => { - if (e.key === "Enter" && !e.shiftKey) { - e.preventDefault(); - onSend(); - } - }, - }} - className="hidden md:flex" - > - <div className="text-rgray-11/70 flex h-full w-fit items-center justify-center pl-0 md:w-full md:p-2"> - <FilterSpaces - name={"Filter"} - onClose={() => { - textArea.current?.querySelector("textarea")?.focus(); - }} - className="hidden md:flex" - selectedSpaces={selectedSpaces} - setSelectedSpaces={setSelectedSpaces} - /> - <button - onClick={onSend} - disabled={value.trim().length < 1} - className="text-rgray-11/70 bg-rgray-3 focus-visible:ring-rgray-8 hover:bg-rgray-4 mt-auto flex items-center justify-center rounded-full p-2 ring-2 ring-transparent focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 md:ml-auto md:mt-0" - > - <ArrowRight className="h-5 w-5" /> - </button> - </div> - </Textarea2> - </main> - )} - {width <= 768 && <MemoryDrawer hide={hide} />} - </AnimatePresence> - </> - ); -} - -export function Chat({ - sidebarOpen, - chatHistory, - isLoading = false, - askQuestion, - setValue, - value, - selectedSpaces, - setSelectedSpaces, -}: { - sidebarOpen: boolean; - isLoading?: boolean; - chatHistory: ChatHistory[]; - askQuestion: () => void; - setValue: (value: string) => void; - value: string; - selectedSpaces: number[]; - setSelectedSpaces: React.Dispatch<React.SetStateAction<number[]>>; -}) { - const textArea = useRef<HTMLDivElement>(null); - - function onValueChange(e: React.ChangeEvent<HTMLTextAreaElement>) { - const value = e.target.value; - setValue(value); - const lines = countLines(e.target); - e.target.rows = Math.min(5, lines); - } - - const { width } = useViewport(); - - return ( - <main - data-sidebar-open={sidebarOpen} - className={cn( - "sidebar relative flex w-full flex-col items-end gap-5 px-5 pt-5 transition-[padding-left,padding-top,padding-right] delay-200 duration-200 md:items-center md:gap-10 md:px-72 [&[data-sidebar-open='true']]:pr-10 [&[data-sidebar-open='true']]:delay-0 md:[&[data-sidebar-open='true']]:pl-[calc(2.5rem+30vw)]", - )} - > - <div className="absolute right-10 top-10 z-[100] block md:hidden"> - {width <= 768 && <ProfileDrawer />} - </div> - <div className="scrollbar-none h-[70vh] w-full overflow-y-auto px-2 md:h-screen md:px-5"> - {chatHistory.map((msg, i) => ( - <ChatMessage index={i} key={i} isLast={i === chatHistory.length - 1}> - <ChatQuestion>{msg.question}</ChatQuestion> - <ChatAnswer - loading={i === chatHistory.length - 1 ? isLoading : false} - sources={msg.answer.sources} - > - {msg.answer.parts - .map((part) => part.text) - .join("") - .replace("</s>", "")} - </ChatAnswer> - </ChatMessage> - ))} - </div> - <div className="from-rgray-2 via-rgray-2 to-rgray-2/0 absolute bottom-0 left-0 w-full bg-gradient-to-t" /> - <div - data-sidebar-open={sidebarOpen} - className="absolute flex w-full items-center justify-center" - > - <div className="animate-from-top bottom-padding fixed left-1/2 mt-auto flex w-[90%] -translate-x-1/2 flex-col items-center justify-center gap-2 md:bottom-10 md:left-[auto] md:w-[50%] md:translate-x-0"> - <FilterSpaces - name={"Filter"} - onClose={() => { - textArea.current?.querySelector("textarea")?.focus(); - }} - side="top" - align="start" - className="mr-auto bg-[#252525]" - selectedSpaces={selectedSpaces} - setSelectedSpaces={setSelectedSpaces} - /> - <Textarea2 - ref={textArea} - className="bg-rgray-2 h-auto w-full flex-row items-start justify-center overflow-auto px-3 md:items-center md:justify-center" - textAreaProps={{ - placeholder: "Ask your SuperMemory...", - className: - "overflow-auto h-auto p-3 md:resize-none text-lg w-auto resize-y text-rgray-11 w-full", - value, - rows: 1, - autoFocus: true, - onChange: onValueChange, - onKeyDown: (e) => { - if (e.key === "Enter" && !e.shiftKey) { - e.preventDefault(); - askQuestion(); - } - }, - }} - > - <div className="text-rgray-11/70 ml-auto mt-auto flex h-full w-min items-center justify-center pb-3 pr-2"> - <button - onClick={askQuestion} - disabled={value.trim().length < 1} - className="text-rgray-11/70 bg-rgray-3 focus-visible:ring-rgray-8 hover:bg-rgray-4 mt-auto flex items-center justify-center rounded-full p-2 ring-2 ring-transparent transition-[filter] focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50" - > - <ArrowUp className="h-5 w-5" /> - </button> - </div> - </Textarea2> - </div> - </div> - </main> - ); -} diff --git a/apps/web/src/components/MemoryDrawer.tsx b/apps/web/src/components/MemoryDrawer.tsx deleted file mode 100644 index 14283281..00000000 --- a/apps/web/src/components/MemoryDrawer.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { useRef, useState } from "react"; -import { Drawer, DrawerContent, DrawerOverlay } from "./ui/drawer"; -import { MemoryIcon } from "@/assets/Memories"; -import { cn } from "@/lib/utils"; -import { MemoriesBar } from "./Sidebar/MemoriesBar"; - -export interface Props extends React.ButtonHTMLAttributes<HTMLButtonElement> { - hide?: boolean; -} - -export function MemoryDrawer({ className, hide = false, ...props }: Props) { - const [activeSnapPoint, setActiveSnapPoint] = useState< - number | null | string - >(0.1); - - return ( - <Drawer - snapPoints={[0.1, 0.9]} - activeSnapPoint={activeSnapPoint} - shouldScaleBackground={false} - setActiveSnapPoint={setActiveSnapPoint} - open={true} - dismissible={false} - modal={false} - > - <DrawerContent - overlay={false} - data-expanded={activeSnapPoint === 0.9} - className={cn( - "border-rgray-6 DrawerContent data-[expanded=true]:bg-rgray-3 h-full w-screen border transition-[background] focus-visible:outline-none", - hide ? "hidden" : "", - )} - handle={false} - > - <button - onClick={() => - setActiveSnapPoint((prev) => (prev === 0.9 ? 0.1 : 0.9)) - } - className="bg-rgray-4 border-rgray-6 text-rgray-11 absolute left-1/2 top-0 flex w-fit -translate-x-1/2 -translate-y-1/2 items-center justify-center gap-2 rounded-md border px-3 py-2" - > - <MemoryIcon className="h-7 w-7" /> - Memories - </button> - <div className="h-full w-full overflow-y-auto"> - <MemoriesBar isOpen={true} /> - </div> - </DrawerContent> - <DrawerOverlay className="relative bg-transparent" /> - </Drawer> - ); -} diff --git a/apps/web/src/components/ProfileDrawer.tsx b/apps/web/src/components/ProfileDrawer.tsx deleted file mode 100644 index bdb32e03..00000000 --- a/apps/web/src/components/ProfileDrawer.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { useRef, useState } from "react"; -import { - Drawer, - DrawerContent, - DrawerOverlay, - DrawerTrigger, -} from "./ui/drawer"; -import { cn } from "@/lib/utils"; -import { SettingsTab } from "./Sidebar/SettingsTab"; -import { useSession } from "next-auth/react"; - -export interface Props extends React.ButtonHTMLAttributes<HTMLButtonElement> { - hide?: boolean; -} - -export function ProfileDrawer({ className, hide = false, ...props }: Props) { - const { data: session } = useSession(); - - return ( - <Drawer snapPoints={[0.9]} shouldScaleBackground={false}> - <DrawerTrigger> - <img - src={session?.user?.image ?? "/icons/white_without_bg.png"} - className="h-10 w-10 rounded-full" - /> - </DrawerTrigger> - <DrawerContent - overlay={false} - className={cn( - "border-rgray-6 DrawerContent data-[expanded=true]:bg-rgray-3 z-[101] h-full w-screen border bg-white transition-[background] focus-visible:outline-none", - hide ? "hidden" : "", - )} - > - <div className="h-[85vh] w-full overflow-y-auto"> - <SettingsTab open={true} /> - </div> - </DrawerContent> - </Drawer> - ); -} diff --git a/apps/web/src/components/SearchResults.tsx b/apps/web/src/components/SearchResults.tsx deleted file mode 100644 index d348814e..00000000 --- a/apps/web/src/components/SearchResults.tsx +++ /dev/null @@ -1,40 +0,0 @@ -"use client"; - -import React from "react"; -import { Card, CardContent } from "./ui/card"; -import Markdown from "react-markdown"; -import remarkGfm from "remark-gfm"; - -function SearchResults({ - aiResponse, - sources, -}: { - aiResponse: string; - sources: string[]; -}) { - return ( - <div - style={{ - backgroundImage: `linear-gradient(to right, #E5D9F2, #CDC1FF)`, - }} - className="mx-auto mt-4 w-full max-w-2xl space-y-6 rounded-xl border px-4 py-6" - > - <div className="text-start"> - <div className="text-xl text-black"> - <Markdown remarkPlugins={[remarkGfm]}> - {aiResponse.replace("</s>", "")} - </Markdown> - </div> - </div> - <div className="grid gap-6"> - {sources.map((value, index) => ( - <Card key={index}> - <CardContent className="space-y-2">{value}</CardContent> - </Card> - ))} - </div> - </div> - ); -} - -export default SearchResults; diff --git a/apps/web/src/components/Sidebar/AddMemoryDialog.tsx b/apps/web/src/components/Sidebar/AddMemoryDialog.tsx deleted file mode 100644 index 64147b1e..00000000 --- a/apps/web/src/components/Sidebar/AddMemoryDialog.tsx +++ /dev/null @@ -1,480 +0,0 @@ -import { Editor } from "novel"; -import { - DialogClose, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "../ui/dialog"; -import { Input } from "../ui/input"; -import { Label } from "../ui/label"; -import { Markdown } from "tiptap-markdown"; -import { useEffect, useRef, useState } from "react"; -import { FilterMemories, FilterSpaces } from "./FilterCombobox"; -import { useMemory } from "@/contexts/MemoryContext"; -import { Loader, Plus, X } from "lucide-react"; -import { StoredContent, StoredSpace } from "@/server/db/schema"; -import { cleanUrl } from "@/lib/utils"; -import { motion } from "framer-motion"; -import { getMetaData } from "@/server/helpers"; - -export function AddMemoryPage({ - closeDialog, - defaultSpaces, - onAdd, -}: { - closeDialog: () => void; - defaultSpaces?: number[]; - onAdd?: (addedData: StoredContent) => void; -}) { - const { addMemory } = useMemory(); - - const [loading, setLoading] = useState(false); - const [url, setUrl] = useState(""); - const [selectedSpacesId, setSelectedSpacesId] = useState<number[]>( - defaultSpaces ?? [], - ); - - return ( - <div className="w-[80vw] max-w-[80vw] md:w-[40vw]"> - <DialogHeader> - <DialogTitle>Add a web page to memory</DialogTitle> - <DialogDescription> - This will fetch the content of the web page and add it to the memory - </DialogDescription> - </DialogHeader> - <Label className="mt-5 block">URL</Label> - <Input - placeholder="Enter the URL of the page" - type="url" - data-modal-autofocus - className="mt-2 w-full disabled:cursor-not-allowed disabled:opacity-70" - value={url} - onChange={(e) => setUrl(e.target.value)} - disabled={loading} - /> - <DialogFooter> - <FilterSpaces - selectedSpaces={selectedSpacesId} - setSelectedSpaces={setSelectedSpacesId} - className="mr-auto bg-white/5 hover:bg-slate-100 disabled:cursor-not-allowed disabled:opacity-70" - name={"Spaces"} - disabled={loading} - /> - <button - type={"submit"} - disabled={loading} - onClick={async () => { - setLoading(true); - const metadata = await getMetaData(url); - const data = await addMemory( - { - title: metadata.title, - description: metadata.description, - content: "", - type: "page", - url: url, - image: metadata.image, - savedAt: new Date(), - }, - selectedSpacesId, - ); - if (data) onAdd?.(data.memory); - closeDialog(); - }} - className="bg-rgray-4 focus-visible:ring-rgray-7 relative rounded-md px-4 py-2 ring-transparent transition hover:bg-slate-100 focus-visible:bg-slate-100 focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-70" - > - <motion.div - initial={{ x: "-50%", y: "-100%" }} - animate={loading && { y: "-50%", x: "-50%", opacity: 1 }} - className="absolute left-1/2 top-1/2 -translate-x-1/2 translate-y-[-100%] opacity-0" - > - <Loader className="text-rgray-11 h-5 w-5 animate-spin" /> - </motion.div> - <motion.div - initial={{ y: "0%" }} - animate={loading && { opacity: 0, y: "30%" }} - > - Add - </motion.div> - </button> - <DialogClose - disabled={loading} - className="focus-visible:ring-rgray-7 rounded-md px-3 py-2 ring-transparent transition hover:bg-white focus-visible:bg-[#F4F3F2] focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-70" - > - Cancel - </DialogClose> - </DialogFooter> - </div> - ); -} - -export function NoteAddPage({ - closeDialog, - defaultSpaces, - onAdd, -}: { - closeDialog: () => void; - defaultSpaces?: number[]; - onAdd?: (addedData: StoredContent) => void; -}) { - const { addMemory } = useMemory(); - - const [selectedSpacesId, setSelectedSpacesId] = useState<number[]>( - defaultSpaces ?? [], - ); - - const inputRef = useRef<HTMLInputElement>(null); - const [name, setName] = useState(""); - const [content, setContent] = useState(""); - const [loading, setLoading] = useState(false); - - function check(): boolean { - const data = { - name: name.trim(), - content, - }; - if (!data.name || data.name.length < 1) { - if (!inputRef.current) { - alert("Please enter a name for the note"); - return false; - } - inputRef.current.value = ""; - inputRef.current.placeholder = "Please enter a title for the note"; - inputRef.current.dataset["error"] = "true"; - setTimeout(() => { - inputRef.current!.placeholder = "Title of the note"; - inputRef.current!.dataset["error"] = "false"; - }, 500); - inputRef.current.focus(); - return false; - } - return true; - } - - return ( - <div className="w-[80vw] md:w-auto"> - <Input - ref={inputRef} - data-error="false" - className="w-full border-none p-0 text-xl ring-0 placeholder:transition placeholder:duration-500 focus-visible:ring-0 data-[error=true]:placeholder:text-red-400" - placeholder="Title of the note" - data-modal-autofocus - value={name} - disabled={loading} - onChange={(e) => setName(e.target.value)} - /> - <Editor - disableLocalStorage - defaultValue={""} - onUpdate={(editor) => { - if (!editor) return; - setContent(editor.storage.markdown.getMarkdown()); - }} - extensions={[Markdown]} - className="novel-editor border-rgray-7 dark mt-5 max-h-[60vh] min-h-[40vh] w-full overflow-y-auto rounded-lg border bg-white md:w-[50vw] [&>div>div]:p-5" - /> - <DialogFooter> - <FilterSpaces - selectedSpaces={selectedSpacesId} - setSelectedSpaces={setSelectedSpacesId} - className="hover:bg-rgray-5 mr-auto bg-white/5" - name={"Spaces"} - /> - <button - onClick={() => { - if (check()) { - setLoading(true); - addMemory( - { - content, - title: name, - type: "note", - url: `https://notes.supermemory.dhr.wtf/`, - image: "", - savedAt: new Date(), - }, - selectedSpacesId, - ).then((data) => { - if (data?.memory) onAdd?.(data.memory); - closeDialog(); - }); - } - }} - disabled={loading} - className="hover:bg-rgray-5 focus-visible:bg-rgray-5 focus-visible:ring-rgray-7 relative rounded-md bg-[#F4F3F2] px-4 py-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-70" - > - <motion.div - initial={{ x: "-50%", y: "-100%" }} - animate={loading && { y: "-50%", x: "-50%", opacity: 1 }} - className="absolute left-1/2 top-1/2 -translate-x-1/2 translate-y-[-100%] opacity-0" - > - <Loader className="text-rgray-11 h-5 w-5 animate-spin" /> - </motion.div> - <motion.div - initial={{ y: "0%" }} - animate={loading && { opacity: 0, y: "30%" }} - > - Add - </motion.div> - </button> - <DialogClose - type={undefined} - disabled={loading} - className="hover:bg-rgray-4 focus-visible:bg-rgray-4 focus-visible:ring-rgray-7 rounded-md px-3 py-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-70" - > - Cancel - </DialogClose> - </DialogFooter> - </div> - ); -} - -export function SpaceAddPage({ - closeDialog, - onAdd, -}: { - closeDialog: () => void; - onAdd?: (addedData: StoredSpace) => void; -}) { - const { addSpace } = useMemory(); - - const inputRef = useRef<HTMLInputElement>(null); - const [name, setName] = useState(""); - - const [loading, setLoading] = useState(false); - - const [selected, setSelected] = useState<StoredContent[]>([]); - - function check(): boolean { - const data = { - name: name.trim(), - }; - if (!data.name || data.name.length < 1) { - if (!inputRef.current) { - alert("Please enter a name for the note"); - return false; - } - inputRef.current.value = ""; - inputRef.current.placeholder = "Please enter a title for the space"; - inputRef.current.dataset["error"] = "true"; - setTimeout(() => { - inputRef.current!.placeholder = "Enter the name of the space"; - inputRef.current!.dataset["error"] = "false"; - }, 500); - inputRef.current.focus(); - return false; - } - return true; - } - - return ( - <div className="w-[80vw] md:w-[40vw]"> - <DialogHeader> - <DialogTitle>Add a space</DialogTitle> - </DialogHeader> - <Label className="mt-5 block">Name</Label> - <Input - ref={inputRef} - placeholder="Enter the name of the space" - type="url" - data-modal-autofocus - value={name} - disabled={loading} - onChange={(e) => setName(e.target.value)} - className="mt-2 w-full placeholder:transition placeholder:duration-500 data-[error=true]:placeholder:text-red-400 focus-visible:data-[error=true]:ring-red-500/10" - /> - {selected.length > 0 && ( - <> - <Label className="mt-5 block">Add Memories</Label> - <div className="flex min-h-5 flex-col items-center justify-center py-2"> - {selected.map((i) => ( - <MemorySelectedItem - key={i.id} - onRemove={() => - setSelected((prev) => prev.filter((p) => p.id !== i.id)) - } - {...i} - /> - ))} - </div> - </> - )} - <DialogFooter> - <FilterMemories - selected={selected} - setSelected={setSelected} - disabled={loading} - className="mr-auto bg-white/5 hover:hover:bg-slate-100 focus-visible:hover:bg-slate-100 disabled:cursor-not-allowed disabled:opacity-70" - > - <Plus className="h-5 w-5" /> - Memory - </FilterMemories> - <button - type={undefined} - onClick={() => { - if (check()) { - setLoading(true); - addSpace( - name, - selected.map((s) => s.id), - ).then((data) => { - if (data) onAdd?.(data.space); - closeDialog(); - }); - } - }} - disabled={loading} - className="bg-rgray-4 focus-visible:ring-rgray-7 relative rounded-md px-4 py-2 ring-transparent transition hover:hover:bg-slate-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:hover:bg-slate-100 disabled:cursor-not-allowed disabled:opacity-70" - > - <motion.div - initial={{ x: "-50%", y: "-100%" }} - animate={loading && { y: "-50%", x: "-50%", opacity: 1 }} - className="absolute left-1/2 top-1/2 -translate-x-1/2 translate-y-[-100%] opacity-0" - > - <Loader className="text-rgray-11 h-5 w-5 animate-spin" /> - </motion.div> - <motion.div - initial={{ y: "0%" }} - animate={loading && { opacity: 0, y: "30%" }} - > - Add - </motion.div> - </button> - <DialogClose - disabled={loading} - className="focus-visible:ring-rgray-7 rounded-md px-3 py-2 ring-transparent transition hover:bg-white focus-visible:bg-[#F4F3F2] focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-70" - > - Cancel - </DialogClose> - </DialogFooter> - </div> - ); -} - -export function MemorySelectedItem({ - id, - title, - url, - type, - image, - onRemove, -}: StoredContent & { onRemove: () => void }) { - return ( - <div className="hover:bg-rgray-4 focus-within-bg-rgray-4 flex w-full items-center justify-start gap-2 rounded-md p-2 px-3 text-sm [&:hover_[data-icon]]:block [&:hover_img]:hidden"> - <button - onClick={onRemove} - className="ring-rgray-7 ring-offset-rgray-3 m-0 h-5 w-5 rounded-sm p-0 ring-offset-2 focus-visible:outline-none focus-visible:ring-2 [&:focus-visible>[data-icon]]:block [&:focus-visible>img]:hidden" - > - <img - src={ - type === "note" - ? "/note.svg" - : image ?? "/icons/logo_without_bg.png" - } - className="h-5 w-5" - /> - <X data-icon className="hidden h-5 w-5 scale-90" /> - </button> - <span>{title}</span> - <span className="ml-auto block opacity-50"> - {type === "note" ? "Note" : cleanUrl(url)} - </span> - </div> - ); -} - -export function AddExistingMemoryToSpace({ - space, - closeDialog, - fromSpaces, - notInSpaces, - onAdd, -}: { - space: { title: string; id: number }; - closeDialog: () => void; - fromSpaces?: number[]; - notInSpaces?: number[]; - onAdd?: () => void; -}) { - const { addMemoriesToSpace } = useMemory(); - - const [loading, setLoading] = useState(false); - - const [selected, setSelected] = useState<StoredContent[]>([]); - - return ( - <div className="w-[80vw] md:w-[40vw]"> - <DialogHeader> - <DialogTitle>Add an existing memory to {space.title}</DialogTitle> - <DialogDescription> - Pick the memories you want to add to this space - </DialogDescription> - </DialogHeader> - {selected.length > 0 && ( - <> - <Label className="mt-5 block">Add Memories</Label> - <div className="flex min-h-5 flex-col items-center justify-center py-2"> - {selected.map((i) => ( - <MemorySelectedItem - key={i.id} - onRemove={() => - setSelected((prev) => prev.filter((p) => p.id !== i.id)) - } - {...i} - /> - ))} - </div> - </> - )} - <DialogFooter> - <FilterMemories - selected={selected} - setSelected={setSelected} - disabled={loading} - fromSpaces={fromSpaces} - notInSpaces={notInSpaces} - className="hover:bg-rgray-4 focus-visible:bg-rgray-4 mr-auto bg-white/5 disabled:cursor-not-allowed disabled:opacity-70" - > - <Plus className="h-5 w-5" /> - Memory - </FilterMemories> - <button - type={undefined} - onClick={() => { - setLoading(true); - addMemoriesToSpace( - space.id, - selected.map((i) => i.id), - ).then(() => { - onAdd?.(); - closeDialog(); - }); - }} - disabled={loading} - className="bg-rgray-4 hover:bg-rgray-5 focus-visible:bg-rgray-5 focus-visible:ring-rgray-7 relative rounded-md px-4 py-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-70" - > - <motion.div - initial={{ x: "-50%", y: "-100%" }} - animate={loading && { y: "-50%", x: "-50%", opacity: 1 }} - className="absolute left-1/2 top-1/2 -translate-x-1/2 translate-y-[-100%] opacity-0" - > - <Loader className="text-rgray-11 h-5 w-5 animate-spin" /> - </motion.div> - <motion.div - initial={{ y: "0%" }} - animate={loading && { opacity: 0, y: "30%" }} - > - Add - </motion.div> - </button> - <DialogClose - disabled={loading} - className="focus-visible:ring-rgray-7 rounded-md px-3 py-2 ring-transparent transition hover:bg-white focus-visible:bg-[#F4F3F2] focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-70" - > - Cancel - </DialogClose> - </DialogFooter> - </div> - ); -} diff --git a/apps/web/src/components/Sidebar/DeleteConfirmation.tsx b/apps/web/src/components/Sidebar/DeleteConfirmation.tsx deleted file mode 100644 index 7955df0d..00000000 --- a/apps/web/src/components/Sidebar/DeleteConfirmation.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { - Dialog, - DialogContent, - DialogTrigger, - DialogTitle, - DialogDescription, - DialogClose, - DialogFooter, -} from "../ui/dialog"; - -export default function DeleteConfirmation({ - onDelete, - trigger = true, - children, -}: { - trigger?: boolean; - onDelete?: () => void; - children: React.ReactNode; -}) { - return ( - <Dialog> - {trigger ? ( - <DialogTrigger asChild>{children}</DialogTrigger> - ) : ( - <>{children}</> - )} - <DialogContent> - <DialogTitle className="text-xl">Are you sure?</DialogTitle> - <DialogDescription className="text-md"> - You will not be able to recover this it. - </DialogDescription> - <DialogFooter> - <DialogClose - type={undefined} - onClick={onDelete} - className="ml-auto flex items-center justify-center rounded-md bg-red-100/10 px-3 py-2 text-red-400 transition hover:bg-red-100/5 focus-visible:bg-red-100/5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-100/30" - > - Delete - </DialogClose> - <DialogClose className="focus-visible:bg-rgray-4 focus-visible:ring-rgray-7 hover:bg-rgray-4 ml-auto flex items-center justify-center rounded-md px-3 py-2 transition focus-visible:outline-none focus-visible:ring-2"> - Cancel - </DialogClose> - </DialogFooter> - </DialogContent> - </Dialog> - ); -} diff --git a/apps/web/src/components/Sidebar/EditNoteDialog.tsx b/apps/web/src/components/Sidebar/EditNoteDialog.tsx deleted file mode 100644 index c0ad716d..00000000 --- a/apps/web/src/components/Sidebar/EditNoteDialog.tsx +++ /dev/null @@ -1,155 +0,0 @@ -import { Editor } from "novel"; -import { DialogClose, DialogFooter } from "../ui/dialog"; -import { Input } from "../ui/input"; -import { Markdown } from "tiptap-markdown"; -import { useEffect, useRef, useState } from "react"; -import { FilterSpaces } from "./FilterCombobox"; -import { useMemory } from "@/contexts/MemoryContext"; -import { Loader, Plus, Trash, X } from "lucide-react"; -import { motion } from "framer-motion"; -import { StoredContent } from "@/server/db/schema"; -import { fetchContent } from "@/actions/db"; -import { isArraysEqual } from "@/lib/utils"; -import DeleteConfirmation from "./DeleteConfirmation"; - -export function NoteEdit({ - memory, - closeDialog, - onDelete, -}: { - memory: StoredContent; - closeDialog: () => any; - onDelete?: () => void; -}) { - const { updateMemory, deleteMemory } = useMemory(); - - const [initialSpaces, setInitialSpaces] = useState<number[]>([]); - const [selectedSpacesId, setSelectedSpacesId] = useState<number[]>([]); - - const inputRef = useRef<HTMLInputElement>(null); - const [name, setName] = useState(memory.title ?? ""); - const [content, setContent] = useState(memory.content); - const [loading, setLoading] = useState(false); - - function check(): boolean { - const data = { - name: name.trim(), - content, - }; - if (!data.name || data.name.length < 1) { - if (!inputRef.current) { - alert("Please enter a name for the note"); - return false; - } - inputRef.current.value = ""; - inputRef.current.placeholder = "Please enter a title for the note"; - inputRef.current.dataset["error"] = "true"; - setTimeout(() => { - inputRef.current!.placeholder = "Title of the note"; - inputRef.current!.dataset["error"] = "false"; - }, 500); - inputRef.current.focus(); - return false; - } - return true; - } - - useEffect(() => { - fetchContent(memory.id).then((data) => { - if (data?.spaces) { - setInitialSpaces(data.spaces); - setSelectedSpacesId(data.spaces); - } - }); - }, []); - - return ( - <div> - <Input - ref={inputRef} - data-error="false" - className="w-full border-none p-0 text-xl ring-0 placeholder:transition placeholder:duration-500 focus-visible:ring-0 data-[error=true]:placeholder:text-red-400" - placeholder="Title of the note" - value={name} - disabled={loading} - onChange={(e) => setName(e.target.value)} - /> - <Editor - disableLocalStorage - defaultValue={memory.content} - onUpdate={(editor) => { - if (!editor) return; - setContent(editor.storage.markdown.getMarkdown()); - }} - extensions={[Markdown]} - className="novel-editor border-rgray-7 dark mt-5 max-h-[60vh] min-h-[40vh] w-[50vw] overflow-y-auto rounded-lg border bg-white [&>div>div]:p-5" - /> - <DialogFooter> - <FilterSpaces - selectedSpaces={selectedSpacesId} - setSelectedSpaces={setSelectedSpacesId} - className="mr-auto bg-white hover:bg-slate-100" - name={"Spaces"} - /> - <DeleteConfirmation - onDelete={() => { - deleteMemory(memory.id); - onDelete?.(); - }} - > - <button - type={undefined} - disabled={loading} - className="rounded-md px-3 py-2 ring-transparent transition hover:bg-red-100 hover:text-red-400 focus-visible:bg-red-100 focus-visible:text-red-400 focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-70" - > - <Trash className="h-5 w-5" /> - </button> - </DeleteConfirmation> - <button - onClick={() => { - if (check()) { - setLoading(true); - console.log({ - title: name === memory.title ? undefined : name, - content: content === memory.content ? undefined : content, - spaces: isArraysEqual(initialSpaces, selectedSpacesId) - ? undefined - : selectedSpacesId, - }); - updateMemory(memory.id, { - title: name === memory.title ? undefined : name, - content: content === memory.content ? undefined : content, - spaces: isArraysEqual(initialSpaces, selectedSpacesId) - ? undefined - : selectedSpacesId, - }).then(closeDialog); - } - }} - disabled={loading} - className="focus-visible:ring-rgray-7 relative rounded-md bg-white px-4 py-2 ring-transparent transition hover:bg-slate-100 focus-visible:bg-slate-100 focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-70" - > - <motion.div - initial={{ x: "-50%", y: "-100%" }} - animate={loading && { y: "-50%", x: "-50%", opacity: 1 }} - className="absolute left-1/2 top-1/2 -translate-x-1/2 translate-y-[-100%] opacity-0" - > - <Loader className="h-5 w-5 animate-spin" /> - </motion.div> - <motion.div - initial={{ y: "0%" }} - animate={loading && { opacity: 0, y: "30%" }} - > - Save - </motion.div> - </button> - <DialogClose - type={undefined} - disabled={loading} - className="focus-visible:bg-rgray-4 focus-visible:ring-rgray-7 rounded-md px-3 py-2 ring-transparent transition hover:bg-white focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-70" - > - Cancel - </DialogClose> - </DialogFooter> - </div> - ); -} diff --git a/apps/web/src/components/Sidebar/ExpandedSpace.tsx b/apps/web/src/components/Sidebar/ExpandedSpace.tsx deleted file mode 100644 index 55d3f3f8..00000000 --- a/apps/web/src/components/Sidebar/ExpandedSpace.tsx +++ /dev/null @@ -1,287 +0,0 @@ -import { fetchContentForSpace, getSpace } from "@/actions/db"; -import { useMemory } from "@/contexts/MemoryContext"; -import { StoredContent, StoredSpace } from "@/server/db/schema"; -import { - Edit3, - Loader, - Plus, - Search, - Sparkles, - StickyNote, - Text, - Undo2, -} from "lucide-react"; -import { useEffect, useRef, useState } from "react"; -import { Input, InputWithIcon } from "../ui/input"; -import { useDebounce } from "@/hooks/useDebounce"; -import { useAutoAnimate } from "@formkit/auto-animate/react"; -import { AddMemoryModal, MemoryItem } from "./MemoriesBar"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "../ui/dropdown-menu"; -import { DialogTrigger } from "../ui/dialog"; - -export function ExpandedSpace({ - spaceId, - back, -}: { - spaceId: number; - back: () => void; -}) { - const { updateMemory, updateSpace, search } = useMemory(); - - const [parent, enableAnimations] = useAutoAnimate(); - - const inputRef = useRef<HTMLInputElement>(null); - - const [contentForSpace, setContentForSpace] = useState<StoredContent[]>([]); - - const [lastUpdatedTitle, setLastUpdatedTitle] = useState<string | null>(null); - - const [title, setTitle] = useState<string>(""); - const debouncedTitle = useDebounce(title, 500); - - const [loading, setLoading] = useState(true); - - const [saveLoading, setSaveLoading] = useState(false); - - const [searchQuery, setSearcyQuery] = useState(""); - const [searchLoading, setSearchLoading] = useState(false); - const query = useDebounce(searchQuery, 500); - - const [searchResults, setSearchResults] = useState<StoredContent[]>([]); - - const [addMemoryState, setAddMemoryState] = useState< - "page" | "note" | "existing-memory" | "space" | null - >(null); - const [isDropdownOpen, setIsDropdownOpen] = useState(false); - - useEffect(() => { - (async () => { - const title = (await getSpace(spaceId))?.name ?? ""; - setTitle(title); - setLastUpdatedTitle(title); - setContentForSpace((await fetchContentForSpace(spaceId)) ?? []); - setLoading(false); - })(); - }, []); - - useEffect(() => { - if ( - debouncedTitle.trim().length < 1 || - debouncedTitle.trim() === lastUpdatedTitle?.trim() - ) - return; - (async () => { - setSaveLoading(true); - await updateSpace(spaceId, debouncedTitle.trim()); - setLastUpdatedTitle(debouncedTitle); - setSaveLoading(false); - })(); - }, [debouncedTitle]); - - useEffect(() => { - const q = query.trim(); - if (q.length < 1) { - setSearchResults([]); - return; - } - - setSearchLoading(true); - - (async () => { - setSearchResults( - ( - await search(q, { - filter: { spaces: false }, - memoriesRelativeToSpace: { - fromSpaces: [spaceId], - }, - }) - ).map((i) => i.memory!), - ); - setSearchLoading(false); - })(); - }, [query]); - - if (loading) { - return ( - <div className="flex h-full w-full items-center justify-center"> - <Loader className="h-5 w-5 animate-spin" /> - </div> - ); - } - - return ( - <div className="text-rgray-11 flex w-full flex-col items-start py-8 text-left"> - <div className="flex w-full items-center justify-start gap-2 px-8"> - <button - onClick={back} - className="focus-visible:ring-offset-rgray-3 focus-visible:ring-rgray-7 rounded-sm transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2" - > - <Undo2 className="h-5 w-5" /> - </button> - <Input - ref={inputRef} - data-error="false" - className="w-full border-none p-0 text-xl ring-0 placeholder:text-white/30 placeholder:transition placeholder:duration-500 focus-visible:ring-0 data-[error=true]:placeholder:text-red-400" - placeholder="Title of the space" - data-modal-autofocus - value={title} - onChange={(e) => setTitle(e.target.value)} - /> - <button - onClick={() => { - inputRef.current?.focus(); - inputRef.current?.animate( - { - opacity: [1, 0.2, 1], - }, - { - duration: 100, - }, - ); - }} - className="focus-visible:ring-offset-rgray-3 focus-visible:ring-rgray-7 rounded-sm transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2" - > - {saveLoading ? ( - <Loader className="h-5 w-5 animate-spin opacity-70" /> - ) : ( - <Edit3 className="h-5 w-5 opacity-70" /> - )} - </button> - </div> - <div className="w-full px-8"> - <InputWithIcon - placeholder="Search" - icon={ - searchLoading ? ( - <Loader className="text-rgray-11 h-5 w-5 animate-spin opacity-50" /> - ) : ( - <Search className="text-rgray-11 h-5 w-5 opacity-50" /> - ) - } - className="bg-rgray-4 mt-2 w-full" - value={searchQuery} - onChange={(e) => setSearcyQuery(e.target.value)} - /> - </div> - <div className="mt-2 w-full px-8"> - <AddMemoryModal - onAdd={(data) => { - if (!data) { - setLoading(true); - (async () => { - const title = (await getSpace(spaceId))?.name ?? ""; - setTitle(title); - setLastUpdatedTitle(title); - setContentForSpace((await fetchContentForSpace(spaceId)) ?? []); - setLoading(false); - })(); - } else if (Object.hasOwn(data, "url")) { - const _data = data as StoredContent; - setContentForSpace((prev) => [...prev, _data]); - } - }} - data={{ space: { title, id: spaceId }, notInSpaces: [spaceId] }} - defaultSpaces={[spaceId]} - type={addMemoryState} - > - <DropdownMenu open={isDropdownOpen} onOpenChange={setIsDropdownOpen}> - <DropdownMenuTrigger asChild> - <button className="focus-visible:bg-rgray-4 focus-visible:ring-rgray-7 hover:bg-rgray-4 ml-auto flex items-center justify-center rounded-md px-3 py-2 transition focus-visible:outline-none focus-visible:ring-2"> - <Plus className="mr-2 h-5 w-5" /> - Add - </button> - </DropdownMenuTrigger> - <DropdownMenuContent onCloseAutoFocus={(e) => e.preventDefault()}> - <DialogTrigger className="block w-full"> - <DropdownMenuItem - onClick={() => { - setAddMemoryState("existing-memory"); - }} - > - <Sparkles className="mr-2 h-4 w-4" /> - Existing Memory - </DropdownMenuItem> - </DialogTrigger> - <DialogTrigger className="block w-full"> - <DropdownMenuItem - onClick={() => { - setAddMemoryState("page"); - }} - > - <StickyNote className="mr-2 h-4 w-4" /> - Page - </DropdownMenuItem> - </DialogTrigger> - <DialogTrigger className="block w-full"> - <DropdownMenuItem - onClick={() => { - setAddMemoryState("note"); - }} - > - <Text className="mr-2 h-4 w-4" /> - Note - </DropdownMenuItem> - </DialogTrigger> - </DropdownMenuContent> - </DropdownMenu> - </AddMemoryModal> - </div> - <div - ref={parent} - className="grid w-full grid-flow-row grid-cols-3 gap-1 px-2 py-5" - > - {query.trim().length > 0 ? ( - <> - {searchResults.map((memory, i) => ( - <MemoryItem - removeFromSpace={async () => { - await updateMemory(memory.id, { - removedFromSpaces: [spaceId], - }); - setContentForSpace((prev) => - prev.filter((s) => s.id !== memory.id), - ); - setSearchResults((prev) => - prev.filter((i) => i.id !== memory.id), - ); - }} - {...memory!} - key={i} - onDelete={() => { - setContentForSpace((prev) => - prev.filter((s) => s.id !== memory.id), - ); - setSearchResults((prev) => - prev.filter((i) => i.id !== memory.id), - ); - }} - /> - ))} - </> - ) : ( - contentForSpace.map((m) => ( - <MemoryItem - key={m.id} - {...m} - onDelete={() => - setContentForSpace((prev) => prev.filter((s) => s.id !== m.id)) - } - removeFromSpace={async () => { - await updateMemory(m.id, { - removedFromSpaces: [spaceId], - }); - setContentForSpace((prev) => prev.filter((s) => s.id !== m.id)); - }} - /> - )) - )} - </div> - </div> - ); -} diff --git a/apps/web/src/components/Sidebar/FilterCombobox.tsx b/apps/web/src/components/Sidebar/FilterCombobox.tsx deleted file mode 100644 index 634a09e3..00000000 --- a/apps/web/src/components/Sidebar/FilterCombobox.tsx +++ /dev/null @@ -1,303 +0,0 @@ -"use client"; - -import * as React from "react"; -import { Check, ChevronsUpDown } from "lucide-react"; - -import { cn } from "@/lib/utils"; -import { Button } from "@/components/ui/button"; -import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList, -} from "@/components/ui/command"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover"; -import { SpaceIcon } from "@/assets/Memories"; -import { AnimatePresence, LayoutGroup, motion } from "framer-motion"; -import { SearchResult, useMemory } from "@/contexts/MemoryContext"; -import { useDebounce } from "@/hooks/useDebounce"; -import { StoredContent } from "@/server/db/schema"; - -export interface FilterSpacesProps - extends React.ButtonHTMLAttributes<HTMLButtonElement> { - side?: "top" | "bottom"; - align?: "end" | "start" | "center"; - onClose?: () => void; - selectedSpaces: number[]; - setSelectedSpaces: ( - spaces: number[] | ((prev: number[]) => number[]), - ) => void; - name: string; -} - -export function FilterSpaces({ - className, - side = "bottom", - align = "center", - onClose, - selectedSpaces, - setSelectedSpaces, - name, - ...props -}: FilterSpacesProps) { - const { spaces } = useMemory(); - const [open, setOpen] = React.useState(false); - - const sortedSpaces = spaces.sort(({ id: a }, { id: b }) => - selectedSpaces.includes(a) && !selectedSpaces.includes(b) - ? -1 - : selectedSpaces.includes(b) && !selectedSpaces.includes(a) - ? 1 - : 0, - ); - - React.useEffect(() => { - if (!open) { - onClose?.(); - } - }, [open]); - - return ( - <Popover open={open} onOpenChange={setOpen}> - <PopoverTrigger asChild> - <button - type={undefined} - data-state-on={open} - className={cn( - "focus-visible:ring-rgray-8 hover:bg-rgray-3 relative flex items-center justify-center gap-1 rounded-md px-3 py-1.5 ring-2 ring-transparent focus-visible:outline-none", - className, - )} - {...props} - > - <SpaceIcon className="mr-1 h-5 w-5" /> - {name} - <ChevronsUpDown className="h-4 w-4" /> - <div - data-state-on={selectedSpaces.length > 0} - className="on:flex text-rgray-11 border-rgray-6 bg-rgray-2 absolute left-0 top-0 hidden aspect-[1] h-4 w-4 -translate-x-1/3 -translate-y-1/3 items-center justify-center rounded-full border text-center text-[9px]" - > - {selectedSpaces.length} - </div> - </button> - </PopoverTrigger> - <PopoverContent - align={align} - side={side} - className="w-[200px] p-0" - onCloseAutoFocus={(e) => e.preventDefault()} - > - <Command - filter={(val, search) => - spaces - .find((s) => s.id.toString() === val) - ?.name.toLowerCase() - .includes(search.toLowerCase().trim()) - ? 1 - : 0 - } - > - <CommandInput placeholder="Filter spaces..." /> - <CommandList asChild> - <motion.div layoutScroll> - <CommandEmpty>Nothing found</CommandEmpty> - <CommandGroup> - {sortedSpaces.map((space) => ( - <CommandItem - key={space.id} - value={space.id.toString()} - onSelect={(val) => { - setSelectedSpaces((prev: number[]) => - prev.includes(parseInt(val)) - ? prev.filter((v) => v !== parseInt(val)) - : [...prev, parseInt(val)], - ); - }} - asChild - > - <motion.div - initial={{ opacity: 0 }} - animate={{ opacity: 1, transition: { delay: 0.05 } }} - transition={{ duration: 0.15 }} - layout - layoutId={`space-combobox-${space.id}`} - className="text-rgray-11" - > - <SpaceIcon className="mr-2 h-4 w-4" /> - {space.name.length > 10 - ? space.name.slice(0, 10) + "..." - : space.name} - {selectedSpaces.includes(space.id)} - <Check - data-state-on={selectedSpaces.includes(space.id)} - className={cn( - "on:opacity-100 ml-auto h-4 w-4 opacity-0", - )} - /> - </motion.div> - </CommandItem> - ))} - </CommandGroup> - </motion.div> - </CommandList> - </Command> - </PopoverContent> - </Popover> - ); -} - -export type FilterMemoriesProps = { - side?: "top" | "bottom"; - align?: "end" | "start" | "center"; - onClose?: () => void; - selected: StoredContent[]; - setSelected: React.Dispatch<React.SetStateAction<StoredContent[]>>; - fromSpaces?: number[]; - notInSpaces?: number[]; -} & React.ButtonHTMLAttributes<HTMLButtonElement>; - -export function FilterMemories({ - className, - side = "bottom", - align = "center", - onClose, - selected, - setSelected, - fromSpaces, - notInSpaces, - ...props -}: FilterMemoriesProps) { - const { search } = useMemory(); - - const [open, setOpen] = React.useState(false); - const [searchQuery, setSearchQuery] = React.useState(""); - const query = useDebounce(searchQuery, 500); - - const [searchResults, setSearchResults] = React.useState<SearchResult[]>([]); - const [isSearching, setIsSearching] = React.useState(false); - - const results = React.useMemo(() => { - return searchResults.map((r) => r.memory); - }, [searchResults]); - - console.log("memoized", results); - - React.useEffect(() => { - const q = query.trim(); - if (q.length > 0) { - setIsSearching(true); - (async () => { - const results = await search(q, { - filter: { - memories: true, - spaces: false, - }, - memoriesRelativeToSpace: { - fromSpaces, - notInSpaces, - }, - }); - setSearchResults(results); - setIsSearching(false); - })(); - } else { - setSearchResults([]); - } - }, [query]); - - React.useEffect(() => { - if (!open) { - onClose?.(); - } - }, [open]); - - console.log(searchResults); - return ( - <AnimatePresence mode="popLayout"> - <LayoutGroup> - <Popover open={open} onOpenChange={setOpen}> - <PopoverTrigger asChild> - <button - type={undefined} - data-state-on={open} - className={cn( - "text-rgray-11/70 on:bg-rgray-3 focus-visible:ring-rgray-8 hover:bg-rgray-3 relative flex items-center justify-center gap-1 rounded-md px-3 py-1.5 ring-2 ring-transparent focus-visible:outline-none", - className, - )} - {...props} - > - {props.children} - </button> - </PopoverTrigger> - <PopoverContent - onCloseAutoFocus={(e) => e.preventDefault()} - align={align} - side={side} - className="w-[200px] p-0" - > - <Command shouldFilter={false}> - <CommandInput - isSearching={isSearching} - value={searchQuery} - onValueChange={setSearchQuery} - placeholder="Filter memories..." - /> - <CommandList> - <CommandGroup> - <CommandEmpty className="text-rgray-11 py-5 text-center text-sm"> - {isSearching - ? "Searching..." - : query.trim().length > 0 - ? "Nothing Found" - : "Search something"} - </CommandEmpty> - {results.map((m) => ( - <CommandItem - key={m.id} - value={m.id.toString()} - onSelect={(val) => { - setSelected((prev) => - prev.find((p) => p.id === parseInt(val)) - ? prev.filter((v) => v.id !== parseInt(val)) - : [...prev, m], - ); - }} - asChild - > - <div className="text-rgray-11"> - <img - src={ - m.type === "note" - ? "/note.svg" - : m.image ?? "/icons/logo_without_bg.png" - } - className="mr-2 h-4 w-4" - /> - {m.title && m.title?.length > 14 - ? m.title?.slice(0, 14) + "..." - : m.title} - <Check - data-state-on={ - selected.find((i) => i.id === m.id) !== undefined - } - className={cn( - "on:opacity-100 ml-auto h-4 w-4 opacity-0", - )} - /> - </div> - </CommandItem> - ))} - </CommandGroup> - </CommandList> - </Command> - </PopoverContent> - </Popover> - </LayoutGroup> - </AnimatePresence> - ); -} diff --git a/apps/web/src/components/Sidebar/MemoriesBar.tsx b/apps/web/src/components/Sidebar/MemoriesBar.tsx deleted file mode 100644 index a81d00c0..00000000 --- a/apps/web/src/components/Sidebar/MemoriesBar.tsx +++ /dev/null @@ -1,709 +0,0 @@ -import { Editor } from "novel"; -import { useAutoAnimate } from "@formkit/auto-animate/react"; -import { - MemoryWithImage, - MemoryWithImages3, - MemoryWithImages2, -} from "@/assets/MemoryWithImages"; -import { Input, InputWithIcon } from "../ui/input"; -import { - ArrowUpRight, - Edit3, - Loader, - Minus, - MoreHorizontal, - Plus, - Search, - Sparkles, - Text, - Trash2, -} from "lucide-react"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "../ui/dropdown-menu"; -import { useEffect, useMemo, useRef, useState } from "react"; -import { Variant, useAnimate, motion } from "framer-motion"; -import { SearchResult, useMemory } from "@/contexts/MemoryContext"; -import { SpaceIcon } from "@/assets/Memories"; -import { Dialog, DialogContent } from "../ui/dialog"; -import useViewport from "@/hooks/useViewport"; -import useTouchHold from "@/hooks/useTouchHold"; -import { DialogTrigger } from "@radix-ui/react-dialog"; -import { - AddExistingMemoryToSpace, - AddMemoryPage, - NoteAddPage, - SpaceAddPage, -} from "./AddMemoryDialog"; -import { ExpandedSpace } from "./ExpandedSpace"; -import { StoredContent, StoredSpace } from "@/server/db/schema"; -import { useDebounce } from "@/hooks/useDebounce"; -import { NoteEdit } from "./EditNoteDialog"; -import DeleteConfirmation from "./DeleteConfirmation"; - -export function MemoriesBar({ isOpen }: { isOpen: boolean }) { - const [parent, enableAnimations] = useAutoAnimate(); - const { spaces, deleteSpace, freeMemories, search } = useMemory(); - - const [isDropdownOpen, setIsDropdownOpen] = useState(false); - const [addMemoryState, setAddMemoryState] = useState< - "page" | "note" | "space" | "existing-memory" | null - >(null); - - const [expandedSpace, setExpandedSpace] = useState<number | null>(null); - - const [searchQuery, setSearcyQuery] = useState(""); - const [searchLoading, setSearchLoading] = useState(false); - const query = useDebounce(searchQuery, 500); - - const [searchResults, setSearchResults] = useState<SearchResult[]>([]); - - useEffect(() => { - const q = query.trim(); - if (q.length < 1) { - setSearchResults([]); - return; - } - - setSearchLoading(true); - - (async () => { - setSearchResults(await search(q)); - setSearchLoading(false); - })(); - }, [query]); - - useEffect(() => { - if (!isOpen) { - setExpandedSpace(null); - } - }, [isOpen]); - - if (expandedSpace) { - return ( - <ExpandedSpace - spaceId={expandedSpace} - back={() => setExpandedSpace(null)} - // close={() => setExpandedSpace(null)} - /> - ); - } - - return ( - <div className="text-rgray-11 flex w-full flex-col items-start py-8 text-left"> - <div className="w-full px-8"> - <h1 className="w-full text-2xl">Your Memories</h1> - <InputWithIcon - placeholder="Search" - icon={ - searchLoading ? ( - <Loader className="text-rgray-11 h-5 w-5 animate-spin opacity-50" /> - ) : ( - <Search className="text-rgray-11 h-5 w-5 opacity-50" /> - ) - } - className="bg-rgray-4 mt-2 w-full" - value={searchQuery} - onChange={(e) => setSearcyQuery(e.target.value)} - /> - </div> - <div className="mt-2 flex w-full px-8"> - <AddMemoryModal type={addMemoryState}> - <DropdownMenu open={isDropdownOpen} onOpenChange={setIsDropdownOpen}> - <DropdownMenuTrigger asChild> - <button className="focus-visible:bg-rgray-4 focus-visible:ring-rgray-7 hover:bg-rgray-4 ml-auto flex items-center justify-center rounded-md px-3 py-2 transition focus-visible:outline-none focus-visible:ring-2"> - <Plus className="mr-2 h-5 w-5" /> - Add - </button> - </DropdownMenuTrigger> - <DropdownMenuContent onCloseAutoFocus={(e) => e.preventDefault()}> - <DialogTrigger className="block w-full"> - <DropdownMenuItem - onClick={() => { - setAddMemoryState("page"); - }} - > - <Sparkles className="mr-2 h-4 w-4" /> - Page to Memory - </DropdownMenuItem> - </DialogTrigger> - <DialogTrigger className="block w-full"> - <DropdownMenuItem - onClick={() => { - setAddMemoryState("note"); - }} - > - <Text className="mr-2 h-4 w-4" /> - Note - </DropdownMenuItem> - </DialogTrigger> - <DialogTrigger className="block w-full"> - <DropdownMenuItem - onClick={() => { - setAddMemoryState("space"); - }} - > - <SpaceIcon className="mr-2 h-4 w-4" /> - Space - </DropdownMenuItem> - </DialogTrigger> - </DropdownMenuContent> - </DropdownMenu> - </AddMemoryModal> - </div> - <div - ref={parent} - className="grid w-full grid-flow-row grid-cols-3 gap-1 px-2 py-5" - > - {query.trim().length > 0 ? ( - <> - {searchResults.map(({ type, space, memory }, i) => ( - <> - {type === "memory" && ( - <MemoryItem - {...memory!} - key={i} - onDelete={() => { - setSearchResults((prev) => - prev.filter((i) => i.memory?.id !== memory.id), - ); - }} - /> - )} - {type === "space" && ( - <SpaceItem - {...space!} - key={i} - onDelete={() => { - setSearchResults((prev) => - prev.filter((i) => i.space?.id !== space.id), - ); - deleteSpace(space.id); - }} - /> - )} - </> - ))} - </> - ) : ( - <> - {spaces.map((space) => ( - <SpaceItem - onDelete={() => deleteSpace(space.id)} - key={space.id} - onClick={() => setExpandedSpace(space.id)} - {...space} - /> - ))} - {freeMemories.map((m) => ( - <MemoryItem {...m} key={m.id} /> - ))} - </> - )} - </div> - </div> - ); -} - -const SpaceExitVariant: Variant = { - opacity: 0, - scale: 0, - borderRadius: "50%", - background: "var(--gray-1)", - transition: { - duration: 0.2, - }, -}; - -export function MemoryItem( - props: StoredContent & { - onDelete?: () => void; - removeFromSpace?: () => Promise<void>; - }, -) { - const { id, title, image, type, url, onDelete, removeFromSpace } = props; - - const { deleteMemory } = useMemory(); - - const name = title - ? title.length > 10 - ? title.slice(0, 10) + "..." - : title - : "<no title>"; - - const [isDialogOpen, setIsDialogOpen] = useState(false); - - const [moreDropdownOpen, setMoreDropdownOpen] = useState(false); - - const touchEventProps = useTouchHold({ - onHold() { - setMoreDropdownOpen(true); - }, - }); - return ( - <Dialog - open={type === "note" ? isDialogOpen : false} - onOpenChange={setIsDialogOpen} - > - <div - {...touchEventProps} - className="hover:bg-rgray-2 has-[[data-state='true']]:bg-rgray-2 has-[[data-space-text]:focus-visible]:bg-rgray-2 has-[[data-space-text]:focus-visible]:ring-rgray-7 [&:has-[[data-space-text]:focus-visible]>[data-more-button]]:opacity-100 relative flex cursor-pointer select-none flex-col-reverse items-center justify-center rounded-md p-2 pb-4 text-center font-normal ring-transparent transition has-[[data-space-text]:focus-visible]:outline-none has-[[data-space-text]:focus-visible]:ring-2 md:has-[[data-state='true']]:bg-transparent [&:hover>[data-more-button]]:opacity-100" - > - {type === "note" ? ( - <DialogTrigger asChild> - <button data-space-text className="focus-visible:outline-none"> - {name} - </button> - </DialogTrigger> - ) : ( - <button - onClick={() => window.open(url)} - data-space-text - className="focus-visible:outline-none" - > - {name} - </button> - )} - - {type === "page" ? ( - <PageMoreButton - isOpen={moreDropdownOpen} - setIsOpen={setMoreDropdownOpen} - removeFromSpace={removeFromSpace} - onDelete={() => { - deleteMemory(id); - onDelete?.(); - }} - url={url} - /> - ) : type === "note" ? ( - <NoteMoreButton - isOpen={moreDropdownOpen} - setIsOpen={setMoreDropdownOpen} - removeFromSpace={removeFromSpace} - onEdit={() => setIsDialogOpen(true)} - onDelete={() => { - deleteMemory(id); - onDelete?.(); - }} - /> - ) : null} - - <div className="flex h-24 w-24 items-center justify-center"> - {type === "page" ? ( - <img - onClick={() => window.open(url)} - className="h-16 w-16" - id={id.toString()} - src={image!} - onError={(e) => { - (e.target as HTMLImageElement).src = - "/icons/white_without_bg.png"; - }} - /> - ) : type === "note" ? ( - <Text onClick={() => setIsDialogOpen(true)} className="h-16 w-16" /> - ) : ( - <></> - )} - </div> - </div> - <DialogContent className="w-max max-w-[auto]"> - <NoteEdit - onDelete={onDelete} - closeDialog={() => setIsDialogOpen(false)} - memory={props} - /> - </DialogContent> - </Dialog> - ); -} - -export function SpaceItem({ - name, - id, - onDelete, - onClick, -}: StoredSpace & { onDelete: () => void; onClick?: () => void }) { - const { cachedMemories } = useMemory(); - - const [itemRef, animateItem] = useAnimate(); - const { width } = useViewport(); - - const [moreDropdownOpen, setMoreDropdownOpen] = useState(false); - - const touchEventProps = useTouchHold({ - onHold() { - setMoreDropdownOpen(true); - }, - }); - - const spaceMemories = useMemo(() => { - return cachedMemories.filter((m) => m.space === id); - }, [cachedMemories]); - - const _name = name.length > 10 ? name.slice(0, 10) + "..." : name; - - return ( - <motion.div - ref={itemRef} - {...touchEventProps} - className="hover:bg-rgray-2 has-[[data-state='true']]:bg-rgray-2 has-[[data-space-text]:focus-visible]:bg-rgray-2 has-[[data-space-text]:focus-visible]:ring-rgray-7 [&:has-[[data-space-text]:focus-visible]>[data-more-button]]:opacity-100 relative flex select-none flex-col-reverse items-center justify-center rounded-md p-2 pb-4 text-center font-normal ring-transparent transition has-[[data-space-text]:focus-visible]:outline-none has-[[data-space-text]:focus-visible]:ring-2 md:has-[[data-state='true']]:bg-transparent [&:hover>[data-more-button]]:opacity-100" - > - <button - onClick={onClick} - data-space-text - className="focus-visible:outline-none" - > - {_name} - </button> - <SpaceMoreButton - isOpen={moreDropdownOpen} - setIsOpen={setMoreDropdownOpen} - onEdit={onClick} - onDelete={() => { - onDelete(); - return; - if (!itemRef.current || width < 768) { - onDelete(); - return; - } - // const trash = document.querySelector("#trash")! as HTMLDivElement; - // const trashBin = document.querySelector("#trash-button")!; - // const trashRect = trashBin.getBoundingClientRect(); - // const scopeRect = itemRef.current.getBoundingClientRect(); - // const el = document.createElement("div"); - // el.style.position = "fixed"; - // el.style.top = "0"; - // el.style.left = "0"; - // el.style.width = "15px"; - // el.style.height = "15px"; - // el.style.backgroundColor = "var(--gray-7)"; - // el.style.zIndex = "60"; - // el.style.borderRadius = "50%"; - // el.style.transform = "scale(5)"; - // el.style.opacity = "0"; - // trash.dataset["open"] = "true"; - // const initial = { - // x: scopeRect.left + scopeRect.width / 2, - // y: scopeRect.top + scopeRect.height / 2, - // }; - // const delta = { - // x: - // trashRect.left + - // trashRect.width / 2 - - // scopeRect.left + - // scopeRect.width / 2, - // y: - // trashRect.top + - // trashRect.height / 4 - - // scopeRect.top + - // scopeRect.height / 2, - // }; - // const end = { - // x: trashRect.left + trashRect.width / 2, - // y: trashRect.top + trashRect.height / 4, - // }; - // el.style.offsetPath = `path('M ${initial.x} ${initial.y} Q ${delta.x * 0.01} ${delta.y * 0.01} ${end.x} ${end.y}`; - // animateItem(itemRef.current, SpaceExitVariant, { - // duration: 0.2, - // }).then(() => { - // itemRef.current.style.scale = "0"; - // onDelete(); - // }); - // document.body.appendChild(el); - // el.animate( - // { - // transform: ["scale(5)", "scale(1)"], - // opacity: [0, 0.3, 1], - // }, - // { - // duration: 200, - // easing: "cubic-bezier(0.64, 0.57, 0.67, 1.53)", - // fill: "forwards", - // }, - // ); - // el.animate( - // { - // offsetDistance: ["0%", "100%"], - // }, - // { - // duration: 2000, - // easing: "cubic-bezier(0.64, 0.57, 0.67, 1.53)", - // fill: "forwards", - // delay: 200, - // }, - // ).onfinish = () => { - // el.animate( - // { transform: "scale(0)", opacity: 0 }, - // { duration: 200, fill: "forwards" }, - // ).onfinish = () => { - // el.remove(); - // }; - // }; - }} - /> - {spaceMemories.length > 2 ? ( - <MemoryWithImages3 - onClick={onClick} - className="h-24 w-24" - id={id.toString()} - images={ - spaceMemories - .map((c) => (c.type === "note" ? "/note.svg" : c.image)) - .reverse() as string[] - } - /> - ) : spaceMemories.length > 1 ? ( - <MemoryWithImages2 - onClick={onClick} - className="h-24 w-24" - id={id.toString()} - images={ - spaceMemories - .map((c) => (c.type === "note" ? "/note.svg" : c.image)) - .reverse() as string[] - } - /> - ) : spaceMemories.length === 1 ? ( - <MemoryWithImage - onClick={onClick} - className="h-24 w-24" - id={id.toString()} - image={ - spaceMemories[0].type === "note" - ? "/note.svg" - : spaceMemories[0].image! - } - /> - ) : ( - <div - onClick={onClick} - className="bg-rgray-4 shadow- h-24 w-24 scale-50 rounded-full opacity-30" - ></div> - )} - </motion.div> - ); -} - -export function SpaceMoreButton({ - onDelete, - isOpen, - setIsOpen, - onEdit, -}: { - onDelete?: () => void; - isOpen?: boolean; - onEdit?: () => void; - setIsOpen?: (open: boolean) => void; -}) { - return ( - <DeleteConfirmation onDelete={onDelete} trigger={false}> - <DropdownMenu open={isOpen} onOpenChange={setIsOpen}> - <DropdownMenuTrigger asChild> - <button - data-more-button - className="hover:bg-rgray-3 focus-visible:bg-rgray-3 focus-visible:ring-rgray-7 absolute right-2 top-2 scale-0 rounded-md p-1 opacity-0 ring-transparent transition focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 md:block md:scale-100 md:bg-transparent" - > - <MoreHorizontal className="text-rgray-11 h-5 w-5" /> - </button> - </DropdownMenuTrigger> - <DropdownMenuContent align="start"> - <DropdownMenuItem onClick={onEdit}> - <Edit3 className="mr-2 h-4 w-4" strokeWidth={1.5} /> - Edit - </DropdownMenuItem> - <DialogTrigger asChild> - <DropdownMenuItem className="focus:bg-red-100 focus:text-red-400"> - <Trash2 className="mr-2 h-4 w-4" strokeWidth={1.5} /> - Delete - </DropdownMenuItem> - </DialogTrigger> - </DropdownMenuContent> - </DropdownMenu> - </DeleteConfirmation> - ); -} - -export function PageMoreButton({ - onDelete, - isOpen, - setIsOpen, - url, - removeFromSpace, -}: { - onDelete?: () => void; - isOpen?: boolean; - url: string; - setIsOpen?: (open: boolean) => void; - removeFromSpace?: () => Promise<void>; -}) { - return ( - <DeleteConfirmation onDelete={onDelete} trigger={false}> - <DropdownMenu open={isOpen} onOpenChange={setIsOpen}> - <DropdownMenuTrigger asChild> - <button - data-more-button - className="hover:bg-rgray-3 focus-visible:bg-rgray-3 focus-visible:ring-rgray-7 absolute right-2 top-2 scale-0 rounded-md p-1 opacity-0 ring-transparent transition focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 md:block md:scale-100 md:bg-transparent" - > - <MoreHorizontal className="text-rgray-11 h-5 w-5" /> - </button> - </DropdownMenuTrigger> - <DropdownMenuContent align="start"> - <DropdownMenuItem onClick={() => window.open(url)}> - <ArrowUpRight - className="mr-2 h-4 w-4 scale-125" - strokeWidth={1.5} - /> - Open - </DropdownMenuItem> - {removeFromSpace && ( - <DropdownMenuItem onClick={removeFromSpace}> - <Minus className="mr-2 h-4 w-4" strokeWidth={1.5} /> - Remove from space - </DropdownMenuItem> - )} - <DialogTrigger asChild> - <DropdownMenuItem className="focus:bg-red-100 focus:text-red-400"> - <Trash2 className="mr-2 h-4 w-4" strokeWidth={1.5} /> - Delete - </DropdownMenuItem> - </DialogTrigger> - </DropdownMenuContent> - </DropdownMenu> - </DeleteConfirmation> - ); -} - -export function NoteMoreButton({ - onDelete, - isOpen, - setIsOpen, - onEdit, - removeFromSpace, -}: { - onDelete?: () => void; - isOpen?: boolean; - onEdit?: () => void; - setIsOpen?: (open: boolean) => void; - removeFromSpace?: () => Promise<void>; -}) { - return ( - <DeleteConfirmation onDelete={onDelete} trigger={false}> - <DropdownMenu open={isOpen} onOpenChange={setIsOpen}> - <DropdownMenuTrigger asChild> - <button - data-more-button - className="hover:bg-rgray-3 focus-visible:bg-rgray-3 focus-visible:ring-rgray-7 absolute right-2 top-2 scale-0 rounded-md p-1 opacity-0 ring-transparent transition focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 md:block md:scale-100 md:bg-transparent" - > - <MoreHorizontal className="text-rgray-11 h-5 w-5" /> - </button> - </DropdownMenuTrigger> - <DropdownMenuContent align="start"> - <DropdownMenuItem onClick={onEdit}> - <Edit3 className="mr-2 h-4 w-4" strokeWidth={1.5} /> - Edit - </DropdownMenuItem> - {removeFromSpace && ( - <DropdownMenuItem onClick={removeFromSpace}> - <Minus className="mr-2 h-4 w-4" strokeWidth={1.5} /> - Remove from space - </DropdownMenuItem> - )} - <DialogTrigger asChild> - <DropdownMenuItem className="focus:bg-red-100 focus:text-red-400"> - <Trash2 className="mr-2 h-4 w-4" strokeWidth={1.5} /> - Delete - </DropdownMenuItem> - </DialogTrigger> - </DropdownMenuContent> - </DropdownMenu> - </DeleteConfirmation> - ); -} - -export function AddMemoryModal({ - type, - children, - defaultSpaces, - onAdd, - data, -}: { - type: "page" | "note" | "space" | "existing-memory" | null; - children?: React.ReactNode | React.ReactNode[]; - defaultSpaces?: number[]; - data?: { - space?: { - title: string; - id: number; - }; - fromSpaces?: number[]; - notInSpaces?: number[]; - }; - onAdd?: (data?: StoredSpace | StoredContent | StoredContent[]) => void; -}) { - const [isDialogOpen, setIsDialogOpen] = useState(false); - - return ( - <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}> - {children} - <DialogContent - onOpenAutoFocus={(e) => { - e.preventDefault(); - const novel = document.querySelector('[contenteditable="true"]') as - | HTMLDivElement - | undefined; - if (novel) { - novel.autofocus = false; - novel.onfocus = () => { - ( - document.querySelector("[data-modal-autofocus]") as - | HTMLInputElement - | undefined - )?.focus(); - novel.onfocus = null; - }; - } - ( - document.querySelector("[data-modal-autofocus]") as - | HTMLInputElement - | undefined - )?.focus(); - }} - className="w-max max-w-[auto]" - > - {type === "page" ? ( - <AddMemoryPage - onAdd={onAdd} - defaultSpaces={defaultSpaces} - closeDialog={() => setIsDialogOpen(false)} - /> - ) : type === "note" ? ( - <NoteAddPage - onAdd={onAdd} - defaultSpaces={defaultSpaces} - closeDialog={() => setIsDialogOpen(false)} - /> - ) : type === "space" ? ( - <SpaceAddPage - onAdd={onAdd} - closeDialog={() => setIsDialogOpen(false)} - /> - ) : type === "existing-memory" ? ( - <AddExistingMemoryToSpace - onAdd={onAdd} - fromSpaces={data?.fromSpaces} - notInSpaces={data?.notInSpaces} - space={data!.space!} - closeDialog={() => setIsDialogOpen(false)} - /> - ) : ( - <></> - )} - </DialogContent> - </Dialog> - ); -} diff --git a/apps/web/src/components/Sidebar/SettingsTab.tsx b/apps/web/src/components/Sidebar/SettingsTab.tsx deleted file mode 100644 index 31b8380d..00000000 --- a/apps/web/src/components/Sidebar/SettingsTab.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { Box, LogOut } from "lucide-react"; -import { signOut, useSession } from "next-auth/react"; -import { useEffect, useState } from "react"; - -export function SettingsTab({ open }: { open: boolean }) { - const { data: session } = useSession(); - - const [tweetStat, setTweetStat] = useState<[number, number] | null>(); - const [memoryStat, setMemoryStat] = useState<[number, number] | null>(); - - const [loading, setLoading] = useState(true); - - useEffect(() => { - fetch("/api/getCount").then(async (resp) => { - const data = (await resp.json()) as any; - setTweetStat([data.tweetsCount, data.tweetsLimit]); - setMemoryStat([data.pageCount, data.pageLimit]); - setLoading(false); - }); - }, [open]); - - return ( - <div className="flex h-full w-full flex-col items-start py-3 text-left font-normal text-black md:py-8"> - <div className="w-full px-6"> - <h1 className="w-full text-2xl font-medium">Settings</h1> - <div className="mt-5 grid w-full grid-cols-3 gap-1"> - <img - className="rounded-full" - src={session?.user?.image ?? "/icons/white_without_bg.png"} - onError={(e) => { - (e.target as HTMLImageElement).src = - "/icons/white_without_bg.png"; - }} - /> - <div className="col-span-2 flex flex-col items-start justify-center"> - <h1 className="text-xl font-medium">{session?.user?.name}</h1> - <span>{session?.user?.email}</span> - <button - onClick={() => signOut()} - className="bg-rgray-4 hover:bg-rgray-5 focus-visible:bg-rgray-5 focus-visible:ring-rgray-7 relative mt-auto flex items-center justify-center gap-2 rounded-md px-4 py-2 text-white ring-transparent transition focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-70" - > - <LogOut className="h-4 w-4" /> - Logout - </button> - </div> - </div> - </div> - <div className="border-rgray-5 mt-auto w-full px-8 pt-8"> - <h1 className="flex w-full items-center gap-2 text-xl"> - <Box className="h-6 w-6" /> - Storage - </h1> - {loading ? ( - <div className="my-5 flex w-full flex-col items-center justify-center gap-5"> - <div className="bg-rgray-5 h-6 w-full animate-pulse rounded-md text-lg"></div> - <div className="bg-rgray-5 h-6 w-full animate-pulse rounded-md text-lg"></div> - </div> - ) : ( - <> - <div className="my-5"> - <h2 className="text-md flex w-full items-center justify-between"> - Memories - <div className="bg-rgray-4 flex rounded-md px-2 py-2 text-xs text-white/70"> - {memoryStat?.join("/")} - </div> - </h2> - <div className="mt-2 h-5 w-full overflow-hidden rounded-full bg-stone-400"> - <div - style={{ - width: `${((memoryStat?.[0] ?? 0) / (memoryStat?.[1] ?? 100)) * 100}%`, - minWidth: memoryStat?.[0] ?? 0 > 0 ? "5%" : "0%", - }} - className="bg-rgray-5 h-full rounded-full" - /> - </div> - </div> - <div className="my-5"> - <h2 className="text-md flex w-full items-center justify-between"> - Tweets - <div className="bg-rgray-4 flex rounded-md px-2 py-2 text-xs text-white/70"> - {tweetStat?.join("/")} - </div> - </h2> - <div className="mt-2 h-5 w-full overflow-hidden rounded-full bg-stone-400"> - <div - style={{ - width: `${((tweetStat?.[0] ?? 0) / (tweetStat?.[1] ?? 100)) * 100}%`, - minWidth: tweetStat?.[0] ?? 0 > 0 ? "5%" : "0%", - }} - className="h-full rounded-full bg-white" - /> - </div> - </div> - </> - )} - </div> - </div> - ); -} diff --git a/apps/web/src/components/Sidebar/index.tsx b/apps/web/src/components/Sidebar/index.tsx deleted file mode 100644 index ae757afe..00000000 --- a/apps/web/src/components/Sidebar/index.tsx +++ /dev/null @@ -1,172 +0,0 @@ -"use client"; -import { MemoryIcon } from "../../assets/Memories"; -import React, { useEffect, useState } from "react"; -import { AnimatePresence, motion } from "framer-motion"; -import { signOut, useSession } from "next-auth/react"; -import MessagePoster from "@/app/MessagePoster"; -import Link from "next/link"; -import { SettingsTab } from "./SettingsTab"; -import { Avatar, AvatarImage } from "@radix-ui/react-avatar"; -import { AvatarFallback } from "../ui/avatar"; - -export type MenuItem = { - icon: React.ReactNode | React.ReactNode[]; - label: string; - content?: React.ReactNode; - labelDisplay?: React.ReactNode; -}; - -export default function Sidebar({ - selectChange, - jwt, -}: { - selectChange?: (selectedItem: string | null) => void; - jwt: string; -}) { - const { data: session } = useSession(); - - const [selectedItem, setSelectedItem] = useState<string | null>(null); - - const menuItemsTop: Array<MenuItem> = []; - - const menuItemsBottom: Array<MenuItem> = [ - { - label: "Settings", - content: <SettingsTab open={selectedItem !== null} />, - icon: <></>, - }, - ]; - - const menuItems = [...menuItemsTop, ...menuItemsBottom]; - - const Subbar = menuItems.find((i) => i.label === selectedItem)?.content ?? ( - <></> - ); - - useEffect(() => { - void selectChange?.(selectedItem); - }, [selectedItem]); - - return ( - <div className="relative hidden h-screen max-h-screen w-max flex-col items-center text-sm font-light md:flex"> - <div - className={`relative z-[50] flex h-full w-full flex-col items-center justify-center border-r bg-stone-100 px-2 py-5 `} - > - <Link - data-state-on={selectedItem === "Memories"} - href="/" - onClick={() => setSelectedItem(null)} - className="focus-visible:ring-rgray-7 relative z-[100] flex w-full flex-col items-center justify-center rounded-md px-3 py-3 opacity-80 ring-2 ring-transparent transition hover:bg-stone-300 hover:opacity-100 focus-visible:opacity-100 focus-visible:outline-none" - > - <MemoryIcon className="h-12 w-12" /> - <span className="text-black">Memories</span> - </Link> - - <div className="mt-auto" /> - - <MenuItem - item={{ - label: "Settings", - icon: ( - <svg - xmlns="http://www.w3.org/2000/svg" - fill="white" - viewBox="0 0 24 24" - strokeWidth={0.5} - stroke="black" - className="h-10 w-10" - > - <path - strokeLinecap="round" - strokeLinejoin="round" - d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z" - /> - <path - strokeLinecap="round" - strokeLinejoin="round" - d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" - /> - </svg> - ), - content: <SettingsTab open={selectedItem !== null} />, - }} - selectedItem={selectedItem} - setSelectedItem={setSelectedItem} - /> - {/* <MessagePoster jwt={jwt} /> */} - <div className="mt-4 flex cursor-pointer flex-col items-center justify-center gap-2 rounded-b-md border-t border-stone-600 px-2 py-3 pt-4 text-black hover:bg-stone-300 hover:opacity-100"> - <Avatar> - <AvatarImage - className="h-10 w-10 rounded-full" - src={session?.user?.image!} - alt="Profile picture" - /> - <AvatarFallback> - {session?.user?.name?.split(" ").map((n) => n[0])}{" "} - </AvatarFallback> - </Avatar> - <span>{session?.user?.name?.split(" ")[0]}</span> - </div> - </div> - <AnimatePresence> - {selectedItem && <SubSidebar>{Subbar}</SubSidebar>} - </AnimatePresence> - </div> - ); -} - -const MenuItem = ({ - item: { icon, label, labelDisplay }, - selectedItem, - setSelectedItem, - ...props -}: { - item: MenuItem; - selectedItem: string | null; - setSelectedItem: React.Dispatch<React.SetStateAction<string | null>>; -}) => { - const handleClick = () => - setSelectedItem((prev) => (prev === label ? null : label)); - - return ( - <button - data-state-on={selectedItem === label} - onClick={handleClick} - className="on:opacity-100 on:bg-stone-300 focus-visible:ring-rgray-7 relative z-[100] flex w-full flex-col items-center justify-center rounded-md px-3 py-3 text-black opacity-80 ring-2 ring-transparent transition hover:bg-stone-300 hover:opacity-100 focus-visible:opacity-100 focus-visible:outline-none" - {...props} - > - {icon} - <span className="">{labelDisplay ?? label}</span> - </button> - ); -}; - -export function SubSidebar({ children }: { children?: React.ReactNode }) { - return ( - <motion.div - initial={{ opacity: 0, x: "-100%" }} - animate={{ opacity: 1, x: 0 }} - exit={{ - opacity: 0, - x: "-100%", - transition: { delay: 0.2 }, - }} - transition={{ - duration: 0.2, - }} - className="absolute left-[100%] top-0 z-[10] hidden h-screen w-[30vw] items-start justify-center overflow-x-hidden border-r bg-stone-100 font-light md:flex" - > - <motion.div - initial={{ opacity: 0 }} - animate={{ opacity: 1 }} - exit={{ opacity: 0, transition: { delay: 0 } }} - transition={{ - delay: 0.2, - }} - className="z-[10] flex h-full w-full min-w-full flex-col items-center opacity-0" - > - <AnimatePresence>{children}</AnimatePresence> - </motion.div> - </motion.div> - ); -} diff --git a/apps/web/src/components/WordMark.tsx b/apps/web/src/components/WordMark.tsx deleted file mode 100644 index eb55647c..00000000 --- a/apps/web/src/components/WordMark.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { cn } from "@/lib/utils"; -import React from "react"; - -function WordMark({ className }: { className?: string }) { - return ( - <span className={cn(`text-xl font-bold tracking-tight ${className}`)}> - smort. - </span> - ); -} - -export default WordMark; diff --git a/apps/web/src/components/dev/SessionProviderWrapper.tsx b/apps/web/src/components/dev/SessionProviderWrapper.tsx deleted file mode 100644 index 71f77886..00000000 --- a/apps/web/src/components/dev/SessionProviderWrapper.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { SessionProvider } from "next-auth/react"; -import React from "react"; - -function SessionProviderWrapper({ children }: { children: React.ReactNode }) { - if (typeof window === "undefined") { - return <>{children}</>; - } else { - return <SessionProvider>{children}</SessionProvider>; - } -} - -export default SessionProviderWrapper; diff --git a/apps/web/src/components/dev/tailwindindicator.tsx b/apps/web/src/components/dev/tailwindindicator.tsx deleted file mode 100644 index fd70276d..00000000 --- a/apps/web/src/components/dev/tailwindindicator.tsx +++ /dev/null @@ -1,16 +0,0 @@ -export function TailwindIndicator() { - if (process.env.NODE_ENV === "production") return null; - - return ( - <div> - <div className="fixed bottom-1 left-1 z-[999] flex size-6 items-center justify-center rounded-full border-2 bg-white p-3 font-mono text-xs text-black"> - <div className="block sm:hidden">xs</div> - <div className="hidden sm:block md:hidden">sm</div> - <div className="hidden md:block lg:hidden">md</div> - <div className="hidden lg:block xl:hidden">lg</div> - <div className="hidden xl:block 2xl:hidden">xl</div> - <div className="hidden 2xl:block">2xl</div> - </div> - </div> - ); -} diff --git a/apps/web/src/components/ui/avatar.tsx b/apps/web/src/components/ui/avatar.tsx deleted file mode 100644 index b36abf28..00000000 --- a/apps/web/src/components/ui/avatar.tsx +++ /dev/null @@ -1,50 +0,0 @@ -"use client"; - -import * as React from "react"; -import * as AvatarPrimitive from "@radix-ui/react-avatar"; - -import { cn } from "@/lib/utils"; - -const Avatar = React.forwardRef< - React.ElementRef<typeof AvatarPrimitive.Root>, - React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root> ->(({ className, ...props }, ref) => ( - <AvatarPrimitive.Root - ref={ref} - className={cn( - "relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", - className, - )} - {...props} - /> -)); -Avatar.displayName = AvatarPrimitive.Root.displayName; - -const AvatarImage = React.forwardRef< - React.ElementRef<typeof AvatarPrimitive.Image>, - React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image> ->(({ className, ...props }, ref) => ( - <AvatarPrimitive.Image - ref={ref} - className={cn("aspect-square h-full w-full", className)} - {...props} - /> -)); -AvatarImage.displayName = AvatarPrimitive.Image.displayName; - -const AvatarFallback = React.forwardRef< - React.ElementRef<typeof AvatarPrimitive.Fallback>, - React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback> ->(({ className, ...props }, ref) => ( - <AvatarPrimitive.Fallback - ref={ref} - className={cn( - "flex h-full w-full items-center justify-center rounded-full bg-gray-100", - className, - )} - {...props} - /> -)); -AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; - -export { Avatar, AvatarImage, AvatarFallback }; diff --git a/apps/web/src/components/ui/badge.tsx b/apps/web/src/components/ui/badge.tsx deleted file mode 100644 index fa390bec..00000000 --- a/apps/web/src/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( - "inline-flex items-center rounded-full border border-gray-200 px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-gray-950 focus:ring-offset-2", - { - variants: { - variant: { - default: - "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", - secondary: - "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", - destructive: - "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", - 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/apps/web/src/components/ui/button.tsx b/apps/web/src/components/ui/button.tsx deleted file mode 100644 index 24fa903e..00000000 --- a/apps/web/src/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( - "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-white transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-950 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 dark:ring-offset-gray-950 dark:focus-visible:ring-gray-300", - { - variants: { - variant: { - default: "bg-primary text-primary-foreground hover:bg-primary/90", - destructive: - "bg-destructive text-destructive-foreground hover:bg-destructive/90", - outline: - "border border-input bg-background hover:bg-accent hover:text-accent-foreground", - 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: "h-10 w-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 ( - <button - className={cn(buttonVariants({ variant, size, className }))} - ref={ref} - {...props} - /> - ); - }, -); -Button.displayName = "Button"; - -export { Button, buttonVariants }; diff --git a/apps/web/src/components/ui/card.tsx b/apps/web/src/components/ui/card.tsx deleted file mode 100644 index e98d500c..00000000 --- a/apps/web/src/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( - "rounded-lg border border-gray-200 bg-white text-gray-950 shadow-sm dark:border-gray-800 dark:bg-gray-950 dark:text-gray-50", - 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-sm text-gray-500 dark:text-gray-400", 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/apps/web/src/components/ui/command.tsx b/apps/web/src/components/ui/command.tsx deleted file mode 100644 index afc2cf46..00000000 --- a/apps/web/src/components/ui/command.tsx +++ /dev/null @@ -1,161 +0,0 @@ -"use client"; - -import * as React from "react"; -import { type DialogProps } from "@radix-ui/react-dialog"; -import { Command as CommandPrimitive } from "cmdk"; -import { Loader, Search } from "lucide-react"; - -import { cn } from "@/lib/utils"; -import { Dialog, DialogContent } from "@/components/ui/dialog"; - -const Command = React.forwardRef< - React.ElementRef<typeof CommandPrimitive>, - React.ComponentPropsWithoutRef<typeof CommandPrimitive> ->(({ className, ...props }, ref) => ( - <CommandPrimitive - ref={ref} - className={cn( - "bg-rgray-3 text-rgray-11 flex h-full w-full flex-col overflow-hidden rounded-md focus-visible:outline-none [&>[cmdk-list-sizer]]:max-h-[250px] [&>[cmdk-list-sizer]]:overflow-y-scroll", - className, - )} - {...props} - /> -)); -Command.displayName = CommandPrimitive.displayName; - -interface CommandDialogProps extends DialogProps {} - -const CommandDialog = ({ children, ...props }: CommandDialogProps) => { - return ( - <Dialog {...props}> - <DialogContent className="overflow-hidden p-0 shadow-lg"> - <Command className="[&_[cmdk-group-heading]]:text-rgray-11 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5"> - {children} - </Command> - </DialogContent> - </Dialog> - ); -}; - -const CommandInput = React.forwardRef< - React.ElementRef<typeof CommandPrimitive.Input>, - React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input> & { - isSearching?: boolean; - } ->(({ className, isSearching = false, ...props }, ref) => ( - <div - className="border-rgray-6 flex items-center border-b px-3" - cmdk-input-wrapper="" - > - {isSearching ? ( - <Loader className="shrink-9 mr-2 h-4 w-4 animate-spin opacity-50" /> - ) : ( - <Search className="mr-2 h-4 w-4 shrink-0 opacity-50" /> - )} - <CommandPrimitive.Input - ref={ref} - className={cn( - "placeholder:text-rgray-11/50 flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none disabled:cursor-not-allowed disabled:opacity-50", - className, - )} - {...props} - /> - </div> -)); - -CommandInput.displayName = CommandPrimitive.Input.displayName; - -const CommandList = React.forwardRef< - React.ElementRef<typeof CommandPrimitive.List>, - React.ComponentPropsWithoutRef<typeof CommandPrimitive.List> ->(({ className, ...props }, ref) => ( - <CommandPrimitive.List - ref={ref} - className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)} - {...props} - /> -)); - -CommandList.displayName = CommandPrimitive.List.displayName; - -const CommandEmpty = React.forwardRef< - React.ElementRef<typeof CommandPrimitive.Empty>, - React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty> ->((props, ref) => ( - <CommandPrimitive.Empty - ref={ref} - className="py-6 text-center text-sm" - {...props} - /> -)); - -CommandEmpty.displayName = CommandPrimitive.Empty.displayName; - -const CommandGroup = React.forwardRef< - React.ElementRef<typeof CommandPrimitive.Group>, - React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group> ->(({ className, ...props }, ref) => ( - <CommandPrimitive.Group - ref={ref} - className={cn( - "text-rgray-12 [&_[cmdk-group-heading]]:text-rgray-11 overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium", - className, - )} - {...props} - /> -)); - -CommandGroup.displayName = CommandPrimitive.Group.displayName; - -const CommandSeparator = React.forwardRef< - React.ElementRef<typeof CommandPrimitive.Separator>, - React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator> ->(({ className, ...props }, ref) => ( - <CommandPrimitive.Separator - ref={ref} - className={cn("bg-rgray-3 -mx-1 h-px", className)} - {...props} - /> -)); -CommandSeparator.displayName = CommandPrimitive.Separator.displayName; - -const CommandItem = React.forwardRef< - React.ElementRef<typeof CommandPrimitive.Item>, - React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item> ->(({ className, ...props }, ref) => ( - <CommandPrimitive.Item - ref={ref} - className={cn( - "aria-selected:bg-rgray-5 aria-selected:text-rgray-12 relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm opacity-70 outline-none data-[disabled='true']:pointer-events-none data-[disabled='true']:opacity-50", - className, - )} - {...props} - /> -)); - -CommandItem.displayName = CommandPrimitive.Item.displayName; - -const CommandShortcut = ({ - className, - ...props -}: React.HTMLAttributes<HTMLSpanElement>) => { - return ( - <span - className={cn("text-gray-11 ml-auto text-xs tracking-widest", className)} - {...props} - /> - ); -}; -CommandShortcut.displayName = "CommandShortcut"; - -export { - Command, - CommandDialog, - CommandInput, - CommandList, - CommandEmpty, - CommandGroup, - CommandItem, - CommandShortcut, - CommandSeparator, -}; diff --git a/apps/web/src/components/ui/dialog.tsx b/apps/web/src/components/ui/dialog.tsx deleted file mode 100644 index 0da54769..00000000 --- a/apps/web/src/components/ui/dialog.tsx +++ /dev/null @@ -1,119 +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( - "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%] border-rgray-6 fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] rounded-lg border bg-[#F4F3F2] p-6 text-black shadow-lg duration-200", - className, - )} - {...props} - > - {children} - <DialogPrimitive.Close className="ring-offset-rgray-2 focus:ring-rgray-7 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 data-[state=open]:text-black"> - <X className="h-4 w-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-left", className)} - {...props} - /> -); -DialogHeader.displayName = "DialogHeader"; - -const DialogFooter = ({ - className, - ...props -}: React.HTMLAttributes<HTMLDivElement>) => ( - <div - className={cn( - "mt-5 flex flex-row 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( - "mb-1 text-xl font-medium 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-sm text-slate-800", className)} - {...props} - /> -)); -DialogDescription.displayName = DialogPrimitive.Description.displayName; - -export { - Dialog, - DialogPortal, - DialogOverlay, - DialogClose, - DialogTrigger, - DialogContent, - DialogHeader, - DialogFooter, - DialogTitle, - DialogDescription, -}; diff --git a/apps/web/src/components/ui/drawer.tsx b/apps/web/src/components/ui/drawer.tsx deleted file mode 100644 index 8ba01253..00000000 --- a/apps/web/src/components/ui/drawer.tsx +++ /dev/null @@ -1,124 +0,0 @@ -"use client"; - -import * as React from "react"; -import { Drawer as DrawerPrimitive } from "vaul"; - -import { cn } from "@/lib/utils"; - -const Drawer = ({ - shouldScaleBackground = true, - ...props -}: React.ComponentProps<typeof DrawerPrimitive.Root>) => ( - <DrawerPrimitive.Root - shouldScaleBackground={shouldScaleBackground} - {...props} - /> -); -Drawer.displayName = "Drawer"; - -const DrawerTrigger = DrawerPrimitive.Trigger; - -const DrawerPortal = DrawerPrimitive.Portal; - -const DrawerClose = DrawerPrimitive.Close; - -const DrawerOverlay = React.forwardRef< - React.ElementRef<typeof DrawerPrimitive.Overlay>, - React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay> ->(({ className, ...props }, ref) => ( - <DrawerPrimitive.Overlay - ref={ref} - data-drawer-overlay - className={cn("fixed inset-0 z-50 bg-black/80", className)} - {...props} - /> -)); -DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName; - -const DrawerContent = React.forwardRef< - React.ElementRef<typeof DrawerPrimitive.Content>, - React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content> & { - overlay?: boolean; - handle?: boolean; - } ->(({ className, children, overlay = true, handle = true, ...props }, ref) => ( - <DrawerPortal> - {overlay && <DrawerOverlay />} - <DrawerPrimitive.Content - ref={ref} - className={cn( - "border-rgray-6 bg-rgray-2 fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border", - className, - )} - {...props} - > - {handle && ( - <div className="bg-rgray-4 mx-auto mb-1 h-2 w-[100px] rounded-full " /> - )} - {children} - </DrawerPrimitive.Content> - </DrawerPortal> -)); -DrawerContent.displayName = "DrawerContent"; - -const DrawerHeader = ({ - className, - ...props -}: React.HTMLAttributes<HTMLDivElement>) => ( - <div - className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)} - {...props} - /> -); -DrawerHeader.displayName = "DrawerHeader"; - -const DrawerFooter = ({ - className, - ...props -}: React.HTMLAttributes<HTMLDivElement>) => ( - <div - className={cn("mt-auto flex flex-col gap-2 p-4", className)} - {...props} - /> -); -DrawerFooter.displayName = "DrawerFooter"; - -const DrawerTitle = React.forwardRef< - React.ElementRef<typeof DrawerPrimitive.Title>, - React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title> ->(({ className, ...props }, ref) => ( - <DrawerPrimitive.Title - ref={ref} - className={cn( - "text-rgray-12 text-xl font-medium leading-none tracking-tight", - className, - )} - {...props} - /> -)); -DrawerTitle.displayName = DrawerPrimitive.Title.displayName; - -const DrawerDescription = React.forwardRef< - React.ElementRef<typeof DrawerPrimitive.Description>, - React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description> ->(({ className, ...props }, ref) => ( - <DrawerPrimitive.Description - ref={ref} - className={cn("text-rgray-11 text-md", className)} - {...props} - /> -)); -DrawerDescription.displayName = DrawerPrimitive.Description.displayName; - -export { - Drawer, - DrawerPortal, - DrawerOverlay, - DrawerTrigger, - DrawerClose, - DrawerContent, - DrawerHeader, - DrawerFooter, - DrawerTitle, - DrawerDescription, -}; diff --git a/apps/web/src/components/ui/dropdown-menu.tsx b/apps/web/src/components/ui/dropdown-menu.tsx deleted file mode 100644 index fbe2d99c..00000000 --- a/apps/web/src/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( - "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-gray-100 data-[state=open]:bg-gray-100 dark:focus:bg-gray-800 dark:data-[state=open]:bg-gray-800", - inset && "pl-8", - className, - )} - {...props} - > - {children} - <ChevronRight className="ml-auto h-4 w-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( - "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 border-gray-200 bg-white p-1 text-gray-950 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( - "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 border-rgray-6 z-50 min-w-[9rem] overflow-hidden rounded-md border bg-white p-1 text-black 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( - "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-stone-200 focus:text-slate-800 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( - "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-gray-100 focus:text-gray-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-gray-800 dark:focus:text-gray-50", - className, - )} - checked={checked} - {...props} - > - <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> - <DropdownMenuPrimitive.ItemIndicator> - <Check className="h-4 w-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( - "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-gray-100 focus:text-gray-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-gray-800 dark:focus:text-gray-50", - className, - )} - {...props} - > - <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> - <DropdownMenuPrimitive.ItemIndicator> - <Circle className="h-2 w-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("-mx-1 my-1 h-px bg-gray-100", 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/apps/web/src/components/ui/input.tsx b/apps/web/src/components/ui/input.tsx deleted file mode 100644 index 9d925512..00000000 --- a/apps/web/src/components/ui/input.tsx +++ /dev/null @@ -1,53 +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-rgray-6 focus-visible:ring-rgray-7 flex h-10 w-full rounded-md border bg-transparent px-3 py-2 text-sm font-normal transition file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-black/50 focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-50 ", - className, - )} - ref={ref} - {...props} - /> - ); - }, -); - -export interface InputWithIconProps - extends React.InputHTMLAttributes<HTMLInputElement> { - icon: React.ReactNode; -} - -const InputWithIcon = React.forwardRef<HTMLInputElement, InputWithIconProps>( - ({ className, type, icon, ...props }, ref) => { - return ( - <div - className={cn( - "border-rgray-1/70 text-rgray-11 focus-within:ring-rgray-7 flex h-10 w-full items-center justify-center gap-2 rounded-md border bg-transparent px-3 py-2 text-sm font-normal transition focus-within:outline-none focus-within:ring-2 ", // TODO: change to black - className, - )} - > - {icon} - <input - type={type} - className={ - "w-full bg-transparent font-normal file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-black/50 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50" - } - ref={ref} - {...props} - /> - </div> - ); - }, -); -InputWithIcon.displayName = "Input"; - -export { Input, InputWithIcon }; diff --git a/apps/web/src/components/ui/label.tsx b/apps/web/src/components/ui/label.tsx deleted file mode 100644 index 84f8b0c7..00000000 --- a/apps/web/src/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/apps/web/src/components/ui/popover.tsx b/apps/web/src/components/ui/popover.tsx deleted file mode 100644 index cabe76a9..00000000 --- a/apps/web/src/components/ui/popover.tsx +++ /dev/null @@ -1,40 +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> & { - animate?: boolean; - } ->( - ( - { className, align = "center", animate = true, sideOffset = 4, ...props }, - ref, - ) => ( - <PopoverPrimitive.Portal> - <PopoverPrimitive.Content - ref={ref} - align={align} - sideOffset={sideOffset} - className={cn( - "border-rgray-6 bg-rgray-3 text-rgray-11 z-50 w-72 rounded-md border p-4 shadow-md outline-none", - animate && - "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", - className, - )} - {...props} - /> - </PopoverPrimitive.Portal> - ), -); -PopoverContent.displayName = PopoverPrimitive.Content.displayName; - -export { Popover, PopoverTrigger, PopoverContent }; diff --git a/apps/web/src/components/ui/textarea.tsx b/apps/web/src/components/ui/textarea.tsx deleted file mode 100644 index 3b2c9ddd..00000000 --- a/apps/web/src/components/ui/textarea.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import * as React from "react"; - -import { cn } from "@/lib/utils"; -import { HTMLMotionProps, motion } from "framer-motion"; - -export interface TextareaProps - extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {} - -const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>( - ({ className, ...props }, ref) => { - return ( - <textarea - className={cn( - "border-rgray-6 text-rgray-11 placeholder:text-rgray-11/70 focus-within:ring-rgray-7 flex min-h-[80px] w-full rounded-md border bg-transparent px-3 py-2 text-sm font-normal transition focus-within:outline-none focus-within:ring-2 disabled:cursor-not-allowed disabled:opacity-50", - className, - )} - ref={ref} - {...props} - /> - ); - }, -); -Textarea.displayName = "Textarea"; - -export interface Textarea2Props extends HTMLMotionProps<"div"> { - textAreaProps?: TextareaProps; - children: React.ReactNode | React.ReactNode[]; -} - -const Textarea2 = React.forwardRef<HTMLDivElement, Textarea2Props>( - ({ className, children, textAreaProps: _textAreaProps, ...props }, ref) => { - const { className: textAreaClassName, ...textAreaProps } = - _textAreaProps || {}; - return ( - <motion.div - ref={ref} - className={cn( - "border-rgray-6 text-rgray-11 has-[textarea:focus-visible]:ring-rgray-7 flex h-auto min-h-[80px] w-full flex-col items-start justify-center rounded-md border bg-transparent px-3 py-2 text-sm transition has-[textarea:focus-visible]:ring-2", - className, - )} - {...props} - > - <textarea - className={cn( - "text-rgray-11 h-full w-full resize-none bg-transparent placeholder:text-white/50 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50", - textAreaClassName, - )} - {...textAreaProps} - /> - {children} - </motion.div> - ); - }, -); -Textarea2.displayName = "Textarea2"; - -export { Textarea, Textarea2 }; diff --git a/apps/web/src/contexts/MemoryContext.tsx b/apps/web/src/contexts/MemoryContext.tsx deleted file mode 100644 index 09412465..00000000 --- a/apps/web/src/contexts/MemoryContext.tsx +++ /dev/null @@ -1,241 +0,0 @@ -"use client"; -import React, { useCallback } from "react"; -import { - ChachedSpaceContent, - StoredContent, - storedContent, - StoredSpace, -} from "@/server/db/schema"; -import { - addMemory, - searchMemoriesAndSpaces, - addSpace, - fetchContentForSpace, - deleteSpace, - deleteMemory, - fetchFreeMemories, - updateMemory, - updateSpaceTitle, - addContentInSpaces, -} from "@/actions/db"; -import { User } from "next-auth"; - -export type SearchResult = { - type: "memory" | "space"; - space: StoredSpace; - memory: StoredContent; -}; - -// temperory (will change) -export const MemoryContext = React.createContext<{ - spaces: StoredSpace[]; - freeMemories: StoredContent[]; - addSpace: typeof addSpace; - addMemory: typeof addMemory; - cachedMemories: ChachedSpaceContent[]; - search: typeof searchMemoriesAndSpaces; - deleteSpace: typeof deleteSpace; - deleteMemory: typeof deleteMemory; - updateMemory: typeof updateMemory; - updateSpace: typeof updateSpaceTitle; - addMemoriesToSpace: typeof addContentInSpaces; -}>({ - spaces: [], - freeMemories: [], - addMemory: (() => {}) as unknown as typeof addMemory, - addSpace: (async () => {}) as unknown as typeof addSpace, - cachedMemories: [], - search: async () => [], - deleteMemory: (() => {}) as unknown as typeof deleteMemory, - deleteSpace: (() => {}) as unknown as typeof deleteSpace, - updateMemory: (() => {}) as unknown as typeof updateMemory, - updateSpace: (() => {}) as unknown as typeof updateSpaceTitle, - addMemoriesToSpace: (() => {}) as unknown as typeof addContentInSpaces, -}); - -export const MemoryProvider: React.FC< - { - spaces: StoredSpace[]; - freeMemories: StoredContent[]; - cachedMemories: ChachedSpaceContent[]; - user: User; - } & React.PropsWithChildren -> = ({ - children, - user, - spaces: initalSpaces, - freeMemories: initialFreeMemories, - cachedMemories: initialCachedMemories, -}) => { - const [spaces, setSpaces] = React.useState<StoredSpace[]>(initalSpaces); - const [freeMemories, setFreeMemories] = - React.useState<StoredContent[]>(initialFreeMemories); - - const [cachedMemories, setCachedMemories] = React.useState< - ChachedSpaceContent[] - >(initialCachedMemories); - - const _deleteSpace: typeof deleteSpace = async (...params) => { - const deleted = (await deleteSpace(...params))!; - - setSpaces((prev) => prev.filter((i) => i.id !== deleted.id)); - setCachedMemories((prev) => prev.filter((i) => i.space !== deleted.id)); - - setFreeMemories(await fetchFreeMemories()); - - return deleted; - }; - - const _deleteMemory: typeof deleteMemory = async (...params) => { - const deleted = (await deleteMemory(...params))!; - - setCachedMemories((prev) => prev.filter((i) => i.id !== deleted.id)); - setFreeMemories(await fetchFreeMemories()); - - return deleted; - }; - - // const fetchMemories = useCallback(async (query: string) => { - // const response = await fetch(`/api/memories?${query}`); - // }, []); - - const _addSpace: typeof addSpace = async (...params) => { - const { space: addedSpace, addedMemories } = (await addSpace(...params))!; - - setSpaces((prev) => [...prev, addedSpace]); - const cachedMemories = ( - (await fetchContentForSpace(addedSpace.id, { - offset: 0, - limit: 3, - })) ?? [] - ).map((m) => ({ ...m, space: addedSpace.id })); - - setCachedMemories((prev) => [...prev, ...cachedMemories]); - - setFreeMemories(await fetchFreeMemories()); - - return { - space: addedSpace, - addedMemories, - }; - }; - - const _addMemory: typeof addMemory = async (...params) => { - const { memory: addedMemory, addedToSpaces } = (await addMemory( - ...params, - ))!; - - addedToSpaces.length > 0 - ? setCachedMemories((prev) => [ - ...prev, - ...addedToSpaces.map((s) => ({ - ...addedMemory, - space: s.spaceId, - })), - ]) - : setFreeMemories((prev) => [...prev, addedMemory]); - - return { - memory: addedMemory, - addedToSpaces, - }; - }; - - const _updateMemory: typeof updateMemory = async (id, _data) => { - const data = await updateMemory(id, _data); - - let contents: ChachedSpaceContent[] = []; - - await Promise.all([ - spaces.forEach(async (space) => { - console.log("fetching "); - const data = ( - (await fetchContentForSpace(space.id, { - offset: 0, - limit: 3, - })) ?? [] - ).map((data) => ({ - ...data, - space: space.id, - })); - contents = [...contents, ...data]; - }), - ]); - - const freeMemories = await fetchFreeMemories(); - - setCachedMemories(contents); - setFreeMemories(freeMemories); - - return data; - }; - - const _updateSpace: typeof updateSpaceTitle = async (...params) => { - const updatedSpace = await updateSpaceTitle(...params); - - if (updatedSpace) { - setSpaces((prev) => - prev.map((i) => (i.id === updatedSpace.id ? updatedSpace : i)), - ); - } - - return updatedSpace; - }; - - const addMemoriesToSpace: typeof addContentInSpaces = async (...params) => { - const data = await addContentInSpaces(...params); - - let contents: ChachedSpaceContent[] = []; - - await Promise.all([ - spaces.forEach(async (space) => { - console.log("fetching "); - const data = ( - (await fetchContentForSpace(space.id, { - offset: 0, - limit: 3, - })) ?? [] - ).map((data) => ({ - ...data, - space: space.id, - })); - contents = [...contents, ...data]; - }), - ]); - - const freeMemories = await fetchFreeMemories(); - - setCachedMemories(contents); - setFreeMemories(freeMemories); - - return data; - }; - - return ( - <MemoryContext.Provider - value={{ - updateSpace: _updateSpace, - search: searchMemoriesAndSpaces, - spaces, - addSpace: _addSpace, - deleteSpace: _deleteSpace, - freeMemories, - cachedMemories, - deleteMemory: _deleteMemory, - addMemory: _addMemory, - updateMemory: _updateMemory, - addMemoriesToSpace, - }} - > - {children} - </MemoryContext.Provider> - ); -}; - -export const useMemory = () => { - const context = React.useContext(MemoryContext); - if (context === undefined) { - throw new Error("useMemory must be used within a MemoryProvider"); - } - return context; -}; diff --git a/apps/web/src/env.js b/apps/web/src/env.js deleted file mode 100644 index 2495d75b..00000000 --- a/apps/web/src/env.js +++ /dev/null @@ -1,59 +0,0 @@ -import { createEnv } from "@t3-oss/env-nextjs"; -import { z } from "zod"; - -export const env = createEnv({ - /** - * Specify your server-side environment variables schema here. This way you can ensure the app - * isn't built with invalid env vars. - */ - server: { - NODE_ENV: z - .enum(["development", "test", "production"]) - .default("development"), - NEXTAUTH_SECRET: - process.env.NODE_ENV === "production" ? z.string() : z.string(), - NEXTAUTH_URL: z.preprocess( - // This makes Vercel deployments not fail if you don't set NEXTAUTH_URL - // Since NextAuth.js automatically uses the VERCEL_URL if present. - (str) => process.env.VERCEL_URL ?? str, - // VERCEL_URL doesn't include `https` so it cant be validated as a URL - process.env.VERCEL ? z.string() : z.string().url(), - ), - GOOGLE_CLIENT_ID: z.string(), - GOOGLE_CLIENT_SECRET: z.string(), - BACKEND_SECURITY_KEY: z.string(), - }, - - /** - * Specify your client-side environment variables schema here. This way you can ensure the app - * isn't built with invalid env vars. To expose them to the client, prefix them with - * `NEXT_PUBLIC_`. - */ - client: { - // NEXT_PUBLIC_CLIENTVAR: z.string(), - }, - - /** - * You can't destruct `process.env` as a regular object in the Next.js edge runtimes (e.g. - * middlewares) or client-side so we need to destruct manually. - */ - runtimeEnv: { - DATABASE_URL: process.env.DATABASE_URL, - NODE_ENV: process.env.NODE_ENV, - NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET, - NEXTAUTH_URL: process.env.NEXTAUTH_URL, - GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID, - GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET, - BACKEND_SECURITY_KEY: process.env.BACKEND_SECURITY_KEY, - }, - /** - * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially - * useful for Docker builds. - */ - skipValidation: !!process.env.SKIP_ENV_VALIDATION, - /** - * Makes it so that empty strings are treated as undefined. `SOME_VAR: z.string()` and - * `SOME_VAR=''` will throw an error. - */ - emptyStringAsUndefined: true, -}); diff --git a/apps/web/src/hooks/useDebounce.ts b/apps/web/src/hooks/useDebounce.ts deleted file mode 100644 index d133b1ae..00000000 --- a/apps/web/src/hooks/useDebounce.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { useEffect, useState } from "react"; - -/** - * Use this hook when you need to debounce a value. - * @param value - * @param delay in milliseconds - */ -export const useDebounce = <T>(value: T, delay: number) => { - // State and setters for debounced value - const [debouncedValue, setDebouncedValue] = useState<T>(value); - - useEffect(() => { - // Update debounced value after delay - const handler = setTimeout(() => { - setDebouncedValue(value); - }, delay); - - // Cancel the timeout if value changes (also on delay change or unmount) - // This is how we prevent debounced value from updating if the value is changed - // within the delay period. Timeout gets cleared and restarted. - return () => { - clearTimeout(handler); - }; - }, [value, delay]); - - return debouncedValue; -}; diff --git a/apps/web/src/hooks/useTouchHold.ts b/apps/web/src/hooks/useTouchHold.ts deleted file mode 100644 index 52e56491..00000000 --- a/apps/web/src/hooks/useTouchHold.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { useState } from "react"; - -// holdDuration (in ms) -const useTouchHold = ({ - onHold, - holdDuration = 500, -}: { - holdDuration?: number; - onHold: () => Promise<void> | void; -}) => { - const [touchTimeout, setTouchTimeout] = useState<ReturnType< - typeof setTimeout - > | null>(null); - - return { - onTouchStart: () => { - setTouchTimeout(setTimeout(onHold, holdDuration)); - }, - onTouchEnd: () => { - if (touchTimeout) { - clearTimeout(touchTimeout); - } - }, - }; -}; - -export default useTouchHold; diff --git a/apps/web/src/hooks/useViewport.ts b/apps/web/src/hooks/useViewport.ts deleted file mode 100644 index 7ba90861..00000000 --- a/apps/web/src/hooks/useViewport.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { useState, useEffect } from "react"; - -function getViewport() { - try { - const { innerWidth: width, innerHeight: height } = window ?? { - innerWidth: 0, - innerHeight: 0, - }; - return { - width, - height, - }; - } catch { - return { - width: 0, - height: 0, - }; - } -} - -export default function useViewport() { - const [viewport, setViewport] = useState(getViewport()); - - useEffect(() => { - function handleResize() { - setViewport(getViewport()); - } - - window.addEventListener("resize", handleResize); - return () => window.removeEventListener("resize", handleResize); - }, []); - - return viewport; -} diff --git a/apps/web/src/lib/icons.tsx b/apps/web/src/lib/icons.tsx deleted file mode 100644 index 51a4714e..00000000 --- a/apps/web/src/lib/icons.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import * as React from "react"; -import type { SVGProps } from "react"; -const Chrome = (props: SVGProps<SVGSVGElement>) => ( - <svg - xmlns="http://www.w3.org/2000/svg" - preserveAspectRatio="xMidYMid" - viewBox="0 0 190.5 190.5" - width="1em" - height="1em" - {...props} - > - <path - fill="#fff" - d="M95.252 142.873c26.304 0 47.627-21.324 47.627-47.628s-21.323-47.628-47.627-47.628-47.627 21.324-47.627 47.628 21.323 47.628 47.627 47.628z" - /> - <path - fill="#229342" - d="m54.005 119.07-41.24-71.43a95.227 95.227 0 0 0-.003 95.25 95.234 95.234 0 0 0 82.496 47.61l41.24-71.43v-.011a47.613 47.613 0 0 1-17.428 17.443 47.62 47.62 0 0 1-47.632.007 47.62 47.62 0 0 1-17.433-17.437z" - /> - <path - fill="#fbc116" - d="m136.495 119.067-41.239 71.43a95.229 95.229 0 0 0 82.489-47.622A95.24 95.24 0 0 0 190.5 95.248a95.237 95.237 0 0 0-12.772-47.623H95.249l-.01.007a47.62 47.62 0 0 1 23.819 6.372 47.618 47.618 0 0 1 17.439 17.431 47.62 47.62 0 0 1-.001 47.633z" - /> - <path - fill="#1a73e8" - d="M95.252 132.961c20.824 0 37.705-16.881 37.705-37.706S116.076 57.55 95.252 57.55 57.547 74.431 57.547 95.255s16.881 37.706 37.705 37.706z" - /> - <path - fill="#e33b2e" - d="M95.252 47.628h82.479A95.237 95.237 0 0 0 142.87 12.76 95.23 95.23 0 0 0 95.245 0a95.222 95.222 0 0 0-47.623 12.767 95.23 95.23 0 0 0-34.856 34.872l41.24 71.43.011.006a47.62 47.62 0 0 1-.015-47.633 47.61 47.61 0 0 1 41.252-23.815z" - /> - </svg> -); -export default Chrome; diff --git a/apps/web/src/lib/searchParams.ts b/apps/web/src/lib/searchParams.ts deleted file mode 100644 index aae3f4c7..00000000 --- a/apps/web/src/lib/searchParams.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { - createSearchParamsCache, - parseAsInteger, - parseAsString, -} from "nuqs/server"; -// Note: import from 'nuqs/server' to avoid the "use client" directive - -export const searchParamsCache = createSearchParamsCache({ - // List your search param keys and associated parsers here: - q: parseAsString.withDefault(""), - maxResults: parseAsInteger.withDefault(10), -}); diff --git a/apps/web/src/lib/utils.ts b/apps/web/src/lib/utils.ts deleted file mode 100644 index 81fa8549..00000000 --- a/apps/web/src/lib/utils.ts +++ /dev/null @@ -1,111 +0,0 @@ -"use client"; -import { type ClassValue, clsx } from "clsx"; -import { twMerge } from "tailwind-merge"; - -export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)); -} - -// removes http(s?):// and / from the url -export function cleanUrl(url: string) { - if (url.endsWith("/")) { - url = url.slice(0, -1); - } - return url.startsWith("https://") - ? url.slice(8) - : url.startsWith("http://") - ? url.slice(7) - : url; -} - -export function getIdsFromSource(sourceIds: string[]) { - console.log(sourceIds); - return sourceIds.map((id) => { - const parts = id.split("-"); - if (parts.length > 1) { - return parts.slice(0, -1).join("-"); - } else { - return id; - } - }); -} - -export function generateId() { - return Math.random().toString(36).slice(2, 9); -} - -export function svgId(prefix: string, id: string) { - return `${prefix}-${id}`; -} - -export function countLines(textarea: HTMLTextAreaElement): number { - let _buffer: HTMLTextAreaElement | null = null; - - if (_buffer == null) { - _buffer = document.createElement("textarea"); - _buffer.style.border = "none"; - _buffer.style.height = "0"; - _buffer.style.overflow = "hidden"; - _buffer.style.padding = "0"; - _buffer.style.position = "absolute"; - _buffer.style.left = "0"; - _buffer.style.top = "0"; - _buffer.style.zIndex = "-1"; - document.body.appendChild(_buffer); - } - - const cs = window.getComputedStyle(textarea); - const pl = parseInt(cs.paddingLeft as string); - const pr = parseInt(cs.paddingRight as string); - let lh = parseInt(cs.lineHeight as string); - - // [cs.lineHeight] may return 'normal', which means line height = font size. - if (isNaN(lh)) lh = parseInt(cs.fontSize as string); - - // Copy content width. - if (_buffer) { - _buffer.style.width = textarea.clientWidth - pl - pr + "px"; - - // Copy text properties. - _buffer.style.font = cs.font as string; - _buffer.style.letterSpacing = cs.letterSpacing as string; - _buffer.style.whiteSpace = cs.whiteSpace as string; - _buffer.style.wordBreak = cs.wordBreak as string; - _buffer.style.wordSpacing = cs.wordSpacing as string; - _buffer.style.wordWrap = cs.wordWrap as string; - - // Copy value. - _buffer.value = textarea.value; - - const result = Math.floor(_buffer.scrollHeight / lh); - return result > 0 ? result : 1; - } - - return 0; -} - -export function convertRemToPixels(rem: number) { - return rem * parseFloat(getComputedStyle(document.documentElement).fontSize); -} - -export function isArraysEqual(a: any[], b: any[]) { - if (a === b) return true; - if (a == null || b == null) return false; - if (a.length !== b.length) return false; - - let isEqual = true; - - a.forEach((i) => { - if (!isEqual) return; - isEqual = b.includes(i); - }); - - if (!isEqual) return isEqual; - - b.forEach((i) => { - if (!isEqual) return; - isEqual = a.includes(i); - }); - - return isEqual; -} diff --git a/apps/web/src/server/db/test.ts b/apps/web/src/server/db/test.ts deleted file mode 100644 index 37969e5e..00000000 --- a/apps/web/src/server/db/test.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { db } from "."; -import { space, user } from "./schema"; - -const user = await db.select(user).all(); - -await db.insert(space).values([{}]); diff --git a/apps/web/src/server/helpers.ts b/apps/web/src/server/helpers.ts deleted file mode 100644 index 9a9a9607..00000000 --- a/apps/web/src/server/helpers.ts +++ /dev/null @@ -1,39 +0,0 @@ -"use server"; -import * as cheerio from "cheerio"; - -export async function getMetaData(url: string) { - const response = await fetch(url); - const html = await response.text(); - - const $ = cheerio.load(html); - - // Extract the base URL - const baseUrl = new URL(url).origin; - - // Extract title - const title = $("title").text().trim(); - - const description = $("meta[name=description]").attr("content") ?? ""; - - const _favicon = - $("link[rel=icon]").attr("href") ?? "https://supermemory.dhr.wtf/web.svg"; - - let favicon = - _favicon.trim().length > 0 - ? _favicon.trim() - : "https://supermemory.dhr.wtf/web.svg"; - if (favicon.startsWith("/")) { - favicon = baseUrl + favicon; - } else if (favicon.startsWith("./")) { - favicon = baseUrl + favicon.slice(1); - } - - // Prepare the metadata object - const metadata = { - title, - description, - image: favicon, - baseUrl, - }; - return metadata; -} diff --git a/apps/web/tailwind.config.ts b/apps/web/tailwind.config.ts index 5d61bd62..cf1434cf 100644 --- a/apps/web/tailwind.config.ts +++ b/apps/web/tailwind.config.ts @@ -1,99 +1 @@ -import type { Config } from "tailwindcss"; -import plugin from "tailwindcss/plugin"; - -const config: Config = { - content: ["./src/**/*.{js,ts,jsx,tsx,mdx}"], - theme: { - extend: { - keyframes: { - "scale-in": { - "0%": { - transform: "scale(0)", - transformOrigin: "var(--radix-popover-content-transform-origin)", - }, - "100%": { - transform: "scale(1)", - transformOrigin: "var(--radix-popover-content-transform-origin)", - }, - }, - "scale-out": { - "0%": { - transform: "scale(1)", - transformOrigin: "var(--radix-popover-content-transform-origin)", - }, - "100%": { - transform: "scale(0)", - transformOrigin: "var(--radix-popover-content-transform-origin)", - }, - }, - "fade-in": { - "0%": { - opacity: "0", - }, - "100%": { - opacity: "1", - }, - }, - "from-top": { - "0%": { - transform: "translateY(-50px)", - opacity: "0", - }, - "100%": { - transform: "translateY(0)", - opacity: "1", - }, - }, - "input-error": { - "0%": { - color: "var(--gray-11)", - }, - "50%": { - color: "red", - }, - "100%": { - color: "var(--gray-11)", - }, - }, - }, - animation: { - "scale-in": "scale-in 0.2s cubic-bezier(0.16, 1, 0.3, 1)", - "scale-out": "scale-out 0.2s cubic-bezier(0.16, 1, 0.3, 1)", - "fade-in": "fade-in 0.2s 0.5s forwards cubic-bezier(0.16, 1, 0.3, 1)", - "from-top": "from-top 0.2s ease-in-out", - "input-error": "input-error 5s", - }, - backgroundImage: { - "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", - "gradient-conic": - "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", - }, - colors: { - rgray: { - 1: "var(--gray-1)", - 2: "var(--gray-2)", - 3: "var(--gray-3)", - 4: "var(--gray-4)", - 5: "var(--gray-5)", - 6: "var(--gray-6)", - 7: "var(--gray-7)", - 8: "var(--gray-8)", - 9: "var(--gray-9)", - 10: "var(--gray-10)", - 11: "var(--gray-11)", - 12: "var(--gray-12)", - }, - }, - }, - }, - safelist: ["pb-[20vh]", "pb-5", "h-[2em]", "max-h-[2em]", "p-0"], - darkMode: "class", - plugins: [ - require("tailwindcss-animate"), - plugin(function ({ addVariant }) { - addVariant("on", "&[data-state-on='true']"); - }), - require("tailwind-scrollbar"), - ], -}; -export default config; +module.exports = require("@repo/tailwind-config/tailwind.config"); diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index 7b285893..e5590d6b 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -1,26 +1,19 @@ { + "extends": "@repo/typescript-config/nextjs.json", "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, "plugins": [ { "name": "next" } - ], - "paths": { - "@/*": ["./src/*"] - } + ] }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] + "include": [ + "cf-env.d.ts", + "env.d.ts", + "next.config.mjs", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], + "exclude": ["node_modules/"] } diff --git a/apps/web/types/memory.tsx b/apps/web/types/memory.tsx deleted file mode 100644 index 80ed5755..00000000 --- a/apps/web/types/memory.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { db } from "@/server/db"; -import { StoredContent } from "@/server/db/schema"; - -export type CollectedSpaces = { - id: number; - title: string; - content: StoredContent[]; -}; - -export type ChatHistory = { - question: string; - answer: { - parts: { text: string }[]; - sources: { isNote: boolean; source: string }[]; - }; -}; diff --git a/apps/web/wrangler.toml b/apps/web/wrangler.toml index 88e58512..e651069e 100644 --- a/apps/web/wrangler.toml +++ b/apps/web/wrangler.toml @@ -1,19 +1,24 @@ -name = "web" +name = "cloudflare-saas-starter" compatibility_date = "2024-03-29" -compatibility_flags = ["nodejs_compat"] +compatibility_flags = [ "nodejs_compat" ] +pages_build_output_dir = ".vercel/output/static" + +[placement] +mode = "smart" + +[[r2_buckets]] +binding = "STORAGE" +bucket_name = "dev-r2-anycontext" [[d1_databases]] binding = "DATABASE" database_name = "dev-d1-anycontext" database_id = "fc562605-157a-4f60-b439-2a24ffed5b4c" -[[unsafe.bindings]] -name = "RATELIMITER" -type = "ratelimit" -namespace_id = "1001" +# [[unsafe.bindings]] +# name = "RATELIMITER" +# type = "ratelimit" +# namespace_id = "1001" -# 25 requests per 10 seconds -simple = { limit = 25, period = 10 } - -[placement] -mode = "smart"
\ No newline at end of file +# # 25 requests per 10 seconds +# simple = { limit = 25, period = 10 }
\ No newline at end of file diff --git a/package.json b/package.json index 0a68d234..32f0b528 100644 --- a/package.json +++ b/package.json @@ -1,92 +1,80 @@ { - "name": "anycontext", - "version": "0.1.0", + "name": "cloudflare-saas-starter", "private": true, "type": "module", - "workspaces": [ - "apps/*", - "packages/*" - ], - "packageManager": "[email protected]", "scripts": { - "build": "dotenv -- turbo run build", - "dev": "dotenv -- turbo run dev --filter=!cf-ai-backend", - "lint": "dotenv -- turbo run lint", - "start": "dotenv -- turbo run start" - }, - "lint-staged": { - "**/*": "prettier --write --ignore-unknown" + "build": "turbo build", + "dev": "turbo dev", + "lint": "turbo lint", + "format": "prettier --write \"**/*.{ts,tsx,md}\"", + "deploy": "turbo deploy" }, "devDependencies": { - "@types/better-sqlite3": "^7.6.6", - "@types/eslint": "^8.44.7", - "@types/node": "^18.17.0", - "@types/react": "^18.2.37", - "@types/react-dom": "^18.2.15", - "@typescript-eslint/eslint-plugin": "^6.11.0", - "@typescript-eslint/parser": "^6.11.0", - "async-hook-jl": "^1.7.6", - "autoprefixer": "^10.4.17", - "drizzle-kit": "beta", - "esbuild-jest": "^0.5.0", - "eslint": "^8.54.0", - "eslint-config-next": "^14.0.4", - "if-env": "^1.0.4", - "jest": "^29.7.0", - "jest-environment-miniflare": "^2.14.2", - "postcss": "^8.4.35", - "prettier": "^3.1.0", - "prettier-plugin-tailwindcss": "^0.5.7", - "tailwindcss": "^3.4.1", - "ts-loader": "^9.5.1", - "typescript": "^5.1.6", - "webpack-cli": "^5.1.4" + "@clack/prompts": "^0.7.0", + "@cloudflare/next-on-pages": "1", + "@cloudflare/workers-types": "^4.20240512.0", + "@repo/eslint-config": "*", + "@repo/shared-types": "*", + "@repo/tailwind-config": "*", + "@repo/typescript-config": "*", + "@tailwindcss/typography": "^0.5.13", + "autoprefixer": "^10.4.19", + "drizzle-kit": "^0.21.2", + "eslint-plugin-next-on-pages": "^1.11.3", + "lint-staged": "^15.2.5", + "postcss": "^8.4.38", + "prettier": "^3.2.5", + "readline-sync": "^1.4.10", + "tailwindcss": "^3.4.3", + "tailwindcss-animate": "^1.0.7", + "turbo": "latest", + "vercel": "^34.2.0" + }, + "engines": { + "node": ">=18" }, + "packageManager": "[email protected]", + "workspaces": [ + "apps/*", + "packages/*" + ], "dependencies": { - "@ai-sdk/anthropic": "^0.0.13", - "@ai-sdk/google": "^0.0.10", - "@ai-sdk/openai": "^0.0.10", - "@auth/d1-adapter": "^0.6.0", - "@auth/drizzle-adapter": "^0.7.0", - "@cloudflare/ai": "^1.0.52", - "@cloudflare/next-on-pages-next-dev": "^0.0.1", - "@cloudflare/puppeteer": "^0.0.6", - "@crxjs/vite-plugin": "^1.0.14", - "@google/generative-ai": "^0.3.1", - "@headlessui/react": "^2.0.3", - "@heroicons/react": "^2.1.1", - "@langchain/cloudflare": "^0.0.3", - "@radix-ui/colors": "^3.0.0", - "@radix-ui/react-dropdown-menu": "^2.0.6", + "@ai-sdk/anthropic": "^0.0.15", + "@ai-sdk/google": "^0.0.15", + "@ai-sdk/openai": "^0.0.14", + "@auth/drizzle-adapter": "^1.1.0", + "@aws-sdk/client-s3": "^3.577.0", + "@aws-sdk/s3-request-presigner": "^3.577.0", + "@cloudflare/puppeteer": "^0.0.8", + "@headlessui/react": "^2.0.4", + "@hookform/resolvers": "^3.4.2", + "@iarna/toml": "^2.2.5", + "@langchain/cloudflare": "^0.0.6", + "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.0.2", - "@t3-oss/env-nextjs": "^0.9.2", - "@types/chrome": "^0.0.261", - "ai": "^3.1.5", - "better-sqlite3": "^9.4.3", - "class-variance-authority": "^0.7.0", - "cloudflare": "^3.0.0", + "@radix-ui/react-progress": "^1.0.3", + "@radix-ui/react-scroll-area": "^1.0.5", + "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-toast": "^1.1.5", + "@types/readline-sync": "^1.4.8", + "ai": "^3.1.14", "compromise": "^14.13.0", - "crypto": "^1.0.1", - "dotenv-cli": "^7.3.0", - "drizzle-orm": "^0.29.4", - "eslint-plugin-next-on-pages": "^1.9.0", - "framer-motion": "^11.2.4", - "hono": "^4.3.7", - "honox": "^0.1.17", - "html-metadata": "^1.7.1", - "html-metadata-parser": "^2.0.4", - "husky": "^9.0.11", - "lint-staged": "^15.2.2", - "lucide-react": "^0.343.0", - "next-auth": "beta", - "nuqs": "^1.17.1", + "drizzle-orm": "^0.30.10", + "framer-motion": "^11.2.6", + "next-app-theme": "^0.1.10", + "next-auth": "^5.0.0-beta.18", "random-js": "^2.1.0", - "react-markdown": "^9.0.1", - "react-tweet": "^3.2.1", - "remark-gfm": "^4.0.0", - "tailwind-scrollbar": "^3.1.0", - "tailwindcss-animate": "^1.0.7", - "wrangler": "^3.57.0", + "react-dropzone": "^14.2.3", + "react-hook-form": "^7.51.5", + "sonner": "^1.4.41", + "uploadthing": "^6.10.4", "zod": "^3.23.8" + }, + "trustedDependencies": [ + "core-js-pure", + "es5-ext" + ], + "lint-staged": { + "**/*": "prettier --write --ignore-unknown" } } diff --git a/packages/tsconfig/base.json b/packages/tsconfig/base.json deleted file mode 100644 index 3ef2dcf7..00000000 --- a/packages/tsconfig/base.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "compilerOptions": { - "esModuleInterop": true, - "skipLibCheck": true, - "target": "es2022", - "allowJs": true, - "resolveJsonModule": true, - "moduleDetection": "force", - "isolatedModules": true, - "strict": true, - "noUncheckedIndexedAccess": true, - "checkJs": true, - "lib": ["dom", "dom.iterable", "ES2022"], - "noEmit": true, - "module": "ESNext", - "moduleResolution": "Bundler", - "jsx": "preserve", - "plugins": [ - { - "name": "next" - } - ], - "incremental": true, - "baseUrl": "." - }, - "include": [ - "../.eslintrc.cjs", - "next-env.d.ts", - "**/*.ts", - "**/*.tsx", - "**/*.cjs", - "**/*.js", - ".next/types/**/*.ts", - "../tailwind.config.ts", - "../prettier.config.js" - ], - "exclude": ["node_modules"] -} diff --git a/packages/tsconfig/package.json b/packages/tsconfig/package.json deleted file mode 100644 index f4735add..00000000 --- a/packages/tsconfig/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "name": "tsconfig" -} diff --git a/prettier.config.js b/prettier.config.js deleted file mode 100644 index b2d59b46..00000000 --- a/prettier.config.js +++ /dev/null @@ -1,6 +0,0 @@ -/** @type {import('prettier').Config & import('prettier-plugin-tailwindcss').PluginOptions} */ -const config = { - plugins: ["prettier-plugin-tailwindcss"], -}; - -export default config; @@ -1,22 +1,26 @@ { "$schema": "https://turbo.build/schema.json", + "globalDependencies": ["**/.env.*local"], "pipeline": { "build": { + "dependsOn": ["^build"], "outputs": [".next/**", "!.next/cache/**"] }, - "deploy": { - "dependsOn": ["build", "test", "lint"] + "lint": { + "dependsOn": ["^lint"] }, - "db:push": {}, - "db:studio": {}, - "test": { - "dependsOn": ["build"], - "inputs": ["src/**/*.tsx", "src/**/*.ts", "test/**/*.ts", "test/**/*.tsx"] - }, - "lint": {}, - "type-check": {}, + "deploy": {}, "dev": { - "env": ["NODE_ENV=development"], + "env": [ + "NODE_ENV=development", + "GOOGLE_CLIENT_ID", + "GOOGLE_CLIENT_SECRET", + "NEXTAUTH_SECRET", + "R2_ENDPOINT", + "R2_ACCESS_ID", + "R2_SECRET_KEY", + "R2_BUCKET_NAME" + ], "cache": false, "persistent": true } |