├── .gitignore
├── README.md
├── bun.lockb
├── components.json
├── eslint.config.js
├── index.html
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
├── favicon.ico
├── lovable-uploads
│ ├── 0e3b9242-069b-4a19-b5ad-8f96b69d7d54.png
│ ├── 19d0bac1-2f20-4dcb-8a71-c65c4635deb8.png
│ ├── 2e7bc354-d939-480c-b0dc-7aa03dbde994.png
│ └── 56808916-d8ae-4d5a-8aa0-f0d671bc7717.png
├── mockServiceWorker.js
├── placeholder.svg
└── robots.txt
├── src
├── App.css
├── App.tsx
├── ClientProfile.tsx
├── components
│ ├── CustomErrorBoundary.tsx
│ ├── ErrorBoundary.tsx
│ ├── auth
│ │ ├── LoginForm.tsx
│ │ ├── ProtectedRoute.tsx
│ │ └── callback.tsx
│ ├── calendar
│ │ ├── BookingLink.tsx
│ │ ├── BookingView.tsx
│ │ ├── CalendarContacts.tsx
│ │ ├── CalendarIntegration.tsx
│ │ ├── CalendarIntegrationDialog.tsx
│ │ ├── CalendlyBookingSystem.tsx
│ │ ├── EditBookingTypeDialog.tsx
│ │ └── MakeCalendarIntegration.tsx
│ ├── chatbot
│ │ ├── ChatDrawer.tsx
│ │ ├── ChatbotUI.tsx
│ │ └── KnowledgeEditor.tsx
│ ├── clients
│ │ └── ClientsTable.tsx
│ ├── compatibility
│ │ └── BrowserCompatibilityCheck.tsx
│ ├── content
│ │ ├── ContentCreationForm.tsx
│ │ ├── ContentEditDialog.tsx
│ │ ├── ContentList.tsx
│ │ ├── ContentScheduling.tsx
│ │ └── SocialMediaConnect.tsx
│ ├── dashboard
│ │ ├── ActivityFeed.tsx
│ │ ├── DashboardStats.tsx
│ │ ├── DealsOverview.tsx
│ │ ├── StatCard.tsx
│ │ └── TasksPanel.tsx
│ ├── deals
│ │ ├── CustomFieldsManager.tsx
│ │ ├── DealDetailDialog.tsx
│ │ ├── DealForm.tsx
│ │ ├── DealFormFields.tsx
│ │ ├── EditDealDialog.tsx
│ │ └── types.ts
│ ├── deployment
│ │ └── DeploymentChecklist.tsx
│ ├── email
│ │ └── MailchimpConnect.tsx
│ ├── integrations
│ │ ├── CustomWebhookConnect.tsx
│ │ ├── GoogleCalendarConnect.tsx
│ │ ├── MakeConnect.tsx
│ │ ├── YextConnect.tsx
│ │ └── ZapierConnect.tsx
│ ├── layout
│ │ ├── MobileSidebar.tsx
│ │ ├── Navbar.tsx
│ │ └── Sidebar.tsx
│ ├── master-account
│ │ ├── AddClientForm.tsx
│ │ ├── ClientDirectory.tsx
│ │ ├── ClientPerformanceTable.tsx
│ │ ├── ClientSalesChart.tsx
│ │ ├── ClientSwitcher.tsx
│ │ └── SalesSummaryCards.tsx
│ ├── notifications
│ │ └── NotificationsPopover.tsx
│ ├── pipeline
│ │ └── StageManager.tsx
│ ├── settings
│ │ └── TeamMembers.tsx
│ ├── theme
│ │ ├── ThemeProvider.tsx
│ │ └── ThemeToggle.tsx
│ ├── ui
│ │ ├── accordion.tsx
│ │ ├── alert-dialog.tsx
│ │ ├── alert.tsx
│ │ ├── aspect-ratio.tsx
│ │ ├── avatar.tsx
│ │ ├── badge.tsx
│ │ ├── breadcrumb.tsx
│ │ ├── button.tsx
│ │ ├── calendar.tsx
│ │ ├── card.tsx
│ │ ├── carousel.tsx
│ │ ├── chart.tsx
│ │ ├── checkbox.tsx
│ │ ├── collapsible.tsx
│ │ ├── column-customizer.tsx
│ │ ├── command.tsx
│ │ ├── context-menu.tsx
│ │ ├── dialog.tsx
│ │ ├── drawer.tsx
│ │ ├── dropdown-menu.tsx
│ │ ├── error-fallback.tsx
│ │ ├── form.tsx
│ │ ├── hover-card.tsx
│ │ ├── input-otp.tsx
│ │ ├── input.tsx
│ │ ├── label.tsx
│ │ ├── menubar.tsx
│ │ ├── navigation-menu.tsx
│ │ ├── pagination.tsx
│ │ ├── popover.tsx
│ │ ├── progress.tsx
│ │ ├── radio-group.tsx
│ │ ├── resizable.tsx
│ │ ├── scroll-area.tsx
│ │ ├── select.tsx
│ │ ├── separator.tsx
│ │ ├── sheet.tsx
│ │ ├── sidebar.tsx
│ │ ├── skeleton.tsx
│ │ ├── slider.tsx
│ │ ├── sonner.tsx
│ │ ├── switch.tsx
│ │ ├── table.tsx
│ │ ├── tabs.tsx
│ │ ├── textarea.tsx
│ │ ├── toast.tsx
│ │ ├── toaster.tsx
│ │ ├── toggle-group.tsx
│ │ ├── toggle.tsx
│ │ ├── tooltip.tsx
│ │ └── use-toast.ts
│ └── website
│ │ ├── AddPageDialog.tsx
│ │ ├── AllPagesTab.tsx
│ │ ├── DevicesTab.tsx
│ │ ├── EditPageDialog.tsx
│ │ ├── InsightsTab.tsx
│ │ ├── LandingPagesTab.tsx
│ │ ├── RealTimeAnalytics.tsx
│ │ ├── RealTimeTab.tsx
│ │ ├── RealTimeTabAdapter.tsx
│ │ └── WebsiteStats.tsx
├── config.ts
├── config
│ └── deploymentConfig.ts
├── constants
│ └── storageKeys.ts
├── contexts
│ ├── AuthContext.tsx
│ ├── CustomFieldsContext.tsx
│ ├── DealsContext.tsx
│ ├── MasterAccountContext.tsx
│ ├── TasksContext.tsx
│ └── TeamContext.tsx
├── data
│ └── initialData.ts
├── hooks
│ ├── use-mobile.tsx
│ ├── use-toast.ts
│ ├── useActivityTracker.ts
│ ├── useClients.ts
│ ├── useContacts
│ ├── useContentItems.ts
│ ├── useDealsStorage.ts
│ ├── useExternalIntegrations.ts
│ ├── useFormError.ts
│ ├── useMasterAccount.tsx
│ ├── useNotifications.ts
│ ├── useWebhooks.ts
│ ├── useWebsiteActions.ts
│ └── useWebsitePages.ts
├── index.css
├── lib
│ ├── analytics.ts
│ ├── api.ts
│ ├── data.ts
│ ├── errorHandling.ts
│ └── utils.ts
├── main.tsx
├── pages
│ ├── Account.tsx
│ ├── Booking.tsx
│ ├── Calendar.tsx
│ ├── ChatbotManagement.tsx
│ ├── ClientProfile.tsx
│ ├── Clients.tsx
│ ├── Contacts.tsx
│ ├── Content.tsx
│ ├── ContentScheduling.tsx
│ ├── Conversations.tsx
│ ├── Deals.tsx
│ ├── EmailMarketing.tsx
│ ├── Index.tsx
│ ├── Integrations.tsx
│ ├── Login.tsx
│ ├── MasterAccount.tsx
│ ├── NotFound.tsx
│ ├── Pipeline.tsx
│ ├── Projects.tsx
│ ├── Reports.tsx
│ ├── Reputation.tsx
│ ├── Settings.tsx
│ ├── SocialMediaIntegration.tsx
│ └── WebsiteManagement.tsx
├── services
│ ├── api.ts
│ ├── auth.ts
│ ├── hubspot_auth.ts
│ ├── mailchimp.ts
│ ├── socialMedia.ts
│ └── yext.ts
├── types
│ ├── masterAccount.ts
│ └── website.ts
├── utils
│ ├── dateUtils.ts
│ ├── formatters.ts
│ ├── taskUtils.ts
│ ├── websitePageAdapter.ts
│ └── websiteStatsCalculator.ts
└── vite-env.d.ts
├── tailwind.config.ts
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts
/.gitignore:
--------------------------------------------------------------------------------
1 | # Node modules
2 | node_modules/
3 |
4 | # Build outputs
5 | dist/
6 | build/
7 |
8 | # Logs
9 | *.log
10 | npm-debug.log*
11 | yarn-debug.log*
12 | yarn-error.log*
13 | pnpm-debug.log*
14 |
15 | # Environment files
16 | .env
17 | .env.* # covers .env.local, .env.production, etc.
18 |
19 | # OS-specific
20 | .DS_Store
21 | Thumbs.db
22 |
23 | # IDE settings
24 | .vscode/
25 | .idea/
26 |
27 | # TypeScript cache
28 | *.tsbuildinfo
29 |
30 | # Misc
31 | *.local
32 | *.swp
33 | *.swo
34 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Welcome to your Lovable project
2 |
3 | A modern React-based frontend for the Contact Pipeline Haven application, featuring a beautiful UI built with shadcn-ui and Tailwind CSS.
4 |
5 | ## Features
6 |
7 | - Modern, responsive UI with dark mode support
8 | - Real-time activity feed
9 | - Task management and tracking
10 | - Team collaboration features
11 | - Contact management
12 | - Email integration
13 | - Settings and user preferences
14 |
15 | ## Tech Stack
16 |
17 | - **Core**:
18 | - Vite
19 | - TypeScript
20 | - React
21 | - React Router
22 | - React Query
23 |
24 | - **UI/UX**:
25 | - shadcn-ui
26 | - Tailwind CSS
27 | - Lucide Icons
28 | - React Hot Toast
29 |
30 | ## Getting Started
31 |
32 | ### Prerequisites
33 |
34 | - Node.js (v16 or higher)
35 | - npm (v7 or higher)
36 |
37 | ### Installation
38 |
39 | 1. Navigate to the frontend directory:
40 | ```sh
41 | cd frontend
42 | ```
43 |
44 | 2. Install dependencies:
45 | ```sh
46 | npm install
47 | ```
48 |
49 | 3. Set up environment variables:
50 | Create a `.env` file in the frontend directory:
51 | ```env
52 | VITE_API_URL=http://localhost:3000/api
53 | ```
54 |
55 | 4. Start the development server:
56 | ```sh
57 | npm run dev
58 | ```
59 |
60 | The application will be available at `http://localhost:5173`
61 |
62 | ## Development
63 |
64 | - Development server runs on port 5173
65 | - Hot module replacement (HMR) enabled
66 | - TypeScript for type safety
67 | - ESLint and Prettier for code formatting
68 |
69 | ## Available Scripts
70 |
71 | - `npm run dev` - Start development server
72 | - `npm run build` - Build for production
73 | - `npm run preview` - Preview production build
74 | - `npm run lint` - Run ESLint
75 | - `npm run format` - Format code with Prettier
76 |
77 | ## Project Structure
78 |
79 | ```
80 | frontend/
81 | ├── src/
82 | │ ├── components/ # Reusable UI components
83 | │ ├── contexts/ # React contexts
84 | │ ├── hooks/ # Custom React hooks
85 | │ ├── pages/ # Page components
86 | │ ├── services/ # API services
87 | │ ├── styles/ # Global styles
88 | │ ├── types/ # TypeScript types
89 | │ └── utils/ # Utility functions
90 | ├── public/ # Static assets
91 | └── index.html # Entry HTML file
92 | ```
93 |
94 | ## UI Components
95 |
96 | The project uses shadcn-ui components, which are built on top of Radix UI and styled with Tailwind CSS. Key components include:
97 |
98 | - Cards
99 | - Buttons
100 | - Forms
101 | - Modals
102 | - Toast notifications
103 | - Dropdowns
104 | - Tables
105 |
106 | ## Contributing
107 |
108 | 1. Fork the repository
109 | 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
110 | 3. Commit your changes (`git commit -m 'Add some amazing feature'`)
111 | 4. Push to the branch (`git push origin feature/amazing-feature`)
112 | 5. Open a Pull Request
113 |
114 | ## License
115 |
116 | This project is licensed under the MIT License - see the LICENSE file for details.
117 |
--------------------------------------------------------------------------------
/bun.lockb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hermesdev0131/lovable-frontend-react-tailwindcss-vite/103d4ab8aee3559672c7b5fcf307d1097b6f22bc/bun.lockb
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": false,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "src/index.css",
9 | "baseColor": "slate",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils",
16 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | }
20 | }
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | import js from "@eslint/js";
2 | import globals from "globals";
3 | import reactHooks from "eslint-plugin-react-hooks";
4 | import reactRefresh from "eslint-plugin-react-refresh";
5 | import tseslint from "typescript-eslint";
6 |
7 | export default tseslint.config(
8 | { ignores: ["dist"] },
9 | {
10 | extends: [js.configs.recommended, ...tseslint.configs.recommended],
11 | files: ["**/*.{ts,tsx}"],
12 | languageOptions: {
13 | ecmaVersion: 2020,
14 | globals: globals.browser,
15 | },
16 | plugins: {
17 | "react-hooks": reactHooks,
18 | "react-refresh": reactRefresh,
19 | },
20 | rules: {
21 | ...reactHooks.configs.recommended.rules,
22 | "react-refresh/only-export-components": [
23 | "warn",
24 | { allowConstantExport: true },
25 | ],
26 | "@typescript-eslint/no-unused-vars": "off",
27 | },
28 | }
29 | );
30 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | contact-pipeline-haven
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vite_react_shadcn_ts",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vite build",
9 | "build:dev": "vite build --mode development",
10 | "lint": "eslint .",
11 | "preview": "vite preview"
12 | },
13 | "dependencies": {
14 | "@hello-pangea/dnd": "^18.0.1",
15 | "@hookform/resolvers": "^3.9.0",
16 | "@hubspot/api-client": "^12.1.0",
17 | "@radix-ui/react-accordion": "^1.2.0",
18 | "@radix-ui/react-alert-dialog": "^1.1.1",
19 | "@radix-ui/react-aspect-ratio": "^1.1.0",
20 | "@radix-ui/react-avatar": "^1.1.0",
21 | "@radix-ui/react-checkbox": "^1.1.1",
22 | "@radix-ui/react-collapsible": "^1.1.0",
23 | "@radix-ui/react-context-menu": "^2.2.1",
24 | "@radix-ui/react-dialog": "^1.1.2",
25 | "@radix-ui/react-dropdown-menu": "^2.1.1",
26 | "@radix-ui/react-hover-card": "^1.1.1",
27 | "@radix-ui/react-label": "^2.1.0",
28 | "@radix-ui/react-menubar": "^1.1.1",
29 | "@radix-ui/react-navigation-menu": "^1.2.0",
30 | "@radix-ui/react-popover": "^1.1.1",
31 | "@radix-ui/react-progress": "^1.1.0",
32 | "@radix-ui/react-radio-group": "^1.2.0",
33 | "@radix-ui/react-scroll-area": "^1.1.0",
34 | "@radix-ui/react-select": "^2.1.1",
35 | "@radix-ui/react-separator": "^1.1.0",
36 | "@radix-ui/react-slider": "^1.2.0",
37 | "@radix-ui/react-slot": "^1.1.0",
38 | "@radix-ui/react-switch": "^1.1.0",
39 | "@radix-ui/react-tabs": "^1.1.0",
40 | "@radix-ui/react-toast": "^1.2.1",
41 | "@radix-ui/react-toggle": "^1.1.0",
42 | "@radix-ui/react-toggle-group": "^1.1.0",
43 | "@radix-ui/react-tooltip": "^1.1.4",
44 | "@tanstack/react-query": "^5.56.2",
45 | "@types/uuid": "^10.0.0",
46 | "axios": "^1.8.4",
47 | "class-variance-authority": "^0.7.1",
48 | "clsx": "^2.1.1",
49 | "cmdk": "^1.0.0",
50 | "date-fns": "^3.6.0",
51 | "embla-carousel-react": "^8.3.0",
52 | "input-otp": "^1.2.4",
53 | "lucide-react": "^0.462.0",
54 | "next": "^15.3.1",
55 | "next-themes": "^0.3.0",
56 | "react": "^18.3.1",
57 | "react-day-picker": "^8.10.1",
58 | "react-dom": "^18.3.1",
59 | "react-error-boundary": "^5.0.0",
60 | "react-hook-form": "^7.56.1",
61 | "react-resizable-panels": "^2.1.3",
62 | "react-router-dom": "^6.26.2",
63 | "recharts": "^2.12.7",
64 | "sonner": "^1.5.0",
65 | "tailwind-merge": "^2.5.2",
66 | "tailwindcss-animate": "^1.0.7",
67 | "uuid": "^11.1.0",
68 | "vaul": "^0.9.3",
69 | "zod": "^3.23.8"
70 | },
71 | "devDependencies": {
72 | "@eslint/js": "^9.9.0",
73 | "@tailwindcss/typography": "^0.5.15",
74 | "@types/next": "^8.0.7",
75 | "@types/node": "^22.5.5",
76 | "@types/react": "^18.3.3",
77 | "@types/react-dom": "^18.3.0",
78 | "@vitejs/plugin-react-swc": "^3.5.0",
79 | "autoprefixer": "^10.4.21",
80 | "eslint": "^9.9.0",
81 | "eslint-plugin-react-hooks": "^5.1.0-rc.0",
82 | "eslint-plugin-react-refresh": "^0.4.9",
83 | "globals": "^15.9.0",
84 | "lovable-tagger": "^1.1.7",
85 | "postcss": "^8.5.3",
86 | "tailwindcss": "^3.4.17",
87 | "typescript": "^5.5.3",
88 | "typescript-eslint": "^8.0.1",
89 | "vite": "^5.4.1"
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hermesdev0131/lovable-frontend-react-tailwindcss-vite/103d4ab8aee3559672c7b5fcf307d1097b6f22bc/public/favicon.ico
--------------------------------------------------------------------------------
/public/lovable-uploads/0e3b9242-069b-4a19-b5ad-8f96b69d7d54.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hermesdev0131/lovable-frontend-react-tailwindcss-vite/103d4ab8aee3559672c7b5fcf307d1097b6f22bc/public/lovable-uploads/0e3b9242-069b-4a19-b5ad-8f96b69d7d54.png
--------------------------------------------------------------------------------
/public/lovable-uploads/19d0bac1-2f20-4dcb-8a71-c65c4635deb8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hermesdev0131/lovable-frontend-react-tailwindcss-vite/103d4ab8aee3559672c7b5fcf307d1097b6f22bc/public/lovable-uploads/19d0bac1-2f20-4dcb-8a71-c65c4635deb8.png
--------------------------------------------------------------------------------
/public/lovable-uploads/2e7bc354-d939-480c-b0dc-7aa03dbde994.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hermesdev0131/lovable-frontend-react-tailwindcss-vite/103d4ab8aee3559672c7b5fcf307d1097b6f22bc/public/lovable-uploads/2e7bc354-d939-480c-b0dc-7aa03dbde994.png
--------------------------------------------------------------------------------
/public/lovable-uploads/56808916-d8ae-4d5a-8aa0-f0d671bc7717.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hermesdev0131/lovable-frontend-react-tailwindcss-vite/103d4ab8aee3559672c7b5fcf307d1097b6f22bc/public/lovable-uploads/56808916-d8ae-4d5a-8aa0-f0d671bc7717.png
--------------------------------------------------------------------------------
/public/placeholder.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: Googlebot
2 | Allow: /
3 |
4 | User-agent: Bingbot
5 | Allow: /
6 |
7 | User-agent: Twitterbot
8 | Allow: /
9 |
10 | User-agent: facebookexternalhit
11 | Allow: /
12 |
13 | User-agent: *
14 | Allow: /
15 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 |
2 | #root {
3 | width: 100%;
4 | margin: 0 auto;
5 | overflow-x: hidden;
6 | }
7 |
8 | .logo {
9 | height: 6em;
10 | padding: 1.5em;
11 | will-change: filter;
12 | transition: filter 300ms;
13 | }
14 | .logo:hover {
15 | filter: drop-shadow(0 0 2em #D35400aa);
16 | }
17 | .logo.react:hover {
18 | filter: drop-shadow(0 0 2em #D35400aa);
19 | }
20 |
21 | @keyframes logo-spin {
22 | from {
23 | transform: rotate(0deg);
24 | }
25 | to {
26 | transform: rotate(360deg);
27 | }
28 | }
29 |
30 | @media (prefers-reduced-motion: no-preference) {
31 | a:nth-of-type(2) .logo {
32 | animation: logo-spin infinite 20s linear;
33 | }
34 | }
35 |
36 | .card {
37 | padding: 2em;
38 | background-color: white;
39 | color: black;
40 | }
41 |
42 | .read-the-docs {
43 | color: #888;
44 | }
45 |
46 | /* Animation for activity feed items */
47 | @keyframes slide-in-right {
48 | 0% {
49 | transform: translateX(20px);
50 | opacity: 0;
51 | }
52 | 100% {
53 | transform: translateX(0);
54 | opacity: 1;
55 | }
56 | }
57 |
58 | .animate-slide-in-right {
59 | animation: slide-in-right 0.3s ease-out forwards;
60 | }
61 |
62 | /* Animation for deal cards */
63 | @keyframes fade-in-up {
64 | 0% {
65 | transform: translateY(10px);
66 | opacity: 0;
67 | }
68 | 100% {
69 | transform: translateY(0);
70 | opacity: 1;
71 | }
72 | }
73 |
74 | .animate-fade-in-up {
75 | animation: fade-in-up 0.3s ease-out forwards;
76 | }
77 |
78 | /* Animation for form fields */
79 | @keyframes fade-in {
80 | 0% {
81 | opacity: 0;
82 | }
83 | 100% {
84 | opacity: 1;
85 | }
86 | }
87 |
88 | .animate-fade-in {
89 | animation: fade-in 0.3s ease-out forwards;
90 | }
91 |
92 | /* Staggered animation for list items */
93 | .stagger-animation > *:nth-child(1) { animation-delay: 0s; }
94 | .stagger-animation > *:nth-child(2) { animation-delay: 0.05s; }
95 | .stagger-animation > *:nth-child(3) { animation-delay: 0.1s; }
96 | .stagger-animation > *:nth-child(4) { animation-delay: 0.15s; }
97 | .stagger-animation > *:nth-child(5) { animation-delay: 0.2s; }
98 | .stagger-animation > *:nth-child(6) { animation-delay: 0.25s; }
99 | .stagger-animation > *:nth-child(7) { animation-delay: 0.3s; }
100 | .stagger-animation > *:nth-child(8) { animation-delay: 0.35s; }
101 | .stagger-animation > *:nth-child(9) { animation-delay: 0.4s; }
102 | .stagger-animation > *:nth-child(10) { animation-delay: 0.45s; }
103 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React, { use } from 'react';
2 | //import { BrowserRouter } from 'react-router-dom';
3 | import { ThemeProvider } from '@/components/theme/ThemeProvider';
4 | import { MasterAccountProvider } from './contexts/MasterAccountContext';
5 | import { CustomFieldsProvider } from './contexts/CustomFieldsContext';
6 | import Sidebar from '@/components/layout/Sidebar';
7 | import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
8 | import { Toaster } from "@/components/ui/toaster";
9 | import { DealsProvider } from './contexts/DealsContext';
10 | import { TasksProvider } from './contexts/TasksContext';
11 | import { TeamProvider } from './contexts/TeamContext';
12 | import Index from './pages/Index';
13 | import Reputation from './pages/Reputation';
14 | import Contacts from './pages/Contacts';
15 | import MasterAccount from './pages/MasterAccount';
16 | import Reports from './pages/Reports';
17 | import ClientProfile from './pages/ClientProfile'; // Ensure correct import
18 | import Deals from './pages/Deals';
19 | import Login from './pages/Login';
20 | import EmailMarketing from './pages/EmailMarketing';
21 | import Calendar from './pages/Calendar';
22 | import WebsiteManagement from './pages/WebsiteManagement';
23 | import Socials from './pages/Content';
24 | import Clients from './pages/Clients';
25 | import SettingsPage from './pages/Settings';
26 | import ChatbotManagement from './pages/ChatbotManagement';
27 | import { ProtectedRoute } from './components/auth/ProtectedRoute';
28 | import { AuthProvider } from '@/contexts/AuthContext';
29 | //import { useAuth } from '@/contexts/AuthContext';
30 | //import { AuthCallback } from './components/auth/callback';
31 |
32 | // MainLayout component inline since it was missing
33 | const MainLayout = ({ children }: { children: React.ReactNode }) => {
34 | const [isExpanded, setIsExpanded] = React.useState(true);
35 |
36 | const handleToggleSidebar = () => {
37 | setIsExpanded(!isExpanded);
38 | };
39 |
40 | return (
41 |
42 | {children}
43 |
44 | );
45 | };
46 |
47 | const RoutesComponent = () => {
48 | return (
49 |
50 |
51 | } />
52 | } />
53 |
54 | }>
55 | } />
56 | } />
57 | } />
58 | } />
59 | } />
60 | } />
61 | } />
62 | } />
63 | } />
64 | } />
65 | } />
66 | } />
67 | {}} />} />
68 |
69 |
70 | {/* } /> */}
71 |
72 | );
73 | };
74 |
75 | function App() {
76 | return (
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 | );
99 | }
100 |
101 |
102 | export default App;
103 |
--------------------------------------------------------------------------------
/src/ClientProfile.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useRouter } from 'next/router';
3 |
4 | const ClientProfile = () => {
5 | const router = useRouter();
6 | const { clientId } = router.query;
7 |
8 | // Fetch client data based on clientId (this is a placeholder for actual data fetching logic)
9 | const clientData = {
10 | id: clientId,
11 | name: 'Client Name',
12 | email: 'client@example.com',
13 | phone: '123-456-7890',
14 | };
15 |
16 | return (
17 |
18 |
Client Profile
19 |
ID: {clientData.id}
20 |
Name: {clientData.name}
21 |
Email: {clientData.email}
22 |
Phone: {clientData.phone}
23 |
24 | );
25 | };
26 |
27 | export default ClientProfile;
28 |
--------------------------------------------------------------------------------
/src/components/CustomErrorBoundary.tsx:
--------------------------------------------------------------------------------
1 |
2 | import React from 'react';
3 | import { ErrorBoundary } from 'react-error-boundary';
4 | import { Button } from './ui/button';
5 |
6 | function ErrorFallback({ error, resetErrorBoundary }: { error: Error; resetErrorBoundary: () => void }) {
7 | return (
8 |
9 |
10 |
Something went wrong
11 |
12 |
An error occurred in the application.
13 |
14 | {error.message}
15 |
16 |
17 |
20 |
21 |
22 | );
23 | }
24 |
25 | const CustomErrorBoundary: React.FC<{ children: React.ReactNode }> = ({ children }) => {
26 | return (
27 |
28 | {children}
29 |
30 | );
31 | };
32 |
33 | export default CustomErrorBoundary;
34 |
--------------------------------------------------------------------------------
/src/components/ErrorBoundary.tsx:
--------------------------------------------------------------------------------
1 |
2 | import React from 'react';
3 | import { ErrorBoundary as ReactErrorBoundary, FallbackProps } from 'react-error-boundary';
4 | import { AlertCircle } from 'lucide-react';
5 | import { Button } from "@/components/ui/button";
6 |
7 | const ErrorFallback = ({ error, resetErrorBoundary }: FallbackProps) => {
8 | return (
9 |
10 |
11 |
14 |
Something went wrong
15 |
16 | An error occurred in the application. Please try refreshing the page.
17 |
18 |
19 |
20 |
Error details:
21 |
22 | {error.message}
23 |
24 | {error.stack && (
25 | <>
26 |
Stack trace:
27 |
28 | {error.stack}
29 |
30 | >
31 | )}
32 |
33 |
34 |
35 |
41 |
47 |
48 |
49 |
50 | );
51 | };
52 |
53 | export const ErrorBoundary: React.FC<{children: React.ReactNode}> = ({ children }) => {
54 | return (
55 |
56 | {children}
57 |
58 | );
59 | };
60 |
61 | export default ErrorBoundary;
62 |
--------------------------------------------------------------------------------
/src/components/auth/ProtectedRoute.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { Navigate, Outlet, useLocation } from 'react-router-dom';
3 | import { useAuth } from '@/contexts/AuthContext';
4 | import { toast } from '@/hooks/use-toast';
5 |
6 | export const ProtectedRoute = () => {
7 | const { authState } = useAuth();
8 | const location = useLocation();
9 |
10 | if (!authState.isAuthenticated) {
11 | const isRedirectFromLogin = location.state?.from?.pathname === '/login';
12 |
13 | if (location.pathname !== '/login' && location.pathname !== '/' && !isRedirectFromLogin) {
14 | toast({
15 | title: "Authentication Required",
16 | description: "Please log in to access this page.",
17 | variant: "destructive"
18 | });
19 | }
20 | return ;
21 | }
22 |
23 | if (authState.user && authState.user.role) {
24 | const { role } = authState.user;
25 | const adminOnlyPaths = ['/master-account'];
26 | const editorRestrictedPaths = ['/settings'];
27 |
28 | if (role === 'viewer' && (adminOnlyPaths.includes(location.pathname))) {
29 | toast({
30 | title: "Access Denied",
31 | description: "You don't have permission to access this page",
32 | variant: "destructive"
33 | });
34 | return ;
35 | }
36 |
37 | if (role === 'editor' && adminOnlyPaths.includes(location.pathname)) {
38 | toast({
39 | title: "Access Denied",
40 | description: "Admin access required for this page",
41 | variant: "destructive"
42 | });
43 | return ;
44 | }
45 | }
46 |
47 | // If we reach here, the user is authenticated and has the right permissions
48 | return ;
49 | }
50 |
--------------------------------------------------------------------------------
/src/components/auth/callback.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { useNavigate, useSearchParams } from 'react-router-dom';
3 | import { hubspotAuthService } from '@/services/hubspot_auth';
4 | import { useToast } from '@/hooks/use-toast';
5 |
6 | export const AuthCallback = () => {
7 | const [searchParams] = useSearchParams();
8 | const navigate = useNavigate();
9 | const { toast } = useToast();
10 |
11 | useEffect(() => {
12 | const code = searchParams.get('code');
13 | const error = searchParams.get('error');
14 |
15 | if (error) {
16 | toast({
17 | title: "Authentication Error",
18 | description: `Failed to authenticate with HubSpot: ${error}`,
19 | variant: "destructive"
20 | });
21 | navigate('/login');
22 | return;
23 | }
24 |
25 | if (!code) {
26 | toast({
27 | title: "Authentication Error",
28 | description: "No authorization code received",
29 | variant: "destructive"
30 | });
31 | navigate('/login');
32 | return;
33 | }
34 |
35 | const handleAuth = async () => {
36 | try {
37 | await hubspotAuthService.login(code);
38 | const user = await hubspotAuthService.getUser();
39 |
40 | toast({
41 | title: "Success",
42 | description: `Successfully connected to HubSpot as ${user.email}`
43 | });
44 |
45 | navigate('/');
46 | } catch (error) {
47 | console.error('Authentication error:', error);
48 | toast({
49 | title: "Authentication Error",
50 | description: "Failed to authenticate with Hubspot",
51 | variant: "destructive"
52 | });
53 | navigate('/login');
54 | }
55 | };
56 |
57 | handleAuth();
58 | }, [searchParams, navigate, toast]);
59 |
60 | return (
61 |
62 |
63 |
Connecting to HubSpot...
64 |
Please wait while we authenticate your account.
65 |
66 |
67 | );
68 | };
--------------------------------------------------------------------------------
/src/components/calendar/CalendarIntegrationDialog.tsx:
--------------------------------------------------------------------------------
1 |
2 | import React from 'react';
3 | import { Dialog, DialogContent, DialogTrigger } from '@/components/ui/dialog';
4 | import { Button } from '@/components/ui/button';
5 | import { Calendar } from 'lucide-react';
6 | import CalendarIntegration from './CalendarIntegration';
7 |
8 | interface CalendarIntegrationDialogProps {
9 | onSync?: () => void;
10 | triggerButtonText?: string;
11 | triggerButtonVariant?: 'default' | 'outline' | 'secondary' | 'ghost' | 'link' | 'destructive';
12 | }
13 |
14 | const CalendarIntegrationDialog: React.FC = ({
15 | onSync = () => {},
16 | triggerButtonText = "Connect Calendar",
17 | triggerButtonVariant = "default"
18 | }) => {
19 | const [open, setOpen] = React.useState(false);
20 |
21 | const handleSync = () => {
22 | onSync();
23 | };
24 |
25 | const handleClose = () => {
26 | setOpen(false);
27 | };
28 |
29 | return (
30 |
44 | );
45 | };
46 |
47 | export default CalendarIntegrationDialog;
48 |
--------------------------------------------------------------------------------
/src/components/chatbot/ChatDrawer.tsx:
--------------------------------------------------------------------------------
1 |
2 | import React, { useState } from 'react';
3 | import { Bot, X } from 'lucide-react';
4 | import { Button } from '@/components/ui/button';
5 | import { Drawer, DrawerContent, DrawerTrigger } from '@/components/ui/drawer';
6 | import { ChatbotUI } from './ChatbotUI';
7 | import { useActivityTracker } from '@/hooks/useActivityTracker';
8 |
9 | interface ChatDrawerProps {
10 | knowledgeBase: string[];
11 | onAddKnowledge?: (knowledge: string) => void;
12 | }
13 |
14 | export function ChatDrawer({ knowledgeBase, onAddKnowledge }: ChatDrawerProps) {
15 | const [open, setOpen] = useState(false);
16 | const { trackChatbotInteraction } = useActivityTracker();
17 |
18 | const handleAddKnowledge = (knowledge: string) => {
19 | if (onAddKnowledge) {
20 | onAddKnowledge(knowledge);
21 | }
22 | };
23 |
24 | // Track when user opens the chatbot
25 | const handleOpenChange = (isOpen: boolean) => {
26 | setOpen(isOpen);
27 | if (isOpen) {
28 | trackChatbotInteraction("Opened chatbot");
29 | }
30 | };
31 |
32 | return (
33 |
34 |
35 |
41 |
42 |
43 |
44 |
45 |
50 |
51 |
52 |
53 | );
54 | }
55 |
--------------------------------------------------------------------------------
/src/components/compatibility/BrowserCompatibilityCheck.tsx:
--------------------------------------------------------------------------------
1 |
2 | import React, { useEffect, useState } from 'react';
3 | import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
4 | import { AlertTriangle } from "lucide-react";
5 |
6 | const BrowserCompatibilityCheck: React.FC = () => {
7 | const [isCompatible, setIsCompatible] = useState(true);
8 | const [browserInfo, setBrowserInfo] = useState('');
9 |
10 | useEffect(() => {
11 | const checkBrowserCompatibility = () => {
12 | const userAgent = navigator.userAgent.toLowerCase();
13 |
14 | // Get browser information for display
15 | let browserName = 'Unknown Browser';
16 | if (userAgent.indexOf('chrome') > -1) browserName = 'Google Chrome';
17 | else if (userAgent.indexOf('firefox') > -1) browserName = 'Firefox';
18 | else if (userAgent.indexOf('safari') > -1 && userAgent.indexOf('chrome') === -1) browserName = 'Safari';
19 | else if (userAgent.indexOf('edge') > -1 || userAgent.indexOf('edg') > -1) browserName = 'Microsoft Edge';
20 | else if (userAgent.indexOf('msie') > -1 || userAgent.indexOf('trident') > -1) browserName = 'Internet Explorer';
21 |
22 | // Set browser info for display
23 | setBrowserInfo(browserName);
24 |
25 | // Check for Internet Explorer
26 | const isIE = userAgent.indexOf('msie') > -1 || userAgent.indexOf('trident') > -1;
27 |
28 | // Check for very old browsers that don't support modern JS features
29 | const isOldBrowser = !window.Promise || !window.fetch;
30 |
31 | setIsCompatible(!isIE && !isOldBrowser);
32 | };
33 |
34 | checkBrowserCompatibility();
35 | }, []);
36 |
37 | if (isCompatible) {
38 | return null;
39 | }
40 |
41 | return (
42 |
43 |
44 | Browser Compatibility Issue
45 |
46 | Your browser ({browserInfo}) may not be fully compatible with this application.
47 | For the best experience, please use a modern browser like Google Chrome, Firefox, Microsoft Edge, or Safari.
48 |
49 |
50 | );
51 | };
52 |
53 | export default BrowserCompatibilityCheck;
54 |
--------------------------------------------------------------------------------
/src/components/content/ContentEditDialog.tsx:
--------------------------------------------------------------------------------
1 |
2 | import React, { useState } from 'react';
3 | import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog";
4 | import { Button } from "@/components/ui/button";
5 | import ContentCreationForm from './ContentCreationForm';
6 | import { useIsMobile } from "@/hooks/use-mobile";
7 | import { ContentItem } from "@/types/masterAccount";
8 | import { toast } from "@/hooks/use-toast";
9 | import { logError } from "@/lib/errorHandling";
10 | import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@/components/ui/sheet";
11 |
12 | interface ContentEditDialogProps {
13 | isOpen: boolean;
14 | onClose: () => void;
15 | contentItem: ContentItem | null;
16 | }
17 |
18 | const ContentEditDialog: React.FC = ({
19 | isOpen,
20 | onClose,
21 | contentItem
22 | }) => {
23 | const isMobile = useIsMobile();
24 | const [isSubmitting, setIsSubmitting] = useState(false);
25 |
26 | if (!contentItem) return null;
27 |
28 | const handleSuccess = () => {
29 | toast({
30 | title: "Content Updated",
31 | description: "Your content has been updated successfully."
32 | });
33 | onClose();
34 | };
35 |
36 | const handleError = (error: unknown) => {
37 | setIsSubmitting(false);
38 | logError(error, "Failed to update content");
39 | };
40 |
41 | // Use Sheet component for tablet-sized devices
42 | if (isMobile) {
43 | return (
44 |
45 |
46 |
47 | Edit Content
48 |
49 | Make changes to your content
50 |
51 |
52 |
53 |
60 |
61 |
62 |
65 |
66 |
67 |
68 | );
69 | }
70 |
71 | return (
72 |
89 | );
90 | };
91 |
92 | export default ContentEditDialog;
93 |
--------------------------------------------------------------------------------
/src/components/content/ContentScheduling.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
3 | import { formatDate } from '@/utils/formatters';
4 | import { useMasterAccount } from '@/contexts/MasterAccountContext';
5 |
6 | // This is just to make sure we have a formatter function available
7 | // in case it's needed for handling any potentially undefined dates
8 | const safeFormatDate = (dateString?: string | null) => {
9 | if (!dateString) return '';
10 | return formatDate(dateString);
11 | };
12 |
13 | const ContentScheduling = () => {
14 | const { contentItems } = useMasterAccount();
15 |
16 | // Rest of the component implementation
17 | return (
18 |
19 |
20 | Content Scheduling
21 |
22 |
23 | {/* Your content scheduling UI */}
24 |
25 | {contentItems.length === 0 ? (
26 |
No content items scheduled yet.
27 | ) : (
28 |
29 | {contentItems.map(item => (
30 | -
31 | {item.title} - {item.scheduledFor ? safeFormatDate(item.scheduledFor) : 'Not scheduled'}
32 |
33 | ))}
34 |
35 | )}
36 |
37 |
38 |
39 | );
40 | };
41 |
42 | export default ContentScheduling;
43 |
--------------------------------------------------------------------------------
/src/components/dashboard/DashboardStats.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Users, LineChart, PieChart } from 'lucide-react';
3 | import StatCard from './StatCard';
4 | import { formatCurrency } from '@/utils/formatters';
5 | import { useAuth } from '@/contexts/AuthContext';
6 |
7 | interface DashboardStatsProps {
8 | totalContacts: number;
9 | openDeals: number;
10 | totalDealValue: number;
11 | onCardClick: (title: string, path: string) => void;
12 | isLoading?: boolean;
13 | }
14 |
15 | const DashboardStats: React.FC = ({
16 | totalContacts,
17 | openDeals,
18 | totalDealValue,
19 | onCardClick,
20 | isLoading = false
21 | }) => {
22 | const { authState } = useAuth();
23 | const isAuthenticated = authState?.isAuthenticated ?? false;
24 |
25 | const handleCardClick = (title: string, path: string) => {
26 | if (!isAuthenticated) return;
27 | onCardClick(title, path);
28 | };
29 |
30 | return (
31 |
32 | handleCardClick("Contacts", "/clients")}
38 | isLoading={isLoading}
39 | disabled={!isAuthenticated}
40 | />
41 |
42 | handleCardClick("Deals", "/deals")}
48 | isLoading={isLoading}
49 | disabled={!isAuthenticated}
50 | />
51 |
52 | handleCardClick("Opportunities", "/reports")}
58 | isLoading={isLoading}
59 | disabled={!isAuthenticated}
60 | />
61 |
62 | );
63 | };
64 |
65 | export default DashboardStats;
66 |
--------------------------------------------------------------------------------
/src/components/dashboard/DealsOverview.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
3 | import { PieChart, Pie, Cell, ResponsiveContainer, Legend, Tooltip } from 'recharts';
4 | import { Button } from '@/components/ui/button';
5 | import { useNavigate } from 'react-router-dom';
6 | import { Plus } from 'lucide-react';
7 | import { useAuth } from '@/contexts/AuthContext';
8 |
9 | interface ChartData {
10 | name: string;
11 | value: number;
12 | color: string;
13 | }
14 |
15 | interface DealsOverviewProps {
16 | dealStageData: ChartData[];
17 | hasDeals: boolean;
18 | }
19 |
20 | const DealsOverview: React.FC = ({ dealStageData, hasDeals }) => {
21 | const navigate = useNavigate();
22 | const { authState } = useAuth();
23 | const isAuthenticated = authState?.isAuthenticated ?? false;
24 |
25 | const handleCreateDeal = () => {
26 | if (!isAuthenticated) return;
27 | navigate('/deals');
28 | };
29 |
30 | return (
31 |
34 |
35 | Deals Overview
36 |
37 |
38 | {hasDeals ? (
39 |
40 |
41 |
51 | {dealStageData.map((entry, index) => (
52 | |
53 | ))}
54 |
55 |
73 |
74 | ) : (
75 |
76 |
No deals data available
77 |
86 |
87 | )}
88 |
89 |
90 | );
91 | };
92 |
93 | export default DealsOverview;
94 |
--------------------------------------------------------------------------------
/src/components/dashboard/StatCard.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
3 | import { Loader2 } from 'lucide-react';
4 |
5 | interface StatCardProps {
6 | title: string;
7 | value: number | string;
8 | icon: React.ElementType;
9 | subtitle: string;
10 | onClick?: () => void;
11 | isLoading?: boolean;
12 | disabled?: boolean;
13 | }
14 |
15 | const StatCard = ({
16 | title,
17 | value,
18 | icon: Icon,
19 | subtitle,
20 | onClick,
21 | isLoading = false,
22 | disabled = false
23 | }: StatCardProps) => {
24 | return (
25 |
33 |
34 | {title}
35 |
36 |
37 |
38 |
39 | {isLoading ? (
40 |
41 |
42 | Loading...
43 |
44 | ) : (
45 | value
46 | )}
47 |
48 |
49 | {isLoading ? (
50 |
51 | ) : (
52 |
53 | )}
54 |
55 |
56 |
57 |
58 | {isLoading ? "Loading data..." : subtitle}
59 |
60 |
61 |
62 |
63 | );
64 | };
65 |
66 | export default StatCard;
67 |
--------------------------------------------------------------------------------
/src/components/deals/EditDealDialog.tsx:
--------------------------------------------------------------------------------
1 |
2 | import React from 'react';
3 | import { Dialog, DialogContent } from "@/components/ui/dialog";
4 | import { Deal, Stage } from './types';
5 | import { TeamMember } from '@/components/settings/TeamMembers';
6 | import DealForm, { DealFormField } from './DealForm';
7 |
8 | interface EditDealDialogProps {
9 | isOpen: boolean;
10 | onClose: () => void;
11 | deal: Deal | null;
12 | onSave: (updatedDeal: Deal) => void;
13 | stages: Stage[];
14 | teamMembers: TeamMember[];
15 | customFields?: DealFormField[];
16 | isLoading?: boolean;
17 | }
18 |
19 | const EditDealDialog: React.FC = ({
20 | isOpen,
21 | onClose,
22 | deal,
23 | onSave,
24 | stages,
25 | teamMembers,
26 | customFields = [],
27 | isLoading = false
28 | }) => {
29 | if (!deal) return null;
30 |
31 | const handleSave = (updatedDealData: Partial) => {
32 | const updatedDeal = {
33 | ...deal,
34 | ...updatedDealData,
35 | updatedAt: new Date().toISOString()
36 | };
37 |
38 | onSave(updatedDeal);
39 | };
40 |
41 | return (
42 |
55 | );
56 | };
57 |
58 | export default EditDealDialog;
59 |
--------------------------------------------------------------------------------
/src/components/deals/types.ts:
--------------------------------------------------------------------------------
1 |
2 | export interface Deal {
3 | id: string;
4 | name: string;
5 | company: string;
6 | value: number;
7 | currency: string;
8 | probability: number;
9 | stage: string;
10 | closingDate: string;
11 | description: string;
12 | assignedTo?: string;
13 | contactId?: string;
14 | expectedCloseDate?: string;
15 | createdAt: string;
16 | updatedAt: string;
17 | customFields?: Record;
18 | attachments?: Array<{
19 | name: string;
20 | size: number;
21 | type: string;
22 | lastModified: number;
23 | }>;
24 | appointments?: Array<{
25 | title: string;
26 | datetime: string;
27 | }>;
28 | }
29 |
30 | export interface Stage {
31 | id: string;
32 | label: string;
33 | }
34 |
35 | export interface Column {
36 | id: string;
37 | label: string;
38 | }
39 |
40 | export type DealStage = string;
41 |
42 | export const DEFAULT_COLUMNS: Column[] = [
43 | { id: 'discovery', label: 'Discovery' },
44 | { id: 'proposal', label: 'Proposal' },
45 | { id: 'negotiation', label: 'Negotiation' },
46 | { id: 'decision', label: 'Decision' },
47 | { id: 'contract_sent', label: 'Contract Sent' },
48 | { id: 'closed_won', label: 'Closed Won' },
49 | { id: 'closed_lost', label: 'Closed Lost' }
50 |
51 |
52 | ];
53 |
54 |
55 |
56 | export const STORAGE_KEYS = {
57 | DEALS_COLUMNS: 'crm_deals_columns',
58 | DEALS_DATA: 'crm_deals_data'
59 | };
60 |
--------------------------------------------------------------------------------
/src/components/deployment/DeploymentChecklist.tsx:
--------------------------------------------------------------------------------
1 |
2 | import React from 'react';
3 | import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
4 | import { Checkbox } from "@/components/ui/checkbox";
5 | import { Label } from "@/components/ui/label";
6 | import { IS_PRODUCTION } from '@/config/deploymentConfig';
7 |
8 | // Only show this component in development mode
9 | const DeploymentChecklist: React.FC = () => {
10 | if (IS_PRODUCTION) {
11 | return null;
12 | }
13 |
14 | return (
15 |
16 |
17 | Deployment Checklist
18 |
19 | Items to verify before deploying to production. This component only appears in development mode.
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | Ensure all API endpoints are pointing to production URLs in the deploymentConfig.ts file.
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | Ensure no test accounts or dummy data will be visible in production.
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | Test on various screen sizes to ensure proper responsiveness.
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 | Verify the application works in Chrome, Firefox, Safari, and Edge.
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 | Set analyticsEnabled to true in the production configuration.
70 |
71 |
72 |
73 |
74 |
75 |
76 | );
77 | };
78 |
79 | export default DeploymentChecklist;
80 |
--------------------------------------------------------------------------------
/src/components/master-account/ClientDirectory.tsx:
--------------------------------------------------------------------------------
1 |
2 | import React from 'react';
3 | import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
4 | import { Button } from "@/components/ui/button";
5 | import { Trash2, Mail, Phone } from "lucide-react";
6 | import { useMasterAccount } from "@/contexts/MasterAccountContext";
7 | import { Badge } from "@/components/ui/badge";
8 |
9 | const ClientDirectory = () => {
10 | const { clients, removeClient } = useMasterAccount();
11 |
12 | const deleteClient = (id: number) => {
13 | removeClient(id);
14 | };
15 |
16 | if (clients.length === 0) {
17 | return (
18 |
19 |
No clients have been added yet
20 |
23 |
24 | );
25 | }
26 |
27 | return (
28 |
29 |
30 |
31 | Client Name
32 | Contact
33 | Company
34 | Lead Type
35 | Tags
36 | Actions
37 |
38 |
39 |
40 | {clients.map((client) => (
41 |
42 | {client.firstName} {client.lastName}
43 |
44 |
45 | {client.emails[0] && (
46 |
47 |
48 | {client.emails[0]}
49 |
50 | )}
51 | {client.phoneNumbers[0] && (
52 |
53 |
54 | {client.phoneNumbers[0]}
55 |
56 | )}
57 |
58 |
59 | {client.company}
60 | {client.leadType}
61 |
62 |
63 | {client.tags && client.tags.map(tag => (
64 | {tag}
65 | ))}
66 |
67 |
68 |
69 |
77 |
78 |
79 | ))}
80 |
81 |
82 | );
83 | };
84 |
85 | export default ClientDirectory;
86 |
--------------------------------------------------------------------------------
/src/components/master-account/ClientPerformanceTable.tsx:
--------------------------------------------------------------------------------
1 |
2 | import React from 'react';
3 | import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
4 | import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
5 | import { Button } from "@/components/ui/button";
6 | import { Building2 } from "lucide-react";
7 | import { useMasterAccount } from "@/contexts/MasterAccountContext";
8 |
9 | interface SalesData {
10 | name: string;
11 | sales: number;
12 | leads: number;
13 | conversions: number;
14 | }
15 |
16 | interface ClientPerformanceTableProps {
17 | clientSalesData: SalesData[];
18 | }
19 |
20 | const ClientPerformanceTable = ({ clientSalesData }: ClientPerformanceTableProps) => {
21 | const { clients } = useMasterAccount();
22 |
23 | if (clients.length === 0) {
24 | return (
25 |
26 |
27 | Detailed Client Performance
28 | Sales and conversion metrics for all clients
29 |
30 |
31 |
32 |
No clients have been added yet
33 |
37 |
38 |
39 |
40 | );
41 | }
42 |
43 | return (
44 |
45 |
46 | Detailed Client Performance
47 | Sales and conversion metrics for all clients
48 |
49 |
50 |
51 |
52 |
53 | Client Name
54 | Sales
55 | Leads
56 | Conversions
57 | Rate
58 | Status
59 |
60 |
61 |
62 | {clientSalesData.map((data, index) => {
63 | const client = clients[index];
64 | if (!client) return null;
65 |
66 | return (
67 |
68 | {`${client.firstName} ${client.lastName}`}
69 | ${data.sales.toLocaleString()}
70 | {data.leads}
71 | {data.conversions}
72 | {Math.round((data.conversions / data.leads) * 100)}%
73 |
74 |
81 | {client.leadType}
82 |
83 |
84 |
85 | );
86 | })}
87 |
88 |
89 |
90 |
91 | );
92 | };
93 |
94 | export default ClientPerformanceTable;
95 |
--------------------------------------------------------------------------------
/src/components/master-account/ClientSalesChart.tsx:
--------------------------------------------------------------------------------
1 |
2 | import React from 'react';
3 | import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
4 | import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
5 |
6 | interface SalesData {
7 | name: string;
8 | sales: number;
9 | leads: number;
10 | conversions: number;
11 | }
12 |
13 | interface ClientSalesChartProps {
14 | clientSalesData: SalesData[];
15 | }
16 |
17 | const ClientSalesChart = ({ clientSalesData }: ClientSalesChartProps) => {
18 | return (
19 |
20 |
21 | Client Sales Overview
22 | Monthly sales performance by client
23 |
24 |
25 |
26 |
27 |
36 |
37 |
38 |
39 | ['$' + value.toLocaleString(), 'Sales']} />
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | );
48 | };
49 |
50 | export default ClientSalesChart;
51 |
--------------------------------------------------------------------------------
/src/components/master-account/SalesSummaryCards.tsx:
--------------------------------------------------------------------------------
1 |
2 | import React from 'react';
3 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
4 |
5 | interface SalesSummaryCardsProps {
6 | totalSales: number;
7 | averageSales: number;
8 | totalLeads: number;
9 | totalConversions: number;
10 | }
11 |
12 | const SalesSummaryCards = ({ totalSales, averageSales, totalLeads, totalConversions }: SalesSummaryCardsProps) => {
13 | return (
14 |
15 |
16 |
17 | Total Client Sales
18 |
19 |
20 | ${totalSales.toLocaleString()}
21 | Across all clients
22 |
23 |
24 |
25 |
26 | Average Sales per Client
27 |
28 |
29 | ${Math.round(averageSales).toLocaleString()}
30 | Monthly average
31 |
32 |
33 |
34 |
35 | Total Leads / Conversions
36 |
37 |
38 | {totalLeads} / {totalConversions}
39 |
40 | {Math.round((totalConversions / totalLeads) * 100)}% conversion rate
41 |
42 |
43 |
44 |
45 | );
46 | };
47 |
48 | export default SalesSummaryCards;
49 |
--------------------------------------------------------------------------------
/src/components/theme/ThemeProvider.tsx:
--------------------------------------------------------------------------------
1 |
2 | import React, { createContext, useContext, useEffect, useState } from "react";
3 |
4 | type Theme = "light" | "dark" | "system";
5 |
6 | type ThemeProviderProps = {
7 | children: React.ReactNode;
8 | defaultTheme?: Theme;
9 | storageKey?: string;
10 | };
11 |
12 | type ThemeProviderState = {
13 | theme: Theme;
14 | setTheme: (theme: Theme) => void;
15 | };
16 |
17 | const initialState: ThemeProviderState = {
18 | theme: "system",
19 | setTheme: () => null,
20 | };
21 |
22 | const ThemeProviderContext = createContext(initialState);
23 |
24 | export function ThemeProvider({
25 | children,
26 | defaultTheme = "system",
27 | storageKey = "vite-ui-theme",
28 | ...props
29 | }: ThemeProviderProps) {
30 | const [theme, setTheme] = useState(
31 | () => (localStorage.getItem(storageKey) as Theme) || defaultTheme
32 | );
33 |
34 | useEffect(() => {
35 | const root = window.document.documentElement;
36 | root.classList.remove("light", "dark");
37 |
38 | if (theme === "system") {
39 | const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
40 | .matches
41 | ? "dark"
42 | : "light";
43 |
44 | root.classList.add(systemTheme);
45 | return;
46 | }
47 |
48 | root.classList.add(theme);
49 | }, [theme]);
50 |
51 | const value = {
52 | theme,
53 | setTheme: (theme: Theme) => {
54 | localStorage.setItem(storageKey, theme);
55 | setTheme(theme);
56 | },
57 | };
58 |
59 | return (
60 |
61 | {children}
62 |
63 | );
64 | }
65 |
66 | export const useTheme = () => {
67 | const context = useContext(ThemeProviderContext);
68 |
69 | if (context === undefined)
70 | throw new Error("useTheme must be used within a ThemeProvider");
71 |
72 | return context;
73 | };
74 |
--------------------------------------------------------------------------------
/src/components/theme/ThemeToggle.tsx:
--------------------------------------------------------------------------------
1 |
2 | import { MoonStar, Sun } from "lucide-react";
3 | import { useTheme } from "./ThemeProvider";
4 | import { Button } from "@/components/ui/button";
5 | import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
6 |
7 | interface ThemeToggleProps {
8 | variant?: "button" | "dropdown";
9 | }
10 |
11 | export function ThemeToggle({ variant = "button" }: ThemeToggleProps) {
12 | const { theme, setTheme } = useTheme();
13 |
14 | const toggleTheme = () => {
15 | setTheme(theme === "dark" ? "light" : "dark");
16 | };
17 |
18 | if (variant === "dropdown") {
19 | return (
20 |
24 | {theme === "dark" ? (
25 | <>
26 |
27 | Light Mode
28 | >
29 | ) : (
30 | <>
31 |
32 | Dark Mode
33 | >
34 | )}
35 |
36 | );
37 | }
38 |
39 | return (
40 |
47 | );
48 | }
49 |
--------------------------------------------------------------------------------
/src/components/ui/accordion.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as AccordionPrimitive from "@radix-ui/react-accordion"
3 | import { ChevronDown } from "lucide-react"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const Accordion = AccordionPrimitive.Root
8 |
9 | const AccordionItem = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => (
13 |
18 | ))
19 | AccordionItem.displayName = "AccordionItem"
20 |
21 | const AccordionTrigger = React.forwardRef<
22 | React.ElementRef,
23 | React.ComponentPropsWithoutRef
24 | >(({ className, children, ...props }, ref) => (
25 |
26 | svg]:rotate-180",
30 | className
31 | )}
32 | {...props}
33 | >
34 | {children}
35 |
36 |
37 |
38 | ))
39 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
40 |
41 | const AccordionContent = React.forwardRef<
42 | React.ElementRef,
43 | React.ComponentPropsWithoutRef
44 | >(({ className, children, ...props }, ref) => (
45 |
50 | {children}
51 |
52 | ))
53 |
54 | AccordionContent.displayName = AccordionPrimitive.Content.displayName
55 |
56 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
57 |
--------------------------------------------------------------------------------
/src/components/ui/alert.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cva, type VariantProps } from "class-variance-authority"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const alertVariants = cva(
7 | "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
8 | {
9 | variants: {
10 | variant: {
11 | default: "bg-background text-foreground",
12 | destructive:
13 | "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
14 | },
15 | },
16 | defaultVariants: {
17 | variant: "default",
18 | },
19 | }
20 | )
21 |
22 | const Alert = React.forwardRef<
23 | HTMLDivElement,
24 | React.HTMLAttributes & VariantProps
25 | >(({ className, variant, ...props }, ref) => (
26 |
32 | ))
33 | Alert.displayName = "Alert"
34 |
35 | const AlertTitle = React.forwardRef<
36 | HTMLParagraphElement,
37 | React.HTMLAttributes
38 | >(({ className, ...props }, ref) => (
39 |
44 | ))
45 | AlertTitle.displayName = "AlertTitle"
46 |
47 | const AlertDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ))
57 | AlertDescription.displayName = "AlertDescription"
58 |
59 | export { Alert, AlertTitle, AlertDescription }
60 |
--------------------------------------------------------------------------------
/src/components/ui/aspect-ratio.tsx:
--------------------------------------------------------------------------------
1 | import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
2 |
3 | const AspectRatio = AspectRatioPrimitive.Root
4 |
5 | export { AspectRatio }
6 |
--------------------------------------------------------------------------------
/src/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as AvatarPrimitive from "@radix-ui/react-avatar"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const Avatar = React.forwardRef<
7 | React.ElementRef,
8 | React.ComponentPropsWithoutRef
9 | >(({ className, ...props }, ref) => (
10 |
18 | ))
19 | Avatar.displayName = AvatarPrimitive.Root.displayName
20 |
21 | const AvatarImage = React.forwardRef<
22 | React.ElementRef,
23 | React.ComponentPropsWithoutRef
24 | >(({ className, ...props }, ref) => (
25 |
30 | ))
31 | AvatarImage.displayName = AvatarPrimitive.Image.displayName
32 |
33 | const AvatarFallback = React.forwardRef<
34 | React.ElementRef,
35 | React.ComponentPropsWithoutRef
36 | >(({ className, ...props }, ref) => (
37 |
45 | ))
46 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
47 |
48 | export { Avatar, AvatarImage, AvatarFallback }
49 |
--------------------------------------------------------------------------------
/src/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cva, type VariantProps } from "class-variance-authority"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const badgeVariants = cva(
7 | "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
8 | {
9 | variants: {
10 | variant: {
11 | default:
12 | "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
13 | secondary:
14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
15 | destructive:
16 | "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
17 | outline: "text-foreground",
18 | },
19 | },
20 | defaultVariants: {
21 | variant: "default",
22 | },
23 | }
24 | )
25 |
26 | export interface BadgeProps
27 | extends React.HTMLAttributes,
28 | VariantProps {}
29 |
30 | function Badge({ className, variant, ...props }: BadgeProps) {
31 | return (
32 |
33 | )
34 | }
35 |
36 | export { Badge, badgeVariants }
37 |
--------------------------------------------------------------------------------
/src/components/ui/breadcrumb.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { ChevronRight, MoreHorizontal } from "lucide-react"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const Breadcrumb = React.forwardRef<
8 | HTMLElement,
9 | React.ComponentPropsWithoutRef<"nav"> & {
10 | separator?: React.ReactNode
11 | }
12 | >(({ ...props }, ref) => )
13 | Breadcrumb.displayName = "Breadcrumb"
14 |
15 | const BreadcrumbList = React.forwardRef<
16 | HTMLOListElement,
17 | React.ComponentPropsWithoutRef<"ol">
18 | >(({ className, ...props }, ref) => (
19 |
27 | ))
28 | BreadcrumbList.displayName = "BreadcrumbList"
29 |
30 | const BreadcrumbItem = React.forwardRef<
31 | HTMLLIElement,
32 | React.ComponentPropsWithoutRef<"li">
33 | >(({ className, ...props }, ref) => (
34 |
39 | ))
40 | BreadcrumbItem.displayName = "BreadcrumbItem"
41 |
42 | const BreadcrumbLink = React.forwardRef<
43 | HTMLAnchorElement,
44 | React.ComponentPropsWithoutRef<"a"> & {
45 | asChild?: boolean
46 | }
47 | >(({ asChild, className, ...props }, ref) => {
48 | const Comp = asChild ? Slot : "a"
49 |
50 | return (
51 |
56 | )
57 | })
58 | BreadcrumbLink.displayName = "BreadcrumbLink"
59 |
60 | const BreadcrumbPage = React.forwardRef<
61 | HTMLSpanElement,
62 | React.ComponentPropsWithoutRef<"span">
63 | >(({ className, ...props }, ref) => (
64 |
72 | ))
73 | BreadcrumbPage.displayName = "BreadcrumbPage"
74 |
75 | const BreadcrumbSeparator = ({
76 | children,
77 | className,
78 | ...props
79 | }: React.ComponentProps<"li">) => (
80 | svg]:size-3.5", className)}
84 | {...props}
85 | >
86 | {children ?? }
87 |
88 | )
89 | BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
90 |
91 | const BreadcrumbEllipsis = ({
92 | className,
93 | ...props
94 | }: React.ComponentProps<"span">) => (
95 |
101 |
102 | More
103 |
104 | )
105 | BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
106 |
107 | export {
108 | Breadcrumb,
109 | BreadcrumbList,
110 | BreadcrumbItem,
111 | BreadcrumbLink,
112 | BreadcrumbPage,
113 | BreadcrumbSeparator,
114 | BreadcrumbEllipsis,
115 | }
116 |
--------------------------------------------------------------------------------
/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-primary text-primary-foreground hover:bg-primary/90",
13 | destructive:
14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90",
15 | outline:
16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17 | secondary:
18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19 | ghost: "hover:bg-accent hover:text-accent-foreground",
20 | link: "text-primary underline-offset-4 hover:underline",
21 | },
22 | size: {
23 | default: "h-10 px-4 py-2",
24 | sm: "h-9 rounded-md px-3",
25 | lg: "h-11 rounded-md px-8",
26 | icon: "h-10 w-10",
27 | },
28 | },
29 | defaultVariants: {
30 | variant: "default",
31 | size: "default",
32 | },
33 | }
34 | )
35 |
36 | export interface ButtonProps
37 | extends React.ButtonHTMLAttributes,
38 | VariantProps {
39 | asChild?: boolean
40 | }
41 |
42 | const Button = React.forwardRef(
43 | ({ className, variant, size, asChild = false, ...props }, ref) => {
44 | const Comp = asChild ? Slot : "button"
45 | return (
46 |
51 | )
52 | }
53 | )
54 | Button.displayName = "Button"
55 |
56 | export { Button, buttonVariants }
57 |
--------------------------------------------------------------------------------
/src/components/ui/calendar.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { ChevronLeft, ChevronRight } from "lucide-react";
3 | import { DayPicker } from "react-day-picker";
4 |
5 | import { cn } from "@/lib/utils";
6 | import { buttonVariants } from "@/components/ui/button";
7 |
8 | export type CalendarProps = React.ComponentProps;
9 |
10 | function Calendar({
11 | className,
12 | classNames,
13 | showOutsideDays = true,
14 | ...props
15 | }: CalendarProps) {
16 | return (
17 | ,
56 | IconRight: ({ ..._props }) => ,
57 | }}
58 | {...props}
59 | />
60 | );
61 | }
62 | Calendar.displayName = "Calendar";
63 |
64 | export { Calendar };
65 |
--------------------------------------------------------------------------------
/src/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 |
2 | import * as React from "react"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const Card = React.forwardRef<
7 | HTMLDivElement,
8 | React.HTMLAttributes
9 | >(({ className, ...props }, ref) => (
10 |
18 | ))
19 | Card.displayName = "Card"
20 |
21 | const CardHeader = React.forwardRef<
22 | HTMLDivElement,
23 | React.HTMLAttributes
24 | >(({ className, ...props }, ref) => (
25 |
30 | ))
31 | CardHeader.displayName = "CardHeader"
32 |
33 | const CardTitle = React.forwardRef<
34 | HTMLParagraphElement,
35 | React.HTMLAttributes
36 | >(({ className, ...props }, ref) => (
37 |
45 | ))
46 | CardTitle.displayName = "CardTitle"
47 |
48 | const CardDescription = React.forwardRef<
49 | HTMLParagraphElement,
50 | React.HTMLAttributes
51 | >(({ className, ...props }, ref) => (
52 |
57 | ))
58 | CardDescription.displayName = "CardDescription"
59 |
60 | const CardContent = React.forwardRef<
61 | HTMLDivElement,
62 | React.HTMLAttributes
63 | >(({ className, ...props }, ref) => (
64 |
65 | ))
66 | CardContent.displayName = "CardContent"
67 |
68 | const CardFooter = React.forwardRef<
69 | HTMLDivElement,
70 | React.HTMLAttributes
71 | >(({ className, ...props }, ref) => (
72 |
77 | ))
78 | CardFooter.displayName = "CardFooter"
79 |
80 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
81 |
--------------------------------------------------------------------------------
/src/components/ui/checkbox.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
3 | import { Check } from "lucide-react"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const Checkbox = React.forwardRef<
8 | React.ElementRef,
9 | React.ComponentPropsWithoutRef
10 | >(({ className, ...props }, ref) => (
11 |
19 |
22 |
23 |
24 |
25 | ))
26 | Checkbox.displayName = CheckboxPrimitive.Root.displayName
27 |
28 | export { Checkbox }
29 |
--------------------------------------------------------------------------------
/src/components/ui/collapsible.tsx:
--------------------------------------------------------------------------------
1 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
2 |
3 | const Collapsible = CollapsiblePrimitive.Root
4 |
5 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
6 |
7 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
8 |
9 | export { Collapsible, CollapsibleTrigger, CollapsibleContent }
10 |
--------------------------------------------------------------------------------
/src/components/ui/drawer.tsx:
--------------------------------------------------------------------------------
1 |
2 | import * as React from "react"
3 | import { Drawer as DrawerPrimitive } from "vaul"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const Drawer = ({
8 | shouldScaleBackground = true,
9 | ...props
10 | }: React.ComponentProps) => (
11 |
15 | )
16 | Drawer.displayName = "Drawer"
17 |
18 | const DrawerTrigger = DrawerPrimitive.Trigger
19 |
20 | const DrawerPortal = DrawerPrimitive.Portal
21 |
22 | const DrawerClose = DrawerPrimitive.Close
23 |
24 | const DrawerOverlay = React.forwardRef<
25 | React.ElementRef,
26 | React.ComponentPropsWithoutRef
27 | >(({ className, ...props }, ref) => (
28 |
33 | ))
34 | DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
35 |
36 | const DrawerContent = React.forwardRef<
37 | React.ElementRef,
38 | React.ComponentPropsWithoutRef
39 | >(({ className, children, ...props }, ref) => (
40 |
41 |
42 |
50 |
51 | {children}
52 |
53 | Close
54 |
55 |
56 |
57 | ))
58 | DrawerContent.displayName = "DrawerContent"
59 |
60 | const DrawerHeader = ({
61 | className,
62 | ...props
63 | }: React.HTMLAttributes) => (
64 |
68 | )
69 | DrawerHeader.displayName = "DrawerHeader"
70 |
71 | const DrawerFooter = ({
72 | className,
73 | ...props
74 | }: React.HTMLAttributes) => (
75 |
79 | )
80 | DrawerFooter.displayName = "DrawerFooter"
81 |
82 | const DrawerTitle = React.forwardRef<
83 | React.ElementRef,
84 | React.ComponentPropsWithoutRef
85 | >(({ className, ...props }, ref) => (
86 |
94 | ))
95 | DrawerTitle.displayName = DrawerPrimitive.Title.displayName
96 |
97 | const DrawerDescription = React.forwardRef<
98 | React.ElementRef,
99 | React.ComponentPropsWithoutRef
100 | >(({ className, ...props }, ref) => (
101 |
106 | ))
107 | DrawerDescription.displayName = DrawerPrimitive.Description.displayName
108 |
109 | export {
110 | Drawer,
111 | DrawerPortal,
112 | DrawerOverlay,
113 | DrawerTrigger,
114 | DrawerClose,
115 | DrawerContent,
116 | DrawerHeader,
117 | DrawerFooter,
118 | DrawerTitle,
119 | DrawerDescription,
120 | }
121 |
--------------------------------------------------------------------------------
/src/components/ui/error-fallback.tsx:
--------------------------------------------------------------------------------
1 |
2 | import React from 'react';
3 | import { Button } from "@/components/ui/button";
4 | import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
5 | import { AlertTriangle, RefreshCw, Home } from "lucide-react";
6 | import { FallbackProps } from 'react-error-boundary';
7 |
8 | export const ErrorFallback = ({ error, resetErrorBoundary }: FallbackProps) => {
9 | return (
10 |
11 |
12 |
13 |
16 | Something went wrong
17 |
18 | We're sorry, but we encountered an unexpected error.
19 |
20 |
21 |
22 |
23 |
24 | {error.message || 'Unknown error'}
25 |
26 |
27 |
28 |
29 |
37 |
44 |
45 |
46 |
47 | );
48 | };
49 |
50 | export default ErrorFallback;
51 |
--------------------------------------------------------------------------------
/src/components/ui/hover-card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const HoverCard = HoverCardPrimitive.Root
7 |
8 | const HoverCardTrigger = HoverCardPrimitive.Trigger
9 |
10 | const HoverCardContent = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
14 |
24 | ))
25 | HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
26 |
27 | export { HoverCard, HoverCardTrigger, HoverCardContent }
28 |
--------------------------------------------------------------------------------
/src/components/ui/input-otp.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { OTPInput, OTPInputContext } from "input-otp"
3 | import { Dot } from "lucide-react"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const InputOTP = React.forwardRef<
8 | React.ElementRef,
9 | React.ComponentPropsWithoutRef
10 | >(({ className, containerClassName, ...props }, ref) => (
11 |
20 | ))
21 | InputOTP.displayName = "InputOTP"
22 |
23 | const InputOTPGroup = React.forwardRef<
24 | React.ElementRef<"div">,
25 | React.ComponentPropsWithoutRef<"div">
26 | >(({ className, ...props }, ref) => (
27 |
28 | ))
29 | InputOTPGroup.displayName = "InputOTPGroup"
30 |
31 | const InputOTPSlot = React.forwardRef<
32 | React.ElementRef<"div">,
33 | React.ComponentPropsWithoutRef<"div"> & { index: number }
34 | >(({ index, className, ...props }, ref) => {
35 | const inputOTPContext = React.useContext(OTPInputContext)
36 | const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index]
37 |
38 | return (
39 |
48 | {char}
49 | {hasFakeCaret && (
50 |
53 | )}
54 |
55 | )
56 | })
57 | InputOTPSlot.displayName = "InputOTPSlot"
58 |
59 | const InputOTPSeparator = React.forwardRef<
60 | React.ElementRef<"div">,
61 | React.ComponentPropsWithoutRef<"div">
62 | >(({ ...props }, ref) => (
63 |
64 |
65 |
66 | ))
67 | InputOTPSeparator.displayName = "InputOTPSeparator"
68 |
69 | export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }
70 |
--------------------------------------------------------------------------------
/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Input = React.forwardRef>(
6 | ({ className, type, ...props }, ref) => {
7 | return (
8 |
17 | )
18 | }
19 | )
20 | Input.displayName = "Input"
21 |
22 | export { Input }
23 |
--------------------------------------------------------------------------------
/src/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 |
2 | import * as React from "react"
3 | import * as LabelPrimitive from "@radix-ui/react-label"
4 | import { cva, type VariantProps } from "class-variance-authority"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const labelVariants = cva(
9 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
10 | )
11 |
12 | const Label = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef &
15 | VariantProps
16 | >(({ className, ...props }, ref) => (
17 |
22 | ))
23 | Label.displayName = LabelPrimitive.Root.displayName
24 |
25 | export { Label }
26 |
--------------------------------------------------------------------------------
/src/components/ui/pagination.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react"
3 |
4 | import { cn } from "@/lib/utils"
5 | import { ButtonProps, buttonVariants } from "@/components/ui/button"
6 |
7 | const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
8 |
14 | )
15 | Pagination.displayName = "Pagination"
16 |
17 | const PaginationContent = React.forwardRef<
18 | HTMLUListElement,
19 | React.ComponentProps<"ul">
20 | >(({ className, ...props }, ref) => (
21 |
26 | ))
27 | PaginationContent.displayName = "PaginationContent"
28 |
29 | const PaginationItem = React.forwardRef<
30 | HTMLLIElement,
31 | React.ComponentProps<"li">
32 | >(({ className, ...props }, ref) => (
33 |
34 | ))
35 | PaginationItem.displayName = "PaginationItem"
36 |
37 | type PaginationLinkProps = {
38 | isActive?: boolean
39 | } & Pick &
40 | React.ComponentProps<"a">
41 |
42 | const PaginationLink = ({
43 | className,
44 | isActive,
45 | size = "icon",
46 | ...props
47 | }: PaginationLinkProps) => (
48 |
59 | )
60 | PaginationLink.displayName = "PaginationLink"
61 |
62 | const PaginationPrevious = ({
63 | className,
64 | ...props
65 | }: React.ComponentProps) => (
66 |
72 |
73 | Previous
74 |
75 | )
76 | PaginationPrevious.displayName = "PaginationPrevious"
77 |
78 | const PaginationNext = ({
79 | className,
80 | ...props
81 | }: React.ComponentProps) => (
82 |
88 | Next
89 |
90 |
91 | )
92 | PaginationNext.displayName = "PaginationNext"
93 |
94 | const PaginationEllipsis = ({
95 | className,
96 | ...props
97 | }: React.ComponentProps<"span">) => (
98 |
103 |
104 | More pages
105 |
106 | )
107 | PaginationEllipsis.displayName = "PaginationEllipsis"
108 |
109 | export {
110 | Pagination,
111 | PaginationContent,
112 | PaginationEllipsis,
113 | PaginationItem,
114 | PaginationLink,
115 | PaginationNext,
116 | PaginationPrevious,
117 | }
118 |
--------------------------------------------------------------------------------
/src/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as PopoverPrimitive from "@radix-ui/react-popover"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const Popover = PopoverPrimitive.Root
7 |
8 | const PopoverTrigger = PopoverPrimitive.Trigger
9 |
10 | const PopoverContent = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
14 |
15 |
25 |
26 | ))
27 | PopoverContent.displayName = PopoverPrimitive.Content.displayName
28 |
29 | export { Popover, PopoverTrigger, PopoverContent }
30 |
--------------------------------------------------------------------------------
/src/components/ui/progress.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as ProgressPrimitive from "@radix-ui/react-progress"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const Progress = React.forwardRef<
7 | React.ElementRef,
8 | React.ComponentPropsWithoutRef
9 | >(({ className, value, ...props }, ref) => (
10 |
18 |
22 |
23 | ))
24 | Progress.displayName = ProgressPrimitive.Root.displayName
25 |
26 | export { Progress }
27 |
--------------------------------------------------------------------------------
/src/components/ui/radio-group.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
3 | import { Circle } from "lucide-react"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const RadioGroup = React.forwardRef<
8 | React.ElementRef,
9 | React.ComponentPropsWithoutRef
10 | >(({ className, ...props }, ref) => {
11 | return (
12 |
17 | )
18 | })
19 | RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
20 |
21 | const RadioGroupItem = React.forwardRef<
22 | React.ElementRef,
23 | React.ComponentPropsWithoutRef
24 | >(({ className, ...props }, ref) => {
25 | return (
26 |
34 |
35 |
36 |
37 |
38 | )
39 | })
40 | RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
41 |
42 | export { RadioGroup, RadioGroupItem }
43 |
--------------------------------------------------------------------------------
/src/components/ui/resizable.tsx:
--------------------------------------------------------------------------------
1 | import { GripVertical } from "lucide-react"
2 | import * as ResizablePrimitive from "react-resizable-panels"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const ResizablePanelGroup = ({
7 | className,
8 | ...props
9 | }: React.ComponentProps) => (
10 |
17 | )
18 |
19 | const ResizablePanel = ResizablePrimitive.Panel
20 |
21 | const ResizableHandle = ({
22 | withHandle,
23 | className,
24 | ...props
25 | }: React.ComponentProps & {
26 | withHandle?: boolean
27 | }) => (
28 | div]:rotate-90",
31 | className
32 | )}
33 | {...props}
34 | >
35 | {withHandle && (
36 |
37 |
38 |
39 | )}
40 |
41 | )
42 |
43 | export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
44 |
--------------------------------------------------------------------------------
/src/components/ui/scroll-area.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const ScrollArea = React.forwardRef<
7 | React.ElementRef,
8 | React.ComponentPropsWithoutRef
9 | >(({ className, children, ...props }, ref) => (
10 |
15 |
16 | {children}
17 |
18 |
19 |
20 |
21 | ))
22 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
23 |
24 | const ScrollBar = React.forwardRef<
25 | React.ElementRef,
26 | React.ComponentPropsWithoutRef
27 | >(({ className, orientation = "vertical", ...props }, ref) => (
28 |
41 |
42 |
43 | ))
44 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
45 |
46 | export { ScrollArea, ScrollBar }
47 |
--------------------------------------------------------------------------------
/src/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as SeparatorPrimitive from "@radix-ui/react-separator"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const Separator = React.forwardRef<
7 | React.ElementRef,
8 | React.ComponentPropsWithoutRef
9 | >(
10 | (
11 | { className, orientation = "horizontal", decorative = true, ...props },
12 | ref
13 | ) => (
14 |
25 | )
26 | )
27 | Separator.displayName = SeparatorPrimitive.Root.displayName
28 |
29 | export { Separator }
30 |
--------------------------------------------------------------------------------
/src/components/ui/skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils"
2 |
3 | function Skeleton({
4 | className,
5 | ...props
6 | }: React.HTMLAttributes) {
7 | return (
8 |
12 | )
13 | }
14 |
15 | export { Skeleton }
16 |
--------------------------------------------------------------------------------
/src/components/ui/slider.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as SliderPrimitive from "@radix-ui/react-slider"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const Slider = React.forwardRef<
7 | React.ElementRef,
8 | React.ComponentPropsWithoutRef
9 | >(({ className, ...props }, ref) => (
10 |
18 |
19 |
20 |
21 |
22 |
23 | ))
24 | Slider.displayName = SliderPrimitive.Root.displayName
25 |
26 | export { Slider }
27 |
--------------------------------------------------------------------------------
/src/components/ui/sonner.tsx:
--------------------------------------------------------------------------------
1 |
2 | import { useTheme } from "next-themes"
3 | import { Toaster as Sonner } from "sonner"
4 |
5 | type ToasterProps = React.ComponentProps
6 |
7 | const Toaster = ({ ...props }: ToasterProps) => {
8 | const { theme = "system" } = useTheme()
9 |
10 | return (
11 |
31 | )
32 | }
33 |
34 | export { Toaster }
35 |
--------------------------------------------------------------------------------
/src/components/ui/switch.tsx:
--------------------------------------------------------------------------------
1 |
2 | import * as React from "react"
3 | import * as SwitchPrimitives from "@radix-ui/react-switch"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const Switch = React.forwardRef<
8 | React.ElementRef,
9 | React.ComponentPropsWithoutRef
10 | >(({ className, ...props }, ref) => (
11 |
19 |
24 |
25 | ))
26 | Switch.displayName = SwitchPrimitives.Root.displayName
27 |
28 | export { Switch }
29 |
--------------------------------------------------------------------------------
/src/components/ui/table.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Table = React.forwardRef<
6 | HTMLTableElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
16 | ))
17 | Table.displayName = "Table"
18 |
19 | const TableHeader = React.forwardRef<
20 | HTMLTableSectionElement,
21 | React.HTMLAttributes
22 | >(({ className, ...props }, ref) => (
23 |
24 | ))
25 | TableHeader.displayName = "TableHeader"
26 |
27 | const TableBody = React.forwardRef<
28 | HTMLTableSectionElement,
29 | React.HTMLAttributes
30 | >(({ className, ...props }, ref) => (
31 |
36 | ))
37 | TableBody.displayName = "TableBody"
38 |
39 | const TableFooter = React.forwardRef<
40 | HTMLTableSectionElement,
41 | React.HTMLAttributes
42 | >(({ className, ...props }, ref) => (
43 | tr]:last:border-b-0",
47 | className
48 | )}
49 | {...props}
50 | />
51 | ))
52 | TableFooter.displayName = "TableFooter"
53 |
54 | const TableRow = React.forwardRef<
55 | HTMLTableRowElement,
56 | React.HTMLAttributes
57 | >(({ className, ...props }, ref) => (
58 |
66 | ))
67 | TableRow.displayName = "TableRow"
68 |
69 | const TableHead = React.forwardRef<
70 | HTMLTableCellElement,
71 | React.ThHTMLAttributes
72 | >(({ className, ...props }, ref) => (
73 | |
81 | ))
82 | TableHead.displayName = "TableHead"
83 |
84 | const TableCell = React.forwardRef<
85 | HTMLTableCellElement,
86 | React.TdHTMLAttributes
87 | >(({ className, ...props }, ref) => (
88 | |
93 | ))
94 | TableCell.displayName = "TableCell"
95 |
96 | const TableCaption = React.forwardRef<
97 | HTMLTableCaptionElement,
98 | React.HTMLAttributes
99 | >(({ className, ...props }, ref) => (
100 |
105 | ))
106 | TableCaption.displayName = "TableCaption"
107 |
108 | export {
109 | Table,
110 | TableHeader,
111 | TableBody,
112 | TableFooter,
113 | TableHead,
114 | TableRow,
115 | TableCell,
116 | TableCaption,
117 | }
118 |
--------------------------------------------------------------------------------
/src/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as TabsPrimitive from "@radix-ui/react-tabs"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const Tabs = TabsPrimitive.Root
7 |
8 | const TabsList = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 | ))
21 | TabsList.displayName = TabsPrimitive.List.displayName
22 |
23 | const TabsTrigger = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => (
27 |
35 | ))
36 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
37 |
38 | const TabsContent = React.forwardRef<
39 | React.ElementRef,
40 | React.ComponentPropsWithoutRef
41 | >(({ className, ...props }, ref) => (
42 |
50 | ))
51 | TabsContent.displayName = TabsPrimitive.Content.displayName
52 |
53 | export { Tabs, TabsList, TabsTrigger, TabsContent }
54 |
--------------------------------------------------------------------------------
/src/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 |
2 | import * as React from "react"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | export interface TextareaProps
7 | extends React.TextareaHTMLAttributes {}
8 |
9 | const Textarea = React.forwardRef(
10 | ({ className, ...props }, ref) => {
11 | return (
12 |
20 | )
21 | }
22 | )
23 | Textarea.displayName = "Textarea"
24 |
25 | export { Textarea }
26 |
--------------------------------------------------------------------------------
/src/components/ui/toaster.tsx:
--------------------------------------------------------------------------------
1 |
2 | import { useToast } from "@/hooks/use-toast"
3 | import {
4 | Toast,
5 | ToastClose,
6 | ToastDescription,
7 | ToastProvider,
8 | ToastTitle,
9 | ToastViewport,
10 | } from "@/components/ui/toast"
11 |
12 | export function Toaster() {
13 | const { toasts } = useToast()
14 |
15 | return (
16 |
17 | {toasts.map(function ({ id, title, description, action, ...props }) {
18 | return (
19 |
20 |
21 | {title && {title}}
22 | {description && (
23 | {description}
24 | )}
25 |
26 | {action}
27 |
28 |
29 | )
30 | })}
31 |
32 |
33 | )
34 | }
35 |
--------------------------------------------------------------------------------
/src/components/ui/toggle-group.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
3 | import { type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 | import { toggleVariants } from "@/components/ui/toggle"
7 |
8 | const ToggleGroupContext = React.createContext<
9 | VariantProps
10 | >({
11 | size: "default",
12 | variant: "default",
13 | })
14 |
15 | const ToggleGroup = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef &
18 | VariantProps
19 | >(({ className, variant, size, children, ...props }, ref) => (
20 |
25 |
26 | {children}
27 |
28 |
29 | ))
30 |
31 | ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName
32 |
33 | const ToggleGroupItem = React.forwardRef<
34 | React.ElementRef,
35 | React.ComponentPropsWithoutRef &
36 | VariantProps
37 | >(({ className, children, variant, size, ...props }, ref) => {
38 | const context = React.useContext(ToggleGroupContext)
39 |
40 | return (
41 |
52 | {children}
53 |
54 | )
55 | })
56 |
57 | ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName
58 |
59 | export { ToggleGroup, ToggleGroupItem }
60 |
--------------------------------------------------------------------------------
/src/components/ui/toggle.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as TogglePrimitive from "@radix-ui/react-toggle"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const toggleVariants = cva(
8 | "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-transparent",
13 | outline:
14 | "border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
15 | },
16 | size: {
17 | default: "h-10 px-3",
18 | sm: "h-9 px-2.5",
19 | lg: "h-11 px-5",
20 | },
21 | },
22 | defaultVariants: {
23 | variant: "default",
24 | size: "default",
25 | },
26 | }
27 | )
28 |
29 | const Toggle = React.forwardRef<
30 | React.ElementRef,
31 | React.ComponentPropsWithoutRef &
32 | VariantProps
33 | >(({ className, variant, size, ...props }, ref) => (
34 |
39 | ))
40 |
41 | Toggle.displayName = TogglePrimitive.Root.displayName
42 |
43 | export { Toggle, toggleVariants }
44 |
--------------------------------------------------------------------------------
/src/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 |
2 | import * as React from "react"
3 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const TooltipProvider = TooltipPrimitive.Provider
8 |
9 | const Tooltip = TooltipPrimitive.Root
10 |
11 | const TooltipTrigger = TooltipPrimitive.Trigger
12 |
13 | const TooltipContent = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef
16 | >(({ className, sideOffset = 4, ...props }, ref) => (
17 |
26 | ))
27 | TooltipContent.displayName = TooltipPrimitive.Content.displayName
28 |
29 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
30 |
--------------------------------------------------------------------------------
/src/components/ui/use-toast.ts:
--------------------------------------------------------------------------------
1 |
2 | // Import from the hook path instead of referencing itself
3 | import { useToast, toast } from "@/hooks/use-toast";
4 |
5 | export { useToast, toast };
6 |
--------------------------------------------------------------------------------
/src/components/website/DevicesTab.tsx:
--------------------------------------------------------------------------------
1 |
2 | import React from 'react';
3 | import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
4 | import { Monitor, Smartphone, Tablet, PieChart } from 'lucide-react';
5 |
6 | interface DevicesTabProps {
7 | totalViews: number;
8 | }
9 |
10 | const DevicesTab = ({ totalViews }: DevicesTabProps) => {
11 | return (
12 |
13 |
14 | Device Breakdown
15 | How users access your website across different devices
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | Desktop
24 |
25 |
26 |
27 | 58%
28 |
29 | {Math.round(totalViews * 0.58).toLocaleString()} views
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | Mobile
38 |
39 |
40 |
41 | 36%
42 |
43 | {Math.round(totalViews * 0.36).toLocaleString()} views
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | Tablet
52 |
53 |
54 |
55 | 6%
56 |
57 | {Math.round(totalViews * 0.06).toLocaleString()} views
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
Device distribution visualization will appear here
67 |
68 |
69 |
70 |
71 | );
72 | };
73 |
74 | export default DevicesTab;
75 |
--------------------------------------------------------------------------------
/src/components/website/InsightsTab.tsx:
--------------------------------------------------------------------------------
1 |
2 | import React from 'react';
3 | import { WebsitePage } from '@/types/website';
4 | import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
5 | import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
6 | import { Button } from '@/components/ui/button';
7 | import { BarChart } from 'lucide-react';
8 |
9 | interface InsightsTabProps {
10 | websitePages: WebsitePage[];
11 | }
12 |
13 | const InsightsTab = ({ websitePages }: InsightsTabProps) => {
14 | return (
15 |
16 |
17 | Page Insights
18 | Performance metrics for your top pages
19 |
20 |
21 |
22 |
23 |
24 |
Detailed analytics visualization will appear here
25 |
28 |
29 |
30 |
31 |
32 |
Top Performing Pages
33 |
34 |
35 |
36 | Page
37 | Views
38 | Conversions
39 | Bounce Rate
40 | Avg. Time on Page
41 |
42 |
43 |
44 | {websitePages
45 | .sort((a, b) => (b.views || b.visits || 0) - (a.views || a.visits || 0))
46 | .slice(0, 5)
47 | .map((page) => (
48 |
49 | {page.title}
50 | {(page.views || page.visits || 0).toLocaleString()}
51 | {(page.conversions || 0).toLocaleString()}
52 | {(page.bounceRate || 0).toFixed(1)}%
53 | 2m 34s
54 |
55 | ))}
56 |
57 |
58 |
59 |
60 |
61 | );
62 | };
63 |
64 | export default InsightsTab;
65 |
--------------------------------------------------------------------------------
/src/components/website/RealTimeTab.tsx:
--------------------------------------------------------------------------------
1 |
2 | import React from 'react';
3 | import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
4 | import RealTimeAnalytics from './RealTimeAnalytics';
5 |
6 | interface RealTimeTabProps {
7 | selectedPage: string | null;
8 | }
9 |
10 | const RealTimeTab = ({ selectedPage }: RealTimeTabProps) => {
11 | return (
12 |
13 |
14 | Real-Time Analytics
15 |
16 | {selectedPage
17 | ? 'Monitoring activity for the selected page'
18 | : 'Select a page to track or view overall site metrics'}
19 |
20 |
21 |
22 |
23 |
24 |
25 | );
26 | };
27 |
28 | export default RealTimeTab;
29 |
--------------------------------------------------------------------------------
/src/components/website/RealTimeTabAdapter.tsx:
--------------------------------------------------------------------------------
1 |
2 | import React from 'react';
3 | import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
4 | import RealTimeAnalytics from './RealTimeAnalytics';
5 |
6 | interface RealTimeTabAdapterProps {
7 | selectedPage: string | null;
8 | }
9 |
10 | const RealTimeTabAdapter = ({ selectedPage }: RealTimeTabAdapterProps) => {
11 | return (
12 |
13 |
14 | Real-Time Analytics
15 |
16 | {selectedPage
17 | ? 'Monitoring activity for the selected page'
18 | : 'Select a page to track or view overall site metrics'}
19 |
20 |
21 |
22 |
23 |
24 |
25 | );
26 | };
27 |
28 | export default RealTimeTabAdapter;
29 |
--------------------------------------------------------------------------------
/src/config.ts:
--------------------------------------------------------------------------------
1 | // Safe access to environment variables for Vite
2 | const getEnv = (key: string, defaultValue: string): string => {
3 | // For Vite, we use import.meta.env instead of process.env
4 | return (import.meta.env?.[key] as string) || defaultValue;
5 | };
6 |
7 | export const config = {
8 | apiUrl: getEnv('VITE_API_URL', 'http://localhost:3000/api'),
9 | //hubspot: {
10 | // portalId: getEnv('VITE_HUBSPOT_PORTAL_ID', ''),
11 | // apiKey: getEnv('VITE_HUBSPOT_API_KEY', ''),
12 | // clientId: getEnv('VITE_HUBSPOT_CLIENT_ID', ''),
13 | // clientSecret: getEnv('VITE_HUBSPOT_CLIENT_SECRET', ''),
14 | // redirectUri: getEnv('VITE_HUBSPOT_REDIRECT_URI', 'http://localhost:3000/auth/callback'),
15 | // scopes: [
16 | // 'crm.objects.contact.read',
17 | // 'crm.objects.contact.write',
18 | // 'crm.objects.deals.read',
19 | // 'crm.objects.deals.write'
20 | // ]
21 | //},
22 | //n8n: {
23 | // webhookUrl: getEnv('VITE_N8N_WEBHOOK_URL', ''),
24 | //},
25 | //backend: {
26 | // baseUrl: getEnv('VITE_API_BASE_URL', 'http://localhost:3000'),
27 | // hubspotAccessToken: getEnv('VITE_HUBSPOT_ACCESS_TOKEN', ''),
28 | // databaseUrl: getEnv('VITE_DATABASE_URL', 'postgresql://postgres:postgres@localhost:5432/contact_pipeline'),
29 | // databaseSsl: getEnv('VITE_DATABASE_SSL', 'false') === 'true',
30 | //},
31 | }
--------------------------------------------------------------------------------
/src/config/deploymentConfig.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file contains configuration settings for different environments.
3 | * You can set different values for development and production.
4 | */
5 |
6 | export interface DeploymentConfig {
7 | // Add other configuration properties as needed
8 | }
9 |
10 | // Determine the current environment
11 | const isProduction = import.meta.env.PROD;
12 |
13 | // Get environment variables with fallbacks
14 | const getEnvVar = (key: string, fallback: string = ''): string => {
15 | const value = import.meta.env[key];
16 | return value !== undefined ? String(value) : fallback;
17 | };
18 |
19 | // Development configuration (local development)
20 | const developmentConfig: DeploymentConfig = {
21 | apiUrl: 'http://localhost:8000',
22 | appName: 'CRM System (Development)',
23 | isProduction: false,
24 | analyticsEnabled: false,
25 | };
26 |
27 | // Production configuration
28 | const productionConfig: DeploymentConfig = {
29 | apiUrl: 'https://api.yourproductionapi.com', // Change this to your actual production API URL
30 | appName: 'CRM System',
31 | isProduction: true,
32 | analyticsEnabled: true,
33 | };
34 |
35 | // Export the appropriate config based on the environment
36 | export const config: DeploymentConfig = isProduction ? productionConfig : developmentConfig;
37 |
38 | // Export useful environment checks
39 | export const IS_PRODUCTION = config.isProduction;
40 | export const ANALYTICS_ENABLED = config.analyticsEnabled;
41 |
--------------------------------------------------------------------------------
/src/constants/storageKeys.ts:
--------------------------------------------------------------------------------
1 |
2 | export const STORAGE_KEYS = {
3 | CLIENTS: 'master_account_clients',
4 | CURRENT_CLIENT: 'master_account_current_client',
5 | MASTER_MODE: 'master_account_is_master_mode',
6 | WEBHOOKS: 'master_account_webhooks',
7 | WEBSITE_PAGES: 'master_account_website_pages',
8 | CONTENT_ITEMS: 'master_account_content_items',
9 | NOTIFICATIONS: 'master_account_notifications'
10 | };
11 |
--------------------------------------------------------------------------------
/src/contexts/AuthContext.tsx:
--------------------------------------------------------------------------------
1 | import React, {createContext, useContext, useEffect, useState, useCallback } from 'react';
2 | import { AuthState, authService } from '@/services/auth';
3 | import { useNavigate } from 'react-router-dom';
4 | import { STORAGE_KEYS } from '@/constants/storageKeys';
5 |
6 |
7 | interface ActivityItem {
8 | id: number;
9 | action: string;
10 | time: string;
11 | name: string;
12 | }
13 | interface AuthContextType {
14 | authState: AuthState;
15 | login: (email: string, password: string) => Promise;
16 | logout: () => Promise;
17 | refreshToken: () => Promise;
18 | logoutAndRedirect: () => Promise;
19 | addActivity: (activity: ActivityItem) => void;
20 | clearActivities: () => void;
21 | }
22 |
23 | const AuthContext = createContext(undefined);
24 |
25 | export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
26 | const [auth, setAuthState] = useState(authService.getAuthState());
27 | const navigate = useNavigate();
28 |
29 | useEffect(() => {
30 | const unsubscribe = authService.subscribe((newState: AuthState) => {
31 | setAuthState(newState);
32 | });
33 |
34 | return () => unsubscribe(); // cleanup on unmount
35 | }, []);
36 |
37 | // Flag to prevent multiple navigation attempts
38 | const [isNavigating, setIsNavigating] = useState(false);
39 |
40 | // Custom function that logs out and redirects to login page
41 | const logoutAndRedirect = useCallback(async () => {
42 | // Prevent multiple navigation attempts
43 |
44 | if (isNavigating) {
45 | // console.log("AuthContext: Navigation already in progress");
46 | return;
47 | }
48 |
49 | try {
50 | setIsNavigating(true);
51 | // console.log("AuthContext: Starting logout process");
52 |
53 |
54 | // First attempt to logout via the auth service
55 | await authService.logout();
56 |
57 | // console.log("AuthContext: Logout successful, redirecting to login");
58 | navigate('/login', { replace: true });
59 | } catch (error) {
60 | console.error('AuthContext: Logout error:', error);
61 |
62 | // Still try to navigate to login even if logout fails
63 | navigate('/login', { replace: true });
64 | } finally {
65 | // Reset the navigating flag after a delay
66 | setTimeout(() => {
67 | setIsNavigating(false);
68 | }, 500);
69 | }
70 | }, [navigate, isNavigating]);
71 |
72 | const addActivity = (activity: ActivityItem) => {
73 | setAuthState(prevState => ({
74 | ...prevState,
75 | activities: [...prevState.activities, activity],
76 | }));
77 | };
78 |
79 | // Function to clear all activities
80 | const clearActivities = () => {
81 | setAuthState(prevState => ({
82 | ...prevState,
83 | activities: [],
84 | }));
85 | };
86 |
87 | const value: AuthContextType = {
88 | authState: auth,
89 | login: authService.login.bind(authService),
90 | logout: authService.logout.bind(authService),
91 | refreshToken: authService.refreshToken.bind(authService),
92 | logoutAndRedirect, // Bind the new function,
93 | addActivity,
94 | clearActivities,
95 | };
96 |
97 | return (
98 |
99 | {children}
100 |
101 | );
102 | };
103 |
104 | export const useAuth = () => {
105 | const context = useContext(AuthContext);
106 | if (context === undefined) {
107 | throw new Error('useAuth must be used within an AuthProvider');
108 | }
109 | return context;
110 | }
--------------------------------------------------------------------------------
/src/contexts/CustomFieldsContext.tsx:
--------------------------------------------------------------------------------
1 | import React, { createContext, useContext, ReactNode, useState, useEffect } from 'react';
2 | import { DealFormField } from '@/components/deals/DealForm';
3 | import { initialDealFields } from '@/data/initialData';
4 |
5 | interface CustomFieldsContextType {
6 | dealFields: DealFormField[];
7 | updateDealFields: (fields: DealFormField[]) => void;
8 | getFieldsForAccount: (accountId: string) => DealFormField[];
9 | setFieldsForAccount: (accountId: string, fields: DealFormField[]) => void;
10 | }
11 |
12 | const CustomFieldsContext = createContext(undefined);
13 |
14 | const STORAGE_KEY = 'crm_custom_fields';
15 |
16 | export const CustomFieldsProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
17 | const [dealFields, setDealFields] = useState(initialDealFields);
18 | const [accountFields, setAccountFields] = useState>({});
19 |
20 | // Load custom fields from localStorage on mount
21 | useEffect(() => {
22 | try {
23 | const savedFields = localStorage.getItem(STORAGE_KEY);
24 | if (savedFields) {
25 | const parsed = JSON.parse(savedFields);
26 | setDealFields(parsed.dealFields || []);
27 | setAccountFields(parsed.accountFields || {});
28 | }
29 | } catch (error) {
30 | console.error("Error loading custom fields:", error);
31 | }
32 | }, []);
33 |
34 | // Save to localStorage whenever fields change
35 | useEffect(() => {
36 | localStorage.setItem(STORAGE_KEY, JSON.stringify({
37 | dealFields,
38 | accountFields
39 | }));
40 | }, [dealFields, accountFields]);
41 |
42 | const updateDealFields = (fields: DealFormField[]) => {
43 | setDealFields(fields);
44 | };
45 |
46 | const getFieldsForAccount = (accountId: string): DealFormField[] => {
47 | return accountFields[accountId] || dealFields;
48 | };
49 |
50 | const setFieldsForAccount = (accountId: string, fields: DealFormField[]) => {
51 | setAccountFields(prev => ({
52 | ...prev,
53 | [accountId]: fields
54 | }));
55 | };
56 |
57 | return (
58 |
66 | {children}
67 |
68 | );
69 | };
70 |
71 | export const useCustomFields = (): CustomFieldsContextType => {
72 | const context = useContext(CustomFieldsContext);
73 | if (context === undefined) {
74 | throw new Error('useCustomFields must be used within a CustomFieldsProvider');
75 | }
76 | return context;
77 | };
78 |
--------------------------------------------------------------------------------
/src/contexts/MasterAccountContext.tsx:
--------------------------------------------------------------------------------
1 | import React, { createContext, ReactNode, useContext } from 'react';
2 | import { useClients } from '@/hooks/useClients';
3 | import { useWebhooks } from '@/hooks/useWebhooks';
4 | import { useWebsitePages } from '@/hooks/useWebsitePages';
5 | import { useNotifications } from '@/hooks/useNotifications';
6 | import { useContentItems } from '@/hooks/useContentItems';
7 | import { MasterAccountContextType } from '@/types/masterAccount';
8 | import { initialClients } from '@/data/initialData';
9 |
10 | export const MasterAccountContext = createContext(undefined);
11 |
12 | export const MasterAccountProvider = ({ children }: { children: ReactNode }) => {
13 | const {
14 | clients,
15 | currentClientId,
16 | isInMasterMode,
17 | isLoadingClients,
18 | clientsLoaded,
19 | addClient,
20 | removeClient,
21 | switchToClient,
22 | toggleMasterMode,
23 | loginToAccount,
24 | clearAllClients,
25 | fetchClientsData,
26 | refreshClientsData
27 | } = useClients();
28 |
29 | const {
30 | webhooks,
31 | addWebhook,
32 | removeWebhook,
33 | updateWebhook,
34 | triggerWebhook
35 | } = useWebhooks();
36 |
37 | const {
38 | notifications,
39 | addNotification,
40 | markNotificationAsRead,
41 | getNotifications,
42 | getUnreadNotificationsCount
43 | } = useNotifications(isInMasterMode, currentClientId);
44 |
45 | const {
46 | websitePages,
47 | addWebsitePage,
48 | removeWebsitePage,
49 | updateWebsitePage
50 | } = useWebsitePages(isInMasterMode, currentClientId);
51 |
52 | const {
53 | contentItems,
54 | addContentItem,
55 | updateContentItem,
56 | deleteContentItem,
57 | updateContentStatus,
58 | getContentItems
59 | } = useContentItems(isInMasterMode, currentClientId, clients, addNotification);
60 |
61 | // Clean up websitePages and contentItems when removing a client
62 | const handleRemoveClient = (id: string) => {
63 | removeClient(id);
64 |
65 | // These operations are now handled in the main context instead of inside the hooks
66 | // to avoid circular dependencies
67 | if (currentClientId === id) {
68 | switchToClient(null);
69 | }
70 | };
71 |
72 | return (
73 |
110 | {children}
111 |
112 | );
113 | };
114 |
115 | export const useMasterAccount = () => {
116 | const context = useContext(MasterAccountContext);
117 | // console.log("useMasterAccount context:", context);
118 | if (context === undefined) {
119 | throw new Error('useMasterAccount must be used within a MasterAccountProvider');
120 | }
121 | return context;
122 | };
123 |
--------------------------------------------------------------------------------
/src/data/initialData.ts:
--------------------------------------------------------------------------------
1 | import { Deal } from '@/components/deals/types';
2 | import { DealFormField } from '@/components/deals/DealForm';
3 |
4 | export const initialClients = [
5 | {
6 | id: 1,
7 | firstName: "John",
8 | lastName: "Doe",
9 | emails: ["john@example.com"],
10 | phoneNumbers: ["+1234567890"],
11 | company: "Acme Inc",
12 | leadType: "customer",
13 | leadSource: "website",
14 | tags: ["enterprise", "software"],
15 | status: "active",
16 | users: 5,
17 | deals: 2,
18 | contacts: 3,
19 | lastActivity: new Date().toISOString(),
20 | logo: "/logos/acme.png"
21 | }
22 | ];
23 |
24 | export const initialDeals: Deal[] = [
25 | {
26 | id: "1",
27 | name: "Enterprise Software Deal",
28 | company: "TechCorp",
29 | value: 50000,
30 | currency: "USD",
31 | probability: 75,
32 | stage: "negotiation",
33 | closingDate: "2024-06-30",
34 | description: "Enterprise software implementation project",
35 | assignedTo: "1",
36 | createdAt: new Date().toISOString(),
37 | updatedAt: new Date().toISOString()
38 | },
39 | {
40 | id: "2",
41 | name: "Marketing Campaign",
42 | company: "BrandCo",
43 | value: 25000,
44 | currency: "USD",
45 | probability: 50,
46 | stage: "proposal",
47 | closingDate: "2024-07-15",
48 | description: "Q3 Marketing campaign planning",
49 | assignedTo: "2",
50 | createdAt: new Date().toISOString(),
51 | updatedAt: new Date().toISOString()
52 | }
53 | ];
54 |
55 | export const initialDealFields: DealFormField[] = [
56 | {
57 | id: "budget",
58 | label: "Budget",
59 | type: "number",
60 | required: false
61 | },
62 | {
63 | id: "timeline",
64 | label: "Timeline",
65 | type: "text",
66 | required: false
67 | }
68 | ];
--------------------------------------------------------------------------------
/src/hooks/use-mobile.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | const MOBILE_BREAKPOINT = 768
4 |
5 | export function useIsMobile() {
6 | const [isMobile, setIsMobile] = React.useState(undefined)
7 |
8 | React.useEffect(() => {
9 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
10 | const onChange = () => {
11 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
12 | }
13 | mql.addEventListener("change", onChange)
14 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
15 | return () => mql.removeEventListener("change", onChange)
16 | }, [])
17 |
18 | return !!isMobile
19 | }
20 |
--------------------------------------------------------------------------------
/src/hooks/useContacts:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 |
3 | interface Contact {
4 | id?: string;
5 | name: string;
6 | email: string;
7 | }
8 |
9 | export const useContacts = () => {
10 | const [contacts, setContacts] = useState([]);
11 | const [loading, setLoading] = useState(false);
12 | const [error, setError] = useState(null);
13 |
14 | const createContact = async (contact: Contact) => {
15 | setLoading(true);
16 | setError(null);
17 | try {
18 | const response = await fetch('/api/hubspot/contact', {
19 | method: 'POST',
20 | headers: { 'Content-Type': 'application/json' },
21 | body: JSON.stringify(contact),
22 | });
23 | if (!response.ok) throw new Error('Failed to create contact');
24 | const newContact = await response.json();
25 | setContacts((prev) => [...prev, newContact]);
26 | } catch (err) {
27 | setError(err.message);
28 | } finally {
29 | setLoading(false);
30 | }
31 | };
32 |
33 | const updateContact = async (contactId: string, contactData: Partial) => {
34 | setLoading(true);
35 | setError(null);
36 | try {
37 | const response = await fetch('/api/hubspot/contact', {
38 | method: 'PUT',
39 | headers: { 'Content-Type': 'application/json' },
40 | body: JSON.stringify({ contactId, contactData }),
41 | });
42 | if (!response.ok) throw new Error('Failed to update contact');
43 | const updatedContact = await response.json();
44 | setContacts((prev) =>
45 | prev.map((contact) => (contact.id === contactId ? updatedContact : contact))
46 | );
47 | } catch (err) {
48 | setError(err.message);
49 | } finally {
50 | setLoading(false);
51 | }
52 | };
53 |
54 | const deleteContact = async (contactId: string) => {
55 | setLoading(true);
56 | setError(null);
57 | try {
58 | const response = await fetch('/api/hubspot/contact', {
59 | method: 'DELETE',
60 | headers: { 'Content-Type': 'application/json' },
61 | body: JSON.stringify({ contactId }),
62 | });
63 | if (!response.ok) throw new Error('Failed to delete contact');
64 | setContacts((prev) => prev.filter((contact) => contact.id !== contactId));
65 | } catch (err) {
66 | setError(err.message);
67 | } finally {
68 | setLoading(false);
69 | }
70 | };
71 |
72 | return { contacts, loading, error, createContact, updateContact, deleteContact };
73 | };
74 |
--------------------------------------------------------------------------------
/src/hooks/useDealsStorage.ts:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { Deal } from '@/components/deals/types';
3 |
4 | export const useDealsStorage = () => {
5 | const [deals, setDeals] = useState([]);
6 |
7 | const clearDeals = () => {
8 | setDeals([]);
9 | };
10 |
11 | const addDeal = (deal: Deal) => {
12 | setDeals((currentDeals) => [...currentDeals, deal]);
13 | };
14 |
15 | const updateDeal = (updatedDeal: Deal) => {
16 | setDeals((currentDeals) =>
17 | currentDeals.map((deal) => deal.id === updatedDeal.id ? updatedDeal : deal)
18 | );
19 | };
20 |
21 | const deleteDeal = (id: string) => {
22 | setDeals((currentDeals) =>
23 | currentDeals.filter((deal) => deal.id !== id)
24 | );
25 | };
26 |
27 | const getDealById = (id: string) => {
28 | return deals.find((deal) => deal.id === id) || null;
29 | };
30 |
31 | return {
32 | deals,
33 | addDeal,
34 | updateDeal,
35 | deleteDeal,
36 | getDealById,
37 | clearDeals
38 | };
39 | };
40 |
--------------------------------------------------------------------------------
/src/hooks/useFormError.ts:
--------------------------------------------------------------------------------
1 |
2 | import { useState, useCallback } from 'react';
3 | import { toast } from "@/hooks/use-toast";
4 | import { logError } from "@/lib/errorHandling";
5 |
6 | interface UseFormErrorProps {
7 | formId?: string;
8 | logErrors?: boolean;
9 | }
10 |
11 | export function useFormError({ formId = "form", logErrors = true }: UseFormErrorProps = {}) {
12 | const [formError, setFormError] = useState(null);
13 |
14 | const clearError = useCallback(() => {
15 | setFormError(null);
16 | }, []);
17 |
18 | const handleError = useCallback((error: unknown, context = "Form submission failed") => {
19 | let errorMessage = "An unexpected error occurred";
20 |
21 | if (error instanceof Error) {
22 | errorMessage = error.message;
23 | } else if (typeof error === 'string') {
24 | errorMessage = error;
25 | }
26 |
27 | setFormError(errorMessage);
28 |
29 | if (logErrors) {
30 | logError(error, context);
31 | } else {
32 | // Just show toast without detailed logging
33 | toast({
34 | title: "Error",
35 | description: errorMessage,
36 | variant: "destructive",
37 | });
38 | }
39 |
40 | // Focus the first invalid input if available
41 | setTimeout(() => {
42 | const form = document.getElementById(formId);
43 | if (form) {
44 | const firstInvalid = form.querySelector(':invalid') as HTMLElement;
45 | if (firstInvalid) {
46 | firstInvalid.focus();
47 | }
48 | }
49 | }, 100);
50 |
51 | return errorMessage;
52 | }, [formId, logErrors]);
53 |
54 | return {
55 | formError,
56 | setFormError,
57 | clearError,
58 | handleError,
59 | };
60 | }
61 |
--------------------------------------------------------------------------------
/src/hooks/useMasterAccount.tsx:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 | import { MasterAccountContext } from '@/contexts/MasterAccountContext';
3 |
4 | export const useMasterAccount = () => {
5 | const context = useContext(MasterAccountContext);
6 | // console.log("useMasterAccount context:", context);
7 | if (context === undefined) {
8 | throw new Error('useMasterAccount must be used within a MasterAccountProvider');
9 | }
10 | return context;
11 | };
--------------------------------------------------------------------------------
/src/hooks/useNotifications.ts:
--------------------------------------------------------------------------------
1 |
2 | import { useState, useEffect } from 'react';
3 | import { Notification } from '@/types/masterAccount';
4 | import { STORAGE_KEYS } from '@/constants/storageKeys';
5 |
6 | export function useNotifications(isInMasterMode: boolean, currentClientId: string | null) {
7 | const [notifications, setNotifications] = useState(() => {
8 | const savedNotifications = localStorage.getItem(STORAGE_KEYS.NOTIFICATIONS);
9 | return savedNotifications ? JSON.parse(savedNotifications) : [];
10 | });
11 |
12 | useEffect(() => {
13 | localStorage.setItem(STORAGE_KEYS.NOTIFICATIONS, JSON.stringify(notifications));
14 | }, [notifications]);
15 |
16 | const addNotification = (notification: Omit) => {
17 | const newNotification: Notification = {
18 | ...notification,
19 | id: notifications.length > 0 ? Math.max(...notifications.map(n => n.id)) + 1 : 1,
20 | createdAt: new Date().toISOString(),
21 | read: false
22 | };
23 |
24 | setNotifications([newNotification, ...notifications]);
25 | };
26 |
27 | const markNotificationAsRead = (id: number) => {
28 | setNotifications(notifications.map(notification =>
29 | notification.id === id ? { ...notification, read: true } : notification
30 | ));
31 | };
32 |
33 | const getNotifications = (forClientId?: string | null) => {
34 | return notifications.filter(notification => {
35 | if (forClientId !== undefined) {
36 | if (notification.forClientId !== forClientId) return false;
37 | } else if (!isInMasterMode && currentClientId !== null) {
38 | if (notification.forClientId !== currentClientId && notification.forClientId !== null) return false;
39 | }
40 |
41 | return true;
42 | }).sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
43 | };
44 |
45 | const getUnreadNotificationsCount = (forClientId?: string | null) => {
46 | return getNotifications(forClientId).filter(notification => !notification.read).length;
47 | };
48 |
49 | return {
50 | notifications,
51 | addNotification,
52 | markNotificationAsRead,
53 | getNotifications,
54 | getUnreadNotificationsCount
55 | };
56 | }
57 |
--------------------------------------------------------------------------------
/src/hooks/useWebhooks.ts:
--------------------------------------------------------------------------------
1 |
2 | import { useState, useEffect } from 'react';
3 | import { Webhook } from '@/types/masterAccount';
4 | import { STORAGE_KEYS } from '@/constants/storageKeys';
5 | import { toast } from "@/hooks/use-toast";
6 |
7 | export function useWebhooks() {
8 | const [webhooks, setWebhooks] = useState(() => {
9 | const savedWebhooks = localStorage.getItem(STORAGE_KEYS.WEBHOOKS);
10 | return savedWebhooks ? JSON.parse(savedWebhooks) : [];
11 | });
12 |
13 | useEffect(() => {
14 | localStorage.setItem(STORAGE_KEYS.WEBHOOKS, JSON.stringify(webhooks));
15 | }, [webhooks]);
16 |
17 | const addWebhook = (webhook: Omit) => {
18 | const newWebhook = {
19 | ...webhook,
20 | id: webhooks.length > 0 ? Math.max(...webhooks.map(w => w.id)) + 1 : 1
21 | };
22 |
23 | setWebhooks([...webhooks, newWebhook]);
24 | toast({
25 | title: "Webhook Added",
26 | description: `${webhook.name} webhook has been created successfully.`
27 | });
28 | };
29 |
30 | const removeWebhook = (id: number) => {
31 | setWebhooks(webhooks.filter(webhook => webhook.id !== id));
32 | toast({
33 | title: "Webhook Removed",
34 | description: "The webhook has been removed successfully."
35 | });
36 | };
37 |
38 | const updateWebhook = (id: number, data: Partial) => {
39 | setWebhooks(webhooks.map(webhook =>
40 | webhook.id === id ? { ...webhook, ...data } : webhook
41 | ));
42 | toast({
43 | title: "Webhook Updated",
44 | description: "The webhook has been updated successfully."
45 | });
46 | };
47 |
48 | const triggerWebhook = async (webhookId: number, data: any) => {
49 | const webhook = webhooks.find(w => w.id === webhookId);
50 |
51 | if (!webhook) {
52 | toast({
53 | title: "Error",
54 | description: "Webhook not found",
55 | variant: "destructive"
56 | });
57 | return;
58 | }
59 |
60 | if (!webhook.active) {
61 | toast({
62 | title: "Error",
63 | description: "This webhook is currently inactive",
64 | variant: "destructive"
65 | });
66 | return;
67 | }
68 |
69 | try {
70 | await fetch(webhook.url, {
71 | method: "POST",
72 | headers: {
73 | "Content-Type": "application/json"
74 | },
75 | mode: "no-cors",
76 | body: JSON.stringify({
77 | timestamp: new Date().toISOString(),
78 | event: webhook.events[0],
79 | data: data
80 | })
81 | });
82 |
83 | updateWebhook(webhookId, { lastTriggered: new Date().toISOString() });
84 |
85 | toast({
86 | title: "Webhook Triggered",
87 | description: `${webhook.name} webhook was successfully triggered.`
88 | });
89 | } catch (error) {
90 | console.error("Error triggering webhook:", error);
91 | toast({
92 | title: "Error",
93 | description: "Failed to trigger webhook. Please check the URL and try again.",
94 | variant: "destructive"
95 | });
96 | }
97 | };
98 |
99 | return {
100 | webhooks,
101 | addWebhook,
102 | removeWebhook,
103 | updateWebhook,
104 | triggerWebhook
105 | };
106 | }
107 |
--------------------------------------------------------------------------------
/src/hooks/useWebsiteActions.ts:
--------------------------------------------------------------------------------
1 |
2 | import { useState } from 'react';
3 | import { useForm } from 'react-hook-form';
4 | import { useMasterAccount } from '@/contexts/MasterAccountContext';
5 | import { toast } from '@/hooks/use-toast';
6 | import { PageFormValues } from '@/types/website';
7 |
8 | export const useWebsiteActions = () => {
9 | const { websitePages, addWebsitePage, removeWebsitePage, updateWebsitePage, currentClientId } = useMasterAccount();
10 | const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
11 | const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
12 | const [editingPageId, setEditingPageId] = useState(null);
13 | const [selectedPage, setSelectedPage] = useState(null);
14 | const [isTracking, setIsTracking] = useState(false);
15 |
16 | const addForm = useForm({
17 | defaultValues: {
18 | title: '',
19 | slug: '',
20 | status: 'draft',
21 | type: 'landing'
22 | }
23 | });
24 |
25 | const editForm = useForm({
26 | defaultValues: {
27 | title: '',
28 | slug: '',
29 | status: 'published',
30 | type: 'landing'
31 | }
32 | });
33 |
34 | const handleAddPage = (data: PageFormValues) => {
35 | // Convert form data to the context's WebsitePage type format
36 | const newPage = {
37 | title: data.title,
38 | url: data.slug, // map slug to url
39 | status: data.status as 'published' | 'draft' | 'scheduled',
40 | type: data.type as 'landing' | 'blog' | 'product' | 'other',
41 | views: 0,
42 | conversions: 0,
43 | bounceRate: 0,
44 | clientId: currentClientId,
45 | createdAt: new Date().toISOString(),
46 | updatedAt: new Date().toISOString()
47 | };
48 |
49 | addWebsitePage(newPage);
50 | addForm.reset();
51 | setIsAddDialogOpen(false);
52 | };
53 |
54 | const openEditDialog = (pageId: string) => {
55 | const numericId = Number(pageId);
56 | const page = websitePages.find(p => p.id === numericId);
57 |
58 | if (page) {
59 | editForm.reset({
60 | title: page.title,
61 | slug: page.url || '', // map url to slug
62 | status: page.status,
63 | type: page.type,
64 | });
65 | setEditingPageId(numericId);
66 | setIsEditDialogOpen(true);
67 | }
68 | };
69 |
70 | const handleEditPage = (data: PageFormValues) => {
71 | if (editingPageId) {
72 | updateWebsitePage(editingPageId, {
73 | title: data.title,
74 | url: data.slug, // map slug to url
75 | status: data.status as 'published' | 'draft' | 'scheduled',
76 | type: data.type as 'landing' | 'blog' | 'product' | 'other',
77 | updatedAt: new Date().toISOString(),
78 | });
79 | setIsEditDialogOpen(false);
80 | setEditingPageId(null);
81 | }
82 | };
83 |
84 | const deletePage = (pageId: string) => {
85 | const numericId = Number(pageId);
86 | if (window.confirm('Are you sure you want to delete this page?')) {
87 | removeWebsitePage(numericId);
88 | }
89 | };
90 |
91 | const startTracking = (pageId: string) => {
92 | setSelectedPage(pageId);
93 |
94 | if (!isTracking) {
95 | setIsTracking(true);
96 | toast({
97 | title: "Real-time tracking activated",
98 | description: "Now monitoring website traffic and performance metrics",
99 | });
100 | }
101 | };
102 |
103 | return {
104 | websitePages,
105 | isAddDialogOpen,
106 | setIsAddDialogOpen,
107 | isEditDialogOpen,
108 | setIsEditDialogOpen,
109 | selectedPage,
110 | addForm,
111 | editForm,
112 | handleAddPage,
113 | openEditDialog,
114 | handleEditPage,
115 | deletePage,
116 | startTracking
117 | };
118 | };
119 |
--------------------------------------------------------------------------------
/src/hooks/useWebsitePages.ts:
--------------------------------------------------------------------------------
1 |
2 | import { useState, useEffect } from 'react';
3 | import { WebsitePage } from '@/types/masterAccount';
4 | import { STORAGE_KEYS } from '@/constants/storageKeys';
5 | import { toast } from "@/hooks/use-toast";
6 |
7 | export function useWebsitePages(isInMasterMode: boolean, currentClientId: string | null) {
8 | const [websitePages, setWebsitePages] = useState(() => {
9 | const savedPages = localStorage.getItem(STORAGE_KEYS.WEBSITE_PAGES);
10 | return savedPages ? JSON.parse(savedPages) : [];
11 | });
12 |
13 | useEffect(() => {
14 | localStorage.setItem(STORAGE_KEYS.WEBSITE_PAGES, JSON.stringify(websitePages));
15 | }, [websitePages]);
16 |
17 | const addWebsitePage = (page: Omit) => {
18 | const newPage = {
19 | ...page,
20 | id: websitePages.length > 0 ? Math.max(...websitePages.map(p => p.id)) + 1 : 1,
21 | clientId: isInMasterMode ? null : currentClientId,
22 | };
23 |
24 | setWebsitePages([...websitePages, newPage]);
25 | toast({
26 | title: "Page Added",
27 | description: `${page.title} has been created successfully.`
28 | });
29 | };
30 |
31 | const removeWebsitePage = (id: number) => {
32 | const pageToRemove = websitePages.find(page => page.id === id);
33 | if (!pageToRemove) return;
34 |
35 | if (!isInMasterMode && pageToRemove.clientId !== currentClientId) {
36 | toast({
37 | title: "Permission Denied",
38 | description: "You don't have permission to remove this page.",
39 | variant: "destructive"
40 | });
41 | return;
42 | }
43 |
44 | setWebsitePages(websitePages.filter(page => page.id !== id));
45 | toast({
46 | title: "Page Removed",
47 | description: "The page has been removed successfully."
48 | });
49 | };
50 |
51 | const updateWebsitePage = (id: number, data: Partial) => {
52 | const pageToUpdate = websitePages.find(page => page.id === id);
53 | if (!pageToUpdate) return;
54 |
55 | if (!isInMasterMode && pageToUpdate.clientId !== currentClientId) {
56 | toast({
57 | title: "Permission Denied",
58 | description: "You don't have permission to update this page.",
59 | variant: "destructive"
60 | });
61 | return;
62 | }
63 |
64 | setWebsitePages(websitePages.map(page =>
65 | page.id === id ? { ...page, ...data, updatedAt: new Date().toISOString() } : page
66 | ));
67 | toast({
68 | title: "Page Updated",
69 | description: "The page has been updated successfully."
70 | });
71 | };
72 |
73 | return {
74 | websitePages,
75 | addWebsitePage,
76 | removeWebsitePage,
77 | updateWebsitePage
78 | };
79 | }
80 |
--------------------------------------------------------------------------------
/src/lib/analytics.ts:
--------------------------------------------------------------------------------
1 |
2 | import { ANALYTICS_ENABLED } from '@/config/deploymentConfig';
3 |
4 | interface AnalyticsEvent {
5 | category: string;
6 | action: string;
7 | label?: string;
8 | value?: number;
9 | }
10 |
11 | /**
12 | * Track user events for analytics
13 | * This is a simple implementation that can be expanded with actual analytics services
14 | */
15 | export const trackEvent = (event: AnalyticsEvent): void => {
16 | if (!ANALYTICS_ENABLED) {
17 | // Only log in console during development
18 | console.log('Analytics event (disabled):', event);
19 | return;
20 | }
21 |
22 | try {
23 | // This is where you would integrate with an actual analytics service
24 | // like Google Analytics, Mixpanel, etc.
25 |
26 | // For now, we'll just log to console in production too
27 | console.log('Analytics event tracked:', event);
28 |
29 | // Example of how you might integrate with GA4
30 | /*
31 | if (typeof window !== 'undefined' && window.gtag) {
32 | window.gtag('event', event.action, {
33 | event_category: event.category,
34 | event_label: event.label,
35 | value: event.value
36 | });
37 | }
38 | */
39 | } catch (error) {
40 | console.error('Error tracking analytics event:', error);
41 | }
42 | };
43 |
44 | /**
45 | * Track page views
46 | */
47 | export const trackPageView = (pagePath: string, pageTitle: string): void => {
48 | trackEvent({
49 | category: 'Page View',
50 | action: 'view',
51 | label: `${pageTitle} (${pagePath})`
52 | });
53 | };
54 |
55 | /**
56 | * Track feature usage
57 | */
58 | export const trackFeatureUsage = (featureName: string): void => {
59 | trackEvent({
60 | category: 'Feature',
61 | action: 'use',
62 | label: featureName
63 | });
64 | };
65 |
66 | /**
67 | * Track email actions
68 | */
69 | export const trackEmailAction = (actionType: string, email: string): void => {
70 | trackEvent({
71 | category: 'Email',
72 | action: actionType,
73 | label: email
74 | });
75 | };
76 |
77 | /**
78 | * Track errors for monitoring
79 | */
80 | export const trackError = (errorMessage: string, errorSource: string): void => {
81 | trackEvent({
82 | category: 'Error',
83 | action: 'encounter',
84 | label: `${errorSource}: ${errorMessage}`
85 | });
86 | };
87 |
--------------------------------------------------------------------------------
/src/lib/api.ts:
--------------------------------------------------------------------------------
1 |
2 | import { handleAsyncError, logError } from "./errorHandling";
3 |
4 | interface FetchOptions extends RequestInit {
5 | errorMessage?: string;
6 | silent?: boolean;
7 | }
8 |
9 | export async function fetchWithErrorHandling(
10 | url: string,
11 | options: FetchOptions = {}
12 | ): Promise {
13 | const { errorMessage = "Failed to fetch data", silent = false, ...fetchOptions } = options;
14 |
15 | try {
16 | const response = await fetch(url, fetchOptions);
17 |
18 | if (!response.ok) {
19 | // Handle HTTP error responses
20 | const errorData = await response.json().catch(() => null);
21 | const message = errorData?.message || `${errorMessage} (${response.status})`;
22 | throw new Error(message);
23 | }
24 |
25 | return await response.json() as T;
26 | } catch (error) {
27 | if (!silent) {
28 | logError(error, errorMessage);
29 | }
30 | return null;
31 | }
32 | }
33 |
34 | // Helper for GET requests
35 | export const getJSON = (url: string, options: FetchOptions = {}): Promise => {
36 | return fetchWithErrorHandling(url, {
37 | method: 'GET',
38 | ...options,
39 | });
40 | };
41 |
42 | // Helper for POST requests
43 | export const postJSON = >(url: string, data: D, options: FetchOptions = {}): Promise => {
44 | return fetchWithErrorHandling(url, {
45 | method: 'POST',
46 | headers: {
47 | 'Content-Type': 'application/json',
48 | ...options.headers,
49 | },
50 | body: JSON.stringify(data),
51 | ...options,
52 | });
53 | };
54 |
55 | // Helper for PUT requests
56 | export const putJSON = >(url: string, data: D, options: FetchOptions = {}): Promise => {
57 | return fetchWithErrorHandling(url, {
58 | method: 'PUT',
59 | headers: {
60 | 'Content-Type': 'application/json',
61 | ...options.headers,
62 | },
63 | body: JSON.stringify(data),
64 | ...options,
65 | });
66 | };
67 |
68 | // Helper for DELETE requests
69 | export const deleteJSON = (url: string, options: FetchOptions = {}): Promise => {
70 | return fetchWithErrorHandling(url, {
71 | method: 'DELETE',
72 | ...options,
73 | });
74 | };
75 |
--------------------------------------------------------------------------------
/src/lib/data.ts:
--------------------------------------------------------------------------------
1 |
2 | // Types and utilities for the CRM application
3 |
4 | export type Contact = {
5 | id: string;
6 | firstName: string;
7 | lastName: string;
8 | email: string;
9 | phone: string;
10 | company: string;
11 | position: string;
12 | birthday: string | null;
13 | notes: string;
14 | lastContact: string;
15 | avatar: string | null;
16 | tags: string[];
17 | createdAt: string;
18 | updatedAt: string;
19 | };
20 |
21 | export type DealStage = string;
22 |
23 | export const DEFAULT_STAGES: DealStage[] = ['lead', 'contact', 'proposal', 'negotiation', 'closed-won', 'closed-lost'];
24 |
25 | export type Deal = {
26 | id: string;
27 | name: string;
28 | company: string;
29 | contactId: string;
30 | value: number;
31 | currency: string;
32 | stage: DealStage;
33 | probability: number;
34 | expectedCloseDate: string;
35 | notes: string;
36 | createdAt: string;
37 | updatedAt: string;
38 | };
39 |
40 | export type Opportunity = {
41 | id: string;
42 | name: string;
43 | description: string;
44 | potentialValue: number;
45 | currency: string;
46 | probability: number;
47 | expectedCloseDate: string;
48 | contactId: string | null;
49 | status: 'new' | 'qualified' | 'unqualified' | 'won' | 'lost';
50 | source: string;
51 | notes: string;
52 | createdAt: string;
53 | updatedAt: string;
54 | };
55 |
56 | export type Integration = {
57 | id: string;
58 | name: string;
59 | description: string;
60 | status: 'active' | 'inactive' | 'error';
61 | type: 'email' | 'calendar' | 'webhook' | 'api' | 'other';
62 | lastSync: string | null;
63 | apiKey: string;
64 | webhookUrl: string | null;
65 | createdAt: string;
66 | updatedAt: string;
67 | };
68 |
69 | // Empty arrays to replace sample data
70 | export const contacts: Contact[] = [];
71 | export const deals: Deal[] = [];
72 | export const opportunities: Opportunity[] = [];
73 | export const integrations: Integration[] = [];
74 |
75 | // Default stage labels map
76 | export const DEFAULT_STAGE_LABELS: Record = {
77 | 'lead': 'Lead',
78 | 'contact': 'Contact Made',
79 | 'proposal': 'Proposal Sent',
80 | 'negotiation': 'Negotiation',
81 | 'closed-won': 'Closed Won',
82 | 'closed-lost': 'Closed Lost'
83 | };
84 |
85 | // Helper functions for working with data
86 | export const getContactById = (id: string): Contact | undefined => {
87 | return contacts.find(contact => contact.id === id);
88 | };
89 |
90 | export const getStageLabel = (stage: DealStage, customLabels?: Record): string => {
91 | if (customLabels && customLabels[stage]) {
92 | return customLabels[stage];
93 | }
94 | return DEFAULT_STAGE_LABELS[stage] || stage;
95 | };
96 |
97 | export const formatCurrency = (value: number, currency: string): string => {
98 | return new Intl.NumberFormat('en-US', {
99 | style: 'currency',
100 | currency: currency
101 | }).format(value);
102 | };
103 |
104 | export const formatDate = (dateString: string): string => {
105 | const date = new Date(dateString);
106 | return new Intl.DateTimeFormat('en-US', {
107 | year: 'numeric',
108 | month: 'short',
109 | day: 'numeric'
110 | }).format(date);
111 | };
112 |
--------------------------------------------------------------------------------
/src/lib/errorHandling.ts:
--------------------------------------------------------------------------------
1 |
2 | import { toast } from "@/hooks/use-toast";
3 | import { trackError } from "./analytics";
4 |
5 | // Generic error handler for async operations
6 | export const handleAsyncError = (error: unknown, fallbackMessage = "An unexpected error occurred"): void => {
7 | console.error("Error caught:", error);
8 |
9 | let errorMessage = fallbackMessage;
10 |
11 | if (error instanceof Error) {
12 | errorMessage = error.message;
13 | } else if (typeof error === 'string') {
14 | errorMessage = error;
15 | }
16 |
17 | // Track error for analytics
18 | trackError(errorMessage, "Async Operation");
19 |
20 | toast({
21 | title: "Error",
22 | description: errorMessage,
23 | variant: "destructive",
24 | });
25 | };
26 |
27 | // Type for different error severities
28 | export type ErrorSeverity = "info" | "warning" | "error" | "critical";
29 |
30 | // More detailed error handler with severity levels
31 | export const logError = (
32 | error: unknown,
33 | context: string,
34 | severity: ErrorSeverity = "error",
35 | showToast = true
36 | ): void => {
37 | const timestamp = new Date().toISOString();
38 | const errorObj = error instanceof Error ? error : new Error(String(error));
39 |
40 | // Log to console with context
41 | console.group(`[${severity.toUpperCase()}] ${context} - ${timestamp}`);
42 | console.error(errorObj);
43 | console.trace();
44 | console.groupEnd();
45 |
46 | // Track in analytics
47 | if (severity === "error" || severity === "critical") {
48 | trackError(errorObj.message, context);
49 | }
50 |
51 | // Only show toast for user-facing errors
52 | if (showToast) {
53 | const toastVariant = severity === "info" ? "default" :
54 | severity === "warning" ? "warning" : "destructive";
55 |
56 | toast({
57 | title: context,
58 | description: errorObj.message,
59 | variant: toastVariant as any,
60 | });
61 | }
62 |
63 | // Here you could also implement additional error reporting logic:
64 | // - Send errors to a monitoring service like Sentry
65 | // - Log to a backend API
66 | // - Track in analytics
67 | };
68 |
69 | // Utility to wrap async functions with error handling
70 | export function withErrorHandling(
71 | fn: (...args: Args) => Promise,
72 | errorContext: string
73 | ) {
74 | return async (...args: Args): Promise => {
75 | try {
76 | return await fn(...args);
77 | } catch (error) {
78 | logError(error, errorContext);
79 | return undefined;
80 | }
81 | };
82 | }
83 |
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from "clsx";
2 | import { twMerge } from "tailwind-merge";
3 | import { format } from 'date-fns';
4 |
5 | export function cn(...inputs: ClassValue[]) {
6 | return twMerge(clsx(inputs))
7 | }
8 |
9 | export const formatDate = (date: string | Date) => {
10 | return format(new Date(date), 'MMM d, yyyy');
11 | };
12 |
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | import { createRoot } from 'react-dom/client'
2 | import App from './App.tsx'
3 | import './index.css'
4 |
5 |
6 | createRoot(document.getElementById("root")!).render();
7 | //// Initialize MSW in development mode
8 | //async function initializeMocks() {
9 | // if (import.meta.env.DEV) {
10 | // const { worker } = await import('./mocks/browser');
11 | // return worker.start({
12 | // onUnhandledRequest: 'bypass', // Don't warn about unhandled requests
13 | // });
14 | // }
15 | // return Promise.resolve();
16 | //}
17 |
18 | // Start the app after initializing mocks
19 | //initializeMocks().then(() => {
20 | // createRoot(document.getElementById("root")!).render();
21 | //});
22 |
--------------------------------------------------------------------------------
/src/pages/Booking.tsx:
--------------------------------------------------------------------------------
1 |
2 | import React from 'react';
3 | import { useParams, useNavigate } from 'react-router-dom';
4 | import BookingView from '@/components/calendar/BookingView';
5 | import { BookingType } from '@/components/calendar/EditBookingTypeDialog';
6 |
7 | // Sample booking types for demo
8 | const SAMPLE_BOOKING_TYPES: BookingType[] = [
9 | { id: 'sales-call', name: 'Sales Call', duration: 30 },
10 | { id: 'product-demo', name: 'Product Demo', duration: 60 },
11 | { id: 'discovery', name: 'Discovery Call', duration: 45 },
12 | { id: 'consultation', name: 'Consultation', duration: 60 },
13 | ];
14 |
15 | const Booking = () => {
16 | const { bookingTypeId } = useParams<{ bookingTypeId: string }>();
17 | const navigate = useNavigate();
18 |
19 | const selectedBookingType = SAMPLE_BOOKING_TYPES.find(type => type.id === bookingTypeId) || SAMPLE_BOOKING_TYPES[0];
20 |
21 | const handleBookingComplete = () => {
22 | // In a real app, this would update the calendar, send notifications, etc.
23 | // console.log("Booking completed:", selectedBookingType);
24 | };
25 |
26 | return (
27 |
35 | );
36 | };
37 |
38 | export default Booking;
39 |
--------------------------------------------------------------------------------
/src/pages/Content.tsx:
--------------------------------------------------------------------------------
1 |
2 | import React from 'react';
3 | import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
4 | import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
5 | import ContentScheduling from '@/components/content/ContentScheduling';
6 | import ContentList from '@/components/content/ContentList';
7 | import { FileEdit, Calendar, CheckCircle } from 'lucide-react';
8 |
9 | const Socials = () => {
10 | return (
11 |
12 |
Social Content
13 |
14 |
15 |
16 |
17 | Social Media Management
18 |
19 | Create, manage and schedule your social media content
20 |
21 |
22 |
23 |
24 |
25 | Content Calendar
26 |
27 |
28 | Approvals
29 |
30 |
31 | Content Library
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | Content Approvals
49 |
50 | Review and approve content before publishing
51 |
52 |
53 |
54 |
55 |
No pending approvals
56 |
57 |
58 |
59 |
60 |
61 |
62 | );
63 | };
64 |
65 | export default Socials;
66 |
--------------------------------------------------------------------------------
/src/pages/ContentScheduling.tsx:
--------------------------------------------------------------------------------
1 |
2 | import React from 'react';
3 | import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
4 | import ContentScheduling from '@/components/content/ContentScheduling';
5 | import { useMasterAccount } from '@/contexts/MasterAccountContext';
6 |
7 | const ContentSchedulingPage = () => {
8 | const { clients } = useMasterAccount();
9 |
10 | return (
11 |
12 |
13 |
14 | );
15 | };
16 |
17 | export default ContentSchedulingPage;
18 |
--------------------------------------------------------------------------------
/src/pages/Login.tsx:
--------------------------------------------------------------------------------
1 |
2 | import React, { useEffect } from 'react';
3 | import { LoginForm } from "@/components/auth/LoginForm";
4 | import { useLocation, useNavigate } from 'react-router-dom';
5 | import { useMasterAccount } from '@/contexts/MasterAccountContext';
6 |
7 | const Login = () => {
8 | const location = useLocation();
9 | const navigate = useNavigate();
10 | const { isInMasterMode, currentClientId } = useMasterAccount();
11 |
12 | // If user is already authenticated, redirect to home or the page they were trying to access
13 | useEffect(() => {
14 | const isAuthenticated = isInMasterMode || currentClientId !== null;
15 |
16 | if (isAuthenticated) {
17 | // Get the intended destination or default to home page
18 | const destination = location.state?.from?.pathname || '/';
19 | navigate(destination, { replace: true });
20 | }
21 | }, [isInMasterMode, currentClientId, navigate, location]);
22 |
23 | return (
24 |
25 |
26 |
27 | );
28 | };
29 |
30 | export default Login;
31 |
--------------------------------------------------------------------------------
/src/pages/NotFound.tsx:
--------------------------------------------------------------------------------
1 |
2 | import { useLocation } from "react-router-dom";
3 | import { useEffect, useState } from "react";
4 | import { Button } from "@/components/ui/button";
5 | import { AlertCircle, Home } from "lucide-react";
6 |
7 | const NotFound = () => {
8 | const location = useLocation();
9 | const [errorInfo, setErrorInfo] = useState(null);
10 |
11 | useEffect(() => {
12 | // Log the error for debugging
13 | console.error(
14 | "404 Error: User attempted to access non-existent route:",
15 | location.pathname
16 | );
17 |
18 | // Check for error state passed in location
19 | if (location.state && location.state.error) {
20 | setErrorInfo(location.state.error.toString());
21 | }
22 |
23 | // Check for error in URL params
24 | const params = new URLSearchParams(location.search);
25 | const errorParam = params.get("error");
26 | if (errorParam) {
27 | setErrorInfo(decodeURIComponent(errorParam));
28 | }
29 | }, [location]);
30 |
31 | return (
32 |
33 |
34 |
37 |
404
38 |
Oops! Page not found
39 |
40 | {errorInfo && (
41 |
42 |
Error details:
43 |
44 | {errorInfo}
45 |
46 |
47 | )}
48 |
49 |
57 |
58 |
59 | );
60 | };
61 |
62 | export default NotFound;
63 |
--------------------------------------------------------------------------------
/src/pages/Projects.tsx:
--------------------------------------------------------------------------------
1 |
2 | import React, { useState } from 'react';
3 | import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
4 | import { Button } from '@/components/ui/button';
5 | import { Plus, FolderOpen, Users, Clock, CheckCircle } from 'lucide-react';
6 | import { Badge } from '@/components/ui/badge';
7 | import { cn } from '@/lib/utils';
8 |
9 | interface Project {
10 | id: number;
11 | name: string;
12 | client: string;
13 | status: 'active' | 'completed' | 'on-hold';
14 | dueDate: string;
15 | progress: number;
16 | }
17 |
18 | const projects: Project[] = [];
19 |
20 | const ProjectCard: React.FC<{ project: Project }> = ({ project }) => {
21 | return (
22 |
23 |
24 |
25 |
26 | {project.name}
27 | {project.client}
28 |
29 |
39 | {project.status}
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | Due: {new Date(project.dueDate).toLocaleDateString()}
48 |
49 | {project.progress}% Complete
50 |
51 |
57 |
58 |
59 |
60 | );
61 | };
62 |
63 | const Projects = () => {
64 | const [showEmptyState, setShowEmptyState] = useState(projects.length === 0);
65 |
66 | return (
67 |
68 |
69 |
Projects
70 |
73 |
74 |
75 | {showEmptyState ? (
76 |
77 |
78 |
79 | No Projects Yet
80 |
81 | Create your first project to start tracking your work.
82 |
83 |
86 |
87 |
88 | ) : (
89 |
90 | {projects.map(project => (
91 |
92 | ))}
93 |
94 | )}
95 |
96 | );
97 | };
98 |
99 | export default Projects;
100 |
--------------------------------------------------------------------------------
/src/pages/SocialMediaIntegration.tsx:
--------------------------------------------------------------------------------
1 |
2 | import React from 'react';
3 | import { SocialMediaConnect } from '@/components/content/SocialMediaConnect';
4 |
5 | const SocialMediaIntegration = () => {
6 | return (
7 |
8 |
Social Media Integration
9 |
10 | Connect your social media accounts to enable posting directly from the content scheduler.
11 |
12 |
13 |
14 |
15 | );
16 | };
17 |
18 | export default SocialMediaIntegration;
19 |
--------------------------------------------------------------------------------
/src/services/api.ts:
--------------------------------------------------------------------------------
1 |
2 | import axios, { AxiosError, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
3 | import { config } from '@/config';
4 | import { authService } from './auth';
5 |
6 | // Create axios instance with base URL
7 | const api = axios.create({
8 | baseURL: config.apiUrl,
9 | timeout: 10000,
10 | headers: {
11 | 'Content-Type': 'application/json',
12 | },
13 | });
14 |
15 | // Request interceptor to add auth token
16 | api.interceptors.request.use(
17 | (config: InternalAxiosRequestConfig) => {
18 | const token = authService.getAccessToken();
19 | if (token) {
20 | config.headers.Authorization = `Bearer ${token}`;
21 | }
22 | return config;
23 | },
24 | (error: AxiosError) => {
25 | return Promise.reject(error);
26 | }
27 | );
28 |
29 | // Response interceptor to handle token refresh
30 | let isRefreshing = false;
31 | let failedQueue: { resolve: (value: unknown) => void; reject: (reason?: any) => void }[] = [];
32 |
33 | const processQueue = (error: AxiosError | null, token: string | null = null) => {
34 | failedQueue.forEach(promise => {
35 | if (error) {
36 | promise.reject(error);
37 | } else {
38 | promise.resolve(token);
39 | }
40 | });
41 |
42 | failedQueue = [];
43 | };
44 |
45 | api.interceptors.response.use(
46 | (response: AxiosResponse) => {
47 | return response;
48 | },
49 | async (error: AxiosError) => {
50 | const originalRequest = error.config as AxiosRequestConfig & { _retry?: boolean };
51 |
52 | // If the error is not 401 or the request has already been retried, reject
53 | if (!error.response || error.response.status !== 401 || originalRequest._retry) {
54 | return Promise.reject(error);
55 | }
56 |
57 | // If we're already refreshing, queue this request
58 | if (isRefreshing) {
59 | return new Promise((resolve, reject) => {
60 | failedQueue.push({ resolve, reject });
61 | })
62 | .then(token => {
63 | return api(originalRequest);
64 | })
65 | .catch(err => {
66 | return Promise.reject(err);
67 | });
68 | }
69 |
70 | originalRequest._retry = true;
71 | isRefreshing = true;
72 |
73 | try {
74 | // Try to refresh the token
75 | await authService.refreshAccessToken();
76 | const token = authService.getAccessToken();
77 |
78 | // Process the queue with the new token
79 | processQueue(null, token);
80 |
81 | // Update the Authorization header
82 | if (originalRequest.headers) {
83 | originalRequest.headers.Authorization = `Bearer ${token}`;
84 | } else {
85 | originalRequest.headers = { Authorization: `Bearer ${token}` };
86 | }
87 |
88 | // Retry the original request
89 | return api(originalRequest);
90 | } catch (refreshError) {
91 | // If refresh fails, process the queue with the error
92 | processQueue(refreshError as AxiosError);
93 |
94 | // Redirect to login or show error message
95 | // This will be handled by the error handler in the component
96 | return Promise.reject(refreshError);
97 | } finally {
98 | isRefreshing = false;
99 | }
100 | }
101 | );
102 |
103 | export default api;
--------------------------------------------------------------------------------
/src/types/masterAccount.ts:
--------------------------------------------------------------------------------
1 | // Define all types used in the Master Account context
2 |
3 | export interface Client {
4 | id: string;
5 | firstName: string;
6 | lastName: string;
7 | emails: string[];
8 | phoneNumbers: string[];
9 | company: string;
10 | leadType: string;
11 | leadSource: string;
12 | tags: string[];
13 | status: string;
14 | users: number;
15 | deals: number;
16 | contacts: number;
17 | lastActivity: string;
18 | logo: string;
19 | }
20 |
21 | export interface Webhook {
22 | id: number;
23 | name: string;
24 | url: string;
25 | events: string[];
26 | active: boolean;
27 | lastTriggered?: string;
28 | }
29 |
30 | export interface WebsitePage {
31 | id: number;
32 | title: string;
33 | url: string;
34 | status: 'published' | 'draft' | 'scheduled';
35 | type: 'landing' | 'blog' | 'product' | 'other';
36 | createdAt: string;
37 | updatedAt: string;
38 | views: number;
39 | conversions: number;
40 | bounceRate: number;
41 | clientId: string | null;
42 | }
43 |
44 | export interface ContentItem {
45 | id: string;
46 | title: string;
47 | content: string;
48 | type: 'email' | 'social' | 'blog' | 'other';
49 | platform?: string;
50 | createdBy: string;
51 | createdAt: string;
52 | status: 'pending' | 'approved' | 'rejected';
53 | scheduledFor?: string;
54 | rejectionReason?: string;
55 | approvedBy?: string;
56 | approvedAt?: string;
57 | media?: string | null;
58 | clientId: string | null;
59 | skipApproval?: boolean;
60 | }
61 |
62 | export interface Notification {
63 | id: number;
64 | title: string;
65 | message: string;
66 | type: 'approval' | 'rejection' | 'system' | 'other';
67 | createdAt: string;
68 | read: boolean;
69 | relatedContentId?: number;
70 | forClientId?: string | null;
71 | }
72 |
73 | export interface MasterAccountContextType {
74 | clients: Client[];
75 | currentClientId: string | null;
76 | webhooks: Webhook[];
77 | websitePages: WebsitePage[];
78 | contentItems: ContentItem[];
79 | notifications: Notification[];
80 | isLoadingClients: boolean;
81 | clientsLoaded: boolean;
82 | addClient: (client: Client) => void;
83 | removeClient: (id: string) => void;
84 | switchToClient: (id: string | null) => void;
85 | isInMasterMode: boolean;
86 | toggleMasterMode: () => void;
87 | loginToAccount: (email: string, password: string) => boolean;
88 | clearAllClients: () => void;
89 | fetchClientsData: () => Promise;
90 | refreshClientsData: () => Promise;
91 | addWebhook: (webhook: Omit) => void;
92 | removeWebhook: (id: number) => void;
93 | updateWebhook: (id: number, data: Partial) => void;
94 | triggerWebhook: (webhookId: number, data: any) => Promise;
95 | addWebsitePage: (page: Omit) => void;
96 | removeWebsitePage: (id: number) => void;
97 | updateWebsitePage: (id: number, data: Partial) => void;
98 | addContentItem: (item: Omit) => void;
99 | updateContentItem: (id: number, data: Partial) => void;
100 | deleteContentItem: (id: number) => void;
101 | updateContentStatus: (id: number, status: 'approved' | 'rejected', reason?: string) => void;
102 | getContentItems: (clientId?: string | null, status?: string) => ContentItem[];
103 | addNotification: (notification: Omit) => void;
104 | markNotificationAsRead: (id: number) => void;
105 | getNotifications: (forClientId?: string | null) => Notification[];
106 | getUnreadNotificationsCount: (forClientId?: string | null) => number;
107 | }
108 |
--------------------------------------------------------------------------------
/src/types/website.ts:
--------------------------------------------------------------------------------
1 |
2 | export interface YextIntegration {
3 | apiKey: string;
4 | businessId: string;
5 | lastSynced: string | null;
6 | isConnected: boolean;
7 | }
8 |
9 | export interface ReviewFilter {
10 | rating?: number;
11 | platform?: string;
12 | dateRange: string;
13 | keyword?: string;
14 | }
15 |
16 | export interface WebsitePage {
17 | id: string;
18 | title: string;
19 | slug: string;
20 | url?: string;
21 | status: 'published' | 'draft' | 'scheduled';
22 | type: 'landing' | 'content' | 'blog' | 'product' | 'other';
23 | visits: number;
24 | views: number;
25 | conversions: number;
26 | bounceRate: number;
27 | lastUpdated: string;
28 | updatedAt: string;
29 | createdAt: string;
30 | content?: string;
31 | template?: string;
32 | clientId: string;
33 | }
34 |
35 | export interface PageFormValues {
36 | title: string;
37 | slug: string;
38 | url?: string;
39 | type: 'landing' | 'content' | 'blog' | 'product' | 'other';
40 | template?: string;
41 | content?: string;
42 | status: 'published' | 'draft' | 'scheduled';
43 | }
44 |
45 | // Adding integration interfaces
46 | export interface Integration {
47 | id: string;
48 | name: string;
49 | type: 'email' | 'calendar' | 'webhook' | 'api' | 'other';
50 | url?: string;
51 | status: 'active' | 'inactive' | 'error';
52 | }
53 |
54 | export interface WebhookIntegration extends Integration {
55 | webhookUrl: string;
56 | events: string[];
57 | lastTriggered?: string;
58 | }
59 |
--------------------------------------------------------------------------------
/src/utils/dateUtils.ts:
--------------------------------------------------------------------------------
1 | export const getRelativeTimeString = (date: Date | string): string => {
2 | const now = new Date();
3 | const past = new Date(date);
4 | const diffInSeconds = Math.floor((now.getTime() - past.getTime()) / 1000);
5 |
6 | if (diffInSeconds < 60) {
7 | return 'just now';
8 | }
9 |
10 | const diffInMinutes = Math.floor(diffInSeconds / 60);
11 | if (diffInMinutes < 60) {
12 | return `${diffInMinutes} ${diffInMinutes === 1 ? 'minute' : 'minutes'} ago`;
13 | }
14 |
15 | const diffInHours = Math.floor(diffInMinutes / 60);
16 | if (diffInHours < 24) {
17 | return `${diffInHours} ${diffInHours === 1 ? 'hour' : 'hours'} ago`;
18 | }
19 |
20 | const diffInDays = Math.floor(diffInHours / 24);
21 | if (diffInDays < 30) {
22 | return `${diffInDays} ${diffInDays === 1 ? 'day' : 'days'} ago`;
23 | }
24 |
25 | const diffInMonths = Math.floor(diffInDays / 30);
26 | if (diffInMonths < 12) {
27 | return `${diffInMonths} ${diffInMonths === 1 ? 'month' : 'months'} ago`;
28 | }
29 |
30 | const diffInYears = Math.floor(diffInMonths / 12);
31 | return `${diffInYears} ${diffInYears === 1 ? 'year' : 'years'} ago`;
32 | };
--------------------------------------------------------------------------------
/src/utils/formatters.ts:
--------------------------------------------------------------------------------
1 |
2 | /**
3 | * Format a number as currency
4 | */
5 | export const formatCurrency = (value: number | undefined | null): string => {
6 | if (value === undefined || value === null) return '$0';
7 | return new Intl.NumberFormat('en-US', {
8 | style: 'currency',
9 | currency: 'USD',
10 | }).format(value);
11 | };
12 |
13 | /**
14 | * Format a date string to a readable format
15 | */
16 | export const formatDate = (dateString: string): string => {
17 | if (!dateString) return '';
18 | try {
19 | const date = new Date(dateString);
20 | return new Intl.DateTimeFormat('en-US', {
21 | year: 'numeric',
22 | month: 'short',
23 | day: 'numeric',
24 | }).format(date);
25 | } catch (error) {
26 | console.error('Error formatting date:', error);
27 | return '';
28 | }
29 | };
30 |
31 | /**
32 | * Format a review date with relative time if recent
33 | */
34 | export const formatReviewDate = (dateString: string): string => {
35 | if (!dateString) return '';
36 |
37 | try {
38 | const date = new Date(dateString);
39 | const now = new Date();
40 | const diffTime = Math.abs(now.getTime() - date.getTime());
41 | const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
42 |
43 | if (diffDays < 1) {
44 | // Less than a day ago
45 | const diffHours = Math.floor(diffTime / (1000 * 60 * 60));
46 | if (diffHours < 1) {
47 | // Less than an hour ago
48 | const diffMinutes = Math.floor(diffTime / (1000 * 60));
49 | return `${diffMinutes} ${diffMinutes === 1 ? 'minute' : 'minutes'} ago`;
50 | }
51 | return `${diffHours} ${diffHours === 1 ? 'hour' : 'hours'} ago`;
52 | } else if (diffDays < 7) {
53 | // Less than a week ago
54 | return `${diffDays} ${diffDays === 1 ? 'day' : 'days'} ago`;
55 | } else {
56 | // More than a week ago, use standard date format
57 | return formatDate(dateString);
58 | }
59 | } catch (error) {
60 | console.error('Error formatting review date:', error);
61 | return '';
62 | }
63 | };
64 |
65 | /**
66 | * Get badge variant based on status
67 | */
68 | export const getStatusBadgeVariant = (status: string): "default" | "secondary" | "destructive" | "outline" => {
69 | const statusMap: Record = {
70 | published: "default",
71 | draft: "secondary",
72 | archived: "destructive",
73 | pending: "outline"
74 | };
75 |
76 | return statusMap[status.toLowerCase()] || "default";
77 | };
78 |
--------------------------------------------------------------------------------
/src/utils/taskUtils.ts:
--------------------------------------------------------------------------------
1 | import { Task } from '@/contexts/TasksContext';
2 |
3 | /**
4 | * Sort tasks by priority and completion status
5 | */
6 | export const sortTasksByPriority = (tasks: Task[]): Task[] => {
7 | return [...tasks].sort((a, b) => {
8 | // First sort by completion status
9 | if (a.completed !== b.completed) {
10 | return a.completed ? 1 : -1;
11 | }
12 |
13 | // Then sort by priority (if available)
14 | if (a.priority && b.priority) {
15 | const priorityOrder = { high: 0, medium: 1, low: 2 };
16 | return priorityOrder[a.priority] - priorityOrder[b.priority];
17 | } else if (a.priority) {
18 | return -1; // a has priority, b doesn't
19 | } else if (b.priority) {
20 | return 1; // b has priority, a doesn't
21 | }
22 |
23 | // Finally sort by date
24 | return new Date(a.date).getTime() - new Date(b.date).getTime();
25 | });
26 | };
27 |
28 | /**
29 | * Group tasks by date
30 | */
31 | export const groupTasksByDate = (tasks: Task[]): Record => {
32 | const grouped: Record = {};
33 |
34 | tasks.forEach(task => {
35 | if (!grouped[task.date]) {
36 | grouped[task.date] = [];
37 | }
38 | grouped[task.date].push(task);
39 | });
40 |
41 | return grouped;
42 | };
43 |
44 | /**
45 | * Format a date string for display
46 | */
47 | export const formatTaskDate = (dateString: string): string => {
48 | const date = new Date(dateString);
49 | const today = new Date();
50 | const tomorrow = new Date(today);
51 | tomorrow.setDate(tomorrow.getDate() + 1);
52 |
53 | // Check if the date is today
54 | if (
55 | date.getDate() === today.getDate() &&
56 | date.getMonth() === today.getMonth() &&
57 | date.getFullYear() === today.getFullYear()
58 | ) {
59 | return 'Today';
60 | }
61 |
62 | // Check if the date is tomorrow
63 | if (
64 | date.getDate() === tomorrow.getDate() &&
65 | date.getMonth() === tomorrow.getMonth() &&
66 | date.getFullYear() === tomorrow.getFullYear()
67 | ) {
68 | return 'Tomorrow';
69 | }
70 |
71 | // Otherwise format the date
72 | return date.toLocaleDateString('en-US', {
73 | month: 'short',
74 | day: 'numeric',
75 | year: 'numeric'
76 | });
77 | };
78 |
79 | /**
80 | * Get color for task priority
81 | */
82 | export const getTaskPriorityColor = (priority?: 'low' | 'medium' | 'high'): string => {
83 | switch (priority) {
84 | case 'high':
85 | return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300';
86 | case 'medium':
87 | return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300';
88 | case 'low':
89 | return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300';
90 | default:
91 | return 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-300';
92 | }
93 | };
94 |
--------------------------------------------------------------------------------
/src/utils/websitePageAdapter.ts:
--------------------------------------------------------------------------------
1 |
2 | // Create a type adapter to handle context vs component type differences
3 | type ContextWebsitePage = {
4 | id: number;
5 | title: string;
6 | url: string;
7 | status: 'published' | 'draft' | 'scheduled';
8 | type: 'landing' | 'blog' | 'product' | 'other';
9 | createdAt: string;
10 | updatedAt: string;
11 | views: number;
12 | conversions: number;
13 | bounceRate: number;
14 | clientId: number | null;
15 | };
16 |
17 | // Type adapter function to ensure consistency
18 | export const adaptWebsitePageForComponents = (page: ContextWebsitePage) => {
19 | return {
20 | id: String(page.id),
21 | title: page.title,
22 | slug: page.url || '',
23 | url: page.url,
24 | status: page.status,
25 | type: page.type,
26 | visits: page.views || 0,
27 | views: page.views || 0,
28 | conversions: page.conversions || 0,
29 | bounceRate: page.bounceRate || 0,
30 | lastUpdated: page.updatedAt,
31 | updatedAt: page.updatedAt,
32 | createdAt: page.createdAt,
33 | clientId: String(page.clientId)
34 | };
35 | };
36 |
--------------------------------------------------------------------------------
/src/utils/websiteStatsCalculator.ts:
--------------------------------------------------------------------------------
1 |
2 | type WebsitePage = {
3 | views?: number;
4 | conversions?: number;
5 | bounceRate?: number;
6 | status?: string;
7 | type?: string;
8 | };
9 |
10 | export const calculateWebsiteStats = (websitePages: WebsitePage[]) => {
11 | const totalPages = websitePages.length;
12 | const publishedPages = websitePages.filter(page => page.status === 'published').length;
13 | const totalViews = websitePages.reduce((sum, page) => sum + (page.views || 0), 0);
14 | const totalConversions = websitePages.reduce((sum, page) => sum + (page.conversions || 0), 0);
15 | const avgBounceRate = websitePages.length > 0
16 | ? websitePages.reduce((sum, page) => sum + (page.bounceRate || 0), 0) / websitePages.length
17 | : 0;
18 |
19 | // Landing pages specifically
20 | const landingPages = websitePages.filter(page => page.type === 'landing');
21 | const landingPageViews = landingPages.reduce((sum, page) => sum + (page.views || 0), 0);
22 | const landingPageConversions = landingPages.reduce((sum, page) => sum + (page.conversions || 0), 0);
23 |
24 | return {
25 | totalPages,
26 | publishedPages,
27 | totalViews,
28 | totalConversions,
29 | avgBounceRate,
30 | landingPages,
31 | landingPageViews,
32 | landingPageConversions
33 | };
34 | };
35 |
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 |
2 | import type { Config } from "tailwindcss";
3 |
4 | export default {
5 | darkMode: ["class"],
6 | content: [
7 | "./pages/**/*.{ts,tsx}",
8 | "./components/**/*.{ts,tsx}",
9 | "./app/**/*.{ts,tsx}",
10 | "./src/**/*.{ts,tsx}",
11 | ],
12 | prefix: "",
13 | theme: {
14 | container: {
15 | center: true,
16 | padding: '2rem',
17 | screens: {
18 | '2xl': '1400px'
19 | }
20 | },
21 | extend: {
22 | colors: {
23 | border: 'hsl(var(--border))',
24 | input: 'hsl(var(--input))',
25 | ring: 'hsl(var(--ring))',
26 | background: 'hsl(var(--background))',
27 | foreground: 'hsl(var(--foreground))',
28 | primary: {
29 | DEFAULT: 'hsl(var(--primary))',
30 | foreground: 'hsl(var(--primary-foreground))'
31 | },
32 | secondary: {
33 | DEFAULT: 'hsl(var(--secondary))',
34 | foreground: 'hsl(var(--secondary-foreground))'
35 | },
36 | destructive: {
37 | DEFAULT: 'hsl(var(--destructive))',
38 | foreground: 'hsl(var(--destructive-foreground))'
39 | },
40 | muted: {
41 | DEFAULT: 'hsl(var(--muted))',
42 | foreground: 'hsl(var(--muted-foreground))'
43 | },
44 | accent: {
45 | DEFAULT: 'hsl(var(--accent))',
46 | foreground: 'hsl(var(--accent-foreground))'
47 | },
48 | popover: {
49 | DEFAULT: 'hsl(var(--popover))',
50 | foreground: 'hsl(var(--popover-foreground))'
51 | },
52 | card: {
53 | DEFAULT: 'hsl(var(--card))',
54 | foreground: 'hsl(var(--card-foreground))'
55 | },
56 | sidebar: {
57 | DEFAULT: 'hsl(var(--sidebar-background))',
58 | foreground: 'hsl(var(--sidebar-foreground))',
59 | primary: 'hsl(var(--sidebar-primary))',
60 | 'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
61 | accent: 'hsl(var(--sidebar-accent))',
62 | 'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
63 | border: 'hsl(var(--sidebar-border))',
64 | ring: 'hsl(var(--sidebar-ring))'
65 | }
66 | },
67 | borderRadius: {
68 | lg: 'var(--radius)',
69 | md: 'calc(var(--radius) - 2px)',
70 | sm: 'calc(var(--radius) - 4px)'
71 | },
72 | keyframes: {
73 | 'accordion-down': {
74 | from: { height: '0' },
75 | to: { height: 'var(--radix-accordion-content-height)' }
76 | },
77 | 'accordion-up': {
78 | from: { height: 'var(--radix-accordion-content-height)' },
79 | to: { height: '0' }
80 | },
81 | 'fade-in': {
82 | '0%': { opacity: '0' },
83 | '100%': { opacity: '1' }
84 | },
85 | 'fade-out': {
86 | '0%': { opacity: '1' },
87 | '100%': { opacity: '0' }
88 | },
89 | 'slide-in': {
90 | '0%': { transform: 'translateY(10px)', opacity: '0' },
91 | '100%': { transform: 'translateY(0)', opacity: '1' }
92 | },
93 | 'slide-out': {
94 | '0%': { transform: 'translateY(0)', opacity: '1' },
95 | '100%': { transform: 'translateY(10px)', opacity: '0' }
96 | },
97 | 'scale-in': {
98 | '0%': { transform: 'scale(0.95)', opacity: '0' },
99 | '100%': { transform: 'scale(1)', opacity: '1' }
100 | },
101 | 'scale-out': {
102 | '0%': { transform: 'scale(1)', opacity: '1' },
103 | '100%': { transform: 'scale(0.95)', opacity: '0' }
104 | },
105 | 'slide-in-right': {
106 | '0%': { transform: 'translateX(10px)', opacity: '0' },
107 | '100%': { transform: 'translateX(0)', opacity: '1' }
108 | }
109 | },
110 | animation: {
111 | 'accordion-down': 'accordion-down 0.2s ease-out',
112 | 'accordion-up': 'accordion-up 0.2s ease-out',
113 | 'fade-in': 'fade-in 0.3s ease-out',
114 | 'fade-out': 'fade-out 0.3s ease-out',
115 | 'slide-in': 'slide-in 0.3s ease-out',
116 | 'slide-out': 'slide-out 0.3s ease-out',
117 | 'scale-in': 'scale-in 0.2s ease-out',
118 | 'scale-out': 'scale-out 0.2s ease-out',
119 | 'slide-in-right': 'slide-in-right 0.3s ease-out'
120 | }
121 | }
122 | },
123 | plugins: [require("tailwindcss-animate")],
124 | } satisfies Config;
125 |
--------------------------------------------------------------------------------
/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "isolatedModules": true,
13 | "moduleDetection": "force",
14 | "noEmit": true,
15 | "jsx": "react-jsx",
16 |
17 | /* Linting */
18 | "strict": false,
19 | "noUnusedLocals": false,
20 | "noUnusedParameters": false,
21 | "noImplicitAny": false,
22 | "noFallthroughCasesInSwitch": false,
23 |
24 | "baseUrl": ".",
25 | "paths": {
26 | "@/*": ["./src/*"]
27 | }
28 | },
29 | "include": ["src"]
30 | }
31 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [],
3 | "references": [
4 | { "path": "./tsconfig.app.json" },
5 | { "path": "./tsconfig.node.json" }
6 | ],
7 | "compilerOptions": {
8 | "baseUrl": ".",
9 | "paths": {
10 | "@/*": ["src/*"]
11 | },
12 | "noImplicitAny": false,
13 | "noUnusedParameters": false,
14 | "skipLibCheck": true,
15 | "jsx": "react-jsx",
16 | "allowJs": true,
17 | "noUnusedLocals": false,
18 | "strictNullChecks": false,
19 |
20 | },
21 | }
22 |
23 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2022",
4 | "lib": ["ES2023"],
5 | "module": "ESNext",
6 | "skipLibCheck": true,
7 |
8 | /* Bundler mode */
9 | "moduleResolution": "bundler",
10 | "allowImportingTsExtensions": true,
11 | "isolatedModules": true,
12 | "moduleDetection": "force",
13 | "noEmit": true,
14 |
15 | /* Linting */
16 | "strict": true,
17 | "noUnusedLocals": false,
18 | "noUnusedParameters": false,
19 | "noFallthroughCasesInSwitch": true
20 | },
21 | "include": ["vite.config.ts"]
22 | }
23 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import react from "@vitejs/plugin-react-swc";
3 | import path from "path";
4 | import { componentTagger } from "lovable-tagger";
5 |
6 | // https://vitejs.dev/config/
7 | export default defineConfig(({ mode }) => ({
8 | server: {
9 | host: "::",
10 | port: 8080,
11 | },
12 | plugins: [
13 | react(),
14 | mode === 'development' &&
15 | componentTagger(),
16 | ].filter(Boolean),
17 | resolve: {
18 | alias: {
19 | "@": path.resolve(__dirname, "./src"),
20 | },
21 | },
22 | }));
23 |
24 |
25 |
26 | //export default defineConfig({
27 | // plugins: [react()],
28 | // optimizeDeps: {
29 | // exclude: ['@mapbox/node-pre-gyp', 'aws-sdk', 'mock-aws-s3', 'nock'],
30 | // },
31 | // build: {
32 | // rollupOptions: {
33 | // external: ['@mapbox/node-pre-gyp', 'aws-sdk', 'mock-aws-s3', 'nock'],
34 | // },
35 | // },
36 | //});
--------------------------------------------------------------------------------