diff --git a/.eslintignore b/.eslintignore
index 1437c53..7fcb275 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -4,6 +4,7 @@
/node_modules
/.pnp
.pnp.js
+*.lock
# testing
/coverage
diff --git a/.gitignore b/.gitignore
index d0b842f..db238fd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,6 +4,7 @@
/node_modules
/.pnp
.pnp.js
+*.lock
# testing
/coverage
diff --git a/.prettierignore b/.prettierignore
index 1437c53..7fcb275 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -4,6 +4,7 @@
/node_modules
/.pnp
.pnp.js
+*.lock
# testing
/coverage
diff --git a/app/about/head.js b/app/about/head.js
index 4aa05f0..655870f 100644
--- a/app/about/head.js
+++ b/app/about/head.js
@@ -1,16 +1,11 @@
+import DefaultTags from "@app-components/head/default-tags";
+
function AboutHead() {
return (
<>
About Me - Pravasta Caraka
-
-
-
-
-
-
-
-
+
>
);
}
diff --git a/app/about/layout.js b/app/about/layout.js
new file mode 100644
index 0000000..a4f7dda
--- /dev/null
+++ b/app/about/layout.js
@@ -0,0 +1,11 @@
+import { Stack } from "@app-providers/chakra-ui";
+
+function AboutLayout({ children }) {
+ return (
+
+ {children}
+
+ );
+}
+
+export default AboutLayout;
diff --git a/app/about/page.js b/app/about/page.js
index ad1f396..8dbc3d5 100644
--- a/app/about/page.js
+++ b/app/about/page.js
@@ -1,93 +1,97 @@
-"use client";
-
import { KnowledgeCard } from "@app-components/Card";
-import { BaseMarkdown } from "@app-components/markdown";
-import { AspectRatio, SimpleGrid, Skeleton, SkeletonText, Stack } from "@chakra-ui/react";
-import { useGetAbout, useGetKnowledge } from "helper/api.helper";
+import { CustomReactMarkdown } from "@app-components/markdown";
+import { AspectRatio, SimpleGrid } from "@app-providers/chakra-ui";
import Image from "next/image";
-import ReactMarkdown from "react-markdown";
+import { getPlaiceholder } from "plaiceholder";
+
+async function fetchAboutData() {
+ const res = await fetch(`https://api.airtable.com/v0/${process.env.AIRTABLE_BASE_ID}/About%20Me`, {
+ method: "GET",
+ headers: {
+ Authorization: `Bearer ${process.env.AIRTABLE_API_KEY}`,
+ },
+ next: { revalidate: 60 },
+ });
+
+ if (!res.ok) {
+ throw new Error("Failed to fetch data");
+ }
+
+ const json = await res.json();
+
+ const records = await Promise.all(
+ json.records.map(async (record) => {
+ const { base64, img } = await getPlaiceholder(record.fields.cover_im[0]?.url);
+ record.fields.cover_im = { src: img.src, type: img.type, blurDataURL: base64 };
+ return { id: record.id, ...record.fields };
+ })
+ ).then((values) => values);
+
+ return records[0];
+}
+
+async function fetchKnowledgeData() {
+ const res = await fetch(`https://api.airtable.com/v0/${process.env.AIRTABLE_BASE_ID}/Knowledge%20Base`, {
+ method: "GET",
+ headers: {
+ Authorization: `Bearer ${process.env.AIRTABLE_API_KEY}`,
+ },
+ next: { revalidate: 60 },
+ });
-function Page() {
- const { about, aboutError, aboutLoading } = useGetAbout();
- const { knowledge, knowledgeError, knowledgeLoading } = useGetKnowledge();
+ if (!res.ok) {
+ throw new Error("Failed to fetch data");
+ }
- const languages = knowledge?.data?.filter((val) => val.fields.type === "language");
- const frameworks = knowledge?.data?.filter((val) => val.fields.type === "framework");
- const databases = knowledge?.data?.filter((val) => val.fields.type === "database");
- const cms = knowledge?.data?.filter((val) => val.fields.type === "cms");
- const tools = knowledge?.data?.filter((val) => val.fields.type === "tool");
- const deployments = knowledge?.data?.filter((val) => val.fields.type === "ci/cd");
+ const json = await res.json();
+
+ const records = json.records.map((record) => {
+ return { id: record.id, ...record.fields };
+ });
+
+ return records;
+}
+
+async function Page() {
+ const aboutData = await fetchAboutData();
+ const knowledgeData = await fetchKnowledgeData();
+
+ const languages = knowledgeData.filter((val) => val.type === "language");
+ const frameworks = knowledgeData.filter((val) => val.type === "framework");
+ const databases = knowledgeData.filter((val) => val.type === "database");
+ const cms = knowledgeData.filter((val) => val.type === "cms");
+ const tools = knowledgeData.filter((val) => val.type === "tool");
+ const deployments = knowledgeData.filter((val) => val.type === "ci/cd");
return (
-
+ <>
- {aboutLoading || aboutError ? (
-
- ) : (
-
- )}
+
-
- {aboutLoading || aboutError ? (
-
- ) : (
- {about.data.about.fields.long_desc}
- )}
-
-
- {/*
- Knowledge Base
- */}
-
- {/*
- After a few years of being a computer engineering student and also learning something new on my own, I
- have a wide range of skills and knowledge about software development.
- */}
-
-
-
-
-
-
-
-
-
-
-
-
- You can reach out via email at [hello@pravastacaraka.my.id](mailto:hello@pravastacaraka.my.id), or via
- socials below.
-
-
-
+ {aboutData.long_desc}
+
+
+
+
+
+
+
+
+
+
+
+ You can reach out via email at [hello@pravastacaraka.my.id](mailto:hello@pravastacaraka.my.id), or via
+ socials below.
+
+ >
);
}
diff --git a/app/achievements/Awards.js b/app/achievements/Awards.js
index 88fccfc..626cda6 100644
--- a/app/achievements/Awards.js
+++ b/app/achievements/Awards.js
@@ -2,104 +2,77 @@
import { Button } from "@app-components/Button";
import { NextChakraLink } from "@app-components/NextChakraLink";
-import { useGetAwards } from "@app-helper/api.helper";
import { arrayGroupBy } from "@app-helper/function.helper";
-import {
- Box,
- Divider,
- Flex,
- Heading,
- Icon,
- List,
- ListItem,
- SkeletonText,
- Stack,
- Text,
-} from "@app-providers/chakra-ui";
+import { Divider, Flex, Heading, Icon, List, ListItem, Stack, Text } from "@app-providers/chakra-ui";
import { useBoolean, useColorModeValue } from "@chakra-ui/react";
import { HiOutlineBadgeCheck, HiOutlineChevronDown, HiOutlineChevronUp, HiOutlineNewspaper } from "react-icons/hi";
-function loadingSkeleton() {
- return (
-
-
-
-
-
-
- );
-}
-
-function Awards() {
+function Awards({ data }) {
+ let countAward = 0;
const [showAwards, setShowAwards] = useBoolean();
- const { liColor } = useColorModeValue("gray.600", "gray.400");
- const { iconColor } = useColorModeValue("green.600", "yellow.200");
- const { textColor } = useColorModeValue("green.900", "gray.100");
-
- const { awards, awardsError, awardsLoading } = useGetAwards();
+ const iconColor = useColorModeValue("blue.600", "yellow.200");
- if (awardsError) return Oh no! Something went wrong, please try again!
;
- if (awardsLoading) return loadingSkeleton();
+ if (!data || data?.length < 1) return Don't have any awards and funding.;
const awardsGroupByYear = arrayGroupBy("year");
- const awardsGroupedByYear = awardsGroupByYear(awards.data);
+ const awardsGroupedByYear = awardsGroupByYear(data);
+
+ return (
+
+ {Object.keys(awardsGroupedByYear)
+ .reverse()
+ .map((year, yearIdx) => {
+ let showDivider = true;
+
+ if (countAward >= 4 && !showAwards) {
+ return;
+ }
- return awardsGroupedByYear ? (
- <>
-
- {Object.keys(awardsGroupedByYear)
- .reverse()
- .map((year, yearIdx) => {
- return (
-
-
+ return (
+ <>
+
+
{year}
- {awardsGroupedByYear[year].map((award) => (
-
-
-
-
- {award.title}
-
-
-
- {award.desc}
-
- {award.news_url && (
-
-
-
- {award.news_url}
-
+ {awardsGroupedByYear[year].map((award) => {
+ if (countAward >= 4 && !showAwards) {
+ showDivider = false;
+ return;
+ }
+
+ countAward++;
+
+ return (
+
+
+
+ {award.title}
- )}
-
- ))}
+
+ {award.desc}
+
+ {award.news_url && (
+
+
+
+ {award.news_url}
+
+
+ )}
+
+ );
+ })}
- {Object.keys(awardsGroupedByYear).length - 1 !== yearIdx && }
-
- );
- })}
-
+
+ {showDivider && Object.keys(awardsGroupedByYear).length - 1 !== yearIdx && }
+ >
+ );
+ })}
{showAwards ? (
}
- fontWeight="medium"
- mt={4}
mx="auto"
onClick={setShowAwards.toggle}
>
@@ -109,17 +82,13 @@ function Awards() {
}
- fontWeight="medium"
- mt={4}
mx="auto"
onClick={setShowAwards.toggle}
>
See More
)}
- >
- ) : (
- Don't have any awards and funding.
+
);
}
diff --git a/app/achievements/Licenses.js b/app/achievements/Licenses.js
index 5613494..d42fa11 100644
--- a/app/achievements/Licenses.js
+++ b/app/achievements/Licenses.js
@@ -2,99 +2,77 @@
import { Button } from "@app-components/Button";
import { NextChakraLink } from "@app-components/NextChakraLink";
-import { useGetLicenses } from "@app-helper/api.helper";
import { arrayGroupBy } from "@app-helper/function.helper";
-import {
- Box,
- Divider,
- Flex,
- Heading,
- Icon,
- List,
- ListItem,
- SkeletonText,
- Stack,
- Text,
-} from "@app-providers/chakra-ui";
+import { Divider, Flex, Heading, Icon, List, ListItem, Stack, Text } from "@app-providers/chakra-ui";
import { useBoolean, useColorModeValue } from "@chakra-ui/react";
import { HiOutlineBadgeCheck, HiOutlineChevronDown, HiOutlineChevronUp, HiOutlineNewspaper } from "react-icons/hi";
-function loadingSkeleton() {
- return (
-
-
-
-
-
-
- );
-}
-
-function Licenses() {
+function Licenses({ data }) {
+ let countLicense = 0;
const [showLicenses, setShowLicenses] = useBoolean();
- const { liColor } = useColorModeValue("gray.600", "gray.400");
- const { iconColor } = useColorModeValue("green.600", "yellow.200");
- const { textColor } = useColorModeValue("green.900", "gray.100");
-
- const { licenses, licensesError, licensesLoading } = useGetLicenses();
+ const iconColor = useColorModeValue("blue.600", "yellow.200");
- if (licensesError) return Oh no! Something went wrong, please try again!
;
- if (licensesLoading) return loadingSkeleton();
+ if (!data || data?.length < 1) return Don't have any certifications.;
const licensesGroupByYear = arrayGroupBy("year");
- const licensesGroupedByYear = licensesGroupByYear(licenses.data);
+ const licensesGroupedByYear = licensesGroupByYear(data);
+
+ return (
+
+ {Object.keys(licensesGroupedByYear)
+ .reverse()
+ .map((year, yearIdx) => {
+ let showDivider = true;
+
+ if (countLicense >= 5 && !showLicenses) {
+ return;
+ }
- return licensesGroupedByYear ? (
- <>
-
- {Object.keys(licensesGroupedByYear)
- .reverse()
- .map((year, yearIdx) => {
- return (
-
-
+ return (
+ <>
+
+
{year}
- {licensesGroupedByYear[year].map((license) => (
-
-
-
-
- {license.title}
-
-
-
- {license.desc}
-
- {license.news_url && (
-
-
-
- {license.news_url}
-
+ {licensesGroupedByYear[year].map((license) => {
+ if (countLicense >= 5 && !showLicenses) {
+ showDivider = false;
+ return;
+ }
+
+ countLicense++;
+
+ return (
+
+
+
+ {license.title}
- )}
-
- ))}
+
+ {license.desc}
+
+ {license.news_url && (
+
+
+
+ {license.news_url}
+
+
+ )}
+
+ );
+ })}
- {Object.keys(licensesGroupedByYear).length - 1 !== yearIdx && }
-
- );
- })}
-
+
+ {showDivider && Object.keys(licensesGroupedByYear).length - 1 !== yearIdx && }
+ >
+ );
+ })}
{showLicenses ? (
}
- fontWeight="medium"
- mt={4}
mx="auto"
onClick={setShowLicenses.toggle}
>
@@ -104,17 +82,13 @@ function Licenses() {
}
- fontWeight="medium"
- mt={4}
mx="auto"
onClick={setShowLicenses.toggle}
>
See More
)}
- >
- ) : (
- Don't have any certifications.
+
);
}
diff --git a/app/achievements/head.js b/app/achievements/head.js
index ce51974..e41b244 100644
--- a/app/achievements/head.js
+++ b/app/achievements/head.js
@@ -1,21 +1,16 @@
-function AboutHead() {
+import DefaultTags from "@app-components/head/default-tags";
+
+function AchievementHead() {
return (
<>
Achievements - Pravasta Caraka
-
-
-
-
-
-
-
-
+
>
);
}
-export default AboutHead;
+export default AchievementHead;
diff --git a/app/achievements/layout.js b/app/achievements/layout.js
index d7fea24..44d8776 100644
--- a/app/achievements/layout.js
+++ b/app/achievements/layout.js
@@ -3,13 +3,10 @@ import { Heading, Stack, Text } from "@app-providers/chakra-ui";
function AchievementsLayout({ children }) {
return (
-
+
Achievements
-
- Here are some of the accomplishment on my journey to becoming a developer.
-
+ Here are some of the accomplishment on my journey to becoming a developer.
-
{children}
);
diff --git a/app/achievements/page.js b/app/achievements/page.js
index 23212ad..ae541e5 100644
--- a/app/achievements/page.js
+++ b/app/achievements/page.js
@@ -1,26 +1,68 @@
-import { Flex, Heading, Stack } from "@app-providers/chakra-ui";
+import { Heading, Stack } from "@app-providers/chakra-ui";
import Awards from "./Awards";
import Licenses from "./Licenses";
-function Page() {
+async function fetchAwardsData() {
+ const res = await fetch(`https://api.airtable.com/v0/${process.env.AIRTABLE_BASE_ID}/Awards`, {
+ method: "GET",
+ headers: {
+ Authorization: `Bearer ${process.env.AIRTABLE_API_KEY}`,
+ },
+ next: { revalidate: 60 },
+ });
+
+ if (!res.ok) {
+ throw new Error("Failed to fetch data");
+ }
+
+ const json = await res.json();
+
+ const records = json.records.map((record) => {
+ return { id: record.id, ...record.fields };
+ });
+
+ return records;
+}
+
+async function fetchLicensesData() {
+ const res = await fetch(`https://api.airtable.com/v0/${process.env.AIRTABLE_BASE_ID}/Licenses`, {
+ method: "GET",
+ headers: {
+ Authorization: `Bearer ${process.env.AIRTABLE_API_KEY}`,
+ },
+ next: { revalidate: 60 },
+ });
+
+ if (!res.ok) {
+ throw new Error("Failed to fetch data");
+ }
+
+ const json = await res.json();
+
+ const records = json.records.map((record) => {
+ return { id: record.id, ...record.fields };
+ });
+
+ return records;
+}
+
+async function Page() {
+ const awardsData = await fetchAwardsData();
+ const licensesData = await fetchLicensesData();
return (
<>
-
+
Awards and Funding
-
-
-
+
-
+
Licenses and Certifications
-
-
-
+
>
);
diff --git a/app/blog/head.js b/app/blog/head.js
index cc61a0d..40be783 100644
--- a/app/blog/head.js
+++ b/app/blog/head.js
@@ -1,19 +1,14 @@
+import DefaultTags from "@app-components/head/default-tags";
+
function BlogHead() {
return (
<>
Blog - Pravasta Caraka
-
-
-
-
-
-
-
-
+
>
);
}
diff --git a/app/blog/page.js b/app/blog/page.js
index be75a99..24aa72b 100644
--- a/app/blog/page.js
+++ b/app/blog/page.js
@@ -9,8 +9,8 @@ function Page() {
diff --git a/app/contact/head.js b/app/contact/head.js
index b1a7684..ae6cc8d 100644
--- a/app/contact/head.js
+++ b/app/contact/head.js
@@ -1,19 +1,14 @@
+import DefaultTags from "@app-components/head/default-tags";
+
function ContactHead() {
return (
<>
Get in Touch - Pravasta Caraka
-
-
-
-
-
-
-
-
+
>
);
}
diff --git a/app/head.js b/app/head.js
index c1f59e8..a3a3054 100644
--- a/app/head.js
+++ b/app/head.js
@@ -1,16 +1,11 @@
+import DefaultTags from "@app-components/head/default-tags";
+
function RootHead() {
return (
<>
Pravasta Caraka
-
-
-
-
-
-
-
-
+
>
);
}
diff --git a/app/layout.js b/app/layout.js
index dcbfccf..aeec4b0 100644
--- a/app/layout.js
+++ b/app/layout.js
@@ -1,18 +1,14 @@
-"use client";
-
import Footer from "@app-components/Footer";
import Header from "@app-components/Header";
import MobileNavigation from "@app-components/MobileNavigation";
-import { ChakraProvider, ColorModeScript, Container } from "@app-providers/chakra-ui";
-import customTheme from "@app-styles/theme";
-import { Analytics } from "@vercel/analytics/react";
+import { ChakraWrapper, Container } from "@app-providers/chakra-ui";
+import { Analytics } from "@app-providers/vercel";
function RootLayout({ children }) {
return (
-
-
+
{children}
@@ -20,7 +16,7 @@ function RootLayout({ children }) {
-
+
);
diff --git a/app/page.js b/app/page.js
index dda7e37..c902b1a 100644
--- a/app/page.js
+++ b/app/page.js
@@ -1,13 +1,10 @@
-"use client";
-
import { NextChakraLink, NextChakraLinkButton } from "@app-components/NextChakraLink";
-import { Box, Center, Heading, HStack, Icon, Stack, Text, useColorModeValue } from "@chakra-ui/react";
+import { Box, Center, Heading, HStack, Stack, Text } from "@app-providers/chakra-ui";
import Image from "next/image";
-import Link from "next/link";
import { HiOutlineDocumentDownload } from "react-icons/hi";
import profilePic from "../public/static/images/avatar.webp";
-function Page() {
+function Home() {
return (
-
+
Hey, I'm Pravasta Caraka.
-
+
Software Engineer at
PT Tokopedia
@@ -39,37 +36,15 @@ function Page() {
}
- variant="outline"
- _hover={{
- borderColor: useColorModeValue("black", "white"),
- transform: "scale(1.05)",
- textDecoration: "none",
- }}
+ rightIcon={}
+ isExternal={true}
>
My Resume
-
+
Get in Touch
@@ -77,4 +52,4 @@ function Page() {
);
}
-export default Page;
+export default Home;
diff --git a/app/projects/Projects.js b/app/projects/Projects.js
deleted file mode 100644
index 5746664..0000000
--- a/app/projects/Projects.js
+++ /dev/null
@@ -1,74 +0,0 @@
-"use client";
-
-import { ProjectCard } from "@app-components/Card";
-import { useGetProjects } from "@app-helper/api.helper";
-import { AspectRatio, Center, SimpleGrid, Skeleton, Text } from "@app-providers/chakra-ui";
-
-function Loading() {
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-}
-
-function Projects() {
- const { data, error, isLoading } = useGetProjects();
-
- if (error)
- return (
-
- Oh no! Something went wrong, please try again!
-
- );
- if (isLoading) return ;
-
- const projects = data.data;
-
- return projects ? (
-
- {projects.map((project) => (
-
- ))}
-
- ) : (
-
- Don't have any projects.
-
- );
-}
-
-export default Projects;
diff --git a/app/projects/head.js b/app/projects/head.js
index 388424f..c714c2a 100644
--- a/app/projects/head.js
+++ b/app/projects/head.js
@@ -1,19 +1,14 @@
+import DefaultTags from "@app-components/head/default-tags";
+
function AboutHead() {
return (
<>
Recent Projects - Pravasta Caraka
-
-
-
-
-
-
-
-
+
>
);
}
diff --git a/app/projects/layout.js b/app/projects/layout.js
new file mode 100644
index 0000000..ad6dcf3
--- /dev/null
+++ b/app/projects/layout.js
@@ -0,0 +1,15 @@
+import { Heading, Stack, Text } from "@app-providers/chakra-ui";
+
+function ProjectsLayout({ children }) {
+ return (
+
+
+ Recent Projects
+ Here are some of my past works from personal projects and open source ones.
+
+ {children}
+
+ );
+}
+
+export default ProjectsLayout;
diff --git a/app/projects/page.js b/app/projects/page.js
index b46af15..70c5f52 100644
--- a/app/projects/page.js
+++ b/app/projects/page.js
@@ -1,18 +1,72 @@
-import { Heading, Stack, Text } from "@app-providers/chakra-ui";
-import Projects from "./Projects";
+import { ProjectCard } from "@app-components/Card";
+import { Center, SimpleGrid, Stack, Text } from "@app-providers/chakra-ui";
+import { getPlaiceholder } from "plaiceholder";
+
+async function fetchProjectsData() {
+ const res = await fetch(
+ `https://api.airtable.com/v0/${process.env.AIRTABLE_BASE_ID}/Recent%20Projects/listRecords`,
+ {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${process.env.AIRTABLE_API_KEY}`,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ sort: [
+ {
+ field: "id",
+ direction: "desc",
+ },
+ ],
+ }),
+ next: { revalidate: 60 },
+ }
+ );
+
+ if (!res.ok) {
+ throw new Error("Failed to fetch data");
+ }
+
+ const json = await res.json();
+
+ const records = await Promise.all(
+ json.records.map(async (record) => {
+ const { base64, img } = await getPlaiceholder(record.fields.images[0]?.url);
+ record.fields.image = { src: img.src, type: img.type, blurDataURL: base64 };
+ delete record.fields.images;
+ return { id: record.id, ...record.fields };
+ })
+ ).then((values) => values);
+
+ return records;
+}
+
+async function Page() {
+ const projectsData = await fetchProjectsData();
+
+ if (projectsData.length < 1) {
+ return (
+
+ Don't have any projects.
+
+ );
+ }
-function Page() {
return (
-
-
- Recent Projects
-
- Here are some of my past works from personal projects and open source ones.
-
-
-
-
-
+
+
+ {projectsData.map((project) => (
+
+ ))}
+
);
}
diff --git a/components/Button.js b/components/Button.js
index 9992d0f..319850a 100644
--- a/components/Button.js
+++ b/components/Button.js
@@ -5,35 +5,38 @@ import { useColorMode, useColorModeValue } from "@chakra-ui/react";
import { HiMoon, HiSun } from "react-icons/hi";
import MobileMenuButton from "./mobile-menu/mobile-menu-button";
-function ThemeToggle({ mobile }) {
- const { colorMode, toggleColorMode } = useColorMode();
+function ThemeToggle({ isMobile }) {
+ const { toggleColorMode } = useColorMode();
+ const blackWhite = useColorModeValue("black", "white");
+ const btnIcon = useColorModeValue(HiMoon, HiSun);
+ const btnBgColorHover = useColorModeValue("blackAlpha.200", "whiteAlpha.200");
- return mobile ? (
- toggleColorMode()}
- />
- ) : (
+ return !isMobile ? (
}
variant="ghost"
- onClick={() => toggleColorMode()}
+ aria-label="Toogle dark mode"
+ icon={}
+ _hover={{
+ bg: btnBgColorHover,
+ }}
+ onClick={toggleColorMode}
/>
+ ) : (
+
);
}
function Button({ children, variant, ...restProps }) {
const blackWhite = useColorModeValue("black", "white");
const whiteBlack = useColorModeValue("white", "black");
- const textColor = useColorModeValue("gray.900", "gray.100");
switch (variant) {
case "solid":
return (
);
- case "ghost":
- console.log("masuk ghost");
+ case "outline":
return (
{children}
);
- default:
+ case "ghost":
return (
{label}
-
-
- {isError || isLoading ? (
-
- ) : (
-
- {data?.map((item) => (
-
-
-
- {`${item.fields.name}`}
-
-
- {item.fields.name}
-
-
- ))}
-
- )}
+
+
+ {data.map((item) => (
+
+
+
+ {`${item.name}`}
+
+
+ {item.name}
+
+
+ ))}
+
);
diff --git a/components/Footer.js b/components/Footer.js
index c6c6974..14d51fa 100644
--- a/components/Footer.js
+++ b/components/Footer.js
@@ -1,4 +1,4 @@
-import { ButtonGroup, Container, HStack, Icon, Stack, Text } from "@chakra-ui/react";
+import { ButtonGroup, Container, HStack, Stack, Text } from "@app-providers/chakra-ui";
import { FaEnvelope, FaFacebook, FaGithub, FaInstagram, FaLinkedinIn, FaTwitter } from "react-icons/fa";
import { NextChakraLink, NextChakraLinkIconButton } from "./NextChakraLink";
@@ -39,38 +39,34 @@ function Footer() {
const date = new Date().getFullYear();
return (
-
+
{footerSocials.map((social) => (
}
- _hover={{
- transform: "scale(1.05)",
- }}
- isExternal
+ icon={}
+ isExternal={true}
/>
))}
Made using
-
+
Next.js
,
-
+
Chakra UI
, and
-
+
Airtable
. Hosted in
-
+
Vercel
diff --git a/components/Header.js b/components/Header.js
index ac75f8e..0cf36c5 100644
--- a/components/Header.js
+++ b/components/Header.js
@@ -1,34 +1,33 @@
+"use client";
+
import { ThemeToggle } from "@app-components/Button";
import { _app_routes } from "@app-config/app.config";
-import { Container, HStack, useColorMode, useColorModeValue } from "@chakra-ui/react";
+import { Box, Container, HStack, useColorModeValue } from "@app-providers/chakra-ui";
import { usePathname } from "next/navigation";
import { NextChakraLinkButton } from "./NextChakraLink";
function HeaderLink({ children, href }) {
- const pathname = usePathname();
let isActive = false;
+ const pathname = usePathname();
+ const linkColor = useColorModeValue("blue.600", "yellow.200");
if (href !== "/") {
const [, group] = href.split("/");
isActive = pathname.includes(group);
- } else {
- if (href === pathname) isActive = true;
+ } else if (href === pathname) {
+ isActive = true;
}
- const { colorMode } = useColorMode();
- const textColor = isActive ? (colorMode === "dark" ? "yellow.200" : "blue.600") : undefined;
-
return (
{children}
@@ -36,10 +35,11 @@ function HeaderLink({ children, href }) {
}
function Header() {
+ const contBgColor = useColorModeValue("whiteAlpha.700", "blackAlpha.700");
return (
-
+
{_app_routes.map(({ title, href }) => (
@@ -57,7 +57,7 @@ function Header() {
))}
-
+
);
diff --git a/components/MobileNavigation.js b/components/MobileNavigation.js
index 7426089..223dd1b 100644
--- a/components/MobileNavigation.js
+++ b/components/MobileNavigation.js
@@ -1,13 +1,15 @@
+"use client";
+
import MobileMenuButton from "@app-components/mobile-menu/mobile-menu-button";
import MobileMenuToggle from "@app-components/mobile-menu/mobile-menu-toggle";
-import { Box, Grid, useColorModeValue } from "@chakra-ui/react";
-import { motion, useCycle } from "framer-motion";
-import Link from "next/link";
+import { Box, Grid, useBoolean, useColorModeValue } from "@app-providers/chakra-ui";
+import { motion } from "framer-motion";
import { HiHome } from "react-icons/hi";
import { ThemeToggle } from "./Button";
+import { NextChakraLink } from "./NextChakraLink";
function MobileNavigation() {
- const [isOpen, toggleOpen] = useCycle(false, true);
+ const [isOpen, toggleOpen] = useBoolean();
const MotionBox = motion(Box);
return (
@@ -29,12 +31,13 @@ function MobileNavigation() {
borderTopWidth="2px"
borderTopColor={useColorModeValue("gray.100", "gray.800")}
shadow="0 -2px 10px 0 rgba(0, 0, 0, 0.035)"
+ minHeight="60px"
>
-
+
-
- toggleOpen()} />
-
+
+
+
);
diff --git a/components/NextChakraLink.js b/components/NextChakraLink.js
index 315f8c5..7317f5d 100644
--- a/components/NextChakraLink.js
+++ b/components/NextChakraLink.js
@@ -1,7 +1,7 @@
-import { Button, IconButton, Link as ChakraLink } from "@app-providers/chakra-ui";
+import { Button } from "@app-components/Button";
+import { IconButton, Link as ChakraLink } from "@app-providers/chakra-ui";
import NextLink from "next/link";
-// has to be a new component because both chakra and next share the `as` keyword
const NextChakraLink = ({ href, as, replace, scroll, shallow, prefetch, isExternal = false, ...chakraProps }) => {
return (
@@ -62,6 +61,9 @@ const NextChakraLinkIconButton = ({
prefetch={prefetch}
variant="ghost"
transition="transform .3s cubic-bezier(.175,.885,.32,1.275), border-color .2s cubic-bezier(.39,.575,.565,1), background-color .2s cubic-bezier(.39,.575,.565,1)"
+ _hover={{
+ transform: "scale(1.05)",
+ }}
{...(isExternal && { target: "_blank", rel: "noopener noreferrer" })}
{...chakraProps}
/>
diff --git a/components/head/default-tags.js b/components/head/default-tags.js
new file mode 100644
index 0000000..344f617
--- /dev/null
+++ b/components/head/default-tags.js
@@ -0,0 +1,16 @@
+function DefaultTags() {
+ return (
+ <>
+
+
+
+
+
+
+
+
+ >
+ );
+}
+
+export default DefaultTags;
diff --git a/components/markdown/index.js b/components/markdown/index.js
index 7d1a390..985e5fc 100644
--- a/components/markdown/index.js
+++ b/components/markdown/index.js
@@ -1,13 +1,19 @@
+"use client";
+
import { NextChakraLink } from "@app-components/NextChakraLink";
-import { Text } from "@chakra-ui/react";
+import { Text } from "@app-providers/chakra-ui";
+import ReactMarkdown from "react-markdown";
export const BaseMarkdown = {
a({ node, ...rest }) {
const href = rest.href;
return ;
},
-
p({ node, ...rest }) {
- return ;
+ return ;
},
};
+
+export const CustomReactMarkdown = ({ children }) => (
+ {children}
+);
diff --git a/components/mobile-menu/mobile-menu-button.js b/components/mobile-menu/mobile-menu-button.js
index 740dba7..4de13a3 100644
--- a/components/mobile-menu/mobile-menu-button.js
+++ b/components/mobile-menu/mobile-menu-button.js
@@ -1,18 +1,14 @@
-import { Icon, Text, useColorModeValue, VStack } from "@chakra-ui/react";
-import React from "react";
+"use client";
+
+import { Icon, Text, useColorModeValue, VStack } from "@app-providers/chakra-ui";
function MobileMenuButton({ label, icon, ...rest }) {
+ const textColor = useColorModeValue("gray.700", "gray.200");
+ const btnTextColor = useColorModeValue("blue.600", "yellow.200");
return (
-
-
-
+
+
+
{label}
diff --git a/components/mobile-menu/mobile-menu-item.js b/components/mobile-menu/mobile-menu-item.js
index 37a2e46..28d9ee9 100644
--- a/components/mobile-menu/mobile-menu-item.js
+++ b/components/mobile-menu/mobile-menu-item.js
@@ -1,33 +1,32 @@
+"use client";
+
import { NextChakraLinkButton } from "@app-components/NextChakraLink";
-import { useColorMode, useColorModeValue } from "@chakra-ui/react";
-import Link from "next/link";
+import { useColorModeValue } from "@chakra-ui/react";
import { usePathname } from "next/navigation";
function MobileMenuItem({ children, href }) {
- const pathname = usePathname();
let isActive = false;
+ const pathname = usePathname();
+ const linkColor = useColorModeValue("blue.600", "yellow.200");
if (href !== "/") {
const [, group] = href.split("/");
isActive = pathname.includes(group);
- } else {
- if (href === pathname) isActive = true;
+ } else if (href === pathname) {
+ isActive = true;
}
- const { colorMode } = useColorMode();
- const textColor = isActive ? (colorMode === "dark" ? "yellow.200" : "blue.600") : undefined;
-
return (
{children}
diff --git a/components/mobile-menu/mobile-menu-toggle.js b/components/mobile-menu/mobile-menu-toggle.js
index 9cbebbe..46f0049 100644
--- a/components/mobile-menu/mobile-menu-toggle.js
+++ b/components/mobile-menu/mobile-menu-toggle.js
@@ -1,4 +1,5 @@
-import MobileMenuItem from "@app-components/mobile-menu/mobile-menu-item";
+"use client";
+
import { _app_routes } from "@app-config/app.config";
import {
Drawer,
@@ -9,9 +10,10 @@ import {
DrawerOverlay,
useDisclosure,
VStack,
-} from "@chakra-ui/react";
+} from "@app-providers/chakra-ui";
import { HiOutlineMenuAlt1 } from "react-icons/hi";
import MobileMenuButton from "./mobile-menu-button";
+import MobileMenuItem from "./mobile-menu-item";
function MobileMenuToggle() {
const { isOpen, onOpen, onClose } = useDisclosure();
@@ -22,7 +24,7 @@ function MobileMenuToggle() {
- Menu
+ Menu
{_app_routes.map(({ title, href }) => (
diff --git a/helper/api.helper.js b/helper/api.helper.js
deleted file mode 100644
index 8aa6d7e..0000000
--- a/helper/api.helper.js
+++ /dev/null
@@ -1,47 +0,0 @@
-"use client";
-
-import useSWR from "swr";
-
-const fetcher = (...args) => fetch(...args).then((res) => res.json());
-
-function useGetAbout() {
- const res = useSWR("/api/about", fetcher, { refreshInterval: 600000 });
- return {
- about: res.data,
- aboutError: res.error,
- aboutLoading: res.isLoading,
- };
-}
-
-function useGetKnowledge() {
- const res = useSWR("/api/knowledge", fetcher, { refreshInterval: 600000 });
- return {
- knowledge: res.data,
- knowledgeError: res.error,
- knowledgeLoading: res.isLoading,
- };
-}
-
-function useGetProjects() {
- return useSWR("/api/projects", fetcher, { refreshInterval: 600000 });
-}
-
-function useGetAwards() {
- const res = useSWR("/api/awards", fetcher, { refreshInterval: 600000 });
- return {
- awards: res.data,
- awardsError: res.error,
- awardsLoading: res.isLoading,
- };
-}
-
-function useGetLicenses() {
- const res = useSWR("/api/licenses", fetcher, { refreshInterval: 600000 });
- return {
- licenses: res.data,
- licensesError: res.error,
- licensesLoading: res.isLoading,
- };
-}
-
-export { useGetAbout, useGetKnowledge, useGetProjects, useGetAwards, useGetLicenses };
diff --git a/libs/airtable.js b/libs/airtable.js
index 64bd009..78f4565 100644
--- a/libs/airtable.js
+++ b/libs/airtable.js
@@ -8,7 +8,7 @@ const base = new Airtable({
const minifyRecord = (record) => {
return {
id: record.id,
- fields: record.fields,
+ ...record.fields,
};
};
diff --git a/package.json b/package.json
index 19377de..f4404a8 100644
--- a/package.json
+++ b/package.json
@@ -20,16 +20,15 @@
"@plaiceholder/next": "2.5.0",
"@vercel/analytics": "0.1.6",
"airtable": "0.11.6",
- "framer-motion": "7.10.3",
+ "framer-motion": "8.0.2",
"next": "13.1.1",
"plaiceholder": "2.5.0",
"react": "18.2.0",
"react-dom": "18.2.0",
- "react-hook-form": "7.41.0",
+ "react-hook-form": "7.41.2",
"react-icons": "4.7.1",
"react-markdown": "8.0.4",
- "sharp": "0.31.2",
- "swr": "2.0.0"
+ "sharp": "0.31.3"
},
"devDependencies": {
"@commitlint/cli": "17.3.0",
diff --git a/pages/api/about.js b/pages/api/about.js
deleted file mode 100644
index 951d3a4..0000000
--- a/pages/api/about.js
+++ /dev/null
@@ -1,39 +0,0 @@
-import { getTable } from "@app-libs/airtable";
-import { getPlaiceholder } from "plaiceholder";
-
-export default async function handler(req, res) {
- let status = 200;
-
- let response = {
- header: {
- process_time: "",
- error_msg: "",
- error_code: "",
- },
- data: {
- about: null,
- aboutPic: null,
- },
- status: "OK",
- };
-
- switch (req.method) {
- case "GET":
- const aboutData = await getTable("About Me");
- if (aboutData.length < 0) {
- break;
- }
- response.data.about = aboutData[0];
- if (aboutData[0].fields.cover_im.length > 0) {
- const { base64, img } = await getPlaiceholder(aboutData[0].fields.cover_im[0].url);
- response.data.aboutPic = { blurDataURL: base64, src: img.src, type: img.type };
- }
- break;
- default:
- status = 405;
- response.status = "Method Not Allowed";
- response.header.error_msg = `method ${req.method} is not allowed`;
- }
-
- res.status(status).json(response);
-}
diff --git a/pages/api/awards.js b/pages/api/awards.js
deleted file mode 100644
index 04d1ed6..0000000
--- a/pages/api/awards.js
+++ /dev/null
@@ -1,39 +0,0 @@
-import { getTable } from "@app-libs/airtable";
-
-export default async function handler(req, res) {
- let status = 200;
-
- let response = {
- header: {
- process_time: "",
- error_msg: "",
- error_code: "",
- },
- data: null,
- status: "OK",
- };
-
- switch (req.method) {
- case "GET":
- const awards = await getTable("Awards", {
- sort: [{ field: "id", direction: "desc" }],
- });
- response.data = awards.map((award) => ({ ...award.fields, id: award.id }));
-
- // const awardsPlaiceholder = await Promise.all(
- // awards.map(async (item) => {
- // const path = item.fields?.images ? item.fields.images[0].url : undefined;
- // const { base64, img } = path != undefined ? await getPlaiceholder(path) : { base64: "", img: "" };
- // return { base64, img };
- // })
- // ).then((values) => values);
-
- break;
- default:
- status = 405;
- response.status = "Method Not Allowed";
- response.header.error_msg = `method ${req.method} is not allowed`;
- }
-
- res.status(status).json(response);
-}
diff --git a/pages/api/knowledge.js b/pages/api/knowledge.js
deleted file mode 100644
index 5efa15a..0000000
--- a/pages/api/knowledge.js
+++ /dev/null
@@ -1,30 +0,0 @@
-import { getTable } from "@app-libs/airtable";
-
-export default async function handler(req, res) {
- let status = 200;
-
- let response = {
- header: {
- process_time: "",
- error_msg: "",
- error_code: "",
- },
- data: null,
- status: "OK",
- };
-
- switch (req.method) {
- case "GET":
- const knowledgeData = await getTable("Knowledge Base", {
- sort: [{ field: "name", direction: "asc" }],
- });
- response.data = knowledgeData;
- break;
- default:
- status = 405;
- response.status = "Method Not Allowed";
- response.header.error_msg = `method ${req.method} is not allowed`;
- }
-
- res.status(status).json(response);
-}
diff --git a/pages/api/licenses.js b/pages/api/licenses.js
deleted file mode 100644
index 045add1..0000000
--- a/pages/api/licenses.js
+++ /dev/null
@@ -1,30 +0,0 @@
-import { getTable } from "@app-libs/airtable";
-
-export default async function handler(req, res) {
- let status = 200;
-
- let response = {
- header: {
- process_time: "",
- error_msg: "",
- error_code: "",
- },
- data: null,
- status: "OK",
- };
-
- switch (req.method) {
- case "GET":
- const licenses = await getTable("Licenses", {
- sort: [{ field: "id", direction: "desc" }],
- });
- response.data = licenses.map((license) => ({ ...license.fields, id: license.id }));
- break;
- default:
- status = 405;
- response.status = "Method Not Allowed";
- response.header.error_msg = `method ${req.method} is not allowed`;
- }
-
- res.status(status).json(response);
-}
diff --git a/pages/api/projects.js b/pages/api/projects.js
deleted file mode 100644
index 314e298..0000000
--- a/pages/api/projects.js
+++ /dev/null
@@ -1,45 +0,0 @@
-import { getTable } from "@app-libs/airtable";
-import { getPlaiceholder } from "plaiceholder";
-
-export default async function handler(req, res) {
- let status = 200;
-
- let response = {
- header: {
- process_time: "",
- error_msg: "",
- error_code: "",
- },
- data: null,
- status: "OK",
- };
-
- switch (req.method) {
- case "GET":
- const projects = await getTable("Recent Projects", {
- sort: [{ field: "id", direction: "desc" }],
- });
- response.data = await Promise.all(
- projects.map(async (project) => {
- const res = { ...project.fields, id: project.id };
- const { base64, img } = await getPlaiceholder(res.images[0]?.url);
-
- res.image = {
- src: img.src,
- type: img.type,
- blurDataURL: base64,
- };
-
- delete res.images;
- return res;
- })
- ).then((values) => values);
- break;
- default:
- status = 405;
- response.status = "Method Not Allowed";
- response.header.error_msg = `method ${req.method} is not allowed`;
- }
-
- res.status(status).json(response);
-}
diff --git a/providers/chakra-ui/index.js b/providers/chakra-ui/index.js
index 2b925e1..b04f8ba 100644
--- a/providers/chakra-ui/index.js
+++ b/providers/chakra-ui/index.js
@@ -1,3 +1,16 @@
"use client";
+import customTheme from "@app-styles/theme";
+import { ChakraProvider, ColorModeScript } from "@chakra-ui/react";
+
+function ChakraWrapper({ children }) {
+ return (
+
+
+ {children}
+
+ );
+}
+
export * from "@chakra-ui/react";
+export { ChakraWrapper };
diff --git a/providers/vercel/index.js b/providers/vercel/index.js
new file mode 100644
index 0000000..9b2aa22
--- /dev/null
+++ b/providers/vercel/index.js
@@ -0,0 +1,3 @@
+"use client";
+
+export * from "@vercel/analytics/react";
diff --git a/styles/theme.js b/styles/theme.js
index acd6b0c..c3da40d 100644
--- a/styles/theme.js
+++ b/styles/theme.js
@@ -25,7 +25,7 @@ const styles = {
minW: "320px",
color: mode("black", "white")(props),
bg: mode("white", "black")(props),
- transition: "none",
+ fontSize: { base: "sm", md: "md" },
...inter.style,
},
}),
@@ -54,7 +54,7 @@ const customTheme = extendTheme({
baseStyle: (props) => ({
color: mode("blue.600", "yellow.200")(props),
fontWeight: "500",
- borderBottom: "dotted",
+ borderBottom: { base: "1px dotted", lg: "2px dotted" },
_hover: {
textDecoration: "none",
color: mode("blue.500", "yellow.100")(props),
@@ -63,7 +63,8 @@ const customTheme = extendTheme({
},
Text: {
baseStyle: (props) => ({
- color: mode("gray.600", "gray.400")(props),
+ color: mode("gray.700", "gray.200")(props),
+ lineHeight: "tall",
}),
},
},