Spaces:
Sleeping
Sleeping
Commit ·
826cc2a
1
Parent(s): cc2e3b4
feat: complete editorial UI redesign and robust auth overhaul
Browse files- .gitignore +5 -0
- Dockerfile +21 -0
- README.md +19 -0
- app/__init__.py +0 -0
- app/main.py +10 -3
- app/models/__init__.py +0 -0
- app/requirements.txt +2 -1
- app/routes/__init__.py +0 -0
- app/routes/generate.py +76 -0
- app/routes/portfolio.py +65 -0
- app/services/__init__.py +0 -0
- app/services/github_service.py +57 -33
- app/services/groq_service.py +12 -2
- app/templates/cv_bold.html +119 -0
- app/templates/cv_classic.html +121 -0
- app/templates/cv_minimal.html +114 -0
- app/templates/cv_modern.html +111 -0
.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 |
-
|
|
|
|
|
|
|
| 14 |
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 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 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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>
|