mirror of
https://github.com/Zie619/n8n-workflows.git
synced 2025-11-25 03:15:25 +08:00
Compare commits
2 Commits
ebcdcc4734
...
f01937f71e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f01937f71e | ||
|
|
56789e895e |
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"
|
||||
|
||||
# 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"
|
||||
|
||||
# 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,8 +199,12 @@ class WorkflowDatabase:
|
||||
workflow['trigger_type'] = trigger_type
|
||||
workflow['integrations'] = list(integrations)
|
||||
|
||||
# Generate description
|
||||
workflow['description'] = self.generate_description(workflow, trigger_type, integrations)
|
||||
# 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)
|
||||
|
||||
return workflow
|
||||
|
||||
@@ -353,7 +357,7 @@ class WorkflowDatabase:
|
||||
service_name = service_mappings.get(raw_service, raw_service.title() if raw_service else None)
|
||||
|
||||
# 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"
|
||||
parts = node_type.lower().split('.')
|
||||
for part in parts:
|
||||
@@ -366,10 +370,16 @@ class WorkflowDatabase:
|
||||
elif 'discord' in part:
|
||||
service_name = 'Discord'
|
||||
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():
|
||||
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
|
||||
break
|
||||
|
||||
@@ -530,33 +540,16 @@ class WorkflowDatabase:
|
||||
where_conditions.append("w.complexity = ?")
|
||||
params.append(complexity_filter)
|
||||
|
||||
# Use FTS search if query provided and FTS table exists
|
||||
# Use FTS search if query provided
|
||||
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
|
||||
base_query = """
|
||||
SELECT w.*, rank
|
||||
FROM workflows_fts fts
|
||||
JOIN workflows w ON w.id = fts.rowid
|
||||
WHERE workflows_fts MATCH ?
|
||||
"""
|
||||
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])
|
||||
# FTS search with ranking
|
||||
base_query = """
|
||||
SELECT w.*, rank
|
||||
FROM workflows_fts fts
|
||||
JOIN workflows w ON w.id = fts.rowid
|
||||
WHERE workflows_fts MATCH ?
|
||||
"""
|
||||
params.insert(0, query)
|
||||
else:
|
||||
# Regular query without FTS
|
||||
base_query = """
|
||||
@@ -566,10 +559,7 @@ class WorkflowDatabase:
|
||||
"""
|
||||
|
||||
if where_conditions:
|
||||
if "WHERE" in base_query:
|
||||
base_query += " AND " + " AND ".join(where_conditions)
|
||||
else:
|
||||
base_query += " WHERE " + " AND ".join(where_conditions)
|
||||
base_query += " AND " + " AND ".join(where_conditions)
|
||||
|
||||
# Count total results
|
||||
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'],
|
||||
'database': ['PostgreSQL', 'MySQL', 'MongoDB', 'Redis', 'Airtable', 'Notion'],
|
||||
'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'],
|
||||
'ecommerce': ['Shopify', 'Stripe', 'PayPal'],
|
||||
'analytics': ['Google Analytics', 'Mixpanel'],
|
||||
|
||||
Reference in New Issue
Block a user