├── .DS_Store ├── .env.example ├── .gitignore ├── .vscode └── settings.json ├── README.md ├── app.vue ├── assets ├── .DS_Store ├── css │ └── tailwind.css ├── fonts │ ├── .DS_Store │ └── Recoleta-Black │ │ ├── .DS_Store │ │ ├── Recoleta-Black.ttf │ │ ├── Recoleta-Black.woff │ │ ├── Recoleta-Black.woff2 │ │ ├── Recoleta-Regular.ttf │ │ ├── Recoleta-Regular.woff │ │ ├── Recoleta-Regular.woff2 │ │ ├── Recoleta-SemiBold.ttf │ │ ├── Recoleta-SemiBold.woff │ │ ├── Recoleta-SemiBold.woff2 │ │ └── style.css ├── icon-white.svg └── img │ └── .DS_Store ├── components ├── .DS_Store ├── ColorTheme.vue ├── Footer.vue ├── InputVal.vue ├── Navbar.vue ├── TextAreaVal.vue ├── blog │ └── Post.vue ├── content │ ├── Disclaimer.vue │ ├── PrevNext.vue │ ├── info.vue │ └── toc.vue ├── home │ ├── BlogPost.vue │ ├── ContactForm.vue │ └── RepoCard.vue ├── icons │ ├── ArrowDropdown.vue │ ├── ArrowLeft.vue │ ├── ArrowRight.vue │ ├── ArrowUp.vue │ ├── ForkIcon.vue │ ├── Github.vue │ ├── Link.vue │ ├── Loader.vue │ ├── Logo.vue │ ├── LogoIcon.vue │ ├── ReadIcon.vue │ ├── RepoIcon.vue │ ├── RightArrow.vue │ ├── Search.vue │ ├── StarIcon.vue │ ├── TagsIcon.vue │ ├── Twitter.vue │ ├── UpRightArrow-2.vue │ └── UpRightArrow.vue └── post │ └── Newsletter.vue ├── content └── posts │ └── why-did-i-create-a-blog.md ├── error.vue ├── layouts └── default.vue ├── nuxt.config.ts ├── package-lock.json ├── package.json ├── pages ├── blog.vue ├── index.vue ├── posts │ └── [...slug].vue ├── projects.vue ├── snippets.vue └── tags │ └── [slug].vue ├── public ├── .DS_Store ├── img │ ├── .DS_Store │ ├── cover_1.svg │ ├── my_logo.png │ └── open-graph-preview.png └── preview │ ├── .DS_Store │ └── chrome-capture-2023-0-20.gif ├── schema.d.ts ├── server ├── api │ ├── github.get.ts │ └── notion.post.ts └── routes │ ├── rss.xml.ts │ └── sitemap.xml.ts ├── tailwind.config.ts ├── tokens.config.ts └── tsconfig.json /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dmytro-Tihunov/nuxt3-portfolio-blog/040306c69c1a446b03adfea4c90d8fd6f3143947/.DS_Store -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | // Your notion token for contact form 2 | NUXT_PUBLIC_NOTION_TOKEN= 3 | // Your github token to show repositories 4 | NUXT_PUBLIC_GITHUB_TOKEN= -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log* 3 | .nuxt 4 | .nitro 5 | .cache 6 | .output 7 | .env 8 | dist 9 | 10 | # IDE / Editor 11 | .idea 12 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "**/.git": true, 4 | "**/.svn": true, 5 | "**/.hg": true, 6 | "**/CVS": true, 7 | "**/.DS_Store": true, 8 | "**/Thumbs.db": true, 9 | "**/node_modules": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](./public/preview/chrome-capture-2023-0-20.gif) 2 | 3 | Soon coming update 4 | 5 | # Tihunov.com 6 | 7 | - Framework: [Nuxt 3](https://nuxt.com/v3) 8 | - Deployment: [Vercel](https://vercel.com) 9 | - Styling: [Tainwind CSS](https://tailwindcss.com) 10 | - Forms: [Notion](https://developers.notion.com) as a database 11 | - Blog: [Nuxt content](https://tailwindcss.com) v2 12 | 13 | ## Setup 14 | 15 | ```bash 16 | git clone https://github.com/Dmytro-Tihunov/nuxt3-portfolio-blog 17 | npm install 18 | pnpm install 19 | ``` 20 | 21 | Create a `.env` file similar to `.env.example.` 22 | 23 | ## Development Server 24 | 25 | Start the development server on http://localhost:3000 26 | 27 | ```bash 28 | npm run dev 29 | ``` 30 | 31 | ## Production 32 | 33 | Build the application for production: 34 | 35 | ```bash 36 | npm run build 37 | ``` 38 | 39 | Locally preview production build: 40 | 41 | ```bash 42 | npm run preview 43 | ``` 44 | 45 | Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information. 46 | -------------------------------------------------------------------------------- /app.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /assets/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dmytro-Tihunov/nuxt3-portfolio-blog/040306c69c1a446b03adfea4c90d8fd6f3143947/assets/.DS_Store -------------------------------------------------------------------------------- /assets/css/tailwind.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;700&display=swap"); 2 | 3 | @font-face { 4 | font-family: "Recoleta"; 5 | src: local("Recoleta Black"), local("Recoleta-Black"), 6 | url("../fonts/Recoleta-Black/Recoleta-Black.woff2") format("woff2"), 7 | url("../fonts/Recoleta-Black/Recoleta-Black.woff") format("woff"), 8 | url("../fonts/Recoleta-Black/Recoleta-Black.ttf") format("truetype"); 9 | font-weight: 900; 10 | font-style: normal; 11 | } 12 | 13 | @font-face { 14 | font-family: "Recoleta"; 15 | src: local("Recoleta SemiBold"), local("Recoleta-SemiBold"), 16 | url("../fonts/Recoleta-Black/Recoleta-SemiBold.woff2") format("woff2"), 17 | url("../fonts/Recoleta-Black/Recoleta-SemiBold.woff") format("woff"), 18 | url("../fonts/Recoleta-Black/Recoleta-SemiBold.ttf") format("truetype"); 19 | font-weight: 600; 20 | font-style: normal; 21 | } 22 | 23 | @font-face { 24 | font-family: "Recoleta"; 25 | src: local("Recoleta Regular"), local("Recoleta-Regular"), 26 | url("../fonts/Recoleta-Black/Recoleta-Regular.woff2") format("woff2"), 27 | url("../fonts/Recoleta-Black/Recoleta-Regular.woff") format("woff"), 28 | url("../fonts/Recoleta-Black/Recoleta-Regular.ttf") format("truetype"); 29 | font-weight: 400; 30 | font-style: normal; 31 | } 32 | 33 | @tailwind base; 34 | @tailwind components; 35 | @tailwind utilities; 36 | 37 | @layer utilities { 38 | body { 39 | @apply font-inter bg-[#f1f2f4] text-[#171717] dark:bg-[#151718] dark:text-[#ededef]; 40 | } 41 | .title { 42 | @apply text-3xl sm:text-4xl font-semibold font-recoleta; 43 | } 44 | .input { 45 | @apply h-[40px] rounded-md border border-[#c7c7c7] focus:border-[#303063] pl-4 outline-0 dark:border-[#34343a] bg-[#f1f2f4] pr-[18px] text-[13px] dark:bg-[#232425]; 46 | } 47 | .input.has-error { 48 | @apply border-red-500 dark:border-[#FCA5A5]; 49 | } 50 | .input-search { 51 | @apply h-[40px] rounded-md border border-[#c7c7c7] focus:border-[#303063] pl-8 52 | sm:w-[300px] outline-0 dark:border-[#34343a] bg-[#ffffff] pr-[18px] dark:bg-[#232425]; 53 | } 54 | .textarea.has-error { 55 | @apply border-red-500 dark:border-[#FCA5A5]; 56 | } 57 | .button { 58 | @apply inline-flex rounded-full relative border py-[8px] pl-[8px] pr-[13px] text-[15px] border-[#c7c7c7] dark:border-[#34343a] focus:scale-90; 59 | } 60 | .button-more-outlined { 61 | @apply flex items-center justify-center py-3 text-center duration-300; 62 | } 63 | .button-more-outlined:hover { 64 | @apply text-[#9CA3AF] duration-300; 65 | } 66 | .button-more { 67 | @apply mt-4 flex items-center justify-center rounded-md bg-[#E8E8E8] py-3 text-center text-[#6f6f6f] dark:text-[#6372a5] duration-300 dark:bg-[#171C34]; 68 | } 69 | .button-more:hover { 70 | @apply text-white duration-300; 71 | } 72 | .button-readnow { 73 | @apply mt-2; 74 | } 75 | .textarea { 76 | @apply rounded-md border border-[#c7c7c7] dark:border-[#34343a] bg-[#f1f2f4] p-4 pr-[18px] text-[13px] outline-none focus:border-[#303063] dark:bg-[#232425]; 77 | } 78 | .social-home-button { 79 | @apply mr-[11px] h-[36px] flex justify-center items-center bg-[#E8E8E8] w-[36px] rounded-full dark:bg-[#232425]; 80 | } 81 | .posthome { 82 | @apply pb-4 flex items-center; 83 | } 84 | .posthome:nth-child(1), 85 | .posthome:nth-child(2) { 86 | @apply border-b border-[#c7c7c7] dark:border-[#475584]; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /assets/fonts/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dmytro-Tihunov/nuxt3-portfolio-blog/040306c69c1a446b03adfea4c90d8fd6f3143947/assets/fonts/.DS_Store -------------------------------------------------------------------------------- /assets/fonts/Recoleta-Black/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dmytro-Tihunov/nuxt3-portfolio-blog/040306c69c1a446b03adfea4c90d8fd6f3143947/assets/fonts/Recoleta-Black/.DS_Store -------------------------------------------------------------------------------- /assets/fonts/Recoleta-Black/Recoleta-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dmytro-Tihunov/nuxt3-portfolio-blog/040306c69c1a446b03adfea4c90d8fd6f3143947/assets/fonts/Recoleta-Black/Recoleta-Black.ttf -------------------------------------------------------------------------------- /assets/fonts/Recoleta-Black/Recoleta-Black.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dmytro-Tihunov/nuxt3-portfolio-blog/040306c69c1a446b03adfea4c90d8fd6f3143947/assets/fonts/Recoleta-Black/Recoleta-Black.woff -------------------------------------------------------------------------------- /assets/fonts/Recoleta-Black/Recoleta-Black.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dmytro-Tihunov/nuxt3-portfolio-blog/040306c69c1a446b03adfea4c90d8fd6f3143947/assets/fonts/Recoleta-Black/Recoleta-Black.woff2 -------------------------------------------------------------------------------- /assets/fonts/Recoleta-Black/Recoleta-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dmytro-Tihunov/nuxt3-portfolio-blog/040306c69c1a446b03adfea4c90d8fd6f3143947/assets/fonts/Recoleta-Black/Recoleta-Regular.ttf -------------------------------------------------------------------------------- /assets/fonts/Recoleta-Black/Recoleta-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dmytro-Tihunov/nuxt3-portfolio-blog/040306c69c1a446b03adfea4c90d8fd6f3143947/assets/fonts/Recoleta-Black/Recoleta-Regular.woff -------------------------------------------------------------------------------- /assets/fonts/Recoleta-Black/Recoleta-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dmytro-Tihunov/nuxt3-portfolio-blog/040306c69c1a446b03adfea4c90d8fd6f3143947/assets/fonts/Recoleta-Black/Recoleta-Regular.woff2 -------------------------------------------------------------------------------- /assets/fonts/Recoleta-Black/Recoleta-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dmytro-Tihunov/nuxt3-portfolio-blog/040306c69c1a446b03adfea4c90d8fd6f3143947/assets/fonts/Recoleta-Black/Recoleta-SemiBold.ttf -------------------------------------------------------------------------------- /assets/fonts/Recoleta-Black/Recoleta-SemiBold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dmytro-Tihunov/nuxt3-portfolio-blog/040306c69c1a446b03adfea4c90d8fd6f3143947/assets/fonts/Recoleta-Black/Recoleta-SemiBold.woff -------------------------------------------------------------------------------- /assets/fonts/Recoleta-Black/Recoleta-SemiBold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dmytro-Tihunov/nuxt3-portfolio-blog/040306c69c1a446b03adfea4c90d8fd6f3143947/assets/fonts/Recoleta-Black/Recoleta-SemiBold.woff2 -------------------------------------------------------------------------------- /assets/fonts/Recoleta-Black/style.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Recoleta'; 3 | src: local('Recoleta Regular'), local('Recoleta-Regular'), 4 | url('Recoleta-Regular.woff2') format('woff2'), 5 | url('Recoleta-Regular.woff') format('woff'), 6 | url('Recoleta-Regular.ttf') format('truetype'); 7 | font-weight: 400; 8 | font-style: normal; 9 | } -------------------------------------------------------------------------------- /assets/icon-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/img/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dmytro-Tihunov/nuxt3-portfolio-blog/040306c69c1a446b03adfea4c90d8fd6f3143947/assets/img/.DS_Store -------------------------------------------------------------------------------- /components/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dmytro-Tihunov/nuxt3-portfolio-blog/040306c69c1a446b03adfea4c90d8fd6f3143947/components/.DS_Store -------------------------------------------------------------------------------- /components/ColorTheme.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 26 | -------------------------------------------------------------------------------- /components/Footer.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 44 | -------------------------------------------------------------------------------- /components/InputVal.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 55 | -------------------------------------------------------------------------------- /components/Navbar.vue: -------------------------------------------------------------------------------- 1 | 46 | -------------------------------------------------------------------------------- /components/TextAreaVal.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 58 | -------------------------------------------------------------------------------- /components/blog/Post.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 61 | -------------------------------------------------------------------------------- /components/content/Disclaimer.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /components/content/PrevNext.vue: -------------------------------------------------------------------------------- 1 | 7 | 32 | -------------------------------------------------------------------------------- /components/content/info.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 83 | -------------------------------------------------------------------------------- /components/content/toc.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 31 | -------------------------------------------------------------------------------- /components/home/BlogPost.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 55 | 56 | 67 | -------------------------------------------------------------------------------- /components/home/ContactForm.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 72 | -------------------------------------------------------------------------------- /components/home/RepoCard.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 43 | -------------------------------------------------------------------------------- /components/icons/ArrowDropdown.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /components/icons/ArrowLeft.vue: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /components/icons/ArrowRight.vue: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /components/icons/ArrowUp.vue: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /components/icons/ForkIcon.vue: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /components/icons/Github.vue: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /components/icons/Link.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /components/icons/Loader.vue: -------------------------------------------------------------------------------- 1 | 39 | -------------------------------------------------------------------------------- /components/icons/Logo.vue: -------------------------------------------------------------------------------- 1 | 34 | -------------------------------------------------------------------------------- /components/icons/LogoIcon.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 86 | -------------------------------------------------------------------------------- /components/icons/ReadIcon.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /components/icons/RepoIcon.vue: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /components/icons/RightArrow.vue: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /components/icons/Search.vue: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /components/icons/StarIcon.vue: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /components/icons/TagsIcon.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /components/icons/Twitter.vue: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /components/icons/UpRightArrow-2.vue: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /components/icons/UpRightArrow.vue: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /components/post/Newsletter.vue: -------------------------------------------------------------------------------- 1 | 24 | -------------------------------------------------------------------------------- /content/posts/why-did-i-create-a-blog.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Why did I create the blog? 3 | description: Short story about me, my way as a developer and decision of creating the blog. 4 | publishDate: Junuary 01, 2023 5 | date: 2023-01-01 6 | img: "/img/cover_1.svg" 7 | tags: ["blog"] 8 | --- 9 | 10 | ::Disclaimer 11 | Probably this story may be seemed as a piece of shit but I don't give a shit to it :) 12 |
(I'm not good at writing blogs and I'm not even good at English) 13 | :: 14 | 15 | ## Short story about me 16 | 17 | Welcome to my blog! I think for the first blog post in my life it would be nice to introduce myself however I'm a shy person however I have to :) 18 | 19 | I'm Dmytro a guy from Ukraine who has been programming for 7 years, I'm a self-taught passionate programmer and former marathon runner. Last 5 years I worked on freelance as a frontend developer, eventually I used to create only simple projects where you only need to know a little bit of Vanilla JS, HTML, CSS. 20 | 21 | It seems to me that I have stucked as a developer for 5 years and now I decided to curb web development with the best technologies, tools, and libraries. 22 | 23 | ## War in Ukraine 24 | 25 | On 24th February 2022, all Ukrainians were waked up by Russian missiles and bombs since this day everything changed for good. 26 | 27 | The most stresful and tragedic moment in one's life 28 | 29 | The most impressive phrase that I've heard in my life, There are many phrases that have been said but this one is my favourite 30 | 31 | > “Russian terrotists do not have suck missles that could fall into ukranians desire to live.” © President Volodymyr Zelensky 32 | 33 | ## Why am I Blogging? 34 | 35 | There are several reasons to start blogging like: 36 | 37 | - I hope my articles will be useful for some people, I will work hard to make my articles useful. 38 | - Through this blog, I hope to inspire others by sharing my journey and help them find their own unique voice. 39 | - It will help me to improve my English, I have been studying English for 3 years but I lack english practice. 40 | - I wanted to use these stories as a way of expressing myself. 41 | - Writing a blog will allow me to explore different web development technologies (frameworks, libraries, tools) thus I will enhance my development skills. 42 | - Probably it can help to get a job or work in a team with great ideas :) 43 | 44 | ## What I'm going to blog about 45 | 46 | I know that The Internet is filled with unlimited resources, and blogs already. Starting a blog is like a marathon not a sprint thus I won't give up even if I don't succeed :) 47 | 48 | I hope my posts will stand out, mainly I'm going to blog about my journey as a web developer, about what I know and I don't. 49 | -------------------------------------------------------------------------------- /error.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 40 | -------------------------------------------------------------------------------- /layouts/default.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /nuxt.config.ts: -------------------------------------------------------------------------------- 1 | export default defineNuxtConfig({ 2 | runtimeConfig: { 3 | public: {}, 4 | }, 5 | 6 | modules: [ 7 | "@nuxtjs/tailwindcss", 8 | "@nuxtjs/color-mode", 9 | "@nuxt/content", 10 | "@nuxtjs/web-vitals", 11 | "@nuxtjs/robots", 12 | ], 13 | 14 | extends: "@nuxt-themes/typography", 15 | 16 | colorMode: { 17 | preference: "dark", 18 | classSuffix: "", 19 | }, 20 | 21 | content: { 22 | markdown: { 23 | remarkPlugins: ["remark-reading-time"], 24 | }, 25 | highlight: { 26 | theme: { 27 | default: "github-light", 28 | dark: "github-dark", 29 | }, 30 | }, 31 | }, 32 | 33 | tailwindcss: { 34 | cssPath: "~/assets/css/tailwind.css", 35 | }, 36 | 37 | webVitals: { 38 | provider: "log", 39 | debug: true, // debug enable metrics reporting on dev environments 40 | disabled: false, 41 | }, 42 | 43 | nitro: { 44 | prerender: { 45 | routes: ["/sitemap.xml", "/rss.xml"], 46 | }, 47 | }, 48 | 49 | compatibilityDate: "2024-12-31", 50 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "build": "nuxt build", 5 | "dev": "nuxt dev", 6 | "generate": "nuxt generate", 7 | "preview": "nuxt preview", 8 | "postinstall": "nuxt prepare" 9 | }, 10 | "devDependencies": { 11 | "@nuxt-themes/typography": "^0.1.3", 12 | "@nuxt/content": "^2.2.2", 13 | "@nuxtjs/color-mode": "^3.1.8", 14 | "@nuxtjs/google-fonts": "^2.0.0", 15 | "@nuxtjs/tailwindcss": "^6.1.3", 16 | "content-wind": "^0.3.0", 17 | "nuxt": "^3.15.0", 18 | "sitemap": "^7.1.1" 19 | }, 20 | "dependencies": { 21 | "@notionhq/client": "^2.2.2", 22 | "@nuxtjs/robots": "^3.0.0", 23 | "@nuxtjs/web-vitals": "^0.2.2", 24 | "@tailwindcss/typography": "^0.5.8", 25 | "@vueuse/nuxt": "^9.9.0", 26 | "octokit": "^2.0.10", 27 | "remark-reading-time": "^2.0.1", 28 | "rss": "^1.2.2", 29 | "vee-validate": "^4.7.3", 30 | "yup": "^0.32.11" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /pages/blog.vue: -------------------------------------------------------------------------------- 1 | 110 | 111 | 177 | -------------------------------------------------------------------------------- /pages/index.vue: -------------------------------------------------------------------------------- 1 | 67 | 68 | 167 | -------------------------------------------------------------------------------- /pages/posts/[...slug].vue: -------------------------------------------------------------------------------- 1 | 59 | 60 | 152 | 153 | 158 | -------------------------------------------------------------------------------- /pages/projects.vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /pages/snippets.vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /pages/tags/[slug].vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 41 | -------------------------------------------------------------------------------- /public/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dmytro-Tihunov/nuxt3-portfolio-blog/040306c69c1a446b03adfea4c90d8fd6f3143947/public/.DS_Store -------------------------------------------------------------------------------- /public/img/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dmytro-Tihunov/nuxt3-portfolio-blog/040306c69c1a446b03adfea4c90d8fd6f3143947/public/img/.DS_Store -------------------------------------------------------------------------------- /public/img/cover_1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | Dmytro Tihunov 🎉 Dmytro Tihunov 🎉 Dmytro Tihunov 🎉 Dmytro Tihunov 🎉 Dmytro Tihunov 🎉 Dmytro Tihunov 🎉 Dmytro Tihunov 🎉 Dmytro Tihunov 🎉 Dmytro Tihunov 🎉 Dmytro Tihunov 🎉 22 | 23 | 24 | -------------------------------------------------------------------------------- /public/img/my_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dmytro-Tihunov/nuxt3-portfolio-blog/040306c69c1a446b03adfea4c90d8fd6f3143947/public/img/my_logo.png -------------------------------------------------------------------------------- /public/img/open-graph-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dmytro-Tihunov/nuxt3-portfolio-blog/040306c69c1a446b03adfea4c90d8fd6f3143947/public/img/open-graph-preview.png -------------------------------------------------------------------------------- /public/preview/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dmytro-Tihunov/nuxt3-portfolio-blog/040306c69c1a446b03adfea4c90d8fd6f3143947/public/preview/.DS_Store -------------------------------------------------------------------------------- /public/preview/chrome-capture-2023-0-20.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dmytro-Tihunov/nuxt3-portfolio-blog/040306c69c1a446b03adfea4c90d8fd6f3143947/public/preview/chrome-capture-2023-0-20.gif -------------------------------------------------------------------------------- /schema.d.ts: -------------------------------------------------------------------------------- 1 | export interface post { 2 | _path: string; 3 | title: string; 4 | description: string; 5 | date: number; 6 | img: string; 7 | publishDate: string; 8 | tags: string[]; 9 | readingTime: { text: string }; 10 | } 11 | 12 | export type repoCard = { 13 | name: string; 14 | description: string; 15 | created: string; 16 | url: string; 17 | stars: number; 18 | forks: number; 19 | } 20 | -------------------------------------------------------------------------------- /server/api/github.get.ts: -------------------------------------------------------------------------------- 1 | import { Octokit } from "octokit"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const octokit = new Octokit({ 5 | auth: process.env.NUXT_PUBLIC_GITHUB_TOKEN, // Change in .env 6 | }); 7 | 8 | function transformGithubData(repo: any) { 9 | const desc = repo.description ?? "No description"; 10 | return { 11 | name: repo.name, 12 | description: desc, 13 | created: repo.created_at.split("T")[0], 14 | url: repo.html_url, 15 | stars: repo.stargazers_count, 16 | forks: repo.forks, 17 | }; 18 | } 19 | 20 | const response = await octokit.request("GET /users/{username}/repos", { 21 | username: "Dmytro-Tihunov", // Your github username 22 | }); 23 | 24 | if (!response) { 25 | throw createError({ 26 | statusCode: 500, 27 | statusMessage: "Github API response is undefined", 28 | }); 29 | } 30 | 31 | return response.data.map((rep) => { 32 | return transformGithubData(rep); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /server/api/notion.post.ts: -------------------------------------------------------------------------------- 1 | import { Client } from "@notionhq/client"; 2 | 3 | export default eventHandler(async (event) => { 4 | function toDate(unixTimestamp: number) { 5 | return new Date(unixTimestamp * 1000).toString(); 6 | } 7 | 8 | const body = await readBody(event); 9 | 10 | const notion = new Client({ 11 | auth: process.env.NUXT_PUBLIC_NOTION_TOKEN, // Change in .env 12 | }); 13 | const formDatabaseId: string = "3cc733cd7673445099be0657960893ea" ?? ""; // Change accordingly to your database id 14 | 15 | const response = await notion.pages.create({ 16 | parent: { 17 | type: "database_id", 18 | database_id: formDatabaseId, 19 | }, 20 | properties: { 21 | Date: { 22 | rich_text: [ 23 | { 24 | text: { 25 | content: toDate(Date.now()), 26 | }, 27 | }, 28 | ], 29 | }, 30 | Name: { 31 | title: [ 32 | { 33 | text: { 34 | content: body.name, 35 | }, 36 | }, 37 | ], 38 | }, 39 | Email: { 40 | email: body.email, 41 | }, 42 | Message: { 43 | rich_text: [ 44 | { 45 | text: { 46 | content: body.message, 47 | }, 48 | }, 49 | ], 50 | }, 51 | }, 52 | }); 53 | 54 | if (!response) { 55 | throw createError({ 56 | statusCode: 500, 57 | statusMessage: "Notion API response is undefined", 58 | }); 59 | } 60 | 61 | return response; 62 | }); 63 | -------------------------------------------------------------------------------- /server/routes/rss.xml.ts: -------------------------------------------------------------------------------- 1 | import { Title } from "./../../.nuxt/components.d"; 2 | import { serverQueryContent } from "#content/server"; 3 | import RSS from "rss"; 4 | 5 | export default defineEventHandler(async (event) => { 6 | const feed = new RSS({ 7 | Title: "Dmytro Tihunov", 8 | site_url: "https://tihunov.com", 9 | feed_url: `https://tihunov.com/rss.xml`, 10 | }); 11 | 12 | const docs = await serverQueryContent(event) 13 | .sort({ date: -1 }) 14 | .where({ _partial: false }) 15 | .find(); 16 | const blogPosts = docs.filter((doc) => doc?._path?.includes("/posts")); 17 | 18 | for (const doc of blogPosts) { 19 | feed.item({ 20 | title: doc.title ?? "-", 21 | url: `https://tihunov.com${doc._path}`, 22 | date: doc.date, 23 | description: doc.description, 24 | }); 25 | } 26 | 27 | const feedString = feed.xml({ indent: true }); 28 | event.res.setHeader("content-type", "text/xml"); 29 | event.res.end(feedString); 30 | }); 31 | -------------------------------------------------------------------------------- /server/routes/sitemap.xml.ts: -------------------------------------------------------------------------------- 1 | import { serverQueryContent } from "#content/server"; 2 | import { SitemapStream, streamToPromise } from "sitemap"; 3 | import { dirname, resolve } from "path"; 4 | import { fileURLToPath } from "url"; 5 | import fs from "fs"; 6 | 7 | export default defineEventHandler(async (event) => { 8 | const sitemap = new SitemapStream({ 9 | hostname: "https://tihunov.com", 10 | }); 11 | 12 | const docs = await serverQueryContent(event).find(); 13 | 14 | const staticEndpoints = getStaticEndpoints(); 15 | 16 | const dynamicTags: string[] = [ 17 | ...new Set( 18 | flatten( 19 | docs.map((el) => el.tags), 20 | "tags" 21 | ) 22 | ), 23 | ]; 24 | 25 | for (const staticEndpoint of staticEndpoints) { 26 | sitemap.write({ url: staticEndpoint, changefreq: "monthly" }); 27 | } 28 | 29 | for (const doc of docs) { 30 | sitemap.write({ 31 | url: doc._path, 32 | changefreq: "monthly", 33 | }); 34 | } 35 | 36 | for (const tags of dynamicTags) { 37 | sitemap.write({ url: `/tags/${tags}`, changefreq: "monthly" }); 38 | } 39 | 40 | sitemap.end(); 41 | return streamToPromise(sitemap); 42 | }); 43 | 44 | function flatten(tags: any[], key: string) { 45 | let _tags = tags 46 | .map((tag) => { 47 | let _tag = tag; 48 | if (tag[key]) { 49 | let flattened = flatten(tag[key]); 50 | _tag = flattened; 51 | } 52 | return _tag; 53 | }) 54 | .flat(1); 55 | return _tags; 56 | } 57 | 58 | function getStaticEndpoints(): string[] { 59 | const __dirname = dirname(fileURLToPath(import.meta.url)); 60 | const files = getFiles(`${__dirname}/../../pages`); 61 | return files 62 | .filter((file) => !file.includes("slug")) // exclude dynamic content 63 | .map((file) => file.split("pages")[1]) 64 | .map((file) => { 65 | return file.endsWith("index.vue") 66 | ? file.split("/index.vue")[0] 67 | : file.split(".vue")[0]; 68 | }); 69 | } 70 | 71 | function getFiles(dir: string): string[] { 72 | const dirents = fs.readdirSync(dir, { withFileTypes: true }); 73 | const files = dirents.map((dirent) => { 74 | const res = resolve(dir, dirent.name); 75 | return dirent.isDirectory() ? getFiles(res) : res; 76 | }); 77 | return files.flat(); 78 | } 79 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | //import defaultTheme from "@tailwindcss/defaultTheme"; 3 | export default >{ 4 | darkMode: "class", 5 | plugins: [require("@tailwindcss/typography")], 6 | content: ["content/**/*.md"], 7 | theme: { 8 | extend: { 9 | colors: {}, 10 | fontFamily: { 11 | recoleta: ["Recoleta", "sans-serif"], 12 | inter: ["Inter", "sans-serif"], 13 | }, 14 | }, 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /tokens.config.ts: -------------------------------------------------------------------------------- 1 | import { defineTheme } from "pinceau"; 2 | 3 | export default defineTheme({ 4 | typography: { 5 | color: { 6 | blue: "#232425", 7 | white: "#ffffff", 8 | whitec: "#ffffff", 9 | borderb: "#34343a", 10 | borderw: "#c7c7c7", 11 | }, 12 | }, 13 | prose: { 14 | code: { 15 | block: { 16 | borderColor: { 17 | light: "{typography.color.borderw}", 18 | dark: "{typography.color.borderb}", 19 | }, 20 | backgroundColor: { 21 | light: "{typography.color.white}", 22 | dark: "{typography.color.blue}", 23 | }, 24 | }, 25 | inline: { 26 | backgroundColor: { 27 | light: "{typography.color.whitec}", 28 | dark: "{typography.color.blue}", 29 | }, 30 | }, 31 | }, 32 | }, 33 | }); 34 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://nuxt.com/docs/guide/concepts/typescript 3 | "extends": "./.nuxt/tsconfig.json" 4 | } 5 | --------------------------------------------------------------------------------