Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
@@ -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.
64 changes: 33 additions & 31 deletions src/pages/Documentaries.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { useState, useMemo } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faBook,
Expand Down Expand Up @@ -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");
Expand 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,
Expand Down Expand Up @@ -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) {
Expand Down
150 changes: 76 additions & 74 deletions src/pages/Forum.jsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -27,88 +27,90 @@ 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('');

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 (
<div className="min-h-screen bg-gray-50">
Expand Down
118 changes: 60 additions & 58 deletions src/pages/Roadmap.jsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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 (
Expand Down