Refactor and update project components for Next.js 15 compatibility (#6)

* refactor: update all modules to latest version

* refactor: update layout to match nextjs 15 convention

* fix: implement TopProgressBar as Client

* fix: remove unnecessary locale prop

* fix: update postcss.config.js export

* fix: comment out styles

* fix: ref usage in useIntersectionObserver and update Navbar

* chore: performance optimizations

* fix: simplify Loading component structure

* refactor: move ProjectCard component and add prop types validation
This commit is contained in:
Alvalen Shafel 2025-03-18 12:39:32 +07:00 • committed by GitHub
parent 481bc8324e
commit 7f0c142dda
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 3422 additions and 2569 deletions

View file

@ -1,11 +1,7 @@
export default function Loading() { export default function Loading() {
return ( return (
<>
(
<div className="fixed top-0 left-0 flex justify-center items-center h-screen w-screen bg-gray-300 z-[999]"> <div className="fixed top-0 left-0 flex justify-center items-center h-screen w-screen bg-gray-300 z-[999]">
<h1 className="text-4xl text-gray-900">Loading...</h1> <h1 className="text-4xl text-gray-900">Loading...</h1>
</div> </div>
)
</>
); );
} }

View file

@ -104,7 +104,6 @@ const MyPage = () => {
href={"/docs/cv.pdf"} href={"/docs/cv.pdf"}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
locale={false}
download> download>
Download CV Download CV
</Link> </Link>

View file

@ -17,13 +17,15 @@ export function useIntersectionObserver() {
} }
); );
if (ref.current) { const currentRef = ref.current; // Store the ref value in a variable
observer.observe(ref.current);
if (currentRef) {
observer.observe(currentRef);
} }
return () => { return () => {
if (ref.current) { if (currentRef) {
observer.unobserve(ref.current); observer.unobserve(currentRef);
} }
}; };
}, []); }, []);

View file

@ -1,7 +1,7 @@
import Footer from "@/components/Footer"; import Footer from "@/components/Footer";
export const metadata = { export const metadata = {
title: "Alvalens | About", title: "Alvalens | About"
}; };
export default function Layout({ children }) { export default function Layout({ children }) {
return ( return (

View file

@ -1,11 +1,7 @@
export default function Loading() { export default function Loading() {
return ( return (
<>
(
<div className="fixed top-0 left-0 flex justify-center items-center h-screen w-screen bg-gray-300 z-[999]"> <div className="fixed top-0 left-0 flex justify-center items-center h-screen w-screen bg-gray-300 z-[999]">
<h1 className="text-4xl text-gray-900">Loading...</h1> <h1 className="text-4xl text-gray-900">Loading...</h1>
</div> </div>
)
</>
); );
} }

View file

@ -3,9 +3,7 @@
@import url('https://fonts.googleapis.com/css2?family=Jost:wght@400;500;600;700&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Jost:wght@400;500;600;700&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,300;0,400;0,500;0,600;0,700;1,400&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,300;0,400;0,500;0,600;0,700;1,400&display=swap');
@tailwind base; @import "tailwindcss";
@tailwind components;
@tailwind utilities;
html { html {
font-size: 17px !important; font-size: 17px !important;
@ -53,9 +51,9 @@ h1, h2, h3, h4, h5, h6 {
} }
h2, h3, h4, h5, h6 { /* h2, h3, h4, h5, h6 {
@apply text-gray-600; @apply text-gray-600;
} } */
span { span {
font-family: "Jost", sans-serif; font-family: "Jost", sans-serif;
@ -64,6 +62,7 @@ span {
li { li {
@apply list-none relative; @apply list-none relative;
} }
li.active:after { li.active:after {
content: ""; content: "";
position: absolute; position: absolute;

View file

@ -1,4 +1,3 @@
import dynamic from "next/dynamic";
import "./globals.css"; import "./globals.css";
import Navbar from "@/components/Navbar"; import Navbar from "@/components/Navbar";
import { config } from "@fortawesome/fontawesome-svg-core"; import { config } from "@fortawesome/fontawesome-svg-core";
@ -6,14 +5,19 @@ import "@fortawesome/fontawesome-svg-core/styles.css";
config.autoAddCss = false; config.autoAddCss = false;
import "./nprogress.css"; import "./nprogress.css";
import { Analytics } from "@vercel/analytics/react"; import { Analytics } from "@vercel/analytics/react";
import Chat from "@/components/Chat";
import ClientTopProgressBar from "@/components/ClientTopProgressBar";
export const metadata = { export const metadata = {
title: "Alvalens | Portofolio", title: "Alvalens | Portofolio",
description: description:
"My name is Alvalens, I'm a web developer and I'm passionate about it. I'm currently studying at Universitas Negeri Malang.", "My name is Alvalens, I'm a web developer and I'm passionate about it. I'm currently studying at Universitas Negeri Malang.",
author: "Alvalen Shafelbilyunazra", author: "Alvalen Shafelbilyunazra",
siteUrl: "https://www.alvalens.my.id", siteUrl: "https://www.alvalens.my.id",
applicationName: "Alvalens", applicationName: "Alvalens",
keywords: [ keywords: [
"alvalens", "alvalens",
"alvalen", "alvalen",
@ -25,6 +29,7 @@ export const metadata = {
"alvalen porto", "alvalen porto",
"alvalen um", "alvalen um",
], ],
openGraph: { openGraph: {
type: "website", type: "website",
url: "https://www.alvalens.my.id", url: "https://www.alvalens.my.id",
@ -40,23 +45,14 @@ export const metadata = {
}, },
], ],
site_name: "Alvalens | Portofolio", site_name: "Alvalens | Portofolio",
}, }
}; };
import Chat from "@/components/Chat";
const TopProgressBar = dynamic(() => import("@/components/TopProgressbar"), {
ssr: false,
});
export default function RootLayout({ children }) { export default function RootLayout({ children }) {
return ( return (
<html lang="en"> <html lang="en">
<body> <body>
<TopProgressBar /> <ClientTopProgressBar />
<Navbar /> <Navbar />
{children} {children}
<Chat /> <Chat />

View file

@ -1,9 +1,9 @@
import Link from "next/link"; import Link from "next/link";
import { motion } from "framer-motion";
import Image from "next/image"; import Image from "next/image";
export default function projectCard({project, index, activeCategory}){ import { motion } from "framer-motion";
console.log(project.category[0], activeCategory); import PropTypes from "prop-types";
console.log(project.category.includes(activeCategory));
export default function ProjectCard({ project, index, activeCategory }) {
return ( return (
<> <>
{project.category.includes(parseInt(activeCategory)) && ( {project.category.includes(parseInt(activeCategory)) && (
@ -56,3 +56,10 @@ export default function projectCard({project, index, activeCategory}){
</> </>
); );
} }
ProjectCard.propTypes = {
project: PropTypes.object.isRequired,
index: PropTypes.number.isRequired,
activeCategory: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
.isRequired,
};

View file

@ -1,5 +1,5 @@
"use client"; "use client";
import { useState, useEffect } from "react"; import { useState, useEffect, use } from "react";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import jsonData from "@/json/data.json"; import jsonData from "@/json/data.json";
@ -53,7 +53,8 @@ function ScrollDownButton() {
} }
function Page({ params }) { function Page(props) {
const params = use(props.params);
const [data, setData] = useState(null); const [data, setData] = useState(null);
useEffect(() => { useEffect(() => {
const selectedData = jsonData.Projects.find( const selectedData = jsonData.Projects.find(

View file

@ -0,0 +1,65 @@
import Link from "next/link";
import Image from "next/image";
import { motion } from "framer-motion";
import PropTypes from "prop-types";
export default function ProjectCard({ project, index, activeCategory }) {
return (
<>
{project.category.includes(parseInt(activeCategory)) && (
<Link href={"projects/" + project.slug} key={index}>
<motion.div
className="z-10 relative flex justify-center items-start flex-col mb-5 md:px-10 w-full h-auto bg-gray-400 group/tes py-20 px-5 md:py-2 aspect-video "
initial={{
opacity: 0,
x: -200,
}}
whileInView={{
opacity: 1,
x: 0,
}}
transition={{
type: "spring",
}}>
<Image
src={project.thumbnail}
alt="Alvalens"
layout="fill"
objectFit="cover"
placeholder="blur"
className="bg-slate-950 opacity-10 group-hover/tes:opacity-100 transition-all ease duration-500"
blurDataURL={project.thumbnail}
/>
<div className="absolute top-0 left-0 bg-gray-600 px-4 py-2">
<h4 className="text-white">{project.year}</h4>
</div>
<div className="transition-all ease duration-500 opacity-100 content text-center group-hover/tes:opacity-0 z-10">
<h1 className="text-3xl font-bold mb-3">{project.title}</h1>
<p>
{project.desc[0].length > 125
? `${project.desc[0].slice(0, 125)}...`
: project.desc[0]}
</p>
<div className="flex justify-center items-center flex-row mt-5 flex-wrap">
{project.tech.map((t, index) => (
<span
key={index}
className="m-1 px-4 py-2 bg-gray-600 text-white ">
{t}
</span>
))}
</div>
</div>
</motion.div>
</Link>
)}
</>
);
}
ProjectCard.propTypes = {
project: PropTypes.object.isRequired,
index: PropTypes.number.isRequired,
activeCategory: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
.isRequired,
};

View file

@ -2,7 +2,7 @@
import Footer from "@/components/Footer"; import Footer from "@/components/Footer";
export const metadata = { export const metadata = {
title: "Alvalens | Projects", title: "Alvalens | Projects"
}; };
export default function Layout({ children }) { export default function Layout({ children }) {
return ( return (

View file

@ -1,11 +1,7 @@
export default function Loading() { export default function Loading() {
return ( return (
<>
(
<div className="fixed top-0 left-0 flex justify-center items-center h-screen w-screen bg-gray-300 z-[999]"> <div className="fixed top-0 left-0 flex justify-center items-center h-screen w-screen bg-gray-300 z-[999]">
<h1 className="text-4xl text-gray-900">Loading...</h1> <h1 className="text-4xl text-gray-900">Loading...</h1>
</div> </div>
)
</>
); );
} }

View file

@ -12,7 +12,7 @@ import ReactChat3 from "@/public/image/projects/web/trivia/trivia3.png";
import ProjectAll from "@/public/image/projects.png"; import ProjectAll from "@/public/image/projects.png";
import Hr from "@/components/Hr"; import Hr from "@/components/Hr";
import ProjectCard from "./(project-card)"; import ProjectCard from "./components/ProjectCard";
import Projects from "@/json/data.json"; import Projects from "@/json/data.json";
import FixedButon from "@/components/FixedButton"; import FixedButon from "@/components/FixedButton";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@ -22,12 +22,11 @@ const category = {
1: "Web Development", 1: "Web Development",
2: "AI & Machine Learning", 2: "AI & Machine Learning",
9: "Other", 9: "Other",
} };
export default function Page() { export default function Page() {
const [activeCategory, setActiveCategory] = useState(1); const [activeCategory, setActiveCategory] = useState(1);
const projects = Projects.Projects.filter ((item) => item.show === true const projects = Projects.Projects.filter((item) => item.show === true);
);
useEffect(() => { useEffect(() => {
window.scrollTo(0, 0); window.scrollTo(0, 0);
@ -36,10 +35,7 @@ export default function Page() {
<> <>
<main className="overflow-hidden"> <main className="overflow-hidden">
<FixedButon href="/#projects"> <FixedButon href="/#projects">
<FontAwesomeIcon <FontAwesomeIcon icon={faChevronLeft} className="text-black pr-10" />
icon={faChevronLeft}
className="text-black pr-10"
/>
</FixedButon> </FixedButon>
<div className="relative h-screen w-screen gap-4 p-10 flex justify-center items-center flex-col mb-10 overflow-hidden"> <div className="relative h-screen w-screen gap-4 p-10 flex justify-center items-center flex-col mb-10 overflow-hidden">
<div className="z-0 mb-48 md:mb-0 md:absolute top-1/4 md:right-[10%] md:-translate-y-16 "> <div className="z-0 mb-48 md:mb-0 md:absolute top-1/4 md:right-[10%] md:-translate-y-16 ">
@ -181,15 +177,13 @@ export default function Page() {
Generative Trivia Generative Trivia
</h2> </h2>
<p className="text-gray-600 text-justify title text-lg"> <p className="text-gray-600 text-justify title text-lg">
A generative trivia website built with Next.js, A generative trivia website built with Next.js, Firebase,
Firebase, NextAuth, Google OAuth, and TypeScript. NextAuth, Google OAuth, and TypeScript. Users can generate trivia
Users can generate trivia questions for predefined questions for predefined school subjects and answer them with a
school subjects and answer them with a unique unique interactive input style. The application supports user
interactive input style. The application supports authentication with Google and stores data in Firestore. You can
user authentication with Google and stores data in see the source code of this project on my github account or
Firestore. You can see the source code of this preview the project by clicking the button below.
project on my github account or preview the project
by clicking the button below.
</p>{" "} </p>{" "}
<div className="mt-3"> <div className="mt-3">
<Button variation="primary"> <Button variation="primary">

View file

@ -0,0 +1,11 @@
"use client";
import dynamic from "next/dynamic";
const TopProgressBar = dynamic(() => import("@/components/TopProgressbar"), {
ssr: false,
});
export default function ClientTopProgressBar() {
return <TopProgressBar />;
}

View file

@ -111,7 +111,7 @@ const NavItems = ({ isNavOpen, setIsNavOpen }) => {
animate={isNavOpen ? "open" : "closed"} animate={isNavOpen ? "open" : "closed"}
initial={false}> initial={false}>
<div className="relative backdrop-blur-sm opacity-95 flex flex-col items-center space-x-8 min-h-[100vh] bg-gray-700 min-w-[100vw] "> <div className="relative backdrop-blur-sm opacity-95 flex flex-col items-center space-x-8 min-h-[100vh] bg-gray-700 min-w-[100vw] ">
<div className="flex flex-col items-center space-y-8 my-auto mx-0"> <div className="flex flex-col items-center space-y-8 my-auto mx-0 z-50">
{/* title */} {/* title */}
<motion.h1 <motion.h1
variants={itemVariants} variants={itemVariants}
@ -119,7 +119,7 @@ const NavItems = ({ isNavOpen, setIsNavOpen }) => {
className="text-6xl font-bold text-white "> className="text-6xl font-bold text-white ">
Menu Menu
</motion.h1> </motion.h1>
<a href="/#home"> <Link href="/#home">
<div <div
className="text-2xl font-bold text-white" className="text-2xl font-bold text-white"
onClick={handleItemClick}> onClick={handleItemClick}>
@ -131,7 +131,7 @@ const NavItems = ({ isNavOpen, setIsNavOpen }) => {
Home Home
</motion.h2> </motion.h2>
</div> </div>
</a> </Link>
<Link href="/about"> <Link href="/about">
<div <div
onClick={handleItemClick} onClick={handleItemClick}
@ -158,7 +158,7 @@ const NavItems = ({ isNavOpen, setIsNavOpen }) => {
</motion.h2> </motion.h2>
</div> </div>
</Link> </Link>
<a href="/#contact"> <Link href="/#contact">
<div <div
onClick={handleItemClick} onClick={handleItemClick}
className="text-2xl font-bold text-white"> className="text-2xl font-bold text-white">
@ -170,7 +170,7 @@ const NavItems = ({ isNavOpen, setIsNavOpen }) => {
Contact Contact
</motion.h2> </motion.h2>
</div> </div>
</a> </Link>
</div> </div>
</div> </div>
</motion.div> </motion.div>
@ -209,15 +209,11 @@ const Navbar = () => {
onClick={toggleNav}> onClick={toggleNav}>
<div <div
className={`w-10 h-1 bg-black rounded-full transition-all ease duration-300 ${ className={`w-10 h-1 bg-black rounded-full transition-all ease duration-300 ${
isNavOpen isNavOpen ? "rotate-45 bg-white translate-y-[2px]" : ""
? "rotate-45 bg-white translate-y-[2px]"
: ""
}`}></div> }`}></div>
<div <div
className={`w-10 h-1 bg-black rounded-full transition-all ease duration-300 ${ className={`w-10 h-1 bg-black rounded-full transition-all ease duration-300 ${
isNavOpen isNavOpen ? "-rotate-45 -translate-y-2 bg-white" : ""
? "-rotate-45 -translate-y-2 bg-white"
: ""
}`}></div> }`}></div>
</button> </button>
</div> </div>

View file

@ -5,6 +5,9 @@ module.exports = withBundleAnalyzer({
// your Next.js configuration // your Next.js configuration
images: { images: {
domains: ["i.scdn.co"], domains: ["i.scdn.co"],
// Add image optimization settings
formats: ['image/avif', 'image/webp'],
minimumCacheTTL: 60,
}, },
webpack: (config, options) => { webpack: (config, options) => {
config.module.rules.push({ config.module.rules.push({
@ -23,8 +26,37 @@ module.exports = withBundleAnalyzer({
key: "Content-Type", key: "Content-Type",
value: "application/gzip", value: "application/gzip",
}, },
{
key: "Cache-Control",
value: "public, max-age=3600", // Cache for 1 hour
},
],
},
{
source: '/:path*',
headers: [
{
key: 'X-Content-Type-Options',
value: 'nosniff',
},
{
key: 'X-Frame-Options',
value: 'DENY',
},
{
key: 'X-XSS-Protection',
value: '1; mode=block',
},
], ],
}, },
]; ];
}, },
// Add performance optimizations
swcMinify: true,
reactStrictMode: true,
compiler: {
removeConsole: process.env.NODE_ENV === 'production' ? {
exclude: ['error', 'warn'],
} : false,
},
}); });

View file

@ -3,36 +3,37 @@
"version": "1.0.0", "version": "1.0.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev --turbo", "dev": "next dev --turbopack",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint", "lint": "next lint",
"generate-sitemap": "node generate-sitemap.js" "generate-sitemap": "node generate-sitemap.js"
}, },
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.4.2", "@fortawesome/fontawesome-svg-core": "^6.7.2",
"@fortawesome/free-brands-svg-icons": "^6.4.2", "@fortawesome/free-brands-svg-icons": "^6.7.2",
"@fortawesome/free-regular-svg-icons": "^6.4.2", "@fortawesome/free-regular-svg-icons": "^6.7.2",
"@fortawesome/free-solid-svg-icons": "^6.4.2", "@fortawesome/free-solid-svg-icons": "^6.7.2",
"@fortawesome/react-fontawesome": "^0.2.0", "@fortawesome/react-fontawesome": "^0.2.2",
"@fullpage/react-fullpage": "^0.1.40", "@fullpage/react-fullpage": "^0.1.48",
"@vercel/analytics": "^1.0.2", "@tailwindcss/postcss": "^4.0.14",
"autoprefixer": "10.4.15", "@vercel/analytics": "^1.5.0",
"eslint": "8.48.0", "eslint": "9.22.0",
"eslint-config-next": "13.4.19", "eslint-config-next": "15.2.3",
"framer-motion": "^10.16.2", "framer-motion": "^12.5.0",
"next": "13.4.19", "next": "15.2.3",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"postcss": "8.4.28", "postcss": "^8.5.3",
"react": "18.2.0", "prop-types": "^15.8.1",
"react-dom": "18.2.0", "react": "19.0.0",
"sharp": "^0.32.5", "react-dom": "19.0.0",
"sitemap": "^7.1.1", "sharp": "^0.33.5",
"swr": "^2.2.2", "sitemap": "^8.0.0",
"tailwindcss": "3.3.3" "swr": "^2.3.3",
"tailwindcss": "^4.0.14"
}, },
"devDependencies": { "devDependencies": {
"@next/bundle-analyzer": "^13.4.19", "@next/bundle-analyzer": "15.2.3",
"cross-env": "^7.0.3" "cross-env": "^7.0.3"
} }
} }

5589
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,5 @@
module.exports = { module.exports = {
plugins: { plugins: {
tailwindcss: {}, "@tailwindcss/postcss": {},
autoprefixer: {},
}, },
} };

View file

@ -1,12 +1,18 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
module.exports = { module.exports = {
important: true,
content: [ content: [
'./pages/**/*.{js,ts,jsx,tsx,mdx}', "./pages/**/*.{js,ts,jsx,tsx,mdx}",
'./components/**/*.{js,ts,jsx,tsx,mdx}', "./components/**/*.{js,ts,jsx,tsx,mdx}",
'./app/**/*.{js,ts,jsx,tsx,mdx}', "./app/**/*.{js,ts,jsx,tsx,mdx}",
], ],
theme: { theme: {
extend: {
},
}, },
plugins: [], plugins: [],
} // Add this to reduce CSS file size in production
future: {
removeDeprecatedGapUtilities: true,
purgeLayersByDefault: true,
},
};