diff --git a/content/projects/itour.md b/content/projects/itour.md
new file mode 100644
index 0000000..57aa30b
--- /dev/null
+++ b/content/projects/itour.md
@@ -0,0 +1,7 @@
+---
+title: 'iTour'
+image: '/iTour.jpg'
+technologies: ['JavaScript', 'React', 'Next.js', 'TailwindCSS', 'i18n']
+---
+
+iTour project info goes here
diff --git a/content/projects/skauna.md b/content/projects/skauna.md
new file mode 100644
index 0000000..b20f736
--- /dev/null
+++ b/content/projects/skauna.md
@@ -0,0 +1,7 @@
+---
+title: 'Skauna'
+image: '/Skauna.jpg'
+technologies: ['HTML', 'CSS', 'JavaScript']
+---
+
+Skauna project info goes here
diff --git a/package-lock.json b/package-lock.json
index 07834a6..a9a7a38 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,14 +8,21 @@
"name": "portfolio-site",
"version": "0.1.0",
"dependencies": {
+ "@emailjs/nodejs": "^2.2.0",
+ "@hookform/resolvers": "^3.3.4",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
+ "gray-matter": "^4.0.3",
+ "marked": "^12.0.0",
"next": "14.1.0",
"next-themes": "^0.2.1",
"react": "^18",
"react-dom": "^18",
+ "react-hook-form": "^7.50.1",
"react-icons": "^5.0.1",
- "tailwind-merge": "^2.2.1"
+ "sonner": "^1.4.0",
+ "tailwind-merge": "^2.2.1",
+ "zod": "^3.22.4"
},
"devDependencies": {
"@types/node": "^20",
@@ -495,6 +502,14 @@
"node": ">=18"
}
},
+ "node_modules/@emailjs/nodejs": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@emailjs/nodejs/-/nodejs-2.2.0.tgz",
+ "integrity": "sha512-R5V14yCNmdQ5CrzE09BlsV/5s2DN8UcmxgBTo/Jk4T8TKxYT6s8lcEBUgfHSSvPRv/GGsNUknW7MNypgibVOpA==",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
"node_modules/@eslint-community/eslint-utils": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
@@ -551,6 +566,14 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
+ "node_modules/@hookform/resolvers": {
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.3.4.tgz",
+ "integrity": "sha512-o5cgpGOuJYrd+iMKvkttOclgwRW86EsWJZZRC23prf0uU2i48Htq4PuT73AVb9ionFyZrwYEITuOFGF+BydEtQ==",
+ "peerDependencies": {
+ "react-hook-form": "^7.0.0"
+ }
+ },
"node_modules/@humanwhocodes/config-array": {
"version": "0.11.14",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
@@ -3124,7 +3147,6 @@
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
- "dev": true,
"bin": {
"esparse": "bin/esparse.js",
"esvalidate": "bin/esvalidate.js"
@@ -3204,6 +3226,17 @@
"url": "https://github.com/sindresorhus/execa?sponsor=1"
}
},
+ "node_modules/extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==",
+ "dependencies": {
+ "is-extendable": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -3642,6 +3675,40 @@
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
"dev": true
},
+ "node_modules/gray-matter": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz",
+ "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==",
+ "dependencies": {
+ "js-yaml": "^3.13.1",
+ "kind-of": "^6.0.2",
+ "section-matter": "^1.0.0",
+ "strip-bom-string": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=6.0"
+ }
+ },
+ "node_modules/gray-matter/node_modules/argparse": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "dependencies": {
+ "sprintf-js": "~1.0.2"
+ }
+ },
+ "node_modules/gray-matter/node_modules/js-yaml": {
+ "version": "3.14.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
+ "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
+ "dependencies": {
+ "argparse": "^1.0.7",
+ "esprima": "^4.0.0"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
"node_modules/has-bigints": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz",
@@ -3947,6 +4014,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-extendable": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
+ "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -4324,6 +4399,14 @@
"json-buffer": "3.0.1"
}
},
+ "node_modules/kind-of": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
+ "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/language-subtag-registry": {
"version": "0.3.22",
"resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz",
@@ -4696,6 +4779,17 @@
"node": ">=10"
}
},
+ "node_modules/marked": {
+ "version": "12.0.0",
+ "resolved": "https://registry.npmjs.org/marked/-/marked-12.0.0.tgz",
+ "integrity": "sha512-Vkwtq9rLqXryZnWaQc86+FHLC6tr/fycMfYAhiOIXkrNmeGAyhSxjqu0Rs1i0bBqw5u0S7+lV9fdH2ZSVaoa0w==",
+ "bin": {
+ "marked": "bin/marked.js"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
"node_modules/merge-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
@@ -5571,6 +5665,21 @@
"react": "^18.2.0"
}
},
+ "node_modules/react-hook-form": {
+ "version": "7.50.1",
+ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.50.1.tgz",
+ "integrity": "sha512-3PCY82oE0WgeOgUtIr3nYNNtNvqtJ7BZjsbxh6TnYNbXButaD5WpjOmTjdxZfheuHKR68qfeFnEDVYoSSFPMTQ==",
+ "engines": {
+ "node": ">=12.22.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/react-hook-form"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17 || ^18"
+ }
+ },
"node_modules/react-icons": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.0.1.tgz",
@@ -5835,6 +5944,18 @@
"loose-envify": "^1.1.0"
}
},
+ "node_modules/section-matter": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz",
+ "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==",
+ "dependencies": {
+ "extend-shallow": "^2.0.1",
+ "kind-of": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/semver": {
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
@@ -5976,6 +6097,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/sonner": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.4.0.tgz",
+ "integrity": "sha512-nvkTsIuOmi9e5Wz5If8ldasJjZNVfwiXYijBi2dbijvTQnQppvMcXTFNxL/NUFWlI2yJ1JX7TREDsg+gYm9WyA==",
+ "peerDependencies": {
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0"
+ }
+ },
"node_modules/source-map-js": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
@@ -5984,6 +6114,11 @@
"node": ">=0.10.0"
}
},
+ "node_modules/sprintf-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="
+ },
"node_modules/streamsearch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
@@ -6165,6 +6300,14 @@
"node": ">=4"
}
},
+ "node_modules/strip-bom-string": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz",
+ "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/strip-final-newline": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
@@ -6886,6 +7029,14 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
+ },
+ "node_modules/zod": {
+ "version": "3.22.4",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz",
+ "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
}
}
}
diff --git a/package.json b/package.json
index 5ea7398..368b2f6 100644
--- a/package.json
+++ b/package.json
@@ -15,14 +15,21 @@
"prepare": "husky install"
},
"dependencies": {
+ "@emailjs/nodejs": "^2.2.0",
+ "@hookform/resolvers": "^3.3.4",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
+ "gray-matter": "^4.0.3",
+ "marked": "^12.0.0",
"next": "14.1.0",
"next-themes": "^0.2.1",
"react": "^18",
"react-dom": "^18",
+ "react-hook-form": "^7.50.1",
"react-icons": "^5.0.1",
- "tailwind-merge": "^2.2.1"
+ "sonner": "^1.4.0",
+ "tailwind-merge": "^2.2.1",
+ "zod": "^3.22.4"
},
"devDependencies": {
"@types/node": "^20",
diff --git a/project-words.txt b/project-words.txt
index b68e65a..d8e64d7 100644
--- a/project-words.txt
+++ b/project-words.txt
@@ -2,4 +2,7 @@ raleway
Bochkovskyi
clsx
Nextdotjs
-Tailwindcss
\ No newline at end of file
+Tailwindcss
+Skauna
+hookform
+sonner
\ No newline at end of file
diff --git a/public/Anton_Bochkovskyi_Front-End_Developer_CV.pdf b/public/Anton_Bochkovskyi_Front-End_Developer_CV.pdf
new file mode 100644
index 0000000..7a3fca2
Binary files /dev/null and b/public/Anton_Bochkovskyi_Front-End_Developer_CV.pdf differ
diff --git a/public/Skauna.jpg b/public/Skauna.jpg
new file mode 100644
index 0000000..694f207
Binary files /dev/null and b/public/Skauna.jpg differ
diff --git a/public/iTour.jpg b/public/iTour.jpg
new file mode 100644
index 0000000..9cee09d
Binary files /dev/null and b/public/iTour.jpg differ
diff --git a/public/next.svg b/public/next.svg
deleted file mode 100644
index 5174b28..0000000
--- a/public/next.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/public/vercel.svg b/public/vercel.svg
deleted file mode 100644
index d2f8422..0000000
--- a/public/vercel.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/app/actions.ts b/src/app/actions.ts
new file mode 100644
index 0000000..7e6e272
--- /dev/null
+++ b/src/app/actions.ts
@@ -0,0 +1,37 @@
+'use server'
+
+import { ContactFormInputs, ContactFormSchema } from '@/utils'
+import emailjs, { EmailJSResponseStatus } from '@emailjs/nodejs'
+
+export const sendEmail = async (data: ContactFormInputs) => {
+ const dataParseResult = ContactFormSchema.safeParse(data)
+
+ if (dataParseResult.success) {
+ const parsedData = dataParseResult.data
+
+ try {
+ const response = await emailjs.send(
+ process.env.EMAILJS_SERVICE_ID!,
+ process.env.EMAILJS_TEMPLATE_ID!,
+ parsedData,
+ {
+ privateKey: process.env.EMAILJS_PRIVATE_KEY!,
+ publicKey: process.env.EMAILJS_PUBLIC_KEY!,
+ }
+ )
+
+ return { message: response.text, success: true }
+ } catch (error) {
+ if (error instanceof EmailJSResponseStatus) {
+ return { message: error.text, success: false }
+ }
+ }
+ }
+
+ if (!dataParseResult.success) {
+ return {
+ message: JSON.stringify(dataParseResult.error.format()),
+ success: false,
+ }
+ }
+}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 474da00..fe745eb 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,11 +1,15 @@
import About from '@/components/sections/about'
+import Contact from '@/components/sections/contact/contact'
import Hero from '@/components/sections/hero/hero'
+import Projects from '@/components/sections/projects/projects'
export default function Home() {
return (
<>
-
+
+
+
>
)
}
diff --git a/src/components/header/header-navbar.tsx b/src/components/header/header-navbar.tsx
index 5a78459..20039cc 100644
--- a/src/components/header/header-navbar.tsx
+++ b/src/components/header/header-navbar.tsx
@@ -29,7 +29,7 @@ const HeaderNavbar: FC = ({ className }) => {
orientation={isMobile ? 'vertical' : 'horizontal'}
>
-
+
diff --git a/src/components/header/header.tsx b/src/components/header/header.tsx
index b8ad8a2..b583702 100644
--- a/src/components/header/header.tsx
+++ b/src/components/header/header.tsx
@@ -19,7 +19,7 @@ const Header: FC = ({ className }) => {
>
-
+
diff --git a/src/components/sections/about.tsx b/src/components/sections/about.tsx
index abe7ed0..6cc72ec 100644
--- a/src/components/sections/about.tsx
+++ b/src/components/sections/about.tsx
@@ -1,15 +1,16 @@
+import { buttonVariants } from '@/components/ui/button'
import Content from '@/components/ui/content'
import Heading from '@/components/ui/heading'
import Technologies from '@/components/ui/technologies/technologies'
import { cn } from '@/utils'
-import { FC } from 'react'
+import Link from 'next/link'
+import { FC, HTMLAttributes } from 'react'
-const About: FC = () => {
+interface AboutProps extends HTMLAttributes {}
+
+const About: FC = ({ className, ...props }) => {
return (
-
+
@@ -37,6 +38,15 @@ const About: FC = () => {
lasting impression!
+
+ Download CV
+
diff --git a/src/components/sections/contact/contact-form.tsx b/src/components/sections/contact/contact-form.tsx
new file mode 100644
index 0000000..c8be73d
--- /dev/null
+++ b/src/components/sections/contact/contact-form.tsx
@@ -0,0 +1,105 @@
+'use client'
+
+import { sendEmail } from '@/app/actions'
+import Button from '@/components/ui/button'
+import FormItem from '@/components/ui/form-item'
+import Input from '@/components/ui/input'
+import Label from '@/components/ui/label'
+import Textarea from '@/components/ui/textarea'
+import { ContactFormInputs, ContactFormSchema, cn } from '@/utils'
+import { zodResolver } from '@hookform/resolvers/zod'
+import { FC, FormHTMLAttributes, useState } from 'react'
+import { SubmitHandler, useForm } from 'react-hook-form'
+import { toast } from 'sonner'
+
+interface ContactFormProps extends FormHTMLAttributes {}
+
+const ContactForm: FC = ({ className, ...props }) => {
+ const [isSubmitting, setIsSubmitting] = useState(false)
+ const {
+ formState: { errors },
+ handleSubmit,
+ register,
+ reset,
+ } = useForm({
+ resolver: zodResolver(ContactFormSchema),
+ })
+
+ const onSubmit: SubmitHandler = async (data) => {
+ setIsSubmitting(true)
+
+ const loadingToast = toast.loading('Sending a message...')
+ const result = await sendEmail(data)
+
+ if (result?.success) {
+ setIsSubmitting(false)
+
+ toast.dismiss(loadingToast)
+ toast.success('Your message has been sent!', {
+ closeButton: true,
+ })
+
+ reset()
+ return
+ }
+
+ setIsSubmitting(false)
+ toast.dismiss(loadingToast)
+ toast.error('Oops! Something went wrong!', {
+ closeButton: true,
+ })
+ }
+
+ return (
+
+ )
+}
+
+export default ContactForm
diff --git a/src/components/sections/contact/contact.tsx b/src/components/sections/contact/contact.tsx
new file mode 100644
index 0000000..167897a
--- /dev/null
+++ b/src/components/sections/contact/contact.tsx
@@ -0,0 +1,32 @@
+import ContactForm from '@/components/sections/contact/contact-form'
+import Content from '@/components/ui/content'
+import Heading from '@/components/ui/heading'
+import { cn } from '@/utils'
+import { FC, HTMLAttributes } from 'react'
+
+interface ContactProps extends HTMLAttributes {}
+
+const Contact: FC = ({ className, ...props }) => {
+ return (
+
+ )
+}
+
+export default Contact
diff --git a/src/components/sections/hero/hero.module.css b/src/components/sections/hero/hero.module.css
index bbdb78f..8f4c265 100644
--- a/src/components/sections/hero/hero.module.css
+++ b/src/components/sections/hero/hero.module.css
@@ -1,4 +1,4 @@
.hero-container {
- margin-top: var(--header-height);
- height: calc(100dvh - var(--header-height));
+ padding-top: var(--header-height);
+ height: 100dvh;
}
diff --git a/src/components/sections/hero/hero.tsx b/src/components/sections/hero/hero.tsx
index dc592f7..6479f7c 100644
--- a/src/components/sections/hero/hero.tsx
+++ b/src/components/sections/hero/hero.tsx
@@ -1,20 +1,28 @@
-import Button from '@/components/ui/button'
+import Button, { buttonVariants } from '@/components/ui/button'
import Content from '@/components/ui/content'
import Heading from '@/components/ui/heading'
import Socials from '@/components/ui/socials/socials'
import { cn } from '@/utils'
-import { FC } from 'react'
+import Link from 'next/link'
+import { FC, HTMLAttributes } from 'react'
import styles from './hero.module.css'
-const Hero: FC = () => {
+interface HeroProps extends HTMLAttributes {}
+
+const Hero: FC = ({ className, ...props }) => {
return (
-
+
Hi, I'm Anton Bochkovskyi
@@ -22,9 +30,12 @@ const Hero: FC = () => {
elegant and responsive user interfaces.
-
+
+ My Projects
+
{
+ data: Project
+ imageSizes?: string
+}
+
+const ProjectCard: FC = ({
+ className,
+ data: { body, image, slug, technologies, title },
+ imageSizes,
+}) => {
+ return (
+
+
+
+ {image ? (
+
+ ) : (
+
+ )}
+
+
{title || slug}
+
+ {technologies && (
+
+ {technologies.map((technology) => (
+
+ {technology}
+
+ ))}
+
+ )}
+
+
+
+
+ )
+}
+
+export default ProjectCard
diff --git a/src/components/sections/projects/projects.tsx b/src/components/sections/projects/projects.tsx
new file mode 100644
index 0000000..c50a6b9
--- /dev/null
+++ b/src/components/sections/projects/projects.tsx
@@ -0,0 +1,41 @@
+import ProjectCard from '@/components/sections/projects/project-card'
+import Content from '@/components/ui/content'
+import Heading from '@/components/ui/heading'
+import Project from '@/types/Project'
+import { cn } from '@/utils'
+import { getProjects } from '@/utils/projects'
+import { FC, HTMLAttributes } from 'react'
+
+interface ProjectsProps extends HTMLAttributes {}
+
+const Projects: FC = async ({ className, ...props }) => {
+ const projects: Project[] = await getProjects()
+
+ return (
+
+
+
+ My Projects
+
+ {!projects?.length ? (
+
+ No projects found at the moment. Check back later for updates or
+ explore other sections of the site.
+
+ ) : (
+
+ {projects.map((project) => (
+
+ ))}
+
+ )}
+
+
+ )
+}
+
+export default Projects
diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx
index 817b61b..7e8f18a 100644
--- a/src/components/ui/button.tsx
+++ b/src/components/ui/button.tsx
@@ -3,7 +3,7 @@ import { VariantProps, cva } from 'class-variance-authority'
import { ButtonHTMLAttributes, FC } from 'react'
export const buttonVariants = cva(
- 'font-medium transition-all active:scale-95 md:text-lg',
+ 'font-medium transition-all active:scale-95 md:text-lg disabled:pointer-events-none',
{
defaultVariants: {
size: 'default',
@@ -11,15 +11,15 @@ export const buttonVariants = cva(
},
variants: {
size: {
- default: 'py-2 px-5 rounded-md',
- sm: 'p-2',
+ default: 'block w-fit py-2 px-5 rounded-md',
+ sm: 'block w-fit p-2',
},
variant: {
default:
- 'border-2 border-accent bg-accent text-light md:hover:bg-opacity-80',
+ 'border-2 border-accent bg-accent text-light md:hover:bg-accent-light disabled:border-accent-light disabled:bg-accent-light md:hover:border-accent-light',
icon: 'bg-transparent md:hover:text-accent',
outline:
- 'border-2 border-accent bg-transparent text-accent md:hover:bg-accent md:hover:text-light',
+ 'border-2 border-accent bg-transparent text-accent disabled:border-accent-light disabled:text-accent-light md:hover:bg-accent md:hover:text-light',
},
},
}
diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx
new file mode 100644
index 0000000..093aa8d
--- /dev/null
+++ b/src/components/ui/card.tsx
@@ -0,0 +1,17 @@
+import { cn } from '@/utils'
+import { FC, HTMLAttributes } from 'react'
+
+interface CardProps extends HTMLAttributes {}
+
+const Card: FC = ({ children, className, ...props }) => {
+ return (
+
+ {children}
+
+ )
+}
+
+export default Card
diff --git a/src/components/ui/form-item.tsx b/src/components/ui/form-item.tsx
new file mode 100644
index 0000000..b62f7c9
--- /dev/null
+++ b/src/components/ui/form-item.tsx
@@ -0,0 +1,26 @@
+import { cn } from '@/utils'
+import { FC, HTMLAttributes } from 'react'
+
+interface FormItemProps extends HTMLAttributes {
+ error?: string
+}
+
+const FormItem: FC = ({
+ children,
+ className,
+ error,
+ ...props
+}) => {
+ return (
+
+ {children}
+ {error && (
+
+ {error}
+
+ )}
+
+ )
+}
+
+export default FormItem
diff --git a/src/components/ui/heading.tsx b/src/components/ui/heading.tsx
index 6dd53ec..c324e84 100644
--- a/src/components/ui/heading.tsx
+++ b/src/components/ui/heading.tsx
@@ -4,16 +4,25 @@ import { FC, HTMLAttributes } from 'react'
export const headingVariants = cva('w-fit', {
defaultVariants: {
+ position: 'default',
size: 'h1',
},
variants: {
+ position: {
+ center: 'mx-auto after:left-1/2 after:-translate-x-1/2',
+ default: 'after:left-0',
+ },
size: {
h1: 'text-4xl font-bold md:text-5xl',
h2: 'text-3xl font-semibold md:text-4xl',
+ h3: 'text-2xl font-semibold md:text-3xl',
+ h4: 'text-xl font-semibold md:text-2xl',
+ h5: 'text-lg font-semibold md:text-xl',
+ h6: 'font-semibold md:text-4xl',
},
variant: {
underline:
- 'pb-1.5 relative after:absolute after:bottom-0 after:left-0 after:h-0.5 after:bg-accent after:w-2/6',
+ 'pb-1.5 relative after:absolute after:bottom-0 after:h-0.5 after:bg-accent after:w-2/6',
},
},
})
@@ -22,11 +31,19 @@ interface HeadingProps
extends HTMLAttributes,
VariantProps {}
-const Heading: FC = ({ children, className, size, variant }) => {
+const Heading: FC = ({
+ children,
+ className,
+ position,
+ size,
+ variant,
+}) => {
const HeadingTag = size ? size : 'h1'
return (
-
+
{children}
)
diff --git a/src/components/ui/image-placeholder.tsx b/src/components/ui/image-placeholder.tsx
new file mode 100644
index 0000000..38fcfa0
--- /dev/null
+++ b/src/components/ui/image-placeholder.tsx
@@ -0,0 +1,25 @@
+import { cn } from '@/utils'
+import { FC, HTMLAttributes } from 'react'
+import { CiImageOff } from 'react-icons/ci'
+
+interface ImagePlaceholderProps extends HTMLAttributes {}
+
+const ImagePlaceholder: FC = ({
+ className,
+ ...props
+}) => {
+ return (
+
+
+ Image Not Found
+
+ )
+}
+
+export default ImagePlaceholder
diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx
new file mode 100644
index 0000000..075a41c
--- /dev/null
+++ b/src/components/ui/input.tsx
@@ -0,0 +1,76 @@
+import { ContactFormInputs, cn } from '@/utils'
+import { VariantProps, cva } from 'class-variance-authority'
+import { FC, InputHTMLAttributes } from 'react'
+import { Path, UseFormRegister } from 'react-hook-form'
+
+export const inputVariants = cva(
+ 'w-full transition-colors outline-none md:text-lg',
+ {
+ defaultVariants: {
+ customSize: 'default',
+ variant: 'default',
+ },
+ variants: {
+ customSize: {
+ default: 'rounded-2xl px-4 py-3.5',
+ placeholder: 'rounded-2xl px-4 pb-2 pt-5',
+ },
+ variant: {
+ default:
+ 'border border-neutral-400 bg-light dark:border-neutral-600 dark:bg-dark',
+ placeholder:
+ 'border border-neutral-400 bg-light dark:border-neutral-600 dark:bg-dark peer relative placeholder:opacity-0',
+ },
+ },
+ }
+)
+
+export interface InputProps
+ extends InputHTMLAttributes,
+ VariantProps {
+ name: Path
+ register: UseFormRegister
+}
+
+const Input: FC = ({
+ className,
+ customSize,
+ name,
+ placeholder,
+ register,
+ required,
+ variant,
+ ...props
+}) => {
+ return (
+ <>
+ {placeholder ? (
+
+
+
+ {placeholder}
+
+
+ ) : (
+
+ )}
+ >
+ )
+}
+
+export default Input
diff --git a/src/components/ui/label.tsx b/src/components/ui/label.tsx
new file mode 100644
index 0000000..f6f3a2a
--- /dev/null
+++ b/src/components/ui/label.tsx
@@ -0,0 +1,14 @@
+import { cn } from '@/utils'
+import { FC, LabelHTMLAttributes } from 'react'
+
+interface LabelProps extends LabelHTMLAttributes {}
+
+const Label: FC = ({ children, className, ...props }) => {
+ return (
+
+ )
+}
+
+export default Label
diff --git a/src/components/ui/technologies/technologies.tsx b/src/components/ui/technologies/technologies.tsx
index f156702..9fb4dc5 100644
--- a/src/components/ui/technologies/technologies.tsx
+++ b/src/components/ui/technologies/technologies.tsx
@@ -1,10 +1,13 @@
import TechnologiesItem from '@/components/ui/technologies/technologies-item'
import { cn } from '@/utils'
import { FC, HTMLAttributes } from 'react'
-import { FaReact, FaSass } from 'react-icons/fa'
+import { FaSass } from 'react-icons/fa'
import {
+ SiCss3,
+ SiHtml5,
SiJavascript,
SiNextdotjs,
+ SiReact,
SiTailwindcss,
SiTypescript,
} from 'react-icons/si'
@@ -20,13 +23,25 @@ const Technologies: FC = ({ className, ...props }) => {
)}
{...props}
>
+
+
+
+
@@ -35,17 +50,11 @@ const Technologies: FC = ({ className, ...props }) => {
className="text-black dark:text-white"
title="Next.js"
/>
-
-
)
}
diff --git a/src/components/ui/textarea.tsx b/src/components/ui/textarea.tsx
new file mode 100644
index 0000000..7051c02
--- /dev/null
+++ b/src/components/ui/textarea.tsx
@@ -0,0 +1,56 @@
+import { inputVariants } from '@/components/ui/input'
+import { ContactFormInputs, cn } from '@/utils'
+import { VariantProps } from 'class-variance-authority'
+import { FC, TextareaHTMLAttributes } from 'react'
+import { Path, UseFormRegister } from 'react-hook-form'
+
+export interface TextareaProps
+ extends TextareaHTMLAttributes,
+ VariantProps {
+ name: Path
+ register: UseFormRegister
+}
+
+const Textarea: FC = ({
+ className,
+ customSize,
+ name,
+ placeholder,
+ register,
+ required,
+ variant,
+ ...props
+}) => {
+ return (
+ <>
+ {placeholder ? (
+
+
+
+ {placeholder}
+
+
+ ) : (
+
+ )}
+ >
+ )
+}
+
+export default Textarea
diff --git a/src/components/ui/theme-toggler.tsx b/src/components/ui/theme-toggler.tsx
index 46870c4..735529f 100644
--- a/src/components/ui/theme-toggler.tsx
+++ b/src/components/ui/theme-toggler.tsx
@@ -17,26 +17,18 @@ const ThemeToggler: FC = ({ className }) => {
useEffect(() => setMounted(true), [])
- if (!mounted) {
- return (
-
- )
- }
-
return (
<>
{!mounted ? (
) : (