Musadiq7860 commited on
Commit
826cc2a
·
1 Parent(s): cc2e3b4

feat: complete editorial UI redesign and robust auth overhaul

Browse files
.gitignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ venv/
2
+ .env
3
+ __pycache__/
4
+ *.pyc
5
+ .ipynb_checkpoints/
Dockerfile ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ # Install system dependencies for WeasyPrint
4
+ RUN apt-get update && apt-get install -y \
5
+ libpango-1.0-0 \
6
+ libharfbuzz0b \
7
+ libpangoft2-1.0-0 \
8
+ libpangocairo-1.0-0 \
9
+ libcairo2 \
10
+ libffi-dev \
11
+ shared-mime-info \
12
+ && apt-get clean && rm -rf /var/lib/apt/lists/*
13
+
14
+ WORKDIR /code
15
+
16
+ COPY ./app/requirements.txt /code/requirements.txt
17
+ RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
18
+
19
+ COPY ./app /code/app
20
+
21
+ CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"]
README.md ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: AI Portfolio API
3
+ emoji: 💼
4
+ colorFrom: green
5
+ colorTo: blue
6
+ sdk: docker
7
+ app_port: 7860
8
+ pinned: false
9
+ ---
10
+
11
+ # AI Portfolio & CV Generator API
12
+
13
+ This is the backend for the AI Portfolio & CV Generator.
14
+
15
+ Tech Stack:
16
+ - FastAPI
17
+ - Groq AI (Llama 3.3 70B)
18
+ - WeasyPrint
19
+ - Supabase
app/__init__.py ADDED
File without changes
app/main.py CHANGED
@@ -1,10 +1,17 @@
 
 
 
 
 
 
 
 
 
 
1
  from fastapi import FastAPI
2
  from fastapi.middleware.cors import CORSMiddleware
3
- from dotenv import load_dotenv
4
  from app.routes import github, generate, portfolio
5
 
6
- load_dotenv()
7
-
8
  app = FastAPI(title="AI Portfolio Generator API")
9
 
10
  app.add_middleware(
 
1
+ from dotenv import load_dotenv
2
+ load_dotenv()
3
+
4
+ import os
5
+ print("--- BACKEND STARTUP DIAGNOSTICS ---")
6
+ print(f"GROQ_API_KEY present: {bool(os.getenv('GROQ_API_KEY'))}")
7
+ print(f"SUPABASE_URL present: {bool(os.getenv('SUPABASE_URL'))}")
8
+ print(f"SUPABASE_KEY present: {bool(os.getenv('SUPABASE_KEY'))}")
9
+ print("-----------------------------------")
10
+
11
  from fastapi import FastAPI
12
  from fastapi.middleware.cors import CORSMiddleware
 
13
  from app.routes import github, generate, portfolio
14
 
 
 
15
  app = FastAPI(title="AI Portfolio Generator API")
16
 
17
  app.add_middleware(
app/models/__init__.py ADDED
File without changes
app/requirements.txt CHANGED
@@ -6,4 +6,5 @@ PyMuPDF
6
  weasyprint
7
  supabase
8
  python-multipart
9
- httpx
 
 
6
  weasyprint
7
  supabase
8
  python-multipart
9
+ httpx
10
+ jinja2
app/routes/__init__.py ADDED
File without changes
app/routes/generate.py ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, UploadFile, File, Form
2
+ from app.services.github_service import fetch_github_data
3
+ from app.services.pdf_extract import extract_text_from_pdf, extract_linkedin_sections
4
+ from app.services.groq_service import generate_portfolio_content
5
+ from app.services.cv_generator import generate_cv_pdf
6
+ from app.models.schemas import GenerateRequest
7
+ from fastapi.responses import Response
8
+ import json
9
+
10
+ router = APIRouter()
11
+
12
+ @router.post("/portfolio")
13
+ async def generate_portfolio(
14
+ github_url: str = Form(...),
15
+ role: str = Form(...),
16
+ job_target: str = Form(...),
17
+ skills_to_emphasize: str = Form(...),
18
+ one_liner: str = Form(...),
19
+ highlighted_projects: str = Form(...),
20
+ template: str = Form(...),
21
+ user_id: str = Form(...),
22
+ username: str = Form(...),
23
+ full_name: str = Form(""),
24
+ linkedin_pdf: UploadFile = File(...)
25
+ ):
26
+ pdf_bytes = await linkedin_pdf.read()
27
+ linkedin_text = extract_text_from_pdf(pdf_bytes)
28
+ linkedin_sections = extract_linkedin_sections(linkedin_text)
29
+
30
+ github_data = await fetch_github_data(github_url)
31
+ if "error" in github_data:
32
+ return {"error": github_data["error"]}
33
+
34
+ onboarding = {
35
+ "github_url": github_url,
36
+ "role": role,
37
+ "job_target": job_target,
38
+ "skills_to_emphasize": skills_to_emphasize,
39
+ "one_liner": one_liner,
40
+ "highlighted_projects": json.loads(highlighted_projects),
41
+ "template": template,
42
+ "full_name": full_name
43
+ }
44
+
45
+ portfolio_content = generate_portfolio_content(
46
+ github_data=github_data,
47
+ linkedin_sections=linkedin_sections,
48
+ onboarding=onboarding
49
+ )
50
+
51
+ portfolio_content["user_id"] = user_id
52
+ portfolio_content["username"] = username
53
+ portfolio_content["name"] = full_name or portfolio_content.get("name", username)
54
+ portfolio_content["role"] = role
55
+ portfolio_content["one_liner"] = one_liner
56
+ portfolio_content["template"] = template
57
+ portfolio_content["contact"] = {
58
+ "github": github_data.get("github_url", ""),
59
+ "avatar": github_data.get("avatar", "")
60
+ }
61
+
62
+ return portfolio_content
63
+
64
+
65
+ @router.post("/cv")
66
+ async def generate_cv(
67
+ portfolio: str = Form(...),
68
+ cv_template: str = Form("modern")
69
+ ):
70
+ portfolio_data = json.loads(portfolio)
71
+ pdf_bytes = generate_cv_pdf(portfolio_data, cv_template)
72
+ return Response(
73
+ content=pdf_bytes,
74
+ media_type="application/pdf",
75
+ headers={"Content-Disposition": "attachment; filename=cv.pdf"}
76
+ )
app/routes/portfolio.py ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, HTTPException
2
+ from supabase import create_client
3
+ import os
4
+
5
+ router = APIRouter()
6
+
7
+ _supabase = None
8
+
9
+ def get_supabase():
10
+ global _supabase
11
+ if _supabase is None:
12
+ url = (os.getenv("SUPABASE_URL") or "").strip()
13
+ key = (os.getenv("SUPABASE_KEY") or "").strip()
14
+ print(f"--- DEBUG: Initializing Supabase Client ---")
15
+ print(f"URL provided: '{url}'")
16
+ print(f"Key present: {bool(key)}")
17
+ if not url or not key:
18
+ raise ValueError(f"REQUIRED: SUPABASE_URL and SUPABASE_KEY. Current URL: '{url}'")
19
+ try:
20
+ _supabase = create_client(url, key)
21
+ print("--- DEBUG: Supabase Client Initialized Successfully ---")
22
+ except Exception as e:
23
+ print(f"--- DEBUG: create_client FAILED with: {str(e)} ---")
24
+ raise e
25
+ return _supabase
26
+
27
+ @router.post("/save")
28
+ async def save_portfolio(portfolio: dict):
29
+ try:
30
+ supabase = get_supabase()
31
+ result = supabase.table("portfolios").upsert({
32
+ "user_id": portfolio.get("user_id"),
33
+ "username": portfolio.get("username"),
34
+ "style": portfolio.get("style"),
35
+ "content": portfolio.get("content"),
36
+ "github_data": portfolio.get("github_data"),
37
+ "focus_role": portfolio.get("focus_role"),
38
+ "target_job": portfolio.get("target_job")
39
+ }).execute()
40
+ return {"message": "Portfolio saved successfully", "data": result.data}
41
+ except Exception as e:
42
+ raise HTTPException(status_code=500, detail=str(e))
43
+
44
+ @router.get("/{username}")
45
+ async def get_portfolio(username: str):
46
+ try:
47
+ supabase = get_supabase()
48
+ result = supabase.table("portfolios").select("*").eq(
49
+ "username", username
50
+ ).single().execute()
51
+ if not result.data:
52
+ raise HTTPException(status_code=404, detail="Portfolio not found")
53
+ return result.data
54
+ except Exception as e:
55
+ raise HTTPException(status_code=404, detail="Portfolio not found")
56
+
57
+ @router.get("/")
58
+ async def get_all_portfolios():
59
+ try:
60
+ supabase = get_supabase()
61
+ # Corrected columns to match actual schema
62
+ result = supabase.table("portfolios").select("username, style, content->name, content->role").execute()
63
+ return result.data
64
+ except Exception as e:
65
+ raise HTTPException(status_code=500, detail=str(e))
app/services/__init__.py ADDED
File without changes
app/services/github_service.py CHANGED
@@ -3,6 +3,8 @@ import os
3
  from typing import List
4
 
5
  GITHUB_TOKEN = os.getenv("GITHUB_TOKEN")
 
 
6
 
7
  headers = {
8
  "Authorization": f"token {GITHUB_TOKEN}",
@@ -10,41 +12,63 @@ headers = {
10
  }
11
 
12
  async def fetch_github_data(github_url: str) -> dict:
13
- username = github_url.rstrip("/").split("/")[-1]
 
 
14
 
15
- async with httpx.AsyncClient() as client:
16
- user_res = await client.get(
17
- f"https://api.github.com/users/{username}",
18
- headers=headers
19
- )
20
- user_data = user_res.json()
21
-
22
- repos_res = await client.get(
23
- f"https://api.github.com/users/{username}/repos?sort=updated&per_page=30",
24
- headers=headers
25
- )
26
- repos_data = repos_res.json()
27
-
28
- repos = []
29
- for repo in repos_data:
30
- if not repo.get("fork"):
31
- repos.append({
32
- "name": repo.get("name"),
33
- "description": repo.get("description") or "",
34
- "languages": repo.get("language") or "Not specified",
35
- "stars": repo.get("stargazers_count"),
36
- "github_url": repo.get("html_url"),
37
- "topics": repo.get("topics", []),
38
- })
39
 
40
- return {
41
- "username": username,
42
- "name": user_data.get("name") or username,
43
- "bio": user_data.get("bio") or "",
44
- "avatar": user_data.get("avatar_url"),
45
- "location": user_data.get("location") or "",
46
- "repos": repos
47
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
 
49
  async def fetch_repo_languages(username: str, repo_name: str) -> List[str]:
50
  async with httpx.AsyncClient() as client:
 
3
  from typing import List
4
 
5
  GITHUB_TOKEN = os.getenv("GITHUB_TOKEN")
6
+ if not GITHUB_TOKEN:
7
+ print("INFO: GITHUB_TOKEN not found in Environment Variables (optional)")
8
 
9
  headers = {
10
  "Authorization": f"token {GITHUB_TOKEN}",
 
12
  }
13
 
14
  async def fetch_github_data(github_url: str) -> dict:
15
+ # Improved parsing to handle queries and trailing slashes
16
+ clean_url = github_url.split('?')[0].split('#')[0].rstrip('/')
17
+ username = clean_url.split("/")[-1]
18
 
19
+ # Prepare headers safely
20
+ request_headers = {"Accept": "application/vnd.github.v3+json"}
21
+ if GITHUB_TOKEN and GITHUB_TOKEN != "None":
22
+ request_headers["Authorization"] = f"token {GITHUB_TOKEN}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
+ try:
25
+ async with httpx.AsyncClient() as client:
26
+ user_res = await client.get(
27
+ f"https://api.github.com/users/{username}",
28
+ headers=request_headers,
29
+ timeout=10.0
30
+ )
31
+ if user_res.status_code != 200:
32
+ return {"error": f"GitHub user not found or API error: {user_res.status_code}"}
33
+
34
+ user_data = user_res.json()
35
+
36
+ repos_res = await client.get(
37
+ f"https://api.github.com/users/{username}/repos?sort=updated&per_page=30",
38
+ headers=request_headers,
39
+ timeout=10.0
40
+ )
41
+ if repos_res.status_code != 200:
42
+ return {"error": f"Error fetching repos: {repos_res.status_code}"}
43
+
44
+ repos_data = repos_res.json()
45
+
46
+ if not isinstance(repos_data, list):
47
+ return {"error": "Unexpected response format from GitHub API"}
48
+
49
+ repos = []
50
+ for repo in repos_data:
51
+ if not repo.get("fork"):
52
+ repos.append({
53
+ "name": repo.get("name"),
54
+ "description": repo.get("description") or "",
55
+ "languages": repo.get("language") or "Not specified",
56
+ "stars": repo.get("stargazers_count"),
57
+ "github_url": repo.get("html_url"),
58
+ "topics": repo.get("topics", []),
59
+ })
60
+
61
+ return {
62
+ "username": username,
63
+ "name": user_data.get("name") or username,
64
+ "bio": user_data.get("bio") or "",
65
+ "avatar": user_data.get("avatar_url"),
66
+ "location": user_data.get("location") or "",
67
+ "repos": repos
68
+ }
69
+ except Exception as e:
70
+ print(f"Error in fetch_github_data: {e}")
71
+ return {"error": str(e)}
72
 
73
  async def fetch_repo_languages(username: str, repo_name: str) -> List[str]:
74
  async with httpx.AsyncClient() as client:
app/services/groq_service.py CHANGED
@@ -2,13 +2,23 @@ import os
2
  from groq import Groq
3
  import json
4
 
5
- client = Groq(api_key=os.getenv("GROQ_API_KEY"))
 
 
 
 
 
 
 
 
 
6
 
7
  def generate_portfolio_content(
8
  github_data: dict,
9
  linkedin_sections: dict,
10
  onboarding: dict
11
  ) -> dict:
 
12
  highlighted = onboarding.get("highlighted_projects", [])
13
  all_repos = github_data.get("repos", [])
14
 
@@ -19,7 +29,7 @@ def generate_portfolio_content(
19
  You are a professional portfolio writer. Generate portfolio content for a developer.
20
 
21
  DEVELOPER INFO:
22
- - Name: {github_data.get('name')}
23
  - Role: {onboarding.get('role')}
24
  - One Liner: {onboarding.get('one_liner')}
25
  - Job Target: {onboarding.get('job_target')}
 
2
  from groq import Groq
3
  import json
4
 
5
+ _groq_client = None
6
+
7
+ def get_groq_client():
8
+ global _groq_client
9
+ if _groq_client is None:
10
+ key = os.getenv("GROQ_API_KEY")
11
+ if not key:
12
+ raise ValueError("GROQ_API_KEY must be set")
13
+ _groq_client = Groq(api_key=key)
14
+ return _groq_client
15
 
16
  def generate_portfolio_content(
17
  github_data: dict,
18
  linkedin_sections: dict,
19
  onboarding: dict
20
  ) -> dict:
21
+ client = get_groq_client()
22
  highlighted = onboarding.get("highlighted_projects", [])
23
  all_repos = github_data.get("repos", [])
24
 
 
29
  You are a professional portfolio writer. Generate portfolio content for a developer.
30
 
31
  DEVELOPER INFO:
32
+ - Name: {onboarding.get('full_name') or github_data.get('name') or github_data.get('username')}
33
  - Role: {onboarding.get('role')}
34
  - One Liner: {onboarding.get('one_liner')}
35
  - Job Target: {onboarding.get('job_target')}
app/templates/cv_bold.html ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <style>
6
+ * { margin: 0; padding: 0; box-sizing: border-box; }
7
+ body { font-family: 'Inter', Arial, sans-serif; color: #111; background: #fff; padding: 0; }
8
+ .sidebar { position: absolute; left: 0; top: 0; bottom: 0; width: 30%; background: #1a1a1a; color: #fff; padding: 40px 30px; }
9
+ .main { margin-left: 30%; width: 70%; padding: 40px 40px; }
10
+ .name { font-size: 32px; font-weight: 800; line-height: 1.1; margin-bottom: 10px; text-transform: uppercase; }
11
+ .role { font-size: 16px; color: #ff3e3e; font-weight: 600; text-transform: uppercase; letter-spacing: 2px; margin-bottom: 20px; }
12
+ .contact-item { margin-bottom: 15px; font-size: 12px; color: #ccc; }
13
+ .contact-label { font-weight: bold; color: #fff; display: block; margin-bottom: 2px; text-transform: uppercase; font-size: 10px; letter-spacing: 1px; }
14
+ .sidebar-section { margin-top: 40px; }
15
+ .sidebar-title { font-size: 14px; font-weight: bold; text-transform: uppercase; letter-spacing: 2px; border-bottom: 2px solid #ff3e3e; padding-bottom: 5px; margin-bottom: 15px; }
16
+ .skill-tag { display: inline-block; background: #333; color: #fff; padding: 4px 10px; border-radius: 4px; font-size: 11px; margin-right: 5px; margin-bottom: 8px; }
17
+ .section-title { font-size: 18px; font-weight: 800; text-transform: uppercase; letter-spacing: 2px; margin-bottom: 20px; color: #1a1a1a; display: flex; align-items: center; }
18
+ .section-title::after { content: ''; flex: 1; height: 2px; background: #eee; margin-left: 15px; }
19
+ .bio { font-size: 14px; line-height: 1.6; color: #444; margin-bottom: 30px; }
20
+ .project { margin-bottom: 25px; }
21
+ .project-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px; }
22
+ .project-name { font-size: 15px; font-weight: 700; color: #1a1a1a; }
23
+ .project-featured { font-size: 10px; background: #ff3e3e; color: #fff; padding: 2px 8px; border-radius: 20px; font-weight: bold; }
24
+ .project-desc { font-size: 13px; line-height: 1.5; color: #555; }
25
+ .project-tech { font-size: 11px; color: #888; margin-top: 5px; font-weight: 600; }
26
+ .exp-item { margin-bottom: 20px; }
27
+ .exp-title { font-size: 15px; font-weight: 700; color: #1a1a1a; }
28
+ .exp-company-dates { font-size: 12px; color: #ff3e3e; font-weight: 600; margin-bottom: 5px; }
29
+ .exp-desc { font-size: 13px; line-height: 1.5; color: #555; }
30
+ .edu-item { margin-bottom: 15px; }
31
+ .edu-degree { font-size: 15px; font-weight: 700; color: #1a1a1a; }
32
+ .edu-school-year { font-size: 12px; color: #555; }
33
+ </style>
34
+ </head>
35
+ <body>
36
+
37
+ <div class="sidebar">
38
+ <div class="name">{{ name }}</div>
39
+ <div class="role">{{ role }}</div>
40
+
41
+ <div class="sidebar-section">
42
+ <div class="contact-item">
43
+ <span class="contact-label">GitHub</span>
44
+ {{ contact.github or 'Not Provided' }}
45
+ </div>
46
+ {% if suggested_rate %}
47
+ <div class="contact-item">
48
+ <span class="contact-label">Expected Rate</span>
49
+ {{ suggested_rate }}
50
+ </div>
51
+ {% endif %}
52
+ {% if one_liner %}
53
+ <div class="contact-item">
54
+ <span class="contact-label">Focus</span>
55
+ {{ one_liner }}
56
+ </div>
57
+ {% endif %}
58
+ </div>
59
+
60
+ <div class="sidebar-section">
61
+ <div class="sidebar-title">Skills</div>
62
+ <div class="skills-list">
63
+ {% for skill in skills %}
64
+ <span class="skill-tag">{{ skill }}</span>
65
+ {% endfor %}
66
+ </div>
67
+ </div>
68
+ </div>
69
+
70
+ <div class="main">
71
+ <div class="section">
72
+ <div class="section-title">About Me</div>
73
+ <div class="bio">{{ bio }}</div>
74
+ </div>
75
+
76
+ {% if featured_projects %}
77
+ <div class="section">
78
+ <div class="section-title">Featured Work</div>
79
+ {% for project in featured_projects %}
80
+ <div class="project">
81
+ <div class="project-header">
82
+ <span class="project-name">{{ project.name }}</span>
83
+ <span class="project-featured">FEATURED</span>
84
+ </div>
85
+ <div class="project-desc">{{ project.description }}</div>
86
+ <div class="project-tech">{{ project.technologies | join(' // ') }}</div>
87
+ </div>
88
+ {% endfor %}
89
+ </div>
90
+ {% endif %}
91
+
92
+ {% if experience %}
93
+ <div class="section">
94
+ <div class="section-title">Experience</div>
95
+ {% for exp in experience %}
96
+ <div class="exp-item">
97
+ <div class="exp-title">{{ exp.title }}</div>
98
+ <div class="exp-company-dates">{{ exp.company }} // {{ exp.duration }}</div>
99
+ <div class="exp-desc">{{ exp.description }}</div>
100
+ </div>
101
+ {% endfor %}
102
+ </div>
103
+ {% endif %}
104
+
105
+ {% if education %}
106
+ <div class="section">
107
+ <div class="section-title">Education</div>
108
+ {% for edu in education %}
109
+ <div class="edu-item">
110
+ <div class="edu-degree">{{ edu.degree }}</div>
111
+ <div class="edu-school-year">{{ edu.institution }} // {{ edu.year }}</div>
112
+ </div>
113
+ {% endfor %}
114
+ </div>
115
+ {% endif %}
116
+ </div>
117
+
118
+ </body>
119
+ </html>
app/templates/cv_classic.html ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <style>
6
+ * { margin: 0; padding: 0; box-sizing: border-box; }
7
+ body { font-family: 'Times New Roman', Times, serif; color: #333; background: #fff; padding: 40px 50px; line-height: 1.5; }
8
+ .header { text-align: center; margin-bottom: 30px; border-bottom: 2px solid #333; padding-bottom: 10px; }
9
+ .name { font-size: 32px; font-weight: bold; text-transform: uppercase; letter-spacing: 1px; }
10
+ .role { font-size: 18px; color: #555; margin-top: 5px; }
11
+ .contact { margin-top: 10px; font-size: 12px; }
12
+ .contact span { margin: 0 10px; }
13
+ .section { margin-bottom: 25px; }
14
+ .section-title { font-size: 16px; font-weight: bold; text-transform: uppercase; border-bottom: 1px solid #ccc; margin-bottom: 10px; color: #000; }
15
+ .bio { font-size: 13px; text-align: justify; }
16
+ .skills-list { font-size: 13px; margin-left: 20px; }
17
+ .skill-item { display: inline-block; margin-right: 15px; margin-bottom: 5px; }
18
+ .project { margin-bottom: 15px; }
19
+ .project-header { display: flex; justify-content: space-between; align-items: baseline; }
20
+ .project-name { font-size: 14px; font-weight: bold; }
21
+ .project-featured { font-size: 10px; font-style: italic; color: #666; margin-left: 5px; }
22
+ .project-desc { font-size: 13px; margin-top: 3px; }
23
+ .project-tech { font-size: 12px; color: #666; font-style: italic; margin-top: 2px; }
24
+ .exp-item { margin-bottom: 15px; }
25
+ .exp-header { display: flex; justify-content: space-between; align-items: baseline; }
26
+ .exp-title { font-size: 14px; font-weight: bold; }
27
+ .exp-company-dates { font-size: 14px; color: #555; }
28
+ .exp-desc { font-size: 13px; margin-top: 3px; }
29
+ .edu-item { margin-bottom: 10px; }
30
+ .edu-header { display: flex; justify-content: space-between; align-items: baseline; }
31
+ .edu-degree { font-size: 14px; font-weight: bold; }
32
+ .edu-school-year { font-size: 14px; color: #555; }
33
+ </style>
34
+ </head>
35
+ <body>
36
+
37
+ <div class="header">
38
+ <div class="name">{{ name }}</div>
39
+ <div class="role">{{ role }}</div>
40
+ <div class="contact">
41
+ {% if contact.github %}<span>GitHub: {{ contact.github }}</span>{% endif %}
42
+ {% if one_liner %}<span>{{ one_liner }}</span>{% endif %}
43
+ {% if suggested_rate %}<span>Rate: {{ suggested_rate }}</span>{% endif %}
44
+ </div>
45
+ </div>
46
+
47
+ <div class="section">
48
+ <div class="section-title">Professional Summary</div>
49
+ <div class="bio">{{ bio }}</div>
50
+ </div>
51
+
52
+ <div class="section">
53
+ <div class="section-title">Technical Skills</div>
54
+ <div class="skills-list">
55
+ {% for skill in skills %}
56
+ <span class="skill-item">{{ skill }}{% if not loop.last %}, {% endif %}</span>
57
+ {% endfor %}
58
+ </div>
59
+ </div>
60
+
61
+ {% if featured_projects %}
62
+ <div class="section">
63
+ <div class="section-title">Featured Projects</div>
64
+ {% for project in featured_projects %}
65
+ <div class="project">
66
+ <div class="project-header">
67
+ <span class="project-name">{{ project.name }}<span class="project-featured">(Featured)</span></span>
68
+ </div>
69
+ <div class="project-desc">{{ project.description }}</div>
70
+ <div class="project-tech">Technologies: {{ project.technologies | join(', ') }}</div>
71
+ </div>
72
+ {% endfor %}
73
+ </div>
74
+ {% endif %}
75
+
76
+ {% if other_projects %}
77
+ <div class="section">
78
+ <div class="section-title">Other Projects</div>
79
+ {% for project in other_projects %}
80
+ <div class="project">
81
+ <div class="project-header">
82
+ <span class="project-name">{{ project.name }}</span>
83
+ </div>
84
+ <div class="project-desc">{{ project.description }}</div>
85
+ <div class="project-tech">Technologies: {{ project.technologies | join(', ') }}</div>
86
+ </div>
87
+ {% endfor %}
88
+ </div>
89
+ {% endif %}
90
+
91
+ {% if experience %}
92
+ <div class="section">
93
+ <div class="section-title">Work Experience</div>
94
+ {% for exp in experience %}
95
+ <div class="exp-item">
96
+ <div class="exp-header">
97
+ <span class="exp-title">{{ exp.title }}</span>
98
+ <span class="exp-company-dates">{{ exp.company }} | {{ exp.duration }}</span>
99
+ </div>
100
+ <div class="exp-desc">{{ exp.description }}</div>
101
+ </div>
102
+ {% endfor %}
103
+ </div>
104
+ {% endif %}
105
+
106
+ {% if education %}
107
+ <div class="section">
108
+ <div class="section-title">Education</div>
109
+ {% for edu in education %}
110
+ <div class="edu-item">
111
+ <div class="edu-header">
112
+ <span class="edu-degree">{{ edu.degree }}</span>
113
+ <span class="edu-school-year">{{ edu.institution }} | {{ edu.year }}</span>
114
+ </div>
115
+ </div>
116
+ {% endfor %}
117
+ </div>
118
+ {% endif %}
119
+
120
+ </body>
121
+ </html>
app/templates/cv_minimal.html ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <style>
6
+ * { margin: 0; padding: 0; box-sizing: border-box; }
7
+ body { font-family: 'Helvetica Neue', Arial, sans-serif; color: #2d2d2d; background: #fff; padding: 50px 60px; }
8
+ .header { margin-bottom: 36px; }
9
+ .name { font-size: 28px; font-weight: 300; letter-spacing: 4px; text-transform: uppercase; }
10
+ .role { font-size: 12px; color: #999; margin-top: 6px; letter-spacing: 3px; text-transform: uppercase; }
11
+ .one-liner { font-size: 13px; color: #555; margin-top: 10px; }
12
+ .contact { margin-top: 10px; font-size: 11px; color: #aaa; }
13
+ .divider { height: 1px; background: #eee; margin: 20px 0; }
14
+ .section { margin-bottom: 28px; }
15
+ .section-title { font-size: 10px; font-weight: 600; letter-spacing: 4px; text-transform: uppercase; color: #aaa; margin-bottom: 14px; }
16
+ .bio { font-size: 12px; line-height: 1.8; color: #555; }
17
+ .skills-list { display: flex; flex-wrap: wrap; gap: 6px; }
18
+ .skill-tag { border: 1px solid #ddd; padding: 3px 10px; font-size: 11px; color: #666; }
19
+ .project { margin-bottom: 14px; padding-bottom: 14px; border-bottom: 1px solid #f5f5f5; }
20
+ .project:last-child { border-bottom: none; }
21
+ .project-name { font-size: 13px; font-weight: 600; color: #2d2d2d; }
22
+ .project-featured { font-size: 9px; color: #aaa; letter-spacing: 2px; text-transform: uppercase; margin-left: 8px; }
23
+ .project-desc { font-size: 11px; color: #777; margin-top: 4px; line-height: 1.7; }
24
+ .project-tech { font-size: 10px; color: #bbb; margin-top: 4px; }
25
+ .exp-item { margin-bottom: 14px; }
26
+ .exp-title { font-size: 13px; font-weight: 600; }
27
+ .exp-company { font-size: 11px; color: #aaa; margin-top: 2px; }
28
+ .exp-desc { font-size: 11px; color: #777; margin-top: 4px; line-height: 1.7; }
29
+ .edu-item { margin-bottom: 10px; }
30
+ .edu-degree { font-size: 13px; font-weight: 600; }
31
+ .edu-school { font-size: 11px; color: #aaa; margin-top: 2px; }
32
+ </style>
33
+ </head>
34
+ <body>
35
+
36
+ <div class="header">
37
+ <div class="name">{{ name }}</div>
38
+ <div class="role">{{ role }}</div>
39
+ <div class="one-liner">{{ one_liner }}</div>
40
+ <div class="contact">
41
+ {% if contact.github %}{{ contact.github }}{% endif %}
42
+ {% if suggested_rate %} · {{ suggested_rate }}{% endif %}
43
+ </div>
44
+ </div>
45
+
46
+ <div class="divider"></div>
47
+
48
+ <div class="section">
49
+ <div class="section-title">Profile</div>
50
+ <div class="bio">{{ bio }}</div>
51
+ </div>
52
+
53
+ <div class="section">
54
+ <div class="section-title">Skills</div>
55
+ <div class="skills-list">
56
+ {% for skill in skills %}
57
+ <span class="skill-tag">{{ skill }}</span>
58
+ {% endfor %}
59
+ </div>
60
+ </div>
61
+
62
+ {% if featured_projects %}
63
+ <div class="section">
64
+ <div class="section-title">Featured Projects</div>
65
+ {% for project in featured_projects %}
66
+ <div class="project">
67
+ <div class="project-name">{{ project.name }}<span class="project-featured">featured</span></div>
68
+ <div class="project-desc">{{ project.description }}</div>
69
+ <div class="project-tech">{{ project.technologies | join(' · ') }}</div>
70
+ </div>
71
+ {% endfor %}
72
+ </div>
73
+ {% endif %}
74
+
75
+ {% if other_projects %}
76
+ <div class="section">
77
+ <div class="section-title">Other Projects</div>
78
+ {% for project in other_projects %}
79
+ <div class="project">
80
+ <div class="project-name">{{ project.name }}</div>
81
+ <div class="project-desc">{{ project.description }}</div>
82
+ <div class="project-tech">{{ project.technologies | join(' · ') }}</div>
83
+ </div>
84
+ {% endfor %}
85
+ </div>
86
+ {% endif %}
87
+
88
+ {% if experience %}
89
+ <div class="section">
90
+ <div class="section-title">Experience</div>
91
+ {% for exp in experience %}
92
+ <div class="exp-item">
93
+ <div class="exp-title">{{ exp.title }}</div>
94
+ <div class="exp-company">{{ exp.company }} · {{ exp.duration }}</div>
95
+ <div class="exp-desc">{{ exp.description }}</div>
96
+ </div>
97
+ {% endfor %}
98
+ </div>
99
+ {% endif %}
100
+
101
+ {% if education %}
102
+ <div class="section">
103
+ <div class="section-title">Education</div>
104
+ {% for edu in education %}
105
+ <div class="edu-item">
106
+ <div class="edu-degree">{{ edu.degree }}</div>
107
+ <div class="edu-school">{{ edu.institution }} · {{ edu.year }}</div>
108
+ </div>
109
+ {% endfor %}
110
+ </div>
111
+ {% endif %}
112
+
113
+ </body>
114
+ </html>
app/templates/cv_modern.html ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <style>
6
+ * { margin: 0; padding: 0; box-sizing: border-box; }
7
+ body { font-family: 'Georgia', serif; color: #1a1a1a; background: #fff; padding: 40px; }
8
+ .header { border-bottom: 3px solid #000; padding-bottom: 20px; margin-bottom: 24px; }
9
+ .name { font-size: 36px; font-weight: 700; letter-spacing: -1px; }
10
+ .role { font-size: 16px; color: #555; margin-top: 4px; }
11
+ .one-liner { font-size: 14px; color: #333; margin-top: 8px; font-style: italic; }
12
+ .contact { display: flex; gap: 20px; margin-top: 12px; font-size: 12px; color: #555; }
13
+ .section { margin-bottom: 24px; }
14
+ .section-title { font-size: 13px; font-weight: 700; letter-spacing: 3px; text-transform: uppercase; border-bottom: 1px solid #ddd; padding-bottom: 6px; margin-bottom: 14px; color: #000; }
15
+ .bio { font-size: 13px; line-height: 1.7; color: #333; }
16
+ .skills-list { display: flex; flex-wrap: wrap; gap: 8px; }
17
+ .skill-tag { background: #f0f0f0; padding: 4px 10px; border-radius: 3px; font-size: 12px; }
18
+ .project { margin-bottom: 16px; }
19
+ .project-name { font-size: 14px; font-weight: 700; }
20
+ .project-featured { font-size: 10px; background: #000; color: #fff; padding: 2px 6px; border-radius: 2px; margin-left: 8px; vertical-align: middle; }
21
+ .project-desc { font-size: 12px; color: #444; margin-top: 4px; line-height: 1.6; }
22
+ .project-tech { font-size: 11px; color: #777; margin-top: 4px; }
23
+ .exp-item { margin-bottom: 14px; }
24
+ .exp-title { font-size: 14px; font-weight: 700; }
25
+ .exp-company { font-size: 12px; color: #555; }
26
+ .exp-desc { font-size: 12px; color: #444; margin-top: 4px; line-height: 1.6; }
27
+ .edu-item { margin-bottom: 10px; }
28
+ .edu-degree { font-size: 14px; font-weight: 700; }
29
+ .edu-school { font-size: 12px; color: #555; }
30
+ .rate { font-size: 13px; color: #333; font-weight: 600; }
31
+ </style>
32
+ </head>
33
+ <body>
34
+
35
+ <div class="header">
36
+ <div class="name">{{ name }}</div>
37
+ <div class="role">{{ role }}</div>
38
+ <div class="one-liner">{{ one_liner }}</div>
39
+ <div class="contact">
40
+ {% if contact.github %}<span>GitHub: {{ contact.github }}</span>{% endif %}
41
+ {% if suggested_rate %}<span>Rate: {{ suggested_rate }}</span>{% endif %}
42
+ </div>
43
+ </div>
44
+
45
+ <div class="section">
46
+ <div class="section-title">About</div>
47
+ <div class="bio">{{ bio }}</div>
48
+ </div>
49
+
50
+ <div class="section">
51
+ <div class="section-title">Skills</div>
52
+ <div class="skills-list">
53
+ {% for skill in skills %}
54
+ <span class="skill-tag">{{ skill }}</span>
55
+ {% endfor %}
56
+ </div>
57
+ </div>
58
+
59
+ {% if featured_projects %}
60
+ <div class="section">
61
+ <div class="section-title">Featured Projects</div>
62
+ {% for project in featured_projects %}
63
+ <div class="project">
64
+ <div class="project-name">{{ project.name }}<span class="project-featured">FEATURED</span></div>
65
+ <div class="project-desc">{{ project.description }}</div>
66
+ <div class="project-tech">{{ project.technologies | join(', ') }}</div>
67
+ </div>
68
+ {% endfor %}
69
+ </div>
70
+ {% endif %}
71
+
72
+ {% if other_projects %}
73
+ <div class="section">
74
+ <div class="section-title">Other Projects</div>
75
+ {% for project in other_projects %}
76
+ <div class="project">
77
+ <div class="project-name">{{ project.name }}</div>
78
+ <div class="project-desc">{{ project.description }}</div>
79
+ <div class="project-tech">{{ project.technologies | join(', ') }}</div>
80
+ </div>
81
+ {% endfor %}
82
+ </div>
83
+ {% endif %}
84
+
85
+ {% if experience %}
86
+ <div class="section">
87
+ <div class="section-title">Experience</div>
88
+ {% for exp in experience %}
89
+ <div class="exp-item">
90
+ <div class="exp-title">{{ exp.title }}</div>
91
+ <div class="exp-company">{{ exp.company }} · {{ exp.duration }}</div>
92
+ <div class="exp-desc">{{ exp.description }}</div>
93
+ </div>
94
+ {% endfor %}
95
+ </div>
96
+ {% endif %}
97
+
98
+ {% if education %}
99
+ <div class="section">
100
+ <div class="section-title">Education</div>
101
+ {% for edu in education %}
102
+ <div class="edu-item">
103
+ <div class="edu-degree">{{ edu.degree }}</div>
104
+ <div class="edu-school">{{ edu.institution }} · {{ edu.year }}</div>
105
+ </div>
106
+ {% endfor %}
107
+ </div>
108
+ {% endif %}
109
+
110
+ </body>
111
+ </html>