diff --git a/src/assets/styles.scss b/src/assets/styles.scss index 2bc1fcd6..5d96e10d 100644 --- a/src/assets/styles.scss +++ b/src/assets/styles.scss @@ -2,6 +2,7 @@ a { color: $purpleColor; + cursor: pointer; text-decoration: none; &:hover { diff --git a/src/assets/variables.scss b/src/assets/variables.scss index 04ad1203..f8e7dc80 100644 --- a/src/assets/variables.scss +++ b/src/assets/variables.scss @@ -1,5 +1,5 @@ $footerHeight: 2vh; $headerHeight: 5vh; -$padding-lg: 5em; -$padding-md: 2.5em; +$padding-lg: 10em; +$padding-md: 5em; $padding-sm: 1em; diff --git a/src/client/app/http-client.ts b/src/client/app/http-client.ts index bd62d17d..81e7ba94 100644 --- a/src/client/app/http-client.ts +++ b/src/client/app/http-client.ts @@ -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, }); diff --git a/src/client/features/blog/BlogList.tsx b/src/client/features/blog/BlogList.tsx index d07bb3f7..e305eaa3 100644 --- a/src/client/features/blog/BlogList.tsx +++ b/src/client/features/blog/BlogList.tsx @@ -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 (
{error &&
Failed to load
} - {isLoading ?
Loading...
: blogList.map((item) => )} + {isFetching ?
Loading...
: blogList.map((item) => )}
); }; diff --git a/src/client/features/blog/BlogListItem.tsx b/src/client/features/blog/BlogListItem.tsx index 025ff4cd..a7d5027e 100644 --- a/src/client/features/blog/BlogListItem.tsx +++ b/src/client/features/blog/BlogListItem.tsx @@ -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 ( -
+
{dayjs(date).utc().format('MMMM DD, YYYY')}
{title}
{link}
{linkCaption}
- +
diff --git a/src/client/features/blog/blog-api.ts b/src/client/features/blog/blog-api.ts index 0818d99c..6052c6bb 100644 --- a/src/client/features/blog/blog-api.ts +++ b/src/client/features/blog/blog-api.ts @@ -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 => { - const blogListDTO = await httpClient.get('blog').json(); - 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 => { + const dto = await httpClient.get('blog').json(); + return dto.map(blogItemAdapter); }; -export const blogRequest = async (id: string): Promise => { - const blogDTO = await httpClient.get(`blog/${id}`).json(); - return { - ...blogDTO, - date: new Date(blogDTO.date), - }; +export const blogItemRequest = async (id: string): Promise => { + const dto = await httpClient.get(`blog/${id}`).json(); + return blogItemAdapter(dto); }; diff --git a/src/client/features/blog/blog-types.ts b/src/client/features/blog/blog-types.ts index fdac59a8..a8e85685 100644 --- a/src/client/features/blog/blog-types.ts +++ b/src/client/features/blog/blog-types.ts @@ -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; } diff --git a/src/client/features/blog/hooks/use-blog-list-query.ts b/src/client/features/blog/hooks/use-blog-list-query.ts index b7cc6e84..1b8437a0 100644 --- a/src/client/features/blog/hooks/use-blog-list-query.ts +++ b/src/client/features/blog/hooks/use-blog-list-query.ts @@ -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, 'queryKey' | 'queryFn'> = { + { + refetchOnMount = false, + enabled = false, + ...restProps + }: Omit, 'queryKey' | 'queryFn'> = { enabled: false, refetchOnMount: false, } -): UseQueryResult => - useQuery({ +): UseQueryResult => + useQuery({ ...restProps, enabled: enabled, queryFn: blogListRequest, diff --git a/src/client/features/career/CareerList.tsx b/src/client/features/career/CareerList.tsx new file mode 100644 index 00000000..a5e3c7a2 --- /dev/null +++ b/src/client/features/career/CareerList.tsx @@ -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 ( +
+ {error &&
Failed to load
} + {isFetching ?
Loading...
: careerList.map((item) => )} +
+ ); +}; + +export default CareerList; diff --git a/src/client/features/career/CareerListItem.module.scss b/src/client/features/career/CareerListItem.module.scss new file mode 100644 index 00000000..8a79bdf3 --- /dev/null +++ b/src/client/features/career/CareerListItem.module.scss @@ -0,0 +1,5 @@ +@import 'src/assets/variables'; + +.container { + padding: $padding-sm 0; +} diff --git a/src/client/features/career/CareerListItem.tsx b/src/client/features/career/CareerListItem.tsx new file mode 100644 index 00000000..1f162f35 --- /dev/null +++ b/src/client/features/career/CareerListItem.tsx @@ -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 ( +
+
{title}
+
+ {dayjs(startDate).utc().format('MMMM DD, YYYY')} + {endDate ? - {dayjs(endDate).utc().format('MMMM DD, YYYY')} : null} +
+
{post}
+
{site}
+
{description}
+
{tools}
+ + + +
+ ); +}; + +export default CareerListItem; diff --git a/src/client/features/career/career-api.ts b/src/client/features/career/career-api.ts new file mode 100644 index 00000000..fb684871 --- /dev/null +++ b/src/client/features/career/career-api.ts @@ -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 => { + const dto = await httpClient.get('career').json(); + return dto.map(careerItemAdapter); +}; + +export const careerItemRequest = async (id: string): Promise => { + const dto = await httpClient.get(`career/${id}`).json(); + return careerItemAdapter(dto); +}; diff --git a/src/client/features/career/career-constants.ts b/src/client/features/career/career-constants.ts new file mode 100644 index 00000000..9c99a747 --- /dev/null +++ b/src/client/features/career/career-constants.ts @@ -0,0 +1,3 @@ +export const CAREER_LIST_QUERY_KEY = 'CAREER_LIST_QUERY_KEY'; + +export const CAREER_ITEM_QUERY_KEY = 'CAREER_ITEM_QUERY_KEY'; diff --git a/src/client/features/career/career-types.ts b/src/client/features/career/career-types.ts new file mode 100644 index 00000000..d1b4f654 --- /dev/null +++ b/src/client/features/career/career-types.ts @@ -0,0 +1,5 @@ +import { CareerModel } from 'src/common/types/common-career-types'; + +export interface CareerListItemProps { + item: CareerModel; +} diff --git a/src/client/features/career/hooks/use-career-list-query.ts b/src/client/features/career/hooks/use-career-list-query.ts new file mode 100644 index 00000000..8e5cea18 --- /dev/null +++ b/src/client/features/career/hooks/use-career-list-query.ts @@ -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, 'queryKey' | 'queryFn'> = { + enabled: false, + refetchOnMount: false, + } +): UseQueryResult => + useQuery({ + ...restProps, + enabled: enabled, + queryFn: careerListRequest, + queryKey: [CAREER_LIST_QUERY_KEY], + refetchOnMount: refetchOnMount, + }); diff --git a/src/client/features/summary/Summary.module.scss b/src/client/features/summary/Summary.module.scss new file mode 100644 index 00000000..6376796d --- /dev/null +++ b/src/client/features/summary/Summary.module.scss @@ -0,0 +1,5 @@ +@import 'src/assets/variables'; + +.link { + padding: $padding-sm 0; +} diff --git a/src/client/features/summary/Summary.tsx b/src/client/features/summary/Summary.tsx new file mode 100644 index 00000000..d49c44a8 --- /dev/null +++ b/src/client/features/summary/Summary.tsx @@ -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 => ( +
    +
  • + Blog page +
  • +
  • + Career page +
  • +
  • + API documentation page +
  • +
+); + +export default Summary; diff --git a/src/common/common-constants.ts b/src/common/common-constants.ts index d5181332..858292fc 100644 --- a/src/common/common-constants.ts +++ b/src/common/common-constants.ts @@ -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); } diff --git a/src/common/components/ApiLink.tsx b/src/common/components/ApiLink.tsx new file mode 100644 index 00000000..af724d13 --- /dev/null +++ b/src/common/components/ApiLink.tsx @@ -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): ReactElement => ( + + {children} + +); + +export default ApiLink; diff --git a/src/common/components/Navigation.module.scss b/src/common/components/Navigation.module.scss index 35993b7b..8acd940e 100644 --- a/src/common/components/Navigation.module.scss +++ b/src/common/components/Navigation.module.scss @@ -2,6 +2,7 @@ .nav { height: $headerHeight; + &Link { padding: $padding-sm; } diff --git a/src/common/components/Navigation.tsx b/src/common/components/Navigation.tsx index 86c7b2db..fba2b651 100644 --- a/src/common/components/Navigation.tsx +++ b/src/common/components/Navigation.tsx @@ -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 => ( ); diff --git a/src/common/types/common-blog-types.ts b/src/common/types/common-blog-types.ts index 329ab3cd..390c648d 100644 --- a/src/common/types/common-blog-types.ts +++ b/src/common/types/common-blog-types.ts @@ -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; } diff --git a/src/common/types/common-career-types.ts b/src/common/types/common-career-types.ts new file mode 100644 index 00000000..ba236d9f --- /dev/null +++ b/src/common/types/common-career-types.ts @@ -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; +} diff --git a/src/common/types/common-types.ts b/src/common/types/common-types.ts index dec5dc7a..66cea69e 100644 --- a/src/common/types/common-types.ts +++ b/src/common/types/common-types.ts @@ -1,3 +1,7 @@ -export interface IBaseEntity { +export interface BaseEntity { _id?: string; } + +export interface WithClass { + className?: string; +} diff --git a/src/pages/blog/[id].tsx b/src/pages/blog/[id].tsx index edfa3d98..94c8a32d 100644 --- a/src/pages/blog/[id].tsx +++ b/src/pages/blog/[id].tsx @@ -2,20 +2,20 @@ import type { JSX } from 'react'; import { useQuery } from '@tanstack/react-query'; import { useRouter } from 'next/router'; -import { blogRequest } from 'src/client/features/blog/blog-api'; -import type { IBlog } from 'src/common/types/common-blog-types'; +import { blogItemRequest } from 'src/client/features/blog/blog-api'; +import type { BlogModel } from 'src/common/types/common-blog-types'; import { BLOG_ID_QUERY_KEY } from '../../client/features/blog/blog-constants'; const Blog = (): JSX.Element => { const router = useRouter(); const id = router.query?.id as string; - const { data, error } = useQuery({ - queryFn: () => blogRequest(id), + const { data, error } = useQuery({ + queryFn: () => blogItemRequest(id), queryKey: [BLOG_ID_QUERY_KEY, id], refetchOnMount: false, }); - const blog = data ?? ({} as IBlog); + const blog = data ?? ({} as BlogModel); if (error) return
Failed to load
; if (!blog) return
Loading...
; diff --git a/src/pages/career.tsx b/src/pages/career.tsx index 8444a9e1..c6bd7186 100644 --- a/src/pages/career.tsx +++ b/src/pages/career.tsx @@ -1,5 +1,22 @@ import type { JSX } from 'react'; +import { useCallback } from 'react'; -const Career = (): JSX.Element =>

Career

; +import { Button } from '@mui/base'; +import CareerList from 'src/client/features/career/CareerList'; +import { useCareerListQuery } from 'src/client/features/career/hooks/use-career-list-query'; -export default Career; +const Careers = (): JSX.Element => { + const { refetch } = useCareerListQuery({ enabled: true }); + const handleRefresh = useCallback(() => refetch(), [refetch]); + return ( +
+
+

Career

+ +
+ +
+ ); +}; + +export default Careers; diff --git a/src/pages/career/[id].tsx b/src/pages/career/[id].tsx new file mode 100644 index 00000000..22d80eee --- /dev/null +++ b/src/pages/career/[id].tsx @@ -0,0 +1,32 @@ +import type { JSX } from 'react'; + +import { useQuery } from '@tanstack/react-query'; +import { useRouter } from 'next/router'; + +import { BLOG_ID_QUERY_KEY } from '../../client/features/blog/blog-constants'; +import { careerItemRequest } from '../../client/features/career/career-api'; +import { CareerModel } from '../../common/types/common-career-types'; + +const Career = (): JSX.Element => { + const router = useRouter(); + const id = router.query?.id as string; + const { data, error } = useQuery({ + queryFn: () => careerItemRequest(id), + queryKey: [BLOG_ID_QUERY_KEY, id], + refetchOnMount: false, + }); + const career = data ?? ({} as CareerModel); + + if (error) return
Failed to load
; + if (!career) return
Loading...
; + return ( + <> +

{career.title}

+
+
{JSON.stringify(career)}
+
+ + ); +}; + +export default Career; diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 2077573a..91a9fd4f 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,23 +1,11 @@ import type { JSX } from 'react'; -import Link from 'next/link'; +import Summary from 'src/client/features/summary/Summary'; const Home = (): JSX.Element => (

Site structure

- +
); diff --git a/src/server/features/blog/blog-api-controller.ts b/src/server/features/blog/blog-api-controller.ts index 385a7f48..68e781e1 100644 --- a/src/server/features/blog/blog-api-controller.ts +++ b/src/server/features/blog/blog-api-controller.ts @@ -1,30 +1,35 @@ -import { Body, Controller, Get, Param, Post, Put } from '@nestjs/common'; +import { Body, Controller, Delete, Get, Param, Patch, Post } from '@nestjs/common'; -import type { CreateBlogDto } from './blog-models'; -import type { Blog } from './blog-schema'; +import { CreateBlogDTO, UpdateBlogDTO } from './blog-models'; +import { Blog } from './blog-schema'; import { BlogService } from './blog-service'; -@Controller() +@Controller('api/blog') export class BlogApiController { constructor(private readonly blogService: BlogService) {} - @Post('api/blog') - async create(@Body() dto: CreateBlogDto): Promise { - return await this.blogService.create(dto); + @Get() + findAll(): Promise { + return this.blogService.findAll(); } - @Put('api/blog/:id') - async update(@Param('id') id: string, @Body() dto: CreateBlogDto): Promise { - return await this.blogService.update(id, dto); + @Get(':id') + findOne(@Param('id') prodId: string): Promise { + return this.blogService.findOne(prodId); } - @Get('api/blog') - async getAll(): Promise { - return await this.blogService.getAll(); + @Post() + create(@Body() dto: CreateBlogDTO): Promise { + return this.blogService.create(dto); } - @Get('api/blog/:id') - getById(@Param('id') prodId: string): Promise { - return this.blogService.getById(prodId); + @Patch(':id') + update(@Param('id') id: string, @Body() dto: UpdateBlogDTO): Promise { + return this.blogService.update(id, dto); + } + + @Delete(':id') + delete(@Param('id') id: string): Promise { + return this.blogService.delete(id); } } diff --git a/src/server/features/blog/blog-controller.ts b/src/server/features/blog/blog-controller.ts index 3d9271f6..a7ff207f 100644 --- a/src/server/features/blog/blog-controller.ts +++ b/src/server/features/blog/blog-controller.ts @@ -1,10 +1,11 @@ import { Controller, Get, Render, UseInterceptors } from '@nestjs/common'; +import { BLOG_PAGE_ID, BLOG_PAGE_URL } from 'src/common/common-constants'; import { ParamsInterceptor } from 'src/server/common/params/params-interceptor'; @Controller() export class BlogController { - @Get('/blog') - @Render('blog') + @Get(BLOG_PAGE_URL) + @Render(BLOG_PAGE_ID) @UseInterceptors(ParamsInterceptor) blogList() { return {}; @@ -13,7 +14,7 @@ export class BlogController { @Get('/blog/:id') @Render('blog/[id]') @UseInterceptors(ParamsInterceptor) - blogPost() { + blogItem() { return {}; } } diff --git a/src/server/features/blog/blog-models.ts b/src/server/features/blog/blog-models.ts index b18d56f4..eca60c79 100644 --- a/src/server/features/blog/blog-models.ts +++ b/src/server/features/blog/blog-models.ts @@ -1,8 +1,8 @@ -import { IsString } from '@nestjs/class-validator'; +import { IsDate, IsString } from '@nestjs/class-validator'; import { PartialType } from '@nestjs/mapped-types'; -import type { IBlogDTO } from 'src/common/types/common-blog-types'; +import { BlogModel } from 'src/common/types/common-blog-types'; -export class CreateBlogDto implements IBlogDTO { +export class CreateBlogDTO implements BlogModel { @IsString() readonly title: string; @@ -12,11 +12,8 @@ export class CreateBlogDto implements IBlogDTO { @IsString() readonly linkCaption: string; - // @IsDate() - // readonly date: Date; - // TODO: temporary solution before dates migration - @IsString() - readonly date: string; + @IsDate() + readonly date: Date; } -export class UpdateBlogDto extends PartialType(CreateBlogDto) {} +export class UpdateBlogDTO extends PartialType(CreateBlogDTO) {} diff --git a/src/server/features/blog/blog-schema.ts b/src/server/features/blog/blog-schema.ts index 505a83a9..c713cf6a 100644 --- a/src/server/features/blog/blog-schema.ts +++ b/src/server/features/blog/blog-schema.ts @@ -1,9 +1,9 @@ import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { Document } from 'mongoose'; -import type { IBlog } from 'src/common/types/common-blog-types'; +import type { BlogModel } from 'src/common/types/common-blog-types'; @Schema() -export class Blog extends Document implements IBlog { +export class Blog extends Document implements BlogModel { @Prop({ required: true, type: String }) readonly title: string; diff --git a/src/server/features/blog/blog-service.ts b/src/server/features/blog/blog-service.ts index 04e7d1ed..1786ebfd 100644 --- a/src/server/features/blog/blog-service.ts +++ b/src/server/features/blog/blog-service.ts @@ -1,47 +1,44 @@ -import { Injectable, InternalServerErrorException, NotFoundException } from '@nestjs/common'; +import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import type { Model } from 'mongoose'; -import type { CreateBlogDto, UpdateBlogDto } from './blog-models'; +import type { CreateBlogDTO, UpdateBlogDTO } from './blog-models'; import { Blog } from './blog-schema'; @Injectable() export class BlogService { constructor(@InjectModel(Blog.name) private readonly blogModel: Model) {} - async create(createBlogDto: CreateBlogDto): Promise { - console.log(`BlogService create(${JSON.stringify(createBlogDto, null, 1)})`); - // TODO: temporary solution before dates migration - const date = new Date(createBlogDto.date); - const blog = new this.blogModel({ ...createBlogDto, date }); - return blog.save(); + // TODO: add filters and pagination support + async findAll(): Promise { + return await this.blogModel.find({}).sort({ date: 'desc' }).exec(); } - async update(id: string, updateBlogDto: UpdateBlogDto): Promise { - console.log(`BlogService update(${id}, ${JSON.stringify(updateBlogDto, null, 1)})`); - // TODO: temporary solution before dates migration - const date = new Date(updateBlogDto.date); - const blog = await this.blogModel.findByIdAndUpdate(id, { ...updateBlogDto, date }, { new: true }); - if (!blog) { - throw new InternalServerErrorException('Could not update blogReducer record.'); - } - return blog; + async findOne(id: string): Promise { + const item = await this.blogModel.findOne({ _id: id }).exec(); + return this.returnIfExists(id, item); } - async getAll(): Promise { - return await this.blogModel.find({}).sort({ date: 'desc' }).exec(); + async create(dto: CreateBlogDTO): Promise { + console.log(`BlogService create(${JSON.stringify(dto, null, 1)})`); + return this.blogModel.create(dto); } - async getById(id: string): Promise { - let blog; - try { - blog = await this.blogModel.findById(id).exec(); - } catch (error) { - throw new NotFoundException('Could not find blogReducer record.'); - } - if (!blog) { - throw new NotFoundException('Could not find blogReducer record.'); + async update(id: string, dto: UpdateBlogDTO): Promise { + console.log(`BlogService update(${id}, ${JSON.stringify(dto, null, 1)})`); + const item = await this.blogModel.findByIdAndUpdate(id, dto, { new: true }).exec(); + return this.returnIfExists(id, item); + } + + async delete(id: string): Promise { + const word = await this.blogModel.findByIdAndRemove(id).exec(); + return this.returnIfExists(id, word); + } + + private returnIfExists(id: string, item: Blog | null): Blog { + if (!item) { + throw new NotFoundException(`A blog with id: "${id}" is not found`); } - return blog; + return item; } } diff --git a/src/server/features/career/career-api-controller.ts b/src/server/features/career/career-api-controller.ts new file mode 100644 index 00000000..82df7fc9 --- /dev/null +++ b/src/server/features/career/career-api-controller.ts @@ -0,0 +1,35 @@ +import { Body, Controller, Delete, Get, Param, Patch, Post } from '@nestjs/common'; + +import { CreateCareerDTO, UpdateCareerDTO } from './career-models'; +import { Career } from './career-schema'; +import { CareerService } from './career-service'; + +@Controller('api/career') +export class CareerApiController { + constructor(private readonly careerService: CareerService) {} + + @Get() + findAll(): Promise { + return this.careerService.findAll(); + } + + @Get(':id') + findOne(@Param('id') prodId: string): Promise { + return this.careerService.findOne(prodId); + } + + @Post() + create(@Body() dto: CreateCareerDTO): Promise { + return this.careerService.create(dto); + } + + @Patch(':id') + update(@Param('id') id: string, @Body() dto: UpdateCareerDTO): Promise { + return this.careerService.update(id, dto); + } + + @Delete(':id') + delete(@Param('id') id: string): Promise { + return this.careerService.delete(id); + } +} diff --git a/src/server/features/career/career-controller.ts b/src/server/features/career/career-controller.ts index 7cd8ead6..5ec8e6d9 100644 --- a/src/server/features/career/career-controller.ts +++ b/src/server/features/career/career-controller.ts @@ -1,25 +1,20 @@ -import { Body, Controller, Get, Param, Post } from '@nestjs/common'; +import { Controller, Get, Render, UseInterceptors } from '@nestjs/common'; +import { CAREER_PAGE_ID, CAREER_PAGE_URL } from 'src/common/common-constants'; +import { ParamsInterceptor } from 'src/server/common/params/params-interceptor'; -import { CareerService } from './career-service'; -import type { CreateCareerDto } from './dto/create-career.dto'; -import type { Career } from './entities/career.entity'; - -@Controller('api/career') +@Controller() export class CareerController { - constructor(private readonly careerService: CareerService) {} - - @Post() - async create(@Body() dto: CreateCareerDto): Promise { - return await this.careerService.create(dto); - } - - @Get() - async getAll(): Promise { - return await this.careerService.getAll(); + @Get(CAREER_PAGE_URL) + @Render(CAREER_PAGE_ID) + @UseInterceptors(ParamsInterceptor) + careerList() { + return {}; } - @Get(':id') - getById(@Param('id') prodId: string): Promise { - return this.careerService.getById(prodId); + @Get('/career/:id') + @Render('career/[id]') + @UseInterceptors(ParamsInterceptor) + careerItem() { + return {}; } } diff --git a/src/server/features/career/career-models.ts b/src/server/features/career/career-models.ts new file mode 100644 index 00000000..1a14f6fd --- /dev/null +++ b/src/server/features/career/career-models.ts @@ -0,0 +1,28 @@ +import { IsDate, IsString } from '@nestjs/class-validator'; +import { PartialType } from '@nestjs/mapped-types'; +import { CareerModel } from 'src/common/types/common-career-types'; + +export class CreateCareerDTO implements CareerModel { + @IsString() + readonly title: string; + + @IsString() + readonly site: string; + + @IsString() + readonly post: string; + + @IsString() + readonly description: string; + + @IsString() + readonly tools: string; + + @IsDate() + readonly startDate: Date; + + @IsDate() + readonly endDate: Date; +} + +export class UpdateCareerDTO extends PartialType(CreateCareerDTO) {} diff --git a/src/server/features/career/career-module.ts b/src/server/features/career/career-module.ts index d743ea24..de7f3d6e 100644 --- a/src/server/features/career/career-module.ts +++ b/src/server/features/career/career-module.ts @@ -1,9 +1,10 @@ import { Module } from '@nestjs/common'; import { MongooseModule } from '@nestjs/mongoose'; +import { CareerApiController } from './career-api-controller'; import { CareerController } from './career-controller'; +import { Career, CareerSchema } from './career-schema'; import { CareerService } from './career-service'; -import { Career, CareerSchema } from './entities/career.entity'; @Module({ imports: [ @@ -14,7 +15,7 @@ import { Career, CareerSchema } from './entities/career.entity'; }, ]), ], - controllers: [CareerController], + controllers: [CareerApiController, CareerController], providers: [CareerService], }) export class CareerModule {} diff --git a/src/server/features/career/entities/career.entity.ts b/src/server/features/career/career-schema.ts similarity index 78% rename from src/server/features/career/entities/career.entity.ts rename to src/server/features/career/career-schema.ts index 1791cb94..2c7ff0cd 100644 --- a/src/server/features/career/entities/career.entity.ts +++ b/src/server/features/career/career-schema.ts @@ -1,8 +1,9 @@ import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { Document } from 'mongoose'; +import { CareerModel } from 'src/common/types/common-career-types'; @Schema() -export class Career extends Document { +export class Career extends Document implements CareerModel { @Prop({ required: true, type: String }) readonly title: string; @@ -22,7 +23,7 @@ export class Career extends Document { readonly startDate: Date; @Prop({ type: Date }) - readonly endDate: Date; + readonly endDate?: Date; } export const CareerSchema = SchemaFactory.createForClass(Career); diff --git a/src/server/features/career/career-service.ts b/src/server/features/career/career-service.ts index 1d220ae8..f0d5bd52 100644 --- a/src/server/features/career/career-service.ts +++ b/src/server/features/career/career-service.ts @@ -1,33 +1,44 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; -import type { Model } from 'mongoose'; +import { Model } from 'mongoose'; -import type { CreateCareerDto } from './dto/create-career.dto'; -import { Career } from './entities/career.entity'; +import { CreateCareerDTO, UpdateCareerDTO } from './career-models'; +import { Career } from './career-schema'; @Injectable() export class CareerService { constructor(@InjectModel(Career.name) private readonly careerModel: Model) {} - async create(createCareerDto: CreateCareerDto): Promise { - const career = new this.careerModel(createCareerDto); - return career.save(); + // TODO: add filters and pagination support + async findAll(): Promise { + return await this.careerModel.find({}).sort({ startDate: 'desc' }).exec(); } - async getAll(): Promise { - return await this.careerModel.find({}).sort({ startDate: 'desc' }).exec(); + async findOne(id: string): Promise { + const item = await this.careerModel.findOne({ _id: id }).exec(); + return this.returnIfExists(id, item); } - async getById(id: string): Promise { - let career; - try { - career = await this.careerModel.findById(id).exec(); - } catch (error) { - throw new NotFoundException('Could not find career record.'); - } - if (!career) { - throw new NotFoundException('Could not find career record.'); + async create(dto: CreateCareerDTO): Promise { + console.log(`CareerService create(${JSON.stringify(dto, null, 1)})`); + return this.careerModel.create(dto); + } + + async update(id: string, dto: UpdateCareerDTO): Promise { + console.log(`CareerService update(${id}, ${JSON.stringify(dto, null, 1)})`); + const item = await this.careerModel.findByIdAndUpdate(id, dto, { new: true }).exec(); + return this.returnIfExists(id, item); + } + + async delete(id: string): Promise { + const item = await this.careerModel.findByIdAndRemove(id).exec(); + return this.returnIfExists(id, item); + } + + private returnIfExists(id: string, item: Career | null): Career { + if (!item) { + throw new NotFoundException(`A career item with id: "${id}" is not found`); } - return career; + return item; } } diff --git a/src/server/features/career/dto/create-career.dto.ts b/src/server/features/career/dto/create-career.dto.ts deleted file mode 100644 index 6b11a1b8..00000000 --- a/src/server/features/career/dto/create-career.dto.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { IsString, IsDate } from '@nestjs/class-validator'; - -export class CreateCareerDto { - @IsString() - readonly title: string; - - @IsString() - readonly site: string; - - @IsString() - readonly post: string; - - @IsString() - readonly description: string; - - @IsString() - readonly tools: string; - - @IsDate() - readonly startDate: Date; - - @IsDate() - readonly endDate: Date; -} diff --git a/src/server/features/career/dto/update-career.dto.ts b/src/server/features/career/dto/update-career.dto.ts deleted file mode 100644 index 2ed8fdb7..00000000 --- a/src/server/features/career/dto/update-career.dto.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { PartialType } from '@nestjs/mapped-types'; - -import { CreateCareerDto } from './create-career.dto'; - -export class UpdateCareerDto extends PartialType(CreateCareerDto) {}