diff --git a/.env.example b/.env.example index c66b5a5..c67f360 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,4 @@ -VERSION=0.1.12 - - +VERSION=0.1.13 # Nest run in docker, change host to database container name DB_HOST=localhost diff --git a/.github/workflows/build-docker-image.yml b/.github/workflows/build-docker-image.yml index b784c09..5f53f5b 100644 --- a/.github/workflows/build-docker-image.yml +++ b/.github/workflows/build-docker-image.yml @@ -7,6 +7,32 @@ on: workflow_dispatch: jobs: + build-init: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + with: + ref: main + + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - name: Login to Docker Registry + run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin + + - name: Build and Push Frontend Docker Image + uses: docker/build-push-action@v2 + with: + context: . + file: ./apps/init/Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: redplanethq/init:${{ github.ref_name }} + build-webapp: runs-on: ubuntu-latest diff --git a/README.md b/README.md index e66fdff..52ca7e6 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,3 @@ -
🌐 Language @@ -63,7 +62,7 @@ C.O.R.E is a portable memory graph built from your llm interactions and personal core-memory-graph -## 🧩 Key Features +## 🧩 Key Features - **Memory Graph**: Visualise how your facts and preferences link together - **Chat with Memory**: Ask questions about memory for instant insights and understanding @@ -85,14 +84,19 @@ Check out our [docs](https://docs.heysol.ai/self-hosting/docker) for modular dep ## Documentation Explore our documentation to get the most out of CORE + - [Basic Concepts](https://docs.heysol.ai/concepts/memory_graph) - [Self Hosting](https://docs.heysol.ai/self-hosting/overview) - [Connect Core MCP with Claude](https://docs.heysol.ai/providers/claude) - [Connect Core MCP with Cursor](https://docs.heysol.ai/providers/cursor) +- [Basic Concepts](https://docs.heysol.ai/overview) +- [API Reference](https://docs.heysol.ai/local-setup) ## šŸ§‘ā€šŸ’» Support + Have questions or feedback? We're here to help: + - Discord: [Join core-support channel](https://discord.gg/YGUZcvDjUa) - Documentation: [docs.heysol.ai](https://docs.heysol.ai) - Email: manik@poozle.dev diff --git a/apps/init/.gitignore b/apps/init/.gitignore new file mode 100644 index 0000000..3814592 --- /dev/null +++ b/apps/init/.gitignore @@ -0,0 +1,51 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# Dependencies +node_modules +.pnp +.pnp.js + +# Local env files +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Testing +coverage + +# Turbo +.turbo + +# Vercel +.vercel + +# Build Outputs +.next/ +out/ +build +dist +.tshy/ +.tshy-build/ + +# Debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Misc +.DS_Store +*.pem + +docker-compose.dev.yaml + +clickhouse/ +.vscode/ +registry/ + +.cursor +CLAUDE.md + +.claude + diff --git a/apps/init/Dockerfile b/apps/init/Dockerfile new file mode 100644 index 0000000..46ec445 --- /dev/null +++ b/apps/init/Dockerfile @@ -0,0 +1,70 @@ +ARG NODE_IMAGE=node:20.11.1-bullseye-slim@sha256:5a5a92b3a8d392691c983719dbdc65d9f30085d6dcd65376e7a32e6fe9bf4cbe + +FROM ${NODE_IMAGE} AS pruner + +WORKDIR /core + +COPY --chown=node:node . . +RUN npx -q turbo@2.5.3 prune --scope=@redplanethq/init --docker +RUN find . -name "node_modules" -type d -prune -exec rm -rf '{}' + + +# Base strategy to have layer caching +FROM ${NODE_IMAGE} AS base +RUN apt-get update && apt-get install -y openssl dumb-init postgresql-client +WORKDIR /core +COPY --chown=node:node .gitignore .gitignore +COPY --from=pruner --chown=node:node /core/out/json/ . +COPY --from=pruner --chown=node:node /core/out/pnpm-lock.yaml ./pnpm-lock.yaml +COPY --from=pruner --chown=node:node /core/out/pnpm-workspace.yaml ./pnpm-workspace.yaml + +## Dev deps +FROM base AS dev-deps +WORKDIR /core +# Corepack is used to install pnpm +RUN corepack enable +ENV NODE_ENV development +RUN pnpm install --ignore-scripts --no-frozen-lockfile + +## Production deps +FROM base AS production-deps +WORKDIR /core +# Corepack is used to install pnpm +RUN corepack enable +ENV NODE_ENV production +RUN pnpm install --prod --no-frozen-lockfile + +## Builder (builds the init CLI) +FROM base AS builder +WORKDIR /core +# Corepack is used to install pnpm +RUN corepack enable + +COPY --from=pruner --chown=node:node /core/out/full/ . +COPY --from=dev-deps --chown=node:node /core/ . +COPY --chown=node:node turbo.json turbo.json +COPY --chown=node:node .configs/tsconfig.base.json .configs/tsconfig.base.json +RUN pnpm run build --filter=@redplanethq/init... + +# Runner +FROM ${NODE_IMAGE} AS runner +RUN apt-get update && apt-get install -y openssl postgresql-client ca-certificates +WORKDIR /core +RUN corepack enable +ENV NODE_ENV production + +COPY --from=base /usr/bin/dumb-init /usr/bin/dumb-init +COPY --from=pruner --chown=node:node /core/out/full/ . +COPY --from=production-deps --chown=node:node /core . +COPY --from=builder --chown=node:node /core/apps/init/dist ./apps/init/dist + +# Copy the trigger dump file +COPY --chown=node:node apps/init/trigger.dump ./apps/init/trigger.dump + +# Copy and set up entrypoint script +COPY --chown=node:node apps/init/entrypoint.sh ./apps/init/entrypoint.sh +RUN chmod +x ./apps/init/entrypoint.sh + +USER node +WORKDIR /core/apps/init +ENTRYPOINT ["dumb-init", "--"] +CMD ["./entrypoint.sh"] \ No newline at end of file diff --git a/packages/core-cli/README.md b/apps/init/README.md similarity index 100% rename from packages/core-cli/README.md rename to apps/init/README.md diff --git a/apps/init/entrypoint.sh b/apps/init/entrypoint.sh new file mode 100644 index 0000000..86b64f0 --- /dev/null +++ b/apps/init/entrypoint.sh @@ -0,0 +1,22 @@ +#!/bin/sh + +# Exit on any error +set -e + +echo "Starting init CLI..." + +# Wait for database to be ready +echo "Waiting for database connection..." +until pg_isready -h "${DB_HOST:-localhost}" -p "${DB_PORT:-5432}" -U "${POSTGRES_USER:-docker}"; do + echo "Database is unavailable - sleeping" + sleep 2 +done + +echo "Database is ready!" + +# Run the init command +echo "Running init command..." +node ./dist/esm/index.js init + +echo "Init completed successfully!" +exit 0 \ No newline at end of file diff --git a/packages/core-cli/package.json b/apps/init/package.json similarity index 95% rename from packages/core-cli/package.json rename to apps/init/package.json index d76c865..7477ad6 100644 --- a/packages/core-cli/package.json +++ b/apps/init/package.json @@ -1,13 +1,13 @@ { - "name": "@redplanethq/core", - "version": "0.1.8", - "description": "A Command-Line Interface for Core", + "name": "@redplanethq/init", + "version": "0.1.0", + "description": "A init service to create trigger instance", "type": "module", "license": "MIT", "repository": { "type": "git", "url": "https://github.com/redplanethq/core", - "directory": "packages/core-cli" + "directory": "apps/init" }, "publishConfig": { "access": "public" @@ -16,7 +16,8 @@ "typescript" ], "files": [ - "dist" + "dist", + "trigger.dump" ], "bin": { "core": "./dist/esm/index.js" diff --git a/packages/core-cli/src/cli/index.ts b/apps/init/src/cli/index.ts similarity index 58% rename from packages/core-cli/src/cli/index.ts rename to apps/init/src/cli/index.ts index 8157f4b..e20545c 100644 --- a/packages/core-cli/src/cli/index.ts +++ b/apps/init/src/cli/index.ts @@ -1,7 +1,5 @@ import { Command } from "commander"; import { initCommand } from "../commands/init.js"; -import { startCommand } from "../commands/start.js"; -import { stopCommand } from "../commands/stop.js"; import { VERSION } from "./version.js"; const program = new Command(); @@ -13,8 +11,4 @@ program .description("Initialize Core development environment (run once)") .action(initCommand); -program.command("start").description("Start Core development environment").action(startCommand); - -program.command("stop").description("Stop Core development environment").action(stopCommand); - program.parse(process.argv); diff --git a/apps/init/src/cli/version.ts b/apps/init/src/cli/version.ts new file mode 100644 index 0000000..2985a76 --- /dev/null +++ b/apps/init/src/cli/version.ts @@ -0,0 +1,3 @@ +import { env } from "../utils/env.js"; + +export const VERSION = env.VERSION; diff --git a/apps/init/src/commands/init.ts b/apps/init/src/commands/init.ts new file mode 100644 index 0000000..83ad673 --- /dev/null +++ b/apps/init/src/commands/init.ts @@ -0,0 +1,36 @@ +import { intro, outro, note } from "@clack/prompts"; +import { printCoreBrainLogo } from "../utils/ascii.js"; +import { initTriggerDatabase, updateWorkerImage } from "../utils/trigger.js"; + +export async function initCommand() { + // Display the CORE brain logo + printCoreBrainLogo(); + + intro("šŸš€ Core Development Environment Setup"); + + try { + await initTriggerDatabase(); + await updateWorkerImage(); + + note( + [ + "Your services will start running:", + "", + "• Core Application: http://localhost:3033", + "• Trigger.dev: http://localhost:8030", + "• PostgreSQL: localhost:5432", + "", + "You can now start developing with Core!", + "", + "ā„¹ļø When logging in to the Core Application, you can find the login URL in the Docker container logs:", + " docker logs core-app --tail 50", + ].join("\n"), + "šŸš€ Services Running" + ); + outro("šŸŽ‰ Setup Complete!"); + process.exit(0); + } catch (error: any) { + outro(`āŒ Setup failed: ${error.message}`); + process.exit(1); + } +} diff --git a/apps/init/src/index.ts b/apps/init/src/index.ts new file mode 100644 index 0000000..44007a1 --- /dev/null +++ b/apps/init/src/index.ts @@ -0,0 +1,3 @@ +#!/usr/bin/env node + +import "./cli/index.js"; diff --git a/packages/core-cli/src/utils/ascii.ts b/apps/init/src/utils/ascii.ts similarity index 100% rename from packages/core-cli/src/utils/ascii.ts rename to apps/init/src/utils/ascii.ts diff --git a/apps/init/src/utils/env.ts b/apps/init/src/utils/env.ts new file mode 100644 index 0000000..f50166c --- /dev/null +++ b/apps/init/src/utils/env.ts @@ -0,0 +1,24 @@ +import { z } from "zod"; + +const EnvironmentSchema = z.object({ + // Version + VERSION: z.string().default("0.1.13"), + + // Database + DB_HOST: z.string().default("localhost"), + DB_PORT: z.string().default("5432"), + TRIGGER_DB: z.string().default("trigger"), + POSTGRES_USER: z.string().default("docker"), + POSTGRES_PASSWORD: z.string().default("docker"), + + // Trigger database + TRIGGER_TASKS_IMAGE: z.string().default("redplanethq/proj_core:latest"), + + // Node environment + NODE_ENV: z + .union([z.literal("development"), z.literal("production"), z.literal("test")]) + .default("development"), +}); + +export type Environment = z.infer; +export const env = EnvironmentSchema.parse(process.env); diff --git a/apps/init/src/utils/trigger.ts b/apps/init/src/utils/trigger.ts new file mode 100644 index 0000000..b8bd389 --- /dev/null +++ b/apps/init/src/utils/trigger.ts @@ -0,0 +1,182 @@ +import Knex from "knex"; +import path from "path"; +import { fileURLToPath } from "url"; +import { env } from "./env.js"; +import { spinner, note, log } from "@clack/prompts"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +/** + * Returns a PostgreSQL database URL for the given database name. + * Throws if required environment variables are missing. + */ +export function getDatabaseUrl(dbName: string): string { + const { POSTGRES_USER, POSTGRES_PASSWORD, DB_HOST, DB_PORT } = env; + + if (!POSTGRES_USER || !POSTGRES_PASSWORD || !DB_HOST || !DB_PORT || !dbName) { + throw new Error( + "One or more required environment variables are missing: POSTGRES_USER, POSTGRES_PASSWORD, DB_HOST, DB_PORT, dbName" + ); + } + + return `postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${DB_HOST}:${DB_PORT}/${dbName}`; +} + +/** + * Checks if the database specified by TRIGGER_DB exists, and creates it if it does not. + * Returns { exists: boolean, created: boolean } - exists indicates success, created indicates if database was newly created. + */ +export async function ensureDatabaseExists(): Promise<{ exists: boolean; created: boolean }> { + const { TRIGGER_DB } = env; + + if (!TRIGGER_DB) { + throw new Error("TRIGGER_DB environment variable is missing"); + } + + // Build a connection string to the default 'postgres' database + const adminDbUrl = getDatabaseUrl("postgres"); + + // Create a Knex instance for the admin connection + const adminKnex = Knex({ + client: "pg", + connection: adminDbUrl, + }); + + const s = spinner(); + s.start("Checking for Trigger.dev database..."); + + try { + // Check if the database exists + const result = await adminKnex.select(1).from("pg_database").where("datname", TRIGGER_DB); + + if (result.length === 0) { + s.message("Database not found. Creating..."); + // Database does not exist, create it + await adminKnex.raw(`CREATE DATABASE "${TRIGGER_DB}"`); + s.stop("Database created."); + return { exists: true, created: true }; + } else { + s.stop("Database exists."); + return { exists: true, created: false }; + } + } catch (err) { + s.stop("Failed to ensure database exists."); + log.warning("Failed to ensure database exists: " + (err as Error).message); + return { exists: false, created: false }; + } finally { + await adminKnex.destroy(); + } +} + +// Main initialization function +export async function initTriggerDatabase() { + const { TRIGGER_DB } = env; + + if (!TRIGGER_DB) { + throw new Error("TRIGGER_DB environment variable is missing"); + } + + // Ensure the database exists + const { exists, created } = await ensureDatabaseExists(); + if (!exists) { + throw new Error("Failed to create or verify database exists"); + } + + // Only run pg_restore if the database was newly created + if (!created) { + note("Database already exists, skipping restore from trigger.dump"); + return; + } + + // Run pg_restore with the trigger.dump file + const dumpFilePath = path.join(__dirname, "../../../trigger.dump"); + const connectionString = getDatabaseUrl(TRIGGER_DB); + + const s = spinner(); + s.start("Restoring database from trigger.dump..."); + + try { + // Use execSync and capture stdout/stderr, send to spinner.log + const { spawn } = await import("child_process"); + await new Promise((resolve, reject) => { + const child = spawn( + "pg_restore", + ["--verbose", "--no-acl", "--no-owner", "-d", connectionString, dumpFilePath], + { stdio: ["ignore", "pipe", "pipe"] } + ); + + child.stdout.on("data", (data) => { + s.message(data.toString()); + }); + + child.stderr.on("data", (data) => { + s.message(data.toString()); + }); + + child.on("close", (code) => { + if (code === 0) { + s.stop("Database restored successfully from trigger.dump"); + resolve(); + } else { + s.stop("Failed to restore database."); + log.warning(`Failed to restore database: pg_restore exited with code ${code}`); + reject(new Error(`Database restore failed: pg_restore exited with code ${code}`)); + } + }); + + child.on("error", (err) => { + s.stop("Failed to restore database."); + log.warning("Failed to restore database: " + err.message); + reject(new Error(`Database restore failed: ${err.message}`)); + }); + }); + } catch (error: any) { + s.stop("Failed to restore database."); + log.warning("Failed to restore database: " + error.message); + throw new Error(`Database restore failed: ${error.message}`); + } +} + +export async function updateWorkerImage() { + const { TRIGGER_DB, TRIGGER_TASKS_IMAGE } = env; + + if (!TRIGGER_DB) { + throw new Error("TRIGGER_DB environment variable is missing"); + } + + const connectionString = getDatabaseUrl(TRIGGER_DB); + + const knex = Knex({ + client: "pg", + connection: connectionString, + }); + + const s = spinner(); + s.start("Updating worker image reference..."); + + try { + // Get the first record from WorkerDeployment table + const firstWorkerDeployment = await knex("WorkerDeployment").select("id").first(); + + if (!firstWorkerDeployment) { + s.stop("No WorkerDeployment records found, skipping image update"); + note("No WorkerDeployment records found, skipping image update"); + return; + } + + // Update the imageReference column with the TRIGGER_TASKS_IMAGE value + await knex("WorkerDeployment").where("id", firstWorkerDeployment.id).update({ + imageReference: TRIGGER_TASKS_IMAGE, + updatedAt: new Date(), + }); + + s.stop(`Successfully updated worker image reference to: ${TRIGGER_TASKS_IMAGE}`); + } catch (error: any) { + s.stop("Failed to update worker image."); + log.warning("Failed to update worker image: " + error.message); + throw new Error(`Worker image update failed: ${error.message}`); + } finally { + await knex.destroy(); + } +} diff --git a/apps/init/trigger.dump b/apps/init/trigger.dump new file mode 100644 index 0000000..46f069f Binary files /dev/null and b/apps/init/trigger.dump differ diff --git a/packages/core-cli/tsconfig.json b/apps/init/tsconfig.json similarity index 100% rename from packages/core-cli/tsconfig.json rename to apps/init/tsconfig.json diff --git a/packages/core-cli/vite.config.ts b/apps/init/vite.config.ts similarity index 100% rename from packages/core-cli/vite.config.ts rename to apps/init/vite.config.ts diff --git a/apps/webapp/app/components/app-sidebar.tsx b/apps/webapp/app/components/app-sidebar.tsx new file mode 100644 index 0000000..74349c9 --- /dev/null +++ b/apps/webapp/app/components/app-sidebar.tsx @@ -0,0 +1,183 @@ +import * as React from "react" +import { + BookOpen, + Bot, + Command, + Frame, + LifeBuoy, + Map, + PieChart, + Send, + Settings2, + SquareTerminal, +} from "lucide-react" + +import { NavMain } from "~/components/nav-main" +import { NavProjects } from "~/components/nav-projects" +import { NavSecondary } from "~/components/nav-secondary" +import { NavUser } from "~/components/nav-user" +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarHeader, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, +} from "~/components/ui/sidebar" + +const data = { + user: { + name: "shadcn", + email: "m@example.com", + avatar: "/avatars/shadcn.jpg", + }, + navMain: [ + { + title: "Playground", + url: "#", + icon: SquareTerminal, + isActive: true, + items: [ + { + title: "History", + url: "#", + }, + { + title: "Starred", + url: "#", + }, + { + title: "Settings", + url: "#", + }, + ], + }, + { + title: "Models", + url: "#", + icon: Bot, + items: [ + { + title: "Genesis", + url: "#", + }, + { + title: "Explorer", + url: "#", + }, + { + title: "Quantum", + url: "#", + }, + ], + }, + { + title: "Documentation", + url: "#", + icon: BookOpen, + items: [ + { + title: "Introduction", + url: "#", + }, + { + title: "Get Started", + url: "#", + }, + { + title: "Tutorials", + url: "#", + }, + { + title: "Changelog", + url: "#", + }, + ], + }, + { + title: "Settings", + url: "#", + icon: Settings2, + items: [ + { + title: "General", + url: "#", + }, + { + title: "Team", + url: "#", + }, + { + title: "Billing", + url: "#", + }, + { + title: "Limits", + url: "#", + }, + ], + }, + ], + navSecondary: [ + { + title: "Support", + url: "#", + icon: LifeBuoy, + }, + { + title: "Feedback", + url: "#", + icon: Send, + }, + ], + projects: [ + { + name: "Design Engineering", + url: "#", + icon: Frame, + }, + { + name: "Sales & Marketing", + url: "#", + icon: PieChart, + }, + { + name: "Travel", + url: "#", + icon: Map, + }, + ], +} + +export function AppSidebar({ ...props }: React.ComponentProps) { + return ( + + + + + + +
+ +
+
+ Acme Inc + Enterprise +
+
+
+
+
+
+ + + + + + + + +
+ ) +} diff --git a/apps/webapp/app/components/graph/graph.tsx b/apps/webapp/app/components/graph/graph.tsx index f3063e3..36543de 100644 --- a/apps/webapp/app/components/graph/graph.tsx +++ b/apps/webapp/app/components/graph/graph.tsx @@ -5,12 +5,12 @@ import { useCallback, useImperativeHandle, forwardRef, - useState, } from "react"; import Sigma from "sigma"; import GraphologyGraph from "graphology"; import forceAtlas2 from "graphology-layout-forceatlas2"; -import noverlap from "graphology-layout-noverlap"; +import FA2Layout from "graphology-layout-forceatlas2/worker"; +import { EdgeLineProgram } from "sigma/rendering"; import colors from "tailwindcss/colors"; import type { GraphTriplet, IdValue, GraphNode } from "./type"; import { @@ -369,6 +369,10 @@ export const Graph = forwardRef( const optimalParams = calculateOptimalParameters(graph); const settings = forceAtlas2.inferSettings(graph); + const fa2Layout = new FA2Layout(graph, { + settings: settings, + }); + forceAtlas2.assign(graph, { iterations: optimalParams.iterations, settings: { @@ -381,21 +385,25 @@ export const Graph = forwardRef( }, }); - noverlap.assign(graph, { - maxIterations: 200, - settings: { - margin: 10, - expansion: 1.5, - gridSize: 30, - }, - }); + // noverlap.assign(graph, { + // maxIterations: 200, + // settings: { + // margin: 10, + // expansion: 1.5, + // gridSize: 30, + // }, + // }); } // Create Sigma instance const sigma = new Sigma(graph, containerRef.current, { renderEdgeLabels: true, - defaultEdgeColor: theme.link.stroke, + defaultEdgeColor: "#0000001A", defaultNodeColor: theme.node.fill, + defaultEdgeType: "edges-fast", + edgeProgramClasses: { + "edges-fast": EdgeLineProgram, + }, enableEdgeEvents: true, minCameraRatio: 0.1, maxCameraRatio: 2, @@ -526,28 +534,6 @@ export const Graph = forwardRef( }; }, [nodes, edges]); - // // Theme update effect - // useEffect(() => { - // if (!sigmaRef.current || !graphRef.current || !isInitializedRef.current) - // return; - // const graph = graphRef.current; - // graph.forEachNode((node) => { - // const nodeData = graph.getNodeAttribute(node, "nodeData"); - // const isHighlighted = graph.getNodeAttribute(node, "highlighted"); - // if (!isHighlighted) { - // graph.setNodeAttribute(node, "color", getNodeColor(nodeData)); - // } - // }); - // graph.forEachEdge((edge) => { - // const isHighlighted = graph.getEdgeAttribute(edge, "highlighted"); - // if (!isHighlighted) { - // graph.setEdgeAttribute(edge, "color", theme.link.stroke); - // } - // }); - // sigmaRef.current.setSetting("defaultEdgeColor", theme.link.stroke); - // sigmaRef.current.setSetting("defaultNodeColor", "red"); - // }, [theme, getNodeColor]); - return (
+
-
+
Image + Platform + + {items.map((item) => ( + + + + + + {item.title} + + + {item.items?.length ? ( + <> + + + + Toggle + + + + + {item.items?.map((subItem) => ( + + + + {subItem.title} + + + + ))} + + + + ) : null} + + + ))} + + + ) +} diff --git a/apps/webapp/app/components/nav-projects.tsx b/apps/webapp/app/components/nav-projects.tsx new file mode 100644 index 0000000..d779ad0 --- /dev/null +++ b/apps/webapp/app/components/nav-projects.tsx @@ -0,0 +1,87 @@ +import { + Folder, + MoreHorizontal, + Share, + Trash2, + type LucideIcon, +} from "lucide-react" + +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "~/components/ui/dropdown-menu" +import { + SidebarGroup, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuAction, + SidebarMenuButton, + SidebarMenuItem, + useSidebar, +} from "~/components/ui/sidebar" + +export function NavProjects({ + projects, +}: { + projects: { + name: string + url: string + icon: LucideIcon + }[] +}) { + const { isMobile } = useSidebar() + + return ( + + Projects + + {projects.map((item) => ( + + + + + {item.name} + + + + + + + More + + + + + + View Project + + + + Share Project + + + + + Delete Project + + + + + ))} + + + + More + + + + + ) +} diff --git a/apps/webapp/app/components/nav-secondary.tsx b/apps/webapp/app/components/nav-secondary.tsx new file mode 100644 index 0000000..2299d95 --- /dev/null +++ b/apps/webapp/app/components/nav-secondary.tsx @@ -0,0 +1,40 @@ +import * as React from "react" +import { type LucideIcon } from "lucide-react" + +import { + SidebarGroup, + SidebarGroupContent, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, +} from "~/components/ui/sidebar" + +export function NavSecondary({ + items, + ...props +}: { + items: { + title: string + url: string + icon: LucideIcon + }[] +} & React.ComponentPropsWithoutRef) { + return ( + + + + {items.map((item) => ( + + + + + {item.title} + + + + ))} + + + + ) +} diff --git a/apps/webapp/app/components/nav-user.tsx b/apps/webapp/app/components/nav-user.tsx new file mode 100644 index 0000000..e2dfec4 --- /dev/null +++ b/apps/webapp/app/components/nav-user.tsx @@ -0,0 +1,114 @@ +"use client" + +import { + BadgeCheck, + Bell, + ChevronsUpDown, + CreditCard, + LogOut, + Sparkles, +} from "lucide-react" + +import { + Avatar, + AvatarFallback, + AvatarImage, +} from "~/components/ui/avatar" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "~/components/ui/dropdown-menu" +import { + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + useSidebar, +} from "~/components/ui/sidebar" + +export function NavUser({ + user, +}: { + user: { + name: string + email: string + avatar: string + } +}) { + const { isMobile } = useSidebar() + + return ( + + + + + + + + CN + +
+ {user.name} + {user.email} +
+ +
+
+ + +
+ + + CN + +
+ {user.name} + {user.email} +
+
+
+ + + + + Upgrade to Pro + + + + + + + Account + + + + Billing + + + + Notifications + + + + + + Log out + +
+
+
+
+ ) +} diff --git a/apps/webapp/app/components/sidebar/app-sidebar.tsx b/apps/webapp/app/components/sidebar/app-sidebar.tsx index 84cfab2..eefa9f6 100644 --- a/apps/webapp/app/components/sidebar/app-sidebar.tsx +++ b/apps/webapp/app/components/sidebar/app-sidebar.tsx @@ -28,7 +28,7 @@ const data = { icon: Network, }, { - title: "Logs", + title: "Activity", url: "/home/logs", icon: Activity, }, diff --git a/apps/webapp/app/components/ui/breadcrumb.tsx b/apps/webapp/app/components/ui/breadcrumb.tsx new file mode 100644 index 0000000..6da6cbc --- /dev/null +++ b/apps/webapp/app/components/ui/breadcrumb.tsx @@ -0,0 +1,109 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { ChevronRight, MoreHorizontal } from "lucide-react" + +import { cn } from "~/lib/utils" + +function Breadcrumb({ ...props }: React.ComponentProps<"nav">) { + return