From 0a4b77865affd94686f0f4edf365ad2308271337 Mon Sep 17 00:00:00 2001 From: Cristian Dibrell Date: Fri, 22 Aug 2025 13:18:23 -0500 Subject: [PATCH 1/9] feat(web): bootstrap Pravado FE shell MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create React + Vite app with TypeScript in apps/web - Implement PRAVADO design system with beige-first palette - Configure Tailwind CSS with CSS variables for theming - Build app shell with collapsible sidebar and responsive topbar - Add React Router with all core routes (dashboard, campaigns, content, etc.) - Create Dashboard with Visibility Score KPI and AI recommendations - Build Content Studio with forced light mode and split-panel layout - Implement PR Credits wallet widget and basic page structures - Add dark/light theme toggle with localStorage persistence - Full TypeScript compliance with zero errors - Production build tested and working (apps/web/dist) ๐Ÿค– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .editorconfig | 12 ++ .gitignore | 77 +++++++++ apps/web/.gitignore | 24 +++ apps/web/README.md | 69 ++++++++ apps/web/eslint.config.js | 23 +++ apps/web/index.html | 16 ++ apps/web/package.json | 41 +++++ apps/web/postcss.config.cjs | 6 + apps/web/public/vite.svg | 1 + apps/web/src/App.css | 42 +++++ apps/web/src/App.tsx | 35 ++++ apps/web/src/assets/react.svg | 1 + apps/web/src/hooks/useTheme.ts | 37 ++++ apps/web/src/index.css | 68 ++++++++ apps/web/src/layouts/AppLayout.tsx | 153 +++++++++++++++++ apps/web/src/lib/utils.ts | 6 + apps/web/src/main.tsx | 10 ++ apps/web/src/pages/Analytics.tsx | 27 +++ apps/web/src/pages/Campaigns.tsx | 15 ++ apps/web/src/pages/ContentStudio.tsx | 204 ++++++++++++++++++++++ apps/web/src/pages/Copilot.tsx | 27 +++ apps/web/src/pages/Dashboard.tsx | 246 +++++++++++++++++++++++++++ apps/web/src/pages/MediaDB.tsx | 15 ++ apps/web/src/pages/PR.tsx | 53 ++++++ apps/web/src/pages/SEO.tsx | 32 ++++ apps/web/src/pages/Settings.tsx | 32 ++++ apps/web/src/styles/globals.css | 110 ++++++++++++ apps/web/src/vite-env.d.ts | 1 + apps/web/tailwind.config.js | 48 ++++++ apps/web/tsconfig.app.json | 27 +++ apps/web/tsconfig.json | 7 + apps/web/tsconfig.node.json | 25 +++ apps/web/vite.config.ts | 7 + package.json | 24 +++ 34 files changed, 1521 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 apps/web/.gitignore create mode 100644 apps/web/README.md create mode 100644 apps/web/eslint.config.js create mode 100644 apps/web/index.html create mode 100644 apps/web/package.json create mode 100644 apps/web/postcss.config.cjs create mode 100644 apps/web/public/vite.svg create mode 100644 apps/web/src/App.css create mode 100644 apps/web/src/App.tsx create mode 100644 apps/web/src/assets/react.svg create mode 100644 apps/web/src/hooks/useTheme.ts create mode 100644 apps/web/src/index.css create mode 100644 apps/web/src/layouts/AppLayout.tsx create mode 100644 apps/web/src/lib/utils.ts create mode 100644 apps/web/src/main.tsx create mode 100644 apps/web/src/pages/Analytics.tsx create mode 100644 apps/web/src/pages/Campaigns.tsx create mode 100644 apps/web/src/pages/ContentStudio.tsx create mode 100644 apps/web/src/pages/Copilot.tsx create mode 100644 apps/web/src/pages/Dashboard.tsx create mode 100644 apps/web/src/pages/MediaDB.tsx create mode 100644 apps/web/src/pages/PR.tsx create mode 100644 apps/web/src/pages/SEO.tsx create mode 100644 apps/web/src/pages/Settings.tsx create mode 100644 apps/web/src/styles/globals.css create mode 100644 apps/web/src/vite-env.d.ts create mode 100644 apps/web/tailwind.config.js create mode 100644 apps/web/tsconfig.app.json create mode 100644 apps/web/tsconfig.json create mode 100644 apps/web/tsconfig.node.json create mode 100644 apps/web/vite.config.ts create mode 100644 package.json diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..8f1d938b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..8f935cc3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,77 @@ +# Dependencies +node_modules/ +.pnpm-store/ + +# Build outputs +dist/ +build/ +.next/ +.nuxt/ + +# Environment files +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage/ +*.lcov + +# Dependency directories +jspm_packages/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Temporary folders +tmp/ +temp/ + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? \ No newline at end of file diff --git a/apps/web/.gitignore b/apps/web/.gitignore new file mode 100644 index 00000000..a547bf36 --- /dev/null +++ b/apps/web/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/apps/web/README.md b/apps/web/README.md new file mode 100644 index 00000000..7959ce42 --- /dev/null +++ b/apps/web/README.md @@ -0,0 +1,69 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default tseslint.config([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + ...tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + ...tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + ...tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default tseslint.config([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/apps/web/eslint.config.js b/apps/web/eslint.config.js new file mode 100644 index 00000000..d94e7deb --- /dev/null +++ b/apps/web/eslint.config.js @@ -0,0 +1,23 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { globalIgnores } from 'eslint/config' + +export default tseslint.config([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs['recommended-latest'], + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/apps/web/index.html b/apps/web/index.html new file mode 100644 index 00000000..bd625e1e --- /dev/null +++ b/apps/web/index.html @@ -0,0 +1,16 @@ + + + + + + + PRAVADO - Marketing Intelligence Platform + + + + + +
+ + + diff --git a/apps/web/package.json b/apps/web/package.json new file mode 100644 index 00000000..4fb01991 --- /dev/null +++ b/apps/web/package.json @@ -0,0 +1,41 @@ +{ + "name": "web", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview", + "type-check": "tsc --noEmit" + }, + "dependencies": { + "@radix-ui/react-icons": "^1.3.2", + "@radix-ui/react-slot": "^1.2.3", + "@tailwindcss/postcss": "^4.1.12", + "autoprefixer": "^10.4.21", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.541.0", + "postcss": "^8.5.6", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "react-router-dom": "^7.8.1", + "tailwind-merge": "^3.3.1", + "tailwindcss": "^4.1.12" + }, + "devDependencies": { + "@eslint/js": "^9.33.0", + "@types/react": "^19.1.10", + "@types/react-dom": "^19.1.7", + "@vitejs/plugin-react": "^5.0.0", + "eslint": "^9.33.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.20", + "globals": "^16.3.0", + "typescript": "~5.8.3", + "typescript-eslint": "^8.39.1", + "vite": "^7.1.2" + } +} diff --git a/apps/web/postcss.config.cjs b/apps/web/postcss.config.cjs new file mode 100644 index 00000000..dc655aa4 --- /dev/null +++ b/apps/web/postcss.config.cjs @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + '@tailwindcss/postcss': {}, + autoprefixer: {}, + }, +} \ No newline at end of file diff --git a/apps/web/public/vite.svg b/apps/web/public/vite.svg new file mode 100644 index 00000000..e7b8dfb1 --- /dev/null +++ b/apps/web/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/App.css b/apps/web/src/App.css new file mode 100644 index 00000000..b9d355df --- /dev/null +++ b/apps/web/src/App.css @@ -0,0 +1,42 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx new file mode 100644 index 00000000..4f74dd49 --- /dev/null +++ b/apps/web/src/App.tsx @@ -0,0 +1,35 @@ +import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom' +import { AppLayout } from './layouts/AppLayout' +import { Dashboard } from './pages/Dashboard' +import { Campaigns } from './pages/Campaigns' +import { MediaDB } from './pages/MediaDB' +import { ContentStudio } from './pages/ContentStudio' +import { SEO } from './pages/SEO' +import { PR } from './pages/PR' +import { Analytics } from './pages/Analytics' +import { Copilot } from './pages/Copilot' +import { Settings } from './pages/Settings' +import './styles/globals.css' + +function App() { + return ( + + + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + + ) +} + +export default App diff --git a/apps/web/src/assets/react.svg b/apps/web/src/assets/react.svg new file mode 100644 index 00000000..6c87de9b --- /dev/null +++ b/apps/web/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/hooks/useTheme.ts b/apps/web/src/hooks/useTheme.ts new file mode 100644 index 00000000..f250a20b --- /dev/null +++ b/apps/web/src/hooks/useTheme.ts @@ -0,0 +1,37 @@ +import { useEffect, useState } from 'react' + +type Theme = 'light' | 'dark' + +export function useTheme() { + const [theme, setTheme] = useState(() => { + // Get theme from localStorage or default to dark for dashboard + const stored = localStorage.getItem('pravado-theme') + return (stored as Theme) || 'dark' + }) + + useEffect(() => { + const root = window.document.documentElement + + root.classList.remove('light', 'dark') + root.classList.add(theme) + + localStorage.setItem('pravado-theme', theme) + }, [theme]) + + const toggleTheme = () => { + setTheme(prev => prev === 'light' ? 'dark' : 'light') + } + + const setLightMode = () => setTheme('light') + const setDarkMode = () => setTheme('dark') + + return { + theme, + setTheme, + toggleTheme, + setLightMode, + setDarkMode, + isDark: theme === 'dark', + isLight: theme === 'light' + } +} \ No newline at end of file diff --git a/apps/web/src/index.css b/apps/web/src/index.css new file mode 100644 index 00000000..08a3ac9e --- /dev/null +++ b/apps/web/src/index.css @@ -0,0 +1,68 @@ +:root { + font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/apps/web/src/layouts/AppLayout.tsx b/apps/web/src/layouts/AppLayout.tsx new file mode 100644 index 00000000..3e47baed --- /dev/null +++ b/apps/web/src/layouts/AppLayout.tsx @@ -0,0 +1,153 @@ +import { useState } from 'react' +import type { ReactNode } from 'react' +import { Link, useLocation } from 'react-router-dom' +import { + LayoutDashboard, + Megaphone, + Database, + FileText, + Search, + BarChart3, + Bot, + Settings, + Menu, + X, + Bell, + User, + Moon, + Sun +} from 'lucide-react' +import { cn } from '../lib/utils' +import { useTheme } from '../hooks/useTheme' + +interface AppLayoutProps { + children: ReactNode +} + +const navigation = [ + { name: 'Dashboard', href: '/dashboard', icon: LayoutDashboard }, + { name: 'Campaigns', href: '/campaigns', icon: Megaphone }, + { name: 'Media DB', href: '/media', icon: Database }, + { name: 'Content Studio', href: '/content', icon: FileText }, + { name: 'SEO/GEO', href: '/seo', icon: Search }, + { name: 'PR & Outreach', href: '/pr', icon: BarChart3 }, + { name: 'Analytics', href: '/analytics', icon: BarChart3 }, + { name: 'Copilot', href: '/copilot', icon: Bot }, + { name: 'Settings', href: '/settings', icon: Settings }, +] + +export function AppLayout({ children }: AppLayoutProps) { + const [sidebarOpen, setSidebarOpen] = useState(false) + const location = useLocation() + const { theme, toggleTheme } = useTheme() + + return ( +
+ {/* Mobile sidebar overlay */} + {sidebarOpen && ( +
setSidebarOpen(false)} + /> + )} + + {/* Sidebar */} +
+
+ PRAVADO +
+ + +
+ + {/* Main content */} +
+ {/* Top bar */} +
+ + +
+ {/* Search */} +
+ + +
+ + {/* Theme toggle */} + + + {/* Notifications */} + + + {/* Profile */} + +
+
+ + {/* Page content */} +
+ {children} +
+
+ + {/* Mobile sidebar close button */} + {sidebarOpen && ( + + )} +
+ ) +} \ No newline at end of file diff --git a/apps/web/src/lib/utils.ts b/apps/web/src/lib/utils.ts new file mode 100644 index 00000000..1a860ee3 --- /dev/null +++ b/apps/web/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { type ClassValue, clsx } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} \ No newline at end of file diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx new file mode 100644 index 00000000..97188bc2 --- /dev/null +++ b/apps/web/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './styles/globals.css' +import App from './App.tsx' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/apps/web/src/pages/Analytics.tsx b/apps/web/src/pages/Analytics.tsx new file mode 100644 index 00000000..36bc9bfb --- /dev/null +++ b/apps/web/src/pages/Analytics.tsx @@ -0,0 +1,27 @@ +export function Analytics() { + return ( +
+
+

Analytics

+

Cross-pillar performance analysis and attribution

+
+ +
+
+

Attribution Map

+

Track campaign influence across channels

+
+ +
+

Share of Voice

+

Market presence and competitor analysis

+
+ +
+

Conversions

+

Revenue attribution and ROI tracking

+
+
+
+ ) +} \ No newline at end of file diff --git a/apps/web/src/pages/Campaigns.tsx b/apps/web/src/pages/Campaigns.tsx new file mode 100644 index 00000000..d58c6799 --- /dev/null +++ b/apps/web/src/pages/Campaigns.tsx @@ -0,0 +1,15 @@ +export function Campaigns() { + return ( +
+
+

Campaigns

+

Manage your marketing campaigns with Kanban workflows

+
+ +
+

Campaign Management

+

Kanban board with Planning โ†’ Drafting โ†’ Outreach โ†’ Results workflow coming soon

+
+
+ ) +} \ No newline at end of file diff --git a/apps/web/src/pages/ContentStudio.tsx b/apps/web/src/pages/ContentStudio.tsx new file mode 100644 index 00000000..77f3bac8 --- /dev/null +++ b/apps/web/src/pages/ContentStudio.tsx @@ -0,0 +1,204 @@ +import { useEffect } from 'react' +import { FileText, Target, Users, Lightbulb, Wand2, RotateCcw, Save, Send } from 'lucide-react' +import { useTheme } from '../hooks/useTheme' + +export function ContentStudio() { + const { setLightMode } = useTheme() + + // Force light mode for Content Studio per spec + useEffect(() => { + setLightMode() + }, [setLightMode]) + + return ( +
+ {/* Header */} +
+

Content Studio

+

Create, edit, and optimize content with AI assistance

+
+ + {/* Main Content Area */} +
+ {/* Brief Panel */} +
+ {/* Content Brief */} +
+
+ +

Content Brief

+
+ +
+
+ + +
+ +
+ +
+
+ + Marketing Directors +
+
+ + Enterprise Leaders +
+
+ + SMB Owners +
+
+
+ +
+ + +
+ +
+ + +
+ +
+ +