feat(pessoas): cartão "Quem é?" com o rosto, no Ao vivo e em Pessoas
Um cartão por pessoa desconhecida (grupo de rostos parecidos), com o rosto grande: toca no nome de quem já conhece ou digita um novo, e a resposta vale para todas as fotos do grupo. Não é ninguém / Pular. No Ao vivo só aparece de quem ele já viu 3 vezes ou mais. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
<script lang="ts">
|
||||
import { ChevronRight, UserCheck, UserX } from '@lucide/svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { pessoas, urlRosto } from '../pessoas.svelte';
|
||||
|
||||
// "Quem é?": um cartão por pessoa desconhecida (grupo de rostos parecidos), com o rosto grande.
|
||||
// A resposta vale para todas as fotos do grupo. minimo: só pergunta de quem ele já viu tantas vezes.
|
||||
let { minimo = 1 }: { minimo?: number } = $props();
|
||||
onMount(() => {
|
||||
if (!pessoas.status) pessoas.carregar();
|
||||
});
|
||||
|
||||
const st = $derived(pessoas.status);
|
||||
let pulados = $state<number[]>([]); // primeiro id de cada grupo pulado nesta tela
|
||||
let nome = $state('');
|
||||
let ocupado = $state(false);
|
||||
|
||||
const grupo = $derived.by(() => {
|
||||
if (!st?.disponivel || !st.config.ligado) return null;
|
||||
return st.grupos.find((ids) => ids.length >= minimo && !pulados.includes(ids[0])) ?? null;
|
||||
});
|
||||
const rostos = $derived(grupo && st ? st.sem_nome.filter((r) => grupo.includes(r.id)) : []);
|
||||
const restantes = $derived(st ? st.grupos.filter((ids) => ids.length >= minimo && !pulados.includes(ids[0])).length : 0);
|
||||
const capa = $derived(rostos[0] ?? null);
|
||||
const quando = $derived(capa ? new Date(Math.min(...rostos.map((r) => r.horario)) * 1000).toLocaleDateString('pt-BR') : '');
|
||||
|
||||
async function responder(alvo: { pessoa?: number; nome?: string } | null) {
|
||||
if (!grupo || ocupado) return;
|
||||
ocupado = true;
|
||||
const ok = alvo ? await pessoas.marcar([...grupo], alvo) : await pessoas.descartar([...grupo]);
|
||||
ocupado = false;
|
||||
if (ok) nome = '';
|
||||
}
|
||||
async function salvar(e: SubmitEvent) {
|
||||
e.preventDefault();
|
||||
if (nome.trim()) await responder({ nome: nome.trim() });
|
||||
}
|
||||
function pular() {
|
||||
if (grupo) pulados = [...pulados, grupo[0]];
|
||||
nome = '';
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if grupo && capa}
|
||||
<section class="quem" aria-label="Quem é esta pessoa?">
|
||||
<img class="capa" src={urlRosto(capa.recorte)} alt="Rosto que ele viu e ainda não conhece" />
|
||||
<div class="corpo">
|
||||
<h3>Quem é?</h3>
|
||||
<p>Ele viu esta pessoa {grupo.length} {grupo.length === 1 ? 'vez' : 'vezes'} desde {quando}.</p>
|
||||
{#if rostos.length > 1}
|
||||
<div class="mais">
|
||||
{#each rostos.slice(1, 7) as r (r.id)}<img src={urlRosto(r.recorte)} alt="" loading="lazy" />{/each}
|
||||
{#if rostos.length > 7}<span>+{rostos.length - 7}</span>{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{#if st?.pessoas.length}
|
||||
<div class="conhecidos">
|
||||
{#each st.pessoas as p (p.id)}
|
||||
<button class="btn pequeno" disabled={ocupado} onclick={() => responder({ pessoa: p.id })}><UserCheck size={14} /> {p.nome}</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<form class="novo" onsubmit={salvar}>
|
||||
<input bind:value={nome} placeholder={st?.pessoas.length ? 'Ou alguém novo: nome' : 'Nome'} maxlength="30" aria-label="Nome desta pessoa" />
|
||||
<button class="btn pequeno primario" type="submit" disabled={!nome.trim() || ocupado}>Salvar</button>
|
||||
</form>
|
||||
<div class="rodape">
|
||||
<button class="btn pequeno fantasma" disabled={ocupado} onclick={() => responder(null)}><UserX size={14} /> Não é ninguém</button>
|
||||
<button class="btn pequeno fantasma" onclick={pular}>Pular <ChevronRight size={14} /></button>
|
||||
{#if restantes > 1}<small>mais {restantes - 1} para conhecer</small>{/if}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.quem {
|
||||
display: grid;
|
||||
grid-template-columns: 132px minmax(0, 1fr);
|
||||
gap: var(--e-4);
|
||||
align-items: start;
|
||||
padding: var(--e-4);
|
||||
border: 1px solid var(--destaque);
|
||||
border-radius: var(--raio-lg);
|
||||
background: var(--destaque-suave);
|
||||
}
|
||||
@media (max-width: 520px) {
|
||||
.quem {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
justify-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
.mais, .conhecidos, .rodape {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
.capa {
|
||||
width: 132px;
|
||||
height: 132px;
|
||||
border-radius: var(--raio);
|
||||
object-fit: cover;
|
||||
background: var(--superficie-3);
|
||||
}
|
||||
.corpo {
|
||||
display: grid;
|
||||
gap: var(--e-2);
|
||||
min-width: 0;
|
||||
}
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: var(--t-lg);
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
color: var(--texto-2);
|
||||
font-size: var(--t-sm);
|
||||
}
|
||||
.mais {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.mais img {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.mais span {
|
||||
font-size: var(--t-xs);
|
||||
color: var(--texto-3);
|
||||
}
|
||||
.conhecidos, .novo, .rodape {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--e-2);
|
||||
align-items: center;
|
||||
}
|
||||
.novo input {
|
||||
flex: 1;
|
||||
min-width: 140px;
|
||||
}
|
||||
.rodape small {
|
||||
color: var(--texto-3);
|
||||
font-size: var(--t-xs);
|
||||
}
|
||||
</style>
|
||||
@@ -15,6 +15,7 @@
|
||||
import PresencaPir from '../lib/componentes/PresencaPir.svelte';
|
||||
import RostoPrevia from '../lib/componentes/RostoPrevia.svelte';
|
||||
import VidaCartao from '../lib/componentes/VidaCartao.svelte';
|
||||
import QuemE from '../lib/componentes/QuemE.svelte';
|
||||
import AprendizadoCartao from '../lib/componentes/AprendizadoCartao.svelte';
|
||||
import { estado } from '../lib/estado.svelte';
|
||||
import { Rastreamento } from '../lib/rastreamento.svelte';
|
||||
@@ -128,6 +129,7 @@ const pirAtivo = $derived(
|
||||
<!-- No PC: à direita Vida e o que ele aprendeu; embaixo da câmera, o resto.
|
||||
No celular as duas colunas se desfazem (display: contents) e a ordem vem do `order`. -->
|
||||
<div class="coluna direita">
|
||||
<div class="c-quem"><QuemE minimo={3} /></div>
|
||||
<div class="c-vida"><VidaCartao /></div>
|
||||
{#if estado.vida?.config.ligado}<div class="c-aprendizado"><AprendizadoCartao /></div>{/if}
|
||||
|
||||
@@ -394,6 +396,8 @@ const pirAtivo = $derived(
|
||||
min-width: 0;
|
||||
}
|
||||
.palco { order: 0; }
|
||||
.c-quem { order: 1; }
|
||||
.c-quem:empty { display: none; }
|
||||
.c-vida { order: 1; }
|
||||
.c-aprendizado { order: 2; }
|
||||
.c-celular { order: 3; }
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { Check, Pencil, ScanFace, ShieldCheck, Trash2, UserPlus, X } from '@lucide/svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import Card from '../lib/componentes/Card.svelte';
|
||||
import QuemE from '../lib/componentes/QuemE.svelte';
|
||||
import Selo from '../lib/componentes/Selo.svelte';
|
||||
import Toggle from '../lib/componentes/Toggle.svelte';
|
||||
import { estado } from '../lib/estado.svelte';
|
||||
@@ -48,6 +49,7 @@
|
||||
</script>
|
||||
|
||||
<div class="pessoas">
|
||||
<QuemE />
|
||||
<Card titulo="Reconhecimento de pessoas" ajuda="pessoas" descricao="Pelas fotos dos eventos ele descobre quem chegou e cumprimenta pelo nome.">
|
||||
{#snippet acoes()}
|
||||
<button class="btn pequeno" onclick={() => pessoas.varrer()} disabled={!st?.disponivel || !!st?.varrendo}>
|
||||
|
||||
Reference in New Issue
Block a user