Skip to content

Commit

Permalink
feat: create the OSS projects page
Browse files Browse the repository at this point in the history
  • Loading branch information
mrlemoos committed Mar 25, 2024
1 parent 6e2373c commit dd3c023
Show file tree
Hide file tree
Showing 27 changed files with 788 additions and 58 deletions.
50 changes: 50 additions & 0 deletions src/app/icon.tsx
Original file line number Diff line number Diff line change
@@ -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(
<div
style={{
fontSize: 48,
fontWeight: 900,
background: 'black',
color: 'white',
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 18,
position: 'relative',
}}
>
<span
style={{ transform: 'rotate(90deg)', position: 'absolute', top: -8 }}
>
L
</span>
<span
style={{
transform: 'rotate(-90deg)',
position: 'absolute',
bottom: -8,
}}
>
L
</span>
</div>,
{
...size,
},
);
}
4 changes: 3 additions & 1 deletion src/app/posts/(read-post)/[slug]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ interface LayoutProps {

function Layout({ children }: LayoutProps): JSX.Element {
return (
<main className='min-h-screen p-24 flex justify-center'>{children}</main>
<main className='min-h-screen p-3 sm:p-8 md:p-16 lg:p-24 flex justify-center'>
{children}
</main>
);
}

Expand Down
106 changes: 106 additions & 0 deletions src/app/projects/page.tsx
Original file line number Diff line number Diff line change
@@ -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<GitHubPinnedRepositoryModel[]> {
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<JSX.Element> {
const projects = await repo.fetchPinnedRepositories(GitHub.USERNAME);

return (
<main className='max-w-2xl pt-16 md:pt-24 pb-10 mx-2 md:mx-auto min-h-screen'>
<h1 className='text-2xl font-semibold'>Projects</h1>
<p className='my-5 text-zinc-500'>
Below are my open-source projects that I have worked on. Feel free to
check them out and contribute if you are interested.
</p>
<ul
className={merge(
'flex flex-col',
'[&_li]:list-none [&_li]:rounded-none',
'rounded-t-xl rounded-b-xl overflow-hidden',
'[&_li]:first:border-t [&_li]:last:border-b',
'border border-zinc-300 dark:border-zinc-700',
)}
>
{projects.map(({ id, name, description, remoteURL, stars }) => {
const projectPathname = remoteURL.split('/').slice(-2).join('/');

return (
<Card asChild={true} key={id}>
<li className='relative cursor-pointer'>
<Link
href={remoteURL}
target='_blank'
className='absolute inset-0 z-10'
aria-label='Click to get redirected to the repository on GitHub'
>
<span className='sr-only'>
Go to the repository on GitHub
</span>
</Link>
<CardTitle>{name}</CardTitle>
<CardContent>
<p className='mb-3'>{description}</p>
<div className='flex justify-between items-center'>
<span className='flex items-center gap-x-1'>
<StarIcon
height={18}
width={18}
className='fill-yellow-400 dark:fill-yellow-100'
/>
{stars}
</span>
<span className='text-purple-500 dark:text-purple-400'>
{projectPathname}
</span>
</div>
</CardContent>
</li>
</Card>
);
})}
</ul>
</main>
);
}

export default Page;
18 changes: 18 additions & 0 deletions src/components/card/card-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
7 changes: 7 additions & 0 deletions src/components/card/card-datetime.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
12 changes: 12 additions & 0 deletions src/components/card/card-title.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
44 changes: 40 additions & 4 deletions src/components/card/card.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,56 @@
'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>
* <CardTitle>Card Title</CardTitle>
* <CardContent>Card Content</CardContent>
* </Card>
* ```
*
* @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 (
<article
<RootElement
{...props}
className={merge(
'flex flex-col gap-3 p-5 rounded-xl bg-transparent dark:hover:bg-zinc-900 hover:bg-zinc-100',
className,
)}
>
{children}
</article>
</RootElement>
);
}

Expand Down
9 changes: 9 additions & 0 deletions src/components/image/image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,17 @@ import merge from '~/styles/merge';

type Next$ImageProps = ComponentPropsWithoutRef<typeof Next$Image>;

/**
* 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,
Expand Down
30 changes: 30 additions & 0 deletions src/components/link-icon/link-icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,41 @@ import merge from '~/styles/merge';
type PickedIconProps = Pick<IconProps, 'height' | 'width'>;
type PickedLinkProps = Omit<LinkProps, 'variant'>;

/**
* 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
* <LinkIcon>
* <ArrowRightIcon />
* </Link>
* ```
*/
children: ReactElement<IconProps>;
/**
* 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,
Expand Down
26 changes: 26 additions & 0 deletions src/components/link/link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,38 @@ import merge from '~/styles/merge';

type Next$LinkProps = ComponentPropsWithoutRef<typeof Next$Link>;

/**
* 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,
Expand Down
Loading

0 comments on commit dd3c023

Please sign in to comment.