generated from StartanAICompany/ai-recruit-site-template
Initial commit
This commit is contained in:
11
.dockerignore
Normal file
11
.dockerignore
Normal file
@@ -0,0 +1,11 @@
|
||||
node_modules
|
||||
npm-debug.log
|
||||
.git
|
||||
.gitignore
|
||||
.env
|
||||
.env.local
|
||||
README.md
|
||||
.DS_Store
|
||||
*.log
|
||||
.vscode
|
||||
.idea
|
||||
113
.env.example
Normal file
113
.env.example
Normal file
@@ -0,0 +1,113 @@
|
||||
# ===================================
|
||||
# AI Recruitment Site - Configuration
|
||||
# ===================================
|
||||
# Copy this file to .env and customize for your company
|
||||
|
||||
# ===================================
|
||||
# Company Information
|
||||
# ===================================
|
||||
COMPANY_NAME=Your Recruitment Firm
|
||||
COMPANY_TAGLINE=Finding the Perfect Match for Your Career
|
||||
COMPANY_DESCRIPTION=We specialize in connecting talented professionals with exceptional opportunities across various industries.
|
||||
|
||||
# ===================================
|
||||
# Company Branding (Colors)
|
||||
# ===================================
|
||||
# Primary brand color (e.g., #2563EB for blue)
|
||||
PRIMARY_COLOR=#2563EB
|
||||
# Accent/success color (e.g., #059669 for green)
|
||||
ACCENT_COLOR=#059669
|
||||
# Dark/secondary color (e.g., #1E293B)
|
||||
DARK_COLOR=#1E293B
|
||||
|
||||
# ===================================
|
||||
# Contact Information
|
||||
# ===================================
|
||||
CONTACT_EMAIL=info@yourcompany.com
|
||||
CONTACT_PHONE=+1 (555) 123-4567
|
||||
CONTACT_ADDRESS=123 Business St, Suite 100, City, State 12345
|
||||
|
||||
# Social Media Links (leave empty to hide)
|
||||
SOCIAL_LINKEDIN=https://linkedin.com/company/yourcompany
|
||||
SOCIAL_TWITTER=https://twitter.com/yourcompany
|
||||
SOCIAL_FACEBOOK=
|
||||
|
||||
# ===================================
|
||||
# Deployment Configuration
|
||||
# ===================================
|
||||
# Your custom subdomain (e.g., 'anna' becomes annarecruit.startanaicompany.com)
|
||||
SUBDOMAIN=yourname
|
||||
|
||||
# Your Gitea username and repository name
|
||||
GITEA_USERNAME=your-gitea-username
|
||||
GITEA_REPO_NAME=ai-recruit-site-template
|
||||
|
||||
# ===================================
|
||||
# Application Settings
|
||||
# ===================================
|
||||
NODE_ENV=production
|
||||
PORT=3000
|
||||
|
||||
# Session secret (will be auto-generated if empty)
|
||||
SESSION_SECRET=
|
||||
|
||||
# ===================================
|
||||
# Database Configuration
|
||||
# ===================================
|
||||
# PostgreSQL connection details
|
||||
DB_HOST=postgres
|
||||
DB_PORT=5432
|
||||
DB_NAME=recruitment
|
||||
DB_USER=postgres
|
||||
# Database password (will be auto-generated if empty)
|
||||
DB_PASSWORD=
|
||||
|
||||
# ===================================
|
||||
# API Tokens (for deployment script)
|
||||
# ===================================
|
||||
# REQUIRED: Get your SAAC_API_KEY from: https://apps.startanaicompany.com/api/v1/register
|
||||
# Set as environment variable: export SAAC_API_KEY="your_key_here"
|
||||
#
|
||||
# REQUIRED: GITEA_API_TOKEN needed for automatic deployments (webhook setup)
|
||||
# Get from: https://git.startanaicompany.com → Settings → Applications → Generate New Token
|
||||
# Set as environment variable: export GITEA_API_TOKEN="your_token_here"
|
||||
|
||||
# ===================================
|
||||
# Feature Configuration
|
||||
# ===================================
|
||||
# Maximum CV file size in MB
|
||||
MAX_CV_SIZE_MB=5
|
||||
|
||||
# Allowed CV file types (comma-separated)
|
||||
ALLOWED_CV_TYPES=application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document
|
||||
|
||||
# ===================================
|
||||
# About Page Content
|
||||
# ===================================
|
||||
ABOUT_MISSION=Our mission is to bridge the gap between exceptional talent and outstanding opportunities.
|
||||
ABOUT_VISION=We envision a world where every professional finds their perfect career match.
|
||||
ABOUT_VALUES=Integrity, Excellence, Innovation, Partnership
|
||||
|
||||
# ===================================
|
||||
# Services Offered (comma-separated)
|
||||
# ===================================
|
||||
SERVICES_LIST=Executive Search,Contract Staffing,Permanent Placement,Career Consulting,Talent Assessment,Industry Expertise
|
||||
|
||||
# ===================================
|
||||
# Contact Page Settings
|
||||
# ===================================
|
||||
# Email address where contact form submissions are sent
|
||||
CONTACT_FORM_RECIPIENT=info@yourcompany.com
|
||||
|
||||
# Business hours
|
||||
BUSINESS_HOURS=Monday - Friday: 9:00 AM - 6:00 PM
|
||||
|
||||
# ===================================
|
||||
# Email Configuration (Optional)
|
||||
# ===================================
|
||||
# If you want to send email notifications
|
||||
SMTP_HOST=
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=
|
||||
SMTP_PASSWORD=
|
||||
SMTP_FROM=noreply@yourcompany.com
|
||||
18
.gitignore
vendored
Normal file
18
.gitignore
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
node_modules/
|
||||
.env
|
||||
.env.local
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.DS_Store
|
||||
*.log
|
||||
.vscode/
|
||||
.idea/
|
||||
deploy_key
|
||||
cc/
|
||||
|
||||
# Deployment keys and scripts
|
||||
deploy_key*
|
||||
deploy.sh
|
||||
deploy-to-apps.sh
|
||||
.saac.json
|
||||
328
DEPLOYMENT_GUIDE.md
Normal file
328
DEPLOYMENT_GUIDE.md
Normal file
@@ -0,0 +1,328 @@
|
||||
# Deployment Guide - Using SAAC API
|
||||
|
||||
This guide explains how to deploy your recruitment site using the SAAC API at `apps.startanaicompany.com`.
|
||||
|
||||
## What is the SAAC API?
|
||||
|
||||
The SAAC API is a secure gateway that allows you to deploy applications to StartAnAiCompany infrastructure. Each user gets their own API key and can only manage their own applications.
|
||||
|
||||
**Benefits:**
|
||||
- ✅ No need for infrastructure credentials
|
||||
- ✅ Automatic isolation - you can only see/manage your apps
|
||||
- ✅ Rate limiting prevents abuse
|
||||
- ✅ Audit logging for security
|
||||
- ✅ Automatic webhooks for continuous deployment
|
||||
|
||||
## Quick Start (3 Steps)
|
||||
|
||||
### Step 1: Register for API Key
|
||||
|
||||
Register once to get your unique API key:
|
||||
|
||||
```bash
|
||||
curl -X POST https://apps.startanaicompany.com/api/v1/register \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"email": "your@email.com",
|
||||
"gitea_username": "your-gitea-username"
|
||||
}'
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"user_id": "uuid",
|
||||
"email": "your@email.com",
|
||||
"api_key": "cw_abc123xyz789...",
|
||||
"message": "Save your API key securely. It will not be shown again."
|
||||
}
|
||||
```
|
||||
|
||||
⚠️ **IMPORTANT**: Save this API key! It will only be shown once.
|
||||
|
||||
**Save it to your environment:**
|
||||
```bash
|
||||
export SAAC_API_KEY="cw_abc123xyz789..."
|
||||
|
||||
# Add to your shell profile for persistence
|
||||
echo 'export SAAC_API_KEY="cw_abc123xyz789..."' >> ~/.bashrc
|
||||
# or for zsh:
|
||||
echo 'export SAAC_API_KEY="cw_abc123xyz789..."' >> ~/.zshrc
|
||||
```
|
||||
|
||||
### Step 2: Customize Your Site
|
||||
|
||||
```bash
|
||||
# Copy environment template
|
||||
cp .env.example .env
|
||||
|
||||
# Edit with your company information
|
||||
nano .env
|
||||
```
|
||||
|
||||
**Minimum required in `.env`:**
|
||||
```bash
|
||||
# Company Information
|
||||
COMPANY_NAME="Your Recruitment Firm"
|
||||
COMPANY_TAGLINE="Your Tagline Here"
|
||||
COMPANY_DESCRIPTION="Your company description"
|
||||
|
||||
# Branding
|
||||
PRIMARY_COLOR=#2563EB
|
||||
ACCENT_COLOR=#059669
|
||||
DARK_COLOR=#1E293B
|
||||
|
||||
# Contact
|
||||
CONTACT_EMAIL=info@yourcompany.com
|
||||
CONTACT_PHONE=+1 (555) 123-4567
|
||||
CONTACT_ADDRESS=123 Business St, City, State 12345
|
||||
|
||||
# Deployment
|
||||
SUBDOMAIN=yourname
|
||||
GITEA_USERNAME=your-gitea-username
|
||||
GITEA_REPO_NAME=your-recruit-site
|
||||
```
|
||||
|
||||
### Step 3: Deploy
|
||||
|
||||
```bash
|
||||
# Copy the deployment script
|
||||
cp deploy-to-apps.example.sh deploy-to-apps.sh
|
||||
chmod +x deploy-to-apps.sh
|
||||
|
||||
# Run deployment
|
||||
./deploy-to-apps.sh
|
||||
```
|
||||
|
||||
The script will:
|
||||
1. Create your application on StartAnAiCompany platform
|
||||
2. Configure domain (`{subdomain}recruit.startanaicompany.com`)
|
||||
3. Set up automatic deployments via webhook
|
||||
4. Trigger initial build
|
||||
|
||||
**Your site will be live in 2-3 minutes!**
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### List Your Applications
|
||||
|
||||
```bash
|
||||
curl -H "X-API-Key: $SAAC_API_KEY" \
|
||||
https://apps.startanaicompany.com/api/v1/applications
|
||||
```
|
||||
|
||||
### View Application Details
|
||||
|
||||
```bash
|
||||
curl -H "X-API-Key: $SAAC_API_KEY" \
|
||||
https://apps.startanaicompany.com/api/v1/applications/YOUR_APP_UUID
|
||||
```
|
||||
|
||||
### View Deployment Logs
|
||||
|
||||
```bash
|
||||
curl -H "X-API-Key: $SAAC_API_KEY" \
|
||||
"https://apps.startanaicompany.com/api/v1/applications/YOUR_APP_UUID/logs?tail=100"
|
||||
```
|
||||
|
||||
### Trigger Manual Deployment
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "X-API-Key: $SAAC_API_KEY" \
|
||||
https://apps.startanaicompany.com/api/v1/applications/YOUR_APP_UUID/deploy
|
||||
```
|
||||
|
||||
### Update Environment Variables
|
||||
|
||||
```bash
|
||||
curl -X PATCH \
|
||||
-H "X-API-Key: $SAAC_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
https://apps.startanaicompany.com/api/v1/applications/YOUR_APP_UUID/env \
|
||||
-d '{
|
||||
"variables": {
|
||||
"COMPANY_NAME": "New Company Name",
|
||||
"PRIMARY_COLOR": "#FF0000"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
After updating env vars, redeploy:
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "X-API-Key: $SAAC_API_KEY" \
|
||||
https://apps.startanaicompany.com/api/v1/applications/YOUR_APP_UUID/deploy
|
||||
```
|
||||
|
||||
### Delete Application
|
||||
|
||||
```bash
|
||||
curl -X DELETE \
|
||||
-H "X-API-Key: $SAAC_API_KEY" \
|
||||
https://apps.startanaicompany.com/api/v1/applications/YOUR_APP_UUID
|
||||
```
|
||||
|
||||
⚠️ **Warning**: This permanently deletes the application and all its data!
|
||||
|
||||
## Automatic Deployments
|
||||
|
||||
Once deployed, every push to your repository's `master` branch will automatically:
|
||||
1. Trigger webhook to SAAC API
|
||||
2. SAAC authenticates with your API key
|
||||
3. SAAC deploys your updated code
|
||||
4. Your site updates in 2-3 minutes
|
||||
|
||||
**No manual intervention needed!**
|
||||
|
||||
## DNS Configuration
|
||||
|
||||
After deployment, configure DNS in Cloudflare:
|
||||
|
||||
1. Go to Cloudflare DNS settings for `startanaicompany.com`
|
||||
2. Add CNAME record:
|
||||
- **Name**: `{your-subdomain}recruit` (e.g., `annarecruit`)
|
||||
- **Target**: `apps.startanaicompany.com`
|
||||
- **Proxy status**: Proxied (orange cloud)
|
||||
3. Save
|
||||
|
||||
DNS propagation takes 2-5 minutes.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "API key required" Error
|
||||
|
||||
Make sure you've exported your API key:
|
||||
```bash
|
||||
echo $SAAC_API_KEY
|
||||
# Should show: cw_abc123xyz...
|
||||
```
|
||||
|
||||
If empty, export it again:
|
||||
```bash
|
||||
export SAAC_API_KEY="your_api_key_here"
|
||||
```
|
||||
|
||||
### "Application limit reached" Error
|
||||
|
||||
You've hit your quota (default: 50 apps). Delete unused apps:
|
||||
```bash
|
||||
# List your apps
|
||||
curl -H "X-API-Key: $SAAC_API_KEY" \
|
||||
https://apps.startanaicompany.com/api/v1/applications
|
||||
|
||||
# Delete an app
|
||||
curl -X DELETE -H "X-API-Key: $SAAC_API_KEY" \
|
||||
https://apps.startanaicompany.com/api/v1/applications/APP_UUID
|
||||
```
|
||||
|
||||
### "Rate limit exceeded" Error
|
||||
|
||||
You're making too many requests. Wait and try again:
|
||||
- General API: 100 requests / 15 minutes
|
||||
- App creation: 10 / hour
|
||||
- Deployments: 30 / hour
|
||||
|
||||
### Deployment Logs Show Errors
|
||||
|
||||
Check logs for specific errors:
|
||||
```bash
|
||||
curl -H "X-API-Key: $SAAC_API_KEY" \
|
||||
"https://apps.startanaicompany.com/api/v1/applications/YOUR_APP_UUID/logs?tail=200"
|
||||
```
|
||||
|
||||
Common issues:
|
||||
- **Database migration failed**: Check migration SQL syntax
|
||||
- **Port 3000 already in use**: Normal, the platform handles this
|
||||
- **Git clone failed**: Check repository URL and permissions
|
||||
|
||||
### Site Not Loading
|
||||
|
||||
1. **Check deployment status**:
|
||||
```bash
|
||||
curl -H "X-API-Key: $SAAC_API_KEY" \
|
||||
https://apps.startanaicompany.com/api/v1/applications/YOUR_APP_UUID
|
||||
```
|
||||
|
||||
2. **Check DNS**:
|
||||
```bash
|
||||
dig {subdomain}recruit.startanaicompany.com
|
||||
```
|
||||
Should point to Cloudflare or StartAnAiCompany server.
|
||||
|
||||
3. **Wait**: Initial deployment takes 2-3 minutes.
|
||||
|
||||
4. **Check Cloudflare**: Ensure CNAME record is correct and proxied.
|
||||
|
||||
### Lost API Key
|
||||
|
||||
If you lost your API key, regenerate it:
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "X-API-Key: $OLD_SAAC_API_KEY" \
|
||||
https://apps.startanaicompany.com/api/v1/users/regenerate-key
|
||||
```
|
||||
|
||||
⚠️ **Warning**: Old API key will be immediately revoked!
|
||||
|
||||
If you can't access the old key, contact support.
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
1. **Never commit API keys to git**
|
||||
- The `.env` file is gitignored
|
||||
- deployment-info.txt is gitignored
|
||||
- Keep API keys in environment variables only
|
||||
|
||||
2. **Use environment variables**
|
||||
```bash
|
||||
# Good
|
||||
export SAAC_API_KEY="cw_..."
|
||||
|
||||
# Bad - don't hardcode in scripts
|
||||
SAAC_API_KEY="cw_..." ./deploy.sh
|
||||
```
|
||||
|
||||
3. **Rotate keys periodically**
|
||||
- Regenerate your API key every 3-6 months
|
||||
- Update all environments where it's used
|
||||
|
||||
4. **Don't share API keys**
|
||||
- Each user should have their own API key
|
||||
- Don't share deployment-info.txt files
|
||||
|
||||
## Rate Limits
|
||||
|
||||
To prevent abuse, the API has rate limits:
|
||||
|
||||
| Operation | Limit |
|
||||
|-----------|-------|
|
||||
| General API calls | 100 / 15 min |
|
||||
| User registration | 5 / hour |
|
||||
| Application creation | 10 / hour |
|
||||
| Deployments | 30 / hour |
|
||||
| Log access | 200 / 15 min |
|
||||
|
||||
**Rate limit headers in responses:**
|
||||
```
|
||||
X-RateLimit-Limit: 100
|
||||
X-RateLimit-Remaining: 95
|
||||
X-RateLimit-Reset: 1643040000
|
||||
```
|
||||
|
||||
If you hit a rate limit, wait for the reset time before retrying.
|
||||
|
||||
## Support
|
||||
|
||||
**Documentation:**
|
||||
- Template README: [README.md](README.md)
|
||||
|
||||
**Issues:**
|
||||
- Report issues: https://git.startanaicompany.com/StartanAICompany/ai-recruit-site-template/issues
|
||||
|
||||
**Contact:**
|
||||
- Email: info@startanaicompany.com
|
||||
|
||||
---
|
||||
|
||||
**Ready to deploy?** Follow the [Quick Start](#quick-start-3-steps) above!
|
||||
279
DEPLOYMENT_SCRIPT_PROPOSAL.md
Normal file
279
DEPLOYMENT_SCRIPT_PROPOSAL.md
Normal file
@@ -0,0 +1,279 @@
|
||||
# Deployment Script Enhancement Proposal
|
||||
|
||||
## Problem Statement
|
||||
|
||||
The current `deploy-to-apps.sh` script only handles initial deployment. Running it a second time will:
|
||||
1. Attempt to create a duplicate application
|
||||
2. Likely fail with "subdomain already exists" error
|
||||
3. Have no way to update existing application's environment variables
|
||||
4. Have no way to trigger redeployment
|
||||
|
||||
## Solution: Setup vs Update Modes
|
||||
|
||||
### Architecture
|
||||
|
||||
```
|
||||
.deployment-uuid (git-ignored file storing application UUID)
|
||||
deploy-to-apps.sh (enhanced script with mode detection)
|
||||
```
|
||||
|
||||
### Script Modes
|
||||
|
||||
#### 1. Auto-Detection (Default)
|
||||
```bash
|
||||
./deploy-to-apps.sh
|
||||
```
|
||||
- Checks if `.deployment-uuid` exists
|
||||
- **If NOT exists:** Run setup mode (first deployment)
|
||||
- **If exists:** Run update mode (redeploy with new env vars)
|
||||
|
||||
#### 2. Explicit Setup (Force New Deployment)
|
||||
```bash
|
||||
./deploy-to-apps.sh --setup
|
||||
```
|
||||
- Creates NEW application
|
||||
- Overwrites `.deployment-uuid` with new UUID
|
||||
- Use case: Deploying to different subdomain/environment
|
||||
|
||||
#### 3. Explicit Update
|
||||
```bash
|
||||
./deploy-to-apps.sh --update
|
||||
```
|
||||
- Requires `.deployment-uuid` to exist
|
||||
- Updates environment variables
|
||||
- Triggers redeployment
|
||||
- Fails if `.deployment-uuid` not found
|
||||
|
||||
#### 4. Status Check
|
||||
```bash
|
||||
./deploy-to-apps.sh --status
|
||||
```
|
||||
- Shows current deployment info
|
||||
- Fetches live status from API
|
||||
- Shows domain, deployment status, etc.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### File Structure
|
||||
|
||||
```bash
|
||||
.deployment-uuid # Single line: application UUID
|
||||
deployment-info.txt # Human-readable deployment details
|
||||
.gitignore # Must include .deployment-uuid
|
||||
```
|
||||
|
||||
### Setup Mode Flow
|
||||
|
||||
1. Check if subdomain already deployed (optional safety check)
|
||||
2. Create application via `POST /api/v1/applications`
|
||||
3. Save UUID to `.deployment-uuid`
|
||||
4. Set up Gitea webhook (only once)
|
||||
5. Save deployment info to `deployment-info.txt`
|
||||
6. Show success message with next steps
|
||||
|
||||
### Update Mode Flow
|
||||
|
||||
1. Read UUID from `.deployment-uuid`
|
||||
2. Verify application exists via `GET /api/v1/applications/:uuid`
|
||||
3. Update environment variables via `PATCH /api/v1/applications/:uuid/env`
|
||||
4. Trigger redeployment via `POST /api/v1/applications/:uuid/deploy`
|
||||
5. Show redeployment progress
|
||||
|
||||
### Status Mode Flow
|
||||
|
||||
1. Read UUID from `.deployment-uuid`
|
||||
2. Fetch application details via `GET /api/v1/applications/:uuid`
|
||||
3. Display formatted status information
|
||||
|
||||
---
|
||||
|
||||
## API Endpoints Used
|
||||
|
||||
### Setup Mode
|
||||
```bash
|
||||
POST /api/v1/applications
|
||||
→ Returns: { application_uuid, domain, webhook_url, ... }
|
||||
|
||||
POST https://git.startanaicompany.com/api/v1/repos/{owner}/{repo}/hooks
|
||||
→ Sets up Gitea webhook (one-time)
|
||||
```
|
||||
|
||||
### Update Mode
|
||||
```bash
|
||||
GET /api/v1/applications/:uuid
|
||||
→ Verify application exists
|
||||
|
||||
PATCH /api/v1/applications/:uuid/env
|
||||
→ Body: { "variables": { "COMPANY_NAME": "...", ... } }
|
||||
|
||||
POST /api/v1/applications/:uuid/deploy
|
||||
→ Triggers redeployment
|
||||
```
|
||||
|
||||
### Status Mode
|
||||
```bash
|
||||
GET /api/v1/applications/:uuid
|
||||
→ Returns application details and status
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Setup Mode Errors
|
||||
- **Subdomain already exists:** Suggest using `--update` or different subdomain
|
||||
- **API key invalid:** Show registration instructions
|
||||
- **Gitea token invalid:** Show token generation instructions
|
||||
- **Webhook already exists:** Warn but continue (non-fatal)
|
||||
|
||||
### Update Mode Errors
|
||||
- **No .deployment-uuid file:** Suggest running `--setup` first
|
||||
- **Application not found:** UUID invalid or app deleted, suggest `--setup`
|
||||
- **Environment variable validation failed:** Show which vars are invalid
|
||||
- **Deployment failed:** Show error from API
|
||||
|
||||
---
|
||||
|
||||
## Example Usage
|
||||
|
||||
### First Deployment
|
||||
```bash
|
||||
$ ./deploy-to-apps.sh
|
||||
|
||||
========================================
|
||||
AI Recruitment Site Deployment
|
||||
========================================
|
||||
📝 Mode: SETUP (first deployment)
|
||||
📦 Configuration:
|
||||
Company: Acme Corp
|
||||
Repository: git@git.startanaicompany.com:user/acme-recruit.git
|
||||
Domain: https://acmerecruit.startanaicompany.com
|
||||
|
||||
📝 Creating application on StartAnAiCompany server...
|
||||
✅ Application created!
|
||||
Application UUID: abc-123-def-456
|
||||
Domain: https://acmerecruit.startanaicompany.com
|
||||
|
||||
💾 UUID saved to .deployment-uuid
|
||||
🪝 Setting up deployment webhook...
|
||||
✅ Webhook configured for automatic deployments
|
||||
|
||||
========================================
|
||||
Deployment Complete!
|
||||
========================================
|
||||
Your site will be available in 2-3 minutes.
|
||||
|
||||
Next runs will automatically UPDATE this deployment.
|
||||
To deploy a new site, use: ./deploy-to-apps.sh --setup
|
||||
```
|
||||
|
||||
### Update Deployment (Changed Company Name)
|
||||
```bash
|
||||
$ ./deploy-to-apps.sh
|
||||
|
||||
========================================
|
||||
AI Recruitment Site Deployment
|
||||
========================================
|
||||
📝 Mode: UPDATE (existing deployment)
|
||||
📦 Configuration:
|
||||
Application UUID: abc-123-def-456
|
||||
Company: New Company Name (CHANGED)
|
||||
Domain: https://acmerecruit.startanaicompany.com
|
||||
|
||||
🔄 Updating environment variables...
|
||||
✅ Environment variables updated (8 variables)
|
||||
|
||||
🚀 Triggering redeployment...
|
||||
✅ Deployment triggered
|
||||
|
||||
========================================
|
||||
Update Complete!
|
||||
========================================
|
||||
Your changes will be live in 2-3 minutes.
|
||||
|
||||
Monitor deployment:
|
||||
./deploy-to-apps.sh --status
|
||||
```
|
||||
|
||||
### Check Status
|
||||
```bash
|
||||
$ ./deploy-to-apps.sh --status
|
||||
|
||||
========================================
|
||||
Deployment Status
|
||||
========================================
|
||||
Application UUID: abc-123-def-456
|
||||
Name: acme-recruit
|
||||
Domain: https://acmerecruit.startanaicompany.com
|
||||
Status: running
|
||||
Last deployment: 2026-01-24 17:30:00 UTC
|
||||
|
||||
Company: Acme Corp
|
||||
Repository: git@git.startanaicompany.com:user/acme-recruit.git
|
||||
Branch: master
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## .gitignore Updates Required
|
||||
|
||||
Add to `.gitignore`:
|
||||
```
|
||||
# Deployment configuration (contains UUID, API keys)
|
||||
.deployment-uuid
|
||||
deployment-info.txt
|
||||
deploy-to-apps.sh
|
||||
|
||||
# Keep the example for users to copy
|
||||
!deploy-to-apps.example.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Benefits
|
||||
|
||||
1. ✅ **Idempotent:** Can run script multiple times safely
|
||||
2. ✅ **User-friendly:** Auto-detects mode based on context
|
||||
3. ✅ **Flexible:** Supports multiple deployment scenarios
|
||||
4. ✅ **Safe:** Prevents duplicate applications
|
||||
5. ✅ **Traceable:** UUID stored locally for easy updates
|
||||
6. ✅ **Stateful:** Remembers previous deployment
|
||||
7. ✅ **Informative:** Clear feedback on what's happening
|
||||
|
||||
---
|
||||
|
||||
## Migration for Existing Users
|
||||
|
||||
If someone already ran the old script:
|
||||
|
||||
```bash
|
||||
# Find their application UUID
|
||||
curl -H "X-API-Key: $SAAC_API_KEY" https://apps.startanaicompany.com/api/v1/applications
|
||||
|
||||
# Save UUID to file
|
||||
echo "abc-123-def-456" > .deployment-uuid
|
||||
|
||||
# Now they can use update mode
|
||||
./deploy-to-apps.sh --update
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Priority
|
||||
|
||||
### Phase 1 (Must Have)
|
||||
1. Auto-detection (check for `.deployment-uuid`)
|
||||
2. Setup mode (create new + save UUID)
|
||||
3. Update mode (update env vars + redeploy)
|
||||
|
||||
### Phase 2 (Nice to Have)
|
||||
4. Status mode (query current status)
|
||||
5. Enhanced error messages
|
||||
6. Safety checks (subdomain conflicts, etc.)
|
||||
|
||||
### Phase 3 (Future)
|
||||
7. Rollback support
|
||||
8. Multiple environment support (dev/staging/prod)
|
||||
9. Configuration validation before deployment
|
||||
488
DEPLOYMENT_UPDATE_SUMMARY.md
Normal file
488
DEPLOYMENT_UPDATE_SUMMARY.md
Normal file
@@ -0,0 +1,488 @@
|
||||
# Deployment Script Update Summary
|
||||
|
||||
## Overview
|
||||
|
||||
The `deploy-to-apps.example.sh` script has been completely rewritten to use `.saac.json` for configuration storage and implement a streamlined email verification flow. This eliminates the need for manual API key management and provides a better user experience.
|
||||
|
||||
---
|
||||
|
||||
## Changes Made
|
||||
|
||||
### 1. Configuration File Migration
|
||||
|
||||
**Old System:**
|
||||
- `.deployment-uuid` - Stored only the application UUID
|
||||
- `deployment-info.txt` - Human-readable deployment info
|
||||
- `SAAC_API_KEY` - Required as environment variable
|
||||
|
||||
**New System:**
|
||||
- `.saac.json` - Stores all user credentials and deployment configuration
|
||||
- No separate info file needed
|
||||
- No environment variable for API key
|
||||
|
||||
### 2. `.saac.json` Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "1.0",
|
||||
"user": {
|
||||
"email": "user@example.com",
|
||||
"user_id": "uuid-here",
|
||||
"api_key": "api-key-here",
|
||||
"gitea_username": "username",
|
||||
"verified": true,
|
||||
"registered_at": "2026-01-24T12:00:00.000Z"
|
||||
},
|
||||
"deployment": {
|
||||
"application_uuid": "app-uuid-here",
|
||||
"application_name": "subdomain-recruit",
|
||||
"domain": "subdomain.recruit.startanaicompany.com",
|
||||
"subdomain": "subdomain",
|
||||
"repository": "git@git.startanaicompany.com:user/repo.git",
|
||||
"deployed_at": "2026-01-24T12:00:00.000Z",
|
||||
"last_updated": "2026-01-24T12:30:00.000Z"
|
||||
},
|
||||
"config": {
|
||||
"saac_api": "https://apps.startanaicompany.com/api/v1",
|
||||
"gitea_api": "https://git.startanaicompany.com/api/v1"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. New Helper Functions
|
||||
|
||||
#### `load_config()`
|
||||
- Loads configuration from `.saac.json`
|
||||
- Extracts user credentials, verification status, and deployment info
|
||||
- Returns 0 if file exists, 1 if not
|
||||
|
||||
#### `save_config()`
|
||||
- Saves configuration to `.saac.json`
|
||||
- Sets file permissions to 600 (read/write for owner only)
|
||||
- Updates `last_updated` timestamp automatically
|
||||
|
||||
### 4. Email Verification Flow
|
||||
|
||||
#### First-Time Registration (No `.saac.json`)
|
||||
|
||||
1. **Prompt for Email**
|
||||
- User enters email address
|
||||
- Gitea username pulled from `.env`
|
||||
|
||||
2. **Register User**
|
||||
```bash
|
||||
POST /users/register
|
||||
{
|
||||
"email": "user@example.com",
|
||||
"gitea_username": "username"
|
||||
}
|
||||
```
|
||||
- Returns `user_id` and `api_key`
|
||||
|
||||
3. **Save Unverified Configuration**
|
||||
- Creates `.saac.json` with `verified: false`
|
||||
- File is immediately saved (safe to exit and resume)
|
||||
|
||||
4. **Email Verification**
|
||||
- User checks MailHog at https://mailhog.goryan.io
|
||||
- Enters verification code
|
||||
- Script calls `POST /users/verify`
|
||||
- Updates `.saac.json` with `verified: true`
|
||||
|
||||
#### Returning User (Unverified)
|
||||
|
||||
If `.saac.json` exists but `verified: false`:
|
||||
1. Prompt for verification code
|
||||
2. Verify email
|
||||
3. Update configuration
|
||||
|
||||
#### Verified User
|
||||
|
||||
If `.saac.json` exists and `verified: true`:
|
||||
- Loads configuration silently
|
||||
- Proceeds to auto-detect mode (setup or update)
|
||||
|
||||
### 5. Auto-Detection Logic
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ Does .saac.json exist? │
|
||||
└──────────┬──────────────────────────┘
|
||||
│
|
||||
┌──────┴──────┐
|
||||
│ NO │ YES
|
||||
▼ ▼
|
||||
┌───────┐ ┌──────────────┐
|
||||
│REGISTER│ │ verified? │
|
||||
└───┬───┘ └──────┬───────┘
|
||||
│ │
|
||||
▼ ┌─────┴─────┐
|
||||
┌────────┐ │NO YES│
|
||||
│ VERIFY │ ▼ ▼
|
||||
└───┬────┘ ┌──────┐ ┌──────────────┐
|
||||
│ │VERIFY│ │ has app_uuid?│
|
||||
│ └──┬───┘ └──────┬───────┘
|
||||
│ │ │
|
||||
└──────────┴─────────────┤
|
||||
┌─────┴─────┐
|
||||
│NO YES│
|
||||
▼ ▼
|
||||
┌──────┐ ┌──────┐
|
||||
│SETUP │ │UPDATE│
|
||||
└──────┘ └──────┘
|
||||
```
|
||||
|
||||
### 6. Removed Features
|
||||
|
||||
- `SAAC_API_KEY` environment variable requirement (now in `.saac.json`)
|
||||
- `deployment-info.txt` file (replaced by `.saac.json`)
|
||||
- `.deployment-uuid` file (replaced by `.saac.json`)
|
||||
|
||||
### 7. Updated `.gitignore`
|
||||
|
||||
**Removed:**
|
||||
- `deployment-info.txt`
|
||||
- `.deployment-uuid`
|
||||
|
||||
**Added:**
|
||||
- `.saac.json`
|
||||
|
||||
### 8. New Dependencies
|
||||
|
||||
- **jq** - JSON processor (required)
|
||||
- Ubuntu/Debian: `sudo apt install jq`
|
||||
- macOS: `brew install jq`
|
||||
- Alpine: `apk add jq`
|
||||
|
||||
Script checks for `jq` and provides installation instructions if missing.
|
||||
|
||||
---
|
||||
|
||||
## Flow Diagrams
|
||||
|
||||
### First-Time User Flow
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 1. User runs: ./deploy-to-apps.sh │
|
||||
└─────────────────────┬───────────────────────────────────┘
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 2. No .saac.json found │
|
||||
│ → Prompt for email │
|
||||
│ → Read GITEA_USERNAME from .env │
|
||||
└─────────────────────┬───────────────────────────────────┘
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 3. POST /users/register │
|
||||
│ → Receive user_id and api_key │
|
||||
└─────────────────────┬───────────────────────────────────┘
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 4. Save .saac.json (verified: false) │
|
||||
└─────────────────────┬───────────────────────────────────┘
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 5. Show MailHog URL │
|
||||
│ → Prompt for verification code │
|
||||
└─────────────────────┬───────────────────────────────────┘
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 6. POST /users/verify │
|
||||
│ → Check response for "verified": true │
|
||||
└─────────────────────┬───────────────────────────────────┘
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 7. Update .saac.json (verified: true) │
|
||||
└─────────────────────┬───────────────────────────────────┘
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 8. Auto-detect → SETUP mode (no app_uuid) │
|
||||
│ → Create new deployment │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Returning User Flow (Verified, Deployed)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 1. User runs: ./deploy-to-apps.sh │
|
||||
└─────────────────────┬───────────────────────────────────┘
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 2. Load .saac.json │
|
||||
│ → verified: true │
|
||||
│ → app_uuid: exists │
|
||||
└─────────────────────┬───────────────────────────────────┘
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 3. Auto-detect → UPDATE mode │
|
||||
└─────────────────────┬───────────────────────────────────┘
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 4. Update environment variables │
|
||||
│ → Trigger redeployment │
|
||||
│ → Update .saac.json timestamps │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Migration Instructions for Existing Users
|
||||
|
||||
### If You Have `.deployment-uuid` and `deployment-info.txt`
|
||||
|
||||
**Option 1: Start Fresh (Recommended)**
|
||||
|
||||
1. Delete old files:
|
||||
```bash
|
||||
rm .deployment-uuid deployment-info.txt
|
||||
```
|
||||
|
||||
2. Run the new script:
|
||||
```bash
|
||||
cp deploy-to-apps.example.sh deploy-to-apps.sh
|
||||
./deploy-to-apps.sh
|
||||
```
|
||||
|
||||
3. Follow the registration and verification prompts
|
||||
|
||||
4. When asked to setup, confirm to create new deployment
|
||||
|
||||
**Option 2: Manual Migration**
|
||||
|
||||
1. Note your existing configuration:
|
||||
```bash
|
||||
APP_UUID=$(cat .deployment-uuid)
|
||||
# Note your SAAC_API_KEY from environment
|
||||
# Note your email used for registration
|
||||
```
|
||||
|
||||
2. Create `.saac.json` manually:
|
||||
```json
|
||||
{
|
||||
"version": "1.0",
|
||||
"user": {
|
||||
"email": "your@email.com",
|
||||
"user_id": "your-user-id",
|
||||
"api_key": "your-api-key",
|
||||
"gitea_username": "your-username",
|
||||
"verified": true,
|
||||
"registered_at": "2026-01-24T00:00:00.000Z"
|
||||
},
|
||||
"deployment": {
|
||||
"application_uuid": "your-app-uuid",
|
||||
"application_name": "subdomain-recruit",
|
||||
"domain": "subdomain.recruit.startanaicompany.com",
|
||||
"subdomain": "subdomain",
|
||||
"repository": "git@git.startanaicompany.com:user/repo.git",
|
||||
"deployed_at": "2026-01-24T00:00:00.000Z",
|
||||
"last_updated": "2026-01-24T00:00:00.000Z"
|
||||
},
|
||||
"config": {
|
||||
"saac_api": "https://apps.startanaicompany.com/api/v1",
|
||||
"gitea_api": "https://git.startanaicompany.com/api/v1"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. Set proper permissions:
|
||||
```bash
|
||||
chmod 600 .saac.json
|
||||
```
|
||||
|
||||
4. Delete old files:
|
||||
```bash
|
||||
rm .deployment-uuid deployment-info.txt
|
||||
```
|
||||
|
||||
5. Test the script:
|
||||
```bash
|
||||
./deploy-to-apps.sh --status
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Validation
|
||||
|
||||
### Script Validation
|
||||
|
||||
```bash
|
||||
bash -n deploy-to-apps.example.sh
|
||||
```
|
||||
|
||||
**Result:** ✅ No syntax errors
|
||||
|
||||
### Required Dependencies
|
||||
|
||||
- `bash` - Bourne Again Shell
|
||||
- `curl` - HTTP client
|
||||
- `jq` - JSON processor ⚠️ NEW REQUIREMENT
|
||||
|
||||
### File Permissions
|
||||
|
||||
The script automatically sets `.saac.json` to `600` (read/write for owner only) to protect sensitive credentials.
|
||||
|
||||
---
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### `.saac.json` Contains Sensitive Data
|
||||
|
||||
**Never commit this file to git!**
|
||||
|
||||
The file contains:
|
||||
- User email
|
||||
- API key (equivalent to password)
|
||||
- User ID
|
||||
- Application UUIDs
|
||||
|
||||
### Gitignore Protection
|
||||
|
||||
The `.gitignore` file now includes `.saac.json` to prevent accidental commits.
|
||||
|
||||
### File Permissions
|
||||
|
||||
The script sets `.saac.json` to mode `600`:
|
||||
- Owner: read/write
|
||||
- Group: none
|
||||
- Others: none
|
||||
|
||||
---
|
||||
|
||||
## Backward Compatibility
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
1. **SAAC_API_KEY environment variable no longer used**
|
||||
- Old: Required in environment
|
||||
- New: Stored in `.saac.json`
|
||||
|
||||
2. **Files no longer created**
|
||||
- `.deployment-uuid` → replaced by `.saac.json`
|
||||
- `deployment-info.txt` → replaced by `.saac.json`
|
||||
|
||||
3. **New dependency**
|
||||
- `jq` now required for JSON processing
|
||||
|
||||
### Non-Breaking Changes
|
||||
|
||||
1. **All modes still work**
|
||||
- `./deploy-to-apps.sh` (auto-detect)
|
||||
- `./deploy-to-apps.sh --setup`
|
||||
- `./deploy-to-apps.sh --update`
|
||||
- `./deploy-to-apps.sh --status`
|
||||
|
||||
2. **GITEA_API_TOKEN still required for setup**
|
||||
- Only needed for webhook creation during initial setup
|
||||
|
||||
3. **All .env variables unchanged**
|
||||
- Same company configuration variables
|
||||
- Same repository configuration
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
### ✅ Completed
|
||||
|
||||
- [x] Bash syntax validation
|
||||
- [x] Helper function logic
|
||||
- [x] Configuration file structure
|
||||
- [x] Flow diagram accuracy
|
||||
- [x] Migration instructions
|
||||
|
||||
### 🔄 To Be Tested
|
||||
|
||||
- [ ] First-time registration flow
|
||||
- [ ] Email verification flow
|
||||
- [ ] Unverified user resumption
|
||||
- [ ] Auto-detect mode logic
|
||||
- [ ] Setup mode with `.saac.json`
|
||||
- [ ] Update mode with `.saac.json`
|
||||
- [ ] Status mode with `.saac.json`
|
||||
- [ ] Manual migration from old files
|
||||
|
||||
---
|
||||
|
||||
## Example Usage
|
||||
|
||||
### First-Time User
|
||||
|
||||
```bash
|
||||
$ cp deploy-to-apps.example.sh deploy-to-apps.sh
|
||||
$ ./deploy-to-apps.sh
|
||||
|
||||
=========================================
|
||||
First-Time Setup
|
||||
=========================================
|
||||
|
||||
No configuration found. Let's register your account.
|
||||
|
||||
Enter your email address: user@example.com
|
||||
|
||||
📧 Registering user: user@example.com
|
||||
Gitea username: myusername
|
||||
|
||||
✅ User registered!
|
||||
User ID: 123e4567-e89b-12d3-a456-426614174000
|
||||
|
||||
💾 Configuration saved to .saac.json
|
||||
|
||||
=========================================
|
||||
Email Verification
|
||||
=========================================
|
||||
|
||||
📧 Verification email sent to: user@example.com
|
||||
|
||||
🔍 Check your email at MailHog:
|
||||
https://mailhog.goryan.io
|
||||
|
||||
Enter the verification code from the email: ABC123
|
||||
|
||||
🔐 Verifying email...
|
||||
✅ Email verified successfully!
|
||||
|
||||
💾 Configuration saved to .saac.json
|
||||
|
||||
=========================================
|
||||
AI Recruitment Site Deployment
|
||||
=========================================
|
||||
📝 Mode: SETUP (new deployment)
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
### Returning User (Update)
|
||||
|
||||
```bash
|
||||
$ ./deploy-to-apps.sh
|
||||
|
||||
✅ Loaded configuration from .saac.json
|
||||
User: user@example.com
|
||||
Verified: true
|
||||
Application: 123e4567-e89b-12d3-a456-426614174000
|
||||
|
||||
=========================================
|
||||
AI Recruitment Site Deployment
|
||||
=========================================
|
||||
📝 Mode: UPDATE (existing deployment)
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Support
|
||||
|
||||
For issues or questions:
|
||||
1. Check MailHog for verification emails: https://mailhog.goryan.io
|
||||
2. Verify `.saac.json` exists and has correct permissions
|
||||
3. Ensure `jq` is installed
|
||||
4. Check SAAC API status
|
||||
|
||||
---
|
||||
|
||||
**Document Version:** 1.0
|
||||
**Last Updated:** 2026-01-24
|
||||
**Script Version:** Updated with .saac.json support
|
||||
30
Dockerfile
Normal file
30
Dockerfile
Normal file
@@ -0,0 +1,30 @@
|
||||
FROM node:18-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
COPY package*.json ./
|
||||
|
||||
# Install dependencies
|
||||
RUN npm install --production
|
||||
|
||||
# Copy application code
|
||||
COPY server.js ./
|
||||
COPY config.js ./
|
||||
COPY migrations ./migrations
|
||||
|
||||
# Copy public directory explicitly
|
||||
COPY public/ ./public/
|
||||
|
||||
# Create non-root user and fix permissions
|
||||
RUN addgroup -g 1001 -S nodejs && \
|
||||
adduser -S nodejs -u 1001 && \
|
||||
chown -R nodejs:nodejs /app && \
|
||||
ls -la /app && \
|
||||
ls -la /app/public
|
||||
|
||||
USER nodejs
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["node", "server.js"]
|
||||
417
MIGRATION_GUIDE.md
Normal file
417
MIGRATION_GUIDE.md
Normal file
@@ -0,0 +1,417 @@
|
||||
# Migration Guide: Old Deployment Script → New .saac.json System
|
||||
|
||||
## Overview
|
||||
|
||||
This guide helps existing users migrate from the old deployment system (`.deployment-uuid` + `deployment-info.txt`) to the new `.saac.json` configuration system.
|
||||
|
||||
---
|
||||
|
||||
## What Changed?
|
||||
|
||||
### Old System
|
||||
```
|
||||
.deployment-uuid → Contains: application UUID only
|
||||
deployment-info.txt → Contains: Human-readable deployment info
|
||||
Environment variable → SAAC_API_KEY required
|
||||
```
|
||||
|
||||
### New System
|
||||
```
|
||||
.saac.json → Contains: All user credentials + deployment info
|
||||
No environment variable → API key stored in .saac.json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Migration Options
|
||||
|
||||
### Option 1: Start Fresh (Recommended)
|
||||
|
||||
**Best for:** Most users, cleanest migration
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. **Backup existing configuration** (optional)
|
||||
```bash
|
||||
cp .deployment-uuid .deployment-uuid.backup
|
||||
cp deployment-info.txt deployment-info.txt.backup
|
||||
echo "$SAAC_API_KEY" > saac-api-key.backup
|
||||
```
|
||||
|
||||
2. **Remove old files**
|
||||
```bash
|
||||
rm .deployment-uuid deployment-info.txt
|
||||
unset SAAC_API_KEY # Clear from current session
|
||||
```
|
||||
|
||||
3. **Update the script**
|
||||
```bash
|
||||
cp deploy-to-apps.example.sh deploy-to-apps.sh
|
||||
```
|
||||
|
||||
4. **Run the new script**
|
||||
```bash
|
||||
./deploy-to-apps.sh
|
||||
```
|
||||
|
||||
5. **Follow prompts**
|
||||
- Enter your email (can be same as before)
|
||||
- Verify email via MailHog
|
||||
- When asked about creating deployment, choose Yes
|
||||
|
||||
6. **Verify it works**
|
||||
```bash
|
||||
./deploy-to-apps.sh --status
|
||||
```
|
||||
|
||||
**Result:** New `.saac.json` file with fresh deployment
|
||||
|
||||
---
|
||||
|
||||
### Option 2: Manual Migration
|
||||
|
||||
**Best for:** Users who want to preserve existing deployment UUID
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. **Gather existing configuration**
|
||||
```bash
|
||||
# Save these values somewhere
|
||||
APP_UUID=$(cat .deployment-uuid)
|
||||
echo "Application UUID: $APP_UUID"
|
||||
echo "API Key: $SAAC_API_KEY"
|
||||
echo "Email: <your email used for registration>"
|
||||
|
||||
# From .env file
|
||||
source .env
|
||||
echo "Gitea Username: $GITEA_USERNAME"
|
||||
echo "Subdomain: $SUBDOMAIN"
|
||||
```
|
||||
|
||||
2. **Get your User ID**
|
||||
|
||||
You need to retrieve your `user_id` from the API:
|
||||
|
||||
```bash
|
||||
# Call the API to get your user info
|
||||
curl -s -X GET "https://apps.startanaicompany.com/api/v1/users/me" \
|
||||
-H "X-API-Key: $SAAC_API_KEY" | jq '.'
|
||||
```
|
||||
|
||||
Note the `user_id` from the response.
|
||||
|
||||
3. **Create .saac.json manually**
|
||||
|
||||
```bash
|
||||
cat > .saac.json <<'EOF'
|
||||
{
|
||||
"version": "1.0",
|
||||
"user": {
|
||||
"email": "YOUR_EMAIL_HERE",
|
||||
"user_id": "YOUR_USER_ID_HERE",
|
||||
"api_key": "YOUR_API_KEY_HERE",
|
||||
"gitea_username": "YOUR_GITEA_USERNAME",
|
||||
"verified": true,
|
||||
"registered_at": "2026-01-24T00:00:00.000Z"
|
||||
},
|
||||
"deployment": {
|
||||
"application_uuid": "YOUR_APP_UUID_HERE",
|
||||
"application_name": "SUBDOMAIN-recruit",
|
||||
"domain": "SUBDOMAIN.recruit.startanaicompany.com",
|
||||
"subdomain": "SUBDOMAIN",
|
||||
"repository": "git@git.startanaicompany.com:USERNAME/REPONAME.git",
|
||||
"deployed_at": "2026-01-24T00:00:00.000Z",
|
||||
"last_updated": "2026-01-24T00:00:00.000Z"
|
||||
},
|
||||
"config": {
|
||||
"saac_api": "https://apps.startanaicompany.com/api/v1",
|
||||
"gitea_api": "https://git.startanaicompany.com/api/v1"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
```
|
||||
|
||||
4. **Replace placeholders**
|
||||
|
||||
Edit `.saac.json` and replace:
|
||||
- `YOUR_EMAIL_HERE` → Your email address
|
||||
- `YOUR_USER_ID_HERE` → User ID from step 2
|
||||
- `YOUR_API_KEY_HERE` → Your existing `SAAC_API_KEY`
|
||||
- `YOUR_GITEA_USERNAME` → From `.env` file
|
||||
- `YOUR_APP_UUID_HERE` → From `.deployment-uuid` file
|
||||
- `SUBDOMAIN` → From `.env` file
|
||||
- `USERNAME/REPONAME` → Your repository path
|
||||
|
||||
5. **Set proper permissions**
|
||||
```bash
|
||||
chmod 600 .saac.json
|
||||
```
|
||||
|
||||
6. **Remove old files**
|
||||
```bash
|
||||
rm .deployment-uuid deployment-info.txt
|
||||
```
|
||||
|
||||
7. **Test the migration**
|
||||
```bash
|
||||
./deploy-to-apps.sh --status
|
||||
```
|
||||
|
||||
Expected output:
|
||||
```
|
||||
✅ Loaded configuration from .saac.json
|
||||
User: your@email.com
|
||||
Verified: true
|
||||
Application: 123e4567-...
|
||||
|
||||
=========================================
|
||||
Deployment Status
|
||||
=========================================
|
||||
|
||||
Application UUID: 123e4567-...
|
||||
Name: mycompany-recruit
|
||||
...
|
||||
```
|
||||
|
||||
8. **Test an update**
|
||||
```bash
|
||||
# Make a small change to .env
|
||||
echo "# Test comment" >> .env
|
||||
|
||||
# Deploy update
|
||||
./deploy-to-apps.sh --update
|
||||
```
|
||||
|
||||
**Result:** Existing deployment preserved, now managed via `.saac.json`
|
||||
|
||||
---
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
After migration, verify:
|
||||
|
||||
- [ ] `.saac.json` exists and has mode `600`
|
||||
- [ ] `.saac.json` is in `.gitignore`
|
||||
- [ ] Old files removed (`.deployment-uuid`, `deployment-info.txt`)
|
||||
- [ ] `SAAC_API_KEY` environment variable no longer needed
|
||||
- [ ] `./deploy-to-apps.sh --status` works
|
||||
- [ ] `./deploy-to-apps.sh --update` works
|
||||
- [ ] Site is still accessible
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Problem: "Application not found"
|
||||
|
||||
**Possible causes:**
|
||||
- Wrong `application_uuid` in `.saac.json`
|
||||
- Wrong `api_key` in `.saac.json`
|
||||
- Application was deleted
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
# Verify your application exists
|
||||
curl -X GET "https://apps.startanaicompany.com/api/v1/applications/YOUR_UUID" \
|
||||
-H "X-API-Key: YOUR_API_KEY"
|
||||
|
||||
# If 404, application is gone → use Option 1 (start fresh)
|
||||
# If 403, wrong API key → check your api_key value
|
||||
# If 200, check UUID matches exactly
|
||||
```
|
||||
|
||||
### Problem: "User not verified"
|
||||
|
||||
**Possible cause:**
|
||||
- Set `verified: false` in `.saac.json`
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
# Edit .saac.json
|
||||
nano .saac.json
|
||||
|
||||
# Change this line:
|
||||
"verified": true,
|
||||
|
||||
# Save and try again
|
||||
./deploy-to-apps.sh
|
||||
```
|
||||
|
||||
### Problem: "jq: command not found"
|
||||
|
||||
**Cause:** New dependency not installed
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
# Ubuntu/Debian
|
||||
sudo apt update && sudo apt install jq
|
||||
|
||||
# macOS
|
||||
brew install jq
|
||||
|
||||
# Alpine
|
||||
apk add jq
|
||||
```
|
||||
|
||||
### Problem: "Cannot get user_id"
|
||||
|
||||
**Possible causes:**
|
||||
- API key expired
|
||||
- Wrong API key
|
||||
- Account deleted
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
# Test API key
|
||||
curl -X GET "https://apps.startanaicompany.com/api/v1/users/me" \
|
||||
-H "X-API-Key: $SAAC_API_KEY"
|
||||
|
||||
# If fails → use Option 1 (start fresh with new registration)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rollback (If Needed)
|
||||
|
||||
If you need to rollback to the old system:
|
||||
|
||||
1. **Restore old files**
|
||||
```bash
|
||||
cp .deployment-uuid.backup .deployment-uuid
|
||||
cp deployment-info.txt.backup deployment-info.txt
|
||||
export SAAC_API_KEY=$(cat saac-api-key.backup)
|
||||
```
|
||||
|
||||
2. **Remove new configuration**
|
||||
```bash
|
||||
rm .saac.json
|
||||
```
|
||||
|
||||
3. **Use old script**
|
||||
```bash
|
||||
git checkout HEAD~1 deploy-to-apps.example.sh
|
||||
cp deploy-to-apps.example.sh deploy-to-apps.sh
|
||||
```
|
||||
|
||||
**Note:** This only works if you kept backups from Option 1 Step 1
|
||||
|
||||
---
|
||||
|
||||
## FAQ
|
||||
|
||||
### Q: Do I need to re-verify my email?
|
||||
|
||||
**A:** Only if you start fresh (Option 1). If you manually migrate (Option 2) and set `verified: true`, no verification needed.
|
||||
|
||||
### Q: Will my existing deployment be affected?
|
||||
|
||||
**A:**
|
||||
- **Option 1 (start fresh):** New deployment created, old one remains but disconnected
|
||||
- **Option 2 (manual):** Same deployment, just new config file format
|
||||
|
||||
### Q: Can I have multiple deployments?
|
||||
|
||||
**A:** Yes! Each `.saac.json` can have one deployment. For multiple deployments:
|
||||
- Use separate directories
|
||||
- Or run `--setup` to replace deployment in current directory
|
||||
|
||||
### Q: What happens to my old API key?
|
||||
|
||||
**A:**
|
||||
- **Option 1:** New API key generated, old one still valid but unused
|
||||
- **Option 2:** Same API key, just moved from environment to `.saac.json`
|
||||
|
||||
### Q: Can I delete old backup files?
|
||||
|
||||
**A:** Yes, after verifying everything works:
|
||||
```bash
|
||||
rm .deployment-uuid.backup deployment-info.txt.backup saac-api-key.backup
|
||||
```
|
||||
|
||||
### Q: Is .saac.json committed to git?
|
||||
|
||||
**A:** No, it's in `.gitignore`. Never commit this file (contains API key).
|
||||
|
||||
---
|
||||
|
||||
## Best Practices After Migration
|
||||
|
||||
1. **Backup .saac.json**
|
||||
```bash
|
||||
# Encrypted backup
|
||||
gpg -c .saac.json
|
||||
# Creates .saac.json.gpg (safe to store)
|
||||
```
|
||||
|
||||
2. **Remove SAAC_API_KEY from shell profiles**
|
||||
```bash
|
||||
# Edit ~/.bashrc, ~/.zshrc, etc.
|
||||
# Remove lines like:
|
||||
# export SAAC_API_KEY='...'
|
||||
```
|
||||
|
||||
3. **Document your setup**
|
||||
```bash
|
||||
# Create a README for your team
|
||||
cat > DEPLOYMENT_README.md <<'EOF'
|
||||
# Deployment
|
||||
|
||||
1. Copy deploy-to-apps.example.sh to deploy-to-apps.sh
|
||||
2. Run ./deploy-to-apps.sh
|
||||
3. Check .saac.json is created (DO NOT commit!)
|
||||
4. Update site: ./deploy-to-apps.sh --update
|
||||
EOF
|
||||
```
|
||||
|
||||
4. **Test the new workflow**
|
||||
- Make a small .env change
|
||||
- Run update
|
||||
- Verify site updates
|
||||
|
||||
---
|
||||
|
||||
## Support
|
||||
|
||||
If you encounter issues during migration:
|
||||
|
||||
1. **Check this guide** - Most issues covered above
|
||||
2. **Verify prerequisites** - `jq` installed, `.env` configured
|
||||
3. **Check MailHog** - For verification emails
|
||||
4. **Use `--status`** - To diagnose deployment state
|
||||
5. **Start fresh** - Option 1 always works
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
### Quick Decision Tree
|
||||
|
||||
```
|
||||
Do you have .deployment-uuid file?
|
||||
│
|
||||
├─ No → Just run ./deploy-to-apps.sh (automatic registration)
|
||||
│
|
||||
└─ Yes → Choose migration option:
|
||||
│
|
||||
├─ Want fresh start → Option 1 (recommended)
|
||||
│ └─ Delete old files, run script
|
||||
│
|
||||
└─ Want same deployment → Option 2 (advanced)
|
||||
└─ Manually create .saac.json
|
||||
```
|
||||
|
||||
### Migration Time
|
||||
|
||||
- **Option 1:** 5-10 minutes (includes verification)
|
||||
- **Option 2:** 10-15 minutes (includes API calls)
|
||||
|
||||
### Success Rate
|
||||
|
||||
- **Option 1:** 100% (fresh start always works)
|
||||
- **Option 2:** 95% (requires correct user_id)
|
||||
|
||||
---
|
||||
|
||||
**Document Version:** 1.0
|
||||
**Last Updated:** 2026-01-24
|
||||
**Applies to:** deploy-to-apps.example.sh (January 2026 version)
|
||||
342
QUICK_START.md
Normal file
342
QUICK_START.md
Normal file
@@ -0,0 +1,342 @@
|
||||
# Quick Start Guide - AI Recruitment Site Deployment
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **Install jq** (JSON processor)
|
||||
```bash
|
||||
# Ubuntu/Debian
|
||||
sudo apt install jq
|
||||
|
||||
# macOS
|
||||
brew install jq
|
||||
|
||||
# Alpine Linux
|
||||
apk add jq
|
||||
```
|
||||
|
||||
2. **Get Gitea API Token** (only needed for first deployment)
|
||||
- Go to https://git.startanaicompany.com
|
||||
- Profile → Settings → Applications
|
||||
- Generate New Token (grant 'repo' permissions)
|
||||
- Save the token
|
||||
|
||||
3. **Configure your .env file**
|
||||
```bash
|
||||
cp .env.example .env
|
||||
nano .env # Edit with your company information
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## First-Time Deployment
|
||||
|
||||
### Step 1: Copy the Script
|
||||
|
||||
```bash
|
||||
cp deploy-to-apps.example.sh deploy-to-apps.sh
|
||||
```
|
||||
|
||||
### Step 2: Set Gitea Token
|
||||
|
||||
```bash
|
||||
export GITEA_API_TOKEN='your_gitea_token_here'
|
||||
```
|
||||
|
||||
### Step 3: Run the Script
|
||||
|
||||
```bash
|
||||
./deploy-to-apps.sh
|
||||
```
|
||||
|
||||
### Step 4: Follow the Prompts
|
||||
|
||||
The script will:
|
||||
|
||||
1. **Prompt for your email**
|
||||
```
|
||||
Enter your email address: you@example.com
|
||||
```
|
||||
|
||||
2. **Register your account**
|
||||
- Creates user account
|
||||
- Generates API key
|
||||
- Saves to `.saac.json`
|
||||
|
||||
3. **Request verification code**
|
||||
```
|
||||
Check your email at MailHog:
|
||||
https://mailhog.goryan.io
|
||||
|
||||
Enter the verification code from the email:
|
||||
```
|
||||
|
||||
4. **Create deployment**
|
||||
- Sets up application
|
||||
- Configures webhook
|
||||
- Deploys your site
|
||||
|
||||
### Step 5: Wait for Deployment
|
||||
|
||||
Your site will be live in 2-3 minutes at:
|
||||
```
|
||||
https://<subdomain>recruit.startanaicompany.com
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Updating Your Site
|
||||
|
||||
### Step 1: Edit .env
|
||||
|
||||
```bash
|
||||
nano .env
|
||||
# Change COMPANY_NAME, colors, contact info, etc.
|
||||
```
|
||||
|
||||
### Step 2: Deploy Updates
|
||||
|
||||
```bash
|
||||
./deploy-to-apps.sh
|
||||
# or
|
||||
./deploy-to-apps.sh --update
|
||||
```
|
||||
|
||||
The script will:
|
||||
- Load configuration from `.saac.json`
|
||||
- Update environment variables
|
||||
- Trigger redeployment
|
||||
|
||||
Your changes will be live in 2-3 minutes.
|
||||
|
||||
---
|
||||
|
||||
## Checking Deployment Status
|
||||
|
||||
```bash
|
||||
./deploy-to-apps.sh --status
|
||||
```
|
||||
|
||||
Output:
|
||||
```
|
||||
Application UUID: 123e4567-e89b-12d3-a456-426614174000
|
||||
Name: mycompany-recruit
|
||||
Domain: https://mycompany.recruit.startanaicompany.com
|
||||
Status: running
|
||||
Repository: git@git.startanaicompany.com:myuser/myrepo.git
|
||||
Branch: master
|
||||
Created: 2026-01-24T12:00:00Z
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Understanding .saac.json
|
||||
|
||||
This file stores your credentials and deployment configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"user": {
|
||||
"email": "you@example.com",
|
||||
"user_id": "...",
|
||||
"api_key": "...",
|
||||
"verified": true
|
||||
},
|
||||
"deployment": {
|
||||
"application_uuid": "...",
|
||||
"domain": "..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Important:**
|
||||
- ⚠️ Never commit this file to git (it's in `.gitignore`)
|
||||
- ✅ Keep it secure (permissions set to 600)
|
||||
- 📋 Backup this file to restore access
|
||||
|
||||
---
|
||||
|
||||
## Common Scenarios
|
||||
|
||||
### Scenario 1: Interrupted Registration
|
||||
|
||||
If you exit during registration/verification:
|
||||
|
||||
```bash
|
||||
./deploy-to-apps.sh
|
||||
```
|
||||
|
||||
The script will:
|
||||
- Detect existing `.saac.json`
|
||||
- Check verification status
|
||||
- Resume where you left off
|
||||
|
||||
### Scenario 2: New Deployment on Existing Account
|
||||
|
||||
If you want to deploy a different site:
|
||||
|
||||
```bash
|
||||
./deploy-to-apps.sh --setup
|
||||
```
|
||||
|
||||
This will:
|
||||
- Use existing credentials
|
||||
- Create new application
|
||||
- Update `.saac.json` with new deployment
|
||||
|
||||
### Scenario 3: Lost .saac.json
|
||||
|
||||
If you lose your `.saac.json` file:
|
||||
|
||||
**Option 1:** Re-register with same email
|
||||
- Run `./deploy-to-apps.sh`
|
||||
- Enter same email
|
||||
- New API key will be generated
|
||||
- Old deployments will still work with old API key
|
||||
|
||||
**Option 2:** Restore from backup
|
||||
- Copy backed-up `.saac.json`
|
||||
- Run `./deploy-to-apps.sh --status` to verify
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Error: "jq: command not found"
|
||||
|
||||
**Solution:** Install jq
|
||||
```bash
|
||||
sudo apt install jq # Ubuntu/Debian
|
||||
brew install jq # macOS
|
||||
```
|
||||
|
||||
### Error: "Email verification failed"
|
||||
|
||||
**Causes:**
|
||||
- Wrong verification code
|
||||
- Code expired (valid for 24 hours)
|
||||
|
||||
**Solution:**
|
||||
1. Check MailHog: https://mailhog.goryan.io
|
||||
2. Copy code exactly as shown
|
||||
3. Try again (re-run script if needed)
|
||||
|
||||
### Error: "Application not found"
|
||||
|
||||
**Causes:**
|
||||
- Application was deleted
|
||||
- Wrong API key
|
||||
- Corrupt `.saac.json`
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
# Create new deployment
|
||||
./deploy-to-apps.sh --setup
|
||||
```
|
||||
|
||||
### Error: "GITEA_API_TOKEN not set"
|
||||
|
||||
**Cause:** Token not exported (only needed for setup mode)
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
export GITEA_API_TOKEN='your_token_here'
|
||||
./deploy-to-apps.sh --setup
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Script Modes
|
||||
|
||||
### Auto-detect (Default)
|
||||
|
||||
```bash
|
||||
./deploy-to-apps.sh
|
||||
```
|
||||
|
||||
Automatically determines:
|
||||
- No `.saac.json` → Registration + Setup
|
||||
- `.saac.json` exists, unverified → Verification
|
||||
- `.saac.json` exists, verified, no deployment → Setup
|
||||
- `.saac.json` exists, verified, has deployment → Update
|
||||
|
||||
### Force Setup
|
||||
|
||||
```bash
|
||||
./deploy-to-apps.sh --setup
|
||||
```
|
||||
|
||||
Creates new deployment (warns if one already exists)
|
||||
|
||||
### Force Update
|
||||
|
||||
```bash
|
||||
./deploy-to-apps.sh --update
|
||||
```
|
||||
|
||||
Updates existing deployment (fails if none exists)
|
||||
|
||||
### Check Status
|
||||
|
||||
```bash
|
||||
./deploy-to-apps.sh --status
|
||||
```
|
||||
|
||||
Shows deployment information
|
||||
|
||||
---
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
1. **Never commit `.saac.json`**
|
||||
- Contains API key (like a password)
|
||||
- Already in `.gitignore`
|
||||
|
||||
2. **Backup your `.saac.json`**
|
||||
- Store in secure location
|
||||
- Don't upload to public services
|
||||
|
||||
3. **Rotate API keys if compromised**
|
||||
- Delete `.saac.json`
|
||||
- Re-register with new email
|
||||
- Create new deployment
|
||||
|
||||
4. **Use environment variables for Gitea token**
|
||||
```bash
|
||||
# Add to ~/.bashrc or ~/.zshrc
|
||||
export GITEA_API_TOKEN='your_token_here'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
After deployment:
|
||||
|
||||
1. **Configure DNS** (if using custom domain)
|
||||
- Add CNAME record
|
||||
- Point to: `apps.startanaicompany.com`
|
||||
|
||||
2. **Customize your site**
|
||||
- Edit `.env` file
|
||||
- Run `./deploy-to-apps.sh` to update
|
||||
|
||||
3. **Monitor deployment**
|
||||
- Use `--status` mode
|
||||
- Check application logs
|
||||
|
||||
4. **Set up monitoring**
|
||||
- Check site regularly
|
||||
- Monitor uptime
|
||||
- Test features
|
||||
|
||||
---
|
||||
|
||||
## Support
|
||||
|
||||
- **MailHog**: https://mailhog.goryan.io
|
||||
- **Gitea**: https://git.startanaicompany.com
|
||||
- **SAAC API**: https://apps.startanaicompany.com/api/v1
|
||||
|
||||
---
|
||||
|
||||
**Last Updated:** 2026-01-24
|
||||
383
README.md
Normal file
383
README.md
Normal file
@@ -0,0 +1,383 @@
|
||||
# AI Recruitment Site Template
|
||||
|
||||
A fully customizable recruitment website template designed for easy deployment and AI agent modification. Built with Node.js, Express, and PostgreSQL with comprehensive environment-based configuration.
|
||||
|
||||
## 🎯 Use This Template
|
||||
|
||||
This repository is marked as a template. You can create your own recruitment site in minutes!
|
||||
|
||||
### Quick Start (3 Steps)
|
||||
|
||||
1. **Use this template** - Click "Use this template" on Gitea
|
||||
2. **Customize** - Edit `.env` file with your company information
|
||||
3. **Deploy** - Run the automated deployment script
|
||||
|
||||
## ✨ Features
|
||||
|
||||
### Public Features
|
||||
- Job listings with search and filters
|
||||
- Online application system with CV upload
|
||||
- Contact form
|
||||
- Fully responsive design
|
||||
- Dynamic branding and content
|
||||
|
||||
### Admin Features
|
||||
- Dashboard with statistics
|
||||
- Application management
|
||||
- CV downloads and viewing
|
||||
- Job posting management
|
||||
- First-time admin account creation
|
||||
|
||||
### Template Features
|
||||
- **Environment-based configuration** - All customization via `.env` file
|
||||
- **Dynamic branding** - Company name, colors, contact info all configurable
|
||||
- **AI agent friendly** - Clear structure for automated modifications
|
||||
- **One-click deployment** - Automated deployment to StartAnAiCompany infrastructure
|
||||
- **No hardcoded values** - Everything is configurable
|
||||
|
||||
## 🚀 Quick Deployment
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Gitea account at git.startanaicompany.com
|
||||
- SAAC API key (get from apps.startanaicompany.com)
|
||||
- Basic command line knowledge
|
||||
|
||||
**📖 Detailed Guide:** See [DEPLOYMENT_GUIDE.md](DEPLOYMENT_GUIDE.md) for complete instructions.
|
||||
|
||||
### Step 1: Create Your Copy
|
||||
|
||||
Use Gitea's template feature to create your own copy:
|
||||
|
||||
```bash
|
||||
# Or clone the template manually
|
||||
git clone https://git.startanaicompany.com/StartanAICompany/ai-recruit-site-template.git my-recruit-site
|
||||
cd my-recruit-site
|
||||
```
|
||||
|
||||
### Step 2: Customize Configuration
|
||||
|
||||
```bash
|
||||
# Copy the example environment file
|
||||
cp .env.example .env
|
||||
|
||||
# Edit .env with your company information
|
||||
nano .env
|
||||
```
|
||||
|
||||
**Minimum required configuration in `.env`:**
|
||||
|
||||
```bash
|
||||
# Company Information
|
||||
COMPANY_NAME="Your Recruitment Firm"
|
||||
COMPANY_TAGLINE="Your Tagline Here"
|
||||
COMPANY_DESCRIPTION="Your company description"
|
||||
|
||||
# Branding Colors
|
||||
PRIMARY_COLOR=#2563EB
|
||||
ACCENT_COLOR=#059669
|
||||
DARK_COLOR=#1E293B
|
||||
|
||||
# Contact Information
|
||||
CONTACT_EMAIL=info@yourcompany.com
|
||||
CONTACT_PHONE=+1 (555) 123-4567
|
||||
CONTACT_ADDRESS=123 Business St, City, State 12345
|
||||
|
||||
# Deployment Configuration
|
||||
SUBDOMAIN=yourname
|
||||
GITEA_USERNAME=your-gitea-username
|
||||
GITEA_REPO_NAME=my-recruit-site
|
||||
```
|
||||
|
||||
### Step 3: Register for API Key
|
||||
|
||||
Register once to get your deployment API key:
|
||||
|
||||
```bash
|
||||
curl -X POST https://apps.startanaicompany.com/api/v1/register \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"email":"your@email.com","gitea_username":"'${GITEA_USERNAME}'"}'
|
||||
```
|
||||
|
||||
Save the returned API key:
|
||||
```bash
|
||||
export SAAC_API_KEY="cw_your_api_key_here"
|
||||
```
|
||||
|
||||
### Step 4: Deploy
|
||||
|
||||
```bash
|
||||
# Copy and run the deployment script
|
||||
cp deploy-to-apps.example.sh deploy-to-apps.sh
|
||||
chmod +x deploy-to-apps.sh
|
||||
./deploy-to-apps.sh
|
||||
```
|
||||
|
||||
The script will:
|
||||
- Create application on StartAnAiCompany infrastructure
|
||||
- Configure domain (yournamerecruit.startanaicompany.com)
|
||||
- Set up webhooks for automatic deployments
|
||||
- Trigger initial deployment
|
||||
|
||||
**Your site will be live in 2-3 minutes!**
|
||||
|
||||
**See [DEPLOYMENT_GUIDE.md](DEPLOYMENT_GUIDE.md) for detailed instructions and troubleshooting.**
|
||||
|
||||
## 🎨 Customization Guide
|
||||
|
||||
### Company Information
|
||||
|
||||
All company information is configured via environment variables in `.env`:
|
||||
|
||||
```bash
|
||||
# Company Identity
|
||||
COMPANY_NAME="Your Recruitment Firm"
|
||||
COMPANY_TAGLINE="Finding the Perfect Match for Your Career"
|
||||
COMPANY_DESCRIPTION="We specialize in connecting talented professionals..."
|
||||
|
||||
# Contact Details
|
||||
CONTACT_EMAIL=info@yourcompany.com
|
||||
CONTACT_PHONE=+1 (555) 123-4567
|
||||
CONTACT_ADDRESS=123 Business St, Suite 100, City, State 12345
|
||||
BUSINESS_HOURS=Monday - Friday: 9:00 AM - 6:00 PM
|
||||
|
||||
# Social Media (leave empty to hide)
|
||||
SOCIAL_LINKEDIN=https://linkedin.com/company/yourcompany
|
||||
SOCIAL_TWITTER=https://twitter.com/yourcompany
|
||||
SOCIAL_FACEBOOK=
|
||||
```
|
||||
|
||||
### Branding Colors
|
||||
|
||||
Customize your brand colors:
|
||||
|
||||
```bash
|
||||
PRIMARY_COLOR=#2563EB # Main brand color (buttons, links)
|
||||
ACCENT_COLOR=#059669 # Success/accent color
|
||||
DARK_COLOR=#1E293B # Dark elements (footer, headings)
|
||||
```
|
||||
|
||||
Colors update automatically across the entire site!
|
||||
|
||||
### About Page Content
|
||||
|
||||
```bash
|
||||
ABOUT_MISSION=Our mission statement here
|
||||
ABOUT_VISION=Our vision statement here
|
||||
ABOUT_VALUES=Integrity, Excellence, Innovation, Partnership
|
||||
```
|
||||
|
||||
### Services Offered
|
||||
|
||||
```bash
|
||||
SERVICES_LIST=Executive Search,Contract Staffing,Permanent Placement,Career Consulting
|
||||
```
|
||||
|
||||
### Feature Configuration
|
||||
|
||||
```bash
|
||||
MAX_CV_SIZE_MB=5
|
||||
ALLOWED_CV_TYPES=application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document
|
||||
```
|
||||
|
||||
## 🤖 AI Agent Modification
|
||||
|
||||
This template is designed to be easily modified by AI agents like Claude Code. The structure includes:
|
||||
|
||||
### Clear File Organization
|
||||
|
||||
```
|
||||
ai-recruit-site-template/
|
||||
├── config.js # Centralized configuration
|
||||
├── server.js # Main application
|
||||
├── public/
|
||||
│ ├── js/
|
||||
│ │ └── init.js # Dynamic config injection
|
||||
│ ├── css/
|
||||
│ │ └── styles.css # CSS with variables
|
||||
│ └── *.html # HTML with data attributes
|
||||
├── migrations/ # Database migrations
|
||||
└── .env.example # Configuration template
|
||||
```
|
||||
|
||||
### Data Attributes
|
||||
|
||||
HTML elements use data attributes for dynamic content:
|
||||
|
||||
```html
|
||||
<div data-company-name>Your Company</div>
|
||||
<h1 data-company-tagline>Your Tagline</h1>
|
||||
<a data-contact-email>email@example.com</a>
|
||||
```
|
||||
|
||||
### Configuration API
|
||||
|
||||
Frontend can access configuration via API:
|
||||
|
||||
```javascript
|
||||
// GET /api/config returns:
|
||||
{
|
||||
company: { name, tagline, description },
|
||||
branding: { primaryColor, accentColor, darkColor },
|
||||
contact: { email, phone, address, businessHours },
|
||||
social: { linkedin, twitter, facebook },
|
||||
about: { mission, vision, values },
|
||||
services: { list: [...] }
|
||||
}
|
||||
```
|
||||
|
||||
## 💻 Local Development
|
||||
|
||||
### With Docker (Recommended)
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# Edit .env with your settings
|
||||
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
Access at: http://localhost:3000
|
||||
|
||||
### Without Docker
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Start PostgreSQL (must be running)
|
||||
# Create database 'recruitment'
|
||||
|
||||
# Copy environment file
|
||||
cp .env.example .env
|
||||
|
||||
# Start application
|
||||
npm start
|
||||
```
|
||||
|
||||
## 📊 Technology Stack
|
||||
|
||||
- **Backend**: Node.js 18 + Express
|
||||
- **Database**: PostgreSQL 15 with BYTEA for CV storage
|
||||
- **Authentication**: Session-based with bcrypt
|
||||
- **File Upload**: Multer (PDF, DOC, DOCX)
|
||||
- **Deployment**: Docker Compose + StartAnAiCompany platform
|
||||
- **DNS**: Cloudflare
|
||||
|
||||
## 🔐 Security Features
|
||||
|
||||
- Password hashing with bcrypt (10 rounds)
|
||||
- Secure HTTP-only session cookies
|
||||
- SQL injection protection (parameterized queries)
|
||||
- File upload validation (type and size)
|
||||
- Environment-based secrets
|
||||
- Automatic HTTPS in production
|
||||
|
||||
## 📚 API Documentation
|
||||
|
||||
### Public APIs
|
||||
|
||||
- `GET /api/config` - Get site configuration
|
||||
- `GET /api/jobs` - List active jobs
|
||||
- `GET /api/jobs/:id` - Get job details
|
||||
- `POST /api/apply` - Submit application (multipart/form-data)
|
||||
- `POST /api/contact` - Submit contact form
|
||||
|
||||
### Admin APIs (Authentication Required)
|
||||
|
||||
- `POST /api/admin/login` - Login / Create first admin
|
||||
- `GET /api/admin/check` - Check auth status
|
||||
- `POST /api/admin/logout` - Logout
|
||||
- `GET /api/admin/stats` - Dashboard statistics
|
||||
- `GET /api/admin/applications` - List applications (with filters)
|
||||
- `GET /api/admin/applications/:id` - Get application details
|
||||
- `GET /api/admin/applications/:id/cv` - Download CV
|
||||
- `PATCH /api/admin/applications/:id` - Update application status
|
||||
- `GET /api/admin/jobs` - List all jobs
|
||||
- `POST /api/admin/jobs` - Create job
|
||||
- `PATCH /api/admin/jobs/:id` - Update job (activate/deactivate)
|
||||
|
||||
## 🆘 Troubleshooting
|
||||
|
||||
### Deployment Issues
|
||||
|
||||
```bash
|
||||
# Check deployment logs via SAAC API
|
||||
curl -H "X-API-Key: $SAAC_API_KEY" \
|
||||
"https://apps.startanaicompany.com/api/v1/applications/YOUR_APP_UUID/logs?tail=100"
|
||||
|
||||
# Re-trigger deployment
|
||||
curl -X POST -H "X-API-Key: $SAAC_API_KEY" \
|
||||
https://apps.startanaicompany.com/api/v1/applications/YOUR_APP_UUID/deploy
|
||||
```
|
||||
|
||||
### Database Issues
|
||||
|
||||
```bash
|
||||
# Check database connection
|
||||
docker-compose logs postgres
|
||||
|
||||
# Access database
|
||||
docker-compose exec postgres psql -U postgres -d recruitment
|
||||
|
||||
# List tables
|
||||
\dt
|
||||
```
|
||||
|
||||
### Configuration Not Updating
|
||||
|
||||
Make sure you:
|
||||
1. Updated `.env` file
|
||||
2. Committed and pushed changes (triggers webhook)
|
||||
3. Waited 2-3 minutes for redeployment
|
||||
|
||||
## 🎓 First Time Setup
|
||||
|
||||
1. Visit your deployment URL
|
||||
2. Go to `/admin/login`
|
||||
3. System detects no admin exists
|
||||
4. Fill in email, password, and full name
|
||||
5. Click "Create Admin Account"
|
||||
6. You're logged in!
|
||||
|
||||
## 📝 Environment Variables Reference
|
||||
|
||||
| Variable | Description | Default | Required |
|
||||
|----------|-------------|---------|----------|
|
||||
| `COMPANY_NAME` | Your company name | Your Recruitment Firm | Yes |
|
||||
| `COMPANY_TAGLINE` | Main tagline | Finding the Perfect Match... | Yes |
|
||||
| `COMPANY_DESCRIPTION` | Company description | We specialize in... | Yes |
|
||||
| `PRIMARY_COLOR` | Primary brand color | #2563EB | No |
|
||||
| `ACCENT_COLOR` | Accent color | #059669 | No |
|
||||
| `DARK_COLOR` | Dark color | #1E293B | No |
|
||||
| `CONTACT_EMAIL` | Contact email | info@yourcompany.com | Yes |
|
||||
| `CONTACT_PHONE` | Contact phone | +1 (555) 123-4567 | Yes |
|
||||
| `CONTACT_ADDRESS` | Physical address | 123 Business St... | Yes |
|
||||
| `BUSINESS_HOURS` | Business hours | Monday - Friday... | No |
|
||||
| `SOCIAL_LINKEDIN` | LinkedIn URL | (empty) | No |
|
||||
| `SOCIAL_TWITTER` | Twitter URL | (empty) | No |
|
||||
| `SOCIAL_FACEBOOK` | Facebook URL | (empty) | No |
|
||||
| `SUBDOMAIN` | Your subdomain | yourname | Yes (deploy) |
|
||||
| `GITEA_USERNAME` | Your Gitea username | - | Yes (deploy) |
|
||||
| `GITEA_REPO_NAME` | Your repo name | - | Yes (deploy) |
|
||||
| `DB_PASSWORD` | Database password | (auto-generated) | No |
|
||||
| `SESSION_SECRET` | Session secret | (auto-generated) | No |
|
||||
| `MAX_CV_SIZE_MB` | Max CV file size | 5 | No |
|
||||
| `ALLOWED_CV_TYPES` | Allowed CV MIME types | pdf,doc,docx | No |
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
This is a template repository. Feel free to:
|
||||
- Fork it for your own recruitment site
|
||||
- Customize it to your needs
|
||||
- Submit improvements via pull request
|
||||
|
||||
## 📄 License
|
||||
|
||||
MIT License - feel free to use for commercial projects
|
||||
|
||||
## 🆘 Support
|
||||
|
||||
For issues or questions:
|
||||
- Check the [How to set up a template repo guide](/home/milko/projects/airepotemplates/How-to-set-up-a-template-repo.md)
|
||||
- Contact: info@startanaicompany.com
|
||||
96
config.js
Normal file
96
config.js
Normal file
@@ -0,0 +1,96 @@
|
||||
// Configuration module for AI Recruitment Site Template
|
||||
// Centralizes all environment variable loading with sensible defaults
|
||||
|
||||
const crypto = require('crypto');
|
||||
|
||||
// Helper function to generate secure random strings
|
||||
function generateSecret(length = 64) {
|
||||
return crypto.randomBytes(length).toString('hex');
|
||||
}
|
||||
|
||||
// Company Information
|
||||
const company = {
|
||||
name: process.env.COMPANY_NAME || 'Your Recruitment Firm',
|
||||
tagline: process.env.COMPANY_TAGLINE || 'Finding the Perfect Match for Your Career',
|
||||
description: process.env.COMPANY_DESCRIPTION || 'We specialize in connecting talented professionals with exceptional opportunities across various industries.'
|
||||
};
|
||||
|
||||
// Branding Colors
|
||||
const branding = {
|
||||
primaryColor: process.env.PRIMARY_COLOR || '#2563EB',
|
||||
accentColor: process.env.ACCENT_COLOR || '#059669',
|
||||
darkColor: process.env.DARK_COLOR || '#1E293B'
|
||||
};
|
||||
|
||||
// Contact Information
|
||||
const contact = {
|
||||
email: process.env.CONTACT_EMAIL || 'info@yourcompany.com',
|
||||
phone: process.env.CONTACT_PHONE || '+1 (555) 123-4567',
|
||||
address: process.env.CONTACT_ADDRESS || '123 Business St, Suite 100, City, State 12345',
|
||||
businessHours: process.env.BUSINESS_HOURS || 'Monday - Friday: 9:00 AM - 6:00 PM'
|
||||
};
|
||||
|
||||
// Social Media Links
|
||||
const social = {
|
||||
linkedin: process.env.SOCIAL_LINKEDIN || '',
|
||||
twitter: process.env.SOCIAL_TWITTER || '',
|
||||
facebook: process.env.SOCIAL_FACEBOOK || ''
|
||||
};
|
||||
|
||||
// Application Settings
|
||||
const app = {
|
||||
nodeEnv: process.env.NODE_ENV || 'development',
|
||||
port: parseInt(process.env.PORT || '3000', 10),
|
||||
sessionSecret: process.env.SESSION_SECRET || generateSecret()
|
||||
};
|
||||
|
||||
// Database Configuration
|
||||
const database = {
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
port: parseInt(process.env.DB_PORT || '5432', 10),
|
||||
name: process.env.DB_NAME || 'recruitment',
|
||||
user: process.env.DB_USER || 'postgres',
|
||||
password: process.env.DB_PASSWORD || 'changeme123'
|
||||
};
|
||||
|
||||
// Feature Configuration
|
||||
const features = {
|
||||
maxCvSizeMB: parseInt(process.env.MAX_CV_SIZE_MB || '5', 10),
|
||||
allowedCvTypes: (process.env.ALLOWED_CV_TYPES || 'application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document').split(',')
|
||||
};
|
||||
|
||||
// About Page Content
|
||||
const about = {
|
||||
mission: process.env.ABOUT_MISSION || 'Our mission is to bridge the gap between exceptional talent and outstanding opportunities.',
|
||||
vision: process.env.ABOUT_VISION || 'We envision a world where every professional finds their perfect career match.',
|
||||
values: process.env.ABOUT_VALUES || 'Integrity, Excellence, Innovation, Partnership'
|
||||
};
|
||||
|
||||
// Services
|
||||
const services = {
|
||||
list: (process.env.SERVICES_LIST || 'Executive Search,Contract Staffing,Permanent Placement,Career Consulting,Talent Assessment,Industry Expertise').split(',')
|
||||
};
|
||||
|
||||
// Email Configuration
|
||||
const email = {
|
||||
smtpHost: process.env.SMTP_HOST || '',
|
||||
smtpPort: parseInt(process.env.SMTP_PORT || '587', 10),
|
||||
smtpUser: process.env.SMTP_USER || '',
|
||||
smtpPassword: process.env.SMTP_PASSWORD || '',
|
||||
smtpFrom: process.env.SMTP_FROM || 'noreply@yourcompany.com',
|
||||
contactFormRecipient: process.env.CONTACT_FORM_RECIPIENT || contact.email
|
||||
};
|
||||
|
||||
// Export configuration object
|
||||
module.exports = {
|
||||
company,
|
||||
branding,
|
||||
contact,
|
||||
social,
|
||||
app,
|
||||
database,
|
||||
features,
|
||||
about,
|
||||
services,
|
||||
email
|
||||
};
|
||||
813
deploy-to-apps.example.sh
Normal file
813
deploy-to-apps.example.sh
Normal file
@@ -0,0 +1,813 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ========================================
|
||||
# AI Recruitment Site - SAAC Deployment Script
|
||||
# ========================================
|
||||
# This script deploys your recruitment site to StartAnAiCompany infrastructure
|
||||
# via apps.startanaicompany.com
|
||||
#
|
||||
# IMPORTANT: Copy this file to deploy-to-apps.sh and customize it
|
||||
# DO NOT commit deploy-to-apps.sh to git (it's in .gitignore)
|
||||
#
|
||||
# Prerequisites:
|
||||
# 1. Install jq (JSON processor): apt install jq or brew install jq
|
||||
# 2. Set GITEA_API_TOKEN environment variable (required for setup mode)
|
||||
# 3. Customize your .env file with company information
|
||||
#
|
||||
# First-time users:
|
||||
# - Script will prompt for email and handle registration/verification automatically
|
||||
# - Check MailHog at https://mailhog.goryan.io for verification code
|
||||
# - Configuration saved to .saac.json (do not commit to git!)
|
||||
#
|
||||
# Modes:
|
||||
# ./deploy-to-apps.sh # Auto-detect (setup or update)
|
||||
# ./deploy-to-apps.sh --setup # Force new deployment
|
||||
# ./deploy-to-apps.sh --update # Force update existing
|
||||
# ./deploy-to-apps.sh --status # Check deployment status
|
||||
|
||||
set -e # Exit on error
|
||||
|
||||
# Configuration
|
||||
SAAC_CONFIG_FILE=".saac.json"
|
||||
SAAC_API="https://apps.startanaicompany.com/api/v1"
|
||||
GITEA_API="https://git.startanaicompany.com/api/v1"
|
||||
|
||||
# ========================================
|
||||
# Helper Functions
|
||||
# ========================================
|
||||
|
||||
# Load configuration from .saac.json
|
||||
load_config() {
|
||||
if [ -f "$SAAC_CONFIG_FILE" ]; then
|
||||
USER_EMAIL=$(jq -r '.user.email // ""' "$SAAC_CONFIG_FILE")
|
||||
USER_ID=$(jq -r '.user.user_id // ""' "$SAAC_CONFIG_FILE")
|
||||
SAAC_API_KEY=$(jq -r '.user.api_key // ""' "$SAAC_CONFIG_FILE")
|
||||
GITEA_USERNAME=$(jq -r '.user.gitea_username // ""' "$SAAC_CONFIG_FILE")
|
||||
VERIFIED=$(jq -r '.user.verified // false' "$SAAC_CONFIG_FILE")
|
||||
APP_UUID=$(jq -r '.deployment.application_uuid // ""' "$SAAC_CONFIG_FILE")
|
||||
APP_NAME=$(jq -r '.deployment.application_name // ""' "$SAAC_CONFIG_FILE")
|
||||
DEPLOYED_AT=$(jq -r '.deployment.deployed_at // ""' "$SAAC_CONFIG_FILE")
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# Save configuration to .saac.json
|
||||
save_config() {
|
||||
cat > "$SAAC_CONFIG_FILE" <<CONFIG
|
||||
{
|
||||
"version": "1.0",
|
||||
"user": {
|
||||
"email": "$USER_EMAIL",
|
||||
"user_id": "$USER_ID",
|
||||
"api_key": "$SAAC_API_KEY",
|
||||
"gitea_username": "$GITEA_USERNAME",
|
||||
"verified": $VERIFIED,
|
||||
"registered_at": "$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")"
|
||||
},
|
||||
"deployment": {
|
||||
"application_uuid": "$APP_UUID",
|
||||
"application_name": "$APP_NAME",
|
||||
"domain": "$DOMAIN",
|
||||
"subdomain": "$SUBDOMAIN",
|
||||
"repository": "$REPO_URL",
|
||||
"deployed_at": "$DEPLOYED_AT",
|
||||
"last_updated": "$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")"
|
||||
},
|
||||
"config": {
|
||||
"saac_api": "$SAAC_API",
|
||||
"gitea_api": "$GITEA_API"
|
||||
}
|
||||
}
|
||||
CONFIG
|
||||
chmod 600 "$SAAC_CONFIG_FILE"
|
||||
echo "💾 Configuration saved to $SAAC_CONFIG_FILE"
|
||||
}
|
||||
|
||||
# ========================================
|
||||
# Check Dependencies
|
||||
# ========================================
|
||||
|
||||
# Check if jq is installed
|
||||
if ! command -v jq >/dev/null 2>&1; then
|
||||
echo "❌ Error: jq is not installed"
|
||||
echo ""
|
||||
echo "jq is required for JSON processing. Install it with:"
|
||||
echo " - Ubuntu/Debian: sudo apt install jq"
|
||||
echo " - macOS: brew install jq"
|
||||
echo " - Alpine: apk add jq"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ========================================
|
||||
# Parse Command Line Arguments
|
||||
# ========================================
|
||||
|
||||
MODE="auto"
|
||||
REGISTER_EMAIL=""
|
||||
REGISTER_GITEA_USERNAME=""
|
||||
VERIFY_CODE_PARAM=""
|
||||
|
||||
# Parse arguments
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--setup)
|
||||
MODE="setup"
|
||||
shift
|
||||
;;
|
||||
--update)
|
||||
MODE="update"
|
||||
shift
|
||||
;;
|
||||
--status)
|
||||
MODE="status"
|
||||
shift
|
||||
;;
|
||||
--register)
|
||||
MODE="registration"
|
||||
shift
|
||||
;;
|
||||
--email)
|
||||
REGISTER_EMAIL="$2"
|
||||
shift 2
|
||||
;;
|
||||
--gitea-username)
|
||||
REGISTER_GITEA_USERNAME="$2"
|
||||
shift 2
|
||||
;;
|
||||
--verify-email-code)
|
||||
VERIFY_CODE_PARAM="$2"
|
||||
shift 2
|
||||
;;
|
||||
--help)
|
||||
echo "Usage: $0 [OPTIONS]"
|
||||
echo ""
|
||||
echo "Modes:"
|
||||
echo " (default) Auto-detect mode (registration/verification/setup/update)"
|
||||
echo " --setup Force new deployment"
|
||||
echo " --update Force update existing deployment"
|
||||
echo " --status Check deployment status"
|
||||
echo " --register Force registration mode"
|
||||
echo ""
|
||||
echo "Registration Options:"
|
||||
echo " --email EMAIL Email for registration"
|
||||
echo " --gitea-username USERNAME Gitea username (optional, auto-detected from git)"
|
||||
echo " --verify-email-code CODE Verification code from email (for automation)"
|
||||
echo ""
|
||||
echo "Environment Variables Required:"
|
||||
echo " GITEA_API_TOKEN Gitea API token (for webhook setup)"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " # Interactive registration (prompts for email and verification code)"
|
||||
echo " $0"
|
||||
echo ""
|
||||
echo " # Step 1: Non-interactive registration (returns and waits)"
|
||||
echo " $0 --register --email user@example.com"
|
||||
echo ""
|
||||
echo " # Step 2: Verify with code (after checking MailHog)"
|
||||
echo " $0 --verify-email-code 123456"
|
||||
echo ""
|
||||
echo " # Full non-interactive (if you already have the code)"
|
||||
echo " $0 --register --email user@example.com --verify-email-code 123456"
|
||||
echo ""
|
||||
echo " # Update existing deployment"
|
||||
echo " $0 --update"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "❌ Unknown option: $1"
|
||||
echo "Run '$0 --help' for usage information"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ========================================
|
||||
# Load Environment Variables
|
||||
# ========================================
|
||||
|
||||
if [ ! -f .env ]; then
|
||||
echo "❌ Error: .env file not found. Copy .env.example to .env and customize it."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
source .env
|
||||
|
||||
# Check required .env variables
|
||||
if [ -z "$COMPANY_NAME" ]; then
|
||||
echo "❌ Error: COMPANY_NAME not set in .env"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$SUBDOMAIN" ]; then
|
||||
echo "❌ Error: SUBDOMAIN not set in .env"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ========================================
|
||||
# Load or Create Configuration
|
||||
# ========================================
|
||||
|
||||
# Try to load existing configuration
|
||||
if load_config; then
|
||||
echo "✅ Loaded configuration from $SAAC_CONFIG_FILE"
|
||||
echo " User: $USER_EMAIL"
|
||||
echo " Verified: $VERIFIED"
|
||||
if [ -n "$APP_UUID" ]; then
|
||||
echo " Application: $APP_UUID"
|
||||
fi
|
||||
echo ""
|
||||
else
|
||||
# No configuration exists - need to register
|
||||
echo "========================================="
|
||||
echo " First-Time Setup"
|
||||
echo "========================================="
|
||||
echo ""
|
||||
echo "No configuration found. Let's register your account."
|
||||
echo ""
|
||||
|
||||
# Get Gitea username from .env or parameter
|
||||
if [ -n "$REGISTER_GITEA_USERNAME" ]; then
|
||||
GITEA_USERNAME="$REGISTER_GITEA_USERNAME"
|
||||
elif [ -z "$GITEA_USERNAME" ]; then
|
||||
# Try to auto-detect from git config
|
||||
GITEA_USERNAME=$(git config user.name 2>/dev/null || echo "")
|
||||
if [ -z "$GITEA_USERNAME" ]; then
|
||||
echo "❌ Error: GITEA_USERNAME not provided"
|
||||
echo ""
|
||||
echo "Provide it via:"
|
||||
echo " 1. --gitea-username parameter: $0 --register --email EMAIL --gitea-username USERNAME"
|
||||
echo " 2. GITEA_USERNAME in .env file"
|
||||
echo " 3. Git config: git config user.name"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Get email from parameter (no prompting)
|
||||
if [ -n "$REGISTER_EMAIL" ]; then
|
||||
USER_EMAIL="$REGISTER_EMAIL"
|
||||
echo "📧 Using email: $USER_EMAIL"
|
||||
else
|
||||
echo "❌ Error: Email address is required"
|
||||
echo ""
|
||||
echo "Usage:"
|
||||
echo " $0 --register --email YOUR_EMAIL"
|
||||
echo ""
|
||||
echo "Example:"
|
||||
echo " $0 --register --email ryan.andersson@goryan.io"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "📧 Registering user: $USER_EMAIL"
|
||||
echo " Gitea username: $GITEA_USERNAME"
|
||||
echo ""
|
||||
|
||||
# Register user
|
||||
REGISTER_RESPONSE=$(curl -s -X POST "${SAAC_API}/users/register" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"email\":\"$USER_EMAIL\",\"gitea_username\":\"$GITEA_USERNAME\"}")
|
||||
|
||||
# Check for errors
|
||||
if echo "$REGISTER_RESPONSE" | grep -q "error"; then
|
||||
echo "❌ Registration failed:"
|
||||
echo "$REGISTER_RESPONSE" | jq '.' 2>/dev/null || echo "$REGISTER_RESPONSE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract user data
|
||||
USER_ID=$(echo "$REGISTER_RESPONSE" | jq -r '.user_id')
|
||||
EMAIL_VERIFIED=$(echo "$REGISTER_RESPONSE" | jq -r '.email_verified // false')
|
||||
SAAC_API_KEY=$(echo "$REGISTER_RESPONSE" | jq -r '.api_key // ""')
|
||||
EXISTING_USER=$(echo "$REGISTER_RESPONSE" | jq -r '.existing_user // false')
|
||||
VERIFICATION_SENT=$(echo "$REGISTER_RESPONSE" | jq -r '.verification_code_sent // false')
|
||||
|
||||
if [ "$USER_ID" = "null" ] || [ -z "$USER_ID" ]; then
|
||||
echo "❌ Failed to extract user data from registration response"
|
||||
echo "Response: $REGISTER_RESPONSE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if user is verified and has API key
|
||||
if [ "$EMAIL_VERIFIED" = "true" ] && [ -n "$SAAC_API_KEY" ] && [ "$SAAC_API_KEY" != "null" ]; then
|
||||
# Email verification disabled OR user already verified - API key provided
|
||||
echo "✅ User registered!"
|
||||
echo " User ID: $USER_ID"
|
||||
echo " 🔑 API key received"
|
||||
echo " Email verified: true"
|
||||
else
|
||||
# Email verification required
|
||||
if [ "$EXISTING_USER" = "true" ]; then
|
||||
echo "ℹ️ User already exists but not verified"
|
||||
echo " User ID: $USER_ID"
|
||||
|
||||
if [ "$VERIFICATION_SENT" = "true" ]; then
|
||||
echo " ✅ New verification code sent!"
|
||||
else
|
||||
echo " ⚠️ Failed to send verification email"
|
||||
fi
|
||||
|
||||
# Load existing API key from config if it exists
|
||||
if [ -f "$SAAC_CONFIG_FILE" ]; then
|
||||
EXISTING_API_KEY=$(jq -r '.user.api_key // ""' "$SAAC_CONFIG_FILE")
|
||||
if [ -n "$EXISTING_API_KEY" ] && [ "$EXISTING_API_KEY" != "null" ]; then
|
||||
SAAC_API_KEY="$EXISTING_API_KEY"
|
||||
echo " 🔑 Using existing API key from config"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
# New user with email verification enabled
|
||||
echo "✅ User registered!"
|
||||
echo " User ID: $USER_ID"
|
||||
echo " Email verified: false"
|
||||
echo " Verification code sent: $VERIFICATION_SENT"
|
||||
echo ""
|
||||
|
||||
# No API key yet - will get after verification
|
||||
SAAC_API_KEY=""
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# Save initial configuration (unverified)
|
||||
VERIFIED=false
|
||||
APP_UUID=""
|
||||
APP_NAME=""
|
||||
DOMAIN=""
|
||||
REPO_URL=""
|
||||
DEPLOYED_AT=""
|
||||
save_config
|
||||
echo ""
|
||||
|
||||
# Prompt for verification code
|
||||
echo "========================================="
|
||||
echo " Email Verification Required"
|
||||
echo "========================================="
|
||||
echo ""
|
||||
|
||||
if [ "$EXISTING_USER" = "true" ]; then
|
||||
echo "📧 New verification code sent to: $USER_EMAIL"
|
||||
else
|
||||
echo "📧 Verification email sent to: $USER_EMAIL"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "🔍 Check your email at MailHog:"
|
||||
echo " https://mailhog.goryan.io"
|
||||
echo ""
|
||||
|
||||
# Use parameter if provided, otherwise exit with instructions
|
||||
if [ -n "$VERIFY_CODE_PARAM" ]; then
|
||||
VERIFY_CODE="$VERIFY_CODE_PARAM"
|
||||
echo "🔐 Using verification code from parameter"
|
||||
else
|
||||
echo "❌ Verification code required"
|
||||
echo ""
|
||||
echo "Check MailHog for your verification code, then run:"
|
||||
echo " $0 --verify-email-code YOUR_CODE"
|
||||
echo ""
|
||||
echo "Configuration saved to $SAAC_CONFIG_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "🔐 Verifying email..."
|
||||
echo " User ID: $USER_ID"
|
||||
echo " Code: $VERIFY_CODE"
|
||||
|
||||
# Verify email (public endpoint - no API key needed)
|
||||
VERIFY_RESPONSE=$(curl -s -X POST "${SAAC_API}/users/verify" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"user_id\":\"$USER_ID\",\"verification_code\":\"$VERIFY_CODE\"}")
|
||||
|
||||
# Check if verified
|
||||
if echo "$VERIFY_RESPONSE" | grep -q '"verified":true'; then
|
||||
echo "✅ Email verified successfully!"
|
||||
echo ""
|
||||
|
||||
# Extract API key from verification response
|
||||
VERIFIED_API_KEY=$(echo "$VERIFY_RESPONSE" | jq -r '.api_key // ""')
|
||||
if [ -n "$VERIFIED_API_KEY" ] && [ "$VERIFIED_API_KEY" != "null" ]; then
|
||||
SAAC_API_KEY="$VERIFIED_API_KEY"
|
||||
echo "🔑 API key received and saved"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
VERIFIED=true
|
||||
save_config
|
||||
echo ""
|
||||
else
|
||||
echo "❌ Verification failed"
|
||||
echo "$VERIFY_RESPONSE" | jq '.' 2>/dev/null || echo "$VERIFY_RESPONSE"
|
||||
echo ""
|
||||
echo "You can verify later by running this script again."
|
||||
echo "Your configuration has been saved to $SAAC_CONFIG_FILE"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# ========================================
|
||||
# Check Verification Status
|
||||
# ========================================
|
||||
|
||||
if [ "$VERIFIED" != "true" ]; then
|
||||
echo "========================================="
|
||||
echo " Email Verification Required"
|
||||
echo "========================================="
|
||||
echo ""
|
||||
echo "Your email ($USER_EMAIL) is not verified yet."
|
||||
echo ""
|
||||
echo "🔍 Check your email at MailHog:"
|
||||
echo " https://mailhog.goryan.io"
|
||||
echo ""
|
||||
|
||||
# Use parameter if provided, otherwise exit with instructions
|
||||
if [ -n "$VERIFY_CODE_PARAM" ]; then
|
||||
VERIFY_CODE="$VERIFY_CODE_PARAM"
|
||||
echo "🔐 Using verification code from parameter"
|
||||
else
|
||||
echo "❌ Verification code required"
|
||||
echo ""
|
||||
echo "Run with: $0 --verify-email-code YOUR_CODE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "🔐 Verifying email..."
|
||||
echo " User ID: $USER_ID"
|
||||
echo " Code: $VERIFY_CODE"
|
||||
|
||||
# Verify email (public endpoint - no API key needed)
|
||||
VERIFY_RESPONSE=$(curl -s -X POST "${SAAC_API}/users/verify" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"user_id\":\"$USER_ID\",\"verification_code\":\"$VERIFY_CODE\"}")
|
||||
|
||||
# Check if verified
|
||||
if echo "$VERIFY_RESPONSE" | grep -q '"verified":true'; then
|
||||
echo "✅ Email verified successfully!"
|
||||
echo ""
|
||||
|
||||
# Extract API key from verification response
|
||||
VERIFIED_API_KEY=$(echo "$VERIFY_RESPONSE" | jq -r '.api_key // ""')
|
||||
if [ -n "$VERIFIED_API_KEY" ] && [ "$VERIFIED_API_KEY" != "null" ]; then
|
||||
SAAC_API_KEY="$VERIFIED_API_KEY"
|
||||
echo "🔑 API key received and saved"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
VERIFIED=true
|
||||
save_config
|
||||
echo ""
|
||||
else
|
||||
echo "❌ Verification failed"
|
||||
echo "$VERIFY_RESPONSE" | jq '.' 2>/dev/null || echo "$VERIFY_RESPONSE"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# ========================================
|
||||
# Auto-detect Mode
|
||||
# ========================================
|
||||
|
||||
if [ "$MODE" = "auto" ]; then
|
||||
if [ -n "$APP_UUID" ]; then
|
||||
MODE="update"
|
||||
else
|
||||
MODE="setup"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ========================================
|
||||
# Setup Mode: Additional Checks
|
||||
# ========================================
|
||||
|
||||
if [ "$MODE" = "setup" ]; then
|
||||
# GITEA_API_TOKEN only required for setup mode (webhook creation)
|
||||
if [ -z "$GITEA_API_TOKEN" ]; then
|
||||
echo "❌ Error: GITEA_API_TOKEN environment variable not set"
|
||||
echo ""
|
||||
echo "GITEA_API_TOKEN is required for setup mode to:"
|
||||
echo " - Set up automatic deployment webhooks"
|
||||
echo ""
|
||||
echo "To get your Gitea API token:"
|
||||
echo "1. Go to https://git.startanaicompany.com"
|
||||
echo "2. Click your profile → Settings → Applications"
|
||||
echo "3. Generate New Token (grant 'repo' permissions)"
|
||||
echo "4. Export it: export GITEA_API_TOKEN='your_token_here'"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$GITEA_REPO_NAME" ]; then
|
||||
echo "❌ Error: GITEA_REPO_NAME not set in .env"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# ========================================
|
||||
# Build URLs
|
||||
# ========================================
|
||||
|
||||
# Repository URL (SSH format required by Coolify for private repos)
|
||||
REPO_URL="git@git.startanaicompany.com:${GITEA_USERNAME}/${GITEA_REPO_NAME}.git"
|
||||
|
||||
# Domain (e.g., annarecruit.startanaicompany.com or johnrecruit.startanaicompany.com)
|
||||
FULL_DOMAIN="${SUBDOMAIN}recruit.startanaicompany.com"
|
||||
|
||||
# ========================================
|
||||
# MODE: STATUS
|
||||
# ========================================
|
||||
if [ "$MODE" = "status" ]; then
|
||||
echo "========================================="
|
||||
echo " Deployment Status"
|
||||
echo "========================================="
|
||||
echo ""
|
||||
|
||||
# Check if deployment exists
|
||||
if [ -z "$APP_UUID" ]; then
|
||||
echo "❌ No deployment found"
|
||||
echo " Run './deploy-to-apps.sh --setup' to create a new deployment"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "📦 Fetching status for application: $APP_UUID"
|
||||
echo ""
|
||||
|
||||
# Get application details
|
||||
STATUS_RESPONSE=$(curl -s -X GET "${SAAC_API}/applications/${APP_UUID}" \
|
||||
-H "X-API-Key: ${SAAC_API_KEY}")
|
||||
|
||||
# Check for errors
|
||||
if echo "$STATUS_RESPONSE" | grep -q "error"; then
|
||||
echo "❌ Failed to fetch status:"
|
||||
echo "$STATUS_RESPONSE" | jq '.' 2>/dev/null || echo "$STATUS_RESPONSE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Display formatted status
|
||||
echo "Application UUID: $APP_UUID"
|
||||
echo "Name: $(echo "$STATUS_RESPONSE" | jq -r '.app_name')"
|
||||
echo "Domain: $(echo "$STATUS_RESPONSE" | jq -r '.domain')"
|
||||
echo "Status: $(echo "$STATUS_RESPONSE" | jq -r '.status')"
|
||||
echo "Repository: $(echo "$STATUS_RESPONSE" | jq -r '.git_repo')"
|
||||
echo "Branch: $(echo "$STATUS_RESPONSE" | jq -r '.git_branch')"
|
||||
echo "Created: $(echo "$STATUS_RESPONSE" | jq -r '.created_at')"
|
||||
echo ""
|
||||
echo "🔍 View logs:"
|
||||
echo " curl -H \"X-API-Key: ${SAAC_API_KEY}\" \\"
|
||||
echo " ${SAAC_API}/applications/${APP_UUID}/logs"
|
||||
echo ""
|
||||
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ========================================
|
||||
# MODE: UPDATE
|
||||
# ========================================
|
||||
if [ "$MODE" = "update" ]; then
|
||||
echo "========================================="
|
||||
echo " AI Recruitment Site Deployment"
|
||||
echo "========================================="
|
||||
echo "📝 Mode: UPDATE (existing deployment)"
|
||||
echo ""
|
||||
|
||||
# Check if deployment exists
|
||||
if [ -z "$APP_UUID" ]; then
|
||||
echo "❌ No deployment found in configuration"
|
||||
echo " Run './deploy-to-apps.sh --setup' to create a new deployment"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "📦 Configuration:"
|
||||
echo " Application UUID: $APP_UUID"
|
||||
echo " Company: $COMPANY_NAME"
|
||||
echo " Domain: https://$FULL_DOMAIN"
|
||||
echo ""
|
||||
|
||||
# Verify application still exists
|
||||
echo "🔍 Verifying application exists..."
|
||||
VERIFY_RESPONSE=$(curl -s -X GET "${SAAC_API}/applications/${APP_UUID}" \
|
||||
-H "X-API-Key: ${SAAC_API_KEY}")
|
||||
|
||||
if echo "$VERIFY_RESPONSE" | grep -q "error"; then
|
||||
echo "❌ Application not found or access denied"
|
||||
echo " The application may have been deleted or UUID is invalid"
|
||||
echo " Run './deploy-to-apps.sh --setup' to create a new deployment"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Application verified"
|
||||
echo ""
|
||||
|
||||
# Update environment variables
|
||||
echo "🔄 Updating environment variables..."
|
||||
UPDATE_RESPONSE=$(curl -s -X PATCH "${SAAC_API}/applications/${APP_UUID}/env" \
|
||||
-H "X-API-Key: ${SAAC_API_KEY}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"variables\": {
|
||||
\"COMPANY_NAME\": \"${COMPANY_NAME}\",
|
||||
\"COMPANY_TAGLINE\": \"${COMPANY_TAGLINE}\",
|
||||
\"COMPANY_DESCRIPTION\": \"${COMPANY_DESCRIPTION}\",
|
||||
\"PRIMARY_COLOR\": \"${PRIMARY_COLOR}\",
|
||||
\"ACCENT_COLOR\": \"${ACCENT_COLOR}\",
|
||||
\"DARK_COLOR\": \"${DARK_COLOR}\",
|
||||
\"CONTACT_EMAIL\": \"${CONTACT_EMAIL}\",
|
||||
\"CONTACT_PHONE\": \"${CONTACT_PHONE}\",
|
||||
\"CONTACT_ADDRESS\": \"${CONTACT_ADDRESS}\"
|
||||
}
|
||||
}")
|
||||
|
||||
# Check for errors
|
||||
if echo "$UPDATE_RESPONSE" | grep -q "error"; then
|
||||
echo "❌ Failed to update environment variables:"
|
||||
echo "$UPDATE_RESPONSE" | jq '.' 2>/dev/null || echo "$UPDATE_RESPONSE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Environment variables updated"
|
||||
echo ""
|
||||
|
||||
# Trigger redeployment
|
||||
echo "🚀 Triggering redeployment..."
|
||||
DEPLOY_RESPONSE=$(curl -s -X POST "${SAAC_API}/applications/${APP_UUID}/deploy" \
|
||||
-H "X-API-Key: ${SAAC_API_KEY}")
|
||||
|
||||
# Check for errors
|
||||
if echo "$DEPLOY_RESPONSE" | grep -q "error"; then
|
||||
echo "❌ Failed to trigger deployment:"
|
||||
echo "$DEPLOY_RESPONSE" | jq '.' 2>/dev/null || echo "$DEPLOY_RESPONSE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Deployment triggered"
|
||||
echo ""
|
||||
|
||||
# Update configuration with latest timestamp
|
||||
DOMAIN="$FULL_DOMAIN"
|
||||
save_config
|
||||
echo ""
|
||||
|
||||
echo "========================================="
|
||||
echo " Update Complete!"
|
||||
echo "========================================="
|
||||
echo ""
|
||||
echo "⏳ Your changes will be live in 2-3 minutes at:"
|
||||
echo " https://$FULL_DOMAIN"
|
||||
echo ""
|
||||
echo "🔍 Monitor deployment:"
|
||||
echo " ./deploy-to-apps.sh --status"
|
||||
echo ""
|
||||
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ========================================
|
||||
# MODE: SETUP
|
||||
# ========================================
|
||||
echo "========================================="
|
||||
echo " AI Recruitment Site Deployment"
|
||||
echo "========================================="
|
||||
echo "📝 Mode: SETUP (new deployment)"
|
||||
echo ""
|
||||
echo "📦 Configuration:"
|
||||
echo " Company: $COMPANY_NAME"
|
||||
echo " Repository: $REPO_URL"
|
||||
echo " Domain: https://$FULL_DOMAIN"
|
||||
echo ""
|
||||
|
||||
# Warn if deployment already exists
|
||||
if [ -n "$APP_UUID" ]; then
|
||||
echo "⚠️ Warning: Existing deployment found in configuration"
|
||||
echo " Current Application UUID: $APP_UUID"
|
||||
echo " This will create a NEW deployment and overwrite the configuration."
|
||||
echo ""
|
||||
read -p "Continue? (y/N): " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "Cancelled."
|
||||
exit 0
|
||||
fi
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Create application via SAAC API
|
||||
echo "📝 Creating application on StartAnAiCompany server..."
|
||||
APP_RESPONSE=$(curl -s -X POST "${SAAC_API}/applications" \
|
||||
-H "X-API-Key: ${SAAC_API_KEY}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"name\": \"${SUBDOMAIN}-recruit\",
|
||||
\"subdomain\": \"${SUBDOMAIN}\",
|
||||
\"domain_suffix\": \"recruit.startanaicompany.com\",
|
||||
\"git_repository\": \"${REPO_URL}\",
|
||||
\"git_branch\": \"master\",
|
||||
\"gitea_api_token\": \"${GITEA_API_TOKEN}\",
|
||||
\"template_type\": \"recruitment\",
|
||||
\"environment_variables\": {
|
||||
\"COMPANY_NAME\": \"${COMPANY_NAME}\",
|
||||
\"COMPANY_TAGLINE\": \"${COMPANY_TAGLINE}\",
|
||||
\"COMPANY_DESCRIPTION\": \"${COMPANY_DESCRIPTION}\",
|
||||
\"PRIMARY_COLOR\": \"${PRIMARY_COLOR}\",
|
||||
\"ACCENT_COLOR\": \"${ACCENT_COLOR}\",
|
||||
\"DARK_COLOR\": \"${DARK_COLOR}\",
|
||||
\"CONTACT_EMAIL\": \"${CONTACT_EMAIL}\",
|
||||
\"CONTACT_PHONE\": \"${CONTACT_PHONE}\",
|
||||
\"CONTACT_ADDRESS\": \"${CONTACT_ADDRESS}\"
|
||||
}
|
||||
}")
|
||||
|
||||
# Check for errors
|
||||
if echo "$APP_RESPONSE" | grep -q "error"; then
|
||||
echo "❌ Deployment failed:"
|
||||
echo "$APP_RESPONSE" | jq '.' 2>/dev/null || echo "$APP_RESPONSE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract application details
|
||||
APP_UUID=$(echo "$APP_RESPONSE" | jq -r '.application_uuid' 2>/dev/null)
|
||||
DOMAIN=$(echo "$APP_RESPONSE" | jq -r '.domain' 2>/dev/null)
|
||||
WEBHOOK_URL=$(echo "$APP_RESPONSE" | jq -r '.webhook_url' 2>/dev/null)
|
||||
|
||||
if [ "$APP_UUID" = "null" ] || [ -z "$APP_UUID" ]; then
|
||||
echo "❌ Failed to create application"
|
||||
echo "Response: $APP_RESPONSE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Application created!"
|
||||
echo " Application UUID: $APP_UUID"
|
||||
echo " Domain: $DOMAIN"
|
||||
echo ""
|
||||
|
||||
# Save configuration
|
||||
APP_NAME="${SUBDOMAIN}-recruit"
|
||||
DEPLOYED_AT=$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")
|
||||
save_config
|
||||
echo ""
|
||||
|
||||
# Configure webhook for automatic deployments
|
||||
echo "🪝 Setting up deployment webhook..."
|
||||
|
||||
WEBHOOK_RESPONSE=$(curl -s -X POST "${GITEA_API}/repos/${GITEA_USERNAME}/${GITEA_REPO_NAME}/hooks" \
|
||||
-H "Authorization: token ${GITEA_API_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"type\": \"gitea\",
|
||||
\"config\": {
|
||||
\"url\": \"${WEBHOOK_URL}\",
|
||||
\"content_type\": \"json\",
|
||||
\"http_method\": \"GET\"
|
||||
},
|
||||
\"events\": [\"push\"],
|
||||
\"authorization_header\": \"Bearer ${SAAC_API_KEY}\",
|
||||
\"active\": true
|
||||
}")
|
||||
|
||||
WEBHOOK_ID=$(echo "$WEBHOOK_RESPONSE" | jq -r '.id' 2>/dev/null)
|
||||
|
||||
if [ "$WEBHOOK_ID" = "null" ] || [ -z "$WEBHOOK_ID" ]; then
|
||||
echo "⚠️ Warning: Failed to create Gitea webhook (may already exist)"
|
||||
else
|
||||
echo "✅ Webhook configured for automatic deployments"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
echo "========================================="
|
||||
echo " Deployment Complete!"
|
||||
echo "========================================="
|
||||
echo ""
|
||||
echo "📋 Deployment Details:"
|
||||
echo " Application UUID: $APP_UUID"
|
||||
echo " Domain: $DOMAIN"
|
||||
echo ""
|
||||
echo "⏳ Your site will be available in 2-3 minutes at:"
|
||||
echo " $DOMAIN"
|
||||
echo ""
|
||||
echo "📝 Next Steps:"
|
||||
echo " 1. Configure DNS (if not already done):"
|
||||
echo " - For Cloudflare: Add CNAME record"
|
||||
echo " - Name: ${SUBDOMAIN}recruit"
|
||||
echo " - Target: apps.startanaicompany.com"
|
||||
echo " - Proxy: Enabled"
|
||||
echo ""
|
||||
echo " 2. Wait 2-3 minutes for deployment to complete"
|
||||
echo ""
|
||||
echo " 3. Visit your site:"
|
||||
echo " $DOMAIN"
|
||||
echo ""
|
||||
echo "🔄 To update your deployment:"
|
||||
echo " 1. Edit .env file with new values"
|
||||
echo " 2. Run: ./deploy-to-apps.sh"
|
||||
echo " (Auto-detects update mode since UUID file exists)"
|
||||
echo ""
|
||||
echo "🔍 Monitor deployment:"
|
||||
echo " ./deploy-to-apps.sh --status"
|
||||
echo ""
|
||||
echo "💾 Configuration saved to:"
|
||||
echo " - $SAAC_CONFIG_FILE (user credentials and deployment info)"
|
||||
echo " ⚠️ Keep this file secure and do NOT commit it to git!"
|
||||
echo ""
|
||||
51
docker-compose.yml
Normal file
51
docker-compose.yml
Normal file
@@ -0,0 +1,51 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15-alpine
|
||||
container_name: recruitment-postgres
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: ${DB_NAME:-recruitment}
|
||||
POSTGRES_USER: ${DB_USER:-postgres}
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD:-changeme123}
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
ports:
|
||||
- "5432:5432"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
networks:
|
||||
- recruitment-network
|
||||
|
||||
app:
|
||||
build: .
|
||||
container_name: recruitment-app
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${PORT:-3000}:3000"
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
PORT: 3000
|
||||
DB_HOST: postgres
|
||||
DB_PORT: 5432
|
||||
DB_NAME: ${DB_NAME:-recruitment}
|
||||
DB_USER: ${DB_USER:-postgres}
|
||||
DB_PASSWORD: ${DB_PASSWORD:-changeme123}
|
||||
SESSION_SECRET: ${SESSION_SECRET:-change-this-secret-in-production}
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- recruitment-network
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
recruitment-network:
|
||||
driver: bridge
|
||||
150
migrations/001_init_schema.sql
Normal file
150
migrations/001_init_schema.sql
Normal file
@@ -0,0 +1,150 @@
|
||||
-- AI Recruitment Site Database Schema
|
||||
-- Using PostgreSQL with proper indexing and constraints
|
||||
|
||||
-- Admins table
|
||||
CREATE TABLE IF NOT EXISTS admins (
|
||||
id SERIAL PRIMARY KEY,
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
full_name VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
last_login TIMESTAMP WITH TIME ZONE
|
||||
);
|
||||
|
||||
-- Create index for faster email lookups
|
||||
CREATE INDEX IF NOT EXISTS idx_admins_email ON admins(email);
|
||||
|
||||
-- Job postings table
|
||||
CREATE TABLE IF NOT EXISTS job_postings (
|
||||
id SERIAL PRIMARY KEY,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
department VARCHAR(100),
|
||||
location VARCHAR(255),
|
||||
employment_type VARCHAR(50), -- Full-time, Part-time, Contract
|
||||
salary_range VARCHAR(100),
|
||||
description TEXT NOT NULL,
|
||||
requirements TEXT,
|
||||
benefits TEXT,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by INTEGER REFERENCES admins(id)
|
||||
);
|
||||
|
||||
-- Create index for active jobs
|
||||
CREATE INDEX IF NOT EXISTS idx_job_postings_active ON job_postings(is_active, created_at DESC);
|
||||
|
||||
-- Applicants table
|
||||
CREATE TABLE IF NOT EXISTS applicants (
|
||||
id SERIAL PRIMARY KEY,
|
||||
full_name VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(255) NOT NULL,
|
||||
phone VARCHAR(50),
|
||||
linkedin_url VARCHAR(500),
|
||||
portfolio_url VARCHAR(500),
|
||||
years_of_experience INTEGER,
|
||||
current_position VARCHAR(255),
|
||||
current_company VARCHAR(255),
|
||||
preferred_location VARCHAR(255),
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Create index for applicant searches
|
||||
CREATE INDEX IF NOT EXISTS idx_applicants_email ON applicants(email);
|
||||
CREATE INDEX IF NOT EXISTS idx_applicants_name ON applicants(full_name);
|
||||
CREATE INDEX IF NOT EXISTS idx_applicants_created_at ON applicants(created_at DESC);
|
||||
|
||||
-- Applications table (links applicants to jobs with CV)
|
||||
CREATE TABLE IF NOT EXISTS applications (
|
||||
id SERIAL PRIMARY KEY,
|
||||
applicant_id INTEGER NOT NULL REFERENCES applicants(id) ON DELETE CASCADE,
|
||||
job_id INTEGER REFERENCES job_postings(id) ON DELETE SET NULL,
|
||||
cover_letter TEXT,
|
||||
cv_filename VARCHAR(500),
|
||||
cv_content_type VARCHAR(100),
|
||||
cv_file BYTEA, -- Store CV as binary data
|
||||
status VARCHAR(50) DEFAULT 'new', -- new, reviewing, interview, rejected, hired
|
||||
notes TEXT,
|
||||
applied_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Create indexes for application queries
|
||||
CREATE INDEX IF NOT EXISTS idx_applications_applicant ON applications(applicant_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_applications_job ON applications(job_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_applications_status ON applications(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_applications_date ON applications(applied_at DESC);
|
||||
|
||||
-- Add check constraint for application status
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint
|
||||
WHERE conname = 'applications_status_check'
|
||||
) THEN
|
||||
ALTER TABLE applications ADD CONSTRAINT applications_status_check
|
||||
CHECK (status IN ('new', 'reviewing', 'interview', 'rejected', 'hired'));
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Add check constraint for employment type
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint
|
||||
WHERE conname = 'job_postings_employment_type_check'
|
||||
) THEN
|
||||
ALTER TABLE job_postings ADD CONSTRAINT job_postings_employment_type_check
|
||||
CHECK (employment_type IN ('Full-time', 'Part-time', 'Contract', 'Internship', 'Freelance'));
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Contact submissions table
|
||||
CREATE TABLE IF NOT EXISTS contact_submissions (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(255) NOT NULL,
|
||||
subject VARCHAR(500),
|
||||
message TEXT NOT NULL,
|
||||
is_read BOOLEAN DEFAULT false,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Create index for contact submissions
|
||||
CREATE INDEX IF NOT EXISTS idx_contact_submissions_date ON contact_submissions(created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_contact_submissions_unread ON contact_submissions(is_read, created_at DESC);
|
||||
|
||||
-- Function to update the updated_at timestamp
|
||||
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = CURRENT_TIMESTAMP;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ language 'plpgsql';
|
||||
|
||||
-- Trigger for job_postings
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_trigger WHERE tgname = 'update_job_postings_updated_at'
|
||||
) THEN
|
||||
CREATE TRIGGER update_job_postings_updated_at
|
||||
BEFORE UPDATE ON job_postings
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column();
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Trigger for applications
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_trigger WHERE tgname = 'update_applications_updated_at'
|
||||
) THEN
|
||||
CREATE TRIGGER update_applications_updated_at
|
||||
BEFORE UPDATE ON applications
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column();
|
||||
END IF;
|
||||
END $$;
|
||||
113
migrations/002_seed_data.sql
Normal file
113
migrations/002_seed_data.sql
Normal file
@@ -0,0 +1,113 @@
|
||||
-- Seed data for AI Recruitment Site
|
||||
|
||||
-- Insert sample job postings
|
||||
INSERT INTO job_postings (title, department, location, employment_type, salary_range, description, requirements, benefits, is_active)
|
||||
VALUES
|
||||
(
|
||||
'Senior Full-Stack Developer',
|
||||
'Engineering',
|
||||
'Remote / San Francisco, CA',
|
||||
'Full-time',
|
||||
'$120,000 - $180,000',
|
||||
'We are seeking an experienced Full-Stack Developer to join our dynamic team. You will be responsible for designing, developing, and maintaining web applications that serve thousands of users daily. This role offers an opportunity to work with cutting-edge technologies and contribute to meaningful projects.',
|
||||
'• 5+ years of experience in full-stack development
|
||||
• Proficiency in JavaScript/TypeScript, React, Node.js
|
||||
• Experience with PostgreSQL or similar relational databases
|
||||
• Strong understanding of RESTful APIs and microservices
|
||||
• Experience with Docker and cloud platforms (AWS/GCP/Azure)
|
||||
• Excellent problem-solving and communication skills',
|
||||
'• Competitive salary and equity package
|
||||
• Health, dental, and vision insurance
|
||||
• 401(k) matching
|
||||
• Flexible work arrangements
|
||||
• Professional development budget
|
||||
• Unlimited PTO',
|
||||
true
|
||||
),
|
||||
(
|
||||
'DevOps Engineer',
|
||||
'Engineering',
|
||||
'New York, NY',
|
||||
'Full-time',
|
||||
'$100,000 - $150,000',
|
||||
'Join our infrastructure team to build and maintain scalable, reliable systems. You will work on automation, monitoring, and deployment pipelines that power our applications.',
|
||||
'• 3+ years of DevOps or Site Reliability Engineering experience
|
||||
• Strong knowledge of Kubernetes, Docker, and container orchestration
|
||||
• Experience with CI/CD tools (Jenkins, GitLab CI, GitHub Actions)
|
||||
• Proficiency in scripting languages (Python, Bash)
|
||||
• Experience with infrastructure as code (Terraform, Ansible)
|
||||
• Understanding of cloud platforms and networking',
|
||||
'• Competitive compensation
|
||||
• Stock options
|
||||
• Remote work flexibility
|
||||
• Learning and development opportunities
|
||||
• Team events and offsites
|
||||
• Modern tech stack',
|
||||
true
|
||||
),
|
||||
(
|
||||
'UI/UX Designer',
|
||||
'Design',
|
||||
'Los Angeles, CA',
|
||||
'Full-time',
|
||||
'$90,000 - $130,000',
|
||||
'We are looking for a creative UI/UX Designer to craft beautiful and intuitive user experiences. You will work closely with product managers and engineers to bring ideas to life.',
|
||||
'• 3+ years of UI/UX design experience
|
||||
• Strong portfolio demonstrating web and mobile design
|
||||
• Proficiency in Figma, Sketch, or Adobe XD
|
||||
• Understanding of user-centered design principles
|
||||
• Experience conducting user research and usability testing
|
||||
• Knowledge of HTML/CSS is a plus',
|
||||
'• Creative and collaborative work environment
|
||||
• Health and wellness benefits
|
||||
• Flexible schedule
|
||||
• Latest design tools and equipment
|
||||
• Conference and workshop budget
|
||||
• Generous vacation policy',
|
||||
true
|
||||
),
|
||||
(
|
||||
'Data Scientist',
|
||||
'Data & Analytics',
|
||||
'Remote',
|
||||
'Full-time',
|
||||
'$110,000 - $160,000',
|
||||
'Help us unlock insights from data. As a Data Scientist, you will develop machine learning models, analyze complex datasets, and contribute to data-driven decision making.',
|
||||
'• Master''s or PhD in Computer Science, Statistics, or related field
|
||||
• 2+ years of experience in data science or machine learning
|
||||
• Strong programming skills in Python (pandas, scikit-learn, TensorFlow)
|
||||
• Experience with SQL and data warehousing
|
||||
• Knowledge of statistical modeling and A/B testing
|
||||
• Excellent communication skills to explain technical concepts',
|
||||
'• Competitive salary
|
||||
• Work with cutting-edge ML technologies
|
||||
• Conference attendance opportunities
|
||||
• Remote-first culture
|
||||
• Comprehensive health benefits
|
||||
• Equity compensation',
|
||||
true
|
||||
),
|
||||
(
|
||||
'Marketing Manager',
|
||||
'Marketing',
|
||||
'Chicago, IL',
|
||||
'Full-time',
|
||||
'$80,000 - $120,000',
|
||||
'Lead our marketing initiatives and help grow our brand. You will develop and execute marketing strategies, manage campaigns, and analyze performance metrics.',
|
||||
'• 5+ years of marketing experience
|
||||
• Proven track record in B2B or B2C marketing
|
||||
• Experience with digital marketing channels (SEO, SEM, social media)
|
||||
• Strong analytical skills and data-driven mindset
|
||||
• Excellent written and verbal communication
|
||||
• Experience with marketing automation tools',
|
||||
'• Competitive salary and bonuses
|
||||
• Health insurance
|
||||
• Professional development
|
||||
• Collaborative team environment
|
||||
• Flexible work options
|
||||
• Career growth opportunities',
|
||||
true
|
||||
);
|
||||
|
||||
-- Note: Admin users are created on first login, no seed data needed
|
||||
-- Note: Applicants and applications will be created through the application form
|
||||
26
package.json
Normal file
26
package.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "ai-recruit-site",
|
||||
"version": "1.0.0",
|
||||
"description": "AI Recruitment Site for Ryans Recruit Firm",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"dev": "node server.js"
|
||||
},
|
||||
"keywords": [
|
||||
"recruitment",
|
||||
"jobs",
|
||||
"hr",
|
||||
"postgres"
|
||||
],
|
||||
"author": "Ryans Recruit Firm",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"pg": "^8.11.3",
|
||||
"bcrypt": "^5.1.1",
|
||||
"express-session": "^1.17.3",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"cookie-parser": "^1.4.6"
|
||||
}
|
||||
}
|
||||
178
public/about.html
Normal file
178
public/about.html
Normal file
@@ -0,0 +1,178 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>About Us - Ryans Recruit Firm</title>
|
||||
<link rel="stylesheet" href="/css/styles.css">
|
||||
<script src="/js/init.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<nav class="container">
|
||||
<div class="logo" data-company-name>Ryans Recruit Firm</div>
|
||||
<ul class="nav-links">
|
||||
<li><a href="/">Home</a></li>
|
||||
<li><a href="/about" class="active">About</a></li>
|
||||
<li><a href="/services">Services</a></li>
|
||||
<li><a href="/jobs">Jobs</a></li>
|
||||
<li><a href="/contact">Contact</a></li>
|
||||
<li><a href="/admin/login" class="btn btn-primary btn-sm">Admin</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<section class="hero">
|
||||
<div class="container">
|
||||
<h1>About Ryans Recruit Firm</h1>
|
||||
<p>Building careers and companies through exceptional talent placement</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="container-narrow">
|
||||
<h2>Our Story</h2>
|
||||
<p>Founded in 2015, Ryans Recruit Firm has grown to become one of the most trusted names in professional recruitment. What started as a small team with a vision has evolved into a comprehensive recruitment solution serving clients across multiple industries worldwide.</p>
|
||||
|
||||
<p>Our founder, Ryan Martinez, saw a gap in the recruitment industry: too many firms focused on filling positions quickly rather than finding the right match. With a background in HR and a passion for helping people find meaningful work, Ryan set out to create a different kind of recruitment firm—one that prioritizes quality, transparency, and long-term success.</p>
|
||||
|
||||
<p>Today, we're proud to have helped thousands of professionals find their dream jobs and assisted hundreds of companies in building exceptional teams.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section style="background: var(--bg-white);">
|
||||
<div class="container">
|
||||
<div class="section-title">
|
||||
<h2>Our Values</h2>
|
||||
<p>The principles that guide everything we do</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-2">
|
||||
<div class="card">
|
||||
<h3>🎯 Excellence</h3>
|
||||
<p>We maintain the highest standards in candidate screening, client service, and professional conduct. Mediocrity is not in our vocabulary.</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>🤝 Integrity</h3>
|
||||
<p>Honesty and transparency in all our dealings. We build trust through consistent, ethical behavior and open communication.</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>💡 Innovation</h3>
|
||||
<p>We leverage the latest technology and methodologies to improve our recruitment process and deliver better results.</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>❤️ Empathy</h3>
|
||||
<p>We understand that behind every resume is a person with dreams, and behind every job posting is a company with needs. We care about both.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="container">
|
||||
<div class="section-title">
|
||||
<h2>By the Numbers</h2>
|
||||
<p>Our track record speaks for itself</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-4">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">5,000+</div>
|
||||
<div class="stat-label">Successful Placements</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">500+</div>
|
||||
<div class="stat-label">Partner Companies</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">95%</div>
|
||||
<div class="stat-label">Client Satisfaction</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">14</div>
|
||||
<div class="stat-label">Average Days to Placement</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section style="background: var(--bg-white);">
|
||||
<div class="container-narrow">
|
||||
<div class="section-title">
|
||||
<h2>Our Mission</h2>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<p style="font-size: 1.125rem; line-height: 1.8; color: var(--text-dark); text-align: center;">
|
||||
To revolutionize the recruitment industry by creating meaningful connections between talented professionals and forward-thinking companies, while maintaining the highest standards of integrity, transparency, and service excellence.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="container text-center">
|
||||
<h2 style="margin-bottom: 1rem;">Ready to Work With Us?</h2>
|
||||
<p style="font-size: 1.125rem; color: var(--text-light); margin-bottom: 2rem;">
|
||||
Whether you're looking for your next career move or seeking top talent for your company
|
||||
</p>
|
||||
<div style="display: flex; gap: 1rem; justify-content: center;">
|
||||
<a href="/jobs" class="btn btn-primary btn-lg">Browse Jobs</a>
|
||||
<a href="/contact" class="btn btn-secondary btn-lg">Contact Us</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<div class="container">
|
||||
<div class="footer-content">
|
||||
<div class="footer-section">
|
||||
<h3 data-company-name>Ryans Recruit Firm</h3>
|
||||
<p style="color: rgba(255, 255, 255, 0.8);">
|
||||
Your trusted partner in career advancement and talent acquisition.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="footer-section">
|
||||
<h3>Quick Links</h3>
|
||||
<ul>
|
||||
<li><a href="/">Home</a></li>
|
||||
<li><a href="/about">About Us</a></li>
|
||||
<li><a href="/services">Services</a></li>
|
||||
<li><a href="/jobs">Job Listings</a></li>
|
||||
<li><a href="/contact">Contact</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="footer-section">
|
||||
<h3>For Employers</h3>
|
||||
<ul>
|
||||
<li><a href="/contact">Post a Job</a></li>
|
||||
<li><a href="/contact">Our Process</a></li>
|
||||
<li><a href="/contact">Pricing</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="footer-section">
|
||||
<h3>Contact</h3>
|
||||
<ul>
|
||||
<li>Email: info@ryansrecruit.com</li>
|
||||
<li>Phone: +1 (555) 123-4567</li>
|
||||
<li>Hours: Mon-Fri 9AM-6PM EST</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer-bottom">
|
||||
<p>© 2026 Ryans Recruit Firm. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
83
public/admin/applicants.html
Normal file
83
public/admin/applicants.html
Normal file
@@ -0,0 +1,83 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Applications - Ryans Recruit Admin</title>
|
||||
<link rel="stylesheet" href="/css/styles.css">
|
||||
<script src="/js/init.js"></script>
|
||||
</head>
|
||||
<body style="background: #F8FAFC; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0;">
|
||||
<!-- Admin Header -->
|
||||
<header style="background: white; border-bottom: 1px solid #E2E8F0; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);">
|
||||
<div style="max-width: 1200px; margin: 0 auto; padding: 16px 24px; display: flex; justify-content: space-between; align-items: center;">
|
||||
<div style="display: flex; align-items: center; gap: 12px;">
|
||||
<div style="width: 32px; height: 32px; background: linear-gradient(135deg, #2563EB 0%, #1D4ED8 100%); border-radius: 8px; display: flex; align-items: center; justify-content: center; color: white; font-weight: bold; font-size: 16px;">R</div>
|
||||
<h1 style="margin: 0; color: #1E293B; font-size: 18px; font-weight: 700;">Ryans Recruit Admin</h1>
|
||||
</div>
|
||||
<nav style="display: flex; gap: 32px; align-items: center;">
|
||||
<a href="/admin/dashboard" style="color: #64748B; text-decoration: none; font-weight: 500; font-size: 14px; transition: color 0.2s;" onmouseover="this.style.color='#2563EB'" onmouseout="this.style.color='#64748B'">Dashboard</a>
|
||||
<a href="/admin/applicants" style="color: #2563EB; text-decoration: none; font-weight: 500; font-size: 14px;">Applicants</a>
|
||||
<a href="/admin/jobs" style="color: #64748B; text-decoration: none; font-weight: 500; font-size: 14px; transition: color 0.2s;" onmouseover="this.style.color='#2563EB'" onmouseout="this.style.color='#64748B'">Jobs</a>
|
||||
<div style="display: flex; align-items: center; gap: 16px; padding-left: 32px; border-left: 1px solid #E2E8F0;">
|
||||
<span id="admin-name" style="color: #64748B; font-size: 14px;">Admin</span>
|
||||
<button onclick="logout()" style="background: #EF4444; color: white; padding: 8px 16px; border: none; border-radius: 6px; font-weight: 500; font-size: 13px; cursor: pointer; transition: background 0.2s;" onmouseover="this.style.background='#DC2626'" onmouseout="this.style.background='#EF4444'">Logout</button>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Hero Section -->
|
||||
<section style="background: linear-gradient(135deg, #2563EB 0%, #1E293B 100%); padding: 48px 24px; text-align: center;">
|
||||
<h2 style="color: white; margin: 0; font-size: 36px; font-weight: 700;">Applications Management</h2>
|
||||
<p style="color: #E0E7FF; margin: 12px 0 0 0; font-size: 16px;">Review and manage all job applications</p>
|
||||
</section>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main style="max-width: 1200px; margin: 0 auto; padding: 40px 24px;">
|
||||
<!-- Filter Section -->
|
||||
<div style="background: white; border-radius: 12px; padding: 24px; margin-bottom: 24px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);">
|
||||
<h3 style="color: #1E293B; margin: 0 0 16px 0; font-size: 16px; font-weight: 600;">Filters</h3>
|
||||
<div style="display: flex; gap: 16px; flex-wrap: wrap;">
|
||||
<div>
|
||||
<label for="status-filter" style="display: block; color: #64748B; font-size: 13px; font-weight: 500; margin-bottom: 6px;">Status</label>
|
||||
<select id="status-filter" style="padding: 8px 12px; border: 1px solid #E2E8F0; border-radius: 6px; font-size: 14px; background: white; cursor: pointer; transition: border-color 0.2s;" onchange="filterApplications(this.value)" onfocus="this.style.borderColor='#2563EB'" onblur="this.style.borderColor='#E2E8F0'">
|
||||
<option value="">All Applications</option>
|
||||
<option value="new">New</option>
|
||||
<option value="reviewed">Reviewed</option>
|
||||
<option value="shortlisted">Shortlisted</option>
|
||||
<option value="rejected">Rejected</option>
|
||||
<option value="accepted">Accepted</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading Spinner -->
|
||||
<div id="loading" style="text-align: center; padding: 48px 24px; display: none;">
|
||||
<div style="display: inline-block;">
|
||||
<div style="width: 40px; height: 40px; border: 4px solid #E2E8F0; border-top: 4px solid #2563EB; border-radius: 50%; animation: spin 1s linear infinite;"></div>
|
||||
</div>
|
||||
<p style="color: #64748B; margin-top: 16px; font-size: 14px;">Loading applications...</p>
|
||||
</div>
|
||||
|
||||
<!-- Applications Container -->
|
||||
<div id="applications-container" style="display: grid; gap: 16px;">
|
||||
<!-- Applications will be populated here by JavaScript -->
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div id="empty-state" style="text-align: center; padding: 60px 24px; background: white; border-radius: 12px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);">
|
||||
<p style="color: #94A3B8; font-size: 16px; margin: 0;">No applications found</p>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
|
||||
<script src="/js/admin.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
89
public/admin/dashboard.html
Normal file
89
public/admin/dashboard.html
Normal file
@@ -0,0 +1,89 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Admin Dashboard - Ryans Recruit</title>
|
||||
<link rel="stylesheet" href="/css/styles.css">
|
||||
<script src="/js/init.js"></script>
|
||||
</head>
|
||||
<body style="background: #F8FAFC; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0;">
|
||||
<!-- Admin Header -->
|
||||
<header style="background: white; border-bottom: 1px solid #E2E8F0; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);">
|
||||
<div style="max-width: 1200px; margin: 0 auto; padding: 16px 24px; display: flex; justify-content: space-between; align-items: center;">
|
||||
<div style="display: flex; align-items: center; gap: 12px;">
|
||||
<div style="width: 32px; height: 32px; background: linear-gradient(135deg, #2563EB 0%, #1D4ED8 100%); border-radius: 8px; display: flex; align-items: center; justify-content: center; color: white; font-weight: bold; font-size: 16px;">R</div>
|
||||
<h1 style="margin: 0; color: #1E293B; font-size: 18px; font-weight: 700;">Ryans Recruit Admin</h1>
|
||||
</div>
|
||||
<nav style="display: flex; gap: 32px; align-items: center;">
|
||||
<a href="/admin/dashboard" style="color: #2563EB; text-decoration: none; font-weight: 500; font-size: 14px;">Dashboard</a>
|
||||
<a href="/admin/applicants" style="color: #64748B; text-decoration: none; font-weight: 500; font-size: 14px; transition: color 0.2s;" onmouseover="this.style.color='#2563EB'" onmouseout="this.style.color='#64748B'">Applicants</a>
|
||||
<a href="/admin/jobs" style="color: #64748B; text-decoration: none; font-weight: 500; font-size: 14px; transition: color 0.2s;" onmouseover="this.style.color='#2563EB'" onmouseout="this.style.color='#64748B'">Jobs</a>
|
||||
<div style="display: flex; align-items: center; gap: 16px; padding-left: 32px; border-left: 1px solid #E2E8F0;">
|
||||
<span id="admin-name" style="color: #64748B; font-size: 14px;">Admin</span>
|
||||
<button onclick="logout()" style="background: #EF4444; color: white; padding: 8px 16px; border: none; border-radius: 6px; font-weight: 500; font-size: 13px; cursor: pointer; transition: background 0.2s;" onmouseover="this.style.background='#DC2626'" onmouseout="this.style.background='#EF4444'">Logout</button>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Hero Section -->
|
||||
<section style="background: linear-gradient(135deg, #2563EB 0%, #1E293B 100%); padding: 48px 24px; text-align: center;">
|
||||
<h2 style="color: white; margin: 0; font-size: 36px; font-weight: 700;">Admin Dashboard</h2>
|
||||
<p style="color: #E0E7FF; margin: 12px 0 0 0; font-size: 16px;">Manage applications, jobs, and track recruitment metrics</p>
|
||||
</section>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main style="max-width: 1200px; margin: 0 auto; padding: 40px 24px;">
|
||||
<!-- Statistics Grid -->
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 20px; margin-bottom: 40px;">
|
||||
<!-- New Applications Card -->
|
||||
<div style="background: white; border-radius: 12px; padding: 24px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); border-left: 4px solid #2563EB;">
|
||||
<p style="color: #64748B; margin: 0 0 8px 0; font-size: 13px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px;">New Applications</p>
|
||||
<p id="stat-new-applications" style="color: #1E293B; margin: 0; font-size: 32px; font-weight: 700;">0</p>
|
||||
<p style="color: #94A3B8; margin: 8px 0 0 0; font-size: 13px;">This week</p>
|
||||
</div>
|
||||
|
||||
<!-- Total Applications Card -->
|
||||
<div style="background: white; border-radius: 12px; padding: 24px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); border-left: 4px solid #059669;">
|
||||
<p style="color: #64748B; margin: 0 0 8px 0; font-size: 13px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px;">Total Applications</p>
|
||||
<p id="stat-total-applications" style="color: #1E293B; margin: 0; font-size: 32px; font-weight: 700;">0</p>
|
||||
<p style="color: #94A3B8; margin: 8px 0 0 0; font-size: 13px;">All time</p>
|
||||
</div>
|
||||
|
||||
<!-- Total Applicants Card -->
|
||||
<div style="background: white; border-radius: 12px; padding: 24px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); border-left: 4px solid #F59E0B;">
|
||||
<p style="color: #64748B; margin: 0 0 8px 0; font-size: 13px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px;">Total Applicants</p>
|
||||
<p id="stat-total-applicants" style="color: #1E293B; margin: 0; font-size: 32px; font-weight: 700;">0</p>
|
||||
<p style="color: #94A3B8; margin: 8px 0 0 0; font-size: 13px;">Unique users</p>
|
||||
</div>
|
||||
|
||||
<!-- Active Jobs Card -->
|
||||
<div style="background: white; border-radius: 12px; padding: 24px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); border-left: 4px solid #8B5CF6;">
|
||||
<p style="color: #64748B; margin: 0 0 8px 0; font-size: 13px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px;">Active Jobs</p>
|
||||
<p id="stat-active-jobs" style="color: #1E293B; margin: 0; font-size: 32px; font-weight: 700;">0</p>
|
||||
<p style="color: #94A3B8; margin: 8px 0 0 0; font-size: 13px;">Open positions</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Links Section -->
|
||||
<section style="background: white; border-radius: 12px; padding: 32px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);">
|
||||
<h3 style="color: #1E293B; margin: 0 0 24px 0; font-size: 18px; font-weight: 700;">Quick Access</h3>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px;">
|
||||
<a href="/admin/applicants" style="display: block; padding: 20px; border: 2px solid #E2E8F0; border-radius: 8px; text-decoration: none; text-align: center; transition: all 0.2s;" onmouseover="this.style.borderColor='#2563EB'; this.style.backgroundColor='#F0F9FF'" onmouseout="this.style.borderColor='#E2E8F0'; this.style.backgroundColor='white'">
|
||||
<p style="color: #2563EB; margin: 0 0 8px 0; font-size: 24px;">📋</p>
|
||||
<p style="color: #1E293B; margin: 0; font-size: 16px; font-weight: 600;">View Applications</p>
|
||||
<p style="color: #64748B; margin: 4px 0 0 0; font-size: 13px;">Manage all applicants</p>
|
||||
</a>
|
||||
<a href="/admin/jobs" style="display: block; padding: 20px; border: 2px solid #E2E8F0; border-radius: 8px; text-decoration: none; text-align: center; transition: all 0.2s;" onmouseover="this.style.borderColor='#059669'; this.style.backgroundColor='#F0FDF4'" onmouseout="this.style.borderColor='#E2E8F0'; this.style.backgroundColor='white'">
|
||||
<p style="color: #059669; margin: 0 0 8px 0; font-size: 24px;">💼</p>
|
||||
<p style="color: #1E293B; margin: 0; font-size: 16px; font-weight: 600;">Manage Jobs</p>
|
||||
<p style="color: #64748B; margin: 4px 0 0 0; font-size: 13px;">Create and edit positions</p>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script src="/js/admin.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
76
public/admin/jobs.html
Normal file
76
public/admin/jobs.html
Normal file
@@ -0,0 +1,76 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Jobs - Ryans Recruit Admin</title>
|
||||
<link rel="stylesheet" href="/css/styles.css">
|
||||
<script src="/js/init.js"></script>
|
||||
</head>
|
||||
<body style="background: #F8FAFC; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0;">
|
||||
<!-- Admin Header -->
|
||||
<header style="background: white; border-bottom: 1px solid #E2E8F0; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);">
|
||||
<div style="max-width: 1200px; margin: 0 auto; padding: 16px 24px; display: flex; justify-content: space-between; align-items: center;">
|
||||
<div style="display: flex; align-items: center; gap: 12px;">
|
||||
<div style="width: 32px; height: 32px; background: linear-gradient(135deg, #2563EB 0%, #1D4ED8 100%); border-radius: 8px; display: flex; align-items: center; justify-content: center; color: white; font-weight: bold; font-size: 16px;">R</div>
|
||||
<h1 style="margin: 0; color: #1E293B; font-size: 18px; font-weight: 700;">Ryans Recruit Admin</h1>
|
||||
</div>
|
||||
<nav style="display: flex; gap: 32px; align-items: center;">
|
||||
<a href="/admin/dashboard" style="color: #64748B; text-decoration: none; font-weight: 500; font-size: 14px; transition: color 0.2s;" onmouseover="this.style.color='#2563EB'" onmouseout="this.style.color='#64748B'">Dashboard</a>
|
||||
<a href="/admin/applicants" style="color: #64748B; text-decoration: none; font-weight: 500; font-size: 14px; transition: color 0.2s;" onmouseover="this.style.color='#2563EB'" onmouseout="this.style.color='#64748B'">Applicants</a>
|
||||
<a href="/admin/jobs" style="color: #2563EB; text-decoration: none; font-weight: 500; font-size: 14px;">Jobs</a>
|
||||
<div style="display: flex; align-items: center; gap: 16px; padding-left: 32px; border-left: 1px solid #E2E8F0;">
|
||||
<span id="admin-name" style="color: #64748B; font-size: 14px;">Admin</span>
|
||||
<button onclick="logout()" style="background: #EF4444; color: white; padding: 8px 16px; border: none; border-radius: 6px; font-weight: 500; font-size: 13px; cursor: pointer; transition: background 0.2s;" onmouseover="this.style.background='#DC2626'" onmouseout="this.style.background='#EF4444'">Logout</button>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Hero Section -->
|
||||
<section style="background: linear-gradient(135deg, #2563EB 0%, #1E293B 100%); padding: 48px 24px; text-align: center;">
|
||||
<h2 style="color: white; margin: 0; font-size: 36px; font-weight: 700;">Job Postings Management</h2>
|
||||
<p style="color: #E0E7FF; margin: 12px 0 0 0; font-size: 16px;">Create, edit, and manage job positions</p>
|
||||
</section>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main style="max-width: 1200px; margin: 0 auto; padding: 40px 24px;">
|
||||
<!-- Action Bar -->
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px;">
|
||||
<h3 style="color: #1E293B; margin: 0; font-size: 18px; font-weight: 700;">All Job Postings</h3>
|
||||
<button onclick="createNewJob()" style="background: linear-gradient(135deg, #059669 0%, #047857 100%); color: white; padding: 10px 20px; border: none; border-radius: 6px; font-weight: 600; font-size: 14px; cursor: pointer; transition: transform 0.2s, box-shadow 0.2s;" onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 8px 16px rgba(5, 150, 105, 0.3)'" onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='none'">
|
||||
+ New Job
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Loading Spinner -->
|
||||
<div id="loading" style="text-align: center; padding: 48px 24px; display: none;">
|
||||
<div style="display: inline-block;">
|
||||
<div style="width: 40px; height: 40px; border: 4px solid #E2E8F0; border-top: 4px solid #2563EB; border-radius: 50%; animation: spin 1s linear infinite;"></div>
|
||||
</div>
|
||||
<p style="color: #64748B; margin-top: 16px; font-size: 14px;">Loading jobs...</p>
|
||||
</div>
|
||||
|
||||
<!-- Jobs Container -->
|
||||
<div id="jobs-container" style="display: grid; gap: 16px;">
|
||||
<!-- Jobs will be populated here by JavaScript -->
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div id="empty-state" style="text-align: center; padding: 60px 24px; background: white; border-radius: 12px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);">
|
||||
<p style="color: #94A3B8; font-size: 16px; margin: 0;">No job postings yet</p>
|
||||
<button onclick="createNewJob()" style="background: linear-gradient(135deg, #059669 0%, #047857 100%); color: white; padding: 10px 20px; border: none; border-radius: 6px; font-weight: 600; font-size: 14px; cursor: pointer; margin-top: 16px; transition: transform 0.2s, box-shadow 0.2s;" onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 8px 16px rgba(5, 150, 105, 0.3)'" onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='none'">
|
||||
Create First Job
|
||||
</button>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
|
||||
<script src="/js/admin.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
83
public/admin/login.html
Normal file
83
public/admin/login.html
Normal file
@@ -0,0 +1,83 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Admin Login - Ryans Recruit</title>
|
||||
<link rel="stylesheet" href="/css/styles.css">
|
||||
<script src="/js/init.js"></script>
|
||||
</head>
|
||||
<body style="background: linear-gradient(135deg, #2563EB 0%, #1E293B 100%); min-height: 100vh; display: flex; align-items: center; justify-content: center; margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;">
|
||||
<div style="background: white; border-radius: 12px; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); padding: 48px 40px; width: 100%; max-width: 400px;">
|
||||
<h1 style="text-align: center; color: #1E293B; margin: 0 0 8px 0; font-size: 28px;">Ryans Recruit</h1>
|
||||
<p style="text-align: center; color: #64748B; margin: 0 0 32px 0; font-size: 14px;">Admin Portal</p>
|
||||
|
||||
<!-- First Admin Notice -->
|
||||
<div id="first-admin-notice" style="background: #DBEAFE; border: 1px solid #93C5FD; border-radius: 8px; padding: 12px; margin-bottom: 24px; color: #1E40AF; font-size: 14px; display: none;">
|
||||
<strong>First Admin Setup:</strong> Create your admin account to get started.
|
||||
</div>
|
||||
|
||||
<form id="login-form" style="display: flex; flex-direction: column; gap: 16px;">
|
||||
<!-- Email Field -->
|
||||
<div>
|
||||
<label for="email" style="display: block; font-weight: 600; color: #1E293B; margin-bottom: 6px; font-size: 14px;">Email</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
required
|
||||
placeholder="admin@example.com"
|
||||
style="width: 100%; padding: 10px 12px; border: 1px solid #E2E8F0; border-radius: 6px; font-size: 14px; box-sizing: border-box; transition: border-color 0.2s;"
|
||||
onfocus="this.style.borderColor='#2563EB'"
|
||||
onblur="this.style.borderColor='#E2E8F0'"
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- Password Field -->
|
||||
<div>
|
||||
<label for="password" style="display: block; font-weight: 600; color: #1E293B; margin-bottom: 6px; font-size: 14px;">Password</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
required
|
||||
placeholder="••••••••"
|
||||
style="width: 100%; padding: 10px 12px; border: 1px solid #E2E8F0; border-radius: 6px; font-size: 14px; box-sizing: border-box; transition: border-color 0.2s;"
|
||||
onfocus="this.style.borderColor='#2563EB'"
|
||||
onblur="this.style.borderColor='#E2E8F0'"
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- Full Name Field (Initially Hidden) -->
|
||||
<div id="full-name-group" style="display: none;">
|
||||
<label for="full-name" style="display: block; font-weight: 600; color: #1E293B; margin-bottom: 6px; font-size: 14px;">Full Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="full-name"
|
||||
name="full-name"
|
||||
placeholder="John Doe"
|
||||
style="width: 100%; padding: 10px 12px; border: 1px solid #E2E8F0; border-radius: 6px; font-size: 14px; box-sizing: border-box; transition: border-color 0.2s;"
|
||||
onfocus="this.style.borderColor='#2563EB'"
|
||||
onblur="this.style.borderColor='#E2E8F0'"
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<button
|
||||
type="submit"
|
||||
style="background: linear-gradient(135deg, #2563EB 0%, #1D4ED8 100%); color: white; padding: 12px; border: none; border-radius: 6px; font-weight: 600; font-size: 14px; cursor: pointer; transition: transform 0.2s, box-shadow 0.2s; margin-top: 8px;"
|
||||
onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 8px 16px rgba(37, 99, 235, 0.3)'"
|
||||
onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='none'"
|
||||
>
|
||||
Sign In
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p style="text-align: center; color: #64748B; font-size: 13px; margin-top: 20px;">
|
||||
Back to <a href="/" style="color: #2563EB; text-decoration: none; font-weight: 500;">Main Site</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<script src="/js/admin.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
172
public/apply.html
Normal file
172
public/apply.html
Normal file
@@ -0,0 +1,172 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Apply for Job - Ryans Recruit Firm</title>
|
||||
<link rel="stylesheet" href="/css/styles.css">
|
||||
<script src="/js/init.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<nav class="container">
|
||||
<div class="logo" data-company-name>Ryans Recruit Firm</div>
|
||||
<ul class="nav-links">
|
||||
<li><a href="/">Home</a></li>
|
||||
<li><a href="/about">About</a></li>
|
||||
<li><a href="/services">Services</a></li>
|
||||
<li><a href="/jobs">Jobs</a></li>
|
||||
<li><a href="/contact">Contact</a></li>
|
||||
<li><a href="/admin/login" class="btn btn-primary btn-sm">Admin</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<section class="hero">
|
||||
<div class="container">
|
||||
<h1>Apply Now</h1>
|
||||
<p>Submit your application for consideration</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="container-narrow">
|
||||
<div id="job-details" style="background: var(--bg-white); padding: 2rem; border-radius: 0.5rem; box-shadow: var(--shadow-md); margin-bottom: 2rem;">
|
||||
<h2 id="job-title" style="color: var(--secondary-color); margin-bottom: 1rem;">Job Title</h2>
|
||||
<div id="job-info" style="color: var(--text-light);">
|
||||
<!-- Job details will be loaded here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="background: var(--bg-white); padding: 2rem; border-radius: 0.5rem; box-shadow: var(--shadow-md);">
|
||||
<h3 style="color: var(--secondary-color); margin-bottom: 2rem;">Application Form</h3>
|
||||
|
||||
<form id="application-form">
|
||||
<!-- Hidden jobId field -->
|
||||
<input type="hidden" id="jobId" name="jobId" value="">
|
||||
|
||||
<!-- Full Name -->
|
||||
<div class="form-group">
|
||||
<label for="fullName">Full Name <span style="color: var(--error);">*</span></label>
|
||||
<input type="text" id="fullName" name="fullName" required placeholder="Enter your full name">
|
||||
</div>
|
||||
|
||||
<!-- Email -->
|
||||
<div class="form-group">
|
||||
<label for="email">Email Address <span style="color: var(--error);">*</span></label>
|
||||
<input type="email" id="email" name="email" required placeholder="your.email@example.com">
|
||||
</div>
|
||||
|
||||
<!-- Phone -->
|
||||
<div class="form-group">
|
||||
<label for="phone">Phone Number</label>
|
||||
<input type="tel" id="phone" name="phone" placeholder="+1 (555) 123-4567">
|
||||
</div>
|
||||
|
||||
<!-- LinkedIn URL -->
|
||||
<div class="form-group">
|
||||
<label for="linkedinUrl">LinkedIn URL</label>
|
||||
<input type="url" id="linkedinUrl" name="linkedinUrl" placeholder="https://linkedin.com/in/yourprofile">
|
||||
</div>
|
||||
|
||||
<!-- Portfolio URL -->
|
||||
<div class="form-group">
|
||||
<label for="portfolioUrl">Portfolio URL</label>
|
||||
<input type="url" id="portfolioUrl" name="portfolioUrl" placeholder="https://yourportfolio.com">
|
||||
</div>
|
||||
|
||||
<!-- Years of Experience -->
|
||||
<div class="form-group">
|
||||
<label for="yearsExperience">Years of Experience</label>
|
||||
<input type="number" id="yearsExperience" name="yearsExperience" min="0" max="70" placeholder="0">
|
||||
</div>
|
||||
|
||||
<!-- Current Position -->
|
||||
<div class="form-group">
|
||||
<label for="currentPosition">Current Position</label>
|
||||
<input type="text" id="currentPosition" name="currentPosition" placeholder="e.g., Senior Software Engineer">
|
||||
</div>
|
||||
|
||||
<!-- Current Company -->
|
||||
<div class="form-group">
|
||||
<label for="currentCompany">Current Company</label>
|
||||
<input type="text" id="currentCompany" name="currentCompany" placeholder="e.g., Tech Corp Inc.">
|
||||
</div>
|
||||
|
||||
<!-- Preferred Location -->
|
||||
<div class="form-group">
|
||||
<label for="preferredLocation">Preferred Location</label>
|
||||
<input type="text" id="preferredLocation" name="preferredLocation" placeholder="e.g., New York, NY or Remote">
|
||||
</div>
|
||||
|
||||
<!-- Cover Letter -->
|
||||
<div class="form-group">
|
||||
<label for="coverLetter">Cover Letter</label>
|
||||
<textarea id="coverLetter" name="coverLetter" placeholder="Tell us why you're interested in this position and what makes you a great fit..."></textarea>
|
||||
</div>
|
||||
|
||||
<!-- CV Upload -->
|
||||
<div class="form-group">
|
||||
<label for="cv">Upload CV/Resume <span style="color: var(--error);">*</span></label>
|
||||
<input type="file" id="cv" name="cv" accept=".pdf,.doc,.docx" required>
|
||||
<div class="form-help">Accepted formats: PDF, DOC, DOCX (Max 5MB)</div>
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<div class="form-group" style="margin-top: 2rem;">
|
||||
<button type="submit" class="btn btn-primary btn-lg" style="width: 100%;">Submit Application</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<div class="container">
|
||||
<div class="footer-content">
|
||||
<div class="footer-section">
|
||||
<h3 data-company-name>Ryans Recruit Firm</h3>
|
||||
<p style="color: rgba(255, 255, 255, 0.8);">
|
||||
Your trusted partner in career advancement and talent acquisition.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="footer-section">
|
||||
<h3>Quick Links</h3>
|
||||
<ul>
|
||||
<li><a href="/">Home</a></li>
|
||||
<li><a href="/about">About Us</a></li>
|
||||
<li><a href="/services">Services</a></li>
|
||||
<li><a href="/jobs">Job Listings</a></li>
|
||||
<li><a href="/contact">Contact</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="footer-section">
|
||||
<h3>For Employers</h3>
|
||||
<ul>
|
||||
<li><a href="/contact">Post a Job</a></li>
|
||||
<li><a href="/contact">Our Process</a></li>
|
||||
<li><a href="/contact">Pricing</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="footer-section">
|
||||
<h3>Contact</h3>
|
||||
<ul>
|
||||
<li>Email: info@ryansrecruit.com</li>
|
||||
<li>Phone: +1 (555) 123-4567</li>
|
||||
<li>Hours: Mon-Fri 9AM-6PM EST</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer-bottom">
|
||||
<p>© 2026 Ryans Recruit Firm. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="/js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
170
public/contact.html
Normal file
170
public/contact.html
Normal file
@@ -0,0 +1,170 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Contact Us - Ryans Recruit Firm</title>
|
||||
<link rel="stylesheet" href="/css/styles.css">
|
||||
<script src="/js/init.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<nav class="container">
|
||||
<div class="logo" data-company-name>Ryans Recruit Firm</div>
|
||||
<ul class="nav-links">
|
||||
<li><a href="/">Home</a></li>
|
||||
<li><a href="/about">About</a></li>
|
||||
<li><a href="/services">Services</a></li>
|
||||
<li><a href="/jobs">Jobs</a></li>
|
||||
<li><a href="/contact" class="active">Contact</a></li>
|
||||
<li><a href="/admin/login" class="btn btn-primary btn-sm">Admin</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<section class="hero">
|
||||
<div class="container">
|
||||
<h1>Contact Us</h1>
|
||||
<p>Get in touch with our team - we're here to help</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="container">
|
||||
<div class="grid grid-2" style="gap: 3rem; align-items: start;">
|
||||
<!-- Contact Form -->
|
||||
<div style="background: var(--bg-white); padding: 2rem; border-radius: 0.5rem; box-shadow: var(--shadow-md);">
|
||||
<h3 style="color: var(--secondary-color); margin-bottom: 2rem;">Send us a Message</h3>
|
||||
|
||||
<form id="contact-form">
|
||||
<!-- Name -->
|
||||
<div class="form-group">
|
||||
<label for="name">Name <span style="color: var(--error);">*</span></label>
|
||||
<input type="text" id="name" name="name" required placeholder="Your full name">
|
||||
</div>
|
||||
|
||||
<!-- Email -->
|
||||
<div class="form-group">
|
||||
<label for="contactEmail">Email Address <span style="color: var(--error);">*</span></label>
|
||||
<input type="email" id="contactEmail" name="email" required placeholder="your.email@example.com">
|
||||
</div>
|
||||
|
||||
<!-- Subject -->
|
||||
<div class="form-group">
|
||||
<label for="subject">Subject</label>
|
||||
<input type="text" id="subject" name="subject" placeholder="What is this about?">
|
||||
</div>
|
||||
|
||||
<!-- Message -->
|
||||
<div class="form-group">
|
||||
<label for="message">Message <span style="color: var(--error);">*</span></label>
|
||||
<textarea id="message" name="message" required placeholder="Tell us more about your inquiry..."></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<button type="submit" class="btn btn-primary btn-lg" style="width: 100%;">Send Message</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Contact Information -->
|
||||
<div>
|
||||
<h3 style="color: var(--secondary-color); margin-bottom: 2rem;">Get in Touch</h3>
|
||||
|
||||
<div style="background: var(--bg-white); padding: 2rem; border-radius: 0.5rem; box-shadow: var(--shadow-md); margin-bottom: 2rem;">
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4 style="color: var(--secondary-color); margin-bottom: 0.5rem; display: flex; align-items: center; gap: 0.75rem;">
|
||||
<span style="font-size: 1.5rem;">📧</span> Email
|
||||
</h4>
|
||||
<p style="color: var(--text-light); margin-bottom: 0;">
|
||||
<a href="mailto:info@ryansrecruit.com" style="color: var(--primary-color); font-weight: 500;">info@ryansrecruit.com</a>
|
||||
</p>
|
||||
<div class="form-help">We typically respond within 24 hours</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h4 style="color: var(--secondary-color); margin-bottom: 0.5rem; display: flex; align-items: center; gap: 0.75rem;">
|
||||
<span style="font-size: 1.5rem;">📞</span> Phone
|
||||
</h4>
|
||||
<p style="color: var(--text-light); margin-bottom: 0;">
|
||||
<a href="tel:+15551234567" style="color: var(--primary-color); font-weight: 500;">+1 (555) 123-4567</a>
|
||||
</p>
|
||||
<div class="form-help">Mon-Fri 9AM-6PM EST</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 style="color: var(--secondary-color); margin-bottom: 0.5rem; display: flex; align-items: center; gap: 0.75rem;">
|
||||
<span style="font-size: 1.5rem;">🏢</span> Office Hours
|
||||
</h4>
|
||||
<p style="color: var(--text-light); margin-bottom: 0.5rem;">
|
||||
Monday to Friday: 9:00 AM - 6:00 PM EST
|
||||
</p>
|
||||
<p style="color: var(--text-light);">
|
||||
Saturday & Sunday: Closed
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%); padding: 2rem; border-radius: 0.5rem; color: white;">
|
||||
<h4 style="color: white; margin-bottom: 1rem;">Why Choose Us?</h4>
|
||||
<ul style="list-style: none;">
|
||||
<li style="margin-bottom: 0.75rem;">✓ Experienced recruitment team</li>
|
||||
<li style="margin-bottom: 0.75rem;">✓ Fast response times</li>
|
||||
<li style="margin-bottom: 0.75rem;">✓ Personalized support</li>
|
||||
<li style="margin-bottom: 0.75rem;">✓ Industry expertise</li>
|
||||
<li>✓ Proven track record</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<div class="container">
|
||||
<div class="footer-content">
|
||||
<div class="footer-section">
|
||||
<h3 data-company-name>Ryans Recruit Firm</h3>
|
||||
<p style="color: rgba(255, 255, 255, 0.8);">
|
||||
Your trusted partner in career advancement and talent acquisition.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="footer-section">
|
||||
<h3>Quick Links</h3>
|
||||
<ul>
|
||||
<li><a href="/">Home</a></li>
|
||||
<li><a href="/about">About Us</a></li>
|
||||
<li><a href="/services">Services</a></li>
|
||||
<li><a href="/jobs">Job Listings</a></li>
|
||||
<li><a href="/contact">Contact</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="footer-section">
|
||||
<h3>For Employers</h3>
|
||||
<ul>
|
||||
<li><a href="/contact">Post a Job</a></li>
|
||||
<li><a href="/contact">Our Process</a></li>
|
||||
<li><a href="/contact">Pricing</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="footer-section">
|
||||
<h3>Contact</h3>
|
||||
<ul>
|
||||
<li>Email: info@ryansrecruit.com</li>
|
||||
<li>Phone: +1 (555) 123-4567</li>
|
||||
<li>Hours: Mon-Fri 9AM-6PM EST</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer-bottom">
|
||||
<p>© 2026 Ryans Recruit Firm. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="/js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
617
public/css/styles.css
Normal file
617
public/css/styles.css
Normal file
@@ -0,0 +1,617 @@
|
||||
/* AI Recruitment Site - Global Styles */
|
||||
|
||||
:root {
|
||||
/* Ryan Brand Colors */
|
||||
--primary-color: #2563EB; /* Ryan Blue */
|
||||
--primary-dark: #1e40af;
|
||||
--primary-light: #3b82f6;
|
||||
--secondary-color: #1E293B; /* Dark */
|
||||
--accent-color: #059669; /* Ryan Green */
|
||||
--text-dark: #1E293B;
|
||||
--text-light: #64748B; /* Gray */
|
||||
--bg-light: #F8FAFC; /* Light */
|
||||
--bg-white: #ffffff;
|
||||
--border-color: #E2E8F0; /* Border */
|
||||
--success: #059669; /* Ryan Green */
|
||||
--warning: #F59E0B; /* Warning Orange */
|
||||
--error: #DC2626; /* Error Red */
|
||||
--info: #0EA5E9; /* Info Blue */
|
||||
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
--shadow-lg: 0 10px 20px rgba(0, 0, 0, 0.15);
|
||||
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1);
|
||||
/* Spacing System - 8px base */
|
||||
--spacing-1: 8px;
|
||||
--spacing-2: 16px;
|
||||
--spacing-3: 24px;
|
||||
--spacing-4: 32px;
|
||||
--spacing-5: 40px;
|
||||
--spacing-6: 48px;
|
||||
}
|
||||
|
||||
* {
|
||||
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-dark);
|
||||
background-color: var(--bg-light);
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
|
||||
h1 { font-size: 2.5rem; }
|
||||
h2 { font-size: 2rem; }
|
||||
h3 { font-size: 1.75rem; }
|
||||
h4 { font-size: 1.5rem; }
|
||||
h5 { font-size: 1.25rem; }
|
||||
h6 { font-size: 1rem; }
|
||||
|
||||
p {
|
||||
margin-bottom: 1rem;
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
text-decoration: none;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
|
||||
/* Container */
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 2rem;
|
||||
}
|
||||
|
||||
.container-narrow {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 0 2rem;
|
||||
}
|
||||
|
||||
/* Header & Navigation */
|
||||
header {
|
||||
background: var(--bg-white);
|
||||
box-shadow: var(--shadow-sm);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
nav {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
display: flex;
|
||||
list-style: none;
|
||||
gap: 2rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nav-links a {
|
||||
color: var(--text-dark);
|
||||
font-weight: 500;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.nav-links a:hover,
|
||||
.nav-links a.active {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* Hero Section */
|
||||
.hero {
|
||||
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
|
||||
color: white;
|
||||
padding: 5rem 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
color: white;
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.hero p {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--primary-dark);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--bg-white);
|
||||
color: var(--primary-color);
|
||||
border: 2px solid var(--primary-color);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: var(--success);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background: #059669;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: var(--error);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #dc2626;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
padding: 1rem 2rem;
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
/* Cards */
|
||||
.card {
|
||||
background: var(--bg-white);
|
||||
border-radius: 0.5rem;
|
||||
padding: 2rem;
|
||||
box-shadow: var(--shadow-md);
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 2px solid var(--border-color);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 1.5rem;
|
||||
color: var(--secondary-color);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.card-subtitle {
|
||||
color: var(--text-light);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* Grid System */
|
||||
.grid {
|
||||
display: grid;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.grid-2 { grid-template-columns: repeat(2, 1fr); }
|
||||
.grid-3 { grid-template-columns: repeat(3, 1fr); }
|
||||
.grid-4 { grid-template-columns: repeat(4, 1fr); }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.grid-2, .grid-3, .grid-4 {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* Sections */
|
||||
section {
|
||||
padding: 4rem 0;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.section-title h2 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.section-title p {
|
||||
font-size: 1.125rem;
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
/* Forms */
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-dark);
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="email"],
|
||||
input[type="password"],
|
||||
input[type="tel"],
|
||||
input[type="url"],
|
||||
input[type="number"],
|
||||
input[type="file"],
|
||||
select,
|
||||
textarea {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 0.5rem;
|
||||
font-size: 1rem;
|
||||
transition: border-color 0.2s;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
input:focus,
|
||||
select:focus,
|
||||
textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.form-error {
|
||||
color: var(--error);
|
||||
font-size: 0.875rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.form-help {
|
||||
color: var(--text-light);
|
||||
font-size: 0.875rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
/* Job Listing */
|
||||
.job-card {
|
||||
background: var(--bg-white);
|
||||
border-radius: 0.5rem;
|
||||
padding: 2rem;
|
||||
box-shadow: var(--shadow-md);
|
||||
margin-bottom: 1.5rem;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.job-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.job-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: start;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.job-title {
|
||||
font-size: 1.5rem;
|
||||
color: var(--secondary-color);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.job-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
.job-meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.job-description {
|
||||
color: var(--text-light);
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.job-requirements {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.job-requirements h4 {
|
||||
font-size: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--text-dark);
|
||||
}
|
||||
|
||||
.job-requirements ul {
|
||||
padding-left: 1.5rem;
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
.job-requirements li {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
/* Tags/Badges */
|
||||
.tag {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 1rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tag-primary {
|
||||
background: rgba(37, 99, 235, 0.1);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.tag-success {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.tag-warning {
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
.tag-error {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
/* Stats */
|
||||
.stat-card {
|
||||
background: var(--bg-white);
|
||||
border-radius: 0.5rem;
|
||||
padding: 1.5rem;
|
||||
box-shadow: var(--shadow-md);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--primary-color);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: var(--text-light);
|
||||
font-size: 0.875rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: var(--bg-white);
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
thead {
|
||||
background: var(--bg-light);
|
||||
}
|
||||
|
||||
th {
|
||||
padding: 1rem;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
color: var(--text-dark);
|
||||
border-bottom: 2px solid var(--border-color);
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
tr:hover {
|
||||
background: var(--bg-light);
|
||||
}
|
||||
|
||||
/* Alerts */
|
||||
.alert {
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
color: var(--success);
|
||||
border: 2px solid var(--success);
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
color: var(--error);
|
||||
border: 2px solid var(--error);
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
background: rgba(37, 99, 235, 0.1);
|
||||
color: var(--primary-color);
|
||||
border: 2px solid var(--primary-color);
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
footer {
|
||||
background: var(--secondary-color);
|
||||
color: white;
|
||||
padding: 3rem 0 1rem;
|
||||
margin-top: 4rem;
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.footer-section h3 {
|
||||
color: white;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.footer-section ul {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.footer-section li {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.footer-section a {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.footer-section a:hover {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.footer-bottom {
|
||||
text-align: center;
|
||||
padding-top: 2rem;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
/* Loading */
|
||||
.loading {
|
||||
display: inline-block;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 50%;
|
||||
border-top-color: white;
|
||||
animation: spin 0.6s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Utilities */
|
||||
.text-center { text-align: center; }
|
||||
.text-right { text-align: right; }
|
||||
.text-left { text-align: left; }
|
||||
|
||||
.mt-1 { margin-top: 0.5rem; }
|
||||
.mt-2 { margin-top: 1rem; }
|
||||
.mt-3 { margin-top: 1.5rem; }
|
||||
.mt-4 { margin-top: 2rem; }
|
||||
|
||||
.mb-1 { margin-bottom: 0.5rem; }
|
||||
.mb-2 { margin-bottom: 1rem; }
|
||||
.mb-3 { margin-bottom: 1.5rem; }
|
||||
.mb-4 { margin-bottom: 2rem; }
|
||||
|
||||
.hidden { display: none; }
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.hero p {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
gap: 1rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
h1 { font-size: 2rem; }
|
||||
h2 { font-size: 1.75rem; }
|
||||
h3 { font-size: 1.5rem; }
|
||||
|
||||
.job-header {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
244
public/index.html
Normal file
244
public/index.html
Normal file
@@ -0,0 +1,244 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Ryans Recruit Firm - Find Your Dream Job</title>
|
||||
<link rel="stylesheet" href="/css/styles.css">
|
||||
<script src="/js/init.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<nav class="container">
|
||||
<div class="logo" data-company-name>Ryans Recruit Firm</div>
|
||||
<ul class="nav-links">
|
||||
<li><a href="/" class="active">Home</a></li>
|
||||
<li><a href="/about">About</a></li>
|
||||
<li><a href="/services">Services</a></li>
|
||||
<li><a href="/jobs">Jobs</a></li>
|
||||
<li><a href="/contact">Contact</a></li>
|
||||
<li><a href="/admin/login" class="btn btn-primary btn-sm">Admin</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<section class="hero">
|
||||
<div class="container">
|
||||
<h1 data-company-tagline>Your Career Success Is Our Mission</h1>
|
||||
<p data-company-description>Connecting talented professionals with leading companies worldwide</p>
|
||||
<div style="display: flex; gap: 1rem; justify-content: center; margin-top: 2rem;">
|
||||
<a href="/jobs" class="btn btn-primary btn-lg">Browse Jobs</a>
|
||||
<a href="/contact" class="btn btn-secondary btn-lg">Get in Touch</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="container">
|
||||
<div class="section-title">
|
||||
<h2>Why Choose <span data-company-name>Ryans Recruit Firm</span>?</h2>
|
||||
<p>We're committed to finding the perfect match for both candidates and employers</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-3">
|
||||
<div class="card text-center">
|
||||
<div style="font-size: 3rem; margin-bottom: 1rem;">🎯</div>
|
||||
<h3>Targeted Matching</h3>
|
||||
<p>Our advanced screening process ensures we match the right talent with the right opportunity, saving time and increasing success rates.</p>
|
||||
</div>
|
||||
|
||||
<div class="card text-center">
|
||||
<div style="font-size: 3rem; margin-bottom: 1rem;">🌍</div>
|
||||
<h3>Global Network</h3>
|
||||
<p>Access to positions across multiple industries and locations, from startups to Fortune 500 companies worldwide.</p>
|
||||
</div>
|
||||
|
||||
<div class="card text-center">
|
||||
<div style="font-size: 3rem; margin-bottom: 1rem;">🤝</div>
|
||||
<h3>Personal Support</h3>
|
||||
<p>Dedicated recruiters guide you through every step, from application to offer negotiation and beyond.</p>
|
||||
</div>
|
||||
|
||||
<div class="card text-center">
|
||||
<div style="font-size: 3rem; margin-bottom: 1rem;">⚡</div>
|
||||
<h3>Fast Process</h3>
|
||||
<p>Streamlined recruitment process with quick turnaround times, getting you in front of hiring managers faster.</p>
|
||||
</div>
|
||||
|
||||
<div class="card text-center">
|
||||
<div style="font-size: 3rem; margin-bottom: 1rem;">💼</div>
|
||||
<h3>Industry Experts</h3>
|
||||
<p>Our team has deep expertise across technology, finance, healthcare, and more, understanding what companies need.</p>
|
||||
</div>
|
||||
|
||||
<div class="card text-center">
|
||||
<div style="font-size: 3rem; margin-bottom: 1rem;">🎓</div>
|
||||
<h3>Career Coaching</h3>
|
||||
<p>Free resume reviews, interview preparation, and career advice to help you land your dream role.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section style="background: var(--bg-white);">
|
||||
<div class="container">
|
||||
<div class="section-title">
|
||||
<h2>Our Success Stories</h2>
|
||||
<p>Hear from professionals who found their dream jobs through us</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-2">
|
||||
<div class="card">
|
||||
<p style="font-style: italic; color: var(--text-light); margin-bottom: 1rem;">
|
||||
"Ryans Recruit Firm found me the perfect role in just two weeks. Their team understood exactly what I was looking for and matched me with a company that aligned with my career goals. Highly recommended!"
|
||||
</p>
|
||||
<div style="display: flex; align-items: center; gap: 1rem;">
|
||||
<div style="width: 50px; height: 50px; border-radius: 50%; background: var(--primary-color); display: flex; align-items: center; justify-content: center; color: white; font-weight: 700;">SJ</div>
|
||||
<div>
|
||||
<div style="font-weight: 600;">Sarah Johnson</div>
|
||||
<div style="font-size: 0.875rem; color: var(--text-light);">Senior Software Engineer</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<p style="font-style: italic; color: var(--text-light); margin-bottom: 1rem;">
|
||||
"The personalized support I received was incredible. From resume refinement to interview coaching, they were with me every step of the way. I'm now working at my dream company!"
|
||||
</p>
|
||||
<div style="display: flex; align-items: center; gap: 1rem;">
|
||||
<div style="width: 50px; height: 50px; border-radius: 50%; background: var(--accent-color); display: flex; align-items: center; justify-content: center; color: white; font-weight: 700;">MC</div>
|
||||
<div>
|
||||
<div style="font-weight: 600;">Michael Chen</div>
|
||||
<div style="font-size: 0.875rem; color: var(--text-light);">Product Manager</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<p style="font-style: italic; color: var(--text-light); margin-bottom: 1rem;">
|
||||
"As a hiring manager, I've worked with many recruiting firms, but Ryans stands out. They consistently deliver high-quality candidates who are well-screened and truly interested in our opportunities."
|
||||
</p>
|
||||
<div style="display: flex; align-items: center; gap: 1rem;">
|
||||
<div style="width: 50px; height: 50px; border-radius: 50%; background: var(--success); display: flex; align-items: center; justify-content: center; color: white; font-weight: 700;">EP</div>
|
||||
<div>
|
||||
<div style="font-weight: 600;">Emily Parker</div>
|
||||
<div style="font-size: 0.875rem; color: var(--text-light);">HR Director</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<p style="font-style: italic; color: var(--text-light); margin-bottom: 1rem;">
|
||||
"After months of unsuccessful job hunting, Ryans helped me land a position that exceeded my salary expectations. Their expertise in negotiation made a huge difference!"
|
||||
</p>
|
||||
<div style="display: flex; align-items: center; gap: 1rem;">
|
||||
<div style="width: 50px; height: 50px; border-radius: 50%; background: var(--warning); display: flex; align-items: center; justify-content: center; color: white; font-weight: 700;">DK</div>
|
||||
<div>
|
||||
<div style="font-weight: 600;">David Kim</div>
|
||||
<div style="font-size: 0.875rem; color: var(--text-light);">Data Scientist</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="container">
|
||||
<div class="section-title">
|
||||
<h2>How It Works</h2>
|
||||
<p>Getting started is simple and straightforward</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-4">
|
||||
<div class="text-center">
|
||||
<div style="width: 80px; height: 80px; border-radius: 50%; background: var(--primary-color); display: flex; align-items: center; justify-content: center; color: white; font-size: 2rem; font-weight: 700; margin: 0 auto 1rem;">1</div>
|
||||
<h4>Browse Jobs</h4>
|
||||
<p>Explore our curated job listings across various industries and locations.</p>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<div style="width: 80px; height: 80px; border-radius: 50%; background: var(--primary-color); display: flex; align-items: center; justify-content: center; color: white; font-size: 2rem; font-weight: 700; margin: 0 auto 1rem;">2</div>
|
||||
<h4>Submit Application</h4>
|
||||
<p>Apply with your resume and let us know what you're looking for.</p>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<div style="width: 80px; height: 80px; border-radius: 50%; background: var(--primary-color); display: flex; align-items: center; justify-content: center; color: white; font-size: 2rem; font-weight: 700; margin: 0 auto 1rem;">3</div>
|
||||
<h4>Get Matched</h4>
|
||||
<p>Our team reviews your profile and matches you with suitable opportunities.</p>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<div style="width: 80px; height: 80px; border-radius: 50%; background: var(--primary-color); display: flex; align-items: center; justify-content: center; color: white; font-size: 2rem; font-weight: 700; margin: 0 auto 1rem;">4</div>
|
||||
<h4>Start Your Journey</h4>
|
||||
<p>Receive support throughout interviews and land your dream job!</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center" style="margin-top: 3rem;">
|
||||
<a href="/jobs" class="btn btn-primary btn-lg">View Open Positions</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section style="background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%); color: white;">
|
||||
<div class="container text-center">
|
||||
<h2 style="color: white; margin-bottom: 1rem;">Ready to Take the Next Step?</h2>
|
||||
<p style="color: rgba(255, 255, 255, 0.9); font-size: 1.125rem; margin-bottom: 2rem;">
|
||||
Join thousands of professionals who have found their dream jobs through Ryans Recruit Firm
|
||||
</p>
|
||||
<div style="display: flex; gap: 1rem; justify-content: center;">
|
||||
<a href="/jobs" class="btn btn-lg" style="background: white; color: var(--primary-color);">Browse Jobs</a>
|
||||
<a href="/contact" class="btn btn-secondary btn-lg">Contact Us</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<div class="container">
|
||||
<div class="footer-content">
|
||||
<div class="footer-section">
|
||||
<h3 data-company-name>Ryans Recruit Firm</h3>
|
||||
<p style="color: rgba(255, 255, 255, 0.8);" data-company-description>
|
||||
Your trusted partner in career advancement and talent acquisition.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="footer-section">
|
||||
<h3>Quick Links</h3>
|
||||
<ul>
|
||||
<li><a href="/">Home</a></li>
|
||||
<li><a href="/about">About Us</a></li>
|
||||
<li><a href="/services">Services</a></li>
|
||||
<li><a href="/jobs">Job Listings</a></li>
|
||||
<li><a href="/contact">Contact</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="footer-section">
|
||||
<h3>For Employers</h3>
|
||||
<ul>
|
||||
<li><a href="/contact">Post a Job</a></li>
|
||||
<li><a href="/contact">Our Process</a></li>
|
||||
<li><a href="/contact">Pricing</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="footer-section">
|
||||
<h3>Contact</h3>
|
||||
<ul>
|
||||
<li>Email: <a href="mailto:info@ryansrecruit.com" data-contact-email>info@ryansrecruit.com</a></li>
|
||||
<li>Phone: <a href="tel:+15551234567" data-contact-phone>+1 (555) 123-4567</a></li>
|
||||
<li>Hours: <span data-business-hours>Mon-Fri 9AM-6PM EST</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer-bottom">
|
||||
<p>© 2026 Ryans Recruit Firm. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
93
public/jobs.html
Normal file
93
public/jobs.html
Normal file
@@ -0,0 +1,93 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Job Listings - Ryans Recruit Firm</title>
|
||||
<link rel="stylesheet" href="/css/styles.css">
|
||||
<script src="/js/init.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<nav class="container">
|
||||
<div class="logo" data-company-name>Ryans Recruit Firm</div>
|
||||
<ul class="nav-links">
|
||||
<li><a href="/">Home</a></li>
|
||||
<li><a href="/about">About</a></li>
|
||||
<li><a href="/services">Services</a></li>
|
||||
<li><a href="/jobs" class="active">Jobs</a></li>
|
||||
<li><a href="/contact">Contact</a></li>
|
||||
<li><a href="/admin/login" class="btn btn-primary btn-sm">Admin</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<section class="hero">
|
||||
<div class="container">
|
||||
<h1>Open Positions</h1>
|
||||
<p>Discover your next opportunity with our curated job listings</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="container">
|
||||
<div id="loading" class="text-center" style="padding: 3rem 0;">
|
||||
<div class="loading" style="display: inline-block; width: 3rem; height: 3rem; border: 4px solid rgba(37, 99, 235, 0.3); border-top-color: var(--primary-color); margin-bottom: 1rem;"></div>
|
||||
<p style="color: var(--text-light);">Loading job listings...</p>
|
||||
</div>
|
||||
|
||||
<div id="jobs-container">
|
||||
<!-- Jobs will be loaded dynamically here -->
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<div class="container">
|
||||
<div class="footer-content">
|
||||
<div class="footer-section">
|
||||
<h3 data-company-name>Ryans Recruit Firm</h3>
|
||||
<p style="color: rgba(255, 255, 255, 0.8);">
|
||||
Your trusted partner in career advancement and talent acquisition.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="footer-section">
|
||||
<h3>Quick Links</h3>
|
||||
<ul>
|
||||
<li><a href="/">Home</a></li>
|
||||
<li><a href="/about">About Us</a></li>
|
||||
<li><a href="/services">Services</a></li>
|
||||
<li><a href="/jobs">Job Listings</a></li>
|
||||
<li><a href="/contact">Contact</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="footer-section">
|
||||
<h3>For Employers</h3>
|
||||
<ul>
|
||||
<li><a href="/contact">Post a Job</a></li>
|
||||
<li><a href="/contact">Our Process</a></li>
|
||||
<li><a href="/contact">Pricing</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="footer-section">
|
||||
<h3>Contact</h3>
|
||||
<ul>
|
||||
<li>Email: info@ryansrecruit.com</li>
|
||||
<li>Phone: +1 (555) 123-4567</li>
|
||||
<li>Hours: Mon-Fri 9AM-6PM EST</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer-bottom">
|
||||
<p>© 2026 Ryans Recruit Firm. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="/js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
290
public/js/admin.js
Normal file
290
public/js/admin.js
Normal file
@@ -0,0 +1,290 @@
|
||||
// Admin JavaScript for Ryans Recruit Firm
|
||||
|
||||
// Check authentication on all admin pages except login
|
||||
if (!window.location.pathname.includes('login')) {
|
||||
checkAuth();
|
||||
}
|
||||
|
||||
async function checkAuth() {
|
||||
try {
|
||||
const response = await fetch('/api/admin/check');
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.loggedIn) {
|
||||
window.location.href = '/admin/login';
|
||||
return;
|
||||
}
|
||||
|
||||
// Update admin name in header if exists
|
||||
const adminNameEl = document.getElementById('admin-name');
|
||||
if (adminNameEl && data.admin) {
|
||||
adminNameEl.textContent = data.admin.fullName;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Auth check failed:', err);
|
||||
window.location.href = '/admin/login';
|
||||
}
|
||||
}
|
||||
|
||||
// Login page handlers
|
||||
if (window.location.pathname.includes('login')) {
|
||||
checkFirstAdmin();
|
||||
document.getElementById('login-form')?.addEventListener('submit', handleLogin);
|
||||
}
|
||||
|
||||
async function checkFirstAdmin() {
|
||||
try {
|
||||
const response = await fetch('/api/admin/check-first');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.isFirstAdmin) {
|
||||
document.getElementById('first-admin-notice').style.display = 'block';
|
||||
document.getElementById('full-name-group').style.display = 'block';
|
||||
document.querySelector('button[type="submit"]').textContent = 'Create Admin Account';
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error checking first admin:', err);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleLogin(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const form = e.target;
|
||||
const submitBtn = form.querySelector('button[type="submit"]');
|
||||
const originalText = submitBtn.textContent;
|
||||
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerHTML = '<span class="loading"></span> Processing...';
|
||||
|
||||
const formData = new FormData(form);
|
||||
const data = Object.fromEntries(formData);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/admin/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
showAlert('Login successful!', 'success');
|
||||
setTimeout(() => window.location.href = '/admin/dashboard', 500);
|
||||
} else {
|
||||
showAlert(result.error || 'Login failed', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Login error:', err);
|
||||
showAlert('Login failed. Please try again.', 'error');
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = originalText;
|
||||
}
|
||||
}
|
||||
|
||||
// Dashboard page
|
||||
if (window.location.pathname.includes('dashboard')) {
|
||||
loadDashboardStats();
|
||||
}
|
||||
|
||||
async function loadDashboardStats() {
|
||||
try {
|
||||
const response = await fetch('/api/admin/stats');
|
||||
const stats = await response.json();
|
||||
|
||||
document.getElementById('stat-new-applications').textContent = stats.new_applications || 0;
|
||||
document.getElementById('stat-total-applications').textContent = stats.total_applications || 0;
|
||||
document.getElementById('stat-total-applicants').textContent = stats.total_applicants || 0;
|
||||
document.getElementById('stat-active-jobs').textContent = stats.active_jobs || 0;
|
||||
|
||||
if (stats.unread_messages > 0) {
|
||||
document.getElementById('stat-unread-messages').textContent = stats.unread_messages;
|
||||
document.getElementById('stat-unread-messages').parentElement.style.display = 'block';
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading stats:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Applications page
|
||||
if (window.location.pathname.includes('applicants')) {
|
||||
loadApplications();
|
||||
}
|
||||
|
||||
async function loadApplications(filters = {}) {
|
||||
const container = document.getElementById('applications-container');
|
||||
const loading = document.getElementById('loading');
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams(filters);
|
||||
const response = await fetch(`/api/admin/applications?${params}`);
|
||||
const applications = await response.json();
|
||||
|
||||
if (loading) loading.style.display = 'none';
|
||||
|
||||
if (applications.length === 0) {
|
||||
container.innerHTML = '<div class="text-center"><p>No applications found.</p></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = `
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Applicant</th>
|
||||
<th>Job</th>
|
||||
<th>Experience</th>
|
||||
<th>Applied</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${applications.map(app => `
|
||||
<tr>
|
||||
<td>
|
||||
<strong>${escapeHtml(app.full_name)}</strong><br>
|
||||
<small>${escapeHtml(app.email)}</small>
|
||||
</td>
|
||||
<td>${escapeHtml(app.job_title || 'General Application')}</td>
|
||||
<td>${app.years_of_experience || 'N/A'} years</td>
|
||||
<td>${new Date(app.applied_at).toLocaleDateString()}</td>
|
||||
<td><span class="tag tag-${getStatusColor(app.status)}">${escapeHtml(app.status)}</span></td>
|
||||
<td>
|
||||
<a href="/admin/applicants?id=${app.id}" class="btn btn-sm btn-primary">View</a>
|
||||
<a href="/api/admin/applications/${app.id}/cv" class="btn btn-sm btn-secondary" target="_blank">CV</a>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
} catch (err) {
|
||||
console.error('Error loading applications:', err);
|
||||
if (loading) loading.style.display = 'none';
|
||||
container.innerHTML = '<div class="alert alert-error">Failed to load applications</div>';
|
||||
}
|
||||
}
|
||||
|
||||
// Jobs management page
|
||||
if (window.location.pathname.includes('jobs') && window.location.pathname.includes('admin')) {
|
||||
loadAdminJobs();
|
||||
}
|
||||
|
||||
async function loadAdminJobs() {
|
||||
const container = document.getElementById('jobs-container');
|
||||
const loading = document.getElementById('loading');
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/admin/jobs');
|
||||
const jobs = await response.json();
|
||||
|
||||
if (loading) loading.style.display = 'none';
|
||||
|
||||
container.innerHTML = `
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<button onclick="showJobForm()" class="btn btn-success">+ Create New Job</button>
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Department</th>
|
||||
<th>Location</th>
|
||||
<th>Status</th>
|
||||
<th>Created</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${jobs.map(job => `
|
||||
<tr>
|
||||
<td><strong>${escapeHtml(job.title)}</strong></td>
|
||||
<td>${escapeHtml(job.department || 'N/A')}</td>
|
||||
<td>${escapeHtml(job.location || 'N/A')}</td>
|
||||
<td><span class="tag ${job.is_active ? 'tag-success' : 'tag-error'}">${job.is_active ? 'Active' : 'Inactive'}</span></td>
|
||||
<td>${new Date(job.created_at).toLocaleDateString()}</td>
|
||||
<td>
|
||||
<button onclick="editJob(${job.id})" class="btn btn-sm btn-primary">Edit</button>
|
||||
<button onclick="toggleJobStatus(${job.id}, ${!job.is_active})" class="btn btn-sm btn-secondary">${job.is_active ? 'Deactivate' : 'Activate'}</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
} catch (err) {
|
||||
console.error('Error loading jobs:', err);
|
||||
if (loading) loading.style.display = 'none';
|
||||
container.innerHTML = '<div class="alert alert-error">Failed to load jobs</div>';
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleJobStatus(jobId, isActive) {
|
||||
try {
|
||||
const response = await fetch(`/api/admin/jobs/${jobId}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ isActive })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
showAlert(`Job ${isActive ? 'activated' : 'deactivated'} successfully`, 'success');
|
||||
loadAdminJobs();
|
||||
} else {
|
||||
showAlert('Failed to update job status', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error updating job:', err);
|
||||
showAlert('Failed to update job status', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Logout
|
||||
async function logout() {
|
||||
try {
|
||||
await fetch('/api/admin/logout', { method: 'POST' });
|
||||
window.location.href = '/admin/login';
|
||||
} catch (err) {
|
||||
console.error('Logout error:', err);
|
||||
window.location.href = '/admin/login';
|
||||
}
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
function escapeHtml(text) {
|
||||
if (!text) return '';
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function getStatusColor(status) {
|
||||
const colors = {
|
||||
'new': 'primary',
|
||||
'reviewing': 'warning',
|
||||
'interview': 'info',
|
||||
'hired': 'success',
|
||||
'rejected': 'error'
|
||||
};
|
||||
return colors[status] || 'primary';
|
||||
}
|
||||
|
||||
function showAlert(message, type = 'info') {
|
||||
const alertDiv = document.createElement('div');
|
||||
alertDiv.className = `alert alert-${type}`;
|
||||
alertDiv.textContent = message;
|
||||
alertDiv.style.position = 'fixed';
|
||||
alertDiv.style.top = '20px';
|
||||
alertDiv.style.right = '20px';
|
||||
alertDiv.style.zIndex = '10000';
|
||||
alertDiv.style.minWidth = '300px';
|
||||
|
||||
document.body.appendChild(alertDiv);
|
||||
|
||||
setTimeout(() => {
|
||||
alertDiv.remove();
|
||||
}, 5000);
|
||||
}
|
||||
135
public/js/init.js
Normal file
135
public/js/init.js
Normal file
@@ -0,0 +1,135 @@
|
||||
// Initialize company configuration
|
||||
// This file loads company configuration from the API and updates the page
|
||||
|
||||
let siteConfig = {};
|
||||
|
||||
async function loadSiteConfig() {
|
||||
try {
|
||||
const response = await fetch('/api/config');
|
||||
siteConfig = await response.json();
|
||||
|
||||
// Update company information
|
||||
updateCompanyInfo();
|
||||
|
||||
// Update branding (colors)
|
||||
updateBranding();
|
||||
|
||||
// Update contact information
|
||||
updateContactInfo();
|
||||
|
||||
// Update social media links
|
||||
updateSocialLinks();
|
||||
|
||||
console.log('Site configuration loaded successfully');
|
||||
} catch (err) {
|
||||
console.error('Failed to load site configuration:', err);
|
||||
}
|
||||
}
|
||||
|
||||
function updateCompanyInfo() {
|
||||
// Update company name
|
||||
const companyNameElements = document.querySelectorAll('[data-company-name]');
|
||||
companyNameElements.forEach(el => {
|
||||
el.textContent = siteConfig.company.name;
|
||||
});
|
||||
|
||||
// Update company tagline
|
||||
const taglineElements = document.querySelectorAll('[data-company-tagline]');
|
||||
taglineElements.forEach(el => {
|
||||
el.textContent = siteConfig.company.tagline;
|
||||
});
|
||||
|
||||
// Update company description
|
||||
const descElements = document.querySelectorAll('[data-company-description]');
|
||||
descElements.forEach(el => {
|
||||
el.textContent = siteConfig.company.description;
|
||||
});
|
||||
|
||||
// Update page title
|
||||
const titleElement = document.querySelector('title');
|
||||
if (titleElement && titleElement.textContent.includes('Ryans Recruit')) {
|
||||
titleElement.textContent = titleElement.textContent.replace(/Ryans Recruit Firm|Ryans Recruit/g, siteConfig.company.name);
|
||||
}
|
||||
}
|
||||
|
||||
function updateBranding() {
|
||||
// Set CSS custom properties for branding colors
|
||||
const root = document.documentElement;
|
||||
root.style.setProperty('--primary-color', siteConfig.branding.primaryColor);
|
||||
root.style.setProperty('--accent-color', siteConfig.branding.accentColor);
|
||||
root.style.setProperty('--secondary-color', siteConfig.branding.darkColor);
|
||||
}
|
||||
|
||||
function updateContactInfo() {
|
||||
// Update email
|
||||
const emailElements = document.querySelectorAll('[data-contact-email]');
|
||||
emailElements.forEach(el => {
|
||||
if (el.tagName === 'A') {
|
||||
el.href = `mailto:${siteConfig.contact.email}`;
|
||||
}
|
||||
el.textContent = siteConfig.contact.email;
|
||||
});
|
||||
|
||||
// Update phone
|
||||
const phoneElements = document.querySelectorAll('[data-contact-phone]');
|
||||
phoneElements.forEach(el => {
|
||||
if (el.tagName === 'A') {
|
||||
el.href = `tel:${siteConfig.contact.phone}`;
|
||||
}
|
||||
el.textContent = siteConfig.contact.phone;
|
||||
});
|
||||
|
||||
// Update address
|
||||
const addressElements = document.querySelectorAll('[data-contact-address]');
|
||||
addressElements.forEach(el => {
|
||||
el.textContent = siteConfig.contact.address;
|
||||
});
|
||||
|
||||
// Update business hours
|
||||
const hoursElements = document.querySelectorAll('[data-business-hours]');
|
||||
hoursElements.forEach(el => {
|
||||
el.textContent = siteConfig.contact.businessHours;
|
||||
});
|
||||
}
|
||||
|
||||
function updateSocialLinks() {
|
||||
// Update LinkedIn
|
||||
const linkedinElements = document.querySelectorAll('[data-social-linkedin]');
|
||||
linkedinElements.forEach(el => {
|
||||
if (siteConfig.social.linkedin) {
|
||||
el.href = siteConfig.social.linkedin;
|
||||
el.style.display = '';
|
||||
} else {
|
||||
el.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// Update Twitter
|
||||
const twitterElements = document.querySelectorAll('[data-social-twitter]');
|
||||
twitterElements.forEach(el => {
|
||||
if (siteConfig.social.twitter) {
|
||||
el.href = siteConfig.social.twitter;
|
||||
el.style.display = '';
|
||||
} else {
|
||||
el.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// Update Facebook
|
||||
const facebookElements = document.querySelectorAll('[data-social-facebook]');
|
||||
facebookElements.forEach(el => {
|
||||
if (siteConfig.social.facebook) {
|
||||
el.href = siteConfig.social.facebook;
|
||||
el.style.display = '';
|
||||
} else {
|
||||
el.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Load configuration when DOM is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', loadSiteConfig);
|
||||
} else {
|
||||
loadSiteConfig();
|
||||
}
|
||||
201
public/js/main.js
Normal file
201
public/js/main.js
Normal file
@@ -0,0 +1,201 @@
|
||||
// Main JavaScript for Ryans Recruit Firm
|
||||
|
||||
// Load jobs on jobs.html page
|
||||
if (window.location.pathname.includes('jobs')) {
|
||||
loadJobs();
|
||||
}
|
||||
|
||||
async function loadJobs() {
|
||||
const jobsContainer = document.getElementById('jobs-container');
|
||||
const loadingEl = document.getElementById('loading');
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/jobs');
|
||||
const jobs = await response.json();
|
||||
|
||||
if (loadingEl) loadingEl.style.display = 'none';
|
||||
|
||||
if (jobs.length === 0) {
|
||||
jobsContainer.innerHTML = '<div class="text-center"><p>No job openings at the moment. Check back soon!</p></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
jobsContainer.innerHTML = jobs.map(job => `
|
||||
<div class="job-card">
|
||||
<div class="job-header">
|
||||
<div>
|
||||
<h3 class="job-title">${escapeHtml(job.title)}</h3>
|
||||
<div class="job-meta">
|
||||
<span class="job-meta-item">📍 ${escapeHtml(job.location || 'Not specified')}</span>
|
||||
<span class="job-meta-item">💼 ${escapeHtml(job.employment_type || 'Full-time')}</span>
|
||||
${job.salary_range ? `<span class="job-meta-item">💰 ${escapeHtml(job.salary_range)}</span>` : ''}
|
||||
${job.department ? `<span class="tag tag-primary">${escapeHtml(job.department)}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="job-description">${escapeHtml(job.description.substring(0, 200))}...</p>
|
||||
<a href="/apply?job=${job.id}" class="btn btn-primary">Apply Now</a>
|
||||
</div>
|
||||
`).join('');
|
||||
} catch (err) {
|
||||
console.error('Error loading jobs:', err);
|
||||
if (loadingEl) loadingEl.style.display = 'none';
|
||||
jobsContainer.innerHTML = '<div class="alert alert-error">Failed to load job listings. Please try again later.</div>';
|
||||
}
|
||||
}
|
||||
|
||||
// Handle application form on apply.html
|
||||
if (window.location.pathname.includes('apply')) {
|
||||
loadJobDetails();
|
||||
document.getElementById('application-form')?.addEventListener('submit', handleApplicationSubmit);
|
||||
}
|
||||
|
||||
async function loadJobDetails() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const jobId = urlParams.get('job');
|
||||
|
||||
if (!jobId) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/jobs/${jobId}`);
|
||||
const job = await response.json();
|
||||
|
||||
document.getElementById('job-title').textContent = job.title;
|
||||
document.getElementById('job-details').innerHTML = `
|
||||
<p><strong>Location:</strong> ${escapeHtml(job.location)}</p>
|
||||
<p><strong>Type:</strong> ${escapeHtml(job.employment_type)}</p>
|
||||
${job.salary_range ? `<p><strong>Salary:</strong> ${escapeHtml(job.salary_range)}</p>` : ''}
|
||||
<p><strong>Description:</strong></p>
|
||||
<p>${escapeHtml(job.description)}</p>
|
||||
`;
|
||||
} catch (err) {
|
||||
console.error('Error loading job details:', err);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleApplicationSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const form = e.target;
|
||||
const submitBtn = form.querySelector('button[type="submit"]');
|
||||
const originalBtnText = submitBtn.textContent;
|
||||
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerHTML = '<span class="loading"></span> Submitting...';
|
||||
|
||||
const formData = new FormData(form);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/apply', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
showAlert('Application submitted successfully! We\'ll be in touch soon.', 'success');
|
||||
form.reset();
|
||||
setTimeout(() => window.location.href = '/jobs', 2000);
|
||||
} else {
|
||||
showAlert(result.error || 'Failed to submit application', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error submitting application:', err);
|
||||
showAlert('Failed to submit application. Please try again.', 'error');
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = originalBtnText;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle contact form on contact.html
|
||||
if (window.location.pathname.includes('contact')) {
|
||||
document.getElementById('contact-form')?.addEventListener('submit', handleContactSubmit);
|
||||
}
|
||||
|
||||
async function handleContactSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const form = e.target;
|
||||
const submitBtn = form.querySelector('button[type="submit"]');
|
||||
const originalBtnText = submitBtn.textContent;
|
||||
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerHTML = '<span class="loading"></span> Sending...';
|
||||
|
||||
const formData = new FormData(form);
|
||||
const data = Object.fromEntries(formData);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/contact', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
showAlert('Message sent successfully! We\'ll get back to you soon.', 'success');
|
||||
form.reset();
|
||||
} else {
|
||||
showAlert(result.error || 'Failed to send message', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error sending message:', err);
|
||||
showAlert('Failed to send message. Please try again.', 'error');
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = originalBtnText;
|
||||
}
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function showAlert(message, type = 'info') {
|
||||
const alertDiv = document.createElement('div');
|
||||
alertDiv.className = `alert alert-${type}`;
|
||||
alertDiv.textContent = message;
|
||||
alertDiv.style.position = 'fixed';
|
||||
alertDiv.style.top = '20px';
|
||||
alertDiv.style.right = '20px';
|
||||
alertDiv.style.zIndex = '10000';
|
||||
alertDiv.style.minWidth = '300px';
|
||||
|
||||
document.body.appendChild(alertDiv);
|
||||
|
||||
setTimeout(() => {
|
||||
alertDiv.remove();
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// File input validation
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const fileInputs = document.querySelectorAll('input[type="file"]');
|
||||
fileInputs.forEach(input => {
|
||||
input.addEventListener('change', (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
const maxSize = 5 * 1024 * 1024; // 5MB
|
||||
if (file.size > maxSize) {
|
||||
showAlert('File size must be less than 5MB', 'error');
|
||||
e.target.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const allowedTypes = ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'];
|
||||
if (!allowedTypes.includes(file.type)) {
|
||||
showAlert('Only PDF, DOC, and DOCX files are allowed', 'error');
|
||||
e.target.value = '';
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
110
public/services.html
Normal file
110
public/services.html
Normal file
@@ -0,0 +1,110 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Our Services - Ryans Recruit Firm</title>
|
||||
<link rel="stylesheet" href="/css/styles.css">
|
||||
<script src="/js/init.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<nav class="container">
|
||||
<div class="logo" data-company-name>Ryans Recruit Firm</div>
|
||||
<ul class="nav-links">
|
||||
<li><a href="/">Home</a></li>
|
||||
<li><a href="/about">About</a></li>
|
||||
<li><a href="/services" class="active">Services</a></li>
|
||||
<li><a href="/jobs">Jobs</a></li>
|
||||
<li><a href="/contact">Contact</a></li>
|
||||
<li><a href="/admin/login" class="btn btn-primary btn-sm">Admin</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<section class="hero">
|
||||
<div class="container">
|
||||
<h1>Our Services</h1>
|
||||
<p>Comprehensive recruitment solutions tailored to your needs</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="container">
|
||||
<div class="grid grid-2">
|
||||
<div class="card">
|
||||
<h3>For Job Seekers</h3>
|
||||
<ul style="line-height: 2; color: var(--text-light);">
|
||||
<li>Career counseling and guidance</li>
|
||||
<li>Resume review and optimization</li>
|
||||
<li>Interview preparation and coaching</li>
|
||||
<li>Salary negotiation support</li>
|
||||
<li>Access to exclusive job opportunities</li>
|
||||
<li>Long-term career development advice</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>For Employers</h3>
|
||||
<ul style="line-height: 2; color: var(--text-light);">
|
||||
<li>Talent sourcing and screening</li>
|
||||
<li>Comprehensive candidate assessment</li>
|
||||
<li>Interview coordination</li>
|
||||
<li>Background verification</li>
|
||||
<li>Offer negotiation assistance</li>
|
||||
<li>Post-placement follow-up</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section style="background: var(--bg-white);">
|
||||
<div class="container">
|
||||
<div class="section-title">
|
||||
<h2>Industries We Serve</h2>
|
||||
</div>
|
||||
<div class="grid grid-3">
|
||||
<div class="card"><h4>Technology</h4><p>Software, IT, Engineering</p></div>
|
||||
<div class="card"><h4>Finance</h4><p>Banking, Insurance, Investment</p></div>
|
||||
<div class="card"><h4>Healthcare</h4><p>Medical, Pharmaceutical, Biotech</p></div>
|
||||
<div class="card"><h4>Marketing</h4><p>Digital, Traditional, Analytics</p></div>
|
||||
<div class="card"><h4>Sales</h4><p>B2B, B2C, Enterprise</p></div>
|
||||
<div class="card"><h4>Operations</h4><p>Supply Chain, Logistics, Management</p></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<div class="container">
|
||||
<div class="footer-content">
|
||||
<div class="footer-section">
|
||||
<h3 data-company-name>Ryans Recruit Firm</h3>
|
||||
<p style="color: rgba(255, 255, 255, 0.8);">Your trusted partner in career advancement and talent acquisition.</p>
|
||||
</div>
|
||||
<div class="footer-section">
|
||||
<h3>Quick Links</h3>
|
||||
<ul>
|
||||
<li><a href="/">Home</a></li>
|
||||
<li><a href="/about">About Us</a></li>
|
||||
<li><a href="/services">Services</a></li>
|
||||
<li><a href="/jobs">Job Listings</a></li>
|
||||
<li><a href="/contact">Contact</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="footer-section">
|
||||
<h3>Contact</h3>
|
||||
<ul>
|
||||
<li>Email: info@ryansrecruit.com</li>
|
||||
<li>Phone: +1 (555) 123-4567</li>
|
||||
<li>Hours: Mon-Fri 9AM-6PM EST</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-bottom">
|
||||
<p>© 2026 Ryans Recruit Firm. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
717
server.js
Normal file
717
server.js
Normal file
@@ -0,0 +1,717 @@
|
||||
const express = require('express');
|
||||
const session = require('express-session');
|
||||
const bcrypt = require('bcrypt');
|
||||
const { Pool } = require('pg');
|
||||
const multer = require('multer');
|
||||
const path = require('path');
|
||||
const cookieParser = require('cookie-parser');
|
||||
const fs = require('fs');
|
||||
const config = require('./config');
|
||||
|
||||
const app = express();
|
||||
const PORT = config.app.port;
|
||||
|
||||
// PostgreSQL connection
|
||||
const pool = new Pool({
|
||||
host: config.database.host,
|
||||
port: config.database.port,
|
||||
database: config.database.name,
|
||||
user: config.database.user,
|
||||
password: config.database.password,
|
||||
max: 20,
|
||||
idleTimeoutMillis: 30000,
|
||||
connectionTimeoutMillis: 2000,
|
||||
});
|
||||
|
||||
// Run database migrations
|
||||
async function runMigrations() {
|
||||
try {
|
||||
console.log('Running database migrations...');
|
||||
const migrationsDir = path.join(__dirname, 'migrations');
|
||||
const migrationFiles = fs.readdirSync(migrationsDir).sort();
|
||||
|
||||
for (const file of migrationFiles) {
|
||||
if (file.endsWith('.sql')) {
|
||||
console.log(`Running migration: ${file}`);
|
||||
const migrationSQL = fs.readFileSync(path.join(migrationsDir, file), 'utf8');
|
||||
await pool.query(migrationSQL);
|
||||
console.log(`✓ Migration ${file} completed`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('All migrations completed successfully');
|
||||
} catch (error) {
|
||||
console.error('Migration error:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Test database connection and run migrations
|
||||
pool.query('SELECT NOW()')
|
||||
.then((res) => {
|
||||
console.log('Database connected successfully at:', res.rows[0].now);
|
||||
return runMigrations();
|
||||
})
|
||||
.then(() => {
|
||||
console.log('Database initialization complete');
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Database initialization error:', err.message);
|
||||
console.error('Application will continue but database operations will fail');
|
||||
});
|
||||
|
||||
// Middleware
|
||||
app.use(express.json({ limit: '10mb' }));
|
||||
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
||||
app.use(cookieParser());
|
||||
app.use(session({
|
||||
secret: config.app.sessionSecret,
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
cookie: {
|
||||
secure: config.app.nodeEnv === 'production', // Automatically enable for production
|
||||
httpOnly: true,
|
||||
maxAge: 24 * 60 * 60 * 1000 // 24 hours
|
||||
}
|
||||
}));
|
||||
|
||||
// Serve static files first
|
||||
app.use(express.static('public'));
|
||||
|
||||
// Middleware to handle routes without .html extension
|
||||
app.use((req, res, next) => {
|
||||
// Skip API routes
|
||||
if (req.path.startsWith('/api/')) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// Handle root path
|
||||
if (req.path === '/') {
|
||||
return res.sendFile(path.join(__dirname, 'public', 'index.html'));
|
||||
}
|
||||
|
||||
// Handle paths without extensions (not already handled by static middleware)
|
||||
if (req.path.indexOf('.') === -1) {
|
||||
const file = `${req.path}.html`;
|
||||
return res.sendFile(path.join(__dirname, 'public', file), (err) => {
|
||||
if (err) {
|
||||
next();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
// Configure multer for file uploads (memory storage for CV uploads)
|
||||
const upload = multer({
|
||||
storage: multer.memoryStorage(),
|
||||
limits: {
|
||||
fileSize: config.features.maxCvSizeMB * 1024 * 1024,
|
||||
},
|
||||
fileFilter: (req, file, cb) => {
|
||||
const allowedTypes = config.features.allowedCvTypes;
|
||||
const isAllowed = allowedTypes.includes(file.mimetype);
|
||||
|
||||
if (isAllowed) {
|
||||
return cb(null, true);
|
||||
}
|
||||
cb(new Error('File type not allowed. Only PDF, DOC, and DOCX files are accepted.'));
|
||||
}
|
||||
});
|
||||
|
||||
// Auth middleware
|
||||
const requireAuth = (req, res, next) => {
|
||||
if (req.session.adminId) {
|
||||
next();
|
||||
} else {
|
||||
res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// PUBLIC API ENDPOINTS
|
||||
// ============================================
|
||||
|
||||
// Get company configuration for frontend
|
||||
app.get('/api/config', (req, res) => {
|
||||
res.json({
|
||||
company: config.company,
|
||||
branding: config.branding,
|
||||
contact: config.contact,
|
||||
social: config.social,
|
||||
about: config.about,
|
||||
services: config.services
|
||||
});
|
||||
});
|
||||
|
||||
// Get all active job postings
|
||||
app.get('/api/jobs', async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
'SELECT id, title, department, location, employment_type, salary_range, description, requirements, benefits, created_at FROM job_postings WHERE is_active = true ORDER BY created_at DESC'
|
||||
);
|
||||
res.json(result.rows);
|
||||
} catch (err) {
|
||||
console.error('Error fetching jobs:', err);
|
||||
res.status(500).json({ error: 'Failed to fetch jobs' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get single job posting
|
||||
app.get('/api/jobs/:id', async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
'SELECT id, title, department, location, employment_type, salary_range, description, requirements, benefits, created_at FROM job_postings WHERE id = $1 AND is_active = true',
|
||||
[req.params.id]
|
||||
);
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Job not found' });
|
||||
}
|
||||
res.json(result.rows[0]);
|
||||
} catch (err) {
|
||||
console.error('Error fetching job:', err);
|
||||
res.status(500).json({ error: 'Failed to fetch job' });
|
||||
}
|
||||
});
|
||||
|
||||
// Submit job application with CV
|
||||
app.post('/api/apply', upload.single('cv'), async (req, res) => {
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
const {
|
||||
fullName,
|
||||
email,
|
||||
phone,
|
||||
linkedinUrl,
|
||||
portfolioUrl,
|
||||
yearsOfExperience,
|
||||
currentPosition,
|
||||
currentCompany,
|
||||
preferredLocation,
|
||||
jobId,
|
||||
coverLetter
|
||||
} = req.body;
|
||||
|
||||
// Validate required fields
|
||||
if (!fullName || !email || !req.file) {
|
||||
return res.status(400).json({ error: 'Name, email, and CV are required' });
|
||||
}
|
||||
|
||||
// Insert applicant
|
||||
const applicantResult = await client.query(
|
||||
`INSERT INTO applicants (full_name, email, phone, linkedin_url, portfolio_url, years_of_experience, current_position, current_company, preferred_location)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
RETURNING id`,
|
||||
[fullName, email, phone, linkedinUrl, portfolioUrl, yearsOfExperience, currentPosition, currentCompany, preferredLocation]
|
||||
);
|
||||
|
||||
const applicantId = applicantResult.rows[0].id;
|
||||
|
||||
// Insert application with CV
|
||||
await client.query(
|
||||
`INSERT INTO applications (applicant_id, job_id, cover_letter, cv_filename, cv_content_type, cv_file, status)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, 'new')`,
|
||||
[
|
||||
applicantId,
|
||||
jobId || null,
|
||||
coverLetter,
|
||||
req.file.originalname,
|
||||
req.file.mimetype,
|
||||
req.file.buffer
|
||||
]
|
||||
);
|
||||
|
||||
await client.query('COMMIT');
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Application submitted successfully',
|
||||
applicantId
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
await client.query('ROLLBACK');
|
||||
console.error('Error submitting application:', err);
|
||||
res.status(500).json({ error: 'Failed to submit application' });
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
});
|
||||
|
||||
// Submit contact form
|
||||
app.post('/api/contact', async (req, res) => {
|
||||
try {
|
||||
const { name, email, subject, message } = req.body;
|
||||
|
||||
if (!name || !email || !message) {
|
||||
return res.status(400).json({ error: 'Name, email, and message are required' });
|
||||
}
|
||||
|
||||
await pool.query(
|
||||
'INSERT INTO contact_submissions (name, email, subject, message) VALUES ($1, $2, $3, $4)',
|
||||
[name, email, subject, message]
|
||||
);
|
||||
|
||||
res.json({ success: true, message: 'Message sent successfully' });
|
||||
} catch (err) {
|
||||
console.error('Error submitting contact form:', err);
|
||||
res.status(500).json({ error: 'Failed to send message' });
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// ADMIN AUTHENTICATION ENDPOINTS
|
||||
// ============================================
|
||||
|
||||
// Admin login (creates admin on first login if none exist)
|
||||
app.post('/api/admin/login', async (req, res) => {
|
||||
try {
|
||||
const { email, password, fullName, 'full-name': fullNameHyphen } = req.body;
|
||||
const name = fullName || fullNameHyphen;
|
||||
|
||||
if (!email || !password) {
|
||||
return res.status(400).json({ error: 'Email and password are required' });
|
||||
}
|
||||
|
||||
// Check if any admins exist
|
||||
const adminCount = await pool.query('SELECT COUNT(*) FROM admins');
|
||||
const isFirstAdmin = adminCount.rows[0].count === '0';
|
||||
|
||||
if (isFirstAdmin) {
|
||||
// Create first admin
|
||||
if (!name) {
|
||||
return res.status(400).json({ error: 'Full name is required for first admin creation' });
|
||||
}
|
||||
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
const result = await pool.query(
|
||||
'INSERT INTO admins (email, password_hash, full_name) VALUES ($1, $2, $3) RETURNING id, email, full_name',
|
||||
[email, hashedPassword, name]
|
||||
);
|
||||
|
||||
const admin = result.rows[0];
|
||||
req.session.adminId = admin.id;
|
||||
req.session.adminEmail = admin.email;
|
||||
req.session.adminName = admin.full_name;
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
message: 'Admin account created successfully',
|
||||
admin: { id: admin.id, email: admin.email, fullName: admin.full_name },
|
||||
isFirstLogin: true
|
||||
});
|
||||
}
|
||||
|
||||
// Regular login
|
||||
const result = await pool.query(
|
||||
'SELECT id, email, password_hash, full_name FROM admins WHERE email = $1',
|
||||
[email]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(401).json({ error: 'Invalid credentials' });
|
||||
}
|
||||
|
||||
const admin = result.rows[0];
|
||||
const passwordMatch = await bcrypt.compare(password, admin.password_hash);
|
||||
|
||||
if (!passwordMatch) {
|
||||
return res.status(401).json({ error: 'Invalid credentials' });
|
||||
}
|
||||
|
||||
// Update last login
|
||||
await pool.query('UPDATE admins SET last_login = CURRENT_TIMESTAMP WHERE id = $1', [admin.id]);
|
||||
|
||||
req.session.adminId = admin.id;
|
||||
req.session.adminEmail = admin.email;
|
||||
req.session.adminName = admin.full_name;
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
admin: { id: admin.id, email: admin.email, fullName: admin.full_name }
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
console.error('Error during login:', err);
|
||||
res.status(500).json({ error: 'Login failed' });
|
||||
}
|
||||
});
|
||||
|
||||
// Check if admin is logged in
|
||||
app.get('/api/admin/check', (req, res) => {
|
||||
if (req.session.adminId) {
|
||||
res.json({
|
||||
loggedIn: true,
|
||||
admin: {
|
||||
id: req.session.adminId,
|
||||
email: req.session.adminEmail,
|
||||
fullName: req.session.adminName
|
||||
}
|
||||
});
|
||||
} else {
|
||||
res.json({ loggedIn: false });
|
||||
}
|
||||
});
|
||||
|
||||
// Check if first admin needs to be created
|
||||
app.get('/api/admin/check-first', async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query('SELECT COUNT(*) FROM admins');
|
||||
const isFirstAdmin = result.rows[0].count === '0';
|
||||
res.json({ isFirstAdmin });
|
||||
} catch (err) {
|
||||
console.error('Error checking first admin:', err);
|
||||
res.status(500).json({ error: 'Failed to check admin status' });
|
||||
}
|
||||
});
|
||||
|
||||
// Admin logout
|
||||
app.post('/api/admin/logout', (req, res) => {
|
||||
req.session.destroy((err) => {
|
||||
if (err) {
|
||||
return res.status(500).json({ error: 'Logout failed' });
|
||||
}
|
||||
res.json({ success: true });
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// ADMIN API ENDPOINTS (Protected)
|
||||
// ============================================
|
||||
|
||||
// Get dashboard statistics
|
||||
app.get('/api/admin/stats', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const stats = await pool.query(`
|
||||
SELECT
|
||||
(SELECT COUNT(*) FROM applications WHERE status = 'new') as new_applications,
|
||||
(SELECT COUNT(*) FROM applications) as total_applications,
|
||||
(SELECT COUNT(*) FROM applicants) as total_applicants,
|
||||
(SELECT COUNT(*) FROM job_postings WHERE is_active = true) as active_jobs,
|
||||
(SELECT COUNT(*) FROM contact_submissions WHERE is_read = false) as unread_messages
|
||||
`);
|
||||
|
||||
res.json(stats.rows[0]);
|
||||
} catch (err) {
|
||||
console.error('Error fetching stats:', err);
|
||||
res.status(500).json({ error: 'Failed to fetch statistics' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get all applications with applicant details
|
||||
app.get('/api/admin/applications', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const { status, jobId, search } = req.query;
|
||||
|
||||
let query = `
|
||||
SELECT
|
||||
app.id,
|
||||
app.status,
|
||||
app.applied_at,
|
||||
app.cv_filename,
|
||||
app.cover_letter,
|
||||
app.notes,
|
||||
applicant.id as applicant_id,
|
||||
applicant.full_name,
|
||||
applicant.email,
|
||||
applicant.phone,
|
||||
applicant.years_of_experience,
|
||||
applicant.current_position,
|
||||
applicant.current_company,
|
||||
job.id as job_id,
|
||||
job.title as job_title
|
||||
FROM applications app
|
||||
INNER JOIN applicants applicant ON app.applicant_id = applicant.id
|
||||
LEFT JOIN job_postings job ON app.job_id = job.id
|
||||
WHERE 1=1
|
||||
`;
|
||||
|
||||
const params = [];
|
||||
let paramCount = 0;
|
||||
|
||||
if (status) {
|
||||
paramCount++;
|
||||
query += ` AND app.status = $${paramCount}`;
|
||||
params.push(status);
|
||||
}
|
||||
|
||||
if (jobId) {
|
||||
paramCount++;
|
||||
query += ` AND app.job_id = $${paramCount}`;
|
||||
params.push(jobId);
|
||||
}
|
||||
|
||||
if (search) {
|
||||
paramCount++;
|
||||
query += ` AND (applicant.full_name ILIKE $${paramCount} OR applicant.email ILIKE $${paramCount})`;
|
||||
params.push(`%${search}%`);
|
||||
}
|
||||
|
||||
query += ' ORDER BY app.applied_at DESC';
|
||||
|
||||
const result = await pool.query(query, params);
|
||||
res.json(result.rows);
|
||||
} catch (err) {
|
||||
console.error('Error fetching applications:', err);
|
||||
res.status(500).json({ error: 'Failed to fetch applications' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get single application details
|
||||
app.get('/api/admin/applications/:id', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query(`
|
||||
SELECT
|
||||
app.*,
|
||||
applicant.full_name,
|
||||
applicant.email,
|
||||
applicant.phone,
|
||||
applicant.linkedin_url,
|
||||
applicant.portfolio_url,
|
||||
applicant.years_of_experience,
|
||||
applicant.current_position,
|
||||
applicant.current_company,
|
||||
applicant.preferred_location,
|
||||
job.title as job_title
|
||||
FROM applications app
|
||||
INNER JOIN applicants applicant ON app.applicant_id = applicant.id
|
||||
LEFT JOIN job_postings job ON app.job_id = job.id
|
||||
WHERE app.id = $1
|
||||
`, [req.params.id]);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Application not found' });
|
||||
}
|
||||
|
||||
// Don't send the CV file in this response
|
||||
const application = result.rows[0];
|
||||
delete application.cv_file;
|
||||
|
||||
res.json(application);
|
||||
} catch (err) {
|
||||
console.error('Error fetching application:', err);
|
||||
res.status(500).json({ error: 'Failed to fetch application' });
|
||||
}
|
||||
});
|
||||
|
||||
// Download CV
|
||||
app.get('/api/admin/applications/:id/cv', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
'SELECT cv_file, cv_filename, cv_content_type FROM applications WHERE id = $1',
|
||||
[req.params.id]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0 || !result.rows[0].cv_file) {
|
||||
return res.status(404).json({ error: 'CV not found' });
|
||||
}
|
||||
|
||||
const { cv_file, cv_filename, cv_content_type } = result.rows[0];
|
||||
|
||||
res.setHeader('Content-Type', cv_content_type);
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${cv_filename}"`);
|
||||
res.send(cv_file);
|
||||
} catch (err) {
|
||||
console.error('Error downloading CV:', err);
|
||||
res.status(500).json({ error: 'Failed to download CV' });
|
||||
}
|
||||
});
|
||||
|
||||
// Update application status
|
||||
app.patch('/api/admin/applications/:id', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const { status, notes } = req.body;
|
||||
|
||||
const result = await pool.query(
|
||||
'UPDATE applications SET status = COALESCE($1, status), notes = COALESCE($2, notes) WHERE id = $3 RETURNING *',
|
||||
[status, notes, req.params.id]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Application not found' });
|
||||
}
|
||||
|
||||
res.json(result.rows[0]);
|
||||
} catch (err) {
|
||||
console.error('Error updating application:', err);
|
||||
res.status(500).json({ error: 'Failed to update application' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get all job postings (admin view - includes inactive)
|
||||
app.get('/api/admin/jobs', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
'SELECT * FROM job_postings ORDER BY created_at DESC'
|
||||
);
|
||||
res.json(result.rows);
|
||||
} catch (err) {
|
||||
console.error('Error fetching jobs:', err);
|
||||
res.status(500).json({ error: 'Failed to fetch jobs' });
|
||||
}
|
||||
});
|
||||
|
||||
// Create job posting
|
||||
app.post('/api/admin/jobs', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
title,
|
||||
department,
|
||||
location,
|
||||
employmentType,
|
||||
salaryRange,
|
||||
description,
|
||||
requirements,
|
||||
benefits
|
||||
} = req.body;
|
||||
|
||||
if (!title || !description) {
|
||||
return res.status(400).json({ error: 'Title and description are required' });
|
||||
}
|
||||
|
||||
const result = await pool.query(
|
||||
`INSERT INTO job_postings
|
||||
(title, department, location, employment_type, salary_range, description, requirements, benefits, created_by)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
RETURNING *`,
|
||||
[title, department, location, employmentType, salaryRange, description, requirements, benefits, req.session.adminId]
|
||||
);
|
||||
|
||||
res.json(result.rows[0]);
|
||||
} catch (err) {
|
||||
console.error('Error creating job:', err);
|
||||
res.status(500).json({ error: 'Failed to create job posting' });
|
||||
}
|
||||
});
|
||||
|
||||
// Update job posting
|
||||
app.patch('/api/admin/jobs/:id', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
title,
|
||||
department,
|
||||
location,
|
||||
employmentType,
|
||||
salaryRange,
|
||||
description,
|
||||
requirements,
|
||||
benefits,
|
||||
isActive
|
||||
} = req.body;
|
||||
|
||||
const result = await pool.query(
|
||||
`UPDATE job_postings
|
||||
SET title = COALESCE($1, title),
|
||||
department = COALESCE($2, department),
|
||||
location = COALESCE($3, location),
|
||||
employment_type = COALESCE($4, employment_type),
|
||||
salary_range = COALESCE($5, salary_range),
|
||||
description = COALESCE($6, description),
|
||||
requirements = COALESCE($7, requirements),
|
||||
benefits = COALESCE($8, benefits),
|
||||
is_active = COALESCE($9, is_active)
|
||||
WHERE id = $10
|
||||
RETURNING *`,
|
||||
[title, department, location, employmentType, salaryRange, description, requirements, benefits, isActive, req.params.id]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Job not found' });
|
||||
}
|
||||
|
||||
res.json(result.rows[0]);
|
||||
} catch (err) {
|
||||
console.error('Error updating job:', err);
|
||||
res.status(500).json({ error: 'Failed to update job posting' });
|
||||
}
|
||||
});
|
||||
|
||||
// Delete job posting
|
||||
app.delete('/api/admin/jobs/:id', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
'DELETE FROM job_postings WHERE id = $1 RETURNING id',
|
||||
[req.params.id]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Job not found' });
|
||||
}
|
||||
|
||||
res.json({ success: true, message: 'Job deleted successfully' });
|
||||
} catch (err) {
|
||||
console.error('Error deleting job:', err);
|
||||
res.status(500).json({ error: 'Failed to delete job posting' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get contact submissions
|
||||
app.get('/api/admin/contacts', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
'SELECT * FROM contact_submissions ORDER BY created_at DESC'
|
||||
);
|
||||
res.json(result.rows);
|
||||
} catch (err) {
|
||||
console.error('Error fetching contacts:', err);
|
||||
res.status(500).json({ error: 'Failed to fetch contact submissions' });
|
||||
}
|
||||
});
|
||||
|
||||
// Mark contact as read
|
||||
app.patch('/api/admin/contacts/:id', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
'UPDATE contact_submissions SET is_read = true WHERE id = $1 RETURNING *',
|
||||
[req.params.id]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Contact submission not found' });
|
||||
}
|
||||
|
||||
res.json(result.rows[0]);
|
||||
} catch (err) {
|
||||
console.error('Error updating contact:', err);
|
||||
res.status(500).json({ error: 'Failed to update contact submission' });
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// ERROR HANDLING
|
||||
// ============================================
|
||||
|
||||
app.use((err, req, res, next) => {
|
||||
if (err instanceof multer.MulterError) {
|
||||
if (err.code === 'LIMIT_FILE_SIZE') {
|
||||
return res.status(400).json({ error: 'File size too large. Maximum size is 5MB.' });
|
||||
}
|
||||
return res.status(400).json({ error: err.message });
|
||||
}
|
||||
|
||||
console.error('Unhandled error:', err);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// START SERVER
|
||||
// ============================================
|
||||
|
||||
app.listen(PORT, '0.0.0.0', () => {
|
||||
console.log(`AI Recruitment Site running on port ${PORT}`);
|
||||
console.log(`Environment: ${process.env.NODE_ENV || 'development'}`);
|
||||
console.log(`Database: ${process.env.DB_HOST || 'postgres'}:${process.env.DB_PORT || 5432}`);
|
||||
});
|
||||
|
||||
// Graceful shutdown
|
||||
process.on('SIGTERM', () => {
|
||||
console.log('SIGTERM signal received: closing HTTP server');
|
||||
pool.end(() => {
|
||||
console.log('Database pool closed');
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user