├── .env.example
├── .eslintrc.json
├── .gitignore
├── .prettierignore
├── .prettierrc
├── README.md
├── jsconfig.json
├── next-env.d.js
├── next.config.js
├── package.json
├── pnpm-lock.yaml
├── postcss.config.js
├── public
└── images
│ ├── Cosmic_OGImage.png
│ ├── avatar_4.png
│ └── dev-portfolio.png
├── src
├── app
│ ├── about
│ │ └── page.jsx
│ ├── api
│ │ ├── disable-draft
│ │ │ └── route.js
│ │ └── draft
│ │ │ └── route.js
│ ├── favicon
│ │ ├── android-chrome-192x192.png
│ │ ├── android-chrome-512x512.png
│ │ ├── apple-touch-icon.png
│ │ ├── browserconfig.xml
│ │ ├── favicon-16x16.png
│ │ ├── favicon-32x32.png
│ │ ├── icon.ico
│ │ ├── mstile-150x150.png
│ │ ├── safari-pinned-tab.svg
│ │ └── site.webmanifest
│ ├── layout.jsx
│ ├── not-found.jsx
│ ├── page.jsx
│ ├── posts
│ │ ├── [slug]
│ │ │ └── page.jsx
│ │ └── page.jsx
│ ├── providers.jsx
│ └── works
│ │ ├── [slug]
│ │ └── page.jsx
│ │ └── page.jsx
├── components
│ ├── AlertPreview.jsx
│ ├── CoverImage.jsx
│ ├── Date.jsx
│ ├── DevIcon.jsx
│ ├── FilteredPosts.jsx
│ ├── Footer.jsx
│ ├── Header.jsx
│ ├── Layout.jsx
│ ├── Loader.jsx
│ ├── Logo.jsx
│ ├── MenuItems.jsx
│ ├── MorePosts.jsx
│ ├── Navbar.jsx
│ ├── PageContainer.jsx
│ ├── PostBody.jsx
│ ├── PostHeader.jsx
│ ├── PostList.jsx
│ ├── PostPreview.jsx
│ ├── PostTitle.jsx
│ ├── ProductCard.jsx
│ ├── Socials.jsx
│ ├── ThemeChanger.jsx
│ └── markdown-styles.module.css
├── configs
│ ├── dev-icons.js
│ └── icons.jsx
├── helpers
│ └── getMetadata.js
├── lib
│ └── cosmic.js
├── sections
│ ├── AboutMeSection.jsx
│ ├── ContactSection.jsx
│ ├── IntroSection.jsx
│ ├── PostsSection.jsx
│ ├── ToolboxSection.jsx
│ └── WorksSection.jsx
└── styles
│ └── globals.css
└── tailwind.config.js
/.env.example:
--------------------------------------------------------------------------------
1 | COSMIC_BUCKET_SLUG=
2 | COSMIC_READ_KEY=
3 | COSMIC_PREVIEW_SECRET=
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["next/core-web-vitals", "prettier"]
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # dependencies
2 | /node_modules
3 | /.pnp
4 | .pnp.js
5 |
6 | # testing
7 | /coverage
8 |
9 | # next.js
10 | /.next/
11 | /out/
12 |
13 | # production
14 | /build
15 |
16 | # misc
17 | .DS_Store
18 | *.pem
19 |
20 | # debug
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
25 | # local env files
26 | .env
27 | .env.local
28 | .env.development.local
29 | .env.test.local
30 | .env.production.local
31 |
32 | # vercel
33 | .vercel
34 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | .cache
2 | package.json
3 | package-lock.json
4 | public
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "arrowParens": "avoid",
3 | "semi": false,
4 | "singleQuote": true
5 | }
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Developer Portfolio built with Next.js and Cosmic
2 |
3 | 
4 |
5 | To build this app, we’re going to use the following technologies:
6 |
7 | - [Next.js](https://nextjs.org/docs) - A React framework for production that makes it easy to spin up a full-stack application.
8 | - [Cosmic](https://www.cosmicjs.com/) - A Headless CMS enables the independence of the data (content) layer and gives us the ability to quickly manage template content.
9 | - [Tailwind CSS](https://tailwindcss.com/) - A performant utility-first CSS framework that can be composed directly in your markup.
10 |
11 | ### Links
12 |
13 | - [Read how the template was built](https://www.cosmicjs.com/blog/creating-a-developer-portfolio-with-nextjs-and-cosmic)
14 | - [Install the template](https://www.cosmicjs.com/marketplace/templates/developer-portfolio)
15 | - [View the live demo](https://nextjs-developer-portfolio-cms.vercel.app/)
16 |
17 | ## Getting started
18 |
19 | ### Environment Variables
20 |
21 | You'll need to create an .env file in the root of the project. Log in to Cosmic and from Bucket Settings > API Access take the following values:
22 |
23 | ```
24 | //.env
25 | COSMIC_BUCKET_SLUG=your_cosmic_slug
26 | COSMIC_READ_KEY=your_cosmic_read_key
27 | COSMIC_PREVIEW_SECRET=your_preview_secret
28 | ```
29 |
30 | Install the dependencies by running one of the following commands:
31 |
32 | ```
33 | pnpm install
34 | # or
35 | yarn
36 | # or
37 | npm install
38 | ```
39 |
40 | Then run the development server:
41 |
42 | ```
43 | pnpm dev
44 | # or
45 | yarn dev
46 | # or
47 | npm run dev
48 | ```
49 |
50 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
51 |
52 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file.
53 |
54 | ## Previewing Unpublished Content
55 |
56 | This template supports [Next.js Draft Mode](https://nextjs.org/docs/app/building-your-application/configuring/draft-mode), allowing you to preview unpublished blog posts and works pages from the Cosmic dashboard.
57 |
58 | ### Setup in the codebase
59 |
60 | To enable preview mode, you'll need to set the `COSMIC_PREVIEW_SECRET` environment variable in your `.env` file. This secret can be any random string, though we recommend creating a secure string for security reasons.
61 |
62 | ### Setup in Cosmic
63 |
64 | \*Note that this template currently supports Draft Mode for **Posts** and **Works** Object types.
65 |
66 | In your Cosmic dashboard, go to your **Content** and select **Posts** or **Works** from the list of Object types. Next, select **Object type settings > Additional settings**.
67 |
68 | Start by copying and pasting this URL into the **Preview link** field:
69 |
70 | `https://YOUR_LOCAL_OR_PROD_HOST/api/draft?secret=YOUR_SECRET_PREVIEW_KEY&path=OBJECT_TYPE_PATH&slug=[object_slug]`
71 |
72 | Replace `YOUR_LOCAL_OR_PROD_HOST` with your local host (e.g. `http://localhost:3000`) or production host (e.g. `https://your-app.vercel.app`), and replace `YOUR_SECRET_PREVIEW_KEY` with the secret you set in your `.env` file. Match the `OBJECT_TYPE_PATH` to the Object type you're editing (e.g. `posts` or `works`).
73 |
74 | From here, you can now preview unpublished content from the Cosmic dashboard by clicking the **Preview** button in the righthand sidebar when editing an individual Object.
75 |
76 | ## Deploy on Vercel
77 |
78 |
Use the following button to deploy to Vercel . You will need to add API accesss keys as environment variables. Find these in Bucket Settings > API Access .
79 |
80 |
81 |
82 |
83 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
84 |
85 | Your feedback and contributions are welcome!
86 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "jsx": "react",
4 | "baseUrl": "./src",
5 | "paths": {
6 | "@/components/*": ["components/*"],
7 | "@/lib/*": ["lib/*"],
8 | "@/configs/*": ["configs/*"],
9 | "@/sections/*": ["sections/*"],
10 | "@/styles/*": ["styles/*"]
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/next-env.d.js:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | images: {
3 | domains: ['imgix.cosmicjs.com'],
4 | },
5 | }
6 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "scripts": {
4 | "dev": "next",
5 | "preinstall": "npx only-allow pnpm",
6 | "build": "next build",
7 | "start": "next start",
8 | "lint": "next lint"
9 | },
10 | "dependencies": {
11 | "@cosmicjs/sdk": "^1.0.5",
12 | "@fontsource/public-sans": "^4.5.8",
13 | "date-fns": "2.28.0",
14 | "isomorphic-dompurify": "^1.7.0",
15 | "next": "^13.4.8",
16 | "next-themes": "^0.2.1",
17 | "react": "^18.2.0",
18 | "react-dom": "^18.2.0",
19 | "react-markdown": "^8.0.3",
20 | "sharp": "^0.30.6",
21 | "swr": "^1.3.0"
22 | },
23 | "devDependencies": {
24 | "@tailwindcss/typography": "^0.5.2",
25 | "autoprefixer": "10.4.2",
26 | "eslint": "8.14.0",
27 | "eslint-config-next": "13.4.4",
28 | "eslint-config-prettier": "^8.5.0",
29 | "postcss": "8.4.5",
30 | "prettier": "2.6.2",
31 | "tailwindcss": "^3.3.2"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/images/Cosmic_OGImage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cosmicjs/nextjs-developer-portfolio/5a11b8a87486931b2dd2a2f99c09faa0eec81a0a/public/images/Cosmic_OGImage.png
--------------------------------------------------------------------------------
/public/images/avatar_4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cosmicjs/nextjs-developer-portfolio/5a11b8a87486931b2dd2a2f99c09faa0eec81a0a/public/images/avatar_4.png
--------------------------------------------------------------------------------
/public/images/dev-portfolio.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cosmicjs/nextjs-developer-portfolio/5a11b8a87486931b2dd2a2f99c09faa0eec81a0a/public/images/dev-portfolio.png
--------------------------------------------------------------------------------
/src/app/about/page.jsx:
--------------------------------------------------------------------------------
1 | import Image from 'next/image'
2 | import { getPageBySlug } from '@/lib/cosmic'
3 | import Socials from '@/components/Socials'
4 | import { sanitize } from 'isomorphic-dompurify'
5 | import getMetadata from 'helpers/getMetadata'
6 |
7 | async function getData() {
8 | const pageData = (await getPageBySlug('about-page', 'content,metadata')) || []
9 | return {
10 | pageData,
11 | }
12 | }
13 |
14 | export async function generateMetadata() {
15 | const [pageData, socialData, siteSettings] = await Promise.all([
16 | getPageBySlug('about-page', 'metadata'),
17 | getPageBySlug('social-config', 'metadata'),
18 | getPageBySlug('site-settings', 'metadata'),
19 | ])
20 |
21 | const title = getMetadata(pageData?.metadata?.meta_title)
22 | const description = getMetadata(pageData?.metadata?.meta_description)
23 | const image = getMetadata(
24 | pageData?.metadata?.meta_image?.imgix_url,
25 | siteSettings?.metadata?.default_meta_image?.imgix_url ?? ''
26 | )
27 | const url = getMetadata(`${siteSettings?.metadata?.site_url}/about`)
28 | const twitterHandle = getMetadata(socialData?.metadata?.twitter)
29 |
30 | return {
31 | title: title,
32 | description: description,
33 | image: image,
34 | openGraph: {
35 | title: title,
36 | description: description,
37 | url: url,
38 | images: [
39 | {
40 | url: image,
41 | width: 800,
42 | height: 600,
43 | },
44 | {
45 | url: image,
46 | width: 1800,
47 | height: 1600,
48 | },
49 | ],
50 | locale: 'en_US',
51 | type: 'website',
52 | },
53 | twitter: {
54 | card: 'summary_large_image',
55 | title: title,
56 | description: description,
57 | creator: twitterHandle,
58 | images: [image],
59 | },
60 | }
61 | }
62 |
63 | const AboutPage = async () => {
64 | const data = await getData()
65 | const pageData = data.pageData
66 |
67 | return (
68 | <>
69 |
70 |
71 | {pageData?.metadata.heading}
72 |
73 |
74 | {pageData.metadata.image && (
75 |
76 |
89 |
90 | )}
91 |
105 |
106 |
107 | >
108 | )
109 | }
110 |
111 | export const revalidate = 60
112 | export default AboutPage
113 |
--------------------------------------------------------------------------------
/src/app/api/disable-draft/route.js:
--------------------------------------------------------------------------------
1 | import { draftMode } from 'next/headers'
2 | import { redirect } from 'next/navigation'
3 |
4 | export async function GET(request) {
5 | draftMode().disable()
6 | redirect('/')
7 | }
8 |
--------------------------------------------------------------------------------
/src/app/api/draft/route.js:
--------------------------------------------------------------------------------
1 | // route handler with secret and slug
2 | import { draftMode } from 'next/headers'
3 | import { redirect } from 'next/navigation'
4 | import { getPreviewPostBySlug } from '@/lib/cosmic'
5 |
6 | export async function GET(request) {
7 | // Parse query string parameters
8 | const { searchParams } = new URL(request.url)
9 | const secret = searchParams.get('secret')
10 | const slug = searchParams.get('slug')
11 | const path = searchParams.get('path')
12 |
13 | // Check the secret and next parameters
14 | // This secret should only be known to this route handler and the CMS
15 | if (secret !== process.env.COSMIC_PREVIEW_SECRET || !slug) {
16 | return new Response('Invalid token', { status: 401 })
17 | }
18 |
19 | // Fetch the headless CMS to check if the provided `slug` exists
20 | // getPostBySlug would implement the required fetching logic to the headless CMS
21 | const post = await getPreviewPostBySlug(slug)
22 |
23 | // If the slug doesn't exist prevent draft mode from being enabled
24 | if (!post) {
25 | return new Response('Invalid slug', { status: 401 })
26 | }
27 |
28 | // Enable Draft Mode by setting the cookie
29 | draftMode().enable()
30 |
31 | // Redirect to the path from the fetched post
32 | // We don't redirect to searchParams.slug as that might lead to open redirect vulnerabilities
33 | redirect(`/${path}/${post.slug}`)
34 | }
35 |
--------------------------------------------------------------------------------
/src/app/favicon/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cosmicjs/nextjs-developer-portfolio/5a11b8a87486931b2dd2a2f99c09faa0eec81a0a/src/app/favicon/android-chrome-192x192.png
--------------------------------------------------------------------------------
/src/app/favicon/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cosmicjs/nextjs-developer-portfolio/5a11b8a87486931b2dd2a2f99c09faa0eec81a0a/src/app/favicon/android-chrome-512x512.png
--------------------------------------------------------------------------------
/src/app/favicon/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cosmicjs/nextjs-developer-portfolio/5a11b8a87486931b2dd2a2f99c09faa0eec81a0a/src/app/favicon/apple-touch-icon.png
--------------------------------------------------------------------------------
/src/app/favicon/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | #da532c
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/app/favicon/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cosmicjs/nextjs-developer-portfolio/5a11b8a87486931b2dd2a2f99c09faa0eec81a0a/src/app/favicon/favicon-16x16.png
--------------------------------------------------------------------------------
/src/app/favicon/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cosmicjs/nextjs-developer-portfolio/5a11b8a87486931b2dd2a2f99c09faa0eec81a0a/src/app/favicon/favicon-32x32.png
--------------------------------------------------------------------------------
/src/app/favicon/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cosmicjs/nextjs-developer-portfolio/5a11b8a87486931b2dd2a2f99c09faa0eec81a0a/src/app/favicon/icon.ico
--------------------------------------------------------------------------------
/src/app/favicon/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cosmicjs/nextjs-developer-portfolio/5a11b8a87486931b2dd2a2f99c09faa0eec81a0a/src/app/favicon/mstile-150x150.png
--------------------------------------------------------------------------------
/src/app/favicon/safari-pinned-tab.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
7 |
8 | Created by potrace 1.14, written by Peter Selinger 2001-2017
9 |
10 |
12 |
41 |
72 |
82 |
93 |
124 |
137 |
165 |
175 |
176 |
177 |
--------------------------------------------------------------------------------
/src/app/favicon/site.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "name": "",
3 | "short_name": "",
4 | "icons": [
5 | {
6 | "src": "/android-chrome-192x192.png",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | },
10 | {
11 | "src": "/android-chrome-512x512.png",
12 | "sizes": "512x512",
13 | "type": "image/png"
14 | }
15 | ],
16 | "theme_color": "#ffffff",
17 | "background_color": "#ffffff",
18 | "display": "standalone"
19 | }
20 |
--------------------------------------------------------------------------------
/src/app/layout.jsx:
--------------------------------------------------------------------------------
1 | import '@/styles/globals.css'
2 | import Providers from './providers'
3 | import Header from '@/components/Header'
4 | import AlertPreview from '@/components/AlertPreview'
5 | import Footer from '@/components/Footer'
6 | import { draftMode } from 'next/headers'
7 | import { getSiteSettings } from '@/lib/cosmic'
8 | import getMetadata from 'helpers/getMetadata'
9 |
10 | const siteSettings = await getSiteSettings()
11 | const enableRobots = getMetadata(siteSettings?.metadata?.enable_robots, false)
12 | const siteUrl = getMetadata(siteSettings?.metadata?.site_url)
13 |
14 | export const metadata = {
15 | metadataBase: new URL(siteUrl),
16 | icons: {
17 | icon: '/favicon/icon.ico',
18 | shortcut: '/favicon/shortcut-icon.png',
19 | apple: '/favicon/apple-touch-icon.png',
20 | },
21 | viewport: {
22 | width: 'device-width',
23 | initialScale: 1,
24 | maximumScale: 1,
25 | },
26 | robots: {
27 | index: enableRobots,
28 | follow: enableRobots,
29 | nocache: enableRobots,
30 | googleBot: {
31 | index: enableRobots,
32 | follow: enableRobots,
33 | noimageindex: enableRobots,
34 | 'max-video-preview': -1,
35 | 'max-image-preview': 'large',
36 | 'max-snippet': -1,
37 | },
38 | },
39 | }
40 |
41 | export default function RootLayout({ children }) {
42 | const { isEnabled } = draftMode()
43 |
44 | return (
45 |
46 |
47 |
51 |
52 |
53 |
54 |
55 |
56 | {isEnabled && }
57 |
58 | {children}
59 |
60 |
61 |
62 |
63 |
64 | )
65 | }
66 |
--------------------------------------------------------------------------------
/src/app/not-found.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Link from 'next/link'
3 | import { HomeIcon } from '@/configs/icons'
4 |
5 | export default function NotFound() {
6 | return (
7 | <>
8 |
9 |
Page Not Found
10 |
11 |
12 |
13 |
14 | Return Home
15 |
16 |
17 | >
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/src/app/page.jsx:
--------------------------------------------------------------------------------
1 | import { getAllPosts, getPageBySlug } from '@/lib/cosmic'
2 | import IntroSection from '@/sections/IntroSection'
3 | import AboutMeSection from '@/sections/AboutMeSection'
4 | import ToolboxSection from '@/sections/ToolboxSection'
5 | import WorksSection from '@/sections/WorksSection'
6 | import PostsSection from '@/sections/PostsSection'
7 | import ContactSection from '@/sections/ContactSection'
8 | import { draftMode } from 'next/headers'
9 | import getMetadata from 'helpers/getMetadata'
10 |
11 | async function getData() {
12 | const { isEnabled } = draftMode()
13 | const [allPosts, allWorks, pageData] = await Promise.all([
14 | getAllPosts(isEnabled, 'posts', 3) || [],
15 | getAllPosts(isEnabled, 'works', 3) || [],
16 | getPageBySlug('home-page', 'metadata'),
17 | ])
18 | return {
19 | allPosts,
20 | allWorks,
21 | pageData,
22 | }
23 | }
24 |
25 | export async function generateMetadata() {
26 | const [pageData, socialData, siteSettings] = await Promise.all([
27 | getPageBySlug('home-page', 'metadata'),
28 | getPageBySlug('social-config', 'metadata'),
29 | getPageBySlug('site-settings', 'metadata'),
30 | ])
31 |
32 | const title = getMetadata(pageData?.metadata?.meta_title)
33 | const description = getMetadata(pageData?.metadata?.meta_description)
34 | const image = getMetadata(
35 | pageData?.metadata?.meta_image?.imgix_url,
36 | siteSettings?.metadata?.default_meta_image?.imgix_url ?? ''
37 | )
38 | const url = getMetadata(siteSettings?.metadata?.site_url)
39 | const twitterHandle = getMetadata(socialData?.metadata?.twitter)
40 |
41 | return {
42 | title: title,
43 | description: description,
44 | image: image,
45 | openGraph: {
46 | title: title,
47 | description: description,
48 | url: url,
49 | images: [
50 | {
51 | url: image,
52 | width: 800,
53 | height: 600,
54 | },
55 | {
56 | url: image,
57 | width: 1800,
58 | height: 1600,
59 | },
60 | ],
61 | locale: 'en_US',
62 | type: 'website',
63 | },
64 | twitter: {
65 | card: 'summary_large_image',
66 | title: title,
67 | description: description,
68 | creator: twitterHandle,
69 | images: [image],
70 | },
71 | }
72 | }
73 |
74 | const HomePage = async () => {
75 | const data = await getData()
76 | const allPosts = data.allPosts
77 | const allWorks = data.allWorks
78 | const pageData = data.pageData
79 |
80 | return (
81 | <>
82 |
88 |
89 |
90 |
91 |
92 |
97 | >
98 | )
99 | }
100 | export const revalidate = 60
101 | export default HomePage
102 |
--------------------------------------------------------------------------------
/src/app/posts/[slug]/page.jsx:
--------------------------------------------------------------------------------
1 | import PostBody from '@/components/PostBody'
2 | import PostHeader from '@/components/PostHeader'
3 | import { getPostAndMorePosts, getPageBySlug } from '@/lib/cosmic'
4 | import { notFound } from 'next/navigation'
5 | import { draftMode } from 'next/headers'
6 | import getMetadata from 'helpers/getMetadata'
7 |
8 | export async function generateMetadata({ params }) {
9 | const [getData, socialData, siteSettings] = await Promise.all([
10 | getPostAndMorePosts(params.slug),
11 | getPageBySlug('social-config', 'metadata'),
12 | getPageBySlug('site-settings', 'metadata'),
13 | ])
14 |
15 | const currentPage = 'posts'
16 |
17 | const title = getMetadata(getData?.post?.title)
18 | const description = getMetadata(getData?.post?.metadata?.excerpt)
19 | const image = getMetadata(
20 | getData?.post?.metadata?.cover_image?.imgix_url,
21 | siteSettings?.metadata?.default_meta_image?.imgix_url ?? ''
22 | )
23 | const url = getMetadata(
24 | `${siteSettings?.metadata.site_url}/${currentPage}/${params.slug}`
25 | )
26 | const twitterHandle = getMetadata(socialData?.metadata?.twitter)
27 |
28 | return {
29 | title: title,
30 | description: description,
31 | image: image,
32 | openGraph: {
33 | title: title,
34 | description: description,
35 | url: url,
36 | images: [
37 | {
38 | url: image,
39 | width: 800,
40 | height: 600,
41 | },
42 | {
43 | url: image,
44 | width: 1800,
45 | height: 1600,
46 | },
47 | ],
48 | locale: 'en_US',
49 | type: 'website',
50 | },
51 | twitter: {
52 | card: 'summary_large_image',
53 | title: title,
54 | description: description,
55 | creator: twitterHandle,
56 | images: [image],
57 | },
58 | }
59 | }
60 |
61 | const SinglePost = async ({ params }) => {
62 | const { isEnabled } = draftMode()
63 | const getData = await getPostAndMorePosts(params.slug, isEnabled)
64 |
65 | if (!getData) {
66 | return notFound()
67 | }
68 |
69 | const post = getData.post
70 |
71 | return (
72 | <>
73 |
74 |
75 |
76 |
77 | >
78 | )
79 | }
80 | export default SinglePost
81 |
--------------------------------------------------------------------------------
/src/app/posts/page.jsx:
--------------------------------------------------------------------------------
1 | import { getAllPosts, getAllCategories, getPageBySlug } from '@/lib/cosmic'
2 | import FilteredPosts from '@/components/FilteredPosts'
3 | import { draftMode } from 'next/headers'
4 | import getMetadata from 'helpers/getMetadata'
5 |
6 | async function getData() {
7 | const { isEnabled } = draftMode()
8 | const [allPosts, allPostCategories] = await Promise.all([
9 | getAllPosts(isEnabled, 'posts') || [],
10 | getAllCategories('post-categories') || [],
11 | ])
12 | return {
13 | allPosts,
14 | allPostCategories,
15 | }
16 | }
17 |
18 | export async function generateMetadata() {
19 | const [socialData, siteSettings] = await Promise.all([
20 | getPageBySlug('social-config', 'metadata'),
21 | getPageBySlug('site-settings', 'metadata'),
22 | ])
23 |
24 | const title = 'Posts | Developer Portfolio'
25 | const description = 'The blog posts of this developer'
26 | const image = getMetadata(
27 | siteSettings?.metadata?.default_meta_image?.imgix_url
28 | )
29 | const url = getMetadata(`${siteSettings?.metadata.site_url}/posts`)
30 | const twitterHandle = getMetadata(socialData?.metadata?.twitter)
31 |
32 | return {
33 | title: title,
34 | description: description,
35 | image: image,
36 | openGraph: {
37 | title: title,
38 | description: description,
39 | url: url,
40 | images: [
41 | {
42 | url: image,
43 | width: 800,
44 | height: 600,
45 | },
46 | {
47 | url: image,
48 | width: 1800,
49 | height: 1600,
50 | },
51 | ],
52 | locale: 'en_US',
53 | type: 'website',
54 | },
55 | twitter: {
56 | card: 'summary_large_image',
57 | title: title,
58 | description: description,
59 | creator: twitterHandle,
60 | images: [image],
61 | },
62 | }
63 | }
64 |
65 | const PostsPage = async () => {
66 | const data = await getData()
67 | const allPosts = data.allPosts
68 | const allPostCategories = data.allPostCategories
69 |
70 | return (
71 | <>
72 |
73 | Posts
74 |
75 |
80 | >
81 | )
82 | }
83 |
84 | export const revalidate = 60
85 | export default PostsPage
86 |
--------------------------------------------------------------------------------
/src/app/providers.jsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { ThemeProvider } from 'next-themes'
3 |
4 | export default function Providers({ children }) {
5 | return (
6 |
11 | {children}
12 |
13 | )
14 | }
15 |
--------------------------------------------------------------------------------
/src/app/works/[slug]/page.jsx:
--------------------------------------------------------------------------------
1 | import PostBody from '@/components/PostBody'
2 | import PostHeader from '@/components/PostHeader'
3 | import { getPostAndMorePosts, getPageBySlug } from '@/lib/cosmic'
4 | import AlertPreview from '@/components/AlertPreview'
5 | import { draftMode } from 'next/headers'
6 | import { notFound } from 'next/navigation'
7 | import getMetadata from 'helpers/getMetadata'
8 |
9 | export async function generateMetadata({ params }) {
10 | const [getData, socialData, siteSettings] = await Promise.all([
11 | getPostAndMorePosts(params.slug),
12 | getPageBySlug('social-config', 'metadata'),
13 | getPageBySlug('site-settings', 'metadata'),
14 | ])
15 | const currentPage = 'works'
16 |
17 | const title = getMetadata(getData?.post?.title)
18 | const description = getMetadata(getData?.post?.metadata?.excerpt)
19 | const image = getMetadata(
20 | getData?.post?.metadata?.cover_image?.imgix_url,
21 | siteSettings?.metadata?.default_meta_image?.imgix_url ?? ''
22 | )
23 | const url = getMetadata(
24 | `${siteSettings?.metadata.site_url}/${currentPage}/${params.slug}`
25 | )
26 | const twitterHandle = getMetadata(socialData?.metadata?.twitter)
27 |
28 | return {
29 | title: title,
30 | description: description,
31 | image: image,
32 | openGraph: {
33 | title: title,
34 | description: description,
35 | url: url,
36 | images: [
37 | {
38 | url: image,
39 | width: 800,
40 | height: 600,
41 | },
42 | {
43 | url: image,
44 | width: 1800,
45 | height: 1600,
46 | },
47 | ],
48 | locale: 'en_US',
49 | type: 'website',
50 | },
51 | twitter: {
52 | card: 'summary_large_image',
53 | title: title,
54 | description: description,
55 | creator: twitterHandle,
56 | images: [image],
57 | },
58 | }
59 | }
60 |
61 | const SingleWork = async ({ params }) => {
62 | const { isEnabled } = draftMode()
63 | const getData = await getPostAndMorePosts(params.slug, isEnabled)
64 |
65 | if (!getData) {
66 | return notFound()
67 | }
68 |
69 | const post = getData.post
70 |
71 | return (
72 | <>
73 |
74 | {post.status === 'draft' ? : undefined}
75 |
76 |
77 |
78 | >
79 | )
80 | }
81 | export default SingleWork
82 |
--------------------------------------------------------------------------------
/src/app/works/page.jsx:
--------------------------------------------------------------------------------
1 | import { getAllPosts, getAllCategories, getPageBySlug } from '@/lib/cosmic'
2 | import FilteredPosts from '@/components/FilteredPosts'
3 | import { draftMode } from 'next/headers'
4 | import getMetadata from 'helpers/getMetadata'
5 |
6 | async function getData() {
7 | const { isEnabled } = draftMode()
8 | const [allPosts, allWorkCategories] = await Promise.all([
9 | getAllPosts(isEnabled, 'works') || [],
10 | getAllCategories('work-categories') || [],
11 | ])
12 | return {
13 | allPosts,
14 | allWorkCategories,
15 | }
16 | }
17 |
18 | export async function generateMetadata() {
19 | const [socialData, siteSettings] = await Promise.all([
20 | getPageBySlug('social-config', 'metadata'),
21 | getPageBySlug('site-settings', 'metadata'),
22 | ])
23 |
24 | const title = 'Works | Developer Portfolio'
25 | const description = 'The projects of this developer'
26 | const image = getMetadata(
27 | siteSettings?.metadata?.default_meta_image?.imgix_url
28 | )
29 | const url = getMetadata(`${siteSettings?.metadata.site_url}/works`)
30 | const twitterHandle = getMetadata(socialData?.metadata?.twitter)
31 |
32 | return {
33 | title: title,
34 | description: description,
35 | image: image,
36 | openGraph: {
37 | title: title,
38 | description: description,
39 | url: url,
40 | images: [
41 | {
42 | url: image,
43 | width: 800,
44 | height: 600,
45 | },
46 | {
47 | url: image,
48 | width: 1800,
49 | height: 1600,
50 | },
51 | ],
52 | locale: 'en_US',
53 | type: 'website',
54 | },
55 | twitter: {
56 | card: 'summary_large_image',
57 | title: title,
58 | description: description,
59 | creator: twitterHandle,
60 | images: [image],
61 | },
62 | }
63 | }
64 |
65 | const WorksPage = async () => {
66 | const data = await getData()
67 | const allPosts = data.allPosts
68 | const allWorkCategories = data.allWorkCategories
69 |
70 | return (
71 | <>
72 |
73 | Works
74 |
75 |
80 | >
81 | )
82 | }
83 |
84 | export const revalidate = 60
85 | export default WorksPage
86 |
--------------------------------------------------------------------------------
/src/components/AlertPreview.jsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import Link from 'next/link'
3 | import { useState } from 'react'
4 | import { useRouter } from 'next/navigation'
5 |
6 | const AlertPreview = ({ enabled }) => {
7 | const router = useRouter()
8 | const [show, setShow] = useState(enabled)
9 |
10 | const handleExit = () => {
11 | router.refresh()
12 | setShow(false)
13 | }
14 | return (
15 | show && (
16 |
17 |
18 |
19 | You're in preview mode.{' '}
20 | handleExit()}
22 | href="/api/disable-draft"
23 | className="underline hover:text-accent transition-colors cursor-pointer"
24 | >
25 | Click here
26 | {' '}
27 | to exit.
28 |
29 |
30 |
31 | )
32 | )
33 | }
34 | export default AlertPreview
35 |
--------------------------------------------------------------------------------
/src/components/CoverImage.jsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 |
3 | const CoverImage = ({ title, url }) => {
4 | return (
5 |
6 |
18 |
19 | );
20 | }
21 | export default CoverImage
22 |
--------------------------------------------------------------------------------
/src/components/Date.jsx:
--------------------------------------------------------------------------------
1 | import { parseISO, format } from 'date-fns'
2 |
3 | const Date = ({ dateString, formatStyle }) => {
4 | const date = parseISO(dateString)
5 | return {format(date, formatStyle)}
6 | }
7 | export default Date
8 |
--------------------------------------------------------------------------------
/src/components/DevIcon.jsx:
--------------------------------------------------------------------------------
1 | const DevIcon = ({ iconName, name }) => {
2 | return (
3 |
4 |
5 | {name}
6 |
7 | )
8 | }
9 | export default DevIcon
10 |
--------------------------------------------------------------------------------
/src/components/FilteredPosts.jsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { useState } from 'react'
3 | import PostList from './PostList'
4 |
5 | const FilteredPosts = ({ posts, categories, postType }) => {
6 | const [filterCategory, setFilterCategory] = useState('All')
7 |
8 | const filteredPosts = posts?.filter(
9 | post => post.metadata.category.title === filterCategory
10 | )
11 | return (
12 | <>
13 |
14 | setFilterCategory('All')}
21 | key={'All'}
22 | >
23 | All
24 |
25 | {categories.map(category => (
26 | setFilterCategory(category.title)}
33 | key={category.title}
34 | >
35 | {category.title}
36 |
37 | ))}
38 |
39 |
44 | >
45 | )
46 | }
47 |
48 | export default FilteredPosts
49 |
--------------------------------------------------------------------------------
/src/components/Footer.jsx:
--------------------------------------------------------------------------------
1 | import { CosmicLogo } from '@/configs/icons'
2 | import MenuItems from './MenuItems'
3 |
4 | const Footer = () => {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 |
12 | © {new Date().getFullYear()} Developer Portfolio All Rights
13 | Reserved.
14 |
15 |
16 | Powered by
17 |
23 |
24 |
25 |
26 |
27 |
28 | )
29 | }
30 | export default Footer
31 |
--------------------------------------------------------------------------------
/src/components/Header.jsx:
--------------------------------------------------------------------------------
1 | import MenuItems from './MenuItems'
2 | import ThemeChanger from './ThemeChanger'
3 | import Logo from './Logo'
4 | import Navbar from './Navbar'
5 |
6 | const Header = () => {
7 | return (
8 | <>
9 |
20 |
21 |
25 |
26 |
27 |
32 |
33 |
34 |
35 | >
36 | )
37 | }
38 | export default Header
39 |
--------------------------------------------------------------------------------
/src/components/Layout.jsx:
--------------------------------------------------------------------------------
1 | import Footer from './Footer'
2 | import { Meta } from './Meta'
3 | import Header from './Header'
4 | import AlertPreview from './AlertPreview'
5 |
6 | const Layout = ({ children, preview }) => {
7 | return (
8 | <>
9 |
10 |
11 | {preview && }
12 |
13 | {children}
14 |
15 |
16 | >
17 | )
18 | }
19 | export default Layout
20 |
--------------------------------------------------------------------------------
/src/components/Loader.jsx:
--------------------------------------------------------------------------------
1 | const Loader = () => {
2 | return (
3 |
4 |
10 |
18 |
23 |
24 | Loading...
25 |
26 | )
27 | }
28 | export default Loader
29 |
--------------------------------------------------------------------------------
/src/components/Logo.jsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link'
2 |
3 | const Logo = () => {
4 | return (
5 | (
9 |
10 |
11 | cosmicjs
12 | .com
13 |
14 |
15 | )
16 | );
17 | }
18 | export default Logo
19 |
--------------------------------------------------------------------------------
/src/components/MenuItems.jsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import Link from 'next/link'
3 | import { usePathname } from 'next/navigation'
4 |
5 | export const routes = [
6 | {
7 | path: '/',
8 | label: 'Home',
9 | },
10 | {
11 | path: '/posts',
12 | label: 'Posts',
13 | },
14 | {
15 | path: '/works',
16 | label: 'Works',
17 | },
18 | {
19 | path: '/about',
20 | label: 'About',
21 | },
22 | ]
23 |
24 | const MenuItems = () => {
25 | const removeFocus = e => {
26 | e.currentTarget.blur()
27 | }
28 | const currentRoute = usePathname()
29 | return (
30 | <>
31 |
32 | {routes.map(route => (
33 |
43 | {route.label}
44 |
45 | ))}
46 |
47 | >
48 | )
49 | }
50 | export default MenuItems
51 |
--------------------------------------------------------------------------------
/src/components/MorePosts.jsx:
--------------------------------------------------------------------------------
1 | import PostPreview from './PostPreview'
2 |
3 | const MorePosts = ({ posts }) => {
4 | return (
5 |
6 |
7 | More Stories
8 |
9 |
10 | {posts.map(post => (
11 | //
17 | <>>
18 | ))}
19 |
20 |
21 | )
22 | }
23 | export default MorePosts
24 |
--------------------------------------------------------------------------------
/src/components/Navbar.jsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { useEffect, useState } from 'react'
3 | import { CloseIcon, MenuIcon } from '@/configs/icons'
4 | import ThemeChanger from './ThemeChanger'
5 | import Logo from './Logo'
6 | import { routes } from './MenuItems'
7 | import Link from 'next/link'
8 | import { usePathname } from 'next/navigation'
9 |
10 | const Navbar = () => {
11 | const [navOpen, setNavOpen] = useState(false)
12 | const currentPage = usePathname()
13 |
14 | useEffect(() => {
15 | const body = document.body
16 |
17 | if (navOpen) {
18 | body.style.setProperty('touch-action', 'none')
19 | }
20 |
21 | if (!navOpen) {
22 | body.style.removeProperty('touch-action')
23 | }
24 | }, [navOpen])
25 |
26 | useEffect(() => {
27 | if (navOpen) {
28 | setNavOpen(!navOpen)
29 | }
30 | }, [currentPage])
31 |
32 | return (
33 |
34 | {
38 | setNavOpen(!navOpen)
39 | }}
40 | >
41 | {!navOpen ? : }
42 |
43 | {!navOpen ? (
44 |
45 |
46 |
47 | ) : (
48 |
49 |
50 | {routes.map(route => (
51 |
55 |
56 | {route.label}
57 |
58 |
59 | ))}
60 |
61 |
62 |
63 |
64 |
65 | )}
66 |
67 | )
68 | }
69 |
70 | export default Navbar
71 |
--------------------------------------------------------------------------------
/src/components/PageContainer.jsx:
--------------------------------------------------------------------------------
1 | const PageContainer = ({ children }) => {
2 | return {children}
3 | }
4 | export default PageContainer
5 |
--------------------------------------------------------------------------------
/src/components/PostBody.jsx:
--------------------------------------------------------------------------------
1 | import markdownStyles from './markdown-styles.module.css'
2 | import ReactMarkdown from 'react-markdown'
3 | import Image from "next/image";
4 |
5 | const components = {
6 | a: a => {
7 | return (
8 |
9 | {a.children}
10 |
11 | )
12 | },
13 | img: img => {
14 | return (
15 |
28 | );
29 | },
30 | }
31 |
32 | const PostBody = ({ content }) => {
33 | return (
34 |
35 |
39 | {content}
40 |
41 |
42 | )
43 | }
44 | export default PostBody
45 |
--------------------------------------------------------------------------------
/src/components/PostHeader.jsx:
--------------------------------------------------------------------------------
1 | import Date from './Date'
2 | import CoverImage from './CoverImage'
3 | import PostTitle from './PostTitle'
4 | import { ExternalLinkIcon } from '@/configs/icons'
5 | import Image from "next/image";
6 | import avatar from '../../public/images/avatar_4.png'
7 |
8 | const PostHeader = ({ post }) => {
9 | return <>
10 | {post.title}
11 |
12 |
13 |
24 |
25 | Stefan Kudla |{' '}
26 | |{' '}
27 | {post.metadata.category.title}
28 |
29 |
30 |
31 |
35 |
66 | >;
67 | }
68 | export default PostHeader
69 |
--------------------------------------------------------------------------------
/src/components/PostList.jsx:
--------------------------------------------------------------------------------
1 | import Date from './Date'
2 | import Link from 'next/link'
3 | import { ForwardArrowIcon } from '@/configs/icons'
4 |
5 | const PostList = ({ allPosts, postType, home }) => {
6 | return <>
7 |
63 | >;
64 | }
65 | export default PostList
66 |
--------------------------------------------------------------------------------
/src/components/PostPreview.jsx:
--------------------------------------------------------------------------------
1 | import CoverImage from './CoverImage'
2 | import Link from 'next/link'
3 |
4 | const PostPreview = ({ title, excerpt, slug }) => {
5 | return (
6 |
7 |
8 | {/* */}
9 |
10 |
11 |
12 | {title}
13 |
14 |
15 |
{excerpt}
16 |
17 | );
18 | }
19 | export default PostPreview
20 |
--------------------------------------------------------------------------------
/src/components/PostTitle.jsx:
--------------------------------------------------------------------------------
1 | const PostTitle = ({ children }) => {
2 | return (
3 |
4 | {children}
5 |
6 | )
7 | }
8 | export default PostTitle
9 |
--------------------------------------------------------------------------------
/src/components/ProductCard.jsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 |
3 | const ProductCard = ({ productName, imgPath, description, productLink }) => {
4 | return (
5 |
9 |
10 |
11 |
20 |
21 |
22 |
{productName}
23 |
{description}
24 |
25 |
26 |
27 | );
28 | }
29 |
30 | export default ProductCard
31 |
--------------------------------------------------------------------------------
/src/components/Socials.jsx:
--------------------------------------------------------------------------------
1 | import { EmailIcon, GithubIcon, LinkedinIcon, PaperIcon } from '@/configs/icons'
2 |
3 | const Socials = ({ resume, email, github, linkedin }) => {
4 | return (
5 |
44 | )
45 | }
46 | export default Socials
47 |
--------------------------------------------------------------------------------
/src/components/ThemeChanger.jsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { useEffect, useState } from 'react'
3 | import { useTheme } from 'next-themes'
4 |
5 | const ThemeChanger = ({ styles }) => {
6 | const [mounted, setMounted] = useState(false)
7 | const { resolvedTheme, setTheme } = useTheme()
8 |
9 | useEffect(() => setMounted(true), [])
10 |
11 | if (!mounted) return null
12 |
13 | return (
14 | {
22 | setTheme(resolvedTheme === 'dark' ? 'light' : 'dark')
23 | }}
24 | className={styles}
25 | >
26 | {resolvedTheme === 'dark' ? (
27 |
28 | ) : (
29 |
30 | )}
31 |
32 | )
33 | }
34 |
35 | export default ThemeChanger
36 |
--------------------------------------------------------------------------------
/src/components/markdown-styles.module.css:
--------------------------------------------------------------------------------
1 | .markdown {
2 | @apply text-lg leading-relaxed;
3 | }
4 |
5 | .markdown p,
6 | .markdown ul,
7 | .markdown ol,
8 | .markdown blockquote {
9 | @apply my-6 text-fore-secondary text-[16px] md:text-[18px];
10 | }
11 |
12 | .markdown h2 {
13 | @apply text-fore-primary text-2xl font-bold mt-12 mb-4 leading-snug;
14 | }
15 |
16 | .markdown h3 {
17 | @apply text-fore-primary text-xl font-bold mt-8 mb-4 leading-snug;
18 | }
19 |
20 | .markdown p a {
21 | @apply text-accent underline hover:text-opacity-70;
22 | }
23 |
24 | .markdown ul li {
25 | @apply list-disc list-inside mb-2 bg-back-subtle p-2 rounded text-[16px] md:text-[18px] font-semibold;
26 | }
27 |
28 | .markdown ol li {
29 | @apply list-decimal list-inside mb-2 bg-back-subtle p-2 rounded text-[16px] md:text-[18px] font-semibold;
30 | }
31 |
--------------------------------------------------------------------------------
/src/configs/dev-icons.js:
--------------------------------------------------------------------------------
1 | export const devIcons = [
2 | { iconName: 'devicon-html5-plain', name: 'HTML' },
3 | { iconName: 'devicon-css3-plain', name: 'CSS' },
4 | { iconName: 'devicon-javascript-plain', name: 'JavaScript' },
5 | { iconName: 'devicon-typescript-original', name: 'TypeScript' },
6 | { iconName: 'devicon-nodejs-plain', name: 'Node.js' },
7 | { iconName: 'devicon-react-original', name: 'React.js' },
8 | { iconName: 'devicon-nextjs-original', name: 'Next.js' },
9 | { iconName: 'devicon-tailwindcss-plain', name: 'Tailwind CSS' },
10 | { iconName: 'devicon-npm-original-wordmark', name: 'NPM' },
11 | { iconName: 'devicon-wordpress-plain', name: 'WordPress' },
12 | { iconName: 'devicon-webpack-plain', name: 'Webpack' },
13 | { iconName: 'devicon-babel-original', name: 'Babel' },
14 | { iconName: 'devicon-git-plain', name: 'Git' },
15 | { iconName: 'devicon-graphql-plain', name: 'GraphQL' },
16 | { iconName: 'devicon-firebase-plain', name: 'Firebase' },
17 | { iconName: 'devicon-figma-plain', name: 'Figma' },
18 | ]
19 |
--------------------------------------------------------------------------------
/src/configs/icons.jsx:
--------------------------------------------------------------------------------
1 | // Aesthetic Icons
2 |
3 | export const PencilIcon = () => {
4 | return (
5 |
15 |
16 |
17 | )
18 | }
19 |
20 | export const ToolboxIcon = () => {
21 | return (
22 |
32 |
33 |
34 | )
35 | }
36 |
37 | export const FlaskIcon = () => {
38 | return (
39 |
49 |
50 |
51 | )
52 | }
53 |
54 | export const LetterIcon = () => {
55 | return (
56 |
66 |
67 |
68 | )
69 | }
70 |
71 | export const PaperIcon = () => {
72 | return (
73 |
82 |
83 |
84 | )
85 | }
86 |
87 | export const DeskSetupIcon = () => {
88 | return (
89 |
99 |
100 |
101 | )
102 | }
103 |
104 | export const CoffeePotIcon = () => {
105 | return (
106 |
116 |
117 |
118 | )
119 | }
120 |
121 | export const HeadphonesIcon = () => {
122 | return (
123 |
133 |
134 |
135 | )
136 | }
137 |
138 | export const SpotifyIcon = () => {
139 | return (
140 |
149 |
150 |
151 | )
152 | }
153 |
154 | // Social Icons
155 |
156 | export const GithubIcon = () => {
157 | return (
158 |
170 |
171 |
172 | )
173 | }
174 |
175 | export const LinkedinIcon = () => {
176 | return (
177 |
189 |
190 |
191 |
192 |
193 | )
194 | }
195 |
196 | export const EmailIcon = () => {
197 | return (
198 |
208 |
209 |
210 |
211 | )
212 | }
213 |
214 | export const CosmicLogo = ({ width, height }) => {
215 | const iconColor = 'fill-[#29abe2]'
216 | return (
217 |
226 |
230 |
234 |
238 |
242 |
246 |
250 |
254 |
258 |
262 |
266 |
270 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 | )
282 | }
283 |
284 | // Semantic Icons
285 |
286 | export const ForwardArrowIcon = () => {
287 | return (
288 |
297 |
298 |
299 |
300 | )
301 | }
302 |
303 | export const MenuIcon = () => {
304 | return (
305 |
313 |
319 |
320 | )
321 | }
322 |
323 | export const CloseIcon = () => {
324 | return (
325 |
333 |
339 |
340 | )
341 | }
342 |
343 | export const HomeIcon = () => {
344 | return (
345 |
354 |
355 |
356 | )
357 | }
358 |
359 | export const ExternalLinkIcon = () => {
360 | return (
361 |
370 |
371 |
372 |
373 | )
374 | }
375 |
--------------------------------------------------------------------------------
/src/helpers/getMetadata.js:
--------------------------------------------------------------------------------
1 | const getMetadata = (object, fallback = '') => {
2 | return object ?? fallback
3 | }
4 | export default getMetadata
5 |
--------------------------------------------------------------------------------
/src/lib/cosmic.js:
--------------------------------------------------------------------------------
1 | const { createBucketClient } = require('@cosmicjs/sdk')
2 |
3 | const BUCKET_SLUG = process.env.COSMIC_BUCKET_SLUG
4 | const READ_KEY = process.env.COSMIC_READ_KEY
5 |
6 | const cosmic = createBucketClient({
7 | bucketSlug: BUCKET_SLUG,
8 | readKey: READ_KEY,
9 | })
10 |
11 | const is404 = error => /not found/i.test(error.message)
12 |
13 | export async function getPreviewPostBySlug(slug) {
14 | try {
15 | const data = await cosmic.objects
16 | .findOne({
17 | slug: slug,
18 | })
19 | .props('title,slug,metadata')
20 | .status('any')
21 | return data.object
22 | } catch (error) {
23 | if (is404(error)) return
24 | throw error
25 | }
26 | }
27 |
28 | export async function getAllPosts(preview, postType, postCount) {
29 | try {
30 | const data = await cosmic.objects
31 | .find({
32 | type: postType,
33 | })
34 | .props(
35 | 'title,slug,metadata.category,metadata.excerpt,metadata.published_date,created_at,status'
36 | )
37 | .limit(postCount)
38 | .sort('-created_at')
39 | .status(preview ? 'any' : 'published')
40 | .depth(1)
41 | return data.objects
42 | } catch (error) {
43 | if (is404(error)) return
44 | throw error
45 | }
46 | }
47 |
48 | export async function getAllPostsWithSlug() {
49 | try {
50 | const data = await cosmic.objects.find({
51 | type: 'posts',
52 | props: 'title,slug,metadata,created_at',
53 | })
54 | return data.objects
55 | } catch (error) {
56 | if (is404(error)) return
57 | throw error
58 | }
59 | }
60 |
61 | export async function getPostAndMorePosts(slug, preview) {
62 | try {
63 | const data = await cosmic.objects
64 | .findOne({
65 | slug: slug,
66 | })
67 | .props('slug,title,metadata,created_at,status')
68 | .status(preview ? 'any' : 'published')
69 |
70 | const moreObjects = await cosmic.objects
71 | .find({
72 | type: 'posts',
73 | })
74 | .props('slug,title,metadata,created_at')
75 | .status(preview ? 'any' : 'published')
76 |
77 | const morePosts = moreObjects.objects
78 | ?.filter(({ slug: object_slug }) => object_slug !== slug)
79 | .slice(0, 2)
80 |
81 | return {
82 | post: data?.object,
83 | morePosts,
84 | }
85 | } catch (error) {
86 | if (is404(error)) throw error
87 | }
88 | }
89 |
90 | export async function getAllCategories(category) {
91 | try {
92 | const data = await cosmic.objects
93 | .find({
94 | type: category,
95 | })
96 | .props('title')
97 | return data.objects
98 | } catch (error) {
99 | if (is404(error)) return
100 | throw error
101 | }
102 | }
103 |
104 | export async function getPageBySlug(slug, props) {
105 | try {
106 | const data = await cosmic.objects
107 | .findOne({
108 | slug: slug,
109 | })
110 | .props(props)
111 | .depth(1)
112 | return data.object
113 | } catch (error) {
114 | if (is404(error)) return
115 | throw error
116 | }
117 | }
118 |
119 | export async function getSiteSettings() {
120 | try {
121 | const data = await cosmic.objects
122 | .findOne({
123 | type: 'site-settings',
124 | slug: 'site-settings',
125 | })
126 | .props('metadata')
127 | return data.object
128 | } catch (error) {
129 | if (is404(error)) return
130 | throw error
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/src/sections/AboutMeSection.jsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link'
2 | import { ForwardArrowIcon } from '@/configs/icons'
3 | import { sanitize } from 'isomorphic-dompurify'
4 |
5 | const AboutMeSection = ({ bodyText }) => {
6 | return (
7 |
8 |
9 | About Me
10 |
11 |
17 |
21 |
22 |
23 |
24 | Learn more
25 |
26 |
27 | )
28 | }
29 |
30 | export default AboutMeSection
31 |
--------------------------------------------------------------------------------
/src/sections/ContactSection.jsx:
--------------------------------------------------------------------------------
1 | import { LetterIcon } from '@/configs/icons'
2 | import { sanitize } from 'isomorphic-dompurify'
3 |
4 | const ContactSection = ({ heading, bodyText, email }) => {
5 | return (
6 |
26 | )
27 | }
28 |
29 | export default ContactSection
30 |
--------------------------------------------------------------------------------
/src/sections/IntroSection.jsx:
--------------------------------------------------------------------------------
1 | import Socials from '@/components/Socials'
2 | import Image from "next/image"
3 |
4 | const IntroSection = ({ heading, subHeading, avatar, socials }) => {
5 | return (
6 |
7 |
8 |
9 | {heading || 'Developer Portfolio'}
10 |
11 |
12 | {subHeading || 'This portfolio template is powered by Cosmic.'}
13 |
14 |
20 |
21 |
22 |
33 |
34 |
35 | );
36 | }
37 |
38 | export default IntroSection
39 |
--------------------------------------------------------------------------------
/src/sections/PostsSection.jsx:
--------------------------------------------------------------------------------
1 | import PostList from '@/components/PostList'
2 | import { PencilIcon } from '@/configs/icons'
3 |
4 | const WorksSection = ({ posts }) => {
5 | return (
6 |
7 |
8 |
11 | Posts
12 |
13 |
14 |
15 | )
16 | }
17 |
18 | export default WorksSection
19 |
--------------------------------------------------------------------------------
/src/sections/ToolboxSection.jsx:
--------------------------------------------------------------------------------
1 | import { ToolboxIcon } from '@/configs/icons'
2 | import React from 'react'
3 | import DevIcon from '@/components/DevIcon'
4 | import { devIcons } from '@/configs/dev-icons'
5 |
6 | const TechSection = () => {
7 | return (
8 |
9 |
10 |
11 |
12 |
13 | Toolbox
14 |
15 |
16 | {devIcons.map(icon => (
17 |
18 | ))}
19 |
20 |
21 | )
22 | }
23 |
24 | export default TechSection
25 |
--------------------------------------------------------------------------------
/src/sections/WorksSection.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PostList from '@/components/PostList'
3 | import { FlaskIcon } from '@/configs/icons'
4 |
5 | const WorksSection = ({ posts }) => {
6 | return (
7 |
8 |
9 |
10 |
11 |
12 | Works
13 |
14 |
15 |
16 | )
17 | }
18 |
19 | export default WorksSection
20 |
--------------------------------------------------------------------------------
/src/styles/globals.css:
--------------------------------------------------------------------------------
1 | /* Import the fonts from the local NPM package 'fontsource'. You can copy the import statements to use whichever font types you need. */
2 | @import '@fontsource/public-sans/300.css';
3 | @import '@fontsource/public-sans/400.css';
4 | @import '@fontsource/public-sans/400-italic.css';
5 | @import '@fontsource/public-sans/600.css';
6 | @import '@fontsource/public-sans/700.css';
7 | @import 'tailwindcss/base';
8 | @import 'tailwindcss/components';
9 | @import 'tailwindcss/utilities';
10 |
11 | /* Theme Classes for light and dark mode */
12 |
13 | .light {
14 | --color-fore-primary: hsla(240, 6%, 10%);
15 | --color-fore-secondary: hsla(240, 4%, 16%);
16 | --color-fore-subtle: hsla(218, 17%, 35%);
17 |
18 | --color-back-primary: hsla(240, 5%, 96%);
19 | --color-back-secondary: hsla(240, 6%, 93%);
20 | --color-back-subtle: hsla(210, 38%, 92%);
21 |
22 | --color-accent: hsla(221, 100%, 63%);
23 | --color-second-accent: hsla(243, 78%, 68%);
24 | --color-back-accent: hsla(217, 100%, 81%);
25 | }
26 |
27 | .dark {
28 | --color-fore-primary: hsla(214, 32%, 91%);
29 | --color-fore-secondary: hsla(214, 32%, 91%);
30 | --color-fore-subtle: hsla(214, 20%, 69%);
31 |
32 | --color-back-primary: hsla(228, 10%, 11%);
33 | --color-back-secondary: hsla(221, 39%, 8%);
34 | --color-back-subtle: hsla(221, 39%, 16%);
35 |
36 | --color-accent: hsla(201, 100%, 63%);
37 | --color-second-accent: hsla(243, 78%, 68%);
38 | --color-back-accent: hsla(172, 67%, 70%);
39 | }
40 |
41 | /* End Theme Classes */
42 |
43 | /* Custom Classes */
44 |
45 | .nav--item {
46 | position: relative;
47 | }
48 | .nav--item::after,
49 | .nav--item:focus::after {
50 | content: '';
51 | position: absolute;
52 | height: 2px;
53 | background: linear-gradient(
54 | 200deg,
55 | var(--color-accent),
56 | var(--color-second-accent)
57 | );
58 | border-radius: 50px;
59 | bottom: -1.5px;
60 | left: 0;
61 | right: 0;
62 | transform: scaleX(0);
63 | transform-origin: right;
64 | transition: transform 150ms ease-in-out;
65 | }
66 |
67 | .nav--item:hover:after,
68 | .nav--item:focus::after {
69 | transform: scaleX(1);
70 | transform-origin: left;
71 | }
72 |
73 | .filter--active {
74 | position: relative;
75 | z-index: -1;
76 | }
77 |
78 | .filter--active::after,
79 | .filter--active:focus::after {
80 | content: '';
81 | position: absolute;
82 | height: 2px;
83 | background: linear-gradient(
84 | 200deg,
85 | var(--color-accent),
86 | var(--color-second-accent)
87 | );
88 | left: 0;
89 | right: 0;
90 | bottom: 0;
91 | transform: scaleX(1);
92 | transform-origin: right;
93 | transition: transform 150ms ease-in-out;
94 | }
95 |
96 | .minimal--border {
97 | position: relative;
98 | }
99 | .minimal--border::before {
100 | content: '';
101 | position: absolute;
102 | background: var(--color-accent);
103 | top: 50%;
104 | left: 0;
105 | width: 6px;
106 | height: 2px;
107 | margin: 0 -0.5rem;
108 | border-radius: 5px;
109 | }
110 | .minimal--border::after {
111 | content: '';
112 | position: absolute;
113 | background: var(--color-accent);
114 | top: 50%;
115 | right: 0;
116 | width: 6px;
117 | height: 2px;
118 | margin: 0 -0.5rem;
119 | border-radius: 5px;
120 | }
121 |
122 | /* End Custom Classes */
123 |
124 | @layer base {
125 | ::-moz-selection {
126 | background: var(--color-accent);
127 | color: var(--color-back-primary);
128 | }
129 |
130 | ::selection {
131 | background: var(--color-accent);
132 | color: var(--color-back-primary);
133 | }
134 |
135 | html {
136 | scroll-behavior: smooth;
137 | }
138 |
139 | body {
140 | background-color: var(--color-back-primary);
141 | color: var(--color-fore-primary);
142 | @apply overflow-x-hidden antialiased;
143 | }
144 |
145 | .full-width-container {
146 | width: 100vw;
147 | margin-left: calc(-50vw + 50%);
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | const { fontFamily } = require('tailwindcss/defaultTheme')
2 |
3 | module.exports = {
4 | mode: 'jit',
5 | content: [
6 | './src/pages/**/*.{js,jsx}',
7 | './src/components/**/*.{js,jsx}',
8 | './src/sections/**/*.{js,jsx}',
9 | './src/configs/**/*.{js,jsx}',
10 | ],
11 | darkMode: 'class',
12 | theme: {
13 | extend: {
14 | colors: {
15 | accent: 'var(--color-accent)',
16 | fore: {
17 | primary: 'var(--color-fore-primary)',
18 | secondary: 'var(--color-fore-secondary)',
19 | subtle: 'var(--color-fore-subtle)',
20 | },
21 | back: {
22 | primary: 'var(--color-back-primary)',
23 | secondary: 'var(--color-back-secondary)',
24 | subtle: 'var(--color-back-subtle)',
25 | accent: 'var(--color-back-accent)',
26 | },
27 | },
28 | fontFamily: {
29 | sans: ['Public Sans', ...fontFamily.sans],
30 | },
31 | },
32 | },
33 | }
34 |
--------------------------------------------------------------------------------