added photo endpoint to server profile pictures and integrated them into person modal in frontend (using claude ofc)
This commit is contained in:
@@ -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
@@ -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)}]"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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(){
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user