Created
October 24, 2024 12:53
-
-
Save yasirismail009/7804b08c695a7d5bded945a5e23e43fa to your computer and use it in GitHub Desktop.
HENNGE CHALLENGES
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
`; | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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