Compare commits

...

3 Commits

Author SHA1 Message Date
Flatlogic Bot
d5d05fd44f 03 2025-09-24 06:37:02 +00:00
Flatlogic Bot
fa86852137 02 2025-09-24 06:32:27 +00:00
Flatlogic Bot
19ef21748b 01 2025-09-24 06:25:52 +00:00
11 changed files with 1273 additions and 125 deletions

81
api/generate.php Normal file
View File

@ -0,0 +1,81 @@
<?php
session_start();
require_once __DIR__ . '/../includes/gemini.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$apiKey = getenv('GEMINI_API_KEY');
if (!$apiKey) {
header('Content-Type: application/json');
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'API key is not configured.']);
exit;
}
$request_body = file_get_contents('php://input');
$form_data = json_decode($request_body, true);
// --- PROMPT ENGINEERING --- //
$prompt = "Please generate a professional resume based on the following details. Format the output as clean, semantic HTML suitable for direct display on a web page. The design should be modern and professional. Use a single-column layout. Do not include any CSS or ```html markers, only the raw HTML body content.";
$prompt .= "\n\n---\n\n";
// Personal Details
$prompt .= "**Personal Details:**\n";
$prompt .= "- Name: " . ($form_data['fullName'] ?? 'N/A') . "\n";
$prompt .= "- Email: " . ($form_data['email'] ?? 'N/A') . "\n";
$prompt .= "- Phone: " . ($form_data['phone'] ?? 'N/A') . "\n";
$prompt .= "- LinkedIn: " . ($form_data['linkedin'] ?? 'N/A') . "\n\n";
// Summary
if (!empty($form_data['summary'])) {
$prompt .= "**Professional Summary:**\n" . $form_data['summary'] . "\n\n";
}
// Work Experience
if (!empty($form_data['experience'])) {
$prompt .= "**Work Experience:**\n";
foreach ($form_data['experience'] as $exp) {
$prompt .= "- Job Title: " . ($exp['title'] ?? 'N/A') . " at " . ($exp['company'] ?? 'N/A') . "\n";
$prompt .= " (From: " . ($exp['startDate'] ?? 'N/A') . " to " . ($exp['endDate'] ?? 'Present') . ")\n";
if (!empty($exp['responsibilities'])) {
$prompt .= " Responsibilities: " . $exp['responsibilities'] . "\n";
}
}
$prompt .= "\n";
}
// Education
if (!empty($form_data['education'])) {
$prompt .= "**Education:**\n";
foreach ($form_data['education'] as $edu) {
$prompt .= "- Degree: " . ($edu['degree'] ?? 'N/A') . " from " . ($edu['school'] ?? 'N/A') . "\n";
$prompt .= " (From: " . ($edu['startDate'] ?? 'N/A') . " to " . ($edu['endDate'] ?? 'Present') . ")\n";
}
$prompt .= "\n";
}
// Skills
if (!empty($form_data['skills'])) {
$prompt .= "**Skills:**\n" . $form_data['skills'] . "\n";
}
// Call the Gemini API
$api_response = callGeminiApi($prompt, $apiKey);
header('Content-Type: application/json');
if (isset($api_response['error'])) {
http_response_code(500);
echo json_encode(['success' => false, 'message' => $api_response['error'], 'details' => $api_response['response'] ?? null]);
exit;
}
$_SESSION['generated_resume'] = $api_response['html'];
$_SESSION['resume_prompt'] = $prompt; // Storing for debug or retry purposes
echo json_encode(['success' => true, 'message' => 'Resume generated successfully.']);
} else {
header('HTTP/1.1 405 Method Not Allowed');
echo json_encode(['success' => false, 'message' => 'Invalid request method.']);
}

40
api/pexels.php Normal file
View File

@ -0,0 +1,40 @@
<?php
header('Content-Type: application/json');
require_once __DIR__.'/../includes/pexels.php';
// Fetch a variety of professional-looking document/resume templates
$query = 'resume template document';
$orientation = 'portrait';
$per_page = 12;
$url = 'https://api.pexels.com/v1/search?query=' . urlencode($query) . '&orientation=' . urlencode($orientation) . '&per_page=' . $per_page;
$data = pexels_get($url);
if (!$data || empty($data['photos'])) {
echo json_encode(['error' => 'Failed to fetch images from Pexels.']);
exit;
}
$output = [];
foreach ($data['photos'] as $photo) {
// Use a smaller, optimized image for the grid view
$src = $photo['src']['large'] ?? $photo['src']['original'];
$filename = $photo['id'] . '.jpg';
$local_path = __DIR__ . '/../assets/images/pexels/' . $filename;
// Download the image if it doesn't already exist to avoid repeated downloads
if (!file_exists($local_path)) {
download_to($src, $local_path);
}
$output[] = [
'id' => $photo['id'],
'local_url' => 'assets/images/pexels/' . $filename,
'photographer' => $photo['photographer'] ?? 'Unknown',
'photographer_url' => $photo['photographer_url'] ?? '#',
'alt' => $photo['alt'] ?? 'Resume template preview'
];
}
echo json_encode($output);
?>

423
assets/css/custom.css Normal file
View File

@ -0,0 +1,423 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap');
:root {
--background-color: #121212;
--surface-color: #1E1E1E;
--primary-gradient: linear-gradient(90deg, #6A11CB 0%, #2575FC 100%);
--text-primary: #FFFFFF;
--text-secondary: #B0B0B0;
--border-color: #333333;
--border-radius: 0.5rem;
}
body {
font-family: 'Inter', sans-serif;
background-color: var(--background-color);
color: var(--text-primary);
margin: 0;
}
/* Used on login page */
body.auth-page {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
}
.auth-container {
display: flex;
width: 100%;
max-width: 1200px;
min-height: 700px;
background-color: var(--surface-color);
border-radius: var(--border-radius);
overflow: hidden;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
}
.auth-visual {
flex: 1;
background: url('https://picsum.photos/seed/resume-art/800/1200') no-repeat center center;
background-size: cover;
}
.auth-content {
flex: 1;
padding: 4rem;
display: flex;
flex-direction: column;
justify-content: center;
}
.auth-header h1 {
font-weight: 700;
font-size: 2.5rem;
margin-bottom: 0.5rem;
}
.auth-header p {
color: var(--text-secondary);
margin-bottom: 2rem;
}
.nav-tabs {
border-bottom: 1px solid var(--border-color);
}
.nav-tabs .nav-link {
background: none;
border: none;
color: var(--text-secondary);
padding: 1rem 0;
margin-right: 1.5rem;
font-weight: 500;
cursor: pointer;
border-bottom: 2px solid transparent;
}
.nav-tabs .nav-link.active {
color: var(--text-primary);
border-bottom: 2px solid #6A11CB;
background: none;
}
.nav-tabs .nav-link:hover {
border-color: #333;
}
.nav-tabs .nav-link.active:hover {
border-bottom-color: #6A11CB;
}
.form-control {
background-color: #2a2a2a;
border: 1px solid var(--border-color);
color: var(--text-primary);
border-radius: var(--border-radius);
padding: 0.75rem 1rem;
width: 100%;
box-sizing: border-box;
}
.form-control:focus {
background-color: #2a2a2a;
color: var(--text-primary);
border-color: #6A11CB;
box-shadow: none;
outline: none;
}
.form-control::placeholder {
color: #6c757d;
}
.btn-primary {
background: var(--primary-gradient);
border: none;
border-radius: var(--border-radius);
padding: 0.75rem;
font-weight: 500;
color: var(--text-primary);
width: 100%;
cursor: pointer;
transition: opacity 0.3s ease;
}
.btn-primary:hover {
opacity: 0.9;
}
.btn-google {
background-color: #fff;
color: #000;
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
padding: 0.75rem;
font-weight: 500;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
cursor: pointer;
transition: background-color 0.3s ease;
}
.btn-google:hover {
background-color: #f0f0f0;
}
.divider {
display: flex;
align-items: center;
text-align: center;
color: var(--text-secondary);
margin: 1.5rem 0;
}
.divider::before,
.divider::after {
content: '';
flex: 1;
border-bottom: 1px solid var(--border-color);
}
.divider:not(:empty)::before {
margin-right: .25em;
}
.divider:not(:empty)::after {
margin-left: .25em;
}
/* General App Layout */
.page-wrapper {
display: flex;
height: 100vh;
width: 100%;
}
.sidebar {
width: 260px;
background-color: var(--surface-color);
display: flex;
flex-direction: column;
padding: 1.5rem;
border-right: 1px solid var(--border-color);
}
.sidebar-header h2 {
font-weight: 700;
font-size: 1.8rem;
background: var(--primary-gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 2rem;
}
.sidebar-nav a {
color: var(--text-secondary);
text-decoration: none;
display: block;
padding: 0.8rem 1rem;
border-radius: var(--border-radius);
margin-bottom: 0.5rem;
transition: background-color 0.2s ease, color 0.2s ease;
}
.sidebar-nav a:hover {
background-color: #2a2a2a;
color: var(--text-primary);
}
.sidebar-nav a.active {
background-color: rgba(106, 17, 203, 0.2);
color: var(--text-primary);
font-weight: 500;
}
#logout-btn-sidebar {
margin-top: auto;
cursor: pointer;
}
.main-content {
flex: 1;
overflow-y: auto;
padding: 2rem 3rem;
}
.page-header h1 {
font-size: 2.2rem;
font-weight: 700;
margin-bottom: 0.25rem;
}
.page-header p {
color: var(--text-secondary);
font-size: 1.1rem;
margin-bottom: 2.5rem;
}
/* Template Selection Grid */
.template-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 1.5rem;
}
.template-card {
position: relative;
border-radius: var(--border-radius);
overflow: hidden;
cursor: pointer;
border: 1px solid var(--border-color);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.template-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.4);
}
.template-card img {
display: block;
width: 100%;
aspect-ratio: 3 / 4;
object-fit: cover;
}
.template-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
opacity: 0;
transition: opacity 0.3s ease;
}
.template-card:hover .template-overlay {
opacity: 1;
}
.template-overlay .btn-select {
background: var(--primary-gradient);
border: none;
border-radius: var(--border-radius);
padding: 0.6rem 1.5rem;
font-weight: 500;
color: var(--text-primary);
cursor: pointer;
text-decoration: none;
}
/* Form Styles */
.form-container {
max-width: 900px;
}
.form-section {
background-color: var(--surface-color);
padding: 2rem;
border-radius: var(--border-radius);
margin-bottom: 2rem;
border: 1px solid var(--border-color);
}
.form-section h2 {
font-size: 1.5rem;
margin-top: 0;
margin-bottom: 1.5rem;
border-bottom: 1px solid var(--border-color);
padding-bottom: 1rem;
}
.form-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1.5rem;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: var(--text-secondary);
}
textarea.form-control {
resize: vertical;
min-height: 80px;
}
.btn-secondary {
background: #3a3a3a;
border: 1px solid var(--border-color);
color: var(--text-primary);
padding: 0.6rem 1.2rem;
border-radius: var(--border-radius);
cursor: pointer;
transition: background-color 0.2s ease;
}
.btn-secondary:hover {
background: #4a4a4a;
}
.form-actions {
margin-top: 1rem;
}
.dynamic-section {
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
padding: 1.5rem;
margin-bottom: 1.5rem;
position: relative;
background-color: #1a1a1a;
}
.btn-remove {
position: absolute;
top: 1rem;
right: 1rem;
background: none;
border: none;
color: var(--text-secondary);
cursor: pointer;
font-size: 1rem;
padding: 0.5rem;
}
.btn-remove:hover {
color: #ff4d4d;
}
/* Loading Overlay */
.loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
display: none; /* Hidden by default */
justify-content: center;
align-items: center;
z-index: 1000;
flex-direction: column;
}
.loading-overlay.show {
display: flex;
}
.spinner {
border: 8px solid var(--border-color);
border-top: 8px solid #6A11CB;
border-radius: 50%;
width: 60px;
height: 60px;
animation: spin 1s linear infinite;
}
.loading-overlay p {
color: var(--text-primary);
margin-top: 1rem;
font-size: 1.1rem;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}

105
assets/js/auth.js Normal file
View File

@ -0,0 +1,105 @@
import { initializeApp } from "https://www.gstatic.com/firebasejs/9.6.1/firebase-app.js";
import {
getAuth,
createUserWithEmailAndPassword,
signInWithEmailAndPassword,
GoogleAuthProvider,
signInWithPopup,
onAuthStateChanged
} from "https://www.gstatic.com/firebasejs/9.6.1/firebase-auth.js";
// Your web app's Firebase configuration
const firebaseConfig = {
apiKey: "AIzaSyAuabZynwAn8r91_toMzdN2_vCVOwLsAf8",
authDomain: "ai-resume-builder-90135.firebaseapp.com",
projectId: "ai-resume-builder-90135",
storageBucket: "ai-resume-builder-90135.firebasestorage.app",
messagingSenderId: "465451014283",
appId: "1:465451014283:web:355ddebe12bc8a2dfedc8e",
measurementId: "G-4B91V0ZY58"
};
// Initialize Firebase
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
const provider = new GoogleAuthProvider();
// DOM Elements
const loginForm = document.getElementById('loginForm');
const signupForm = document.getElementById('signupForm');
const googleLoginBtn = document.getElementById('googleLoginBtn');
const googleSignupBtn = document.getElementById('googleSignupBtn');
const errorContainer = document.getElementById('errorContainer');
// Function to display errors
const showError = (message) => {
errorContainer.textContent = message;
errorContainer.style.display = 'block';
};
// Redirect if user is already logged in
onAuthStateChanged(auth, (user) => {
if (user) {
window.location.href = 'home.php';
}
});
// Email/Password Sign Up
if (signupForm) {
signupForm.addEventListener('submit', (e) => {
e.preventDefault();
const email = signupForm.querySelector('#signupEmail').value;
const password = signupForm.querySelector('#signupPassword').value;
createUserWithEmailAndPassword(auth, email, password)
.then((userCredential) => {
// Signed in
window.location.href = 'home.php';
})
.catch((error) => {
showError(error.message);
});
});
}
// Email/Password Login
if (loginForm) {
loginForm.addEventListener('submit', (e) => {
e.preventDefault();
const email = loginForm.querySelector('#loginEmail').value;
const password = loginForm.querySelector('#loginPassword').value;
signInWithEmailAndPassword(auth, email, password)
.then((userCredential) => {
// Signed in
window.location.href = 'home.php';
})
.catch((error) => {
showError(error.message);
});
});
}
// Google Sign-In
const handleGoogleSignIn = (e) => {
e.preventDefault();
signInWithPopup(auth, provider)
.then((result) => {
// This gives you a Google Access Token. You can use it to access the Google API.
const credential = GoogleAuthProvider.credentialFromResult(result);
const token = credential.accessToken;
// The signed-in user info.
const user = result.user;
window.location.href = 'home.php';
}).catch((error) => {
// Handle Errors here.
showError(error.message);
});
};
if (googleLoginBtn) {
googleLoginBtn.addEventListener('click', handleGoogleSignIn);
}
if (googleSignupBtn) {
googleSignupBtn.addEventListener('click', handleGoogleSignIn);
}

157
assets/js/form.js Normal file
View File

@ -0,0 +1,157 @@
document.addEventListener('DOMContentLoaded', function () {
const experienceWrapper = document.getElementById('experience-wrapper');
const addExperienceBtn = document.getElementById('add-experience-btn');
let experienceCount = 0;
const educationWrapper = document.getElementById('education-wrapper');
const addEducationBtn = document.getElementById('add-education-btn');
let educationCount = 0;
// Function to add a new experience block
const addExperience = () => {
experienceCount++;
const div = document.createElement('div');
div.classList.add('dynamic-section');
div.innerHTML = `
<div class="form-grid">
<div class="form-group">
<label for="job-title-${experienceCount}">Job Title</label>
<input type="text" id="job-title-${experienceCount}" name="experience[${experienceCount}][title]">
</div>
<div class="form-group">
<label for="company-${experienceCount}">Company</label>
<input type="text" id="company-${experienceCount}" name="experience[${experienceCount}][company]">
</div>
<div class="form-group">
<label for="exp-start-date-${experienceCount}">Start Date</label>
<input type="month" id="exp-start-date-${experienceCount}" name="experience[${experienceCount}][startDate]">
</div>
<div class="form-group">
<label for="exp-end-date-${experienceCount}">End Date</label>
<input type="month" id="exp-end-date-${experienceCount}" name="experience[${experienceCount}][endDate]">
</div>
</div>
<div class="form-group">
<label for="responsibilities-${experienceCount}">Responsibilities</label>
<textarea id="responsibilities-${experienceCount}" name="experience[${experienceCount}][responsibilities]" rows="3"></textarea>
</div>
<button type="button" class="btn-remove">Remove</button>
`;
experienceWrapper.appendChild(div);
};
// Function to add a new education block
const addEducation = () => {
educationCount++;
const div = document.createElement('div');
div.classList.add('dynamic-section');
div.innerHTML = `
<div class="form-grid">
<div class="form-group">
<label for="degree-${educationCount}">Degree / Certificate</label>
<input type="text" id="degree-${educationCount}" name="education[${educationCount}][degree]">
</div>
<div class="form-group">
<label for="school-${educationCount}">School / Institution</label>
<input type="text" id="school-${educationCount}" name="education[${educationCount}][school]">
</div>
<div class="form-group">
<label for="edu-start-date-${educationCount}">Start Date</label>
<input type="month" id="edu-start-date-${educationCount}" name="education[${educationCount}][startDate]">
</div>
<div class="form-group">
<label for="edu-end-date-${educationCount}">End Date</label>
<input type="month" id="edu-end-date-${educationCount}" name="education[${educationCount}][endDate]">
</div>
</div>
<button type="button" class="btn-remove">Remove</button>
`;
educationWrapper.appendChild(div);
};
// Event listeners
addExperienceBtn.addEventListener('click', addExperience);
addEducationBtn.addEventListener('click', addEducation);
// Remove button delegation
experienceWrapper.addEventListener('click', (e) => {
if (e.target.classList.contains('btn-remove')) {
e.target.closest('.dynamic-section').remove();
}
});
educationWrapper.addEventListener('click', (e) => {
if (e.target.classList.contains('btn-remove')) {
e.target.closest('.dynamic-section').remove();
}
});
// Add one of each by default
addExperience();
addEducation();
// Form Submission
const resumeForm = document.getElementById('resume-form');
const loadingOverlay = document.getElementById('loading-overlay');
resumeForm.addEventListener('submit', function (e) {
e.preventDefault();
loadingOverlay.classList.add('show');
const formData = new FormData(resumeForm);
const data = {
fullName: formData.get('fullName'),
email: formData.get('email'),
phone: formData.get('phone'),
linkedin: formData.get('linkedin'),
summary: formData.get('summary'),
skills: formData.get('skills'),
experience: [],
education: []
};
// Process dynamic fields
const expSections = experienceWrapper.querySelectorAll('.dynamic-section');
expSections.forEach((sec, index) => {
data.experience.push({
title: sec.querySelector(`input[name='experience[${index + 1}][title]']`).value,
company: sec.querySelector(`input[name='experience[${index + 1}][company]']`).value,
startDate: sec.querySelector(`input[name='experience[${index + 1}][startDate]']`).value,
endDate: sec.querySelector(`input[name='experience[${index + 1}][endDate]']`).value,
responsibilities: sec.querySelector(`textarea[name='experience[${index + 1}][responsibilities]']`).value
});
});
const eduSections = educationWrapper.querySelectorAll('.dynamic-section');
eduSections.forEach((sec, index) => {
data.education.push({
degree: sec.querySelector(`input[name='education[${index + 1}][degree]']`).value,
school: sec.querySelector(`input[name='education[${index + 1}][school]']`).value,
startDate: sec.querySelector(`input[name='education[${index + 1}][startDate]']`).value,
endDate: sec.querySelector(`input[name='education[${index + 1}][endDate]']`).value
});
});
fetch('api/generate.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
})
.then(response => response.json())
.then(result => {
if (result.success) {
window.location.href = 'result.php';
} else {
alert('An error occurred: ' + result.message);
loadingOverlay.classList.remove('show');
}
})
.catch(error => {
console.error('Error:', error);
alert('A network error occurred. Please try again.');
loadingOverlay.classList.remove('show');
});
});
});

123
create.php Normal file
View File

@ -0,0 +1,123 @@
<?php
// Initialize the session
session_start();
// Check if the user is logged in, if not then redirect him to login page
if (!isset($_SESSION["loggedin"]) || $_SESSION["loggedin"] !== true) {
header("location: index.php");
exit;
}
$template_img = isset($_GET['template']) ? htmlspecialchars($_GET['template']) : 'assets/images/pexels/placeholder.jpg';
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Create Your Resume</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
</head>
<body class="dark-theme">
<div class="page-wrapper">
<!-- Sidebar -->
<aside class="sidebar">
<div class="sidebar-header">
<h2>AI Resume Builder</h2>
</div>
<nav class="sidebar-nav">
<a href="home.php">Home</a>
<a href="create.php" class="active">Create</a>
<a href="#" id="logout-btn-sidebar">Sign Out</a>
</nav>
</aside>
<!-- Main Content -->
<main class="main-content">
<div class="page-header">
<h1>Create Your Resume</h1>
<p>Fill out the details below to generate your professional resume.</p>
</div>
<div class="form-container">
<form id="resume-form">
<!-- Personal Details -->
<section class="form-section">
<h2>Personal Details</h2>
<div class="form-grid">
<div class="form-group">
<label for="full-name">Full Name</label>
<input type="text" id="full-name" name="fullName" required>
</div>
<div class="form-group">
<label for="email">Email</label>
<input type="email" id="email" name="email" required>
</div>
<div class="form-group">
<label for="phone">Phone Number</label>
<input type="tel" id="phone" name="phone">
</div>
<div class="form-group">
<label for="linkedin">LinkedIn Profile</label>
<input type="url" id="linkedin" name="linkedin">
</div>
</div>
<div class="form-group">
<label for="summary">Professional Summary</label>
<textarea id="summary" name="summary" rows="4"></textarea>
</div>
</section>
<!-- Work Experience -->
<section class="form-section">
<h2>Work Experience</h2>
<div id="experience-wrapper">
<!-- JS will inject fields here -->
</div>
<button type="button" id="add-experience-btn" class="btn-secondary">Add Experience</button>
</section>
<!-- Education -->
<section class="form-section">
<h2>Education</h2>
<div id="education-wrapper">
<!-- JS will inject fields here -->
</div>
<button type="button" id="add-education-btn" class="btn-secondary">Add Education</button>
</section>
<!-- Skills -->
<section class="form-section">
<h2>Skills</h2>
<div class="form-group">
<label for="skills">Skills (comma-separated)</label>
<input type="text" id="skills" name="skills" placeholder="e.g., Python, Project Management, Graphic Design">
</div>
</section>
<div class="form-actions">
<button type="submit" class="btn-primary">Generate Resume</button>
</div>
</form>
</div>
</main>
</div>
<script src="https://www.gstatic.com/firebasejs/9.6.1/firebase-app-compat.js"></script>
<script src="https://www.gstatic.com/firebasejs/9.6.1/firebase-auth-compat.js"></script>
<div id="loading-overlay" class="loading-overlay">
<div class="spinner"></div>
<p>Generating your resume...</p>
</div>
<script src="https://www.gstatic.com/firebasejs/9.6.1/firebase-app-compat.js"></script>
<script src="https://www.gstatic.com/firebasejs/9.6.1/firebase-auth-compat.js"></script>
<script src="assets/js/auth.js?v=<?php echo time(); ?>"></script>
<script src="assets/js/form.js?v=<?php echo time(); ?>"></script>
</body>
</html>

85
home.php Normal file
View File

@ -0,0 +1,85 @@
<?php
// Initialize the session
session_start();
// Check if the user is logged in, if not then redirect him to login page
if (!isset($_SESSION["loggedin"]) || $_SESSION["loggedin"] !== true) {
header("location: index.php");
exit;
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Home - AI Resume Builder</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
</head>
<body class="dark-theme">
<div class="page-wrapper">
<!-- Sidebar -->
<aside class="sidebar">
<div class="sidebar-header">
<h2>AI Resume Builder</h2>
</div>
<nav class="sidebar-nav">
<a href="home.php" class="active">Home</a>
<a href="create.php">Create</a>
<a href="#" id="logout-btn-sidebar">Sign Out</a>
</nav>
</aside>
<!-- Main Content -->
<main class="main-content">
<div class="page-header">
<h1>Choose Your Template</h1>
<p>Select a template to start building your professional resume.</p>
</div>
<div id="template-grid" class="template-grid">
<!-- Templates will be loaded here by JavaScript -->
</div>
</main>
</div>
<script src="https://www.gstatic.com/firebasejs/9.6.1/firebase-app-compat.js"></script>
<script src="https://www.gstatic.com/firebasejs/9.6.1/firebase-auth-compat.js"></script>
<script src="assets/js/auth.js?v=<?php echo time(); ?>"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
const grid = document.getElementById('template-grid');
fetch('api/pexels.php')
.then(response => response.json())
.then(images => {
if (!images || images.error) {
grid.innerHTML = `<p style="color: #ff4d4d;">Error loading templates: ${images ? images.error : 'Unknown error'}</p>`;
return;
}
let html = '';
images.forEach(image => {
const templateURL = `create.php?template=${encodeURIComponent(image.local_url)}`;
html += `
<a href="${templateURL}" class="template-card">
<img src="${image.local_url}" alt="${image.alt || 'Resume Template'}">
<div class="template-overlay">
<span class="btn-select">Select</span>
</div>
</a>
`;
});
grid.innerHTML = html;
})
.catch(error => {
console.error('Failed to fetch templates:', error);
grid.innerHTML = '<p style="color: #ffcc00;">Could not load templates. Please try again later.</p>';
});
});
</script>
</body>
</html>

50
includes/gemini.php Normal file
View File

@ -0,0 +1,50 @@
<?php
function callGeminiApi($prompt, $apiKey) {
$url = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key=' . $apiKey;
$data = [
'contents' => [
[
'parts' => [
[
'text' => $prompt
]
]
]
]
];
$json_data = json_encode($data);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $json_data);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
]);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curl_error = curl_error($ch);
curl_close($ch);
if ($curl_error) {
return ['error' => 'cURL Error: ' . $curl_error];
}
if ($http_code !== 200) {
return ['error' => 'API responded with HTTP code ' . $http_code, 'response' => json_decode($response, true)];
}
$result = json_decode($response, true);
if (isset($result['candidates'][0]['content']['parts'][0]['text'])) {
return ['success' => true, 'html' => $result['candidates'][0]['content']['parts'][0]['text']];
} else {
return ['error' => 'Could not extract content from API response.', 'response' => $result];
}
}

33
includes/pexels.php Normal file
View File

@ -0,0 +1,33 @@
<?php
function pexels_key() {
$k = getenv('PEXELS_KEY');
return $k && strlen($k) > 0 ? $k : 'Vc99rnmOhHhJAbgGQoKLZtsaIVfkeownoQNbTj78VemUjKh08ZYRbf18';
}
function pexels_get($url) {
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [ 'Authorization: '. pexels_key() ],
CURLOPT_TIMEOUT => 15,
]);
$resp = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($code >= 200 && $code < 300 && $resp) return json_decode($resp, true);
return null;
}
function download_to($srcUrl, $destPath) {
// Ensure the destination directory exists
$dir = dirname($destPath);
if (!is_dir($dir)) {
mkdir($dir, 0775, true);
}
$data = file_get_contents($srcUrl);
if ($data === false) return false;
return file_put_contents($destPath, $data) !== false;
}
?>

220
index.php
View File

@ -1,131 +1,101 @@
<?php <!DOCTYPE html>
declare(strict_types=1);
@ini_set('display_errors', '1');
@error_reporting(E_ALL);
@date_default_timezone_set('UTC');
$phpVersion = PHP_VERSION;
$now = date('Y-m-d H:i:s');
?>
<!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>New Style</title> <title>AI Resume Builder - Login</title>
<link rel="preconnect" href="https://fonts.googleapis.com"> <meta name="description" content="Log in or sign up to create your professional resume with the help of AI.">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet"> <!-- Open Graph -->
<style> <meta property="og:title" content="AI Resume Builder">
:root { <meta property="og:description" content="Build your perfect resume with AI-powered assistance.">
--bg-color-start: #6a11cb; <meta property="og:type" content="website">
--bg-color-end: #2575fc; <meta property="og:url" content="http://<?php echo $_SERVER['HTTP_HOST']; ?>">
--text-color: #ffffff; <meta property="og:image" content="https://picsum.photos/seed/resume-og/1200/630">
--card-bg-color: rgba(255, 255, 255, 0.01);
--card-border-color: rgba(255, 255, 255, 0.1); <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
} <link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
body {
margin: 0;
font-family: 'Inter', sans-serif;
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
color: var(--text-color);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
text-align: center;
overflow: hidden;
position: relative;
}
body::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path d="M-10 10L110 10M10 -10L10 110" stroke-width="1" stroke="rgba(255,255,255,0.05)"/></svg>');
animation: bg-pan 20s linear infinite;
z-index: -1;
}
@keyframes bg-pan {
0% { background-position: 0% 0%; }
100% { background-position: 100% 100%; }
}
main {
padding: 2rem;
}
.card {
background: var(--card-bg-color);
border: 1px solid var(--card-border-color);
border-radius: 16px;
padding: 2rem;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
}
.loader {
margin: 1.25rem auto 1.25rem;
width: 48px;
height: 48px;
border: 3px solid rgba(255, 255, 255, 0.25);
border-top-color: #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.hint {
opacity: 0.9;
}
.sr-only {
position: absolute;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap; border: 0;
}
h1 {
font-size: 3rem;
font-weight: 700;
margin: 0 0 1rem;
letter-spacing: -1px;
}
p {
margin: 0.5rem 0;
font-size: 1.1rem;
}
code {
background: rgba(0,0,0,0.2);
padding: 2px 6px;
border-radius: 4px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
footer {
position: absolute;
bottom: 1rem;
font-size: 0.8rem;
opacity: 0.7;
}
</style>
</head> </head>
<body> <body>
<main>
<div class="card"> <div class="auth-container">
<h1>Analyzing your requirements and generating your website…</h1> <div class="auth-visual">
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes"> <!-- Alt text is handled by CSS background, but if it were an <img>:
<span class="sr-only">Loading…</span> alt="Abstract visual representing creativity and document creation." -->
</div> </div>
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWiZZy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p> <div class="auth-content">
<p class="hint">This page will update automatically as the plan is implemented.</p> <div class="auth-header">
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p> <h1>AI Resume Builder</h1>
<p>Welcome back! Please login to your account.</p>
</div>
<!-- Tabs -->
<ul class="nav nav-tabs" id="authTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="login-tab" data-bs-toggle="tab" data-bs-target="#login" type="button" role="tab" aria-controls="login" aria-selected="true">Login</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="signup-tab" data-bs-toggle="tab" data-bs-target="#signup" type="button" role="tab" aria-controls="signup" aria-selected="false">Sign Up</button>
</li>
</ul>
<!-- Error Messages -->
<div id="errorContainer" class="alert alert-danger mt-3" style="display: none;"></div>
<!-- Tab Content -->
<div class="tab-content pt-4" id="authTabsContent">
<!-- Login Pane -->
<div class="tab-pane fade show active" id="login" role="tabpanel" aria-labelledby="login-tab">
<form id="loginForm">
<div class="mb-3">
<label for="loginEmail" class="form-label visually-hidden">Email address</label>
<input type="email" class="form-control" id="loginEmail" placeholder="Email address" required>
</div>
<div class="mb-3">
<label for="loginPassword" class="form-label visually-hidden">Password</label>
<input type="password" class="form-control" id="loginPassword" placeholder="Password" required>
</div>
<button type="submit" class="btn btn-primary mt-3">Login</button>
</form>
<div class="divider">or</div>
<button class="btn btn-google" id="googleLoginBtn">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" class="bi bi-google" viewBox="0 0 16 16">
<path d="M15.545 6.558a9.42 9.42 0 0 1 .139 1.626c0 2.434-.87 4.492-2.384 5.885h.002C11.978 15.292 10.158 16 8 16A8 8 0 1 1 8 0a7.689 7.689 0 0 1 5.352 2.082l-2.284 2.284A4.347 4.347 0 0 0 8 3.166c-2.087 0-3.86 1.408-4.492 3.304a4.792 4.792 0 0 0 0 3.063h.003c.635 1.893 2.405 3.301 4.492 3.301 1.078 0 2.004-.276 2.722-.764h-.003a3.702 3.702 0 0 0 1.599-2.431H8v-3.08h7.545z"/>
</svg>
Sign in with Google
</button>
</div>
<!-- Sign Up Pane -->
<div class="tab-pane fade" id="signup" role="tabpanel" aria-labelledby="signup-tab">
<form id="signupForm">
<div class="mb-3">
<label for="signupName" class="form-label visually-hidden">Full Name</label>
<input type="text" class="form-control" id="signupName" placeholder="Full Name" required>
</div>
<div class="mb-3">
<label for="signupEmail" class="form-label visually-hidden">Email address</label>
<input type="email" class="form-control" id="signupEmail" placeholder="Email address" required>
</div>
<div class="mb-3">
<label for="signupPassword" class="form-label visually-hidden">Password</label>
<input type="password" class="form-control" id="signupPassword" placeholder="Password" required>
</div>
<button type="submit" class="btn btn-primary mt-3">Create Account</button>
</form>
<div class="divider">or</div>
<button class="btn btn-google" id="googleSignupBtn">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" class="bi bi-google" viewBox="0 0 16 16">
<path d="M15.545 6.558a9.42 9.42 0 0 1 .139 1.626c0 2.434-.87 4.492-2.384 5.885h.002C11.978 15.292 10.158 16 8 16A8 8 0 1 1 8 0a7.689 7.689 0 0 1 5.352 2.082l-2.284 2.284A4.347 4.347 0 0 0 8 3.166c-2.087 0-3.86 1.408-4.492 3.304a4.792 4.792 0 0 0 0 3.063h.003c.635 1.893 2.405 3.301 4.492 3.301 1.078 0 2.004-.276 2.722-.764h-.003a3.702 3.702 0 0 0 1.599-2.431H8v-3.08h7.545z"/>
</svg>
Sign up with Google
</button>
</div>
</div>
</div>
</div> </div>
</main>
<footer> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
Page updated: <?= htmlspecialchars($now) ?> (UTC) <script type="module" src="assets/js/auth.js?v=<?php echo time(); ?>"></script>
</footer>
</body> </body>
</html> </html>

81
result.php Normal file
View File

@ -0,0 +1,81 @@
<?php
session_start();
// Check if the user is logged in
if (!isset($_SESSION["loggedin"]) || $_SESSION["loggedin"] !== true) {
header("location: index.php");
exit;
}
// Get the generated resume from the session
$resume_html = $_SESSION['generated_resume'] ?? '<div class="error-message">Could not find the generated resume. Please try again.</div>';
// Unset the session variable so it doesn't persist
// unset($_SESSION['generated_resume']);
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Your Generated Resume</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
<style>
.resume-preview-container {
background-color: #fff;
color: #333;
max-width: 8.5in;
min-height: 11in;
margin: 2rem auto;
padding: 1in;
box-shadow: 0 0 15px rgba(0,0,0,0.5);
border-radius: 5px;
}
.action-buttons {
text-align: center;
margin-bottom: 2rem;
}
</style>
</head>
<body class="dark-theme">
<div class="page-wrapper">
<!-- Sidebar -->
<aside class="sidebar">
<div class="sidebar-header">
<h2>AI Resume Builder</h2>
</div>
<nav class="sidebar-nav">
<a href="home.php">Home</a>
<a href="create.php">Create</a>
<a href="#" id="logout-btn-sidebar">Sign Out</a>
</nav>
</aside>
<!-- Main Content -->
<main class="main-content">
<div class="page-header">
<h1>Your Generated Resume</h1>
<p>Review your resume below. You can copy the content or download it in various formats.</p>
</div>
<div class="action-buttons">
<button class="btn-secondary">Download as PDF</button>
<button class="btn-secondary">Download as DOCX</button>
</div>
<div class="resume-preview-container">
<?php echo $resume_html; ?>
</div>
</main>
</div>
<script src="https://www.gstatic.com/firebasejs/9.6.1/firebase-app-compat.js"></script>
<script src="https://www.gstatic.com/firebasejs/9.6.1/firebase-auth-compat.js"></script>
<script src="assets/js/auth.js?v=<?php echo time(); ?>"></script>
</body>
</html>