Initial commit: AI Recruitment Site for Ryans Recruit Firm

- Complete PostgreSQL schema with migrations
- Node.js/Express backend with authentication
- Public website (home, about, services, jobs, apply, contact)
- Admin dashboard with applicant and job management
- CV upload and storage in PostgreSQL BYTEA
- Docker Compose setup for deployment
- Session-based authentication
- Responsive design with Ryan brand colors
This commit is contained in:
Mikael Westöö
2026-01-23 21:17:24 +01:00
commit 406d278a39
23 changed files with 3842 additions and 0 deletions

11
.dockerignore Normal file
View File

@@ -0,0 +1,11 @@
node_modules
npm-debug.log
.git
.gitignore
.env
.env.local
README.md
.DS_Store
*.log
.vscode
.idea

16
.env.example Normal file
View File

@@ -0,0 +1,16 @@
# Database Configuration
DB_HOST=postgres
DB_PORT=5432
DB_NAME=recruitment
DB_USER=postgres
DB_PASSWORD=changeme123
# Application Configuration
PORT=3000
NODE_ENV=production
# Session Secret (CHANGE THIS IN PRODUCTION!)
SESSION_SECRET=your-very-secret-session-key-change-this-in-production
# Optional: Application URL
APP_URL=http://localhost:3000

10
.gitignore vendored Normal file
View File

@@ -0,0 +1,10 @@
node_modules/
.env
.env.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.DS_Store
*.log
.vscode/
.idea/

29
Dockerfile Normal file
View File

@@ -0,0 +1,29 @@
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 public ./public
COPY migrations ./migrations
# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001 && \
chown -R nodejs:nodejs /app
USER nodejs
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"
CMD ["node", "server.js"]

368
README.md Normal file
View File

@@ -0,0 +1,368 @@
# Ryans Recruit Firm - AI Recruitment Site
A professional recruitment website built with Node.js, Express, and PostgreSQL. Features include job listings, application management, CV storage, and admin dashboard.
## Features
### Public Features
- **Job Listings** - Browse open positions with detailed information
- **Online Applications** - Submit applications with CV upload
- **Contact Form** - Get in touch with the recruitment team
- **Responsive Design** - Works on desktop, tablet, and mobile
### Admin Features
- **Dashboard** - Overview statistics and quick access
- **Application Management** - View, filter, and manage applicants
- **CV Downloads** - Download applicant CVs directly
- **Job Management** - Create, edit, and manage job postings
- **First-time Setup** - Automatic admin account creation on first login
## Technology Stack
- **Backend**: Node.js + Express
- **Database**: PostgreSQL with BYTEA for CV storage
- **Authentication**: Session-based with bcrypt
- **File Upload**: Multer (PDF, DOC, DOCX support)
- **Deployment**: Docker + Docker Compose + Coolify
## Quick Start
### Local Development
1. **Clone the repository**
```bash
git clone <repository-url>
cd ai-recruit-site-template
```
2. **Install dependencies**
```bash
npm install
```
3. **Set up environment**
```bash
cp .env.example .env
# Edit .env with your configuration
```
4. **Start PostgreSQL** (if not using Docker)
```bash
# Install PostgreSQL and create database 'recruitment'
psql -U postgres -c "CREATE DATABASE recruitment;"
```
5. **Run migrations**
```bash
psql -U postgres -d recruitment -f migrations/001_init_schema.sql
psql -U postgres -d recruitment -f migrations/002_seed_data.sql
```
6. **Start the application**
```bash
npm start
```
7. **Access the site**
- Public site: http://localhost:3000
- Admin panel: http://localhost:3000/admin/login
### Docker Development
1. **Start all services**
```bash
docker-compose up -d
```
2. **View logs**
```bash
docker-compose logs -f app
```
3. **Stop services**
```bash
docker-compose down
```
## Deployment to Coolify
### Prerequisites
- Coolify account and API token
- Gitea repository
- Cloudflare account (for DNS)
### Environment Variables
```bash
export COOLIFY_TOKEN="your-coolify-api-token"
export GITEA_TOKEN="your-gitea-access-token"
export CLOUDFLARE_TOKEN="your-cloudflare-api-token"
export COOLIFY_API="https://app.coolify.io/api/v1"
export GITEA_API="https://git.startanaicompany.com/api/v1"
export CLOUDFLARE_API="https://api.cloudflare.com/client/v4"
```
### Step 1: Generate SSH Deploy Keys
```bash
ssh-keygen -t ed25519 -f /tmp/recruitai_deploy_key -C "coolify-recruitai-deploy" -N ""
```
### Step 2: Create Coolify Project
```bash
curl -s -X POST "${COOLIFY_API}/projects" \
-H "Authorization: Bearer ${COOLIFY_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"name": "RecruitAI",
"description": "AI Recruitment Site"
}'
```
### Step 3: Add Deploy Key to Coolify
```bash
PRIVATE_KEY=$(cat /tmp/recruitai_deploy_key | jq -R -s .)
curl -s -X POST "${COOLIFY_API}/private-keys" \
-H "Authorization: Bearer ${COOLIFY_TOKEN}" \
-H "Content-Type: application/json" \
-d "{
\"name\": \"recruitai-deploy-key\",
\"description\": \"Deploy key for recruitai repository\",
\"private_key\": ${PRIVATE_KEY}
}"
```
### Step 4: Push to Gitea
```bash
git remote add origin git@git.startanaicompany.com:username/ai-recruit-site-template.git
git add .
git commit -m "Initial commit: AI Recruitment Site"
git push -u origin main
```
### Step 5: Add Deploy Key to Gitea
```bash
cat > /tmp/gitea_deploy_key.json << EOF
{
"title": "Coolify Deploy Key",
"key": "$(cat /tmp/recruitai_deploy_key.pub)",
"read_only": true
}
EOF
curl -s -X POST "${GITEA_API}/repos/username/ai-recruit-site-template/keys" \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d @/tmp/gitea_deploy_key.json
```
### Step 6: Create Coolify Application
```bash
curl -s -X POST "${COOLIFY_API}/applications/public" \
-H "Authorization: Bearer ${COOLIFY_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"project_uuid": "YOUR_PROJECT_UUID",
"environment_name": "production",
"server_uuid": 0,
"type": "public",
"source": {
"type": "git",
"url": "git@git.startanaicompany.com:username/ai-recruit-site-template",
"branch": "main",
"private_key_uuid": "YOUR_PRIVATE_KEY_UUID"
},
"build_pack": "dockerfile",
"ports_exposes": "3000",
"name": "recruitai"
}'
```
### Step 7: Add Webhook to Gitea
```bash
curl -s -X POST "${GITEA_API}/repos/username/ai-recruit-site-template/hooks" \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"type": "gitea",
"config": {
"url": "https://app.coolify.io/api/v1/deploy?uuid=YOUR_APP_UUID",
"content_type": "json",
"http_method": "GET"
},
"events": ["push"],
"active": true
}'
```
### Step 8: Configure Cloudflare DNS
```bash
curl -s -X POST "${CLOUDFLARE_API}/zones/YOUR_ZONE_ID/dns_records" \
-H "Authorization: Bearer ${CLOUDFLARE_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"type": "CNAME",
"name": "recruitai",
"content": "YOUR_COOLIFY_FQDN",
"ttl": 1,
"proxied": false
}'
```
### Step 9: Update Application Domain
```bash
curl -s -X PATCH "${COOLIFY_API}/applications/YOUR_APP_UUID" \
-H "Authorization: Bearer ${COOLIFY_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"domains": "http://recruitai.startanaicompany.com"
}'
```
### Step 10: Deploy
```bash
curl -s -X GET "${COOLIFY_API}/deploy?uuid=YOUR_APP_UUID&force=true" \
-H "Authorization: Bearer ${COOLIFY_TOKEN}"
```
## First Time Setup
### Admin Account
1. Navigate to `/admin/login`
2. On first visit, you'll be prompted to create an admin account
3. Enter your email, password, and full name
4. Click "Create Admin Account"
5. You'll be logged in automatically
### Add Jobs
1. Go to Admin Dashboard → Jobs
2. Click "Create New Job"
3. Fill in job details
4. Click "Save"
## Database Schema
### Tables
- **admins** - Admin user accounts
- **job_postings** - Job listings
- **applicants** - Applicant information
- **applications** - Application submissions with CV storage (BYTEA)
- **contact_submissions** - Contact form messages
### Migrations
Migrations are located in `/migrations/` and run automatically on first Docker startup.
## API Endpoints
### Public APIs
- `GET /api/jobs` - List all 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
- `GET /api/admin/applications/:id` - Get application details
- `GET /api/admin/applications/:id/cv` - Download CV
- `PATCH /api/admin/applications/:id` - Update application
- `GET /api/admin/jobs` - List all jobs
- `POST /api/admin/jobs` - Create job
- `PATCH /api/admin/jobs/:id` - Update job
- `DELETE /api/admin/jobs/:id` - Delete job
## Security Features
- **Password Hashing** - bcrypt with 10 rounds
- **Session Management** - Secure HTTP-only cookies
- **SQL Injection Protection** - Parameterized queries
- **File Upload Validation** - Type and size checks
- **CSRF Protection** - Session-based authentication
- **Rate Limiting** - (Recommended to add in production)
## Environment Variables
| Variable | Description | Default |
|----------|-------------|---------|
| `DB_HOST` | PostgreSQL host | `postgres` |
| `DB_PORT` | PostgreSQL port | `5432` |
| `DB_NAME` | Database name | `recruitment` |
| `DB_USER` | Database user | `postgres` |
| `DB_PASSWORD` | Database password | `changeme123` |
| `PORT` | Application port | `3000` |
| `NODE_ENV` | Environment | `production` |
| `SESSION_SECRET` | Session encryption key | *(required)* |
## File Upload Limits
- **Max CV Size**: 5MB
- **Allowed Types**: PDF, DOC, DOCX
- **Storage**: PostgreSQL BYTEA column
## Troubleshooting
### Database Connection Issues
```bash
# Check if PostgreSQL is running
docker-compose ps
# View PostgreSQL logs
docker-compose logs postgres
# Restart PostgreSQL
docker-compose restart postgres
```
### Migration Errors
```bash
# Connect to database
docker-compose exec postgres psql -U postgres -d recruitment
# Check tables
\dt
# Re-run migrations manually
docker-compose exec postgres psql -U postgres -d recruitment -f /docker-entrypoint-initdb.d/001_init_schema.sql
```
### Application Won't Start
```bash
# Check application logs
docker-compose logs app
# Rebuild and restart
docker-compose down
docker-compose build --no-cache
docker-compose up -d
```
## License
MIT
## Support
For issues or questions, contact: info@ryansrecruit.com

54
docker-compose.yml Normal file
View File

@@ -0,0 +1,54 @@
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
- ./migrations:/docker-entrypoint-initdb.d
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:
- ./public:/app/public
volumes:
postgres_data:
driver: local
networks:
recruitment-network:
driver: bridge

View 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 $$;

View 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
View 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"
}
}

177
public/about.html Normal file
View File

@@ -0,0 +1,177 @@
<!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">
</head>
<body>
<header>
<nav class="container">
<div class="logo">Ryans Recruit Firm</div>
<ul class="nav-links">
<li><a href="/">Home</a></li>
<li><a href="/about.html" class="active">About</a></li>
<li><a href="/services.html">Services</a></li>
<li><a href="/jobs.html">Jobs</a></li>
<li><a href="/contact.html">Contact</a></li>
<li><a href="/admin/login.html" 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.html" class="btn btn-primary btn-lg">Browse Jobs</a>
<a href="/contact.html" 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>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.html">About Us</a></li>
<li><a href="/services.html">Services</a></li>
<li><a href="/jobs.html">Job Listings</a></li>
<li><a href="/contact.html">Contact</a></li>
</ul>
</div>
<div class="footer-section">
<h3>For Employers</h3>
<ul>
<li><a href="/contact.html">Post a Job</a></li>
<li><a href="/contact.html">Our Process</a></li>
<li><a href="/contact.html">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>&copy; 2026 Ryans Recruit Firm. All rights reserved.</p>
</div>
</div>
</footer>
</body>
</html>

View File

@@ -0,0 +1,82 @@
<!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">
</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.html" 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.html" style="color: #2563EB; text-decoration: none; font-weight: 500; font-size: 14px;">Applicants</a>
<a href="/admin/jobs.html" 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>

View File

@@ -0,0 +1,88 @@
<!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">
</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.html" style="color: #2563EB; text-decoration: none; font-weight: 500; font-size: 14px;">Dashboard</a>
<a href="/admin/applicants.html" 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.html" 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.html" 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.html" 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>

75
public/admin/jobs.html Normal file
View File

@@ -0,0 +1,75 @@
<!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">
</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.html" 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.html" 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.html" 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>

82
public/admin/login.html Normal file
View File

@@ -0,0 +1,82 @@
<!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">
</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>

171
public/apply.html Normal file
View File

@@ -0,0 +1,171 @@
<!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">
</head>
<body>
<header>
<nav class="container">
<div class="logo">Ryans Recruit Firm</div>
<ul class="nav-links">
<li><a href="/">Home</a></li>
<li><a href="/about.html">About</a></li>
<li><a href="/services.html">Services</a></li>
<li><a href="/jobs.html">Jobs</a></li>
<li><a href="/contact.html">Contact</a></li>
<li><a href="/admin/login.html" 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>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.html">About Us</a></li>
<li><a href="/services.html">Services</a></li>
<li><a href="/jobs.html">Job Listings</a></li>
<li><a href="/contact.html">Contact</a></li>
</ul>
</div>
<div class="footer-section">
<h3>For Employers</h3>
<ul>
<li><a href="/contact.html">Post a Job</a></li>
<li><a href="/contact.html">Our Process</a></li>
<li><a href="/contact.html">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>&copy; 2026 Ryans Recruit Firm. All rights reserved.</p>
</div>
</div>
</footer>
<script src="/js/main.js"></script>
</body>
</html>

169
public/contact.html Normal file
View File

@@ -0,0 +1,169 @@
<!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">
</head>
<body>
<header>
<nav class="container">
<div class="logo">Ryans Recruit Firm</div>
<ul class="nav-links">
<li><a href="/">Home</a></li>
<li><a href="/about.html">About</a></li>
<li><a href="/services.html">Services</a></li>
<li><a href="/jobs.html">Jobs</a></li>
<li><a href="/contact.html" class="active">Contact</a></li>
<li><a href="/admin/login.html" 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>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.html">About Us</a></li>
<li><a href="/services.html">Services</a></li>
<li><a href="/jobs.html">Job Listings</a></li>
<li><a href="/contact.html">Contact</a></li>
</ul>
</div>
<div class="footer-section">
<h3>For Employers</h3>
<ul>
<li><a href="/contact.html">Post a Job</a></li>
<li><a href="/contact.html">Our Process</a></li>
<li><a href="/contact.html">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>&copy; 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
View 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;
}
}

243
public/index.html Normal file
View File

@@ -0,0 +1,243 @@
<!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">
</head>
<body>
<header>
<nav class="container">
<div class="logo">Ryans Recruit Firm</div>
<ul class="nav-links">
<li><a href="/" class="active">Home</a></li>
<li><a href="/about.html">About</a></li>
<li><a href="/services.html">Services</a></li>
<li><a href="/jobs.html">Jobs</a></li>
<li><a href="/contact.html">Contact</a></li>
<li><a href="/admin/login.html" class="btn btn-primary btn-sm">Admin</a></li>
</ul>
</nav>
</header>
<section class="hero">
<div class="container">
<h1>Your Career Success Is Our Mission</h1>
<p>Connecting talented professionals with leading companies worldwide</p>
<div style="display: flex; gap: 1rem; justify-content: center; margin-top: 2rem;">
<a href="/jobs.html" class="btn btn-primary btn-lg">Browse Jobs</a>
<a href="/contact.html" class="btn btn-secondary btn-lg">Get in Touch</a>
</div>
</div>
</section>
<section>
<div class="container">
<div class="section-title">
<h2>Why Choose Ryans Recruit Firm?</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.html" 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.html" class="btn btn-lg" style="background: white; color: var(--primary-color);">Browse Jobs</a>
<a href="/contact.html" 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>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.html">About Us</a></li>
<li><a href="/services.html">Services</a></li>
<li><a href="/jobs.html">Job Listings</a></li>
<li><a href="/contact.html">Contact</a></li>
</ul>
</div>
<div class="footer-section">
<h3>For Employers</h3>
<ul>
<li><a href="/contact.html">Post a Job</a></li>
<li><a href="/contact.html">Our Process</a></li>
<li><a href="/contact.html">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>&copy; 2026 Ryans Recruit Firm. All rights reserved.</p>
</div>
</div>
</footer>
</body>
</html>

92
public/jobs.html Normal file
View File

@@ -0,0 +1,92 @@
<!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">
</head>
<body>
<header>
<nav class="container">
<div class="logo">Ryans Recruit Firm</div>
<ul class="nav-links">
<li><a href="/">Home</a></li>
<li><a href="/about.html">About</a></li>
<li><a href="/services.html">Services</a></li>
<li><a href="/jobs.html" class="active">Jobs</a></li>
<li><a href="/contact.html">Contact</a></li>
<li><a href="/admin/login.html" 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>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.html">About Us</a></li>
<li><a href="/services.html">Services</a></li>
<li><a href="/jobs.html">Job Listings</a></li>
<li><a href="/contact.html">Contact</a></li>
</ul>
</div>
<div class="footer-section">
<h3>For Employers</h3>
<ul>
<li><a href="/contact.html">Post a Job</a></li>
<li><a href="/contact.html">Our Process</a></li>
<li><a href="/contact.html">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>&copy; 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
View 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.html')) {
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.html';
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.html';
}
}
// Login page handlers
if (window.location.pathname.includes('login.html')) {
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.html', 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.html')) {
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.html')) {
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.html?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.html') && 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.html';
} catch (err) {
console.error('Logout error:', err);
window.location.href = '/admin/login.html';
}
}
// 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);
}

201
public/js/main.js Normal file
View File

@@ -0,0 +1,201 @@
// Main JavaScript for Ryans Recruit Firm
// Load jobs on jobs.html page
if (window.location.pathname.includes('jobs.html')) {
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.html?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.html')) {
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.html', 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.html')) {
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;
}
}
});
});
});

109
public/services.html Normal file
View File

@@ -0,0 +1,109 @@
<!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">
</head>
<body>
<header>
<nav class="container">
<div class="logo">Ryans Recruit Firm</div>
<ul class="nav-links">
<li><a href="/">Home</a></li>
<li><a href="/about.html">About</a></li>
<li><a href="/services.html" class="active">Services</a></li>
<li><a href="/jobs.html">Jobs</a></li>
<li><a href="/contact.html">Contact</a></li>
<li><a href="/admin/login.html" 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>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.html">About Us</a></li>
<li><a href="/services.html">Services</a></li>
<li><a href="/jobs.html">Job Listings</a></li>
<li><a href="/contact.html">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>&copy; 2026 Ryans Recruit Firm. All rights reserved.</p>
</div>
</div>
</footer>
</body>
</html>

669
server.js Normal file
View File

@@ -0,0 +1,669 @@
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 app = express();
const PORT = process.env.PORT || 3000;
// PostgreSQL connection
const pool = new Pool({
host: process.env.DB_HOST || 'postgres',
port: process.env.DB_PORT || 5432,
database: process.env.DB_NAME || 'recruitment',
user: process.env.DB_USER || 'postgres',
password: process.env.DB_PASSWORD || 'postgres',
max: 20,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
});
// Test database connection
pool.query('SELECT NOW()', (err, res) => {
if (err) {
console.error('Database connection error:', err);
} else {
console.log('Database connected successfully at:', res.rows[0].now);
}
});
// Middleware
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
app.use(cookieParser());
app.use(session({
secret: process.env.SESSION_SECRET || 'recruitment-site-secret-key-change-in-production',
resave: false,
saveUninitialized: false,
cookie: {
secure: false, // Set to true if using HTTPS
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000 // 24 hours
}
}));
// Middleware to handle routes without .html extension
app.use((req, res, next) => {
if (req.path.indexOf('.') === -1 && !req.path.startsWith('/api/') && !req.path.startsWith('/admin/')) {
const file = `${req.path}.html`;
res.sendFile(path.join(__dirname, 'public', file), (err) => {
if (err) next();
});
} else if (req.path.indexOf('.') === -1 && req.path.startsWith('/admin/') && req.path !== '/admin/') {
const file = `${req.path}.html`;
res.sendFile(path.join(__dirname, 'public', file), (err) => {
if (err) next();
});
} else {
next();
}
});
// Serve static files
app.use(express.static('public'));
// Configure multer for file uploads (memory storage for CV uploads)
const upload = multer({
storage: multer.memoryStorage(),
limits: {
fileSize: 5 * 1024 * 1024, // 5MB limit
},
fileFilter: (req, file, cb) => {
const allowedTypes = /pdf|doc|docx/;
const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase());
const mimetype = allowedTypes.test(file.mimetype) ||
file.mimetype === 'application/msword' ||
file.mimetype === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
if (mimetype && extname) {
return cb(null, true);
}
cb(new Error('Only PDF, DOC, and DOCX files are allowed'));
}
});
// Auth middleware
const requireAuth = (req, res, next) => {
if (req.session.adminId) {
next();
} else {
res.status(401).json({ error: 'Unauthorized' });
}
};
// ============================================
// PUBLIC API ENDPOINTS
// ============================================
// 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 } = req.body;
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 (!fullName) {
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, fullName]
);
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);
});
});