Skip to content

Commit

Permalink
Merge pull request #5 from SantiagoJavierRubio/dev
Browse files Browse the repository at this point in the history
Implement main features
  • Loading branch information
SantiagoJavierRubio committed Nov 30, 2023
2 parents bd75cf8 + ac0c669 commit 01b5926
Show file tree
Hide file tree
Showing 26 changed files with 810 additions and 64 deletions.
4 changes: 4 additions & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

yarn lint
4 changes: 4 additions & 0 deletions .husky/pre-push
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

yarn test
2 changes: 2 additions & 0 deletions apps/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
},
"dependencies": {
"@tanstack/react-query": "^5.9.0",
"@tanstack/react-table": "^8.10.7",
"axios": "^1.6.2",
"react": "^18.2.0",
"react-dom": "^18.2.0"
Expand All @@ -28,6 +29,7 @@
"eslint-plugin-tailwindcss": "^3.13.0",
"postcss": "^8.4.31",
"tailwindcss": "^3.3.5",
"types": "*",
"typescript": "^5.2.2",
"vite": "^5.0.0"
}
Expand Down
2 changes: 2 additions & 0 deletions apps/client/src/api/getCommits.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import axios from 'axios'
import type {} from 'types'

export default function getCommits() {
return axios.get('http://localhost:3000/commits').then(res => res.data)
}
14 changes: 14 additions & 0 deletions apps/client/src/hooks/useDebounce.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { useState, useEffect } from 'react'

export default function useDebounce<T>(value: T, delay: number) {
const [debouncedValue, setDebouncedValue] = useState<T>(value)
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value)
}, delay)
return () => {
clearTimeout(handler)
}
}, [value, delay])
return debouncedValue
}
100 changes: 100 additions & 0 deletions apps/client/src/pages/Home/CommitTable/CommitTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/* eslint-disable no-console */
import { ChangeEvent, useState } from 'react'
import { CommitListElement } from 'types'
import {
createColumnHelper,
useReactTable,
getCoreRowModel,
flexRender,
getPaginationRowModel,
getSortedRowModel,
getFilteredRowModel,
type SortingState
} from '@tanstack/react-table'

import useDebounce from '@/hooks/useDebounce'

const columnHelper = createColumnHelper<CommitListElement>()
const columns = [
columnHelper.accessor('sha', { header: () => <span>Id</span>, cell: props => <span>{props.getValue().substring(0, 7)}</span> }),
columnHelper.group({
id: 'author',
columns: [
columnHelper.display({ id: 'avatar', cell: props => <img src={props.row.original.author.avatar_url} alt={`${props.row.original.commit.author.name}'s avatar`} className='aspect-square rounded-full w-6'/> }),
columnHelper.accessor('commit.author.name', { id: 'name', header: () => <span>Author</span>, cell: props => <span>{props.getValue()}</span> }),
columnHelper.accessor('commit.author.email', { id: 'email', header: () => null, cell: () => null })
]
}),
columnHelper.accessor('commit.message', { id: 'message', enableSorting: false, header: () => <span>Message</span>, cell: props => <span>{props.row.original.commit.message}</span> }),
columnHelper.accessor('commit.author.date', { id: 'date', header: () => <span>Date</span>, cell: props => <span>{new Date(props.getValue()).toLocaleDateString()}</span> })
]
export default function CommitTable({ commits }: { commits: CommitListElement[] }) {
const [sorting, setSorting] = useState<SortingState>([])
const [filtering, setFiltering] = useState<string>('')

const filter = useDebounce(filtering, 250)

const table = useReactTable({
data: commits,
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
state: {
sorting,
globalFilter: filter
},
onSortingChange: setSorting,
onGlobalFilterChange: setFiltering
})

const handleSortInput = (e: ChangeEvent<HTMLInputElement>) => {
setFiltering(e.target.value)
}

return (
<>
<input type="text" name="filter" onChange={handleSortInput} value={filtering} />
<p>{table.getState().pagination.pageIndex + 1} of {table.getPageCount()}</p>
<table>
<thead>
{table.getHeaderGroups().map(hGroup => (
<tr key={hGroup.id}>
{hGroup.headers.map(header => (
<th key={header.id}>
{header.isPlaceholder ? null : (
<div className={header.column.getCanSort() ? 'cursor-pointer select-none' : 'cursor-default'} onClick={header.column.getToggleSortingHandler()}>
{flexRender(header.column.columnDef.header, header.getContext())}
</div>
)}
</th>
))}
</tr>
))}
</thead>
<tbody>
{table.getRowModel().rows.map(row => (
<tr key={row.id} onClick={() => console.log(row.getValue('sha'))} >
{row.getVisibleCells().map(cell => (
<td key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
</tbody>
</table>
<div>
<button disabled={!table.getCanPreviousPage()} onClick={() => table.previousPage()}>previous</button>
<button disabled={!table.getCanNextPage()} onClick={() => table.nextPage()}>next</button>
<label htmlFor='pageSize'>Size</label>
<select id="pageSize" name="page-size" onChange={(e) => table.setPageSize(parseInt(e.target.value))}>
<option defaultChecked value={10}>10</option>
<option value={20}>20</option>
<option value={50}>50</option>
</select>
</div>
</>
)
}
8 changes: 4 additions & 4 deletions apps/client/src/pages/Home/index.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import getCommits from '@/api/getCommits'
import { useQuery } from '@tanstack/react-query'
import { CommitListElement } from 'types'
import CommitTable from './CommitTable/CommitTable'

export default function Home() {
const { isLoading, error, data, isFetching } = useQuery({
const commitQuery = useQuery<CommitListElement[]>({
queryKey: ['commits'],
queryFn: getCommits
})

// eslint-disable-next-line no-console
console.log(isLoading, error, data, isFetching)
return (
<div>Home</div>
<div>{commitQuery.isSuccess ? <CommitTable commits={commitQuery.data} /> : <h1>loadin</h1>}</div>
)
}
8 changes: 8 additions & 0 deletions apps/server/config/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export default () => ({
NODE_ENV: process.env.NODE_ENV || 'development',
githubToken: process.env.GITHUB_TOKEN,
repo: {
owner: process.env.REPO_OWNER ?? 'SantiagoJavierRubio',
name: process.env.REPO_NAME ?? 'git-commit-history'
}
})
15 changes: 12 additions & 3 deletions apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,22 @@
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test": "jest --passWithNoTests",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@anatine/zod-openapi": "^2.2.1",
"@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.1.1",
"@nestjs/core": "^10.0.0",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/swagger": "^7.1.16",
"octokit": "^3.1.2",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1"
"zod": "^3.22.4"
},
"devDependencies": {
"@nestjs/cli": "^10.0.0",
Expand Down Expand Up @@ -56,7 +59,13 @@
"json",
"ts"
],
"rootDir": "src",
"modulePaths": [
"<rootDir>"
],
"moduleDirectories": [
"node_modules",
"src"
],
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
Expand Down
22 changes: 0 additions & 22 deletions apps/server/src/app.controller.spec.ts

This file was deleted.

12 changes: 0 additions & 12 deletions apps/server/src/app.controller.ts

This file was deleted.

17 changes: 12 additions & 5 deletions apps/server/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import { Module } from '@nestjs/common'
import { AppController } from './app.controller'
import { AppService } from './app.service'
import { ConfigModule } from '@nestjs/config'
import { CommitsModule } from './commits/commits.module'
import config from 'config/config'

@Module({
imports: [],
controllers: [AppController],
providers: [AppService]
imports: [
ConfigModule.forRoot({
isGlobal: true,
load: [config]
}),
CommitsModule
],
controllers: [],
providers: []
})
export class AppModule {}
8 changes: 0 additions & 8 deletions apps/server/src/app.service.ts

This file was deleted.

36 changes: 36 additions & 0 deletions apps/server/src/commits/commit.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { z } from 'zod'
import { generateSchema } from '@anatine/zod-openapi'

export const basicCommitSchema = z.object({
sha: z.string(),
node_id: z.string(),
commit: z.object({
author: z.object({
name: z.string(),
email: z.string(),
date: z.string()
}),
message: z.string(),
tree: z.object({
sha: z.string(),
url: z.string()
})
}),
url: z.string(),
html_url: z.string(),
parents: z.object({
sha: z.string(),
url: z.string()
}).array(),
author: z.object({
avatar_url: z.string(),
html_url: z.string()
})
})

export const commitList = z.array(basicCommitSchema)

export type CommitListElement = z.infer<typeof basicCommitSchema>
export type CommitList = z.infer<typeof commitList>

export const commitApiSchema = generateSchema(basicCommitSchema)
28 changes: 28 additions & 0 deletions apps/server/src/commits/commits.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Controller, Get, Param } from '@nestjs/common'
import { ApiTags, ApiParam, ApiOkResponse } from '@nestjs/swagger'
import { CommitsService } from './commits.service'
import { commitApiSchema } from './commit.entity'

@ApiTags('commits')
@Controller('commits')
export class CommitsController {
constructor(private readonly service: CommitsService) {}

@ApiOkResponse({
schema: commitApiSchema,
isArray: true
})
@Get()
async getAll() {
return await this.service.getAllCommits()
}

@ApiParam({
name: 'ref',
description: 'The commit reference. Can be a commit SHA, branch name, or tag name.'
})
@Get('/:ref')
async getOne(@Param('ref') ref: string) {
return await this.service.getById(ref)
}
}
12 changes: 12 additions & 0 deletions apps/server/src/commits/commits.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common'
import { CommitsController } from './commits.controller'
import { CommitsService } from './commits.service'
import { OctokitClient } from 'src/octokit/octokitClient'

@Module({
imports: [],
controllers: [CommitsController],
providers: [CommitsService, OctokitClient]
})

export class CommitsModule {}
28 changes: 28 additions & 0 deletions apps/server/src/commits/commits.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Injectable, InternalServerErrorException } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
import { OctokitClient } from 'src/octokit/octokitClient'
import { commitList } from './commit.entity'

@Injectable()
export class CommitsService {
constructor(private readonly octokit: OctokitClient, private readonly configService: ConfigService) {}

async getAllCommits() {
const response = await this.octokit.getClient().paginate('GET /repos/{owner}/{repo}/commits', {
owner: this.configService.get<string>('repo.owner'),
repo: this.configService.get<string>('repo.name')
}).catch(err => { throw new InternalServerErrorException(err) })
const parsed = commitList.safeParse(response)
if (parsed.success) return parsed.data
else throw new InternalServerErrorException('Github response did not match parameters')
}

async getById(ref: string) {
const response = await this.octokit.getClient().request('GET /repos/{owner}/{repo}/commits/{ref}', {
owner: this.configService.get<string>('repo.owner'),
repo: this.configService.get<string>('repo.name'),
ref
})
return response.data
}
}
Loading

0 comments on commit 01b5926

Please sign in to comment.