diff --git a/.gitignore b/.gitignore index 96fab4f..31f8f51 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,5 @@ yarn-error.log* # Misc .DS_Store *.pem + +**/landing-context.json \ No newline at end of file diff --git a/README.md b/README.md index 240569f..71970ac 100644 --- a/README.md +++ b/README.md @@ -1,135 +1,102 @@ -# Turborepo starter +# CoBrain -This Turborepo starter is maintained by the Turborepo core team. +> **CoBrain is your second brain for the internet. Save anything, organize it into slices, and recall it when you need it. Simple, flexible, and built for how you think.** -## Using this example +CoBrain is a modern full-stack web application designed to help users capture and organize information efficiently. It features a responsive dashboard, intuitive brain/memo management, and "slices" for categorizing thoughts. -Run the following command: +## 🏗 Repository Structure -```sh -npx create-turbo@latest -``` +This is a monorepo managed by [Turborepo](https://turbo.build/repo), containing the following workspaces: -## What's inside? +### Apps -This Turborepo includes the following packages/apps: +- **`apps/landing`**: The frontend application built with [Next.js 15](https://nextjs.org/) (App Router), Tailwind CSS, and Framer Motion. Handles the landing page, authentication UI, and the user dashboard. +- **`apps/server`**: The backend API server built with [Express.js](https://expressjs.com/). handles business logic, authentication (Google OAuth + JWT), and database communication. -### Apps and Packages +### Packages -- `docs`: a [Next.js](https://nextjs.org/) app -- `web`: another [Next.js](https://nextjs.org/) app -- `@repo/ui`: a stub React component library shared by both `web` and `docs` applications -- `@repo/eslint-config`: `eslint` configurations (includes `eslint-config-next` and `eslint-config-prettier`) -- `@repo/typescript-config`: `tsconfig.json`s used throughout the monorepo +- **`packages/db`**: Database schema and ORM configuration using [Drizzle ORM](https://orm.drizzle.team/) and PostgreSQL. +- **`@repo/ui`**: Shared UI component library. +- **`@repo/types`**: Shared TypeScript type definitions (DTOs, interfaces). +- **`@repo/eslint-config`**, **`@repo/typescript-config`**, **`@repo/tailwind-config`**: Shared configuration files. -Each package/app is 100% [TypeScript](https://www.typescriptlang.org/). +## 🚀 Getting Started -### Utilities +### Prerequisites -This Turborepo has some additional tools already setup for you: +- **[Bun](https://bun.sh/)** (v1.1+): This project uses Bun as the package manager and runtime for scripts. +- **Node.js** (v18+): Required for some Turbo/Next.js operations. +- **Docker**: (Optional but recommended) For running the PostgreSQL database locally. -- [TypeScript](https://www.typescriptlang.org/) for static type checking -- [ESLint](https://eslint.org/) for code linting -- [Prettier](https://prettier.io) for code formatting +### Installation -### Build +1. **Clone the repository:** -To build all apps and packages, run the following command: + ```bash + git clone https://github.com/your-username/cobrain.git + cd cobrain + ``` -``` -cd my-turborepo +2. **Install dependencies:** + ```bash + bun install + ``` -# With [global `turbo`](https://turborepo.com/docs/getting-started/installation#global-installation) installed (recommended) -turbo build +### Environment Setup -# Without [global `turbo`](https://turborepo.com/docs/getting-started/installation#global-installation), use your package manager -npx turbo build -yarn dlx turbo build -pnpm exec turbo build -``` +You need to configure environment variables for the apps and database. Check the `README.md` in `apps/landing`, `apps/server`, and `packages/db` for specific details. -You can build a specific package by using a [filter](https://turborepo.com/docs/crafting-your-repository/running-tasks#using-filters): +Broadly, you will need to set up `.env` files in: -``` -# With [global `turbo`](https://turborepo.com/docs/getting-started/installation#global-installation) installed (recommended) -turbo build --filter=docs +- `apps/landing/.env` +- `apps/server/.env` +- `packages/db/.env` -# Without [global `turbo`](https://turborepo.com/docs/getting-started/installation#global-installation), use your package manager -npx turbo build --filter=docs -yarn exec turbo build --filter=docs -pnpm exec turbo build --filter=docs -``` +### Database Setup -### Develop +1. **Start PostgreSQL:** + Navigate to `packages/db` and start the database container: -To develop all apps and packages, run the following command: + ```bash + cd packages/db + docker-compose up -d + ``` -``` -cd my-turborepo +2. **Push Schema:** + Apply the database schema: + ```bash + bun run drizzle:push + ``` -# With [global `turbo`](https://turborepo.com/docs/getting-started/installation#global-installation) installed (recommended) -turbo dev +### Running the Project -# Without [global `turbo`](https://turborepo.com/docs/getting-started/installation#global-installation), use your package manager -npx turbo dev -yarn exec turbo dev -pnpm exec turbo dev -``` - -You can develop a specific package by using a [filter](https://turborepo.com/docs/crafting-your-repository/running-tasks#using-filters): +To start both the frontend and backend in development mode: +```bash +bun run dev ``` -# With [global `turbo`](https://turborepo.com/docs/getting-started/installation#global-installation) installed (recommended) -turbo dev --filter=web - -# Without [global `turbo`](https://turborepo.com/docs/getting-started/installation#global-installation), use your package manager -npx turbo dev --filter=web -yarn exec turbo dev --filter=web -pnpm exec turbo dev --filter=web -``` - -### Remote Caching -> [!TIP] -> Vercel Remote Cache is free for all plans. Get started today at [vercel.com](https://vercel.com/signup?/signup?utm_source=remote-cache-sdk&utm_campaign=free_remote_cache). +This uses Turborepo to run `dev` scripts in all apps simultaneously. -Turborepo can use a technique known as [Remote Caching](https://turborepo.com/docs/core-concepts/remote-caching) to share cache artifacts across machines, enabling you to share build caches with your team and CI/CD pipelines. +- **Frontend**: http://localhost:3000 +- **Backend**: http://localhost:3001 (or configured port) -By default, Turborepo will cache locally. To enable Remote Caching you will need an account with Vercel. If you don't have an account you can [create one](https://vercel.com/signup?utm_source=turborepo-examples), then enter the following commands: +## 🛠 Commands -``` -cd my-turborepo - -# With [global `turbo`](https://turborepo.com/docs/getting-started/installation#global-installation) installed (recommended) -turbo login - -# Without [global `turbo`](https://turborepo.com/docs/getting-started/installation#global-installation), use your package manager -npx turbo login -yarn exec turbo login -pnpm exec turbo login -``` - -This will authenticate the Turborepo CLI with your [Vercel account](https://vercel.com/docs/concepts/personal-accounts/overview). - -Next, you can link your Turborepo to your Remote Cache by running the following command from the root of your Turborepo: - -``` -# With [global `turbo`](https://turborepo.com/docs/getting-started/installation#global-installation) installed (recommended) -turbo link - -# Without [global `turbo`](https://turborepo.com/docs/getting-started/installation#global-installation), use your package manager -npx turbo link -yarn exec turbo link -pnpm exec turbo link -``` +Run these commands from the root directory: -## Useful Links +| Command | Description | +|Str |---| +| `bun run dev` | Start development server for all apps | +| `bun run build` | Build all apps and packages | +| `bun run lint` | shared linting check | +| `bun run format` | Format code with Prettier | +| `bun run check-types` | Run TypeScript type checking | -Learn more about the power of Turborepo: +## 🤝 Contribution -- [Tasks](https://turborepo.com/docs/crafting-your-repository/running-tasks) -- [Caching](https://turborepo.com/docs/crafting-your-repository/caching) -- [Remote Caching](https://turborepo.com/docs/core-concepts/remote-caching) -- [Filtering](https://turborepo.com/docs/crafting-your-repository/running-tasks#using-filters) -- [Configuration Options](https://turborepo.com/docs/reference/configuration) -- [CLI Usage](https://turborepo.com/docs/reference/command-line-reference) +1. Fork the repo +2. Create your feature branch (`git checkout -b feature/amazing-feature`) +3. Commit your changes (`git commit -m 'Add some amazing feature'`) +4. Push to the branch (`git push origin feature/amazing-feature`) +5. Open a Pull Request diff --git a/apps/landing/README.md b/apps/landing/README.md index a98bfa8..3e89d52 100644 --- a/apps/landing/README.md +++ b/apps/landing/README.md @@ -1,36 +1,78 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/create-next-app). +# CoBrain Frontend (Landing & Dashboard) -## Getting Started +This directory contains the user interface for CoBrain, built with **Next.js 15 (App Router)** and **Tailwind CSS**. -First, run the development server: +## ✨ Features + +- **Landing Page**: Modern, responsive landing page explaining the product value. +- **Authentication**: Seamless sign-in/sign-up integration with Google OAuth (handled via backend). +- **Dashboard**: + - View and manage your "Brains" (Memos). + - Organize content into "Slices". + - Responsive sidebar and navigation. +- **Modern UI**: Polished look with custom fonts, Framer Motion animations, and specific design tokens (Glassmorphism, gradients). + +## 🛠 Tech Stack + +- **Framework**: [Next.js 15](https://nextjs.org/) +- **Styling**: [Tailwind CSS v4](https://tailwindcss.com/) +- **Icons**: [React Icons](https://react-icons.github.io/react-icons/) (Lucide, Tabler) +- **Animations**: [Framer Motion](https://www.framer.com/motion/) +- **HTTP Client**: [Axios](https://axios-http.com/) + +## 🚀 Getting Started + +### 1. Environment Variables + +Create a `.env` file in `apps/landing` based on `.env_example`: ```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev +cp .env_example .env ``` -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. +**Required Variables:** -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. +```env +NEXT_PUBLIC_API_URL=http://localhost:3001 # URL of the running server app +``` -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load Inter, a custom Google Font. +### 2. Run Development Server -## Learn More +You can run the frontend in isolation: -To learn more about Next.js, take a look at the following resources: +```bash +bun dev +``` -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. +Open [http://localhost:3000](http://localhost:3000) with your browser. -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! +## 📂 Project Structure + +``` +apps/landing/ +├── app/ # Next.js App Router pages +│ ├── (auth)/ # Authentication routes (sign-in, sign-up) +│ ├── dashboard/ # Main app interface (protected) +│ ├── api/ # Local API routes (if any) +│ ├── globals.css # Global styles and Tailwind directives +│ ├── layout.tsx # Root layout +│ └── page.tsx # Landing page +├── components/ # React components +│ ├── dashboard/ # Dashboard-specific components (Sidebar, Nav) +│ ├── landing/ # Landing page sections (Hero, Features, Footer) +│ └── ui/ # Reusable UI elements (Buttons, Cards, Inputs) +├── hooks/ # Custom React hooks +├── lib/ # Utility libraries (Axios setup, Utils) +├── services/ # API service layers (Auth, Slice, etc.) +└── public/ # Static assets +``` -## Deploy on Vercel +## 🔐 Authentication Flow -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. +The frontend relies on cookie-based authentication managed by the backend. -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. +1. User clicks "Sign in with Google". +2. Redirects to backend OAuth endpoint. +3. Backend sets `access_token` and `refresh_token` httpOnly cookies. +4. Frontend Middleware protects `/dashboard` routes by checking for these tokens. +5. `axios` interceptors handle 401 errors by attempting a silent token refresh via the `/token-refresh` route. diff --git a/apps/landing/app/(auth)/signin/page.tsx b/apps/landing/app/(auth)/signin/page.tsx index 64c92b7..4a510bf 100644 --- a/apps/landing/app/(auth)/signin/page.tsx +++ b/apps/landing/app/(auth)/signin/page.tsx @@ -1,59 +1,8 @@ "use client"; import React from "react"; -import { AuthCard } from "../../../components/auth/auth-card"; -import { Button, Input, Capsule } from "@repo/ui"; -import { FcGoogle } from "react-icons/fc"; +import { SignInView } from "../../../components/auth/signin-view"; export default function SignInPage() { - return ( - } - > -
- - - -
- -
-
- -
-
- - Or continue with - -
-
- - - -
- Don't have an account?{" "} - - Sign Up - -
-
- ); + return ; } diff --git a/apps/landing/app/(auth)/signup/page.tsx b/apps/landing/app/(auth)/signup/page.tsx index ee4cdd0..bdae3fd 100644 --- a/apps/landing/app/(auth)/signup/page.tsx +++ b/apps/landing/app/(auth)/signup/page.tsx @@ -1,59 +1,8 @@ "use client"; import React from "react"; -import { AuthCard } from "../../../components/auth/auth-card"; -import { Button, Input, Capsule } from "@repo/ui"; -import { FcGoogle } from "react-icons/fc"; +import { SignUpView } from "../../../components/auth/signup-view"; export default function SignUpPage() { - return ( - } - > -
- - - -
- -
-
- -
-
- - Or continue with - -
-
- - - -
- Already have an account?{" "} - - Sign In - -
-
- ); + return ; } diff --git a/apps/landing/app/dashboard/[id]/page.tsx b/apps/landing/app/dashboard/[id]/page.tsx new file mode 100644 index 0000000..d2408ba --- /dev/null +++ b/apps/landing/app/dashboard/[id]/page.tsx @@ -0,0 +1,128 @@ +"use client"; + +import React, { useState } from "react"; +import { useRouter } from "next/navigation"; +import { TbLoader } from "react-icons/tb"; +import { Button } from "@repo/ui"; +import type { Memo } from "@repo/types"; +import { UpdateSliceDialog } from "../../../components/dashboard/slice/update-slice-dialog"; +import { DeleteSliceDialog } from "../../../components/dashboard/slice/delete-slice-dialog"; +import { ShareSliceDialog } from "../../../components/dashboard/slice/share-slice-dialog"; +import { CreateBrainDialog } from "../../../components/dashboard/brain/create-brain-dialog"; +import { DeleteBrainDialog } from "../../../components/dashboard/brain/delete-brain-dialog"; +import { useSliceData } from "../../../hooks/use-slice-data"; +import { SliceHeader } from "../../../components/dashboard/slice/ui/slice-header"; +import { BrainGrid } from "../../../components/dashboard/slice/ui/brain-grid"; + +export default function SliceDashboardPage() { + const router = useRouter(); + const { slice, brains, loading, error, refresh } = useSliceData(); + + // Slice Dialog states + const [isUpdateDialogOpen, setIsUpdateDialogOpen] = useState(false); + const [isShareDialogOpen, setIsShareDialogOpen] = useState(false); + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); + + // Brain Dialog States + const [isCreateBrainDialogOpen, setIsCreateBrainDialogOpen] = useState(false); + const [isDeleteBrainDialogOpen, setIsDeleteBrainDialogOpen] = useState(false); + const [selectedBrain, setSelectedBrain] = useState(null); + + const handleEditBrain = (brain: Memo) => { + setSelectedBrain(brain); + setIsCreateBrainDialogOpen(true); + }; + + const handleDeleteBrain = (brain: Memo) => { + setSelectedBrain(brain); + setIsDeleteBrainDialogOpen(true); + }; + + const handleCloseCreateDialog = () => { + setIsCreateBrainDialogOpen(false); + setTimeout(() => setSelectedBrain(null), 300); // Clear after animation + }; + + const handleNewBrain = () => { + setSelectedBrain(null); + setIsCreateBrainDialogOpen(true); + }; + + if (loading) { + return ( +
+ +
+ ); + } + + if (error || !slice) { + return ( +
+
+ {error || "Slice not found"} +
+ +
+ ); + } + + return ( +
+ setIsUpdateDialogOpen(true)} + onShareSlice={() => setIsShareDialogOpen(true)} + onDeleteSlice={() => setIsDeleteDialogOpen(true)} + /> + +
+ + + + {/* Dialogs */} + setIsUpdateDialogOpen(false)} + onSuccess={refresh} + slice={slice} + /> + + setIsShareDialogOpen(false)} + slice={slice} + /> + + setIsDeleteDialogOpen(false)} + onSuccess={() => router.push("/dashboard")} + slice={slice} + /> + + + + setIsDeleteBrainDialogOpen(false)} + onSuccess={refresh} + memo={selectedBrain} + /> +
+ ); +} diff --git a/apps/landing/app/dashboard/layout.tsx b/apps/landing/app/dashboard/layout.tsx new file mode 100644 index 0000000..528655e --- /dev/null +++ b/apps/landing/app/dashboard/layout.tsx @@ -0,0 +1,28 @@ +"use client"; + +import React from "react"; +import { + DashboardLayout, + Sidebar, + SidebarHeader, + SidebarFooter, + SidebarSearchSlice, + // SidebarOptions, +} from "../../components/dashboard/sidebar"; +import { RefreshProvider } from "../../components/dashboard/refresh-context"; + +export default function Layout({ children }: { children: React.ReactNode }) { + const sidebar = ( + }> + + + {/* */} + + ); + + return ( + + {children} + + ); +} diff --git a/apps/landing/app/dashboard/page.tsx b/apps/landing/app/dashboard/page.tsx new file mode 100644 index 0000000..9738f9d --- /dev/null +++ b/apps/landing/app/dashboard/page.tsx @@ -0,0 +1,8 @@ +"use client"; + +import React from "react"; +import { DashboardView } from "../../components/dashboard/dashboard-view"; + +export default function Dashboard() { + return ; +} diff --git a/apps/landing/app/fonts/GeistMonoVF.woff b/apps/landing/app/fonts/GeistMonoVF.woff deleted file mode 100644 index f2ae185..0000000 Binary files a/apps/landing/app/fonts/GeistMonoVF.woff and /dev/null differ diff --git a/apps/landing/app/fonts/GeistVF.woff b/apps/landing/app/fonts/GeistVF.woff deleted file mode 100644 index 1b62daa..0000000 Binary files a/apps/landing/app/fonts/GeistVF.woff and /dev/null differ diff --git a/apps/landing/app/layout.tsx b/apps/landing/app/layout.tsx index fbc7747..0059876 100644 --- a/apps/landing/app/layout.tsx +++ b/apps/landing/app/layout.tsx @@ -1,17 +1,7 @@ import type { Metadata } from "next"; -import localFont from "next/font/local"; import "./globals.css"; import { ThemeProvider } from "../components/theme/theme-provider"; -const geistSans = localFont({ - src: "./fonts/GeistVF.woff", - variable: "--font-geist-sans", -}); -const geistMono = localFont({ - src: "./fonts/GeistMonoVF.woff", - variable: "--font-geist-mono", -}); - export const metadata: Metadata = { title: "CoBrain", description: "CoBrain is a platform for sharing a slice of your mind.", @@ -25,7 +15,6 @@ export default function RootLayout({ return ( diff --git a/apps/landing/app/token-refresh/page.tsx b/apps/landing/app/token-refresh/page.tsx new file mode 100644 index 0000000..f188240 --- /dev/null +++ b/apps/landing/app/token-refresh/page.tsx @@ -0,0 +1,43 @@ +"use client"; + +import { useEffect, Suspense } from "react"; +import { useRouter, useSearchParams } from "next/navigation"; +import { refresh } from "../../services/auth.service"; + +function TokenRefreshContent() { + const router = useRouter(); + const searchParams = useSearchParams(); + const from = searchParams.get("from") || "/dashboard"; + + useEffect(() => { + const performRefresh = async () => { + try { + await refresh(); + // Refresh successful, retry the original navigation + router.replace(from); + } catch { + // Refresh failed, user must sign in again + router.replace("/signin"); + } + }; + + performRefresh(); + }, [router, from]); + + return ( +
+
+
+

Refreshing session...

+
+
+ ); +} + +export default function TokenRefreshPage() { + return ( + + + + ); +} diff --git a/apps/landing/components/auth/auth-card.tsx b/apps/landing/components/auth/auth-card.tsx index 02e39f7..2a6cf82 100644 --- a/apps/landing/components/auth/auth-card.tsx +++ b/apps/landing/components/auth/auth-card.tsx @@ -28,7 +28,7 @@ export function AuthCard({ alt="Auth Background" width={1000} height={1000} - className="h-full w-full object-contain" + className="h-full w-full object-cover" priority /> {imageOverlay && ( diff --git a/apps/landing/components/auth/signin-view.tsx b/apps/landing/components/auth/signin-view.tsx new file mode 100644 index 0000000..7dba94d --- /dev/null +++ b/apps/landing/components/auth/signin-view.tsx @@ -0,0 +1,117 @@ +"use client"; + +import React, { useState } from "react"; +import { useRouter } from "next/navigation"; +import { AuthCard } from "./auth-card"; +import { Button, Input, Capsule } from "@repo/ui"; +import { FcGoogle } from "react-icons/fc"; +import { signin } from "../../services/auth.service"; + +export function SignInView() { + const router = useRouter(); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + // Form State + const [formData, setFormData] = useState({ + email: "", + password: "", + }); + + const handleChange = (e: React.ChangeEvent) => { + setFormData({ ...formData, [e.target.id]: e.target.value }); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + setError(null); + + try { + await signin(formData); + router.push("/dashboard"); + } catch (err: unknown) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const msg = (err as any)?.response?.data?.error || "Invalid credentials"; + setError(msg); + } finally { + setLoading(false); + } + }; + + const handleGoogleLogin = () => { + window.location.href = `${process.env.NEXT_PUBLIC_API_URL}/auth/google`; + }; + + return ( + } + > +
+ {error && ( +
+ {error} +
+ )} + + + +
+ +
+
+ +
+
+ + Or continue with + +
+
+ + + +
+ Don't have an account?{" "} + + Sign Up + +
+
+ ); +} diff --git a/apps/landing/components/auth/signup-view.tsx b/apps/landing/components/auth/signup-view.tsx new file mode 100644 index 0000000..5513602 --- /dev/null +++ b/apps/landing/components/auth/signup-view.tsx @@ -0,0 +1,127 @@ +"use client"; + +import React, { useState } from "react"; +import { useRouter } from "next/navigation"; +import { AuthCard } from "./auth-card"; +import { Button, Input, Capsule } from "@repo/ui"; +import { FcGoogle } from "react-icons/fc"; +import { signup } from "../../services/auth.service"; + +export function SignUpView() { + const router = useRouter(); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + // Form State + const [formData, setFormData] = useState({ + name: "", + email: "", + password: "", + }); + + const handleChange = (e: React.ChangeEvent) => { + setFormData({ ...formData, [e.target.id]: e.target.value }); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + setError(null); + + try { + await signup(formData); + router.push("/dashboard"); + } catch (err: unknown) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const msg = (err as any)?.response?.data?.error || "Failed to sign up"; + setError(msg); + } finally { + setLoading(false); + } + }; + + const handleGoogleLogin = () => { + window.location.href = `${process.env.NEXT_PUBLIC_API_URL}/auth/google`; + }; + + return ( + } + > +
+ {error && ( +
+ {error} +
+ )} + + + + +
+ +
+
+ +
+
+ + Or continue with + +
+
+ + + +
+ Already have an account?{" "} + + Sign In + +
+
+ ); +} diff --git a/apps/landing/components/dashboard/brain/create-brain-dialog.tsx b/apps/landing/components/dashboard/brain/create-brain-dialog.tsx new file mode 100644 index 0000000..3176f50 --- /dev/null +++ b/apps/landing/components/dashboard/brain/create-brain-dialog.tsx @@ -0,0 +1,548 @@ +"use client"; + +import React, { useState, useEffect } from "react"; +import { Button, Input, cn } from "@repo/ui"; +import { createMemo, updateMemo } from "../../../services/memo.service"; +import { TbLoader, TbX, TbPlus, TbTrash } from "react-icons/tb"; +import { motion, AnimatePresence } from "motion/react"; +import type { MemoType, Memo } from "@repo/types"; +import type { CreateMemoRequest } from "../../../services/memo.service"; + +// --- Sub-components (Inlined) --- + +const MEMO_TYPES: { value: MemoType; label: string }[] = [ + { value: "TEXT", label: "Text Note" }, + { value: "TODO", label: "Todo List" }, + { value: "LINK", label: "Bookmark / Link" }, + { value: "QA", label: "Q&A" }, + { value: "CODE", label: "Code Snippet" }, +]; + +const BrainTypeSelector = ({ + currentType, + onSelect, + disabled, +}: { + currentType: MemoType; + onSelect: (type: MemoType) => void; + disabled?: boolean; +}) => { + return ( +
+ {MEMO_TYPES.map((t) => ( + + ))} +
+ ); +}; + +interface TodoItem { + id: string; + text: string; + completed: boolean; +} + +interface BrainFormContentProps { + type: "TEXT" | "LINK" | "QA" | "CODE" | "TODO"; + // Text + textContent: string; + setTextContent: (value: string) => void; + // Link + linkUrl: string; + setLinkUrl: (value: string) => void; + linkNote: string; + setLinkNote: (value: string) => void; + // QA + qaQuestion: string; + setQaQuestion: (value: string) => void; + qaAnswer: string; + setQaAnswer: (value: string) => void; + // Code + codeLanguage: string; + setCodeLanguage: (value: string) => void; + codeSnippet: string; + setCodeSnippet: (value: string) => void; + codeNote: string; + setCodeNote: (value: string) => void; + // Todo + todoItems: TodoItem[]; + setTodoItems: (items: TodoItem[]) => void; +} + +const BrainFormContent = ({ + type, + textContent, + setTextContent, + linkUrl, + setLinkUrl, + linkNote, + setLinkNote, + qaQuestion, + setQaQuestion, + qaAnswer, + setQaAnswer, + codeLanguage, + setCodeLanguage, + codeSnippet, + setCodeSnippet, + codeNote, + setCodeNote, + todoItems, + setTodoItems, +}: BrainFormContentProps) => { + // Todo helpers + const addTodoItem = () => { + setTodoItems([ + ...todoItems, + { id: crypto.randomUUID(), text: "", completed: false }, + ]); + }; + + const removeTodoItem = (id: string) => { + setTodoItems(todoItems.filter((item) => item.id !== id)); + }; + + const updateTodoItem = (id: string, text: string) => { + setTodoItems( + todoItems.map((item) => (item.id === id ? { ...item, text } : item)), + ); + }; + + return ( +
+

+ Content +

+ + {type === "TEXT" && ( +