├── .gitignore ├── screenshot.png ├── pnpm-workspace.yaml ├── public └── static │ ├── ProductLog.png │ ├── release-1.jpg │ └── logo.svg ├── pages ├── _app.tsx ├── index.tsx └── [folder] │ ├── [slug].tsx │ └── index.tsx ├── lib ├── types │ ├── sourceConfig.ts │ ├── source.ts │ └── post.ts ├── utils │ └── getConfig.ts ├── index.ts └── sources │ ├── notion.ts │ └── local.ts ├── next.config.js ├── components ├── markdown │ └── image.tsx ├── sidebar.tsx ├── header.tsx ├── layout.tsx ├── analytics.tsx ├── footer.tsx ├── postView.tsx └── navMenu.tsx ├── misc ├── getFolders.ts ├── style.css ├── getPageObj.ts ├── useWindowSize.tsx └── useAnalytics.ts ├── next-env.d.ts ├── postcss.config.js ├── tsconfig.json ├── .github └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── tailwind.config.js ├── data ├── 2023 │ └── initial.md └── 2024 │ ├── pre-release.md │ └── release-v1.md ├── LICENSE ├── package.json ├── config.json ├── README.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md └── pnpm-lock.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | .vercel 4 | .next 5 | *.log 6 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apvarun/productlog-nextjs-theme/HEAD/screenshot.png -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | onlyBuiltDependencies: 2 | - '@tailwindcss/oxide' 3 | - esbuild 4 | - sharp 5 | -------------------------------------------------------------------------------- /public/static/ProductLog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apvarun/productlog-nextjs-theme/HEAD/public/static/ProductLog.png -------------------------------------------------------------------------------- /public/static/release-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apvarun/productlog-nextjs-theme/HEAD/public/static/release-1.jpg -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '../misc/style.css' 2 | 3 | function MyApp({ Component, pageProps }) { 4 | return 5 | } 6 | 7 | export default MyApp 8 | -------------------------------------------------------------------------------- /lib/types/sourceConfig.ts: -------------------------------------------------------------------------------- 1 | interface LocalSourceConfig { 2 | data: { 3 | type: 'local' | 'notion'; 4 | name: string; 5 | }; 6 | } 7 | 8 | export type SourceConfig = LocalSourceConfig; 9 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | experimental: {}, 3 | images: { 4 | domains: [], 5 | }, 6 | i18n: { 7 | locales: ['en-US'], 8 | defaultLocale: 'en-US' 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /components/markdown/image.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | 3 | const MarkdownImage = (props) => ( 4 | 5 | ); 6 | 7 | export default MarkdownImage; 8 | -------------------------------------------------------------------------------- /lib/types/source.ts: -------------------------------------------------------------------------------- 1 | import { Post } from './post'; 2 | 3 | export interface Source { 4 | getAllPosts: () => Promise; 5 | getPost: (slug: string) => Promise; 6 | getPostsCount: () => Promise; 7 | } 8 | -------------------------------------------------------------------------------- /misc/getFolders.ts: -------------------------------------------------------------------------------- 1 | import { Post } from '../lib/types/post'; 2 | 3 | export default function getFolders(posts: Post[]) { 4 | return Array.from( 5 | new Set(posts.map((post) => post.path.split('/').filter((p) => !!p)[0])) 6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/pages/api-reference/config/typescript for more information. 6 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | // If you want to use other PostCSS plugins, see the following: 2 | // https://tailwindcss.com/docs/using-with-preprocessors 3 | module.exports = { 4 | plugins: { 5 | "@tailwindcss/postcss": {}, 6 | autoprefixer: {}, 7 | }, 8 | } 9 | -------------------------------------------------------------------------------- /lib/utils/getConfig.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import { SourceConfig } from '../types/sourceConfig'; 4 | 5 | export default function getConfig(): SourceConfig | null { 6 | try { 7 | return JSON.parse( 8 | fs.readFileSync(path.join(process.cwd(), 'config.json'), 'utf-8') 9 | ); 10 | } catch { 11 | return null; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/types/post.ts: -------------------------------------------------------------------------------- 1 | interface LocalPost { 2 | type: 'local'; 3 | content: string; 4 | } 5 | interface NotionPost { 6 | type: 'notion'; 7 | content: any; 8 | } 9 | 10 | interface PostCommon { 11 | path: string; 12 | slug: string; 13 | permalink: string; 14 | createdAt: number; 15 | title: string; 16 | content: string; 17 | } 18 | 19 | export type Post = PostCommon & (LocalPost | NotionPost); 20 | -------------------------------------------------------------------------------- /misc/style.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | 3 | img { 4 | @apply rounded mx-auto; 5 | } 6 | 7 | /* Markdown Image */ 8 | .markdown-img { 9 | object-fit: contain; 10 | width: 100% !important; 11 | position: relative !important; 12 | height: unset !important; 13 | } 14 | 15 | .markdown-img-container { 16 | width: 100%; 17 | } 18 | .markdown-img-container > p { 19 | position: unset !important; 20 | } 21 | -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | import { getLocalSource } from './sources/local'; 2 | import getConfig from './utils/getConfig'; 3 | import { getNotionSource } from './sources/notion'; 4 | 5 | export default function getSource() { 6 | const config = getConfig(); 7 | 8 | switch (config.data.type) { 9 | case 'local': 10 | return getLocalSource(); 11 | case 'notion': 12 | return getNotionSource(); 13 | default: 14 | return null; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": false, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve", 20 | "incremental": true 21 | }, 22 | "include": [ 23 | "next-env.d.ts", 24 | "**/*.ts", 25 | "**/*.tsx" 26 | ], 27 | "exclude": [ 28 | "node_modules" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | const defaultTheme = require('tailwindcss/defaultTheme'); 2 | const colors = require('tailwindcss/colors') 3 | 4 | module.exports = { 5 | content: ['./pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'], 6 | darkMode: 'class', 7 | theme: { 8 | extend: { 9 | colors: { 10 | primary: { 11 | light: colors.gray['100'], 12 | medium: colors.gray['200'], 13 | dark: colors.gray['300'] 14 | }, 15 | }, 16 | fontFamily: { 17 | sans: ['Ubuntu', ...defaultTheme.fontFamily.sans] 18 | }, 19 | }, 20 | }, 21 | variants: { 22 | extend: {}, 23 | }, 24 | plugins: [require('@tailwindcss/typography')], 25 | }; 26 | -------------------------------------------------------------------------------- /components/sidebar.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import React from 'react'; 3 | import { Post } from '../lib/types/post'; 4 | import getFolders from '../misc/getFolders'; 5 | 6 | export default function Sidebar({ 7 | posts, 8 | showBackHome = false, 9 | }: { 10 | posts: Post[]; 11 | showBackHome?: boolean; 12 | }) { 13 | const paths = getFolders(posts); 14 | 15 | return ( 16 |
17 | {showBackHome && ( 18 | 19 | ← Back home 20 | 21 | )} 22 | {paths.map((path, i) => ( 23 | 24 | {path} 25 | 26 | ))} 27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /data/2024/pre-release.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: February release 3 | createdAt: '2024-02-21' 4 | --- 5 | 6 | Nam at erat sed arcu interdum hendrerit. Duis lectus sapien, congue quis rutrum vel, gravida vitae est. Vivamus imperdiet justo non ullamcorper dictum. Pellentesque lorem enim, posuere eget sollicitudin sit amet, consequat id ipsum. Donec sit amet est congue, vehicula nisl sed, sodales odio. 7 | 8 | Cras ut purus eget justo ultricies auctor sit amet non dolor. Fusce mattis in nisl vitae faucibus. Ut at felis quam. Integer tincidunt lobortis neque at facilisis. Nam nulla justo, interdum sed mauris ac, blandit tristique tortor. 9 | 10 | Quisque non pulvinar turpis. Praesent malesuada nunc ac urna luctus, vel pellentesque tellus auctor. In ut dapibus orci. Nullam sodales justo convallis pulvinar blandit. Cras luctus vitae nisi id euismod. 11 | -------------------------------------------------------------------------------- /misc/getPageObj.ts: -------------------------------------------------------------------------------- 1 | export type PageItem = { 2 | frontMatter?: { 3 | [key: string]: string; 4 | }; 5 | name: string; 6 | route: string; 7 | }; 8 | 9 | export type PageList = { 10 | children: Array; 11 | name: string; 12 | route: string; 13 | }; 14 | 15 | export type PageMap = Array; 16 | 17 | export default function getPageObj(routePaths: string[], pageMap: PageMap) { 18 | let pageItem = pageMap; 19 | 20 | for (let i = 0; i < routePaths.length; i++) { 21 | const tmp = pageItem.find((pg) => pg.name === routePaths[i]); 22 | 23 | if (!('children' in tmp) || routePaths.length === i + 1) { 24 | if ('children' in tmp) return tmp.children; 25 | 26 | return tmp; 27 | } 28 | 29 | pageItem = tmp.children; 30 | } 31 | 32 | return pageItem; 33 | } 34 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import getSource from '../lib'; 4 | import Layout from '../components/layout'; 5 | import PostView from '../components/postView'; 6 | import { Post } from '../lib/types/post'; 7 | import Sidebar from '../components/sidebar'; 8 | 9 | export default function Home({ posts }: { posts: Post[] }) { 10 | return ( 11 | 12 | 13 |
14 | {posts.map((post) => ( 15 | 16 | ))} 17 |
18 |
19 | ); 20 | } 21 | 22 | export async function getStaticProps({ params }) { 23 | const source = getSource(); 24 | const posts = await source.getAllPosts(); 25 | 26 | return { 27 | props: { 28 | posts, 29 | }, 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /data/2023/initial.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Initial planning 3 | createdAt: '2023-12-21' 4 | --- 5 | 6 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent malesuada diam libero, a bibendum neque fermentum non. Sed consectetur vel erat sed vestibulum. Mauris cursus odio non lectus viverra cursus. 7 | 8 | Etiam eu mi non nisi vulputate molestie. Suspendisse nunc dolor, convallis porttitor tempus nec, finibus id sapien. Integer sed velit cursus, sodales velit vitae, malesuada mi. Nam et nulla fringilla, lacinia justo pellentesque, pretium sem. Morbi mattis malesuada dolor eu tincidunt. 9 | 10 | - Mauris facilisis ex vitae justo condimentum, eu hendrerit risus tincidunt. Fusce tellus est, mattis lacinia mi eu, accumsan pharetra orci. 11 | - In vitae vestibulum nisi. Proin lacus quam, tincidunt eget arcu sit amet, posuere aliquam diam. Donec nec libero tortor. 12 | - Mauris pharetra nulla at metus maximus, ac varius lorem congue. 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 Varun A P 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /components/header.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | 3 | import config from '../config.json'; 4 | import NavMenu from './navMenu'; 5 | 6 | export default function Header() { 7 | return ( 8 |
9 |
10 | 11 | 12 | {config.logo.image && ( 13 | {config.logo.text} 18 | )} 19 | {config.logo.text} 20 | 21 | 22 | 23 |
24 |
25 |

{config.header.title}

26 |

{config.header.description}

27 |
28 |
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /misc/useWindowSize.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | function useWindowSize() { 4 | // Initialize state with undefined width/height so server and client renders match 5 | // Learn more here: https://joshwcomeau.com/react/the-perils-of-rehydration/ 6 | const [windowSize, setWindowSize] = useState({ 7 | width: undefined, 8 | height: undefined, 9 | }); 10 | useEffect(() => { 11 | // Handler to call on window resize 12 | function handleResize() { 13 | // Set window width/height to state 14 | setWindowSize({ 15 | width: window.innerWidth, 16 | height: window.innerHeight, 17 | }); 18 | } 19 | // Add event listener 20 | window.addEventListener('resize', handleResize); 21 | // Call handler right away so state gets updated with initial window size 22 | handleResize(); 23 | // Remove event listener on cleanup 24 | return () => window.removeEventListener('resize', handleResize); 25 | }, []); // Empty array ensures that effect is only run on mount 26 | return windowSize; 27 | } 28 | 29 | export default useWindowSize; 30 | -------------------------------------------------------------------------------- /pages/[folder]/[slug].tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Layout from '../../components/layout'; 3 | import PostView from '../../components/postView'; 4 | import Sidebar from '../../components/sidebar'; 5 | import getSource from '../../lib'; 6 | import { Post } from '../../lib/types/post'; 7 | 8 | export default function Change({ post }: { post: Post }) { 9 | return ( 10 | 11 | 12 |
13 | 14 |
15 |
16 | ); 17 | } 18 | 19 | export async function getStaticProps({ params: { folder, slug } }) { 20 | const source = getSource(); 21 | const post = await source.getPost(`/${folder}/${slug}`); 22 | 23 | return { 24 | props: { 25 | post: post, 26 | }, 27 | }; 28 | } 29 | 30 | export async function getStaticPaths() { 31 | const source = getSource(); 32 | const posts = await source.getAllPosts(); 33 | 34 | return { 35 | paths: posts.map((post) => ({ 36 | params: { folder: post.path.slice(1), slug: post.slug }, 37 | })), 38 | fallback: false, 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /data/2024/release-v1.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: March release 3 | createdAt: '2024-03-21' 4 | --- 5 | 6 | Nam at erat sed arcu interdum hendrerit. Duis lectus sapien, congue quis rutrum vel, gravida vitae est. Vivamus imperdiet justo non ullamcorper dictum. Pellentesque lorem enim, posuere eget sollicitudin sit amet, consequat id ipsum. Donec sit amet est congue, vehicula nisl sed, sodales odio. 7 | 8 | ![Photo by Samantha Gades on Unsplash](/static/release-1.jpg) 9 | 10 | ```js 11 | // code 12 | const js = new Date(); 13 | ``` 14 | 15 | Cras ut purus eget justo ultricies auctor sit amet non dolor. Fusce mattis in nisl vitae faucibus. Ut at felis quam. Integer tincidunt lobortis neque at facilisis. Nam nulla justo, interdum sed mauris ac, blandit tristique tortor. 16 | 17 | $$ 18 | a^b + b^2 = c^2 19 | $$ 20 | 21 | Quisque non pulvinar turpis. Praesent malesuada nunc ac urna luctus, vel pellentesque tellus auctor. In ut dapibus orci. Nullam sodales justo convallis pulvinar blandit. Cras luctus vitae nisi id euismod. 22 | -------------------------------------------------------------------------------- /pages/[folder]/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Layout from '../../components/layout'; 3 | import PostView from '../../components/postView'; 4 | import Sidebar from '../../components/sidebar'; 5 | import getSource from '../../lib'; 6 | import { Post } from '../../lib/types/post'; 7 | import getFolders from '../../misc/getFolders'; 8 | 9 | export default function Folder({ posts }: { posts: Post[] }) { 10 | return ( 11 | 12 | 13 |
14 | {posts.map((post) => ( 15 | 16 | ))} 17 |
18 |
19 | ); 20 | } 21 | 22 | export async function getStaticProps({ params }) { 23 | const source = getSource(); 24 | const posts = await source.getAllPosts(); 25 | 26 | return { 27 | props: { 28 | posts: posts.filter((post) => post.path === `/${params.folder}`), 29 | }, 30 | }; 31 | } 32 | 33 | export async function getStaticPaths() { 34 | const source = getSource(); 35 | const posts = await source.getAllPosts(); 36 | 37 | const paths = getFolders(posts); 38 | 39 | return { 40 | paths: paths.map((path) => ({ params: { folder: path } })), 41 | fallback: false, 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /misc/useAnalytics.ts: -------------------------------------------------------------------------------- 1 | import config from '../config.json'; 2 | 3 | export enum AnalyticsProviders { 4 | Google = 'Google Analytics', 5 | Simple = 'Simple Analytics', 6 | Plausible = 'Plausible Analytics', 7 | } 8 | 9 | export default function useAnalytics() { 10 | const { type } = config.analytics; 11 | 12 | let logEvent = (...rest: any) => undefined; 13 | 14 | if (type === AnalyticsProviders.Google) { 15 | logEvent = (action, category, label, value) => { 16 | if ('gtag' in window) { 17 | (window as any).gtag?.('event', action, { 18 | event_category: category, 19 | event_label: label, 20 | value: value, 21 | }); 22 | } 23 | }; 24 | } 25 | 26 | if (type === AnalyticsProviders.Google) { 27 | logEvent = (eventName, callback) => { 28 | if ('sa_event' in window) { 29 | if (callback) { 30 | return (window as any).sa_event?.(eventName, callback); 31 | } else { 32 | return (window as any).sa_event?.(eventName); 33 | } 34 | } 35 | }; 36 | } 37 | 38 | if (type === AnalyticsProviders.Plausible) { 39 | logEvent = (eventName, ...rest) => { 40 | if ('plausible' in window) { 41 | return (window as any).plausible?.(eventName, ...rest); 42 | } 43 | }; 44 | } 45 | 46 | return { logEvent }; 47 | } 48 | -------------------------------------------------------------------------------- /components/layout.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head'; 2 | import React from 'react'; 3 | import Header from './header'; 4 | 5 | import config from '../config.json'; 6 | import Footer from './footer'; 7 | 8 | export default function Layout({ 9 | children, 10 | title = config.meta.title, 11 | description = config.meta.description, 12 | }) { 13 | return ( 14 | <> 15 | 16 | {title} 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 30 | 31 |
32 |
33 | {children} 34 |
35 |