Put blog data to dummy data JSON, render other blog controls.master
@@ -1,4 +1,34 @@ | |||
{ | |||
"blog": [ | |||
{ | |||
"id": 1, | |||
"title": "Article Title", | |||
"createdAt": "2024-03-14T13:37:00.000Z", | |||
"slug": "article-title", | |||
"content": "Excerpt lorem ipsum dolor sit amet consectetur adipisicing elit sed do eiusmod aliquam. Lorem ipsum dolor sit amet consectetur adipisicing elit sed do eiusmod aliquam. Lorem ipsum dolor sit amet consectetur adipisicing elit sed do eiusmod aliquam. Lorem ipsum dolor sit amet consectetur adipisicing elit sed do eiusmod aliquam. Lorem ipsum dolor sit amet consectetur adipisicing elit sed do eiusmod aliquam. Lorem ipsum dolor sit amet consectetur adipisicing elit sed do eiusmod aliquam." | |||
}, | |||
{ | |||
"id": 2, | |||
"title": "New Article Title", | |||
"createdAt": "2024-02-14T06:09:00.000Z", | |||
"slug": "new-article-title", | |||
"content": "Excerpt lorem ipsum dolor sit amet consectetur adipisicing elit sed do eiusmod aliquam. Lorem ipsum dolor sit amet consectetur adipisicing elit sed do eiusmod aliquam. Lorem ipsum dolor sit amet consectetur adipisicing elit sed do eiusmod aliquam. Lorem ipsum dolor sit amet consectetur adipisicing elit sed do eiusmod aliquam. Lorem ipsum dolor sit amet consectetur adipisicing elit sed do eiusmod aliquam. Lorem ipsum dolor sit amet consectetur adipisicing elit sed do eiusmod aliquam." | |||
}, | |||
{ | |||
"id": 3, | |||
"title": "Another Article Title", | |||
"createdAt": "2024-01-25T04:20:00.000Z", | |||
"slug": "another-article-title", | |||
"content": "Excerpt lorem ipsum dolor sit amet consectetur adipisicing elit sed do eiusmod aliquam. Lorem ipsum dolor sit amet consectetur adipisicing elit sed do eiusmod aliquam. Lorem ipsum dolor sit amet consectetur adipisicing elit sed do eiusmod aliquam. Lorem ipsum dolor sit amet consectetur adipisicing elit sed do eiusmod aliquam. Lorem ipsum dolor sit amet consectetur adipisicing elit sed do eiusmod aliquam. Lorem ipsum dolor sit amet consectetur adipisicing elit sed do eiusmod aliquam." | |||
}, | |||
{ | |||
"id": 4, | |||
"title": "Yet Another Article Title", | |||
"createdAt": "2024-01-03T10:10:00.000Z", | |||
"slug": "yet-another-article-title", | |||
"content": "Excerpt lorem ipsum dolor sit amet consectetur adipisicing elit sed do eiusmod aliquam. Lorem ipsum dolor sit amet consectetur adipisicing elit sed do eiusmod aliquam. Lorem ipsum dolor sit amet consectetur adipisicing elit sed do eiusmod aliquam. Lorem ipsum dolor sit amet consectetur adipisicing elit sed do eiusmod aliquam. Lorem ipsum dolor sit amet consectetur adipisicing elit sed do eiusmod aliquam. Lorem ipsum dolor sit amet consectetur adipisicing elit sed do eiusmod aliquam." | |||
} | |||
], | |||
"make": [ | |||
{ | |||
"id": 1, | |||
@@ -0,0 +1,111 @@ | |||
import {IconText} from '@/components/molecules/IconText'; | |||
import {ClockIcon} from '@/components/molecules/ClockIcon'; | |||
import * as WebNavigationReact from '@tesseract-design/web-navigation-react'; | |||
import Link from 'next/link'; | |||
import * as React from 'react'; | |||
export interface BlogItemProps { | |||
title: string; | |||
createdAt: number | string | Date; | |||
slug: string; | |||
content: string; | |||
} | |||
export const BlogItem: React.FC<BlogItemProps> = ({ | |||
title, | |||
createdAt, | |||
slug, | |||
content, | |||
}) => { | |||
const dateObj = new Date(createdAt); | |||
const dateFormat = new Intl.DateTimeFormat('en-PH', { | |||
month: 'short', | |||
day: 'numeric', | |||
year: 'numeric', | |||
}).formatToParts(dateObj); | |||
const month = dateFormat.find((f) => f.type === 'month')?.value; | |||
const date = dateFormat.find((f) => f.type === 'day')?.value; | |||
const year = dateFormat.find((f) => f.type === 'year')?.value; | |||
const timeFormat = new Intl.DateTimeFormat('en-PH', { | |||
hour12: false, | |||
hour: '2-digit', | |||
minute: '2-digit', | |||
}).formatToParts(dateObj); | |||
const hour = timeFormat.find((v) => v.type === 'hour')?.value; | |||
const minute = timeFormat.find((v) => v.type === 'minute')?.value; | |||
const timeZoneFormat = new Intl.DateTimeFormat('en-PH', { | |||
timeZoneName: 'shortOffset', | |||
}).formatToParts(dateObj); | |||
const timeZone = timeZoneFormat.find((v) => v.type === 'timeZoneName')?.value?.replace('GMT', 'UTC'); | |||
return ( | |||
<article | |||
className="rounded overflow-hidden relative flex flex-col gap-8 before:absolute before:top-0 before:left-0 before:w-full before:h-full before:border-2 before:rounded-inherit before:pointer-events-none before:opacity-10" | |||
> | |||
<header className="flex flex-col gap-4"> | |||
<h1 className="m-0 h-24 flex items-end px-8"> | |||
<Link | |||
className="no-underline" | |||
href={{ | |||
pathname: '/blog/articles/[slug]', | |||
query: { | |||
slug, | |||
} | |||
} as unknown as string} | |||
> | |||
{title} | |||
</Link> | |||
</h1> | |||
<time className="px-8 tabular-nums font-headings lowercase font-extralight text-5xl flex gap-4"> | |||
{month && date && year && ( | |||
<IconText | |||
icon={date} | |||
topText={month.slice(0, 3)} | |||
bottomText={year} | |||
/> | |||
)} | |||
{(month && date && year) && (hour && minute) && ' '} | |||
{hour && minute && ( | |||
<IconText | |||
icon={ | |||
<span className="text-[0.6667em]"> | |||
<ClockIcon | |||
hour={Number(hour)} | |||
minute={Number(minute)} | |||
/> | |||
</span> | |||
} | |||
topText={timeFormat.map((f) => f.value).join('')} | |||
bottomText={timeZone} | |||
/> | |||
)} | |||
</time> | |||
</header> | |||
<div className="px-8"> | |||
{content} | |||
</div> | |||
<footer className="flex justify-end px-8 py-4 relative before:absolute before:border-t-2 before:top-0 before:left-0 before:w-full before:opacity-10"> | |||
<div className="w-48"> | |||
<WebNavigationReact.LinkButton | |||
component={Link as unknown as 'a'} | |||
href={{ | |||
pathname: '/blog/articles/[slug]', | |||
query: { | |||
slug, | |||
} | |||
} as unknown as string} | |||
menuItem | |||
block | |||
subtext={title} | |||
> | |||
Read more | |||
</WebNavigationReact.LinkButton> | |||
</div> | |||
</footer> | |||
</article> | |||
); | |||
} |
@@ -0,0 +1,28 @@ | |||
import * as React from 'react'; | |||
export const ClockIconDerivedElementComponent = 'svg' as const; | |||
export type ClockIconDerivedElement = SVGElementTagNameMap[typeof ClockIconDerivedElementComponent]; | |||
export interface ClockIconProps extends React.SVGProps<ClockIconDerivedElement> { | |||
hour: number; | |||
minute: number; | |||
} | |||
export const ClockIcon = React.forwardRef<ClockIconDerivedElement, ClockIconProps>(({ | |||
hour, | |||
minute, | |||
className, | |||
...etcProps | |||
}, forwardedRef) => ( | |||
<ClockIconDerivedElementComponent | |||
{...etcProps} | |||
ref={forwardedRef} | |||
viewBox="0 0 24 24" | |||
className={`w-[1.5em] h-[1.5em] linejoin-round linecap-round stroke-2 stroke-current fill-none ${className ?? ''}`.trim()} | |||
> | |||
<circle r="40%" cx="50%" cy="50%" /> | |||
<line x1="50%" x2="50%" y1="50%" y2="30%" transform={`rotate(${(hour + (minute / 60)) / 12 * 360}, 12, 12)`} /> | |||
<line x1="50%" x2="50%" y1="50%" y2="20%" transform={`rotate(${minute / 60 * 360}, 12, 12)`} /> | |||
</ClockIconDerivedElementComponent> | |||
)); |
@@ -21,7 +21,7 @@ export const EnvisionSection: React.FC<EnvisionSectionProps> = ({ | |||
span="wide" | |||
> | |||
<div className="pb-8"> | |||
<h2 className="text-base leading-none m-0"> | |||
<h2 className="text-base leading-none m-0 font-thin"> | |||
<span className="text-7xl sm:text-8xl"> | |||
I | |||
{' '} | |||
@@ -35,7 +35,7 @@ export const Icon = React.forwardRef<IconDerivedElement, IconProps>(({ | |||
{...etcProps} | |||
ref={forwardedRef} | |||
viewBox="0 0 24 24" | |||
className={`w-6 h-6 linejoin-round linecap-round stroke-2 stroke-current fill-none ${className ?? ''}`.trim()} | |||
className={`w-[1.5em] h-[1.5em] linejoin-round linecap-round stroke-2 stroke-current fill-none ${className ?? ''}`.trim()} | |||
> | |||
{ICONS[name]} | |||
</IconDerivedElementComponent> | |||
@@ -0,0 +1,31 @@ | |||
import * as React from 'react'; | |||
export interface IconTextProps { | |||
icon: React.ReactNode; | |||
topText?: React.ReactNode; | |||
bottomText?: React.ReactNode; | |||
} | |||
export const IconText: React.FC<IconTextProps> = ({ | |||
icon, | |||
topText, | |||
bottomText, | |||
}) => ( | |||
<span className="font-sans align-middle inline-flex flex-col items-center justify-center leading-none w-[1em] h-[1em]"> | |||
<span className="order-2 text-[0.5em] font-light"> | |||
{icon} | |||
</span> | |||
{' '} | |||
{topText && ( | |||
<span className="order-1 uppercase text-[0.25em] font-semibold"> | |||
{topText} | |||
</span> | |||
)} | |||
{topText && bottomText && ' '} | |||
{bottomText && ( | |||
<span className="order-3 uppercase text-[0.25em] font-semibold"> | |||
{bottomText} | |||
</span> | |||
)} | |||
</span> | |||
); |
@@ -3,16 +3,11 @@ import Link from 'next/link'; | |||
import {Layouts} from '@tesseract-design/viewfinder-react'; | |||
import {Brand} from '@/components/molecules/Brand'; | |||
import {Pillar} from '@/components/molecules/Pillar'; | |||
import {BackgroundGrid} from '@/components/molecules/BackgroundGrid'; | |||
import {Icon} from '@/components/molecules/Icon'; | |||
export interface MainLandingSectionProps { | |||
backgroundImages: string[]; | |||
} | |||
export interface MainLandingSectionProps {} | |||
export const MainLandingSection: React.FC<MainLandingSectionProps> = ({ | |||
backgroundImages | |||
}) => { | |||
export const MainLandingSection: React.FC<MainLandingSectionProps> = () => { | |||
const scrollDown: React.MouseEventHandler<HTMLAnchorElement> = React.useCallback((event) => { | |||
event.preventDefault(); | |||
@@ -38,7 +33,7 @@ export const MainLandingSection: React.FC<MainLandingSectionProps> = ({ | |||
className="relative py-24" | |||
> | |||
<div className="flex flex-col sm:flex-row sm:items-center gap-8 sm:gap-16"> | |||
<h1 className="text-center m-0 before:text-7xl sm:before:text-8xl before:block before:content-['I_am.'] text-base leading-none"> | |||
<h1 className="text-center m-0 before:text-7xl before:font-thin sm:before:text-8xl before:block before:content-['I_am.'] text-base leading-none text-4xl"> | |||
<span className="text-4xl"> | |||
<Brand/> | |||
</span> | |||
@@ -43,21 +43,21 @@ export const MakeSection: React.FC<MakeSectionProps> = ({ | |||
className="relative" | |||
> | |||
<div className="pb-8"> | |||
<h2 className="text-base leading-none m-0"> | |||
<span className="text-7xl sm:text-8xl"> | |||
I | |||
{' '} | |||
<Link | |||
className="no-underline px-1 -mx-1" | |||
href={{ | |||
query: { | |||
feed: 'all', | |||
}, | |||
}} | |||
> | |||
make. | |||
</Link> | |||
</span> | |||
<h2 className="text-base leading-none m-0 font-thin"> | |||
<span className="text-7xl sm:text-8xl"> | |||
I | |||
{' '} | |||
<Link | |||
className="no-underline px-1 -mx-1" | |||
href={{ | |||
query: { | |||
feed: 'all', | |||
}, | |||
}} | |||
> | |||
make. | |||
</Link> | |||
</span> | |||
</h2> | |||
<div className="flex gap-2 font-headings lowercase text-3xl sm:text-4xl leading-none font-light"> | |||
<div><Link href={{ query: { feed: 'software' }}} className="p-1 -m-1 after:content-['.']">Software</Link></div> | |||
@@ -1,11 +1,18 @@ | |||
import * as React from 'react'; | |||
import {Layouts, Widgets} from '@tesseract-design/viewfinder-react'; | |||
import Link from 'next/link'; | |||
import * as WebNavigationReact from '@tesseract-design/web-navigation-react'; | |||
import {BlogItem, BlogItemProps} from '@/components/molecules/BlogItem'; | |||
export interface BlogLayoutProps {} | |||
interface SingleBlogItem extends BlogItemProps { | |||
id: string; | |||
} | |||
export const BlogLayout: React.FC<BlogLayoutProps> = () => ( | |||
export interface BlogLayoutProps { | |||
blogItems: SingleBlogItem[]; | |||
} | |||
export const BlogLayout: React.FC<BlogLayoutProps> = ({ | |||
blogItems, | |||
}) => ( | |||
<Layouts.LeftSidebar.Root | |||
sidebarBaseWidget={ | |||
<Widgets.LeftSidebarBase> | |||
@@ -23,41 +30,13 @@ export const BlogLayout: React.FC<BlogLayoutProps> = () => ( | |||
} | |||
> | |||
<Layouts.LeftSidebar.MainContentContainer> | |||
<main className="my-16 flex gap-8"> | |||
<article | |||
className="rounded overflow-hidden relative flex flex-col gap-4 before:absolute before:top-0 before:left-0 before:w-full before:h-full before:border-2 before:rounded-inherit px-4 py-3 before:pointer-events-none before:opacity-10" | |||
> | |||
<header> | |||
<h1 className="m-0"> | |||
Article title | |||
</h1> | |||
<time className="font-headings lowercase font-extralight text-3xl"> | |||
2024 March 5 23:58 | |||
</time> | |||
</header> | |||
<div> | |||
Excerpt lorem ipsum dolor sit amet consectetur adipisicing elit sed do eiusmod aliquam. | |||
Lorem ipsum dolor sit amet consectetur adipisicing elit sed do eiusmod aliquam. | |||
Lorem ipsum dolor sit amet consectetur adipisicing elit sed do eiusmod aliquam. | |||
Lorem ipsum dolor sit amet consectetur adipisicing elit sed do eiusmod aliquam. | |||
Lorem ipsum dolor sit amet consectetur adipisicing elit sed do eiusmod aliquam. | |||
</div> | |||
<footer className="flex justify-end"> | |||
<div className="w-48"> | |||
<WebNavigationReact.LinkButton | |||
component={Link as unknown as 'a'} | |||
href={{ | |||
pathname: '/feed', | |||
} as unknown as string} | |||
menuItem | |||
block | |||
subtext="Article Title" | |||
> | |||
Read more | |||
</WebNavigationReact.LinkButton> | |||
</div> | |||
</footer> | |||
</article> | |||
<main className="py-16 flex flex-col gap-8"> | |||
{blogItems.map((blogItem) => ( | |||
<BlogItem | |||
key={blogItem.id} | |||
{...blogItem} | |||
/> | |||
))} | |||
</main> | |||
</Layouts.LeftSidebar.MainContentContainer> | |||
</Layouts.LeftSidebar.Root> | |||
@@ -2,14 +2,35 @@ import {NextPage} from 'next'; | |||
import {useHuePulsate} from '@/hooks/effects'; | |||
import * as config from '@/config'; | |||
import {BlogLayout} from '@/components/organisms/BlogLayout'; | |||
import * as Iceform from '@modal-sh/iceform-next'; | |||
const BlogPage: NextPage = () => { | |||
interface BlogPageProps { | |||
data: any; | |||
} | |||
const BlogPage: Iceform.NextPage<BlogPageProps> = ({ | |||
data, | |||
}) => { | |||
useHuePulsate(config.effects.huePulsate); | |||
return ( | |||
<BlogLayout | |||
blogItems={data.blog} | |||
/> | |||
); | |||
}; | |||
export const getServerSideProps = Iceform.destination.getServerSideProps({ | |||
fn: async (actionReq, actionRes, context) => { | |||
const { readFile } = await import('fs/promises'); | |||
const dataJson = await readFile('data.json', 'utf-8'); | |||
const data = JSON.parse(dataJson); | |||
return { | |||
props: { | |||
data, | |||
}, | |||
}; | |||
}, | |||
}); | |||
export default BlogPage; |
@@ -465,11 +465,11 @@ a { | |||
} | |||
h1 { | |||
@apply font-headings lowercase text-5xl font-thin leading-none my-8; | |||
@apply font-headings lowercase text-5xl font-extralight leading-none my-8; | |||
} | |||
h2 { | |||
@apply font-headings lowercase text-4xl font-thin leading-none my-8; | |||
@apply font-headings lowercase text-4xl font-extralight leading-none my-8; | |||
} | |||
h3 { | |||