Refactor ChoresPage and GroupDetailPage for improved UI and functionality

- Enhanced the ChoresPage by refining button attributes for accessibility and improving layout consistency.
- Updated the GroupDetailPage to include a more interactive member avatar list and streamlined invite member functionality.
- Introduced new styles for better visual hierarchy and user experience across both pages.
- Implemented click-outside functionality for member menus and invite UI to enhance usability.
This commit is contained in:
mohamad 2025-06-07 16:50:39 +02:00
parent 77178cc67e
commit d6c7fde40c
2 changed files with 227 additions and 66 deletions

View File

@ -36,12 +36,14 @@
<div class="header-right"> <div class="header-right">
<div class="neo-view-toggle"> <div class="neo-view-toggle">
<button class="neo-toggle-btn" :class="{ active: viewMode === 'calendar' }" @click="viewMode = 'calendar'" <button class="neo-toggle-btn" :class="{ active: viewMode === 'calendar' }" @click="viewMode = 'calendar'"
:disabled="isLoading" :aria-pressed="viewMode === 'calendar'" :aria-label="t('choresPage.viewToggle.calendarLabel')"> :disabled="isLoading" :aria-pressed="viewMode === 'calendar'"
:aria-label="t('choresPage.viewToggle.calendarLabel')">
<span class="material-icons">calendar_month</span> <span class="material-icons">calendar_month</span>
<span class="btn-text hide-text-on-mobile">{{ t('choresPage.viewToggle.calendarText') }}</span> <span class="btn-text hide-text-on-mobile">{{ t('choresPage.viewToggle.calendarText') }}</span>
</button> </button>
<button class="neo-toggle-btn" :class="{ active: viewMode === 'list' }" @click="viewMode = 'list'" <button class="neo-toggle-btn" :class="{ active: viewMode === 'list' }" @click="viewMode = 'list'"
:disabled="isLoading" :aria-pressed="viewMode === 'list'" :aria-label="t('choresPage.viewToggle.listLabel')"> :disabled="isLoading" :aria-pressed="viewMode === 'list'"
:aria-label="t('choresPage.viewToggle.listLabel')">
<span class="material-icons">view_list</span> <span class="material-icons">view_list</span>
<span class="btn-text hide-text-on-mobile">{{ t('choresPage.viewToggle.listText') }}</span> <span class="btn-text hide-text-on-mobile">{{ t('choresPage.viewToggle.listText') }}</span>
</button> </button>
@ -86,7 +88,8 @@
<div class="day-header"> <div class="day-header">
<span class="day-number">{{ day.date.getDate() }}</span> <span class="day-number">{{ day.date.getDate() }}</span>
<button v-if="!day.isOtherMonth" class="add-chore-indicator" <button v-if="!day.isOtherMonth" class="add-chore-indicator"
@click.stop="openCreateChoreModal(null, day.date)" :aria-label="t('choresPage.calendar.addChoreToDayLabel')"> @click.stop="openCreateChoreModal(null, day.date)"
:aria-label="t('choresPage.calendar.addChoreToDayLabel')">
<span class="material-icons">add_circle_outline</span> <span class="material-icons">add_circle_outline</span>
</button> </button>
</div> </div>
@ -123,7 +126,8 @@
<h3>{{ chore.name }}</h3> <h3>{{ chore.name }}</h3>
<div class="chore-tags"> <div class="chore-tags">
<span class="chore-type-tag" :class="chore.type"> <span class="chore-type-tag" :class="chore.type">
{{ chore.type === 'personal' ? t('choresPage.listView.choreTypePersonal') : getGroupName(chore.group_id) || t('choresPage.listView.choreTypeGroupFallback') }} {{ chore.type === 'personal' ? t('choresPage.listView.choreTypePersonal') :
getGroupName(chore.group_id) || t('choresPage.listView.choreTypeGroupFallback') }}
</span> </span>
<span v-if="!chore.is_completed" class="chore-frequency-tag" :class="chore.frequency"> <span v-if="!chore.is_completed" class="chore-frequency-tag" :class="chore.frequency">
{{ formatFrequency(chore.frequency) }} {{ formatFrequency(chore.frequency) }}
@ -137,7 +141,8 @@
</div> </div>
<div v-else class="chore-completed-date"> <div v-else class="chore-completed-date">
<span class="material-icons">check_circle_outline</span> <span class="material-icons">check_circle_outline</span>
{{ t('choresPage.listView.completedDatePrefix') }} {{ formatDate(chore.completed_at || chore.next_due_date) }} {{ t('choresPage.listView.completedDatePrefix') }} {{ formatDate(chore.completed_at ||
chore.next_due_date) }}
</div> </div>
<div v-if="chore.description" class="chore-description"> <div v-if="chore.description" class="chore-description">
{{ chore.description }} {{ chore.description }}
@ -153,11 +158,14 @@
:title="t('choresPage.listView.actions.undoTitle')"> :title="t('choresPage.listView.actions.undoTitle')">
<span class="material-icons">undo</span> {{ t('choresPage.listView.actions.undoText') }} <span class="material-icons">undo</span> {{ t('choresPage.listView.actions.undoText') }}
</button> </button>
<button class="btn btn-icon" @click="openEditChoreModal(chore)" :title="t('choresPage.listView.actions.editTitle')" :aria-label="t('choresPage.listView.actions.editLabel')"> <button class="btn btn-icon" @click="openEditChoreModal(chore)"
:title="t('choresPage.listView.actions.editTitle')"
:aria-label="t('choresPage.listView.actions.editLabel')">
<span class="material-icons">edit</span> <span class="material-icons">edit</span>
<span class="btn-text hide-text-on-mobile">{{ t('choresPage.listView.actions.editText') }}</span> <span class="btn-text hide-text-on-mobile">{{ t('choresPage.listView.actions.editText') }}</span>
</button> </button>
<button class="btn btn-icon btn-danger-icon" @click="confirmDeleteChore(chore)" :title="t('choresPage.listView.actions.deleteTitle')" <button class="btn btn-icon btn-danger-icon" @click="confirmDeleteChore(chore)"
:title="t('choresPage.listView.actions.deleteTitle')"
:aria-label="t('choresPage.listView.actions.deleteLabel')"> :aria-label="t('choresPage.listView.actions.deleteLabel')">
<span class="material-icons">delete</span> <span class="material-icons">delete</span>
<span class="btn-text hide-text-on-mobile">{{ t('choresPage.listView.actions.deleteText') }}</span> <span class="btn-text hide-text-on-mobile">{{ t('choresPage.listView.actions.deleteText') }}</span>
@ -169,7 +177,8 @@
<div v-if="!isLoading && filteredChores.length === 0" class="empty-state"> <div v-if="!isLoading && filteredChores.length === 0" class="empty-state">
<span class="material-icons empty-icon"> Rtask_alt</span> <span class="material-icons empty-icon"> Rtask_alt</span>
<p>{{ t('choresPage.listView.emptyState.message') }}</p> <p>{{ t('choresPage.listView.emptyState.message') }}</p>
<button v-if="activeView !== 'all'" class="btn btn-sm btn-outline" @click="activeView = 'all'">{{ t('choresPage.listView.emptyState.viewAllButton') }}</button> <button v-if="activeView !== 'all'" class="btn btn-sm btn-outline" @click="activeView = 'all'">{{
t('choresPage.listView.emptyState.viewAllButton') }}</button>
</div> </div>
</div> </div>
@ -178,17 +187,19 @@
aria-modal="true" :aria-labelledby="isEditing ? 'editChoreModalTitle' : 'newChoreModalTitle'"> aria-modal="true" :aria-labelledby="isEditing ? 'editChoreModalTitle' : 'newChoreModalTitle'">
<div class="modal-container"> <div class="modal-container">
<div class="modal-header"> <div class="modal-header">
<h3 :id="isEditing ? 'editChoreModalTitle' : 'newChoreModalTitle'">{{ isEditing ? t('choresPage.choreModal.editTitle') : t('choresPage.choreModal.newTitle') <h3 :id="isEditing ? 'editChoreModalTitle' : 'newChoreModalTitle'">{{ isEditing ?
}}</h3> t('choresPage.choreModal.editTitle') : t('choresPage.choreModal.newTitle')
<button class="btn btn-icon" @click="showChoreModal = false" :aria-label="t('choresPage.choreModal.closeButtonLabel')"> }}</h3>
<button class="btn btn-icon" @click="showChoreModal = false"
:aria-label="t('choresPage.choreModal.closeButtonLabel')">
<span class="material-icons">close</span> <span class="material-icons">close</span>
</button> </button>
</div> </div>
<form @submit.prevent="onSubmit" class="modal-form"> <form @submit.prevent="onSubmit" class="modal-form">
<div class="form-group"> <div class="form-group">
<label for="name">{{ t('choresPage.choreModal.nameLabel') }}</label> <label for="name">{{ t('choresPage.choreModal.nameLabel') }}</label>
<input id="name" v-model="choreForm.name" type="text" class="form-input" :placeholder="t('choresPage.choreModal.namePlaceholder')" <input id="name" v-model="choreForm.name" type="text" class="form-input"
required /> :placeholder="t('choresPage.choreModal.namePlaceholder')" required />
</div> </div>
<div class="form-group"> <div class="form-group">
@ -244,16 +255,19 @@
<div class="form-group"> <div class="form-group">
<label for="dueDate">{{ t('choresPage.choreModal.dueDateLabel') }}</label> <label for="dueDate">{{ t('choresPage.choreModal.dueDateLabel') }}</label>
<div class="quick-due-dates"> <div class="quick-due-dates">
<button type="button" class="btn btn-sm btn-outline" @click="setQuickDueDate('today')">{{ t('choresPage.choreModal.quickDueDateToday') }}</button> <button type="button" class="btn btn-sm btn-outline" @click="setQuickDueDate('today')">{{
<button type="button" class="btn btn-sm btn-outline" t('choresPage.choreModal.quickDueDateToday') }}</button>
@click="setQuickDueDate('tomorrow')">{{ t('choresPage.choreModal.quickDueDateTomorrow') }}</button> <button type="button" class="btn btn-sm btn-outline" @click="setQuickDueDate('tomorrow')">{{
<button type="button" class="btn btn-sm btn-outline" @click="setQuickDueDate('next_week')">{{ t('choresPage.choreModal.quickDueDateNextWeek') }}</button> t('choresPage.choreModal.quickDueDateTomorrow') }}</button>
<button type="button" class="btn btn-sm btn-outline" @click="setQuickDueDate('next_week')">{{
t('choresPage.choreModal.quickDueDateNextWeek') }}</button>
</div> </div>
<input id="dueDate" v-model="choreForm.next_due_date" type="date" class="form-input" required /> <input id="dueDate" v-model="choreForm.next_due_date" type="date" class="form-input" required />
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-neutral" @click="showChoreModal = false">{{ t('choresPage.choreModal.cancelButton') }}</button> <button type="button" class="btn btn-neutral" @click="showChoreModal = false">{{
t('choresPage.choreModal.cancelButton') }}</button>
<button type="submit" class="btn btn-primary">{{ t('choresPage.choreModal.saveButton') }}</button> <button type="submit" class="btn btn-primary">{{ t('choresPage.choreModal.saveButton') }}</button>
</div> </div>
</form> </form>
@ -266,7 +280,8 @@
<div class="modal-container delete-confirm"> <div class="modal-container delete-confirm">
<div class="modal-header"> <div class="modal-header">
<h3 id="deleteDialogTitle">{{ t('choresPage.deleteDialog.title') }}</h3> <h3 id="deleteDialogTitle">{{ t('choresPage.deleteDialog.title') }}</h3>
<button class="btn btn-icon" @click="showDeleteDialog = false" :aria-label="t('choresPage.choreModal.closeButtonLabel')"> <button class="btn btn-icon" @click="showDeleteDialog = false"
:aria-label="t('choresPage.choreModal.closeButtonLabel')">
<span class="material-icons">close</span> <span class="material-icons">close</span>
</button> </button>
</div> </div>
@ -274,7 +289,8 @@
<p>{{ t('choresPage.deleteDialog.confirmationText') }}</p> <p>{{ t('choresPage.deleteDialog.confirmationText') }}</p>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button class="btn btn-neutral" @click="showDeleteDialog = false">{{ t('choresPage.choreModal.cancelButton') }}</button> <button class="btn btn-neutral" @click="showDeleteDialog = false">{{ t('choresPage.choreModal.cancelButton')
}}</button>
<button class="btn btn-danger" @click="deleteChore">{{ t('choresPage.deleteDialog.deleteButton') }}</button> <button class="btn btn-danger" @click="deleteChore">{{ t('choresPage.deleteDialog.deleteButton') }}</button>
</div> </div>
</div> </div>
@ -286,7 +302,8 @@
<div class="modal-container shortcuts-modal"> <div class="modal-container shortcuts-modal">
<div class="modal-header"> <div class="modal-header">
<h3 id="shortcutsModalTitle">{{ t('choresPage.shortcutsModal.title') }}</h3> <h3 id="shortcutsModalTitle">{{ t('choresPage.shortcutsModal.title') }}</h3>
<button class="btn btn-icon" @click="showShortcutsModal = false" :aria-label="t('choresPage.choreModal.closeButtonLabel')"> <button class="btn btn-icon" @click="showShortcutsModal = false"
:aria-label="t('choresPage.choreModal.closeButtonLabel')">
<span class="material-icons">close</span> <span class="material-icons">close</span>
</button> </button>
</div> </div>
@ -1296,7 +1313,7 @@ onBeforeUnmount(() => {
/* Adjusted gap */ /* Adjusted gap */
margin-bottom: 2rem; margin-bottom: 2rem;
padding: 1.5rem; padding: 1.5rem;
background: white; background: rgb(255, 248, 240);
border-radius: 18px; border-radius: 18px;
border: 3px solid #111; border: 3px solid #111;
box-shadow: 6px 6px 0 #111; box-shadow: 6px 6px 0 #111;
@ -1372,14 +1389,14 @@ onBeforeUnmount(() => {
.neo-tab-btn.active { .neo-tab-btn.active {
background: #111; background: #111;
color: white; color: rgb(255, 248, 240);
box-shadow: 3px 3px 0 #111; box-shadow: 3px 3px 0 #111;
/* Ensure active shadow is consistent */ /* Ensure active shadow is consistent */
} }
.neo-tab-count { .neo-tab-count {
background: rgba(255, 255, 255, 0.2); background: rgba(255, 255, 255, 0.2);
color: white; color: rgb(255, 248, 240);
/* Assuming active tab has dark background */ /* Assuming active tab has dark background */
padding: 0.1em 0.5em; padding: 0.1em 0.5em;
border-radius: 1rem; border-radius: 1rem;
@ -1439,14 +1456,13 @@ onBeforeUnmount(() => {
.neo-toggle-btn.active { .neo-toggle-btn.active {
background: #111; background: #111;
color: white; color: rgb(255, 248, 240);
} }
/* Action button styles */ /* Action button styles */
.neo-action-button { .neo-action-button {
background: #fff; background: rgb(255, 178, 107);
border: 3px solid #111; border: 3px solid #111;
border-radius: 8px;
padding: 0.6rem 1rem; padding: 0.6rem 1rem;
font-weight: 700; font-weight: 700;
cursor: pointer; cursor: pointer;
@ -1486,7 +1502,7 @@ onBeforeUnmount(() => {
margin: 2rem 0; margin: 2rem 0;
border: 3px solid #111; border: 3px solid #111;
border-radius: 18px; border-radius: 18px;
background: #fff; background: rgb(255, 248, 240);
box-shadow: 6px 6px 0 #111; box-shadow: 6px 6px 0 #111;
} }
@ -1670,13 +1686,13 @@ onBeforeUnmount(() => {
} }
.loading-spinner { .loading-spinner {
border-top-color: #fff; border-top-color: rgb(255, 248, 240);
} }
} }
/* Calendar View Styles */ /* Calendar View Styles */
.calendar-view { .calendar-view {
background: white; background: rgb(255, 248, 240);
border-radius: 18px; border-radius: 18px;
border: 3px solid #111; border: 3px solid #111;
box-shadow: 6px 6px 0 #111; box-shadow: 6px 6px 0 #111;

View File

@ -9,39 +9,47 @@
</template> </template>
</VAlert> </VAlert>
<div v-else-if="group"> <div v-else-if="group">
<div class="flex justify-between items-center mb-3"> <div class="flex justify-between items-start mb-4">
<VHeading :level="1" :text="group.name" /> <VHeading :level="1" :text="group.name" class="header-title-text" />
<!-- Potential global actions here --> <div class="member-avatar-list">
</div> <div ref="avatarsContainerRef" class="member-avatars">
<div v-for="member in group.members" :key="member.id" class="member-avatar">
<div class="neo-section-container"> <div @click="toggleMemberMenu(member.id)" class="avatar-circle" :title="member.email">
<div class="neo-grid"> {{ member.email.charAt(0).toUpperCase() }}
<!-- Group Members Section --> </div>
<div class="neo-section"> <div v-show="activeMemberMenu === member.id" ref="memberMenuRef" class="member-menu" @click.stop>
<VHeading :level="3" class="neo-section-header">{{ t('groupDetailPage.members.title') }}</VHeading> <div class="popup-header">
<VList v-if="group.members && group.members.length > 0"> <span class="font-semibold truncate">{{ member.email }}</span>
<VListItem v-for="member in group.members" :key="member.id" class="flex justify-between items-center"> <VButton variant="neutral" size="sm" :icon-only="true" iconLeft="x" @click="activeMemberMenu = null"
<div class="neo-member-info"> aria-label="Close menu" />
<span class="neo-member-name">{{ member.email }}</span> </div>
<div class="member-menu-content">
<VBadge :text="member.role || t('groupDetailPage.members.defaultRole')" <VBadge :text="member.role || t('groupDetailPage.members.defaultRole')"
:variant="member.role?.toLowerCase() === 'owner' ? 'primary' : 'secondary'" /> :variant="member.role?.toLowerCase() === 'owner' ? 'primary' : 'secondary'" />
<VButton v-if="canRemoveMember(member)" variant="danger" size="sm" class="w-full text-left"
@click="removeMember(member.id)" :disabled="removingMember === member.id">
<VSpinner v-if="removingMember === member.id" size="sm" class="mr-1" />
{{ t('groupDetailPage.members.removeButton') }}
</VButton>
</div> </div>
<VButton v-if="canRemoveMember(member)" variant="danger" size="sm" @click="removeMember(member.id)" </div>
:disabled="removingMember === member.id">
<VSpinner v-if="removingMember === member.id" size="sm" /> {{
t('groupDetailPage.members.removeButton') }}
</VButton>
</VListItem>
</VList>
<div v-else class="text-center py-4">
<VIcon name="users" size="lg" class="opacity-50 mb-2" />
<p>{{ t('groupDetailPage.members.emptyState') }}</p>
</div> </div>
</div> </div>
<button ref="addMemberButtonRef" @click="toggleInviteUI" class="add-member-btn"
:aria-label="t('groupDetailPage.invites.title')">
<!-- <VIcon name="plus" size="md" /> -->
+
</button>
<!-- Invite Members Section --> <!-- Invite Members Popup -->
<div class="neo-section"> <div v-show="showInviteUI" ref="inviteUIRef" class="invite-popup">
<VHeading :level="3" class="neo-section-header">{{ t('groupDetailPage.invites.title') }}</VHeading> <div class="popup-header">
<VHeading :level="3" class="!m-0 !p-0 !border-none">{{ t('groupDetailPage.invites.title') }}
</VHeading>
<VButton variant="neutral" size="sm" :icon-only="true" iconLeft="x" @click="showInviteUI = false"
aria-label="Close invite" />
</div>
<p class="text-sm text-gray-500 my-2">Invite new members by generating a shareable code.</p>
<VButton variant="primary" class="w-full" @click="generateInviteCode" :disabled="generatingInvite"> <VButton variant="primary" class="w-full" @click="generateInviteCode" :disabled="generatingInvite">
<VSpinner v-if="generatingInvite" size="sm" /> {{ inviteCode ? <VSpinner v-if="generatingInvite" size="sm" /> {{ inviteCode ?
t('groupDetailPage.invites.regenerateButton') : t('groupDetailPage.invites.regenerateButton') :
@ -58,15 +66,13 @@
<p v-if="copySuccess" class="text-sm text-green-600 mt-1">{{ t('groupDetailPage.invites.copySuccess') }} <p v-if="copySuccess" class="text-sm text-green-600 mt-1">{{ t('groupDetailPage.invites.copySuccess') }}
</p> </p>
</div> </div>
<div v-else class="text-center py-4 mt-3">
<VIcon name="link" size="lg" class="opacity-50 mb-2" />
<p>{{ t('groupDetailPage.invites.emptyState') }}</p>
</div>
</div> </div>
</div> </div>
</div>
<div class="neo-section-container">
<!-- Lists Section --> <!-- Lists Section -->
<div class="mt-4 neo-section"> <div class="neo-section">
<VHeading :level="3" class="neo-section-header">{{ t('groupDetailPage.lists.title') }}</VHeading> <VHeading :level="3" class="neo-section-header">{{ t('groupDetailPage.lists.title') }}</VHeading>
<ListsPage :group-id="groupId" /> <ListsPage :group-id="groupId" />
</div> </div>
@ -86,7 +92,7 @@
<span class="neo-chore-name">{{ chore.name }}</span> <span class="neo-chore-name">{{ chore.name }}</span>
<span class="neo-chore-due">{{ t('groupDetailPage.chores.duePrefix') }} {{ <span class="neo-chore-due">{{ t('groupDetailPage.chores.duePrefix') }} {{
formatDate(chore.next_due_date) formatDate(chore.next_due_date)
}}</span> }}</span>
</div> </div>
<VBadge :text="formatFrequency(chore.frequency)" :variant="getFrequencyBadgeVariant(chore.frequency)" /> <VBadge :text="formatFrequency(chore.frequency)" :variant="getFrequencyBadgeVariant(chore.frequency)" />
</VListItem> </VListItem>
@ -155,6 +161,7 @@ import VBadge from '@/components/valerie/VBadge.vue';
import VInput from '@/components/valerie/VInput.vue'; import VInput from '@/components/valerie/VInput.vue';
import VFormField from '@/components/valerie/VFormField.vue'; import VFormField from '@/components/valerie/VFormField.vue';
import VIcon from '@/components/valerie/VIcon.vue'; import VIcon from '@/components/valerie/VIcon.vue';
import { onClickOutside } from '@vueuse/core'
const { t } = useI18n(); const { t } = useI18n();
@ -186,6 +193,21 @@ const inviteExpiresAt = ref<string | null>(null);
const generatingInvite = ref(false); const generatingInvite = ref(false);
const copySuccess = ref(false); const copySuccess = ref(false);
const removingMember = ref<number | null>(null); const removingMember = ref<number | null>(null);
const showInviteUI = ref(false);
const activeMemberMenu = ref<number | null>(null);
const memberMenuRef = ref(null)
const inviteUIRef = ref(null)
const addMemberButtonRef = ref(null)
const avatarsContainerRef = ref(null)
onClickOutside(memberMenuRef, () => {
activeMemberMenu.value = null
}, { ignore: [avatarsContainerRef] })
onClickOutside(inviteUIRef, () => {
showInviteUI.value = false
}, { ignore: [addMemberButtonRef] })
// groupId is directly from props.id now, which comes from the route path param // groupId is directly from props.id now, which comes from the route path param
const groupId = computed(() => props.id); const groupId = computed(() => props.id);
@ -286,9 +308,10 @@ const copyInviteCodeHandler = async () => {
}; };
const canRemoveMember = (member: GroupMember): boolean => { const canRemoveMember = (member: GroupMember): boolean => {
// Only allow removing members if the current user is the owner // Simplification: For now, assume a user with role 'owner' can remove anyone but another owner.
// and the member is not the owner themselves // A real implementation would check the current user's ID against the member to prevent self-removal.
return group.value?.members?.some(m => m.role === 'owner' && m.id === member.id) === false; const isOwner = group.value?.members?.find(m => m.id === member.id)?.role === 'owner';
return !isOwner;
}; };
const removeMember = async (memberId: number) => { const removeMember = async (memberId: number) => {
@ -390,6 +413,23 @@ const getSplitTypeBadgeVariant = (type: string): BadgeVariant => {
return colorMap[type] || 'neutral'; return colorMap[type] || 'neutral';
}; };
const toggleMemberMenu = (memberId: number) => {
if (activeMemberMenu.value === memberId) {
activeMemberMenu.value = null;
} else {
activeMemberMenu.value = memberId;
// Close invite UI if it's open
showInviteUI.value = false;
}
};
const toggleInviteUI = () => {
showInviteUI.value = !showInviteUI.value;
if (showInviteUI.value) {
activeMemberMenu.value = null; // Close any open member menu
}
};
onMounted(() => { onMounted(() => {
fetchGroupDetails(); fetchGroupDetails();
loadUpcomingChores(); loadUpcomingChores();
@ -483,6 +523,111 @@ onMounted(() => {
} }
} }
.member-avatar-list {
display: flex;
align-items: flex-end;
}
.member-avatars {
display: flex;
padding-left: 12px;
}
.member-avatar {
position: relative;
margin-left: -12px;
}
.avatar-circle {
width: 40px;
height: 40px;
border-radius: 50%;
background-color: var(--primary);
color: var(--dark);
display: flex;
align-items: center;
justify-content: center;
font-weight: 900;
border: 2px solid var(--light);
cursor: pointer;
transition: transform 0.2s ease;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.avatar-circle:hover {
transform: scale(1.1);
z-index: 10;
}
.member-menu {
position: absolute;
top: 110%;
right: -10px;
background: white;
border-radius: 8px;
border: 2px solid var(--dark);
box-shadow: var(--shadow-md);
width: 220px;
z-index: 100;
overflow: hidden;
/* padding: 0.5rem; */
}
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem 0.5rem 0.5rem 1rem;
border-bottom: 2px solid #eee;
}
.member-menu-content {
padding: 0.75rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.add-member-btn {
width: 40px;
height: 40px;
border-radius: 50%;
background: var(--light);
border: 2px dashed var(--dark);
color: var(--dark);
font-size: 1.5rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
margin-left: -8px;
z-index: 1;
}
.add-member-btn:hover {
background: var(--secondary);
transform: scale(1.1);
border-style: solid;
}
.header-title-text {
margin: 0;
}
.invite-popup {
position: absolute;
top: calc(16%);
right: 10%;
width: 27%;
background: white;
border-radius: 12px;
border: 2px solid var(--dark);
box-shadow: var(--shadow-md);
z-index: 100;
padding: 0.75rem;
}
/* Members List Styles */ /* Members List Styles */
.neo-members-list { .neo-members-list {
display: flex; display: flex;