sakthi07 commited on
Commit
ab028ac
·
1 Parent(s): 222e44f

pushing to hugging face

Browse files
.gitignore ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python virtual environments
2
+ .venv/
3
+ venv/
4
+ ENV/
5
+ env/
6
+
7
+ # Python cache / bytecode
8
+ __pycache__/
9
+ *.py[cod]
10
+ *$py.class
11
+
12
+ # Distribution / packaging
13
+ build/
14
+ dist/
15
+ *.egg-info/
16
+ .eggs/
17
+
18
+ # Logs and runtime files
19
+ *.log
20
+ *.out
21
+ *.pid
22
+
23
+ # Unit test / coverage reports
24
+ .coverage
25
+ coverage.xml
26
+ htmlcov/
27
+ .pytest_cache/
28
+ .tox/
29
+ .nox/
30
+ .hypothesis/
31
+
32
+ # Environment variables / secrets
33
+ .env
34
+ .env.*
35
+ *.secret
36
+
37
+ # IDE / editor
38
+ .vscode/
39
+ .idea/
40
+
41
+ # OS files
42
+ .DS_Store
43
+ Thumbs.db
44
+
45
+ claude.md
46
+ CLAUDE.md
Dockerfile ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Read the doc: https://huggingface.co/docs/hub/spaces-sdks-docker
2
+ # you will also find guides on how best to write your Dockerfile
3
+
4
+ FROM python:3.12.3
5
+
6
+ RUN useradd -m -u 1000 user
7
+ USER user
8
+ ENV PATH="/home/user/.local/bin:$PATH"
9
+
10
+ WORKDIR /app
11
+
12
+ COPY --chown=user ./requirements.txt requirements.txt
13
+ RUN pip install --no-cache-dir --upgrade -r requirements.txt
14
+
15
+ COPY --chown=user . /app
16
+ CMD ["gunicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
Procfile ADDED
@@ -0,0 +1 @@
 
 
1
+ web: python app.py
app.py ADDED
@@ -0,0 +1,1489 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from dotenv import load_dotenv
3
+ import json
4
+
5
+ # Load environment variables FIRST
6
+ load_dotenv()
7
+
8
+ from flask import Flask, render_template, redirect, url_for, flash, session, request, jsonify, send_file
9
+ from flask_login import LoginManager, login_user, logout_user, login_required, current_user
10
+ from werkzeug.security import generate_password_hash, check_password_hash
11
+ import requests
12
+ import uuid
13
+ from urllib.parse import urlencode
14
+ import re
15
+ from datetime import datetime
16
+ from sqlalchemy.orm import joinedload
17
+
18
+ # Import models and database
19
+ from models import db, User, Introduction, ProfileSummary, WorkExperience, Project, Education, Skill, Achievement, ProfileSectionOrder
20
+
21
+ app = Flask(__name__)
22
+
23
+ # Configure Flask
24
+ app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'dev-secret-key-change-in-production')
25
+ app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DATABASE_URL') or os.environ.get('SQLALCHEMY_DATABASE_URI')
26
+ app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
27
+
28
+ # Initialize extensions
29
+ db.init_app(app)
30
+ # Note: CSRFProtect disabled for now, using simple token validation
31
+
32
+ # Configure Flask-Login
33
+ login_manager = LoginManager()
34
+ login_manager.init_app(app)
35
+ login_manager.login_view = 'signin'
36
+ login_manager.login_message = 'Please sign in to access this page.'
37
+ login_manager.login_message_category = 'info'
38
+
39
+ @login_manager.user_loader
40
+ def load_user(user_id):
41
+ return db.session.get(User, str(user_id))
42
+
43
+ # Admin required decorator
44
+ def admin_required(f):
45
+ """Decorator to require admin access"""
46
+ @login_required
47
+ def decorated_function(*args, **kwargs):
48
+ if not current_user.is_admin:
49
+ flash('You do not have permission to access this page.', 'error')
50
+ return redirect(url_for('profile'))
51
+ return f(*args, **kwargs)
52
+ decorated_function.__name__ = f.__name__
53
+ return decorated_function
54
+
55
+ # Note: CSRF protection disabled for simplicity in this development version
56
+
57
+ # GitHub OAuth Configuration
58
+ GITHUB_CLIENT_ID = os.environ.get('GITHUB_CLIENT_ID')
59
+ GITHUB_CLIENT_SECRET = os.environ.get('GITHUB_CLIENT_SECRET')
60
+ GITHUB_REDIRECT_URI = os.environ.get('GITHUB_OAUTH_BACKEND_REDIRECT', 'http://127.0.0.1:5000/auth/github/callback')
61
+
62
+ # GitHub OAuth Functions
63
+ def generate_github_auth_url():
64
+ """Generate GitHub OAuth authorization URL"""
65
+ params = {
66
+ 'client_id': GITHUB_CLIENT_ID,
67
+ 'redirect_uri': GITHUB_REDIRECT_URI,
68
+ 'scope': 'user:email',
69
+ 'state': str(uuid.uuid4()) # Simple CSRF protection
70
+ }
71
+ return f"https://github.com/login/oauth/authorize?{urlencode(params)}"
72
+
73
+ def exchange_code_for_token(code):
74
+ """Exchange authorization code for access token"""
75
+ data = {
76
+ 'client_id': GITHUB_CLIENT_ID,
77
+ 'client_secret': GITHUB_CLIENT_SECRET,
78
+ 'code': code,
79
+ 'redirect_uri': GITHUB_REDIRECT_URI,
80
+ }
81
+
82
+ headers = {
83
+ 'Accept': 'application/json'
84
+ }
85
+
86
+ response = requests.post('https://github.com/login/oauth/access_token',
87
+ data=data, headers=headers)
88
+
89
+ if response.status_code == 200:
90
+ result = response.json()
91
+ return result.get('access_token')
92
+ return None
93
+
94
+ def get_github_user_info(access_token):
95
+ """Get user information from GitHub API"""
96
+ headers = {
97
+ 'Authorization': f'token {access_token}',
98
+ 'Accept': 'application/json'
99
+ }
100
+
101
+ response = requests.get('https://api.github.com/user', headers=headers)
102
+
103
+ if response.status_code == 200:
104
+ return response.json()
105
+ return None
106
+
107
+ def get_github_user_email(access_token):
108
+ """Get user's primary email from GitHub API"""
109
+ headers = {
110
+ 'Authorization': f'token {access_token}',
111
+ 'Accept': 'application/json'
112
+ }
113
+
114
+ response = requests.get('https://api.github.com/user/emails', headers=headers)
115
+
116
+ if response.status_code == 200:
117
+ emails = response.json()
118
+ # Find primary email
119
+ for email in emails:
120
+ if email.get('primary') and email.get('verified'):
121
+ return email.get('email')
122
+ # If no primary verified email, use the first one
123
+ if emails:
124
+ return emails[0].get('email')
125
+ return None
126
+
127
+ # Email validation function
128
+ def validate_email_format(email):
129
+ """Validate email format with flexible rules"""
130
+ import re
131
+ basic_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
132
+ if re.match(basic_pattern, email):
133
+ return None
134
+ return "Invalid email format"
135
+
136
+ # Routes
137
+ @app.route('/')
138
+ def index():
139
+ if current_user.is_authenticated:
140
+ return redirect(url_for('profile'))
141
+ return redirect(url_for('signin'))
142
+
143
+ @app.route('/signin', methods=['GET', 'POST'])
144
+ def signin():
145
+ if current_user.is_authenticated:
146
+ return redirect(url_for('profile'))
147
+
148
+ if request.method == 'POST':
149
+ email = request.form.get('email', '').strip()
150
+ password = request.form.get('password', '')
151
+
152
+ if not email or not password:
153
+ flash('Please enter both email and password.', 'error')
154
+ return render_template('signin.html')
155
+
156
+ user = User.query.filter_by(email=email).first()
157
+
158
+ if user and user.check_password(password):
159
+ # Check if user should be admin
160
+ admin_email = os.environ.get('ADMIN_EMAIL')
161
+ if admin_email and email.lower() == admin_email.lower():
162
+ if not user.is_admin:
163
+ user.is_admin = True
164
+ user.role = 'Admin'
165
+ db.session.commit()
166
+ flash('Admin access granted!', 'success')
167
+
168
+ login_user(user)
169
+ next_page = request.args.get('next')
170
+ return redirect(next_page or url_for('profile'))
171
+ else:
172
+ flash('Invalid email or password.', 'error')
173
+
174
+ return render_template('signin.html')
175
+
176
+ @app.route('/signup', methods=['GET', 'POST'])
177
+ def signup():
178
+ if current_user.is_authenticated:
179
+ return redirect(url_for('profile'))
180
+
181
+ if request.method == 'POST':
182
+ name = request.form.get('name', '').strip()
183
+ email = request.form.get('email', '').strip()
184
+ password = request.form.get('password', '')
185
+ password_confirm = request.form.get('password_confirm', '')
186
+
187
+ # Validation
188
+ errors = []
189
+
190
+ if not name:
191
+ errors.append('Name is required')
192
+
193
+ if not email:
194
+ errors.append('Email is required')
195
+ else:
196
+ email_error = validate_email_format(email)
197
+ if email_error:
198
+ errors.append(email_error)
199
+
200
+ if not password:
201
+ errors.append('Password is required')
202
+ elif len(password) < 8:
203
+ errors.append('Password must be at least 8 characters long')
204
+
205
+ if not password_confirm:
206
+ errors.append('Please confirm your password')
207
+ elif password != password_confirm:
208
+ errors.append('Passwords do not match')
209
+
210
+ # Check if email already exists
211
+ if email and User.query.filter_by(email=email).first():
212
+ errors.append('Email already registered')
213
+
214
+ if errors:
215
+ for error in errors:
216
+ flash(error, 'error')
217
+ return render_template('signup.html')
218
+
219
+ # Create new user
220
+ new_user = User(
221
+ id=uuid.uuid4(),
222
+ name=name,
223
+ email=email
224
+ )
225
+ new_user.set_password(password)
226
+
227
+ # Check if user should be admin
228
+ admin_email = os.environ.get('ADMIN_EMAIL')
229
+ if admin_email and email.lower() == admin_email.lower():
230
+ new_user.is_admin = True
231
+ new_user.role = 'Admin'
232
+
233
+ try:
234
+ db.session.add(new_user)
235
+ db.session.commit()
236
+
237
+ if new_user.is_admin:
238
+ flash('Admin account created successfully! Please sign in.', 'success')
239
+ else:
240
+ flash('Account created successfully! Please sign in.', 'success')
241
+ return redirect(url_for('signin'))
242
+ except Exception as e:
243
+ db.session.rollback()
244
+ flash('An error occurred while creating your account. Please try again.', 'error')
245
+
246
+ return render_template('signup.html')
247
+
248
+ @app.route('/logout')
249
+ @login_required
250
+ def logout():
251
+ logout_user()
252
+ flash('You have been logged out successfully.', 'success')
253
+ return redirect(url_for('signin'))
254
+
255
+ @app.route('/profile')
256
+ @login_required
257
+ def profile():
258
+ # Check if user has a profile
259
+ intro = Introduction.query.filter_by(user_id=current_user.id).first()
260
+ has_profile = intro is not None
261
+
262
+ if has_profile:
263
+ # Get all profile data
264
+ summary = ProfileSummary.query.filter_by(user_id=current_user.id).first()
265
+ work_experiences = WorkExperience.query.filter_by(user_id=current_user.id).order_by(WorkExperience.order).all()
266
+ projects = Project.query.filter_by(user_id=current_user.id).order_by(Project.order).all()
267
+ educations = Education.query.filter_by(user_id=current_user.id).order_by(Education.order).all()
268
+ skills = Skill.query.filter_by(user_id=current_user.id).order_by(Skill.order).all()
269
+ achievements = Achievement.query.filter_by(user_id=current_user.id).order_by(Achievement.order).all()
270
+
271
+ # Get section order if exists
272
+ section_order_obj = ProfileSectionOrder.query.filter_by(user_id=current_user.id).first()
273
+ section_order = section_order_obj.section_order if section_order_obj else [
274
+ 'introduction', 'profile_summary', 'work_experience',
275
+ 'projects', 'education', 'skills', 'achievements'
276
+ ]
277
+
278
+ return render_template('profile.html',
279
+ has_profile=True,
280
+ intro=intro,
281
+ summary=summary,
282
+ work_experiences=work_experiences,
283
+ projects=projects,
284
+ educations=educations,
285
+ skills=skills,
286
+ achievements=achievements,
287
+ section_order=section_order)
288
+ else:
289
+ return render_template('profile.html', has_profile=False)
290
+
291
+ @app.route('/forgot-password')
292
+ def forgot_password():
293
+ return render_template('forgot_password.html')
294
+
295
+ # GitHub OAuth Routes
296
+ @app.route('/auth/github')
297
+ def github_auth():
298
+ """Initiate GitHub OAuth flow"""
299
+ if not GITHUB_CLIENT_ID or not GITHUB_CLIENT_SECRET:
300
+ flash('GitHub OAuth is not configured. Please check your environment variables.', 'error')
301
+ return redirect(url_for('signin'))
302
+
303
+ auth_url = generate_github_auth_url()
304
+ return redirect(auth_url)
305
+
306
+ @app.route('/api/auth/github/callback')
307
+ def github_callback():
308
+ """Handle GitHub OAuth callback"""
309
+ # Check for errors
310
+ error = request.args.get('error')
311
+ if error:
312
+ flash(f'GitHub authentication failed: {error}', 'error')
313
+ return redirect(url_for('signin'))
314
+
315
+ # Get authorization code and state
316
+ code = request.args.get('code')
317
+ state = request.args.get('state')
318
+
319
+ if not code:
320
+ flash('Authorization code not received from GitHub.', 'error')
321
+ return redirect(url_for('signin'))
322
+
323
+ # Exchange code for access token
324
+ access_token = exchange_code_for_token(code)
325
+ if not access_token:
326
+ flash('Failed to exchange authorization code for access token.', 'error')
327
+ return redirect(url_for('signin'))
328
+
329
+ # Get user information from GitHub
330
+ github_user = get_github_user_info(access_token)
331
+ if not github_user:
332
+ flash('Failed to retrieve user information from GitHub.', 'error')
333
+ return redirect(url_for('signin'))
334
+
335
+ # Get user email
336
+ email = get_github_user_email(access_token)
337
+ if not email:
338
+ flash('Could not retrieve email from GitHub. Please ensure your email is public and verified.', 'error')
339
+ return redirect(url_for('signin'))
340
+
341
+ # Check if user exists
342
+ user = User.query.filter_by(email=email).first()
343
+
344
+ if user:
345
+ # Existing user - check if they should be admin
346
+ admin_email = os.environ.get('ADMIN_EMAIL')
347
+ if admin_email and email.lower() == admin_email.lower():
348
+ if not user.is_admin:
349
+ user.is_admin = True
350
+ user.role = 'Admin'
351
+ db.session.commit()
352
+ flash('Admin access granted!', 'success')
353
+
354
+ login_user(user)
355
+ flash(f'Welcome back, {user.name}!', 'success')
356
+ return redirect(url_for('profile'))
357
+ else:
358
+ # New user - create account
359
+ # Generate a random password for GitHub users
360
+ import secrets
361
+ random_password = secrets.token_urlsafe(32)
362
+
363
+ new_user = User(
364
+ id=uuid.uuid4(),
365
+ name=github_user.get('name', github_user.get('login', 'GitHub User')),
366
+ email=email
367
+ )
368
+ new_user.set_password(random_password)
369
+
370
+ # Check if user should be admin
371
+ admin_email = os.environ.get('ADMIN_EMAIL')
372
+ if admin_email and email.lower() == admin_email.lower():
373
+ new_user.is_admin = True
374
+ new_user.role = 'Admin'
375
+
376
+ try:
377
+ db.session.add(new_user)
378
+ db.session.commit()
379
+ login_user(new_user)
380
+
381
+ if new_user.is_admin:
382
+ flash(f'Admin account created successfully! Welcome, {new_user.name}!', 'success')
383
+ else:
384
+ flash(f'Account created successfully! Welcome, {new_user.name}!', 'success')
385
+ return redirect(url_for('profile'))
386
+ except Exception as e:
387
+ db.session.rollback()
388
+ flash('Failed to create account. Please try again.', 'error')
389
+ return redirect(url_for('signin'))
390
+
391
+ # Profile Creation Routes
392
+ @app.route('/profile/create', methods=['GET', 'POST'])
393
+ @login_required
394
+ def create_profile():
395
+ """Start profile creation process"""
396
+ # Check if user already has a profile
397
+ if current_user.introduction:
398
+ flash('You already have a profile. View or edit it from your profile page.', 'info')
399
+ return redirect(url_for('profile'))
400
+
401
+ return render_template('create_profile.html')
402
+
403
+ @app.route('/profile/create/introduction', methods=['GET', 'POST'])
404
+ @login_required
405
+ def create_introduction():
406
+ """Create introduction section"""
407
+ form_data = {}
408
+ form_errors = {}
409
+
410
+ if request.method == 'POST':
411
+ name = request.form.get('name', '').strip()
412
+ email = request.form.get('email', '').strip()
413
+ phone = request.form.get('phone', '').strip()
414
+ linkedin = request.form.get('linkedin', '').strip()
415
+ github = request.form.get('github', '').strip()
416
+ website = request.form.get('website', '').strip()
417
+
418
+ # Store form data
419
+ form_data = {
420
+ 'name': name,
421
+ 'email': email,
422
+ 'phone': phone,
423
+ 'linkedin': linkedin,
424
+ 'github': github,
425
+ 'website': website
426
+ }
427
+
428
+ # Validation
429
+ if not name:
430
+ form_errors['name'] = ['Name is required']
431
+ if not email:
432
+ form_errors['email'] = ['Email is required']
433
+ elif not re.match(r'^[^@]+@[^@]+\.[^@]+$', email):
434
+ form_errors['email'] = ['Invalid email format']
435
+ if not phone:
436
+ form_errors['phone'] = ['Phone is required']
437
+
438
+ if form_errors:
439
+ return render_template('create_introduction.html',
440
+ form_data=form_data,
441
+ form_errors=form_errors)
442
+
443
+ # Create introduction
444
+ introduction = Introduction(
445
+ user_id=current_user.id,
446
+ name=name,
447
+ email=email,
448
+ phone=phone,
449
+ linkedin=linkedin or None,
450
+ github=github or None,
451
+ website=website or None
452
+ )
453
+
454
+ try:
455
+ db.session.add(introduction)
456
+ db.session.commit()
457
+ return redirect(url_for('create_profile_summary'))
458
+ except Exception as e:
459
+ db.session.rollback()
460
+ flash('An error occurred while saving your introduction. Please try again.', 'error')
461
+
462
+ return render_template('create_introduction.html',
463
+ form_data=form_data,
464
+ form_errors=form_errors)
465
+
466
+ @app.route('/profile/create/profile-summary', methods=['GET', 'POST'])
467
+ @login_required
468
+ def create_profile_summary():
469
+ """Create profile summary section"""
470
+ form_data = {}
471
+ form_errors = {}
472
+
473
+ if not current_user.introduction:
474
+ flash('Please complete your introduction first.', 'error')
475
+ return redirect(url_for('create_introduction'))
476
+
477
+ if request.method == 'POST':
478
+ summary = request.form.get('summary', '').strip()
479
+
480
+ # Store form data
481
+ form_data = {
482
+ 'summary': summary
483
+ }
484
+
485
+ # Validation
486
+ if not summary:
487
+ form_errors['summary'] = ['Profile summary is required']
488
+
489
+ if form_errors:
490
+ return render_template('create_profile_summary.html',
491
+ form_data=form_data,
492
+ form_errors=form_errors)
493
+
494
+ # Create or update profile summary
495
+ profile_summary = ProfileSummary.query.filter_by(user_id=current_user.id).first()
496
+ if not profile_summary:
497
+ profile_summary = ProfileSummary(user_id=current_user.id)
498
+
499
+ profile_summary.summary = summary
500
+ profile_summary.ai_generated = False
501
+
502
+ try:
503
+ db.session.add(profile_summary)
504
+ db.session.commit()
505
+ return redirect(url_for('create_work_experience'))
506
+ except Exception as e:
507
+ db.session.rollback()
508
+ flash('An error occurred while saving your profile summary. Please try again.', 'error')
509
+
510
+ # For GET request or if returning from POST with error
511
+ # Get existing summary if any
512
+ existing_summary = ProfileSummary.query.filter_by(user_id=current_user.id).first()
513
+ if existing_summary:
514
+ form_data['summary'] = existing_summary.summary
515
+
516
+ return render_template('create_profile_summary.html',
517
+ form_data=form_data,
518
+ form_errors=form_errors)
519
+
520
+ @app.route('/profile/create/generate-summary', methods=['POST'])
521
+ @login_required
522
+ def generate_ai_summary():
523
+ """Generate AI-powered profile summary using OpenAI"""
524
+ if not current_user.introduction:
525
+ return jsonify({'error': 'Please complete your introduction first'}), 400
526
+
527
+ try:
528
+ # Get user's introduction and other relevant info
529
+ intro = current_user.introduction
530
+
531
+ # Get additional profile data if available
532
+ work_experiences = WorkExperience.query.filter_by(user_id=current_user.id).all()
533
+ projects = Project.query.filter_by(user_id=current_user.id).all()
534
+ educations = Education.query.filter_by(user_id=current_user.id).all()
535
+ skills = Skill.query.filter_by(user_id=current_user.id).all()
536
+
537
+ # Prepare context for AI
538
+ context = f"""
539
+ Personal Information:
540
+ - Name: {intro.name}
541
+ - Email: {intro.email}
542
+ - Phone: {intro.phone}
543
+ - LinkedIn: {intro.linkedin or 'Not provided'}
544
+ - GitHub: {intro.github or 'Not provided'}
545
+ - Website: {intro.website or 'Not provided'}
546
+ """
547
+
548
+ if work_experiences:
549
+ context += "\n\nWork Experience:\n"
550
+ for exp in work_experiences[:3]: # Limit to first 3 experiences
551
+ context += f"- {exp.title} at {exp.organization} ({exp.start_month}/{exp.start_year} - {'Present' if not exp.end_month else f'{exp.end_month}/{exp.end_year}'})\n"
552
+
553
+ if educations:
554
+ context += "\nEducation:\n"
555
+ for edu in educations[:2]: # Limit to first 2 educations
556
+ context += f"- {edu.title} at {edu.organization} ({edu.start_month}/{edu.start_year} - {'Present' if not edu.end_month else f'{edu.end_month}/{edu.end_year}'})\n"
557
+
558
+ if skills:
559
+ skill_list = [s.skill for s in skills[:10]] # Limit to first 10 skills
560
+ context += f"\nSkills: {', '.join(skill_list)}"
561
+
562
+ # Using OpenAI API directly
563
+ def generate_with_openai():
564
+ from openai import OpenAI
565
+ client = OpenAI(api_key=os.environ.get('OPENAI_API_KEY'))
566
+
567
+ prompt = f"""
568
+ Create a professional profile summary based on this information:
569
+
570
+ {context}
571
+
572
+ Requirements:
573
+ - Write in third person
574
+ - Keep it concise (3-5 sentences)
575
+ - Highlight professional strengths and achievements
576
+ - Make it engaging and professional
577
+ - Focus on what makes this person unique
578
+ """
579
+
580
+ response = client.chat.completions.create(
581
+ model=os.environ.get('OPENAI_MODEL', 'gpt-4o'),
582
+ messages=[
583
+ {"role": "system", "content": "You are an expert resume writer and career coach. Write compelling professional profiles that stand out."},
584
+ {"role": "user", "content": prompt}
585
+ ],
586
+ max_tokens=250,
587
+ temperature=0.8
588
+ )
589
+ return response.choices[0].message.content.strip()
590
+
591
+ # Generate the summary
592
+ ai_summary = generate_with_openai()
593
+
594
+ # Save the AI-generated summary
595
+ profile_summary = ProfileSummary.query.filter_by(user_id=current_user.id).first()
596
+ if not profile_summary:
597
+ profile_summary = ProfileSummary(user_id=current_user.id)
598
+
599
+ profile_summary.summary = ai_summary
600
+ profile_summary.ai_generated = True
601
+
602
+ db.session.add(profile_summary)
603
+ db.session.commit()
604
+
605
+ return jsonify({
606
+ 'success': True,
607
+ 'summary': ai_summary,
608
+ 'message': 'Profile summary generated successfully!'
609
+ })
610
+
611
+ except Exception as e:
612
+ db.session.rollback()
613
+ return jsonify({'success': False, 'error': f'Failed to generate summary: {str(e)}'}), 500
614
+
615
+ @app.route('/profile/create/work-experience', methods=['GET', 'POST'])
616
+ @login_required
617
+ def create_work_experience():
618
+ """Create work experience section"""
619
+ if not current_user.introduction:
620
+ flash('Please complete your introduction first.', 'error')
621
+ return redirect(url_for('create_introduction'))
622
+
623
+ if request.method == 'POST':
624
+ # Clear existing work experiences for this user
625
+ WorkExperience.query.filter_by(user_id=current_user.id).delete()
626
+
627
+ # Get form data
628
+ organizations = request.form.getlist('organization[]')
629
+ titles = request.form.getlist('title[]')
630
+ start_months = request.form.getlist('start_month[]')
631
+ start_years = request.form.getlist('start_year[]')
632
+ end_months = request.form.getlist('end_month[]')
633
+ end_years = request.form.getlist('end_year[]')
634
+ is_present_list = request.form.getlist('is_present[]')
635
+ remarks_list = request.form.getlist('remarks[]')
636
+
637
+ # Find the maximum length among all lists to handle inconsistent lengths
638
+ max_length = max(len(organizations), len(titles), len(start_months),
639
+ len(start_years), len(end_months), len(end_years),
640
+ len(remarks_list))
641
+
642
+ # Save each work experience
643
+ for i in range(max_length):
644
+ org = organizations[i] if i < len(organizations) else ''
645
+ title = titles[i] if i < len(titles) else ''
646
+
647
+ if org.strip(): # Only save if organization is provided
648
+ work_exp = WorkExperience(
649
+ user_id=current_user.id,
650
+ organization=org.strip(),
651
+ title=title.strip(),
652
+ start_month=int(start_months[i]) if i < len(start_months) and start_months[i] else None,
653
+ start_year=int(start_years[i]) if i < len(start_years) and start_years[i] else None,
654
+ end_month=int(end_months[i]) if i < len(end_months) and end_months[i] and str(i) not in is_present_list else None,
655
+ end_year=int(end_years[i]) if i < len(end_years) and end_years[i] and str(i) not in is_present_list else None,
656
+ remarks=remarks_list[i].strip() if i < len(remarks_list) and remarks_list[i] else None,
657
+ order=i
658
+ )
659
+ db.session.add(work_exp)
660
+
661
+ try:
662
+ db.session.commit()
663
+ flash('Work experience saved successfully.', 'success')
664
+ return redirect(url_for('create_projects'))
665
+ except Exception as e:
666
+ db.session.rollback()
667
+ flash('An error occurred while saving work experience.', 'error')
668
+
669
+ # GET request - show form
670
+ form_data = {
671
+ 'work_experiences': WorkExperience.query.filter_by(user_id=current_user.id).order_by(WorkExperience.order).all()
672
+ }
673
+
674
+ return render_template('create_work_experience.html',
675
+ form_data=form_data,
676
+ current_year=datetime.now().year)
677
+
678
+ @app.route('/profile/create/projects', methods=['GET', 'POST'])
679
+ @login_required
680
+ def create_projects():
681
+ """Create projects section"""
682
+ if not current_user.introduction:
683
+ flash('Please complete your introduction first.', 'error')
684
+ return redirect(url_for('create_introduction'))
685
+
686
+ if request.method == 'POST':
687
+ # Clear existing projects for this user
688
+ Project.query.filter_by(user_id=current_user.id).delete()
689
+
690
+ # Get form data
691
+ organizations = request.form.getlist('organization[]')
692
+ titles = request.form.getlist('title[]')
693
+ start_months = request.form.getlist('start_month[]')
694
+ start_years = request.form.getlist('start_year[]')
695
+ end_months = request.form.getlist('end_month[]')
696
+ end_years = request.form.getlist('end_year[]')
697
+ is_present_list = request.form.getlist('is_present[]')
698
+ remarks_list = request.form.getlist('remarks[]')
699
+
700
+ # Find the maximum length among all lists to handle inconsistent lengths
701
+ max_length = max(len(organizations), len(titles), len(start_months),
702
+ len(start_years), len(end_months), len(end_years),
703
+ len(remarks_list))
704
+
705
+ # Save each project
706
+ for i in range(max_length):
707
+ title = titles[i] if i < len(titles) else ''
708
+ org = organizations[i] if i < len(organizations) else ''
709
+
710
+ if title.strip(): # Only save if title is provided
711
+ project = Project(
712
+ user_id=current_user.id,
713
+ organization=org.strip() if org else None,
714
+ title=title.strip(),
715
+ start_month=int(start_months[i]) if i < len(start_months) and start_months[i] else None,
716
+ start_year=int(start_years[i]) if i < len(start_years) and start_years[i] else None,
717
+ end_month=int(end_months[i]) if i < len(end_months) and end_months[i] and str(i) not in is_present_list else None,
718
+ end_year=int(end_years[i]) if i < len(end_years) and end_years[i] and str(i) not in is_present_list else None,
719
+ remarks=remarks_list[i].strip() if i < len(remarks_list) and remarks_list[i] else None,
720
+ order=i
721
+ )
722
+ db.session.add(project)
723
+
724
+ try:
725
+ db.session.commit()
726
+ flash('Projects saved successfully.', 'success')
727
+ return redirect(url_for('create_education'))
728
+ except Exception as e:
729
+ db.session.rollback()
730
+ flash('An error occurred while saving projects.', 'error')
731
+
732
+ # GET request - show form
733
+ form_data = {
734
+ 'projects': Project.query.filter_by(user_id=current_user.id).order_by(Project.order).all()
735
+ }
736
+
737
+ return render_template('create_projects.html',
738
+ form_data=form_data,
739
+ current_year=datetime.now().year)
740
+
741
+ @app.route('/profile/create/education', methods=['GET', 'POST'])
742
+ @login_required
743
+ def create_education():
744
+ """Create education section"""
745
+ if not current_user.introduction:
746
+ flash('Please complete your introduction first.', 'error')
747
+ return redirect(url_for('create_introduction'))
748
+
749
+ if request.method == 'POST':
750
+ # Clear existing education for this user
751
+ Education.query.filter_by(user_id=current_user.id).delete()
752
+
753
+ # Get form data
754
+ organizations = request.form.getlist('organization[]')
755
+ titles = request.form.getlist('title[]')
756
+ start_months = request.form.getlist('start_month[]')
757
+ start_years = request.form.getlist('start_year[]')
758
+ end_months = request.form.getlist('end_month[]')
759
+ end_years = request.form.getlist('end_year[]')
760
+ is_present_list = request.form.getlist('is_present[]')
761
+ remarks_list = request.form.getlist('remarks[]')
762
+
763
+ # Find the maximum length among all lists to handle inconsistent lengths
764
+ max_length = max(len(organizations), len(titles), len(start_months),
765
+ len(start_years), len(end_months), len(end_years),
766
+ len(remarks_list))
767
+
768
+ # Save each education
769
+ for i in range(max_length):
770
+ org = organizations[i] if i < len(organizations) else ''
771
+ title = titles[i] if i < len(titles) else ''
772
+
773
+ if org.strip(): # Only save if organization is provided
774
+ education = Education(
775
+ user_id=current_user.id,
776
+ organization=org.strip(),
777
+ title=title.strip(),
778
+ start_month=int(start_months[i]) if i < len(start_months) and start_months[i] else None,
779
+ start_year=int(start_years[i]) if i < len(start_years) and start_years[i] else None,
780
+ end_month=int(end_months[i]) if i < len(end_months) and end_months[i] and str(i) not in is_present_list else None,
781
+ end_year=int(end_years[i]) if i < len(end_years) and end_years[i] and str(i) not in is_present_list else None,
782
+ remarks=remarks_list[i].strip() if i < len(remarks_list) and remarks_list[i] else None,
783
+ order=i
784
+ )
785
+ db.session.add(education)
786
+
787
+ try:
788
+ db.session.commit()
789
+ flash('Education saved successfully.', 'success')
790
+ return redirect(url_for('create_skills'))
791
+ except Exception as e:
792
+ db.session.rollback()
793
+ flash('An error occurred while saving education.', 'error')
794
+
795
+ # GET request - show form
796
+ form_data = {
797
+ 'educations': Education.query.filter_by(user_id=current_user.id).order_by(Education.order).all()
798
+ }
799
+
800
+ return render_template('create_education.html',
801
+ form_data=form_data,
802
+ current_year=datetime.now().year)
803
+
804
+ @app.route('/profile/create/skills', methods=['GET', 'POST'])
805
+ @login_required
806
+ def create_skills():
807
+ """Create skills section"""
808
+ form_data = {}
809
+ form_errors = {}
810
+
811
+ if not current_user.introduction:
812
+ flash('Please complete your introduction first.', 'error')
813
+ return redirect(url_for('create_introduction'))
814
+
815
+ if request.method == 'POST':
816
+ # Get skills from form
817
+ skills_text = request.form.get('skills', '').strip()
818
+
819
+ # Store form data
820
+ form_data = {
821
+ 'skills': skills_text,
822
+ 'skills_preview': [skill.strip() for skill in skills_text.split(',') if skill.strip()] if skills_text else []
823
+ }
824
+
825
+ # Validation - skills are optional but if provided, they should be valid
826
+ if skills_text and len(skills_text.split(',')) > 50:
827
+ form_errors['skills'] = ['You can add up to 50 skills maximum']
828
+
829
+ if form_errors:
830
+ return render_template('create_skills.html',
831
+ form_data=form_data,
832
+ form_errors=form_errors)
833
+
834
+ # Clear existing skills for this user
835
+ Skill.query.filter_by(user_id=current_user.id).delete()
836
+
837
+ if skills_text:
838
+ skills_list = [skill.strip() for skill in skills_text.split(',') if skill.strip()]
839
+
840
+ # Save each skill
841
+ for i, skill in enumerate(skills_list):
842
+ new_skill = Skill(
843
+ user_id=current_user.id,
844
+ skill=skill,
845
+ order=i
846
+ )
847
+ db.session.add(new_skill)
848
+
849
+ try:
850
+ db.session.commit()
851
+ flash('Skills saved successfully.', 'success')
852
+ return redirect(url_for('create_achievements'))
853
+ except Exception as e:
854
+ db.session.rollback()
855
+ flash('An error occurred while saving skills.', 'error')
856
+
857
+ # GET request - show form
858
+ existing_skills = Skill.query.filter_by(user_id=current_user.id).order_by(Skill.order).all()
859
+ skills_text = ', '.join([skill.skill for skill in existing_skills])
860
+
861
+ form_data = {
862
+ 'skills': skills_text,
863
+ 'skills_preview': [skill.skill for skill in existing_skills]
864
+ }
865
+
866
+ return render_template('create_skills.html',
867
+ form_data=form_data,
868
+ form_errors=form_errors)
869
+
870
+ @app.route('/profile/create/achievements', methods=['GET', 'POST'])
871
+ @login_required
872
+ def create_achievements():
873
+ """Create achievements section"""
874
+ form_data = {}
875
+ form_errors = {}
876
+
877
+ if not current_user.introduction:
878
+ flash('Please complete your introduction first.', 'error')
879
+ return redirect(url_for('create_introduction'))
880
+
881
+ if request.method == 'POST':
882
+ # Get achievements from form
883
+ achievements_text = request.form.get('achievements', '').strip()
884
+
885
+ # Store form data
886
+ form_data = {
887
+ 'achievements': achievements_text,
888
+ 'achievements_preview': [achievement.strip() for achievement in achievements_text.split(',') if achievement.strip()] if achievements_text else []
889
+ }
890
+
891
+ # Validation - achievements are optional but if provided, they should be valid
892
+ if achievements_text and len(achievements_text.split(',')) > 50:
893
+ form_errors['achievements'] = ['You can add up to 50 achievements maximum']
894
+
895
+ if form_errors:
896
+ return render_template('create_achievements.html',
897
+ form_data=form_data,
898
+ form_errors=form_errors)
899
+
900
+ # Clear existing achievements for this user
901
+ Achievement.query.filter_by(user_id=current_user.id).delete()
902
+
903
+ if achievements_text:
904
+ achievements_list = [achievement.strip() for achievement in achievements_text.split(',') if achievement.strip()]
905
+
906
+ # Save each achievement
907
+ for i, achievement in enumerate(achievements_list):
908
+ new_achievement = Achievement(
909
+ user_id=current_user.id,
910
+ achievement=achievement,
911
+ order=i
912
+ )
913
+ db.session.add(new_achievement)
914
+
915
+ try:
916
+ db.session.commit()
917
+ flash('Achievements saved successfully.', 'success')
918
+ return redirect(url_for('create_preview'))
919
+ except Exception as e:
920
+ db.session.rollback()
921
+ flash('An error occurred while saving achievements.', 'error')
922
+
923
+ # GET request - show form
924
+ existing_achievements = Achievement.query.filter_by(user_id=current_user.id).order_by(Achievement.order).all()
925
+ achievements_text = ', '.join([achievement.achievement for achievement in existing_achievements])
926
+
927
+ form_data = {
928
+ 'achievements': achievements_text,
929
+ 'achievements_preview': [achievement.achievement for achievement in existing_achievements]
930
+ }
931
+
932
+ return render_template('create_achievements.html',
933
+ form_data=form_data,
934
+ form_errors=form_errors)
935
+
936
+ @app.route('/profile/create/preview', methods=['GET', 'POST'])
937
+ @login_required
938
+ def create_preview():
939
+ """Preview and finalize profile"""
940
+ if not current_user.introduction:
941
+ flash('Please complete your introduction first.', 'error')
942
+ return redirect(url_for('create_introduction'))
943
+
944
+ if request.method == 'POST':
945
+ action = request.form.get('action')
946
+
947
+ if action == 'submit':
948
+ # Save section order
949
+ section_order = request.form.get('section_order', '[]')
950
+ try:
951
+ order_data = json.loads(section_order)
952
+
953
+ # Update or create section order
954
+ profile_order = ProfileSectionOrder.query.filter_by(user_id=current_user.id).first()
955
+ if not profile_order:
956
+ profile_order = ProfileSectionOrder(user_id=current_user.id)
957
+
958
+ profile_order.section_order = order_data
959
+ db.session.add(profile_order)
960
+ db.session.commit()
961
+
962
+ flash('Profile created successfully!', 'success')
963
+ return redirect(url_for('profile'))
964
+
965
+ except Exception as e:
966
+ db.session.rollback()
967
+ flash('An error occurred while saving your profile.', 'error')
968
+
969
+ elif action == 'clear':
970
+ # Delete all profile data
971
+ try:
972
+ # Delete all profile sections
973
+ if current_user.introduction:
974
+ db.session.delete(current_user.introduction)
975
+ if current_user.profile_summary:
976
+ db.session.delete(current_user.profile_summary)
977
+ if current_user.section_order:
978
+ db.session.delete(current_user.section_order)
979
+
980
+ # Delete collections
981
+ WorkExperience.query.filter_by(user_id=current_user.id).delete()
982
+ Project.query.filter_by(user_id=current_user.id).delete()
983
+ Education.query.filter_by(user_id=current_user.id).delete()
984
+ Skill.query.filter_by(user_id=current_user.id).delete()
985
+ Achievement.query.filter_by(user_id=current_user.id).delete()
986
+
987
+ db.session.commit()
988
+ flash('Profile cleared successfully.', 'success')
989
+ return redirect(url_for('profile'))
990
+
991
+ except Exception as e:
992
+ db.session.rollback()
993
+ flash('An error occurred while clearing your profile.', 'error')
994
+
995
+ # Get all profile data
996
+ intro = current_user.introduction
997
+ summary = current_user.profile_summary
998
+ work_experiences = WorkExperience.query.filter_by(user_id=current_user.id).order_by(WorkExperience.order).all()
999
+ projects = Project.query.filter_by(user_id=current_user.id).order_by(Project.order).all()
1000
+ educations = Education.query.filter_by(user_id=current_user.id).order_by(Education.order).all()
1001
+ skills = Skill.query.filter_by(user_id=current_user.id).order_by(Skill.order).all()
1002
+ achievements = Achievement.query.filter_by(user_id=current_user.id).order_by(Achievement.order).all()
1003
+
1004
+ # Get default section order
1005
+ default_order = ['introduction', 'profile_summary', 'work_experience', 'projects', 'education', 'skills', 'achievements']
1006
+
1007
+ return render_template('create_preview.html',
1008
+ intro=intro,
1009
+ summary=summary,
1010
+ work_experiences=work_experiences,
1011
+ projects=projects,
1012
+ educations=educations,
1013
+ skills=skills,
1014
+ achievements=achievements,
1015
+ default_order=default_order)
1016
+
1017
+ @app.route('/ping')
1018
+ def ping():
1019
+ return {"ok": True, "msg": "pong from app.py"}
1020
+
1021
+ @app.cli.command()
1022
+ def create_tables():
1023
+ """Create database tables"""
1024
+ with app.app_context():
1025
+ db.create_all()
1026
+ print("Database tables created successfully!")
1027
+
1028
+ # Resume Generation Routes
1029
+ @app.route('/profile/generate-resume/<format_type>')
1030
+ @login_required
1031
+ def generate_resume(format_type):
1032
+ """Generate resume in specified format"""
1033
+ # Check if user has profile data
1034
+ intro = Introduction.query.filter_by(user_id=current_user.id).first()
1035
+ if not intro:
1036
+ flash('Please create your profile first.', 'error')
1037
+ return redirect(url_for('profile'))
1038
+
1039
+ # Get all profile data
1040
+ summary = ProfileSummary.query.filter_by(user_id=current_user.id).first()
1041
+ work_experiences = WorkExperience.query.filter_by(user_id=current_user.id).order_by(WorkExperience.order).all()
1042
+ projects = Project.query.filter_by(user_id=current_user.id).order_by(Project.order).all()
1043
+ educations = Education.query.filter_by(user_id=current_user.id).order_by(Education.order).all()
1044
+ skills = Skill.query.filter_by(user_id=current_user.id).order_by(Skill.order).all()
1045
+ achievements = Achievement.query.filter_by(user_id=current_user.id).order_by(Achievement.order).all()
1046
+
1047
+ # Get section order
1048
+ section_order_obj = ProfileSectionOrder.query.filter_by(user_id=current_user.id).first()
1049
+ section_order = section_order_obj.section_order if section_order_obj else [
1050
+ 'introduction', 'profile_summary', 'work_experience',
1051
+ 'projects', 'education', 'skills', 'achievements'
1052
+ ]
1053
+
1054
+ try:
1055
+ if format_type == 'word':
1056
+ return generate_word_resume(
1057
+ intro, summary, work_experiences, projects,
1058
+ educations, skills, achievements, section_order
1059
+ )
1060
+ elif format_type == 'pdf-standard':
1061
+ return generate_pdf_resume(
1062
+ intro, summary, work_experiences, projects,
1063
+ educations, skills, achievements, section_order, 'standard'
1064
+ )
1065
+ elif format_type == 'pdf-modern':
1066
+ return generate_pdf_resume(
1067
+ intro, summary, work_experiences, projects,
1068
+ educations, skills, achievements, section_order, 'modern'
1069
+ )
1070
+ else:
1071
+ flash('Invalid resume format.', 'error')
1072
+ return redirect(url_for('profile'))
1073
+
1074
+ except Exception as e:
1075
+ import traceback
1076
+ app.logger.error(f"Error generating resume: {str(e)}")
1077
+ app.logger.error(traceback.format_exc())
1078
+ flash(f'An error occurred while generating your resume: {str(e)}', 'error')
1079
+ return redirect(url_for('profile'))
1080
+
1081
+ def generate_word_resume(intro, summary, work_experiences, projects, educations, skills, achievements, section_order):
1082
+ """Generate Word document resume"""
1083
+ try:
1084
+ from docx import Document
1085
+ from docx.shared import Pt, Inches, RGBColor
1086
+ from docx.enum.text import WD_ALIGN_PARAGRAPH
1087
+
1088
+ # Create document
1089
+ doc = Document()
1090
+
1091
+ # Set default font
1092
+ style = doc.styles['Normal']
1093
+ font = style.font
1094
+ font.name = 'Calibri'
1095
+ font.size = Pt(11)
1096
+
1097
+ # Header section
1098
+ name_para = doc.add_paragraph()
1099
+ name_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
1100
+ name_run = name_para.add_run(intro.name.upper())
1101
+ name_run.bold = True
1102
+ name_run.size = Pt(16)
1103
+
1104
+ # Contact info
1105
+ contact_para = doc.add_paragraph()
1106
+ contact_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
1107
+ contact_para.add_run(f"Email: {intro.email} | Phone: {intro.phone}")
1108
+ if intro.linkedin:
1109
+ contact_para.add_run(" | LinkedIn")
1110
+ if intro.github:
1111
+ contact_para.add_run(" | GitHub")
1112
+ if intro.website:
1113
+ contact_para.add_run(" | Website")
1114
+
1115
+ doc.add_paragraph()
1116
+
1117
+ # Profile summary
1118
+ if summary:
1119
+ doc.add_heading('Professional Summary', level=1)
1120
+ summary_para = doc.add_paragraph(summary.summary)
1121
+ if summary.ai_generated:
1122
+ ai_para = doc.add_paragraph("AI Generated")
1123
+ ai_para.italic = True
1124
+ ai_para.runs[0].font.size = Pt(9)
1125
+ doc.add_paragraph()
1126
+
1127
+ for section_name in section_order:
1128
+ if section_name == 'work_experience' and work_experiences:
1129
+ doc.add_heading('Work Experience', level=1)
1130
+ for exp in work_experiences:
1131
+ # Title and organization
1132
+ exp_para = doc.add_paragraph()
1133
+ exp_para.add_run(exp.title).bold = True
1134
+ exp_para.add_run(f" at {exp.organization}").italic = True
1135
+
1136
+ # Date
1137
+ date_para = doc.add_paragraph()
1138
+ date_text = f"{exp.start_month}/{exp.start_year} - "
1139
+ if exp.end_year:
1140
+ date_text += f"{exp.end_month}/{exp.end_year}"
1141
+ else:
1142
+ date_text += "Present"
1143
+ date_para.add_run(date_text)
1144
+
1145
+ # Remarks
1146
+ if exp.remarks:
1147
+ remarks_para = doc.add_paragraph(exp.remarks)
1148
+
1149
+ doc.add_paragraph()
1150
+
1151
+ elif section_name == 'projects' and projects:
1152
+ doc.add_heading('Projects', level=1)
1153
+ for project in projects:
1154
+ # Title and organization
1155
+ proj_para = doc.add_paragraph()
1156
+ proj_para.add_run(project.title).bold = True
1157
+ if project.organization:
1158
+ proj_para.add_run(f" at {project.organization}").italic = True
1159
+
1160
+ # Date
1161
+ date_para = doc.add_paragraph()
1162
+ date_text = f"{project.start_month}/{project.start_year} - "
1163
+ if project.end_year:
1164
+ date_text += f"{project.end_month}/{project.end_year}"
1165
+ else:
1166
+ date_text += "Present"
1167
+ date_para.add_run(date_text)
1168
+
1169
+ # Remarks
1170
+ if project.remarks:
1171
+ remarks_para = doc.add_paragraph(project.remarks)
1172
+
1173
+ doc.add_paragraph()
1174
+
1175
+ elif section_name == 'education' and educations:
1176
+ doc.add_heading('Education', level=1)
1177
+ for edu in educations:
1178
+ # Title and organization
1179
+ edu_para = doc.add_paragraph()
1180
+ edu_para.add_run(edu.title).bold = True
1181
+ edu_para.add_run(f" at {edu.organization}").italic = True
1182
+
1183
+ # Date
1184
+ date_para = doc.add_paragraph()
1185
+ date_text = f"{edu.start_month}/{edu.start_year} - "
1186
+ if edu.end_year:
1187
+ date_text += f"{edu.end_month}/{edu.end_year}"
1188
+ else:
1189
+ date_text += "Present"
1190
+ date_para.add_run(date_text)
1191
+
1192
+ # Remarks
1193
+ if edu.remarks:
1194
+ remarks_para = doc.add_paragraph(edu.remarks)
1195
+
1196
+ doc.add_paragraph()
1197
+
1198
+ elif section_name == 'skills' and skills:
1199
+ doc.add_heading('Skills', level=1)
1200
+ skills_text = ", ".join([skill.skill for skill in skills])
1201
+ doc.add_paragraph(skills_text)
1202
+ doc.add_paragraph()
1203
+
1204
+ elif section_name == 'achievements' and achievements:
1205
+ doc.add_heading('Achievements', level=1)
1206
+ for achievement in achievements:
1207
+ doc.add_paragraph(f"• {achievement.achievement}")
1208
+ doc.add_paragraph()
1209
+
1210
+ # Save to bytes
1211
+ from io import BytesIO
1212
+ doc_buffer = BytesIO()
1213
+ doc.save(doc_buffer)
1214
+ doc_buffer.seek(0)
1215
+
1216
+ # Return as downloadable file
1217
+ username = intro.name.replace(' ', '_')
1218
+ return send_file(
1219
+ doc_buffer,
1220
+ mimetype='application/vnd.openxmlformats-officedocument.wordprocessingml.document',
1221
+ as_attachment=True,
1222
+ download_name=f'{username}_resume.docx'
1223
+ )
1224
+
1225
+ except Exception as e:
1226
+ app.logger.error(f"Error generating Word resume: {str(e)}")
1227
+ raise
1228
+
1229
+ def generate_pdf_resume(intro, summary, work_experiences, projects, educations, skills, achievements, section_order, template_type):
1230
+ """Generate PDF resume using ReportLab"""
1231
+ try:
1232
+ from pdf_generator import create_pdf_resume
1233
+ from io import BytesIO
1234
+ import calendar
1235
+
1236
+ def format_date(month, year):
1237
+ """Format month and year as 'Month Year'"""
1238
+ if month and year:
1239
+ try:
1240
+ month_name = calendar.month_name[int(month)]
1241
+ return f"{month_name[:3]} {year}"
1242
+ except:
1243
+ return f"{month}/{year}"
1244
+ return ""
1245
+
1246
+ # Prepare data for PDF generation
1247
+ work_exp_list = []
1248
+ if work_experiences:
1249
+ for exp in work_experiences:
1250
+ start_date = format_date(exp.start_month, exp.start_year)
1251
+ end_date = "Present" if not exp.end_month or not exp.end_year else format_date(exp.end_month, exp.end_year)
1252
+
1253
+ work_exp_list.append({
1254
+ 'title': exp.title,
1255
+ 'organization': exp.organization,
1256
+ 'start_date': start_date,
1257
+ 'end_date': end_date,
1258
+ 'remarks': exp.remarks or ''
1259
+ })
1260
+
1261
+ projects_list = []
1262
+ if projects:
1263
+ for proj in projects:
1264
+ start_date = format_date(proj.start_month, proj.start_year)
1265
+ end_date = "Present" if not proj.end_month or not proj.end_year else format_date(proj.end_month, proj.end_year)
1266
+
1267
+ projects_list.append({
1268
+ 'title': proj.title,
1269
+ 'organization': proj.organization,
1270
+ 'start_date': start_date,
1271
+ 'end_date': end_date,
1272
+ 'remarks': proj.remarks or ''
1273
+ })
1274
+
1275
+ education_list = []
1276
+ if educations:
1277
+ for edu in educations:
1278
+ start_date = format_date(edu.start_month, edu.start_year)
1279
+ end_date = "Present" if not edu.end_month or not edu.end_year else format_date(edu.end_month, edu.end_year)
1280
+
1281
+ education_list.append({
1282
+ 'title': edu.title,
1283
+ 'organization': edu.organization,
1284
+ 'start_date': start_date,
1285
+ 'end_date': end_date,
1286
+ 'remarks': edu.remarks or ''
1287
+ })
1288
+
1289
+ # Convert skills and achievements to comma-separated strings
1290
+ skills_text = ', '.join([skill.skill for skill in skills]) if skills else ''
1291
+ achievements_text = ', '.join([achievement.achievement for achievement in achievements]) if achievements else ''
1292
+
1293
+ # Create data dictionary
1294
+ data = {
1295
+ 'name': intro.name,
1296
+ 'email': intro.email,
1297
+ 'phone': intro.phone,
1298
+ 'linkedin': intro.linkedin,
1299
+ 'github': intro.github,
1300
+ 'website': intro.website,
1301
+ 'summary': summary.summary if summary else '',
1302
+ 'work_experience': work_exp_list,
1303
+ 'projects': projects_list,
1304
+ 'education': education_list,
1305
+ 'skills': skills_text,
1306
+ 'achievements': achievements_text,
1307
+ 'sections_order': section_order
1308
+ }
1309
+
1310
+ # Generate PDF
1311
+ pdf_bytes = create_pdf_resume(data, template_type)
1312
+
1313
+ if pdf_bytes:
1314
+ # Return as downloadable file
1315
+ username = intro.name.replace(' ', '_')
1316
+ filename = f'{username}_resume.pdf' if template_type == 'standard' else f'{username}_resume_modern.pdf'
1317
+
1318
+ pdf_buffer = BytesIO(pdf_bytes)
1319
+
1320
+ return send_file(
1321
+ pdf_buffer,
1322
+ mimetype='application/pdf',
1323
+ as_attachment=True,
1324
+ download_name=filename
1325
+ )
1326
+ else:
1327
+ raise Exception("PDF generation failed")
1328
+
1329
+ except Exception as e:
1330
+ app.logger.error(f"Error generating PDF resume: {str(e)}")
1331
+ raise
1332
+
1333
+ # Admin Panel Route
1334
+ @app.route('/admin')
1335
+ @admin_required
1336
+ def admin_panel():
1337
+ """Admin panel to manage users"""
1338
+ users = User.query.order_by(User.created_at.desc()).all()
1339
+ return render_template('admin.html', users=users)
1340
+
1341
+ # Admin API Endpoints
1342
+ @app.route('/api/admin/users', methods=['GET'])
1343
+ @admin_required
1344
+ def api_get_users():
1345
+ """Get list of all users"""
1346
+ try:
1347
+ users = User.query.order_by(User.created_at.desc()).all()
1348
+ users_data = []
1349
+
1350
+ for user in users:
1351
+ users_data.append({
1352
+ 'id': str(user.id),
1353
+ 'email': user.email,
1354
+ 'name': user.name,
1355
+ 'created_at': user.created_at.isoformat() if user.created_at else None,
1356
+ 'is_admin': user.is_admin,
1357
+ 'role': user.role
1358
+ })
1359
+
1360
+ return jsonify({
1361
+ 'success': True,
1362
+ 'users': users_data
1363
+ })
1364
+ except Exception as e:
1365
+ app.logger.error(f"Error fetching users: {str(e)}")
1366
+ return jsonify({
1367
+ 'success': False,
1368
+ 'error': str(e)
1369
+ }), 500
1370
+
1371
+ @app.route('/api/admin/users/<user_id>', methods=['DELETE'])
1372
+ @admin_required
1373
+ def api_delete_user(user_id):
1374
+ """Delete a user and all their profile data"""
1375
+ try:
1376
+ # Find the user and store email before deletion
1377
+ user = User.query.get(user_id)
1378
+ if not user:
1379
+ return jsonify({
1380
+ 'success': False,
1381
+ 'error': 'User not found'
1382
+ }), 404
1383
+
1384
+ # Store user email for success message
1385
+ user_email = user.email
1386
+
1387
+ # Don't allow deleting admin users
1388
+ if user.is_admin:
1389
+ return jsonify({
1390
+ 'success': False,
1391
+ 'error': 'Cannot delete admin users'
1392
+ }), 400
1393
+
1394
+ # Don't allow deleting self
1395
+ if str(user.id) == str(current_user.id):
1396
+ return jsonify({
1397
+ 'success': False,
1398
+ 'error': 'Cannot delete your own account'
1399
+ }), 400
1400
+
1401
+ # Use direct SQL to delete all profile data first
1402
+ # This ensures we handle any duplicate data that might exist
1403
+
1404
+ # Delete from all profile tables
1405
+ from sqlalchemy import text
1406
+
1407
+ # Delete introductions (handle potential duplicates)
1408
+ db.session.execute(text("DELETE FROM introductions WHERE user_id = :user_id"), {'user_id': str(user_id)})
1409
+
1410
+ # Delete profile summaries
1411
+ db.session.execute(text("DELETE FROM profile_summaries WHERE user_id = :user_id"), {'user_id': str(user_id)})
1412
+
1413
+ # Delete work experiences
1414
+ db.session.execute(text("DELETE FROM work_experiences WHERE user_id = :user_id"), {'user_id': str(user_id)})
1415
+
1416
+ # Delete projects
1417
+ db.session.execute(text("DELETE FROM projects WHERE user_id = :user_id"), {'user_id': str(user_id)})
1418
+
1419
+ # Delete education
1420
+ db.session.execute(text("DELETE FROM educations WHERE user_id = :user_id"), {'user_id': str(user_id)})
1421
+
1422
+ # Delete skills
1423
+ db.session.execute(text("DELETE FROM skills WHERE user_id = :user_id"), {'user_id': str(user_id)})
1424
+
1425
+ # Delete achievements
1426
+ db.session.execute(text("DELETE FROM achievements WHERE user_id = :user_id"), {'user_id': str(user_id)})
1427
+
1428
+ # Delete section order
1429
+ db.session.execute(text("DELETE FROM profile_section_orders WHERE user_id = :user_id"), {'user_id': str(user_id)})
1430
+
1431
+ # Finally delete the user
1432
+ db.session.execute(text("DELETE FROM users WHERE id = :user_id"), {'user_id': str(user_id)})
1433
+
1434
+ # Expunge the user object from session to avoid the deleted instance warning
1435
+ db.session.expunge(user)
1436
+
1437
+ db.session.commit()
1438
+
1439
+ return jsonify({
1440
+ 'success': True,
1441
+ 'message': f'User {user_email} and all their profile data have been deleted successfully'
1442
+ })
1443
+
1444
+ except Exception as e:
1445
+ db.session.rollback()
1446
+ app.logger.error(f"Error deleting user: {str(e)}")
1447
+ return jsonify({
1448
+ 'success': False,
1449
+ 'error': str(e)
1450
+ }), 500
1451
+
1452
+ def create_tables_if_needed():
1453
+ """Create database tables if they don't exist"""
1454
+ with app.app_context():
1455
+ # Check if tables exist
1456
+ inspector = db.inspect(db.engine)
1457
+ table_names = inspector.get_table_names()
1458
+
1459
+ if not table_names or 'users' not in table_names:
1460
+ print("Creating database tables...")
1461
+ db.create_all()
1462
+ print("Database tables created successfully!")
1463
+
1464
+ # Run admin migration if needed
1465
+ try:
1466
+ from sqlalchemy import text
1467
+ columns = [column['name'] for column in inspector.get_columns('users')]
1468
+ if 'is_admin' not in columns:
1469
+ print("Running admin migration...")
1470
+ db.session.execute(text('ALTER TABLE users ADD COLUMN is_admin BOOLEAN DEFAULT FALSE'))
1471
+ db.session.execute(text('ALTER TABLE users ADD COLUMN role VARCHAR(50) DEFAULT \'User\''))
1472
+ db.session.commit()
1473
+ print("Admin migration completed!")
1474
+ except Exception as e:
1475
+ print(f"Migration error: {e}")
1476
+
1477
+ # Initialize database
1478
+ create_tables_if_needed()
1479
+
1480
+ if __name__ == "__main__":
1481
+ port = int(os.environ.get('PORT', 5000))
1482
+ # Railway requires binding to 0.0.0.0
1483
+ # Check if we're running on Railway
1484
+ is_railway = os.environ.get('RAILWAY_ENVIRONMENT') or os.environ.get('RAILWAY_ENVIRONMENT_NAME')
1485
+ host = '0.0.0.0' if is_railway else '127.0.0.1'
1486
+ print(f"Flask running at http://{host}:{port}")
1487
+ # Disable debug mode in production
1488
+ debug = not is_railway
1489
+ app.run(host=host, port=port, debug=debug)
check_profiles.py ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Check all users and their profiles.
4
+ """
5
+
6
+ from models import db, User, Introduction, WorkExperience, Project, Education, Skill, Achievement
7
+ from app import app
8
+
9
+ def check_all_profiles():
10
+ """Check all users and their profile data."""
11
+
12
+ with app.app_context():
13
+ users = User.query.all()
14
+ print(f"Total users: {len(users)}")
15
+
16
+ for user in users:
17
+ print(f"\nUser: {user.email} (ID: {user.id})")
18
+
19
+ # Check introduction
20
+ intro = Introduction.query.filter_by(user_id=user.id).first()
21
+ print(f" Has introduction: {'Yes' if intro else 'No'}")
22
+ if intro:
23
+ print(f" Name: {intro.name}")
24
+
25
+ # Check profile summary
26
+ from models import ProfileSummary
27
+ summary = ProfileSummary.query.filter_by(user_id=user.id).first()
28
+ print(f" Has profile summary: {'Yes' if summary else 'No'}")
29
+
30
+ # Check other sections
31
+ work_count = WorkExperience.query.filter_by(user_id=user.id).count()
32
+ project_count = Project.query.filter_by(user_id=user.id).count()
33
+ education_count = Education.query.filter_by(user_id=user.id).count()
34
+ skill_count = Skill.query.filter_by(user_id=user.id).count()
35
+ achievement_count = Achievement.query.filter_by(user_id=user.id).count()
36
+
37
+ print(f" Work experiences: {work_count}")
38
+ print(f" Projects: {project_count}")
39
+ print(f" Education: {education_count}")
40
+ print(f" Skills: {skill_count}")
41
+ print(f" Achievements: {achievement_count}")
42
+
43
+ if __name__ == "__main__":
44
+ check_all_profiles()
cleanup_duplicates.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from app import app, db, Introduction, ProfileSummary
2
+ from sqlalchemy import func
3
+
4
+ def cleanup_duplicate_records():
5
+ """Clean up duplicate introduction and profile summary records"""
6
+ with app.app_context():
7
+ # Find duplicate introductions
8
+ dup_intros = db.session.query(
9
+ Introduction.user_id,
10
+ func.count(Introduction.id).label('count')
11
+ ).group_by(Introduction.user_id).having(func.count(Introduction.id) > 1).all()
12
+
13
+ print(f"Found {len(dup_intros)} users with duplicate introductions")
14
+
15
+ # For each user with duplicates, keep the most recent one
16
+ for user_id, count in dup_intros:
17
+ # Get all introductions for this user, ordered by creation date
18
+ intros = Introduction.query.filter_by(user_id=user_id).order_by(Introduction.created_at.desc()).all()
19
+
20
+ # Keep the first (most recent), delete the rest
21
+ for intro in intros[1:]:
22
+ print(f"Deleting duplicate introduction for user {user_id}: {intro.id}")
23
+ db.session.delete(intro)
24
+
25
+ # Find duplicate profile summaries
26
+ dup_summaries = db.session.query(
27
+ ProfileSummary.user_id,
28
+ func.count(ProfileSummary.id).label('count')
29
+ ).group_by(ProfileSummary.user_id).having(func.count(ProfileSummary.id) > 1).all()
30
+
31
+ print(f"Found {len(dup_summaries)} users with duplicate profile summaries")
32
+
33
+ # For each user with duplicates, keep the most recent one
34
+ for user_id, count in dup_summaries:
35
+ # Get all profile summaries for this user, ordered by creation date
36
+ summaries = ProfileSummary.query.filter_by(user_id=user_id).order_by(ProfileSummary.created_at.desc()).all()
37
+
38
+ # Keep the first (most recent), delete the rest
39
+ for summary in summaries[1:]:
40
+ print(f"Deleting duplicate profile summary for user {user_id}: {summary.id}")
41
+ db.session.delete(summary)
42
+
43
+ # Commit the changes
44
+ db.session.commit()
45
+ print("Cleanup completed!")
46
+
47
+ if __name__ == "__main__":
48
+ cleanup_duplicate_records()
create_tables.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Database setup script to create tables for the auth system
3
+ """
4
+ import os
5
+ from dotenv import load_dotenv
6
+ from flask import Flask
7
+ from models import db, User
8
+
9
+ def create_app():
10
+ """Create Flask app for database operations"""
11
+ app = Flask(__name__)
12
+
13
+ # Configure Flask
14
+ app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY')
15
+ app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('SQLALCHEMY_DATABASE_URI')
16
+ app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
17
+
18
+ # Initialize database
19
+ db.init_app(app)
20
+
21
+ return app
22
+
23
+ def create_tables():
24
+ """Create database tables"""
25
+ print("Creating database tables...")
26
+
27
+ app = create_app()
28
+
29
+ with app.app_context():
30
+ try:
31
+ # Create all tables
32
+ db.create_all()
33
+ print("Database tables created successfully!")
34
+
35
+ # Check if tables exist
36
+ inspector = db.inspect(db.engine)
37
+ tables = inspector.get_table_names()
38
+ print(f"Tables created: {tables}")
39
+
40
+ except Exception as e:
41
+ print(f"Error creating tables: {e}")
42
+ raise
43
+
44
+ if __name__ == "__main__":
45
+ # Load environment variables
46
+ load_dotenv()
47
+
48
+ # Verify database URL
49
+ db_url = os.environ.get('SQLALCHEMY_DATABASE_URI')
50
+ if not db_url:
51
+ print("DATABASE_URL not found in environment variables")
52
+ exit(1)
53
+
54
+ print(f"Using database: {db_url.split('@')[1] if '@' in db_url else 'local'}")
55
+
56
+ # Create tables
57
+ create_tables()
debug_profile.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Debug script to check database state and test PDF generation.
4
+ """
5
+
6
+ from models import db, User, Introduction, WorkExperience, Project, Education, Skill, Achievement, ProfileSectionOrder
7
+ from app import app
8
+ import calendar
9
+
10
+ def debug_user_profile():
11
+ """Debug user profile data."""
12
+
13
+ with app.app_context():
14
+ # Get first user (for testing)
15
+ user = User.query.first()
16
+ if not user:
17
+ print("No users found in database")
18
+ return
19
+
20
+ print(f"Debugging profile for user: {user.email}")
21
+
22
+ # Get profile data
23
+ intro = Introduction.query.filter_by(user_id=user.id).first()
24
+ if not intro:
25
+ print("No introduction found")
26
+ return
27
+
28
+ print(f"Introduction: {intro.name}")
29
+
30
+ # Get work experiences
31
+ work_experiences = WorkExperience.query.filter_by(user_id=user.id).all()
32
+ print(f"Work experiences: {len(work_experiences)}")
33
+ for exp in work_experiences:
34
+ print(f" - {exp.title} at {exp.organization} ({exp.start_month}/{exp.start_year} - {exp.end_month or 'Present'}/{exp.end_year or ''})")
35
+
36
+ # Get projects
37
+ projects = Project.query.filter_by(user_id=user.id).all()
38
+ print(f"Projects: {len(projects)}")
39
+
40
+ # Get education
41
+ educations = Education.query.filter_by(user_id=user.id).all()
42
+ print(f"Education: {len(educations)}")
43
+
44
+ # Get skills
45
+ skills = Skill.query.filter_by(user_id=user.id).all()
46
+ print(f"Skills: {len(skills)}")
47
+ for skill in skills:
48
+ print(f" - {skill.skill}")
49
+
50
+ # Get achievements
51
+ achievements = Achievement.query.filter_by(user_id=user.id).all()
52
+ print(f"Achievements: {len(achievements)}")
53
+ for achievement in achievements:
54
+ print(f" - {achievement.achievement}")
55
+
56
+ # Test data preparation (same as in app.py)
57
+ def format_date(month, year):
58
+ """Format month and year as 'Month Year'"""
59
+ if month and year:
60
+ try:
61
+ month_name = calendar.month_name[int(month)]
62
+ return f"{month_name[:3]} {year}"
63
+ except:
64
+ return f"{month}/{year}"
65
+ return ""
66
+
67
+ # Prepare work experiences
68
+ work_exp_list = []
69
+ for exp in work_experiences:
70
+ start_date = format_date(exp.start_month, exp.start_year)
71
+ end_date = "Present" if not exp.end_month or not exp.end_year else format_date(exp.end_month, exp.end_year)
72
+
73
+ work_exp_list.append({
74
+ 'title': exp.title,
75
+ 'organization': exp.organization,
76
+ 'start_date': start_date,
77
+ 'end_date': end_date,
78
+ 'remarks': exp.remarks or ''
79
+ })
80
+ print(f"Formatted work exp: {work_exp_list[-1]}")
81
+
82
+ if __name__ == "__main__":
83
+ debug_user_profile()
debug_user_delete.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Debug script to check user and their relationships
4
+ """
5
+ import os
6
+ import sys
7
+ from dotenv import load_dotenv
8
+
9
+ # Load environment variables
10
+ load_dotenv()
11
+
12
+ # Add current directory to Python path
13
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
14
+
15
+ from flask import Flask
16
+ from models import db, User, Introduction, ProfileSummary, WorkExperience, Project, Education, Skill, Achievement, ProfileSectionOrder
17
+
18
+ def debug_user_relationships(user_email):
19
+ """Debug user relationships"""
20
+ app = Flask(__name__)
21
+ app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'dev-secret-key-change-in-production')
22
+ app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('SQLALCHEMY_DATABASE_URI')
23
+ app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
24
+
25
+ db.init_app(app)
26
+
27
+ with app.app_context():
28
+ user = User.query.filter_by(email=user_email).first()
29
+ if not user:
30
+ print(f"User {user_email} not found")
31
+ return
32
+
33
+ print(f"User: {user.email} (ID: {user.id})")
34
+ print(f"Is admin: {user.is_admin}")
35
+
36
+ # Check relationships
37
+ print("\nProfile data:")
38
+ print(f"Introduction: {user.introduction}")
39
+ print(f"Profile Summary: {user.profile_summary}")
40
+ print(f"Work experiences: {len(user.work_experiences)}")
41
+ print(f"Projects: {len(user.projects)}")
42
+ print(f"Education: {len(user.educations)}")
43
+ print(f"Skills: {len(user.skills)}")
44
+ print(f"Achievements: {len(user.achievements)}")
45
+ print(f"Section order: {user.section_order}")
46
+
47
+ # Check direct queries
48
+ print("\nDirect database queries:")
49
+ intro = Introduction.query.filter_by(user_id=user.id).first()
50
+ print(f"Direct intro query: {intro}")
51
+
52
+ # Check if user has any introductions
53
+ from sqlalchemy import text
54
+ result = db.session.execute(text("SELECT COUNT(*) FROM introductions WHERE user_id = :user_id"), {'user_id': str(user.id)})
55
+ count = result.scalar()
56
+ print(f"Introduction count from direct SQL: {count}")
57
+
58
+ if __name__ == '__main__':
59
+ debug_user_relationships('test1@example.com')
migrate_admin_columns.py ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Migration script to add admin columns to users table
4
+ Run this script to update the database schema for admin functionality
5
+ """
6
+
7
+ import os
8
+ import sys
9
+ from dotenv import load_dotenv
10
+
11
+ # Load environment variables
12
+ load_dotenv()
13
+
14
+ # Add current directory to Python path
15
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
16
+
17
+ from flask import Flask
18
+ from flask_sqlalchemy import SQLAlchemy
19
+ from models import db, User
20
+ import uuid
21
+ from sqlalchemy import text
22
+
23
+ def migrate_database():
24
+ """Add is_admin and role columns to users table"""
25
+
26
+ app = Flask(__name__)
27
+ app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'dev-secret-key-change-in-production')
28
+ app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('SQLALCHEMY_DATABASE_URI')
29
+ app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
30
+
31
+ db.init_app(app)
32
+
33
+ with app.app_context():
34
+ # Check if columns already exist
35
+ inspector = db.inspect(db.engine)
36
+ columns = [column['name'] for column in inspector.get_columns('users')]
37
+
38
+ if 'is_admin' not in columns:
39
+ print("Adding is_admin column...")
40
+ db.session.execute(text('ALTER TABLE users ADD COLUMN is_admin BOOLEAN DEFAULT FALSE'))
41
+
42
+ if 'role' not in columns:
43
+ print("Adding role column...")
44
+ db.session.execute(text('ALTER TABLE users ADD COLUMN role VARCHAR(50) DEFAULT \'User\''))
45
+
46
+ db.session.commit()
47
+ print("Migration completed successfully!")
48
+
49
+ # Update admin user if exists
50
+ admin_email = os.environ.get('ADMIN_EMAIL')
51
+ if admin_email:
52
+ admin_user = User.query.filter_by(email=admin_email).first()
53
+ if admin_user:
54
+ admin_user.is_admin = True
55
+ admin_user.role = 'Admin'
56
+ db.session.commit()
57
+ print(f"Updated {admin_email} as admin user")
58
+ else:
59
+ print(f"Admin user {admin_email} not found in database")
60
+
61
+ if __name__ == '__main__':
62
+ migrate_database()
models.py ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask_sqlalchemy import SQLAlchemy
2
+ from flask_login import UserMixin
3
+ from sqlalchemy.dialects.postgresql import UUID
4
+ from sqlalchemy.orm import relationship
5
+ import uuid
6
+ from datetime import datetime
7
+ from werkzeug.security import generate_password_hash, check_password_hash
8
+
9
+ db = SQLAlchemy()
10
+
11
+ class User(UserMixin, db.Model):
12
+ __tablename__ = 'users'
13
+
14
+ id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
15
+ email = db.Column(db.String(255), unique=True, nullable=False)
16
+ password_hash = db.Column(db.String(255), nullable=False)
17
+ name = db.Column(db.String(255), nullable=False)
18
+ created_at = db.Column(db.DateTime, default=datetime.utcnow)
19
+ is_admin = db.Column(db.Boolean, default=False, nullable=False)
20
+ role = db.Column(db.String(50), default='User', nullable=False)
21
+
22
+ # Profile sections
23
+ introduction = relationship("Introduction", back_populates="user", uselist=False, cascade="all, delete-orphan")
24
+ profile_summary = relationship("ProfileSummary", back_populates="user", uselist=False, cascade="all, delete-orphan")
25
+ work_experiences = relationship("WorkExperience", back_populates="user", cascade="all, delete-orphan")
26
+ projects = relationship("Project", back_populates="user", cascade="all, delete-orphan")
27
+ educations = relationship("Education", back_populates="user", cascade="all, delete-orphan")
28
+ skills = relationship("Skill", back_populates="user", cascade="all, delete-orphan")
29
+ achievements = relationship("Achievement", back_populates="user", cascade="all, delete-orphan")
30
+ section_order = relationship("ProfileSectionOrder", back_populates="user", uselist=False, cascade="all, delete-orphan")
31
+
32
+ def set_password(self, password):
33
+ self.password_hash = generate_password_hash(password)
34
+
35
+ def check_password(self, password):
36
+ # Check if it's a bcrypt hash (starts with $2a$, $2b$, etc.)
37
+ if self.password_hash.startswith('$2'):
38
+ import bcrypt
39
+ return bcrypt.checkpw(password.encode('utf-8'), self.password_hash.encode('utf-8'))
40
+ else:
41
+ # Use Werkzeug's check_password_hash for other hash types
42
+ return check_password_hash(self.password_hash, password)
43
+
44
+ def get_id(self):
45
+ return str(self.id)
46
+
47
+ def __repr__(self):
48
+ return f'<User {self.email}>'
49
+
50
+ class Introduction(db.Model):
51
+ __tablename__ = 'introductions'
52
+
53
+ id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
54
+ user_id = db.Column(UUID(as_uuid=True), db.ForeignKey('users.id'), nullable=False)
55
+ name = db.Column(db.String(255), nullable=False)
56
+ email = db.Column(db.String(255), nullable=False)
57
+ phone = db.Column(db.String(50), nullable=False)
58
+ linkedin = db.Column(db.String(255))
59
+ github = db.Column(db.String(255))
60
+ website = db.Column(db.String(255))
61
+ created_at = db.Column(db.DateTime, default=datetime.utcnow)
62
+ updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
63
+
64
+ user = relationship("User", back_populates="introduction")
65
+
66
+ class ProfileSummary(db.Model):
67
+ __tablename__ = 'profile_summaries'
68
+
69
+ id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
70
+ user_id = db.Column(UUID(as_uuid=True), db.ForeignKey('users.id'), nullable=False)
71
+ summary = db.Column(db.Text)
72
+ ai_generated = db.Column(db.Boolean, default=False)
73
+ created_at = db.Column(db.DateTime, default=datetime.utcnow)
74
+ updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
75
+
76
+ user = relationship("User", back_populates="profile_summary")
77
+
78
+ class WorkExperience(db.Model):
79
+ __tablename__ = 'work_experiences'
80
+
81
+ id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
82
+ user_id = db.Column(UUID(as_uuid=True), db.ForeignKey('users.id'), nullable=False)
83
+ organization = db.Column(db.String(255), nullable=False)
84
+ title = db.Column(db.String(255), nullable=False)
85
+ start_month = db.Column(db.Integer, nullable=False) # 1-12
86
+ start_year = db.Column(db.Integer, nullable=False)
87
+ end_month = db.Column(db.Integer) # NULL if present
88
+ end_year = db.Column(db.Integer) # NULL if present
89
+ remarks = db.Column(db.Text)
90
+ order = db.Column(db.Integer, default=0)
91
+ created_at = db.Column(db.DateTime, default=datetime.utcnow)
92
+ updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
93
+
94
+ user = relationship("User", back_populates="work_experiences")
95
+
96
+ class Project(db.Model):
97
+ __tablename__ = 'projects'
98
+
99
+ id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
100
+ user_id = db.Column(UUID(as_uuid=True), db.ForeignKey('users.id'), nullable=False)
101
+ organization = db.Column(db.String(255))
102
+ title = db.Column(db.String(255), nullable=False)
103
+ start_month = db.Column(db.Integer, nullable=False) # 1-12
104
+ start_year = db.Column(db.Integer, nullable=False)
105
+ end_month = db.Column(db.Integer) # NULL if present
106
+ end_year = db.Column(db.Integer) # NULL if present
107
+ remarks = db.Column(db.Text)
108
+ order = db.Column(db.Integer, default=0)
109
+ created_at = db.Column(db.DateTime, default=datetime.utcnow)
110
+ updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
111
+
112
+ user = relationship("User", back_populates="projects")
113
+
114
+ class Education(db.Model):
115
+ __tablename__ = 'educations'
116
+
117
+ id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
118
+ user_id = db.Column(UUID(as_uuid=True), db.ForeignKey('users.id'), nullable=False)
119
+ organization = db.Column(db.String(255), nullable=False)
120
+ title = db.Column(db.String(255), nullable=False)
121
+ start_month = db.Column(db.Integer, nullable=False) # 1-12
122
+ start_year = db.Column(db.Integer, nullable=False)
123
+ end_month = db.Column(db.Integer) # NULL if present
124
+ end_year = db.Column(db.Integer) # NULL if present
125
+ remarks = db.Column(db.Text)
126
+ order = db.Column(db.Integer, default=0)
127
+ created_at = db.Column(db.DateTime, default=datetime.utcnow)
128
+ updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
129
+
130
+ user = relationship("User", back_populates="educations")
131
+
132
+ class Skill(db.Model):
133
+ __tablename__ = 'skills'
134
+
135
+ id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
136
+ user_id = db.Column(UUID(as_uuid=True), db.ForeignKey('users.id'), nullable=False)
137
+ skill = db.Column(db.String(255), nullable=False)
138
+ order = db.Column(db.Integer, default=0)
139
+ created_at = db.Column(db.DateTime, default=datetime.utcnow)
140
+
141
+ user = relationship("User", back_populates="skills")
142
+
143
+ class Achievement(db.Model):
144
+ __tablename__ = 'achievements'
145
+
146
+ id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
147
+ user_id = db.Column(UUID(as_uuid=True), db.ForeignKey('users.id'), nullable=False)
148
+ achievement = db.Column(db.String(255), nullable=False)
149
+ order = db.Column(db.Integer, default=0)
150
+ created_at = db.Column(db.DateTime, default=datetime.utcnow)
151
+
152
+ user = relationship("User", back_populates="achievements")
153
+
154
+ class ProfileSectionOrder(db.Model):
155
+ __tablename__ = 'profile_section_orders'
156
+
157
+ id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
158
+ user_id = db.Column(UUID(as_uuid=True), db.ForeignKey('users.id'), nullable=False, unique=True)
159
+ section_order = db.Column(db.JSON, nullable=False) # Stores order as JSON array
160
+ created_at = db.Column(db.DateTime, default=datetime.utcnow)
161
+ updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
162
+
163
+ user = relationship("User", back_populates="section_order")
pdf_generator.py ADDED
@@ -0,0 +1,358 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PDF generation using ReportLab.
3
+ This module provides functions to create PDF documents directly.
4
+ """
5
+
6
+ import os
7
+ from io import BytesIO
8
+ from reportlab.lib.pagesizes import letter, A4
9
+ from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
10
+ from reportlab.lib.units import inch, cm
11
+ from reportlab.lib.colors import black, grey
12
+ from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
13
+ from reportlab.platypus.flowables import PageBreak
14
+ from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_RIGHT
15
+ from utils import log_error
16
+
17
+
18
+ def create_pdf_resume(data, template_type="standard"):
19
+ """
20
+ Create a PDF resume using ReportLab.
21
+
22
+ Args:
23
+ data (dict): Resume data
24
+ template_type (str): 'standard' or 'modern'
25
+
26
+ Returns:
27
+ bytes: PDF bytes
28
+ """
29
+ try:
30
+ buffer = BytesIO()
31
+
32
+ if template_type == "modern":
33
+ doc = SimpleDocTemplate(buffer, pagesize=A4,
34
+ leftMargin=0.5*inch, rightMargin=0.5*inch,
35
+ topMargin=0.5*inch, bottomMargin=0.5*inch)
36
+ elements = _create_modern_resume_elements(data)
37
+ else:
38
+ doc = SimpleDocTemplate(buffer, pagesize=A4,
39
+ leftMargin=0.75*inch, rightMargin=0.75*inch,
40
+ topMargin=0.75*inch, bottomMargin=0.75*inch)
41
+ elements = _create_standard_resume_elements(data)
42
+
43
+ doc.build(elements)
44
+ pdf_bytes = buffer.getvalue()
45
+ buffer.close()
46
+
47
+ return pdf_bytes
48
+
49
+ except Exception as e:
50
+ log_error(f"PDF creation failed: {str(e)}", e)
51
+ return None
52
+
53
+
54
+ def _create_standard_resume_elements(data):
55
+ """Create elements for standard resume template."""
56
+ styles = getSampleStyleSheet()
57
+ elements = []
58
+
59
+ # Custom styles
60
+ styles.add(ParagraphStyle(
61
+ name='Name',
62
+ parent=styles['Heading1'],
63
+ fontSize=24,
64
+ spaceAfter=12,
65
+ alignment=TA_CENTER
66
+ ))
67
+
68
+ styles.add(ParagraphStyle(
69
+ name='Contact',
70
+ parent=styles['Normal'],
71
+ fontSize=10,
72
+ spaceAfter=30,
73
+ alignment=TA_CENTER
74
+ ))
75
+
76
+ styles.add(ParagraphStyle(
77
+ name='SectionTitle',
78
+ parent=styles['Heading2'],
79
+ fontSize=14,
80
+ spaceBefore=20,
81
+ spaceAfter=10,
82
+ borderWidth=1,
83
+ borderColor=grey,
84
+ borderPadding=5
85
+ ))
86
+
87
+ styles.add(ParagraphStyle(
88
+ name='JobTitle',
89
+ parent=styles['Heading3'],
90
+ fontSize=12,
91
+ spaceAfter=2
92
+ ))
93
+
94
+ styles.add(ParagraphStyle(
95
+ name='Company',
96
+ parent=styles['Normal'],
97
+ fontSize=11,
98
+ textColor=grey,
99
+ spaceAfter=5
100
+ ))
101
+
102
+ styles.add(ParagraphStyle(
103
+ name='Date',
104
+ parent=styles['Normal'],
105
+ fontSize=10,
106
+ textColor=grey,
107
+ alignment=TA_RIGHT
108
+ ))
109
+
110
+ # Name
111
+ elements.append(Paragraph(data.get('name', ''), styles['Name']))
112
+
113
+ # Contact info
114
+ contact_info = []
115
+ if data.get('email'):
116
+ contact_info.append(f"Email: {data['email']}")
117
+ if data.get('phone'):
118
+ contact_info.append(f"Phone: {data['phone']}")
119
+ if data.get('linkedin'):
120
+ contact_info.append(f"LinkedIn: {data['linkedin']}")
121
+ if data.get('github'):
122
+ contact_info.append(f"GitHub: {data['github']}")
123
+
124
+ if contact_info:
125
+ elements.append(Paragraph(" | ".join(contact_info), styles['Contact']))
126
+
127
+ # Profile Summary
128
+ if data.get('summary'):
129
+ elements.append(Paragraph("Profile Summary", styles['SectionTitle']))
130
+ elements.append(Paragraph(data['summary'], styles['Normal']))
131
+
132
+ # Add sections based on order
133
+ sections_order = data.get('sections_order', [])
134
+
135
+ for section in sections_order:
136
+ if section == 'work_experience' and data.get('work_experience'):
137
+ elements.append(Paragraph("Work Experience", styles['SectionTitle']))
138
+ for exp in data['work_experience']:
139
+ elements.append(Paragraph(exp.get('title', ''), styles['JobTitle']))
140
+ elements.append(Paragraph(exp.get('organization', ''), styles['Company']))
141
+
142
+ # Create table for date and remarks
143
+ date_data = [[
144
+ Paragraph(f"{exp.get('start_date', '')} - {exp.get('end_date', 'Present')}", styles['Date']),
145
+ Paragraph(exp.get('remarks', ''), styles['Normal'])
146
+ ]]
147
+ date_table = Table(date_data, colWidths=[2*inch, 4*inch])
148
+ date_table.setStyle(TableStyle([
149
+ ('VALIGN', (0, 0), (-1, -1), 'TOP'),
150
+ ]))
151
+ elements.append(date_table)
152
+ elements.append(Spacer(1, 10))
153
+
154
+ elif section == 'projects' and data.get('projects'):
155
+ elements.append(Paragraph("Projects", styles['SectionTitle']))
156
+ for proj in data['projects']:
157
+ elements.append(Paragraph(proj.get('title', ''), styles['JobTitle']))
158
+ elements.append(Paragraph(proj.get('organization', ''), styles['Company']))
159
+
160
+ date_data = [[
161
+ Paragraph(f"{proj.get('start_date', '')} - {proj.get('end_date', 'Present')}", styles['Date']),
162
+ Paragraph(proj.get('remarks', ''), styles['Normal'])
163
+ ]]
164
+ date_table = Table(date_data, colWidths=[2*inch, 4*inch])
165
+ date_table.setStyle(TableStyle([
166
+ ('VALIGN', (0, 0), (-1, -1), 'TOP'),
167
+ ]))
168
+ elements.append(date_table)
169
+ elements.append(Spacer(1, 10))
170
+
171
+ elif section == 'education' and data.get('education'):
172
+ elements.append(Paragraph("Education", styles['SectionTitle']))
173
+ for edu in data['education']:
174
+ elements.append(Paragraph(edu.get('title', ''), styles['JobTitle']))
175
+ elements.append(Paragraph(edu.get('organization', ''), styles['Company']))
176
+
177
+ date_data = [[
178
+ Paragraph(f"{edu.get('start_date', '')} - {edu.get('end_date', 'Present')}", styles['Date']),
179
+ Paragraph(edu.get('remarks', ''), styles['Normal'])
180
+ ]]
181
+ date_table = Table(date_data, colWidths=[2*inch, 4*inch])
182
+ date_table.setStyle(TableStyle([
183
+ ('VALIGN', (0, 0), (-1, -1), 'TOP'),
184
+ ]))
185
+ elements.append(date_table)
186
+ elements.append(Spacer(1, 10))
187
+
188
+ elif section == 'skills' and data.get('skills'):
189
+ elements.append(Paragraph("Skills", styles['SectionTitle']))
190
+ skills_list = [skill.strip() for skill in data['skills'].split(',')]
191
+ elements.append(Paragraph(", ".join(skills_list), styles['Normal']))
192
+
193
+ elif section == 'achievements' and data.get('achievements'):
194
+ elements.append(Paragraph("Achievements", styles['SectionTitle']))
195
+ achievements_list = [achievement.strip() for achievement in data['achievements'].split(',')]
196
+ elements.append(Paragraph(", ".join(achievements_list), styles['Normal']))
197
+
198
+ return elements
199
+
200
+
201
+ def _create_modern_resume_elements(data):
202
+ """Create elements for modern resume template (two-column)."""
203
+ styles = getSampleStyleSheet()
204
+ elements = []
205
+
206
+ # Custom styles
207
+ styles.add(ParagraphStyle(
208
+ name='Name',
209
+ parent=styles['Heading1'],
210
+ fontSize=20,
211
+ spaceAfter=12,
212
+ alignment=TA_CENTER
213
+ ))
214
+
215
+ styles.add(ParagraphStyle(
216
+ name='Contact',
217
+ parent=styles['Normal'],
218
+ fontSize=9,
219
+ spaceAfter=20,
220
+ alignment=TA_CENTER
221
+ ))
222
+
223
+ styles.add(ParagraphStyle(
224
+ name='SectionTitle',
225
+ parent=styles['Heading2'],
226
+ fontSize=12,
227
+ spaceBefore=15,
228
+ spaceAfter=8,
229
+ textColor=black
230
+ ))
231
+
232
+ styles.add(ParagraphStyle(
233
+ name='JobTitle',
234
+ parent=styles['Heading3'],
235
+ fontSize=11,
236
+ spaceAfter=2
237
+ ))
238
+
239
+ styles.add(ParagraphStyle(
240
+ name='Company',
241
+ parent=styles['Normal'],
242
+ fontSize=10,
243
+ textColor=grey,
244
+ spaceAfter=5
245
+ ))
246
+
247
+ styles.add(ParagraphStyle(
248
+ name='Date',
249
+ parent=styles['Normal'],
250
+ fontSize=9,
251
+ textColor=grey,
252
+ alignment=TA_RIGHT
253
+ ))
254
+
255
+ # Header (full width)
256
+ elements.append(Paragraph(data.get('name', ''), styles['Name']))
257
+
258
+ # Contact info
259
+ contact_info = []
260
+ if data.get('email'):
261
+ contact_info.append(f"Email: {data['email']}")
262
+ if data.get('phone'):
263
+ contact_info.append(f"Phone: {data['phone']}")
264
+ if data.get('linkedin'):
265
+ contact_info.append(f"LinkedIn: {data['linkedin']}")
266
+ if data.get('github'):
267
+ contact_info.append(f"GitHub: {data['github']}")
268
+
269
+ if contact_info:
270
+ elements.append(Paragraph(" | ".join(contact_info), styles['Contact']))
271
+
272
+ # Two-column layout
273
+ left_col_width = 2.5 * inch
274
+ right_col_width = 4.5 * inch
275
+
276
+ # Main content (right column)
277
+ main_elements = []
278
+
279
+ # Profile Summary
280
+ if data.get('summary'):
281
+ main_elements.append(Paragraph("Profile Summary", styles['SectionTitle']))
282
+ main_elements.append(Paragraph(data['summary'], styles['Normal']))
283
+
284
+ # Add sections to main content
285
+ sections_order = data.get('sections_order', [])
286
+
287
+ for section in sections_order:
288
+ if section == 'work_experience' and data.get('work_experience'):
289
+ main_elements.append(Paragraph("Work Experience", styles['SectionTitle']))
290
+ for exp in data['work_experience']:
291
+ main_elements.append(Paragraph(exp.get('title', ''), styles['JobTitle']))
292
+ main_elements.append(Paragraph(exp.get('organization', ''), styles['Company']))
293
+ main_elements.append(Paragraph(
294
+ f"{exp.get('start_date', '')} - {exp.get('end_date', 'Present')}",
295
+ styles['Date']
296
+ ))
297
+ main_elements.append(Paragraph(exp.get('remarks', ''), styles['Normal']))
298
+ main_elements.append(Spacer(1, 10))
299
+
300
+ elif section == 'projects' and data.get('projects'):
301
+ main_elements.append(Paragraph("Projects", styles['SectionTitle']))
302
+ for proj in data['projects']:
303
+ main_elements.append(Paragraph(proj.get('title', ''), styles['JobTitle']))
304
+ main_elements.append(Paragraph(proj.get('organization', ''), styles['Company']))
305
+ main_elements.append(Paragraph(
306
+ f"{proj.get('start_date', '')} - {proj.get('end_date', 'Present')}",
307
+ styles['Date']
308
+ ))
309
+ main_elements.append(Paragraph(proj.get('remarks', ''), styles['Normal']))
310
+ main_elements.append(Spacer(1, 10))
311
+
312
+ elif section == 'education' and data.get('education'):
313
+ main_elements.append(Paragraph("Education", styles['SectionTitle']))
314
+ for edu in data['education']:
315
+ main_elements.append(Paragraph(edu.get('title', ''), styles['JobTitle']))
316
+ main_elements.append(Paragraph(edu.get('organization', ''), styles['Company']))
317
+ main_elements.append(Paragraph(
318
+ f"{edu.get('start_date', '')} - {edu.get('end_date', 'Present')}",
319
+ styles['Date']
320
+ ))
321
+ main_elements.append(Paragraph(edu.get('remarks', ''), styles['Normal']))
322
+ main_elements.append(Spacer(1, 10))
323
+
324
+ # Sidebar (left column)
325
+ sidebar_elements = []
326
+
327
+ # Skills
328
+ if data.get('skills'):
329
+ sidebar_elements.append(Paragraph("Skills", styles['SectionTitle']))
330
+ skills_list = [skill.strip() for skill in data['skills'].split(',')]
331
+ for skill in skills_list:
332
+ sidebar_elements.append(Paragraph(f"• {skill}", styles['Normal']))
333
+ sidebar_elements.append(Spacer(1, 10))
334
+
335
+ # Achievements
336
+ if data.get('achievements'):
337
+ sidebar_elements.append(Paragraph("Achievements", styles['SectionTitle']))
338
+ achievements_list = [achievement.strip() for achievement in data['achievements'].split(',')]
339
+ for achievement in achievements_list:
340
+ sidebar_elements.append(Paragraph(f"• {achievement}", styles['Normal']))
341
+
342
+ # Create two-column table
343
+ all_elements = []
344
+ max_len = max(len(main_elements), len(sidebar_elements))
345
+
346
+ for i in range(max_len):
347
+ left = sidebar_elements[i] if i < len(sidebar_elements) else Spacer(1, 1)
348
+ right = main_elements[i] if i < len(main_elements) else Spacer(1, 1)
349
+ all_elements.append([left, right])
350
+
351
+ if all_elements:
352
+ col_table = Table(all_elements, colWidths=[left_col_width, right_col_width])
353
+ col_table.setStyle(TableStyle([
354
+ ('VALIGN', (0, 0), (-1, -1), 'TOP'),
355
+ ]))
356
+ elements.append(col_table)
357
+
358
+ return elements
requirements.txt ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # backend/requirements.txt
2
+ # Core web framework
3
+ Flask==3.1.2
4
+
5
+ # Auth + JWT
6
+ Flask-JWT-Extended==4.7.1
7
+ Flask-Login==0.6.3
8
+
9
+ # Redis client
10
+ redis==6.4.0
11
+
12
+ # Environment variable loader
13
+ python-dotenv==1.1.1
14
+
15
+ # DB (Postgres driver) + ORM & migrations (if you use Postgres)
16
+ psycopg2-binary==2.9.10
17
+ SQLAlchemy==2.0.43
18
+ alembic==1.16.5
19
+
20
+ # Password hashing
21
+ passlib==1.7.4
22
+ bcrypt==4.3.0
23
+
24
+ # PDF generation
25
+ # reportlab==4.0.4
26
+
27
+ # Word (DOCX) generation
28
+ python-docx==1.2.0
29
+
30
+ # Asynchronous file helpers (optional, useful for file writes)
31
+ aiofiles==24.1.0
32
+
33
+ # OpenAI for profile summarization
34
+ openai==1.107.1
35
+
36
+ # Optional: utilities and production server
37
+ python-multipart==0.0.20 # if you accept multipart uploads
38
+ gunicorn==23.0.0
39
+
40
+ # Dev / testing tools
41
+ pytest==8.4.2
42
+ pytest-cov==7.0.0
43
+
44
+ # HTTP clients (if needed)
45
+ httpx==0.28.1
46
+
47
+ # Optional helpers
48
+ boto3==1.40.28 # only if you later switch to S3 (currently not required)
49
+
50
+ flask-cors==6.0.1
51
+ requests==2.32.5
52
+
53
+ email-validator==2.3.0
54
+ Flask-Login==0.6.3
55
+ requests-oauthlib==2.0.0
56
+ Flask-SQLAlchemy==3.1.1
57
+ reportlab==4.4.3
templates/admin.html ADDED
@@ -0,0 +1,242 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Admin Panel - AI Resume Builder</title>
7
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
8
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
9
+ <style>
10
+ .navbar-brand {
11
+ font-weight: bold;
12
+ }
13
+ .table-actions {
14
+ white-space: nowrap;
15
+ }
16
+ .user-count {
17
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
18
+ color: white;
19
+ padding: 20px;
20
+ border-radius: 10px;
21
+ margin-bottom: 30px;
22
+ }
23
+ .delete-btn {
24
+ transition: all 0.3s ease;
25
+ }
26
+ .delete-btn:hover {
27
+ transform: scale(1.1);
28
+ }
29
+ .admin-badge {
30
+ background-color: #dc3545;
31
+ color: white;
32
+ padding: 4px 8px;
33
+ border-radius: 4px;
34
+ font-size: 0.75rem;
35
+ }
36
+ .user-badge {
37
+ background-color: #6c757d;
38
+ color: white;
39
+ padding: 4px 8px;
40
+ border-radius: 4px;
41
+ font-size: 0.75rem;
42
+ }
43
+ </style>
44
+ </head>
45
+ <body>
46
+ <!-- Navigation -->
47
+ <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
48
+ <div class="container">
49
+ <a class="navbar-brand" href="{{ url_for('admin_panel') }}">
50
+ <i class="bi bi-shield-lock"></i> Admin Panel
51
+ </a>
52
+ <div class="navbar-nav ms-auto">
53
+ <a class="nav-link" href="{{ url_for('profile') }}">
54
+ <i class="bi bi-person-circle"></i> Back to Profile
55
+ </a>
56
+ <a class="nav-link" href="{{ url_for('logout') }}">
57
+ <i class="bi bi-box-arrow-right"></i> Logout
58
+ </a>
59
+ </div>
60
+ </div>
61
+ </nav>
62
+
63
+ <!-- Main Content -->
64
+ <div class="container mt-4">
65
+ <!-- User Statistics -->
66
+ <div class="user-count text-center">
67
+ <h2><i class="bi bi-people-fill"></i> User Management</h2>
68
+ <p class="mb-0 h4">Total Users: {{ users|length }}</p>
69
+ </div>
70
+
71
+ <!-- Users Table -->
72
+ <div class="card">
73
+ <div class="card-header bg-primary text-white">
74
+ <h5 class="mb-0"><i class="bi bi-table"></i> Registered Users</h5>
75
+ </div>
76
+ <div class="card-body">
77
+ <div class="table-responsive">
78
+ <table class="table table-hover" id="usersTable">
79
+ <thead>
80
+ <tr>
81
+ <th>ID</th>
82
+ <th>Name</th>
83
+ <th>Email</th>
84
+ <th>Role</th>
85
+ <th>Created At</th>
86
+ <th>Actions</th>
87
+ </tr>
88
+ </thead>
89
+ <tbody>
90
+ {% for user in users %}
91
+ <tr data-user-id="{{ user.id }}">
92
+ <td><code>{{ user.id|string|truncate(8, True, '') }}...</code></td>
93
+ <td>{{ user.name }}</td>
94
+ <td>{{ user.email }}</td>
95
+ <td>
96
+ {% if user.is_admin %}
97
+ <span class="admin-badge">Admin</span>
98
+ {% else %}
99
+ <span class="user-badge">User</span>
100
+ {% endif %}
101
+ </td>
102
+ <td>{{ user.created_at.strftime('%Y-%m-%d %H:%M') if user.created_at else 'N/A' }}</td>
103
+ <td class="table-actions">
104
+ {% if not user.is_admin %}
105
+ <button class="btn btn-sm btn-danger delete-btn"
106
+ onclick="deleteUser('{{ user.id }}', '{{ user.email }}')"
107
+ title="Delete User">
108
+ <i class="bi bi-trash"></i>
109
+ </button>
110
+ {% endif %}
111
+ </td>
112
+ </tr>
113
+ {% endfor %}
114
+ </tbody>
115
+ </table>
116
+ </div>
117
+ </div>
118
+ </div>
119
+ </div>
120
+
121
+ <!-- Delete Confirmation Modal -->
122
+ <div class="modal fade" id="deleteModal" tabindex="-1">
123
+ <div class="modal-dialog">
124
+ <div class="modal-content">
125
+ <div class="modal-header bg-danger text-white">
126
+ <h5 class="modal-title">
127
+ <i class="bi bi-exclamation-triangle"></i> Confirm Delete
128
+ </h5>
129
+ <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
130
+ </div>
131
+ <div class="modal-body">
132
+ <p>Are you sure you want to delete this user?</p>
133
+ <div class="alert alert-warning">
134
+ <i class="bi bi-warning"></i>
135
+ <strong>Warning:</strong> This will permanently delete the user and all their profile data including:
136
+ <ul class="mb-0 mt-2">
137
+ <li>Introduction information</li>
138
+ <li>Profile summary</li>
139
+ <li>Work experience</li>
140
+ <li>Projects</li>
141
+ <li>Education</li>
142
+ <li>Skills</li>
143
+ <li>Achievements</li>
144
+ </ul>
145
+ </div>
146
+ <p><strong>User to delete:</strong> <span id="deleteUserEmail"></span></p>
147
+ </div>
148
+ <div class="modal-footer">
149
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
150
+ <button type="button" class="btn btn-danger" id="confirmDelete">
151
+ <i class="bi bi-trash"></i> Delete User
152
+ </button>
153
+ </div>
154
+ </div>
155
+ </div>
156
+ </div>
157
+
158
+ <!-- Success Toast -->
159
+ <div class="position-fixed bottom-0 end-0 p-3" style="z-index: 11">
160
+ <div id="successToast" class="toast" role="alert">
161
+ <div class="toast-header bg-success text-white">
162
+ <i class="bi bi-check-circle me-2"></i>
163
+ <strong class="me-auto">Success</strong>
164
+ <button type="button" class="btn-close btn-close-white" data-bs-dismiss="toast"></button>
165
+ </div>
166
+ <div class="toast-body" id="successMessage">
167
+ User deleted successfully!
168
+ </div>
169
+ </div>
170
+ </div>
171
+
172
+ <!-- Error Toast -->
173
+ <div class="position-fixed bottom-0 end-0 p-3" style="z-index: 11">
174
+ <div id="errorToast" class="toast" role="alert">
175
+ <div class="toast-header bg-danger text-white">
176
+ <i class="bi bi-exclamation-circle me-2"></i>
177
+ <strong class="me-auto">Error</strong>
178
+ <button type="button" class="btn-close btn-close-white" data-bs-dismiss="toast"></button>
179
+ </div>
180
+ <div class="toast-body" id="errorMessage">
181
+ An error occurred!
182
+ </div>
183
+ </div>
184
+ </div>
185
+
186
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
187
+ <script>
188
+ let userToDelete = null;
189
+ const deleteModal = new bootstrap.Modal(document.getElementById('deleteModal'));
190
+ const successToast = new bootstrap.Toast(document.getElementById('successToast'));
191
+ const errorToast = new bootstrap.Toast(document.getElementById('errorToast'));
192
+
193
+ function deleteUser(userId, userEmail) {
194
+ userToDelete = userId;
195
+ document.getElementById('deleteUserEmail').textContent = userEmail;
196
+ deleteModal.show();
197
+ }
198
+
199
+ document.getElementById('confirmDelete').addEventListener('click', async function() {
200
+ if (!userToDelete) return;
201
+
202
+ try {
203
+ const response = await fetch(`/api/admin/users/${userToDelete}`, {
204
+ method: 'DELETE',
205
+ headers: {
206
+ 'Content-Type': 'application/json'
207
+ }
208
+ });
209
+
210
+ const data = await response.json();
211
+
212
+ if (data.success) {
213
+ // Remove row from table
214
+ const row = document.querySelector(`tr[data-user-id="${userToDelete}"]`);
215
+ if (row) {
216
+ row.remove();
217
+ }
218
+
219
+ // Update user count
220
+ const userCount = document.querySelectorAll('#usersTable tbody tr').length;
221
+ document.querySelector('.user-count p').textContent = `Total Users: ${userCount}`;
222
+
223
+ // Show success message
224
+ document.getElementById('successMessage').textContent = data.message;
225
+ successToast.show();
226
+ } else {
227
+ // Show error message
228
+ document.getElementById('errorMessage').textContent = data.error || 'Failed to delete user';
229
+ errorToast.show();
230
+ }
231
+ } catch (error) {
232
+ console.error('Error:', error);
233
+ document.getElementById('errorMessage').textContent = 'Network error occurred';
234
+ errorToast.show();
235
+ } finally {
236
+ deleteModal.hide();
237
+ userToDelete = null;
238
+ }
239
+ });
240
+ </script>
241
+ </body>
242
+ </html>
templates/base.html ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>{% block title %}AI Resume Builder{% endblock %}</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <style>
9
+ /* Custom animations */
10
+ @keyframes fadeIn {
11
+ from { opacity: 0; transform: translateY(10px); }
12
+ to { opacity: 1; transform: translateY(0); }
13
+ }
14
+
15
+ .fade-in {
16
+ animation: fadeIn 0.5s ease-out;
17
+ }
18
+
19
+ /* Carousel transitions */
20
+ .carousel-image {
21
+ transition: opacity 1s ease-in-out;
22
+ }
23
+
24
+ /* Custom scrollbar */
25
+ ::-webkit-scrollbar {
26
+ width: 8px;
27
+ }
28
+
29
+ ::-webkit-scrollbar-track {
30
+ background: #f1f1f1;
31
+ }
32
+
33
+ ::-webkit-scrollbar-thumb {
34
+ background: #888;
35
+ border-radius: 4px;
36
+ }
37
+
38
+ ::-webkit-scrollbar-thumb:hover {
39
+ background: #555;
40
+ }
41
+ </style>
42
+ </head>
43
+ <body class="bg-gray-50">
44
+ <!-- Flash Messages -->
45
+ {% with messages = get_flashed_messages(with_categories=true) %}
46
+ {% if messages %}
47
+ <div class="fixed top-4 right-4 z-50 space-y-2">
48
+ {% for category, message in messages %}
49
+ <div class="fade-in px-4 py-3 rounded-lg shadow-lg {% if category == 'success' %}bg-green-50 border border-green-200 text-green-800{% elif category == 'error' %}bg-red-50 border border-red-200 text-red-800{% else %}bg-blue-50 border border-blue-200 text-blue-800{% endif %}">
50
+ <div class="flex items-center">
51
+ <span class="text-sm font-medium">{{ message }}</span>
52
+ <button onclick="this.parentElement.parentElement.remove()" class="ml-3 text-gray-400 hover:text-gray-600">
53
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
54
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
55
+ </svg>
56
+ </button>
57
+ </div>
58
+ </div>
59
+ {% endfor %}
60
+ </div>
61
+ {% endif %}
62
+ {% endwith %}
63
+
64
+ <!-- Main Content -->
65
+ <main class="min-h-screen">
66
+ {% block content %}{% endblock %}
67
+ </main>
68
+
69
+ <!-- Footer -->
70
+ {% block footer %}
71
+ <footer class="bg-white border-t border-gray-200 mt-12">
72
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
73
+ <div class="text-center text-gray-600">
74
+ <p class="text-sm">© 2024 AI Resume Builder. Built with ❤️ using Flask and AI.</p>
75
+ </div>
76
+ </div>
77
+ </footer>
78
+ {% endblock %}
79
+
80
+ <!-- JavaScript -->
81
+ <script>
82
+ // Auto-hide flash messages after 5 seconds
83
+ document.addEventListener('DOMContentLoaded', function() {
84
+ const messages = document.querySelectorAll('.fixed.top-4 > div');
85
+ messages.forEach(function(message) {
86
+ setTimeout(function() {
87
+ message.style.opacity = '0';
88
+ message.style.transform = 'translateX(100%)';
89
+ message.style.transition = 'all 0.3s ease-out';
90
+ setTimeout(function() {
91
+ message.remove();
92
+ }, 300);
93
+ }, 5000);
94
+ });
95
+ });
96
+ </script>
97
+
98
+ {% block scripts %}{% endblock %}
99
+ </body>
100
+ </html>
templates/create_achievements.html ADDED
@@ -0,0 +1,288 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Achievements - AI Resume Builder</title>
7
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
+ <style>
10
+ body {
11
+ background-color: #f8f9fa;
12
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
13
+ }
14
+ .form-container {
15
+ max-width: 800px;
16
+ margin: 40px auto;
17
+ padding: 30px;
18
+ background: white;
19
+ border-radius: 10px;
20
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
21
+ }
22
+ .section-title {
23
+ color: #2c3e50;
24
+ margin-bottom: 30px;
25
+ text-align: center;
26
+ font-weight: 600;
27
+ }
28
+ .btn-logout {
29
+ position: absolute;
30
+ top: 20px;
31
+ right: 20px;
32
+ }
33
+ .form-label {
34
+ font-weight: 500;
35
+ margin-bottom: 8px;
36
+ color: #495057;
37
+ }
38
+ .form-control:focus {
39
+ border-color: #4e73df;
40
+ box-shadow: 0 0 0 0.2rem rgba(78, 115, 223, 0.25);
41
+ }
42
+ .navigation-buttons {
43
+ display: flex;
44
+ justify-content: space-between;
45
+ margin-top: 30px;
46
+ padding-top: 20px;
47
+ border-top: 1px solid #e9ecef;
48
+ }
49
+ .error-message {
50
+ color: #e74c3c;
51
+ font-size: 0.875rem;
52
+ margin-top: 5px;
53
+ }
54
+ .progress-indicator {
55
+ margin-bottom: 30px;
56
+ text-align: center;
57
+ color: #6c757d;
58
+ font-size: 0.9rem;
59
+ }
60
+ .progress-indicator span {
61
+ display: inline-block;
62
+ margin: 0 10px;
63
+ }
64
+ .progress-indicator span.active {
65
+ color: #4e73df;
66
+ font-weight: 600;
67
+ }
68
+ .achievements-display {
69
+ background-color: #f8f9fa;
70
+ border: 1px solid #e9ecef;
71
+ border-radius: 8px;
72
+ padding: 20px;
73
+ margin-top: 20px;
74
+ min-height: 100px;
75
+ }
76
+ .achievement-item {
77
+ display: flex;
78
+ align-items: center;
79
+ background-color: white;
80
+ border: 1px solid #dee2e6;
81
+ border-radius: 6px;
82
+ padding: 12px 15px;
83
+ margin-bottom: 10px;
84
+ transition: all 0.3s;
85
+ }
86
+ .achievement-item:hover {
87
+ border-color: #4e73df;
88
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
89
+ }
90
+ .achievement-item .achievement-icon {
91
+ color: #ffc107;
92
+ margin-right: 15px;
93
+ font-size: 1.2rem;
94
+ }
95
+ .achievement-item .achievement-text {
96
+ flex: 1;
97
+ margin: 0;
98
+ }
99
+ .achievement-item .remove-achievement {
100
+ color: #dc3545;
101
+ cursor: pointer;
102
+ opacity: 0.6;
103
+ transition: opacity 0.3s;
104
+ }
105
+ .achievement-item .remove-achievement:hover {
106
+ opacity: 1;
107
+ }
108
+ .empty-achievements {
109
+ text-align: center;
110
+ color: #6c757d;
111
+ font-style: italic;
112
+ padding: 20px;
113
+ }
114
+ .help-text {
115
+ font-size: 0.875rem;
116
+ color: #6c757d;
117
+ margin-top: 5px;
118
+ }
119
+ .achievements-preview-section {
120
+ margin-top: 30px;
121
+ }
122
+ .achievements-preview-section h5 {
123
+ color: #495057;
124
+ margin-bottom: 15px;
125
+ }
126
+ .form-hint {
127
+ background-color: #e3f2fd;
128
+ border-left: 4px solid #2196f3;
129
+ padding: 12px 15px;
130
+ margin-bottom: 20px;
131
+ border-radius: 4px;
132
+ }
133
+ .form-hint p {
134
+ margin: 0;
135
+ color: #1976d2;
136
+ font-size: 0.9rem;
137
+ }
138
+ </style>
139
+ </head>
140
+ <body>
141
+ <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
142
+ <div class="container">
143
+ <a class="navbar-brand" href="{{ url_for('profile') }}">AI Resume Builder</a>
144
+ <div class="ms-auto">
145
+ <a href="{{ url_for('logout') }}" class="btn btn-outline-light">
146
+ <i class="fas fa-sign-out-alt"></i> Logout
147
+ </a>
148
+ </div>
149
+ </div>
150
+ </nav>
151
+
152
+ <div class="container">
153
+ <div class="form-container">
154
+ <div class="progress-indicator">
155
+ <span>Introduction</span>
156
+ <span>→</span>
157
+ <span>Profile Summary</span>
158
+ <span>→</span>
159
+ <span>Work Experience</span>
160
+ <span>→</span>
161
+ <span>Projects</span>
162
+ <span>→</span>
163
+ <span>Education</span>
164
+ <span>→</span>
165
+ <span>Skills</span>
166
+ <span>→</span>
167
+ <span class="active">Achievements</span>
168
+ <span>→</span>
169
+ <span>Preview</span>
170
+ </div>
171
+
172
+ <h2 class="section-title">Achievements</h2>
173
+
174
+ {% with messages = get_flashed_messages(with_categories=true) %}
175
+ {% if messages %}
176
+ {% for category, message in messages %}
177
+ <div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
178
+ {{ message }}
179
+ <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
180
+ </div>
181
+ {% endfor %}
182
+ {% endif %}
183
+ {% endwith %}
184
+
185
+ <div class="form-hint">
186
+ <p><i class="fas fa-lightbulb"></i> <strong>Tip:</strong> Highlight your awards, certifications, publications, or other notable accomplishments. These help make your resume stand out!</p>
187
+ </div>
188
+
189
+ <form method="POST" action="{{ url_for('create_achievements') }}" id="achievementsForm">
190
+ <div class="mb-3">
191
+ <label for="achievements" class="form-label">Enter Your Achievements</label>
192
+ <textarea class="form-control" id="achievements" name="achievements" rows="5"
193
+ placeholder="Enter your achievements separated by commas. For example: Employee of the Year 2023, AWS Certified Solutions Architect, Published research paper on AI ethics...">{{ form_data.achievements if form_data }}</textarea>
194
+ <div class="help-text">
195
+ Enter your awards, certifications, publications, honors, or other achievements separated by commas. Each will be displayed as a separate item.
196
+ </div>
197
+ {% if form_errors.achievements %}
198
+ <div class="error-message">{{ form_errors.achievements[0] }}</div>
199
+ {% endif %}
200
+ </div>
201
+
202
+ <div class="achievements-preview-section">
203
+ <h5><i class="fas fa-eye"></i> Achievements Preview</h5>
204
+ <div class="achievements-display" id="achievementsPreview">
205
+ {% if form_data.achievements_preview %}
206
+ {% for achievement in form_data.achievements_preview %}
207
+ <div class="achievement-item">
208
+ <i class="fas fa-trophy achievement-icon"></i>
209
+ <p class="achievement-text mb-0">{{ achievement }}</p>
210
+ <i class="fas fa-times remove-achievement" onclick="removeAchievement(this)"></i>
211
+ </div>
212
+ {% endfor %}
213
+ {% else %}
214
+ <div class="empty-achievements">
215
+ Your achievements will appear here as you type them above
216
+ </div>
217
+ {% endif %}
218
+ </div>
219
+ </div>
220
+
221
+ <div class="navigation-buttons">
222
+ <div>
223
+ <a href="{{ url_for('create_skills') }}" class="btn btn-outline-secondary">
224
+ <i class="fas fa-arrow-left"></i> Back
225
+ </a>
226
+ <button type="button" class="btn btn-outline-primary" onclick="skipToPreview()">
227
+ Skip <i class="fas fa-forward"></i>
228
+ </button>
229
+ </div>
230
+ <div>
231
+ <button type="submit" class="btn btn-primary">
232
+ Next <i class="fas fa-arrow-right"></i>
233
+ </button>
234
+ </div>
235
+ </div>
236
+ </form>
237
+ </div>
238
+ </div>
239
+
240
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
241
+ <script>
242
+ const achievementsTextarea = document.getElementById('achievements');
243
+ const achievementsPreview = document.getElementById('achievementsPreview');
244
+
245
+ function updateAchievementsPreview() {
246
+ const input = achievementsTextarea.value;
247
+ const newAchievements = input.split(',').map(achievement => achievement.trim()).filter(achievement => achievement);
248
+
249
+ // Update preview
250
+ if (newAchievements.length === 0) {
251
+ achievementsPreview.innerHTML = '<div class="empty-achievements">Your achievements will appear here as you type them above</div>';
252
+ } else {
253
+ achievementsPreview.innerHTML = newAchievements.map(achievement => `
254
+ <div class="achievement-item">
255
+ <i class="fas fa-trophy achievement-icon"></i>
256
+ <p class="achievement-text mb-0">${achievement}</p>
257
+ <i class="fas fa-times remove-achievement" onclick="removeAchievement(this)"></i>
258
+ </div>
259
+ `).join('');
260
+ }
261
+ }
262
+
263
+ function removeAchievement(element) {
264
+ const achievementItem = element.closest('.achievement-item');
265
+ const achievementText = achievementItem.querySelector('.achievement-text').textContent.trim();
266
+
267
+ // Remove from textarea
268
+ const currentAchievements = achievementsTextarea.value.split(',').map(a => a.trim()).filter(a => a);
269
+ const updatedAchievements = currentAchievements.filter(achievement => achievement !== achievementText);
270
+ achievementsTextarea.value = updatedAchievements.join(', ');
271
+
272
+ // Update preview
273
+ updateAchievementsPreview();
274
+ }
275
+
276
+ function skipToPreview() {
277
+ document.getElementById('achievementsForm').action = '{{ url_for("create_preview") }}';
278
+ document.getElementById('achievementsForm').submit();
279
+ }
280
+
281
+ // Update preview as user types
282
+ achievementsTextarea.addEventListener('input', updateAchievementsPreview);
283
+
284
+ // Initialize preview on page load
285
+ updateAchievementsPreview();
286
+ </script>
287
+ </body>
288
+ </html>
templates/create_education.html ADDED
@@ -0,0 +1,425 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Education - AI Resume Builder</title>
7
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
+ <style>
10
+ body {
11
+ background-color: #f8f9fa;
12
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
13
+ }
14
+ .form-container {
15
+ max-width: 900px;
16
+ margin: 40px auto;
17
+ padding: 30px;
18
+ background: white;
19
+ border-radius: 10px;
20
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
21
+ }
22
+ .section-title {
23
+ color: #2c3e50;
24
+ margin-bottom: 30px;
25
+ text-align: center;
26
+ font-weight: 600;
27
+ }
28
+ .btn-logout {
29
+ position: absolute;
30
+ top: 20px;
31
+ right: 20px;
32
+ }
33
+ .form-label {
34
+ font-weight: 500;
35
+ margin-bottom: 8px;
36
+ color: #495057;
37
+ }
38
+ .form-control:focus {
39
+ border-color: #4e73df;
40
+ box-shadow: 0 0 0 0.2rem rgba(78, 115, 223, 0.25);
41
+ }
42
+ .navigation-buttons {
43
+ display: flex;
44
+ justify-content: space-between;
45
+ margin-top: 30px;
46
+ padding-top: 20px;
47
+ border-top: 1px solid #e9ecef;
48
+ }
49
+ .error-message {
50
+ color: #e74c3c;
51
+ font-size: 0.875rem;
52
+ margin-top: 5px;
53
+ }
54
+ .progress-indicator {
55
+ margin-bottom: 30px;
56
+ text-align: center;
57
+ color: #6c757d;
58
+ font-size: 0.9rem;
59
+ }
60
+ .progress-indicator span {
61
+ display: inline-block;
62
+ margin: 0 10px;
63
+ }
64
+ .progress-indicator span.active {
65
+ color: #4e73df;
66
+ font-weight: 600;
67
+ }
68
+ .education-item {
69
+ background-color: #f8f9fa;
70
+ border: 1px solid #e9ecef;
71
+ border-radius: 8px;
72
+ padding: 20px;
73
+ margin-bottom: 20px;
74
+ position: relative;
75
+ }
76
+ .education-item.has-error {
77
+ border-color: #e74c3c;
78
+ background-color: #fdf2f2;
79
+ }
80
+ .btn-remove {
81
+ position: absolute;
82
+ top: 10px;
83
+ right: 10px;
84
+ color: #e74c3c;
85
+ background: white;
86
+ border: 1px solid #e74c3c;
87
+ padding: 5px 10px;
88
+ border-radius: 5px;
89
+ transition: all 0.3s;
90
+ }
91
+ .btn-remove:hover {
92
+ color: white;
93
+ background-color: #e74c3c;
94
+ }
95
+ .btn-add {
96
+ margin-bottom: 20px;
97
+ background-color: #28a745;
98
+ border: none;
99
+ color: white;
100
+ }
101
+ .btn-add:hover {
102
+ background-color: #218838;
103
+ color: white;
104
+ }
105
+ .present-checkbox {
106
+ margin-top: 10px;
107
+ }
108
+ .date-inputs {
109
+ display: flex;
110
+ gap: 10px;
111
+ }
112
+ .date-inputs .form-control {
113
+ flex: 1;
114
+ }
115
+ .empty-state {
116
+ text-align: center;
117
+ padding: 40px;
118
+ color: #6c757d;
119
+ }
120
+ .empty-state i {
121
+ font-size: 3rem;
122
+ margin-bottom: 20px;
123
+ color: #dee2e6;
124
+ }
125
+ .section-header {
126
+ display: flex;
127
+ justify-content: space-between;
128
+ align-items: center;
129
+ margin-bottom: 20px;
130
+ }
131
+ </style>
132
+ </head>
133
+ <body>
134
+ <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
135
+ <div class="container">
136
+ <a class="navbar-brand" href="{{ url_for('profile') }}">AI Resume Builder</a>
137
+ <div class="ms-auto">
138
+ <a href="{{ url_for('logout') }}" class="btn btn-outline-light">
139
+ <i class="fas fa-sign-out-alt"></i> Logout
140
+ </a>
141
+ </div>
142
+ </div>
143
+ </nav>
144
+
145
+ <div class="container">
146
+ <div class="form-container">
147
+ <div class="progress-indicator">
148
+ <span>Introduction</span>
149
+ <span>→</span>
150
+ <span>Profile Summary</span>
151
+ <span>→</span>
152
+ <span>Work Experience</span>
153
+ <span>→</span>
154
+ <span>Projects</span>
155
+ <span>→</span>
156
+ <span class="active">Education</span>
157
+ <span>→</span>
158
+ <span>Skills</span>
159
+ <span>→</span>
160
+ <span>Achievements</span>
161
+ <span>→</span>
162
+ <span>Preview</span>
163
+ </div>
164
+
165
+ <div class="section-header">
166
+ <h2 class="section-title mb-0">Education</h2>
167
+ <button type="button" class="btn btn-add" onclick="addEducation()">
168
+ <i class="fas fa-plus"></i> Add Education
169
+ </button>
170
+ </div>
171
+
172
+ {% with messages = get_flashed_messages(with_categories=true) %}
173
+ {% if messages %}
174
+ {% for category, message in messages %}
175
+ <div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
176
+ {{ message }}
177
+ <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
178
+ </div>
179
+ {% endfor %}
180
+ {% endif %}
181
+ {% endwith %}
182
+
183
+ <form method="POST" action="{{ url_for('create_education') }}" id="educationForm">
184
+ <div id="educationContainer">
185
+ {% if form_data and form_data.educations %}
186
+ {% for edu in form_data.educations %}
187
+ <div class="education-item">
188
+ <button type="button" class="btn btn-sm btn-remove" onclick="removeEducation(this)">
189
+ <i class="fas fa-times"></i>
190
+ </button>
191
+
192
+ <div class="row">
193
+ <div class="col-md-6 mb-3">
194
+ <label class="form-label required">Institution</label>
195
+ <input type="text" class="form-control" name="organization[]" value="{{ edu.organization }}" required>
196
+ </div>
197
+ <div class="col-md-6 mb-3">
198
+ <label class="form-label required">Degree/Certification</label>
199
+ <input type="text" class="form-control" name="title[]" value="{{ edu.title }}" required>
200
+ </div>
201
+ </div>
202
+
203
+ <div class="row">
204
+ <div class="col-md-6 mb-3">
205
+ <label class="form-label required">Start Date</label>
206
+ <div class="date-inputs">
207
+ <select class="form-control" name="start_month[]" required>
208
+ <option value="">Month</option>
209
+ {% for i in range(1, 13) %}
210
+ <option value="{{ i }}" {% if edu.start_month == i %}selected{% endif %}>
211
+ {{ i }}
212
+ </option>
213
+ {% endfor %}
214
+ </select>
215
+ <select class="form-control" name="start_year[]" required>
216
+ <option value="">Year</option>
217
+ {% for year in range(1980, current_year + 1) %}
218
+ <option value="{{ year }}" {% if edu.start_year == year %}selected{% endif %}>
219
+ {{ year }}
220
+ </option>
221
+ {% endfor %}
222
+ </select>
223
+ </div>
224
+ </div>
225
+ <div class="col-md-6 mb-3">
226
+ <label class="form-label">End Date</label>
227
+ <div class="date-inputs">
228
+ <select class="form-control" name="end_month[]" {% if edu.is_present %}disabled{% endif %}>
229
+ <option value="">Month</option>
230
+ {% for i in range(1, 13) %}
231
+ <option value="{{ i }}" {% if edu.end_month == i %}selected{% endif %}>
232
+ {{ i }}
233
+ </option>
234
+ {% endfor %}
235
+ </select>
236
+ <select class="form-control" name="end_year[]" {% if edu.is_present %}disabled{% endif %}>
237
+ <option value="">Year</option>
238
+ {% for year in range(1980, current_year + 1) %}
239
+ <option value="{{ year }}" {% if edu.end_year == year %}selected{% endif %}>
240
+ {{ year }}
241
+ </option>
242
+ {% endfor %}
243
+ </select>
244
+ </div>
245
+ <div class="present-checkbox">
246
+ <div class="form-check">
247
+ <input class="form-check-input" type="checkbox" name="is_present[]" id="present_edu_{{ loop.index0 }}"
248
+ {% if edu.is_present %}checked{% endif %}
249
+ onchange="toggleEndDate(this)">
250
+ <label class="form-check-label" for="present_edu_{{ loop.index0 }}">
251
+ Present
252
+ </label>
253
+ </div>
254
+ </div>
255
+ </div>
256
+ </div>
257
+
258
+ <div class="mb-3">
259
+ <label class="form-label">Remarks</label>
260
+ <textarea class="form-control" name="remarks[]" rows="3" placeholder="Describe your studies, achievements, or relevant coursework...">{{ edu.remarks }}</textarea>
261
+ </div>
262
+ </div>
263
+ {% endfor %}
264
+ {% else %}
265
+ <div class="empty-state">
266
+ <i class="fas fa-graduation-cap"></i>
267
+ <h5>No Education Added</h5>
268
+ <p>Click "Add Education" to add your educational background</p>
269
+ </div>
270
+ {% endif %}
271
+ </div>
272
+
273
+ <div class="navigation-buttons">
274
+ <div>
275
+ <a href="{{ url_for('create_projects') }}" class="btn btn-outline-secondary">
276
+ <i class="fas fa-arrow-left"></i> Back
277
+ </a>
278
+ <button type="button" class="btn btn-outline-primary" onclick="skipToSkills()">
279
+ Skip <i class="fas fa-forward"></i>
280
+ </button>
281
+ </div>
282
+ <div>
283
+ <button type="submit" class="btn btn-primary">
284
+ Next <i class="fas fa-arrow-right"></i>
285
+ </button>
286
+ </div>
287
+ </div>
288
+ </form>
289
+ </div>
290
+ </div>
291
+
292
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
293
+ <script>
294
+ let educationCount = {{ form_data.educations|length if form_data and form_data.educations else 0 }};
295
+
296
+ function addEducation() {
297
+ const container = document.getElementById('educationContainer');
298
+ const emptyState = container.querySelector('.empty-state');
299
+ if (emptyState) {
300
+ emptyState.remove();
301
+ }
302
+
303
+ const div = document.createElement('div');
304
+ div.className = 'education-item';
305
+ div.innerHTML = `
306
+ <button type="button" class="btn btn-sm btn-remove" onclick="removeEducation(this)">
307
+ <i class="fas fa-times"></i>
308
+ </button>
309
+
310
+ <div class="row">
311
+ <div class="col-md-6 mb-3">
312
+ <label class="form-label required">Institution</label>
313
+ <input type="text" class="form-control" name="organization[]" required>
314
+ </div>
315
+ <div class="col-md-6 mb-3">
316
+ <label class="form-label required">Degree/Certification</label>
317
+ <input type="text" class="form-control" name="title[]" required>
318
+ </div>
319
+ </div>
320
+
321
+ <div class="row">
322
+ <div class="col-md-6 mb-3">
323
+ <label class="form-label required">Start Date</label>
324
+ <div class="date-inputs">
325
+ <select class="form-control" name="start_month[]" required>
326
+ <option value="">Month</option>
327
+ ${generateMonthOptions()}
328
+ </select>
329
+ <select class="form-control" name="start_year[]" required>
330
+ <option value="">Year</option>
331
+ ${generateYearOptions()}
332
+ </select>
333
+ </div>
334
+ </div>
335
+ <div class="col-md-6 mb-3">
336
+ <label class="form-label">End Date</label>
337
+ <div class="date-inputs">
338
+ <select class="form-control" name="end_month[]">
339
+ <option value="">Month</option>
340
+ ${generateMonthOptions()}
341
+ </select>
342
+ <select class="form-control" name="end_year[]">
343
+ <option value="">Year</option>
344
+ ${generateYearOptions()}
345
+ </select>
346
+ </div>
347
+ <div class="present-checkbox">
348
+ <div class="form-check">
349
+ <input class="form-check-input" type="checkbox" name="is_present[]" id="present_edu_${educationCount}" onchange="toggleEndDate(this)">
350
+ <label class="form-check-label" for="present_edu_${educationCount}">
351
+ Present
352
+ </label>
353
+ </div>
354
+ </div>
355
+ </div>
356
+ </div>
357
+
358
+ <div class="mb-3">
359
+ <label class="form-label">Remarks</label>
360
+ <textarea class="form-control" name="remarks[]" rows="3" placeholder="Describe your studies, achievements, or relevant coursework..."></textarea>
361
+ </div>
362
+ `;
363
+
364
+ container.appendChild(div);
365
+ educationCount++;
366
+ }
367
+
368
+ function removeEducation(button) {
369
+ const item = button.closest('.education-item');
370
+ item.remove();
371
+
372
+ const container = document.getElementById('educationContainer');
373
+ const remainingItems = container.querySelectorAll('.education-item');
374
+
375
+ if (remainingItems.length === 0) {
376
+ container.innerHTML = `
377
+ <div class="empty-state">
378
+ <i class="fas fa-graduation-cap"></i>
379
+ <h5>No Education Added</h5>
380
+ <p>Click "Add Education" to add your educational background</p>
381
+ </div>
382
+ `;
383
+ }
384
+ }
385
+
386
+ function toggleEndDate(checkbox) {
387
+ const item = checkbox.closest('.education-item');
388
+ const endMonth = item.querySelector('select[name="end_month[]"]');
389
+ const endYear = item.querySelector('select[name="end_year[]"]');
390
+
391
+ if (checkbox.checked) {
392
+ endMonth.disabled = true;
393
+ endYear.disabled = true;
394
+ endMonth.value = '';
395
+ endYear.value = '';
396
+ } else {
397
+ endMonth.disabled = false;
398
+ endYear.disabled = false;
399
+ }
400
+ }
401
+
402
+ function skipToSkills() {
403
+ document.getElementById('educationForm').action = '{{ url_for("create_skills") }}';
404
+ document.getElementById('educationForm').submit();
405
+ }
406
+
407
+ function generateMonthOptions() {
408
+ let options = '';
409
+ for (let i = 1; i <= 12; i++) {
410
+ options += `<option value="${i}">${i}</option>`;
411
+ }
412
+ return options;
413
+ }
414
+
415
+ function generateYearOptions() {
416
+ const currentYear = new Date().getFullYear();
417
+ let options = '';
418
+ for (let year = 1980; year <= currentYear + 1; year++) {
419
+ options += `<option value="${year}">${year}</option>`;
420
+ }
421
+ return options;
422
+ }
423
+ </script>
424
+ </body>
425
+ </html>
templates/create_introduction.html ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Create Introduction - AI Resume Builder</title>
7
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
+ <style>
10
+ body {
11
+ background-color: #f8f9fa;
12
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
13
+ }
14
+ .form-container {
15
+ max-width: 800px;
16
+ margin: 40px auto;
17
+ padding: 30px;
18
+ background: white;
19
+ border-radius: 10px;
20
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
21
+ }
22
+ .section-title {
23
+ color: #2c3e50;
24
+ margin-bottom: 30px;
25
+ text-align: center;
26
+ font-weight: 600;
27
+ }
28
+ .required::after {
29
+ content: " *";
30
+ color: #e74c3c;
31
+ }
32
+ .btn-logout {
33
+ position: absolute;
34
+ top: 20px;
35
+ right: 20px;
36
+ }
37
+ .form-label {
38
+ font-weight: 500;
39
+ margin-bottom: 8px;
40
+ color: #495057;
41
+ }
42
+ .form-control:focus {
43
+ border-color: #4e73df;
44
+ box-shadow: 0 0 0 0.2rem rgba(78, 115, 223, 0.25);
45
+ }
46
+ .navigation-buttons {
47
+ display: flex;
48
+ justify-content: space-between;
49
+ margin-top: 30px;
50
+ padding-top: 20px;
51
+ border-top: 1px solid #e9ecef;
52
+ }
53
+ .error-message {
54
+ color: #e74c3c;
55
+ font-size: 0.875rem;
56
+ margin-top: 5px;
57
+ }
58
+ .progress-indicator {
59
+ margin-bottom: 30px;
60
+ text-align: center;
61
+ color: #6c757d;
62
+ font-size: 0.9rem;
63
+ }
64
+ .progress-indicator span {
65
+ display: inline-block;
66
+ margin: 0 10px;
67
+ }
68
+ .progress-indicator span.active {
69
+ color: #4e73df;
70
+ font-weight: 600;
71
+ }
72
+ </style>
73
+ </head>
74
+ <body>
75
+ <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
76
+ <div class="container">
77
+ <a class="navbar-brand" href="{{ url_for('profile') }}">AI Resume Builder</a>
78
+ <div class="ms-auto">
79
+ <a href="{{ url_for('logout') }}" class="btn btn-outline-light">
80
+ <i class="fas fa-sign-out-alt"></i> Logout
81
+ </a>
82
+ </div>
83
+ </div>
84
+ </nav>
85
+
86
+ <div class="container">
87
+ <div class="form-container">
88
+ <div class="progress-indicator">
89
+ <span class="active">Introduction</span>
90
+ <span>→</span>
91
+ <span>Profile Summary</span>
92
+ <span>→</span>
93
+ <span>Work Experience</span>
94
+ <span>→</span>
95
+ <span>Projects</span>
96
+ <span>→</span>
97
+ <span>Education</span>
98
+ <span>→</span>
99
+ <span>Skills</span>
100
+ <span>→</span>
101
+ <span>Achievements</span>
102
+ <span>→</span>
103
+ <span>Preview</span>
104
+ </div>
105
+
106
+ <h2 class="section-title">Introduction</h2>
107
+
108
+ {% with messages = get_flashed_messages(with_categories=true) %}
109
+ {% if messages %}
110
+ {% for category, message in messages %}
111
+ <div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
112
+ {{ message }}
113
+ <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
114
+ </div>
115
+ {% endfor %}
116
+ {% endif %}
117
+ {% endwith %}
118
+
119
+ <form method="POST" action="{{ url_for('create_introduction') }}" novalidate>
120
+ <div class="row">
121
+ <div class="col-md-6 mb-3">
122
+ <label for="name" class="form-label required">Name</label>
123
+ <input type="text" class="form-control" id="name" name="name" value="{{ form_data.name if form_data }}" required>
124
+ {% if form_errors.name %}
125
+ <div class="error-message">{{ form_errors.name[0] }}</div>
126
+ {% endif %}
127
+ </div>
128
+ <div class="col-md-6 mb-3">
129
+ <label for="email" class="form-label required">Email</label>
130
+ <input type="email" class="form-control" id="email" name="email" value="{{ form_data.email if form_data }}" required>
131
+ {% if form_errors.email %}
132
+ <div class="error-message">{{ form_errors.email[0] }}</div>
133
+ {% endif %}
134
+ </div>
135
+ </div>
136
+
137
+ <div class="row">
138
+ <div class="col-md-6 mb-3">
139
+ <label for="phone" class="form-label required">Phone</label>
140
+ <input type="tel" class="form-control" id="phone" name="phone" value="{{ form_data.phone if form_data }}" required>
141
+ {% if form_errors.phone %}
142
+ <div class="error-message">{{ form_errors.phone[0] }}</div>
143
+ {% endif %}
144
+ </div>
145
+ <div class="col-md-6 mb-3">
146
+ <label for="linkedin" class="form-label">LinkedIn URL</label>
147
+ <input type="url" class="form-control" id="linkedin" name="linkedin" value="{{ form_data.linkedin if form_data }}" placeholder="https://linkedin.com/in/username">
148
+ {% if form_errors.linkedin %}
149
+ <div class="error-message">{{ form_errors.linkedin[0] }}</div>
150
+ {% endif %}
151
+ </div>
152
+ </div>
153
+
154
+ <div class="row">
155
+ <div class="col-md-6 mb-3">
156
+ <label for="github" class="form-label">GitHub URL</label>
157
+ <input type="url" class="form-control" id="github" name="github" value="{{ form_data.github if form_data }}" placeholder="https://github.com/username">
158
+ {% if form_errors.github %}
159
+ <div class="error-message">{{ form_errors.github[0] }}</div>
160
+ {% endif %}
161
+ </div>
162
+ <div class="col-md-6 mb-3">
163
+ <label for="website" class="form-label">Website</label>
164
+ <input type="url" class="form-control" id="website" name="website" value="{{ form_data.website if form_data }}" placeholder="https://yourwebsite.com">
165
+ {% if form_errors.website %}
166
+ <div class="error-message">{{ form_errors.website[0] }}</div>
167
+ {% endif %}
168
+ </div>
169
+ </div>
170
+
171
+ <div class="navigation-buttons">
172
+ <div></div>
173
+ <div>
174
+ <button type="submit" class="btn btn-primary">
175
+ Next <i class="fas fa-arrow-right"></i>
176
+ </button>
177
+ </div>
178
+ </div>
179
+ </form>
180
+ </div>
181
+ </div>
182
+
183
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
184
+ </body>
185
+ </html>
templates/create_preview.html ADDED
@@ -0,0 +1,537 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Preview Profile - AI Resume Builder</title>
7
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.css">
10
+ <style>
11
+ body {
12
+ background-color: #f8f9fa;
13
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
14
+ }
15
+ .preview-container {
16
+ max-width: 1200px;
17
+ margin: 40px auto;
18
+ padding: 30px;
19
+ background: white;
20
+ border-radius: 10px;
21
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
22
+ }
23
+ .section-title {
24
+ color: #2c3e50;
25
+ margin-bottom: 30px;
26
+ text-align: center;
27
+ font-weight: 600;
28
+ }
29
+ .btn-logout {
30
+ position: absolute;
31
+ top: 20px;
32
+ right: 20px;
33
+ }
34
+ .progress-indicator {
35
+ margin-bottom: 30px;
36
+ text-align: center;
37
+ color: #6c757d;
38
+ font-size: 0.9rem;
39
+ }
40
+ .progress-indicator span {
41
+ display: inline-block;
42
+ margin: 0 10px;
43
+ }
44
+ .progress-indicator span.active {
45
+ color: #4e73df;
46
+ font-weight: 600;
47
+ }
48
+ .preview-layout {
49
+ display: grid;
50
+ grid-template-columns: 1fr 1fr;
51
+ gap: 30px;
52
+ margin-bottom: 30px;
53
+ }
54
+ .reorder-panel {
55
+ background-color: #f8f9fa;
56
+ border: 1px solid #e9ecef;
57
+ border-radius: 8px;
58
+ padding: 20px;
59
+ }
60
+ .reorder-panel h5 {
61
+ color: #495057;
62
+ margin-bottom: 15px;
63
+ }
64
+ .help-text {
65
+ font-size: 0.875rem;
66
+ color: #6c757d;
67
+ margin-bottom: 15px;
68
+ }
69
+ .sortable-list {
70
+ list-style: none;
71
+ padding: 0;
72
+ }
73
+ .sortable-item {
74
+ background: white;
75
+ border: 1px solid #dee2e6;
76
+ border-radius: 6px;
77
+ padding: 12px 15px;
78
+ margin-bottom: 8px;
79
+ cursor: move;
80
+ display: flex;
81
+ align-items: center;
82
+ transition: all 0.3s;
83
+ }
84
+ .sortable-item:hover {
85
+ border-color: #4e73df;
86
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
87
+ }
88
+ .sortable-item.dragging {
89
+ opacity: 0.5;
90
+ }
91
+ .sortable-item.sortable-ghost {
92
+ background: #f8f9fa;
93
+ }
94
+ .sortable-item .drag-handle {
95
+ color: #6c757d;
96
+ margin-right: 10px;
97
+ }
98
+ .sortable-item.fixed {
99
+ background-color: #e9ecef;
100
+ cursor: not-allowed;
101
+ opacity: 0.7;
102
+ }
103
+ .sortable-item.fixed .drag-handle {
104
+ display: none;
105
+ }
106
+ .preview-panel {
107
+ background-color: #ffffff;
108
+ border: 1px solid #e9ecef;
109
+ border-radius: 8px;
110
+ padding: 30px;
111
+ min-height: 600px;
112
+ }
113
+ .preview-section {
114
+ margin-bottom: 25px;
115
+ padding: 15px;
116
+ border-left: 4px solid #4e73df;
117
+ background-color: #f8f9fa;
118
+ }
119
+ .preview-section h5 {
120
+ color: #2c3e50;
121
+ margin-bottom: 15px;
122
+ font-weight: 600;
123
+ }
124
+ .preview-section h6 {
125
+ color: #495057;
126
+ margin-bottom: 10px;
127
+ }
128
+ .contact-info {
129
+ display: flex;
130
+ justify-content: space-between;
131
+ align-items: center;
132
+ margin-bottom: 15px;
133
+ flex-wrap: wrap;
134
+ gap: 10px;
135
+ }
136
+ .contact-item {
137
+ display: flex;
138
+ align-items: center;
139
+ color: #495057;
140
+ font-size: 0.9rem;
141
+ }
142
+ .contact-item i {
143
+ margin-right: 5px;
144
+ width: 16px;
145
+ }
146
+ .experience-item, .project-item, .education-item {
147
+ margin-bottom: 15px;
148
+ padding-bottom: 15px;
149
+ border-bottom: 1px solid #dee2e6;
150
+ }
151
+ .experience-item:last-child, .project-item:last-child, .education-item:last-child {
152
+ border-bottom: none;
153
+ margin-bottom: 0;
154
+ padding-bottom: 0;
155
+ }
156
+ .date-range {
157
+ color: #6c757d;
158
+ font-size: 0.9rem;
159
+ font-weight: 500;
160
+ }
161
+ .skills-grid, .achievements-grid {
162
+ display: flex;
163
+ flex-wrap: wrap;
164
+ gap: 8px;
165
+ }
166
+ .skill-tag, .achievement-tag {
167
+ background-color: #4e73df;
168
+ color: white;
169
+ padding: 4px 12px;
170
+ border-radius: 15px;
171
+ font-size: 0.85rem;
172
+ }
173
+ .achievement-tag {
174
+ background-color: #28a745;
175
+ }
176
+ .action-buttons {
177
+ display: flex;
178
+ justify-content: space-between;
179
+ margin-top: 30px;
180
+ padding-top: 20px;
181
+ border-top: 1px solid #e9ecef;
182
+ }
183
+ .empty-state {
184
+ text-align: center;
185
+ color: #6c757d;
186
+ font-style: italic;
187
+ padding: 20px;
188
+ }
189
+ .ai-generated-badge {
190
+ background-color: #17a2b8;
191
+ color: white;
192
+ padding: 2px 8px;
193
+ border-radius: 4px;
194
+ font-size: 0.75rem;
195
+ margin-left: 10px;
196
+ }
197
+ @media (max-width: 768px) {
198
+ .preview-layout {
199
+ grid-template-columns: 1fr;
200
+ }
201
+ .contact-info {
202
+ flex-direction: column;
203
+ align-items: flex-start;
204
+ }
205
+ }
206
+ </style>
207
+ </head>
208
+ <body>
209
+ <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
210
+ <div class="container">
211
+ <a class="navbar-brand" href="{{ url_for('profile') }}">AI Resume Builder</a>
212
+ <div class="ms-auto">
213
+ <a href="{{ url_for('logout') }}" class="btn btn-outline-light">
214
+ <i class="fas fa-sign-out-alt"></i> Logout
215
+ </a>
216
+ </div>
217
+ </div>
218
+ </nav>
219
+
220
+ <div class="container">
221
+ <div class="preview-container">
222
+ <div class="progress-indicator">
223
+ <span>Introduction</span>
224
+ <span>→</span>
225
+ <span>Profile Summary</span>
226
+ <span>→</span>
227
+ <span>Work Experience</span>
228
+ <span>→</span>
229
+ <span>Projects</span>
230
+ <span>→</span>
231
+ <span>Education</span>
232
+ <span>→</span>
233
+ <span>Skills</span>
234
+ <span>→</span>
235
+ <span>Achievements</span>
236
+ <span>→</span>
237
+ <span class="active">Preview</span>
238
+ </div>
239
+
240
+ <h2 class="section-title">Preview Your Profile</h2>
241
+
242
+ {% with messages = get_flashed_messages(with_categories=true) %}
243
+ {% if messages %}
244
+ {% for category, message in messages %}
245
+ <div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
246
+ {{ message }}
247
+ <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
248
+ </div>
249
+ {% endfor %}
250
+ {% endif %}
251
+ {% endwith %}
252
+
253
+ <div class="preview-layout">
254
+ <div class="reorder-panel">
255
+ <h5><i class="fas fa-arrows-alt"></i> Arrange Your Sections</h5>
256
+ <p class="help-text">
257
+ Drag and drop sections to reorder them. Introduction and Profile Summary are fixed at the top.
258
+ </p>
259
+ <ul id="sortableSections" class="sortable-list">
260
+ <li class="sortable-item fixed" data-section="introduction">
261
+ <i class="fas fa-user"></i> Introduction
262
+ </li>
263
+ <li class="sortable-item fixed" data-section="profile_summary">
264
+ <i class="fas fa-file-alt"></i> Profile Summary
265
+ </li>
266
+ {% if work_experiences %}
267
+ <li class="sortable-item" data-section="work_experience">
268
+ <i class="fas fa-grip-vertical drag-handle"></i>
269
+ <i class="fas fa-briefcase"></i> Work Experience
270
+ </li>
271
+ {% endif %}
272
+ {% if projects %}
273
+ <li class="sortable-item" data-section="projects">
274
+ <i class="fas fa-grip-vertical drag-handle"></i>
275
+ <i class="fas fa-project-diagram"></i> Projects
276
+ </li>
277
+ {% endif %}
278
+ {% if educations %}
279
+ <li class="sortable-item" data-section="education">
280
+ <i class="fas fa-grip-vertical drag-handle"></i>
281
+ <i class="fas fa-graduation-cap"></i> Education
282
+ </li>
283
+ {% endif %}
284
+ {% if skills %}
285
+ <li class="sortable-item" data-section="skills">
286
+ <i class="fas fa-grip-vertical drag-handle"></i>
287
+ <i class="fas fa-tools"></i> Skills
288
+ </li>
289
+ {% endif %}
290
+ {% if achievements %}
291
+ <li class="sortable-item" data-section="achievements">
292
+ <i class="fas fa-grip-vertical drag-handle"></i>
293
+ <i class="fas fa-trophy"></i> Achievements
294
+ </li>
295
+ {% endif %}
296
+ </ul>
297
+ </div>
298
+
299
+ <div class="preview-panel">
300
+ <div id="previewContent">
301
+ <!-- Introduction Section -->
302
+ {% if intro %}
303
+ <div class="preview-section" data-section="introduction">
304
+ <h5><i class="fas fa-user"></i> Introduction</h5>
305
+ <h3 class="text-center mb-3">{{ intro.name }}</h3>
306
+ <div class="contact-info">
307
+ <div class="contact-item">
308
+ <i class="fas fa-envelope"></i> {{ intro.email }}
309
+ </div>
310
+ <div class="contact-item">
311
+ <i class="fas fa-phone"></i> {{ intro.phone }}
312
+ </div>
313
+ {% if intro.linkedin %}
314
+ <div class="contact-item">
315
+ <i class="fab fa-linkedin"></i> LinkedIn
316
+ </div>
317
+ {% endif %}
318
+ {% if intro.github %}
319
+ <div class="contact-item">
320
+ <i class="fab fa-github"></i> GitHub
321
+ </div>
322
+ {% endif %}
323
+ {% if intro.website %}
324
+ <div class="contact-item">
325
+ <i class="fas fa-globe"></i> Website
326
+ </div>
327
+ {% endif %}
328
+ </div>
329
+ </div>
330
+ {% endif %}
331
+
332
+ <!-- Profile Summary Section -->
333
+ {% if summary %}
334
+ <div class="preview-section" data-section="profile_summary">
335
+ <h5>
336
+ <i class="fas fa-file-alt"></i> Profile Summary
337
+ {% if summary.ai_generated %}
338
+ <span class="ai-generated-badge">AI Generated</span>
339
+ {% endif %}
340
+ </h5>
341
+ <p>{{ summary.summary }}</p>
342
+ </div>
343
+ {% endif %}
344
+
345
+ <!-- Dynamic sections will be inserted here based on order -->
346
+ </div>
347
+ </div>
348
+ </div>
349
+
350
+ <form method="POST" action="{{ url_for('create_preview') }}" id="previewForm">
351
+ <input type="hidden" name="section_order" id="sectionOrderInput" value='[]'>
352
+
353
+ <div class="action-buttons">
354
+ <div>
355
+ <a href="{{ url_for('create_achievements') }}" class="btn btn-outline-secondary">
356
+ <i class="fas fa-arrow-left"></i> Back
357
+ </a>
358
+ </div>
359
+ <div>
360
+ <button type="button" class="btn btn-danger" onclick="confirmClear()">
361
+ <i class="fas fa-trash"></i> Clear All
362
+ </button>
363
+ <button type="submit" name="action" value="submit" class="btn btn-primary">
364
+ <i class="fas fa-check"></i> Submit Profile
365
+ </button>
366
+ </div>
367
+ </div>
368
+ </form>
369
+ </div>
370
+ </div>
371
+
372
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
373
+ <script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script>
374
+ <script>
375
+ // Initialize sortable
376
+ const sortable = new Sortable(document.getElementById('sortableSections'), {
377
+ animation: 150,
378
+ ghostClass: 'sortable-ghost',
379
+ dragClass: 'dragging',
380
+ handle: '.drag-handle',
381
+ filter: '.fixed',
382
+ onEnd: function(evt) {
383
+ updateSectionOrder();
384
+ updatePreview();
385
+ }
386
+ });
387
+
388
+ // Get all sections data
389
+ const sectionsData = {
390
+ introduction: {% if intro %}true{% else %}false{% endif %},
391
+ profile_summary: {% if summary %}true{% else %}false{% endif %},
392
+ work_experience: {% if work_experiences %}true{% else %}false{% endif %},
393
+ projects: {% if projects %}true{% else %}false{% endif %},
394
+ education: {% if educations %}true{% else %}false{% endif %},
395
+ skills: {% if skills %}true{% else %}false{% endif %},
396
+ achievements: {% if achievements %}true{% else %}false{% endif %}
397
+ };
398
+
399
+ // Render templates for each section
400
+ const sectionTemplates = {
401
+ work_experience: `
402
+ {% for exp in work_experiences %}
403
+ <div class="experience-item">
404
+ <h6>{{ exp.title }}</h6>
405
+ <p class="text-muted mb-1">{{ exp.organization }}</p>
406
+ <p class="date-range mb-2">
407
+ {{ exp.start_month }}/{{ exp.start_year }} -
408
+ {% if exp.end_year %}{{ exp.end_month }}/{{ exp.end_year }}{% else %}Present{% endif %}
409
+ </p>
410
+ {% if exp.remarks %}
411
+ <p class="mb-0">{{ exp.remarks }}</p>
412
+ {% endif %}
413
+ </div>
414
+ {% endfor %}
415
+ `,
416
+ projects: `
417
+ {% for project in projects %}
418
+ <div class="project-item">
419
+ <h6>{{ project.title }}</h6>
420
+ {% if project.organization %}
421
+ <p class="text-muted mb-1">{{ project.organization }}</p>
422
+ {% endif %}
423
+ <p class="date-range mb-2">
424
+ {{ project.start_month }}/{{ project.start_year }} -
425
+ {% if project.end_year %}{{ project.end_month }}/{{ project.end_year }}{% else %}Present{% endif %}
426
+ </p>
427
+ {% if project.remarks %}
428
+ <p class="mb-0">{{ project.remarks }}</p>
429
+ {% endif %}
430
+ </div>
431
+ {% endfor %}
432
+ `,
433
+ education: `
434
+ {% for edu in educations %}
435
+ <div class="education-item">
436
+ <h6>{{ edu.title }}</h6>
437
+ <p class="text-muted mb-1">{{ edu.organization }}</p>
438
+ <p class="date-range mb-2">
439
+ {{ edu.start_month }}/{{ edu.start_year }} -
440
+ {% if edu.end_year %}{{ edu.end_month }}/{{ edu.end_year }}{% else %}Present{% endif %}
441
+ </p>
442
+ {% if edu.remarks %}
443
+ <p class="mb-0">{{ edu.remarks }}</p>
444
+ {% endif %}
445
+ </div>
446
+ {% endfor %}
447
+ `,
448
+ skills: `
449
+ <div class="skills-grid">
450
+ {% for skill in skills %}
451
+ <span class="skill-tag">{{ skill.skill }}</span>
452
+ {% endfor %}
453
+ </div>
454
+ `,
455
+ achievements: `
456
+ <div class="achievements-grid">
457
+ {% for achievement in achievements %}
458
+ <span class="achievement-tag">{{ achievement.achievement }}</span>
459
+ {% endfor %}
460
+ </div>
461
+ `
462
+ };
463
+
464
+ function updateSectionOrder() {
465
+ const items = document.querySelectorAll('#sortableSections .sortable-item');
466
+ const order = [];
467
+
468
+ items.forEach(item => {
469
+ const section = item.dataset.section;
470
+ if (section && sectionsData[section]) {
471
+ order.push(section);
472
+ }
473
+ });
474
+
475
+ document.getElementById('sectionOrderInput').value = JSON.stringify(order);
476
+ }
477
+
478
+ function updatePreview() {
479
+ const items = document.querySelectorAll('#sortableSections .sortable-item');
480
+ const previewContent = document.getElementById('previewContent');
481
+
482
+ // Keep introduction and summary
483
+ const fixedSections = previewContent.querySelectorAll('[data-section="introduction"], [data-section="profile_summary"]');
484
+ const newContent = Array.from(fixedSections);
485
+
486
+ // Add dynamic sections in order
487
+ items.forEach(item => {
488
+ const section = item.dataset.section;
489
+ if (section && section !== 'introduction' && section !== 'profile_summary' && sectionsData[section]) {
490
+ const sectionDiv = document.createElement('div');
491
+ sectionDiv.className = 'preview-section';
492
+ sectionDiv.dataset.section = section;
493
+
494
+ let title = '';
495
+ switch(section) {
496
+ case 'work_experience':
497
+ title = '<i class="fas fa-briefcase"></i> Work Experience';
498
+ break;
499
+ case 'projects':
500
+ title = '<i class="fas fa-project-diagram"></i> Projects';
501
+ break;
502
+ case 'education':
503
+ title = '<i class="fas fa-graduation-cap"></i> Education';
504
+ break;
505
+ case 'skills':
506
+ title = '<i class="fas fa-tools"></i> Skills';
507
+ break;
508
+ case 'achievements':
509
+ title = '<i class="fas fa-trophy"></i> Achievements';
510
+ break;
511
+ }
512
+
513
+ sectionDiv.innerHTML = `<h5>${title}</h5>${sectionTemplates[section]}`;
514
+ newContent.push(sectionDiv);
515
+ }
516
+ });
517
+
518
+ previewContent.innerHTML = '';
519
+ newContent.forEach(section => previewContent.appendChild(section));
520
+ }
521
+
522
+ function confirmClear() {
523
+ if (confirm('Are you sure you want to clear all your profile data? This action cannot be undone.')) {
524
+ document.getElementById('previewForm').innerHTML +=
525
+ '<input type="hidden" name="action" value="clear">';
526
+ document.getElementById('previewForm').submit();
527
+ }
528
+ }
529
+
530
+ // Initialize on page load
531
+ document.addEventListener('DOMContentLoaded', function() {
532
+ updateSectionOrder();
533
+ updatePreview();
534
+ });
535
+ </script>
536
+ </body>
537
+ </html>
templates/create_profile_summary.html ADDED
@@ -0,0 +1,269 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Profile Summary - AI Resume Builder</title>
7
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
+ <style>
10
+ body {
11
+ background-color: #f8f9fa;
12
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
13
+ }
14
+ .form-container {
15
+ max-width: 800px;
16
+ margin: 40px auto;
17
+ padding: 30px;
18
+ background: white;
19
+ border-radius: 10px;
20
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
21
+ }
22
+ .section-title {
23
+ color: #2c3e50;
24
+ margin-bottom: 30px;
25
+ text-align: center;
26
+ font-weight: 600;
27
+ }
28
+ .btn-logout {
29
+ position: absolute;
30
+ top: 20px;
31
+ right: 20px;
32
+ }
33
+ .form-label {
34
+ font-weight: 500;
35
+ margin-bottom: 8px;
36
+ color: #495057;
37
+ }
38
+ .form-control:focus {
39
+ border-color: #4e73df;
40
+ box-shadow: 0 0 0 0.2rem rgba(78, 115, 223, 0.25);
41
+ }
42
+ .navigation-buttons {
43
+ display: flex;
44
+ justify-content: space-between;
45
+ margin-top: 30px;
46
+ padding-top: 20px;
47
+ border-top: 1px solid #e9ecef;
48
+ }
49
+ .error-message {
50
+ color: #e74c3c;
51
+ font-size: 0.875rem;
52
+ margin-top: 5px;
53
+ }
54
+ .progress-indicator {
55
+ margin-bottom: 30px;
56
+ text-align: center;
57
+ color: #6c757d;
58
+ font-size: 0.9rem;
59
+ }
60
+ .progress-indicator span {
61
+ display: inline-block;
62
+ margin: 0 10px;
63
+ }
64
+ .progress-indicator span.active {
65
+ color: #4e73df;
66
+ font-weight: 600;
67
+ }
68
+ .ai-generation-section {
69
+ background-color: #f8f9fa;
70
+ padding: 20px;
71
+ border-radius: 8px;
72
+ margin-top: 20px;
73
+ }
74
+ .ai-generation-section h5 {
75
+ color: #4e73df;
76
+ margin-bottom: 15px;
77
+ }
78
+ .btn-generate-ai {
79
+ background: linear-gradient(135deg, #4e73df 0%, #224abe 100%);
80
+ border: none;
81
+ color: white;
82
+ padding: 10px 20px;
83
+ border-radius: 5px;
84
+ transition: all 0.3s;
85
+ }
86
+ .btn-generate-ai:hover {
87
+ background: linear-gradient(135deg, #224abe 0%, #1a2d8a 100%);
88
+ color: white;
89
+ }
90
+ .btn-generate-ai:disabled {
91
+ background: #6c757d;
92
+ cursor: not-allowed;
93
+ }
94
+ .loading-spinner {
95
+ display: none;
96
+ margin-left: 10px;
97
+ }
98
+ .ai-generated-label {
99
+ background-color: #d4edda;
100
+ color: #155724;
101
+ padding: 5px 10px;
102
+ border-radius: 4px;
103
+ font-size: 0.875rem;
104
+ margin-top: 10px;
105
+ display: none;
106
+ }
107
+ textarea.form-control {
108
+ min-height: 150px;
109
+ resize: vertical;
110
+ }
111
+ .help-text {
112
+ font-size: 0.875rem;
113
+ color: #6c757d;
114
+ margin-top: 5px;
115
+ }
116
+ </style>
117
+ </head>
118
+ <body>
119
+ <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
120
+ <div class="container">
121
+ <a class="navbar-brand" href="{{ url_for('profile') }}">AI Resume Builder</a>
122
+ <div class="ms-auto">
123
+ <a href="{{ url_for('logout') }}" class="btn btn-outline-light">
124
+ <i class="fas fa-sign-out-alt"></i> Logout
125
+ </a>
126
+ </div>
127
+ </div>
128
+ </nav>
129
+
130
+ <div class="container">
131
+ <div class="form-container">
132
+ <div class="progress-indicator">
133
+ <span>Introduction</span>
134
+ <span>→</span>
135
+ <span class="active">Profile Summary</span>
136
+ <span>→</span>
137
+ <span>Work Experience</span>
138
+ <span>→</span>
139
+ <span>Projects</span>
140
+ <span>→</span>
141
+ <span>Education</span>
142
+ <span>→</span>
143
+ <span>Skills</span>
144
+ <span>→</span>
145
+ <span>Achievements</span>
146
+ <span>→</span>
147
+ <span>Preview</span>
148
+ </div>
149
+
150
+ <h2 class="section-title">Profile Summary</h2>
151
+
152
+ {% with messages = get_flashed_messages(with_categories=true) %}
153
+ {% if messages %}
154
+ {% for category, message in messages %}
155
+ <div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
156
+ {{ message }}
157
+ <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
158
+ </div>
159
+ {% endfor %}
160
+ {% endif %}
161
+ {% endwith %}
162
+
163
+ <form method="POST" action="{{ url_for('create_profile_summary') }}" id="profileSummaryForm">
164
+ <div class="mb-3">
165
+ <label for="summary" class="form-label">Profile Summary</label>
166
+ <textarea class="form-control" id="summary" name="summary" rows="6" placeholder="Write a brief summary about yourself...">{{ form_data.summary if form_data }}</textarea>
167
+ <div class="help-text">
168
+ Write a concise summary of your professional background, skills, and career goals (2-4 paragraphs).
169
+ </div>
170
+ {% if form_errors.summary %}
171
+ <div class="error-message">{{ form_errors.summary[0] }}</div>
172
+ {% endif %}
173
+ </div>
174
+
175
+ <div class="ai-generation-section">
176
+ <h5><i class="fas fa-magic"></i> Generate with AI</h5>
177
+ <p class="text-muted mb-3">
178
+ Let AI help you create a compelling profile summary based on your introduction.
179
+ The AI will generate a professional summary that highlights your strengths.
180
+ </p>
181
+ <button type="button" class="btn btn-generate-ai" id="generateAIBtn" onclick="generateAISummary()">
182
+ <i class="fas fa-wand-magic-sparkles"></i> Generate Profile Summary with AI
183
+ <span class="spinner-border spinner-border-sm loading-spinner" id="aiLoading" role="status" aria-hidden="true"></span>
184
+ </button>
185
+ <div class="ai-generated-label" id="aiGeneratedLabel">
186
+ <i class="fas fa-check-circle"></i> AI-generated summary
187
+ </div>
188
+ </div>
189
+
190
+ <div class="navigation-buttons">
191
+ <div>
192
+ <a href="{{ url_for('create_introduction') }}" class="btn btn-outline-secondary">
193
+ <i class="fas fa-arrow-left"></i> Back
194
+ </a>
195
+ </div>
196
+ <div>
197
+ <button type="submit" class="btn btn-primary">
198
+ Next <i class="fas fa-arrow-right"></i>
199
+ </button>
200
+ </div>
201
+ </div>
202
+ </form>
203
+ </div>
204
+ </div>
205
+
206
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
207
+ <script>
208
+ function generateAISummary() {
209
+ const btn = document.getElementById('generateAIBtn');
210
+ const loading = document.getElementById('aiLoading');
211
+ const aiLabel = document.getElementById('aiGeneratedLabel');
212
+ const summaryTextarea = document.getElementById('summary');
213
+
214
+ btn.disabled = true;
215
+ loading.style.display = 'inline-block';
216
+ aiLabel.style.display = 'none';
217
+
218
+ fetch('/profile/create/generate-summary', {
219
+ method: 'POST',
220
+ headers: {
221
+ 'Content-Type': 'application/json',
222
+ 'X-CSRFToken': '{{ csrf_token() if csrf_token else "" }}'
223
+ },
224
+ credentials: 'same-origin'
225
+ })
226
+ .then(response => response.json())
227
+ .then(data => {
228
+ if (data.success) {
229
+ summaryTextarea.value = data.summary;
230
+ aiLabel.style.display = 'block';
231
+
232
+ // Show success message
233
+ const alertDiv = document.createElement('div');
234
+ alertDiv.className = 'alert alert-success alert-dismissible fade show mt-3';
235
+ alertDiv.innerHTML = `
236
+ ${data.message}
237
+ <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
238
+ `;
239
+ document.querySelector('.ai-generation-section').appendChild(alertDiv);
240
+ } else {
241
+ // Show error message
242
+ const alertDiv = document.createElement('div');
243
+ alertDiv.className = 'alert alert-danger alert-dismissible fade show mt-3';
244
+ alertDiv.innerHTML = `
245
+ ${data.message}
246
+ <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
247
+ `;
248
+ document.querySelector('.ai-generation-section').appendChild(alertDiv);
249
+ }
250
+ })
251
+ .catch(error => {
252
+ console.error('Error:', error);
253
+ // Show error message
254
+ const alertDiv = document.createElement('div');
255
+ alertDiv.className = 'alert alert-danger alert-dismissible fade show mt-3';
256
+ alertDiv.innerHTML = `
257
+ Failed to generate summary. Please try again.
258
+ <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
259
+ `;
260
+ document.querySelector('.ai-generation-section').appendChild(alertDiv);
261
+ })
262
+ .finally(() => {
263
+ btn.disabled = false;
264
+ loading.style.display = 'none';
265
+ });
266
+ }
267
+ </script>
268
+ </body>
269
+ </html>
templates/create_projects.html ADDED
@@ -0,0 +1,433 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Projects - AI Resume Builder</title>
7
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
+ <style>
10
+ body {
11
+ background-color: #f8f9fa;
12
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
13
+ }
14
+ .form-container {
15
+ max-width: 900px;
16
+ margin: 40px auto;
17
+ padding: 30px;
18
+ background: white;
19
+ border-radius: 10px;
20
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
21
+ }
22
+ .section-title {
23
+ color: #2c3e50;
24
+ margin-bottom: 30px;
25
+ text-align: center;
26
+ font-weight: 600;
27
+ }
28
+ .btn-logout {
29
+ position: absolute;
30
+ top: 20px;
31
+ right: 20px;
32
+ }
33
+ .form-label {
34
+ font-weight: 500;
35
+ margin-bottom: 8px;
36
+ color: #495057;
37
+ }
38
+ .form-control:focus {
39
+ border-color: #4e73df;
40
+ box-shadow: 0 0 0 0.2rem rgba(78, 115, 223, 0.25);
41
+ }
42
+ .navigation-buttons {
43
+ display: flex;
44
+ justify-content: space-between;
45
+ margin-top: 30px;
46
+ padding-top: 20px;
47
+ border-top: 1px solid #e9ecef;
48
+ }
49
+ .error-message {
50
+ color: #e74c3c;
51
+ font-size: 0.875rem;
52
+ margin-top: 5px;
53
+ }
54
+ .progress-indicator {
55
+ margin-bottom: 30px;
56
+ text-align: center;
57
+ color: #6c757d;
58
+ font-size: 0.9rem;
59
+ }
60
+ .progress-indicator span {
61
+ display: inline-block;
62
+ margin: 0 10px;
63
+ }
64
+ .progress-indicator span.active {
65
+ color: #4e73df;
66
+ font-weight: 600;
67
+ }
68
+ .project-item {
69
+ background-color: #f8f9fa;
70
+ border: 1px solid #e9ecef;
71
+ border-radius: 8px;
72
+ padding: 20px;
73
+ margin-bottom: 20px;
74
+ position: relative;
75
+ }
76
+ .project-item.has-error {
77
+ border-color: #e74c3c;
78
+ background-color: #fdf2f2;
79
+ }
80
+ .btn-remove {
81
+ position: absolute;
82
+ top: 10px;
83
+ right: 10px;
84
+ color: #e74c3c;
85
+ background: white;
86
+ border: 1px solid #e74c3c;
87
+ padding: 5px 10px;
88
+ border-radius: 5px;
89
+ transition: all 0.3s;
90
+ }
91
+ .btn-remove:hover {
92
+ color: white;
93
+ background-color: #e74c3c;
94
+ }
95
+ .btn-add {
96
+ margin-bottom: 20px;
97
+ background-color: #28a745;
98
+ border: none;
99
+ color: white;
100
+ }
101
+ .btn-add:hover {
102
+ background-color: #218838;
103
+ color: white;
104
+ }
105
+ .present-checkbox {
106
+ margin-top: 10px;
107
+ }
108
+ .date-inputs {
109
+ display: flex;
110
+ gap: 10px;
111
+ }
112
+ .date-inputs .form-control {
113
+ flex: 1;
114
+ }
115
+ .empty-state {
116
+ text-align: center;
117
+ padding: 40px;
118
+ color: #6c757d;
119
+ }
120
+ .empty-state i {
121
+ font-size: 3rem;
122
+ margin-bottom: 20px;
123
+ color: #dee2e6;
124
+ }
125
+ .section-header {
126
+ display: flex;
127
+ justify-content: space-between;
128
+ align-items: center;
129
+ margin-bottom: 20px;
130
+ }
131
+ .optional-badge {
132
+ background-color: #6c757d;
133
+ color: white;
134
+ padding: 2px 8px;
135
+ border-radius: 4px;
136
+ font-size: 0.75rem;
137
+ margin-left: 5px;
138
+ }
139
+ </style>
140
+ </head>
141
+ <body>
142
+ <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
143
+ <div class="container">
144
+ <a class="navbar-brand" href="{{ url_for('profile') }}">AI Resume Builder</a>
145
+ <div class="ms-auto">
146
+ <a href="{{ url_for('logout') }}" class="btn btn-outline-light">
147
+ <i class="fas fa-sign-out-alt"></i> Logout
148
+ </a>
149
+ </div>
150
+ </div>
151
+ </nav>
152
+
153
+ <div class="container">
154
+ <div class="form-container">
155
+ <div class="progress-indicator">
156
+ <span>Introduction</span>
157
+ <span>→</span>
158
+ <span>Profile Summary</span>
159
+ <span>→</span>
160
+ <span>Work Experience</span>
161
+ <span>→</span>
162
+ <span class="active">Projects</span>
163
+ <span>→</span>
164
+ <span>Education</span>
165
+ <span>→</span>
166
+ <span>Skills</span>
167
+ <span>→</span>
168
+ <span>Achievements</span>
169
+ <span>→</span>
170
+ <span>Preview</span>
171
+ </div>
172
+
173
+ <div class="section-header">
174
+ <h2 class="section-title mb-0">Projects</h2>
175
+ <button type="button" class="btn btn-add" onclick="addProject()">
176
+ <i class="fas fa-plus"></i> Add Project
177
+ </button>
178
+ </div>
179
+
180
+ {% with messages = get_flashed_messages(with_categories=true) %}
181
+ {% if messages %}
182
+ {% for category, message in messages %}
183
+ <div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
184
+ {{ message }}
185
+ <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
186
+ </div>
187
+ {% endfor %}
188
+ {% endif %}
189
+ {% endwith %}
190
+
191
+ <form method="POST" action="{{ url_for('create_projects') }}" id="projectsForm">
192
+ <div id="projectsContainer">
193
+ {% if form_data and form_data.projects %}
194
+ {% for project in form_data.projects %}
195
+ <div class="project-item">
196
+ <button type="button" class="btn btn-sm btn-remove" onclick="removeProject(this)">
197
+ <i class="fas fa-times"></i>
198
+ </button>
199
+
200
+ <div class="row">
201
+ <div class="col-md-6 mb-3">
202
+ <label class="form-label">Organization <span class="optional-badge">Optional</span></label>
203
+ <input type="text" class="form-control" name="organization[]" value="{{ project.organization }}">
204
+ </div>
205
+ <div class="col-md-6 mb-3">
206
+ <label class="form-label required">Project Title</label>
207
+ <input type="text" class="form-control" name="title[]" value="{{ project.title }}" required>
208
+ </div>
209
+ </div>
210
+
211
+ <div class="row">
212
+ <div class="col-md-6 mb-3">
213
+ <label class="form-label required">Start Date</label>
214
+ <div class="date-inputs">
215
+ <select class="form-control" name="start_month[]" required>
216
+ <option value="">Month</option>
217
+ {% for i in range(1, 13) %}
218
+ <option value="{{ i }}" {% if project.start_month == i %}selected{% endif %}>
219
+ {{ i }}
220
+ </option>
221
+ {% endfor %}
222
+ </select>
223
+ <select class="form-control" name="start_year[]" required>
224
+ <option value="">Year</option>
225
+ {% for year in range(1980, current_year + 1) %}
226
+ <option value="{{ year }}" {% if project.start_year == year %}selected{% endif %}>
227
+ {{ year }}
228
+ </option>
229
+ {% endfor %}
230
+ </select>
231
+ </div>
232
+ </div>
233
+ <div class="col-md-6 mb-3">
234
+ <label class="form-label">End Date</label>
235
+ <div class="date-inputs">
236
+ <select class="form-control" name="end_month[]" {% if project.is_present %}disabled{% endif %}>
237
+ <option value="">Month</option>
238
+ {% for i in range(1, 13) %}
239
+ <option value="{{ i }}" {% if project.end_month == i %}selected{% endif %}>
240
+ {{ i }}
241
+ </option>
242
+ {% endfor %}
243
+ </select>
244
+ <select class="form-control" name="end_year[]" {% if project.is_present %}disabled{% endif %}>
245
+ <option value="">Year</option>
246
+ {% for year in range(1980, current_year + 1) %}
247
+ <option value="{{ year }}" {% if project.end_year == year %}selected{% endif %}>
248
+ {{ year }}
249
+ </option>
250
+ {% endfor %}
251
+ </select>
252
+ </div>
253
+ <div class="present-checkbox">
254
+ <div class="form-check">
255
+ <input class="form-check-input" type="checkbox" name="is_present[]" id="present_project_{{ loop.index0 }}"
256
+ {% if project.is_present %}checked{% endif %}
257
+ onchange="toggleEndDate(this)">
258
+ <label class="form-check-label" for="present_project_{{ loop.index0 }}">
259
+ Present
260
+ </label>
261
+ </div>
262
+ </div>
263
+ </div>
264
+ </div>
265
+
266
+ <div class="mb-3">
267
+ <label class="form-label">Description</label>
268
+ <textarea class="form-control" name="remarks[]" rows="3" placeholder="Describe the project, your role, technologies used, and outcomes...">{{ project.remarks }}</textarea>
269
+ </div>
270
+ </div>
271
+ {% endfor %}
272
+ {% else %}
273
+ <div class="empty-state">
274
+ <i class="fas fa-project-diagram"></i>
275
+ <h5>No Projects Added</h5>
276
+ <p>Click "Add Project" to add your project experience</p>
277
+ </div>
278
+ {% endif %}
279
+ </div>
280
+
281
+ <div class="navigation-buttons">
282
+ <div>
283
+ <a href="{{ url_for('create_work_experience') }}" class="btn btn-outline-secondary">
284
+ <i class="fas fa-arrow-left"></i> Back
285
+ </a>
286
+ <button type="button" class="btn btn-outline-primary" onclick="skipToEducation()">
287
+ Skip <i class="fas fa-forward"></i>
288
+ </button>
289
+ </div>
290
+ <div>
291
+ <button type="submit" class="btn btn-primary">
292
+ Next <i class="fas fa-arrow-right"></i>
293
+ </button>
294
+ </div>
295
+ </div>
296
+ </form>
297
+ </div>
298
+ </div>
299
+
300
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
301
+ <script>
302
+ let projectCount = {{ form_data.projects|length if form_data and form_data.projects else 0 }};
303
+
304
+ function addProject() {
305
+ const container = document.getElementById('projectsContainer');
306
+ const emptyState = container.querySelector('.empty-state');
307
+ if (emptyState) {
308
+ emptyState.remove();
309
+ }
310
+
311
+ const div = document.createElement('div');
312
+ div.className = 'project-item';
313
+ div.innerHTML = `
314
+ <button type="button" class="btn btn-sm btn-remove" onclick="removeProject(this)">
315
+ <i class="fas fa-times"></i>
316
+ </button>
317
+
318
+ <div class="row">
319
+ <div class="col-md-6 mb-3">
320
+ <label class="form-label">Organization <span class="optional-badge">Optional</span></label>
321
+ <input type="text" class="form-control" name="organization[]">
322
+ </div>
323
+ <div class="col-md-6 mb-3">
324
+ <label class="form-label required">Project Title</label>
325
+ <input type="text" class="form-control" name="title[]" required>
326
+ </div>
327
+ </div>
328
+
329
+ <div class="row">
330
+ <div class="col-md-6 mb-3">
331
+ <label class="form-label required">Start Date</label>
332
+ <div class="date-inputs">
333
+ <select class="form-control" name="start_month[]" required>
334
+ <option value="">Month</option>
335
+ ${generateMonthOptions()}
336
+ </select>
337
+ <select class="form-control" name="start_year[]" required>
338
+ <option value="">Year</option>
339
+ ${generateYearOptions()}
340
+ </select>
341
+ </div>
342
+ </div>
343
+ <div class="col-md-6 mb-3">
344
+ <label class="form-label">End Date</label>
345
+ <div class="date-inputs">
346
+ <select class="form-control" name="end_month[]">
347
+ <option value="">Month</option>
348
+ ${generateMonthOptions()}
349
+ </select>
350
+ <select class="form-control" name="end_year[]">
351
+ <option value="">Year</option>
352
+ ${generateYearOptions()}
353
+ </select>
354
+ </div>
355
+ <div class="present-checkbox">
356
+ <div class="form-check">
357
+ <input class="form-check-input" type="checkbox" name="is_present[]" id="present_project_${projectCount}" onchange="toggleEndDate(this)">
358
+ <label class="form-check-label" for="present_project_${projectCount}">
359
+ Present
360
+ </label>
361
+ </div>
362
+ </div>
363
+ </div>
364
+ </div>
365
+
366
+ <div class="mb-3">
367
+ <label class="form-label">Description</label>
368
+ <textarea class="form-control" name="remarks[]" rows="3" placeholder="Describe the project, your role, technologies used, and outcomes..."></textarea>
369
+ </div>
370
+ `;
371
+
372
+ container.appendChild(div);
373
+ projectCount++;
374
+ }
375
+
376
+ function removeProject(button) {
377
+ const item = button.closest('.project-item');
378
+ item.remove();
379
+
380
+ const container = document.getElementById('projectsContainer');
381
+ const remainingItems = container.querySelectorAll('.project-item');
382
+
383
+ if (remainingItems.length === 0) {
384
+ container.innerHTML = `
385
+ <div class="empty-state">
386
+ <i class="fas fa-project-diagram"></i>
387
+ <h5>No Projects Added</h5>
388
+ <p>Click "Add Project" to add your project experience</p>
389
+ </div>
390
+ `;
391
+ }
392
+ }
393
+
394
+ function toggleEndDate(checkbox) {
395
+ const item = checkbox.closest('.project-item');
396
+ const endMonth = item.querySelector('select[name="end_month[]"]');
397
+ const endYear = item.querySelector('select[name="end_year[]"]');
398
+
399
+ if (checkbox.checked) {
400
+ endMonth.disabled = true;
401
+ endYear.disabled = true;
402
+ endMonth.value = '';
403
+ endYear.value = '';
404
+ } else {
405
+ endMonth.disabled = false;
406
+ endYear.disabled = false;
407
+ }
408
+ }
409
+
410
+ function skipToEducation() {
411
+ document.getElementById('projectsForm').action = '{{ url_for("create_education") }}';
412
+ document.getElementById('projectsForm').submit();
413
+ }
414
+
415
+ function generateMonthOptions() {
416
+ let options = '';
417
+ for (let i = 1; i <= 12; i++) {
418
+ options += `<option value="${i}">${i}</option>`;
419
+ }
420
+ return options;
421
+ }
422
+
423
+ function generateYearOptions() {
424
+ const currentYear = new Date().getFullYear();
425
+ let options = '';
426
+ for (let year = 1980; year <= currentYear + 1; year++) {
427
+ options += `<option value="${year}">${year}</option>`;
428
+ }
429
+ return options;
430
+ }
431
+ </script>
432
+ </body>
433
+ </html>
templates/create_skills.html ADDED
@@ -0,0 +1,263 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Skills - AI Resume Builder</title>
7
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
+ <style>
10
+ body {
11
+ background-color: #f8f9fa;
12
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
13
+ }
14
+ .form-container {
15
+ max-width: 800px;
16
+ margin: 40px auto;
17
+ padding: 30px;
18
+ background: white;
19
+ border-radius: 10px;
20
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
21
+ }
22
+ .section-title {
23
+ color: #2c3e50;
24
+ margin-bottom: 30px;
25
+ text-align: center;
26
+ font-weight: 600;
27
+ }
28
+ .btn-logout {
29
+ position: absolute;
30
+ top: 20px;
31
+ right: 20px;
32
+ }
33
+ .form-label {
34
+ font-weight: 500;
35
+ margin-bottom: 8px;
36
+ color: #495057;
37
+ }
38
+ .form-control:focus {
39
+ border-color: #4e73df;
40
+ box-shadow: 0 0 0 0.2rem rgba(78, 115, 223, 0.25);
41
+ }
42
+ .navigation-buttons {
43
+ display: flex;
44
+ justify-content: space-between;
45
+ margin-top: 30px;
46
+ padding-top: 20px;
47
+ border-top: 1px solid #e9ecef;
48
+ }
49
+ .error-message {
50
+ color: #e74c3c;
51
+ font-size: 0.875rem;
52
+ margin-top: 5px;
53
+ }
54
+ .progress-indicator {
55
+ margin-bottom: 30px;
56
+ text-align: center;
57
+ color: #6c757d;
58
+ font-size: 0.9rem;
59
+ }
60
+ .progress-indicator span {
61
+ display: inline-block;
62
+ margin: 0 10px;
63
+ }
64
+ .progress-indicator span.active {
65
+ color: #4e73df;
66
+ font-weight: 600;
67
+ }
68
+ .skills-display {
69
+ background-color: #f8f9fa;
70
+ border: 1px solid #e9ecef;
71
+ border-radius: 8px;
72
+ padding: 20px;
73
+ margin-top: 20px;
74
+ min-height: 100px;
75
+ }
76
+ .skill-tag {
77
+ display: inline-block;
78
+ background-color: #4e73df;
79
+ color: white;
80
+ padding: 5px 12px;
81
+ margin: 5px;
82
+ border-radius: 20px;
83
+ font-size: 0.9rem;
84
+ position: relative;
85
+ }
86
+ .skill-tag .remove-skill {
87
+ margin-left: 8px;
88
+ cursor: pointer;
89
+ font-size: 0.8rem;
90
+ opacity: 0.7;
91
+ }
92
+ .skill-tag .remove-skill:hover {
93
+ opacity: 1;
94
+ }
95
+ .empty-skills {
96
+ text-align: center;
97
+ color: #6c757d;
98
+ font-style: italic;
99
+ padding: 20px;
100
+ }
101
+ .help-text {
102
+ font-size: 0.875rem;
103
+ color: #6c757d;
104
+ margin-top: 5px;
105
+ }
106
+ .skills-preview-section {
107
+ margin-top: 30px;
108
+ }
109
+ .skills-preview-section h5 {
110
+ color: #495057;
111
+ margin-bottom: 15px;
112
+ }
113
+ </style>
114
+ </head>
115
+ <body>
116
+ <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
117
+ <div class="container">
118
+ <a class="navbar-brand" href="{{ url_for('profile') }}">AI Resume Builder</a>
119
+ <div class="ms-auto">
120
+ <a href="{{ url_for('logout') }}" class="btn btn-outline-light">
121
+ <i class="fas fa-sign-out-alt"></i> Logout
122
+ </a>
123
+ </div>
124
+ </div>
125
+ </nav>
126
+
127
+ <div class="container">
128
+ <div class="form-container">
129
+ <div class="progress-indicator">
130
+ <span>Introduction</span>
131
+ <span>→</span>
132
+ <span>Profile Summary</span>
133
+ <span>→</span>
134
+ <span>Work Experience</span>
135
+ <span>→</span>
136
+ <span>Projects</span>
137
+ <span>→</span>
138
+ <span>Education</span>
139
+ <span>→</span>
140
+ <span class="active">Skills</span>
141
+ <span>→</span>
142
+ <span>Achievements</span>
143
+ <span>→</span>
144
+ <span>Preview</span>
145
+ </div>
146
+
147
+ <h2 class="section-title">Skills</h2>
148
+
149
+ {% with messages = get_flashed_messages(with_categories=true) %}
150
+ {% if messages %}
151
+ {% for category, message in messages %}
152
+ <div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
153
+ {{ message }}
154
+ <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
155
+ </div>
156
+ {% endfor %}
157
+ {% endif %}
158
+ {% endwith %}
159
+
160
+ <form method="POST" action="{{ url_for('create_skills') }}" id="skillsForm">
161
+ <div class="mb-3">
162
+ <label for="skills" class="form-label">Enter Your Skills</label>
163
+ <textarea class="form-control" id="skills" name="skills" rows="4"
164
+ placeholder="Enter your skills separated by commas. For example: Python, JavaScript, Project Management, Communication, Data Analysis...">{{ form_data.skills if form_data }}</textarea>
165
+ <div class="help-text">
166
+ Enter your technical and soft skills separated by commas. Each skill will be displayed as a separate tag.
167
+ </div>
168
+ {% if form_errors.skills %}
169
+ <div class="error-message">{{ form_errors.skills[0] }}</div>
170
+ {% endif %}
171
+ </div>
172
+
173
+ <div class="skills-preview-section">
174
+ <h5><i class="fas fa-eye"></i> Skills Preview</h5>
175
+ <div class="skills-display" id="skillsPreview">
176
+ {% if form_data.skills_preview %}
177
+ {% for skill in form_data.skills_preview %}
178
+ <span class="skill-tag">
179
+ {{ skill }}
180
+ <span class="remove-skill" onclick="removeSkill(this)">×</span>
181
+ </span>
182
+ {% endfor %}
183
+ {% else %}
184
+ <div class="empty-skills">
185
+ Your skills will appear here as you type them above
186
+ </div>
187
+ {% endif %}
188
+ </div>
189
+ </div>
190
+
191
+ <div class="navigation-buttons">
192
+ <div>
193
+ <a href="{{ url_for('create_education') }}" class="btn btn-outline-secondary">
194
+ <i class="fas fa-arrow-left"></i> Back
195
+ </a>
196
+ <button type="button" class="btn btn-outline-primary" onclick="skipToAchievements()">
197
+ Skip <i class="fas fa-forward"></i>
198
+ </button>
199
+ </div>
200
+ <div>
201
+ <button type="submit" class="btn btn-primary">
202
+ Next <i class="fas fa-arrow-right"></i>
203
+ </button>
204
+ </div>
205
+ </div>
206
+ </form>
207
+ </div>
208
+ </div>
209
+
210
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
211
+ <script>
212
+ const skillsTextarea = document.getElementById('skills');
213
+ const skillsPreview = document.getElementById('skillsPreview');
214
+ let skills = [];
215
+
216
+ // Initialize skills from form data if exists
217
+ {% if form_data.skills_preview %}
218
+ skills = {{ form_data.skills_preview|tojson }};
219
+ {% endif %}
220
+
221
+ function updateSkillsPreview() {
222
+ const input = skillsTextarea.value;
223
+ const newSkills = input.split(',').map(skill => skill.trim()).filter(skill => skill);
224
+
225
+ // Update preview
226
+ if (newSkills.length === 0) {
227
+ skillsPreview.innerHTML = '<div class="empty-skills">Your skills will appear here as you type them above</div>';
228
+ } else {
229
+ skillsPreview.innerHTML = newSkills.map(skill => `
230
+ <span class="skill-tag">
231
+ ${skill}
232
+ <span class="remove-skill" onclick="removeSkill(this)">×</span>
233
+ </span>
234
+ `).join('');
235
+ }
236
+ }
237
+
238
+ function removeSkill(element) {
239
+ const skillTag = element.parentElement;
240
+ const skillText = skillTag.textContent.replace('×', '').trim();
241
+
242
+ // Remove from textarea
243
+ const currentSkills = skillsTextarea.value.split(',').map(s => s.trim()).filter(s => s);
244
+ const updatedSkills = currentSkills.filter(skill => skill !== skillText);
245
+ skillsTextarea.value = updatedSkills.join(', ');
246
+
247
+ // Update preview
248
+ updateSkillsPreview();
249
+ }
250
+
251
+ function skipToAchievements() {
252
+ document.getElementById('skillsForm').action = '{{ url_for("create_achievements") }}';
253
+ document.getElementById('skillsForm').submit();
254
+ }
255
+
256
+ // Update preview as user types
257
+ skillsTextarea.addEventListener('input', updateSkillsPreview);
258
+
259
+ // Initialize preview on page load
260
+ updateSkillsPreview();
261
+ </script>
262
+ </body>
263
+ </html>
templates/create_work_experience.html ADDED
@@ -0,0 +1,425 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Work Experience - AI Resume Builder</title>
7
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
+ <style>
10
+ body {
11
+ background-color: #f8f9fa;
12
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
13
+ }
14
+ .form-container {
15
+ max-width: 900px;
16
+ margin: 40px auto;
17
+ padding: 30px;
18
+ background: white;
19
+ border-radius: 10px;
20
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
21
+ }
22
+ .section-title {
23
+ color: #2c3e50;
24
+ margin-bottom: 30px;
25
+ text-align: center;
26
+ font-weight: 600;
27
+ }
28
+ .btn-logout {
29
+ position: absolute;
30
+ top: 20px;
31
+ right: 20px;
32
+ }
33
+ .form-label {
34
+ font-weight: 500;
35
+ margin-bottom: 8px;
36
+ color: #495057;
37
+ }
38
+ .form-control:focus {
39
+ border-color: #4e73df;
40
+ box-shadow: 0 0 0 0.2rem rgba(78, 115, 223, 0.25);
41
+ }
42
+ .navigation-buttons {
43
+ display: flex;
44
+ justify-content: space-between;
45
+ margin-top: 30px;
46
+ padding-top: 20px;
47
+ border-top: 1px solid #e9ecef;
48
+ }
49
+ .error-message {
50
+ color: #e74c3c;
51
+ font-size: 0.875rem;
52
+ margin-top: 5px;
53
+ }
54
+ .progress-indicator {
55
+ margin-bottom: 30px;
56
+ text-align: center;
57
+ color: #6c757d;
58
+ font-size: 0.9rem;
59
+ }
60
+ .progress-indicator span {
61
+ display: inline-block;
62
+ margin: 0 10px;
63
+ }
64
+ .progress-indicator span.active {
65
+ color: #4e73df;
66
+ font-weight: 600;
67
+ }
68
+ .work-experience-item {
69
+ background-color: #f8f9fa;
70
+ border: 1px solid #e9ecef;
71
+ border-radius: 8px;
72
+ padding: 20px;
73
+ margin-bottom: 20px;
74
+ position: relative;
75
+ }
76
+ .work-experience-item.has-error {
77
+ border-color: #e74c3c;
78
+ background-color: #fdf2f2;
79
+ }
80
+ .btn-remove {
81
+ position: absolute;
82
+ top: 10px;
83
+ right: 10px;
84
+ color: #e74c3c;
85
+ background: white;
86
+ border: 1px solid #e74c3c;
87
+ padding: 5px 10px;
88
+ border-radius: 5px;
89
+ transition: all 0.3s;
90
+ }
91
+ .btn-remove:hover {
92
+ color: white;
93
+ background-color: #e74c3c;
94
+ }
95
+ .btn-add {
96
+ margin-bottom: 20px;
97
+ background-color: #28a745;
98
+ border: none;
99
+ color: white;
100
+ }
101
+ .btn-add:hover {
102
+ background-color: #218838;
103
+ color: white;
104
+ }
105
+ .present-checkbox {
106
+ margin-top: 10px;
107
+ }
108
+ .date-inputs {
109
+ display: flex;
110
+ gap: 10px;
111
+ }
112
+ .date-inputs .form-control {
113
+ flex: 1;
114
+ }
115
+ .empty-state {
116
+ text-align: center;
117
+ padding: 40px;
118
+ color: #6c757d;
119
+ }
120
+ .empty-state i {
121
+ font-size: 3rem;
122
+ margin-bottom: 20px;
123
+ color: #dee2e6;
124
+ }
125
+ .section-header {
126
+ display: flex;
127
+ justify-content: space-between;
128
+ align-items: center;
129
+ margin-bottom: 20px;
130
+ }
131
+ </style>
132
+ </head>
133
+ <body>
134
+ <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
135
+ <div class="container">
136
+ <a class="navbar-brand" href="{{ url_for('profile') }}">AI Resume Builder</a>
137
+ <div class="ms-auto">
138
+ <a href="{{ url_for('logout') }}" class="btn btn-outline-light">
139
+ <i class="fas fa-sign-out-alt"></i> Logout
140
+ </a>
141
+ </div>
142
+ </div>
143
+ </nav>
144
+
145
+ <div class="container">
146
+ <div class="form-container">
147
+ <div class="progress-indicator">
148
+ <span>Introduction</span>
149
+ <span>→</span>
150
+ <span>Profile Summary</span>
151
+ <span>→</span>
152
+ <span class="active">Work Experience</span>
153
+ <span>→</span>
154
+ <span>Projects</span>
155
+ <span>→</span>
156
+ <span>Education</span>
157
+ <span>→</span>
158
+ <span>Skills</span>
159
+ <span>→</span>
160
+ <span>Achievements</span>
161
+ <span>→</span>
162
+ <span>Preview</span>
163
+ </div>
164
+
165
+ <div class="section-header">
166
+ <h2 class="section-title mb-0">Work Experience</h2>
167
+ <button type="button" class="btn btn-add" onclick="addWorkExperience()">
168
+ <i class="fas fa-plus"></i> Add Work Experience
169
+ </button>
170
+ </div>
171
+
172
+ {% with messages = get_flashed_messages(with_categories=true) %}
173
+ {% if messages %}
174
+ {% for category, message in messages %}
175
+ <div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
176
+ {{ message }}
177
+ <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
178
+ </div>
179
+ {% endfor %}
180
+ {% endif %}
181
+ {% endwith %}
182
+
183
+ <form method="POST" action="{{ url_for('create_work_experience') }}" id="workExperienceForm">
184
+ <div id="workExperienceContainer">
185
+ {% if form_data and form_data.work_experiences %}
186
+ {% for exp in form_data.work_experiences %}
187
+ <div class="work-experience-item">
188
+ <button type="button" class="btn btn-sm btn-remove" onclick="removeWorkExperience(this)">
189
+ <i class="fas fa-times"></i>
190
+ </button>
191
+
192
+ <div class="row">
193
+ <div class="col-md-6 mb-3">
194
+ <label class="form-label required">Organization</label>
195
+ <input type="text" class="form-control" name="organization[]" value="{{ exp.organization }}" required>
196
+ </div>
197
+ <div class="col-md-6 mb-3">
198
+ <label class="form-label required">Title</label>
199
+ <input type="text" class="form-control" name="title[]" value="{{ exp.title }}" required>
200
+ </div>
201
+ </div>
202
+
203
+ <div class="row">
204
+ <div class="col-md-6 mb-3">
205
+ <label class="form-label required">Start Date</label>
206
+ <div class="date-inputs">
207
+ <select class="form-control" name="start_month[]" required>
208
+ <option value="">Month</option>
209
+ {% for i in range(1, 13) %}
210
+ <option value="{{ i }}" {% if exp.start_month == i %}selected{% endif %}>
211
+ {{ i }}
212
+ </option>
213
+ {% endfor %}
214
+ </select>
215
+ <select class="form-control" name="start_year[]" required>
216
+ <option value="">Year</option>
217
+ {% for year in range(1980, current_year + 1) %}
218
+ <option value="{{ year }}" {% if exp.start_year == year %}selected{% endif %}>
219
+ {{ year }}
220
+ </option>
221
+ {% endfor %}
222
+ </select>
223
+ </div>
224
+ </div>
225
+ <div class="col-md-6 mb-3">
226
+ <label class="form-label">End Date</label>
227
+ <div class="date-inputs">
228
+ <select class="form-control" name="end_month[]" {% if exp.is_present %}disabled{% endif %}>
229
+ <option value="">Month</option>
230
+ {% for i in range(1, 13) %}
231
+ <option value="{{ i }}" {% if exp.end_month == i %}selected{% endif %}>
232
+ {{ i }}
233
+ </option>
234
+ {% endfor %}
235
+ </select>
236
+ <select class="form-control" name="end_year[]" {% if exp.is_present %}disabled{% endif %}>
237
+ <option value="">Year</option>
238
+ {% for year in range(1980, current_year + 1) %}
239
+ <option value="{{ year }}" {% if exp.end_year == year %}selected{% endif %}>
240
+ {{ year }}
241
+ </option>
242
+ {% endfor %}
243
+ </select>
244
+ </div>
245
+ <div class="present-checkbox">
246
+ <div class="form-check">
247
+ <input class="form-check-input" type="checkbox" name="is_present[]" id="present_{{ loop.index0 }}"
248
+ {% if exp.is_present %}checked{% endif %}
249
+ onchange="toggleEndDate(this)">
250
+ <label class="form-check-label" for="present_{{ loop.index0 }}">
251
+ Present
252
+ </label>
253
+ </div>
254
+ </div>
255
+ </div>
256
+ </div>
257
+
258
+ <div class="mb-3">
259
+ <label class="form-label">Remarks</label>
260
+ <textarea class="form-control" name="remarks[]" rows="3" placeholder="Describe your responsibilities and achievements...">{{ exp.remarks }}</textarea>
261
+ </div>
262
+ </div>
263
+ {% endfor %}
264
+ {% else %}
265
+ <div class="empty-state">
266
+ <i class="fas fa-briefcase"></i>
267
+ <h5>No Work Experience Added</h5>
268
+ <p>Click "Add Work Experience" to add your work history</p>
269
+ </div>
270
+ {% endif %}
271
+ </div>
272
+
273
+ <div class="navigation-buttons">
274
+ <div>
275
+ <a href="{{ url_for('create_profile_summary') }}" class="btn btn-outline-secondary">
276
+ <i class="fas fa-arrow-left"></i> Back
277
+ </a>
278
+ <button type="button" class="btn btn-outline-primary" onclick="skipToProjects()">
279
+ Skip <i class="fas fa-forward"></i>
280
+ </button>
281
+ </div>
282
+ <div>
283
+ <button type="submit" class="btn btn-primary">
284
+ Next <i class="fas fa-arrow-right"></i>
285
+ </button>
286
+ </div>
287
+ </div>
288
+ </form>
289
+ </div>
290
+ </div>
291
+
292
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
293
+ <script>
294
+ let experienceCount = {{ form_data.work_experiences|length if form_data and form_data.work_experiences else 0 }};
295
+
296
+ function addWorkExperience() {
297
+ const container = document.getElementById('workExperienceContainer');
298
+ const emptyState = container.querySelector('.empty-state');
299
+ if (emptyState) {
300
+ emptyState.remove();
301
+ }
302
+
303
+ const div = document.createElement('div');
304
+ div.className = 'work-experience-item';
305
+ div.innerHTML = `
306
+ <button type="button" class="btn btn-sm btn-remove" onclick="removeWorkExperience(this)">
307
+ <i class="fas fa-times"></i>
308
+ </button>
309
+
310
+ <div class="row">
311
+ <div class="col-md-6 mb-3">
312
+ <label class="form-label required">Organization</label>
313
+ <input type="text" class="form-control" name="organization[]" required>
314
+ </div>
315
+ <div class="col-md-6 mb-3">
316
+ <label class="form-label required">Title</label>
317
+ <input type="text" class="form-control" name="title[]" required>
318
+ </div>
319
+ </div>
320
+
321
+ <div class="row">
322
+ <div class="col-md-6 mb-3">
323
+ <label class="form-label required">Start Date</label>
324
+ <div class="date-inputs">
325
+ <select class="form-control" name="start_month[]" required>
326
+ <option value="">Month</option>
327
+ ${generateMonthOptions()}
328
+ </select>
329
+ <select class="form-control" name="start_year[]" required>
330
+ <option value="">Year</option>
331
+ ${generateYearOptions()}
332
+ </select>
333
+ </div>
334
+ </div>
335
+ <div class="col-md-6 mb-3">
336
+ <label class="form-label">End Date</label>
337
+ <div class="date-inputs">
338
+ <select class="form-control" name="end_month[]">
339
+ <option value="">Month</option>
340
+ ${generateMonthOptions()}
341
+ </select>
342
+ <select class="form-control" name="end_year[]">
343
+ <option value="">Year</option>
344
+ ${generateYearOptions()}
345
+ </select>
346
+ </div>
347
+ <div class="present-checkbox">
348
+ <div class="form-check">
349
+ <input class="form-check-input" type="checkbox" name="is_present[]" id="present_${experienceCount}" onchange="toggleEndDate(this)">
350
+ <label class="form-check-label" for="present_${experienceCount}">
351
+ Present
352
+ </label>
353
+ </div>
354
+ </div>
355
+ </div>
356
+ </div>
357
+
358
+ <div class="mb-3">
359
+ <label class="form-label">Remarks</label>
360
+ <textarea class="form-control" name="remarks[]" rows="3" placeholder="Describe your responsibilities and achievements..."></textarea>
361
+ </div>
362
+ `;
363
+
364
+ container.appendChild(div);
365
+ experienceCount++;
366
+ }
367
+
368
+ function removeWorkExperience(button) {
369
+ const item = button.closest('.work-experience-item');
370
+ item.remove();
371
+
372
+ const container = document.getElementById('workExperienceContainer');
373
+ const remainingItems = container.querySelectorAll('.work-experience-item');
374
+
375
+ if (remainingItems.length === 0) {
376
+ container.innerHTML = `
377
+ <div class="empty-state">
378
+ <i class="fas fa-briefcase"></i>
379
+ <h5>No Work Experience Added</h5>
380
+ <p>Click "Add Work Experience" to add your work history</p>
381
+ </div>
382
+ `;
383
+ }
384
+ }
385
+
386
+ function toggleEndDate(checkbox) {
387
+ const item = checkbox.closest('.work-experience-item');
388
+ const endMonth = item.querySelector('select[name="end_month[]"]');
389
+ const endYear = item.querySelector('select[name="end_year[]"]');
390
+
391
+ if (checkbox.checked) {
392
+ endMonth.disabled = true;
393
+ endYear.disabled = true;
394
+ endMonth.value = '';
395
+ endYear.value = '';
396
+ } else {
397
+ endMonth.disabled = false;
398
+ endYear.disabled = false;
399
+ }
400
+ }
401
+
402
+ function skipToProjects() {
403
+ document.getElementById('workExperienceForm').action = '{{ url_for("create_projects") }}';
404
+ document.getElementById('workExperienceForm').submit();
405
+ }
406
+
407
+ function generateMonthOptions() {
408
+ let options = '';
409
+ for (let i = 1; i <= 12; i++) {
410
+ options += `<option value="${i}">${i}</option>`;
411
+ }
412
+ return options;
413
+ }
414
+
415
+ function generateYearOptions() {
416
+ const currentYear = new Date().getFullYear();
417
+ let options = '';
418
+ for (let year = 1980; year <= currentYear + 1; year++) {
419
+ options += `<option value="${year}">${year}</option>`;
420
+ }
421
+ return options;
422
+ }
423
+ </script>
424
+ </body>
425
+ </html>
templates/forgot_password.html ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Forgot Password - AI Resume Builder{% endblock %}
4
+
5
+ {% block content %}
6
+ <div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
7
+ <div class="max-w-md w-full space-y-8">
8
+ <div class="text-center">
9
+ <h2 class="text-2xl font-semibold text-gray-800 mb-6">
10
+ Forgot your password?
11
+ </h2>
12
+ <p class="text-gray-600">
13
+ Enter your email address and we'll send you a link to reset your password.
14
+ </p>
15
+ </div>
16
+
17
+ <form method="POST" action="{{ url_for('forgot_password') }}" class="mt-8 space-y-6">
18
+
19
+ <div>
20
+ <label for="email" class="block text-sm font-medium text-gray-700 mb-2">
21
+ Email address
22
+ </label>
23
+ <input
24
+ id="email"
25
+ name="email"
26
+ type="email"
27
+ autocomplete="email"
28
+ required
29
+ class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
30
+ placeholder="Enter your email"
31
+ value="{{ request.form.email or '' }}"
32
+ />
33
+ </div>
34
+
35
+ <div>
36
+ <button
37
+ type="submit"
38
+ class="w-full flex justify-center py-3 px-4 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors"
39
+ >
40
+ Send Reset Link
41
+ </button>
42
+ </div>
43
+
44
+ <div class="text-center">
45
+ <p class="text-sm text-gray-600">
46
+ Remember your password?
47
+ <a href="{{ url_for('signin') }}" class="font-medium text-blue-600 hover:text-blue-500">
48
+ Sign in
49
+ </a>
50
+ </p>
51
+ </div>
52
+ </form>
53
+ </div>
54
+ </div>
55
+ {% endblock %}
templates/profile.html ADDED
@@ -0,0 +1,303 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Profile - AI Resume Builder{% endblock %}
4
+
5
+ {% block content %}
6
+ <div class="min-h-screen bg-gray-50">
7
+ <!-- Header -->
8
+ <header class="bg-white shadow-sm border-b border-gray-200">
9
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
10
+ <div class="flex justify-between items-center py-4">
11
+ <div class="flex items-center">
12
+ <h1 class="text-2xl font-bold text-gray-900">My Profile</h1>
13
+ </div>
14
+ <div class="flex items-center space-x-4">
15
+ {% if current_user.is_admin %}
16
+ <div class="relative">
17
+ <button onclick="toggleDropdown()" class="text-sm text-gray-600 hover:text-gray-800 flex items-center space-x-1">
18
+ <span>Welcome, {{ current_user.name }}</span>
19
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
20
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
21
+ </svg>
22
+ </button>
23
+ <div id="adminDropdown" class="hidden absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg py-1 z-50 border border-gray-200">
24
+ <a href="{{ url_for('admin_panel') }}" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
25
+ <i class="fas fa-shield-alt mr-2"></i>Admin Panel
26
+ </a>
27
+ <div class="border-t border-gray-100"></div>
28
+ <a href="{{ url_for('logout') }}" class="block px-4 py-2 text-sm text-red-600 hover:bg-gray-100">
29
+ Logout
30
+ </a>
31
+ </div>
32
+ </div>
33
+ {% else %}
34
+ <span class="text-sm text-gray-600">Welcome, {{ current_user.name }}</span>
35
+ <a href="{{ url_for('logout') }}" class="text-sm text-red-600 hover:text-red-500">
36
+ Logout
37
+ </a>
38
+ {% endif %}
39
+ </div>
40
+ </div>
41
+ </div>
42
+ </header>
43
+
44
+ <!-- Main Content -->
45
+ <main class="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
46
+ <div class="px-4 py-6 sm:px-0">
47
+ {% if has_profile %}
48
+ <!-- Profile Display -->
49
+ <div class="bg-white rounded-lg shadow p-6 mb-6">
50
+ <div class="flex justify-between items-center mb-6">
51
+ <h2 class="text-xl font-semibold text-gray-800">Your Professional Profile</h2>
52
+ <div class="flex space-x-3">
53
+ <button onclick="showResumeOptions()" class="bg-green-600 hover:bg-green-700 text-white font-medium py-2 px-4 rounded-lg transition-colors">
54
+ <i class="fas fa-download mr-2"></i>Generate Resume
55
+ </button>
56
+ <a href="{{ url_for('create_introduction') }}" class="bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-lg transition-colors">
57
+ <i class="fas fa-edit mr-2"></i>Edit Profile
58
+ </a>
59
+ </div>
60
+ </div>
61
+
62
+ <!-- Display sections based on user's order -->
63
+ {% for section_name in section_order %}
64
+ {% if section_name == 'introduction' and intro %}
65
+ <div class="mb-8">
66
+ <div class="text-center mb-6">
67
+ <h3 class="text-2xl font-bold text-gray-900 mb-2">{{ intro.name }}</h3>
68
+ <div class="flex justify-center flex-wrap gap-4 text-sm text-gray-600">
69
+ <span><i class="fas fa-envelope mr-1"></i> {{ intro.email }}</span>
70
+ <span><i class="fas fa-phone mr-1"></i> {{ intro.phone }}</span>
71
+ {% if intro.linkedin %}
72
+ <span><i class="fab fa-linkedin mr-1"></i> LinkedIn</span>
73
+ {% endif %}
74
+ {% if intro.github %}
75
+ <span><i class="fab fa-github mr-1"></i> GitHub</span>
76
+ {% endif %}
77
+ {% if intro.website %}
78
+ <span><i class="fas fa-globe mr-1"></i> Website</span>
79
+ {% endif %}
80
+ </div>
81
+ </div>
82
+ </div>
83
+ {% endif %}
84
+
85
+ {% if section_name == 'profile_summary' and summary %}
86
+ <div class="mb-8">
87
+ <h4 class="text-lg font-semibold text-gray-800 mb-3">Professional Summary</h4>
88
+ <p class="text-gray-700 leading-relaxed">{{ summary.summary }}</p>
89
+ {% if summary.ai_generated %}
90
+ <span class="inline-block bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded mt-2">AI Generated</span>
91
+ {% endif %}
92
+ </div>
93
+ {% endif %}
94
+
95
+ {% if section_name == 'work_experience' and work_experiences %}
96
+ <div class="mb-8">
97
+ <h4 class="text-lg font-semibold text-gray-800 mb-4">Work Experience</h4>
98
+ {% for exp in work_experiences %}
99
+ <div class="mb-4 pb-4 border-b border-gray-200 last:border-0">
100
+ <h5 class="font-medium text-gray-900">{{ exp.title }}</h5>
101
+ <p class="text-gray-600 text-sm">{{ exp.organization }}</p>
102
+ <p class="text-gray-500 text-sm mb-2">
103
+ {{ exp.start_month }}/{{ exp.start_year }} -
104
+ {% if exp.end_year %}{{ exp.end_month }}/{{ exp.end_year }}{% else %}Present{% endif %}
105
+ </p>
106
+ {% if exp.remarks %}
107
+ <p class="text-gray-700 text-sm">{{ exp.remarks }}</p>
108
+ {% endif %}
109
+ </div>
110
+ {% endfor %}
111
+ </div>
112
+ {% endif %}
113
+
114
+ {% if section_name == 'projects' and projects %}
115
+ <div class="mb-8">
116
+ <h4 class="text-lg font-semibold text-gray-800 mb-4">Projects</h4>
117
+ {% for project in projects %}
118
+ <div class="mb-4 pb-4 border-b border-gray-200 last:border-0">
119
+ <h5 class="font-medium text-gray-900">{{ project.title }}</h5>
120
+ {% if project.organization %}
121
+ <p class="text-gray-600 text-sm">{{ project.organization }}</p>
122
+ {% endif %}
123
+ <p class="text-gray-500 text-sm mb-2">
124
+ {{ project.start_month }}/{{ project.start_year }} -
125
+ {% if project.end_year %}{{ project.end_month }}/{{ project.end_year }}{% else %}Present{% endif %}
126
+ </p>
127
+ {% if project.remarks %}
128
+ <p class="text-gray-700 text-sm">{{ project.remarks }}</p>
129
+ {% endif %}
130
+ </div>
131
+ {% endfor %}
132
+ </div>
133
+ {% endif %}
134
+
135
+ {% if section_name == 'education' and educations %}
136
+ <div class="mb-8">
137
+ <h4 class="text-lg font-semibold text-gray-800 mb-4">Education</h4>
138
+ {% for edu in educations %}
139
+ <div class="mb-4 pb-4 border-b border-gray-200 last:border-0">
140
+ <h5 class="font-medium text-gray-900">{{ edu.title }}</h5>
141
+ <p class="text-gray-600 text-sm">{{ edu.organization }}</p>
142
+ <p class="text-gray-500 text-sm mb-2">
143
+ {{ edu.start_month }}/{{ edu.start_year }} -
144
+ {% if edu.end_year %}{{ edu.end_month }}/{{ edu.end_year }}{% else %}Present{% endif %}
145
+ </p>
146
+ {% if edu.remarks %}
147
+ <p class="text-gray-700 text-sm">{{ edu.remarks }}</p>
148
+ {% endif %}
149
+ </div>
150
+ {% endfor %}
151
+ </div>
152
+ {% endif %}
153
+
154
+ {% if section_name == 'skills' and skills %}
155
+ <div class="mb-8">
156
+ <h4 class="text-lg font-semibold text-gray-800 mb-3">Skills</h4>
157
+ <div class="flex flex-wrap gap-2">
158
+ {% for skill in skills %}
159
+ <span class="bg-blue-100 text-blue-800 px-3 py-1 rounded-full text-sm">{{ skill.skill }}</span>
160
+ {% endfor %}
161
+ </div>
162
+ </div>
163
+ {% endif %}
164
+
165
+ {% if section_name == 'achievements' and achievements %}
166
+ <div class="mb-8">
167
+ <h4 class="text-lg font-semibold text-gray-800 mb-3">Achievements</h4>
168
+ <div class="flex flex-wrap gap-2">
169
+ {% for achievement in achievements %}
170
+ <span class="bg-green-100 text-green-800 px-3 py-1 rounded-full text-sm">{{ achievement.achievement }}</span>
171
+ {% endfor %}
172
+ </div>
173
+ </div>
174
+ {% endif %}
175
+ {% endfor %}
176
+ </div>
177
+ {% else %}
178
+ <!-- Empty State -->
179
+ <div class="bg-white rounded-lg shadow p-6">
180
+ <div class="text-center">
181
+ <h2 class="text-xl font-semibold text-gray-800 mb-4">Welcome to AI Resume Builder!</h2>
182
+ <p class="text-gray-600 mb-6">
183
+ Your profile is currently empty. Click the button below to start creating your professional resume.
184
+ </p>
185
+ <a href="{{ url_for('create_introduction') }}" class="bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-6 rounded-lg transition-colors inline-block">
186
+ Create Your Profile
187
+ </a>
188
+ </div>
189
+ </div>
190
+ {% endif %}
191
+ </div>
192
+ </main>
193
+ </div>
194
+
195
+ <!-- Resume Options Modal -->
196
+ <div id="resumeModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden items-center justify-center z-50">
197
+ <div class="bg-white rounded-lg p-6 m-4 max-w-md w-full">
198
+ <h3 class="text-lg font-semibold text-gray-900 mb-4">Choose Resume Format</h3>
199
+ <div class="space-y-3">
200
+ <button onclick="generateResume('pdf-standard')" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-lg transition-colors">
201
+ <i class="fas fa-file-pdf mr-2"></i>Generate PDF (Standard)
202
+ </button>
203
+ <button onclick="generateResume('pdf-modern')" class="w-full bg-purple-600 hover:bg-purple-700 text-white font-medium py-2 px-4 rounded-lg transition-colors">
204
+ <i class="fas fa-file-pdf mr-2"></i>Generate PDF (Modern)
205
+ </button>
206
+ <button onclick="generateResume('word')" class="w-full bg-green-600 hover:bg-green-700 text-white font-medium py-2 px-4 rounded-lg transition-colors">
207
+ <i class="fas fa-file-word mr-2"></i>Generate Word Document
208
+ </button>
209
+ </div>
210
+ <button onclick="hideResumeOptions()" class="mt-4 w-full bg-gray-300 hover:bg-gray-400 text-gray-800 font-medium py-2 px-4 rounded-lg transition-colors">
211
+ Cancel
212
+ </button>
213
+ </div>
214
+ </div>
215
+
216
+ <script>
217
+ function showResumeOptions() {
218
+ document.getElementById('resumeModal').classList.remove('hidden');
219
+ document.getElementById('resumeModal').classList.add('flex');
220
+ }
221
+
222
+ function hideResumeOptions() {
223
+ document.getElementById('resumeModal').classList.add('hidden');
224
+ document.getElementById('resumeModal').classList.remove('flex');
225
+ }
226
+
227
+ function generateResume(format) {
228
+ // Show loading state
229
+ const modal = document.getElementById('resumeModal');
230
+ const buttons = modal.querySelectorAll('button');
231
+ buttons.forEach(btn => btn.disabled = true);
232
+
233
+ // Create and show loading message
234
+ const loadingMsg = document.createElement('div');
235
+ loadingMsg.className = 'text-center mt-4 text-gray-600';
236
+ loadingMsg.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>Generating your resume...';
237
+ modal.querySelector('.bg-white').appendChild(loadingMsg);
238
+
239
+ // Fetch the resume
240
+ fetch(`/profile/generate-resume/${format}`, {
241
+ method: 'GET',
242
+ headers: {
243
+ 'X-Requested-With': 'XMLHttpRequest'
244
+ }
245
+ })
246
+ .then(response => {
247
+ if (!response.ok) {
248
+ throw new Error('Failed to generate resume');
249
+ }
250
+ return response.blob();
251
+ })
252
+ .then(blob => {
253
+ // Create download link
254
+ const url = window.URL.createObjectURL(blob);
255
+ const a = document.createElement('a');
256
+ a.href = url;
257
+
258
+ // Set filename based on format
259
+ const username = '{{ current_user.name }}'.replace(/\s+/g, '_');
260
+ if (format === 'pdf-standard') {
261
+ a.download = `${username}_resume.pdf`;
262
+ } else if (format === 'pdf-modern') {
263
+ a.download = `${username}_resume_modern.pdf`;
264
+ } else if (format === 'word') {
265
+ a.download = `${username}_resume.docx`;
266
+ }
267
+
268
+ document.body.appendChild(a);
269
+ a.click();
270
+ window.URL.revokeObjectURL(url);
271
+ document.body.removeChild(a);
272
+
273
+ hideResumeOptions();
274
+ })
275
+ .catch(error => {
276
+ console.error('Error:', error);
277
+ alert('Failed to generate resume. Please try again.');
278
+ hideResumeOptions();
279
+ })
280
+ .finally(() => {
281
+ // Remove loading message and re-enable buttons
282
+ if (loadingMsg.parentNode) {
283
+ loadingMsg.parentNode.removeChild(loadingMsg);
284
+ }
285
+ buttons.forEach(btn => btn.disabled = false);
286
+ });
287
+ }
288
+
289
+ // Toggle admin dropdown
290
+ function toggleDropdown() {
291
+ const dropdown = document.getElementById('adminDropdown');
292
+ dropdown.classList.toggle('hidden');
293
+
294
+ // Close dropdown when clicking outside
295
+ document.addEventListener('click', function closeDropdown(e) {
296
+ if (!e.target.closest('.relative')) {
297
+ dropdown.classList.add('hidden');
298
+ document.removeEventListener('click', closeDropdown);
299
+ }
300
+ });
301
+ }
302
+ </script>
303
+ {% endblock %}
templates/resumes/resume_modern.html ADDED
@@ -0,0 +1,329 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>{{ intro.name }} - Resume</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ body {
15
+ font-family: 'Calibri', Arial, sans-serif;
16
+ font-size: 11pt;
17
+ line-height: 1.4;
18
+ color: #333;
19
+ max-width: 1000px;
20
+ margin: 0 auto;
21
+ padding: 30px;
22
+ background-color: #fff;
23
+ }
24
+
25
+ .header {
26
+ text-align: center;
27
+ margin-bottom: 30px;
28
+ padding-bottom: 20px;
29
+ border-bottom: 3px solid #2c3e50;
30
+ }
31
+
32
+ .name {
33
+ font-size: 28pt;
34
+ font-weight: 300;
35
+ color: #2c3e50;
36
+ margin-bottom: 10px;
37
+ letter-spacing: 2px;
38
+ text-transform: uppercase;
39
+ }
40
+
41
+ .contact-info {
42
+ display: flex;
43
+ justify-content: center;
44
+ flex-wrap: wrap;
45
+ gap: 25px;
46
+ font-size: 10pt;
47
+ color: #555;
48
+ }
49
+
50
+ .contact-info span {
51
+ display: flex;
52
+ align-items: center;
53
+ gap: 6px;
54
+ }
55
+
56
+ .contact-info i {
57
+ color: #3498db;
58
+ }
59
+
60
+ .profile-summary {
61
+ margin-bottom: 30px;
62
+ text-align: justify;
63
+ color: #555;
64
+ font-style: italic;
65
+ }
66
+
67
+ .ai-generated {
68
+ font-size: 9pt;
69
+ color: #7f8c8d;
70
+ text-align: right;
71
+ margin-top: 5px;
72
+ }
73
+
74
+ .main-content {
75
+ display: grid;
76
+ grid-template-columns: 32% 68%;
77
+ gap: 30px;
78
+ }
79
+
80
+ .left-column {
81
+ background-color: #f8f9fa;
82
+ padding: 25px;
83
+ border-radius: 8px;
84
+ }
85
+
86
+ .right-column {
87
+ padding: 0 10px;
88
+ }
89
+
90
+ .section {
91
+ margin-bottom: 30px;
92
+ }
93
+
94
+ .section-title {
95
+ font-size: 14pt;
96
+ font-weight: 600;
97
+ color: #2c3e50;
98
+ margin-bottom: 15px;
99
+ padding-bottom: 8px;
100
+ border-bottom: 2px solid #3498db;
101
+ text-transform: uppercase;
102
+ letter-spacing: 1px;
103
+ }
104
+
105
+ .skills-list {
106
+ display: flex;
107
+ flex-wrap: wrap;
108
+ gap: 8px;
109
+ }
110
+
111
+ .skill-tag {
112
+ background-color: #3498db;
113
+ color: white;
114
+ padding: 6px 12px;
115
+ border-radius: 20px;
116
+ font-size: 10pt;
117
+ font-weight: 500;
118
+ }
119
+
120
+ .achievements-list {
121
+ display: flex;
122
+ flex-direction: column;
123
+ gap: 8px;
124
+ }
125
+
126
+ .achievement-item {
127
+ display: flex;
128
+ align-items: flex-start;
129
+ gap: 8px;
130
+ font-size: 10pt;
131
+ }
132
+
133
+ .achievement-item::before {
134
+ content: "▸";
135
+ color: #3498db;
136
+ font-weight: bold;
137
+ }
138
+
139
+ .experience-item, .project-item, .education-item {
140
+ margin-bottom: 20px;
141
+ padding-bottom: 15px;
142
+ border-bottom: 1px solid #eee;
143
+ }
144
+
145
+ .experience-item:last-child, .project-item:last-child, .education-item:last-child {
146
+ border-bottom: none;
147
+ }
148
+
149
+ .item-header {
150
+ display: flex;
151
+ justify-content: space-between;
152
+ align-items: baseline;
153
+ margin-bottom: 8px;
154
+ }
155
+
156
+ .title {
157
+ font-weight: 600;
158
+ font-size: 12pt;
159
+ color: #2c3e50;
160
+ }
161
+
162
+ .organization {
163
+ color: #3498db;
164
+ font-style: italic;
165
+ font-size: 11pt;
166
+ margin-left: 10px;
167
+ }
168
+
169
+ .date {
170
+ color: #7f8c8d;
171
+ font-size: 10pt;
172
+ font-weight: 500;
173
+ }
174
+
175
+ .remarks {
176
+ margin-top: 8px;
177
+ text-align: justify;
178
+ color: #555;
179
+ font-size: 10pt;
180
+ }
181
+
182
+ @media print {
183
+ body {
184
+ padding: 20px;
185
+ }
186
+
187
+ .main-content {
188
+ gap: 20px;
189
+ }
190
+ }
191
+ </style>
192
+ </head>
193
+ <body>
194
+ <!-- Header Section -->
195
+ <div class="header">
196
+ <h1 class="name">{{ intro.name }}</h1>
197
+ <div class="contact-info">
198
+ <span><i>📧</i> {{ intro.email }}</span>
199
+ <span><i>📱</i> {{ intro.phone }}</span>
200
+ {% if intro.linkedin %}
201
+ <span><i>💼</i> LinkedIn</span>
202
+ {% endif %}
203
+ {% if intro.github %}
204
+ <span><i>💻</i> GitHub</span>
205
+ {% endif %}
206
+ {% if intro.website %}
207
+ <span><i>🌐</i> Website</span>
208
+ {% endif %}
209
+ </div>
210
+ </div>
211
+
212
+ <!-- Profile Summary -->
213
+ {% if summary %}
214
+ <div class="profile-summary">
215
+ <p>{{ summary.summary }}</p>
216
+ {% if summary.ai_generated %}
217
+ <p class="ai-generated">AI Generated</p>
218
+ {% endif %}
219
+ </div>
220
+ {% endif %}
221
+
222
+ <!-- Main Content with Two Columns -->
223
+ <div class="main-content">
224
+ <!-- Left Column -->
225
+ <div class="left-column">
226
+ <!-- Skills Section -->
227
+ {% if skills %}
228
+ <div class="section">
229
+ <h2 class="section-title">Skills</h2>
230
+ <div class="skills-list">
231
+ {% for skill in skills %}
232
+ <span class="skill-tag">{{ skill.skill }}</span>
233
+ {% endfor %}
234
+ </div>
235
+ </div>
236
+ {% endif %}
237
+
238
+ <!-- Achievements Section -->
239
+ {% if achievements %}
240
+ <div class="section">
241
+ <h2 class="section-title">Achievements</h2>
242
+ <div class="achievements-list">
243
+ {% for achievement in achievements %}
244
+ <div class="achievement-item">{{ achievement.achievement }}</div>
245
+ {% endfor %}
246
+ </div>
247
+ </div>
248
+ {% endif %}
249
+ </div>
250
+
251
+ <!-- Right Column -->
252
+ <div class="right-column">
253
+ <!-- Dynamic Sections -->
254
+ {% for section_name in section_order %}
255
+ {% if section_name == 'work_experience' and work_experiences %}
256
+ <div class="section">
257
+ <h2 class="section-title">Work Experience</h2>
258
+ {% for exp in work_experiences %}
259
+ <div class="experience-item">
260
+ <div class="item-header">
261
+ <div>
262
+ <span class="title">{{ exp.title }}</span>
263
+ <span class="organization">{{ exp.organization }}</span>
264
+ </div>
265
+ <span class="date">
266
+ {{ exp.start_month }}/{{ exp.start_year }} -
267
+ {% if exp.end_year %}{{ exp.end_month }}/{{ exp.end_year }}{% else %}Present{% endif %}
268
+ </span>
269
+ </div>
270
+ {% if exp.remarks %}
271
+ <p class="remarks">{{ exp.remarks }}</p>
272
+ {% endif %}
273
+ </div>
274
+ {% endfor %}
275
+ </div>
276
+ {% endif %}
277
+
278
+ {% if section_name == 'projects' and projects %}
279
+ <div class="section">
280
+ <h2 class="section-title">Projects</h2>
281
+ {% for project in projects %}
282
+ <div class="project-item">
283
+ <div class="item-header">
284
+ <div>
285
+ <span class="title">{{ project.title }}</span>
286
+ {% if project.organization %}
287
+ <span class="organization">{{ project.organization }}</span>
288
+ {% endif %}
289
+ </div>
290
+ <span class="date">
291
+ {{ project.start_month }}/{{ project.start_year }} -
292
+ {% if project.end_year %}{{ project.end_month }}/{{ project.end_year }}{% else %}Present{% endif %}
293
+ </span>
294
+ </div>
295
+ {% if project.remarks %}
296
+ <p class="remarks">{{ project.remarks }}</p>
297
+ {% endif %}
298
+ </div>
299
+ {% endfor %}
300
+ </div>
301
+ {% endif %}
302
+
303
+ {% if section_name == 'education' and educations %}
304
+ <div class="section">
305
+ <h2 class="section-title">Education</h2>
306
+ {% for edu in educations %}
307
+ <div class="education-item">
308
+ <div class="item-header">
309
+ <div>
310
+ <span class="title">{{ edu.title }}</span>
311
+ <span class="organization">{{ edu.organization }}</span>
312
+ </div>
313
+ <span class="date">
314
+ {{ edu.start_month }}/{{ edu.start_year }} -
315
+ {% if edu.end_year %}{{ edu.end_month }}/{{ edu.end_year }}{% else %}Present{% endif %}
316
+ </span>
317
+ </div>
318
+ {% if edu.remarks %}
319
+ <p class="remarks">{{ edu.remarks }}</p>
320
+ {% endif %}
321
+ </div>
322
+ {% endfor %}
323
+ </div>
324
+ {% endif %}
325
+ {% endfor %}
326
+ </div>
327
+ </div>
328
+ </body>
329
+ </html>
templates/resumes/resume_standard.html ADDED
@@ -0,0 +1,248 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>{{ intro.name }} - Resume</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ body {
15
+ font-family: 'Times New Roman', serif;
16
+ font-size: 12pt;
17
+ line-height: 1.4;
18
+ color: #000;
19
+ max-width: 800px;
20
+ margin: 0 auto;
21
+ padding: 20px;
22
+ }
23
+
24
+ .header {
25
+ text-align: center;
26
+ margin-bottom: 30px;
27
+ border-bottom: 2px solid #333;
28
+ padding-bottom: 20px;
29
+ }
30
+
31
+ .name {
32
+ font-size: 24pt;
33
+ font-weight: bold;
34
+ margin-bottom: 10px;
35
+ text-transform: uppercase;
36
+ letter-spacing: 1px;
37
+ }
38
+
39
+ .contact-info {
40
+ display: flex;
41
+ justify-content: center;
42
+ flex-wrap: wrap;
43
+ gap: 20px;
44
+ font-size: 11pt;
45
+ }
46
+
47
+ .contact-info span {
48
+ display: flex;
49
+ align-items: center;
50
+ gap: 5px;
51
+ }
52
+
53
+ .section {
54
+ margin-bottom: 25px;
55
+ }
56
+
57
+ .section-title {
58
+ font-size: 16pt;
59
+ font-weight: bold;
60
+ margin-bottom: 15px;
61
+ text-transform: uppercase;
62
+ border-bottom: 1px solid #999;
63
+ padding-bottom: 5px;
64
+ }
65
+
66
+ .experience-item, .project-item, .education-item {
67
+ margin-bottom: 15px;
68
+ }
69
+
70
+ .item-header {
71
+ display: flex;
72
+ justify-content: space-between;
73
+ align-items: baseline;
74
+ margin-bottom: 5px;
75
+ }
76
+
77
+ .title {
78
+ font-weight: bold;
79
+ font-size: 13pt;
80
+ }
81
+
82
+ .organization {
83
+ font-style: italic;
84
+ color: #555;
85
+ }
86
+
87
+ .date {
88
+ color: #666;
89
+ font-size: 11pt;
90
+ }
91
+
92
+ .remarks {
93
+ margin-top: 5px;
94
+ text-align: justify;
95
+ }
96
+
97
+ .skills-list, .achievements-list {
98
+ display: flex;
99
+ flex-wrap: wrap;
100
+ gap: 8px;
101
+ }
102
+
103
+ .skill-tag, .achievement-tag {
104
+ background-color: #f0f0f0;
105
+ padding: 4px 8px;
106
+ border-radius: 3px;
107
+ font-size: 11pt;
108
+ }
109
+
110
+ .profile-summary {
111
+ text-align: justify;
112
+ margin-bottom: 10px;
113
+ }
114
+
115
+ .ai-generated {
116
+ font-size: 10pt;
117
+ color: #666;
118
+ font-style: italic;
119
+ }
120
+ </style>
121
+ </head>
122
+ <body>
123
+ <!-- Header Section -->
124
+ <div class="header">
125
+ <h1 class="name">{{ intro.name }}</h1>
126
+ <div class="contact-info">
127
+ <span><i>📧</i> {{ intro.email }}</span>
128
+ <span><i>📱</i> {{ intro.phone }}</span>
129
+ {% if intro.linkedin %}
130
+ <span><i>💼</i> LinkedIn</span>
131
+ {% endif %}
132
+ {% if intro.github %}
133
+ <span><i>💻</i> GitHub</span>
134
+ {% endif %}
135
+ {% if intro.website %}
136
+ <span><i>🌐</i> Website</span>
137
+ {% endif %}
138
+ </div>
139
+ </div>
140
+
141
+ <!-- Profile Summary -->
142
+ {% if summary %}
143
+ <div class="section">
144
+ <h2 class="section-title">Professional Summary</h2>
145
+ <p class="profile-summary">{{ summary.summary }}</p>
146
+ {% if summary.ai_generated %}
147
+ <p class="ai-generated">AI Generated</p>
148
+ {% endif %}
149
+ </div>
150
+ {% endif %}
151
+
152
+ <!-- Dynamic Sections -->
153
+ {% for section_name in section_order %}
154
+ {% if section_name == 'work_experience' and work_experiences %}
155
+ <div class="section">
156
+ <h2 class="section-title">Work Experience</h2>
157
+ {% for exp in work_experiences %}
158
+ <div class="experience-item">
159
+ <div class="item-header">
160
+ <div>
161
+ <span class="title">{{ exp.title }}</span>
162
+ <span class="organization">at {{ exp.organization }}</span>
163
+ </div>
164
+ <span class="date">
165
+ {{ exp.start_month }}/{{ exp.start_year }} -
166
+ {% if exp.end_year %}{{ exp.end_month }}/{{ exp.end_year }}{% else %}Present{% endif %}
167
+ </span>
168
+ </div>
169
+ {% if exp.remarks %}
170
+ <p class="remarks">{{ exp.remarks }}</p>
171
+ {% endif %}
172
+ </div>
173
+ {% endfor %}
174
+ </div>
175
+ {% endif %}
176
+
177
+ {% if section_name == 'projects' and projects %}
178
+ <div class="section">
179
+ <h2 class="section-title">Projects</h2>
180
+ {% for project in projects %}
181
+ <div class="project-item">
182
+ <div class="item-header">
183
+ <div>
184
+ <span class="title">{{ project.title }}</span>
185
+ {% if project.organization %}
186
+ <span class="organization">at {{ project.organization }}</span>
187
+ {% endif %}
188
+ </div>
189
+ <span class="date">
190
+ {{ project.start_month }}/{{ project.start_year }} -
191
+ {% if project.end_year %}{{ project.end_month }}/{{ project.end_year }}{% else %}Present{% endif %}
192
+ </span>
193
+ </div>
194
+ {% if project.remarks %}
195
+ <p class="remarks">{{ project.remarks }}</p>
196
+ {% endif %}
197
+ </div>
198
+ {% endfor %}
199
+ </div>
200
+ {% endif %}
201
+
202
+ {% if section_name == 'education' and educations %}
203
+ <div class="section">
204
+ <h2 class="section-title">Education</h2>
205
+ {% for edu in educations %}
206
+ <div class="education-item">
207
+ <div class="item-header">
208
+ <div>
209
+ <span class="title">{{ edu.title }}</span>
210
+ <span class="organization">at {{ edu.organization }}</span>
211
+ </div>
212
+ <span class="date">
213
+ {{ edu.start_month }}/{{ edu.start_year }} -
214
+ {% if edu.end_year %}{{ edu.end_month }}/{{ edu.end_year }}{% else %}Present{% endif %}
215
+ </span>
216
+ </div>
217
+ {% if edu.remarks %}
218
+ <p class="remarks">{{ edu.remarks }}</p>
219
+ {% endif %}
220
+ </div>
221
+ {% endfor %}
222
+ </div>
223
+ {% endif %}
224
+
225
+ {% if section_name == 'skills' and skills %}
226
+ <div class="section">
227
+ <h2 class="section-title">Skills</h2>
228
+ <div class="skills-list">
229
+ {% for skill in skills %}
230
+ <span class="skill-tag">{{ skill.skill }}</span>
231
+ {% endfor %}
232
+ </div>
233
+ </div>
234
+ {% endif %}
235
+
236
+ {% if section_name == 'achievements' and achievements %}
237
+ <div class="section">
238
+ <h2 class="section-title">Achievements</h2>
239
+ <div class="achievements-list">
240
+ {% for achievement in achievements %}
241
+ <span class="achievement-tag">{{ achievement.achievement }}</span>
242
+ {% endfor %}
243
+ </div>
244
+ </div>
245
+ {% endif %}
246
+ {% endfor %}
247
+ </body>
248
+ </html>
templates/signin.html ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Sign In - AI Resume Builder{% endblock %}
4
+
5
+ {% block content %}
6
+ <div class="min-h-screen flex bg-gray-50">
7
+ <!-- Left side - Image Carousel -->
8
+ <div class="w-1/2 hidden lg:block relative overflow-hidden">
9
+ <!-- Carousel Images -->
10
+ <div class="relative h-full">
11
+ <div id="carousel-images" class="relative h-full">
12
+ <!-- Images will be dynamically inserted here -->
13
+ </div>
14
+ </div>
15
+
16
+ <!-- Carousel Content Overlay -->
17
+ <div class="absolute inset-0 flex flex-col justify-center items-center text-white p-8 text-center">
18
+ <div class="max-w-lg">
19
+ <h1 class="text-5xl font-bold mb-6 drop-shadow-lg">
20
+ AI Resume Builder
21
+ </h1>
22
+ <p class="text-xl mb-8 drop-shadow-md">
23
+ Create professional resumes with the power of AI. Stand out from the crowd with intelligent, tailored resumes.
24
+ </p>
25
+ <div class="flex justify-center space-x-8 mb-8">
26
+ <div class="text-center">
27
+ <div class="text-3xl font-bold mb-2">10K+</div>
28
+ <div class="text-sm opacity-90">Resumes Created</div>
29
+ </div>
30
+ <div class="text-center">
31
+ <div class="text-3xl font-bold mb-2">95%</div>
32
+ <div class="text-sm opacity-90">Success Rate</div>
33
+ </div>
34
+ <div class="text-center">
35
+ <div class="text-3xl font-bold mb-2">4.8★</div>
36
+ <div class="text-sm opacity-90">User Rating</div>
37
+ </div>
38
+ </div>
39
+ </div>
40
+ </div>
41
+
42
+ <!-- Carousel Indicators -->
43
+ <div id="carousel-indicators" class="absolute bottom-8 left-1/2 transform -translate-x-1/2 flex space-x-2">
44
+ <!-- Indicators will be dynamically inserted here -->
45
+ </div>
46
+ </div>
47
+
48
+ <!-- Right side - Sign In Form -->
49
+ <div class="w-full lg:w-1/2 flex items-center justify-center p-8">
50
+ <div class="max-w-md w-full space-y-8">
51
+ <div class="text-center">
52
+ <h2 class="text-2xl font-semibold text-gray-800 mb-6">
53
+ Sign in to your account
54
+ </h2>
55
+ </div>
56
+
57
+ <form method="POST" action="{{ url_for('signin') }}" class="mt-8 space-y-6">
58
+
59
+ <div class="space-y-4">
60
+ <div>
61
+ <label for="email" class="block text-sm font-medium text-gray-700 mb-2">
62
+ Email address
63
+ </label>
64
+ <input
65
+ id="email"
66
+ name="email"
67
+ type="email"
68
+ autocomplete="email"
69
+ required
70
+ class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
71
+ placeholder="Enter your email"
72
+ value="{{ request.form.email or '' }}"
73
+ />
74
+ </div>
75
+
76
+ <div>
77
+ <label for="password" class="block text-sm font-medium text-gray-700 mb-2">
78
+ Password
79
+ </label>
80
+ <input
81
+ id="password"
82
+ name="password"
83
+ type="password"
84
+ autocomplete="current-password"
85
+ required
86
+ class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
87
+ placeholder="Enter your password"
88
+ />
89
+ </div>
90
+ </div>
91
+
92
+ <div class="flex items-center justify-between">
93
+ <div class="text-sm">
94
+ <a href="{{ url_for('forgot_password') }}" class="font-medium text-blue-600 hover:text-blue-500">
95
+ Forgot your password?
96
+ </a>
97
+ </div>
98
+ </div>
99
+
100
+ <div>
101
+ <button
102
+ type="submit"
103
+ class="w-full flex justify-center py-3 px-4 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors"
104
+ >
105
+ Sign in
106
+ </button>
107
+ </div>
108
+
109
+ <div class="text-center">
110
+ <a
111
+ href="{{ url_for('github_auth') }}"
112
+ class="w-full flex items-center justify-center py-3 px-4 border border-gray-300 rounded-lg shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors inline-block"
113
+ >
114
+ <svg class="w-4 h-4 mr-2" viewBox="0 0 24 24" fill="currentColor">
115
+ <path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
116
+ </svg>
117
+ Sign in with GitHub
118
+ </a>
119
+ </div>
120
+
121
+ <div class="text-center">
122
+ <p class="text-sm text-gray-600">
123
+ Don't have an account?
124
+ <a href="{{ url_for('signup') }}" class="font-medium text-blue-600 hover:text-blue-500">
125
+ Sign up
126
+ </a>
127
+ </p>
128
+ </div>
129
+ </form>
130
+ </div>
131
+ </div>
132
+ </div>
133
+ {% endblock %}
134
+
135
+ {% block scripts %}
136
+ <script>
137
+ // Carousel functionality
138
+ const carouselImages = [
139
+ 'https://picsum.photos/seed/resume-builder-1/1200/800.jpg',
140
+ 'https://picsum.photos/seed/resume-builder-2/1200/800.jpg',
141
+ 'https://picsum.photos/seed/resume-builder-3/1200/800.jpg',
142
+ 'https://picsum.photos/seed/resume-builder-4/1200/800.jpg',
143
+ ];
144
+
145
+ let currentImageIndex = 0;
146
+
147
+ // Initialize carousel
148
+ function initCarousel() {
149
+ const imagesContainer = document.getElementById('carousel-images');
150
+ const indicatorsContainer = document.getElementById('carousel-indicators');
151
+
152
+ // Create image elements
153
+ carouselImages.forEach((image, index) => {
154
+ const imageDiv = document.createElement('div');
155
+ imageDiv.className = `absolute inset-0 carousel-image ${index === 0 ? 'opacity-100' : 'opacity-0'}`;
156
+ imageDiv.innerHTML = `
157
+ <img src="${image}" alt="Resume Builder ${index + 1}" class="w-full h-full object-cover">
158
+ <div class="absolute inset-0 bg-black bg-opacity-40"></div>
159
+ `;
160
+ imagesContainer.appendChild(imageDiv);
161
+ });
162
+
163
+ // Create indicator elements
164
+ carouselImages.forEach((_, index) => {
165
+ const indicator = document.createElement('button');
166
+ indicator.className = `w-3 h-3 rounded-full transition-all duration-300 ${
167
+ index === 0 ? 'bg-white w-8' : 'bg-white bg-opacity-50 hover:bg-opacity-75'
168
+ }`;
169
+ indicator.onclick = () => showImage(index);
170
+ indicatorsContainer.appendChild(indicator);
171
+ });
172
+
173
+ // Auto-rotate carousel
174
+ setInterval(() => {
175
+ currentImageIndex = (currentImageIndex + 1) % carouselImages.length;
176
+ showImage(currentImageIndex);
177
+ }, 5000);
178
+ }
179
+
180
+ // Show specific image
181
+ function showImage(index) {
182
+ const images = document.querySelectorAll('.carousel-image');
183
+ const indicators = document.querySelectorAll('#carousel-indicators button');
184
+
185
+ images.forEach((img, i) => {
186
+ img.className = `absolute inset-0 carousel-image ${i === index ? 'opacity-100' : 'opacity-0'}`;
187
+ });
188
+
189
+ indicators.forEach((indicator, i) => {
190
+ indicator.className = `w-3 h-3 rounded-full transition-all duration-300 ${
191
+ i === index ? 'bg-white w-8' : 'bg-white bg-opacity-50 hover:bg-opacity-75'
192
+ }`;
193
+ });
194
+
195
+ currentImageIndex = index;
196
+ }
197
+
198
+
199
+ // Initialize carousel when page loads
200
+ document.addEventListener('DOMContentLoaded', initCarousel);
201
+ </script>
202
+ {% endblock %}
templates/signup.html ADDED
@@ -0,0 +1,235 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Sign Up - AI Resume Builder{% endblock %}
4
+
5
+ {% block content %}
6
+ <div class="min-h-screen flex bg-gray-50">
7
+ <!-- Left side - Image Carousel -->
8
+ <div class="w-1/2 hidden lg:block relative overflow-hidden">
9
+ <!-- Carousel Images -->
10
+ <div class="relative h-full">
11
+ <div id="carousel-images" class="relative h-full">
12
+ <!-- Images will be dynamically inserted here -->
13
+ </div>
14
+ </div>
15
+
16
+ <!-- Carousel Content Overlay -->
17
+ <div class="absolute inset-0 flex flex-col justify-center items-center text-white p-8 text-center">
18
+ <div class="max-w-lg">
19
+ <h1 class="text-5xl font-bold mb-6 drop-shadow-lg">
20
+ Join AI Resume Builder
21
+ </h1>
22
+ <p class="text-xl mb-8 drop-shadow-md">
23
+ Start creating professional resumes in minutes. Join thousands of professionals who've transformed their careers.
24
+ </p>
25
+ <div class="flex justify-center space-x-8 mb-8">
26
+ <div class="text-center">
27
+ <div class="text-3xl font-bold mb-2">Free</div>
28
+ <div class="text-sm opacity-90">Get Started</div>
29
+ </div>
30
+ <div class="text-center">
31
+ <div class="text-3xl font-bold mb-2">2 Min</div>
32
+ <div class="text-sm opacity-90">Setup Time</div>
33
+ </div>
34
+ <div class="text-center">
35
+ <div class="text-3xl font-bold mb-2">AI</div>
36
+ <div class="text-sm opacity-90">Powered</div>
37
+ </div>
38
+ </div>
39
+ </div>
40
+ </div>
41
+
42
+ <!-- Carousel Indicators -->
43
+ <div id="carousel-indicators" class="absolute bottom-8 left-1/2 transform -translate-x-1/2 flex space-x-2">
44
+ <!-- Indicators will be dynamically inserted here -->
45
+ </div>
46
+ </div>
47
+
48
+ <!-- Right side - Sign Up Form -->
49
+ <div class="w-full lg:w-1/2 flex items-center justify-center p-8">
50
+ <div class="max-w-md w-full space-y-8">
51
+ <div class="text-center">
52
+ <h2 class="text-2xl font-semibold text-gray-800 mb-6">
53
+ Create your account
54
+ </h2>
55
+ <p class="text-gray-600">
56
+ Join thousands of professionals building amazing resumes with AI
57
+ </p>
58
+ </div>
59
+
60
+ <form method="POST" action="{{ url_for('signup') }}" class="mt-8 space-y-6">
61
+
62
+ <div class="space-y-4">
63
+ <div>
64
+ <label for="name" class="block text-sm font-medium text-gray-700 mb-2">
65
+ Full Name *
66
+ </label>
67
+ <input
68
+ id="name"
69
+ name="name"
70
+ type="text"
71
+ autocomplete="name"
72
+ required
73
+ class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
74
+ placeholder="Enter your full name"
75
+ value="{{ request.form.name or '' }}"
76
+ />
77
+ </div>
78
+
79
+ <div>
80
+ <label for="email" class="block text-sm font-medium text-gray-700 mb-2">
81
+ Email Address *
82
+ </label>
83
+ <input
84
+ id="email"
85
+ name="email"
86
+ type="email"
87
+ autocomplete="email"
88
+ required
89
+ class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
90
+ placeholder="Enter your email"
91
+ value="{{ request.form.email or '' }}"
92
+ />
93
+ </div>
94
+
95
+ <div>
96
+ <label for="password" class="block text-sm font-medium text-gray-700 mb-2">
97
+ Password *
98
+ </label>
99
+ <input
100
+ id="password"
101
+ name="password"
102
+ type="password"
103
+ autocomplete="new-password"
104
+ required
105
+ class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
106
+ placeholder="Create a password"
107
+ />
108
+ <p class="mt-1 text-xs text-gray-500">
109
+ Must be at least 8 characters long
110
+ </p>
111
+ </div>
112
+
113
+ <div>
114
+ <label for="password_confirm" class="block text-sm font-medium text-gray-700 mb-2">
115
+ Confirm Password *
116
+ </label>
117
+ <input
118
+ id="password_confirm"
119
+ name="password_confirm"
120
+ type="password"
121
+ autocomplete="new-password"
122
+ required
123
+ class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
124
+ placeholder="Confirm your password"
125
+ />
126
+ </div>
127
+ </div>
128
+
129
+ <div>
130
+ <button
131
+ type="submit"
132
+ class="w-full flex justify-center py-3 px-4 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors"
133
+ >
134
+ Create account
135
+ </button>
136
+ </div>
137
+
138
+ <div class="text-center">
139
+ <a
140
+ href="{{ url_for('github_auth') }}"
141
+ class="w-full flex items-center justify-center py-3 px-4 border border-gray-300 rounded-lg shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors inline-block"
142
+ >
143
+ <svg class="w-4 h-4 mr-2" viewBox="0 0 24 24" fill="currentColor">
144
+ <path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
145
+ </svg>
146
+ Sign up with GitHub
147
+ </a>
148
+ </div>
149
+
150
+ <div class="text-center">
151
+ <p class="text-sm text-gray-600">
152
+ Already have an account?
153
+ <a href="{{ url_for('signin') }}" class="font-medium text-blue-600 hover:text-blue-500">
154
+ Sign in
155
+ </a>
156
+ </p>
157
+ </div>
158
+ </form>
159
+ </div>
160
+ </div>
161
+ </div>
162
+ {% endblock %}
163
+
164
+ {% block scripts %}
165
+ <script>
166
+ // Carousel functionality
167
+ const carouselImages = [
168
+ 'https://picsum.photos/seed/resume-builder-1/1200/800.jpg',
169
+ 'https://picsum.photos/seed/resume-builder-2/1200/800.jpg',
170
+ 'https://picsum.photos/seed/resume-builder-3/1200/800.jpg',
171
+ 'https://picsum.photos/seed/resume-builder-4/1200/800.jpg',
172
+ ];
173
+
174
+ let currentImageIndex = 0;
175
+
176
+ // Initialize carousel
177
+ function initCarousel() {
178
+ const imagesContainer = document.getElementById('carousel-images');
179
+ const indicatorsContainer = document.getElementById('carousel-indicators');
180
+
181
+ // Create image elements
182
+ carouselImages.forEach((image, index) => {
183
+ const imageDiv = document.createElement('div');
184
+ imageDiv.className = `absolute inset-0 carousel-image ${index === 0 ? 'opacity-100' : 'opacity-0'}`;
185
+ imageDiv.innerHTML = `
186
+ <img src="${image}" alt="Resume Builder ${index + 1}" class="w-full h-full object-cover">
187
+ <div class="absolute inset-0 bg-black bg-opacity-40"></div>
188
+ `;
189
+ imagesContainer.appendChild(imageDiv);
190
+ });
191
+
192
+ // Create indicator elements
193
+ carouselImages.forEach((_, index) => {
194
+ const indicator = document.createElement('button');
195
+ indicator.className = `w-3 h-3 rounded-full transition-all duration-300 ${
196
+ index === 0 ? 'bg-white w-8' : 'bg-white bg-opacity-50 hover:bg-opacity-75'
197
+ }`;
198
+ indicator.onclick = () => showImage(index);
199
+ indicatorsContainer.appendChild(indicator);
200
+ });
201
+
202
+ // Auto-rotate carousel
203
+ setInterval(() => {
204
+ currentImageIndex = (currentImageIndex + 1) % carouselImages.length;
205
+ showImage(currentImageIndex);
206
+ }, 5000);
207
+ }
208
+
209
+ // Show specific image
210
+ function showImage(index) {
211
+ const images = document.querySelectorAll('.carousel-image');
212
+ const indicators = document.querySelectorAll('#carousel-indicators button');
213
+
214
+ images.forEach((img, i) => {
215
+ img.className = `absolute inset-0 carousel-image ${i === index ? 'opacity-100' : 'opacity-0'}`;
216
+ });
217
+
218
+ indicators.forEach((indicator, i) => {
219
+ indicator.className = `w-3 h-3 rounded-full transition-all duration-300 ${
220
+ i === index ? 'bg-white w-8' : 'bg-white bg-opacity-50 hover:bg-opacity-75'
221
+ }`;
222
+ });
223
+
224
+ currentImageIndex = index;
225
+ }
226
+
227
+ // GitHub signup (placeholder for now)
228
+ function handleGitHubSignup() {
229
+ alert('GitHub OAuth will be implemented in the future. Please use email/password for now.');
230
+ }
231
+
232
+ // Initialize carousel when page loads
233
+ document.addEventListener('DOMContentLoaded', initCarousel);
234
+ </script>
235
+ {% endblock %}
test_api.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Test the PDF generation API endpoints.
4
+ """
5
+
6
+ import requests
7
+ import json
8
+ import os
9
+
10
+ # Test the application endpoints
11
+ BASE_URL = "http://127.0.0.1:5000"
12
+
13
+ def test_api_endpoints():
14
+ """Test the PDF generation endpoints."""
15
+
16
+ # First, check if the app is running
17
+ try:
18
+ response = requests.get(f"{BASE_URL}/")
19
+ if response.status_code == 200:
20
+ print("✓ Flask app is running")
21
+ else:
22
+ print("✗ Flask app is not responding properly")
23
+ return
24
+ except:
25
+ print("✗ Flask app is not running. Please start it with 'python app.py'")
26
+ return
27
+
28
+ # Test data - you'll need to be logged in first
29
+ print("\nTo test the PDF generation endpoints:")
30
+ print("1. Go to http://127.0.0.1:5000 and sign up/login")
31
+ print("2. Create a profile with some data")
32
+ print("3. Try generating PDF resumes from the profile page")
33
+
34
+ # The actual endpoints that will be called:
35
+ print("\nPDF generation endpoints:")
36
+ print("- POST /profile/generate-pdf/standard")
37
+ print("- POST /profile/generate-pdf/modern")
38
+ print("- POST /profile/generate-word")
39
+
40
+ if __name__ == "__main__":
41
+ test_api_endpoints()
test_mock_models.py ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Test PDF generation with actual model structure.
4
+ """
5
+
6
+ from datetime import datetime
7
+ import uuid
8
+ from pdf_generator import create_pdf_resume
9
+ from utils import log_info, log_error
10
+
11
+ # Test with mock objects that mimic the model structure
12
+ class MockWorkExperience:
13
+ def __init__(self, title, organization, start_month, start_year, end_month=None, end_year=None, remarks=""):
14
+ self.title = title
15
+ self.organization = organization
16
+ self.start_month = start_month
17
+ self.start_year = start_year
18
+ self.end_month = end_month
19
+ self.end_year = end_year
20
+ self.remarks = remarks
21
+
22
+ class MockProject:
23
+ def __init__(self, title, organization, start_month, start_year, end_month=None, end_year=None, remarks=""):
24
+ self.title = title
25
+ self.organization = organization
26
+ self.start_month = start_month
27
+ self.start_year = start_year
28
+ self.end_month = end_month
29
+ self.end_year = end_year
30
+ self.remarks = remarks
31
+
32
+ class MockEducation:
33
+ def __init__(self, title, organization, start_month, start_year, end_month=None, end_year=None, remarks=""):
34
+ self.title = title
35
+ self.organization = organization
36
+ self.start_month = start_month
37
+ self.start_year = start_year
38
+ self.end_month = end_month
39
+ self.end_year = end_year
40
+ self.remarks = remarks
41
+
42
+ class MockSkill:
43
+ def __init__(self, skill):
44
+ self.skill = skill
45
+
46
+ class MockAchievement:
47
+ def __init__(self, achievement):
48
+ self.achievement = achievement
49
+
50
+ def test_with_mock_models():
51
+ """Test PDF generation with mock model objects."""
52
+ log_info("Testing PDF generation with mock model structure...")
53
+
54
+ # Create mock data
55
+ work_experiences = [
56
+ MockWorkExperience("Software Engineer", "Tech Corp", 1, 2022, None, None, "Developing web applications"),
57
+ MockWorkExperience("Junior Developer", "StartupXYZ", 6, 2020, 12, 2021, "Built mobile apps")
58
+ ]
59
+
60
+ projects = [
61
+ MockProject("E-commerce Platform", "Personal", 3, 2023, 5, 2023, "Full-stack development")
62
+ ]
63
+
64
+ educations = [
65
+ MockEducation("BS Computer Science", "University", 9, 2016, 5, 2020, "Graduated with honors")
66
+ ]
67
+
68
+ skills = [
69
+ MockSkill("Python"),
70
+ MockSkill("JavaScript"),
71
+ MockSkill("React"),
72
+ MockSkill("Node.js")
73
+ ]
74
+
75
+ achievements = [
76
+ MockAchievement("Employee of the Year 2023"),
77
+ MockAchievement("Best Project Award")
78
+ ]
79
+
80
+ # Test data conversion (same as in app.py)
81
+ import calendar
82
+
83
+ def format_date(month, year):
84
+ """Format month and year as 'Month Year'"""
85
+ if month and year:
86
+ try:
87
+ month_name = calendar.month_name[int(month)]
88
+ return f"{month_name[:3]} {year}"
89
+ except:
90
+ return f"{month}/{year}"
91
+ return ""
92
+
93
+ # Prepare data for PDF generation
94
+ work_exp_list = []
95
+ for exp in work_experiences:
96
+ start_date = format_date(exp.start_month, exp.start_year)
97
+ end_date = "Present" if not exp.end_month or not exp.end_year else format_date(exp.end_month, exp.end_year)
98
+
99
+ work_exp_list.append({
100
+ 'title': exp.title,
101
+ 'organization': exp.organization,
102
+ 'start_date': start_date,
103
+ 'end_date': end_date,
104
+ 'remarks': exp.remarks or ''
105
+ })
106
+
107
+ projects_list = []
108
+ for proj in projects:
109
+ start_date = format_date(proj.start_month, proj.start_year)
110
+ end_date = "Present" if not proj.end_month or not proj.end_year else format_date(proj.end_month, proj.end_year)
111
+
112
+ projects_list.append({
113
+ 'title': proj.title,
114
+ 'organization': proj.organization,
115
+ 'start_date': start_date,
116
+ 'end_date': end_date,
117
+ 'remarks': proj.remarks or ''
118
+ })
119
+
120
+ education_list = []
121
+ for edu in educations:
122
+ start_date = format_date(edu.start_month, edu.start_year)
123
+ end_date = "Present" if not edu.end_month or not edu.end_year else format_date(edu.end_month, edu.end_year)
124
+
125
+ education_list.append({
126
+ 'title': edu.title,
127
+ 'organization': edu.organization,
128
+ 'start_date': start_date,
129
+ 'end_date': end_date,
130
+ 'remarks': edu.remarks or ''
131
+ })
132
+
133
+ # Convert skills and achievements to comma-separated strings
134
+ skills_text = ', '.join([skill.skill for skill in skills]) if skills else ''
135
+ achievements_text = ', '.join([achievement.achievement for achievement in achievements]) if achievements else ''
136
+
137
+ # Create data dictionary
138
+ data = {
139
+ 'name': 'John Doe',
140
+ 'email': 'john@example.com',
141
+ 'phone': '+1 (555) 123-4567',
142
+ 'linkedin': 'johndoe',
143
+ 'github': 'johndoe',
144
+ 'website': 'https://johndoe.com',
145
+ 'summary': 'Experienced software developer with expertise in full-stack development.',
146
+ 'work_experience': work_exp_list,
147
+ 'projects': projects_list,
148
+ 'education': education_list,
149
+ 'skills': skills_text,
150
+ 'achievements': achievements_text,
151
+ 'sections_order': ['work_experience', 'projects', 'education', 'skills', 'achievements']
152
+ }
153
+
154
+ # Test PDF generation
155
+ try:
156
+ log_info("Generating standard PDF...")
157
+ pdf_bytes = create_pdf_resume(data, "standard")
158
+ if pdf_bytes:
159
+ with open('test_mock_standard.pdf', 'wb') as f:
160
+ f.write(pdf_bytes)
161
+ log_info("✓ Standard PDF generated successfully: test_mock_standard.pdf")
162
+ else:
163
+ log_info("✗ Failed to generate standard PDF")
164
+
165
+ log_info("Generating modern PDF...")
166
+ pdf_bytes = create_pdf_resume(data, "modern")
167
+ if pdf_bytes:
168
+ with open('test_mock_modern.pdf', 'wb') as f:
169
+ f.write(pdf_bytes)
170
+ log_info("✓ Modern PDF generated successfully: test_mock_modern.pdf")
171
+ else:
172
+ log_info("✗ Failed to generate modern PDF")
173
+
174
+ except Exception as e:
175
+ log_error(f"Test failed: {str(e)}", e)
176
+
177
+ if __name__ == "__main__":
178
+ test_with_mock_models()
test_pdf_generation.py ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Test script for PDF generation using xhtml2pdf.
4
+ """
5
+
6
+ from pdf_generator import create_pdf_resume
7
+ from utils import log_info
8
+
9
+ # Test data
10
+ test_data = {
11
+ 'name': 'John Doe',
12
+ 'email': 'john.doe@example.com',
13
+ 'phone': '+1 (555) 123-4567',
14
+ 'linkedin': 'johndoe',
15
+ 'github': 'johndoe',
16
+ 'website': 'https://johndoe.com',
17
+ 'summary': 'Experienced software developer with 5+ years of expertise in full-stack development, cloud technologies, and team leadership. Passionate about building scalable solutions and mentoring junior developers.',
18
+ 'work_experience': [
19
+ {
20
+ 'title': 'Senior Software Engineer',
21
+ 'organization': 'Tech Corp',
22
+ 'start_date': 'Jan 2022',
23
+ 'end_date': 'Present',
24
+ 'remarks': 'Leading development of microservices architecture serving 1M+ users. Mentoring team of 5 developers.'
25
+ },
26
+ {
27
+ 'title': 'Software Developer',
28
+ 'organization': 'StartupXYZ',
29
+ 'start_date': 'Jun 2020',
30
+ 'end_date': 'Dec 2021',
31
+ 'remarks': 'Developed RESTful APIs and implemented CI/CD pipelines. Reduced deployment time by 60%.'
32
+ }
33
+ ],
34
+ 'projects': [
35
+ {
36
+ 'title': 'E-commerce Platform',
37
+ 'organization': 'Personal Project',
38
+ 'start_date': 'Mar 2023',
39
+ 'end_date': 'May 2023',
40
+ 'remarks': 'Built full-stack e-commerce platform using React, Node.js, and PostgreSQL. Implemented payment processing and inventory management.'
41
+ }
42
+ ],
43
+ 'education': [
44
+ {
45
+ 'title': 'Bachelor of Science in Computer Science',
46
+ 'organization': 'University of Technology',
47
+ 'start_date': 'Sep 2016',
48
+ 'end_date': 'May 2020',
49
+ 'remarks': 'Graduated Magna Cum Laude. President of Computer Science Club.'
50
+ }
51
+ ],
52
+ 'skills': 'Python, JavaScript, React, Node.js, PostgreSQL, MongoDB, AWS, Docker, Kubernetes, Git, CI/CD',
53
+ 'achievements': 'Employee of the Year 2023, Best Project Award at University Hackathon 2019',
54
+ 'sections_order': ['work_experience', 'projects', 'education', 'skills', 'achievements']
55
+ }
56
+
57
+ def test_pdf_generation():
58
+ """Test PDF generation for both templates."""
59
+ try:
60
+ log_info("Testing PDF generation...")
61
+
62
+ # Test standard template
63
+ log_info("Generating standard template PDF...")
64
+ pdf_bytes_standard = create_pdf_resume(test_data, "standard")
65
+
66
+ if pdf_bytes_standard:
67
+ with open('test_resume_standard.pdf', 'wb') as f:
68
+ f.write(pdf_bytes_standard)
69
+ log_info("Standard PDF generated successfully: test_resume_standard.pdf")
70
+ else:
71
+ log_info("Failed to generate standard PDF")
72
+
73
+ # Test modern template
74
+ log_info("Generating modern template PDF...")
75
+ pdf_bytes_modern = create_pdf_resume(test_data, "modern")
76
+
77
+ if pdf_bytes_modern:
78
+ with open('test_resume_modern.pdf', 'wb') as f:
79
+ f.write(pdf_bytes_modern)
80
+ log_info("Modern PDF generated successfully: test_resume_modern.pdf")
81
+ else:
82
+ log_info("Failed to generate modern PDF")
83
+
84
+ except Exception as e:
85
+ log_error(f"Test failed: {str(e)}", e)
86
+
87
+ if __name__ == "__main__":
88
+ test_pdf_generation()
test_user_data.py ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Test PDF generation with actual user data.
4
+ """
5
+
6
+ from models import db, User, Introduction, ProfileSummary, WorkExperience, Project, Education, Skill, Achievement, ProfileSectionOrder
7
+ from app import app
8
+ from pdf_generator import create_pdf_resume
9
+ import calendar
10
+
11
+ def test_with_actual_data():
12
+ """Test PDF generation with actual user data."""
13
+
14
+ with app.app_context():
15
+ # Get user with profile
16
+ user = User.query.filter_by(email='test1@example.com').first()
17
+ if not user:
18
+ print("User test1@example.com not found")
19
+ return
20
+
21
+ print(f"Testing PDF generation for user: {user.email}")
22
+
23
+ # Get profile data
24
+ intro = Introduction.query.filter_by(user_id=user.id).first()
25
+ summary = ProfileSummary.query.filter_by(user_id=user.id).first()
26
+ work_experiences = WorkExperience.query.filter_by(user_id=user.id).order_by(WorkExperience.order).all()
27
+ projects = Project.query.filter_by(user_id=user.id).order_by(Project.order).all()
28
+ educations = Education.query.filter_by(user_id=user.id).order_by(Education.order).all()
29
+ skills = Skill.query.filter_by(user_id=user.id).order_by(Skill.order).all()
30
+ achievements = Achievement.query.filter_by(user_id=user.id).order_by(Achievement.order).all()
31
+
32
+ # Get section order
33
+ section_order_obj = ProfileSectionOrder.query.filter_by(user_id=user.id).first()
34
+ section_order = section_order_obj.section_order if section_order_obj else [
35
+ 'introduction', 'profile_summary', 'work_experience',
36
+ 'projects', 'education', 'skills', 'achievements'
37
+ ]
38
+
39
+ def format_date(month, year):
40
+ """Format month and year as 'Month Year'"""
41
+ if month and year:
42
+ try:
43
+ month_name = calendar.month_name[int(month)]
44
+ return f"{month_name[:3]} {year}"
45
+ except:
46
+ return f"{month}/{year}"
47
+ return ""
48
+
49
+ # Prepare data for PDF generation
50
+ work_exp_list = []
51
+ for exp in work_experiences:
52
+ start_date = format_date(exp.start_month, exp.start_year)
53
+ end_date = "Present" if not exp.end_month or not exp.end_year else format_date(exp.end_month, exp.end_year)
54
+
55
+ work_exp_list.append({
56
+ 'title': exp.title,
57
+ 'organization': exp.organization,
58
+ 'start_date': start_date,
59
+ 'end_date': end_date,
60
+ 'remarks': exp.remarks or ''
61
+ })
62
+
63
+ projects_list = []
64
+ for proj in projects:
65
+ start_date = format_date(proj.start_month, proj.start_year)
66
+ end_date = "Present" if not proj.end_month or not proj.end_year else format_date(proj.end_month, proj.end_year)
67
+
68
+ projects_list.append({
69
+ 'title': proj.title,
70
+ 'organization': proj.organization,
71
+ 'start_date': start_date,
72
+ 'end_date': end_date,
73
+ 'remarks': proj.remarks or ''
74
+ })
75
+
76
+ education_list = []
77
+ for edu in educations:
78
+ start_date = format_date(edu.start_month, edu.start_year)
79
+ end_date = "Present" if not edu.end_month or not edu.end_year else format_date(edu.end_month, edu.end_year)
80
+
81
+ education_list.append({
82
+ 'title': edu.title,
83
+ 'organization': edu.organization,
84
+ 'start_date': start_date,
85
+ 'end_date': end_date,
86
+ 'remarks': edu.remarks or ''
87
+ })
88
+
89
+ # Convert skills and achievements to comma-separated strings
90
+ skills_text = ', '.join([skill.skill for skill in skills]) if skills else ''
91
+ achievements_text = ', '.join([achievement.achievement for achievement in achievements]) if achievements else ''
92
+
93
+ # Create data dictionary
94
+ data = {
95
+ 'name': intro.name,
96
+ 'email': intro.email,
97
+ 'phone': intro.phone,
98
+ 'linkedin': intro.linkedin,
99
+ 'github': intro.github,
100
+ 'website': intro.website,
101
+ 'summary': summary.summary if summary else '',
102
+ 'work_experience': work_exp_list,
103
+ 'projects': projects_list,
104
+ 'education': education_list,
105
+ 'skills': skills_text,
106
+ 'achievements': achievements_text,
107
+ 'sections_order': section_order
108
+ }
109
+
110
+ print("Data prepared:")
111
+ print(f" Name: {data['name']}")
112
+ print(f" Work experiences: {len(work_exp_list)}")
113
+ print(f" Projects: {len(projects_list)}")
114
+ print(f" Education: {len(education_list)}")
115
+ print(f" Skills: {skills_text}")
116
+ print(f" Achievements: {achievements_text}")
117
+
118
+ # Test PDF generation
119
+ try:
120
+ print("\nGenerating standard PDF...")
121
+ pdf_bytes = create_pdf_resume(data, "standard")
122
+ if pdf_bytes:
123
+ with open('test_user_standard.pdf', 'wb') as f:
124
+ f.write(pdf_bytes)
125
+ print("SUCCESS: Standard PDF generated successfully: test_user_standard.pdf")
126
+ else:
127
+ print("✗ Failed to generate standard PDF")
128
+
129
+ print("\nGenerating modern PDF...")
130
+ pdf_bytes = create_pdf_resume(data, "modern")
131
+ if pdf_bytes:
132
+ with open('test_user_modern.pdf', 'wb') as f:
133
+ f.write(pdf_bytes)
134
+ print("SUCCESS: Modern PDF generated successfully: test_user_modern.pdf")
135
+ else:
136
+ print("FAILED: Failed to generate modern PDF")
137
+
138
+ except Exception as e:
139
+ print(f"Error: {str(e)}")
140
+ import traceback
141
+ traceback.print_exc()
142
+
143
+ if __name__ == "__main__":
144
+ test_with_actual_data()
test_weasyprint.py.old ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ import sys
3
+ import os
4
+ sys.path.append(os.path.dirname(os.path.abspath(__file__)))
5
+
6
+ from app import app
7
+
8
+ def test_weasyprint():
9
+ with app.app_context():
10
+ try:
11
+ import weasyprint
12
+ from io import BytesIO
13
+
14
+ # Simple HTML test
15
+ html_content = """
16
+ <!DOCTYPE html>
17
+ <html>
18
+ <head>
19
+ <style>
20
+ body { font-family: Arial; padding: 20px; }
21
+ h1 { color: #333; }
22
+ </style>
23
+ </head>
24
+ <body>
25
+ <h1>Test PDF Generation</h1>
26
+ <p>This is a test of WeasyPrint PDF generation.</p>
27
+ </body>
28
+ </html>
29
+ """
30
+
31
+ # Generate PDF
32
+ pdf_bytes = weasyprint.HTML(string=html_content).write_pdf()
33
+
34
+ # Save test file
35
+ with open('test_weasyprint.pdf', 'wb') as f:
36
+ f.write(pdf_bytes)
37
+
38
+ print("WeasyPrint test successful!")
39
+ print(f"Generated PDF with {len(pdf_bytes)} bytes")
40
+ return True
41
+
42
+ except Exception as e:
43
+ print(f"WeasyPrint test failed: {e}")
44
+ return False
45
+
46
+ if __name__ == "__main__":
47
+ test_weasyprint()
utils.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Utility functions for the application.
3
+ """
4
+
5
+ import logging
6
+ import traceback
7
+ from datetime import datetime
8
+
9
+ # Configure logging
10
+ logging.basicConfig(
11
+ level=logging.INFO,
12
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
13
+ handlers=[
14
+ logging.FileHandler('app.log'),
15
+ logging.StreamHandler()
16
+ ]
17
+ )
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ def log_error(message, exception=None):
23
+ """Log error message with optional exception details."""
24
+ if exception:
25
+ logger.error(f"{message}: {str(exception)}")
26
+ logger.error(traceback.format_exc())
27
+ else:
28
+ logger.error(message)
29
+
30
+
31
+ def log_info(message):
32
+ """Log info message."""
33
+ logger.info(message)
34
+
35
+
36
+ def get_timestamp():
37
+ """Get current timestamp as string."""
38
+ return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
39
+
40
+
41
+ def sanitize_filename(filename):
42
+ """Sanitize filename for safe file operations."""
43
+ # Remove or replace unsafe characters
44
+ unsafe_chars = ['<', '>', ':', '"', '/', '\\', '|', '?', '*']
45
+ for char in unsafe_chars:
46
+ filename = filename.replace(char, '_')
47
+ return filename.strip()