refactored UI, with shared components and UI, better rules and million lint

This commit is contained in:
Dhravya 2024-05-25 18:41:26 -05:00
parent d5218abe25
commit 075f45986f
218 changed files with 1741 additions and 12469 deletions

View file

@ -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;

67
.gitignore vendored
View file

@ -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
# misc
.DS_Store
*.pem
# Build Outputs
.next/
out/
build
dist
# 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

6
.gitmodules vendored
View file

@ -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

View file

@ -1 +0,0 @@
**/.gitignore

21
LICENSE
View file

@ -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.

View file

@ -1,90 +0,0 @@
![og image](https://supermemory.dhr.wtf/og-image.png)
# 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 :)
![chrome](https://i.dhr.wtf/r/Clipboard_Apr_15,_2024_at_10.47AM.png)
## 👨‍💻 The Stack
![overview](https://i.dhr.wtf/r/Clipboard_Apr_14,_2024_at_4.52PM.png)
Supermemory has three main modules, managed by [turborepo](https://turbo.build):
#### `apps/web`: The main web UI.
The database, auth etc logic is here
![App preview](https://i.dhr.wtf/r/Clipboard_Apr_14,_2024_at_4.10PM.png)
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.
![Chrome extension preview](https://i.dhr.wtf/r/Clipboard_Apr_14,_2024_at_3.54PM.png)
You can also use it to import all your twitter bookmarks!
![Import bookmarks](https://i.dhr.wtf/r/Clipboard_Apr_14,_2024_at_3.56PM.png)
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.

View file

@ -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.

View file

@ -1,7 +0,0 @@
module.exports = {
testEnvironment: "miniflare",
testMatch: ["**/test/**/*.+(ts|tsx)", "**/src/**/(*.)+(spec|test).+(ts|tsx)"],
transform: {
"^.+\\.(ts|tsx)$": "esbuild-jest",
},
};

View file

@ -1,6 +1,6 @@
{
"compilerOptions": {
"lib": ["ES2020"],
"types": ["jest", "@cloudflare/workers-types"]
"types": ["@cloudflare/workers-types"]
}
}

View file

@ -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

View file

@ -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 },
],
},
};

View file

@ -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?

View file

@ -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

View file

@ -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"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 588 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

View file

@ -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>

View file

@ -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"
}
}

View file

@ -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"
}
}

View file

@ -1,6 +0,0 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

View file

@ -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>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -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;

View file

@ -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;

View file

@ -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>
);

View file

@ -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>

Before

Width:  |  Height:  |  Size: 4 KiB

View file

@ -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;
}
});

View file

@ -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>
);
}

View file

@ -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 };

View file

@ -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,
};

View file

@ -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,
};

View file

@ -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,
};

View file

@ -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 };

View file

@ -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 };

View file

@ -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 };

View file

@ -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} />);
});

View file

@ -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 */
}

View file

@ -1,6 +0,0 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

View file

@ -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>,
);

View file

@ -1,4 +0,0 @@
export type Space = {
id: number;
name: string;
};

View file

@ -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(),
}),
}),
});

View file

@ -1,13 +0,0 @@
export const getEnv = () => {
// chrome.management.getSelf((self) => {
// if (self.installType === 'development') {
// return "development"
// }
// else {
// return "production"
// }
// })
// return null
return "production";
};

View file

@ -1 +0,0 @@
/// <reference types="vite/client" />

View file

@ -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,
},
};

View file

@ -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"]
}

View file

@ -1,11 +0,0 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true
},
"include": ["vite.config.ts"]
}

View file

@ -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"),
},
},
});

View file

@ -1,7 +0,0 @@
{
"extends": [
"next/core-web-vitals",
"plugin:eslint-plugin-next-on-pages/recommended"
],
"plugins": ["eslint-plugin-next-on-pages"]
}

View file

@ -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

View file

@ -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.

View file

@ -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"
}
}

47
apps/web-v2/env.d.ts vendored
View file

@ -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 {};

View file

@ -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;

View file

@ -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"
}
}

View file

@ -1,6 +0,0 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

View file

@ -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"
}

View file

@ -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>
);
}

View file

@ -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;
}

View file

@ -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>
);
}

View file

@ -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;

View file

@ -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>
);
};

View file

@ -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,
};

View file

@ -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>
);
}

View file

@ -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 };

View file

@ -1,6 +0,0 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

View file

@ -1,6 +0,0 @@
import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

View file

@ -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>
);

View file

@ -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;

View file

@ -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"]
}

View file

@ -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 Cloudflares global network
# Docs: https://developers.cloudflare.com/pages/functions/bindings/#workers-ai
# [ai]
# binding = "AI"
# Bind a D1 database. D1 is Cloudflares 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>"

9
apps/web/.eslintrc.js Normal file
View file

@ -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,
},
};

View file

@ -1,7 +0,0 @@
{
"extends": [
"next/core-web-vitals",
"plugin:eslint-plugin-next-on-pages/recommended"
],
"plugins": ["eslint-plugin-next-on-pages"]
}

41
apps/web/.gitignore vendored
View file

@ -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

View file

@ -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
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
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:
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).
- `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
## Learn More
> **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))
To learn more about Next.js, take a look at the following resources:
### Bindings
- [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.
Cloudflare [Bindings](https://developers.cloudflare.com/pages/functions/bindings/) are what allows you to interact with resources available in the Cloudflare Platform.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
You can use bindings during development, when previewing locally your application and of course in the deployed application:
## Deploy on Vercel
- 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).
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.
- 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 `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
```
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.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

View file

@ -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;

View file

@ -0,0 +1,8 @@
import { getThemeToggler } from "../../helpers/lib/get-theme-button";
async function Signin() {
const SetThemeButton = getThemeToggler();
return <SetThemeButton />;
}
export default Signin;

View file

@ -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("");

View file

@ -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>

View file

@ -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>

View file

@ -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";

View file

@ -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 = [

View file

@ -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}`,
});

View file

@ -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>
);
}

View file

@ -0,0 +1,2 @@
export { GET, POST } from "../../helpers/server/auth";
export const runtime = "edge";

View file

@ -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]!,
};
}

View file

@ -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",
},
});
}

50
apps/web/app/globals.css Normal file
View file

@ -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;
}

View file

@ -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" />,
});

View file

@ -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);
}

View file

@ -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,
}),
],
});

View file

@ -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",
),

Some files were not shown because too many files have changed in this diff Show more