diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 87845ecc6..9d8aa9013 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -59,7 +59,7 @@ body: options: - Production - Node version - - Pnpm version + - bun version - OS Details (Linux, Windows) validations: required: true diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 368dc674c..c7cb92fee 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -36,16 +36,16 @@ jobs: with: node-version: '18.17.0' - - name: Setup pnpm - uses: pnpm/action-setup@v2 + - name: Setup Bun + uses: oven-sh/setup-bun@v1 with: - version: latest + bun-version: latest - name: Install dependencies - run: pnpm install + run: bun install - name: Build - run: pnpm run build + run: bun run build - name: Check sync with preview run: | diff --git a/.npmrc b/.npmrc deleted file mode 100644 index cc8df9de0..000000000 --- a/.npmrc +++ /dev/null @@ -1 +0,0 @@ -node-linker=hoisted \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f35fc8970..df49173b6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -44,7 +44,7 @@ git checkout -b 4. Install packages with pnpm ``` -pnpm install +bun install ``` 5. Set up your .env file @@ -54,7 +54,7 @@ Go to the `app/backend` and `app/frontend` directories and duplicate the `.env.e 6. Run (in development mode) ``` -pnpm dev +bun dev ``` ## Making a Pull Request diff --git a/README.md b/README.md index 13bc9eff6..f8a52c357 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ To get a local copy up and running, please follow these simple steps. Here is what you need to run march. - Node.js (Version: >=18.x) -- pnpm (recommended) +- bun (recommended) ## Development @@ -35,7 +35,8 @@ git checkout -b 4. Install packages with pnpm ``` -pnpm install +bun install + ``` 5. Set up your .env file @@ -45,7 +46,8 @@ Go to the `app/backend` and `app/frontend` directories and duplicate the `.env.e 6. Run (in development mode) ``` -pnpm dev +bun dev + ``` ### Linear integration diff --git a/apps/backend/package.json b/apps/backend/package.json index 98405e226..66d7e9109 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -1,12 +1,12 @@ { - "name": "@satellite/backend", + "name": "@march/backend", "version": "0.0.1", - "description": "satellite Backend", + "description": "march Backend", "author": "march-dev", "private": true, "main": "index.js", "type": "module", - "repository": "git@github.com:marchhq/satellite-backend.git", + "repository": "git@github.com:marchhq/march.git", "license": "MIT", "scripts": { "dev": "nodemon index.js", diff --git a/apps/extension/background.js b/apps/extension/background.js deleted file mode 100644 index a70d5a71b..000000000 --- a/apps/extension/background.js +++ /dev/null @@ -1,58 +0,0 @@ -const frontendURL ='http://localhost:3000'||'https://app.march.cat.com'; - - const backendURL ='http://localhost:8080'||'https://sage.march.cat.com'; - - -chrome.action.onClicked.addListener((tab) => { - chrome.scripting.executeScript({ - target: { tabId: tab.id }, - func: getPageDetails, - }, (results) => { - if (results && results[0].result) { - const { title, url } = results[0].result; - createIssue(title, url); - } - }); -}); - -function getPageDetails() { - return { title: document.title, url: window.location.href }; -} - -function createIssue(title, url) { - // chrome.cookies.get({ url: 'http://localhost:3000', name: '__MARCH_ACCESS_TOKEN__' }, (cookie) => { - chrome.cookies.get({ url: frontendURL, name: '__MARCH_ACCESS_TOKEN__' }, (cookie) => { - - if (cookie) { - // fetch("http://localhost:8080/api/items/create/", { - fetch(`${backendURL}/api/items/create/`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${cookie.value}` - }, - body: JSON.stringify({ - title: title, - source: "marchClipper", - type: "url", - description: url, - metadata: { - url: url - }, - - }) - }) - .then(response => response.json()) - .then(data => { - console.log('Issue created successfully:', data); - }) - .catch(error => { - console.error('Error creating issue:', error); - }); - } else { - console.error('No auth token found. Please log in.'); - } - }); -} - - diff --git a/apps/extension/icons/icon16.png b/apps/extension/icons/icon16.png deleted file mode 100644 index 96c42fb11..000000000 Binary files a/apps/extension/icons/icon16.png and /dev/null differ diff --git a/apps/extension/icons/icon48.png b/apps/extension/icons/icon48.png deleted file mode 100644 index 3eb230ded..000000000 Binary files a/apps/extension/icons/icon48.png and /dev/null differ diff --git a/apps/extension/manifest.json b/apps/extension/manifest.json deleted file mode 100644 index d5d9f3591..000000000 --- a/apps/extension/manifest.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "manifest_version": 3, - "name": "march", - "version": "1.0", - "description": "An extension for march", - "permissions": ["activeTab", "cookies", "storage", "scripting"], - "background": { - "service_worker": "background.js" - }, - "action": { - "default_icon": { - "16": "icons/icon16.png", - "48": "icons/icon48.png" - } - }, - "host_permissions": [""] -} diff --git a/apps/extension/tempCodeRunnerFile.js b/apps/extension/tempCodeRunnerFile.js deleted file mode 100644 index d48939dc2..000000000 --- a/apps/extension/tempCodeRunnerFile.js +++ /dev/null @@ -1 +0,0 @@ -marchClipper \ No newline at end of file diff --git a/apps/frontend/.env.local.example b/apps/frontend/.env.local.example index d7e9503f7..fc17a3d55 100644 --- a/apps/frontend/.env.local.example +++ b/apps/frontend/.env.local.example @@ -1,9 +1,9 @@ - NEXT_PUBLIC_GOOGLE_CLIENT_ID= -NEXT_PUBLIC_GITHUB_CLIENT_ID= +NEXT_PUBLIC_WEBSOCKET_URL= + +GITHUB_CLIENT_ID= +GITHUB_APP_URL= -NEXT_PUBLIC_LINEAR_CLIENT_ID= -NEXT_PUBLIC_LINEAR_REDIRECT_URL= +LINEAR_CLIENT_ID= +LINEAR_REDIRECT_URL= -NEXT_PUBLIC_GITHUB_APP_URL= -NEXT_PUBLIC_WEBSOCKET_URL= \ No newline at end of file diff --git a/apps/frontend/.eslintrc.json b/apps/frontend/.eslintrc.json index 6f64fe054..c3858bdab 100644 --- a/apps/frontend/.eslintrc.json +++ b/apps/frontend/.eslintrc.json @@ -19,7 +19,8 @@ "plugin:@typescript-eslint/recommended", "plugin:@tanstack/eslint-plugin-query/recommended", "next/core-web-vitals", - "prettier" + "prettier", + "plugin:jsx-a11y/recommended" ], "plugins": [ "prettier", @@ -27,8 +28,15 @@ "jsx-a11y", "tailwindcss", "react-refresh", + "@tanstack/query" ], + "overrides": [ + { + "files": ["*.tsx", "*.ts", "*.jsx", "*.js"], + "extends": ["plugin:jsx-a11y/recommended"] + } + ], "rules": { "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-unused-vars": "off", diff --git a/apps/frontend/.github/workflows/node.js.yml b/apps/frontend/.github/workflows/node.js.yml index 5d89398e3..09b6761ae 100644 --- a/apps/frontend/.github/workflows/node.js.yml +++ b/apps/frontend/.github/workflows/node.js.yml @@ -23,6 +23,15 @@ jobs: uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} - - run: npm ci - - run: npm run build - # - run: npm test + + - name: Setup Bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + + - name: Install dependencies + run: bun install + + - name: Build + run: bun run build + # - run: bun test diff --git a/apps/frontend/.gitignore b/apps/frontend/.gitignore index c397e0dbe..4a3e0ee26 100644 --- a/apps/frontend/.gitignore +++ b/apps/frontend/.gitignore @@ -1,35 +1,35 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* -.pnpm-debug.log* - -# local env files -.env.local -.env.development.local -.env.test.local -.env.production.local - -# vercel -.vercel +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.bun +.bun.lockb + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +bun.lockb + +# local env files +.env.local +.env.development.local +.env.test.local +.env.production.local + +# vercel +.vercel diff --git a/apps/frontend/package.json b/apps/frontend/package.json index 513b7d23c..429a8a7d6 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -4,7 +4,7 @@ "description": "Satellite Frontend", "author": "march-dev", "scripts": { - "dev": "next dev --turbopack", + "dev": "next dev", "build": "next build", "start": "next start", "lint": "next lint", @@ -43,6 +43,7 @@ "@tiptap/react": "^2.5.8", "@tiptap/starter-kit": "^2.5.8", "@tiptap/suggestion": "^2.6.6", + "@types/lodash": "^4.17.13", "@types/markdown-it": "^14.1.2", "axios": "^1.7.2", "class-variance-authority": "^0.7.0", @@ -55,11 +56,11 @@ "lucide-react": "^0.439.0", "markdown-it": "^14.1.0", "markdown-it-task-lists": "^2.1.1", - "next": "15.0.3", + "next": "^14.2.5", "nextjs-toploader": "^3.7.15", - "react": "19.0.0-rc-66855b96-20241106", "react-day-picker": "8.10.1", - "react-dom": "19.0.0-rc-66855b96-20241106", + "react": "^18.3.1", + "react-dom": "^18.3.1", "react-hook-form": "^7.53.0", "react-resizable-panels": "^2.0.20", "tailwind-merge": "^2.5.2", @@ -71,12 +72,12 @@ "@iconify-icon/react": "^2.1.0", "@tanstack/eslint-plugin-query": "^5.51.15", "@types/node": "^20.14.11", - "@types/react": "npm:types-react@19.0.0-rc.1", + "@types/react": "^18.3.3", "@typescript-eslint/eslint-plugin": "^7.16.1", "@typescript-eslint/parser": "^7.16.1", "autoprefixer": "^10.4.19", "eslint": "^8.57.0", - "eslint-config-next": "15.0.3", + "eslint-config-next": "^14.2.5", "eslint-config-prettier": "^9.1.0", "eslint-plugin-import": "^2.29.1", "eslint-plugin-jsx-a11y": "^6.9.0", @@ -116,7 +117,7 @@ "bugs": { "url": "/~https://github.com/nirnejak/satellite-frontend/issues" }, - "pnpm": { + "bun": { "overrides": { "@types/react": "npm:types-react@19.0.0-rc.1" } diff --git a/apps/frontend/public/icons/spacesicon.svg b/apps/frontend/public/icons/spacesicon.svg index 95f17b8f1..0598b181e 100755 --- a/apps/frontend/public/icons/spacesicon.svg +++ b/apps/frontend/public/icons/spacesicon.svg @@ -1,4 +1,3 @@ - + - diff --git a/apps/frontend/src/app/(app)/space/[slug]/page.tsx b/apps/frontend/src/app/(app)/space/[slug]/page.tsx deleted file mode 100644 index 84f112d2d..000000000 --- a/apps/frontend/src/app/(app)/space/[slug]/page.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import * as React from "react" - -import Container from "@/src/components/atoms/Container" -import PageSection from "@/src/components/PageSection" - -const AppPage: React.FC = () => { - return ( -
- - - -
- ) -} - -export default AppPage diff --git a/apps/frontend/src/app/(app)/space/layout.tsx b/apps/frontend/src/app/(app)/space/layout.tsx deleted file mode 100644 index baf21f199..000000000 --- a/apps/frontend/src/app/(app)/space/layout.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import React from "react" - -interface Props { - children: React.ReactNode -} - -const SpaceLayout: React.FC = ({ children }) => { - return ( -
-
{children}
-
- ) -} - -export default SpaceLayout diff --git a/apps/frontend/src/app/(app)/space/meetings/[meetingId]/page.tsx b/apps/frontend/src/app/(app)/space/meetings/[meetingId]/page.tsx deleted file mode 100644 index 1b3d69a81..000000000 --- a/apps/frontend/src/app/(app)/space/meetings/[meetingId]/page.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import MeetingPage from "@/src/components/meetings/MeetingsPage" -import generateMetadataHelper from "@/src/utils/seo" - -export async function generateMetadata( - props: { - params: Promise<{ meetingId: string }> - } -) { - const params = await props.params; - const id = params.meetingId - const path = `/space/meetings/${id}` - const title = `Meetings` - - return generateMetadataHelper({ - path, - title, - }) -} - -export default async function MeetingsPage( - props: { - params: Promise<{ - meetingId?: string - }> - } -) { - const params = await props.params; - if (!params.meetingId) { - return console.log("meetId is undefined") - } - - return -} diff --git a/apps/frontend/src/app/(app)/space/meetings/page.tsx b/apps/frontend/src/app/(app)/space/meetings/page.tsx deleted file mode 100644 index 317ac1fc9..000000000 --- a/apps/frontend/src/app/(app)/space/meetings/page.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { Metadata } from "next" - -import InitialMeetings from "@/src/components/meetings/InitialMeet" -import generateMetadataHelper from "@/src/utils/seo" - -export const metadata: Metadata = generateMetadataHelper({ - path: "/space/meetings", - title: "Meetings", - description: "engineered for makers", -}) - -const Meetings: React.FC = () => { - return -} - -export default Meetings diff --git a/apps/frontend/src/app/(app)/space/notes/[noteId]/page.tsx b/apps/frontend/src/app/(app)/space/notes/[noteId]/page.tsx deleted file mode 100644 index 9ab97e027..000000000 --- a/apps/frontend/src/app/(app)/space/notes/[noteId]/page.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import NotesPage from "@/src/components/Notes/NotesPage" -import generateMetadataHelper from "@/src/utils/seo" - -export async function generateMetadata( - props: { - params: Promise<{ noteId: string }> - } -) { - const params = await props.params; - const id = params.noteId - const path = `/space/notes/${id}` - const title = `Notes` - - return generateMetadataHelper({ - path, - title, - }) -} - -export default async function Notes(props: { params: Promise<{ noteId?: string }> }) { - const params = await props.params; - if (!params.noteId) { - console.log("noteId is undefined") - return null - } - - return -} diff --git a/apps/frontend/src/app/(app)/space/notes/page.tsx b/apps/frontend/src/app/(app)/space/notes/page.tsx deleted file mode 100644 index dcb9c45c6..000000000 --- a/apps/frontend/src/app/(app)/space/notes/page.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import NotesPage from "@/src/components/Notes/InitialNotes" - -const Notes: React.FC = () => { - return -} - -export default Notes diff --git a/apps/frontend/src/app/(app)/space/page.tsx b/apps/frontend/src/app/(app)/space/page.tsx deleted file mode 100644 index 7980d2a8d..000000000 --- a/apps/frontend/src/app/(app)/space/page.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import * as React from "react" - -import { Metadata } from "next" - -import generateMetadataHelper from "@/src/utils/seo" - -export const metadata: Metadata = generateMetadataHelper({ - path: "/spaces", - title: "Spaces", - description: "engineered for makers", -}) - -const SpacePage: React.FC = () => { - return ( -
-

- Select a space from the sidebar -

-
- ) -} - -export default SpacePage diff --git a/apps/frontend/src/app/(app)/space/reading-list/page.tsx b/apps/frontend/src/app/(app)/space/reading-list/page.tsx deleted file mode 100644 index 26bd7eb2a..000000000 --- a/apps/frontend/src/app/(app)/space/reading-list/page.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { Metadata } from "next" - -import ReadingListComponent from "@/src/components/Reading/ReadingListComponent" -import generateMetadataHelper from "@/src/utils/seo" - -export const metadata: Metadata = generateMetadataHelper({ - path: "/space/reading-list", - title: "Reading List", - description: "engineered for makers", -}) - -export default function ReadingListPage() { - return -} diff --git a/apps/frontend/src/app/(app)/spaces/[spaceId]/blocks/[blockId]/items/[itemId]/page.tsx b/apps/frontend/src/app/(app)/spaces/[spaceId]/blocks/[blockId]/items/[itemId]/page.tsx new file mode 100644 index 000000000..a61e51225 --- /dev/null +++ b/apps/frontend/src/app/(app)/spaces/[spaceId]/blocks/[blockId]/items/[itemId]/page.tsx @@ -0,0 +1,41 @@ +import { redirect } from "next/navigation" + +import MeetingPage from "@/src/components/meetings/MeetingsPage" +import NotesPage from "@/src/components/Notes/NotesPage" +import { getSession } from "@/src/lib/server/actions/sessions" +import useSpaceStore from "@/src/lib/store/space.store" + +type Params = Promise<{ spaceId: string; blockId: string; itemId: string }> +type SearchParams = Promise<{ [key: string]: string | string[] | undefined }> + +export default async function ItemPage(props: { + params: Params + searchParams: SearchParams +}) { + const params = await props.params + const { spaceId, blockId, itemId } = params + + const session = await getSession() + const { spaces, fetchSpaces } = useSpaceStore.getState() + + if (spaces.length === 0) { + await fetchSpaces(session) + } + + const { spaces: updatedSpaces } = useSpaceStore.getState() + + const space = updatedSpaces.find((space) => space._id === spaceId) + if (!space) + return
no space found
+ + switch (space.name.toLowerCase()) { + case "notes": + return + case "meetings": + return + case "reading list": + return redirect(`/spaces/${spaceId}/${blockId}/items`) + default: + return
Cannot Find Item. Please Create one!
+ } +} diff --git a/apps/frontend/src/app/(app)/spaces/[spaceId]/blocks/[blockId]/items/page.tsx b/apps/frontend/src/app/(app)/spaces/[spaceId]/blocks/[blockId]/items/page.tsx new file mode 100644 index 000000000..08dc4158c --- /dev/null +++ b/apps/frontend/src/app/(app)/spaces/[spaceId]/blocks/[blockId]/items/page.tsx @@ -0,0 +1,47 @@ +import { notFound } from "next/navigation" + +import InitialMeetings from "@/src/components/meetings/InitialMeet" +import InitialNotes from "@/src/components/Notes/InitialNotes" +import ReadingListComponent from "@/src/components/Reading/ReadingListComponent" +import { getSession } from "@/src/lib/server/actions/sessions" +import useReadingStore from "@/src/lib/store/reading.store" +import useSpaceStore from "@/src/lib/store/space.store" + +type Params = Promise<{ spaceId: string; blockId: string }> +type SearchParams = Promise<{ [key: string]: string | string[] | undefined }> + +export default async function ItemsListPage(props: { + params: Params + searchParams: SearchParams +}) { + const params = await props.params + const { spaceId, blockId } = params + const session = await getSession() + const { spaces, fetchSpaces } = useSpaceStore.getState() + const { fetchReadingList } = useReadingStore.getState() + + if (spaces.length === 0) { + await fetchSpaces(session) + } + + const { spaces: updatedSpaces } = useSpaceStore.getState() + + const space = updatedSpaces.find((space) => space._id === spaceId) + if (!space) return notFound() + + //prefetch items + if (space.name.toLowerCase() === "reading list") { + await fetchReadingList(session, spaceId, blockId) + } + + switch (space.name.toLowerCase()) { + case "reading list": + return + case "meetings": + return + case "notes": + return + default: + return notFound() + } +} diff --git a/apps/frontend/src/app/(app)/spaces/[spaceId]/page.tsx b/apps/frontend/src/app/(app)/spaces/[spaceId]/page.tsx new file mode 100644 index 000000000..169e6567d --- /dev/null +++ b/apps/frontend/src/app/(app)/spaces/[spaceId]/page.tsx @@ -0,0 +1,51 @@ +import { notFound, redirect } from "next/navigation" + +import { getSession } from "@/src/lib/server/actions/sessions" +import useBlockStore from "@/src/lib/store/block.store" +import useSpaceStore from "@/src/lib/store/space.store" + +type Params = Promise<{ spaceId: string }> +type SearchParams = Promise<{ [key: string]: string | string[] | undefined }> + +export default async function SpacePage(props: { + params: Params + searchParams: SearchParams +}) { + const params = await props.params + const session = await getSession() + + const { spaces, fetchSpaces } = useSpaceStore.getState() + const space = spaces.find((space) => space._id === params.spaceId) + + if (!space) { + await fetchSpaces(session) + const { spaces: updatedSpaces } = useSpaceStore.getState() + const updatedSpace = updatedSpaces.find((s) => s._id === params.spaceId) + if (!updatedSpace) return notFound() + } + + const { blocks, blockId } = useBlockStore.getState() + const existingBlock = blocks.find((block) => block.space === params.spaceId) + + if (blockId) { + return redirect(`/spaces/${params.spaceId}/blocks/${blockId}/items`) + } + + if (existingBlock) { + return redirect( + `/spaces/${params.spaceId}/blocks/${existingBlock._id}/items` + ) + } + + const { fetchBlocks, createBlock } = useBlockStore.getState() + const result = await fetchBlocks(session, params.spaceId) + + if (result?.noBlocks) { + await createBlock(session, params.spaceId) + } + + const { blockId: newBlockId } = useBlockStore.getState() + if (!newBlockId) return notFound() + + return redirect(`/spaces/${params.spaceId}/blocks/${newBlockId}/items`) +} diff --git a/apps/frontend/src/app/(app)/spaces/page.tsx b/apps/frontend/src/app/(app)/spaces/page.tsx new file mode 100644 index 000000000..2b8f1d827 --- /dev/null +++ b/apps/frontend/src/app/(app)/spaces/page.tsx @@ -0,0 +1,7 @@ +export default async function Space() { + return ( +
+ Please Select Space from sidebar +
+ ) +} diff --git a/apps/frontend/src/app/(onboarding)/layout.tsx b/apps/frontend/src/app/(onboarding)/layout.tsx index 3ebf8b5b3..3db7cbcb4 100644 --- a/apps/frontend/src/app/(onboarding)/layout.tsx +++ b/apps/frontend/src/app/(onboarding)/layout.tsx @@ -1,8 +1,6 @@ import { ProgressBar } from "@/src/components/atoms/Progress" import { AuthProvider } from "@/src/contexts/AuthContext" -import type { JSX } from "react" - const OnboardingLayout = ({ children, }: { diff --git a/apps/frontend/src/app/api/auth/github/url/route.ts b/apps/frontend/src/app/api/auth/github/url/route.ts new file mode 100644 index 000000000..0a656352d --- /dev/null +++ b/apps/frontend/src/app/api/auth/github/url/route.ts @@ -0,0 +1,21 @@ +import { NextRequest, NextResponse } from "next/server" + +export async function GET(req: NextRequest) { + const GITHUB_CLIENT_ID = process.env.GITHUB_CLIENT_ID + const GITHUB_SCOPE = "user:email" + + if (!GITHUB_CLIENT_ID) { + return NextResponse.json( + { + error: "github client_id is not set", + }, + { + status: 500, + } + ) + } + + const authUrl = `/~https://github.com/login/oauth/authorize?client_id=${GITHUB_CLIENT_ID}&scope=${GITHUB_SCOPE}` + + return NextResponse.json({ authUrl }) +} diff --git a/apps/frontend/src/app/api/auth/google-calendar/url/route.ts b/apps/frontend/src/app/api/auth/google-calendar/url/route.ts new file mode 100644 index 000000000..668ae43e2 --- /dev/null +++ b/apps/frontend/src/app/api/auth/google-calendar/url/route.ts @@ -0,0 +1,31 @@ +import { NextRequest, NextResponse } from "next/server" + +import { FRONTEND_URL } from "@/src/lib/constants/urls" + +export async function GET(request: NextRequest) { + const { searchParams } = new URL(request.url) + const redirectAfterAuth = searchParams.get("redirectAfterAuth") + + const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID + const GOOGLE_SCOPE = "https://www.googleapis.com/auth/calendar" + const GOOGLE_REDIRECT_URI = `${FRONTEND_URL}/auth/google-calendar` + + if (!GOOGLE_CLIENT_ID || !GOOGLE_REDIRECT_URI) { + return NextResponse.json( + { + error: "configuration error", + }, + { + status: 500, + } + ) + } + + const state = encodeURIComponent( + JSON.stringify({ redirect: redirectAfterAuth }) + ) + + const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?client_id=${GOOGLE_CLIENT_ID}&redirect_uri=${GOOGLE_REDIRECT_URI}&response_type=code&scope=${GOOGLE_SCOPE}&access_type=offline&state=${state}` + + return NextResponse.json({ authUrl }) +} diff --git a/apps/frontend/src/app/api/auth/linear/url/route.ts b/apps/frontend/src/app/api/auth/linear/url/route.ts new file mode 100644 index 000000000..0a34236e7 --- /dev/null +++ b/apps/frontend/src/app/api/auth/linear/url/route.ts @@ -0,0 +1,23 @@ +import { NextResponse } from "next/server" + +export async function GET() { + const scope = "read write" + const LINEAR_CLIENT_ID = process.env.LINEAR_CLIENT_ID + const LINEAR_REDIRECT_URL = process.env.LINEAR_REDIRECT_URL + const LINEAR_SCOPE = encodeURIComponent(scope) + + if (!LINEAR_CLIENT_ID || !LINEAR_REDIRECT_URL) { + return NextResponse.json( + { + error: "linear client_id or redirect_url is not set", + }, + { + status: 500, + } + ) + } + + const authUrl = `https://linear.app/oauth/authorize?client_id=${LINEAR_CLIENT_ID}&redirect_uri=${LINEAR_REDIRECT_URL}&response_type=code&scope=${LINEAR_SCOPE}` + + return NextResponse.json({ authUrl }) +} diff --git a/apps/frontend/src/app/auth/github/callback/route.ts b/apps/frontend/src/app/auth/github/callback/route.ts index eac3274d3..dc0d09749 100644 --- a/apps/frontend/src/app/auth/github/callback/route.ts +++ b/apps/frontend/src/app/auth/github/callback/route.ts @@ -5,9 +5,6 @@ import { BACKEND_URL, FRONTEND_URL } from "@/src/lib/constants/urls" export async function GET(request: NextRequest) { const searchParams = request.nextUrl.searchParams - const fullUrl = request.nextUrl.toString() - console.log("Full redirect URL from GitHub:", fullUrl) - const code = searchParams.get("code") if (!code) { return NextResponse.json( diff --git a/apps/frontend/src/app/auth/github/route.ts b/apps/frontend/src/app/auth/github/route.ts index ee7c1e7d5..18074d235 100644 --- a/apps/frontend/src/app/auth/github/route.ts +++ b/apps/frontend/src/app/auth/github/route.ts @@ -24,8 +24,6 @@ export async function GET(request: NextRequest) { const { accessToken, refreshToken, isNewUser } = response.data - console.log("response.data: ", response.data) - const res = NextResponse.redirect( // redirecting to inbox for test isNewUser diff --git a/apps/frontend/src/app/auth/google-calendar/route.ts b/apps/frontend/src/app/auth/google-calendar/route.ts index dc7ea0aff..e1ba4354a 100644 --- a/apps/frontend/src/app/auth/google-calendar/route.ts +++ b/apps/frontend/src/app/auth/google-calendar/route.ts @@ -1,10 +1,6 @@ import axios from "axios" import { NextRequest, NextResponse } from "next/server" -import { - GOOGLECALENDAR_ACCESS_TOKEN, - GOOGLECALENDAR_REFRESH_TOKEN, -} from "@/src/lib/constants/cookie" import { BACKEND_URL, FRONTEND_URL } from "@/src/lib/constants/urls" export async function GET(request: NextRequest) { diff --git a/apps/frontend/src/app/auth/google/route.ts b/apps/frontend/src/app/auth/google/route.ts index f21ed03c8..fd5d6b9d4 100644 --- a/apps/frontend/src/app/auth/google/route.ts +++ b/apps/frontend/src/app/auth/google/route.ts @@ -1,5 +1,4 @@ import axios, { type AxiosError } from "axios" -import { cookies } from "next/headers" import { NextResponse, type NextRequest } from "next/server" import { type GoogleAuthResponse } from "@/src/lib/@types/auth/response" diff --git a/apps/frontend/src/components/Notes/InitialNotes.tsx b/apps/frontend/src/components/Notes/InitialNotes.tsx index 974289eec..90e62011f 100644 --- a/apps/frontend/src/components/Notes/InitialNotes.tsx +++ b/apps/frontend/src/components/Notes/InitialNotes.tsx @@ -7,7 +7,12 @@ import { useRouter } from "next/navigation" import { useAuth } from "@/src/contexts/AuthContext" import useNotesStore from "@/src/lib/store/notes.store" -const NotesPage: React.FC = () => { +interface InitialNotesProps { + spaceId: string + blockId: string +} + +const InitialNotes: React.FC = ({ spaceId, blockId }) => { const { session } = useAuth() const router = useRouter() const { notes, addNote, latestNote, fetchNotes, setIsFetched, isFetched } = @@ -15,12 +20,12 @@ const NotesPage: React.FC = () => { const fetchTheNotes = useCallback(async (): Promise => { try { - await fetchNotes(session) + await fetchNotes(session, spaceId, blockId) setIsFetched(true) } catch (error) { setIsFetched(false) } - }, [session, fetchNotes, setIsFetched]) + }, [session, fetchNotes, setIsFetched, spaceId, blockId]) useEffect(() => { if (!isFetched) { @@ -30,20 +35,22 @@ const NotesPage: React.FC = () => { useEffect(() => { if (latestNote) { - router.push(`/space/notes/${latestNote._id}`) + router.push( + `/spaces/${spaceId}/blocks/${blockId}/items/${latestNote._id}` + ) } - }, [latestNote, router]) + }, [latestNote, router, blockId, spaceId]) const addNewNote = useCallback(async (): Promise => { try { const newNote = await addNote(session, "", "

") if (newNote !== null) { - router.push(`/space/notes/${newNote._id}`) + router.push(`/spaces/${spaceId}/blocks/${blockId}/items/${newNote._id}`) } } catch (error) { console.error(error) } - }, [session, addNote, router]) + }, [session, addNote, router, spaceId, blockId]) useEffect(() => { if (notes.length === 0) { @@ -58,4 +65,4 @@ const NotesPage: React.FC = () => { ) } -export default NotesPage +export default InitialNotes diff --git a/apps/frontend/src/components/Notes/NotesPage.tsx b/apps/frontend/src/components/Notes/NotesPage.tsx index c0dca70bc..65e51276a 100644 --- a/apps/frontend/src/components/Notes/NotesPage.tsx +++ b/apps/frontend/src/components/Notes/NotesPage.tsx @@ -1,302 +1,150 @@ "use client" -import { useState, useRef, useEffect, useCallback } from "react" +import { useRef, useCallback, useMemo, useEffect } from "react" -import { Icon } from "@iconify-icon/react/dist/iconify.mjs" -import { PlusIcon } from "@radix-ui/react-icons" -import Link from "next/link" +import { debounce } from "lodash" import { useRouter } from "next/navigation" -import NoteDetails from "../header/note-details" -import TextEditor from "@/src/components/atoms/Editor" -import { useAuth } from "@/src/contexts/AuthContext" +import NoteEditor from "./components/NoteEditor/NoteEditor" +import NoteHeader from "./components/NoteHeader/NoteHeader" +import { NoteStackModal } from "./components/NoteModal/NotesModal" +import { useNoteEffects } from "./hooks/useNoteEffects" +import { useNoteHandlers } from "./hooks/useNoteHandlers" +import { useNoteState } from "./hooks/useNoteState" import useEditorHook from "@/src/hooks/useEditor.hook" import usePersistedState from "@/src/hooks/usePersistedState" -import { Note } from "@/src/lib/@types/Items/Note" import useNotesStore from "@/src/lib/store/notes.store" -import classNames from "@/src/utils/classNames" -import { formatDateHeader, formatDateYear, fromNow } from "@/src/utils/datetime" interface Props { noteId: string + spaceId: string + blockId: string } -const NotesPage: React.FC = ({ noteId }) => { - const { session } = useAuth() +const NotesPage: React.FC = ({ noteId, spaceId, blockId }) => { const router = useRouter() - const { - isFetched, - setIsFetched, - fetchNotes, - notes, - updateNote, - addNote, - saveNote, - deleteNote, - } = useNotesStore() + const { isFetched, notes } = useNotesStore() + + const { state, dispatch, setTitle, setContent, setIsSaved } = useNoteState() + + const { note, title, content, loading, notFound } = state - const [note, setNote] = useState(null) - const [title, setTitle] = useState(note?.title ?? "") const textareaRef = useRef(null) - const timeoutId = useRef(null) - const [content, setContent] = useState(note?.description ?? "

") - const [isSaved, setIsSaved] = useState(true) const editor = useEditorHook({ content, setContent, setIsSaved }) - const [loading, setLoading] = useState(false) - const [notFound, setNotFound] = useState(false) const [closeToggle, setCloseToggle] = usePersistedState("closeToggle", true) - const [isInitialLoad, setIsInitialLoad] = useState(true) - const [isEditingTitle, setIsEditingTitle] = useState(false) - - const fetchTheNotes = useCallback(async (): Promise => { - try { - await fetchNotes(session) - setIsFetched(true) - } catch (error) { - setIsFetched(false) - } - }, [session, fetchNotes, setIsFetched]) - - useEffect(() => { - if (!isFetched) { - fetchTheNotes() - } - }, [fetchTheNotes, isFetched]) - - useEffect(() => { - return () => { - if (timeoutId.current) { - clearTimeout(timeoutId.current) - } - } - }, []) - - const handleClose = () => setCloseToggle(!closeToggle) - - useEffect(() => { - // Wait for data to be ready - if (!isFetched) { - editor?.setEditable(false) - return - } - - // Handle case where we're waiting for a new note to be created - if (notes.length === 0) { - editor?.setEditable(false) - return - } - const currentNote = notes.find((n) => n._id === noteId) - - if (currentNote) { - // Note exists - set it up - editor?.setEditable(true) - editor?.commands.setContent(currentNote.description) - setNote(currentNote) - setTitle(currentNote.title) - setContent(currentNote.description) - setNotFound(false) - } else { - // Note doesn't exist - redirect to first note - // This handles the case when returning to a deleted note's URL - const firstNote = notes[0] - router.push(`/space/notes/${firstNote._id}`) - } - }, [isFetched, editor, notes, noteId, router]) - - useEffect(() => { - if (note !== null && !loading && isInitialLoad) { - if (!title || title.trim() === "") { - textareaRef.current?.focus() - } else { - editor?.commands.focus() - } - setIsInitialLoad(false) - } - }, [note, loading, title, editor, isInitialLoad]) - - useEffect(() => { - const textarea = textareaRef.current - if (textarea) { - textarea.style.height = "auto" - textarea.style.height = `${textarea.scrollHeight}px` - } - }, [title]) - - const saveNoteToServer = useCallback( - async (note: Note): Promise => { - await saveNote(session, note) - }, - [session, saveNote] + const { saveNoteToServer, addNewNote, handleUpdateNote } = useNoteHandlers( + state, + dispatch, + spaceId, + blockId ) - useEffect(() => { - if (note && isSaved) { - saveNoteToServer({ ...note, title, description: content }) - } - }, [note, saveNoteToServer, isSaved]) - - useEffect(() => { - setIsSaved(false) - - if (note !== null) { - updateNote({ ...note, title }) - } - - if (timeoutId.current) { - clearTimeout(timeoutId.current) - } - - timeoutId.current = setTimeout(() => { - setIsSaved(true) - }, 1000) - }, [title]) + useNoteEffects( + state, + dispatch, + editor, + saveNoteToServer, + notes, + isFetched, + noteId, + router, + spaceId, + blockId, + textareaRef, + handleUpdateNote + ) - useEffect(() => { - if (note !== null) { - updateNote({ ...note, description: content }) - } - }, [content]) + const handleClose = useCallback(() => { + setCloseToggle(!closeToggle) + }, [closeToggle, setCloseToggle]) - const addNewNote = async (): Promise => { - if (!isSaved) { - if (note) await saveNoteToServer({ ...note, title, description: content }) - } - try { - setLoading(true) - const newNote = await addNote(session, "", "

") - if (newNote !== null) { - router.push(`/space/notes/${newNote._id}`) - return newNote // Return the new note - } - return null - } catch (error) { - console.error(error) - return null - } finally { - setLoading(false) - } - } + const handleTextareaKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + e.preventDefault() - const handleDeleteNote = async (n: Note): Promise => { - if (!session || !n) return + if (e.shiftKey) { + const textarea = e.currentTarget + const cursorPosition = textarea.selectionStart + const newValue = + title.slice(0, cursorPosition) + "\n" + title.slice(cursorPosition) - try { - await deleteNote(session, n) - const remainingNotes = notes.filter((n_) => n_._id !== n._id) + setTitle(newValue) - if (n._id === note?._id) { - if (remainingNotes.length <= 0) { - const newNote = await addNewNote() - // No need to redirect here since addNewNote already does the routing + requestAnimationFrame(() => { + textarea.selectionStart = cursorPosition + 1 + textarea.selectionEnd = cursorPosition + 1 + }) } else { - router.push(`/space/notes/${remainingNotes[0]._id}`) + editor?.commands.focus() + editor?.commands.setTextSelection(0) } } - } catch (error) { - console.error("Error deleting note:", error) - } - } - - useEffect(() => { - const handleBeforeUnload = (e: BeforeUnloadEvent) => { - if (!isSaved) { - e.preventDefault() - } - } - - window.addEventListener("beforeunload", handleBeforeUnload) - - return () => { - window.removeEventListener("beforeunload", handleBeforeUnload) - } - }, [isSaved]) - - const handleTextareaKeyDown = ( - e: React.KeyboardEvent - ) => { - if (e.key === "Enter") { - e.preventDefault() - - if (e.shiftKey) { - const textarea = e.currentTarget - const cursorPosition = textarea.selectionStart - const newValue = - title.slice(0, cursorPosition) + "\n" + title.slice(cursorPosition) + }, + [editor, title, setTitle] + ) - setTitle(newValue) + const handleTitleChange = useCallback( + (e: React.ChangeEvent) => { + const newTitle = e.target.value + setTitle(newTitle) + }, + [setTitle] + ) - requestAnimationFrame(() => { - textarea.selectionStart = cursorPosition + 1 - textarea.selectionEnd = cursorPosition + 1 - }) - } else { - editor?.commands.focus() - editor?.commands.setTextSelection(0) - } + const handleTitleFocus = useCallback(() => { + dispatch({ type: "SET_EDITING_TITLE", payload: true }) + }, [dispatch]) + + const handleTitleBlur = useCallback(() => { + dispatch({ type: "SET_EDITING_TITLE", payload: false }) + }, [dispatch]) + + const simplifiedNotes = useMemo(() => { + return notes + ? notes.map((n) => ({ + id: n._id, + title: n.title || "Untitled", + href: `/spaces/${spaceId}/blocks/${blockId}/items/${n._id}`, + createdAt: n.createdAt, + isActive: n._id === note?._id, + })) + : [] + }, [notes, spaceId, blockId, note]) + + const handleSaveNote = useCallback(() => { + if (note) { + saveNoteToServer({ ...note, title, description: content }) } - } + }, [note, title, content, saveNoteToServer]) return ( -
+
-
- {note !== null && ( - - )} -
-
- {!loading ? ( - - ) : ( -
- loading... -
- )} - -
+
+ {note !== null ? ( -
{ - saveNoteToServer({ ...note, title, description: content }) - }} - > -