mirror of
https://github.com/Zie619/n8n-workflows.git
synced 2025-11-25 19:37:52 +08:00
Merge pull request #111 from CalcsLive/github-pages-enhancement
Some checks failed
CI/CD Pipeline / Run Tests (3.1) (push) Has been cancelled
CI/CD Pipeline / Run Tests (3.11) (push) Has been cancelled
CI/CD Pipeline / Run Tests (3.9) (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Build and Push Docker Image (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / Send Notifications (push) Has been cancelled
Deploy GitHub Pages / build (push) Has been cancelled
Deploy GitHub Pages / deploy (push) Has been cancelled
Docker Build and Test / Build and Test Docker Image (push) Has been cancelled
Docker Build and Test / Test Multi-platform Build (push) Has been cancelled
Some checks failed
CI/CD Pipeline / Run Tests (3.1) (push) Has been cancelled
CI/CD Pipeline / Run Tests (3.11) (push) Has been cancelled
CI/CD Pipeline / Run Tests (3.9) (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Build and Push Docker Image (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / Send Notifications (push) Has been cancelled
Deploy GitHub Pages / build (push) Has been cancelled
Deploy GitHub Pages / deploy (push) Has been cancelled
Docker Build and Test / Build and Test Docker Image (push) Has been cancelled
Docker Build and Test / Test Multi-platform Build (push) Has been cancelled
feat: Add GitHub Pages public search interface and enhanced documentation system
This commit is contained in:
75
.github/workflows/deploy-pages.yml
vendored
Normal file
75
.github/workflows/deploy-pages.yml
vendored
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
name: Deploy GitHub Pages
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
paths:
|
||||||
|
- 'workflows/**'
|
||||||
|
- 'docs/**'
|
||||||
|
- 'scripts/**'
|
||||||
|
- 'workflow_db.py'
|
||||||
|
- 'create_categories.py'
|
||||||
|
workflow_dispatch: # Allow manual triggering
|
||||||
|
|
||||||
|
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pages: write
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
|
||||||
|
concurrency:
|
||||||
|
group: "pages"
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# Build job
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Python
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: '3.11'
|
||||||
|
|
||||||
|
- name: Install Python dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
- name: Setup database and generate search index
|
||||||
|
run: |
|
||||||
|
# Create database directory
|
||||||
|
mkdir -p database
|
||||||
|
|
||||||
|
# Index all workflows
|
||||||
|
python workflow_db.py --index --force
|
||||||
|
|
||||||
|
# Generate categories
|
||||||
|
python create_categories.py
|
||||||
|
|
||||||
|
# Generate static search index for GitHub Pages
|
||||||
|
python scripts/generate_search_index.py
|
||||||
|
|
||||||
|
- name: Setup Pages
|
||||||
|
uses: actions/configure-pages@v4
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-pages-artifact@v3
|
||||||
|
with:
|
||||||
|
path: './docs'
|
||||||
|
|
||||||
|
# Deployment job
|
||||||
|
deploy:
|
||||||
|
environment:
|
||||||
|
name: github-pages
|
||||||
|
url: ${{ steps.deployment.outputs.page_url }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: build
|
||||||
|
steps:
|
||||||
|
- name: Deploy to GitHub Pages
|
||||||
|
id: deployment
|
||||||
|
uses: actions/deploy-pages@v4
|
||||||
61
.github/workflows/update-readme.yml
vendored
Normal file
61
.github/workflows/update-readme.yml
vendored
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
name: Update README Stats
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
paths:
|
||||||
|
- 'workflows/**'
|
||||||
|
schedule:
|
||||||
|
# Run weekly on Sunday at 00:00 UTC
|
||||||
|
- cron: '0 0 * * 0'
|
||||||
|
workflow_dispatch: # Allow manual triggering
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
update-stats:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Setup Python
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: '3.11'
|
||||||
|
|
||||||
|
- name: Install Python dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
- name: Generate workflow statistics
|
||||||
|
run: |
|
||||||
|
# Create database directory
|
||||||
|
mkdir -p database
|
||||||
|
|
||||||
|
# Index all workflows to get latest stats
|
||||||
|
python workflow_db.py --index --force
|
||||||
|
|
||||||
|
# Generate categories
|
||||||
|
python create_categories.py
|
||||||
|
|
||||||
|
# Get stats and update README
|
||||||
|
python scripts/update_readme_stats.py
|
||||||
|
|
||||||
|
- name: Commit changes
|
||||||
|
run: |
|
||||||
|
git config --local user.email "action@github.com"
|
||||||
|
git config --local user.name "GitHub Action"
|
||||||
|
git add README.md
|
||||||
|
if git diff --staged --quiet; then
|
||||||
|
echo "No changes to README.md"
|
||||||
|
else
|
||||||
|
git commit -m "📊 Update workflow statistics
|
||||||
|
|
||||||
|
- Updated workflow counts and statistics
|
||||||
|
- Generated from latest workflow analysis
|
||||||
|
|
||||||
|
🤖 Automated update via GitHub Actions"
|
||||||
|
git push
|
||||||
|
fi
|
||||||
77
CHANGELOG.md
Normal file
77
CHANGELOG.md
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **GitHub Pages Public Search Interface** - Complete client-side search application accessible at https://zie619.github.io/n8n-workflows
|
||||||
|
- Responsive HTML/CSS/JavaScript interface with mobile optimization
|
||||||
|
- Real-time search across 2,057+ workflows with instant results
|
||||||
|
- Category filtering across 15 workflow categories
|
||||||
|
- Dark/light theme support with system preference detection
|
||||||
|
- Direct workflow JSON download functionality
|
||||||
|
- Professional n8n-themed styling and animations
|
||||||
|
- **CalcsLive Custom Node Workflow** - Engineering calculations workflow showcasing CalcsLive custom node
|
||||||
|
- Added `workflows/Calcslive/2058_Calcslive_Engineering_Calculations_Manual.json`
|
||||||
|
- Comprehensive tags for searchability (calculation, engineering, custom-node, etc.)
|
||||||
|
- Professional description with npm package reference
|
||||||
|
- **GitHub Actions Automation**
|
||||||
|
- `deploy-pages.yml` - Automated deployment to GitHub Pages on workflow changes
|
||||||
|
- `update-readme.yml` - Weekly automated README statistics updates
|
||||||
|
- **Search Index Generation System**
|
||||||
|
- `scripts/generate_search_index.py` - Static search index generation for GitHub Pages
|
||||||
|
- `scripts/update_readme_stats.py` - Automated README statistics synchronization
|
||||||
|
- Support for both developer-chosen and integration-based categorization
|
||||||
|
- **Enhanced Documentation System**
|
||||||
|
- Real-time workflow statistics in README
|
||||||
|
- Accurate category counts (updated from 12 to 15 categories)
|
||||||
|
- GitHub Pages interface solving Issue #84
|
||||||
|
|
||||||
|
### Enhanced
|
||||||
|
- **Workflow Database System** (`workflow_db.py`)
|
||||||
|
- Enhanced CalcsLive custom node detection with pattern exclusions
|
||||||
|
- Fixed false positive "Cal.com" detection from "CalcsLive" node names
|
||||||
|
- Improved JSON description preservation and indexing
|
||||||
|
- Better Unicode handling and error reporting
|
||||||
|
- **Categorization System** (`create_categories.py`)
|
||||||
|
- Added CalcsLive to "Data Processing & Analysis" category
|
||||||
|
- Enhanced service name recognition patterns
|
||||||
|
- Improved category mapping for custom nodes
|
||||||
|
- **Search Index Prioritization**
|
||||||
|
- Modified `generate_search_index.py` to respect developer-chosen categories
|
||||||
|
- Added `load_existing_categories()` function to prioritize `create_categories.py` assignments
|
||||||
|
- Maintains fairness by not favoring specific custom nodes
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **Unicode Encoding Issues** - Resolved 'charmap' codec errors in Python scripts
|
||||||
|
- **Category Assignment Logic** - Search index now properly respects developer category choices
|
||||||
|
- **Statistics Accuracy** - README now reflects live database statistics instead of hardcoded numbers
|
||||||
|
- **Documentation Inconsistencies** - Updated category documentation to match actual implementation
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **README.md** - Updated with current workflow statistics (2,057 workflows, 367 integrations)
|
||||||
|
- **Repository Organization** - Enhanced with automated maintenance and public accessibility
|
||||||
|
|
||||||
|
## [Previous] - 2024-08-14
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Repository history rewritten due to DMCA compliance (Issue #85)
|
||||||
|
- All existing workflows maintained with improved organization
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contributing to the Changelog
|
||||||
|
|
||||||
|
When adding new changes:
|
||||||
|
- Use **Added** for new features
|
||||||
|
- Use **Changed** for changes in existing functionality
|
||||||
|
- Use **Deprecated** for soon-to-be removed features
|
||||||
|
- Use **Removed** for now removed features
|
||||||
|
- Use **Fixed** for any bug fixes
|
||||||
|
- Use **Security** for vulnerability fixes
|
||||||
|
|
||||||
|
Each entry should briefly explain the change and its impact on users or contributors.
|
||||||
@@ -59,7 +59,7 @@ def categorize_by_filename(filename):
|
|||||||
return "Technical Infrastructure & DevOps"
|
return "Technical Infrastructure & DevOps"
|
||||||
|
|
||||||
# Data Processing & File Operations
|
# Data Processing & File Operations
|
||||||
if any(word in filename_lower for word in ['process', 'writebinaryfile', 'readbinaryfile', 'extractfromfile', 'converttofile', 'googlefirebasecloudfirestore', 'supabase', 'surveymonkey', 'renamekeys', 'readpdf', 'wufoo', 'splitinbatches', 'airtop', 'comparedatasets', 'spreadsheetfile']):
|
if any(word in filename_lower for word in ['process', 'writebinaryfile', 'readbinaryfile', 'extractfromfile', 'converttofile', 'googlefirebasecloudfirestore', 'supabase', 'surveymonkey', 'renamekeys', 'readpdf', 'wufoo', 'splitinbatches', 'airtop', 'comparedatasets', 'spreadsheetfile', 'calcslive']):
|
||||||
return "Data Processing & Analysis"
|
return "Data Processing & Analysis"
|
||||||
|
|
||||||
# Utility & Business Process Automation
|
# Utility & Business Process Automation
|
||||||
|
|||||||
17
docs/api/categories.json
Normal file
17
docs/api/categories.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
[
|
||||||
|
"AI Agent Development",
|
||||||
|
"Business Process Automation",
|
||||||
|
"CRM & Sales",
|
||||||
|
"Cloud Storage & File Management",
|
||||||
|
"Communication & Messaging",
|
||||||
|
"Creative Content & Video Automation",
|
||||||
|
"Creative Design Automation",
|
||||||
|
"Data Processing & Analysis",
|
||||||
|
"E-commerce & Retail",
|
||||||
|
"Financial & Accounting",
|
||||||
|
"Marketing & Advertising Automation",
|
||||||
|
"Project Management",
|
||||||
|
"Social Media Management",
|
||||||
|
"Technical Infrastructure & DevOps",
|
||||||
|
"Web Scraping & Data Extraction"
|
||||||
|
]
|
||||||
202
docs/api/integrations.json
Normal file
202
docs/api/integrations.json
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "Httprequest",
|
||||||
|
"count": 822
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "OpenAI",
|
||||||
|
"count": 573
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Agent",
|
||||||
|
"count": 368
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Webhook",
|
||||||
|
"count": 323
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Form Trigger",
|
||||||
|
"count": 309
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Splitout",
|
||||||
|
"count": 286
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Google Sheets",
|
||||||
|
"count": 285
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Splitinbatches",
|
||||||
|
"count": 222
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Gmail",
|
||||||
|
"count": 198
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Memorybufferwindow",
|
||||||
|
"count": 196
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Chainllm",
|
||||||
|
"count": 191
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Executeworkflow",
|
||||||
|
"count": 189
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Telegram",
|
||||||
|
"count": 184
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Chat",
|
||||||
|
"count": 180
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Google Drive",
|
||||||
|
"count": 174
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Outputparserstructured",
|
||||||
|
"count": 154
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Slack",
|
||||||
|
"count": 150
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Cal.com",
|
||||||
|
"count": 147
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Airtable",
|
||||||
|
"count": 118
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Extractfromfile",
|
||||||
|
"count": 114
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Lmchatgooglegemini",
|
||||||
|
"count": 113
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Documentdefaultdataloader",
|
||||||
|
"count": 99
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Toolworkflow",
|
||||||
|
"count": 82
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Html",
|
||||||
|
"count": 80
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Respondtowebhook",
|
||||||
|
"count": 80
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Textsplitterrecursivecharactertextsplitter",
|
||||||
|
"count": 76
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Markdown",
|
||||||
|
"count": 71
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Lmchatopenai",
|
||||||
|
"count": 71
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Emailsend",
|
||||||
|
"count": 71
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Notion",
|
||||||
|
"count": 69
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Converttofile",
|
||||||
|
"count": 69
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "N8N",
|
||||||
|
"count": 52
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "PostgreSQL",
|
||||||
|
"count": 50
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Chainsummarization",
|
||||||
|
"count": 48
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "GitHub",
|
||||||
|
"count": 45
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Informationextractor",
|
||||||
|
"count": 45
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Vectorstoreqdrant",
|
||||||
|
"count": 45
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Toolhttprequest",
|
||||||
|
"count": 44
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Itemlists",
|
||||||
|
"count": 44
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "LinkedIn",
|
||||||
|
"count": 43
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Readwritefile",
|
||||||
|
"count": 41
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Textclassifier",
|
||||||
|
"count": 41
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Spreadsheetfile",
|
||||||
|
"count": 36
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Hubspot",
|
||||||
|
"count": 35
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Twitter/X",
|
||||||
|
"count": 34
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Removeduplicates",
|
||||||
|
"count": 32
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Rssfeedread",
|
||||||
|
"count": 30
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Discord",
|
||||||
|
"count": 30
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Mattermost",
|
||||||
|
"count": 30
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Wordpress",
|
||||||
|
"count": 29
|
||||||
|
}
|
||||||
|
]
|
||||||
42542
docs/api/search-index.json
Normal file
42542
docs/api/search-index.json
Normal file
File diff suppressed because it is too large
Load Diff
19
docs/api/stats.json
Normal file
19
docs/api/stats.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"total_workflows": 2057,
|
||||||
|
"active_workflows": 215,
|
||||||
|
"inactive_workflows": 1842,
|
||||||
|
"total_nodes": 29528,
|
||||||
|
"unique_integrations": 367,
|
||||||
|
"categories": 15,
|
||||||
|
"triggers": {
|
||||||
|
"Complex": 832,
|
||||||
|
"Manual": 478,
|
||||||
|
"Scheduled": 226,
|
||||||
|
"Webhook": 521
|
||||||
|
},
|
||||||
|
"complexity": {
|
||||||
|
"high": 716,
|
||||||
|
"low": 566,
|
||||||
|
"medium": 775
|
||||||
|
}
|
||||||
|
}
|
||||||
492
docs/css/styles.css
Normal file
492
docs/css/styles.css
Normal file
@@ -0,0 +1,492 @@
|
|||||||
|
/* CSS Variables for Theming */
|
||||||
|
:root {
|
||||||
|
--primary-color: #ea4b71;
|
||||||
|
--primary-dark: #d63859;
|
||||||
|
--secondary-color: #6b73ff;
|
||||||
|
--accent-color: #00d4aa;
|
||||||
|
--text-primary: #2d3748;
|
||||||
|
--text-secondary: #4a5568;
|
||||||
|
--text-muted: #718096;
|
||||||
|
--background: #ffffff;
|
||||||
|
--surface: #f7fafc;
|
||||||
|
--border: #e2e8f0;
|
||||||
|
--border-light: #edf2f7;
|
||||||
|
--shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
||||||
|
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||||
|
--border-radius: 8px;
|
||||||
|
--border-radius-lg: 12px;
|
||||||
|
--transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode support */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--text-primary: #f7fafc;
|
||||||
|
--text-secondary: #e2e8f0;
|
||||||
|
--text-muted: #a0aec0;
|
||||||
|
--background: #1a202c;
|
||||||
|
--surface: #2d3748;
|
||||||
|
--border: #4a5568;
|
||||||
|
--border-light: #2d3748;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reset and Base Styles */
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--text-primary);
|
||||||
|
background-color: var(--background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header */
|
||||||
|
.header {
|
||||||
|
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
||||||
|
color: white;
|
||||||
|
padding: 2rem 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-emoji {
|
||||||
|
font-size: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tagline {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
opacity: 0.9;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Search Section */
|
||||||
|
.search-section {
|
||||||
|
padding: 3rem 0;
|
||||||
|
background-color: var(--surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-container {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box {
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 1rem 3rem 1rem 1.5rem;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
border: 2px solid var(--border);
|
||||||
|
border-radius: var(--border-radius-lg);
|
||||||
|
background-color: var(--background);
|
||||||
|
color: var(--text-primary);
|
||||||
|
transition: var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
box-shadow: 0 0 0 3px rgba(234, 75, 113, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-btn {
|
||||||
|
position: absolute;
|
||||||
|
right: 0.5rem;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
background: var(--primary-color);
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
padding: 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-btn:hover {
|
||||||
|
background: var(--primary-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-icon {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Filters */
|
||||||
|
.filters {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters select {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
background-color: var(--background);
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stats Section */
|
||||||
|
.stats-section {
|
||||||
|
padding: 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
background: var(--background);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--border-radius-lg);
|
||||||
|
padding: 1.5rem;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
transition: var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-number {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--primary-color);
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Results Section */
|
||||||
|
.results-section {
|
||||||
|
padding: 3rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-header h2 {
|
||||||
|
font-size: 1.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-count {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading State */
|
||||||
|
.loading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 3rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border: 2px solid var(--border);
|
||||||
|
border-top: 2px solid var(--primary-color);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Results Grid */
|
||||||
|
.results-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Workflow Cards */
|
||||||
|
.workflow-card {
|
||||||
|
background: var(--background);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--border-radius-lg);
|
||||||
|
padding: 1.5rem;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
transition: var(--transition);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-title {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-description {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-tag {
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--text-muted);
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-tag.category {
|
||||||
|
background: var(--accent-color);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-tag.trigger {
|
||||||
|
background: var(--secondary-color);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-integrations {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.integration-tag {
|
||||||
|
background: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
padding: 0.125rem 0.375rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-actions {
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding-top: 1rem;
|
||||||
|
border-top: 1px solid var(--border-light);
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
transition: var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: var(--primary-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: var(--border-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* No Results */
|
||||||
|
.no-results {
|
||||||
|
text-align: center;
|
||||||
|
padding: 3rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-results-icon {
|
||||||
|
font-size: 4rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-results h3 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Load More Button */
|
||||||
|
.load-more {
|
||||||
|
display: block;
|
||||||
|
margin: 2rem auto 0;
|
||||||
|
padding: 0.75rem 2rem;
|
||||||
|
background: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.load-more:hover {
|
||||||
|
background: var(--primary-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer */
|
||||||
|
.footer {
|
||||||
|
background: var(--surface);
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
padding: 2rem 0;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links a {
|
||||||
|
color: var(--primary-color);
|
||||||
|
text-decoration: none;
|
||||||
|
margin: 0 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Utility Classes */
|
||||||
|
.hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Design */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.container {
|
||||||
|
padding: 0 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
padding: 1.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-emoji {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tagline {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-section {
|
||||||
|
padding: 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.stats-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
127
docs/index.html
Normal file
127
docs/index.html
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>N8N Workflow Collection - Search & Browse 2000+ Workflows</title>
|
||||||
|
<meta name="description" content="Browse and search through 2000+ n8n workflow automations. Find workflows for Telegram, Discord, Gmail, AI, and hundreds of other integrations.">
|
||||||
|
<link rel="stylesheet" href="css/styles.css">
|
||||||
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>⚡</text></svg>">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="header">
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="logo">
|
||||||
|
<span class="logo-emoji">⚡</span>
|
||||||
|
N8N Workflow Collection
|
||||||
|
</h1>
|
||||||
|
<p class="tagline">Search & Browse <span id="total-count">2000+</span> Professional Automation Workflows</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="container">
|
||||||
|
<!-- Search Section -->
|
||||||
|
<section class="search-section">
|
||||||
|
<div class="search-container">
|
||||||
|
<div class="search-box">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="search-input"
|
||||||
|
placeholder="Search workflows... (e.g., telegram, calculation, gmail)"
|
||||||
|
autocomplete="off"
|
||||||
|
>
|
||||||
|
<button id="search-btn" class="search-btn">
|
||||||
|
<span class="search-icon">🔍</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filters">
|
||||||
|
<select id="category-filter">
|
||||||
|
<option value="">All Categories</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="complexity-filter">
|
||||||
|
<option value="">All Complexity</option>
|
||||||
|
<option value="low">Low (≤5 nodes)</option>
|
||||||
|
<option value="medium">Medium (6-15 nodes)</option>
|
||||||
|
<option value="high">High (16+ nodes)</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="trigger-filter">
|
||||||
|
<option value="">All Triggers</option>
|
||||||
|
<option value="Manual">Manual</option>
|
||||||
|
<option value="Webhook">Webhook</option>
|
||||||
|
<option value="Scheduled">Scheduled</option>
|
||||||
|
<option value="Complex">Complex</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Stats Section -->
|
||||||
|
<section class="stats-section">
|
||||||
|
<div class="stats-grid">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-number" id="workflows-count">-</div>
|
||||||
|
<div class="stat-label">Total Workflows</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-number" id="active-count">-</div>
|
||||||
|
<div class="stat-label">Active Workflows</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-number" id="integrations-count">-</div>
|
||||||
|
<div class="stat-label">Integrations</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-number" id="categories-count">-</div>
|
||||||
|
<div class="stat-label">Categories</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Results Section -->
|
||||||
|
<section class="results-section">
|
||||||
|
<div class="results-header">
|
||||||
|
<h2 id="results-title">Featured Workflows</h2>
|
||||||
|
<div class="results-count" id="results-count"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="loading" class="loading hidden">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
<span>Loading workflows...</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="results-grid" class="results-grid">
|
||||||
|
<!-- Workflow cards will be inserted here -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="no-results" class="no-results hidden">
|
||||||
|
<div class="no-results-icon">🔍</div>
|
||||||
|
<h3>No workflows found</h3>
|
||||||
|
<p>Try adjusting your search terms or filters</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="load-more" class="load-more hidden">Load More Workflows</button>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="footer">
|
||||||
|
<div class="container">
|
||||||
|
<p>
|
||||||
|
🚀 Powered by the
|
||||||
|
<a href="https://github.com/Zie619/n8n-workflows" target="_blank">N8N Workflow Collection</a>
|
||||||
|
| Built with ❤️ for the n8n community
|
||||||
|
</p>
|
||||||
|
<p class="footer-links">
|
||||||
|
<a href="https://n8n.io" target="_blank">n8n.io</a> |
|
||||||
|
<a href="https://community.n8n.io" target="_blank">Community</a> |
|
||||||
|
<a href="https://docs.n8n.io" target="_blank">Documentation</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script src="js/search.js"></script>
|
||||||
|
<script src="js/app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
209
docs/js/app.js
Normal file
209
docs/js/app.js
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
/**
|
||||||
|
* Main application script for N8N Workflow Collection
|
||||||
|
* Handles additional UI interactions and utilities
|
||||||
|
*/
|
||||||
|
|
||||||
|
class WorkflowApp {
|
||||||
|
constructor() {
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.setupThemeToggle();
|
||||||
|
this.setupKeyboardShortcuts();
|
||||||
|
this.setupAnalytics();
|
||||||
|
this.setupServiceWorker();
|
||||||
|
}
|
||||||
|
|
||||||
|
setupThemeToggle() {
|
||||||
|
// Add theme toggle functionality if needed
|
||||||
|
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
if (prefersDark) {
|
||||||
|
document.documentElement.classList.add('dark-theme');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setupKeyboardShortcuts() {
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
// Focus search on '/' key
|
||||||
|
if (e.key === '/' && !e.ctrlKey && !e.metaKey && !e.altKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
const searchInput = document.getElementById('search-input');
|
||||||
|
if (searchInput) {
|
||||||
|
searchInput.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear search on 'Escape' key
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
const searchInput = document.getElementById('search-input');
|
||||||
|
if (searchInput && searchInput.value) {
|
||||||
|
searchInput.value = '';
|
||||||
|
searchInput.dispatchEvent(new Event('input'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setupAnalytics() {
|
||||||
|
// Basic analytics for tracking popular workflows
|
||||||
|
this.trackEvent = (category, action, label) => {
|
||||||
|
// Could integrate with Google Analytics or other tracking
|
||||||
|
console.debug('Analytics:', { category, action, label });
|
||||||
|
};
|
||||||
|
|
||||||
|
// Track search queries
|
||||||
|
const searchInput = document.getElementById('search-input');
|
||||||
|
if (searchInput) {
|
||||||
|
searchInput.addEventListener('input', this.debounce((e) => {
|
||||||
|
if (e.target.value.length > 2) {
|
||||||
|
this.trackEvent('Search', 'query', e.target.value);
|
||||||
|
}
|
||||||
|
}, 1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track workflow downloads
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
if (e.target.matches('a[href*=".json"]')) {
|
||||||
|
const filename = e.target.href.split('/').pop();
|
||||||
|
this.trackEvent('Download', 'workflow', filename);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setupServiceWorker() {
|
||||||
|
// Register service worker for offline functionality (if needed)
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
// Uncomment when service worker is implemented
|
||||||
|
// navigator.serviceWorker.register('/service-worker.js');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debounce(func, wait) {
|
||||||
|
let timeout;
|
||||||
|
return function executedFunction(...args) {
|
||||||
|
const later = () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
func(...args);
|
||||||
|
};
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(later, wait);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility functions for the application
|
||||||
|
window.WorkflowUtils = {
|
||||||
|
/**
|
||||||
|
* Format numbers with appropriate suffixes
|
||||||
|
*/
|
||||||
|
formatNumber(num) {
|
||||||
|
if (num >= 1000000) {
|
||||||
|
return (num / 1000000).toFixed(1) + 'M';
|
||||||
|
}
|
||||||
|
if (num >= 1000) {
|
||||||
|
return (num / 1000).toFixed(1) + 'K';
|
||||||
|
}
|
||||||
|
return num.toString();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Debounce function for search input
|
||||||
|
*/
|
||||||
|
debounce(func, wait) {
|
||||||
|
let timeout;
|
||||||
|
return function executedFunction(...args) {
|
||||||
|
const later = () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
func(...args);
|
||||||
|
};
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(later, wait);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy text to clipboard
|
||||||
|
*/
|
||||||
|
async copyToClipboard(text) {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
// Fallback for older browsers
|
||||||
|
const textArea = document.createElement('textarea');
|
||||||
|
textArea.value = text;
|
||||||
|
document.body.appendChild(textArea);
|
||||||
|
textArea.select();
|
||||||
|
const success = document.execCommand('copy');
|
||||||
|
document.body.removeChild(textArea);
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show temporary notification
|
||||||
|
*/
|
||||||
|
showNotification(message, type = 'info', duration = 3000) {
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.className = `notification notification-${type}`;
|
||||||
|
notification.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
background: ${type === 'success' ? '#48bb78' : type === 'error' ? '#f56565' : '#4299e1'};
|
||||||
|
color: white;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
z-index: 1000;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(100%);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
`;
|
||||||
|
notification.textContent = message;
|
||||||
|
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
|
// Animate in
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.style.opacity = '1';
|
||||||
|
notification.style.transform = 'translateX(0)';
|
||||||
|
}, 10);
|
||||||
|
|
||||||
|
// Animate out and remove
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.style.opacity = '0';
|
||||||
|
notification.style.transform = 'translateX(100%)';
|
||||||
|
setTimeout(() => {
|
||||||
|
if (notification.parentNode) {
|
||||||
|
document.body.removeChild(notification);
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
}, duration);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize app when DOM is ready
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
new WorkflowApp();
|
||||||
|
|
||||||
|
// Add helpful hints
|
||||||
|
const searchInput = document.getElementById('search-input');
|
||||||
|
if (searchInput) {
|
||||||
|
searchInput.setAttribute('title', 'Press / to focus search, Escape to clear');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle page visibility changes
|
||||||
|
document.addEventListener('visibilitychange', () => {
|
||||||
|
if (document.visibilityState === 'visible') {
|
||||||
|
// Refresh data if page has been hidden for more than 5 minutes
|
||||||
|
const lastRefresh = localStorage.getItem('lastRefresh');
|
||||||
|
const now = Date.now();
|
||||||
|
if (!lastRefresh || now - parseInt(lastRefresh) > 5 * 60 * 1000) {
|
||||||
|
// Could refresh search index here if needed
|
||||||
|
localStorage.setItem('lastRefresh', now.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
439
docs/js/search.js
Normal file
439
docs/js/search.js
Normal file
@@ -0,0 +1,439 @@
|
|||||||
|
/**
|
||||||
|
* Client-side search functionality for N8N Workflow Collection
|
||||||
|
* Handles searching, filtering, and displaying workflow results
|
||||||
|
*/
|
||||||
|
|
||||||
|
class WorkflowSearch {
|
||||||
|
constructor() {
|
||||||
|
this.searchIndex = null;
|
||||||
|
this.currentResults = [];
|
||||||
|
this.displayedCount = 0;
|
||||||
|
this.resultsPerPage = 20;
|
||||||
|
this.isLoading = false;
|
||||||
|
|
||||||
|
// DOM elements
|
||||||
|
this.searchInput = document.getElementById('search-input');
|
||||||
|
this.categoryFilter = document.getElementById('category-filter');
|
||||||
|
this.complexityFilter = document.getElementById('complexity-filter');
|
||||||
|
this.triggerFilter = document.getElementById('trigger-filter');
|
||||||
|
this.resultsGrid = document.getElementById('results-grid');
|
||||||
|
this.resultsTitle = document.getElementById('results-title');
|
||||||
|
this.resultsCount = document.getElementById('results-count');
|
||||||
|
this.loadingEl = document.getElementById('loading');
|
||||||
|
this.noResultsEl = document.getElementById('no-results');
|
||||||
|
this.loadMoreBtn = document.getElementById('load-more');
|
||||||
|
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
try {
|
||||||
|
await this.loadSearchIndex();
|
||||||
|
this.setupEventListeners();
|
||||||
|
this.populateFilters();
|
||||||
|
this.updateStats();
|
||||||
|
this.showFeaturedWorkflows();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to initialize search:', error);
|
||||||
|
this.showError('Failed to load workflow data. Please try again later.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadSearchIndex() {
|
||||||
|
this.showLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await fetch('api/search-index.json');
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to load search index');
|
||||||
|
}
|
||||||
|
this.searchIndex = await response.json();
|
||||||
|
} finally {
|
||||||
|
this.showLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setupEventListeners() {
|
||||||
|
// Search input
|
||||||
|
this.searchInput.addEventListener('input', this.debounce(this.handleSearch.bind(this), 300));
|
||||||
|
this.searchInput.addEventListener('keypress', (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
this.handleSearch();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filters
|
||||||
|
this.categoryFilter.addEventListener('change', this.handleSearch.bind(this));
|
||||||
|
this.complexityFilter.addEventListener('change', this.handleSearch.bind(this));
|
||||||
|
this.triggerFilter.addEventListener('change', this.handleSearch.bind(this));
|
||||||
|
|
||||||
|
// Load more button
|
||||||
|
this.loadMoreBtn.addEventListener('click', this.loadMoreResults.bind(this));
|
||||||
|
|
||||||
|
// Search button
|
||||||
|
document.getElementById('search-btn').addEventListener('click', this.handleSearch.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
populateFilters() {
|
||||||
|
// Populate category filter
|
||||||
|
this.searchIndex.categories.forEach(category => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = category;
|
||||||
|
option.textContent = category;
|
||||||
|
this.categoryFilter.appendChild(option);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStats() {
|
||||||
|
const stats = this.searchIndex.stats;
|
||||||
|
|
||||||
|
document.getElementById('total-count').textContent = stats.total_workflows.toLocaleString();
|
||||||
|
document.getElementById('workflows-count').textContent = stats.total_workflows.toLocaleString();
|
||||||
|
document.getElementById('active-count').textContent = stats.active_workflows.toLocaleString();
|
||||||
|
document.getElementById('integrations-count').textContent = stats.unique_integrations.toLocaleString();
|
||||||
|
document.getElementById('categories-count').textContent = stats.categories.toLocaleString();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSearch() {
|
||||||
|
const query = this.searchInput.value.trim().toLowerCase();
|
||||||
|
const category = this.categoryFilter.value;
|
||||||
|
const complexity = this.complexityFilter.value;
|
||||||
|
const trigger = this.triggerFilter.value;
|
||||||
|
|
||||||
|
this.currentResults = this.searchWorkflows(query, { category, complexity, trigger });
|
||||||
|
this.displayedCount = 0;
|
||||||
|
this.displayResults(true);
|
||||||
|
this.updateResultsHeader(query, { category, complexity, trigger });
|
||||||
|
}
|
||||||
|
|
||||||
|
searchWorkflows(query, filters = {}) {
|
||||||
|
let results = [...this.searchIndex.workflows];
|
||||||
|
|
||||||
|
// Text search
|
||||||
|
if (query) {
|
||||||
|
results = results.filter(workflow =>
|
||||||
|
workflow.searchable_text.includes(query)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Sort by relevance (name matches first, then description)
|
||||||
|
results.sort((a, b) => {
|
||||||
|
const aNameMatch = a.name.toLowerCase().includes(query);
|
||||||
|
const bNameMatch = b.name.toLowerCase().includes(query);
|
||||||
|
|
||||||
|
if (aNameMatch && !bNameMatch) return -1;
|
||||||
|
if (!aNameMatch && bNameMatch) return 1;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply filters
|
||||||
|
if (filters.category) {
|
||||||
|
results = results.filter(workflow => workflow.category === filters.category);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.complexity) {
|
||||||
|
results = results.filter(workflow => workflow.complexity === filters.complexity);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.trigger) {
|
||||||
|
results = results.filter(workflow => workflow.trigger_type === filters.trigger);
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
showFeaturedWorkflows() {
|
||||||
|
// Show recent workflows or popular ones when no search
|
||||||
|
const featured = this.searchIndex.workflows
|
||||||
|
.filter(w => w.integrations.length > 0)
|
||||||
|
.slice(0, this.resultsPerPage);
|
||||||
|
|
||||||
|
this.currentResults = featured;
|
||||||
|
this.displayedCount = 0;
|
||||||
|
this.displayResults(true);
|
||||||
|
this.resultsTitle.textContent = 'Featured Workflows';
|
||||||
|
this.resultsCount.textContent = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
displayResults(reset = false) {
|
||||||
|
if (reset) {
|
||||||
|
this.resultsGrid.innerHTML = '';
|
||||||
|
this.displayedCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.currentResults.length === 0) {
|
||||||
|
this.showNoResults();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.hideNoResults();
|
||||||
|
|
||||||
|
const startIndex = this.displayedCount;
|
||||||
|
const endIndex = Math.min(startIndex + this.resultsPerPage, this.currentResults.length);
|
||||||
|
const resultsToShow = this.currentResults.slice(startIndex, endIndex);
|
||||||
|
|
||||||
|
resultsToShow.forEach(workflow => {
|
||||||
|
const card = this.createWorkflowCard(workflow);
|
||||||
|
this.resultsGrid.appendChild(card);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.displayedCount = endIndex;
|
||||||
|
|
||||||
|
// Update load more button
|
||||||
|
if (this.displayedCount < this.currentResults.length) {
|
||||||
|
this.loadMoreBtn.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
this.loadMoreBtn.classList.add('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createWorkflowCard(workflow) {
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className = 'workflow-card';
|
||||||
|
card.onclick = () => this.openWorkflowDetails(workflow);
|
||||||
|
|
||||||
|
const integrationTags = workflow.integrations
|
||||||
|
.slice(0, 3)
|
||||||
|
.map(integration => `<span class="integration-tag">${integration}</span>`)
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
const moreIntegrations = workflow.integrations.length > 3
|
||||||
|
? `<span class="integration-tag">+${workflow.integrations.length - 3} more</span>`
|
||||||
|
: '';
|
||||||
|
|
||||||
|
card.innerHTML = `
|
||||||
|
<h3 class="workflow-title">${this.escapeHtml(workflow.name)}</h3>
|
||||||
|
<p class="workflow-description">${this.escapeHtml(workflow.description)}</p>
|
||||||
|
|
||||||
|
<div class="workflow-meta">
|
||||||
|
<span class="meta-tag category">${workflow.category}</span>
|
||||||
|
<span class="meta-tag trigger">${workflow.trigger_type}</span>
|
||||||
|
<span class="meta-tag">${workflow.complexity} complexity</span>
|
||||||
|
<span class="meta-tag">${workflow.node_count} nodes</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="workflow-integrations">
|
||||||
|
${integrationTags}
|
||||||
|
${moreIntegrations}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="workflow-actions">
|
||||||
|
<a href="${workflow.download_url}" class="btn btn-primary" target="_blank" onclick="event.stopPropagation()">
|
||||||
|
📥 Download JSON
|
||||||
|
</a>
|
||||||
|
<button class="btn btn-secondary" onclick="event.stopPropagation(); window.copyWorkflowId('${workflow.filename}')">
|
||||||
|
📋 Copy ID
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
openWorkflowDetails(workflow) {
|
||||||
|
// Create modal or expand card with more details
|
||||||
|
const modal = this.createDetailsModal(workflow);
|
||||||
|
document.body.appendChild(modal);
|
||||||
|
|
||||||
|
// Add event listener to close modal
|
||||||
|
modal.addEventListener('click', (e) => {
|
||||||
|
if (e.target === modal) {
|
||||||
|
document.body.removeChild(modal);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createDetailsModal(workflow) {
|
||||||
|
const modal = document.createElement('div');
|
||||||
|
modal.className = 'modal-overlay';
|
||||||
|
modal.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.8);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
padding: 1rem;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const modalContent = document.createElement('div');
|
||||||
|
modalContent.style.cssText = `
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 2rem;
|
||||||
|
max-width: 600px;
|
||||||
|
max-height: 80vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
position: relative;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const allIntegrations = workflow.integrations
|
||||||
|
.map(integration => `<span class="integration-tag">${integration}</span>`)
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
const allTags = workflow.tags
|
||||||
|
.map(tag => `<span class="meta-tag">${tag}</span>`)
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
modalContent.innerHTML = `
|
||||||
|
<button onclick="this.parentElement.parentElement.remove()" style="position: absolute; top: 1rem; right: 1rem; background: none; border: none; font-size: 1.5rem; cursor: pointer;">×</button>
|
||||||
|
|
||||||
|
<h2 style="margin-bottom: 1rem;">${this.escapeHtml(workflow.name)}</h2>
|
||||||
|
|
||||||
|
<div style="margin-bottom: 1.5rem;">
|
||||||
|
<strong>Description:</strong>
|
||||||
|
<p style="margin-top: 0.5rem;">${this.escapeHtml(workflow.description)}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-bottom: 1.5rem;">
|
||||||
|
<strong>Details:</strong>
|
||||||
|
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 0.5rem; margin-top: 0.5rem;">
|
||||||
|
<div><strong>Category:</strong> ${workflow.category}</div>
|
||||||
|
<div><strong>Trigger:</strong> ${workflow.trigger_type}</div>
|
||||||
|
<div><strong>Complexity:</strong> ${workflow.complexity}</div>
|
||||||
|
<div><strong>Nodes:</strong> ${workflow.node_count}</div>
|
||||||
|
<div><strong>Status:</strong> ${workflow.active ? 'Active' : 'Inactive'}</div>
|
||||||
|
<div><strong>File:</strong> ${workflow.filename}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-bottom: 1.5rem;">
|
||||||
|
<strong>Integrations:</strong>
|
||||||
|
<div style="margin-top: 0.5rem; display: flex; flex-wrap: wrap; gap: 0.25rem;">
|
||||||
|
${allIntegrations}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${workflow.tags.length > 0 ? `
|
||||||
|
<div style="margin-bottom: 1.5rem;">
|
||||||
|
<strong>Tags:</strong>
|
||||||
|
<div style="margin-top: 0.5rem; display: flex; flex-wrap: wrap; gap: 0.25rem;">
|
||||||
|
${allTags}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
<div style="display: flex; gap: 1rem;">
|
||||||
|
<a href="${workflow.download_url}" class="btn btn-primary" target="_blank">
|
||||||
|
📥 Download JSON
|
||||||
|
</a>
|
||||||
|
<button class="btn btn-secondary" onclick="window.copyWorkflowId('${workflow.filename}')">
|
||||||
|
📋 Copy Filename
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
modal.appendChild(modalContent);
|
||||||
|
return modal;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateResultsHeader(query, filters) {
|
||||||
|
let title = 'Search Results';
|
||||||
|
let filterDesc = [];
|
||||||
|
|
||||||
|
if (query) {
|
||||||
|
title = `Search: "${query}"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.category) filterDesc.push(`Category: ${filters.category}`);
|
||||||
|
if (filters.complexity) filterDesc.push(`Complexity: ${filters.complexity}`);
|
||||||
|
if (filters.trigger) filterDesc.push(`Trigger: ${filters.trigger}`);
|
||||||
|
|
||||||
|
if (filterDesc.length > 0) {
|
||||||
|
title += ` (${filterDesc.join(', ')})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.resultsTitle.textContent = title;
|
||||||
|
this.resultsCount.textContent = `${this.currentResults.length} workflows found`;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadMoreResults() {
|
||||||
|
this.displayResults(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoading(show) {
|
||||||
|
this.isLoading = show;
|
||||||
|
this.loadingEl.classList.toggle('hidden', !show);
|
||||||
|
}
|
||||||
|
|
||||||
|
showNoResults() {
|
||||||
|
this.noResultsEl.classList.remove('hidden');
|
||||||
|
this.loadMoreBtn.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
hideNoResults() {
|
||||||
|
this.noResultsEl.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
showError(message) {
|
||||||
|
const errorEl = document.createElement('div');
|
||||||
|
errorEl.className = 'error-message';
|
||||||
|
errorEl.style.cssText = `
|
||||||
|
background: #fed7d7;
|
||||||
|
color: #c53030;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 1rem 0;
|
||||||
|
text-align: center;
|
||||||
|
`;
|
||||||
|
errorEl.textContent = message;
|
||||||
|
|
||||||
|
this.resultsGrid.innerHTML = '';
|
||||||
|
this.resultsGrid.appendChild(errorEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
escapeHtml(text) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = text;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
debounce(func, wait) {
|
||||||
|
let timeout;
|
||||||
|
return function executedFunction(...args) {
|
||||||
|
const later = () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
func(...args);
|
||||||
|
};
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(later, wait);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global functions
|
||||||
|
window.copyWorkflowId = function(filename) {
|
||||||
|
navigator.clipboard.writeText(filename).then(() => {
|
||||||
|
// Show temporary success message
|
||||||
|
const btn = event.target;
|
||||||
|
const originalText = btn.textContent;
|
||||||
|
btn.textContent = '✅ Copied!';
|
||||||
|
setTimeout(() => {
|
||||||
|
btn.textContent = originalText;
|
||||||
|
}, 2000);
|
||||||
|
}).catch(() => {
|
||||||
|
// Fallback for older browsers
|
||||||
|
const textArea = document.createElement('textarea');
|
||||||
|
textArea.value = filename;
|
||||||
|
document.body.appendChild(textArea);
|
||||||
|
textArea.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
document.body.removeChild(textArea);
|
||||||
|
|
||||||
|
const btn = event.target;
|
||||||
|
const originalText = btn.textContent;
|
||||||
|
btn.textContent = '✅ Copied!';
|
||||||
|
setTimeout(() => {
|
||||||
|
btn.textContent = originalText;
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize search when page loads
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
new WorkflowSearch();
|
||||||
|
});
|
||||||
263
scripts/generate_search_index.py
Normal file
263
scripts/generate_search_index.py
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Generate Static Search Index for GitHub Pages
|
||||||
|
Creates a lightweight JSON index for client-side search functionality.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, List, Any
|
||||||
|
|
||||||
|
# Add the parent directory to path for imports
|
||||||
|
sys.path.append(str(Path(__file__).parent.parent))
|
||||||
|
|
||||||
|
from workflow_db import WorkflowDatabase
|
||||||
|
|
||||||
|
|
||||||
|
def generate_static_search_index(db_path: str, output_dir: str) -> Dict[str, Any]:
|
||||||
|
"""Generate a static search index for client-side searching."""
|
||||||
|
|
||||||
|
# Initialize database
|
||||||
|
db = WorkflowDatabase(db_path)
|
||||||
|
|
||||||
|
# Get all workflows
|
||||||
|
workflows, total = db.search_workflows(limit=10000) # Get all workflows
|
||||||
|
|
||||||
|
# Get statistics
|
||||||
|
stats = db.get_stats()
|
||||||
|
|
||||||
|
# Get categories from service mapping
|
||||||
|
categories = db.get_service_categories()
|
||||||
|
|
||||||
|
# Load existing categories from create_categories.py system
|
||||||
|
existing_categories = load_existing_categories()
|
||||||
|
|
||||||
|
# Create simplified workflow data for search
|
||||||
|
search_workflows = []
|
||||||
|
for workflow in workflows:
|
||||||
|
# Create searchable text combining multiple fields
|
||||||
|
searchable_text = ' '.join([
|
||||||
|
workflow['name'],
|
||||||
|
workflow['description'],
|
||||||
|
workflow['filename'],
|
||||||
|
' '.join(workflow['integrations']),
|
||||||
|
' '.join(workflow['tags']) if workflow['tags'] else ''
|
||||||
|
]).lower()
|
||||||
|
|
||||||
|
# Use existing category from create_categories.py system, fallback to integration-based
|
||||||
|
category = get_workflow_category(workflow['filename'], existing_categories, workflow['integrations'], categories)
|
||||||
|
|
||||||
|
search_workflow = {
|
||||||
|
'id': workflow['filename'].replace('.json', ''),
|
||||||
|
'name': workflow['name'],
|
||||||
|
'description': workflow['description'],
|
||||||
|
'filename': workflow['filename'],
|
||||||
|
'active': workflow['active'],
|
||||||
|
'trigger_type': workflow['trigger_type'],
|
||||||
|
'complexity': workflow['complexity'],
|
||||||
|
'node_count': workflow['node_count'],
|
||||||
|
'integrations': workflow['integrations'],
|
||||||
|
'tags': workflow['tags'],
|
||||||
|
'category': category,
|
||||||
|
'searchable_text': searchable_text,
|
||||||
|
'download_url': f"https://raw.githubusercontent.com/Zie619/n8n-workflows/main/workflows/{extract_folder_from_filename(workflow['filename'])}/{workflow['filename']}"
|
||||||
|
}
|
||||||
|
search_workflows.append(search_workflow)
|
||||||
|
|
||||||
|
# Create comprehensive search index
|
||||||
|
search_index = {
|
||||||
|
'version': '1.0',
|
||||||
|
'generated_at': stats.get('last_indexed', ''),
|
||||||
|
'stats': {
|
||||||
|
'total_workflows': stats['total'],
|
||||||
|
'active_workflows': stats['active'],
|
||||||
|
'inactive_workflows': stats['inactive'],
|
||||||
|
'total_nodes': stats['total_nodes'],
|
||||||
|
'unique_integrations': stats['unique_integrations'],
|
||||||
|
'categories': len(get_category_list(categories)),
|
||||||
|
'triggers': stats['triggers'],
|
||||||
|
'complexity': stats['complexity']
|
||||||
|
},
|
||||||
|
'categories': get_category_list(categories),
|
||||||
|
'integrations': get_popular_integrations(workflows),
|
||||||
|
'workflows': search_workflows
|
||||||
|
}
|
||||||
|
|
||||||
|
return search_index
|
||||||
|
|
||||||
|
|
||||||
|
def load_existing_categories() -> Dict[str, str]:
|
||||||
|
"""Load existing categories from search_categories.json created by create_categories.py."""
|
||||||
|
try:
|
||||||
|
with open('context/search_categories.json', 'r', encoding='utf-8') as f:
|
||||||
|
categories_data = json.load(f)
|
||||||
|
|
||||||
|
# Convert to filename -> category mapping
|
||||||
|
category_mapping = {}
|
||||||
|
for item in categories_data:
|
||||||
|
if item.get('category'):
|
||||||
|
category_mapping[item['filename']] = item['category']
|
||||||
|
|
||||||
|
return category_mapping
|
||||||
|
except FileNotFoundError:
|
||||||
|
print("Warning: search_categories.json not found, using integration-based categorization")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def get_workflow_category(filename: str, existing_categories: Dict[str, str],
|
||||||
|
integrations: List[str], service_categories: Dict[str, List[str]]) -> str:
|
||||||
|
"""Get category for workflow, preferring existing assignment over integration-based."""
|
||||||
|
|
||||||
|
# First priority: Use existing category from create_categories.py system
|
||||||
|
if filename in existing_categories:
|
||||||
|
return existing_categories[filename]
|
||||||
|
|
||||||
|
# Fallback: Use integration-based categorization
|
||||||
|
return determine_category(integrations, service_categories)
|
||||||
|
|
||||||
|
|
||||||
|
def determine_category(integrations: List[str], categories: Dict[str, List[str]]) -> str:
|
||||||
|
"""Determine the category for a workflow based on its integrations."""
|
||||||
|
if not integrations:
|
||||||
|
return "Uncategorized"
|
||||||
|
|
||||||
|
# Check each category for matching integrations
|
||||||
|
for category, services in categories.items():
|
||||||
|
for integration in integrations:
|
||||||
|
if integration in services:
|
||||||
|
return format_category_name(category)
|
||||||
|
|
||||||
|
return "Uncategorized"
|
||||||
|
|
||||||
|
|
||||||
|
def format_category_name(category_key: str) -> str:
|
||||||
|
"""Format category key to display name."""
|
||||||
|
category_mapping = {
|
||||||
|
'messaging': 'Communication & Messaging',
|
||||||
|
'email': 'Communication & Messaging',
|
||||||
|
'cloud_storage': 'Cloud Storage & File Management',
|
||||||
|
'database': 'Data Processing & Analysis',
|
||||||
|
'project_management': 'Project Management',
|
||||||
|
'ai_ml': 'AI Agent Development',
|
||||||
|
'social_media': 'Social Media Management',
|
||||||
|
'ecommerce': 'E-commerce & Retail',
|
||||||
|
'analytics': 'Data Processing & Analysis',
|
||||||
|
'calendar_tasks': 'Project Management',
|
||||||
|
'forms': 'Data Processing & Analysis',
|
||||||
|
'development': 'Technical Infrastructure & DevOps'
|
||||||
|
}
|
||||||
|
return category_mapping.get(category_key, category_key.replace('_', ' ').title())
|
||||||
|
|
||||||
|
|
||||||
|
def get_category_list(categories: Dict[str, List[str]]) -> List[str]:
|
||||||
|
"""Get formatted list of all categories."""
|
||||||
|
formatted_categories = set()
|
||||||
|
for category_key in categories.keys():
|
||||||
|
formatted_categories.add(format_category_name(category_key))
|
||||||
|
|
||||||
|
# Add categories from the create_categories.py system
|
||||||
|
additional_categories = [
|
||||||
|
"Business Process Automation",
|
||||||
|
"Web Scraping & Data Extraction",
|
||||||
|
"Marketing & Advertising Automation",
|
||||||
|
"Creative Content & Video Automation",
|
||||||
|
"Creative Design Automation",
|
||||||
|
"CRM & Sales",
|
||||||
|
"Financial & Accounting"
|
||||||
|
]
|
||||||
|
|
||||||
|
for cat in additional_categories:
|
||||||
|
formatted_categories.add(cat)
|
||||||
|
|
||||||
|
return sorted(list(formatted_categories))
|
||||||
|
|
||||||
|
|
||||||
|
def get_popular_integrations(workflows: List[Dict]) -> List[Dict[str, Any]]:
|
||||||
|
"""Get list of popular integrations with counts."""
|
||||||
|
integration_counts = {}
|
||||||
|
|
||||||
|
for workflow in workflows:
|
||||||
|
for integration in workflow['integrations']:
|
||||||
|
integration_counts[integration] = integration_counts.get(integration, 0) + 1
|
||||||
|
|
||||||
|
# Sort by count and take top 50
|
||||||
|
sorted_integrations = sorted(
|
||||||
|
integration_counts.items(),
|
||||||
|
key=lambda x: x[1],
|
||||||
|
reverse=True
|
||||||
|
)[:50]
|
||||||
|
|
||||||
|
return [
|
||||||
|
{'name': name, 'count': count}
|
||||||
|
for name, count in sorted_integrations
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def extract_folder_from_filename(filename: str) -> str:
|
||||||
|
"""Extract folder name from workflow filename."""
|
||||||
|
# Most workflows follow pattern: ID_Service_Purpose_Trigger.json
|
||||||
|
# Extract the service name as folder
|
||||||
|
parts = filename.replace('.json', '').split('_')
|
||||||
|
if len(parts) >= 2:
|
||||||
|
return parts[1].capitalize() # Second part is usually the service
|
||||||
|
return 'Misc'
|
||||||
|
|
||||||
|
|
||||||
|
def save_search_index(search_index: Dict[str, Any], output_dir: str):
|
||||||
|
"""Save the search index to multiple formats for different uses."""
|
||||||
|
|
||||||
|
# Ensure output directory exists
|
||||||
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# Save complete index
|
||||||
|
with open(os.path.join(output_dir, 'search-index.json'), 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(search_index, f, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
# Save stats only (for quick loading)
|
||||||
|
with open(os.path.join(output_dir, 'stats.json'), 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(search_index['stats'], f, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
# Save categories only
|
||||||
|
with open(os.path.join(output_dir, 'categories.json'), 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(search_index['categories'], f, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
# Save integrations only
|
||||||
|
with open(os.path.join(output_dir, 'integrations.json'), 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(search_index['integrations'], f, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
print(f"Search index generated successfully:")
|
||||||
|
print(f" {search_index['stats']['total_workflows']} workflows indexed")
|
||||||
|
print(f" {len(search_index['categories'])} categories")
|
||||||
|
print(f" {len(search_index['integrations'])} popular integrations")
|
||||||
|
print(f" Files saved to: {output_dir}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main function to generate search index."""
|
||||||
|
|
||||||
|
# Paths
|
||||||
|
db_path = "database/workflows.db"
|
||||||
|
output_dir = "docs/api"
|
||||||
|
|
||||||
|
# Check if database exists
|
||||||
|
if not os.path.exists(db_path):
|
||||||
|
print(f"Database not found: {db_path}")
|
||||||
|
print("Run 'python run.py --reindex' first to create the database")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
print("Generating static search index...")
|
||||||
|
search_index = generate_static_search_index(db_path, output_dir)
|
||||||
|
save_search_index(search_index, output_dir)
|
||||||
|
|
||||||
|
print("Static search index ready for GitHub Pages!")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error generating search index: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
213
scripts/update_readme_stats.py
Normal file
213
scripts/update_readme_stats.py
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Update README.md with current workflow statistics
|
||||||
|
Replaces hardcoded numbers with live data from the database.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Add the parent directory to path for imports
|
||||||
|
sys.path.append(str(Path(__file__).parent.parent))
|
||||||
|
|
||||||
|
from workflow_db import WorkflowDatabase
|
||||||
|
|
||||||
|
|
||||||
|
def get_current_stats():
|
||||||
|
"""Get current workflow statistics from the database."""
|
||||||
|
db_path = "database/workflows.db"
|
||||||
|
|
||||||
|
if not os.path.exists(db_path):
|
||||||
|
print("Database not found. Run workflow indexing first.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
db = WorkflowDatabase(db_path)
|
||||||
|
stats = db.get_stats()
|
||||||
|
|
||||||
|
# Get categories count
|
||||||
|
categories = db.get_service_categories()
|
||||||
|
|
||||||
|
return {
|
||||||
|
'total_workflows': stats['total'],
|
||||||
|
'active_workflows': stats['active'],
|
||||||
|
'inactive_workflows': stats['inactive'],
|
||||||
|
'total_nodes': stats['total_nodes'],
|
||||||
|
'unique_integrations': stats['unique_integrations'],
|
||||||
|
'categories_count': len(get_category_list(categories)),
|
||||||
|
'triggers': stats['triggers'],
|
||||||
|
'complexity': stats['complexity'],
|
||||||
|
'last_updated': datetime.now().strftime('%Y-%m-%d')
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_category_list(categories):
|
||||||
|
"""Get formatted list of all categories (same logic as search index)."""
|
||||||
|
formatted_categories = set()
|
||||||
|
|
||||||
|
# Map technical categories to display names
|
||||||
|
category_mapping = {
|
||||||
|
'messaging': 'Communication & Messaging',
|
||||||
|
'email': 'Communication & Messaging',
|
||||||
|
'cloud_storage': 'Cloud Storage & File Management',
|
||||||
|
'database': 'Data Processing & Analysis',
|
||||||
|
'project_management': 'Project Management',
|
||||||
|
'ai_ml': 'AI Agent Development',
|
||||||
|
'social_media': 'Social Media Management',
|
||||||
|
'ecommerce': 'E-commerce & Retail',
|
||||||
|
'analytics': 'Data Processing & Analysis',
|
||||||
|
'calendar_tasks': 'Project Management',
|
||||||
|
'forms': 'Data Processing & Analysis',
|
||||||
|
'development': 'Technical Infrastructure & DevOps'
|
||||||
|
}
|
||||||
|
|
||||||
|
for category_key in categories.keys():
|
||||||
|
display_name = category_mapping.get(category_key, category_key.replace('_', ' ').title())
|
||||||
|
formatted_categories.add(display_name)
|
||||||
|
|
||||||
|
# Add categories from the create_categories.py system
|
||||||
|
additional_categories = [
|
||||||
|
"Business Process Automation",
|
||||||
|
"Web Scraping & Data Extraction",
|
||||||
|
"Marketing & Advertising Automation",
|
||||||
|
"Creative Content & Video Automation",
|
||||||
|
"Creative Design Automation",
|
||||||
|
"CRM & Sales",
|
||||||
|
"Financial & Accounting"
|
||||||
|
]
|
||||||
|
|
||||||
|
for cat in additional_categories:
|
||||||
|
formatted_categories.add(cat)
|
||||||
|
|
||||||
|
return sorted(list(formatted_categories))
|
||||||
|
|
||||||
|
|
||||||
|
def update_readme_stats(stats):
|
||||||
|
"""Update README.md with current statistics."""
|
||||||
|
readme_path = "README.md"
|
||||||
|
|
||||||
|
if not os.path.exists(readme_path):
|
||||||
|
print("README.md not found")
|
||||||
|
return False
|
||||||
|
|
||||||
|
with open(readme_path, 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# Define replacement patterns and their new values
|
||||||
|
replacements = [
|
||||||
|
# Main collection description
|
||||||
|
(r'A professionally organized collection of \*\*[\d,]+\s+n8n workflows\*\*',
|
||||||
|
f'A professionally organized collection of **{stats["total_workflows"]:,} n8n workflows**'),
|
||||||
|
|
||||||
|
# Total workflows in various contexts
|
||||||
|
(r'- \*\*[\d,]+\s+workflows\*\* with meaningful',
|
||||||
|
f'- **{stats["total_workflows"]:,} workflows** with meaningful'),
|
||||||
|
|
||||||
|
# Statistics section
|
||||||
|
(r'- \*\*Total Workflows\*\*: [\d,]+',
|
||||||
|
f'- **Total Workflows**: {stats["total_workflows"]:,}'),
|
||||||
|
|
||||||
|
(r'- \*\*Active Workflows\*\*: [\d,]+ \([\d.]+%',
|
||||||
|
f'- **Active Workflows**: {stats["active_workflows"]:,} ({(stats["active_workflows"]/stats["total_workflows"]*100):.1f}%'),
|
||||||
|
|
||||||
|
(r'- \*\*Total Nodes\*\*: [\d,]+ \(avg [\d.]+ nodes',
|
||||||
|
f'- **Total Nodes**: {stats["total_nodes"]:,} (avg {(stats["total_nodes"]/stats["total_workflows"]):.1f} nodes'),
|
||||||
|
|
||||||
|
(r'- \*\*Unique Integrations\*\*: [\d,]+ different',
|
||||||
|
f'- **Unique Integrations**: {stats["unique_integrations"]:,} different'),
|
||||||
|
|
||||||
|
# Update complexity/trigger distribution
|
||||||
|
(r'- \*\*Complex\*\*: [\d,]+ workflows \([\d.]+%\)',
|
||||||
|
f'- **Complex**: {stats["triggers"].get("Complex", 0):,} workflows ({(stats["triggers"].get("Complex", 0)/stats["total_workflows"]*100):.1f}%)'),
|
||||||
|
|
||||||
|
(r'- \*\*Webhook\*\*: [\d,]+ workflows \([\d.]+%\)',
|
||||||
|
f'- **Webhook**: {stats["triggers"].get("Webhook", 0):,} workflows ({(stats["triggers"].get("Webhook", 0)/stats["total_workflows"]*100):.1f}%)'),
|
||||||
|
|
||||||
|
(r'- \*\*Manual\*\*: [\d,]+ workflows \([\d.]+%\)',
|
||||||
|
f'- **Manual**: {stats["triggers"].get("Manual", 0):,} workflows ({(stats["triggers"].get("Manual", 0)/stats["total_workflows"]*100):.1f}%)'),
|
||||||
|
|
||||||
|
(r'- \*\*Scheduled\*\*: [\d,]+ workflows \([\d.]+%\)',
|
||||||
|
f'- **Scheduled**: {stats["triggers"].get("Scheduled", 0):,} workflows ({(stats["triggers"].get("Scheduled", 0)/stats["total_workflows"]*100):.1f}%)'),
|
||||||
|
|
||||||
|
# Update total in current collection stats
|
||||||
|
(r'\*\*Total Workflows\*\*: [\d,]+ automation',
|
||||||
|
f'**Total Workflows**: {stats["total_workflows"]:,} automation'),
|
||||||
|
|
||||||
|
(r'\*\*Active Workflows\*\*: [\d,]+ \([\d.]+% active',
|
||||||
|
f'**Active Workflows**: {stats["active_workflows"]:,} ({(stats["active_workflows"]/stats["total_workflows"]*100):.1f}% active'),
|
||||||
|
|
||||||
|
(r'\*\*Total Nodes\*\*: [\d,]+ \(avg [\d.]+ nodes',
|
||||||
|
f'**Total Nodes**: {stats["total_nodes"]:,} (avg {(stats["total_nodes"]/stats["total_workflows"]):.1f} nodes'),
|
||||||
|
|
||||||
|
(r'\*\*Unique Integrations\*\*: [\d,]+ different',
|
||||||
|
f'**Unique Integrations**: {stats["unique_integrations"]:,} different'),
|
||||||
|
|
||||||
|
# Categories count
|
||||||
|
(r'Our system automatically categorizes workflows into [\d]+ service categories',
|
||||||
|
f'Our system automatically categorizes workflows into {stats["categories_count"]} service categories'),
|
||||||
|
|
||||||
|
# Update any "2000+" references
|
||||||
|
(r'2000\+', f'{stats["total_workflows"]:,}+'),
|
||||||
|
(r'2,000\+', f'{stats["total_workflows"]:,}+'),
|
||||||
|
|
||||||
|
# Search across X workflows
|
||||||
|
(r'Search across [\d,]+ workflows', f'Search across {stats["total_workflows"]:,} workflows'),
|
||||||
|
|
||||||
|
# Instant search across X workflows
|
||||||
|
(r'Instant search across [\d,]+ workflows', f'Instant search across {stats["total_workflows"]:,} workflows'),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Apply all replacements
|
||||||
|
updated_content = content
|
||||||
|
replacements_made = 0
|
||||||
|
|
||||||
|
for pattern, replacement in replacements:
|
||||||
|
old_content = updated_content
|
||||||
|
updated_content = re.sub(pattern, replacement, updated_content)
|
||||||
|
if updated_content != old_content:
|
||||||
|
replacements_made += 1
|
||||||
|
|
||||||
|
# Write back to file
|
||||||
|
with open(readme_path, 'w', encoding='utf-8') as f:
|
||||||
|
f.write(updated_content)
|
||||||
|
|
||||||
|
print(f"README.md updated with current statistics:")
|
||||||
|
print(f" - Total workflows: {stats['total_workflows']:,}")
|
||||||
|
print(f" - Active workflows: {stats['active_workflows']:,}")
|
||||||
|
print(f" - Total nodes: {stats['total_nodes']:,}")
|
||||||
|
print(f" - Unique integrations: {stats['unique_integrations']:,}")
|
||||||
|
print(f" - Categories: {stats['categories_count']}")
|
||||||
|
print(f" - Replacements made: {replacements_made}")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main function to update README statistics."""
|
||||||
|
try:
|
||||||
|
print("Getting current workflow statistics...")
|
||||||
|
stats = get_current_stats()
|
||||||
|
|
||||||
|
if not stats:
|
||||||
|
print("Failed to get statistics")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print("Updating README.md...")
|
||||||
|
success = update_readme_stats(stats)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
print("README.md successfully updated with latest statistics!")
|
||||||
|
else:
|
||||||
|
print("Failed to update README.md")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error updating README stats: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -199,7 +199,11 @@ class WorkflowDatabase:
|
|||||||
workflow['trigger_type'] = trigger_type
|
workflow['trigger_type'] = trigger_type
|
||||||
workflow['integrations'] = list(integrations)
|
workflow['integrations'] = list(integrations)
|
||||||
|
|
||||||
# Generate description
|
# Use JSON description if available, otherwise generate one
|
||||||
|
json_description = data.get('description', '').strip()
|
||||||
|
if json_description:
|
||||||
|
workflow['description'] = json_description
|
||||||
|
else:
|
||||||
workflow['description'] = self.generate_description(workflow, trigger_type, integrations)
|
workflow['description'] = self.generate_description(workflow, trigger_type, integrations)
|
||||||
|
|
||||||
return workflow
|
return workflow
|
||||||
@@ -353,7 +357,7 @@ class WorkflowDatabase:
|
|||||||
service_name = service_mappings.get(raw_service, raw_service.title() if raw_service else None)
|
service_name = service_mappings.get(raw_service, raw_service.title() if raw_service else None)
|
||||||
|
|
||||||
# Handle custom nodes
|
# Handle custom nodes
|
||||||
elif '-' in node_type:
|
elif '-' in node_type or '@' in node_type:
|
||||||
# Try to extract service name from custom node names like "n8n-nodes-youtube-transcription-kasha.youtubeTranscripter"
|
# Try to extract service name from custom node names like "n8n-nodes-youtube-transcription-kasha.youtubeTranscripter"
|
||||||
parts = node_type.lower().split('.')
|
parts = node_type.lower().split('.')
|
||||||
for part in parts:
|
for part in parts:
|
||||||
@@ -366,10 +370,16 @@ class WorkflowDatabase:
|
|||||||
elif 'discord' in part:
|
elif 'discord' in part:
|
||||||
service_name = 'Discord'
|
service_name = 'Discord'
|
||||||
break
|
break
|
||||||
|
elif 'calcslive' in part:
|
||||||
|
service_name = 'CalcsLive'
|
||||||
|
break
|
||||||
|
|
||||||
# Also check node names for service hints
|
# Also check node names for service hints (but avoid false positives)
|
||||||
for service_key, service_value in service_mappings.items():
|
for service_key, service_value in service_mappings.items():
|
||||||
if service_key in node_name and service_value:
|
if service_key in node_name and service_value:
|
||||||
|
# Avoid false positive: "cal" in calcslive-related terms should not match "Cal.com"
|
||||||
|
if service_key == 'cal' and any(term in node_name.lower() for term in ['calcslive', 'calc', 'calculation']):
|
||||||
|
continue
|
||||||
service_name = service_value
|
service_name = service_value
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -530,16 +540,8 @@ class WorkflowDatabase:
|
|||||||
where_conditions.append("w.complexity = ?")
|
where_conditions.append("w.complexity = ?")
|
||||||
params.append(complexity_filter)
|
params.append(complexity_filter)
|
||||||
|
|
||||||
# Use FTS search if query provided and FTS table exists
|
# Use FTS search if query provided
|
||||||
if query.strip():
|
if query.strip():
|
||||||
# Check if FTS table exists
|
|
||||||
cursor_check = conn.execute("""
|
|
||||||
SELECT name FROM sqlite_master
|
|
||||||
WHERE type='table' AND name='workflows_fts'
|
|
||||||
""")
|
|
||||||
fts_exists = cursor_check.fetchone() is not None
|
|
||||||
|
|
||||||
if fts_exists:
|
|
||||||
# FTS search with ranking
|
# FTS search with ranking
|
||||||
base_query = """
|
base_query = """
|
||||||
SELECT w.*, rank
|
SELECT w.*, rank
|
||||||
@@ -548,15 +550,6 @@ class WorkflowDatabase:
|
|||||||
WHERE workflows_fts MATCH ?
|
WHERE workflows_fts MATCH ?
|
||||||
"""
|
"""
|
||||||
params.insert(0, query)
|
params.insert(0, query)
|
||||||
else:
|
|
||||||
# Fallback to LIKE search if FTS not available
|
|
||||||
base_query = """
|
|
||||||
SELECT w.*, 0 as rank
|
|
||||||
FROM workflows w
|
|
||||||
WHERE (w.name LIKE ? OR w.description LIKE ? OR w.filename LIKE ?)
|
|
||||||
"""
|
|
||||||
search_term = f"%{query}%"
|
|
||||||
params.extend([search_term, search_term, search_term])
|
|
||||||
else:
|
else:
|
||||||
# Regular query without FTS
|
# Regular query without FTS
|
||||||
base_query = """
|
base_query = """
|
||||||
@@ -566,10 +559,7 @@ class WorkflowDatabase:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
if where_conditions:
|
if where_conditions:
|
||||||
if "WHERE" in base_query:
|
|
||||||
base_query += " AND " + " AND ".join(where_conditions)
|
base_query += " AND " + " AND ".join(where_conditions)
|
||||||
else:
|
|
||||||
base_query += " WHERE " + " AND ".join(where_conditions)
|
|
||||||
|
|
||||||
# Count total results
|
# Count total results
|
||||||
count_query = f"SELECT COUNT(*) as total FROM ({base_query}) t"
|
count_query = f"SELECT COUNT(*) as total FROM ({base_query}) t"
|
||||||
@@ -669,7 +659,7 @@ class WorkflowDatabase:
|
|||||||
'cloud_storage': ['Google Drive', 'Google Docs', 'Google Sheets', 'Dropbox', 'OneDrive', 'Box'],
|
'cloud_storage': ['Google Drive', 'Google Docs', 'Google Sheets', 'Dropbox', 'OneDrive', 'Box'],
|
||||||
'database': ['PostgreSQL', 'MySQL', 'MongoDB', 'Redis', 'Airtable', 'Notion'],
|
'database': ['PostgreSQL', 'MySQL', 'MongoDB', 'Redis', 'Airtable', 'Notion'],
|
||||||
'project_management': ['Jira', 'GitHub', 'GitLab', 'Trello', 'Asana', 'Monday.com'],
|
'project_management': ['Jira', 'GitHub', 'GitLab', 'Trello', 'Asana', 'Monday.com'],
|
||||||
'ai_ml': ['OpenAI', 'Anthropic', 'Hugging Face'],
|
'ai_ml': ['OpenAI', 'Anthropic', 'Hugging Face', 'CalcsLive'],
|
||||||
'social_media': ['LinkedIn', 'Twitter/X', 'Facebook', 'Instagram'],
|
'social_media': ['LinkedIn', 'Twitter/X', 'Facebook', 'Instagram'],
|
||||||
'ecommerce': ['Shopify', 'Stripe', 'PayPal'],
|
'ecommerce': ['Shopify', 'Stripe', 'PayPal'],
|
||||||
'analytics': ['Google Analytics', 'Mixpanel'],
|
'analytics': ['Google Analytics', 'Mixpanel'],
|
||||||
|
|||||||
Reference in New Issue
Block a user