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 PresencaPir from '../lib/componentes/PresencaPir.svelte';
|
||||||
import RostoPrevia from '../lib/componentes/RostoPrevia.svelte';
|
import RostoPrevia from '../lib/componentes/RostoPrevia.svelte';
|
||||||
import VidaCartao from '../lib/componentes/VidaCartao.svelte';
|
import VidaCartao from '../lib/componentes/VidaCartao.svelte';
|
||||||
|
import QuemE from '../lib/componentes/QuemE.svelte';
|
||||||
import AprendizadoCartao from '../lib/componentes/AprendizadoCartao.svelte';
|
import AprendizadoCartao from '../lib/componentes/AprendizadoCartao.svelte';
|
||||||
import { estado } from '../lib/estado.svelte';
|
import { estado } from '../lib/estado.svelte';
|
||||||
import { Rastreamento } from '../lib/rastreamento.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 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`. -->
|
No celular as duas colunas se desfazem (display: contents) e a ordem vem do `order`. -->
|
||||||
<div class="coluna direita">
|
<div class="coluna direita">
|
||||||
|
<div class="c-quem"><QuemE minimo={3} /></div>
|
||||||
<div class="c-vida"><VidaCartao /></div>
|
<div class="c-vida"><VidaCartao /></div>
|
||||||
{#if estado.vida?.config.ligado}<div class="c-aprendizado"><AprendizadoCartao /></div>{/if}
|
{#if estado.vida?.config.ligado}<div class="c-aprendizado"><AprendizadoCartao /></div>{/if}
|
||||||
|
|
||||||
@@ -394,6 +396,8 @@ const pirAtivo = $derived(
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
.palco { order: 0; }
|
.palco { order: 0; }
|
||||||
|
.c-quem { order: 1; }
|
||||||
|
.c-quem:empty { display: none; }
|
||||||
.c-vida { order: 1; }
|
.c-vida { order: 1; }
|
||||||
.c-aprendizado { order: 2; }
|
.c-aprendizado { order: 2; }
|
||||||
.c-celular { order: 3; }
|
.c-celular { order: 3; }
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { Check, Pencil, ScanFace, ShieldCheck, Trash2, UserPlus, X } from '@lucide/svelte';
|
import { Check, Pencil, ScanFace, ShieldCheck, Trash2, UserPlus, X } from '@lucide/svelte';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import Card from '../lib/componentes/Card.svelte';
|
import Card from '../lib/componentes/Card.svelte';
|
||||||
|
import QuemE from '../lib/componentes/QuemE.svelte';
|
||||||
import Selo from '../lib/componentes/Selo.svelte';
|
import Selo from '../lib/componentes/Selo.svelte';
|
||||||
import Toggle from '../lib/componentes/Toggle.svelte';
|
import Toggle from '../lib/componentes/Toggle.svelte';
|
||||||
import { estado } from '../lib/estado.svelte';
|
import { estado } from '../lib/estado.svelte';
|
||||||
@@ -48,6 +49,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="pessoas">
|
<div class="pessoas">
|
||||||
|
<QuemE />
|
||||||
<Card titulo="Reconhecimento de pessoas" ajuda="pessoas" descricao="Pelas fotos dos eventos ele descobre quem chegou e cumprimenta pelo nome.">
|
<Card titulo="Reconhecimento de pessoas" ajuda="pessoas" descricao="Pelas fotos dos eventos ele descobre quem chegou e cumprimenta pelo nome.">
|
||||||
{#snippet acoes()}
|
{#snippet acoes()}
|
||||||
<button class="btn pequeno" onclick={() => pessoas.varrer()} disabled={!st?.disponivel || !!st?.varrendo}>
|
<button class="btn pequeno" onclick={() => pessoas.varrer()} disabled={!st?.disponivel || !!st?.varrendo}>
|
||||||
|
|||||||
Reference in New Issue
Block a user