Skip to content

Commit

Permalink
Add new career feature implementation (#682)
Browse files Browse the repository at this point in the history
  • Loading branch information
artyom-88 authored Nov 20, 2023
1 parent 5dbba68 commit bf2a3b7
Show file tree
Hide file tree
Showing 41 changed files with 457 additions and 199 deletions.
1 change: 1 addition & 0 deletions src/assets/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

a {
color: $purpleColor;
cursor: pointer;
text-decoration: none;

&:hover {
Expand Down
4 changes: 2 additions & 2 deletions src/assets/variables.scss
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
$footerHeight: 2vh;
$headerHeight: 5vh;
$padding-lg: 5em;
$padding-md: 2.5em;
$padding-lg: 10em;
$padding-md: 5em;
$padding-sm: 1em;
4 changes: 2 additions & 2 deletions src/client/app/http-client.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import type { KyInstance } from 'ky-universal';
import ky from 'ky-universal';
import { PORT } from 'src/common/common-constants';
import { API_URL } from 'src/common/common-constants';

export const httpClient: KyInstance = ky.create({
headers: {
'Content-Type': 'application/json',
},
prefixUrl: `http://localhost:${PORT}/api`,
prefixUrl: API_URL,
retry: 0,
});

Expand Down
4 changes: 2 additions & 2 deletions src/client/features/blog/BlogList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import BlogListItem from './BlogListItem';
import { useBlogListQuery } from './hooks/use-blog-list-query';

const BlogList = (): JSX.Element => {
const { data: blogList = [], error, isLoading } = useBlogListQuery();
const { data: blogList = [], error, isFetching } = useBlogListQuery();
return (
<div className='ag-flexbox ag-flexColumn'>
{error && <div>Failed to load</div>}
{isLoading ? <div>Loading...</div> : blogList.map((item) => <BlogListItem key={item._id} item={item} />)}
{isFetching ? <div>Loading...</div> : blogList.map((item) => <BlogListItem key={item._id} item={item} />)}
</div>
);
};
Expand Down
7 changes: 4 additions & 3 deletions src/client/features/blog/BlogListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,21 @@ import type { JSX } from 'react';

import { Button } from '@mui/base';
import Link from 'next/link';
import { BLOG_PAGE_URL } from 'src/common/common-constants';
import { dayjs } from 'src/common/common-date';

import { BlogListItemProps } from './blog-types';
import styles from './BlogListItem.module.scss';

const BlogListItem = ({ item }: BlogListItemProps): JSX.Element => {
const { _id: blogId, date, title, link, linkCaption } = item;
const { _id: itemId, date, title, link, linkCaption } = item;
return (
<div className={`ag-flexbox ag-justifyContent-between ${styles.container}`} key={blogId}>
<div className={`ag-flexbox ag-justifyContent-between ${styles.container}`} key={itemId}>
<div>{dayjs(date).utc().format('MMMM DD, YYYY')}</div>
<div>{title}</div>
<div>{link}</div>
<div>{linkCaption}</div>
<Link href={`/blog/${blogId}`}>
<Link href={`${BLOG_PAGE_URL}/${itemId}`}>
<Button>Open</Button>
</Link>
</div>
Expand Down
25 changes: 12 additions & 13 deletions src/client/features/blog/blog-api.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
import httpClient from 'src/client/app/http-client';
import type { IBlog, IBlogDTO } from 'src/common/types/common-blog-types';
import type { BlogDTO, BlogModel } from 'src/common/types/common-blog-types';

export const blogListRequest = async (): Promise<IBlog[]> => {
const blogListDTO = await httpClient.get('blog').json<IBlogDTO[]>();
return blogListDTO.map((item) => ({
...item,
date: new Date(item.date),
}));
const blogItemAdapter = (dto: BlogDTO): BlogModel => ({
...dto,
date: new Date(dto.date),
});

export const blogListRequest = async (): Promise<BlogModel[]> => {
const dto = await httpClient.get('blog').json<BlogDTO[]>();
return dto.map(blogItemAdapter);
};

export const blogRequest = async (id: string): Promise<IBlog> => {
const blogDTO = await httpClient.get(`blog/${id}`).json<IBlogDTO>();
return {
...blogDTO,
date: new Date(blogDTO.date),
};
export const blogItemRequest = async (id: string): Promise<BlogModel> => {
const dto = await httpClient.get(`blog/${id}`).json<BlogDTO>();
return blogItemAdapter(dto);
};
4 changes: 2 additions & 2 deletions src/client/features/blog/blog-types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { IBlog } from 'src/common/types/common-blog-types';
import { BlogModel } from 'src/common/types/common-blog-types';

export interface BlogListItemProps {
item: IBlog;
item: BlogModel;
}
12 changes: 8 additions & 4 deletions src/client/features/blog/hooks/use-blog-list-query.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import type { UseQueryOptions, UseQueryResult } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';

import { IBlog } from '../../../../common/types/common-blog-types';
import { BlogModel } from '../../../../common/types/common-blog-types';
import { blogListRequest } from '../blog-api';
import { BLOG_LIST_QUERY_KEY } from '../blog-constants';

export const useBlogListQuery = (
{ refetchOnMount = false, enabled = false, ...restProps }: Omit<UseQueryOptions<IBlog[]>, 'queryKey' | 'queryFn'> = {
{
refetchOnMount = false,
enabled = false,
...restProps
}: Omit<UseQueryOptions<BlogModel[]>, 'queryKey' | 'queryFn'> = {
enabled: false,
refetchOnMount: false,
}
): UseQueryResult<IBlog[]> =>
useQuery<IBlog[]>({
): UseQueryResult<BlogModel[]> =>
useQuery<BlogModel[]>({
...restProps,
enabled: enabled,
queryFn: blogListRequest,
Expand Down
16 changes: 16 additions & 0 deletions src/client/features/career/CareerList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { JSX } from 'react';

import CareerListItem from './CareerListItem';
import { useCareerListQuery } from './hooks/use-career-list-query';

const CareerList = (): JSX.Element => {
const { data: careerList = [], error, isFetching } = useCareerListQuery();
return (
<div className='ag-flexbox ag-flexColumn'>
{error && <div>Failed to load</div>}
{isFetching ? <div>Loading...</div> : careerList.map((item) => <CareerListItem key={item._id} item={item} />)}
</div>
);
};

export default CareerList;
5 changes: 5 additions & 0 deletions src/client/features/career/CareerListItem.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@import 'src/assets/variables';

.container {
padding: $padding-sm 0;
}
31 changes: 31 additions & 0 deletions src/client/features/career/CareerListItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { JSX } from 'react';

import { Button } from '@mui/base';
import Link from 'next/link';
import { CAREER_PAGE_URL } from 'src/common/common-constants';
import { dayjs } from 'src/common/common-date';

import { CareerListItemProps } from './career-types';
import styles from './CareerListItem.module.scss';

const CareerListItem = ({ item }: CareerListItemProps): JSX.Element => {
const { _id: itemId, endDate, description, post, site, startDate, title, tools } = item;
return (
<div className={`ag-flexbox ag-justifyContent-between ${styles.container}`} key={itemId}>
<div>{title}</div>
<div>
<span>{dayjs(startDate).utc().format('MMMM DD, YYYY')}</span>
{endDate ? <span> - {dayjs(endDate).utc().format('MMMM DD, YYYY')}</span> : null}
</div>
<div>{post}</div>
<div>{site}</div>
<div>{description}</div>
<div>{tools}</div>
<Link href={`${CAREER_PAGE_URL}/${itemId}`}>
<Button>Open</Button>
</Link>
</div>
);
};

export default CareerListItem;
18 changes: 18 additions & 0 deletions src/client/features/career/career-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import httpClient from 'src/client/app/http-client';
import { CareerDTO, CareerModel } from 'src/common/types/common-career-types';

const careerItemAdapter = (dto: CareerDTO): CareerModel => ({
...dto,
endDate: dto.endDate && new Date(dto.endDate),
startDate: new Date(dto.startDate),
});

export const careerListRequest = async (): Promise<CareerModel[]> => {
const dto = await httpClient.get('career').json<CareerDTO[]>();
return dto.map(careerItemAdapter);
};

export const careerItemRequest = async (id: string): Promise<CareerModel> => {
const dto = await httpClient.get(`career/${id}`).json<CareerDTO>();
return careerItemAdapter(dto);
};
3 changes: 3 additions & 0 deletions src/client/features/career/career-constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const CAREER_LIST_QUERY_KEY = 'CAREER_LIST_QUERY_KEY';

export const CAREER_ITEM_QUERY_KEY = 'CAREER_ITEM_QUERY_KEY';
5 changes: 5 additions & 0 deletions src/client/features/career/career-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { CareerModel } from 'src/common/types/common-career-types';

export interface CareerListItemProps {
item: CareerModel;
}
24 changes: 24 additions & 0 deletions src/client/features/career/hooks/use-career-list-query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { UseQueryOptions, UseQueryResult } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';
import { CareerModel } from 'src/common/types/common-career-types';

import { careerListRequest } from '../career-api';
import { CAREER_LIST_QUERY_KEY } from '../career-constants';

export const useCareerListQuery = (
{
refetchOnMount = false,
enabled = false,
...restProps
}: Omit<UseQueryOptions<CareerModel[]>, 'queryKey' | 'queryFn'> = {
enabled: false,
refetchOnMount: false,
}
): UseQueryResult<CareerModel[]> =>
useQuery<CareerModel[]>({
...restProps,
enabled: enabled,
queryFn: careerListRequest,
queryKey: [CAREER_LIST_QUERY_KEY],
refetchOnMount: refetchOnMount,
});
5 changes: 5 additions & 0 deletions src/client/features/summary/Summary.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@import 'src/assets/variables';

.link {
padding: $padding-sm 0;
}
23 changes: 23 additions & 0 deletions src/client/features/summary/Summary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { JSX } from 'react';

import Link from 'next/link';
import { BLOG_PAGE_URL, CAREER_PAGE_URL } from 'src/common/common-constants';
import ApiLink from 'src/common/components/ApiLink';

import styles from './Summary.module.scss';

const Summary = (): JSX.Element => (
<ul className='ag-flexbox ag-flexColumn'>
<li className={styles.link}>
<Link href={BLOG_PAGE_URL}>Blog page</Link>
</li>
<li className={styles.link}>
<Link href={CAREER_PAGE_URL}>Career page</Link>
</li>
<li className={styles.link}>
<ApiLink>API documentation page</ApiLink>
</li>
</ul>
);

export default Summary;
14 changes: 12 additions & 2 deletions src/common/common-constants.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
export const isServer = typeof window === 'undefined';

export const isDev = process.env.NODE_ENV === 'development';

export const PROTOCOL = (isServer ? global.location?.protocol : window.location?.protocol) ?? 'http';
export const HOST = (isServer ? global.location?.hostname : window.location?.hostname) ?? 'localhost';
export const PORT = process.env.PORT || 3000;
// protocol comes with ":" on client side
export const API_URL = isServer ? `${PROTOCOL}://${HOST}:${PORT}/api` : `${PROTOCOL}//${HOST}:${PORT}/api`;
// routes
export const BLOG_PAGE_ID = 'blog';
export const BLOG_PAGE_URL = `/${BLOG_PAGE_ID}`;
export const CAREER_PAGE_ID = 'career';
export const CAREER_PAGE_URL = `/${CAREER_PAGE_ID}`;

if (isDev) {
console.log('isServer', isServer);
console.log('isDev', isDev);
console.log('PROTOCOL', PROTOCOL);
console.log('HOST', HOST);
console.log('PORT', PORT);
console.log('API_URL', API_URL);
}
12 changes: 12 additions & 0 deletions src/common/components/ApiLink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { PropsWithChildren, ReactElement } from 'react';

import { API_URL } from 'src/common/common-constants';
import { WithClass } from 'src/common/types/common-types';

const ApiLink = ({ children, className = '' }: PropsWithChildren<WithClass>): ReactElement => (
<a className={className} href={API_URL} target='_blank' rel='noopener noreferrer'>
{children}
</a>
);

export default ApiLink;
1 change: 1 addition & 0 deletions src/common/components/Navigation.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

.nav {
height: $headerHeight;

&Link {
padding: $padding-sm;
}
Expand Down
26 changes: 12 additions & 14 deletions src/common/components/Navigation.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,23 @@
import type { ReactElement } from 'react';

import Link from 'next/link';
import { BLOG_PAGE_URL, CAREER_PAGE_URL } from 'src/common/common-constants';

import ApiLink from './ApiLink';
import styles from './Navigation.module.scss';

const Navigation = (): ReactElement => (
<nav className={`ag-flexbox ag-fullWidth ag-alignItems_center ag-justifyContent_center ${styles.nav}`}>
<Link href='/'>
<a className={styles.navLink}>Home</a>
</Link>
<Link href='/blog'>
<a className={styles.navLink}>Blog</a>
</Link>
<Link href='/career'>
<a className={styles.navLink}>Career</a>
</Link>
<Link href='/api'>
<a className={styles.navLink} target='_blank' rel='noopener noreferrer'>
API
</a>
</Link>
<span className={styles.navLink}>
<Link href='/'>Home</Link>
</span>
<span className={styles.navLink}>
<Link href={BLOG_PAGE_URL}>Blog</Link>
</span>
<span className={styles.navLink}>
<Link href={CAREER_PAGE_URL}>Career</Link>
</span>
<ApiLink className={styles.navLink}>API</ApiLink>
</nav>
);

Expand Down
8 changes: 4 additions & 4 deletions src/common/types/common-blog-types.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { IBaseEntity } from './common-types';
import { BaseEntity } from './common-types';

export interface IBlogBase extends IBaseEntity {
export interface BlogModelBase extends BaseEntity {
link: string;
linkCaption: string;
title: string;
}

export interface IBlogDTO extends IBlogBase {
export interface BlogDTO extends BlogModelBase {
date: string;
}

export interface IBlog extends IBlogBase {
export interface BlogModel extends BlogModelBase {
date: Date;
}
19 changes: 19 additions & 0 deletions src/common/types/common-career-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { BaseEntity } from './common-types';

export interface CareerModelBase extends BaseEntity {
description: string;
post: string;
site: string;
title: string;
tools: string;
}

export interface CareerDTO extends CareerModelBase {
endDate?: string;
startDate: string;
}

export interface CareerModel extends CareerModelBase {
endDate?: Date;
startDate: Date;
}
Loading

0 comments on commit bf2a3b7

Please sign in to comment.