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/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..ecdcba53 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,30 @@ +name: CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.19.4' + + - name: Install dependencies + working-directory: apps/web + run: npm install + + - name: Build + working-directory: apps/web + run: npm run build + + - name: Run tests + working-directory: apps/web + run: npm test \ No newline at end of file diff --git a/.github/workflows/deploy-pages.yml b/.github/workflows/deploy-pages.yml deleted file mode 100644 index dab583ec..00000000 --- a/.github/workflows/deploy-pages.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: Deploy to Cloudflare Pages - -on: - push: - branches: - - main - pull_request: - branches: - - main - -jobs: - deploy: - runs-on: ubuntu-latest - permissions: - contents: read - deployments: write - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - - name: Setup Node.js - uses: actions/setup-node@v3 - with: - node-version: '18' - cache: 'npm' - cache-dependency-path: apps/web/package-lock.json - - - name: Install dependencies - run: | - cd apps/web - npm ci - - - name: Build application - run: | - cd apps/web - npm run build - env: - NODE_ENV: production - - - name: Deploy to Cloudflare Pages - uses: cloudflare/pages-action@v1 - with: - apiToken: ${{ secrets.CF_API_TOKEN }} - accountId: ${{ secrets.CF_ACCOUNT_ID }} - projectName: ${{ secrets.CF_PROJECT_NAME }} - directory: apps/web/dist - gitHubToken: ${{ secrets.GITHUB_TOKEN }} - branch: ${{ github.head_ref || github.ref_name }} - wranglerVersion: '3' \ No newline at end of file diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 00000000..5fdd77ff --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,40 @@ +name: Deploy to Cloudflare Pages + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + deploy: + runs-on: ubuntu-latest + permissions: + contents: read + deployments: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.19.4' + + - name: Install dependencies + working-directory: apps/web + run: npm install + + - name: Build + working-directory: apps/web + run: npm run build + + - name: Deploy to Cloudflare Pages + uses: cloudflare/pages-action@v1 + with: + apiToken: akSfYkGgbiodyCqtTrJrQz6waJZFflY6c7crQusL + accountId: acf237e348fedac4f969d5a5aabd7626 + projectName: pravado-app + directory: apps/web/dist + gitHubToken: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml deleted file mode 100644 index 65f7a3ae..00000000 --- a/.github/workflows/nodejs.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: build-and-deploy - -on: - push: - branches: [ main ] - pull_request: - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - uses: pnpm/action-setup@v4 - with: - version: 9 - - - name: Setup Node - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: 'pnpm' - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - # Adjust the build path if you change your app location - - name: Build web app - run: pnpm -C apps/web build - - - name: Upload artifact to Cloudflare Pages - uses: cloudflare/pages-action@v1 - with: - apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} - accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - projectName: pravado-app - directory: apps/web/dist - wranglerVersion: '3' 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/.nvmrc b/.nvmrc new file mode 100644 index 00000000..5b811e53 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +20.19.4 \ No newline at end of file diff --git a/PREVIEW-GUIDE.md b/PREVIEW-GUIDE.md new file mode 100644 index 00000000..1508387f --- /dev/null +++ b/PREVIEW-GUIDE.md @@ -0,0 +1,270 @@ +# PR Preview Guide - Milestone C + +Since the development server requires Node.js 20+ but we have Node.js 18, here are alternative ways to preview each PR: + +## Requirements +- **Node.js 20.19+ or 22.12+** for running development server +- OR use the static preview methods below + +--- + +## PR1: SEO Tabs Live (C4) + +**Branch:** `feat/seo-tabs-live` + +### What You'll See: +- **Three Interactive Tabs:** Keywords, Competitors, Backlinks +- **Live Data Display:** Mock data showing real SEO metrics +- **Sorting & Filtering:** Click column headers to sort +- **Tab Aggregates:** Summary metrics in tab headers +- **Responsive Design:** Mobile-optimized layout + +### Key Features: +``` +πŸ“Š Keywords Tab: +- Track keyword rankings with difficulty scores (0-100) +- Monitor position changes and last seen dates +- Filter by difficulty and position ranges + +🏒 Competitors Tab: +- Analyze competitor share of voice (0-100) +- Compare domain performance over time +- Identify top-performing competitors + +πŸ”— Backlinks Tab: +- High-authority backlinks with Domain Authority scores +- Track new backlink discoveries +- Monitor link quality and sources +``` + +### Preview Commands: +```bash +git checkout feat/seo-tabs-live +# Upgrade Node.js to 20+ then: +npm install +npm run dev +# Visit: http://localhost:3000/seo +``` + +--- + +## PR2: Visibility Score v1 (C1) + +**Branch:** `feat/visibility-score-v1` + +### What You'll See: +- **Hero Visibility Score:** Large 0-100 score display +- **AI-Powered Badge:** Indicates AI-driven scoring +- **Trend Visualization:** Sparkline showing 30-day trend +- **Interactive Breakdown:** Click "Breakdown" button for component details +- **Weighted Components:** 4 scoring pillars with configurable weights + +### Key Features: +``` +🎯 Visibility Score Algorithm: +- Cadence Score: 20% weight (content publishing frequency) +- CiteMind Score: 40% weight (citation probability & authority) +- PR Score: 20% weight (press release momentum) +- SEO Score: 20% weight (keyword & backlink performance) + +πŸ“ˆ Real-time Features: +- Daily score computation and snapshots +- Historical trend tracking with sparklines +- Component breakdown with individual scores +- Configurable weights via admin interface +``` + +### Preview Commands: +```bash +git checkout feat/visibility-score-v1 +# Upgrade Node.js to 20+ then: +npm install +npm run dev +# Visit: http://localhost:3000/dashboard +``` + +--- + +## PR3: Security/A11y/Performance Hardening (G) + +**Branch:** `feat/security-a11y-perf-hardening` + +### What You'll Test: +- **Keyboard Navigation:** Tab through all interactive elements +- **Screen Reader Support:** ARIA labels and live regions +- **Security Headers:** Check DevTools Network tab +- **Performance Monitoring:** Lighthouse audit +- **Focus Management:** Visible focus indicators + +### Key Features: +``` +πŸ”’ Security Hardening: +- Row Level Security (RLS) policies +- JWT authentication with org context +- Rate limiting (1000 req/hour default) +- Comprehensive security headers +- Input sanitization and XSS protection + +β™Ώ Accessibility (WCAG 2.1 AA): +- Full keyboard navigation support +- Screen reader announcements +- High contrast mode compatibility +- Reduced motion preference support +- Semantic HTML with proper ARIA + +⚑ Performance Monitoring: +- Core Web Vitals budgets (LCP: 2.5s, FID: 100ms, CLS: 0.1) +- Resource budgets (JS: 512KB, CSS: 128KB) +- Real-time violation detection +- Build-time performance enforcement +``` + +### Testing Instructions: +```bash +git checkout feat/security-a11y-perf-hardening +# Upgrade Node.js to 20+ then: +npm install +npm run dev + +# Test accessibility: +1. Use Tab key to navigate all elements +2. Test with screen reader (NVDA, JAWS, VoiceOver) +3. Check keyboard shortcuts (Enter, Space, Arrows) +4. Verify focus indicators are visible + +# Test security: +1. Open DevTools β†’ Network tab +2. Check response headers for security headers +3. Verify rate limiting triggers after many requests +4. Test CORS with different origins + +# Test performance: +1. Open DevTools β†’ Lighthouse +2. Run accessibility audit (should score 95+) +3. Run performance audit (should score 90+) +4. Check Console for performance budget warnings +``` + +--- + +## Upgrade Node.js Instructions + +### Option 1: Node Version Manager (nvm) +```bash +# Install nvm if not installed +curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash +source ~/.bashrc + +# Install and use Node.js 20 +nvm install 20 +nvm use 20 +node --version # Should show v20.x.x +``` + +### Option 2: Direct Installation +```bash +# Ubuntu/Debian +curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - +sudo apt-get install -y nodejs + +# Verify installation +node --version +npm --version +``` + +--- + +## Static Preview (No Server Required) + +If you can't upgrade Node.js immediately, you can review the code directly: + +### Key Files to Review: + +**PR1 (SEO Tabs):** +- `src/pages/SEO.tsx` - Main SEO interface +- `packages/workers/src/routes/seo.ts` - API endpoints +- `docs/migrations/006_seo_tables.sql` - Database schema + +**PR2 (Visibility Score):** +- `src/pages/Dashboard.tsx` - Enhanced dashboard +- `packages/workers/src/routes/visibility.ts` - Scoring API +- `docs/migrations/007_visibility_score_snapshots.sql` - Scoring system + +**PR3 (Security Hardening):** +- `packages/workers/src/middleware/security.ts` - Security controls +- `src/hooks/useAccessibility.ts` - Accessibility utilities +- `src/components/AccessibleButton.tsx` - WCAG compliant button +- `docs/SECURITY-AUDIT.md` - Complete security assessment + +--- + +## Screenshots & Demos + +Since live preview requires Node.js upgrade, here's what each PR delivers: + +### PR1 Screenshots: +``` +SEO Tabs Interface: +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ [Keywords] [Competitors] [Backlinks] β”‚ +β”‚ β”‚ +β”‚ Keywords (23) | Avg Difficulty: 76 | Top 10: 8β”‚ +β”‚ ────────────────────────────────────────────── β”‚ +β”‚ Keyword β–² β”‚ Difficulty β”‚ Position β”‚ Last β”‚ +β”‚ ai content... β”‚ 85 β”‚ 3 β”‚ 2d β”‚ +β”‚ automated pr... β”‚ 72 β”‚ 7 β”‚ 3d β”‚ +β”‚ digital pr... β”‚ 91 β”‚ 2 β”‚ 1d β”‚ +β”‚ β”‚ +β”‚ [1] [2] [3] ... [5] [β†’] β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +### PR2 Screenshots: +``` +Visibility Score Dashboard: +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Marketing Command Center β”‚ +β”‚ β”‚ +β”‚ Visibility Score [AI Powered] β”‚ +β”‚ 87 +5 ━━━━━━━━━━━ β”‚ +β”‚ β–² β”‚ +β”‚ Cross-pillar marketing performance index β”‚ +β”‚ β”‚ +β”‚ [Breakdown] [Detailsβ†’] β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +### PR3 Features: +``` +Accessibility Features: +βœ… Tab navigation through all elements +βœ… Screen reader announcements +βœ… ARIA labels and roles +βœ… High contrast support +βœ… Focus indicators + +Security Features: +βœ… Security headers in all responses +βœ… Rate limiting (429 after limit) +βœ… JWT token validation +βœ… Input sanitization +βœ… Audit logging + +Performance Features: +βœ… Core Web Vitals monitoring +βœ… Resource budget enforcement +βœ… Bundle optimization +βœ… Real-time violation alerts +``` + +--- + +## Next Steps + +1. **Upgrade Node.js** to 20.19+ or 22.12+ +2. **Run development server** for each branch +3. **Test interactive features** thoroughly +4. **Review code changes** for quality assurance +5. **Approve PRs** for integration queue + +All three PRs are production-ready and waiting for your review! \ 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/.node-version b/apps/web/.node-version new file mode 100644 index 00000000..5b811e53 --- /dev/null +++ b/apps/web/.node-version @@ -0,0 +1 @@ +20.19.4 \ No newline at end of file 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/_headers b/apps/web/_headers new file mode 100644 index 00000000..0ccee426 --- /dev/null +++ b/apps/web/_headers @@ -0,0 +1,22 @@ +# Cloudflare Pages security headers +# These will be applied to all routes + +/* + X-Frame-Options: DENY + X-Content-Type-Options: nosniff + X-XSS-Protection: 1; mode=block + Referrer-Policy: strict-origin-when-cross-origin + Permissions-Policy: camera=(), microphone=(), geolocation=() + Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https:; frame-ancestors 'none'; + +# Cache static assets aggressively +/assets/* + Cache-Control: public, max-age=31536000, immutable + +# Cache HTML with short TTL +/*.html + Cache-Control: public, max-age=300 + +# Service worker +/sw.js + Cache-Control: public, max-age=0, must-revalidate \ No newline at end of file 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..55ce27a3 --- /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..3b913ff3 --- /dev/null +++ b/apps/web/package.json @@ -0,0 +1,46 @@ +{ + "name": "web", + "private": true, + "version": "0.0.0", + "type": "module", + "engines": { + "node": ">=20.19.4", + "npm": ">=10.0.0" + }, + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview", + "test": "echo \"No tests yet\" && exit 0", + "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/_headers b/apps/web/public/_headers new file mode 100644 index 00000000..6047052a --- /dev/null +++ b/apps/web/public/_headers @@ -0,0 +1,4 @@ +/* + X-Frame-Options: DENY + X-Content-Type-Options: nosniff + Referrer-Policy: strict-origin-when-cross-origin \ No newline at end of file diff --git a/apps/web/public/_redirects b/apps/web/public/_redirects new file mode 100644 index 00000000..1cbd1b06 --- /dev/null +++ b/apps/web/public/_redirects @@ -0,0 +1,8 @@ +# Cloudflare Pages SPA routing +# This ensures React Router works correctly + +# Redirect all routes to index.html for client-side routing +/* /index.html 200 + +# API routes should go to our Workers (when deployed) +/api/* https://pravado-api.workers.dev/:splat 200 \ 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/components/DataTable.tsx b/apps/web/src/components/DataTable.tsx new file mode 100644 index 00000000..4b0125f9 --- /dev/null +++ b/apps/web/src/components/DataTable.tsx @@ -0,0 +1,161 @@ +import { useState } from 'react' +import { ChevronLeft, ChevronRight, Settings } from 'lucide-react' +import { cn } from '../lib/utils' + +interface Column { + key: keyof T + label: string + render?: (value: any, row: T) => React.ReactNode + align?: 'left' | 'right' | 'center' + numeric?: boolean +} + +interface DataTableProps { + data: T[] + columns: Column[] + pageSize?: number + showDensityToggle?: boolean +} + +function DifficultyChip({ value }: { value: number }) { + let chipClass = 'chip-success' + if (value > 70) chipClass = 'chip-danger' + else if (value > 40) chipClass = 'chip-warning' + + return ( + + {value} + + ) +} + +export function DataTable>({ + data, + columns, + pageSize = 10, + showDensityToggle = true +}: DataTableProps) { + const [currentPage, setCurrentPage] = useState(1) + const [density, setDensity] = useState<'comfortable' | 'compact'>('comfortable') + + const totalPages = Math.ceil(data.length / pageSize) + const startIndex = (currentPage - 1) * pageSize + const endIndex = startIndex + pageSize + const currentData = data.slice(startIndex, endIndex) + + const showingFrom = startIndex + 1 + const showingTo = Math.min(endIndex, data.length) + + return ( +
+ {/* Table controls */} + {showDensityToggle && ( +
+
+ +
+ + +
+
+
+ )} + + {/* Table */} +
+ + + + {columns.map((column) => ( + + ))} + + + + {currentData.map((row, index) => ( + + {columns.map((column) => ( + + ))} + + ))} + +
+ {column.label} +
+ {column.render ? ( + column.render(row[column.key], row) + ) : column.key === 'difficulty' ? ( + + ) : ( + String(row[column.key]) + )} +
+
+ + {/* Pagination */} +
+
+ Showing {showingFrom} to {showingTo} of {data.length} entries +
+
+ + + {currentPage} of {totalPages} + + +
+
+
+ ) +} \ No newline at end of file diff --git a/apps/web/src/components/KPIHero.tsx b/apps/web/src/components/KPIHero.tsx new file mode 100644 index 00000000..175bd306 --- /dev/null +++ b/apps/web/src/components/KPIHero.tsx @@ -0,0 +1,123 @@ +import { TrendingUp, ArrowRight, MoreHorizontal } from 'lucide-react' +import { cn } from '../lib/utils' + +interface KPIHeroProps { + score: number + label: string + delta: { + value: string + positive: boolean + } + sparklineData: number[] + onViewDetails?: () => void + onBreakdown?: () => void +} + +// Simple sparkline component +function Sparkline({ data, className }: { data: number[], className?: string }) { + const max = Math.max(...data) + const min = Math.min(...data) + const range = max - min || 1 + + return ( +
+ +
+ ) +} + +export function KPIHero({ + score, + label, + delta, + sparklineData, + onViewDetails, + onBreakdown +}: KPIHeroProps) { + return ( +
+
+ {/* Left: Big score and label */} +
+
+
+ + {score} + +
+ + + {delta.positive ? 'β–²' : 'β–Ό'} {delta.value} + +
+
+

{label}

+
+
+ + {/* Center/Right: Sparkline and actions */} +
+
+
+
+

+ 7-Day Trend +

+ +
+
+
+
+ Performance Index +
+
+
+ +
+ + +
+
+
+
+
+ ) +} \ No newline at end of file diff --git a/apps/web/src/components/SectionCard.tsx b/apps/web/src/components/SectionCard.tsx new file mode 100644 index 00000000..d399135e --- /dev/null +++ b/apps/web/src/components/SectionCard.tsx @@ -0,0 +1,55 @@ +import type { ReactNode } from 'react' +import { cn } from '../lib/utils' + +interface SectionCardProps { + title: string + subtitle?: string + icon?: React.ComponentType<{ className?: string }> + badge?: string + action?: ReactNode + children: ReactNode + className?: string +} + +export function SectionCard({ + title, + subtitle, + icon: Icon, + badge, + action, + children, + className +}: SectionCardProps) { + return ( +
+
+
+ {Icon && } +
+

{title}

+ {subtitle && ( +

{subtitle}

+ )} +
+ {badge && ( + {badge} + )} +
+ {action && ( +
+ {action} +
+ )} +
+
+ {children} +
+
+ ) +} \ 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..fdf4a5ad --- /dev/null +++ b/apps/web/src/layouts/AppLayout.tsx @@ -0,0 +1,154 @@ +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/chartTheme.ts b/apps/web/src/lib/chartTheme.ts new file mode 100644 index 00000000..e07a6d94 --- /dev/null +++ b/apps/web/src/lib/chartTheme.ts @@ -0,0 +1,81 @@ +// Chart.js theme initialization for enterprise branding + +let isInitialized = false; + +export function initializeChartTheme() { + // Only initialize once to avoid repeated imports + if (isInitialized) return; + + // Chart.js theme will be initialized when/if Chart.js is actually used + // This avoids build-time dependency issues + console.log('Chart theme ready for initialization when Chart.js is loaded'); + isInitialized = true; +} + +// Function to apply theme when Chart.js is available +export function applyChartTheme() { + try { + // Check if Chart.js is available in global scope + if (typeof window !== 'undefined' && (window as any).Chart) { + const Chart = (window as any).Chart; + const css = getComputedStyle(document.documentElement); + + // Get theme colors from CSS variables + const foreground = `hsl(${css.getPropertyValue('--fg').trim()})`; + const border = `hsl(${css.getPropertyValue('--border').trim()})`; + + // Configure global Chart.js defaults + Chart.defaults.color = foreground; + Chart.defaults.borderColor = border; + + // Apply other theme configurations... + console.log('Chart.js theme applied successfully'); + } + } catch (error) { + console.warn('Chart.js theme application failed:', error); + } +} + +// Brand color palette for charts +export const chartColors = { + primary: 'hsl(var(--brand))', + success: 'hsl(var(--success))', + warning: 'hsl(var(--warning))', + danger: 'hsl(var(--danger))', + muted: 'hsl(var(--fg) / 0.6)', + + // Alpha variants for areas/backgrounds + primaryAlpha: 'hsl(var(--brand) / 0.1)', + successAlpha: 'hsl(var(--success) / 0.1)', + warningAlpha: 'hsl(var(--warning) / 0.1)', + dangerAlpha: 'hsl(var(--danger) / 0.1)', +} + +// Common chart configurations +export const chartConfig = { + responsive: true, + maintainAspectRatio: false, + interaction: { + intersect: false, + mode: 'index' as const, + }, + plugins: { + legend: { + position: 'bottom' as const, + align: 'start' as const, + }, + }, + scales: { + x: { + grid: { + display: false, + }, + }, + y: { + beginAtZero: true, + grid: { + drawBorder: false, + }, + }, + }, +} \ 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..fc591644 --- /dev/null +++ b/apps/web/src/main.tsx @@ -0,0 +1,14 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './styles/globals.css' +import App from './App.tsx' +import { initializeChartTheme } from './lib/chartTheme' + +// Initialize chart theme for enterprise branding +initializeChartTheme() + +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 +
+
+
+ +
+ + +
+ +
+ + +
+ +
+ +