Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
b60d697
feat: new Badges and Awards widget (#4986)
nensidosari Oct 9, 2025
a7bb10e
feat: Profile Header & User Form (#4987)
AmarTrebinjac Oct 16, 2025
b54865b
Merge branch 'main' into MI-1027-profile
nensidosari Oct 16, 2025
96eb6e1
feat: reading overview (#4988)
nensidosari Oct 17, 2025
454c69d
Merge branch 'main' of github.com:dailydotdev/apps into MI-1027-profile
nensidosari Oct 20, 2025
70d69dd
Feat profile widget container (#5008)
nensidosari Oct 21, 2025
cb5fab8
feat: add ProfileSquadsWidget (#5002)
ilasw Oct 22, 2025
562c26b
chore: refactor profile widgets (#5010)
nensidosari Oct 22, 2025
dd296f8
Merge branch 'main' into MI-1027-profile
nensidosari Oct 23, 2025
386c84a
feat: profile progress (#5009)
nensidosari Oct 28, 2025
27d201d
feat: profile share widget (#5014)
nensidosari Oct 28, 2025
30e0f57
feat: about section profile (#5017)
nensidosari Oct 31, 2025
4933a20
Merge branch 'main' into MI-1027-profile
AmarTrebinjac Nov 4, 2025
e6f606b
feat: list user experiences (#5015)
sshanzel Nov 4, 2025
1ca700b
feat: new activity section + fix profile betwen laptop tablet transit…
nensidosari Nov 4, 2025
98c5d60
feat: react hook form validation (#5035)
AmarTrebinjac Nov 4, 2025
bfdf88f
Merge branch 'main' into MI-1027-profile
AmarTrebinjac Nov 4, 2025
0def843
Merge branch 'main' into MI-1027-profile
rebelchris Nov 5, 2025
64a54d5
Merge branch 'main' into MI-1027-profile
rebelchris Nov 6, 2025
22031b8
fix: inner pages for experiences (#5038)
rebelchris Nov 6, 2025
30f4521
feat: work experience form (#5039)
AmarTrebinjac Nov 10, 2025
6ce3492
feat: education form (#5042)
AmarTrebinjac Nov 10, 2025
2cb9e24
feat: certification form (#5043)
AmarTrebinjac Nov 10, 2025
be07ce3
feat: project form (#5046)
AmarTrebinjac Nov 10, 2025
c80db9e
feat: experience settings page link, edit and delete handling (#5047)
AmarTrebinjac Nov 12, 2025
2581fde
feat: add location and company to ProfileHeader (#5061)
AmarTrebinjac Nov 12, 2025
78e212c
feat: update experiences on form changes (#5062)
AmarTrebinjac Nov 12, 2025
a9101b0
fix: show custom company name when no company (#5063)
AmarTrebinjac Nov 12, 2025
3e7cda0
fix: add image prop to social signup (#5064)
AmarTrebinjac Nov 13, 2025
55bfca8
fix: various fixes from E2E testing (#5069)
AmarTrebinjac Nov 14, 2025
36232be
fix last test
AmarTrebinjac Nov 15, 2025
d9c7780
update profile completions
AmarTrebinjac Nov 17, 2025
2dde9f4
add volunteer and opensource to experience list
AmarTrebinjac Nov 17, 2025
49817d5
add current switch
AmarTrebinjac Nov 17, 2025
ba544b1
remove company verification on profile form
AmarTrebinjac Nov 17, 2025
b63318b
pluralize certifications
AmarTrebinjac Nov 17, 2025
c2e5bc7
remove unnecessary change
AmarTrebinjac Nov 18, 2025
6685f59
update location to use mapbox
AmarTrebinjac Nov 19, 2025
5415c6a
shortcut
AmarTrebinjac Nov 19, 2025
8d286a6
add max length
AmarTrebinjac Nov 19, 2025
c717002
add break spaces
AmarTrebinjac Nov 20, 2025
c0f8a9d
update cache
AmarTrebinjac Nov 20, 2025
4aaffdb
swap plus icon
AmarTrebinjac Nov 20, 2025
9c1bb61
update tests
AmarTrebinjac Nov 20, 2025
710874b
Merge branch 'main' into MI-1027-profile
AmarTrebinjac Nov 20, 2025
a59ef8c
fix: experience feedback (#5081)
AmarTrebinjac Nov 20, 2025
79fbc4a
refactor: remove truncate from profile completion (#5086)
AmarTrebinjac Nov 20, 2025
640d0ba
fix: activity section (#5085)
AmarTrebinjac Nov 20, 2025
355f263
refactor: revert to old top readers logic (#5087)
AmarTrebinjac Nov 20, 2025
dc40a78
add is same user check to header
AmarTrebinjac Nov 20, 2025
2820769
Merge branch 'MI-1027-profile' of github.com:dailydotdev/apps into MI…
AmarTrebinjac Nov 20, 2025
a96d181
exclude prop
AmarTrebinjac Nov 20, 2025
2d63f45
fix: hardcoded limit for experiences
capJavert Nov 21, 2025
3ce30b5
refactor: minor tweaks to autocomplete props (#5090)
AmarTrebinjac Nov 21, 2025
7858df0
Merge branch 'main' into MI-1027-profile
AmarTrebinjac Nov 21, 2025
1ce8f4d
fix: profile crash
capJavert Nov 21, 2025
539d1be
fix: update show more url and add opensource and volunteer inner page…
AmarTrebinjac Nov 21, 2025
450fc42
refactor: experience dates (#5088)
AmarTrebinjac Nov 23, 2025
e347fbc
add current color to icons
AmarTrebinjac Nov 23, 2025
fc791a2
feat: add endedAt to open source (#5093)
AmarTrebinjac Nov 23, 2025
47d2df7
Merge branch 'main' into MI-1027-profile
AmarTrebinjac Nov 23, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/shared/src/components/CalendarHeatmap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ export function CalendarHeatmap<T extends { date: string }>({

return (
<svg
width={width}
width="100%"
viewBox={`0 0 ${width} ${height}`}
onMouseDown={(e) => e.preventDefault()}
>
Expand Down
184 changes: 184 additions & 0 deletions packages/shared/src/components/ExpandableContent.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import React from 'react';
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
import { ExpandableContent } from './ExpandableContent';
import clearAllMocks = jest.clearAllMocks;

describe('ExpandableContent', () => {
const shortContent = <div>Short content that fits</div>;
const longContent = (
<div>
<p>Long content paragraph 1</p>
<p>Long content paragraph 2</p>
<p>Long content paragraph 3</p>
<p>Long content paragraph 4</p>
<p>Long content paragraph 5</p>
<p>Long content paragraph 6</p>
<p>Long content paragraph 7</p>
<p>Long content paragraph 8</p>
<p>Long content paragraph 9</p>
<p>Long content paragraph 10</p>
</div>
);

beforeEach(() => {
// Reset scrollHeight mock before each test
Object.defineProperty(HTMLElement.prototype, 'scrollHeight', {
configurable: true,
get() {
return 100; // Default value
},
});
});

afterEach(() => {
clearAllMocks();
});

it('should render children content', () => {
render(<ExpandableContent>{shortContent}</ExpandableContent>);
const element = screen.getByText('Short content that fits');
expect(element).toBeInTheDocument();
expect(element).toBeVisible();
});

it('should not show "See More" button when content is short', async () => {
// Mock scrollHeight to be less than maxHeight
Object.defineProperty(HTMLElement.prototype, 'scrollHeight', {
configurable: true,
get() {
return 200; // Less than default 320px
},
});

render(<ExpandableContent>{shortContent}</ExpandableContent>);

// Wait a bit for useEffect to run
await waitFor(
() => {
expect(
screen.queryByRole('button', { name: /see more/i }),
).not.toBeInTheDocument();
},
{ timeout: 200 },
);
});

it('should show "See More" button when content exceeds maxHeight', async () => {
// Mock scrollHeight to be more than maxHeight
Object.defineProperty(HTMLElement.prototype, 'scrollHeight', {
configurable: true,
get() {
return 500; // More than default 320px
},
});

render(<ExpandableContent>{longContent}</ExpandableContent>);

await waitFor(() => {
expect(
screen.getByRole('button', { name: /see more/i }),
).toBeInTheDocument();
});
});

it('should expand content when "See More" button is clicked', async () => {
// Mock scrollHeight to be more than maxHeight
Object.defineProperty(HTMLElement.prototype, 'scrollHeight', {
configurable: true,
get() {
return 500;
},
});

render(
<ExpandableContent maxHeight={320}>{longContent}</ExpandableContent>,
);

await waitFor(() => {
expect(
screen.getByRole('button', { name: /see more/i }),
).toBeInTheDocument();
});

const seeMoreButton = screen.getByRole('button', { name: /see more/i });
fireEvent.click(seeMoreButton);

// Button should disappear after expansion
await waitFor(() => {
expect(
screen.queryByRole('button', { name: /see more/i }),
).not.toBeInTheDocument();
});
});

it('should show gradient overlay when content is collapsed', async () => {
// Mock scrollHeight to be more than maxHeight
Object.defineProperty(HTMLElement.prototype, 'scrollHeight', {
configurable: true,
get() {
return 500;
},
});

render(
<ExpandableContent maxHeight={320}>{longContent}</ExpandableContent>,
);

// Wait for See More button to appear, which indicates collapsed state
await waitFor(() => {
expect(
screen.getByRole('button', { name: /see more/i }),
).toBeInTheDocument();
});
});

it('should hide "See More" button when content is expanded', async () => {
// Mock scrollHeight to be more than maxHeight
Object.defineProperty(HTMLElement.prototype, 'scrollHeight', {
configurable: true,
get() {
return 500;
},
});

render(
<ExpandableContent maxHeight={320}>{longContent}</ExpandableContent>,
);

await waitFor(() => {
expect(
screen.getByRole('button', { name: /see more/i }),
).toBeInTheDocument();
});

const seeMoreButton = screen.getByRole('button', { name: /see more/i });
fireEvent.click(seeMoreButton);

// Both button and gradient should be hidden after expansion
await waitFor(() => {
expect(
screen.queryByRole('button', { name: /see more/i }),
).not.toBeInTheDocument();
});
});

it('should apply custom maxHeight', async () => {
// Mock scrollHeight to exceed custom maxHeight
Object.defineProperty(HTMLElement.prototype, 'scrollHeight', {
configurable: true,
get() {
return 200; // More than 150px
},
});

render(
<ExpandableContent maxHeight={150}>{longContent}</ExpandableContent>,
);

await waitFor(() => {
expect(
screen.getByRole('button', { name: /see more/i }),
).toBeInTheDocument();
});
});
});
100 changes: 100 additions & 0 deletions packages/shared/src/components/ExpandableContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import type { ReactElement, ReactNode } from 'react';
import React, { useState, useRef, useEffect } from 'react';
import classNames from 'classnames';
import {
Button,
ButtonIconPosition,
ButtonSize,
ButtonVariant,
} from './buttons/Button';
import { MoveToIcon } from './icons';
import { IconSize } from './Icon';

export interface ExpandableContentProps {
children: ReactNode;
maxHeight?: number; // in pixels
className?: string;
}

const DEFAULT_MAX_HEIGHT = 320; // pixels

export function ExpandableContent({
children,
maxHeight = DEFAULT_MAX_HEIGHT,
className,
}: ExpandableContentProps): ReactElement {
const [isExpanded, setIsExpanded] = useState(false);
const [showSeeMore, setShowSeeMore] = useState(false);
const contentRef = useRef<HTMLDivElement>(null);

useEffect(() => {
const element = contentRef.current;
if (!element) {
return undefined;
}

const checkHeight = () => {
const contentHeight = element.scrollHeight;
setShowSeeMore(contentHeight > maxHeight);
};

// Wait for browser to complete layout before checking height
// Using double RAF ensures the layout is fully calculated
const rafId = requestAnimationFrame(() => {
requestAnimationFrame(() => {
checkHeight();
});
});

// Only use ResizeObserver if there are images (for async loading)
const hasImages = element.querySelector('img') !== null;
if (!hasImages) {
return () => cancelAnimationFrame(rafId);
}

const resizeObserver = new ResizeObserver(checkHeight);
resizeObserver.observe(element);

return () => {
cancelAnimationFrame(rafId);
resizeObserver.disconnect();
};
}, [maxHeight, children]);

return (
<>
<div
ref={contentRef}
className={classNames(
'relative transition-all duration-500 ease-in-out',
{
'overflow-hidden': !isExpanded,
},
className,
)}
style={{
maxHeight: !isExpanded ? `${maxHeight}px` : undefined,
}}
>
{children}
{!isExpanded && showSeeMore && (
<div className="absolute inset-x-0 bottom-0 h-12 bg-gradient-to-b from-transparent to-background-default" />
)}
</div>

{showSeeMore && !isExpanded && (
<div className="mt-4 flex w-full items-center justify-center">
<Button
variant={ButtonVariant.Subtle}
size={ButtonSize.Medium}
onClick={() => setIsExpanded(true)}
icon={<MoveToIcon size={IconSize.XSmall} className="rotate-90" />}
iconPosition={ButtonIconPosition.Right}
>
See More
</Button>
</div>
)}
</>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,12 @@ function HorizontalScrollComponent(
const titleId = `horizontal-scroll-title-${id}`;
const { ref, header } = useHorizontalScrollHeader({
...scrollProps,
title: { ...scrollProps?.title, id: titleId },
title:
scrollProps.title &&
typeof scrollProps.title === 'object' &&
'copy' in scrollProps.title
? { ...scrollProps.title, id: titleId }
: scrollProps.title,
});

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type { MouseEventHandler, ReactElement, ReactNode } from 'react';
import React from 'react';
import classNames from 'classnames';
import Link from '../utilities/Link';
import { Button } from '../buttons/Button';
import { ButtonVariant } from '../buttons/common';
import { ButtonSize, ButtonVariant } from '../buttons/common';
import ConditionalWrapper from '../ConditionalWrapper';
import { ArrowIcon } from '../icons';
import { Typography, TypographyType } from '../typography/Typography';
Expand All @@ -15,14 +16,16 @@ export interface HorizontalScrollTitleProps {
}

export interface HorizontalScrollHeaderProps {
title: HorizontalScrollTitleProps;
title?: HorizontalScrollTitleProps | ReactNode;
isAtEnd: boolean;
isAtStart: boolean;
onClickNext: MouseEventHandler;
onClickPrevious: MouseEventHandler;
onClickSeeAll?: MouseEventHandler;
linkToSeeAll?: string;
canScroll: boolean;
className?: string;
buttonSize?: ButtonSize;
}

export const HorizontalScrollTitle = ({
Expand Down Expand Up @@ -50,10 +53,25 @@ export function HorizontalScrollHeader({
onClickSeeAll,
linkToSeeAll,
canScroll,
className,
buttonSize = ButtonSize.Medium,
}: HorizontalScrollHeaderProps): ReactElement {
// Check if title is props object or custom ReactNode
const isCustomTitle =
title && typeof title === 'object' && !('copy' in title);

return (
<div className="mx-4 flex min-h-10 w-auto flex-row items-center justify-between laptop:mx-0 laptop:w-full">
<HorizontalScrollTitle {...title} />
<div
className={classNames(
'mx-4 flex min-h-10 w-auto flex-row items-center justify-between laptop:mx-0 laptop:w-full',
className,
)}
>
{isCustomTitle
? title
: title && (
<HorizontalScrollTitle {...(title as HorizontalScrollTitleProps)} />
)}
{canScroll && (
<div className="hidden flex-row items-center gap-3 tablet:flex">
<Button
Expand All @@ -62,13 +80,15 @@ export function HorizontalScrollHeader({
disabled={isAtStart}
onClick={onClickPrevious}
aria-label="Scroll left"
size={buttonSize}
/>
<Button
variant={ButtonVariant.Tertiary}
icon={<ArrowIcon className="rotate-90" />}
disabled={isAtEnd}
onClick={onClickNext}
aria-label="Scroll right"
size={buttonSize}
/>
{(onClickSeeAll || linkToSeeAll) && (
<ConditionalWrapper
Expand Down
Loading