feat(members): align grid UI to spec — avatar colors, badges, join date, invite panel
- MemberCard: white bg, 1px border + shadow-card, centered column layout,
avatar color by role (green-dark/blue), role badge with role-specific colors,
join date "seit DD.MM.YYYY", Du-badge below join date, ⋯ kebab with icons
and divider, inline role-control with Abbrechen, blue editing border #B5D4F4
- InviteCard: white bg, 1.5px dashed border, min-height 180px, plus circle,
label "Mitglied einladen", full hover state (green border/bg/icon/label)
- InvitePanel: white bg, title "Einladelink teilen", description, mono link
box, yellow expiry pill when ≤ 24h, text-link "Neuen Link generieren"
- RemoveDialog: white bg, padding 28px 32px, "?" in title, updated body text
- +page.server.ts: expose householdName from locals.haushalt
- +page.svelte: subtitle "{n} Mitglieder · {householdName}"
- Tests: add join date format test, Abbrechen test, InvitePanel title test
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ export const load: PageServerLoad = async ({ fetch, locals }) => {
|
||||
return {
|
||||
members: membersRes.data ?? [],
|
||||
currentUserId: locals.benutzer!.id,
|
||||
activeInvite: inviteRes.data?.data ?? null
|
||||
activeInvite: inviteRes.data?.data ?? null,
|
||||
householdName: locals.haushalt?.name ?? ''
|
||||
};
|
||||
};
|
||||
|
||||
@@ -70,7 +70,8 @@
|
||||
<svelte:head><title>Mitglieder — Mealprep</title></svelte:head>
|
||||
|
||||
<div class="p-[16px_20px] md:p-[40px_56px]">
|
||||
<h1 class="font-[var(--font-display)] text-[28px] font-medium tracking-[-0.02em] mb-8 text-[var(--color-text)]">Mitglieder</h1>
|
||||
<h1 class="font-[var(--font-display)] text-[28px] font-medium tracking-[-0.02em] mb-1 text-[var(--color-text)]">Mitglieder</h1>
|
||||
<p class="text-[13px] text-[var(--color-text-muted)] mb-8">{members.length} Mitglieder{data.householdName ? ` · ${data.householdName}` : ''}</p>
|
||||
|
||||
<MemberGrid
|
||||
{members}
|
||||
|
||||
@@ -10,8 +10,75 @@
|
||||
type="button"
|
||||
data-testid="invite-card"
|
||||
{onclick}
|
||||
style="background: var(--color-surface); border-radius: var(--radius-xl); border: 2px dashed var(--color-border); padding: 20px; cursor: pointer; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; width: 100%;"
|
||||
class="invite-card"
|
||||
>
|
||||
<span style="font-size: 2rem; line-height: 1;" aria-hidden="true">+</span>
|
||||
<span>Einladen</span>
|
||||
<div class="invite-plus">+</div>
|
||||
<div class="invite-label">Mitglied einladen</div>
|
||||
</button>
|
||||
|
||||
<style>
|
||||
.invite-card {
|
||||
background: white;
|
||||
border: 1.5px dashed var(--color-border);
|
||||
border-radius: var(--radius-xl);
|
||||
padding: 24px 20px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
min-height: 180px;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.invite-card:hover {
|
||||
border-color: var(--green-light);
|
||||
background: var(--green-tint);
|
||||
}
|
||||
|
||||
.invite-plus {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: var(--radius-full);
|
||||
background: var(--color-subtle);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 22px;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.invite-card:hover .invite-plus {
|
||||
background: var(--green-light);
|
||||
color: var(--green-dark);
|
||||
}
|
||||
|
||||
.invite-label {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.invite-card:hover .invite-label {
|
||||
color: var(--green-dark);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.invite-card {
|
||||
padding: 16px;
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.invite-plus {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.invite-label {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -23,28 +23,117 @@
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' });
|
||||
}
|
||||
|
||||
const isExpiringSoon = $derived(
|
||||
new Date(invite.expiresAt).getTime() - Date.now() <= 24 * 60 * 60 * 1000
|
||||
);
|
||||
</script>
|
||||
|
||||
<div
|
||||
style="background: var(--color-surface); border-radius: var(--radius-xl); padding: 20px; margin-top: 16px; border: 1px solid var(--color-border);"
|
||||
>
|
||||
<p
|
||||
style="font-family: monospace; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin: 0 0 12px;"
|
||||
>
|
||||
{invite.shareUrl || invite.inviteCode}
|
||||
</p>
|
||||
<div class="invite-panel">
|
||||
<div class="invite-panel-title">Einladelink teilen</div>
|
||||
<div class="invite-panel-desc">Wer diesen Link öffnet, kann dem Haushalt als Mitglied beitreten.</div>
|
||||
|
||||
<div style="display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 12px;">
|
||||
<button type="button" data-testid="copy-btn" onclick={copy}>
|
||||
<div class="invite-link-row">
|
||||
<div class="invite-link-box">{invite.shareUrl || invite.inviteCode}</div>
|
||||
<button type="button" data-testid="copy-btn" class="btn-copy" onclick={copy}>
|
||||
{copied ? 'Kopiert ✓' : 'Kopieren'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button type="button" data-testid="regenerate-btn" onclick={onregenerate}>
|
||||
Neu generieren
|
||||
<div class="invite-expiry">
|
||||
Läuft ab: <span class:yellow={isExpiringSoon}>{formatExpiry(invite.expiresAt)}</span>
|
||||
</div>
|
||||
|
||||
<button type="button" data-testid="regenerate-btn" class="btn-regen" onclick={onregenerate}>
|
||||
Neuen Link generieren
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p style="margin: 0; font-size: 0.875rem;">
|
||||
Läuft ab: {formatExpiry(invite.expiresAt)}
|
||||
</p>
|
||||
</div>
|
||||
<style>
|
||||
.invite-panel {
|
||||
background: white;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-xl);
|
||||
padding: 24px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.invite-panel-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.invite-panel-desc {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-muted);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.invite-link-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.invite-link-box {
|
||||
flex: 1;
|
||||
background: var(--color-subtle);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 8px 12px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px;
|
||||
color: var(--color-text-muted);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn-copy {
|
||||
padding: 8px 14px;
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--color-border);
|
||||
background: white;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn-copy:hover {
|
||||
background: var(--color-subtle);
|
||||
}
|
||||
|
||||
.invite-expiry {
|
||||
font-size: 11px;
|
||||
color: var(--color-text-muted);
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.invite-expiry span {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.invite-expiry span.yellow {
|
||||
background: var(--yellow-tint);
|
||||
color: var(--yellow-text);
|
||||
padding: 1px 6px;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.btn-regen {
|
||||
margin-top: 12px;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-muted);
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.btn-regen:hover {
|
||||
color: var(--color-text);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -31,4 +31,9 @@ describe('InvitePanel', () => {
|
||||
await userEvent.click(screen.getByTestId('regenerate-btn'));
|
||||
expect(onregenerate).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('shows the panel title', () => {
|
||||
render(InvitePanel, { props: { invite, onregenerate: vi.fn() } });
|
||||
expect(screen.getByText('Einladelink teilen')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { browser } from '$app/environment';
|
||||
import SegmentedControl from '$lib/components/SegmentedControl.svelte';
|
||||
|
||||
type Member = {
|
||||
userId: string;
|
||||
@@ -26,13 +25,18 @@
|
||||
let menuOpen = $state(false);
|
||||
let editingRole = $state(false);
|
||||
|
||||
const roleOptions = [
|
||||
{ value: 'planner', label: 'Planer' },
|
||||
{ value: 'member', label: 'Mitglied' }
|
||||
];
|
||||
|
||||
const initials = $derived(member.displayName.slice(0, 2).toUpperCase());
|
||||
|
||||
const avatarBg = $derived(member.role === 'planner' ? 'var(--green-dark)' : 'var(--blue)');
|
||||
|
||||
const joinDateFormatted = $derived(
|
||||
new Date(member.joinedAt).toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric'
|
||||
})
|
||||
);
|
||||
|
||||
let cardEl: HTMLElement | undefined = $state(undefined);
|
||||
|
||||
$effect(() => {
|
||||
@@ -65,151 +69,318 @@
|
||||
<article
|
||||
bind:this={cardEl}
|
||||
data-testid="member-card"
|
||||
style="
|
||||
background: var(--color-surface);
|
||||
border-radius: var(--radius-xl);
|
||||
padding: 20px;
|
||||
position: relative;
|
||||
{isCurrentUser ? 'border: 2px solid var(--color-success);' : ''}
|
||||
"
|
||||
class="member-card"
|
||||
class:own={isCurrentUser}
|
||||
class:editing={editingRole}
|
||||
>
|
||||
<!-- Avatar -->
|
||||
<div
|
||||
style="
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-success-dark);
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
margin-bottom: 12px;
|
||||
"
|
||||
>
|
||||
<div class="avatar" style="background: {avatarBg};">
|
||||
{initials}
|
||||
</div>
|
||||
|
||||
<!-- Name row -->
|
||||
<div style="display: flex; align-items: center; gap: 6px; margin-bottom: 6px;">
|
||||
<span style="font-weight: 500;">{member.displayName}</span>
|
||||
{#if isCurrentUser}
|
||||
<span
|
||||
style="
|
||||
background: var(--color-success-tint);
|
||||
color: var(--color-success-dark);
|
||||
border-radius: 999px;
|
||||
font-size: 11px;
|
||||
padding: 2px 8px;
|
||||
"
|
||||
>Du</span>
|
||||
{/if}
|
||||
</div>
|
||||
<!-- Name -->
|
||||
<div class="member-name">{member.displayName}</div>
|
||||
|
||||
<!-- Role badge or SegmentedControl -->
|
||||
<!-- Role badge or inline role control -->
|
||||
{#if editingRole}
|
||||
<SegmentedControl
|
||||
options={roleOptions}
|
||||
value={member.role}
|
||||
onchange={(newValue) => {
|
||||
onrolechange(member, newValue);
|
||||
<div role="group" class="role-control">
|
||||
<button
|
||||
type="button"
|
||||
class="role-control-btn"
|
||||
class:active={member.role === 'planner'}
|
||||
onclick={() => {
|
||||
if (member.role !== 'planner') {
|
||||
onrolechange(member, 'planner');
|
||||
editingRole = false;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
>Planer</button>
|
||||
<button
|
||||
type="button"
|
||||
class="role-control-btn"
|
||||
class:active={member.role === 'member'}
|
||||
onclick={() => {
|
||||
if (member.role !== 'member') {
|
||||
onrolechange(member, 'member');
|
||||
editingRole = false;
|
||||
}
|
||||
}}
|
||||
>Mitglied</button>
|
||||
</div>
|
||||
{:else}
|
||||
<span style="font-size: 13px; color: var(--color-text-muted);">
|
||||
<span class="role-badge" class:planer={member.role === 'planner'} class:mitglied={member.role === 'member'}>
|
||||
{member.role === 'planner' ? 'Planer' : 'Mitglied'}
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
<!-- Join date -->
|
||||
<div class="join-date">seit {joinDateFormatted}</div>
|
||||
|
||||
<!-- Du badge (own card) or Abbrechen (when editing role) -->
|
||||
{#if isCurrentUser}
|
||||
<div class="self-badge-wrap">
|
||||
<span class="self-badge">Du</span>
|
||||
</div>
|
||||
{:else if editingRole}
|
||||
<button
|
||||
type="button"
|
||||
class="cancel-btn"
|
||||
onclick={() => { editingRole = false; }}
|
||||
>Abbrechen</button>
|
||||
{/if}
|
||||
|
||||
<!-- Kebab button -->
|
||||
{#if isPlanner && !isCurrentUser}
|
||||
{#if isPlanner && !isCurrentUser && !editingRole}
|
||||
<button
|
||||
data-testid="kebab-btn"
|
||||
type="button"
|
||||
class="kebab-btn"
|
||||
onclick={() => { menuOpen = true; }}
|
||||
aria-label="Optionen"
|
||||
style="
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
border-radius: var(--radius-sm);
|
||||
line-height: 1;
|
||||
font-size: 18px;
|
||||
color: var(--color-text-muted);
|
||||
"
|
||||
>⋮</button>
|
||||
>⋯</button>
|
||||
|
||||
<!-- Dropdown -->
|
||||
{#if menuOpen}
|
||||
<div
|
||||
style="
|
||||
position: absolute;
|
||||
top: 40px;
|
||||
right: 12px;
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-raised);
|
||||
z-index: 10;
|
||||
min-width: 140px;
|
||||
overflow: hidden;
|
||||
"
|
||||
>
|
||||
<div class="dropdown">
|
||||
<button
|
||||
type="button"
|
||||
style="
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 10px 16px;
|
||||
text-align: left;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
"
|
||||
class="dropdown-item"
|
||||
onclick={() => { menuOpen = false; editingRole = true; }}
|
||||
>Rolle ändern</button>
|
||||
><span class="dropdown-icon">🔄</span>Rolle ändern</button>
|
||||
<div class="dropdown-divider"></div>
|
||||
<button
|
||||
type="button"
|
||||
style="
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 10px 16px;
|
||||
text-align: left;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: var(--color-danger, #e53e3e);
|
||||
"
|
||||
class="dropdown-item danger"
|
||||
onclick={() => { menuOpen = false; onremove(member); }}
|
||||
>Entfernen</button>
|
||||
><span class="dropdown-icon">✕</span>Entfernen</button>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</article>
|
||||
|
||||
<style>
|
||||
.member-card {
|
||||
background: white;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-xl);
|
||||
padding: 24px 20px 20px;
|
||||
box-shadow: var(--shadow-card);
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
min-height: 180px;
|
||||
}
|
||||
|
||||
.member-card.own {
|
||||
border-color: var(--green-light);
|
||||
}
|
||||
|
||||
.member-card.editing {
|
||||
border-color: #B5D4F4;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 50%;
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: var(--font-display);
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.member-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.role-badge {
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.04em;
|
||||
padding: 2px 8px;
|
||||
border-radius: var(--radius-full);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.role-badge.planer {
|
||||
background: var(--green-tint);
|
||||
color: var(--green-dark);
|
||||
}
|
||||
|
||||
.role-badge.mitglied {
|
||||
background: var(--blue-tint);
|
||||
color: var(--blue-dark);
|
||||
}
|
||||
|
||||
.join-date {
|
||||
font-size: 11px;
|
||||
color: var(--color-text-muted);
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.self-badge-wrap {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.self-badge {
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.04em;
|
||||
padding: 2px 8px;
|
||||
border-radius: var(--radius-full);
|
||||
background: var(--green-tint);
|
||||
color: var(--green-dark);
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
margin-top: 8px;
|
||||
font-size: 11px;
|
||||
color: var(--color-text-muted);
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.kebab-btn {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: var(--radius-md);
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
color: var(--color-text-muted);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.kebab-btn:hover {
|
||||
background: var(--color-subtle);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
@media (hover: none) {
|
||||
.kebab-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.member-card:hover .kebab-btn {
|
||||
.member-card:hover .kebab-btn,
|
||||
.member-card:focus-within .kebab-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
position: absolute;
|
||||
top: 44px;
|
||||
right: 12px;
|
||||
background: white;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-raised);
|
||||
min-width: 160px;
|
||||
z-index: 10;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 14px;
|
||||
font-size: 13px;
|
||||
color: var(--color-text);
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
background: none;
|
||||
border: none;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.dropdown-item:hover {
|
||||
background: var(--color-subtle);
|
||||
}
|
||||
|
||||
.dropdown-item.danger {
|
||||
color: var(--color-error);
|
||||
}
|
||||
|
||||
.dropdown-item.danger:hover {
|
||||
background: var(--error-tint, #FDECEA);
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
font-size: 14px;
|
||||
width: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dropdown-divider {
|
||||
height: 1px;
|
||||
background: var(--color-border);
|
||||
margin: 2px 0;
|
||||
}
|
||||
|
||||
.role-control {
|
||||
display: flex;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
margin-top: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.role-control-btn {
|
||||
flex: 1;
|
||||
padding: 6px 8px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
background: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.role-control-btn:first-child {
|
||||
border-right: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.role-control-btn.active {
|
||||
background: var(--green-dark);
|
||||
color: white;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.member-card {
|
||||
padding: 16px;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
font-size: 16px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.member-name {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -144,4 +144,32 @@ describe('MemberCard', () => {
|
||||
await userEvent.keyboard('{Escape}');
|
||||
expect(screen.queryByText('Entfernen')).toBeNull();
|
||||
});
|
||||
|
||||
it('shows formatted join date', () => {
|
||||
render(MemberCard, {
|
||||
props: {
|
||||
member: plannerMember,
|
||||
isCurrentUser: false,
|
||||
isPlanner: false,
|
||||
onremove: vi.fn(),
|
||||
onrolechange: vi.fn()
|
||||
}
|
||||
});
|
||||
expect(screen.getByText(/seit 01\.01\.2024/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows Abbrechen button when editing role', async () => {
|
||||
render(MemberCard, {
|
||||
props: {
|
||||
member: regularMember,
|
||||
isCurrentUser: false,
|
||||
isPlanner: true,
|
||||
onremove: vi.fn(),
|
||||
onrolechange: vi.fn()
|
||||
}
|
||||
});
|
||||
await userEvent.click(screen.getByTestId('kebab-btn'));
|
||||
await userEvent.click(screen.getByText('Rolle ändern'));
|
||||
expect(screen.getByText(/abbrechen/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,16 +19,16 @@
|
||||
{#if show}
|
||||
{#if isMobile()}
|
||||
<BottomSheet open={show} onclose={oncancel}>
|
||||
<div data-testid="remove-dialog" style="padding: 24px;">
|
||||
<h2 style="margin: 0 0 12px;">Mitglied entfernen</h2>
|
||||
<p>
|
||||
Soll <strong>{member.displayName}</strong> wirklich entfernt werden?
|
||||
<div data-testid="remove-dialog" style="padding: 24px 24px 32px;">
|
||||
<h2 style="margin: 0 0 8px; font-size: 15px; font-weight: 500;">Mitglied entfernen?</h2>
|
||||
<p style="font-size: 12px; color: var(--color-text-muted); line-height: 1.6; margin-bottom: 24px;">
|
||||
<strong style="color: var(--color-text); font-weight: 500;">{member.displayName}</strong> wird aus dem Haushalt entfernt.
|
||||
</p>
|
||||
<div style="display: flex; gap: 12px; justify-content: flex-end; margin-top: 20px;">
|
||||
<div style="display: flex; gap: 10px; justify-content: flex-end;">
|
||||
<button
|
||||
type="button"
|
||||
onclick={oncancel}
|
||||
style="background: transparent; border: 1px solid var(--color-border); border-radius: var(--radius-md); padding: 10px 20px; cursor: pointer;"
|
||||
style="padding: 9px 18px; border-radius: var(--radius-md); border: 1px solid var(--color-border); background: white; font-size: 12px; font-weight: 500; cursor: pointer;"
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
@@ -36,7 +36,7 @@
|
||||
type="button"
|
||||
data-testid="confirm-remove-btn"
|
||||
onclick={onconfirm}
|
||||
style="background: var(--color-error); color: white; border: none; border-radius: var(--radius-md); padding: 10px 20px; cursor: pointer;"
|
||||
style="padding: 9px 18px; border-radius: var(--radius-md); border: none; background: var(--color-error); color: white; font-size: 12px; font-weight: 500; cursor: pointer;"
|
||||
>
|
||||
Entfernen
|
||||
</button>
|
||||
@@ -47,7 +47,7 @@
|
||||
<div
|
||||
data-testid="dialog-backdrop"
|
||||
role="presentation"
|
||||
style="position: fixed; inset: 0; z-index: 100; background: rgba(28,28,24,0.4); display: flex; align-items: center; justify-content: center;"
|
||||
style="position: fixed; inset: 0; z-index: 100; background: rgba(28,28,24,.45); display: flex; align-items: center; justify-content: center;"
|
||||
>
|
||||
<div
|
||||
data-testid="remove-dialog"
|
||||
@@ -55,19 +55,19 @@
|
||||
aria-modal="true"
|
||||
aria-labelledby="remove-dialog-title"
|
||||
tabindex="-1"
|
||||
style="background: var(--color-surface); border-radius: var(--radius-xl); padding: 24px; max-width: 400px; width: 90%;"
|
||||
style="background: white; border-radius: var(--radius-xl); padding: 28px 32px; max-width: 380px; width: 100%; box-shadow: var(--shadow-raised);"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
onkeydown={(e) => e.stopPropagation()}
|
||||
>
|
||||
<h2 id="remove-dialog-title" style="margin: 0 0 12px;">Mitglied entfernen</h2>
|
||||
<p>
|
||||
Soll <strong>{member.displayName}</strong> wirklich entfernt werden?
|
||||
<h2 id="remove-dialog-title" style="font-size: 16px; font-weight: 500; margin: 0 0 8px;">Mitglied entfernen?</h2>
|
||||
<p style="font-size: 13px; color: var(--color-text-muted); line-height: 1.6; margin-bottom: 24px;">
|
||||
<strong style="color: var(--color-text); font-weight: 500;">{member.displayName}</strong> wird aus dem Haushalt entfernt und verliert sofort den Zugang zu allen Plänen und Rezepten.
|
||||
</p>
|
||||
<div style="display: flex; gap: 12px; justify-content: flex-end; margin-top: 20px;">
|
||||
<div style="display: flex; gap: 10px; justify-content: flex-end;">
|
||||
<button
|
||||
type="button"
|
||||
onclick={oncancel}
|
||||
style="background: transparent; border: 1px solid var(--color-border); border-radius: var(--radius-md); padding: 10px 20px; cursor: pointer;"
|
||||
style="padding: 9px 18px; border-radius: var(--radius-md); border: 1px solid var(--color-border); background: white; font-size: 13px; font-weight: 500; cursor: pointer;"
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
@@ -75,7 +75,7 @@
|
||||
type="button"
|
||||
data-testid="confirm-remove-btn"
|
||||
onclick={onconfirm}
|
||||
style="background: var(--color-error); color: white; border: none; border-radius: var(--radius-md); padding: 10px 20px; cursor: pointer;"
|
||||
style="padding: 9px 18px; border-radius: var(--radius-md); border: none; background: var(--color-error); color: white; font-size: 13px; font-weight: 500; cursor: pointer;"
|
||||
>
|
||||
Entfernen
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user