diff --git a/.jules/bolt.md b/.jules/bolt.md new file mode 100644 index 0000000..b901576 --- /dev/null +++ b/.jules/bolt.md @@ -0,0 +1,3 @@ +## 2024-05-06 - Component Optimizations +**Learning:** Re-renders can be expensive if static arrays (e.g., config arrays) are defined inside the component body, as they break referential equality for `useMemo` dependencies and cause recalculations on every render. +**Action:** Always move static arrays and helper functions outside the component body when possible, and consistently wrap derived arrays (like client-side filtered lists) with `useMemo` to prevent expensive recalculations and unnecessary re-renders. diff --git a/src/pages/Documentaries.jsx b/src/pages/Documentaries.jsx index 8befef4..eba2709 100644 --- a/src/pages/Documentaries.jsx +++ b/src/pages/Documentaries.jsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useState, useMemo } from "react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faBook, @@ -26,6 +26,27 @@ import ErrorMessage from "../components/common/ErrorMessage"; import SkeletonLoader from "../components/common/SkeletonLoader"; import Button from "../components/common/Button"; +const categories = [ + { id: "all", name: "All Resources", icon: faBook }, + { id: "html", name: "HTML", icon: faHtml5, color: "text-orange-600" }, + { id: "css", name: "CSS", icon: faCss3Alt, color: "text-blue-600" }, + { + id: "javascript", + name: "JavaScript", + icon: faJsSquare, + color: "text-yellow-600", + }, + { id: "python", name: "Python", icon: faPython, color: "text-green-600" }, + { id: "react", name: "React", icon: faReact, color: "text-cyan-600" }, + { id: "node", name: "Node.js", icon: faNodeJs, color: "text-green-500" }, + { + id: "database", + name: "Database", + icon: faDatabase, + color: "text-purple-600", + }, +]; + const Documentaries = () => { const [searchTerm, setSearchTerm] = useState(""); const [selectedCategory, setSelectedCategory] = useState("all"); @@ -37,27 +58,6 @@ const Documentaries = () => { refetch, } = useApi(documentariesApi.getAll); - const categories = [ - { id: "all", name: "All Resources", icon: faBook }, - { id: "html", name: "HTML", icon: faHtml5, color: "text-orange-600" }, - { id: "css", name: "CSS", icon: faCss3Alt, color: "text-blue-600" }, - { - id: "javascript", - name: "JavaScript", - icon: faJsSquare, - color: "text-yellow-600", - }, - { id: "python", name: "Python", icon: faPython, color: "text-green-600" }, - { id: "react", name: "React", icon: faReact, color: "text-cyan-600" }, - { id: "node", name: "Node.js", icon: faNodeJs, color: "text-green-500" }, - { - id: "database", - name: "Database", - icon: faDatabase, - color: "text-purple-600", - }, - ]; - const externalResources = [ { id: 1, @@ -149,17 +149,19 @@ const Documentaries = () => { }, ]; - const filteredResources = externalResources.filter((resource) => { - const matchesSearch = - !searchTerm || - resource.title.toLowerCase().includes(searchTerm.toLowerCase()) || - resource.description.toLowerCase().includes(searchTerm.toLowerCase()); + const filteredResources = useMemo(() => { + return externalResources.filter((resource) => { + const matchesSearch = + !searchTerm || + resource.title.toLowerCase().includes(searchTerm.toLowerCase()) || + resource.description.toLowerCase().includes(searchTerm.toLowerCase()); - const matchesCategory = - selectedCategory === "all" || resource.category === selectedCategory; + const matchesCategory = + selectedCategory === "all" || resource.category === selectedCategory; - return matchesSearch && matchesCategory; - }); + return matchesSearch && matchesCategory; + }); + }, [searchTerm, selectedCategory]); const getDifficultyColor = (difficulty) => { switch (difficulty) { diff --git a/src/pages/Forum.jsx b/src/pages/Forum.jsx index 1c15a08..5e6fbec 100644 --- a/src/pages/Forum.jsx +++ b/src/pages/Forum.jsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useMemo } from 'react'; import { Link } from 'react-router-dom'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { @@ -27,6 +27,71 @@ import SkeletonLoader from '../components/common/SkeletonLoader'; import Button from '../components/common/Button'; import { formatDate, truncateText } from '../utils/helpers'; +const communityLinks = [ + { + name: 'Telegram Group', + icon: faTelegram, + url: 'https://t.me/example', + members: '2.5K+', + description: 'Chat real-time dengan developer lainnya', + color: 'bg-blue-500' + }, + { + name: 'Discord Server', + icon: faDiscord, + url: 'https://discord.gg/example', + members: '1.8K+', + description: 'Voice chat dan screen sharing untuk coding session', + color: 'bg-indigo-500' + }, + { + name: 'WhatsApp Group', + icon: faWhatsapp, + url: 'https://chat.whatsapp.com/example', + members: '500+', + description: 'Diskusi ringan dan sharing tips programming', + color: 'bg-green-500' + } +]; + +const recentTopics = [ + { + id: 1, + title: 'Cara deploy aplikasi React ke Vercel?', + author: 'Andi Developer', + replies: 12, + views: 156, + lastActivity: '2 jam lalu', + category: 'React' + }, + { + id: 2, + title: 'Best practice untuk REST API dengan Node.js', + author: 'Sari Backend', + replies: 8, + views: 89, + lastActivity: '4 jam lalu', + category: 'Backend' + }, + { + id: 3, + title: 'Tutorial CSS Grid vs Flexbox', + author: 'Budi Frontend', + replies: 15, + views: 234, + lastActivity: '1 hari lalu', + category: 'CSS' + } +]; + +const categories = [ + { id: 'all', name: 'Semua Diskusi', icon: faComments }, + { id: 'web', name: 'Web Development', icon: faRocket }, + { id: 'data', name: 'Data Science', icon: faQuestionCircle }, + { id: 'frontend', name: 'Frontend', icon: faUsers }, + { id: 'backend', name: 'Backend', icon: faUsers } +]; + const Forum = () => { const [selectedCategory, setSelectedCategory] = useState('all'); const [searchTerm, setSearchTerm] = useState(''); @@ -34,81 +99,18 @@ const Forum = () => { const { data: forums, loading: forumsLoading, error: forumsError } = useApi(forumsApi.getAll); const { data: posts, loading: postsLoading, error: postsError } = useApi(postsApi.getAll); - const communityLinks = [ - { - name: 'Telegram Group', - icon: faTelegram, - url: 'https://t.me/example', - members: '2.5K+', - description: 'Chat real-time dengan developer lainnya', - color: 'bg-blue-500' - }, - { - name: 'Discord Server', - icon: faDiscord, - url: 'https://discord.gg/example', - members: '1.8K+', - description: 'Voice chat dan screen sharing untuk coding session', - color: 'bg-indigo-500' - }, - { - name: 'WhatsApp Group', - icon: faWhatsapp, - url: 'https://chat.whatsapp.com/example', - members: '500+', - description: 'Diskusi ringan dan sharing tips programming', - color: 'bg-green-500' - } - ]; - - const recentTopics = [ - { - id: 1, - title: 'Cara deploy aplikasi React ke Vercel?', - author: 'Andi Developer', - replies: 12, - views: 156, - lastActivity: '2 jam lalu', - category: 'React' - }, - { - id: 2, - title: 'Best practice untuk REST API dengan Node.js', - author: 'Sari Backend', - replies: 8, - views: 89, - lastActivity: '4 jam lalu', - category: 'Backend' - }, - { - id: 3, - title: 'Tutorial CSS Grid vs Flexbox', - author: 'Budi Frontend', - replies: 15, - views: 234, - lastActivity: '1 hari lalu', - category: 'CSS' - } - ]; + const filteredPosts = useMemo(() => { + return posts?.filter(post => { + const matchesSearch = !searchTerm || + post.title.toLowerCase().includes(searchTerm.toLowerCase()) || + post.content.toLowerCase().includes(searchTerm.toLowerCase()); - const filteredPosts = posts?.filter(post => { - const matchesSearch = !searchTerm || - post.title.toLowerCase().includes(searchTerm.toLowerCase()) || - post.content.toLowerCase().includes(searchTerm.toLowerCase()); - - const matchesCategory = selectedCategory === 'all' || - (forums && forums.some(forum => forum.id === post.forum_id && forum.title.toLowerCase().includes(selectedCategory))); - - return matchesSearch && matchesCategory; - }) || []; + const matchesCategory = selectedCategory === 'all' || + (forums && forums.some(forum => forum.id === post.forum_id && forum.title.toLowerCase().includes(selectedCategory))); - const categories = [ - { id: 'all', name: 'Semua Diskusi', icon: faComments }, - { id: 'web', name: 'Web Development', icon: faRocket }, - { id: 'data', name: 'Data Science', icon: faQuestionCircle }, - { id: 'frontend', name: 'Frontend', icon: faUsers }, - { id: 'backend', name: 'Backend', icon: faUsers } - ]; + return matchesSearch && matchesCategory; + }) || []; + }, [posts, searchTerm, selectedCategory, forums]); return (
diff --git a/src/pages/Roadmap.jsx b/src/pages/Roadmap.jsx index ebb3a63..24fc702 100644 --- a/src/pages/Roadmap.jsx +++ b/src/pages/Roadmap.jsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useMemo } from 'react'; import { Link } from 'react-router-dom'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { @@ -22,69 +22,71 @@ import SkeletonLoader from '../components/common/SkeletonLoader'; import Button from '../components/common/Button'; import { parseRoadmapSteps } from '../utils/helpers'; +const categories = [ + { id: 'all', name: 'Semua', icon: faRoute }, + { id: 'web', name: 'Web Development', icon: faCode }, + { id: 'mobile', name: 'Mobile Development', icon: faMobile }, + { id: 'data', name: 'Data Science', icon: faChartBar }, + { id: 'devops', name: 'DevOps', icon: faServer }, + { id: 'security', name: 'Security', icon: faShieldAlt }, + { id: 'design', name: 'UI/UX Design', icon: faPalette }, + { id: 'ai', name: 'AI/ML', icon: faRobot } +]; + +const getIconForPath = (pathName) => { + const name = pathName.toLowerCase(); + if (name.includes('web') || name.includes('frontend') || name.includes('fullstack')) return faCode; + if (name.includes('mobile')) return faMobile; + if (name.includes('data') || name.includes('scientist')) return faChartBar; + if (name.includes('devops')) return faServer; + if (name.includes('security')) return faShieldAlt; + if (name.includes('design') || name.includes('ui')) return faPalette; + if (name.includes('machine') || name.includes('ai')) return faRobot; + return faLaptopCode; +}; + +const getColorForPath = (pathName) => { + const name = pathName.toLowerCase(); + if (name.includes('web') || name.includes('frontend') || name.includes('fullstack')) return 'from-blue-500 to-blue-600'; + if (name.includes('mobile')) return 'from-purple-500 to-purple-600'; + if (name.includes('data') || name.includes('scientist')) return 'from-green-500 to-green-600'; + if (name.includes('devops')) return 'from-orange-500 to-orange-600'; + if (name.includes('security')) return 'from-red-500 to-red-600'; + if (name.includes('design') || name.includes('ui')) return 'from-pink-500 to-pink-600'; + if (name.includes('machine') || name.includes('ai')) return 'from-indigo-500 to-indigo-600'; + return 'from-gray-500 to-gray-600'; +}; + const Roadmap = () => { const [selectedCategory, setSelectedCategory] = useState('all'); const { data: roadmaps, loading, error, refetch } = useApi(roadmapsApi.getAll); - const categories = [ - { id: 'all', name: 'Semua', icon: faRoute }, - { id: 'web', name: 'Web Development', icon: faCode }, - { id: 'mobile', name: 'Mobile Development', icon: faMobile }, - { id: 'data', name: 'Data Science', icon: faChartBar }, - { id: 'devops', name: 'DevOps', icon: faServer }, - { id: 'security', name: 'Security', icon: faShieldAlt }, - { id: 'design', name: 'UI/UX Design', icon: faPalette }, - { id: 'ai', name: 'AI/ML', icon: faRobot } - ]; - - const getIconForPath = (pathName) => { - const name = pathName.toLowerCase(); - if (name.includes('web') || name.includes('frontend') || name.includes('fullstack')) return faCode; - if (name.includes('mobile')) return faMobile; - if (name.includes('data') || name.includes('scientist')) return faChartBar; - if (name.includes('devops')) return faServer; - if (name.includes('security')) return faShieldAlt; - if (name.includes('design') || name.includes('ui')) return faPalette; - if (name.includes('machine') || name.includes('ai')) return faRobot; - return faLaptopCode; - }; - - const getColorForPath = (pathName) => { - const name = pathName.toLowerCase(); - if (name.includes('web') || name.includes('frontend') || name.includes('fullstack')) return 'from-blue-500 to-blue-600'; - if (name.includes('mobile')) return 'from-purple-500 to-purple-600'; - if (name.includes('data') || name.includes('scientist')) return 'from-green-500 to-green-600'; - if (name.includes('devops')) return 'from-orange-500 to-orange-600'; - if (name.includes('security')) return 'from-red-500 to-red-600'; - if (name.includes('design') || name.includes('ui')) return 'from-pink-500 to-pink-600'; - if (name.includes('machine') || name.includes('ai')) return 'from-indigo-500 to-indigo-600'; - return 'from-gray-500 to-gray-600'; - }; + const filteredRoadmaps = useMemo(() => { + return roadmaps?.filter(roadmap => { + if (selectedCategory === 'all') return true; + const pathName = roadmap.career_path.toLowerCase(); - const filteredRoadmaps = roadmaps?.filter(roadmap => { - if (selectedCategory === 'all') return true; - const pathName = roadmap.career_path.toLowerCase(); - - switch (selectedCategory) { - case 'web': - return pathName.includes('web') || pathName.includes('frontend') || pathName.includes('fullstack'); - case 'mobile': - return pathName.includes('mobile'); - case 'data': - return pathName.includes('data') || pathName.includes('scientist'); - case 'devops': - return pathName.includes('devops'); - case 'security': - return pathName.includes('security'); - case 'design': - return pathName.includes('design') || pathName.includes('ui'); - case 'ai': - return pathName.includes('machine') || pathName.includes('ai'); - default: - return true; - } - }) || []; + switch (selectedCategory) { + case 'web': + return pathName.includes('web') || pathName.includes('frontend') || pathName.includes('fullstack'); + case 'mobile': + return pathName.includes('mobile'); + case 'data': + return pathName.includes('data') || pathName.includes('scientist'); + case 'devops': + return pathName.includes('devops'); + case 'security': + return pathName.includes('security'); + case 'design': + return pathName.includes('design') || pathName.includes('ui'); + case 'ai': + return pathName.includes('machine') || pathName.includes('ai'); + default: + return true; + } + }) || []; + }, [roadmaps, selectedCategory]); if (error) { return (