Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
…peacock into update-to-next-14
  • Loading branch information
ProchaLu committed Jan 2, 2024
2 parents d9a5b00 + fb34043 commit 8cfdc7f
Show file tree
Hide file tree
Showing 121 changed files with 1,934 additions and 2,628 deletions.
17 changes: 17 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"extends": "next/core-web-vitals",
"rules": {
"no-console": "error",
"jsx-quotes": [
"error",
"prefer-double"
],
"quotes": [
"error",
"single",
{
"avoidEscape": true
}
]
}
}
Binary file added .github/screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 1 addition & 6 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,4 @@ yarn-error.log*
# vercel
.vercel

.eslintcache
*.tsbuildinfo

/playwright/test-results/
/playwright/report/
/playwright/.cache/
public/rss.xml
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
![Next Starter Peacock](./screenshot.png)
![Next Starter Peacock](./.github/screenshot.png)

# 🦚 Next Starter Peacock

Peacock is a NextJS portfolio Starter for software engineers and designers. Showcase your awesome work and build personal sites you're proud of.

## Features

- Styled with EmotionJS💅🏾
- Written in TypeScript
- Blog, Notes and Work content types 🖊
- Styled with Tailwind CSS
- Written in TypeScript & Next 14 (RSC)
- Blog, Notes and Work content types

## Getting Started

Expand Down Expand Up @@ -98,6 +98,7 @@ The fastest way to get an answer to your question is to reach out via [Twitter](
- [x] Image optimization (Fixed image heights to avoid layout janks)
- [x] RSS Feed
- [ ] Release V1
- [ ] Add page transitions with `react-spring`
- [x] Add page transitions with `framer-motion`
- [ ] Add mdx support
- [x] Code syntax highlighting
- [ ] og image generation with vercel/og
16 changes: 16 additions & 0 deletions app/[contentType]/[slug]/content.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
'use client'
import React, { useEffect } from 'react'

import hljs from 'highlight.js/lib/core';
import javascript from 'highlight.js/lib/languages/javascript';

export default function Content({ html }) {
useEffect(() => {
hljs.registerLanguage('javascript', javascript)
hljs.highlightAll();
}, []);

return (
<div className="content-body mb-8" dangerouslySetInnerHTML={{ __html: html }} />
)
}
157 changes: 157 additions & 0 deletions app/[contentType]/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { Chips, Container } from '@components';
import { IContentType, getContentData, getContentList, getContentTypes } from '@utils/content';
import { CONTENT_TYPES_MAP } from '@utils/content-types';
import { Metadata } from 'next';
import Image from 'next/image';
import { notFound } from 'next/navigation';
import BackButton from '../../../components/back-button';
import { author, site } from '../../../config/index.json';
import Content from './content';

export async function generateMetadata({ params }): Promise<Metadata> {
const { title, previewImage, description } = await getContentData(params.slug, params.contentType)
return {
title: `${title} | ${site.siteTitle}`,
description: description ?? site.siteDescription,
openGraph: {
title: `${title} | ${site.siteName}`,
description: description ?? site.siteDescription,
url: site.siteUrl,
images: previewImage ?? site.siteImage,
siteName: site.siteName,
},
twitter: {
card: 'summary_large_image',
creator: author.twitterHandle,
images: previewImage ?? site.siteImage
}
}
}

/**
* statically generate all content pages
*/
export async function generateStaticParams() {
const contentTypes = getContentTypes();

return contentTypes.map(contentType => {
const contentList = getContentList(contentType);
return contentList.map(({ slug }) => {
return {
contentType,
slug
}
})
}).flat()
}

/**
* Renders articles markdown posts
*/

async function fetchContentData(slug: string, contentType: IContentType) {
return await getContentData(slug, contentType)
}

type Params = {
slug: string
contentType: IContentType
}

export default async function ContentPage({ params }) {
const { slug, contentType } = params as Params

if (!CONTENT_TYPES_MAP.has(contentType)) {
return notFound();
}

const content = await fetchContentData(slug, contentType);
if (content.draft) return notFound();

if (contentType === 'works') return <WorkPage work={content} />

return (
<Container width="narrow">
<header>
<section className="pt-16">
<h1 className="my-0 font-bold font-display mb-2 text-2xl/normal md:text-4xl max-w-xl">{content.title}</h1>
<time className="block text-accent-4 mb-8">{content.date.toString()}</time>
{content.previewImage && (
<Image className="pb-8 block object-cover" src={content.previewImage} height={550} width={1200} alt="" />
)}
</section>
</header>

<Content html={content.contentHtml} />
{content.tags && <Chips items={content.tags} />}
</Container>

);
};

function WorkPage({ work }: { work: IContentData }) {

return (
<Container className="flex flex-col lg:flex-row gap-4 pt-12">
<section className="w-full lg:w-1/3 border-r border-accent-8 p-2 pr-8">
<div className="mb-8 flex flex-col items-start gap-5">
<BackButton />
<h1 className="text-4xl font-bold font-display text-accent-3">{work.title}</h1>
<p className="text-accent-4">{work.description}</p>
<button>Se Demo</button>
</div>

<ul>
<TechStack techStack={work.techStack ?? []} />
<MetadataListItem item="Date" value={work.date.toString()} />
{work.problem && <MetadataListItem item="Problem" value={work.problem ?? ''} />}
</ul>

</section>

<section className="w-full lg:w-2/3 p-2">
<Image src={work.previewImage ?? ''} height={1000} width={1000} alt="" className="mb-4" />
<Content html={work.contentHtml} />
</section>

</Container>
)
}

function MetadataListItem({ item, value }: { item: string, value: string }) {
return (
<li className="list-none flex gap-4 border-b border-accent-8 py-3 text-sm">
<span className="text-accent-4 w-1/3">{item}</span>
<span className="w-2/3 text-accent-2">{value}</span>
</li>

)
}

function TechStack({ techStack }: { techStack: string[] }) {
if (!techStack.length) return null
return (
<li className="list-none flex gap-4 border-b border-accent-8 py-3 text-sm">
<span className="text-accent-4 w-1/3 flex-shrink-0">Tech Stack</span>

<ul className="flex flex-wrap gap-2 flex-grow-0">
{techStack.map(tech =>
<li key={tech} className="select-none bg-accent-8 text-accent-2 px-2 py-1 rounded-md">{tech}</li>
)}
</ul>
</li>
)
}

export interface IContentData {
id: string;
contentHtml: string;
date: Date;
title: string;
previewImage?: string;
description?: string;
tags?: string[];
category?: string;
problem?: string;
techStack?: string[];
}
63 changes: 63 additions & 0 deletions app/[contentType]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { Container, ContentList } from '@components';
import { IContentType, getContentList, getContentTypes } from '@utils/content';
import { CONTENT_TYPES_MAP } from '@utils/content-types';
import { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { site } from '../../config/index.json';
import { generateRSS } from '@utils/rss';

/** generate list page metadata */
export function generateMetadata({ params }): Metadata {
const contentType = CONTENT_TYPES_MAP.get(params.contentType)
return {
title: `${contentType.title} | ${site.siteTitle}`,
description: contentType.description
}
}

export async function generateStaticParams() {
generateRSS();

const contentTypes = getContentTypes();
return Array.from(contentTypes).map(contentType => ({
contentType
}))
}

/**
* Index page `/index`
*/
export default function ContentListPage({ params }) {
const contentType = params.contentType as IContentType

// redirect to 404 with wrong contentType
if (!CONTENT_TYPES_MAP.has(contentType)) {
return notFound();
}

const content = getContentList(contentType);
const isNotes = contentType.toLowerCase() === 'notes';
const { title, path, description } = CONTENT_TYPES_MAP.get(contentType);

return (
<>
<Container width={isNotes ? 'narrow' : 'default'}>
<section className="flex flex-col py-20 gap-2 max-w-2xl">
<h1 className="text-4xl font-bold font-display">
{title}
</h1>
<p className="text-accent-4 text-lg">
{description}
</p>
</section>

<ContentList
basePath={path}
items={content}
contentType={contentType}
/>
</Container>
</>
);
};

41 changes: 41 additions & 0 deletions app/[contentType]/tags/[tag]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@

import { Container, ContentList } from '@components';
import { IContentType, getContentWithTag } from '@utils/content';
import { CONTENT_TYPES_MAP } from '@utils/content-types';

// TODO: I need to rethink the tags page. I don't think I want to have a page for each tag.
// /notes/tags/programming & /articles/tags/programming. Instead I want to have a single page for all tags
// /tags/programming. I think this will be easier to manage and will be more intuitive for readers.

/**
* Index page `/index`
*/
export default function ContentListPage({ params }) {
const contentType = params.contentType as IContentType
const tag = params.tag as IContentType

const content = getContentWithTag(tag, contentType);
const isNotes = contentType.toLowerCase() === 'notes';
const { title, path, description } = CONTENT_TYPES_MAP.get(contentType);

return (
<>
<Container width={isNotes ? 'narrow' : 'default'}>
<section className="flex flex-col py-20 gap-2 max-w-2xl">
<h1 className="text-4xl font-bold font-display">
{title} Tags
</h1>
<p className="text-accent-4 text-lg">
{description}
</p>
</section>
<ContentList
basePath={path}
items={content}
contentType={contentType}
/>
</Container>
</>
);
};

22 changes: 22 additions & 0 deletions app/[contentType]/template.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
'use client'

import React from 'react'
import { motion } from 'framer-motion'

const variants = {
hidden: { opacity: 0, y: 20 },
enter: { opacity: 1, y: 0 }
}

export default function Template({ children }: { children: React.ReactNode }) {
return (
<motion.main
variants={variants}
initial="hidden"
animate="enter"
transition={{ type: 'linear' }}
>
{children}
</motion.main>
);
}
34 changes: 34 additions & 0 deletions app/about/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@

import { Container } from '@components';
import SiteConfig from '../../config/index.json';

export const metadata = {
title: `About Me | ${SiteConfig.site.siteName}`,
description: SiteConfig.site.siteDescription
}

/**
* About page `/about`
*/

export default function About() {
return (
<Container width="narrow">
{/* Content goes here */}
<header className="pt-12 pb-4">
<h1 className="text-3xl font-bold mb-6 md:text-4xl font-display">About Me 🧘🏾‍♂️</h1>
</header>
<div className="flex text-accent-4">
<div>
<p className="mb-4">
Victor is a dynamic and innovative software engineer with over six years of experience in the tech industry. Specializing in full-stack development, Victor combines a deep understanding of backend technologies with a passion for front-end design, creating seamless and efficient user experiences. His journey in the tech world began with a Bachelor&apos;s degree in Computer Science, followed by a rapid ascent through various roles, from junior developer to a lead engineer.
</p>
<p>
Victor&apos;s expertise lies in leveraging the latest technologies to build scalable, responsive web applications and cloud-based solutions. His skill set encompasses a wide range of technologies including JavaScript (React.js, Node.js), Python, and cloud platforms like AWS and Azure.
</p>
</div>
</div>
</Container>
);
};

Binary file added app/favicon.ico
Binary file not shown.
Loading

0 comments on commit 8cfdc7f

Please sign in to comment.