element to use the next/link component.
21 | a: ({ href, children }) => {children},
22 | Video: ({ src }) => ,
23 |
24 | // Add more custom components...
25 | }
26 |
27 | return (
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | )
38 | }
39 |
--------------------------------------------------------------------------------
/components/Layout.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { motion } from 'framer-motion'
4 | import { usePathname } from 'next/navigation'
5 |
6 |
7 | const Layout = ({ children }: { children: any }) => {
8 | const pathname = usePathname()
9 |
10 | return (
11 |
23 | {children}
24 |
25 | )
26 | }
27 | export default Layout
28 |
--------------------------------------------------------------------------------
/components/NetlifyIdentityRedirect.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useEffect } from 'react'
4 |
5 | declare global {
6 | interface Window {
7 | netlifyIdentity: any
8 | }
9 | }
10 |
11 | export default function NetlifyIdentityRedirect() {
12 | useEffect(() => {
13 | if (window.netlifyIdentity) {
14 | window.netlifyIdentity.on('init', (user: unknown) => {
15 | if (!user) {
16 | window.netlifyIdentity.on('login', () => {
17 | document.location.href = '/admin/'
18 | })
19 | }
20 | })
21 | }
22 | }, [])
23 |
24 | return null // This component doesn't render anything visually
25 | }
26 |
--------------------------------------------------------------------------------
/components/Pagnation.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Link from 'next/link'
3 | import { useRouter } from 'next/router'
4 |
5 | function Pagnation({ totalPostCount }: { totalPostCount: number }) {
6 | let router = useRouter()
7 |
8 | /*
9 | pages give number,base on number we create a array. base on array we map a list elements
10 | totalPostCount = 3
11 | conver into array [0,1,2]
12 | base on array create list in array
13 |
14 | */
15 |
16 | let pageIntoArray = Array.from(Array(totalPostCount).keys())
17 |
18 | return (
19 |
35 | )
36 | }
37 |
38 | export default Pagnation
39 |
--------------------------------------------------------------------------------
/components/PostFooter.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link'
2 | import {
3 | allBlogs,
4 | allInspirations,
5 | allPodcasts,
6 | allResources,
7 | allTools,
8 | Blog,
9 | Inspiration,
10 | Podcasts,
11 | Resources,
12 | Tools,
13 | } from '../.contentlayer/generated'
14 | import Tag from './Tag'
15 | import { Icon } from './Icon'
16 |
17 | export default function PostFooter({
18 | data,
19 | }: {
20 | data: Blog | Inspiration | Podcasts | Resources | Tools
21 | }) {
22 | let navPosts: (Blog | Inspiration | Podcasts | Resources | Tools)[] = []
23 |
24 | switch (data.templateKey) {
25 | case 'blog':
26 | navPosts = allBlogs as Blog[]
27 | break
28 | case 'inspiration':
29 | navPosts = allInspirations as Inspiration[]
30 | break
31 | case 'podcasts':
32 | navPosts = allPodcasts as Podcasts[]
33 | break
34 | case 'resources':
35 | navPosts = allResources as Resources[]
36 | break
37 | case 'tools':
38 | navPosts = allTools as Tools[]
39 | break
40 | }
41 |
42 | // Find the index of the current post
43 | const currentIndex = navPosts.findIndex((post) => post.slug === data.slug)
44 |
45 | // Determine the previous and next posts
46 | const prevPost = currentIndex > 0 ? navPosts[currentIndex - 1] : null
47 | const nextPost =
48 | currentIndex < navPosts.length - 1 ? navPosts[currentIndex + 1] : null
49 |
50 | return (
51 | <>
52 | {data.tags && (
53 |
54 |
This post was tagged in:
55 |
56 | {data.tags.map((tag: string) => {
57 | return (
58 | -
59 |
60 |
61 | )
62 | })}
63 |
64 |
65 | )}
66 | {(prevPost || nextPost) && (
67 |
68 | {prevPost && (
69 |
73 |
74 |
75 | Previous
76 |
77 | {prevPost.title}
78 |
79 | )}
80 | {nextPost && (
81 |
85 |
86 | Next
87 |
88 |
89 | {nextPost.title}
90 |
91 | )}
92 |
93 | )}
94 | >
95 | )
96 | }
97 |
--------------------------------------------------------------------------------
/components/PostHeader.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link'
2 | import {
3 | Blog,
4 | Inspiration,
5 | Podcasts,
6 | Resources,
7 | Tools,
8 | } from '../.contentlayer/generated'
9 | import { formatDate } from '../utils'
10 | import { Icon } from './Icon'
11 | import { AUTHOR_NAME } from '../config'
12 |
13 | export default function PostHeader({
14 | data,
15 | }: {
16 | data: Blog | Inspiration | Podcasts | Resources | Tools
17 | }) {
18 | return (
19 | <>
20 |
49 | >
50 | )
51 | }
52 |
--------------------------------------------------------------------------------
/components/Tag.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link'
2 |
3 | export const tagClasses =
4 | 'py-2 px-3 bg-slate-200 dark:bg-slate-700 hover:bg-slate-700 hover:text-slate-100 dark:hover:bg-slate-200 dark:hover:text-slate-900 rounded-sm font-mono text-sm transition-colors'
5 |
6 | export default function Tag({ tag }: { tag: string }) {
7 | return (
8 |
14 | #{tag}
15 |
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/components/Video.tsx:
--------------------------------------------------------------------------------
1 | export default function Video({ src }: { src: string }) {
2 | return (
3 |
4 |
12 |
13 | )
14 | }
15 |
--------------------------------------------------------------------------------
/components/cards/BlogPostCard.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link'
2 | import { Blog } from '../../.contentlayer/generated'
3 |
4 | const cardClasses =
5 | 'px-4 py-4 w-full flex flex-col gap-6 bg-white dark:bg-slate-800 rounded-xl shadow-md hover:shadow-xl dark:hover:shadow-2xl hover:dark:bg-slate-700 transition-shadow dark:transition-colors'
6 |
7 | export default function BlogPostCard({ post }: { post: Blog }) {
8 | return (
9 |
14 | {post.title}
15 | {post.description && (
16 |
17 | {post.description}
18 |
19 | )}
20 |
21 | Read more →
22 |
23 |
24 | )
25 | }
26 |
--------------------------------------------------------------------------------
/components/cards/InspirationPostCard.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link'
2 | import { Inspiration } from '../../.contentlayer/generated'
3 | import ExportedImage from 'next-image-export-optimizer'
4 |
5 | const InspirationCardClasses =
6 | 'w-full flex flex-col gap-2 bg-white dark:bg-slate-800 rounded-xl shadow-md hover:shadow-xl dark:hover:shadow-2xl hover:dark:bg-slate-700 transition-shadow dark:transition-colors'
7 |
8 | export default function InspirationPostCard({ post }: { post: Inspiration }) {
9 | return (
10 |
15 |
16 |
24 |
25 |
26 |
{post.title}
27 | {post.description && (
28 |
29 | {post.description}
30 |
31 | )}
32 |
33 |
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/components/cards/PodcastPostCard.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link'
2 | import { Podcasts } from '../../.contentlayer/generated'
3 | import ExportedImage from 'next-image-export-optimizer'
4 |
5 | const cardClasses =
6 | 'w-full flex flex-col gap-2 bg-white dark:bg-slate-800 rounded-xl shadow-md hover:shadow-xl dark:hover:shadow-2xl hover:dark:bg-slate-700 transition-shadow transition-300 dark:transition-colors'
7 |
8 | export default function PodcastPostCard({ post }: { post: Podcasts }) {
9 | return (
10 |
15 |
16 |
24 |
25 |
26 |
{post.title}
27 | {post.description && (
28 |
29 | {post.description}
30 |
31 | )}
32 |
33 |
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/components/cards/ResourcePostCard.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link'
2 | import { Resources } from '../../.contentlayer/generated'
3 | import ExportedImage from 'next-image-export-optimizer'
4 |
5 | const cardClasses =
6 | 'w-full flex flex-col sm:flex-row gap-2 p-4 bg-white dark:bg-slate-800 rounded-xl shadow-md hover:shadow-xl dark:hover:shadow-2xl hover:dark:bg-slate-700 transition-shadow dark:transition-colors'
7 |
8 | export default function ResourcesPostCard({ post }: { post: Resources }) {
9 | return (
10 |
15 |
16 |
23 |
24 |
25 |
{post.title}
26 |
27 | {post.description}
28 |
29 |
30 |
31 | )
32 | }
33 |
--------------------------------------------------------------------------------
/components/cards/ToolsPostCard.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link'
2 | import { Tools } from '../../.contentlayer/generated'
3 | import ExportedImage from 'next-image-export-optimizer'
4 |
5 | const cardClasses =
6 | 'w-full flex gap-3 sm:gap-2 p-4 bg-white dark:bg-slate-800 rounded-xl shadow-md hover:shadow-xl dark:hover:shadow-2xl hover:dark:bg-slate-700 transition-shadow dark:transition-colors'
7 |
8 | export default function ToolsPostCard({ post }: { post: Tools }) {
9 | return (
10 |
15 |
16 |
23 |
24 |
25 |
26 | {post.title}
27 |
28 |
29 | View tool →
30 |
31 |
32 |
33 | )
34 | }
35 |
--------------------------------------------------------------------------------
/config.js:
--------------------------------------------------------------------------------
1 | export const AUTHOR_NAME = 'Nuno Marques'
2 | export const SITE_URL = 'https://design-code.tips'
3 | export const SITE_NAME = 'design-code.tips'
4 | export const SHOW_PER_PAGE = 10
--------------------------------------------------------------------------------
/content/blog/2024-09-25-placeholder-post-2.md:
--------------------------------------------------------------------------------
1 | ---
2 | templateKey: blog
3 | title: >
4 | Placeholder Post 2
5 | date: 2024-09-25T19:28:37.629Z
6 | featured: true
7 | description: >
8 | A brief description of the placeholder post in code blog.
9 | tags:
10 | - open-source
11 | - example
12 | - placeholder
13 | ---
14 |
15 | ## This is a Placeholder Post
16 |
17 | Feel free to replace this content with your own!
18 |
--------------------------------------------------------------------------------
/content/blog/2024-09-25-placeholder-post-3.md:
--------------------------------------------------------------------------------
1 | ---
2 | templateKey: blog
3 | title: >
4 | Placeholder Post 3
5 | date: 2024-09-25T19:28:37.629Z
6 | featured: false
7 | description: >
8 | A brief description of the placeholder post in code blog.
9 | tags:
10 | - open-source
11 | - example
12 | - placeholder
13 | ---
14 |
15 | ## This is a Placeholder Post
16 |
17 | Feel free to replace this content with your own!
18 |
--------------------------------------------------------------------------------
/content/blog/2024-09-25-placeholder-post.md:
--------------------------------------------------------------------------------
1 | ---
2 | templateKey: blog
3 | title: >
4 | Placeholder Post
5 | date: 2024-09-25T19:28:37.629Z
6 | featured: true
7 | description: >
8 | A brief description of the placeholder post in code blog.
9 | tags:
10 | - example
11 | - placeholder
12 | - open-source
13 | ---
14 |
15 | ## This is a Placeholder Post
16 |
17 | Feel free to replace this content with your own!
18 |
--------------------------------------------------------------------------------
/content/inspiration/2024-09-25-placeholder-post-2.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | templateKey: inspiration
3 | title: >
4 | Placeholder Post 2
5 | date: 2024-09-25T15:04:10.000Z
6 | featured: true
7 | description: >
8 | A brief description of the sample inspiration.
9 | tags:
10 | - example
11 | - Placeholder
12 | - open-source
13 | image: /media/inspiration__placeholder-post-1200x1200.png
14 | ---
15 |
16 |
17 |
18 | ## Describe Your Inspiration
19 |
20 | Feel free to replace this content with your own!
21 |
--------------------------------------------------------------------------------
/content/inspiration/2024-09-25-placeholder-post-3.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | templateKey: inspiration
3 | title: >
4 | Placeholder Post 3
5 | date: 2024-09-25T15:04:10.000Z
6 | featured: true
7 | description: >
8 | A brief description of the sample inspiration.
9 | tags:
10 | - example
11 | - Placeholder
12 | image: /media/inspiration__placeholder-post-1200x1200.png
13 | ---
14 |
15 |
16 |
17 | ## Describe Your Inspiration
18 |
19 | Feel free to replace this content with your own!
20 |
--------------------------------------------------------------------------------
/content/inspiration/2024-09-25-placeholder-post-4.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | templateKey: inspiration
3 | title: >
4 | Placeholder Post 4
5 | date: 2024-09-25T15:04:10.000Z
6 | featured: false
7 | description: >
8 | A brief description of the sample inspiration.
9 | tags:
10 | - example
11 | - Placeholder
12 | - open-source
13 | image: /media/inspiration__placeholder-post-1200x1200.png
14 | ---
15 |
16 |
17 |
18 | ## Describe Your Inspiration
19 |
20 | Feel free to replace this content with your own!
21 |
--------------------------------------------------------------------------------
/content/inspiration/2024-09-25-placeholder-post.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | templateKey: inspiration
3 | title: >
4 | Placeholder Post
5 | date: 2024-09-25T15:04:10.000Z
6 | featured: true
7 | description: >
8 | A brief description of the sample inspiration.
9 | tags:
10 | - example
11 | - Placeholder
12 | - open-source
13 | image: /media/inspiration__placeholder-post-1200x1200.png
14 | ---
15 |
16 |
17 |
18 | ## Describe Your Inspiration
19 |
20 | Feel free to replace this content with your own!
21 |
--------------------------------------------------------------------------------
/content/page/about.md:
--------------------------------------------------------------------------------
1 | ---
2 | slug: about
3 | title: About
4 | description: >-
5 | Placeholder about page.
6 | ---
7 |
--------------------------------------------------------------------------------
/content/page/contact.md:
--------------------------------------------------------------------------------
1 | ---
2 | slug: contact
3 | title: Contact
4 | description: >-
5 | Placeholder contact page.
6 | ---
7 |
--------------------------------------------------------------------------------
/content/page/home.md:
--------------------------------------------------------------------------------
1 | ---
2 | slug: home
3 | title: Learn design, coding, web tools, and get inspired!
4 | description: >-
5 | Discover the best tools and ideas to improve your design and web skills. Find useful resources to learn design, coding, and web tools, and get inspired.
6 | ---
7 |
--------------------------------------------------------------------------------
/content/podcasts/placeholder-podcast-2.md:
--------------------------------------------------------------------------------
1 | ---
2 | templateKey: podcasts
3 | title: >
4 | Sample Podcast 2
5 | date: 2024-09-25T15:04:10.000Z
6 | featured: true
7 | description: >
8 | A brief description of the sample podcast.
9 | link: https://thefutur.com/podcast/
10 | tags:
11 | - example
12 | - placeholder
13 | image: /media/podcast__placeholder-post-1200x1200.png
14 | ---
15 |
16 | ## Describe Your Podcast
17 |
18 | Feel free to replace this content with your own!
19 |
--------------------------------------------------------------------------------
/content/podcasts/placeholder-podcast-3.md:
--------------------------------------------------------------------------------
1 | ---
2 | templateKey: podcasts
3 | title: >
4 | Sample Podcast 3
5 | date: 2024-09-25T15:04:10.000Z
6 | featured: true
7 | description: >
8 | A brief description of the sample podcast.
9 | link: https://thefutur.com/podcast/
10 | tags:
11 | - example
12 | - placeholder
13 | - open-source
14 | image: /media/podcast__placeholder-post-1200x1200.png
15 | ---
16 |
17 | ## Describe Your Podcast
18 |
19 | Feel free to replace this content with your own!
20 |
--------------------------------------------------------------------------------
/content/podcasts/placeholder-podcast-4.md:
--------------------------------------------------------------------------------
1 | ---
2 | templateKey: podcasts
3 | title: >
4 | Sample Podcast 4
5 | date: 2024-09-25T15:04:10.000Z
6 | featured: true
7 | description: >
8 | A brief description of the sample podcast.
9 | link: https://thefutur.com/podcast/
10 | tags:
11 | - example
12 | - placeholder
13 | image: /media/podcast__placeholder-post-1200x1200.png
14 | ---
15 |
16 | ## Describe Your Podcast
17 |
18 | Feel free to replace this content with your own!
19 |
--------------------------------------------------------------------------------
/content/podcasts/placeholder-podcast.md:
--------------------------------------------------------------------------------
1 | ---
2 | templateKey: podcasts
3 | title: >
4 | Sample Podcast
5 | date: 2024-09-25T15:04:10.000Z
6 | featured: true
7 | description: >
8 | A brief description of the sample podcast.
9 | link: https://thefutur.com/podcast/
10 | tags:
11 | - example
12 | - placeholder
13 | image: /media/podcast__placeholder-post-1200x1200.png
14 | ---
15 |
16 | ## Describe Your Podcast
17 |
18 | Feel free to replace this content with your own!
19 |
--------------------------------------------------------------------------------
/content/resources/placeholder-resource.md:
--------------------------------------------------------------------------------
1 | ---
2 | templateKey: resources
3 | title: >
4 | Placeholder Resource
5 | date: 2024-09-25T19:28:37.629Z
6 | featured: false
7 | description: >
8 | A brief description of the placeholder resource.
9 | link: #
10 | tags:
11 | - community
12 | - example
13 | - learning
14 | - placeholder
15 | - open-source
16 | image: /media/resource__placeholder-post-1200x1200.png
17 | ---
18 |
19 | ## Placeholder Resource
20 |
21 | Feel free to replace this content with your own!
22 |
--------------------------------------------------------------------------------
/content/tools/placeholder-tool-2.md:
--------------------------------------------------------------------------------
1 | ---
2 | templateKey: tools
3 | title: >
4 | Placeholder Tool 2
5 | date: 2024-09-25T19:28:37.629Z
6 | featured: true
7 | description: >
8 | A brief description of the placeholder tool 2.
9 | link: #
10 | tags:
11 | - example
12 | - placeholder
13 | - collaborative
14 | - open-source
15 | image: /media/tools__placeholder-post-1200x1200.png
16 | ---
17 |
18 | ## Placeholder Tool
19 |
20 | Feel free to replace this content with your own!
21 |
--------------------------------------------------------------------------------
/content/tools/placeholder-tool-3.md:
--------------------------------------------------------------------------------
1 | ---
2 | templateKey: tools
3 | title: >
4 | Placeholder Tool 3
5 | date: 2024-09-25T19:28:37.629Z
6 | featured: true
7 | description: >
8 | A brief description of the placeholder tool 2.
9 | link: #
10 | tags:
11 | - example
12 | - placeholder
13 | - collaborative
14 | - open-source
15 | image: /media/tools__placeholder-post-1200x1200.png
16 | ---
17 |
18 | ## Placeholder Tool
19 |
20 | Feel free to replace this content with your own!
21 |
--------------------------------------------------------------------------------
/content/tools/placeholder-tool-4.md:
--------------------------------------------------------------------------------
1 | ---
2 | templateKey: tools
3 | title: >
4 | Placeholder Tool 4
5 | date: 2024-09-25T19:28:37.629Z
6 | featured: true
7 | description: >
8 | A brief description of the placeholder tool 2.
9 | link: #
10 | tags:
11 | - example
12 | - placeholder
13 | - collaborative
14 | - open-source
15 | image: /media/tools__placeholder-post-1200x1200.png
16 | ---
17 |
18 | ## Placeholder Tool
19 |
20 | Feel free to replace this content with your own!
21 |
--------------------------------------------------------------------------------
/content/tools/placeholder-tool.md:
--------------------------------------------------------------------------------
1 | ---
2 | templateKey: tools
3 | title: >
4 | Placeholder Tool
5 | date: 2024-09-25T19:28:37.629Z
6 | featured: true
7 | description: >
8 | A brief description of the placeholder tool.
9 | link: #
10 | tags:
11 | - apps
12 | - email
13 | - software
14 | - business
15 | - example
16 | - placeholder
17 | - productivity
18 | - collaborative
19 | - open-source
20 | image: /media/tools__placeholder-post-1200x1200.png
21 | ---
22 |
23 | ## Placeholder Tool
24 |
25 | Feel free to replace this content with your own!
26 |
--------------------------------------------------------------------------------
/contentlayer.config.ts:
--------------------------------------------------------------------------------
1 | import { defineDocumentType, makeSource } from 'contentlayer2/source-files'
2 |
3 | const Page = defineDocumentType(() => ({
4 | name: 'Page',
5 | filePathPattern: `page/*.md`,
6 | contentType: 'markdown',
7 | fields: {
8 | slug: {
9 | type: 'string',
10 | },
11 | title: {
12 | type: 'string',
13 | required: true,
14 | },
15 | description: {
16 | type: 'string',
17 | required: false,
18 | },
19 | gallery: {
20 | type: 'list',
21 | of: { type: 'string' },
22 | },
23 | },
24 | computedFields: {
25 | slug: {
26 | type: 'string',
27 | resolve: (doc) => doc._raw.sourceFileName.replace(/\.md/, ''),
28 | },
29 | },
30 | }))
31 |
32 | const Blog = defineDocumentType(() => ({
33 | name: 'Blog',
34 | filePathPattern: `blog/*.md`,
35 | contentType: 'markdown',
36 | fields: {
37 | title: {
38 | type: 'string',
39 | required: true,
40 | },
41 | date: {
42 | type: 'date',
43 | required: false,
44 | },
45 | description: {
46 | type: 'string',
47 | required: false,
48 | },
49 | tags: {
50 | type: 'json',
51 | required: false,
52 | },
53 | templateKey: {
54 | type: 'string',
55 | required: true,
56 | },
57 | featured: {
58 | type: 'boolean',
59 | required: false,
60 | },
61 | },
62 | computedFields: {
63 | slug: {
64 | type: 'string',
65 | resolve: (doc) => doc._raw.sourceFileName.replace(/\.md/, ''),
66 | },
67 | },
68 | }))
69 |
70 | const Inspiration = defineDocumentType(() => ({
71 | name: 'Inspiration',
72 | filePathPattern: `inspiration/*.mdx`,
73 | contentType: 'mdx',
74 | fields: {
75 | title: {
76 | type: 'string',
77 | required: true,
78 | },
79 | date: {
80 | type: 'date',
81 | required: false,
82 | },
83 | description: {
84 | type: 'string',
85 | required: false,
86 | },
87 | tags: {
88 | type: 'json',
89 | required: false,
90 | },
91 | image: {
92 | type: 'string',
93 | required: true,
94 | },
95 | templateKey: {
96 | type: 'string',
97 | required: true,
98 | },
99 | featured: {
100 | type: 'boolean',
101 | required: false,
102 | },
103 | },
104 | computedFields: {
105 | slug: {
106 | type: 'string',
107 | resolve: (doc) => doc._raw.sourceFileName.replace(/\.mdx/, ''),
108 | },
109 | },
110 | }))
111 |
112 | const Podcasts = defineDocumentType(() => ({
113 | name: 'Podcasts',
114 | filePathPattern: `podcasts/*.md`,
115 | contentType: 'markdown',
116 | fields: {
117 | title: {
118 | type: 'string',
119 | required: true,
120 | },
121 | date: {
122 | type: 'date',
123 | required: false,
124 | },
125 | description: {
126 | type: 'string',
127 | required: false,
128 | },
129 | tags: {
130 | type: 'json',
131 | required: false,
132 | },
133 | link: {
134 | type: 'string',
135 | required: false,
136 | },
137 | image: {
138 | type: 'string',
139 | required: true,
140 | },
141 | templateKey: {
142 | type: 'string',
143 | required: true,
144 | },
145 | featured: {
146 | type: 'boolean',
147 | required: false,
148 | },
149 | },
150 | computedFields: {
151 | slug: {
152 | type: 'string',
153 | resolve: (doc) => doc._raw.sourceFileName.replace(/\.md/, ''),
154 | },
155 | },
156 | }))
157 |
158 | const Tools = defineDocumentType(() => ({
159 | name: 'Tools',
160 | filePathPattern: `tools/*.md`,
161 | contentType: 'markdown',
162 | fields: {
163 | title: {
164 | type: 'string',
165 | required: true,
166 | },
167 | date: {
168 | type: 'date',
169 | required: false,
170 | },
171 | description: {
172 | type: 'string',
173 | required: false,
174 | },
175 | tags: {
176 | type: 'json',
177 | required: false,
178 | },
179 | link: {
180 | type: 'string',
181 | required: false,
182 | },
183 | image: {
184 | type: 'string',
185 | required: true,
186 | },
187 | templateKey: {
188 | type: 'string',
189 | required: true,
190 | },
191 | featured: {
192 | type: 'boolean',
193 | required: false,
194 | },
195 | },
196 | computedFields: {
197 | slug: {
198 | type: 'string',
199 | resolve: (doc) => doc._raw.sourceFileName.replace(/\.md/, ''),
200 | },
201 | },
202 | }))
203 |
204 | const Resources = defineDocumentType(() => ({
205 | name: 'Resources',
206 | filePathPattern: `resources/*.md`,
207 | contentType: 'markdown',
208 | fields: {
209 | title: {
210 | type: 'string',
211 | required: true,
212 | },
213 | date: {
214 | type: 'date',
215 | required: false,
216 | },
217 | description: {
218 | type: 'string',
219 | required: false,
220 | },
221 | tags: {
222 | type: 'json',
223 | required: false,
224 | },
225 | link: {
226 | type: 'string',
227 | required: false,
228 | },
229 | image: {
230 | type: 'string',
231 | required: true,
232 | },
233 | templateKey: {
234 | type: 'string',
235 | required: true,
236 | },
237 | featured: {
238 | type: 'boolean',
239 | required: false,
240 | },
241 | },
242 | computedFields: {
243 | slug: {
244 | type: 'string',
245 | resolve: (doc) => doc._raw.sourceFileName.replace(/\.md/, ''),
246 | },
247 | },
248 | }))
249 |
250 | export default makeSource({
251 | contentDirPath: 'content',
252 | documentTypes: [Page, Blog, Inspiration, Podcasts, Tools, Resources],
253 | disableImportAliasWarning: true,
254 | })
255 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "paths": {
5 | "contentlayer/generated": ["./.contentlayer/generated"]
6 | }
7 | },
8 | "include": [".contentlayer/generated"]
9 | }
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
6 |
--------------------------------------------------------------------------------
/next-seo.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | openGraph: {
3 | type: 'website',
4 | locale: 'en',
5 | url: 'https://design-code.tips',
6 | site_name: 'design-code.tips',
7 | },
8 | };
--------------------------------------------------------------------------------
/next-sitemap.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next-sitemap').IConfig} */
2 |
3 | module.exports = {
4 | siteUrl: process.env.SITE_URL || 'https://example.com',
5 | generateRobotsTxt: true, // (optional)
6 | // Default transformation function
7 | }
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | /**
4 | * @type {import('next').NextConfig}
5 | */
6 |
7 | const { withContentlayer } = require('next-contentlayer2');
8 |
9 | const nextconfig = {
10 | reactStrictMode: true,
11 | trailingSlash: true,
12 | output: "standalone",
13 | images: {
14 | loader: "custom",
15 | imageSizes: [16, 32, 48, 128, 256],
16 | deviceSizes: [640, 750, 828, 1080, 1200, 1920],
17 | minimumCacheTTL: 600000,
18 | },
19 | transpilePackages: ["next-image-export-optimizer"],
20 | env: {
21 | nextImageExportOptimizer_imageFolderPath: "public/media",
22 | nextImageExportOptimizer_exportFolderPath: "out",
23 | nextImageExportOptimizer_quality: "75",
24 | nextImageExportOptimizer_storePicturesInWEBP: "true",
25 | nextImageExportOptimizer_exportFolderName: "nextImageExportOptimizer",
26 |
27 | // If you do not want to use blurry placeholder images, then you can set
28 | // nextImageExportOptimizer_generateAndUseBlurImages to false and pass
29 | // `placeholder="empty"` to all components.
30 | nextImageExportOptimizer_generateAndUseBlurImages: "true",
31 |
32 | // If you want to cache the remote images, you can set the time to live of the cache in seconds.
33 | // The default value is 0 seconds.
34 | nextImageExportOptimizer_remoteImageCacheTTL: "0",
35 | },
36 | }
37 |
38 |
39 | module.exports = withContentlayer(nextconfig)
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "design-code.tips",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "concurrently \"contentlayer2 dev\" \"next dev\" \"next-image-export-optimizer\"",
7 | "build": "contentlayer2 build && next build && next-image-export-optimizer",
8 | "start": "next start",
9 | "postbuild": "next-sitemap"
10 | },
11 | "dependencies": {
12 | "@heroicons/react": "^2.1.5",
13 | "framer-motion": "^11.3.30",
14 | "next": "^14.2.6",
15 | "next-image-export-optimizer": "^1.12.3",
16 | "prismjs": "^1.29.0",
17 | "react": "^18.2.0",
18 | "react-dom": "^18.2.0"
19 | },
20 | "devDependencies": {
21 | "@types/prismjs": "^1.26.3",
22 | "@types/react": "^18.0.17",
23 | "autoprefixer": "^10.4.19",
24 | "concurrently": "^8.2.2",
25 | "contentlayer2": "^0.5.0",
26 | "eslint": "^8.56.0",
27 | "eslint-config-next": "^14.2.6",
28 | "next-contentlayer2": "^0.5.0",
29 | "next-seo": "^6.5.0",
30 | "next-sitemap": "^4.2.3",
31 | "postcss": "^8.4.38",
32 | "react-markdown": "^9.0.1",
33 | "remark-gfm": "^4.0.0",
34 | "tailwindcss": "^3.4.3",
35 | "typescript": "^5.5.4"
36 | },
37 | "overrides": {
38 | "react": "^18.2.0",
39 | "@types/react": "^18.0.17",
40 | "react-dom": "^18.2.0"
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/admin/config.yml:
--------------------------------------------------------------------------------
1 | backend:
2 | name: git-gateway
3 | branch: main
4 |
5 | # local_backend: true
6 |
7 | # publish_mode: editorial_workflow
8 |
9 | editor:
10 | preview: false
11 |
12 | display_url: https://design-code.tips # This url will display in the top-right of the CMS
13 | media_folder: 'public/media' # Media files will be stored in the repo under static/media
14 | public_folder: '/media' # The src attribute for uploaded media will begin with /media
15 | collections:
16 | - name: 'page'
17 | label: 'Pages'
18 | files:
19 | - file: 'content/page/home.md'
20 | label: 'Home'
21 | name: 'home'
22 | fields:
23 | - label: 'Title'
24 | name: 'title'
25 | widget: string
26 |
27 | - file: 'content/page/about.md'
28 | label: 'About'
29 | name: 'about'
30 | fields:
31 | - { label: 'Title', name: 'title', widget: 'string' }
32 | - {
33 | label: 'Description',
34 | name: 'description',
35 | widget: 'markdown',
36 | sanitize_preview: true,
37 | buttons: ['bold', 'italic', 'link'],
38 | modes: ['rich_text'],
39 | editor_components: [''],
40 | }
41 |
42 | - file: 'content/page/contact.md'
43 | label: 'Contact'
44 | name: 'contact'
45 | fields:
46 | - { label: 'Title', name: 'title', widget: 'string' }
47 | - { label: 'Description', name: 'description', widget: 'text' }
48 |
49 | - name: 'blog' # Used in routes, e.g., /admin/collections/blog
50 | label: 'Code Blog' # Used in the UI
51 | folder: 'content/blog' # The path to the folder where the documents are stored
52 | create: true # Allow users to create new documents in this collection
53 | slug: '{{year}}-{{month}}-{{day}}-{{title}}' # Filename template, e.g., 2024-090-25-name-of-doc.md
54 | view_groups:
55 | - label: Year
56 | field: date
57 | pattern: \d{4}
58 | fields: # The fields for each document, usually in front matter
59 | - {
60 | label: 'Template Key', # This field is used to "group" the posts by category
61 | name: 'templateKey',
62 | widget: 'hidden', # It should be an `hidden` field
63 | default: 'blog',
64 | }
65 | - { label: 'Title', name: 'title', widget: string }
66 | - { label: 'Featured', name: 'featured', widget: boolean, default: false }
67 | - {
68 | label: 'Body',
69 | name: 'body',
70 | widget: 'markdown',
71 | sanitize_preview: true,
72 | # buttons: ['bold', 'italic', 'link'],
73 | # modes: ['rich_text'],
74 | # editor_components: [''],
75 | }
76 | - { label: 'Tags', name: 'tags', widget: 'list' }
77 | - { label: 'Publish Date', name: 'date', widget: 'datetime' }
78 |
79 | - name: 'inspiration'
80 | label: 'Inspiration'
81 | folder: 'content/inspiration'
82 | create: true
83 | slug: '{{year}}-{{month}}-{{day}}-{{title}}'
84 | extension: mdx
85 | format: frontmatter
86 | view_groups:
87 | - label: Year
88 | field: date
89 | pattern: \d{4}
90 | fields:
91 | - {
92 | label: 'Template Key',
93 | name: 'templateKey',
94 | widget: 'hidden',
95 | default: 'inspiration',
96 | }
97 | - { label: 'Title', name: 'title', widget: string }
98 | - { label: 'Featured', name: 'featured', widget: boolean, default: false }
99 | - {
100 | label: 'Featured Image',
101 | name: 'image',
102 | widget: 'image',
103 | choose_url: false,
104 | }
105 | - {
106 | label: 'Body',
107 | name: 'body',
108 | widget: 'markdown',
109 | sanitize_preview: true,
110 | minimal: true,
111 | }
112 | - { label: 'Tags', name: 'tags', widget: 'list' }
113 | - { label: 'Publish Date', name: 'date', widget: 'datetime' }
114 |
115 | - name: 'podcasts'
116 | label: 'Podcasts'
117 | label_singular: 'Podcast'
118 | folder: 'content/podcasts'
119 | create: true
120 | slug: '{{title}}'
121 | view_groups:
122 | - label: Year
123 | field: date
124 | pattern: \d{4}
125 | fields:
126 | - {
127 | label: 'Template Key',
128 | name: 'templateKey',
129 | widget: 'hidden',
130 | default: 'podcasts',
131 | }
132 | - { label: 'Title', name: 'title', widget: string }
133 | - { label: 'Featured', name: 'featured', widget: boolean, default: false }
134 | - { label: 'Link', name: 'link', widget: string }
135 | - {
136 | label: 'Featured Image',
137 | name: 'image',
138 | widget: 'image',
139 | choose_url: false,
140 | }
141 | - {
142 | label: 'Body',
143 | name: 'body',
144 | widget: 'markdown',
145 | sanitize_preview: true,
146 | minimal: true,
147 | }
148 | - { label: 'Tags', name: 'tags', widget: 'list' }
149 | - { label: 'Publish Date', name: 'date', widget: 'datetime' }
150 |
151 | - name: 'tools'
152 | label: 'Tools'
153 | label_singular: 'Tool'
154 | folder: 'content/tools'
155 | create: true
156 | slug: '{{title}}'
157 | view_groups:
158 | - label: Year
159 | field: date
160 | pattern: \d{4}
161 | fields:
162 | - {
163 | label: 'Template Key',
164 | name: 'templateKey',
165 | widget: 'hidden',
166 | default: 'tools',
167 | }
168 | - { label: 'Title', name: 'title', widget: string }
169 | - { label: 'Featured', name: 'featured', widget: boolean, default: false }
170 | - {
171 | label: 'App Icon Image',
172 | name: 'image',
173 | widget: 'image',
174 | choose_url: false,
175 | }
176 | - { label: 'Link', name: 'link', widget: string }
177 | - {
178 | label: 'Body',
179 | name: 'body',
180 | widget: 'markdown',
181 | sanitize_preview: true,
182 | minimal: true,
183 | }
184 | - { label: 'Tags', name: 'tags', widget: 'list' }
185 | - { label: 'Publish Date', name: 'date', widget: 'datetime' }
186 |
187 | - name: 'resources'
188 | label: 'Resources'
189 | label_singular: 'Resource'
190 | folder: 'content/resources'
191 | create: true
192 | slug: '{{title}}'
193 | view_groups:
194 | - label: Year
195 | field: date
196 | pattern: \d{4}
197 | fields:
198 | - {
199 | label: 'Template Key',
200 | name: 'templateKey',
201 | widget: 'hidden',
202 | default: 'resources',
203 | }
204 | - { label: 'Title', name: 'title', widget: string }
205 | - { label: 'Featured', name: 'featured', widget: boolean, default: false }
206 | - { label: 'Image', name: 'image', widget: 'image', choose_url: false }
207 | - { label: 'Link', name: 'link', widget: string }
208 | - {
209 | label: 'Body',
210 | name: 'body',
211 | widget: 'markdown',
212 | sanitize_preview: true,
213 | minimal: true,
214 | }
215 | - { label: 'Tags', name: 'tags', widget: 'list' }
216 | - { label: 'Publish Date', name: 'date', widget: 'datetime' }
217 |
218 | # - name: 'authors'
219 | # label: 'Authors'
220 | # folder: 'content/authors'
221 | # create: true
222 | # slug: '{{name}}'
223 | # fields:
224 | # - { label: 'Name', name: 'name', widget: 'string', required: true }
225 | # - {
226 | # label: 'About',
227 | # name: 'description',
228 | # widget: 'markdown',
229 | # sanitize_preview: true,
230 | # buttons: ['bold', 'italic', 'link', 'heading-two', 'heading-three'],
231 | # modes: ['rich_text'],
232 | # editor_components: [''],
233 | # }
234 | # - {
235 | # label: 'Featured Image',
236 | # name: 'image',
237 | # widget: 'image',
238 | # choose_url: false,
239 | # }
240 |
--------------------------------------------------------------------------------
/public/admin/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Content Manager
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ositaka/nextjs-blog-tailwind-starter/bddbee36633e308d6b938a58033c4fd88f149fa1/public/favicon.ico
--------------------------------------------------------------------------------
/public/favicon.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/public/media/inspiration__placeholder-post-1200x1200.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ositaka/nextjs-blog-tailwind-starter/bddbee36633e308d6b938a58033c4fd88f149fa1/public/media/inspiration__placeholder-post-1200x1200.png
--------------------------------------------------------------------------------
/public/media/next-image-export-optimizer-hashes.json:
--------------------------------------------------------------------------------
1 | {
2 | "/inspiration__placeholder-post-1200x1200.png": "36Co3KlGVKRS1Z9pqFqFKRNY3N+1OrpIoAnfZD05Hs0=",
3 | "/podcast__placeholder-post-1200x1200.png": "xNmc-VeeLwuqyhz76QCoiMm402A81N4Sg-Vpm2vUguQ=",
4 | "/resource__placeholder-post-1200x1200.png": "inpBE3NvolpDVpWBOd2A9Y+2AsIGSoR73rSLYJ9ysHs=",
5 | "/tools__placeholder-post-1200x1200.png": "JOGuU2dz-+cXfKR1a2KcBa6HspZlzkQtjt8bdjtuVFE="
6 | }
--------------------------------------------------------------------------------
/public/media/podcast__placeholder-post-1200x1200.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ositaka/nextjs-blog-tailwind-starter/bddbee36633e308d6b938a58033c4fd88f149fa1/public/media/podcast__placeholder-post-1200x1200.png
--------------------------------------------------------------------------------
/public/media/resource__placeholder-post-1200x1200.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ositaka/nextjs-blog-tailwind-starter/bddbee36633e308d6b938a58033c4fd88f149fa1/public/media/resource__placeholder-post-1200x1200.png
--------------------------------------------------------------------------------
/public/media/tools__placeholder-post-1200x1200.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ositaka/nextjs-blog-tailwind-starter/bddbee36633e308d6b938a58033c4fd88f149fa1/public/media/tools__placeholder-post-1200x1200.png
--------------------------------------------------------------------------------
/public/media/video__placeholder-post.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ositaka/nextjs-blog-tailwind-starter/bddbee36633e308d6b938a58033c4fd88f149fa1/public/media/video__placeholder-post.mp4
--------------------------------------------------------------------------------
/public/og-card.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ositaka/nextjs-blog-tailwind-starter/bddbee36633e308d6b938a58033c4fd88f149fa1/public/og-card.png
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # *
2 | User-agent: *
3 | Allow: /
4 |
5 | # Host
6 | Host: https://design-code.tips
7 |
8 | # Sitemaps
9 | Sitemap: https://design-code.tips/sitemap.xml
10 |
--------------------------------------------------------------------------------
/public/sitemap-0.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | https://design-code.tips/blog/2024-09-11T22:48:28.504Zdaily0.7
4 | https://design-code.tips/tags/2024-09-11T22:48:28.505Zdaily0.7
5 | https://design-code.tips/tools/2024-09-11T22:48:28.505Zdaily0.7
6 | https://design-code.tips/2024-09-11T22:48:28.505Zdaily0.7
7 | https://design-code.tips/inspiration/2024-09-11T22:48:28.505Zdaily0.7
8 | https://design-code.tips/resources/2024-09-11T22:48:28.505Zdaily0.7
9 | https://design-code.tips/about/2024-09-11T22:48:28.505Zdaily0.7
10 | https://design-code.tips/podcasts/2024-09-11T22:48:28.505Zdaily0.7
11 |
--------------------------------------------------------------------------------
/public/sitemap.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | https://design-code.tips/sitemap-0.xml
4 |
--------------------------------------------------------------------------------
/screenshot--cms1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ositaka/nextjs-blog-tailwind-starter/bddbee36633e308d6b938a58033c4fd88f149fa1/screenshot--cms1.png
--------------------------------------------------------------------------------
/screenshot--cms2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ositaka/nextjs-blog-tailwind-starter/bddbee36633e308d6b938a58033c4fd88f149fa1/screenshot--cms2.png
--------------------------------------------------------------------------------
/starter-cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ositaka/nextjs-blog-tailwind-starter/bddbee36633e308d6b938a58033c4fd88f149fa1/starter-cover.png
--------------------------------------------------------------------------------
/starter-preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ositaka/nextjs-blog-tailwind-starter/bddbee36633e308d6b938a58033c4fd88f149fa1/starter-preview.png
--------------------------------------------------------------------------------
/styles/index.css:
--------------------------------------------------------------------------------
1 | @import 'tailwindcss/base';
2 | @import 'tailwindcss/components';
3 | @import 'tailwindcss/utilities';
4 |
5 | html {
6 | overflow-x: hidden;
7 | margin-right: calc(-1 * (100vw - 100%));
8 | font-size: 20px;
9 | }
10 |
11 | body {
12 | -webkit-font-smoothing: antialiased;
13 | -moz-osx-font-smoothing: grayscale;
14 | background: #fafafa;
15 | }
16 |
17 | #__next {
18 | min-height: 100dvh;
19 | display: flex;
20 | flex-direction: column;
21 | justify-content: space-between;
22 | }
23 |
24 | main {
25 | margin: auto;
26 | }
27 |
28 | .blog-post {
29 | font-size: 1rem;
30 | letter-spacing: 0.02em;
31 | line-height: 1.8;
32 | font-weight: 300;
33 | }
34 |
35 | .blog-post * {
36 | margin-block: 1.75rem;
37 | }
38 |
39 | .blog-post h2 {
40 | font-size: 1.75rem;
41 | font-weight: 600;
42 | line-height: 1.2;
43 | margin-block: 3rem 1rem;
44 | text-wrap: balance;
45 | }
46 |
47 | .blog-post h2 + h3 {
48 | margin-block-start: 0rem;
49 | text-wrap: balance;
50 | }
51 |
52 | .blog-post h3 {
53 | font-size: 1.3rem;
54 | font-weight: 600;
55 | line-height: 1.2;
56 | margin-block: 2rem 1rem;
57 | text-wrap: balance;
58 | }
59 |
60 | .blog-post h4 {
61 | font-size: 1.125rem;
62 | font-weight: 600;
63 | line-height: 1.2;
64 | margin-block: 2rem 1rem;
65 | text-wrap: balance;
66 | }
67 |
68 | .blog-post a {
69 | font-weight: 550;
70 | border-bottom: currentColor 0.5px solid;
71 | }
72 | .blog-post a:hover,
73 | .blog-post a:focus {
74 | text-decoration: underline;
75 | text-underline-offset: 3px;
76 | }
77 |
78 | .blog-post hr {
79 | border: none;
80 | border-bottom: 1.5px solid currentColor;
81 | opacity: 0.2;
82 | }
83 |
84 | .blog-post img {
85 | width: 100%;
86 | }
87 |
88 | .blog-post pre {
89 | font-size: 0.8rem;
90 | margin-bottom: 2rem !important;
91 | }
92 |
93 | .blog-post pre:last-child {
94 | margin-bottom: 0 !important;
95 | }
96 |
97 | .blog-post p:not(:has(video, img)),
98 | .blog-post ul {
99 | max-width: 860px;
100 | text-wrap: balance;
101 | }
102 |
103 | .blog-post p {
104 | letter-spacing: 0.05ch;
105 | }
106 |
107 | @media screen and (min-width: 600px) {
108 | .blog-post p {
109 | text-wrap: pretty;
110 | }
111 | }
112 |
113 | .blog-post p code,
114 | .blog-post ul code,
115 | .blog-post ol code,
116 | .blog-post blockquote code {
117 | font-size: 0.9rem;
118 | font-weight: bold;
119 | padding-inline: 0.2rem;
120 | }
121 |
122 | .blog-post blockquote,
123 | .blog-post img,
124 | .blog-post video {
125 | border-radius: 0.75rem;
126 | margin-inline: auto;
127 | }
128 |
129 | .blog-post ul,
130 | .blog-post ol {
131 | padding-inline-start: 1rem;
132 | }
133 | .blog-post ul {
134 | list-style-type: disc;
135 | }
136 |
137 | .blog-post ul ul {
138 | list-style-type: circle;
139 | }
140 |
141 | .blog-post ol {
142 | list-style-type: decimal;
143 | }
144 |
145 | .blog-post ul ul,
146 | .blog-post ol ol {
147 | padding-inline-start: 2rem;
148 | margin-block: 0 1rem;
149 | }
150 |
151 | .blog-post li {
152 | display: list-item;
153 | margin-block: 0.5rem;
154 | text-wrap: balance;
155 | }
156 |
157 | .blog-post blockquote {
158 | border-left: 0.25rem solid color-mix(in srgb, currentColor 20%, transparent);
159 | background-color: color-mix(in srgb, currentColor 5%, transparent);
160 | padding: 0.25rem 1.5rem;
161 | margin-block: 1rem;
162 | font-style: italic;
163 | }
164 |
165 | .blog-post ul li > a {
166 | padding: 0.6ch 0;
167 | }
168 |
169 | /* Manually set the background color of the code blocks to match the theme: */
170 | .blog-post :not(pre) > code[class*='language-'],
171 | .blog-post pre[class*='language-'] {
172 | background: #1e293c;
173 | }
174 |
175 | @media (prefers-color-scheme: dark) {
176 | .blog-post :not(pre) > code[class*='language-'],
177 | .blog-post pre[class*='language-'] {
178 | background: #1e293ccc;
179 | }
180 | }
181 |
182 | ::-webkit-scrollbar {
183 | height: 0.25rem;
184 | width: 0.25rem;
185 | }
186 |
187 | ::-webkit-scrollbar-thumb {
188 | background: rgba(255, 255, 255, 0.5);
189 | }
190 |
191 | ::-webkit-scrollbar-track {
192 | background: transparent;
193 | }
194 |
--------------------------------------------------------------------------------
/styles/prism-a11y-dark.css:
--------------------------------------------------------------------------------
1 | /**
2 | * a11y-dark theme for JavaScript, CSS, and HTML
3 | * Based on the okaidia theme: https://github.com/PrismJS/prism/blob/gh-pages/themes/prism-okaidia.css
4 | * @author ericwbailey
5 | */
6 |
7 | code[class*="language-"],
8 | pre[class*="language-"] {
9 | color: #f8f8f2;
10 | background: none;
11 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
12 | text-align: left;
13 | white-space: pre;
14 | word-spacing: normal;
15 | word-break: normal;
16 | word-wrap: normal;
17 | line-height: 1.5;
18 |
19 | -moz-tab-size: 4;
20 | -o-tab-size: 4;
21 | tab-size: 4;
22 |
23 | -webkit-hyphens: none;
24 | -moz-hyphens: none;
25 | -ms-hyphens: none;
26 | hyphens: none;
27 | }
28 |
29 | /* Code blocks */
30 | pre[class*="language-"] {
31 | padding: 1em;
32 | margin: 0.5em 0;
33 | overflow: auto;
34 | border-radius: 0.3em;
35 | }
36 |
37 | :not(pre) > code[class*="language-"],
38 | pre[class*="language-"] {
39 | background: #2b2b2b;
40 | }
41 |
42 | /* Inline code */
43 | :not(pre) > code[class*="language-"] {
44 | padding: 0.1em;
45 | border-radius: 0.3em;
46 | white-space: normal;
47 | }
48 |
49 | .token.comment,
50 | .token.prolog,
51 | .token.doctype,
52 | .token.cdata {
53 | color: #d4d0ab;
54 | }
55 |
56 | .token.punctuation {
57 | color: #fefefe;
58 | }
59 |
60 | .token.property,
61 | .token.tag,
62 | .token.constant,
63 | .token.symbol,
64 | .token.deleted {
65 | color: #ffa07a;
66 | }
67 |
68 | .token.boolean,
69 | .token.number {
70 | color: #00e0e0;
71 | }
72 |
73 | .token.selector,
74 | .token.attr-name,
75 | .token.string,
76 | .token.char,
77 | .token.builtin,
78 | .token.inserted {
79 | color: #abe338;
80 | }
81 |
82 | .token.operator,
83 | .token.entity,
84 | .token.url,
85 | .language-css .token.string,
86 | .style .token.string,
87 | .token.variable {
88 | color: #00e0e0;
89 | }
90 |
91 | .token.atrule,
92 | .token.attr-value,
93 | .token.function {
94 | color: #ffd700;
95 | }
96 |
97 | .token.keyword {
98 | color: #00e0e0;
99 | }
100 |
101 | .token.regex,
102 | .token.important {
103 | color: #ffd700;
104 | }
105 |
106 | .token.important,
107 | .token.bold {
108 | font-weight: bold;
109 | }
110 |
111 | .token.italic {
112 | font-style: italic;
113 | }
114 |
115 | .token.entity {
116 | cursor: help;
117 | }
118 |
119 | @media screen and (-ms-high-contrast: active) {
120 | code[class*="language-"],
121 | pre[class*="language-"] {
122 | color: windowText;
123 | background: window;
124 | }
125 |
126 | :not(pre) > code[class*="language-"],
127 | pre[class*="language-"] {
128 | background: window;
129 | }
130 |
131 | .token.important {
132 | background: highlight;
133 | color: window;
134 | font-weight: normal;
135 | }
136 |
137 | .token.atrule,
138 | .token.attr-value,
139 | .token.function,
140 | .token.keyword,
141 | .token.operator,
142 | .token.selector {
143 | font-weight: bold;
144 | }
145 |
146 | .token.attr-value,
147 | .token.comment,
148 | .token.doctype,
149 | .token.function,
150 | .token.keyword,
151 | .token.operator,
152 | .token.property,
153 | .token.string {
154 | color: highlight;
155 | }
156 |
157 | .token.attr-value,
158 | .token.url {
159 | font-weight: normal;
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/styles/tailwind-classes.js:
--------------------------------------------------------------------------------
1 | // This file it's a backup containing all Tailwind classes used in the project.
2 | // These classes are not used directly in the project, but they are here for reference.
3 |
4 | export const wrapperClasses = "md:max-w-[87%] m-auto flex flex-col gap-6 mb-32"
5 | export const cardClasses = "px-4 py-4 w-full flex flex-col gap-2 bg-white dark:bg-slate-800 rounded-xl shadow-md hover:shadow-md hover:dark:bg-slate-700 transition-shadow dark:transition-colors"
6 | export const bgImage = "bg-slate-200 dark:bg-slate-700"
7 | export const tagClasses = "py-2 px-3 bg-slate-200 dark:bg-slate-700 hover:bg-slate-700 hover:text-slate-100 dark:hover:bg-slate-200 dark:hover:text-slate-900 rounded-sm font-mono text-sm transition-colors"
8 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: [
4 | "./components/**/*.{js,ts,jsx,tsx}",
5 | "./app/**/*.{js,ts,jsx,tsx}",
6 | ],
7 | theme: {
8 | extend: {},
9 | },
10 | plugins: [],
11 | }
12 |
13 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": [
4 | "dom",
5 | "dom.iterable",
6 | "esnext"
7 | ],
8 | "allowJs": true,
9 | "skipLibCheck": true,
10 | "strict": true,
11 | "forceConsistentCasingInFileNames": true,
12 | "noEmit": true,
13 | "incremental": true,
14 | "esModuleInterop": true,
15 | "module": "esnext",
16 | "moduleResolution": "node",
17 | "resolveJsonModule": true,
18 | "isolatedModules": true,
19 | "jsx": "preserve",
20 | "plugins": [
21 | {
22 | "name": "next"
23 | }
24 | ]
25 | },
26 | "include": [
27 | "next-env.d.ts",
28 | "**/*.ts",
29 | "**/*.tsx",
30 | ".next/types/**/*.ts"
31 | ],
32 | "exclude": [
33 | "node_modules"
34 | ]
35 | }
36 |
--------------------------------------------------------------------------------
/utils/index.js:
--------------------------------------------------------------------------------
1 | import { SHOW_PER_PAGE } from '../config'
2 |
3 | // array sortByDate
4 | export function sortByDate(a, b) {
5 | return new Date(b.date) - new Date(a.date)
6 | }
7 |
8 | // count the page number
9 | export function pageCount(number) {
10 | return Math.ceil(number / SHOW_PER_PAGE)
11 | }
12 |
13 | // Utility function to format a date
14 | export function formatDate(date) {
15 | const options = {
16 | dateStyle: 'medium',
17 | timeZone: 'Portugal',
18 | }
19 |
20 | return new Intl.DateTimeFormat('en-UK', options).format(date)
21 | }
--------------------------------------------------------------------------------
/utils/tags.ts:
--------------------------------------------------------------------------------
1 | export function extractUniqueTags(tags: any[]) {
2 | // Collect all tags from all posts
3 | const allTags = tags.reduce((acc, post) => {
4 | // Check if the post has tags
5 | return acc.concat(post.tags || []); // Using `|| []` to handle cases where tags is undefined
6 | }, []);
7 |
8 | // Remove duplicates by converting to Set and then back to an array
9 | let uniqueTags = Array.from(new Set(allTags));
10 |
11 | // Remove undefined tags
12 | uniqueTags = uniqueTags.filter(tag => tag !== undefined);
13 |
14 | // Sort tags alphabetically
15 | uniqueTags.sort();
16 |
17 | return uniqueTags;
18 | }
--------------------------------------------------------------------------------