Merge pull request #52 from Dhravya/v2-refactor

V2 refactor
This commit is contained in:
Dhravya Shah 2024-05-25 23:03:10 -05:00 committed by GitHub
commit 8c24d3e4bc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
248 changed files with 2799 additions and 11301 deletions

15
.eslintrc.js Normal file
View file

@ -0,0 +1,15 @@
// This configuration only applies to the package manager root.
/** @type {import("eslint").Linter.Config} */
module.exports = {
ignorePatterns: ["apps/**", "packages/**"],
extends: ["@repo/eslint-config/library.js"],
parser: "@typescript-eslint/parser",
parserOptions: {
project: true,
},
// ignore some rules
rules: {
"@": "off",
"import/no-unresolved": "off",
},
};

67
.gitignore vendored
View file

@ -1,52 +1,43 @@
.next/
.turbo
*.sqlite
*.lockb
.next
bun.lockb
.npmrc
.vars
.*.vars
.wrangler
drizzle/
dist/
pnpm-lock.yaml
.million
# Dependencies
node_modules
# dependencies
/node_modules
/.pnp
.pnp
.pnp.js
# testing
/coverage
# Local env files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# database
/prisma/db.sqlite
/prisma/db.sqlite-journal
# Testing
coverage
# next.js
/.next/
/out/
next-env.d.ts
# Turbo
.turbo
# production
/build
# Vercel
.vercel
# misc
.DS_Store
*.pem
# Build Outputs
.next/
out/
build
dist
# debug
# Debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
# do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables
.env
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
# Misc
.DS_Store
*.pem

5
.gitmodules vendored
View file

@ -1,6 +1,3 @@
[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
url = https://github.com/dhravya/markdowner

View file

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

View file

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

View file

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

View file

@ -1,5 +1,5 @@
name = "new-cf-ai-backend"
main = "src/server.ts"
main = "src/index.ts"
compatibility_date = "2024-02-23"
node_compat = true

View file

@ -1,18 +0,0 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react-hooks/recommended",
],
ignorePatterns: ["dist", ".eslintrc.cjs"],
parser: "@typescript-eslint/parser",
plugins: ["react-refresh"],
rules: {
"react-refresh/only-export-components": [
"warn",
{ allowConstantExport: true },
],
},
};

View file

@ -1,26 +0,0 @@
# Logs
*.zip
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View file

@ -1,30 +0,0 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
- Configure the top-level `parserOptions` property like this:
```js
export default {
// other rules...
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
project: ["./tsconfig.json", "./tsconfig.node.json"],
tsconfigRootDir: __dirname,
},
};
```
- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list

View file

@ -1,17 +0,0 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "src/global.css",
"baseColor": "stone",
"cssVariables": false,
"prefix": "anycontext-"
},
"aliases": {
"components": "src/components",
"utils": "src/lib/utils"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 588 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

View file

@ -1,13 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View file

@ -1,35 +0,0 @@
{
"manifest_version": 3,
"name": "SuperMemory",
"version": "2.0.0",
"action": {
"default_popup": "index.html"
},
"icons": {
"16": "icons/icon16.png",
"32": "icons/icon32.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
},
"content_scripts": [
{
"js": ["src/content.tsx"],
"matches": [
"http://localhost:3000/*",
"https://opulent-funicular-94rx4v9w775f96q-3000.app.github.dev/*",
"https://anycontext.dhr.wtf/*",
"<all_urls>"
]
}
],
"permissions": [
"activeTab",
"storage",
"http://localhost:3000/*",
"https://opulent-funicular-94rx4v9w775f96q-3000.app.github.dev/*",
"https://anycontext.dhr.wtf/*"
],
"background": {
"service_worker": "src/background.ts"
}
}

View file

@ -1,35 +0,0 @@
{
"name": "extension",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview",
"package": "zip -r extension.zip dist/"
},
"dependencies": {
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-tooltip": "^1.0.7",
"cmdk": "^1.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/node": "^20.11.22",
"@types/react": "^18.2.56",
"@types/react-dom": "^18.2.19",
"@typescript-eslint/eslint-plugin": "^7.0.2",
"@typescript-eslint/parser": "^7.0.2",
"@vitejs/plugin-react": "^4.2.1",
"eslint": "^8.56.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5",
"typescript": "^5.2.2",
"vite": "^5.1.4"
}
}

View file

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

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -1,308 +0,0 @@
import { useEffect, useState } from "react";
import { z } from "zod";
import { userObj } from "./types/zods";
import { getEnv } from "./util";
const backendUrl =
getEnv() === "development"
? "http://localhost:3000"
: "https://supermemory.dhr.wtf";
function App() {
const [userData, setUserData] = useState<z.infer<typeof userObj> | null>(
null,
);
const getUserData = () => {
chrome.runtime.sendMessage({ type: "getJwt" }, (response) => {
const jwt = response.jwt;
const loginButton = document.getElementById("login");
if (loginButton) {
if (jwt) {
fetch(`${backendUrl}/api/me`, {
headers: {
Authorization: `Bearer ${jwt}`,
},
})
.then((res) => res.json())
.then((data) => {
const d = userObj.safeParse(data);
if (d.success) {
setUserData(d.data);
} else {
console.error(d.error);
}
});
loginButton.style.display = "none";
}
}
});
};
useEffect(() => {
getUserData();
}, []);
// TODO: Implement getting bookmarks from Twitter API directly
// const [status, setStatus] = useState('');
// const [bookmarks, setBookmarks] = useState<TweetData[]>([]);
// const fetchBookmarks = (e: React.MouseEvent<HTMLButtonElement>) => {
// e.preventDefault();
// chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
// chrome.tabs.sendMessage(tabs[0].id!, { action: 'showProgressIndicator' });
// });
// chrome.tabs.create(
// { url: 'https://twitter.com/i/bookmarks/all' },
// function (tab) {
// chrome.tabs.onUpdated.addListener(function listener(tabId, info) {
// if (tabId === tab.id && info.status === 'complete') {
// chrome.tabs.onUpdated.removeListener(listener);
// chrome.runtime.sendMessage(
// { action: 'getAuthData' },
// function (response) {
// const authorizationHeader = response.authorizationHeader;
// const csrfToken = response.csrfToken;
// const cookies = response.cookies;
// if (authorizationHeader && csrfToken && cookies) {
// fetchAllBookmarks(authorizationHeader, csrfToken, cookies)
// .then((bookmarks) => {
// console.log('Bookmarks data:', bookmarks);
// setBookmarks(bookmarks);
// chrome.tabs.sendMessage(tabId, {
// action: 'hideProgressIndicator',
// });
// setStatus(
// `Fetched ${bookmarks.length} bookmarked tweets.`,
// );
// })
// .catch((error) => {
// console.error('Error:', error);
// chrome.tabs.sendMessage(tabId, {
// action: 'hideProgressIndicator',
// });
// setStatus(
// 'Error fetching bookmarks. Please check the console for details.',
// );
// });
// } else {
// chrome.tabs.sendMessage(tabId, {
// action: 'hideProgressIndicator',
// });
// setStatus('Missing authentication data');
// }
// },
// );
// }
// });
// },
// );
// };
return (
<div className="p-8">
<button
onClick={() =>
chrome.tabs.create({
url: `${backendUrl}/api/auth/signin`,
})
}
id="login"
>
Log in
</button>
<div>
{userData && (
<div className="flex items-center">
<img
width={40}
className="rounded-full"
src={userData.data.user.image!}
alt=""
/>
<div>
<h3>{userData.data.user.name}</h3>
<p>{userData.data.user.email}</p>
</div>
{/* TODO: Implement getting bookmarks from API directly */}
{/* <button onClick={(e) => fetchBookmarks(e)}>Fetch Bookmarks</button>
<div>{status}</div>
<div>
{bookmarks.map((bookmark) => (
<div key={bookmark.tweet_id}>
<p>{bookmark.author}</p>
<p>{bookmark.date}</p>
<p>{bookmark.full_text}</p>
</div>
))}
</div> */}
</div>
)}
</div>
</div>
);
}
// TODO: Implement getting bookmarks from Twitter API directly
// async function fetchAllBookmarks(
// authorizationHeader: string,
// csrfToken: string,
// cookies: string,
// ): Promise<TweetData[]> {
// const baseUrl =
// 'https://twitter.com/i/api/graphql/uJEL6XARgGmo2EAsO2Pfkg/Bookmarks';
// const params = new URLSearchParams({
// variables: JSON.stringify({
// count: 100,
// includePromotedContent: true,
// }),
// features: JSON.stringify({
// graphql_timeline_v2_bookmark_timeline: true,
// rweb_tipjar_consumption_enabled: false,
// responsive_web_graphql_exclude_directive_enabled: true,
// verified_phone_label_enabled: true,
// creator_subscriptions_tweet_preview_api_enabled: true,
// responsive_web_graphql_timeline_navigation_enabled: true,
// responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
// communities_web_enable_tweet_community_results_fetch: true,
// c9s_tweet_anatomy_moderator_badge_enabled: true,
// tweetypie_unmention_optimization_enabled: true,
// responsive_web_edit_tweet_api_enabled: true,
// graphql_is_translatable_rweb_tweet_is_translatable_enabled: true,
// view_counts_everywhere_api_enabled: true,
// longform_notetweets_consumption_enabled: true,
// responsive_web_twitter_article_tweet_consumption_enabled: true,
// tweet_awards_web_tipping_enabled: false,
// creator_subscriptions_quote_tweet_preview_enabled: false,
// freedom_of_speech_not_reach_fetch_enabled: true,
// standardized_nudges_misinfo: true,
// tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled:
// true,
// tweet_with_visibility_results_prefer_gql_media_interstitial_enabled:
// false,
// rweb_video_timestamps_enabled: true,
// longform_notetweets_rich_text_read_enabled: true,
// longform_notetweets_inline_media_enabled: true,
// responsive_web_enhance_cards_enabled: false,
// }),
// });
// const requestUrl = `${baseUrl}?${params}`;
// const headers = {
// Authorization: authorizationHeader,
// 'X-Csrf-Token': csrfToken,
// Cookie: cookies,
// };
// const bookmarks: TweetData[] = [];
// let nextCursor = null;
// let requestCount = 0;
// const maxRequestsPerWindow = 450;
// const windowDuration = 15 * 60 * 1000; // 15 minutes in milliseconds
// let windowStartTime = Date.now();
// do {
// if (nextCursor) {
// params.set(
// 'variables',
// JSON.stringify({
// count: 100,
// cursor: nextCursor,
// includePromotedContent: true,
// }),
// );
// }
// // Check if the rate limit is exceeded
// if (requestCount >= maxRequestsPerWindow) {
// const elapsedTime = Date.now() - windowStartTime;
// if (elapsedTime < windowDuration) {
// const waitTime = windowDuration - elapsedTime;
// await new Promise((resolve) => setTimeout(resolve, waitTime));
// }
// requestCount = 0;
// windowStartTime = Date.now();
// }
// try {
// const response = await fetch(requestUrl, {
// method: 'GET',
// headers: headers,
// });
// requestCount++;
// if (!response.ok) {
// throw new Error(`HTTP error! status: ${response.status}`);
// }
// const data = await response.json();
// const timeline = data.data.bookmark_timeline_v2.timeline;
// timeline.instructions.forEach(
// (instruction: {
// type: string;
// entries: {
// content: {
// entryType: string;
// itemContent: {
// tweet_results: {
// result: {
// legacy: {
// full_text: string;
// created_at: string;
// };
// core: {
// user_results: {
// result: {
// legacy: {
// screen_name: string;
// };
// };
// };
// };
// rest_id: string;
// };
// };
// };
// };
// }[];
// }) => {
// if (instruction.type === 'TimelineAddEntries') {
// instruction.entries.forEach((entry) => {
// if (entry.content.entryType === 'TimelineTimelineItem') {
// const tweet = entry.content.itemContent.tweet_results.result;
// const tweetData = {
// full_text: tweet.legacy.full_text,
// url: `https://twitter.com/${tweet.core.user_results.result.legacy.screen_name}/status/${tweet.rest_id}`,
// author: tweet.core.user_results.result.legacy.screen_name,
// date: tweet.legacy.created_at,
// tweet_id: tweet.rest_id,
// };
// bookmarks.push(tweetData);
// }
// });
// }
// },
// );
// nextCursor = timeline.instructions.find(
// (instruction: { type: string }) =>
// instruction.type === 'TimelineTerminateTimeline',
// )?.direction?.cursor;
// } catch (error) {
// console.error('Error fetching bookmarks:', error);
// throw error;
// }
// } while (nextCursor);
// return bookmarks;
// }
export default App;

View file

@ -1,361 +0,0 @@
import { useState } from "react";
import "./ext.css";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "./components/ui/tooltip";
import { FilterSpaces } from "./components/FilterCombobox";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogTrigger,
DialogFooter,
DialogClose,
} from "./components/ui/dialog";
import { Space } from "./types/memory";
function sendUrlToAPI(spaces: number[]) {
// get the current URL
const url = window.location.href;
const blacklist = ["localhost:3000", "anycontext.dhr.wtf"];
// check if the URL is blacklisted
if (blacklist.some((blacklisted) => url.includes(blacklisted))) {
console.log("URL is blacklisted");
return;
} else {
// const content = Entire page content, but cleaned up for the LLM. No ads, no scripts, no styles, just the text. if article, just the importnat info abou tit.
const content = document.documentElement.innerText;
chrome.runtime.sendMessage({ type: "urlChange", content, url, spaces });
}
}
function SideBar({ jwt }: { jwt: string }) {
// TODO: Implement getting bookmarks from Twitter API directly
// chrome.runtime.onMessage.addListener(function (request) {
// if (request.action === 'showProgressIndicator') {
// // TODO: SHOW PROGRESS INDICATOR
// // showProgressIndicator();
// } else if (request.action === 'hideProgressIndicator') {
// // hideProgressIndicator();
// }
// });
const [savedWebsites, setSavedWebsites] = useState<string[]>([]);
const [isSendingData, setIsSendingData] = useState(false);
const [loading, setLoading] = useState(false);
const [spaces, setSpaces] = useState<Space[]>();
const [selectedSpaces, setSelectedSpaces] = useState<number[]>([]);
const [isImportingTweets, setIsImportingTweets] = useState(false);
const [log, setLog] = useState<string[]>([]);
interface TweetData {
tweetText: string;
postUrl: string;
authorName: string;
handle: string;
time: string;
saveToUser: string;
}
function sendBookmarkedTweetsToAPI(tweets: TweetData[], token: string) {
chrome.runtime.sendMessage({
type: "sendBookmarkedTweets",
jwt: token,
tweets,
});
}
const fetchSpaces = async () => {
setLoading(true);
chrome.runtime.sendMessage({ type: "fetchSpaces" }, (resp) => {
console.log("response", resp);
setSpaces(resp);
setLoading(false);
});
};
const fetchBookmarks = () => {
const tweets: TweetData[] = []; // Initialize an empty array to hold all tweet elements
setIsImportingTweets(true);
console.log("Importing tweets");
const scrollInterval = 1000;
const scrollStep = 5000; // Pixels to scroll on each step
let previousTweetCount = 0;
let unchangedCount = 0;
const scrollToEndIntervalID = setInterval(() => {
window.scrollBy(0, scrollStep);
const currentTweetCount = tweets.length;
if (currentTweetCount === previousTweetCount) {
unchangedCount++;
if (unchangedCount >= 2) {
setLog([
...log,
"Scraping complete",
`Total tweets scraped: ${tweets.length}`,
"Downloading tweets as JSON...",
]);
clearInterval(scrollToEndIntervalID); // Stop scrolling
observer.disconnect(); // Stop observing DOM changes
downloadTweetsAsJson(tweets); // Download the tweets list as a JSON file
}
} else {
unchangedCount = 0; // Reset counter if new tweets were added
}
previousTweetCount = currentTweetCount; // Update previous count for the next check
}, scrollInterval);
function updateTweets() {
document
.querySelectorAll('article[data-testid="tweet"]')
.forEach((tweetElement) => {
const authorName = (
tweetElement.querySelector(
'[data-testid="User-Name"]',
) as HTMLElement
)?.innerText;
const handle = (
tweetElement.querySelector('[role="link"]') as HTMLLinkElement
).href
.split("/")
.pop();
const tweetText = (
tweetElement.querySelector(
'[data-testid="tweetText"]',
) as HTMLElement
)?.innerText;
const time = (
tweetElement.querySelector("time") as HTMLTimeElement
).getAttribute("datetime");
const postUrl = (
tweetElement.querySelector(
".css-175oi2r.r-18u37iz.r-1q142lx a",
) as HTMLLinkElement
)?.href;
const isTweetNew = !tweets.some((tweet) => tweet.postUrl === postUrl);
if (isTweetNew) {
tweets.push({
authorName,
handle: handle ?? "",
tweetText,
time: time ?? "",
postUrl,
saveToUser: jwt,
});
setLog([...log, `Scraped tweet: ${tweets.length}`]);
}
});
}
// Initially populate the tweets array
updateTweets();
// Create a MutationObserver to observe changes in the DOM
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.addedNodes.length) {
updateTweets(); // Call updateTweets whenever new nodes are added to the DOM
}
});
});
// Start observing the document body for child list changes
observer.observe(document.body, { childList: true, subtree: true });
function downloadTweetsAsJson(tweetsArray: TweetData[]) {
setLog([...log, "Saving the tweets to our database..."]);
sendBookmarkedTweetsToAPI(tweetsArray, jwt);
setIsImportingTweets(false);
}
};
return (
<>
{isImportingTweets && (
<div className="anycontext-overlay anycontext-fixed anycontext-font-sans anycontext-inset-0 anycontext-bg-black anycontext-bg-opacity-50">
<div className="anycontext-flex anycontext-items-center anycontext-justify-center anycontext-h-screen">
<div className="anycontext-flex anycontext-flex-col anycontext-items-center">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={2}
stroke="currentColor"
className="anycontext-w-10 anycontext-h-10 anycontext-animate-spin"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M17.593 3.322c1.1.128 1.907 1.077 1.907 2.185V21L12 17.25 4.5 21V5.507c0-1.108.806-2.057 1.907-2.185a48.507 48.507 0 0 1 11.186 0Z"
/>
</svg>
<p className="anycontext-mt-2">Importing your tweets...</p>
<div className="anycontext-mt-2">
{log.map((message, index) => (
<p key={index}>{message}</p>
))}
</div>
</div>
</div>
</div>
)}
<TooltipProvider>
<div className="anycontext-flex anycontext-group anycontext-flex-col anycontext-gap-2 anycontext-fixed anycontext-bottom-12 anycontext-right-0 anycontext-z-[99999] anycontext-font-sans">
{window.location.href.includes("twitter.com") ||
window.location.href.includes("x.com") ? (
<Tooltip delayDuration={300}>
<TooltipTrigger className="anycontext-bg-transparent anycontext-border-none anycontext-m-0 anycontext-p-0">
<button
onClick={() => {
if (window.location.href.endsWith("/i/bookmarks/all")) {
fetchBookmarks();
} else {
window.location.href =
"https://twitter.com/i/bookmarks/all";
setTimeout(() => {
fetchBookmarks();
}, 2500);
}
}}
className="anycontext-open-button disabled:anycontext-opacity-30 anycontext-bg-transparent
anycontext-border-none anycontext-m-0 anycontext-p-0"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="anycontext-w-6 anycontext-h-6"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M17.593 3.322c1.1.128 1.907 1.077 1.907 2.185V21L12 17.25 4.5 21V5.507c0-1.108.806-2.057 1.907-2.185a48.507 48.507 0 0 1 11.186 0Z"
/>
</svg>
</button>
</TooltipTrigger>
<TooltipContent className="anycontext-p-0" side="left">
<p className="anycontext-p-0 anycontext-m-0">
Import twitter bookmarks
</p>
</TooltipContent>
</Tooltip>
) : (
<></>
)}
<Dialog onOpenChange={(open) => open === true && fetchSpaces()}>
<Tooltip delayDuration={300}>
<TooltipTrigger
className="anycontext-bg-transparent
anycontext-border-none anycontext-m-0 anycontext-p-0
"
>
<DialogTrigger asChild>
<button
disabled={savedWebsites.includes(window.location.href)}
className="anycontext-open-button disabled:anycontext-opacity-30 anycontext-bg-transparent
anycontext-border-none anycontext-m-0 anycontext-p-0"
>
{savedWebsites.includes(window.location.href) ? (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="lucide lucide-file-check-2"
>
<path d="M4 22h14a2 2 0 0 0 2-2V7l-5-5H6a2 2 0 0 0-2 2v4" />
<path d="M14 2v4a2 2 0 0 0 2 2h4" />
<path d="m3 15 2 2 4-4" />
</svg>
) : (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
className={`anycontext-w-5 anycontext-h-5 ${isSendingData ? "anycontext-animate-spin" : ""}`}
>
<path d="M15.98 1.804a1 1 0 0 0-1.96 0l-.24 1.192a1 1 0 0 1-.784.785l-1.192.238a1 1 0 0 0 0 1.962l1.192.238a1 1 0 0 1 .785.785l.238 1.192a1 1 0 0 0 1.962 0l.238-1.192a1 1 0 0 1 .785-.785l1.192-.238a1 1 0 0 0 0-1.962l-1.192-.238a1 1 0 0 1-.785-.785l-.238-1.192ZM6.949 5.684a1 1 0 0 0-1.898 0l-.683 2.051a1 1 0 0 1-.633.633l-2.051.683a1 1 0 0 0 0 1.898l2.051.684a1 1 0 0 1 .633.632l.683 2.051a1 1 0 0 0 1.898 0l.683-2.051a1 1 0 0 1 .633-.633l2.051-.683a1 1 0 0 0 0-1.898l-2.051-.683a1 1 0 0 1-.633-.633L6.95 5.684ZM13.949 13.684a1 1 0 0 0-1.898 0l-.184.551a1 1 0 0 1-.632.633l-.551.183a1 1 0 0 0 0 1.898l.551.183a1 1 0 0 1 .633.633l.183.551a1 1 0 0 0 1.898 0l.184-.551a1 1 0 0 1 .632-.633l.551-.183a1 1 0 0 0 0-1.898l-.551-.184a1 1 0 0 1-.633-.632l-.183-.551Z" />
</svg>
)}
</button>
</DialogTrigger>
</TooltipTrigger>
<TooltipContent className="anycontext-p-0" side="left">
<p className="anycontext-p-0 anycontext-m-0">
{savedWebsites.includes(window.location.href)
? "Added to memory"
: "Add to memory"}
</p>
</TooltipContent>
</Tooltip>
<DialogContent>
<DialogHeader>
<DialogTitle>Add to Memory</DialogTitle>
<DialogDescription>
Add the current page to memory
</DialogDescription>
</DialogHeader>
<FilterSpaces
loading={loading}
className="anycontext-mr-auto"
selectedSpaces={selectedSpaces}
setSelectedSpaces={setSelectedSpaces}
name={"Add to Spaces"}
spaces={spaces ?? []}
/>
<DialogFooter className="anycontext-w-full anycontext-text-sm">
<DialogClose
onClick={() => {
sendUrlToAPI(selectedSpaces);
setIsSendingData(true);
setTimeout(() => {
setIsSendingData(false);
setSavedWebsites([
...savedWebsites,
window.location.href,
]);
}, 1000);
}}
>
Add
</DialogClose>
<DialogClose>Cancel</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</TooltipProvider>
</>
);
}
export default SideBar;

View file

@ -1,69 +0,0 @@
export const MemoryIcon: React.FC<React.SVGAttributes<SVGElement>> = (
props,
) => (
<svg
viewBox="0 0 89 53"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<rect
x="0.40697"
y="8.52821"
width="43.0286"
height="43.0286"
rx="5.5"
transform="rotate(-12 0.40697 8.52821)"
fill="var(--anycontext-icon-fill)"
stroke="var(--anycontext-icon-stroke)"
/>
<rect
x="20.8257"
y="9.19775"
width="43"
height="43"
rx="5.5"
fill="var(--anycontext-icon-fill)"
stroke="var(--anycontext-icon-stroke)"
/>
<rect
x="47.6965"
y="-0.612372"
width="43.0286"
height="43.0286"
rx="5.5"
transform="rotate(15 47.6965 -0.612372)"
fill="var(--anycontext-icon-fill)"
stroke="var(--anycontext-icon-stroke)"
/>
</svg>
);
export const SpaceIcon: React.FC<React.SVGAttributes<SVGElement>> = (props) => (
<svg
viewBox="0 0 34 30"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<rect
x="1.39502"
y="5.2229"
width="24"
height="24"
rx="5.5"
fill="var(--anycontext-icon-fill)"
stroke="var(--anycontext-icon-stroke)"
/>
<rect
x="11.2231"
y="-0.157702"
width="24"
height="24"
rx="5.5"
transform="rotate(20 11.2231 -0.157702)"
fill="var(--anycontext-icon-fill)"
stroke="var(--anycontext-icon-stroke)"
/>
</svg>
);

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

Before

Width:  |  Height:  |  Size: 4 KiB

View file

@ -1,161 +0,0 @@
import { getEnv } from "./util";
import { Space } from "./types/memory";
const backendUrl =
getEnv() === "development"
? "http://localhost:3000"
: "https://supermemory.dhr.wtf";
interface TweetData {
tweetText: string;
postUrl: string;
authorName: string;
handle: string;
time: string;
saveToUser: string;
}
// TODO: Implement getting bookmarks from Twitter API directly
// let authorizationHeader: string | null = null;
// let csrfToken: string | null = null;
// let cookies: string | null = null;
// chrome.webRequest.onBeforeSendHeaders.addListener(
// (details) => {
// for (let i = 0; i < details.requestHeaders!.length; ++i) {
// const header = details.requestHeaders![i];
// if (header.name.toLowerCase() === 'authorization') {
// authorizationHeader = header.value || null;
// } else if (header.name.toLowerCase() === 'x-csrf-token') {
// csrfToken = header.value || null;
// } else if (header.name.toLowerCase() === 'cookie') {
// cookies = header.value || null;
// }
// console.log(header, authorizationHeader, csrfToken, cookies)
// }
// },
// { urls: ['https://twitter.com/*', 'https://x.com/*'] },
// ['requestHeaders']
// );
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.type === "getJwt") {
chrome.storage.local.get(["jwt"], ({ jwt }) => {
sendResponse({ jwt });
});
return true;
} else if (request.type === "urlChange") {
const content = request.content;
const url = request.url;
const spaces = request.spaces(
// eslint-disable-next-line no-unexpected-multiline
async () => {
chrome.storage.local.get(["jwt"], ({ jwt }) => {
if (!jwt) {
console.error("No JWT found");
return;
}
fetch(`${backendUrl}/api/store`, {
method: "POST",
headers: {
Authorization: `Bearer ${jwt}`,
},
body: JSON.stringify({ pageContent: content, url, spaces }),
}).then((ers) => console.log(ers.status));
});
},
)();
} else if (request.type === "fetchSpaces") {
chrome.storage.local.get(["jwt"], async ({ jwt }) => {
if (!jwt) {
console.error("No JWT found");
return;
}
const resp = await fetch(`${backendUrl}/api/spaces`, {
headers: {
Authorization: `Bearer ${jwt}`,
},
});
const data: {
message: "OK" | string;
data: Space[] | undefined;
} = await resp.json();
if (data.message === "OK" && data.data) {
sendResponse(data.data);
}
});
return true;
} else if (request.type === "queryApi") {
const input = request.input;
const jwt = request.jwt;
(async () => {
await fetch(`${backendUrl}/api/ask`, {
method: "POST",
headers: {
Authorization: `Bearer ${jwt}`,
},
body: JSON.stringify({
query: input,
}),
}).then(async (response) => {
if (!response.body) {
throw new Error("No response body");
}
if (!sender.tab?.id) {
throw new Error("No tab ID");
}
const reader = response.body.getReader();
// eslint-disable-next-line no-constant-condition
while (true) {
const { done, value } = await reader.read();
if (done) break;
// For simplicity, we're sending chunks as they come.
// This might need to be adapted based on your data and needs.
const chunkAsString = new TextDecoder("utf-8")
.decode(value)
.replace("data: ", "");
chrome.tabs.sendMessage(sender.tab.id, {
action: "streamData",
data: chunkAsString,
});
}
// Notify the content script that the stream is complete.
chrome.tabs.sendMessage(sender.tab.id, { action: "streamEnd" });
});
// Indicate that sendResponse will be called asynchronously.
return true;
})();
}
// TODO: Implement getting bookmarks from Twitter API directly
// else if (request.action === 'getAuthData') {
// sendResponse({
// authorizationHeader: authorizationHeader,
// csrfToken: csrfToken,
// cookies: cookies
// });
// }
else if (request.type === "sendBookmarkedTweets") {
const jwt = request.jwt;
const tweets = request.tweets as TweetData[];
(async () => {
await fetch(`${backendUrl}/api/vectorizeTweets`, {
method: "POST",
headers: {
Authorization: `Bearer ${jwt}`,
},
body: JSON.stringify(tweets),
}).then(async (response) => {
return response.json();
});
})();
return true;
}
});

View file

@ -1,93 +0,0 @@
import * as React from "react";
import { PlusCircleIcon, X } from "lucide-react";
import { Space } from "../types/memory";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
} from "./ui/dropdown-menu";
import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu";
export interface Props extends React.ButtonHTMLAttributes<HTMLButtonElement> {
selectedSpaces: number[];
setSelectedSpaces: (
spaces: number[] | ((prev: number[]) => number[]),
) => void;
name: string;
spaces: Space[];
loading: boolean;
}
export function FilterSpaces({
loading,
selectedSpaces,
setSelectedSpaces,
spaces,
}: Props) {
console.log(selectedSpaces, spaces);
const filteredSpaces = spaces.filter((space) =>
selectedSpaces.includes(space.id),
);
const leftSpaces = spaces.filter(
(space) => !selectedSpaces.includes(space.id),
);
if (loading) {
return "Loading...";
}
return (
<div className="anycontext-flex anycontext-flex-wrap anycontext-gap-1 anycontext-text-sm anycontext-">
{filteredSpaces.length < 1 && "Add to a space"}
{filteredSpaces.map((space) => (
<SpaceItem
{...space}
key={space.id}
onRemove={() =>
setSelectedSpaces((prev) => prev.filter((s) => s !== space.id))
}
/>
))}
{leftSpaces.length > 0 && (
<DropdownMenu>
<DropdownMenuTrigger className="anycontext-rounded-full">
<PlusCircleIcon
className="anycontext-w-5 anycontext-h-5 [--anycontext-icon-stroke:white] dark:[--anycontext-icon-stroke:black]"
stroke="var(--anycontext-icon-stroke)"
fill="currentColor"
/>
</DropdownMenuTrigger>
<DropdownMenuContent>
{leftSpaces.map((space) => (
<>
{loading && "Loading..."}
<DropdownMenuItem
onClick={() =>
setSelectedSpaces((prev) => [...prev, space.id])
}
>
{space.name}
</DropdownMenuItem>
</>
))}
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
);
}
function SpaceItem({ name, onRemove }: Space & { onRemove: () => void }) {
return (
<div className="anycontext-flex anycontext-justify-center anycontext-items-center anycontext-gap-2 anycontext-p-1 anycontext-pl-2 anycontext-pr-3 anycontext-rounded-full anycontext-bg-black/5 dark:anycontext-bg-white/5 anycontext-border-white/20 dark:anycontext-border-black/20 border">
<button
onClick={onRemove}
className="anycontext-flex hover:anycontext-bg-transparent anycontext-justify-center anycontext-scale-110 anycontext-items-center focus-visible:anycontext-outline-none anycontext-rounded-full anycontext-w-3 anycontext-bg-black/5 dark:anycontext-bg-white/5 anycontext-h-3 anycontext-text-transparent hover:anycontext-text-black dark:hover:anycontext-text-white"
>
<X className="anycontext-w-3 anycontext-h-3" />
</button>
{name}
</div>
);
}

View file

@ -1,58 +0,0 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "../../lib/utils";
const buttonVariants = cva(
"anycontext-inline-flex anycontext-items-center anycontext-justify-center anycontext-whitespace-nowrap anycontext-rounded-md anycontext-text-sm anycontext-font-medium anycontext-ring-offset-white anycontext-transition-colors focus-visible:anycontext-outline-none focus-visible:anycontext-ring-2 focus-visible:anycontext-ring-stone-950 focus-visible:anycontext-ring-offset-2 disabled:anycontext-pointer-events-none disabled:anycontext-opacity-50 dark:anycontext-ring-offset-stone-950 dark:focus-visible:anycontext-ring-stone-300",
{
variants: {
variant: {
default:
"anycontext-bg-stone-900 anycontext-text-stone-50 hover:anycontext-bg-stone-900/90 dark:anycontext-bg-stone-50 dark:anycontext-text-stone-900 dark:hover:anycontext-bg-stone-50/90",
destructive:
"anycontext-bg-red-500 anycontext-text-stone-50 hover:anycontext-bg-red-500/90 dark:anycontext-bg-red-900 dark:anycontext-text-stone-50 dark:hover:anycontext-bg-red-900/90",
outline:
"anycontext-border anycontext-border-stone-200 anycontext-bg-white hover:anycontext-bg-stone-100 hover:anycontext-text-stone-900 dark:anycontext-border-stone-800 dark:anycontext-bg-stone-950 dark:hover:anycontext-bg-stone-800 dark:hover:anycontext-text-stone-50",
secondary:
"anycontext-bg-stone-100 anycontext-text-stone-900 hover:anycontext-bg-stone-100/80 dark:anycontext-bg-stone-800 dark:anycontext-text-stone-50 dark:hover:anycontext-bg-stone-800/80",
ghost:
"hover:anycontext-bg-stone-100 hover:anycontext-text-stone-900 dark:hover:anycontext-bg-stone-800 dark:hover:anycontext-text-stone-50",
link: "anycontext-text-stone-900 anycontext-underline-offset-4 hover:anycontext-underline dark:anycontext-text-stone-50",
},
size: {
default: "anycontext-h-10 anycontext-px-4 anycontext-py-2",
sm: "anycontext-h-9 anycontext-rounded-md anycontext-px-3",
lg: "anycontext-h-11 anycontext-rounded-md anycontext-px-8",
icon: "anycontext-h-10 anycontext-w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
},
);
Button.displayName = "Button";
export { Button };

View file

@ -1,162 +0,0 @@
import * as React from "react";
import { type DialogProps } from "@radix-ui/react-dialog";
import { Command as CommandPrimitive } from "cmdk";
import { Search } from "lucide-react";
import { cn } from "../../lib/utils";
import { Dialog, DialogContent } from "../../components/ui/dialog";
const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
>(({ className, ...props }, ref) => (
<CommandPrimitive
ref={ref}
className={cn(
"anycontext-flex anycontext-h-full anycontext-w-full anycontext-flex-col anycontext-overflow-hidden anycontext-rounded-md anycontext-bg-white anycontext-text-stone-950 dark:anycontext-bg-stone-950 dark:anycontext-text-stone-50",
className,
)}
{...props}
/>
));
Command.displayName = CommandPrimitive.displayName;
interface CommandDialogProps extends DialogProps {}
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
return (
<Dialog {...props}>
<DialogContent className="anycontext-overflow-hidden anycontext-p-0 anycontext-shadow-lg">
<Command className="[&_[cmdk-group-heading]]:anycontext-px-2 [&_[cmdk-group-heading]]:anycontext-font-medium [&_[cmdk-group-heading]]:anycontext-text-stone-500 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:anycontext-pt-0 [&_[cmdk-group]]:anycontext-px-2 [&_[cmdk-input-wrapper]_svg]:anycontext-h-5 [&_[cmdk-input-wrapper]_svg]:anycontext-w-5 [&_[cmdk-input]]:anycontext-h-12 [&_[cmdk-item]]:anycontext-px-2 [&_[cmdk-item]]:anycontext-py-3 [&_[cmdk-item]_svg]:anycontext-h-5 [&_[cmdk-item]_svg]:anycontext-w-5 dark:[&_[cmdk-group-heading]]:anycontext-text-stone-400">
{children}
</Command>
</DialogContent>
</Dialog>
);
};
const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div
className="anycontext-flex anycontext-items-center anycontext-border-b anycontext-px-3"
cmdk-input-wrapper=""
>
<Search className="anycontext-mr-2 anycontext-h-4 anycontext-w-4 anycontext-shrink-0 anycontext-opacity-50" />
<CommandPrimitive.Input
ref={ref}
className={cn(
"anycontext-flex anycontext-h-11 anycontext-w-full anycontext-rounded-md anycontext-bg-transparent anycontext-py-3 anycontext-text-sm anycontext-outline-none placeholder:anycontext-text-stone-500 disabled:anycontext-cursor-not-allowed disabled:anycontext-opacity-50 dark:placeholder:anycontext-text-stone-400",
className,
)}
{...props}
/>
</div>
));
CommandInput.displayName = CommandPrimitive.Input.displayName;
const CommandList = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.List>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
>(({ className, ...props }, ref) => (
<CommandPrimitive.List
ref={ref}
className={cn(
"anycontext-max-h-[300px] anycontext-overflow-y-auto anycontext-overflow-x-hidden",
className,
)}
{...props}
/>
));
CommandList.displayName = CommandPrimitive.List.displayName;
const CommandEmpty = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => (
<CommandPrimitive.Empty
ref={ref}
className="anycontext-py-6 anycontext-text-center anycontext-text-sm"
{...props}
/>
));
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
const CommandGroup = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Group
ref={ref}
className={cn(
"anycontext-overflow-hidden anycontext-p-1 anycontext-text-stone-950 [&_[cmdk-group-heading]]:anycontext-px-2 [&_[cmdk-group-heading]]:anycontext-py-1.5 [&_[cmdk-group-heading]]:anycontext-text-xs [&_[cmdk-group-heading]]:anycontext-font-medium [&_[cmdk-group-heading]]:anycontext-text-stone-500 dark:anycontext-text-stone-50 dark:[&_[cmdk-group-heading]]:anycontext-text-stone-400",
className,
)}
{...props}
/>
));
CommandGroup.displayName = CommandPrimitive.Group.displayName;
const CommandSeparator = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Separator
ref={ref}
className={cn(
"anycontext--mx-1 anycontext-h-px anycontext-bg-stone-200 dark:anycontext-bg-stone-800",
className,
)}
{...props}
/>
));
CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
const CommandItem = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Item
ref={ref}
className={cn(
"anycontext-relative anycontext-flex anycontext-cursor-default anycontext-select-none anycontext-items-center anycontext-rounded-sm anycontext-px-2 anycontext-py-1.5 anycontext-text-sm anycontext-outline-none aria-selected:anycontext-bg-stone-100 aria-selected:anycontext-text-stone-900 data-[disabled]:anycontext-pointer-events-none data-[disabled]:anycontext-opacity-50 dark:aria-selected:anycontext-bg-stone-800 dark:aria-selected:anycontext-text-stone-50",
className,
)}
{...props}
/>
));
CommandItem.displayName = CommandPrimitive.Item.displayName;
const CommandShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"anycontext-ml-auto anycontext-text-xs anycontext-tracking-widest anycontext-text-stone-500 dark:anycontext-text-stone-400",
className,
)}
{...props}
/>
);
};
CommandShortcut.displayName = "CommandShortcut";
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
};

View file

@ -1,123 +0,0 @@
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X } from "lucide-react";
import { cn } from "../../lib/utils";
const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = DialogPrimitive.Portal;
const DialogClose = DialogPrimitive.Close;
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"anycontext-fixed anycontext-inset-0 anycontext-z-50 anycontext-bg-black/80 anycontext- data-[state=open]:anycontext-animate-in data-[state=closed]:anycontext-animate-out data-[state=closed]:anycontext-fade-out-0 data-[state=open]:anycontext-fade-in-0",
className,
)}
{...props}
/>
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"anycontext-text-black dark:anycontext-text-white anycontext-fixed anycontext-left-[50%] anycontext-top-[50%] anycontext-z-50 anycontext-grid anycontext-w-full anycontext-max-w-lg anycontext-translate-x-[-50%] anycontext-translate-y-[-50%] anycontext-gap-4 anycontext-border anycontext-border-stone-200 anycontext-bg-white anycontext-p-6 anycontext-shadow-lg anycontext-duration-200 data-[state=open]:anycontext-animate-in data-[state=closed]:anycontext-animate-out data-[state=closed]:anycontext-fade-out-0 data-[state=open]:anycontext-fade-in-0 data-[state=closed]:anycontext-zoom-out-95 data-[state=open]:anycontext-zoom-in-95 data-[state=closed]:anycontext-slide-out-to-left-1/2 data-[state=closed]:anycontext-slide-out-to-top-[48%] data-[state=open]:anycontext-slide-in-from-left-1/2 data-[state=open]:anycontext-slide-in-from-top-[48%] sm:anycontext-rounded-lg dark:anycontext-border-stone-800 dark:anycontext-bg-stone-950",
className,
)}
{...props}
>
{children}
<DialogPrimitive.Close className="anycontext-absolute anycontext-right-4 anycontext-top-4 anycontext-rounded-sm anycontext-opacity-70 anycontext-ring-offset-white anycontext-transition-opacity hover:anycontext-opacity-100 focus:anycontext-outline-none focus:anycontext-ring-2 focus:anycontext-ring-stone-950 focus:anycontext-ring-offset-2 disabled:anycontext-pointer-events-none data-[state=open]:anycontext-bg-stone-100 data-[state=open]:anycontext-text-stone-500 dark:anycontext-ring-offset-stone-950 dark:focus:anycontext-ring-stone-300 dark:data-[state=open]:anycontext-bg-stone-800 dark:data-[state=open]:anycontext-text-stone-400">
<X className="anycontext-h-4 anycontext-w-4" />
<span className="anycontext-sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
));
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"anycontext-flex anycontext-flex-col anycontext-space-y-1.5 anycontext-text-center sm:anycontext-text-left",
className,
)}
{...props}
/>
);
DialogHeader.displayName = "DialogHeader";
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"anycontext-flex anycontext-flex-col-reverse sm:anycontext-flex-row sm:anycontext-justify-end sm:anycontext-space-x-2",
className,
)}
{...props}
/>
);
DialogFooter.displayName = "DialogFooter";
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"anycontext-text-lg anycontext-font-semibold anycontext-leading-none anycontext-tracking-tight",
className,
)}
{...props}
/>
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn(
"anycontext-text-sm anycontext-text-stone-500 dark:anycontext-text-stone-400",
className,
)}
{...props}
/>
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
};

View file

@ -1,204 +0,0 @@
import * as React from "react";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { Check, ChevronRight, Circle } from "lucide-react";
import { cn } from "../../lib/utils";
const DropdownMenu = DropdownMenuPrimitive.Root;
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean;
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"anycontext-flex anycontext-cursor-default anycontext-select-none anycontext-items-center anycontext-rounded-sm anycontext-px-2 anycontext-py-1.5 anycontext-text-sm anycontext-outline-none focus:anycontext-bg-stone-100 data-[state=open]:anycontext-bg-stone-100 dark:focus:anycontext-bg-stone-800 dark:data-[state=open]:anycontext-bg-stone-800",
inset && "anycontext-pl-8",
className,
)}
{...props}
>
{children}
<ChevronRight className="anycontext-ml-auto anycontext-h-4 anycontext-w-4" />
</DropdownMenuPrimitive.SubTrigger>
));
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName;
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"anycontext-z-50 anycontext-min-w-[8rem] anycontext-overflow-hidden anycontext-rounded-md anycontext-border anycontext-border-stone-200 anycontext-bg-white anycontext-p-1 anycontext-text-stone-950 anycontext-shadow-lg data-[state=open]:anycontext-animate-in data-[state=closed]:anycontext-animate-out data-[state=closed]:anycontext-fade-out-0 data-[state=open]:anycontext-fade-in-0 data-[state=closed]:anycontext-zoom-out-95 data-[state=open]:anycontext-zoom-in-95 data-[side=bottom]:anycontext-slide-in-from-top-2 data-[side=left]:anycontext-slide-in-from-right-2 data-[side=right]:anycontext-slide-in-from-left-2 data-[side=top]:anycontext-slide-in-from-bottom-2 dark:anycontext-border-stone-800 dark:anycontext-bg-stone-950 dark:anycontext-text-stone-50",
className,
)}
{...props}
/>
));
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName;
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"anycontext-z-50 anycontext-min-w-[8rem] anycontext-overflow-hidden anycontext-rounded-md anycontext-border anycontext-border-stone-200 anycontext-bg-white anycontext-p-1 anycontext-text-stone-950 anycontext-shadow-md data-[state=open]:anycontext-animate-in data-[state=closed]:anycontext-animate-out data-[state=closed]:anycontext-fade-out-0 data-[state=open]:anycontext-fade-in-0 data-[state=closed]:anycontext-zoom-out-95 data-[state=open]:anycontext-zoom-in-95 data-[side=bottom]:anycontext-slide-in-from-top-2 data-[side=left]:anycontext-slide-in-from-right-2 data-[side=right]:anycontext-slide-in-from-left-2 data-[side=top]:anycontext-slide-in-from-bottom-2 dark:anycontext-border-stone-800 dark:anycontext-bg-stone-950 dark:anycontext-text-stone-50",
className,
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
));
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"anycontext-relative anycontext-flex anycontext-cursor-default anycontext-select-none anycontext-items-center anycontext-rounded-sm anycontext-px-2 anycontext-py-1.5 anycontext-text-sm anycontext-outline-none anycontext-transition-colors focus:anycontext-bg-stone-100 focus:anycontext-text-stone-900 data-[disabled]:anycontext-pointer-events-none data-[disabled]:anycontext-opacity-50 dark:focus:anycontext-bg-stone-800 dark:focus:anycontext-text-stone-50",
inset && "anycontext-pl-8",
className,
)}
{...props}
/>
));
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"anycontext-relative anycontext-flex anycontext-cursor-default anycontext-select-none anycontext-items-center anycontext-rounded-sm anycontext-py-1.5 anycontext-pl-8 anycontext-pr-2 anycontext-text-sm anycontext-outline-none anycontext-transition-colors focus:anycontext-bg-stone-100 focus:anycontext-text-stone-900 data-[disabled]:anycontext-pointer-events-none data-[disabled]:anycontext-opacity-50 dark:focus:anycontext-bg-stone-800 dark:focus:anycontext-text-stone-50",
className,
)}
checked={checked}
{...props}
>
<span className="anycontext-absolute anycontext-left-2 anycontext-flex anycontext-h-3.5 anycontext-w-3.5 anycontext-items-center anycontext-justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="anycontext-h-4 anycontext-w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
));
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName;
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"anycontext-relative anycontext-flex anycontext-cursor-default anycontext-select-none anycontext-items-center anycontext-rounded-sm anycontext-py-1.5 anycontext-pl-8 anycontext-pr-2 anycontext-text-sm anycontext-outline-none anycontext-transition-colors focus:anycontext-bg-stone-100 focus:anycontext-text-stone-900 data-[disabled]:anycontext-pointer-events-none data-[disabled]:anycontext-opacity-50 dark:focus:anycontext-bg-stone-800 dark:focus:anycontext-text-stone-50",
className,
)}
{...props}
>
<span className="anycontext-absolute anycontext-left-2 anycontext-flex anycontext-h-3.5 anycontext-w-3.5 anycontext-items-center anycontext-justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="anycontext-h-2 anycontext-w-2 anycontext-fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
));
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"anycontext-px-2 anycontext-py-1.5 anycontext-text-sm anycontext-font-semibold",
inset && "anycontext-pl-8",
className,
)}
{...props}
/>
));
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn(
"anycontext--mx-1 anycontext-my-1 anycontext-h-px anycontext-bg-stone-100 dark:anycontext-bg-stone-800",
className,
)}
{...props}
/>
));
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"anycontext-ml-auto anycontext-text-xs anycontext-tracking-widest anycontext-opacity-60",
className,
)}
{...props}
/>
);
};
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
};

View file

@ -1,25 +0,0 @@
import * as React from "react";
import { cn } from "../../lib/utils";
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"anycontext-flex anycontext-h-10 anycontext-w-full anycontext-rounded-md anycontext-border anycontext-border-stone-200 anycontext-bg-white anycontext-px-3 anycontext-py-2 anycontext-text-sm anycontext-ring-offset-white file:anycontext-border-0 file:anycontext-bg-transparent file:anycontext-text-sm file:anycontext-font-medium placeholder:anycontext-text-stone-500 focus-visible:anycontext-outline-none focus-visible:anycontext-ring-2 focus-visible:anycontext-ring-stone-950 focus-visible:anycontext-ring-offset-2 disabled:anycontext-cursor-not-allowed disabled:anycontext-opacity-50 dark:anycontext-border-stone-800 dark:anycontext-bg-stone-950 dark:anycontext-ring-offset-stone-950 dark:placeholder:anycontext-text-stone-400 dark:focus-visible:anycontext-ring-stone-300",
className,
)}
ref={ref}
{...props}
/>
);
},
);
Input.displayName = "Input";
export { Input };

View file

@ -1,29 +0,0 @@
import * as React from "react";
import * as PopoverPrimitive from "@radix-ui/react-popover";
import { cn } from "../../lib/utils";
const Popover = PopoverPrimitive.Root;
const PopoverTrigger = PopoverPrimitive.Trigger;
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"anycontext-z-50 anycontext-w-72 anycontext-rounded-md anycontext-border anycontext-border-stone-200 anycontext-bg-white anycontext-p-4 anycontext-text-stone-950 anycontext-shadow-md anycontext-outline-none data-[state=open]:anycontext-animate-in data-[state=closed]:anycontext-animate-out data-[state=closed]:anycontext-fade-out-0 data-[state=open]:anycontext-fade-in-0 data-[state=closed]:anycontext-zoom-out-95 data-[state=open]:anycontext-zoom-in-95 data-[side=bottom]:anycontext-slide-in-from-top-2 data-[side=left]:anycontext-slide-in-from-right-2 data-[side=right]:anycontext-slide-in-from-left-2 data-[side=top]:anycontext-slide-in-from-bottom-2 dark:anycontext-border-stone-800 dark:anycontext-bg-stone-950 dark:anycontext-text-stone-50",
className,
)}
{...props}
/>
</PopoverPrimitive.Portal>
));
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
export { Popover, PopoverTrigger, PopoverContent };

View file

@ -1,28 +0,0 @@
import * as React from "react";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { cn } from "../../lib/utils";
const TooltipProvider = TooltipPrimitive.Provider;
const Tooltip = TooltipPrimitive.Root;
const TooltipTrigger = TooltipPrimitive.Trigger;
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"anycontext-z-50 anycontext-overflow-hidden anycontext-rounded-md anycontext-border anycontext-border-stone-200 anycontext-bg-white anycontext-px-3 anycontext-py-1.5 anycontext-text-sm anycontext-text-stone-950 anycontext-shadow-md anycontext-animate-in anycontext-fade-in-0 anycontext-zoom-in-95 data-[state=closed]:anycontext-animate-out data-[state=closed]:anycontext-fade-out-0 data-[state=closed]:anycontext-zoom-out-95 data-[side=bottom]:anycontext-slide-in-from-top-2 data-[side=left]:anycontext-slide-in-from-right-2 data-[side=right]:anycontext-slide-in-from-left-2 data-[side=top]:anycontext-slide-in-from-bottom-2 dark:anycontext-border-stone-800 dark:anycontext-bg-stone-950 dark:anycontext-text-stone-50",
className,
)}
{...props}
/>
));
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };

View file

@ -1,45 +0,0 @@
window.addEventListener("message", (event) => {
if (event.source !== window) {
return;
}
const { jwt } = event.data;
if (jwt) {
if (
!(
window.location.hostname === "localhost" ||
window.location.hostname === "anycontext.dhr.wtf" ||
window.location.hostname === "supermemory.dhr.wtf"
)
) {
console.log(
"JWT is only allowed to be used on localhost or anycontext.dhr.wtf",
);
return;
}
chrome.storage.local.set({ jwt }, () => {});
}
});
const appContainer = document.createElement("div");
appContainer.id = "anycontext-app-container";
// First in the body, above the content
document.body.insertBefore(appContainer, document.body.firstChild);
appContainer.style.zIndex = "9999";
import ReactDOM from "react-dom/client";
import SideBar from "./SideBar";
// get JWT from local storage
const jwt = chrome.storage.local.get("jwt").then((data) => {
return data.jwt;
}) as Promise<string>;
jwt.then((token) => {
ReactDOM.createRoot(
document.getElementById("anycontext-app-container")!,
).render(<SideBar jwt={token} />);
});

View file

@ -1,85 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
.anycontext-combobox-button {
padding: 0.5rem 1rem;
display: flex;
flex-direction: row;
justify-items: center;
align-items: center;
gap: 0.5rem;
@apply anycontext-rounded-md dark:anycontext-bg-white/5 anycontext-bg-black/5;
}
.anycontext-overlay {
position: fixed;
top: 0;
left: 0;
min-height: 100vh;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 99998;
}
.anycontext-sidebar {
position: fixed;
top: 0;
right: 0;
min-height: 100vh;
width: 100%;
max-width: 31%; /* Responsive width */
z-index: 99999;
padding: 8px 16px; /* px-4 py-2 */
}
.anycontext-sidebar-content {
position: relative;
display: flex;
flex-direction: column;
height: 95vh;
background-color: white;
border-radius: 8px; /* rounded-lg */
padding: 8px; /* px-2 */
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06); /* shadow-md */
}
.anycontext-close-button {
position: absolute;
right: 0;
padding: 8px; /* p-2 */
border-radius: 4px; /* rounded-md */
margin: 8px; /* m-2 */
}
.anycontext-close-button:hover {
background-color: rgba(114, 87, 255, 0.5); /* hover:bg-[#7257ff]/50 */
color: white; /* hover:text-white */
}
.anycontext-open-button {
color: white;
background-color: #7257ff50; /* bg-indigo-600 */
background-opacity: 75%;
cursor: pointer;
padding: 8px; /* px-4 py-2 */
border-radius: 4px 0 0 4px; /* rounded-l-md */
display: flex;
align-items: center;
justify-content: space-between;
}
.anycontext-header {
margin: 16px; /* m-4 */
font-weight: 600; /* font-semibold */
font-size: 1.25rem; /* text-xl */
color: black;
}
.anycontext-icon {
height: 24px; /* h-6 */
width: 24px; /* w-6 */
}

View file

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

View file

@ -1,9 +0,0 @@
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.tsx";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);

View file

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

View file

@ -1,19 +0,0 @@
import { z } from "zod";
export const userObj = z.object({
message: z.string(),
data: z.object({
session: z.object({
sessionToken: z.string(),
userId: z.string(),
expires: z.string(),
}),
user: z.object({
id: z.string(),
name: z.string(),
email: z.string().nullable().optional(),
emailVerified: z.string().nullable(),
image: z.string().nullable().optional(),
}),
}),
});

View file

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

View file

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

View file

@ -1,42 +0,0 @@
import tailwindcssAnimate from "tailwindcss-animate";
/** @type {import('tailwindcss').Config} */
export default {
//darkMode: "prefe",
content: [
"./pages/**/*.{ts,tsx}",
"./components/**/*.{ts,tsx}",
"./app/**/*.{ts,tsx}",
"./src/**/*.{ts,tsx}",
"index.html",
],
prefix: "anycontext-",
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [tailwindcssAnimate],
corePlugins: {
preflight: false,
},
};

View file

@ -1,26 +0,0 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }],
"types": ["chrome"]
}

View file

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

View file

@ -1,27 +0,0 @@
import { Plugin, defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { crx } from "@crxjs/vite-plugin";
import manifest from "./manifest.json";
import path from "path";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const viteManifestHackIssue846: Plugin & {
renderCrxManifest: (manifest: unknown, bundle: any) => void;
} = {
// Workaround from https://github.com/crxjs/chrome-extension-tools/issues/846#issuecomment-1861880919.
name: "manifestHackIssue846",
renderCrxManifest(_manifest, bundle) {
bundle["manifest.json"] = bundle[".vite/manifest.json"];
bundle["manifest.json"].fileName = "manifest.json";
delete bundle[".vite/manifest.json"];
},
};
export default defineConfig({
plugins: [react(), crx({ manifest }), viteManifestHackIssue846],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
});

View file

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

View file

@ -1,40 +0,0 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
# wrangler files
.wrangler
.dev.vars

View file

@ -1,70 +0,0 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`c3`](https://developers.cloudflare.com/pages/get-started/c3).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
## Cloudflare integration
Besides the `dev` script mentioned above `c3` has added a few extra scripts that allow you to integrate the application with the [Cloudflare Pages](https://pages.cloudflare.com/) environment, these are:
- `pages:build` to build the application for Pages using the [`@cloudflare/next-on-pages`](https://github.com/cloudflare/next-on-pages) CLI
- `preview` to locally preview your Pages application using the [Wrangler](https://developers.cloudflare.com/workers/wrangler/) CLI
- `deploy` to deploy your Pages application using the [Wrangler](https://developers.cloudflare.com/workers/wrangler/) CLI
> **Note:** while the `dev` script is optimal for local development you should preview your Pages application as well (periodically or before deployments) in order to make sure that it can properly work in the Pages environment (for more details see the [`@cloudflare/next-on-pages` recommended workflow](https://github.com/cloudflare/next-on-pages/blob/05b6256/internal-packages/next-dev/README.md#recommended-workflow))
### Bindings
Cloudflare [Bindings](https://developers.cloudflare.com/pages/functions/bindings/) are what allows you to interact with resources available in the Cloudflare Platform.
You can use bindings during development, when previewing locally your application and of course in the deployed application:
- To use bindings in dev mode you need to define them in the `next.config.js` file under `setupDevBindings`, this mode uses the `next-dev` `@cloudflare/next-on-pages` submodule. For more details see its [documentation](https://github.com/cloudflare/next-on-pages/blob/05b6256/internal-packages/next-dev/README.md).
- To use bindings in the preview mode you need to add them to the `pages:preview` script accordingly to the `wrangler pages dev` command. For more details see its [documentation](https://developers.cloudflare.com/workers/wrangler/commands/#dev-1) or the [Pages Bindings documentation](https://developers.cloudflare.com/pages/functions/bindings/).
- To use bindings in the deployed application you will need to configure them in the Cloudflare [dashboard](https://dash.cloudflare.com/). For more details see the [Pages Bindings documentation](https://developers.cloudflare.com/pages/functions/bindings/).
#### KV Example
`c3` has added for you an example showing how you can use a KV binding.
In order to enable the example:
- Search for javascript/typescript lines containing the following comment:
```ts
// KV Example:
```
and uncomment the commented lines below it.
- Do the same in the `wrangler.toml` file, where
the comment is:
```
# KV Example:
```
- If you're using TypeScript run the `cf-typegen` script to update the `env.d.ts` file:
```bash
npm run cf-typegen
# or
yarn cf-typegen
# or
pnpm cf-typegen
# or
bun cf-typegen
```
After doing this you can run the `dev` or `preview` script and visit the `/api/hello` route to see the example in action.
Finally, if you also want to see the example work in the deployed application make sure to add a `MY_KV_NAMESPACE` binding to your Pages application in its [dashboard kv bindings settings section](https://dash.cloudflare.com/?to=/:account/pages/view/:pages-project/settings/functions#kv_namespace_bindings_section). After having configured it make sure to re-deploy your application.

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

@ -1,47 +0,0 @@
// Generated by Wrangler
// by running `wrangler types --env-interface CloudflareEnv env.d.ts`
interface CloudflareEnv {
RATELIMITER: RateLimitBinding;
}
export interface RateLimitBinding {
limit: LimitFunc;
}
export interface LimitFunc {
(options: LimitOptions): Promise<RateLimitResult>;
}
interface RateLimitResult {
success: boolean;
}
export interface LimitOptions {
key: string;
}
export interface RateLimitResponse {
key: string;
success: boolean;
}
export interface RateLimitOptions {
continueOnRateLimit: boolean;
}
export type RateLimitKeyFunc = {
(c: Context): string;
};
declare global {
namespace NodeJS {
interface ProcessEnv {
// [key: string]: string | undefined;
RATELIMITER: RateLimitBinding;
CLOUDFLARE_TURNSTILE_TOKEN: string;
}
}
}
export {};

View file

@ -1,13 +0,0 @@
import { setupDevPlatform } from "@cloudflare/next-on-pages/next-dev";
// Here we use the @cloudflare/next-on-pages next-dev module to allow us to use bindings during local development
// (when running the application with `next dev`), for more information see:
// https://github.com/cloudflare/next-on-pages/blob/5712c57ea7/internal-packages/next-dev/README.md
if (process.env.NODE_ENV === "development") {
await setupDevPlatform();
}
/** @type {import('next').NextConfig} */
const nextConfig = {};
export default nextConfig;

View file

@ -1,42 +0,0 @@
{
"name": "web-v2",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"pages:build": "bunx @cloudflare/next-on-pages",
"preview": "bun pages:build && wrangler pages dev",
"deploy": "bun pages:build && wrangler pages deploy",
"cf-typegen": "wrangler types --env-interface CloudflareEnv env.d.ts"
},
"dependencies": {
"@radix-ui/react-toast": "^1.1.5",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"lucide-react": "^0.378.0",
"next": "14.1.0",
"react": "^18",
"react-dom": "^18",
"tailwind-merge": "^2.3.0",
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@cloudflare/next-on-pages": "1",
"@cloudflare/workers-types": "^4.20240512.0",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"autoprefixer": "^10.0.1",
"eslint": "^8",
"eslint-config-next": "14.1.0",
"eslint-plugin-next-on-pages": "^1.11.3",
"postcss": "^8",
"tailwindcss": "^3.3.0",
"typescript": "^5",
"vercel": "^34.2.0",
"wrangler": "^3.57.0"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

View file

@ -1,19 +0,0 @@
{
"name": "Supermemory - your second brain.",
"short_name": "Supermemory",
"icons": [
{
"src": "/icons/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icons/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

View file

@ -1,102 +0,0 @@
import RotatingIcons from "./RotatingIcons";
import Hero from "./Hero";
import Navbar from "./Navbar";
import Cta from "./Cta";
import { Toaster } from "@/components/ui/toaster";
import Features from "./Features";
import Footer from "./footer";
import { Metadata } from "next";
export const runtime = "edge";
export const metadata: Metadata = {
title: "Supermemory - Your personal second brain.",
description:
"Bring saved information from all over the internet into one place where you can connect it, and ask AI about it",
openGraph: {
images: [
{
url: "https://supermemory.ai/og-image.png",
width: 1200,
height: 627,
alt: "Supermemory - Your personal second brain.",
},
],
},
metadataBase: {
host: "https://supermemory.ai",
href: "/",
origin: "https://supermemory.ai",
password: "supermemory",
hash: "supermemory",
pathname: "/",
search: "",
username: "supermemoryai",
hostname: "supermemory.ai",
port: "",
protocol: "https:",
searchParams: new URLSearchParams(""),
toString: () => "https://supermemory.ai/",
toJSON: () => "https://supermemory.ai/",
},
twitter: {
card: "summary_large_image",
site: "https://supermemory.ai",
creator: "https://supermemory.ai",
title: "Supermemory - Your personal second brain.",
description:
"Bring saved information from all over the internet into one place where you can connect it, and ask AI about it",
images: [
{
url: "https://supermemory.ai/og-image.png",
width: 1200,
height: 627,
alt: "Supermemory - Your personal second brain.",
},
],
},
};
export default function Home() {
return (
<main className="dark flex min-h-screen flex-col items-center overflow-x-hidden px-2 md:px-0">
<Navbar />
{/* Background gradients */}
<div className="absolute left-0 top-0 z-[-1] h-full w-full">
<div className="overflow-x-hidden">
<div
className="absolute left-0 h-32 w-[95%] overflow-x-hidden bg-[#369DFD] bg-opacity-70 blur-[337.4px]"
style={{ transform: "rotate(-30deg)" }}
/>
</div>
{/* a blue gradient line that's slightly tilted with blur (a lotof blur)*/}
<div className="overflow-x-hidden">
<div
className="absolute left-0 top-[100%] h-32 w-[90%] overflow-x-hidden bg-[#369DFD] bg-opacity-40 blur-[337.4px]"
style={{ transform: "rotate(-30deg)" }}
/>
</div>
<div className="overflow-x-hidden">
<div className="absolute right-0 top-[145%] h-40 w-[17%] overflow-x-hidden bg-[#369DFD] bg-opacity-20 blur-[110px]" />
</div>
</div>
{/* Hero section */}
<Hero />
{/* Features section */}
<Features />
<RotatingIcons />
<Cta />
<Toaster />
<Footer />
</main>
);
}

View file

@ -1,132 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--foreground-rgb: 255, 255, 255;
--background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0;
--black-bg: #0f1114;
--soft-foreground: #ffffff;
--soft-foreground-text: #b2bcca;
--background: 216, 14%, 7%;
--foreground: 240 10% 3.9%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--ring: 240 10% 3.9%;
--radius: 0.5rem;
}
.dark {
--background: 216, 14%, 7%;
--foreground: 0 0% 98%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
html {
scroll-behavior: smooth;
}
/* width */
::-webkit-scrollbar {
width: 8px;
}
/* Track */
::-webkit-scrollbar-track {
background: transparent;
}
/* Handle */
::-webkit-scrollbar-thumb {
background: #131f2c;
}
/* Handle on hover */
::-webkit-scrollbar-thumb:hover {
background: #22303d;
}
body {
color: rgb(var(--foreground-rgb));
background: linear-gradient(to bottom, transparent, var(--black-bg))
var(--black-bg);
}
@layer utilities {
.text-balance {
text-wrap: balance;
}
}
@keyframes rotate {
0% {
transform: rotate(0deg) translateX(130px); /* Adjust radius */
}
100% {
transform: rotate(360deg) translateX(130px); /* Adjust radius */
}
}
.icon-container {
animation: rotate 10s linear infinite;
}

View file

@ -1,25 +0,0 @@
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import Script from "next/script";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Supermemory - Your personal second brain.",
description:
"Bring saved information from all over the internet into one place where you can connect it, and ask AI about it",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<Script src="https://challenges.cloudflare.com/turnstile/v0/api.js" />
<body className={inter.className}>{children}</body>
</html>
);
}

View file

@ -1,58 +0,0 @@
export const runtime = "edge";
export default function NotFound() {
return (
<>
<title>404: This page could not be found.</title>
<div style={styles.error}>
<div>
<style
dangerouslySetInnerHTML={{
__html: `body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}`,
}}
/>
<h1 className="next-error-h1" style={styles.h1}>
404
</h1>
<div style={styles.desc}>
<h2 style={styles.h2}>This page could not be found.</h2>
</div>
</div>
</div>
</>
);
}
const styles = {
error: {
fontFamily:
'system-ui,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"',
height: "100vh",
textAlign: "center",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
},
desc: {
display: "inline-block",
},
h1: {
display: "inline-block",
margin: "0 20px 0 0",
padding: "0 23px 0 0",
fontSize: 24,
fontWeight: 500,
verticalAlign: "top",
lineHeight: "49px",
},
h2: {
fontSize: 14,
fontWeight: 400,
lineHeight: "49px",
margin: 0,
},
} as const;

View file

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

View file

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

View file

@ -1,26 +0,0 @@
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}

View file

@ -1,94 +0,0 @@
#:schema node_modules/wrangler/config-schema.json
name = "web-v2"
compatibility_date = "2024-05-12"
compatibility_flags = ["nodejs_compat"]
pages_build_output_dir = ".vercel/output/static"
# [[unsafe.bindings]]
# name = "RATELIMITER"
# type = "ratelimit"
# namespace_id = "1011"
# simple = { limit = 25, period = 10 }
# Automatically place your workloads in an optimal location to minimize latency.
# If you are running back-end logic in a Pages Function, running it closer to your back-end infrastructure
# rather than the end user may result in better performance.
# Docs: https://developers.cloudflare.com/pages/functions/smart-placement/#smart-placement
# [placement]
# mode = "smart"
# Variable bindings. These are arbitrary, plaintext strings (similar to environment variables)
# Docs:
# - https://developers.cloudflare.com/pages/functions/bindings/#environment-variables
# Note: Use secrets to store sensitive data.
# - https://developers.cloudflare.com/pages/functions/bindings/#secrets
# [vars]
# MY_VARIABLE = "production_value"
# Bind the Workers AI model catalog. Run machine learning models, powered by serverless GPUs, on Cloudflares global network
# Docs: https://developers.cloudflare.com/pages/functions/bindings/#workers-ai
# [ai]
# binding = "AI"
# Bind a D1 database. D1 is Cloudflares native serverless SQL database.
# Docs: https://developers.cloudflare.com/pages/functions/bindings/#d1-databases
# [[d1_databases]]
# binding = "MY_DB"
# database_name = "my-database"
# database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
# Bind a Durable Object. Durable objects are a scale-to-zero compute primitive based on the actor model.
# Durable Objects can live for as long as needed. Use these when you need a long-running "server", such as in realtime apps.
# Docs: https://developers.cloudflare.com/workers/runtime-apis/durable-objects
# [[durable_objects.bindings]]
# name = "MY_DURABLE_OBJECT"
# class_name = "MyDurableObject"
# script_name = 'my-durable-object'
# Bind a KV Namespace. Use KV as persistent storage for small key-value pairs.
# Docs: https://developers.cloudflare.com/pages/functions/bindings/#kv-namespaces
# KV Example:
# [[kv_namespaces]]
# binding = "MY_KV_NAMESPACE"
# id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# Bind a Queue producer. Use this binding to schedule an arbitrary task that may be processed later by a Queue consumer.
# Docs: https://developers.cloudflare.com/pages/functions/bindings/#queue-producers
# [[queues.producers]]
# binding = "MY_QUEUE"
# queue = "my-queue"
# Bind an R2 Bucket. Use R2 to store arbitrarily large blobs of data, such as files.
# Docs: https://developers.cloudflare.com/pages/functions/bindings/#r2-buckets
# [[r2_buckets]]
# binding = "MY_BUCKET"
# bucket_name = "my-bucket"
# Bind another Worker service. Use this binding to call another Worker without network overhead.
# Docs: https://developers.cloudflare.com/pages/functions/bindings/#service-bindings
# [[services]]
# binding = "MY_SERVICE"
# service = "my-service"
# To use different bindings for preview and production environments, follow the examples below.
# When using environment-specific overrides for bindings, ALL bindings must be specified on a per-environment basis.
# Docs: https://developers.cloudflare.com/pages/functions/wrangler-configuration#environment-specific-overrides
######## PREVIEW environment config ########
# [env.preview.vars]
# API_KEY = "xyz789"
# [[env.preview.kv_namespaces]]
# binding = "MY_KV_NAMESPACE"
# id = "<PREVIEW_NAMESPACE_ID>"
######## PRODUCTION environment config ########
# [env.production.vars]
# API_KEY = "abc123"
# [[env.production.kv_namespaces]]
# binding = "MY_KV_NAMESPACE"
# id = "<PRODUCTION_NAMESPACE_ID>"

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

@ -0,0 +1,9 @@
/** @type {import("eslint").Linter.Config} */
module.exports = {
root: true,
extends: ["@repo/eslint-config/next.js"],
parser: "@typescript-eslint/parser",
parserOptions: {
project: true,
},
};

View file

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

41
apps/web/.gitignore vendored
View file

@ -1,41 +0,0 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
.next/
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
# wrangler files
.wrangler
.dev.vars

View file

@ -1,70 +1,28 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`c3`](https://developers.cloudflare.com/pages/get-started/c3).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
## Cloudflare integration
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
Besides the `dev` script mentioned above `c3` has added a few extra scripts that allow you to integrate the application with the [Cloudflare Pages](https://pages.cloudflare.com/) environment, these are:
To create [API routes](https://nextjs.org/docs/app/building-your-application/routing/router-handlers) add an `api/` directory to the `app/` directory with a `route.ts` file. For individual endpoints, create a subfolder in the `api` directory, like `api/hello/route.ts` would map to [http://localhost:3000/api/hello](http://localhost:3000/api/hello).
- `pages:build` to build the application for Pages using the [`@cloudflare/next-on-pages`](https://github.com/cloudflare/next-on-pages) CLI
- `preview` to locally preview your Pages application using the [Wrangler](https://developers.cloudflare.com/workers/wrangler/) CLI
- `deploy` to deploy your Pages application using the [Wrangler](https://developers.cloudflare.com/workers/wrangler/) CLI
## Learn More
> **Note:** while the `dev` script is optimal for local development you should preview your Pages application as well (periodically or before deployments) in order to make sure that it can properly work in the Pages environment (for more details see the [`@cloudflare/next-on-pages` recommended workflow](https://github.com/cloudflare/next-on-pages/blob/05b6256/internal-packages/next-dev/README.md#recommended-workflow))
To learn more about Next.js, take a look at the following resources:
### Bindings
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn/foundations/about-nextjs) - an interactive Next.js tutorial.
Cloudflare [Bindings](https://developers.cloudflare.com/pages/functions/bindings/) are what allows you to interact with resources available in the Cloudflare Platform.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
You can use bindings during development, when previewing locally your application and of course in the deployed application:
## Deploy on Vercel
- To use bindings in dev mode you need to define them in the `next.config.js` file under `setupDevBindings`, this mode uses the `next-dev` `@cloudflare/next-on-pages` submodule. For more details see its [documentation](https://github.com/cloudflare/next-on-pages/blob/05b6256/internal-packages/next-dev/README.md).
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_source=github.com&utm_medium=referral&utm_campaign=turborepo-readme) from the creators of Next.js.
- To use bindings in the preview mode you need to add them to the `pages:preview` script accordingly to the `wrangler pages dev` command. For more details see its [documentation](https://developers.cloudflare.com/workers/wrangler/commands/#dev-1) or the [Pages Bindings documentation](https://developers.cloudflare.com/pages/functions/bindings/).
- To use bindings in the deployed application you will need to configure them in the Cloudflare [dashboard](https://dash.cloudflare.com/). For more details see the [Pages Bindings documentation](https://developers.cloudflare.com/pages/functions/bindings/).
#### KV Example
`c3` has added for you an example showing how you can use a KV binding.
In order to enable the example:
- Search for javascript/typescript lines containing the following comment:
```ts
// KV Example:
```
and uncomment the commented lines below it.
- Do the same in the `wrangler.toml` file, where
the comment is:
```
# KV Example:
```
- If you're using TypeScript run the `build-cf-types` script to update the `env.d.ts` file:
```bash
npm run build-cf-types
# or
yarn build-cf-types
# or
pnpm build-cf-types
# or
bun build-cf-types
```
After doing this you can run the `dev` or `preview` script and visit the `/api/hello` route to see the example in action.
Finally, if you also want to see the example work in the deployed application make sure to add a `MY_KV_NAMESPACE` binding to your Pages application in its [dashboard kv bindings settings section](https://dash.cloudflare.com/?to=/:account/pages/view/:pages-project/settings/functions#kv_namespace_bindings_section). After having configured it make sure to re-deploy your application.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

View file

@ -0,0 +1,15 @@
"use client";
import { Button } from "@repo/ui/src/button";
import React from "react";
import { signIn } from "../helpers/server/auth";
function SignIn() {
return (
<Button className="mt-4" onClick={async () => await signIn("google")}>
Login with Google
</Button>
);
}
export default SignIn;

View file

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

View file

@ -2,7 +2,7 @@
import { FormEvent, useState } from "react";
import formSubmitAction from "./formSubmitAction";
import { useToast } from "@/components/ui/use-toast";
import { useToast } from "@repo/ui/src/shadcn/use-toast";
function EmailInput() {
const [email, setEmail] = useState("");

View file

@ -1,13 +1,11 @@
"use client";
import { useState, useRef, useEffect } from "react";
import { Transition } from "@headlessui/react";
import Image from "next/image";
import CarouselIllustration from "@/../public/images/carousel-illustration-01.png";
import { X } from "@/utils/icons";
import { X } from "@repo/ui/src/components/icons";
import { features } from "./FeatureContent";
import { CardClick } from "@/components/ui/cardClick";
import { CardClick } from "@repo/ui/src/components/cardClick";
export default function Features() {
const [tab, setTab] = useState<number>(0);
@ -65,71 +63,62 @@ export default function Features() {
<div className="relative lg:max-w-none">
<div className="relative flex flex-col" ref={tabs}>
{/* Item 1 */}
<Transition
show={tab === 0}
enter="transition ease-in-out duration-700 transform order-first"
enterFrom="opacity-0 -translate-y-4"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in-out duration-300 transform absolute"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-4"
beforeEnter={() => heightFix()}
unmount={false}
<div
className="transition-all duration-700 transform order-first"
style={{
height: tab === 0 ? "auto" : 0,
opacity: tab === 0 ? 1 : 0,
// transform: tab === 0 ? 'translateY(0)' : 'translateY(4rem)',
}}
>
<div>
<Image
className="mx-auto rounded-lg shadow-2xl lg:max-w-none"
src={CarouselIllustration}
src={"/images/carousel-illustration-01.png"}
width={700}
height={520}
alt="Carousel 01"
/>
</div>
</Transition>
</div>
{/* Item 2 */}
<Transition
show={tab === 1}
enter="transition ease-in-out duration-700 transform order-first"
enterFrom="opacity-0 -translate-y-4"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in-out duration-300 transform absolute"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-4"
beforeEnter={() => heightFix()}
unmount={false}
<div
className="transition-all duration-700 transform order-first"
style={{
height: tab === 1 ? "auto" : 0,
opacity: tab === 1 ? 1 : 0,
transform: tab === 1 ? "translateY(0)" : "translateY(4rem)",
}}
>
<div>
<Image
className="mx-auto rounded-lg shadow-2xl lg:max-w-none"
src={CarouselIllustration}
src={"/images/carousel-illustration-01.png"}
width={700}
height={520}
alt="Carousel 02"
/>
</div>
</Transition>
</div>
{/* Item 3 */}
<Transition
show={tab === 2}
enter="transition ease-in-out duration-700 transform order-first"
enterFrom="opacity-0 -translate-y-4"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in-out duration-300 transform absolute"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-4"
beforeEnter={() => heightFix()}
unmount={false}
<div
className="transition-all duration-700 transform order-first"
style={{
height: tab === 2 ? "auto" : 0,
opacity: tab === 2 ? 1 : 0,
transform: tab === 2 ? "translateY(0)" : "translateY(4rem)",
}}
>
<div>
<Image
className="mx-auto rounded-lg shadow-2xl lg:max-w-none"
src={CarouselIllustration}
src={"/images/carousel-illustration-01.png"}
width={700}
height={520}
alt="Carousel 03"
/>
</div>
</Transition>
</div>
</div>
</div>
</div>

View file

@ -1,7 +1,7 @@
"use client";
import React from "react";
import { motion } from "framer-motion";
import { Twitter } from "@/utils/icons";
import { Twitter } from "@repo/ui/src/components/icons";
import EmailInput from "./EmailInput";
import LinkArrow from "./linkArrow";
@ -39,7 +39,7 @@ function Hero() {
...slap,
transition: { ...slap.transition, delay: 0.2 },
}}
className="text-center text-4xl font-semibold tracking-normal text-white/95 md:text-5xl"
className="text-center text-4xl font-semibold tracking-tight text-white/95 md:text-5xl"
>
Build your own second brain with Supermemory
</motion.h1>

View file

@ -1,5 +1,6 @@
import Logo from "@/../public/logo.svg";
import { Github } from "@/utils/icons";
import { ArrowRightIcon } from "@radix-ui/react-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";
@ -24,10 +25,10 @@ function Navbar() {
</div>
<Link
href="https://git.new/memory"
className="m-2 flex items-center gap-3 rounded-xl bg-white/20 px-4 text-center text-white"
className="m-2 flex items-center gap-3 rounded-xl bg-white/20 px-4 text-center text-white group"
>
<Github className="h-4 w-4" />
Open source
Login
<ArrowRightIcon className="h-4 w-4 group-hover:translate-x-1 duration-100 ease-in-out" />
</Link>
</div>
</nav>

View file

@ -1,7 +1,13 @@
"use client";
import { motion } from "framer-motion";
import { Github, Medium, Notion, Reddit, Twitter } from "@/utils/icons";
import {
Github,
Medium,
Notion,
Reddit,
Twitter,
} from "@repo/ui/src/components/icons";
import Image from "next/image";
const icons = [

View file

@ -9,6 +9,7 @@ const formSubmitAction = async (email: string, token: string) => {
if (ip) {
if (process.env.RATELIMITER) {
// @ts-ignore
const { success } = await process.env.RATELIMITER.limit({
key: `waitlist-${ip}`,
});

View file

@ -0,0 +1,63 @@
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";
import { redirect } from "next/navigation";
export const runtime = "edge";
export default async function Home() {
const user = await auth();
console.log(user);
if (user) {
// await redirect("/home")
}
return (
<main className="flex min-h-screen flex-col items-center overflow-x-hidden px-2 md:px-0">
<Navbar />
{/* Background gradients */}
<div className="absolute left-0 top-0 z-[-1] h-full w-full">
<div className="overflow-x-hidden">
<div
className="absolute left-0 h-32 w-[95%] overflow-x-hidden bg-[#369DFD] bg-opacity-70 blur-[337.4px]"
style={{ transform: "rotate(-30deg)" }}
/>
</div>
{/* a blue gradient line that's slightly tilted with blur (a lotof blur)*/}
<div className="overflow-x-hidden">
<div
className="absolute left-0 top-[100%] h-32 w-[90%] overflow-x-hidden bg-[#369DFD] bg-opacity-40 blur-[337.4px]"
style={{ transform: "rotate(-30deg)" }}
/>
</div>
<div className="overflow-x-hidden">
<div className="absolute right-0 top-[145%] h-40 w-[17%] overflow-x-hidden bg-[#369DFD] bg-opacity-20 blur-[110px]" />
</div>
</div>
{/* Hero section */}
<Hero />
{/* Features section */}
<Features />
<RotatingIcons />
<Cta />
<Toaster />
<Footer />
</main>
);
}

View file

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

View file

@ -0,0 +1,33 @@
import { NextRequest } from "next/server";
import { db } from "../helpers/server/db";
import { sessions, users } from "../helpers/server/db/schema";
import { eq } from "drizzle-orm";
export async function ensureAuth(req: NextRequest) {
// A helper function to protect routes
const token =
req.cookies.get("next-auth.session-token")?.value ??
req.cookies.get("__Secure-authjs.session-token")?.value ??
req.cookies.get("authjs.session-token")?.value ??
req.headers.get("Authorization")?.replace("Bearer ", "");
if (!token) {
return undefined;
}
const sessionData = await db
.select()
.from(sessions)
.innerJoin(users, eq(users.id, sessions.userId))
.where(eq(sessions.sessionToken, token!));
if (!sessionData || sessionData.length < 0) {
return undefined;
}
return {
user: sessionData[0]!.user,
session: sessionData[0]!,
};
}

View file

@ -0,0 +1,56 @@
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import type { NextRequest } from "next/server";
import { ensureAuth } from "../ensureAuth";
export const runtime = "edge";
export async function PUT(request: NextRequest) {
const d = await ensureAuth(request);
if (!d) {
return new Response("Unauthorized", { status: 401 });
}
const reqUrl = new URL(request.url);
const filename = reqUrl.searchParams.get("filename");
if (!filename) {
return new Response("Missing filename", { status: 400 });
}
if (
!process.env.R2_ENDPOINT ||
!process.env.R2_ACCESS_ID ||
!process.env.R2_SECRET_KEY ||
!process.env.R2_BUCKET_NAME
) {
return new Response(
"Missing one or more R2 env variables: R2_ENDPOINT, R2_ACCESS_ID, R2_SECRET_KEY, R2_BUCKET_NAME. To get them, go to the R2 console, create and paste keys in a `.dev.vars` file in the root of this project.",
{ status: 500 },
);
}
const s3 = new S3Client({
region: "auto",
endpoint: process.env.R2_ENDPOINT,
credentials: {
accessKeyId: process.env.R2_ACCESS_ID,
secretAccessKey: process.env.R2_SECRET_KEY,
},
});
const url = await getSignedUrl(
s3,
new PutObjectCommand({ Bucket: process.env.R2_BUCKET_NAME, Key: filename }),
{ expiresIn: 3600 },
);
return new Response(JSON.stringify({ url }), {
status: 200,
headers: {
"Content-Type": "application/json",
},
});
}

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

@ -0,0 +1,48 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
/* :root {
--foreground-rgb: 0, 0, 0;
--background-start-rgb: 214, 219, 220;
--background-end-rgb: 255, 255, 255;
} */
@media (prefers-color-scheme: dark) {
:root {
--foreground: rgba(179, 188, 197, 1);
--foreground-menu: rgba(106, 115, 125, 1);
--background: rgba(23, 27, 31, 1);
--secondary: rgba(31, 36, 40, 1);
--primary: rgba(54, 157, 253, 1);
--border: rgba(51, 57, 67, 1);
}
}
body {
color: var(--foreground);
background: var(--background);
font-size: 14px;
}
@layer base {
.all-center {
display: flex;
align-items: center;
justify-content: center;
}
}
@layer utilities {
.text-balance {
text-wrap: balance;
}
}
.gradient-background {
background: linear-gradient(
150deg,
rgba(255, 255, 255, 0.1) 0%,
rgba(255, 255, 255, 0)
);
}

View file

@ -0,0 +1,11 @@
// Theming that works perfectly with app router (no flicker, jumps etc!)
import dynamic from "next/dynamic";
// Don't SSR the toggle since the value on the server will be different than the client
export const getThemeToggler = () =>
dynamic(() => import("@repo/ui/src/shadcn/theme-toggle"), {
ssr: false,
// Make sure to code a placeholder so the UI doesn't jump when the component loads
loading: () => <div className="w-6 h-6" />,
});

View file

@ -0,0 +1,25 @@
import { isRedirectError } from "next/dist/client/components/redirect";
import { toast } from "sonner";
import { z } from "zod";
export function getErrorMessage(err: unknown) {
const unknownError = "Something went wrong, please try again later.";
if (err instanceof z.ZodError) {
const errors = err.issues.map((issue) => {
return issue.message;
});
return errors.join("\n");
} else if (err instanceof Error) {
return err.message;
} else if (isRedirectError(err)) {
throw err;
} else {
return unknownError;
}
}
export function showErrorToast(err: unknown) {
const errorMessage = getErrorMessage(err);
return toast.error(errorMessage);
}

View file

@ -1,15 +1,15 @@
import { env } from "@/env";
import NextAuth from "next-auth";
import NextAuth, { NextAuthResult } from "next-auth";
import Google from "next-auth/providers/google";
import { DrizzleAdapter } from "@auth/drizzle-adapter";
import { db } from "./db";
export const {
handlers: { GET, POST },
signIn,
signOut,
auth,
} = NextAuth({
secret: env.NEXTAUTH_SECRET,
trustHost: true,
secret: process.env.NEXTAUTH_SECRET ?? process.env.AUTH_SECRET,
callbacks: {
session: ({ session, token, user }) => ({
...session,
@ -22,8 +22,8 @@ export const {
adapter: DrizzleAdapter(db),
providers: [
Google({
clientId: env.GOOGLE_CLIENT_ID,
clientSecret: env.GOOGLE_CLIENT_SECRET,
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
}),
],
});

View file

@ -6,7 +6,6 @@ import {
sqliteTableCreator,
text,
integer,
unique,
} from "drizzle-orm/sqlite-core";
export const createTable = sqliteTableCreator((name) => `${name}`);
@ -90,6 +89,7 @@ export const storedContent = createTable(
url: text("url").notNull(),
savedAt: int("savedAt", { mode: "timestamp" }).notNull(),
baseUrl: text("baseUrl", { length: 255 }),
ogImage: text("ogImage", { length: 255 }),
type: text("type", { enum: ["note", "page", "twitter-bookmark"] }).default(
"page",
),

View file

@ -0,0 +1,34 @@
import React from "react";
import Image from "next/image";
import Link from "next/link";
import Logo from "../../public/logo.svg";
import { AddIcon, ChatIcon } from "@repo/ui/src/icons";
function Header() {
return (
<div>
<div className="flex items-center justify-between relative z-10">
<Link href="/">
<Image
src={Logo}
alt="SuperMemory logo"
className="hover:brightness-125 duration-200"
/>
</Link>
<div className="absolute flex justify-center w-full -z-10">
<button className="bg-secondary all-center h-11 rounded-full p-2 min-w-14">
<Image src={AddIcon} alt="Add icon" />
</button>
</div>
<button className="flex shrink-0 duration-200 items-center gap-2 px-2 py-1.5 rounded-xl hover:bg-secondary">
<Image src={ChatIcon} alt="Chat icon" />
Start new chat
</button>
</div>
</div>
);
}
export default Header;

View file

@ -0,0 +1,50 @@
import React from "react";
import Image from "next/image";
import Link from "next/link";
import { MemoriesIcon, ExploreIcon, HistoryIcon } from "@repo/ui/src/icons";
function Menu() {
const menuItems = [
{
icon: MemoriesIcon,
text: "Memories",
url: "/",
},
{
icon: ExploreIcon,
text: "Explore",
url: "/explore",
},
{
icon: HistoryIcon,
text: "History",
url: "/history",
},
];
return (
<div className="absolute h-full p-4 flex items-center top-0 left-0">
<div className="">
<div className="hover:rounded-2x group inline-flex w-14 text-foreground-menu text-[15px] font-medium flex-col items-start gap-6 overflow-hidden rounded-[28px] bg-secondary px-3 py-4 duration-200 hover:w-40">
{menuItems.map((item) => (
<div
key={item.url}
className="flex w-full cursor-pointer items-center gap-3 px-1 duration-200 hover:scale-105 hover:brightness-150 active:scale-90"
>
<Image
src={item.icon}
alt={`${item.text} icon`}
className="hover:brightness-125 duration-200"
/>
<p className="opacity-0 duration-200 group-hover:opacity-100">
{item.text}
</p>
</div>
))}
</div>
</div>
</div>
);
}
export default Menu;

View file

@ -0,0 +1,25 @@
import React from "react";
import Menu from "./menu";
import Header from "./header";
import QueryInput from "./queryinput";
function Page() {
return (
<main className="h-screen flex flex-col p-4 relative">
<Menu />
<Header />
<div className="max-w-3xl flex mx-auto w-full flex-col">
{/* all content goes here */}
<div className=""></div>
<div className="w-full h-96">
<QueryInput />
</div>
</div>
</main>
);
}
export default Page;

View file

@ -0,0 +1,43 @@
import { ArrowRightIcon, MemoriesIcon, SelectIcon } from "@repo/ui/src/icons";
import Image from "next/image";
import React from "react";
import Divider from "@repo/ui/src/shadcn/divider";
function QueryInput() {
return (
<div className="bg-secondary h- [68px] rounded-[24px] w-full mt-40">
{/* input and action button */}
<div className="flex gap-4 p-3">
<textarea
name="query-input"
cols={30}
rows={7}
className="bg-transparent pt-2.5 text-base text-[#989EA4] focus:text-foreground duration-200 tracking-[3%] outline-none resize-none w-full p-1"
placeholder="Ask your second brain..."
></textarea>
<button className="h-12 w-12 rounded-[14px] bg-[#21303D] all-center shrink-0 hover:brightness-125 duration-200 outline-none focus:outline focus:outline-primary active:scale-90">
<Image src={ArrowRightIcon} alt="Right arrow icon" />
</button>
</div>
<Divider />
{/* suggestions */}
<div className="flex items-center gap-6 p-2">
<button className="bg-[#2B3237] h-9 p-2 px-3 flex items-center gap-2 rounded-full">
<Image src={MemoriesIcon} alt="Memories icon" className="w-5" />
<span className="pr-3">Filters</span>
<Image src={SelectIcon} alt="Select icon" className="w-4" />
</button>
<div className="flex gap-6 brightness-75">
<p>Nvidia</p>
<p>Open-source</p>
<p>Artificial Intelligence</p>
</div>
</div>
</div>
);
}
export default QueryInput;

71
apps/web/app/layout.tsx Normal file
View file

@ -0,0 +1,71 @@
import "@repo/tailwind-config/globals.css";
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import { ThemeScript } from "next-app-theme/theme-script";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Supermemory - Your personal second brain.",
description:
"Bring saved information from all over the internet into one place where you can connect it, and ask AI about it",
openGraph: {
images: [
{
url: "https://supermemory.ai/og-image.png",
width: 1200,
height: 627,
alt: "Supermemory - Your personal second brain.",
},
],
},
metadataBase: {
host: "https://supermemory.ai",
href: "/",
origin: "https://supermemory.ai",
password: "supermemory",
hash: "supermemory",
pathname: "/",
search: "",
username: "supermemoryai",
hostname: "supermemory.ai",
port: "",
protocol: "https:",
searchParams: new URLSearchParams(""),
toString: () => "https://supermemory.ai/",
toJSON: () => "https://supermemory.ai/",
},
twitter: {
card: "summary_large_image",
site: "https://supermemory.ai",
creator: "https://supermemory.ai",
title: "Supermemory - Your personal second brain.",
description:
"Bring saved information from all over the internet into one place where you can connect it, and ask AI about it",
images: [
{
url: "https://supermemory.ai/og-image.png",
width: 1200,
height: 627,
alt: "Supermemory - Your personal second brain.",
},
],
},
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}): JSX.Element {
return (
<html lang="en">
{/* <head>
<ThemeScript />
</head> */}
{/* TODO: when lightmode support is added, remove the 'dark' class from the body tag */}
<body className={`${inter.className} dark`}>{children}</body>
</html>
);
}

116
apps/web/app/page-ref.tsx Normal file
View file

@ -0,0 +1,116 @@
import { Button } from "@repo/ui/src/shadcn/button";
import { auth, signIn, signOut } from "./helpers/server/auth";
import { db } from "./helpers/server/db";
import { sql } from "drizzle-orm";
import { users } from "./helpers/server/db/schema";
import { getThemeToggler } from "./helpers/lib/get-theme-button";
export const runtime = "edge";
export default async function Page() {
const usr = await auth();
const userCount = await db
.select({
count: sql<number>`count(*)`.mapWith(Number),
})
.from(users);
const SetThemeButton = getThemeToggler();
return (
<main className="flex flex-col items-center justify-center min-h-screen">
<div className="flex max-w-2xl justify-between w-full">
<SetThemeButton />
<div className="flex gap-2 items-center justify-center">
{" "}
<svg
viewBox="0 0 256 116"
xmlns="http://www.w3.org/2000/svg"
width="45px"
height="45px"
preserveAspectRatio="xMidYMid"
>
<path
fill="#FFF"
d="m202.357 49.394-5.311-2.124C172.085 103.434 72.786 69.289 66.81 85.997c-.996 11.286 54.227 2.146 93.706 4.059 12.039.583 18.076 9.671 12.964 24.484l10.069.031c11.615-36.209 48.683-17.73 50.232-29.68-2.545-7.857-42.601 0-31.425-35.497Z"
/>
<path
fill="#F4811F"
d="M176.332 108.348c1.593-5.31 1.062-10.622-1.593-13.809-2.656-3.187-6.374-5.31-11.154-5.842L71.17 87.634c-.531 0-1.062-.53-1.593-.53-.531-.532-.531-1.063 0-1.594.531-1.062 1.062-1.594 2.124-1.594l92.946-1.062c11.154-.53 22.839-9.56 27.087-20.182l5.312-13.809c0-.532.531-1.063 0-1.594C191.203 20.182 166.772 0 138.091 0 111.535 0 88.697 16.995 80.73 40.896c-5.311-3.718-11.684-5.843-19.12-5.31-12.747 1.061-22.838 11.683-24.432 24.43-.531 3.187 0 6.374.532 9.56C16.996 70.107 0 87.103 0 108.348c0 2.124 0 3.718.531 5.842 0 1.063 1.062 1.594 1.594 1.594h170.489c1.062 0 2.125-.53 2.125-1.594l1.593-5.842Z"
/>
<path
fill="#FAAD3F"
d="M205.544 48.863h-2.656c-.531 0-1.062.53-1.593 1.062l-3.718 12.747c-1.593 5.31-1.062 10.623 1.594 13.809 2.655 3.187 6.373 5.31 11.153 5.843l19.652 1.062c.53 0 1.062.53 1.593.53.53.532.53 1.063 0 1.594-.531 1.063-1.062 1.594-2.125 1.594l-20.182 1.062c-11.154.53-22.838 9.56-27.087 20.182l-1.063 4.78c-.531.532 0 1.594 1.063 1.594h70.108c1.062 0 1.593-.531 1.593-1.593 1.062-4.25 2.124-9.03 2.124-13.81 0-27.618-22.838-50.456-50.456-50.456"
/>
</svg>
<span className="italic">Cloudflare Next Saas Starter</span>
</div>
<div className="border border-black dark:border-white rounded-2xl p-2 flex items-center">
Start by editing apps/web/page.tsx
</div>
</div>
<div className="max-w-2xl text-start w-full mt-16">
Welcome to Cloudflare Next Saas Starter. <br /> Built a full stack app
using production-ready tools and frameworks, host on Cloudflare
instantly.
<br />
An opinionated, batteries-included framework with{" "}
<a
className="text-transparent bg-clip-text bg-gradient-to-r from-[#a93d64] to-[#275ba9]"
href="https://turbo.build"
>
Turborepo
</a>{" "}
and Nextjs. Fully Typesafe. Best practices followed by default.
<br /> <br />
Here's what the stack includes:
<ul className="list-disc mt-4 prose dark:prose-invert">
<li>
Authentication with <code>next-auth</code>
</li>
<li>Database using Cloudflare's D1 serverless databases</li>
<li>Drizzle ORM, already connected to your database and auth </li>
<li>Light/darkmode theming that works with server components (!)</li>
<li>Styling using TailwindCSS and ShadcnUI</li>
<li>Turborepo with a landing page and shared components</li>
<li>Cloudflare wrangler for quick functions on the edge</li>
<li>
... best part: everything's already set up for you. Just code!
</li>
</ul>
<div className="mt-4 flex flex-col gap-2">
<span>Number of users in database: {userCount[0]!.count}</span>
</div>
{usr?.user?.email ? (
<>
<div className="mt-4 flex flex-col gap-2">
<span>Hello {usr.user.name} 👋</span>
<span>{usr.user.email}</span>
</div>
<form
action={async () => {
"use server";
await signOut();
}}
>
<Button className="mt-4">Sign out</Button>
</form>
</>
) : (
<form
action={async () => {
"use server";
await signIn("google");
}}
>
<Button className="mt-4">Login with Google</Button>
</form>
)}
</div>
</main>
);
}

View file

@ -0,0 +1,315 @@
"use client";
import * as React from "react";
import Image from "next/image";
import { Cross2Icon, UploadIcon } from "@radix-ui/react-icons";
import Dropzone, {
type DropzoneProps,
type FileRejection,
} from "react-dropzone";
import { toast } from "sonner";
import { cn, formatBytes } from "@repo/ui/lib/utils";
import { useControllableState } from "@repo/ui/hooks/use-controllable-state";
import { Button } from "@repo/ui/src/button";
import { Progress } from "@repo/ui/src/progress";
import { ScrollArea } from "@repo/ui/src/scroll-area";
interface FileUploaderProps extends React.HTMLAttributes<HTMLDivElement> {
/**
* Value of the uploader.
* @type File[]
* @default undefined
* @example value={files}
*/
value?: File[];
/**
* Function to be called when the value changes.
* @type React.Dispatch<React.SetStateAction<File[]>>
* @default undefined
* @example onValueChange={(files) => setFiles(files)}
*/
onValueChange?: React.Dispatch<React.SetStateAction<File[]>>;
/**
* Function to be called when files are uploaded.
* @type (files: File[]) => Promise<void>
* @default undefined
* @example onUpload={(files) => uploadFiles(files)}
*/
onUpload?: (files: File[]) => Promise<void>;
/**
* Progress of the uploaded files.
* @type Record<string, number> | undefined
* @default undefined
* @example progresses={{ "file1.png": 50 }}
*/
progresses?: Record<string, number>;
/**
* Accepted file types for the uploader.
* @type { [key: string]: string[]}
* @default
* ```ts
* { "image/*": [] }
* ```
* @example accept={["image/png", "image/jpeg"]}
*/
accept?: DropzoneProps["accept"];
/**
* Maximum file size for the uploader.
* @type number | undefined
* @default 1024 * 1024 * 2 // 2MB
* @example maxSize={1024 * 1024 * 2} // 2MB
*/
maxSize?: DropzoneProps["maxSize"];
/**
* Maximum number of files for the uploader.
* @type number | undefined
* @default 1
* @example maxFiles={5}
*/
maxFiles?: DropzoneProps["maxFiles"];
/**
* Whether the uploader should accept multiple files.
* @type boolean
* @default false
* @example multiple
*/
multiple?: boolean;
/**
* Whether the uploader is disabled.
* @type boolean
* @default false
* @example disabled
*/
disabled?: boolean;
}
export function FileUploader(props: FileUploaderProps) {
const {
value: valueProp,
onValueChange,
onUpload,
progresses,
accept = { "image/*": [] },
maxSize = 1024 * 1024 * 2,
maxFiles = 1,
multiple = false,
disabled = false,
className,
...dropzoneProps
} = props;
const [files, setFiles] = useControllableState({
prop: valueProp,
onChange: onValueChange,
});
const onDrop = React.useCallback(
(acceptedFiles: File[], rejectedFiles: FileRejection[]) => {
if (!multiple && maxFiles === 1 && acceptedFiles.length > 1) {
toast.error("Cannot upload more than 1 file at a time");
return;
}
if ((files?.length ?? 0) + acceptedFiles.length > maxFiles) {
toast.error(`Cannot upload more than ${maxFiles} files`);
return;
}
const newFiles = acceptedFiles.map((file) =>
Object.assign(file, {
preview: URL.createObjectURL(file),
}),
);
const updatedFiles = files ? [...files, ...newFiles] : newFiles;
setFiles(updatedFiles);
if (rejectedFiles.length > 0) {
rejectedFiles.forEach(({ file }) => {
toast.error(`File ${file.name} was rejected`);
});
}
if (
onUpload &&
updatedFiles.length > 0 &&
updatedFiles.length <= maxFiles
) {
const target =
updatedFiles.length > 0 ? `${updatedFiles.length} files` : `file`;
toast.promise(onUpload(updatedFiles), {
loading: `Uploading ${target}...`,
success: () => {
setFiles([]);
return `${target} uploaded`;
},
error: `Failed to upload ${target}`,
});
}
},
[files, maxFiles, multiple, onUpload, setFiles],
);
function onRemove(index: number) {
if (!files) return;
const newFiles = files.filter((_, i) => i !== index);
setFiles(newFiles);
onValueChange?.(newFiles);
}
// Revoke preview url when component unmounts
React.useEffect(() => {
return () => {
if (!files) return;
files.forEach((file) => {
if (isFileWithPreview(file)) {
URL.revokeObjectURL(file.preview);
}
});
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const isDisabled = disabled || (files?.length ?? 0) >= maxFiles;
return (
<div className="relative flex flex-col gap-6 overflow-hidden">
<Dropzone
onDrop={onDrop}
accept={accept}
maxSize={maxSize}
maxFiles={maxFiles}
multiple={maxFiles > 1 || multiple}
disabled={isDisabled}
>
{({ getRootProps, getInputProps, isDragActive }) => (
<div
{...getRootProps()}
className={cn(
"group relative grid h-52 w-full cursor-pointer place-items-center rounded-lg border-2 border-dashed border-muted-foreground/25 px-5 py-2.5 text-center transition hover:bg-muted/25",
"ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
isDragActive && "border-muted-foreground/50",
isDisabled && "pointer-events-none opacity-60",
className,
)}
{...dropzoneProps}
>
<input {...getInputProps()} />
{isDragActive ? (
<div className="flex flex-col items-center justify-center gap-4 sm:px-5">
<div className="rounded-full border border-dashed p-3">
<UploadIcon
className="size-7 text-muted-foreground"
aria-hidden="true"
/>
</div>
<p className="font-medium text-muted-foreground">
Drop the files here
</p>
</div>
) : (
<div className="flex flex-col items-center justify-center gap-4 sm:px-5">
<div className="rounded-full border border-dashed p-3">
<UploadIcon
className="size-7 text-muted-foreground"
aria-hidden="true"
/>
</div>
<div className="space-y-px">
<p className="font-medium text-muted-foreground">
Drag {`'n'`} drop files here, or click to select files
</p>
<p className="text-sm text-muted-foreground/70">
You can upload
{maxFiles > 1
? ` ${maxFiles === Infinity ? "multiple" : maxFiles}
files (up to ${formatBytes(maxSize)} each)`
: ` a file with ${formatBytes(maxSize)}`}
</p>
</div>
</div>
)}
</div>
)}
</Dropzone>
{files?.length ? (
<ScrollArea className="h-fit w-full px-3">
<div className="max-h-48 space-y-4">
{files?.map((file, index) => (
<FileCard
key={index}
file={file}
onRemove={() => onRemove(index)}
progress={progresses?.[file.name]}
/>
))}
</div>
</ScrollArea>
) : null}
</div>
);
}
interface FileCardProps {
file: File;
onRemove: () => void;
progress?: number;
}
function FileCard({ file, progress, onRemove }: FileCardProps) {
return (
<div className="relative flex items-center space-x-4">
<div className="flex flex-1 space-x-4">
{isFileWithPreview(file) ? (
<Image
src={file.preview}
alt={file.name}
width={48}
height={48}
loading="lazy"
className="aspect-square shrink-0 rounded-md object-cover"
/>
) : null}
<div className="flex w-full flex-col gap-2">
<div className="space-y-px">
<p className="line-clamp-1 text-sm font-medium text-foreground/80">
{file.name}
</p>
<p className="text-xs text-muted-foreground">
{formatBytes(file.size)}
</p>
</div>
{progress ? <Progress value={progress} /> : null}
</div>
</div>
<div className="flex items-center gap-2">
<Button
type="button"
variant="outline"
size="icon"
className="size-7"
onClick={onRemove}
>
<Cross2Icon className="size-4 " aria-hidden="true" />
<span className="sr-only">Remove file</span>
</Button>
</div>
</div>
);
}
function isFileWithPreview(file: File): file is File & { preview: string } {
return "preview" in file && typeof file.preview === "string";
}

View file

@ -0,0 +1,99 @@
"use client";
import * as React from "react";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { getErrorMessage } from "../helpers/lib/handle-errors";
import { Button } from "@repo/ui/src/button";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@repo/ui/src/form";
import { FileUploader } from "./file-uploader";
import { UploadedFilesCard } from "./uploaded-files-card";
import { useUploadFile } from "@repo/ui/hooks/use-upload-file";
const schema = z.object({
images: z.array(z.instanceof(File)),
});
type Schema = z.infer<typeof schema>;
export default function ReactHookFormDemo() {
const [loading, setLoading] = React.useState(false);
const { uploadFiles, uploadedFiles, isUploading } = useUploadFile(
"imageUploader",
{ defaultUploadedFiles: [] },
);
const form = useForm<Schema>({
resolver: zodResolver(schema),
defaultValues: {
images: [],
},
});
function onSubmit(input: Schema) {
setLoading(true);
toast.promise(uploadFiles(input.images), {
loading: "Uploading images...",
success: () => {
form.reset();
setLoading(false);
return "Images uploaded";
},
error: (err) => {
setLoading(false);
return getErrorMessage(err);
},
});
}
return (
<div className="flex flex-col w-full min-h-screen items-center justify-center">
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="flex max-w-2xl flex-col gap-6 w-full"
>
<FormField
control={form.control}
name="images"
render={({ field }) => (
<div className="space-y-6">
<FormItem className="w-full">
<FormLabel>Images</FormLabel>
<FormControl>
<FileUploader
value={field.value}
onValueChange={field.onChange}
maxFiles={4}
maxSize={4 * 1024 * 1024}
onUpload={uploadFiles}
disabled={isUploading}
/>
</FormControl>
<FormMessage />
</FormItem>
{uploadedFiles.length > 0 ? (
<UploadedFilesCard uploadedFiles={uploadedFiles} />
) : null}
</div>
)}
/>
<Button className="w-fit" disabled={loading}>
Save
</Button>
</form>
</Form>
</div>
);
}

View file

@ -0,0 +1,93 @@
import Image from "next/image";
import type { UploadedFile } from "@repo/shared-types";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@repo/ui/src/card";
import { ScrollArea, ScrollBar } from "@repo/ui/src/scroll-area";
interface UploadedFilesCardProps {
uploadedFiles: UploadedFile[];
}
import { ImageIcon } from "@radix-ui/react-icons";
import { cn } from "@repo/ui/lib/utils";
interface EmptyCardProps extends React.ComponentPropsWithoutRef<typeof Card> {
title: string;
description?: string;
action?: React.ReactNode;
icon?: React.ComponentType<{ className?: string }>;
className?: string;
}
function EmptyCard({
title,
description,
icon: Icon = ImageIcon,
action,
className,
...props
}: EmptyCardProps) {
return (
<Card
className={cn(
"flex w-full flex-col items-center justify-center space-y-6 bg-transparent p-16",
className,
)}
{...props}
>
<div className="mr-4 shrink-0 rounded-full border border-dashed p-4">
<Icon className="size-8 text-muted-foreground" aria-hidden="true" />
</div>
<div className="flex flex-col items-center gap-1.5 text-center">
<CardTitle>{title}</CardTitle>
{description ? <CardDescription>{description}</CardDescription> : null}
</div>
{action ? action : null}
</Card>
);
}
export function UploadedFilesCard({ uploadedFiles }: UploadedFilesCardProps) {
return (
<Card>
<CardHeader>
<CardTitle>Uploaded files</CardTitle>
<CardDescription>View the uploaded files here</CardDescription>
</CardHeader>
<CardContent>
{uploadedFiles.length > 0 ? (
<ScrollArea className="pb-4">
<div className="flex w-max space-x-2.5">
{uploadedFiles.map((file) => (
<div key={file.key} className="relative aspect-video w-64">
<Image
src={file.url}
alt={file.name}
fill
sizes="(min-width: 640px) 640px, 100vw"
loading="lazy"
className="rounded-md object-cover"
/>
</div>
))}
</div>
<ScrollBar orientation="horizontal" />
</ScrollArea>
) : (
<EmptyCard
title="No files uploaded"
description="Upload some files to see them here"
className="w-full"
/>
)}
</CardContent>
</Card>
);
}

7
apps/web/cf-env.d.ts vendored Normal file
View file

@ -0,0 +1,7 @@
declare global {
namespace NodeJS {
interface ProcessEnv extends CloudflareEnv {}
}
}
export {};

View file

@ -1,16 +0,0 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/app/globals.css",
"baseColor": "gray",
"cssVariables": false
},
"aliases": {
"utils": "@/lib/utils",
"components": "@/components"
}
}

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