outshine84 commited on
Commit
fe66d5d
·
1 Parent(s): d78ae20

galleria tue foto

Browse files
Files changed (4) hide show
  1. api.py +55 -14
  2. data/user_plants.db +2 -2
  3. pwa-app/src/App.jsx +42 -11
  4. pwa-app/src/styles.css +17 -2
api.py CHANGED
@@ -9,6 +9,7 @@ from datetime import datetime
9
  from logging.handlers import TimedRotatingFileHandler
10
  from pathlib import Path
11
  from typing import Any
 
12
 
13
  import cloudinary
14
  import cloudinary.uploader
@@ -391,16 +392,48 @@ def ensure_user_plants_table(conn: sqlite3.Connection) -> None:
391
  conn.commit()
392
  except Exception:
393
  pass # Column already exists
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
394
  conn.commit()
395
 
396
 
397
- def _user_plant_row_to_payload(row: sqlite3.Row) -> dict[str, Any]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
398
  return {
399
- "id": int(row["id"]),
400
  "plant_name": row["plant_name"],
401
  "user_given_name": row["user_given_name"],
402
  "user": row["user_email"] or row["user_id"],
403
- "user_photo_url": row["user_photo_url"] if "user_photo_url" in row.keys() else None,
 
404
  "created_at_iso": row["created_at"],
405
  "created_at": _format_datetime_display(row["created_at"]),
406
  }
@@ -439,8 +472,7 @@ def create_user_plant(plant_name: str, user_given_name: str, user: dict[str, Any
439
  ),
440
  (cursor.lastrowid,),
441
  ).fetchone()
442
-
443
- return _user_plant_row_to_payload(row)
444
 
445
 
446
  def list_user_plants(user: dict[str, Any]) -> list[dict[str, Any]]:
@@ -457,8 +489,7 @@ def list_user_plants(user: dict[str, Any]) -> list[dict[str, Any]]:
457
  ),
458
  (user_id,),
459
  ).fetchall()
460
-
461
- return [_user_plant_row_to_payload(row) for row in rows]
462
 
463
 
464
  def delete_user_plant_by_id(user: dict[str, Any], plant_id: int) -> bool:
@@ -476,6 +507,11 @@ def delete_user_plant_by_id(user: dict[str, Any], plant_id: int) -> bool:
476
  if existing is None:
477
  return False
478
 
 
 
 
 
 
479
  conn.execute(
480
  "DELETE FROM user_plants WHERE id = ? AND user_id = ?",
481
  (plant_id, user_id),
@@ -513,10 +549,9 @@ def update_user_plant_created_at_by_id(user: dict[str, Any], plant_id: int, crea
513
  ),
514
  (plant_id, user_id),
515
  ).fetchone()
516
-
517
- if row is None:
518
- return None
519
- return _user_plant_row_to_payload(row)
520
 
521
 
522
  def _build_profile_context(profile: dict[str, Any] | None) -> str:
@@ -959,8 +994,8 @@ async def upload_user_plant_photo(
959
  result = cloudinary.uploader.upload(
960
  tmp_path,
961
  folder="green-assistant/user-plants",
962
- public_id=f"plant_{plant_id}_user_{user_id[:16]}",
963
- overwrite=True,
964
  resource_type="image",
965
  transformation=[{"width": 1200, "crop": "limit", "quality": "auto:good"}],
966
  )
@@ -974,6 +1009,11 @@ async def upload_user_plant_photo(
974
  # Save URL to DB
975
  with get_user_plants_db_connection() as conn:
976
  ensure_user_plants_table(conn)
 
 
 
 
 
977
  conn.execute(
978
  "UPDATE user_plants SET user_photo_url = ? WHERE id = ? AND user_id = ?",
979
  (photo_url, plant_id, user_id),
@@ -984,9 +1024,10 @@ async def upload_user_plant_photo(
984
  "FROM user_plants WHERE id = ?",
985
  (plant_id,),
986
  ).fetchone()
 
987
 
988
  _log_api("/user/plants/{plant_id}/photo", "uploaded", {"plant_id": plant_id})
989
- return JSONResponse(content={"updated": _user_plant_row_to_payload(updated_row)})
990
 
991
 
992
  @app.get("/health")
 
9
  from logging.handlers import TimedRotatingFileHandler
10
  from pathlib import Path
11
  from typing import Any
12
+ from uuid import uuid4
13
 
14
  import cloudinary
15
  import cloudinary.uploader
 
392
  conn.commit()
393
  except Exception:
394
  pass # Column already exists
395
+
396
+ conn.execute(
397
+ """
398
+ CREATE TABLE IF NOT EXISTS user_plant_photos (
399
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
400
+ plant_id INTEGER NOT NULL,
401
+ photo_url TEXT NOT NULL,
402
+ created_at TEXT NOT NULL,
403
+ FOREIGN KEY (plant_id) REFERENCES user_plants(id) ON DELETE CASCADE
404
+ )
405
+ """
406
+ )
407
+ conn.execute(
408
+ "CREATE INDEX IF NOT EXISTS idx_user_plant_photos_plant_id ON user_plant_photos(plant_id)"
409
+ )
410
  conn.commit()
411
 
412
 
413
+ def _get_user_plant_photo_urls(conn: sqlite3.Connection, plant_id: int, fallback_url: str | None) -> list[str]:
414
+ rows = conn.execute(
415
+ "SELECT photo_url FROM user_plant_photos WHERE plant_id = ? ORDER BY id DESC",
416
+ (plant_id,),
417
+ ).fetchall()
418
+ urls = [str(r["photo_url"] or "").strip() for r in rows if str(r["photo_url"] or "").strip()]
419
+ if urls:
420
+ return urls
421
+
422
+ fallback = str(fallback_url or "").strip()
423
+ return [fallback] if fallback else []
424
+
425
+
426
+ def _user_plant_row_to_payload(conn: sqlite3.Connection, row: sqlite3.Row) -> dict[str, Any]:
427
+ plant_id = int(row["id"])
428
+ fallback_photo = row["user_photo_url"] if "user_photo_url" in row.keys() else None
429
+ photo_urls = _get_user_plant_photo_urls(conn, plant_id, fallback_photo)
430
  return {
431
+ "id": plant_id,
432
  "plant_name": row["plant_name"],
433
  "user_given_name": row["user_given_name"],
434
  "user": row["user_email"] or row["user_id"],
435
+ "user_photo_url": (photo_urls[0] if photo_urls else None),
436
+ "user_photos": photo_urls,
437
  "created_at_iso": row["created_at"],
438
  "created_at": _format_datetime_display(row["created_at"]),
439
  }
 
472
  ),
473
  (cursor.lastrowid,),
474
  ).fetchone()
475
+ return _user_plant_row_to_payload(conn, row)
 
476
 
477
 
478
  def list_user_plants(user: dict[str, Any]) -> list[dict[str, Any]]:
 
489
  ),
490
  (user_id,),
491
  ).fetchall()
492
+ return [_user_plant_row_to_payload(conn, row) for row in rows]
 
493
 
494
 
495
  def delete_user_plant_by_id(user: dict[str, Any], plant_id: int) -> bool:
 
507
  if existing is None:
508
  return False
509
 
510
+ conn.execute(
511
+ "DELETE FROM user_plant_photos WHERE plant_id = ?",
512
+ (plant_id,),
513
+ )
514
+
515
  conn.execute(
516
  "DELETE FROM user_plants WHERE id = ? AND user_id = ?",
517
  (plant_id, user_id),
 
549
  ),
550
  (plant_id, user_id),
551
  ).fetchone()
552
+ if row is None:
553
+ return None
554
+ return _user_plant_row_to_payload(conn, row)
 
555
 
556
 
557
  def _build_profile_context(profile: dict[str, Any] | None) -> str:
 
994
  result = cloudinary.uploader.upload(
995
  tmp_path,
996
  folder="green-assistant/user-plants",
997
+ public_id=f"plant_{plant_id}_user_{user_id[:12]}_{uuid4().hex[:10]}",
998
+ overwrite=False,
999
  resource_type="image",
1000
  transformation=[{"width": 1200, "crop": "limit", "quality": "auto:good"}],
1001
  )
 
1009
  # Save URL to DB
1010
  with get_user_plants_db_connection() as conn:
1011
  ensure_user_plants_table(conn)
1012
+ created_at = datetime.utcnow().replace(microsecond=0).isoformat() + "Z"
1013
+ conn.execute(
1014
+ "INSERT INTO user_plant_photos (plant_id, photo_url, created_at) VALUES (?, ?, ?)",
1015
+ (plant_id, photo_url, created_at),
1016
+ )
1017
  conn.execute(
1018
  "UPDATE user_plants SET user_photo_url = ? WHERE id = ? AND user_id = ?",
1019
  (photo_url, plant_id, user_id),
 
1024
  "FROM user_plants WHERE id = ?",
1025
  (plant_id,),
1026
  ).fetchone()
1027
+ updated_payload = _user_plant_row_to_payload(conn, updated_row)
1028
 
1029
  _log_api("/user/plants/{plant_id}/photo", "uploaded", {"plant_id": plant_id})
1030
+ return JSONResponse(content={"updated": updated_payload})
1031
 
1032
 
1033
  @app.get("/health")
data/user_plants.db CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:2a08c80a4d4957fb04471f899b1cd29f7475b830ea28f7c03f2a455ed78acc17
3
- size 16384
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:17c8484b3683c813b0fa16716623911dc62f4498e0cf5d73b40f04f6552fbd4b
3
+ size 24576
pwa-app/src/App.jsx CHANGED
@@ -177,6 +177,19 @@ export default function App({ googleClientIdConfigured = false }) {
177
  }, [plantCard]);
178
 
179
  const activeImage = galleryImages.length ? galleryImages[imageIndex % galleryImages.length] : "";
 
 
 
 
 
 
 
 
 
 
 
 
 
180
 
181
  const wateringMonthCalendar = useMemo(() => {
182
  if (!wateringSchedule.length) {
@@ -1144,7 +1157,15 @@ export default function App({ googleClientIdConfigured = false }) {
1144
 
1145
  {!!myPlants.length && (
1146
  <div className="my-plants-list">
1147
- {myPlants.map((item) => (
 
 
 
 
 
 
 
 
1148
  <article
1149
  key={item.id}
1150
  className={`my-plant-item ${selectedMyPlant?.id === item.id ? "active" : ""}`}
@@ -1169,9 +1190,9 @@ export default function App({ googleClientIdConfigured = false }) {
1169
  {deletingPlantId === item.id ? "Elimino..." : "Elimina"}
1170
  </button>
1171
  </div>
1172
- {item.user_photo_url && (
1173
  <img
1174
- src={item.user_photo_url}
1175
  alt={`Foto di ${item.user_given_name}`}
1176
  className="my-plant-item-photo"
1177
  />
@@ -1184,10 +1205,11 @@ export default function App({ googleClientIdConfigured = false }) {
1184
  onClick={(event) => { event.stopPropagation(); openPlantPhotoDialog(item.id); }}
1185
  disabled={uploadingPhotoId === item.id}
1186
  >
1187
- {uploadingPhotoId === item.id ? "Carico..." : (item.user_photo_url ? "📷 Cambia foto" : "📷 Aggiungi foto")}
1188
  </button>
1189
  </article>
1190
- ))}
 
1191
  </div>
1192
  )}
1193
  </section>
@@ -1204,12 +1226,21 @@ export default function App({ googleClientIdConfigured = false }) {
1204
  {selectedMyPlant?.user_given_name && (
1205
  <p>Il tuo nome: {selectedMyPlant.user_given_name}</p>
1206
  )}
1207
- {selectedMyPlant?.user_photo_url && (
1208
- <img
1209
- src={selectedMyPlant.user_photo_url}
1210
- alt={`La tua foto di ${selectedMyPlant.user_given_name}`}
1211
- className="my-plant-detail-photo"
1212
- />
 
 
 
 
 
 
 
 
 
1213
  )}
1214
  {myPlantCard.common_name && <p>Nome comune: {myPlantCard.common_name}</p>}
1215
 
 
177
  }, [plantCard]);
178
 
179
  const activeImage = galleryImages.length ? galleryImages[imageIndex % galleryImages.length] : "";
180
+ const myPlantUserPhotos = useMemo(() => {
181
+ if (!selectedMyPlant) {
182
+ return [];
183
+ }
184
+
185
+ const raw = Array.isArray(selectedMyPlant.user_photos) && selectedMyPlant.user_photos.length
186
+ ? selectedMyPlant.user_photos
187
+ : (selectedMyPlant.user_photo_url ? [selectedMyPlant.user_photo_url] : []);
188
+
189
+ return raw
190
+ .map((url) => toAbsoluteImage(String(url || "").trim()))
191
+ .filter(Boolean);
192
+ }, [selectedMyPlant]);
193
 
194
  const wateringMonthCalendar = useMemo(() => {
195
  if (!wateringSchedule.length) {
 
1157
 
1158
  {!!myPlants.length && (
1159
  <div className="my-plants-list">
1160
+ {myPlants.map((item) => {
1161
+ const cardPhoto = Array.isArray(item.user_photos) && item.user_photos.length
1162
+ ? item.user_photos[0]
1163
+ : item.user_photo_url;
1164
+ const photoCount = Array.isArray(item.user_photos)
1165
+ ? item.user_photos.length
1166
+ : (item.user_photo_url ? 1 : 0);
1167
+
1168
+ return (
1169
  <article
1170
  key={item.id}
1171
  className={`my-plant-item ${selectedMyPlant?.id === item.id ? "active" : ""}`}
 
1190
  {deletingPlantId === item.id ? "Elimino..." : "Elimina"}
1191
  </button>
1192
  </div>
1193
+ {cardPhoto && (
1194
  <img
1195
+ src={toAbsoluteImage(cardPhoto)}
1196
  alt={`Foto di ${item.user_given_name}`}
1197
  className="my-plant-item-photo"
1198
  />
 
1205
  onClick={(event) => { event.stopPropagation(); openPlantPhotoDialog(item.id); }}
1206
  disabled={uploadingPhotoId === item.id}
1207
  >
1208
+ {uploadingPhotoId === item.id ? "Carico..." : (photoCount > 0 ? `📷 Aggiungi altra foto (${photoCount})` : "📷 Aggiungi foto")}
1209
  </button>
1210
  </article>
1211
+ );
1212
+ })}
1213
  </div>
1214
  )}
1215
  </section>
 
1226
  {selectedMyPlant?.user_given_name && (
1227
  <p>Il tuo nome: {selectedMyPlant.user_given_name}</p>
1228
  )}
1229
+ {!!myPlantUserPhotos.length && (
1230
+ <div className="my-plant-user-gallery">
1231
+ <p className="my-plant-user-gallery-title">Le tue foto</p>
1232
+ <div className="my-plant-user-gallery-grid">
1233
+ {myPlantUserPhotos.map((photoUrl, idx) => (
1234
+ <img
1235
+ key={`${photoUrl}-${idx}`}
1236
+ src={photoUrl}
1237
+ alt={`Foto ${idx + 1} di ${selectedMyPlant?.user_given_name || "questa pianta"}`}
1238
+ className="my-plant-detail-photo"
1239
+ loading="lazy"
1240
+ />
1241
+ ))}
1242
+ </div>
1243
+ </div>
1244
  )}
1245
  {myPlantCard.common_name && <p>Nome comune: {myPlantCard.common_name}</p>}
1246
 
pwa-app/src/styles.css CHANGED
@@ -629,10 +629,25 @@ pre {
629
 
630
  .my-plant-detail-photo {
631
  width: 100%;
632
- max-height: 260px;
633
  object-fit: cover;
634
  border-radius: 8px;
635
- margin: 0.6rem 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
636
  }
637
 
638
  .my-plant-item-head {
 
629
 
630
  .my-plant-detail-photo {
631
  width: 100%;
632
+ height: 180px;
633
  object-fit: cover;
634
  border-radius: 8px;
635
+ }
636
+
637
+ .my-plant-user-gallery {
638
+ margin: 0.55rem 0 0.7rem;
639
+ }
640
+
641
+ .my-plant-user-gallery-title {
642
+ margin: 0 0 0.35rem;
643
+ font-weight: 700;
644
+ color: #285943;
645
+ }
646
+
647
+ .my-plant-user-gallery-grid {
648
+ display: grid;
649
+ grid-template-columns: repeat(2, minmax(0, 1fr));
650
+ gap: 0.45rem;
651
  }
652
 
653
  .my-plant-item-head {