mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:01:09 +02:00
feat(contacts): add self-contact with profile sync and "Mein Profil" page
- Auto-create a self-contact (fixed ID) from the user's auth profile on first visit to /contacts, synced on each load - Pin self-contact to top of all contact lists with "Du" badge - Add "Mein Profil" carousel page showing a profile card view - Add page option to ContactPagePicker Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3556fc18be
commit
31d168c02b
3 changed files with 146 additions and 2 deletions
|
|
@ -145,11 +145,16 @@
|
|||
|
||||
let meta = $derived(PAGE_META[pageId]);
|
||||
|
||||
let selfContact = $derived(allContacts.find((c) => c.id === SELF_CONTACT_ID && !c.isArchived));
|
||||
|
||||
let filtered = $derived.by(() => {
|
||||
const active = allContacts.filter((c) => !c.isArchived);
|
||||
const byPage = active.filter(meta.filterFn);
|
||||
const searched = searchContacts(byPage, searchQuery);
|
||||
|
||||
// My-profile: just show the self contact
|
||||
if (pageId === 'my-profile') return searched;
|
||||
|
||||
// Birthday-soon: sort by upcoming birthday
|
||||
if (pageId === 'birthday-soon') {
|
||||
const today = startOfDay(new Date());
|
||||
|
|
@ -167,7 +172,14 @@
|
|||
);
|
||||
}
|
||||
|
||||
return sortContacts(searched, 'firstName');
|
||||
// Default: sort alphabetically, pin self-contact to top
|
||||
const sorted = sortContacts(searched, 'firstName');
|
||||
const selfIdx = sorted.findIndex((c) => c.id === SELF_CONTACT_ID);
|
||||
if (selfIdx > 0) {
|
||||
const [self] = sorted.splice(selfIdx, 1);
|
||||
sorted.unshift(self);
|
||||
}
|
||||
return sorted;
|
||||
});
|
||||
|
||||
let groups = $derived(
|
||||
|
|
@ -215,7 +227,10 @@
|
|||
{/snippet}
|
||||
|
||||
<div class="page-content">
|
||||
{#if filtered.length === 0}
|
||||
{#if pageId === 'my-profile' && selfContact}
|
||||
<!-- Profile card -->
|
||||
{@render profileCard(selfContact)}
|
||||
{:else if filtered.length === 0}
|
||||
<div class="empty-state">
|
||||
<meta.icon size={28} />
|
||||
<span>Keine Kontakte</span>
|
||||
|
|
@ -239,6 +254,51 @@
|
|||
</div>
|
||||
</PageShell>
|
||||
|
||||
{#snippet profileCard(contact: Contact)}
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="profile-card" onclick={() => onOpenContact?.(contact)}>
|
||||
<div class="profile-avatar">
|
||||
{#if contact.photoUrl}
|
||||
<img src={contact.photoUrl} alt={getDisplayName(contact)} class="profile-avatar-img" />
|
||||
{:else}
|
||||
{getInitials(contact)}
|
||||
{/if}
|
||||
</div>
|
||||
<div class="profile-name">{getDisplayName(contact)}</div>
|
||||
{#if contact.email}
|
||||
<div class="profile-detail">
|
||||
<Envelope size={14} />
|
||||
<span>{contact.email}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if contact.phone || contact.mobile}
|
||||
<div class="profile-detail">
|
||||
<Phone size={14} />
|
||||
<span>{contact.mobile || contact.phone}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if contact.company}
|
||||
<div class="profile-detail">
|
||||
<Briefcase size={14} />
|
||||
<span>{[contact.jobTitle, contact.company].filter(Boolean).join(' @ ')}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if contact.city}
|
||||
<div class="profile-detail">
|
||||
<MapPin size={14} />
|
||||
<span>{[contact.street, contact.postalCode, contact.city].filter(Boolean).join(', ')}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if contact.birthday}
|
||||
<div class="profile-detail">
|
||||
<Cake size={14} />
|
||||
<span>{contact.birthday}</span>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="profile-hint">Tippe zum Bearbeiten</div>
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
{#snippet contactRow(contact: Contact)}
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
|
|
@ -263,6 +323,9 @@
|
|||
<div class="contact-info">
|
||||
<div class="contact-name-row">
|
||||
<span class="contact-name">{getDisplayName(contact)}</span>
|
||||
{#if contact.id === SELF_CONTACT_ID}
|
||||
<span class="self-badge">Du</span>
|
||||
{/if}
|
||||
{#if contact.isFavorite}
|
||||
<Star weight="fill" size={12} class="favorite-star" />
|
||||
{/if}
|
||||
|
|
@ -445,4 +508,76 @@
|
|||
.contact-row:hover .row-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Self badge */
|
||||
.self-badge {
|
||||
font-size: 0.5625rem;
|
||||
font-weight: 600;
|
||||
padding: 0.0625rem 0.375rem;
|
||||
border-radius: 9999px;
|
||||
background: color-mix(in srgb, var(--color-primary, #8b5cf6) 12%, transparent);
|
||||
color: var(--color-primary, #8b5cf6);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Profile card */
|
||||
.profile-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 1.5rem 1rem;
|
||||
cursor: pointer;
|
||||
border-radius: 0.375rem;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.profile-card:hover {
|
||||
background: rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
:global(.dark) .profile-card:hover {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
.profile-avatar {
|
||||
width: 4.5rem;
|
||||
height: 4.5rem;
|
||||
border-radius: 9999px;
|
||||
background: color-mix(in srgb, var(--color-primary, #8b5cf6) 12%, transparent);
|
||||
color: var(--color-primary, #8b5cf6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
overflow: hidden;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
.profile-avatar-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.profile-name {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 700;
|
||||
color: #374151;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
:global(.dark) .profile-name {
|
||||
color: #f3f4f6;
|
||||
}
|
||||
.profile-detail {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.8125rem;
|
||||
color: #6b7280;
|
||||
padding: 0.25rem 0;
|
||||
}
|
||||
:global(.dark) .profile-detail {
|
||||
color: #9ca3af;
|
||||
}
|
||||
.profile-hint {
|
||||
margin-top: 1rem;
|
||||
font-size: 0.6875rem;
|
||||
color: #9ca3af;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import { _ } from 'svelte-i18n';
|
||||
import {
|
||||
Users,
|
||||
User,
|
||||
Star,
|
||||
Cake,
|
||||
Envelope,
|
||||
|
|
@ -21,6 +22,13 @@
|
|||
let { onSelect, onClose, activePageIds = [] }: Props = $props();
|
||||
|
||||
const PAGE_OPTIONS = [
|
||||
{
|
||||
id: 'my-profile',
|
||||
title: 'Mein Profil',
|
||||
description: 'Deine eigene Kontaktkarte',
|
||||
icon: User,
|
||||
color: '#8B5CF6',
|
||||
},
|
||||
{
|
||||
id: 'all',
|
||||
title: 'Alle Kontakte',
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@
|
|||
]);
|
||||
|
||||
const PAGE_META: Record<string, { title: string; color: string }> = {
|
||||
'my-profile': { title: 'Mein Profil', color: '#8B5CF6' },
|
||||
all: { title: 'Alle Kontakte', color: '#3B82F6' },
|
||||
favorites: { title: 'Favoriten', color: '#F59E0B' },
|
||||
'birthday-soon': { title: 'Bald Geburtstag', color: '#EC4899' },
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue