added photo endpoint to server profile pictures and integrated them into person modal in frontend (using claude ofc)

This commit is contained in:
2026-08-17 11:13:29 +02:00
parent 8c0dfaf247
commit 6c5d49e62b
5 changed files with 71 additions and 3 deletions
+10
View File
@@ -37,6 +37,16 @@ def api_person_by_id(id):
return jsonify(person.as_dict())
return jsonify({"error": "not found"}), 404
@app.route("/api/people/<id>/photo")
def api_photo_by_id(id):
for person in addrbook.people:
if person.uid == id:
return jsonify({
"photo":person.photo,
"format":person.photo_fmt,
})
return jsonify({"error": "not found"}), 404
# events
@app.route("/api/events")
def api_events():
+12 -1
View File
@@ -1,4 +1,4 @@
import vobject
import vobject, base64
from datetime import date, datetime
import uuid
@@ -18,6 +18,7 @@ class Person:
self.uid = str(uuid.uuid4())
self.parse_name()
self.parse_photo()
self.parse_birthday()
def as_dict(self):
@@ -27,6 +28,7 @@ class Person:
"last_seen": self.last_seen.isoformat() if self.last_seen else None,
"birthday": self.birthday.isoformat() if self.birthday else None,
"uid": self.uid,
"photo": bool(self.photo)
}
def parse_name(self):
@@ -47,6 +49,15 @@ class Person:
self.birthday = date.fromisoformat(self.vcard.bday.value.strip())
def parse_photo(self):
if not hasattr(self.vcard, "photo"):
return
self.photo_fmt = self.vcard.photo.params.get('type', ['JPEG'])[0].lower()
self.photo = base64.b64encode(self.vcard.photo.value).decode()
def __str__(self):
return f"[name: {self.name}, birthday: {self.birthday}, seen: {str(self.last_seen)}]"
-2
View File
@@ -25,9 +25,7 @@ def get_frequency_score(ab: Addressbook):
scope_events = [e for e in ab.events if event_date(e) >= scope_start]
num_evs = len(scope_events)
print("num: " + str(num_evs))
ev_per_w = num_evs / (scope_in_days / 7)
print("ev_per_w: ", ev_per_w)
score = ev_per_w / target
+33
View File
@@ -236,6 +236,30 @@ function renderDecay(){
badgeHolder.innerHTML = overdue ? `<span class="warn-badge">${overdue}</span>` : '';
}
// ---- avatar photo ----
const photoCache = new Map();
async function loadAvatar(uid){
if(photoCache.has(uid)) return photoCache.get(uid);
try{
const res = await fetch(`/api/people/${uid}/photo`);
if(!res.ok) throw new Error(`${res.status} ${res.statusText}`);
const { format, photo } = await res.json();
const dataUri = `data:image/${format};base64,${photo}`;
photoCache.set(uid, dataUri);
return dataUri;
} catch(err){
console.error('avatar load failed for', uid, err);
return null;
}
}
function avatarHtml(person){
if(!person || !person.photo){
return ``;
}
return `<div class="modal-avatar" id="modal-avatar"></div>`;
}
// ---- modal ----
const overlay = $('#modal-overlay');
const modalBody = $('#modal-body');
@@ -283,6 +307,7 @@ function openPersonModal(uid){
openModal(`
<div class="modal-kicker">Person</div>
${avatarHtml(person)}
<div class="modal-title"><i class="fa-solid fa-circle-user"></i> ${name}</div>
${rows.map(([k,v]) => `<div class="modal-row"><span class="k">${k}</span><span class="v">${v}</span></div>`).join('')}
<div class="modal-section">
@@ -290,6 +315,14 @@ function openPersonModal(uid){
${meetupChips}
</div>
`);
if(person && person.photo){
console.log("loading picture")
loadAvatar(uid).then(src => {
const el = $('#modal-avatar');
if(el) el.style.backgroundImage = `url(${src})`;
});
}
}
function openScoreModal(){
+16
View File
@@ -195,6 +195,22 @@ main.grid{
}
.modal-chip:hover{filter:brightness(1.2)}
.modal-empty{color:var(--dimmer);font-size:.78rem;font-style:italic}
.modal-avatar {
width: 90px;
height: 90px;
border-radius: 50%;
margin: 4px auto 12px;
background-size: cover;
background-position: center;
background-color: var(--border);
}
.modal-avatar.placeholder {
display: flex;
align-items: center;
justify-content: center;
font-size: 32px;
color: var(--border);
}
@media (max-width:980px){
body{height:auto;overflow-y:auto;overflow-x:hidden;padding:1rem}