Skip to content

Instantly share code, notes, and snippets.

@yasirismail009
Created October 24, 2024 12:53
Show Gist options
  • Save yasirismail009/7804b08c695a7d5bded945a5e23e43fa to your computer and use it in GitHub Desktop.
Save yasirismail009/7804b08c695a7d5bded945a5e23e43fa to your computer and use it in GitHub Desktop.
HENNGE CHALLENGES
import { useMemo, type PropsWithChildren } from 'react'
import styled from 'styled-components'
import { groupBy } from 'lodash-es'
import type { Email } from '../types/Email'
import RecipientsDisplay from './RecipientsDisplay'
import DateDisplay from './DateDisplay'
import TimeDisplay from './TimeDisplay'
type AuditTableProps = PropsWithChildren<{ emails: Email[] }>
function AuditTable({ emails, ...rest }: AuditTableProps) {
const emailsByDate = useMemo(
() =>
groupBy<Email>(emails, ({ datetime }) =>
new Date(datetime).toLocaleDateString(),
),
[emails],
)
return (
<table {...rest}>
<thead>
<tr>
<th>Sender</th>
<th>Recipients</th>
<th>Subject</th>
<RightAlignedHeader>Date</RightAlignedHeader>
<RightAlignedHeader>Time</RightAlignedHeader>
</tr>
</thead>
{Object.entries(emailsByDate).map(([datetime, emailGroup]) => (
<tbody key={datetime}>
{emailGroup.map(({ id, from, to: recipients, subject, datetime }) => (
<tr key={id}>
<td>{from}</td>
<td data-testid={id} style={{overflow:'visible'}}>
<RecipientsDisplay recipients={recipients} />
</td>
<td>{subject}</td>
<RightAlignedCell>
<DateDisplay datetime={datetime} />
</RightAlignedCell>
<RightAlignedCell>
<TimeDisplay datetime={datetime} />
</RightAlignedCell>
</tr>
))}
</tbody>
))}
</table>
)
}
const RightAlignedHeader = styled.th`
text-align: right;
`
const RightAlignedCell = styled.td`
text-align: right;
`
export default styled(AuditTable)`
table-layout: fixed;
border: var(--border-style);
border-spacing: 0;
width: 100%;
text-align: left;
th,
td {
border: var(--border-style);
padding: 5px 10px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
height: 34px;
box-sizing: border-box;
}
th {
&:nth-child(1) {
width: 20%;
}
&:nth-child(2) {
width: 30%;
}
&:nth-child(3) {
width: 50%;
}
&:nth-child(4) {
width: 90px;
}
&:nth-child(5) {
width: 70px;
}
}
tbody:nth-child(even) {
background-color: #ddd;
}
`
<script setup lang="ts">
import { computed } from 'vue'
import { groupBy } from 'lodash-es'
import RecipientsDisplay from '@/components/RecipientsDisplay.vue'
import DateDisplay from '@/components/DateDisplay.vue'
import TimeDisplay from '@/components/TimeDisplay.vue'
import type { Email } from '@/types/Email'
const props = defineProps<{
emails: Email[]
}>()
// Convert into an array of arrays based on date sent
const emailGroupsByDate = computed(() => {
const emailsByDate = groupBy<Email>(props.emails, ({ datetime }) =>
new Date(datetime).toLocaleDateString()
)
return Object.values(emailsByDate)
})
</script>
<template>
<table cellspacing="0">
<thead>
<tr>
<th>Sender</th>
<th>Recipients</th>
<th>Subject</th>
<th class="align-right">Date</th>
<th class="align-right">Time</th>
</tr>
</thead>
<tbody v-for="emailGroup in emailGroupsByDate" :key="emailGroup[0].datetime">
<tr
v-for="{ id, from, to: recipients, subject, datetime } in emailGroup"
:key="id"
>
<td>{{ from }}</td>
<td :data-testid="id" style="overflow: visible;">
<RecipientsDisplay :recipients="recipients" />
</td>
<td>{{ subject }}</td>
<td class="align-right">
<DateDisplay :datetime="datetime" />
</td>
<td class="align-right">
<TimeDisplay :datetime="datetime" />
</td>
</tr>
</tbody>
</table>
</template>
<style scoped>
:root {
--border-style: solid 1px #777;
}
table {
table-layout: fixed;
border: var(--border-style);
width: 100%;
text-align: left;
}
th,
td {
border: var(--border-style);
padding: 5px 10px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
height: 34px;
box-sizing: border-box;
}
th:nth-child(1) {
width: 20%;
}
th:nth-child(2) {
width: 30%;
}
th:nth-child(3) {
width: 50%;
}
th:nth-child(4) {
width: 90px;
}
th:nth-child(5) {
width: 70px;
}
tbody:nth-child(even) {
background-color: #ddd;
}
.align-right {
text-align: right;
}
</style>
<script setup lang="ts">
const props = defineProps<{ numTruncated: number }>();
</script>
<template>
<span class="badge" data-testid="badge">+{{ props.numTruncated }}</span>
</template>
<style scoped>
.badge {
padding: 2px 5px;
border-radius: 3px;
background-color: var(--color-primary);
color: #f0f0f0;
cursor: pointer;
}
</style>
<script lang="ts">
// Add this part to ensure it's exported as default
export default {
name: 'RecipientsBadge'
};
</script>
import { useState, useRef, useEffect } from 'react';
import styled from 'styled-components';
import RecipientsBadge from './RecipientsBadge';
interface RecipientsDisplayProps {
recipients: string[];
}
export default function RecipientsDisplay({ recipients }: RecipientsDisplayProps) {
const [isTooltipVisible, setIsTooltipVisible] = useState(false);
const [visibleRecipients, setVisibleRecipients] = useState<string[]>([]);
const [hiddenCount, setHiddenCount] = useState(0);
const containerRef = useRef<HTMLDivElement>(null);
// Adjust displayed recipients based on available width
useEffect(() => {
const adjustRecipientsDisplay = () => {
if (!containerRef.current) return;
const containerWidth = containerRef.current.offsetWidth;
let currentWidth = 0;
let visible: string[] = [];
let hidden = 0;
recipients.forEach((recipient, index) => {
const width = measureTextWidth(recipient);
if (currentWidth + width + measureTextWidth(", ...") < containerWidth || index === 0) {
currentWidth += width + measureTextWidth(', ');
visible.push(recipient);
} else {
hidden += 1;
}
});
setVisibleRecipients(visible);
setHiddenCount(hidden);
};
adjustRecipientsDisplay();
window.addEventListener('resize', adjustRecipientsDisplay);
return () => window.removeEventListener('resize', adjustRecipientsDisplay);
}, [recipients]);
// Measure the width of a text string
const measureTextWidth = (text: string) => {
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
if (context) {
context.font = getComputedStyle(document.body).font;
return context.measureText(text).width;
}
return 0;
};
return (
<Container ref={containerRef}>
{/* Display visible recipients */}
<RecipientList>
{visibleRecipients.join(', ')}
</RecipientList>
{/* Show ellipsis and badge if some recipients are hidden */}
{hiddenCount > 0 && (
<>
<span>, ...</span>
<StyledRecipientsBadge
numTruncated={hiddenCount}
onMouseEnter={() => setIsTooltipVisible(true)}
onMouseLeave={() => setIsTooltipVisible(false)}
/>
</>
)}
{/* Tooltip for displaying all recipients */}
{isTooltipVisible && (
<Tooltip>
<TooltipTitle>Recipients:</TooltipTitle>
<TooltipList>
{recipients.map((recipient, index) => (
<TooltipItem key={index}>{recipient}</TooltipItem>
))}
</TooltipList>
</Tooltip>
)}
</Container>
);
}
// Styled components for the tooltip and container
const Container = styled.div`
display: flex;
align-items: center;
white-space: nowrap;
text-overflow: ellipsis;
width: 100%;
position: relative;
`;
const RecipientList = styled.span`
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
max-width: 100%;
display: inline-block;
`;
const Tooltip = styled.div`
position: absolute;
bottom: 70%; /* Position above the badge */
left: 100%;
transform: translateX(-50%);
padding: 8px 12px;
background-color: #666;
color: #f0f0f0;
border-radius: 8px;
white-space: nowrap;
z-index: 99999999999999999; /* Set z-index to 20 */
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
margin-bottom: 8px; /* Space between badge and tooltip */
@media (max-width: 600px) {
max-width: 90%;
}
`;
const TooltipTitle = styled.div`
font-weight: bold;
margin-bottom: 4px; /* Spacing between title and list */
`;
const TooltipList = styled.ul`
list-style: none; /* Remove default bullet points */
padding: 0; /* Remove default padding */
margin: 0; /* Remove default margin */
`;
const TooltipItem = styled.li`
margin: 4px 0; /* Spacing between items */
`;
// Styled RecipientsBadge component
const StyledRecipientsBadge = styled(RecipientsBadge)`
flex-shrink: 0;
padding: 2px 5px;
border-radius: 3px;
background-color: var(--color-primary);
color: #f0f0f0;
position: relative;
cursor: pointer;
`;
<template>
<div ref="containerRef" class="recipients-container">
<!-- Display visible recipients -->
<span class="recipients-list">{{ visibleRecipients.join(', ') }}</span>
<!-- Show ellipsis and badge if some recipients are hidden -->
<span v-if="hiddenCount > 0">
, ...
<StyledRecipientsBadge
:num-truncated="hiddenCount"
@mouseenter="isTooltipVisible = true"
@mouseleave="isTooltipVisible = false"
/>
</span>
<!-- Tooltip for displaying all recipients -->
<Tooltip v-if="isTooltipVisible" class="tooltip show">
<TooltipTitle class="tooltip-title">Recipients:</TooltipTitle>
<TooltipList class="tooltip-list">
<TooltipItem v-for="(recipient, index) in recipients" :key="index" class="tooltip-item">
{{ recipient }}
</TooltipItem>
</TooltipList>
</Tooltip>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, onMounted, onBeforeUnmount } from 'vue';
import StyledRecipientsBadge from './RecipientsBadge.vue'; // Use the correct name here
interface RecipientsDisplayProps {
recipients: string[];
}
export default defineComponent({
name: 'RecipientsDisplay',
components: {
StyledRecipientsBadge, // Register the StyledRecipientsBadge component
},
props: {
recipients: {
type: Array as () => string[],
required: true,
},
},
setup(props: RecipientsDisplayProps) {
const isTooltipVisible = ref<boolean>(false);
const visibleRecipients = ref<string[]>([]);
const hiddenCount = ref<number>(0);
const containerRef = ref<HTMLDivElement | null>(null);
const adjustRecipientsDisplay = () => {
if (!containerRef.value) return;
const containerWidth = containerRef.value.offsetWidth;
let currentWidth = 0;
let visible: string[] = [];
let hidden = 0;
props.recipients.forEach((recipient, index) => {
const width = measureTextWidth(recipient);
if (currentWidth + width + measureTextWidth(", ...") < containerWidth || index === 0) {
currentWidth += width + measureTextWidth(', ');
visible.push(recipient);
} else {
hidden += 1;
}
});
visibleRecipients.value = visible;
hiddenCount.value = hidden;
};
const measureTextWidth = (text: string): number => {
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
if (context) {
context.font = getComputedStyle(document.body).font;
return context.measureText(text).width;
}
return 0;
};
onMounted(() => {
adjustRecipientsDisplay();
window.addEventListener('resize', adjustRecipientsDisplay);
});
onBeforeUnmount(() => {
window.removeEventListener('resize', adjustRecipientsDisplay);
});
return {
containerRef,
isTooltipVisible,
visibleRecipients,
hiddenCount,
};
},
});
</script>
<style scoped>
.recipients-container {
display: flex;
align-items: center;
white-space: nowrap;
width: 100%;
position: relative; /* Ensure this is set for absolute positioning of children */
}
.recipients-list {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
max-width: 100%;
display: inline-block;
}
.tooltip {
position: absolute;
bottom: 100%; /* Position above the badge */
left: 70%; /* Center horizontally */
transform: translateX(-50%); /* Adjust to center */
padding: 8px 12px;
background-color: #666;
color: #f0f0f0;
border-radius: 8px;
white-space: nowrap;
z-index: 9999; /* Adjusted for safety */
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
margin-bottom: 8px; /* Space between badge and tooltip */
display: none; /* Hide initially */
}
.tooltip.show {
display: block; /* Show when needed */
}
.tooltip-title {
font-weight: bold;
margin-bottom: 4px;
}
.tooltip-list {
display: flex; /* Change this to flex to stack items */
flex-direction: column; /* Stack items in a column */
list-style: none;
padding: 0;
margin: 0;
}
.tooltip-item {
margin: 4px 0;
}
.styled-recipients-badge {
flex-shrink: 0;
padding: 2px 5px;
border-radius: 3px;
background-color: var(--color-primary);
color: #f0f0f0;
position: relative;
cursor: pointer;
}
</style>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment