From 181bd28b016c1abacaeec3cf3581e76011af6152 Mon Sep 17 00:00:00 2001 From: Lewis Carhart Date: Tue, 4 Aug 2026 15:20:35 -0400 Subject: [PATCH 1/5] feat(app): add dashboard and overview components for enhanced user experience - Introduced new dashboard summary and sales dashboard components to provide users with insights into their performance. - Implemented overview greeting and scope toggle for personalized user experience. - Created companies and contacts management pages with search and creation functionalities. - Added data tables for displaying companies and contacts, including sorting and filtering options. - Enhanced routing and layout for better navigation within the application. --- .claude/settings.json | 7 +- apps/agent/test/model.integration.spec.ts | 27 +++- apps/api/src/workspace/workspace.service.ts | 13 +- .../[slug]/companies/[companyId]/page.tsx | 11 ++ .../companies/companies-search-params.ts | 0 .../companies/companies-table.tsx | 0 .../companies/create-company-sheet.tsx | 0 .../app/(app)/{ => [slug]}/companies/page.tsx | 0 .../[slug]/contacts/[contactId]/page.tsx | 11 ++ .../contacts/contacts-search-params.ts | 0 .../{ => [slug]}/contacts/contacts-table.tsx | 0 .../contacts/create-contact-sheet.tsx | 0 .../app/(app)/{ => [slug]}/contacts/page.tsx | 0 .../(app)/{ => [slug]}/dashboard-summary.tsx | 6 +- .../app/(app)/[slug]/deals/[dealId]/page.tsx | 11 ++ .../{ => [slug]}/deals/create-deal-sheet.tsx | 0 .../{ => [slug]}/deals/deals-search-params.ts | 0 .../(app)/{ => [slug]}/deals/deals-table.tsx | 0 .../app/app/(app)/{ => [slug]}/deals/page.tsx | 0 apps/app/app/(app)/{ => [slug]}/layout.tsx | 15 +- .../(app)/{ => [slug]}/overview-greeting.tsx | 0 .../app/(app)/{ => [slug]}/overview-scope.tsx | 0 .../{ => [slug]}/overview-search-params.ts | 0 apps/app/app/(app)/{ => [slug]}/page.tsx | 0 .../(app)/{ => [slug]}/sales-dashboard.tsx | 5 +- .../{ => [slug]}/settings/agent-model.tsx | 0 .../connections/google-connection.tsx | 0 .../settings/connections/page.tsx | 0 .../(app)/{ => [slug]}/settings/layout.tsx | 0 .../settings/members/members-search-params.ts | 0 .../settings/members/members-table.tsx | 0 .../{ => [slug]}/settings/members/page.tsx | 0 .../app/(app)/{ => [slug]}/settings/page.tsx | 0 .../{ => [slug]}/settings/research-key.tsx | 0 .../settings/settings-sidebar.tsx | 21 ++- .../settings/sso/add-sso-provider-sheet.tsx | 0 .../{ => [slug]}/settings/sso/copy-value.tsx | 0 .../(app)/{ => [slug]}/settings/sso/page.tsx | 0 .../settings/sso/sso-search-params.ts | 0 .../{ => [slug]}/settings/sso/sso-table.tsx | 0 .../{ => [slug]}/settings/workspace-form.tsx | 11 +- .../app/(app)/companies/[companyId]/page.tsx | 11 -- .../app/(app)/contacts/[contactId]/page.tsx | 11 -- apps/app/app/(app)/deals/[dealId]/page.tsx | 11 -- .../grant-access/grant-access.tsx | 0 .../grant-access/page.tsx | 0 .../onboarding/onboarding-form.tsx | 0 .../{(auth) => (landing)}/onboarding/page.tsx | 0 .../onboarding/research/page.tsx | 0 .../onboarding/research/research-form.tsx | 0 apps/app/app/(landing)/page.tsx | 61 ++++++++ .../sign-in/google-sign-in.tsx | 0 .../{(auth) => (landing)}/sign-in/page.tsx | 0 .../sign-in/sso-sign-in.tsx | 0 apps/app/components/app-header.tsx | 4 +- apps/app/components/app-icon-rail.tsx | 12 +- apps/app/lib/onboarding.ts | 26 +++- apps/app/lib/record-href.ts | 7 +- apps/app/lib/use-workspace-url.ts | 17 ++ apps/app/lib/workspace-url.ts | 5 + apps/app/proxy.ts | 62 ++++++-- apps/app/test/onboarding-gate.spec.ts | 147 +++++++++++++----- docs/api.md | 51 ++++++ packages/auth/src/index.ts | 1 - packages/auth/src/organization.ts | 17 +- .../test/organization.integration.spec.ts | 9 +- packages/db/src/workspace.ts | 31 ++++ packages/db/test/workspace-slug.spec.ts | 50 ++++++ 68 files changed, 553 insertions(+), 118 deletions(-) create mode 100644 apps/app/app/(app)/[slug]/companies/[companyId]/page.tsx rename apps/app/app/(app)/{ => [slug]}/companies/companies-search-params.ts (100%) rename apps/app/app/(app)/{ => [slug]}/companies/companies-table.tsx (100%) rename apps/app/app/(app)/{ => [slug]}/companies/create-company-sheet.tsx (100%) rename apps/app/app/(app)/{ => [slug]}/companies/page.tsx (100%) create mode 100644 apps/app/app/(app)/[slug]/contacts/[contactId]/page.tsx rename apps/app/app/(app)/{ => [slug]}/contacts/contacts-search-params.ts (100%) rename apps/app/app/(app)/{ => [slug]}/contacts/contacts-table.tsx (100%) rename apps/app/app/(app)/{ => [slug]}/contacts/create-contact-sheet.tsx (100%) rename apps/app/app/(app)/{ => [slug]}/contacts/page.tsx (100%) rename apps/app/app/(app)/{ => [slug]}/dashboard-summary.tsx (97%) create mode 100644 apps/app/app/(app)/[slug]/deals/[dealId]/page.tsx rename apps/app/app/(app)/{ => [slug]}/deals/create-deal-sheet.tsx (100%) rename apps/app/app/(app)/{ => [slug]}/deals/deals-search-params.ts (100%) rename apps/app/app/(app)/{ => [slug]}/deals/deals-table.tsx (100%) rename apps/app/app/(app)/{ => [slug]}/deals/page.tsx (100%) rename apps/app/app/(app)/{ => [slug]}/layout.tsx (68%) rename apps/app/app/(app)/{ => [slug]}/overview-greeting.tsx (100%) rename apps/app/app/(app)/{ => [slug]}/overview-scope.tsx (100%) rename apps/app/app/(app)/{ => [slug]}/overview-search-params.ts (100%) rename apps/app/app/(app)/{ => [slug]}/page.tsx (100%) rename apps/app/app/(app)/{ => [slug]}/sales-dashboard.tsx (97%) rename apps/app/app/(app)/{ => [slug]}/settings/agent-model.tsx (100%) rename apps/app/app/(app)/{ => [slug]}/settings/connections/google-connection.tsx (100%) rename apps/app/app/(app)/{ => [slug]}/settings/connections/page.tsx (100%) rename apps/app/app/(app)/{ => [slug]}/settings/layout.tsx (100%) rename apps/app/app/(app)/{ => [slug]}/settings/members/members-search-params.ts (100%) rename apps/app/app/(app)/{ => [slug]}/settings/members/members-table.tsx (100%) rename apps/app/app/(app)/{ => [slug]}/settings/members/page.tsx (100%) rename apps/app/app/(app)/{ => [slug]}/settings/page.tsx (100%) rename apps/app/app/(app)/{ => [slug]}/settings/research-key.tsx (100%) rename apps/app/app/(app)/{ => [slug]}/settings/settings-sidebar.tsx (74%) rename apps/app/app/(app)/{ => [slug]}/settings/sso/add-sso-provider-sheet.tsx (100%) rename apps/app/app/(app)/{ => [slug]}/settings/sso/copy-value.tsx (100%) rename apps/app/app/(app)/{ => [slug]}/settings/sso/page.tsx (100%) rename apps/app/app/(app)/{ => [slug]}/settings/sso/sso-search-params.ts (100%) rename apps/app/app/(app)/{ => [slug]}/settings/sso/sso-table.tsx (100%) rename apps/app/app/(app)/{ => [slug]}/settings/workspace-form.tsx (91%) delete mode 100644 apps/app/app/(app)/companies/[companyId]/page.tsx delete mode 100644 apps/app/app/(app)/contacts/[contactId]/page.tsx delete mode 100644 apps/app/app/(app)/deals/[dealId]/page.tsx rename apps/app/app/{(auth) => (landing)}/grant-access/grant-access.tsx (100%) rename apps/app/app/{(auth) => (landing)}/grant-access/page.tsx (100%) rename apps/app/app/{(auth) => (landing)}/onboarding/onboarding-form.tsx (100%) rename apps/app/app/{(auth) => (landing)}/onboarding/page.tsx (100%) rename apps/app/app/{(auth) => (landing)}/onboarding/research/page.tsx (100%) rename apps/app/app/{(auth) => (landing)}/onboarding/research/research-form.tsx (100%) create mode 100644 apps/app/app/(landing)/page.tsx rename apps/app/app/{(auth) => (landing)}/sign-in/google-sign-in.tsx (100%) rename apps/app/app/{(auth) => (landing)}/sign-in/page.tsx (100%) rename apps/app/app/{(auth) => (landing)}/sign-in/sso-sign-in.tsx (100%) create mode 100644 apps/app/lib/use-workspace-url.ts create mode 100644 apps/app/lib/workspace-url.ts create mode 100644 packages/db/test/workspace-slug.spec.ts diff --git a/.claude/settings.json b/.claude/settings.json index 4bfc5569..97251ad9 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -1,3 +1,8 @@ { - "worktree": { "bgIsolation": "none" } + "worktree": { + "bgIsolation": "none" + }, + "enabledPlugins": { + "paper-desktop@paper": true + } } diff --git a/apps/agent/test/model.integration.spec.ts b/apps/agent/test/model.integration.spec.ts index f4cc839b..13f5e17c 100644 --- a/apps/agent/test/model.integration.spec.ts +++ b/apps/agent/test/model.integration.spec.ts @@ -1,5 +1,13 @@ -import { afterEach, beforeEach, describe, expect, it } from "bun:test"; -import { db } from "@crm/db"; +import { + afterAll, + afterEach, + beforeAll, + beforeEach, + describe, + expect, + it, +} from "bun:test"; +import { db, type Prisma } from "@crm/db"; import { DEFAULT_AGENT_MODEL, readAgentModel, @@ -12,9 +20,24 @@ async function clear() { await db.appSetting.deleteMany({ where: { id: SETTINGS_ID } }); } +/** + * The row holds the Context key a rep typed and the model they chose, and + * DATABASE_URL is somebody's working database. Deleting it and not putting it + * back sends them through the research-key gate again with nothing saying why. + */ +let saved: Prisma.AppSettingUncheckedCreateInput | null = null; + +beforeAll(async () => { + saved = await db.appSetting.findUnique({ where: { id: SETTINGS_ID } }); +}); + beforeEach(clear); afterEach(clear); +afterAll(async () => { + if (saved) await db.appSetting.create({ data: saved }); +}); + describe("the configured model", () => { it("falls back when nothing has ever been chosen", async () => { const setting = await readAgentModel(db); diff --git a/apps/api/src/workspace/workspace.service.ts b/apps/api/src/workspace/workspace.service.ts index 1f776036..bce1d687 100644 --- a/apps/api/src/workspace/workspace.service.ts +++ b/apps/api/src/workspace/workspace.service.ts @@ -7,7 +7,7 @@ import { type WorkspaceRole, } from "@crm/auth"; import type { Db, Prisma } from "@crm/db"; -import { isOnboarded, markOnboarded } from "@crm/db/workspace"; +import { isOnboarded, markOnboarded, workspaceSlug } from "@crm/db/workspace"; import { BadRequestException, ForbiddenException, @@ -34,6 +34,7 @@ import type { export interface Workspace { id: string; + slug: string; name: string; website: string | null; onboarded: boolean; @@ -104,6 +105,7 @@ export class WorkspaceService { return { id: row.id, + slug: row.slug, name: row.name, website: row.website, onboarded: isOnboarded(row.metadata), @@ -142,6 +144,7 @@ export class WorkspaceService { where: { id: WORKSPACE_ID }, data: { name: input.name, + slug: workspaceSlug(input.name), website, metadata: markOnboarded(before?.metadata ?? null, new Date()), }, @@ -286,7 +289,13 @@ export class WorkspaceService { private async readWorkspace() { return this.db.organization.findUnique({ where: { id: WORKSPACE_ID }, - select: { id: true, name: true, website: true, metadata: true }, + select: { + id: true, + slug: true, + name: true, + website: true, + metadata: true, + }, }); } diff --git a/apps/app/app/(app)/[slug]/companies/[companyId]/page.tsx b/apps/app/app/(app)/[slug]/companies/[companyId]/page.tsx new file mode 100644 index 00000000..bba9b2d4 --- /dev/null +++ b/apps/app/app/(app)/[slug]/companies/[companyId]/page.tsx @@ -0,0 +1,11 @@ +import { redirect } from "next/navigation"; +import { recordHref } from "@/lib/record-href"; + +export default async function RecordRedirect({ + params, +}: { + params: Promise<{ slug: string; companyId: string }>; +}) { + const { slug, companyId } = await params; + redirect(recordHref(slug, "/companies", "company", companyId)); +} diff --git a/apps/app/app/(app)/companies/companies-search-params.ts b/apps/app/app/(app)/[slug]/companies/companies-search-params.ts similarity index 100% rename from apps/app/app/(app)/companies/companies-search-params.ts rename to apps/app/app/(app)/[slug]/companies/companies-search-params.ts diff --git a/apps/app/app/(app)/companies/companies-table.tsx b/apps/app/app/(app)/[slug]/companies/companies-table.tsx similarity index 100% rename from apps/app/app/(app)/companies/companies-table.tsx rename to apps/app/app/(app)/[slug]/companies/companies-table.tsx diff --git a/apps/app/app/(app)/companies/create-company-sheet.tsx b/apps/app/app/(app)/[slug]/companies/create-company-sheet.tsx similarity index 100% rename from apps/app/app/(app)/companies/create-company-sheet.tsx rename to apps/app/app/(app)/[slug]/companies/create-company-sheet.tsx diff --git a/apps/app/app/(app)/companies/page.tsx b/apps/app/app/(app)/[slug]/companies/page.tsx similarity index 100% rename from apps/app/app/(app)/companies/page.tsx rename to apps/app/app/(app)/[slug]/companies/page.tsx diff --git a/apps/app/app/(app)/[slug]/contacts/[contactId]/page.tsx b/apps/app/app/(app)/[slug]/contacts/[contactId]/page.tsx new file mode 100644 index 00000000..42cd6d80 --- /dev/null +++ b/apps/app/app/(app)/[slug]/contacts/[contactId]/page.tsx @@ -0,0 +1,11 @@ +import { redirect } from "next/navigation"; +import { recordHref } from "@/lib/record-href"; + +export default async function RecordRedirect({ + params, +}: { + params: Promise<{ slug: string; contactId: string }>; +}) { + const { slug, contactId } = await params; + redirect(recordHref(slug, "/contacts", "contact", contactId)); +} diff --git a/apps/app/app/(app)/contacts/contacts-search-params.ts b/apps/app/app/(app)/[slug]/contacts/contacts-search-params.ts similarity index 100% rename from apps/app/app/(app)/contacts/contacts-search-params.ts rename to apps/app/app/(app)/[slug]/contacts/contacts-search-params.ts diff --git a/apps/app/app/(app)/contacts/contacts-table.tsx b/apps/app/app/(app)/[slug]/contacts/contacts-table.tsx similarity index 100% rename from apps/app/app/(app)/contacts/contacts-table.tsx rename to apps/app/app/(app)/[slug]/contacts/contacts-table.tsx diff --git a/apps/app/app/(app)/contacts/create-contact-sheet.tsx b/apps/app/app/(app)/[slug]/contacts/create-contact-sheet.tsx similarity index 100% rename from apps/app/app/(app)/contacts/create-contact-sheet.tsx rename to apps/app/app/(app)/[slug]/contacts/create-contact-sheet.tsx diff --git a/apps/app/app/(app)/contacts/page.tsx b/apps/app/app/(app)/[slug]/contacts/page.tsx similarity index 100% rename from apps/app/app/(app)/contacts/page.tsx rename to apps/app/app/(app)/[slug]/contacts/page.tsx diff --git a/apps/app/app/(app)/dashboard-summary.tsx b/apps/app/app/(app)/[slug]/dashboard-summary.tsx similarity index 97% rename from apps/app/app/(app)/dashboard-summary.tsx rename to apps/app/app/(app)/[slug]/dashboard-summary.tsx index 7f5a4581..2ea1128d 100644 --- a/apps/app/app/(app)/dashboard-summary.tsx +++ b/apps/app/app/(app)/[slug]/dashboard-summary.tsx @@ -44,6 +44,7 @@ import { useOpenRecord } from "@/components/crm/record-sheet/record-stack"; import { activityLabel } from "@/components/crm/timeline/activity-icon"; import { useCrmCache } from "@/lib/trpc/cache"; import { useTRPC } from "@/lib/trpc/client"; +import { useWorkspaceUrl } from "@/lib/use-workspace-url"; import { overviewParsers } from "./overview-search-params"; import { SalesDashboard } from "./sales-dashboard"; @@ -53,6 +54,7 @@ export function DashboardSummary() { const trpc = useTRPC(); const cache = useCrmCache(); const openRecord = useOpenRecord(); + const workspaceUrl = useWorkspaceUrl(); const [scope] = useQueryState("scope", overviewParsers.scope); @@ -121,7 +123,7 @@ export function DashboardSummary() { @@ -240,7 +242,7 @@ export function DashboardSummary() { diff --git a/apps/app/app/(app)/[slug]/deals/[dealId]/page.tsx b/apps/app/app/(app)/[slug]/deals/[dealId]/page.tsx new file mode 100644 index 00000000..edb3fcec --- /dev/null +++ b/apps/app/app/(app)/[slug]/deals/[dealId]/page.tsx @@ -0,0 +1,11 @@ +import { redirect } from "next/navigation"; +import { recordHref } from "@/lib/record-href"; + +export default async function RecordRedirect({ + params, +}: { + params: Promise<{ slug: string; dealId: string }>; +}) { + const { slug, dealId } = await params; + redirect(recordHref(slug, "/deals", "deal", dealId)); +} diff --git a/apps/app/app/(app)/deals/create-deal-sheet.tsx b/apps/app/app/(app)/[slug]/deals/create-deal-sheet.tsx similarity index 100% rename from apps/app/app/(app)/deals/create-deal-sheet.tsx rename to apps/app/app/(app)/[slug]/deals/create-deal-sheet.tsx diff --git a/apps/app/app/(app)/deals/deals-search-params.ts b/apps/app/app/(app)/[slug]/deals/deals-search-params.ts similarity index 100% rename from apps/app/app/(app)/deals/deals-search-params.ts rename to apps/app/app/(app)/[slug]/deals/deals-search-params.ts diff --git a/apps/app/app/(app)/deals/deals-table.tsx b/apps/app/app/(app)/[slug]/deals/deals-table.tsx similarity index 100% rename from apps/app/app/(app)/deals/deals-table.tsx rename to apps/app/app/(app)/[slug]/deals/deals-table.tsx diff --git a/apps/app/app/(app)/deals/page.tsx b/apps/app/app/(app)/[slug]/deals/page.tsx similarity index 100% rename from apps/app/app/(app)/deals/page.tsx rename to apps/app/app/(app)/[slug]/deals/page.tsx diff --git a/apps/app/app/(app)/layout.tsx b/apps/app/app/(app)/[slug]/layout.tsx similarity index 68% rename from apps/app/app/(app)/layout.tsx rename to apps/app/app/(app)/[slug]/layout.tsx index 9b3af361..750312fd 100644 --- a/apps/app/app/(app)/layout.tsx +++ b/apps/app/app/(app)/[slug]/layout.tsx @@ -1,3 +1,4 @@ +import { notFound } from "next/navigation"; import { AppHeader } from "@/components/app-header"; import { AppIconRail } from "@/components/app-icon-rail"; import { QuickSwitcher } from "@/components/crm/quick-switcher"; @@ -5,13 +6,25 @@ import { RecordSheetHost } from "@/components/crm/record-sheet/record-sheet-host import { MobileNavProvider } from "@/components/mobile-nav"; import { requireGoogleAccess } from "@/lib/session"; import { HydrateClient } from "@/lib/trpc/hydrate"; +import { getServerQueryClient, getServerTrpc } from "@/lib/trpc/server"; export default async function AppLayout({ children, + params, }: Readonly<{ children: React.ReactNode; + params: Promise<{ slug: string }>; }>) { - const { user } = await requireGoogleAccess(); + const [{ user }, { slug }] = await Promise.all([ + requireGoogleAccess(), + params, + ]); + + const workspace = await getServerQueryClient() + .fetchQuery(getServerTrpc().workspace.get.queryOptions()) + .catch(() => null); + + if (workspace && workspace.slug !== slug) notFound(); return ( diff --git a/apps/app/app/(app)/overview-greeting.tsx b/apps/app/app/(app)/[slug]/overview-greeting.tsx similarity index 100% rename from apps/app/app/(app)/overview-greeting.tsx rename to apps/app/app/(app)/[slug]/overview-greeting.tsx diff --git a/apps/app/app/(app)/overview-scope.tsx b/apps/app/app/(app)/[slug]/overview-scope.tsx similarity index 100% rename from apps/app/app/(app)/overview-scope.tsx rename to apps/app/app/(app)/[slug]/overview-scope.tsx diff --git a/apps/app/app/(app)/overview-search-params.ts b/apps/app/app/(app)/[slug]/overview-search-params.ts similarity index 100% rename from apps/app/app/(app)/overview-search-params.ts rename to apps/app/app/(app)/[slug]/overview-search-params.ts diff --git a/apps/app/app/(app)/page.tsx b/apps/app/app/(app)/[slug]/page.tsx similarity index 100% rename from apps/app/app/(app)/page.tsx rename to apps/app/app/(app)/[slug]/page.tsx diff --git a/apps/app/app/(app)/sales-dashboard.tsx b/apps/app/app/(app)/[slug]/sales-dashboard.tsx similarity index 97% rename from apps/app/app/(app)/sales-dashboard.tsx rename to apps/app/app/(app)/[slug]/sales-dashboard.tsx index e18c6f75..721d72a0 100644 --- a/apps/app/app/(app)/sales-dashboard.tsx +++ b/apps/app/app/(app)/[slug]/sales-dashboard.tsx @@ -20,6 +20,7 @@ import type { ReactNode } from "react"; import { dealStageColor, dealStageLabel } from "@/components/crm/deal-stage"; import { AreaTrend, DonutStat } from "@/components/dashboard-charts"; import type { RouterOutputs } from "@/lib/trpc/types"; +import { useWorkspaceUrl } from "@/lib/use-workspace-url"; type Summary = RouterOutputs["dashboard"]["summary"]; @@ -43,6 +44,8 @@ function changeDelta( } export function SalesDashboard({ summary }: { summary: Summary }) { + const workspaceUrl = useWorkspaceUrl(); + const { pipeline, wonThisMonth, @@ -154,7 +157,7 @@ export function SalesDashboard({ summary }: { summary: Summary }) { {stageSlices.map((slice) => (
  • ITEMS.map((item) => ({ ...item, href: workspaceUrl(item.href) })), + [workspaceUrl], + ); return ( <> @@ -64,11 +73,11 @@ export function SettingsSidebar() { aria-label="Workspace settings" className="flex flex-col gap-0.5 p-3" > - {ITEMS.map((item) => ( + {items.map((item) => ( ))} @@ -79,11 +88,11 @@ export function SettingsSidebar() { aria-label="Workspace settings" className="flex gap-1 overflow-x-auto border-b p-2 md:hidden [view-transition-name:settings-sidebar]" > - {ITEMS.map((item) => ( + {items.map((item) => ( ))} diff --git a/apps/app/app/(app)/settings/sso/add-sso-provider-sheet.tsx b/apps/app/app/(app)/[slug]/settings/sso/add-sso-provider-sheet.tsx similarity index 100% rename from apps/app/app/(app)/settings/sso/add-sso-provider-sheet.tsx rename to apps/app/app/(app)/[slug]/settings/sso/add-sso-provider-sheet.tsx diff --git a/apps/app/app/(app)/settings/sso/copy-value.tsx b/apps/app/app/(app)/[slug]/settings/sso/copy-value.tsx similarity index 100% rename from apps/app/app/(app)/settings/sso/copy-value.tsx rename to apps/app/app/(app)/[slug]/settings/sso/copy-value.tsx diff --git a/apps/app/app/(app)/settings/sso/page.tsx b/apps/app/app/(app)/[slug]/settings/sso/page.tsx similarity index 100% rename from apps/app/app/(app)/settings/sso/page.tsx rename to apps/app/app/(app)/[slug]/settings/sso/page.tsx diff --git a/apps/app/app/(app)/settings/sso/sso-search-params.ts b/apps/app/app/(app)/[slug]/settings/sso/sso-search-params.ts similarity index 100% rename from apps/app/app/(app)/settings/sso/sso-search-params.ts rename to apps/app/app/(app)/[slug]/settings/sso/sso-search-params.ts diff --git a/apps/app/app/(app)/settings/sso/sso-table.tsx b/apps/app/app/(app)/[slug]/settings/sso/sso-table.tsx similarity index 100% rename from apps/app/app/(app)/settings/sso/sso-table.tsx rename to apps/app/app/(app)/[slug]/settings/sso/sso-table.tsx diff --git a/apps/app/app/(app)/settings/workspace-form.tsx b/apps/app/app/(app)/[slug]/settings/workspace-form.tsx similarity index 91% rename from apps/app/app/(app)/settings/workspace-form.tsx rename to apps/app/app/(app)/[slug]/settings/workspace-form.tsx index 737105e5..9ced0371 100644 --- a/apps/app/app/(app)/settings/workspace-form.tsx +++ b/apps/app/app/(app)/[slug]/settings/workspace-form.tsx @@ -24,14 +24,19 @@ import { } from "@crm/ui/components/input-group"; import { Spinner } from "@crm/ui/components/spinner"; import { useMutation, useQuery } from "@tanstack/react-query"; +import { useRouter } from "next/navigation"; import { useId, useState } from "react"; import { toast } from "sonner"; import { useCrmCache } from "@/lib/trpc/cache"; import { useTRPC } from "@/lib/trpc/client"; +import { useWorkspaceSlug } from "@/lib/use-workspace-url"; +import { workspaceUrl } from "@/lib/workspace-url"; export function WorkspaceForm() { const trpc = useTRPC(); const cache = useCrmCache(); + const router = useRouter(); + const slug = useWorkspaceSlug(); const nameId = useId(); const websiteId = useId(); @@ -44,10 +49,14 @@ export function WorkspaceForm() { const save = useMutation( trpc.workspace.update.mutationOptions({ - onSuccess: async () => { + onSuccess: async (saved) => { await cache.workspace(); setDraft(null); toast.success("Workspace saved."); + + if (saved.slug !== slug) { + router.replace(workspaceUrl(saved.slug, "/settings")); + } }, onError: (error) => toast.error(error.message), }), diff --git a/apps/app/app/(app)/companies/[companyId]/page.tsx b/apps/app/app/(app)/companies/[companyId]/page.tsx deleted file mode 100644 index d6f6e3ea..00000000 --- a/apps/app/app/(app)/companies/[companyId]/page.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { redirect } from "next/navigation"; -import { recordHref } from "@/lib/record-href"; - -export default async function CompanyRedirect({ - params, -}: { - params: Promise<{ companyId: string }>; -}) { - const { companyId } = await params; - redirect(recordHref("/companies", "company", companyId)); -} diff --git a/apps/app/app/(app)/contacts/[contactId]/page.tsx b/apps/app/app/(app)/contacts/[contactId]/page.tsx deleted file mode 100644 index ed76688c..00000000 --- a/apps/app/app/(app)/contacts/[contactId]/page.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { redirect } from "next/navigation"; -import { recordHref } from "@/lib/record-href"; - -export default async function ContactRedirect({ - params, -}: { - params: Promise<{ contactId: string }>; -}) { - const { contactId } = await params; - redirect(recordHref("/contacts", "contact", contactId)); -} diff --git a/apps/app/app/(app)/deals/[dealId]/page.tsx b/apps/app/app/(app)/deals/[dealId]/page.tsx deleted file mode 100644 index 5481fcc5..00000000 --- a/apps/app/app/(app)/deals/[dealId]/page.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { redirect } from "next/navigation"; -import { recordHref } from "@/lib/record-href"; - -export default async function DealRedirect({ - params, -}: { - params: Promise<{ dealId: string }>; -}) { - const { dealId } = await params; - redirect(recordHref("/deals", "deal", dealId)); -} diff --git a/apps/app/app/(auth)/grant-access/grant-access.tsx b/apps/app/app/(landing)/grant-access/grant-access.tsx similarity index 100% rename from apps/app/app/(auth)/grant-access/grant-access.tsx rename to apps/app/app/(landing)/grant-access/grant-access.tsx diff --git a/apps/app/app/(auth)/grant-access/page.tsx b/apps/app/app/(landing)/grant-access/page.tsx similarity index 100% rename from apps/app/app/(auth)/grant-access/page.tsx rename to apps/app/app/(landing)/grant-access/page.tsx diff --git a/apps/app/app/(auth)/onboarding/onboarding-form.tsx b/apps/app/app/(landing)/onboarding/onboarding-form.tsx similarity index 100% rename from apps/app/app/(auth)/onboarding/onboarding-form.tsx rename to apps/app/app/(landing)/onboarding/onboarding-form.tsx diff --git a/apps/app/app/(auth)/onboarding/page.tsx b/apps/app/app/(landing)/onboarding/page.tsx similarity index 100% rename from apps/app/app/(auth)/onboarding/page.tsx rename to apps/app/app/(landing)/onboarding/page.tsx diff --git a/apps/app/app/(auth)/onboarding/research/page.tsx b/apps/app/app/(landing)/onboarding/research/page.tsx similarity index 100% rename from apps/app/app/(auth)/onboarding/research/page.tsx rename to apps/app/app/(landing)/onboarding/research/page.tsx diff --git a/apps/app/app/(auth)/onboarding/research/research-form.tsx b/apps/app/app/(landing)/onboarding/research/research-form.tsx similarity index 100% rename from apps/app/app/(auth)/onboarding/research/research-form.tsx rename to apps/app/app/(landing)/onboarding/research/research-form.tsx diff --git a/apps/app/app/(landing)/page.tsx b/apps/app/app/(landing)/page.tsx new file mode 100644 index 00000000..c5f7e540 --- /dev/null +++ b/apps/app/app/(landing)/page.tsx @@ -0,0 +1,61 @@ +import { Button } from "@crm/ui/components/button"; +import Logo from "@crm/ui/components/logo"; +import Link from "next/link"; +import { AuthShader } from "@/components/auth-shader"; + +export default function Home() { + return ( +
    + + +
    + + + + +
    + +
    +
    + +
    +
    +
    +

    + CRM +

    +

    + Every customer, one place. +

    +

    + Your inbox and calendar become the record. A research agent works + out who everybody is, and what each account is worth. +

    +
    + +
    + +
    +
    +
    + + +
    + ); +} diff --git a/apps/app/app/(auth)/sign-in/google-sign-in.tsx b/apps/app/app/(landing)/sign-in/google-sign-in.tsx similarity index 100% rename from apps/app/app/(auth)/sign-in/google-sign-in.tsx rename to apps/app/app/(landing)/sign-in/google-sign-in.tsx diff --git a/apps/app/app/(auth)/sign-in/page.tsx b/apps/app/app/(landing)/sign-in/page.tsx similarity index 100% rename from apps/app/app/(auth)/sign-in/page.tsx rename to apps/app/app/(landing)/sign-in/page.tsx diff --git a/apps/app/app/(auth)/sign-in/sso-sign-in.tsx b/apps/app/app/(landing)/sign-in/sso-sign-in.tsx similarity index 100% rename from apps/app/app/(auth)/sign-in/sso-sign-in.tsx rename to apps/app/app/(landing)/sign-in/sso-sign-in.tsx diff --git a/apps/app/components/app-header.tsx b/apps/app/components/app-header.tsx index 0a402240..318034f2 100644 --- a/apps/app/components/app-header.tsx +++ b/apps/app/components/app-header.tsx @@ -24,6 +24,7 @@ import { useTheme } from "next-themes"; import { toast } from "sonner"; import { useMobileNav } from "@/components/mobile-nav"; import { useTRPC } from "@/lib/trpc/client"; +import { useWorkspaceUrl } from "@/lib/use-workspace-url"; type User = { name: string; email: string; image: string | null }; @@ -44,6 +45,7 @@ export function workspaceLabel(name: string | undefined): string { export function AppHeader({ user }: { user: User }) { const { setOpen: setMobileNavOpen } = useMobileNav(); const trpc = useTRPC(); + const workspaceUrl = useWorkspaceUrl(); const workspace = useQuery(trpc.workspace.get.queryOptions()); const label = workspaceLabel(workspace.data?.name); @@ -71,7 +73,7 @@ export function AppHeader({ user }: { user: User }) { diff --git a/apps/app/components/app-icon-rail.tsx b/apps/app/components/app-icon-rail.tsx index 659ae1df..ba09f274 100644 --- a/apps/app/components/app-icon-rail.tsx +++ b/apps/app/components/app-icon-rail.tsx @@ -22,7 +22,9 @@ import { import { cn } from "@crm/ui/lib/utils"; import Link from "next/link"; import { usePathname } from "next/navigation"; +import { useMemo } from "react"; import { useMobileNav } from "@/components/mobile-nav"; +import { useWorkspaceUrl } from "@/lib/use-workspace-url"; type RailItem = { title: string; @@ -113,15 +115,21 @@ function MobileRailLink({ export function AppIconRail() { const pathname = usePathname(); + const workspaceUrl = useWorkspaceUrl(); const { open, setOpen } = useMobileNav(); + const items = useMemo( + () => ITEMS.map((item) => ({ ...item, href: workspaceUrl(item.href) })), + [workspaceUrl], + ); + return ( <>