refactored UI, with shared components and UI, better rules and million lint
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
@ -1 +0,0 @@
|
|||
**/.gitignore
|
||||
21
LICENSE
|
|
@ -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.
|
||||
90
README.md
|
|
@ -1,90 +0,0 @@
|
|||

|
||||
|
||||
# SuperMemory
|
||||
|
||||
Interested in helping build the best second brain for everyone? Join the discord https://discord.gg/2X2XsKz5AU. Contributions welcome.
|
||||
|
||||
## 👀 What is this?
|
||||
|
||||
Build your own second brain with supermemory. It's a ChatGPT for your bookmarks. Import tweets or save websites and content using the [chrome extension](https://chromewebstore.google.com/detail/supermemory/afpgkkipfdpeaflnpoaffkcankadgjfc?hl=en-GB&authuser=0) (the extension on webstore is not updated, please use the one in the repo)
|
||||
|
||||
Well, here's the thing - me and @yxshv save a _lot_ of content on the internet.
|
||||
|
||||
Twitter bookmarks, websites, snippets, etc.
|
||||
|
||||
But we never look back to it - to us, it's like throwing information in the void.
|
||||
|
||||
Supermemory fixes this.
|
||||
|
||||
## How do I use this?
|
||||
|
||||
Just go to [supermemory.dhr.wtf](https://supermemory.dhr.wtf) and sign in with your google account.
|
||||
|
||||
To use the chrome extension,
|
||||
|
||||
1. Get the chrome ext (click on the button)
|
||||
2. Click on the "Extension Auth" button so the extension knows who you are :)
|
||||

|
||||
|
||||
## 👨💻 The Stack
|
||||
|
||||

|
||||
|
||||
Supermemory has three main modules, managed by [turborepo](https://turbo.build):
|
||||
|
||||
#### `apps/web`: The main web UI.
|
||||
|
||||
The database, auth etc logic is here
|
||||
|
||||

|
||||
|
||||
Built with:
|
||||
|
||||
- Nextjs 14
|
||||
- [Next Auth](https://next-auth.js.org/)
|
||||
- [Drizzle ORM](https://drizzle.team/)
|
||||
- [Cloudflare D1 database](https://developers.cloudflare.com/d1/get-started/)
|
||||
- Cloudflare ratelimiter
|
||||
- [TailwindCSS](https://tailwindcss.com)
|
||||
- [shadcn-ui](https://ui.shadcn.com)
|
||||
- And some other amazing open source projects like [Novel](https://novel.sh) and [vaul](https://vaul.emilkowal.ski/)
|
||||
- Hosted on [Cloudflare Pages](https://pages.cloudflare.com/)
|
||||
|
||||
#### `apps/extension`: Chrome extension
|
||||
|
||||
The chrome extension is one of the most important part of the setup, but is not required.This is to easily add pages to your memory.
|
||||
|
||||

|
||||
|
||||
You can also use it to import all your twitter bookmarks!
|
||||

|
||||
|
||||
Built with:
|
||||
|
||||
- [CRXJS](https://crxjs.dev/vite-plugin/getting-started/react/create-project)
|
||||
- Vite
|
||||
- [TailwindCSS](https://tailwindcss.com)
|
||||
- [shadcn-ui](https://ui.shadcn.com)
|
||||
- React
|
||||
|
||||
#### `apps/cf-ai-backend`: This module handles the vector store and AI response generation
|
||||
|
||||
This is where the magic happens!
|
||||
Built with:
|
||||
|
||||
- Cloudflare Workers
|
||||
- [Cloudflare AI](https://ai.cloudflare.com)
|
||||
- [Cloudflare Vectorize](https://developers.cloudflare.com/vectorize/)
|
||||
- [Cloudflare Queues](https://developers.cloudflare.com/queues/)
|
||||
- [Cloudflare Browser Rendering](https://developers.cloudflare.com/browser-rendering/)
|
||||
- [Cloudflare KV](https://developers.cloudflare.com/kv)
|
||||
|
||||
## Contribute or self host
|
||||
|
||||
Supermemory is design to be set up easily locally and super duper easy to set up 💫
|
||||
|
||||
Please see the [SETUP-GUIDE.md](SETUP-GUIDE.md) for setup instructions.
|
||||
|
||||
### Contributing
|
||||
|
||||
Contributions are very welcome! A contribution can be as small as a ⭐ or even finding and creating issues.
|
||||
|
|
@ -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.
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
module.exports = {
|
||||
testEnvironment: "miniflare",
|
||||
testMatch: ["**/test/**/*.+(ts|tsx)", "**/src/**/(*.)+(spec|test).+(ts|tsx)"],
|
||||
transform: {
|
||||
"^.+\\.(ts|tsx)$": "esbuild-jest",
|
||||
},
|
||||
};
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["ES2020"],
|
||||
"types": ["jest", "@cloudflare/workers-types"]
|
||||
"types": ["@cloudflare/workers-types"]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
],
|
||||
},
|
||||
};
|
||||
26
apps/extension/.gitignore
vendored
|
|
@ -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?
|
||||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 588 B |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
|
|
@ -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>
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
|
|
@ -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 |
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
@ -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 |
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 };
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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 };
|
||||
|
|
@ -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 };
|
||||
|
|
@ -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 };
|
||||
|
|
@ -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} />);
|
||||
});
|
||||
|
|
@ -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 */
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
import { type ClassValue, clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
|
@ -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>,
|
||||
);
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
export type Space = {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
|
|
@ -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(),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
export const getEnv = () => {
|
||||
// chrome.management.getSelf((self) => {
|
||||
// if (self.installType === 'development') {
|
||||
// return "development"
|
||||
// }
|
||||
// else {
|
||||
// return "production"
|
||||
// }
|
||||
// })
|
||||
|
||||
// return null
|
||||
return "production";
|
||||
};
|
||||
1
apps/extension/src/vite-env.d.ts
vendored
|
|
@ -1 +0,0 @@
|
|||
/// <reference types="vite/client" />
|
||||
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
|
|
@ -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"]
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
|
|
@ -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"),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"extends": [
|
||||
"next/core-web-vitals",
|
||||
"plugin:eslint-plugin-next-on-pages/recommended"
|
||||
],
|
||||
"plugins": ["eslint-plugin-next-on-pages"]
|
||||
}
|
||||
40
apps/web-v2/.gitignore
vendored
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
|
@ -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 {};
|
||||
|
|
@ -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;
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 1.8 MiB |
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 };
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
import { type ClassValue, clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
import { ClassValue, clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
@ -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;
|
||||
|
|
@ -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"]
|
||||
}
|
||||
|
|
@ -1,94 +0,0 @@
|
|||
#:schema node_modules/wrangler/config-schema.json
|
||||
name = "web-v2"
|
||||
compatibility_date = "2024-05-12"
|
||||
compatibility_flags = ["nodejs_compat"]
|
||||
pages_build_output_dir = ".vercel/output/static"
|
||||
|
||||
# [[unsafe.bindings]]
|
||||
# name = "RATELIMITER"
|
||||
# type = "ratelimit"
|
||||
# namespace_id = "1011"
|
||||
|
||||
# simple = { limit = 25, period = 10 }
|
||||
|
||||
# Automatically place your workloads in an optimal location to minimize latency.
|
||||
# If you are running back-end logic in a Pages Function, running it closer to your back-end infrastructure
|
||||
# rather than the end user may result in better performance.
|
||||
# Docs: https://developers.cloudflare.com/pages/functions/smart-placement/#smart-placement
|
||||
# [placement]
|
||||
# mode = "smart"
|
||||
|
||||
# Variable bindings. These are arbitrary, plaintext strings (similar to environment variables)
|
||||
# Docs:
|
||||
# - https://developers.cloudflare.com/pages/functions/bindings/#environment-variables
|
||||
# Note: Use secrets to store sensitive data.
|
||||
# - https://developers.cloudflare.com/pages/functions/bindings/#secrets
|
||||
# [vars]
|
||||
# MY_VARIABLE = "production_value"
|
||||
|
||||
# Bind the Workers AI model catalog. Run machine learning models, powered by serverless GPUs, on Cloudflare’s global network
|
||||
# Docs: https://developers.cloudflare.com/pages/functions/bindings/#workers-ai
|
||||
# [ai]
|
||||
# binding = "AI"
|
||||
|
||||
# Bind a D1 database. D1 is Cloudflare’s native serverless SQL database.
|
||||
# Docs: https://developers.cloudflare.com/pages/functions/bindings/#d1-databases
|
||||
# [[d1_databases]]
|
||||
# binding = "MY_DB"
|
||||
# database_name = "my-database"
|
||||
# database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
|
||||
# Bind a Durable Object. Durable objects are a scale-to-zero compute primitive based on the actor model.
|
||||
# Durable Objects can live for as long as needed. Use these when you need a long-running "server", such as in realtime apps.
|
||||
# Docs: https://developers.cloudflare.com/workers/runtime-apis/durable-objects
|
||||
# [[durable_objects.bindings]]
|
||||
# name = "MY_DURABLE_OBJECT"
|
||||
# class_name = "MyDurableObject"
|
||||
# script_name = 'my-durable-object'
|
||||
|
||||
# Bind a KV Namespace. Use KV as persistent storage for small key-value pairs.
|
||||
# Docs: https://developers.cloudflare.com/pages/functions/bindings/#kv-namespaces
|
||||
# KV Example:
|
||||
# [[kv_namespaces]]
|
||||
# binding = "MY_KV_NAMESPACE"
|
||||
# id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
|
||||
# Bind a Queue producer. Use this binding to schedule an arbitrary task that may be processed later by a Queue consumer.
|
||||
# Docs: https://developers.cloudflare.com/pages/functions/bindings/#queue-producers
|
||||
# [[queues.producers]]
|
||||
# binding = "MY_QUEUE"
|
||||
# queue = "my-queue"
|
||||
|
||||
# Bind an R2 Bucket. Use R2 to store arbitrarily large blobs of data, such as files.
|
||||
# Docs: https://developers.cloudflare.com/pages/functions/bindings/#r2-buckets
|
||||
# [[r2_buckets]]
|
||||
# binding = "MY_BUCKET"
|
||||
# bucket_name = "my-bucket"
|
||||
|
||||
# Bind another Worker service. Use this binding to call another Worker without network overhead.
|
||||
# Docs: https://developers.cloudflare.com/pages/functions/bindings/#service-bindings
|
||||
# [[services]]
|
||||
# binding = "MY_SERVICE"
|
||||
# service = "my-service"
|
||||
|
||||
# To use different bindings for preview and production environments, follow the examples below.
|
||||
# When using environment-specific overrides for bindings, ALL bindings must be specified on a per-environment basis.
|
||||
# Docs: https://developers.cloudflare.com/pages/functions/wrangler-configuration#environment-specific-overrides
|
||||
|
||||
######## PREVIEW environment config ########
|
||||
|
||||
# [env.preview.vars]
|
||||
# API_KEY = "xyz789"
|
||||
|
||||
# [[env.preview.kv_namespaces]]
|
||||
# binding = "MY_KV_NAMESPACE"
|
||||
# id = "<PREVIEW_NAMESPACE_ID>"
|
||||
|
||||
######## PRODUCTION environment config ########
|
||||
|
||||
# [env.production.vars]
|
||||
# API_KEY = "abc123"
|
||||
|
||||
# [[env.production.kv_namespaces]]
|
||||
# binding = "MY_KV_NAMESPACE"
|
||||
# id = "<PRODUCTION_NAMESPACE_ID>"
|
||||
9
apps/web/.eslintrc.js
Normal 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,
|
||||
},
|
||||
};
|
||||
|
|
@ -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
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
15
apps/web/app/(auth)/auth-buttons.tsx
Normal 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;
|
||||
8
apps/web/app/(auth)/signin/page.tsx
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { getThemeToggler } from "../../helpers/lib/get-theme-button";
|
||||
|
||||
async function Signin() {
|
||||
const SetThemeButton = getThemeToggler();
|
||||
return <SetThemeButton />;
|
||||
}
|
||||
|
||||
export default Signin;
|
||||
|
|
@ -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("");
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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";
|
||||
|
|
@ -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 = [
|
||||
|
|
@ -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}`,
|
||||
});
|
||||
58
apps/web/app/(landing)/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
2
apps/web/app/api/[...nextauth]/route.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export { GET, POST } from "../../helpers/server/auth";
|
||||
export const runtime = "edge";
|
||||
33
apps/web/app/api/ensureAuth.ts
Normal 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]!,
|
||||
};
|
||||
}
|
||||
56
apps/web/app/api/upload_image/route.ts
Normal 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
|
|
@ -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;
|
||||
}
|
||||
11
apps/web/app/helpers/lib/get-theme-button.tsx
Normal 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" />,
|
||||
});
|
||||
25
apps/web/app/helpers/lib/handle-errors.ts
Normal 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);
|
||||
}
|
||||
|
|
@ -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,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
|
@ -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",
|
||||
),
|
||||