Skip to content

Commit

Permalink
Merge pull request #229 from nyomansunima/project
Browse files Browse the repository at this point in the history
🤖 chore: add list of project
  • Loading branch information
nyomansunima authored Nov 2, 2023
2 parents 7c7cc87 + 5488401 commit 21e6b4d
Show file tree
Hide file tree
Showing 12 changed files with 380 additions and 1 deletion.
Binary file modified bun.lockb
Binary file not shown.
1 change: 1 addition & 0 deletions next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const nextConfig = {
remotePatterns: [
{ hostname: 'cdn.dribbble.com' },
{ hostname: 'cdn.hashnode.com' },
{ hostname: 'media.graphassets.com' },
],
},
}
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"@vercel/analytics": "^1.1.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"date-fns": "^2.30.0",
"eslint-config-prettier": "^9.0.0",
"graphql-request": "^6.1.0",
"gsap": "^3.12.2",
Expand Down
44 changes: 44 additions & 0 deletions src/app/works/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Metadata, ResolvingMetadata } from 'next'
import { notFound } from 'next/navigation'
import { workService } from '~/services/work-service'
import {
defaultOpenGraphMetadata,
defaultTwitterMetadata,
} from '~/lib/shared-metadata'

type Props = {
params: {
slug: string
}
}

export async function generateMetadata(
{ params }: Props,
parent: ResolvingMetadata,
): Promise<Metadata> {
const slug = params.slug
const meta = await workService.getMetadata(slug)

if (!slug || !meta) {
return notFound()
}

return {
title: `${meta.title} | Nyoman Sunima`,
description: meta.desc,
openGraph: {
...defaultOpenGraphMetadata,
title: `${meta.title} | Nyoman Sunima`,
description: meta.desc,
},
twitter: {
...defaultTwitterMetadata,
title: `${meta.title} | Nyoman Sunima`,
description: meta.desc,
},
}
}

export default function WorkDetailPage({ params }: Props) {
return <main></main>
}
56 changes: 56 additions & 0 deletions src/app/works/components/work-list-item.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import Image from 'next/image'
import Link from 'next/link'
import * as React from 'react'
import { Work } from '~/types'
import { mergeClass, parseReadableDate } from '~/utils/helpers'

type ItemPillProps = {
children: React.ReactNode
className?: string
}

type Props = {
work: Work
}

function ItemPill({ children, className }: ItemPillProps) {
return (
<span
className={mergeClass(
'bg-background border border-border rounded-full px-3 py-2',
className,
)}
>
{children}
</span>
)
}

export function WorkListItemCard({ work }: Props) {
const workURL = `/works/${work.slug}`
const parsedDate = parseReadableDate(work.createdAt)
const tag = work.tags[0] || ''

const Comp = work.status === 'DONE' ? 'a' : 'div'

return (
<Comp href={workURL} className="col-span-1 flex flex-col">
<picture className="relative w-full laptop:h-[560px] h-[330px] overflow-hidden rounded-2xl">
<Image src={work.thumbnail} alt="Hello" fill className="object-cover" />

{work.status === 'IN_PROGRESS' && (
<ItemPill className="absolute top-5 right-5">On Going</ItemPill>
)}

<div className="flex absolute bottom-5 inset-x-5 items-center flex-wrap gap-3">
<ItemPill>{tag}</ItemPill>
<ItemPill>{parsedDate}</ItemPill>
</div>
</picture>

<h3 className="text-2xl !leading-tight mt-6 line-clamp-2 laptop:w-10/12">
{work.title}
</h3>
</Comp>
)
}
15 changes: 15 additions & 0 deletions src/app/works/components/works-intro-section.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export default function WorksIntroSection() {
return (
<section className="container mx-auto px-5 laptop:px-20">
<h2
className="text-5xl laptop:text-8xl !leading-tight"
data-cursor-size="200"
data-cursor-exclusion
data-animation="text-char-slide-up"
>
Design, Development & Apps crafting works. Built using heart and mind to
solve problems.
</h2>
</section>
)
}
95 changes: 95 additions & 0 deletions src/app/works/components/works-list-section.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
'use client'

import { Button } from '~/app/components/ui/button'
import { WorkListItemCard } from './work-list-item'
import { useInfiniteQuery } from '@tanstack/react-query'
import { workService } from '~/services/work-service'
import * as React from 'react'
import { useRouter, useSearchParams } from 'next/navigation'

const types = ['Project', 'Exploration', 'Playground']

function WorksFilter() {
const router = useRouter()
const searchParams = useSearchParams()
const typeParam = searchParams.get('type')

function filteringByType(type: string) {
const newParams = new URLSearchParams(searchParams.toString())
if (newParams.get('type') === type) {
newParams.delete('type')
} else {
newParams.set('type', type)
}

const newURL = `/works?${newParams.toString()}`

router.push(newURL)
}

return (
<div className="flex flex-col mt-28">
<div className="flex items-center gap-3">
{types.map((type, i) => (
<Button
size={'md'}
variant={typeParam === type ? 'secondary' : 'outline'}
key={i}
className="trall duration-700 hover:scale-95"
onClick={() => filteringByType(type)}
>
{type}
</Button>
))}
</div>
</div>
)
}

function WorksList() {
const searchParams = useSearchParams()
const { data, isSuccess, isFetchingNextPage, fetchNextPage, hasNextPage } =
useInfiniteQuery({
queryKey: ['works', 'list', { ...searchParams }],
initialPageParam: null,
queryFn: workService.getAllWorks,
getNextPageParam: (lastPage, allPages) => lastPage.pageInfo.endCursor,
})

return (
<div className="flex flex-col mt-28">
<div className="grid w-full laptop:grid-cols-2 grid-cols-1 gap-x-20 laptop:gap-y-24 gap-14">
{isSuccess &&
data.pages.map((group, i) => (
<React.Fragment key={i}>
{group.works.map((work, i) => (
<WorkListItemCard work={work} key={i} />
))}
</React.Fragment>
))}
</div>

<div className="flex justify-center mt-32">
{hasNextPage && data && (
<Button
disabled={isFetchingNextPage || !hasNextPage}
onClick={() => fetchNextPage()}
>
{isFetchingNextPage ? <>Load More</> : <>Loading more</>}
</Button>
)}
</div>
</div>
)
}

export default function WorksListSection() {
return (
<section className="flex container mx-auto">
<div className="flex flex-col laptop:w-9/12 mx-auto px-5">
<WorksFilter />
<WorksList />
</div>
</section>
)
}
31 changes: 31 additions & 0 deletions src/app/works/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Metadata } from 'next'
import {
defaultOpenGraphMetadata,
defaultTwitterMetadata,
} from '~/lib/shared-metadata'
import WorksIntroSection from './components/works-intro-section'
import WorksListSection from './components/works-list-section'

export const metadata: Metadata = {
title: 'Design, development & indie hacking works | Nyoman Sunima',
description: 'My works and all of the playground',
openGraph: {
...defaultOpenGraphMetadata,
title: 'Design, development & indie hacking works | Nyoman Sunima',
description: 'My works and all of the playground',
},
twitter: {
...defaultTwitterMetadata,
title: 'Design, development & indie hacking works | Nyoman Sunima',
description: 'My works and all of the playground',
},
}

export default function WorksPage() {
return (
<main className="flex flex-col laptop:py-28 py-20">
<WorksIntroSection />
<WorksListSection />
</main>
)
}
6 changes: 5 additions & 1 deletion src/constants/menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ type MenuItem = {
export const sideNavMenus: MenuItem[] = [
{
label: 'My Works',
link: '/projects',
link: '/works',
},
{
label: 'Services',
Expand All @@ -20,4 +20,8 @@ export const sideNavMenus: MenuItem[] = [
label: 'Contacts',
link: '/contact',
},
{
label: 'Blog',
link: '/blog',
},
]
88 changes: 88 additions & 0 deletions src/services/work-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { hygraphConnection } from '~/config/hygraph'
import { PaginatedWork, Work, WorkMeta } from '~/types'

class WorkService {
async getAllWorks({ pageParam, queryKey }): Promise<PaginatedWork> {
const searchParams = queryKey[2] as URLSearchParams
let types = ['Project', 'Playground', 'Exploration']
const typeParam = searchParams.get('type')
if (typeParam) {
types = [typeParam]
}

const query = `
query WorksQuery($limit: Int!, $nextCursor: String, $types: [WorkType]) {
worksConnection(first: $limit, after: $nextCursor, where: {type_in: $types}) {
edges {
node {
slug
tags
title
type
workStatus
createdAt
thumbnail {
url
}
}
}
pageInfo {
hasNextPage
pageSize
endCursor
}
}
}
`

const res = await hygraphConnection.request<any>(query, {
limit: 6,
nextCursor: pageParam,
types,
})

const payload = {
works: res.worksConnection.edges
.map((edge) => edge.node)
.map(
(node) =>
({
...node,
status: node.workStatus,
thumbnail: node.thumbnail.url,
} as Work),
),
pageInfo: res.worksConnection.pageInfo,
}

return payload
}

async getMetadata(slug: string): Promise<WorkMeta> {
const query = `
query WorkMetadata($slug: String!) {
work(where: {slug: $slug}) {
title
description
thumbnail {
url
}
}
}
`

const res = await hygraphConnection.request<any>(query, {
slug,
})

const payload: WorkMeta = {
title: res.work.title,
desc: res.work.description,
image: res.work.thumbnail.url,
}

return payload
}
}

export const workService = new WorkService()
Loading

0 comments on commit 21e6b4d

Please sign in to comment.