Skip to content

Commit

Permalink
Merge pull request #230 from mobeigi/add-development-activity-section…
Browse files Browse the repository at this point in the history
…-homepage

Add development activity section to homepage
  • Loading branch information
mobeigi authored Feb 2, 2025
2 parents 1de069c + 7d0da02 commit 834526f
Show file tree
Hide file tree
Showing 29 changed files with 450 additions and 43 deletions.
3 changes: 3 additions & 0 deletions app/.env.local.example
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ NEXT_PUBLIC_GA_TAG_ID="G-XYZ"
AKISMET_API_KEY=""
EMAIL_HASH_SALT=""

GITHUB_USERNAME="mobeigi"
GITHUB_PERSONAL_ACCESS_KEY=""

NTFY_HOST="https://ntfy.example.com"
NTFY_USER="user"
NTFY_PASS="password"
Expand Down
3 changes: 3 additions & 0 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"dependencies": {
"@cedx/akismet": "^17.0.1",
"@next/third-parties": "15.1.3",
"@octokit/graphql": "^8.2.0",
"@octokit/rest": "^21.1.0",
"@payloadcms/db-postgres": "3.11.0",
"@payloadcms/email-nodemailer": "3.11.0",
Expand All @@ -41,6 +42,8 @@
"crawler-user-agents": "^1.0.158",
"cross-env": "^7.0.3",
"date-fns": "^4.1.0",
"echarts": "^5.6.0",
"echarts-for-react": "^3.0.2",
"file-saver": "^2.0.5",
"graphql": "^16.10.0",
"highlight.js": "^11.11.1",
Expand Down
4 changes: 2 additions & 2 deletions app/src/app/(frontend)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { Header, Main, MainContents, Footer } from './styled';
import ThemedToastContainer from '@/components/ThemedToastContainer';
import { GoogleAnalytics } from '@next/third-parties/google';
import { GA_TAG_ID } from '@/constants/analytics';
import GlobalTooltip from '@/containers/GlobalTooltip';
import GlobalTooltip from '@/components/BaseTooltip';
import ConsoleWelcomeMessage from '@/components/ConsoleWelcomeMessage';
import HolyLoader from 'holy-loader';
import { ThemeProvider } from 'next-themes';
Expand Down Expand Up @@ -76,7 +76,7 @@ const RootLayout = ({
<StyledComponentsRegistry>
<ThemeProvider enableSystem={true} defaultTheme="system">
<GlobalStyle />
<GlobalTooltip />
<GlobalTooltip id="global-tooltip" />
<ThemedToastContainer />
<HolyLoader color={holyLoaderColor} height="0.2rem" />
<ConsoleWelcomeMessage />
Expand Down
10 changes: 8 additions & 2 deletions app/src/app/(frontend)/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import HomePage from '@/containers/HomePage';
import { getCachedLatestPhotographyImages } from '@/utils/photography';
import { getPayload } from 'payload';
import config from '@payload-config';
import { mapPostToPostMeta } from '@/utils/payload';
import { BlogPostMeta, BlogPostRelatedMeta } from '@/types/blog';
import { sortBlogPostMetaByPublishedAtDate } from '@/utils/blog/post';
import { generateBreadcrumbs } from './breadcrumbs';
import { getCachedLatestPhotographyImages } from '@/utils/photography';
import { getCachedLatestDevelopmentActivity } from '@/utils/github';

export const revalidate = 900;

Expand Down Expand Up @@ -53,6 +54,7 @@ const Home = async () => {
.sort(sortBlogPostMetaByPublishedAtDate);

const breadcrumbs = generateBreadcrumbs();
const latestDevelopmentActivity = (await getCachedLatestDevelopmentActivity()) ?? undefined;
const latestPhotographyImages = (await getCachedLatestPhotographyImages()) ?? undefined;

return (
Expand All @@ -62,7 +64,11 @@ const Home = async () => {
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbs) }} />
</>
)}
<HomePage latestBlogPostMetas={blogPostMetas} latestPhotographyImages={latestPhotographyImages} />
<HomePage
latestBlogPostMetas={blogPostMetas}
latestDevelopmentActivity={latestDevelopmentActivity}
latestPhotographyImages={latestPhotographyImages}
/>
</div>
);
};
Expand Down
2 changes: 1 addition & 1 deletion app/src/app/sitemap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ const sitemap = async (): Promise<MetadataRoute.Sitemap> => {
*/
{
url: BASE_URL,
lastModified: new Date('2024-09-26T04:30:00+0000'),
lastModified: new Date('2025-02-02T04:21:00+0000'),
changeFrequency: 'daily',
priority: 1.0,
},
Expand Down
19 changes: 19 additions & 0 deletions app/src/components/BaseTooltip/BaseTooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
'use client';

import { useTheme } from 'next-themes';
import { BaseTooltipWrapper, StyledTooltip } from './styled';
import { resolvedThemeToThemeMode } from '@/utils/theme';
import { BaseTooltipProps } from './types';

// Provides a base tooltip that has been styled.
// It is best practice to store one copy of this tooltip and reuse it for performance and to avoid various DOM render bugs
export const BaseTooltip = ({ id, isOpen }: BaseTooltipProps) => {
const { resolvedTheme } = useTheme();
const resolvedThemeMode = resolvedThemeToThemeMode(resolvedTheme);

return (
<BaseTooltipWrapper>
<StyledTooltip id={id} place="bottom" opacity={1.0} className={`rt-theme-${resolvedThemeMode}`} isOpen={isOpen} />
</BaseTooltipWrapper>
);
};
1 change: 1 addition & 0 deletions app/src/components/BaseTooltip/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { BaseTooltip as default } from './BaseTooltip';
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { styled } from 'styled-components';
import { Tooltip } from 'react-tooltip';
import zIndex from '@/styles/zindex';

export const GlobalTooltipWrapper = styled.div`
export const BaseTooltipWrapper = styled.div`
// Set desired z-index
z-index: ${zIndex.tooltip};
Expand Down
4 changes: 4 additions & 0 deletions app/src/components/BaseTooltip/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface BaseTooltipProps {
id: string;
isOpen?: boolean;
}
8 changes: 4 additions & 4 deletions app/src/components/BlogSummary/BlogSummary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export const BlogSummary = ({
<Heading as={headingLevel}>{blogPostMeta.post.title}</Heading>
)}
<DetailContainer>
<Detail data-tooltip-id="base-tooltip" data-tooltip-content="Date published">
<Detail data-tooltip-id="global-tooltip" data-tooltip-content="Date published">
<IconAndTextContainer>
<IconWrapper>
<CalendarSvg />
Expand All @@ -63,22 +63,22 @@ export const BlogSummary = ({
</time>
</IconAndTextContainer>
</Detail>
<Detail data-tooltip-id="base-tooltip" data-tooltip-content="Category">
<Detail data-tooltip-id="global-tooltip" data-tooltip-content="Category">
{linkCategory ? (
<StyledLink href={blogPostMeta.post.category.url}>{categoryElement}</StyledLink>
) : (
categoryElement
)}
</Detail>
<Detail data-tooltip-id="base-tooltip" data-tooltip-content="Views">
<Detail data-tooltip-id="global-tooltip" data-tooltip-content="Views">
<IconAndTextContainer>
<IconWrapper>
<BarChartSvg />
</IconWrapper>
<span>{blogPostMeta.post.views}</span>
</IconAndTextContainer>
</Detail>
<Detail data-tooltip-id="base-tooltip" data-tooltip-content="Comments">
<Detail data-tooltip-id="global-tooltip" data-tooltip-content="Comments">
{commentsAnchor ? (
<StyledLink href={`${blogPostMeta.post.url}#${commentsAnchor}`}>{commentCountElement}</StyledLink>
) : (
Expand Down
170 changes: 170 additions & 0 deletions app/src/components/GitContributionGraph/GitContributionGraph.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
'use client';

import React, { useState, useRef, useCallback, useEffect } from 'react';
import { useTheme } from 'next-themes';
import { ThemeMode } from '@/types/theme';
import { resolvedThemeToThemeMode } from '@/utils/theme';
import { format as formatDate } from 'date-fns';
import BaseTooltip from '../BaseTooltip';
import { GitContributionGraphProps } from './types';
import ClipLoader from 'react-spinners/ClipLoader';
import { GitContributionGraphContainer, SpinnerWrapper } from './styled';

import * as echarts from 'echarts/core';
import type { CallbackDataParams } from 'echarts/types/src/util/types.js';
import ReactEChartsCore from 'echarts-for-react/lib/core';
import { HeatmapChart } from 'echarts/charts';
import { TooltipComponent, VisualMapComponent, CalendarComponent } from 'echarts/components';
import { SVGRenderer } from 'echarts/renderers';

echarts.use([HeatmapChart, TooltipComponent, VisualMapComponent, CalendarComponent, SVGRenderer]);

const tooltipId = 'git-contribution-graph-tooltip';

export const GitContributionGraph = ({ data }: GitContributionGraphProps) => {
const [isLoaded, setIsLoaded] = useState(false);
const chartRef = useRef<ReactEChartsCore>(null);
const tooltipRef = useRef<HTMLDivElement | null>(null);
const [tooltipContent, setTooltipContent] = useState<string | null>(null);
const [isTooltipOpen, setIsTooltipOpen] = useState(false);

const { resolvedTheme } = useTheme();
const resolvedThemeMode = resolvedThemeToThemeMode(resolvedTheme);

useEffect(() => {
setIsLoaded(true);
}, []);

// Set start/end dates based on incoming data
const startDate = data[0][0];
const endDate = data[data.length - 1][0];

const option: echarts.EChartsCoreOption = {
backgroundColor: 'transparent', // avoid darkMode theme background colour
tooltip: { show: false },
visualMap: {
show: false,
min: 0,
max: 20,
inRange: {
color:
resolvedThemeMode === ThemeMode.Dark
? ['#3a2e2f', '#5e2f33', '#84373a', '#af3a3d', '#fb4d56']
: ['#ffe5e7', '#ffb7bb', '#ff8a91', '#ff5c66', '#fb4d56'],
},
type: 'piecewise',
splitNumber: 5,
pieces: [{ min: 20 }, { min: 10, max: 20 }, { min: 3, max: 10 }, { min: 1, max: 3 }, { value: 0 }],
},
calendar: {
top: 20,
left: 50,
right: 5,
cellSize: [20],
range: [startDate, endDate],
itemStyle: {
borderWidth: 0,
color: 'transparent',
},
dayLabel: {
nameMap: ['', 'Mon', '', 'Wed', '', 'Fri', ''],
},
yearLabel: { show: false },
splitLine: { show: false },
},
series: {
type: 'heatmap',
coordinateSystem: 'calendar',
itemStyle: {
borderRadius: 6,
borderWidth: 2,
borderColor: 'var(--theme-background)', // should match background colour of the chart container
},
data: data,
},
};

/**
* We rely on this function to update the tooltip position and content, triggered by the mouseover event.
*/
const updateTooltip = useCallback((params: CallbackDataParams) => {
if (!params.value || !chartRef.current) {
return;
}

const chart = chartRef.current.getEchartsInstance();
if (!chart) {
return;
}

const [x, y] = chart.convertToPixel({ seriesIndex: 0 }, params.value as number[]);

// Get the bounding box of the ECharts container
const echartsContainerRect = chart.getDom().getBoundingClientRect();

// Adjust to obtain absolute page coordinates
const absoluteX = window.scrollX + echartsContainerRect.left + x;
const absoluteY = window.scrollY + echartsContainerRect.top + y;

if (tooltipRef.current) {
tooltipRef.current.style.left = `${absoluteX}px`;
tooltipRef.current.style.top = `${absoluteY}px`;
}

const [date, contributionCount] = params.value as [string, number];
const dateString = formatDate(date, 'd MMMM yyyy');

setTooltipContent(
contributionCount > 0
? `${contributionCount} contributions on ${dateString}`
: `No contributions on ${dateString}`,
);
setIsTooltipOpen(true);
}, []);

const hideTooltip = useCallback(() => {
setIsTooltipOpen(false);
}, []);

const onEvents = {
mouseover: updateTooltip,
mouseout: hideTooltip,
};

if (!isLoaded) {
return (
<GitContributionGraphContainer>
<SpinnerWrapper>
<ClipLoader size={'2em'} color="var(--theme-text-subtle)" />
</SpinnerWrapper>
</GitContributionGraphContainer>
);
}

return (
<GitContributionGraphContainer>
<ReactEChartsCore
ref={chartRef}
echarts={echarts}
option={option}
theme={resolvedThemeMode === ThemeMode.Dark ? 'dark' : undefined}
onEvents={onEvents}
style={{ width: '100%', height: '165px' }}
/>

<BaseTooltip id={tooltipId} isOpen={isTooltipOpen} />
<span
ref={tooltipRef}
data-tooltip-id={tooltipId}
data-tooltip-content={tooltipContent || ''}
style={{
position: 'absolute',
// Initially offscreen
left: '-9999px',
top: '-9999px',
pointerEvents: 'none',
}}
/>
</GitContributionGraphContainer>
);
};
1 change: 1 addition & 0 deletions app/src/components/GitContributionGraph/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { GitContributionGraph as default } from './GitContributionGraph';
16 changes: 16 additions & 0 deletions app/src/components/GitContributionGraph/styled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
'use client';

import styled from 'styled-components';

export const GitContributionGraphContainer = styled.div`
display: flex;
width: 100%;
height: 165px;
`;

export const SpinnerWrapper = styled.div`
display: flex;
width: 100%;
justify-content: center;
align-items: center;
`;
8 changes: 8 additions & 0 deletions app/src/components/GitContributionGraph/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export interface GitContributionGraphProps {
data: HeatmapEntry[];
}

export type HeatmapEntry = [
string, // Date in 'YYYY-MM-DD' format
number, // Contribution count
];
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export const DarkModeSwitchContainer = () => {
const tooltipHtml = useMemo(() => renderToStaticMarkup(tooltipElement), [tooltipElement]);

return (
<IconWrapperBubble onClick={triggerClick} data-tooltip-id="base-tooltip" data-tooltip-html={tooltipHtml}>
<IconWrapperBubble onClick={triggerClick} data-tooltip-id="global-tooltip" data-tooltip-html={tooltipHtml}>
<DarkModeSwitchWrapper>
<DarkModeSwitch
ref={darkModeSwitchRef}
Expand Down
4 changes: 2 additions & 2 deletions app/src/components/PersonShowcase/PersonShowcase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,15 @@ export const PersonShowcase = ({
<StyledHeading as={headingLevel}>{SITE_TITLE}</StyledHeading>
<Tagline>{TAGLINE}</Tagline>
<InfoArea>
<Info data-tooltip-id="base-tooltip" data-tooltip-content="Location">
<Info data-tooltip-id="global-tooltip" data-tooltip-content="Location">
<IconAndTextContainer>
<IconWrapper>
<MapPinSvg />
</IconWrapper>
<span>Sydney, Australia</span>
</IconAndTextContainer>
</Info>
<Info data-tooltip-id="base-tooltip" data-tooltip-content="Languages spoken">
<Info data-tooltip-id="global-tooltip" data-tooltip-content="Languages spoken">
<IconAndTextContainer>
<IconWrapper>
<LanguageOutlineSvg />
Expand Down
Loading

0 comments on commit 834526f

Please sign in to comment.