feat(market): show offer photos in the market list and detail
Render offer.imageUrl (already carried by NIP-99) as a thumbnail on each market card and a hero image on the offer detail page. Remote images degrade gracefully: a spinner while loading, a neutral placeholder when the media server is unreachable. Publishing the photo comes next.
This commit is contained in:
parent
d36fd05741
commit
0150a7ce02
14 changed files with 633 additions and 111 deletions
|
|
@ -214,8 +214,18 @@
|
|||
"anyMonth": "Cualquier mes",
|
||||
"noDate": "Indicar data de coyecha",
|
||||
"monthNames": [
|
||||
"xineru", "febreru", "marzu", "abril", "mayu", "xunu",
|
||||
"xunetu", "agostu", "setiembre", "ochobre", "payares", "avientu"
|
||||
"xineru",
|
||||
"febreru",
|
||||
"marzu",
|
||||
"abril",
|
||||
"mayu",
|
||||
"xunu",
|
||||
"xunetu",
|
||||
"agostu",
|
||||
"setiembre",
|
||||
"ochobre",
|
||||
"payares",
|
||||
"avientu"
|
||||
]
|
||||
},
|
||||
"lotType": {
|
||||
|
|
@ -330,30 +340,102 @@
|
|||
"some": "dalgunes",
|
||||
"plenty": "munches",
|
||||
"pinch": "una migaya",
|
||||
"handful": { "singular": "puñáu", "plural": "puñaos" },
|
||||
"teaspoon": { "singular": "cuyaradina", "plural": "cuyaradines" },
|
||||
"spoon": { "singular": "cuyar", "plural": "cuyares" },
|
||||
"cup": { "singular": "taza", "plural": "tazes" },
|
||||
"jar": { "singular": "bote", "plural": "botes" },
|
||||
"sack": { "singular": "sacu", "plural": "sacos" },
|
||||
"packet": { "singular": "sobre", "plural": "sobres" },
|
||||
"cob": { "singular": "panoya", "plural": "panoyes" },
|
||||
"pod": { "singular": "vaina", "plural": "vaines" },
|
||||
"ear": { "singular": "espiga", "plural": "espigues" },
|
||||
"head": { "singular": "cabezuela", "plural": "cabezueles" },
|
||||
"fruit": { "singular": "frutu", "plural": "frutos" },
|
||||
"bulb": { "singular": "bulbu", "plural": "bulbos" },
|
||||
"tuber": { "singular": "tubérculu", "plural": "tubérculos" },
|
||||
"seedHead": { "singular": "cabeza de simiente", "plural": "cabeces de simiente" },
|
||||
"bunch": { "singular": "manoyu", "plural": "manoyos" },
|
||||
"plant": { "singular": "planta", "plural": "plantes" },
|
||||
"pot": { "singular": "tiestu", "plural": "tiestos" },
|
||||
"tray": { "singular": "bandexa", "plural": "bandexes" },
|
||||
"seedling": { "singular": "plántula", "plural": "plántules" },
|
||||
"tree": { "singular": "árbol", "plural": "árboles" },
|
||||
"cutting": { "singular": "esqueje", "plural": "esquejes" },
|
||||
"grams": { "singular": "gramu", "plural": "gramos" },
|
||||
"count": { "singular": "grana", "plural": "granes" }
|
||||
"handful": {
|
||||
"singular": "puñáu",
|
||||
"plural": "puñaos"
|
||||
},
|
||||
"teaspoon": {
|
||||
"singular": "cuyaradina",
|
||||
"plural": "cuyaradines"
|
||||
},
|
||||
"spoon": {
|
||||
"singular": "cuyar",
|
||||
"plural": "cuyares"
|
||||
},
|
||||
"cup": {
|
||||
"singular": "taza",
|
||||
"plural": "tazes"
|
||||
},
|
||||
"jar": {
|
||||
"singular": "bote",
|
||||
"plural": "botes"
|
||||
},
|
||||
"sack": {
|
||||
"singular": "sacu",
|
||||
"plural": "sacos"
|
||||
},
|
||||
"packet": {
|
||||
"singular": "sobre",
|
||||
"plural": "sobres"
|
||||
},
|
||||
"cob": {
|
||||
"singular": "panoya",
|
||||
"plural": "panoyes"
|
||||
},
|
||||
"pod": {
|
||||
"singular": "vaina",
|
||||
"plural": "vaines"
|
||||
},
|
||||
"ear": {
|
||||
"singular": "espiga",
|
||||
"plural": "espigues"
|
||||
},
|
||||
"head": {
|
||||
"singular": "cabezuela",
|
||||
"plural": "cabezueles"
|
||||
},
|
||||
"fruit": {
|
||||
"singular": "frutu",
|
||||
"plural": "frutos"
|
||||
},
|
||||
"bulb": {
|
||||
"singular": "bulbu",
|
||||
"plural": "bulbos"
|
||||
},
|
||||
"tuber": {
|
||||
"singular": "tubérculu",
|
||||
"plural": "tubérculos"
|
||||
},
|
||||
"seedHead": {
|
||||
"singular": "cabeza de simiente",
|
||||
"plural": "cabeces de simiente"
|
||||
},
|
||||
"bunch": {
|
||||
"singular": "manoyu",
|
||||
"plural": "manoyos"
|
||||
},
|
||||
"plant": {
|
||||
"singular": "planta",
|
||||
"plural": "plantes"
|
||||
},
|
||||
"pot": {
|
||||
"singular": "tiestu",
|
||||
"plural": "tiestos"
|
||||
},
|
||||
"tray": {
|
||||
"singular": "bandexa",
|
||||
"plural": "bandexes"
|
||||
},
|
||||
"seedling": {
|
||||
"singular": "plántula",
|
||||
"plural": "plántules"
|
||||
},
|
||||
"tree": {
|
||||
"singular": "árbol",
|
||||
"plural": "árboles"
|
||||
},
|
||||
"cutting": {
|
||||
"singular": "esqueje",
|
||||
"plural": "esquejes"
|
||||
},
|
||||
"grams": {
|
||||
"singular": "gramu",
|
||||
"plural": "gramos"
|
||||
},
|
||||
"count": {
|
||||
"singular": "grana",
|
||||
"plural": "granes"
|
||||
}
|
||||
},
|
||||
"market": {
|
||||
"title": "Simiente cerca de ti",
|
||||
|
|
@ -400,7 +482,8 @@
|
|||
"sharedBy": "Compartíu por",
|
||||
"noProfile": "Esta persona entá nun compartió'l so perfil",
|
||||
"copyId": "Copiar códigu",
|
||||
"idCopied": "Códigu copiáu"
|
||||
"idCopied": "Códigu copiáu",
|
||||
"photo": "Semeya"
|
||||
},
|
||||
"profile": {
|
||||
"title": "El to perfil",
|
||||
|
|
|
|||
|
|
@ -215,8 +215,18 @@
|
|||
"anyMonth": "Any month",
|
||||
"noDate": "Set harvest date",
|
||||
"monthNames": [
|
||||
"January", "February", "March", "April", "May", "June",
|
||||
"July", "August", "September", "October", "November", "December"
|
||||
"January",
|
||||
"February",
|
||||
"March",
|
||||
"April",
|
||||
"May",
|
||||
"June",
|
||||
"July",
|
||||
"August",
|
||||
"September",
|
||||
"October",
|
||||
"November",
|
||||
"December"
|
||||
]
|
||||
},
|
||||
"lotType": {
|
||||
|
|
@ -331,30 +341,102 @@
|
|||
"some": "some",
|
||||
"plenty": "plenty",
|
||||
"pinch": "a pinch",
|
||||
"handful": { "singular": "handful", "plural": "handfuls" },
|
||||
"teaspoon": { "singular": "teaspoon", "plural": "teaspoons" },
|
||||
"spoon": { "singular": "spoon", "plural": "spoons" },
|
||||
"cup": { "singular": "cup", "plural": "cups" },
|
||||
"jar": { "singular": "jar", "plural": "jars" },
|
||||
"sack": { "singular": "sack", "plural": "sacks" },
|
||||
"packet": { "singular": "packet", "plural": "packets" },
|
||||
"cob": { "singular": "cob", "plural": "cobs" },
|
||||
"pod": { "singular": "pod", "plural": "pods" },
|
||||
"ear": { "singular": "ear", "plural": "ears" },
|
||||
"head": { "singular": "head", "plural": "heads" },
|
||||
"fruit": { "singular": "fruit", "plural": "fruits" },
|
||||
"bulb": { "singular": "bulb", "plural": "bulbs" },
|
||||
"tuber": { "singular": "tuber", "plural": "tubers" },
|
||||
"seedHead": { "singular": "seed head", "plural": "seed heads" },
|
||||
"bunch": { "singular": "bunch", "plural": "bunches" },
|
||||
"plant": { "singular": "plant", "plural": "plants" },
|
||||
"pot": { "singular": "pot", "plural": "pots" },
|
||||
"tray": { "singular": "tray", "plural": "trays" },
|
||||
"seedling": { "singular": "seedling", "plural": "seedlings" },
|
||||
"tree": { "singular": "tree", "plural": "trees" },
|
||||
"cutting": { "singular": "cutting", "plural": "cuttings" },
|
||||
"grams": { "singular": "gram", "plural": "grams" },
|
||||
"count": { "singular": "seed", "plural": "seeds" }
|
||||
"handful": {
|
||||
"singular": "handful",
|
||||
"plural": "handfuls"
|
||||
},
|
||||
"teaspoon": {
|
||||
"singular": "teaspoon",
|
||||
"plural": "teaspoons"
|
||||
},
|
||||
"spoon": {
|
||||
"singular": "spoon",
|
||||
"plural": "spoons"
|
||||
},
|
||||
"cup": {
|
||||
"singular": "cup",
|
||||
"plural": "cups"
|
||||
},
|
||||
"jar": {
|
||||
"singular": "jar",
|
||||
"plural": "jars"
|
||||
},
|
||||
"sack": {
|
||||
"singular": "sack",
|
||||
"plural": "sacks"
|
||||
},
|
||||
"packet": {
|
||||
"singular": "packet",
|
||||
"plural": "packets"
|
||||
},
|
||||
"cob": {
|
||||
"singular": "cob",
|
||||
"plural": "cobs"
|
||||
},
|
||||
"pod": {
|
||||
"singular": "pod",
|
||||
"plural": "pods"
|
||||
},
|
||||
"ear": {
|
||||
"singular": "ear",
|
||||
"plural": "ears"
|
||||
},
|
||||
"head": {
|
||||
"singular": "head",
|
||||
"plural": "heads"
|
||||
},
|
||||
"fruit": {
|
||||
"singular": "fruit",
|
||||
"plural": "fruits"
|
||||
},
|
||||
"bulb": {
|
||||
"singular": "bulb",
|
||||
"plural": "bulbs"
|
||||
},
|
||||
"tuber": {
|
||||
"singular": "tuber",
|
||||
"plural": "tubers"
|
||||
},
|
||||
"seedHead": {
|
||||
"singular": "seed head",
|
||||
"plural": "seed heads"
|
||||
},
|
||||
"bunch": {
|
||||
"singular": "bunch",
|
||||
"plural": "bunches"
|
||||
},
|
||||
"plant": {
|
||||
"singular": "plant",
|
||||
"plural": "plants"
|
||||
},
|
||||
"pot": {
|
||||
"singular": "pot",
|
||||
"plural": "pots"
|
||||
},
|
||||
"tray": {
|
||||
"singular": "tray",
|
||||
"plural": "trays"
|
||||
},
|
||||
"seedling": {
|
||||
"singular": "seedling",
|
||||
"plural": "seedlings"
|
||||
},
|
||||
"tree": {
|
||||
"singular": "tree",
|
||||
"plural": "trees"
|
||||
},
|
||||
"cutting": {
|
||||
"singular": "cutting",
|
||||
"plural": "cuttings"
|
||||
},
|
||||
"grams": {
|
||||
"singular": "gram",
|
||||
"plural": "grams"
|
||||
},
|
||||
"count": {
|
||||
"singular": "seed",
|
||||
"plural": "seeds"
|
||||
}
|
||||
},
|
||||
"market": {
|
||||
"title": "Seeds near you",
|
||||
|
|
@ -403,7 +485,8 @@
|
|||
"sharedBy": "Shared by",
|
||||
"noProfile": "This person hasn't shared a profile yet",
|
||||
"copyId": "Copy code",
|
||||
"idCopied": "Code copied"
|
||||
"idCopied": "Code copied",
|
||||
"photo": "Photo"
|
||||
},
|
||||
"profile": {
|
||||
"title": "Your profile",
|
||||
|
|
|
|||
|
|
@ -214,8 +214,18 @@
|
|||
"anyMonth": "Cualquier mes",
|
||||
"noDate": "Indicar fecha de cosecha",
|
||||
"monthNames": [
|
||||
"enero", "febrero", "marzo", "abril", "mayo", "junio",
|
||||
"julio", "agosto", "septiembre", "octubre", "noviembre", "diciembre"
|
||||
"enero",
|
||||
"febrero",
|
||||
"marzo",
|
||||
"abril",
|
||||
"mayo",
|
||||
"junio",
|
||||
"julio",
|
||||
"agosto",
|
||||
"septiembre",
|
||||
"octubre",
|
||||
"noviembre",
|
||||
"diciembre"
|
||||
]
|
||||
},
|
||||
"lotType": {
|
||||
|
|
@ -330,30 +340,102 @@
|
|||
"some": "algunas",
|
||||
"plenty": "muchas",
|
||||
"pinch": "una pizca",
|
||||
"handful": { "singular": "puñado", "plural": "puñados" },
|
||||
"teaspoon": { "singular": "cucharadita", "plural": "cucharaditas" },
|
||||
"spoon": { "singular": "cuchara", "plural": "cucharas" },
|
||||
"cup": { "singular": "taza", "plural": "tazas" },
|
||||
"jar": { "singular": "bote", "plural": "botes" },
|
||||
"sack": { "singular": "saco", "plural": "sacos" },
|
||||
"packet": { "singular": "sobre", "plural": "sobres" },
|
||||
"cob": { "singular": "mazorca", "plural": "mazorcas" },
|
||||
"pod": { "singular": "vaina", "plural": "vainas" },
|
||||
"ear": { "singular": "espiga", "plural": "espigas" },
|
||||
"head": { "singular": "cabezuela", "plural": "cabezuelas" },
|
||||
"fruit": { "singular": "fruto", "plural": "frutos" },
|
||||
"bulb": { "singular": "bulbo", "plural": "bulbos" },
|
||||
"tuber": { "singular": "tubérculo", "plural": "tubérculos" },
|
||||
"seedHead": { "singular": "cabeza de semillas", "plural": "cabezas de semillas" },
|
||||
"bunch": { "singular": "manojo", "plural": "manojos" },
|
||||
"plant": { "singular": "planta", "plural": "plantas" },
|
||||
"pot": { "singular": "maceta", "plural": "macetas" },
|
||||
"tray": { "singular": "bandeja", "plural": "bandejas" },
|
||||
"seedling": { "singular": "plántula", "plural": "plántulas" },
|
||||
"tree": { "singular": "árbol", "plural": "árboles" },
|
||||
"cutting": { "singular": "esqueje", "plural": "esquejes" },
|
||||
"grams": { "singular": "gramo", "plural": "gramos" },
|
||||
"count": { "singular": "semilla", "plural": "semillas" }
|
||||
"handful": {
|
||||
"singular": "puñado",
|
||||
"plural": "puñados"
|
||||
},
|
||||
"teaspoon": {
|
||||
"singular": "cucharadita",
|
||||
"plural": "cucharaditas"
|
||||
},
|
||||
"spoon": {
|
||||
"singular": "cuchara",
|
||||
"plural": "cucharas"
|
||||
},
|
||||
"cup": {
|
||||
"singular": "taza",
|
||||
"plural": "tazas"
|
||||
},
|
||||
"jar": {
|
||||
"singular": "bote",
|
||||
"plural": "botes"
|
||||
},
|
||||
"sack": {
|
||||
"singular": "saco",
|
||||
"plural": "sacos"
|
||||
},
|
||||
"packet": {
|
||||
"singular": "sobre",
|
||||
"plural": "sobres"
|
||||
},
|
||||
"cob": {
|
||||
"singular": "mazorca",
|
||||
"plural": "mazorcas"
|
||||
},
|
||||
"pod": {
|
||||
"singular": "vaina",
|
||||
"plural": "vainas"
|
||||
},
|
||||
"ear": {
|
||||
"singular": "espiga",
|
||||
"plural": "espigas"
|
||||
},
|
||||
"head": {
|
||||
"singular": "cabezuela",
|
||||
"plural": "cabezuelas"
|
||||
},
|
||||
"fruit": {
|
||||
"singular": "fruto",
|
||||
"plural": "frutos"
|
||||
},
|
||||
"bulb": {
|
||||
"singular": "bulbo",
|
||||
"plural": "bulbos"
|
||||
},
|
||||
"tuber": {
|
||||
"singular": "tubérculo",
|
||||
"plural": "tubérculos"
|
||||
},
|
||||
"seedHead": {
|
||||
"singular": "cabeza de semillas",
|
||||
"plural": "cabezas de semillas"
|
||||
},
|
||||
"bunch": {
|
||||
"singular": "manojo",
|
||||
"plural": "manojos"
|
||||
},
|
||||
"plant": {
|
||||
"singular": "planta",
|
||||
"plural": "plantas"
|
||||
},
|
||||
"pot": {
|
||||
"singular": "maceta",
|
||||
"plural": "macetas"
|
||||
},
|
||||
"tray": {
|
||||
"singular": "bandeja",
|
||||
"plural": "bandejas"
|
||||
},
|
||||
"seedling": {
|
||||
"singular": "plántula",
|
||||
"plural": "plántulas"
|
||||
},
|
||||
"tree": {
|
||||
"singular": "árbol",
|
||||
"plural": "árboles"
|
||||
},
|
||||
"cutting": {
|
||||
"singular": "esqueje",
|
||||
"plural": "esquejes"
|
||||
},
|
||||
"grams": {
|
||||
"singular": "gramo",
|
||||
"plural": "gramos"
|
||||
},
|
||||
"count": {
|
||||
"singular": "semilla",
|
||||
"plural": "semillas"
|
||||
}
|
||||
},
|
||||
"market": {
|
||||
"title": "Semillas cerca de ti",
|
||||
|
|
@ -402,7 +484,8 @@
|
|||
"sharedBy": "Compartido por",
|
||||
"noProfile": "Esta persona aún no ha compartido su perfil",
|
||||
"copyId": "Copiar código",
|
||||
"idCopied": "Código copiado"
|
||||
"idCopied": "Código copiado",
|
||||
"photo": "Foto"
|
||||
},
|
||||
"profile": {
|
||||
"title": "Tu perfil",
|
||||
|
|
|
|||
|
|
@ -211,8 +211,18 @@
|
|||
"anyMonth": "Qualquer mês",
|
||||
"noDate": "Definir data de colheita",
|
||||
"monthNames": [
|
||||
"janeiro", "fevereiro", "março", "abril", "maio", "junho",
|
||||
"julho", "agosto", "setembro", "outubro", "novembro", "dezembro"
|
||||
"janeiro",
|
||||
"fevereiro",
|
||||
"março",
|
||||
"abril",
|
||||
"maio",
|
||||
"junho",
|
||||
"julho",
|
||||
"agosto",
|
||||
"setembro",
|
||||
"outubro",
|
||||
"novembro",
|
||||
"dezembro"
|
||||
]
|
||||
},
|
||||
"lotType": {
|
||||
|
|
@ -327,30 +337,102 @@
|
|||
"some": "bastantes",
|
||||
"plenty": "muitas",
|
||||
"pinch": "uma pitada",
|
||||
"handful": { "singular": "mão-cheia", "plural": "mãos-cheias" },
|
||||
"teaspoon": { "singular": "colher de chá", "plural": "colheres de chá" },
|
||||
"spoon": { "singular": "colher", "plural": "colheres" },
|
||||
"cup": { "singular": "chávena", "plural": "chávenas" },
|
||||
"jar": { "singular": "frasco", "plural": "frascos" },
|
||||
"sack": { "singular": "saco", "plural": "sacos" },
|
||||
"packet": { "singular": "pacote", "plural": "pacotes" },
|
||||
"cob": { "singular": "maçaroca", "plural": "maçarocas" },
|
||||
"pod": { "singular": "vagem", "plural": "vagens" },
|
||||
"ear": { "singular": "espiga", "plural": "espigas" },
|
||||
"head": { "singular": "cabeça", "plural": "cabeças" },
|
||||
"fruit": { "singular": "fruto", "plural": "frutos" },
|
||||
"bulb": { "singular": "bolbo", "plural": "bolbos" },
|
||||
"tuber": { "singular": "tubérculo", "plural": "tubérculos" },
|
||||
"seedHead": { "singular": "capítulo", "plural": "capítulos" },
|
||||
"bunch": { "singular": "molho", "plural": "molhos" },
|
||||
"plant": { "singular": "planta", "plural": "plantas" },
|
||||
"pot": { "singular": "vaso", "plural": "vasos" },
|
||||
"tray": { "singular": "tabuleiro", "plural": "tabuleiros" },
|
||||
"seedling": { "singular": "plântula", "plural": "plântulas" },
|
||||
"tree": { "singular": "árvore", "plural": "árvores" },
|
||||
"cutting": { "singular": "estaca", "plural": "estacas" },
|
||||
"grams": { "singular": "grama", "plural": "gramas" },
|
||||
"count": { "singular": "semente", "plural": "sementes" }
|
||||
"handful": {
|
||||
"singular": "mão-cheia",
|
||||
"plural": "mãos-cheias"
|
||||
},
|
||||
"teaspoon": {
|
||||
"singular": "colher de chá",
|
||||
"plural": "colheres de chá"
|
||||
},
|
||||
"spoon": {
|
||||
"singular": "colher",
|
||||
"plural": "colheres"
|
||||
},
|
||||
"cup": {
|
||||
"singular": "chávena",
|
||||
"plural": "chávenas"
|
||||
},
|
||||
"jar": {
|
||||
"singular": "frasco",
|
||||
"plural": "frascos"
|
||||
},
|
||||
"sack": {
|
||||
"singular": "saco",
|
||||
"plural": "sacos"
|
||||
},
|
||||
"packet": {
|
||||
"singular": "pacote",
|
||||
"plural": "pacotes"
|
||||
},
|
||||
"cob": {
|
||||
"singular": "maçaroca",
|
||||
"plural": "maçarocas"
|
||||
},
|
||||
"pod": {
|
||||
"singular": "vagem",
|
||||
"plural": "vagens"
|
||||
},
|
||||
"ear": {
|
||||
"singular": "espiga",
|
||||
"plural": "espigas"
|
||||
},
|
||||
"head": {
|
||||
"singular": "cabeça",
|
||||
"plural": "cabeças"
|
||||
},
|
||||
"fruit": {
|
||||
"singular": "fruto",
|
||||
"plural": "frutos"
|
||||
},
|
||||
"bulb": {
|
||||
"singular": "bolbo",
|
||||
"plural": "bolbos"
|
||||
},
|
||||
"tuber": {
|
||||
"singular": "tubérculo",
|
||||
"plural": "tubérculos"
|
||||
},
|
||||
"seedHead": {
|
||||
"singular": "capítulo",
|
||||
"plural": "capítulos"
|
||||
},
|
||||
"bunch": {
|
||||
"singular": "molho",
|
||||
"plural": "molhos"
|
||||
},
|
||||
"plant": {
|
||||
"singular": "planta",
|
||||
"plural": "plantas"
|
||||
},
|
||||
"pot": {
|
||||
"singular": "vaso",
|
||||
"plural": "vasos"
|
||||
},
|
||||
"tray": {
|
||||
"singular": "tabuleiro",
|
||||
"plural": "tabuleiros"
|
||||
},
|
||||
"seedling": {
|
||||
"singular": "plântula",
|
||||
"plural": "plântulas"
|
||||
},
|
||||
"tree": {
|
||||
"singular": "árvore",
|
||||
"plural": "árvores"
|
||||
},
|
||||
"cutting": {
|
||||
"singular": "estaca",
|
||||
"plural": "estacas"
|
||||
},
|
||||
"grams": {
|
||||
"singular": "grama",
|
||||
"plural": "gramas"
|
||||
},
|
||||
"count": {
|
||||
"singular": "semente",
|
||||
"plural": "sementes"
|
||||
}
|
||||
},
|
||||
"market": {
|
||||
"title": "Sementes perto de ti",
|
||||
|
|
@ -399,7 +481,8 @@
|
|||
"sharedBy": "Partilhado por",
|
||||
"noProfile": "Esta pessoa ainda não partilhou o seu perfil",
|
||||
"copyId": "Copiar código",
|
||||
"idCopied": "Código copiado"
|
||||
"idCopied": "Código copiado",
|
||||
"photo": "Foto"
|
||||
},
|
||||
"profile": {
|
||||
"title": "O teu perfil",
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@
|
|||
/// To regenerate, run: `dart run slang`
|
||||
///
|
||||
/// Locales: 4
|
||||
/// Strings: 1548 (387 per locale)
|
||||
/// Strings: 1552 (388 per locale)
|
||||
///
|
||||
/// Built on 2026-07-10 at 17:18 UTC
|
||||
/// Built on 2026-07-10 at 18:55 UTC
|
||||
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint, unused_import
|
||||
|
|
|
|||
|
|
@ -691,6 +691,7 @@ class _Translations$market$ast extends Translations$market$en {
|
|||
@override String get noProfile => 'Esta persona entá nun compartió\'l so perfil';
|
||||
@override String get copyId => 'Copiar códigu';
|
||||
@override String get idCopied => 'Códigu copiáu';
|
||||
@override String get photo => 'Semeya';
|
||||
}
|
||||
|
||||
// Path: profile
|
||||
|
|
@ -1456,6 +1457,7 @@ extension on TranslationsAst {
|
|||
'market.noProfile' => 'Esta persona entá nun compartió\'l so perfil',
|
||||
'market.copyId' => 'Copiar códigu',
|
||||
'market.idCopied' => 'Códigu copiáu',
|
||||
'market.photo' => 'Semeya',
|
||||
'profile.title' => 'El to perfil',
|
||||
'profile.name' => 'Nome',
|
||||
'profile.nameHint' => 'Cómo te ven los demás',
|
||||
|
|
|
|||
|
|
@ -1282,6 +1282,9 @@ class Translations$market$en {
|
|||
|
||||
/// en: 'Code copied'
|
||||
String get idCopied => 'Code copied';
|
||||
|
||||
/// en: 'Photo'
|
||||
String get photo => 'Photo';
|
||||
}
|
||||
|
||||
// Path: profile
|
||||
|
|
@ -2220,6 +2223,7 @@ extension on Translations {
|
|||
'market.noProfile' => 'This person hasn\'t shared a profile yet',
|
||||
'market.copyId' => 'Copy code',
|
||||
'market.idCopied' => 'Code copied',
|
||||
'market.photo' => 'Photo',
|
||||
'profile.title' => 'Your profile',
|
||||
'profile.name' => 'Display name',
|
||||
'profile.nameHint' => 'How others see you',
|
||||
|
|
|
|||
|
|
@ -693,6 +693,7 @@ class _Translations$market$es extends Translations$market$en {
|
|||
@override String get noProfile => 'Esta persona aún no ha compartido su perfil';
|
||||
@override String get copyId => 'Copiar código';
|
||||
@override String get idCopied => 'Código copiado';
|
||||
@override String get photo => 'Foto';
|
||||
}
|
||||
|
||||
// Path: profile
|
||||
|
|
@ -1460,6 +1461,7 @@ extension on TranslationsEs {
|
|||
'market.noProfile' => 'Esta persona aún no ha compartido su perfil',
|
||||
'market.copyId' => 'Copiar código',
|
||||
'market.idCopied' => 'Código copiado',
|
||||
'market.photo' => 'Foto',
|
||||
'profile.title' => 'Tu perfil',
|
||||
'profile.name' => 'Nombre',
|
||||
'profile.nameHint' => 'Cómo te ven los demás',
|
||||
|
|
|
|||
|
|
@ -690,6 +690,7 @@ class _Translations$market$pt extends Translations$market$en {
|
|||
@override String get noProfile => 'Esta pessoa ainda não partilhou o seu perfil';
|
||||
@override String get copyId => 'Copiar código';
|
||||
@override String get idCopied => 'Código copiado';
|
||||
@override String get photo => 'Foto';
|
||||
}
|
||||
|
||||
// Path: profile
|
||||
|
|
@ -1454,6 +1455,7 @@ extension on TranslationsPt {
|
|||
'market.noProfile' => 'Esta pessoa ainda não partilhou o seu perfil',
|
||||
'market.copyId' => 'Copiar código',
|
||||
'market.idCopied' => 'Código copiado',
|
||||
'market.photo' => 'Foto',
|
||||
'profile.title' => 'O teu perfil',
|
||||
'profile.name' => 'Nome',
|
||||
'profile.nameHint' => 'Como os outros te veem',
|
||||
|
|
|
|||
|
|
@ -123,6 +123,10 @@ class _MarketOfferDetailScreenState extends State<MarketOfferDetailScreen> {
|
|||
body: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
if (o.imageUrl != null) ...[
|
||||
OfferHeroImage(url: o.imageUrl!, semanticLabel: t.market.photo),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
|
|
|
|||
|
|
@ -380,6 +380,13 @@ class _OfferCard extends StatelessWidget {
|
|||
children: [
|
||||
Row(
|
||||
children: [
|
||||
if (offer.imageUrl != null) ...[
|
||||
OfferThumbnail(
|
||||
url: offer.imageUrl!,
|
||||
semanticLabel: t.market.photo,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
],
|
||||
Expanded(
|
||||
child: Text(
|
||||
offer.summary,
|
||||
|
|
|
|||
|
|
@ -14,6 +14,98 @@ String offerTypeLabel(Translations t, OfferType type) => switch (type) {
|
|||
OfferType.lend => t.share.exchange,
|
||||
};
|
||||
|
||||
/// A small square thumbnail of an offer's photo for the market list card.
|
||||
/// Remote (published) image, so it degrades gracefully: a spinner while it
|
||||
/// loads and a neutral placeholder when the media server can't be reached.
|
||||
class OfferThumbnail extends StatelessWidget {
|
||||
const OfferThumbnail({
|
||||
required this.url,
|
||||
required this.semanticLabel,
|
||||
this.size = 56,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String url;
|
||||
final String semanticLabel;
|
||||
final double size;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: Image.network(
|
||||
url,
|
||||
width: size,
|
||||
height: size,
|
||||
fit: BoxFit.cover,
|
||||
semanticLabel: semanticLabel,
|
||||
loadingBuilder: (context, child, progress) => progress == null
|
||||
? child
|
||||
: _OfferImagePlaceholder(size: size, loading: true),
|
||||
errorBuilder: (context, _, _) => _OfferImagePlaceholder(size: size),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Full-width "hero" image atop the offer detail page. Same graceful
|
||||
/// degradation as [OfferThumbnail]; hidden entirely when the offer has no photo.
|
||||
class OfferHeroImage extends StatelessWidget {
|
||||
const OfferHeroImage({
|
||||
required this.url,
|
||||
required this.semanticLabel,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String url;
|
||||
final String semanticLabel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 4 / 3,
|
||||
child: Image.network(
|
||||
url,
|
||||
fit: BoxFit.cover,
|
||||
semanticLabel: semanticLabel,
|
||||
loadingBuilder: (context, child, progress) => progress == null
|
||||
? child
|
||||
: const _OfferImagePlaceholder(loading: true),
|
||||
errorBuilder: (context, _, _) => const _OfferImagePlaceholder(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Neutral fill shown while a remote offer image loads or when it can't be
|
||||
/// fetched — a marketplace is useless if a broken image blocks the card.
|
||||
class _OfferImagePlaceholder extends StatelessWidget {
|
||||
const _OfferImagePlaceholder({this.size, this.loading = false});
|
||||
|
||||
final double? size;
|
||||
final bool loading;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: size,
|
||||
height: size,
|
||||
color: seedPrimaryContainer.withValues(alpha: 0.4),
|
||||
alignment: Alignment.center,
|
||||
child: loading
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.image_not_supported_outlined, color: seedMuted),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The small pill showing an offer's reciprocity mode (give/swap/sell/wanted).
|
||||
class OfferTypeChip extends StatelessWidget {
|
||||
const OfferTypeChip({required this.label, super.key});
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import 'package:tane/i18n/strings.g.dart';
|
|||
import 'package:tane/services/social_service.dart';
|
||||
import 'package:tane/services/social_settings.dart';
|
||||
import 'package:tane/ui/market_offer_detail_screen.dart';
|
||||
import 'package:tane/ui/market_widgets.dart';
|
||||
|
||||
import '../support/test_support.dart';
|
||||
|
||||
|
|
@ -25,7 +26,7 @@ Widget _wrap(MarketOfferDetailScreen screen) {
|
|||
);
|
||||
}
|
||||
|
||||
Offer _offer({bool organic = false}) => Offer(
|
||||
Offer _offer({bool organic = false, String? imageUrl}) => Offer(
|
||||
id: 'lot-1',
|
||||
authorPubkeyHex: 'ab' * 32,
|
||||
summary: 'Tomate rosa de Barbastro',
|
||||
|
|
@ -33,6 +34,7 @@ Offer _offer({bool organic = false}) => Offer(
|
|||
category: 'Solanaceae',
|
||||
approxGeohash: 'sp3e9',
|
||||
isOrganic: organic,
|
||||
imageUrl: imageUrl,
|
||||
);
|
||||
|
||||
void main() {
|
||||
|
|
@ -70,4 +72,34 @@ void main() {
|
|||
|
||||
expect(find.byKey(const Key('offerDetail.contact')), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('shows a hero image when the offer has a photo', (tester) async {
|
||||
final social = await SocialService.fromRootSeedHex('00' * 32);
|
||||
final settings = SocialSettings(InMemorySecretStore());
|
||||
await settings.setRelayUrls(const []);
|
||||
|
||||
await tester.pumpWidget(_wrap(MarketOfferDetailScreen(
|
||||
offer: _offer(imageUrl: 'https://media.example/abc.jpg'),
|
||||
social: social,
|
||||
settings: settings,
|
||||
)));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(OfferHeroImage), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('shows no hero image when the offer has none', (tester) async {
|
||||
final social = await SocialService.fromRootSeedHex('00' * 32);
|
||||
final settings = SocialSettings(InMemorySecretStore());
|
||||
await settings.setRelayUrls(const []);
|
||||
|
||||
await tester.pumpWidget(_wrap(MarketOfferDetailScreen(
|
||||
offer: _offer(),
|
||||
social: social,
|
||||
settings: settings,
|
||||
)));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(OfferHeroImage), findsNothing);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
45
apps/app_seeds/test/ui/market_widgets_test.dart
Normal file
45
apps/app_seeds/test/ui/market_widgets_test.dart
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:tane/ui/market_widgets.dart';
|
||||
|
||||
Widget _wrap(Widget child) => MaterialApp(home: Scaffold(body: Center(child: child)));
|
||||
|
||||
void main() {
|
||||
testWidgets('OfferThumbnail builds a network image', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
_wrap(const OfferThumbnail(
|
||||
url: 'https://media.example/abc.jpg',
|
||||
semanticLabel: 'Photo',
|
||||
)),
|
||||
);
|
||||
// The image widget is present; its loadingBuilder/errorBuilder handle the
|
||||
// remote fetch, so we only assert the widget is wired (no network in tests).
|
||||
expect(find.byType(Image), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('OfferThumbnail falls back to a placeholder when the load fails',
|
||||
(tester) async {
|
||||
await tester.pumpWidget(
|
||||
_wrap(const OfferThumbnail(
|
||||
url: 'https://media.example/missing.jpg',
|
||||
semanticLabel: 'Photo',
|
||||
)),
|
||||
);
|
||||
// Let the (failed) network fetch resolve; the errorBuilder then shows a
|
||||
// neutral icon instead of a broken image.
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
|
||||
expect(find.byIcon(Icons.image_not_supported_outlined), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('OfferHeroImage builds a network image', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
_wrap(const OfferHeroImage(
|
||||
url: 'https://media.example/abc.jpg',
|
||||
semanticLabel: 'Photo',
|
||||
)),
|
||||
);
|
||||
expect(find.byType(Image), findsOneWidget);
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue