diff --git a/src/app/icon.tsx b/src/app/icon.tsx new file mode 100644 index 0000000..7488e88 --- /dev/null +++ b/src/app/icon.tsx @@ -0,0 +1,50 @@ +import { ImageResponse } from 'next/og'; + +export const size = { + height: 64, + width: 64, +}; + +export const contentType = 'image/jpeg'; + +/** + * The `Icon` function returns a `ImageResponse` object that represents the icon + * of the website. + */ +export default function Icon(): ImageResponse { + return new ImageResponse( +
+ + L + + + L + +
, + { + ...size, + }, + ); +} diff --git a/src/app/posts/(read-post)/[slug]/layout.tsx b/src/app/posts/(read-post)/[slug]/layout.tsx index 927e80a..0b86b32 100644 --- a/src/app/posts/(read-post)/[slug]/layout.tsx +++ b/src/app/posts/(read-post)/[slug]/layout.tsx @@ -6,7 +6,9 @@ interface LayoutProps { function Layout({ children }: LayoutProps): JSX.Element { return ( -
{children}
+
+ {children} +
); } diff --git a/src/app/projects/page.tsx b/src/app/projects/page.tsx new file mode 100644 index 0000000..13920ed --- /dev/null +++ b/src/app/projects/page.tsx @@ -0,0 +1,106 @@ +import { randomUUID } from 'node:crypto'; + +import type { JSX } from 'react'; + +import { StarIcon } from '@radix-ui/react-icons'; + +import Link from 'next/link'; +import Card from '~/components/card/card'; +import CardContent from '~/components/card/card-content'; +import CardTitle from '~/components/card/card-title'; +import GitHub from '~/constants/github'; +import env from '~/domains/environment/env'; +import createGitHubRepository from '~/domains/github/create-github-repository'; +import GitHubRepository from '~/domains/github/github-repository'; +import GitHubPinnedRepositoryModel from '~/domains/github/models/github-pinned-repository-model'; +import merge from '~/styles/merge'; + +const repo = createGitHubRepository( + env('VERCEL_ENV') === 'development' + ? class GitHubRepositoryStub extends GitHubRepository { + async fetchPinnedRepositories( + _username: string, + ): Promise { + return await Promise.resolve([ + new GitHubPinnedRepositoryModel( + randomUUID(), + 'planria', + 'A simple and minimalistic Scrum Planning platform for real Scrum teams', + 'https://github.com/mrlemoos/planria', + 1, + ), + new GitHubPinnedRepositoryModel( + randomUUID(), + 'louffee', + 'The way to connect students to housing abroad.', + 'https://github.com/louffee/louffee.co', + 1, + ), + ]); + } + } + : undefined, +); + +async function Page(): Promise { + const projects = await repo.fetchPinnedRepositories(GitHub.USERNAME); + + return ( +
+

Projects

+

+ Below are my open-source projects that I have worked on. Feel free to + check them out and contribute if you are interested. +

+
    + {projects.map(({ id, name, description, remoteURL, stars }) => { + const projectPathname = remoteURL.split('/').slice(-2).join('/'); + + return ( + +
  • + + + Go to the repository on GitHub + + + {name} + +

    {description}

    +
    + + + {stars} + + + {projectPathname} + +
    +
    +
  • +
    + ); + })} +
+
+ ); +} + +export default Page; diff --git a/src/components/card/card-content.tsx b/src/components/card/card-content.tsx index 9af68d0..c834a0c 100644 --- a/src/components/card/card-content.tsx +++ b/src/components/card/card-content.tsx @@ -4,10 +4,28 @@ import { Slot } from '@radix-ui/react-slot'; import merge from '~/styles/merge'; +/** + * The props for the `CardContent` component. + * + * This interface extends the `div` HTML element attributes. + */ export interface CardContentProps extends ComponentPropsWithoutRef<'div'> { + /** + * The `asChild` property is a boolean that determines whether the component + * will forward the props to the first slottable child. + * + * @default false + */ asChild?: boolean; } +/** + * The `CardContent` is a React component that composes the content of the card. + * + * This component is meant to be used with the `Card` component. + * + * @props {@link CardContentProps} + */ function CardContent({ children, className, diff --git a/src/components/card/card-datetime.tsx b/src/components/card/card-datetime.tsx index 835bd3d..2502cc4 100644 --- a/src/components/card/card-datetime.tsx +++ b/src/components/card/card-datetime.tsx @@ -4,6 +4,13 @@ import merge from '~/styles/merge'; export type CardDatetimeProps = ComponentPropsWithoutRef<'div'>; +/** + * The `CardDatetime` component composes the text for the card's datetime. + * + * This component is meant to be used with the `Card` component. + * + * @props {@link CardDatetime} + */ function CardDatetime({ children, className, diff --git a/src/components/card/card-title.tsx b/src/components/card/card-title.tsx index 6548dc2..8cab594 100644 --- a/src/components/card/card-title.tsx +++ b/src/components/card/card-title.tsx @@ -2,8 +2,20 @@ import type { ComponentPropsWithoutRef, JSX } from 'react'; import merge from '~/styles/merge'; +/** + * The props for the `CardTitle` component. + * + * This component extends the `h2` HTML element attributes. + */ export type CardTitleProps = ComponentPropsWithoutRef<'h2'>; +/** + * The `CardTitle` is a React component that composes the title of the card. + * + * This component is meant to be used with the `Card` component. + * + * @props {@link CardTitleProps} + */ function CardTitle({ children, className, diff --git a/src/components/card/card.tsx b/src/components/card/card.tsx index fe0a2d2..f929756 100644 --- a/src/components/card/card.tsx +++ b/src/components/card/card.tsx @@ -1,12 +1,48 @@ +'use client'; + import type { ComponentPropsWithoutRef, JSX } from 'react'; +import { Slot } from '@radix-ui/react-slot'; + import merge from '~/styles/merge'; -export type CardProps = ComponentPropsWithoutRef<'div'>; +/** + * The props for the `Card` component. + */ +export interface CardProps extends ComponentPropsWithoutRef<'div'> { + /** + * The `asChild` prop is a boolean flag that determines if the card will + * forward the properties to the first slottable element. + * + * @default false + */ + asChild?: boolean; +} + +/** + * The `Card` is a React component that composes the card layout, acting as a + * wrapper for the whole card. + * + * @example + * ```tsx + * + * Card Title + * Card Content + * + * ``` + * + * @props {@link CardProps} + */ +function Card({ + children, + className, + asChild = false, + ...props +}: CardProps): JSX.Element { + const RootElement = asChild ? Slot : 'article'; -function Card({ children, className, ...props }: CardProps): JSX.Element { return ( -
{children} -
+ ); } diff --git a/src/components/image/image.tsx b/src/components/image/image.tsx index c03d35f..ae27575 100644 --- a/src/components/image/image.tsx +++ b/src/components/image/image.tsx @@ -6,8 +6,17 @@ import merge from '~/styles/merge'; type Next$ImageProps = ComponentPropsWithoutRef; +/** + * The props for the `Image` component. + */ export interface ImageProps extends Next$ImageProps {} +/** + * The `Image` is a React component that composes the image element, applying + * the necessary styles. + * + * @props {@link ImageProps} + */ function Image({ src, alt, diff --git a/src/components/link-icon/link-icon.tsx b/src/components/link-icon/link-icon.tsx index 95be0a3..118eef3 100644 --- a/src/components/link-icon/link-icon.tsx +++ b/src/components/link-icon/link-icon.tsx @@ -9,11 +9,41 @@ import merge from '~/styles/merge'; type PickedIconProps = Pick; type PickedLinkProps = Omit; +/** + * The props for the `LinkIcon` component. + */ export interface LinkIconProps extends PickedLinkProps, PickedIconProps { + /** + * The children of the link icon component which should be an icon element. In + * fact, this property should only carry one child element which renders an + * element via the `Icon` component. + * + * @example + * ```tsx + * + * + * + * ``` + */ children: ReactElement; + /** + * The content of the tooltip element that wraps the link icon. If not + * provided, this property defaults to `null` and is not created in the DOM. + * + * See {@link TooltipProps.content | `TooltipProps.content`} for more + * information on the accepted kinds of values. + * + * @default null + */ tooltipContent?: TooltipProps['content']; } +/** + * The `LinkIcon` is a React component that composes the link element displaying + * an icon and optionally wrapped in a tooltip. + * + * @props {@link LinkProps} + */ function LinkIcon({ children, className, diff --git a/src/components/link/link.tsx b/src/components/link/link.tsx index f64d59b..400bf3d 100644 --- a/src/components/link/link.tsx +++ b/src/components/link/link.tsx @@ -6,12 +6,38 @@ import merge from '~/styles/merge'; type Next$LinkProps = ComponentPropsWithoutRef; +/** + * The `LinkVariant` is a string literal that determines the appearance of the + * link component. + */ export type LinkVariant = 'primary' | 'secondary' | 'opaque'; +/** + * The props for the `Link` component. + */ export interface LinkProps extends Next$LinkProps { + /** + * The `variant` property is a string literal which determines the appearance + * of the link. + * + * - `primary`: The primary variant is used for primary actions with a purple + * text/foreground colour. + * - `secondary`: The secondary variant is used for secondary actions with a + * black text/foreground colour. + * - `opaque`: The opaque variant is used for tertiary actions with a zinc + * text/foreground colour. + * + * @default 'primary' + */ variant?: LinkVariant; } +/** + * The `Link` is a React component that composes the link element, applying + * the necessary styles and defining variants. + * + * @props {@link LinkProps} + */ function Link({ children, className, diff --git a/src/components/logo/logo.tsx b/src/components/logo/logo.tsx new file mode 100644 index 0000000..7c08728 --- /dev/null +++ b/src/components/logo/logo.tsx @@ -0,0 +1,110 @@ +import type { HTMLAttributes, JSX } from 'react'; + +import merge from '~/styles/merge'; +import styled, { type VariantProps } from '~/styles/styled'; + +/** + * @internal The `createLogoStyles()` is a function that creates the styles for + * the `Logo` component. + */ +const createLogoStyles = styled( + merge( + 'bg-gradient-to-br rounded-lg w-8 h-8 shadow-2xl relative border', + 'from-zinc-50 to-zinc-200 text-black border-zinc-100', + 'dark:from-black dark:to-gray-900 dark:text-white dark:border-gray-800', + ), + { + variants: { + size: { + sm: 'w-6 h-6 text-base', + md: 'w-8 h-8 text-xl', + lg: 'w-12 h-12 text-2xl', + }, + backdrop: { + shadow: 'shadow-2xl', + none: '', + }, + }, + }, +); + +/** + * The `LogoProps` interface defines the props for the `Logo` component. + */ +export interface LogoProps + extends HTMLAttributes, + VariantProps { + /** + * @ignore + */ + children?: never; +} + +/** + * @internal The class names for HTML span element when the size is "sm". + */ +const CHAR_SIZE_SM_CLASS_NAME = 'data-[size="sm"]:font-normal' as const; +/** + * @internal The class names for HTML span element when the size is "md". + */ +const CHAR_SIZE_MD_CLASS_NAME = 'data-[size="md"]:font-medium' as const; +/** + * @internal The class names for HTML span element when the size is "lg". + */ +const CHAR_SIZE_LG_CLASS_NAME = 'data-[size="lg"]:font-semibold' as const; + +const charSizesClassName = [ + CHAR_SIZE_SM_CLASS_NAME, + CHAR_SIZE_MD_CLASS_NAME, + CHAR_SIZE_LG_CLASS_NAME, +].join(' '); + +/** + * The `Logo` is a React component which renders the logo of the website. + * + * @props {@link LogoProps} + */ +function Logo({ + className, + role = 'img', + 'aria-label': ariaLabel = 'Two letters L - one rotated 90 degrees and other 270 degrees', + size = 'md', + backdrop = 'shadow', + ...props +}: LogoProps): JSX.Element { + return ( +
+ + L + + + L + +
+ ); +} + +export default Logo; diff --git a/src/components/tooltip/tooltip.tsx b/src/components/tooltip/tooltip.tsx index 81b4c2e..068c4b0 100644 --- a/src/components/tooltip/tooltip.tsx +++ b/src/components/tooltip/tooltip.tsx @@ -13,38 +13,116 @@ import { type ElementRef, type JSX, forwardRef } from 'react'; import merge from '~/styles/merge'; +/** + * @internal + */ type PickedRadix$TooltipProviderProps = Pick< Radix$TooltipProviderProps, 'delayDuration' | 'disableHoverableContent' | 'skipDelayDuration' >; +/** + * @internal + */ +type PickedRadix$TooltipContentProps = Omit< + Radix$TooltipContentProps, + 'content' | 'align' | 'side' +>; + +/** + * @internal + */ +type Radix$TooltipContentAlignProp = NonNullable< + Radix$TooltipContentProps['align'] +>; + +/** + * @internal + */ +type Radix$TooltipContentSideProp = NonNullable< + Radix$TooltipContentProps['side'] +>; + +/** + * The `TooltipPosition` type is a string literal type that represents the + * position of the tooltip content relative to the trigger node. + */ +export type TooltipPosition = + `${Radix$TooltipContentSideProp}-${Radix$TooltipContentAlignProp}`; + +/** + * The `TooltipProps` interface defines the props for the `Tooltip` component. + */ export interface TooltipProps - extends Omit, + extends PickedRadix$TooltipContentProps, PickedRadix$TooltipProviderProps { + /** + * The `content` property is the JSX element or string (typography) which will + * be displayed inside the tooltip when the user hovers over the trigger node + * tree provided via the children of this component. + * + * This property may also be `null` if the tooltip should not be displayed. + */ content: JSX.Element | string | null; + /** + * The `isArrow` property determines whether the tooltip should have an arrow + * or not. + * + * @default false + */ isArrow?: boolean; + /** + * The `TooltipPosition` property determines the position of the tooltip + * relative to the trigger node. + * + * @see {@link TooltipPosition} + * + * @default 'top-center' + */ + position?: TooltipPosition; } +/** + * The `TooltipForwardedReferenceType` is the type of the forwarded reference + * passed down to the native HTML element. + */ export type TooltipForwardedReferenceType = ElementRef< typeof Radix$PrimitiveContent >; +/** + * The `Tooltip` is a React Client Component (RCC) that wraps the content with + * an floating tooltip that appears when the user hovers over the trigger. + * + * @props {@link TooltipProps} + */ const Tooltip = forwardRef( ( { children, className, - sideOffset = 4, + sideOffset = 2, content, delayDuration, disableHoverableContent, skipDelayDuration, isArrow = false, + position = 'top-center', ...props }, ref, ) => { - const isTriggerAsChild = typeof children !== 'string'; + const [side, align] = position.split('-') as [ + Radix$TooltipContentSideProp, + Radix$TooltipContentAlignProp, + ]; + const isTriggerAsChild = + // NOTE: This is a type guard to check if the children is not a string. + typeof children !== 'string'; + + if (content === null) { + return children; + } return ( ( {content} - {isArrow && } + + {isArrow && ( + + )} diff --git a/src/components/top-bar/top-bar-button.tsx b/src/components/top-bar/top-bar-button.tsx index 9170f23..7635a75 100644 --- a/src/components/top-bar/top-bar-button.tsx +++ b/src/components/top-bar/top-bar-button.tsx @@ -31,7 +31,7 @@ function TopBarButton({ href={href} target={target} className={merge( - 'relative block px-3 py-2 transition hover:text-purple-500 dark:hover:text-purple-400', + 'relative block px-3 py-2 transition hover:text-purple-900 hover:bg-purple-100', )} > {children} diff --git a/src/constants/current-year.ts b/src/constants/current-year.ts new file mode 100644 index 0000000..13928d7 --- /dev/null +++ b/src/constants/current-year.ts @@ -0,0 +1,3 @@ +const CURRENT_YEAR = new Date().getFullYear(); + +export default CURRENT_YEAR; diff --git a/src/constants/github.ts b/src/constants/github.ts index 87f7255..2dec8b2 100644 --- a/src/constants/github.ts +++ b/src/constants/github.ts @@ -1,7 +1,8 @@ const GitHub = { - PROFILE_URL: 'https://github.com/mrlemoos', - AVATAR_URL: 'https://avatars.githubusercontent.com/u/69330304?v=4', - REPOSITORY_URL: 'https://github.com/mrlemoos/mrlemoos.dev', -} as const + USERNAME: 'mrlemoos', + PROFILE_URL: 'https://github.com/mrlemoos', + AVATAR_URL: 'https://avatars.githubusercontent.com/u/69330304?v=4', + REPOSITORY_URL: 'https://github.com/mrlemoos/mrlemoos.dev', +} as const; -export default GitHub \ No newline at end of file +export default GitHub; diff --git a/src/domains/blog-post/components/copy-link-button.tsx b/src/domains/blog-post/components/copy-link-button.tsx index fe0821f..be3cc65 100644 --- a/src/domains/blog-post/components/copy-link-button.tsx +++ b/src/domains/blog-post/components/copy-link-button.tsx @@ -1,6 +1,6 @@ 'use client'; -import { type ButtonHTMLAttributes, Fragment, type JSX } from 'react'; +import type { ButtonHTMLAttributes, JSX } from 'react'; import Button from '~/components/button/button'; import useClipboardEventHandler from '~/hooks/use-clipboard-event-handler'; @@ -35,25 +35,25 @@ function CopyLinkButton({ const handleCopyLinkToClipboard = useClipboardEventHandler(); return ( - +
- +
); } diff --git a/src/domains/blog-post/read-post.tsx b/src/domains/blog-post/read-post.tsx index e370ab9..bcab2e5 100644 --- a/src/domains/blog-post/read-post.tsx +++ b/src/domains/blog-post/read-post.tsx @@ -14,6 +14,7 @@ import type BlogPostModel from '~/domains/blog-post/models/blog-post-model'; import env from '~/domains/environment/env'; import date from '~/util/date'; import getHttpProtocol from '~/util/get-http-protocol'; + import CopyLinkButton from './components/copy-link-button'; import createBlogPostPath from './util/create-blog-post-path'; @@ -144,8 +145,11 @@ function ReadPost({ DOCUMENT_TO_REACT_COMPONENTS_OPTIONS, )} -
- Like this post? Share it with your friends! 😃 +
+ + Like this post? +  Share it with your friends! 😃 + Copy URL
diff --git a/src/domains/environment/env.ts b/src/domains/environment/env.ts index 14781d3..8e48ca5 100644 --- a/src/domains/environment/env.ts +++ b/src/domains/environment/env.ts @@ -74,6 +74,11 @@ export interface EnvironmentVariables { * authenticate with the Contentful Content Delivery API. */ CONTENTFUL_ACCESS_TOKEN: string; + /** + * The `GITHUB_USER_PINNED_REPOSITORIES` is the username of the GitHub user + * whose pinned repositories will be fetched. + */ + GITHUB_USER_PINNED_REPOSITORIES: string; } /** diff --git a/src/domains/footer/footer.tsx b/src/domains/footer/footer.tsx index 1e3b8ee..82872c6 100644 --- a/src/domains/footer/footer.tsx +++ b/src/domains/footer/footer.tsx @@ -2,13 +2,12 @@ import type { JSX } from 'react'; import { GitHubLogoIcon, LinkedInLogoIcon } from '@radix-ui/react-icons'; +import LinkIcon from '~/components/link-icon/link-icon'; +import WildcardFooter from '~/components/wildcard-footer/wildcard-footer'; import Author from '~/constants/author'; -import LinkedIn from '~/constants/linked-in'; +import CURRENT_YEAR from '~/constants/current-year'; import GitHub from '~/constants/github'; -import WildcardFooter from '~/components/wildcard-footer/wildcard-footer'; -import LinkIcon from '~/components/link-icon/link-icon'; - -const CURRENT_YEAR = new Date().getFullYear(); +import LinkedIn from '~/constants/linked-in'; /** * The `Footer` is a React component that composes the footer section of the diff --git a/src/domains/github/_/graphql-github-repository.ts b/src/domains/github/_/graphql-github-repository.ts new file mode 100644 index 0000000..e0e63cc --- /dev/null +++ b/src/domains/github/_/graphql-github-repository.ts @@ -0,0 +1,112 @@ +import { + ApolloClient, + InMemoryCache, + createHttpLink, + gql, +} from '@apollo/client'; +import { setContext } from '@apollo/client/link/context'; + +import GitHubRepository from '../github-repository'; +import GitHubPinnedRepositoryModel from '../models/github-pinned-repository-model'; + +/** + * @internal The `PartialGitHubUser` is a partial and selective representation + * of a GitHub user. + */ +interface PartialGitHubUser { + pinnedItems: { + totalCount: number; + edges: { + node: { + name: string; + description: string; + id: string; + url: string; + stargazers: { + totalCount: number; + }; + }; + }[]; + }; +} + +/** + * The `GraphQLGitHubRepository` class is an implementation of the + * `GitHubRepository` interface that uses the Apollo Client to fetch pinned + * repositories from the GitHub GraphQL API. + */ +export default class GraphQLGitHubRepository extends GitHubRepository { + private readonly HTTP_LINK = createHttpLink({ + uri: 'https://api.github.com/graphql', + }); + private readonly AUTH_CONTEXT = setContext((_, { headers }) => { + return { + headers: { + ...headers, + authorization: `Bearer ${process.env.GITHUB_ACCESS_TOKEN}`, + }, + }; + }); + + private readonly CLIENT = new ApolloClient({ + link: this.AUTH_CONTEXT.concat(this.HTTP_LINK), + cache: new InMemoryCache(), + }); + + /** + * @inheritdoc + */ + async fetchPinnedRepositories( + username: string, + limit = 6, + ): Promise { + try { + const { data } = await this.CLIENT.query<{ user: PartialGitHubUser }>({ + query: gql` + { + user(login: "${username}") { + pinnedItems(first: ${limit}, types: [REPOSITORY]) { + totalCount + edges { + node { + ... on Repository { + name + description + id + url + stargazers { + totalCount + } + } + } + } + } + } + } + `, + }); + + if (!data.user) { + return []; + } + + const pinnedItems = data.user.pinnedItems.edges.map( + ({ node: { id, name, description, stargazers, url } }) => + new GitHubPinnedRepositoryModel( + id, + name, + description, + url, + stargazers.totalCount, + ), + ); + + return pinnedItems; + } catch (error) { + console.error( + `ERROR: An error occurred while fetching pinned repositories: ${error}`, + ); + } + return []; + } +} diff --git a/src/domains/github/create-github-repository.ts b/src/domains/github/create-github-repository.ts new file mode 100644 index 0000000..34721eb --- /dev/null +++ b/src/domains/github/create-github-repository.ts @@ -0,0 +1,12 @@ +import GraphQLGitHubRepository from './_/graphql-github-repository'; +import type GitHubRepository from './github-repository'; + +/** + * The `createGitHubRepository()` function creates and returns a new instance of + * the `GitHubRepository` base class. + */ +export default function createGitHubRepository( + GitHubRepositoryImpl: new () => GitHubRepository = GraphQLGitHubRepository, +): GitHubRepository { + return new GitHubRepositoryImpl(); +} diff --git a/src/domains/github/github-repository.ts b/src/domains/github/github-repository.ts new file mode 100644 index 0000000..39db30c --- /dev/null +++ b/src/domains/github/github-repository.ts @@ -0,0 +1,22 @@ +import type GitHubPinnedRepositoryModel from './models/github-pinned-repository-model'; + +export default abstract class GitHubRepository { + /** + * The `fetchPinnedRepositories()` method fetches a list of pinned + * repositories from GitHub for a given username. + */ + abstract fetchPinnedRepositories( + /** + * The user name of the GitHub user to fetch pinned repositories for. + */ + username: string, + /** + * The maximum number of pinned repositories to fetch. + * + * If not provided, this argument defaults to `6`. + * + * @default 6 + */ + limit?: number, + ): Promise; +} diff --git a/src/domains/github/models/github-pinned-repository-model.ts b/src/domains/github/models/github-pinned-repository-model.ts new file mode 100644 index 0000000..edde5a7 --- /dev/null +++ b/src/domains/github/models/github-pinned-repository-model.ts @@ -0,0 +1,28 @@ +/** + * The `GitHubPinnedRepository` class represents a pinned repository from + * GitHub. + */ +export default class GitHubPinnedRepositoryModel { + constructor( + /** + * The identifier of the repository. + */ + public readonly id: string, + /** + * The name of the repository from GitHub. + */ + public readonly name: string, + /** + * The description of the repository from GitHub. + */ + public readonly description: string, + /** + * The URL of the repository hosted on GitHub. + */ + public readonly remoteURL: string, + /** + * The number of stars the repository has. + */ + public readonly stars: number, + ) {} +} diff --git a/src/domains/header/header.tsx b/src/domains/header/header.tsx index e30ea98..1bf72a3 100644 --- a/src/domains/header/header.tsx +++ b/src/domains/header/header.tsx @@ -1,5 +1,8 @@ import type { JSX } from 'react'; +import Link from '~/components/link/link'; +import Logo from '~/components/logo/logo'; +import Tooltip from '~/components/tooltip/tooltip'; import TopBar from '~/components/top-bar/top-bar'; import TopBarButton from '~/components/top-bar/top-bar-button'; import TopBarNavigation from '~/components/top-bar/top-bar-navigation'; @@ -12,7 +15,21 @@ import TopCenterCorner from '~/components/top-center-corner/top-center-corner'; function Header(): JSX.Element { return ( - + + + + + + About diff --git a/src/domains/my-experience/components/work-experience-tile.tsx b/src/domains/my-experience/components/work-experience-tile.tsx index 46bb10c..9559031 100644 --- a/src/domains/my-experience/components/work-experience-tile.tsx +++ b/src/domains/my-experience/components/work-experience-tile.tsx @@ -2,8 +2,16 @@ import type { JSX } from 'react'; import Image from 'next/image'; +import CURRENT_YEAR from '~/constants/current-year'; + +/** + * The `WorkExperienceYear` type is a string representation of a year. + */ export type WorkExperienceYear = `${number}`; +/** + * The props for the `WorkExperienceTile` component. + */ export interface WorkExperienceTileProps { imageURL: string; companyName: string; @@ -12,8 +20,12 @@ export interface WorkExperienceTileProps { endYear: WorkExperienceYear | 'Present'; } -const CURRENT_YEAR = `${new Date().getFullYear()}`; - +/** + * The `WorkExperienceTile` is a React component that displays a single work + * experience entry. + * + * @props {@link WorkExperienceTileProps} + */ function WorkExperienceTile({ imageURL, companyName, @@ -50,7 +62,9 @@ function WorkExperienceTile({ > -