Skip to content
48 changes: 23 additions & 25 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -1,48 +1,46 @@
import js from '@eslint/js'
import globals from 'globals'
import react from 'eslint-plugin-react'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import typescript from '@typescript-eslint/eslint-plugin'
import parser from '@typescript-eslint/parser'
import next from 'eslint-plugin-next'
import js from "@eslint/js";
import globals from "globals";
import react from "eslint-plugin-react";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import typescript from "@typescript-eslint/eslint-plugin";
import parser from "@typescript-eslint/parser";

export default [
{ ignores: ['dist', '.next'] },
{ ignores: ["dist", ".next", "out", "node_modules"] },
{
files: ['**/*.{js,jsx,ts,tsx}'],
files: ["**/*.{js,jsx,ts,tsx}"],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parser: parser,
parserOptions: {
ecmaVersion: 'latest',
ecmaVersion: "latest",
ecmaFeatures: { jsx: true },
sourceType: 'module',
sourceType: "module",
},
},
settings: { react: { version: '18.3' } },
settings: { react: { version: "18.3" } },
plugins: {
react,
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
'@typescript-eslint': typescript,
next,
"react-hooks": reactHooks,
"react-refresh": reactRefresh,
"@typescript-eslint": typescript,
},
rules: {
...js.configs.recommended.rules,
...react.configs.recommended.rules,
...react.configs['jsx-runtime'].rules,
...react.configs["jsx-runtime"].rules,
...reactHooks.configs.recommended.rules,
...typescript.configs.recommended.rules,
...next.configs.recommended.rules,
'react/jsx-no-target-blank': 'off',
'react-refresh/only-export-components': [
'warn',
"react/jsx-no-target-blank": "off",
"react/prop-types": "off", // Using TypeScript for prop validation
"react-refresh/only-export-components": [
"warn",
{ allowConstantExport: true },
],
'@typescript-eslint/no-unused-vars': 'off',
'react/no-unknown-property': 'off',
"@typescript-eslint/no-unused-vars": "off",
"react/no-unknown-property": "off",
},
},
]
];
30 changes: 23 additions & 7 deletions src/components/discovery/DiscoveryCard.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,31 @@
import Link from "next/link";
import React from "react";
import React, { memo } from "react";
import { PostMeta } from "../../utils/markdown";
import SafeImage from "../ui/SafeImage";

type Props = {
post: PostMeta;
};

export default function DiscoveryCard({ post }: Props) {
const DiscoveryCard = memo(({ post }: Props) => {
return (
<article className="group h-full">
<Link href={`/discovery/${post.slug}`} className="project-card discovery-card block h-full flex flex-col">
<Link
href={`/discovery/${post.slug}`}
className="project-card discovery-card block h-full flex flex-col"
>
{/* Fixed-height image area so cards align evenly */}
<div className="w-full h-56 overflow-hidden rounded-t">
{post.image ? (
<SafeImage src={post.image} alt={post.title} className="w-full h-full object-cover" />
<SafeImage
src={post.image}
alt={post.title}
className="w-full h-full object-cover"
/>
) : (
<div className="w-full h-full bg-gray-800 flex items-center justify-center text-gray-400">No Image</div>
<div className="w-full h-full bg-gray-800 flex items-center justify-center text-gray-400">
No Image
</div>
)}
</div>

Expand All @@ -27,7 +36,10 @@ export default function DiscoveryCard({ post }: Props) {
<p className="project-subtitle">{post.description}</p>
<div className="mt-3 flex flex-wrap gap-2">
{post.tags?.map((t) => (
<span key={t} className="text-xs bg-gray-700 text-gray-200 px-2 py-1 rounded">
<span
key={t}
className="text-xs bg-gray-700 text-gray-200 px-2 py-1 rounded"
>
{t}
</span>
))}
Expand All @@ -36,4 +48,8 @@ export default function DiscoveryCard({ post }: Props) {
</Link>
</article>
);
}
});

DiscoveryCard.displayName = "DiscoveryCard";

export default DiscoveryCard;
23 changes: 15 additions & 8 deletions src/components/discovery/DiscoveryList.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from "react";
import React, { useMemo } from "react";
import { PostMeta } from "../../utils/markdown";
import DiscoveryCard from "./DiscoveryCard";

Expand All @@ -8,13 +8,20 @@

export default function DiscoveryList({ posts }: Props) {
if (!posts.length)
return <p className="text-center text-gray-400">No posts yet, come back later?</p>;
// Ensure posts are sorted by date (newest first). Some posts may omit dates.
const sorted = [...posts].sort((a, b) => {
const da = a.date ? new Date(a.date).getTime() : 0;
const db = b.date ? new Date(b.date).getTime() : 0;
return db - da; //db - da for descending order (newest first)
});
return (
<p className="text-center text-gray-400">
No posts yet, come back later?
</p>

Check warning on line 14 in src/components/discovery/DiscoveryList.tsx

View check run for this annotation

codefactor.io / CodeFactor

src/components/discovery/DiscoveryList.tsx#L14

React Hook "useMemo" is called conditionally. React Hooks must be called in the exact same order in every component render. Did you accidentally call a React Hook after an early return? (react-hooks/rules-of-hooks)
);

// Ensure posts are sorted by date (newest first). Memoize to avoid re-sorting on every render.
const sorted = useMemo(() => {
return [...posts].sort((a, b) => {
const da = a.date ? new Date(a.date).getTime() : 0;
const db = b.date ? new Date(b.date).getTime() : 0;
return db - da; //db - da for descending order (newest first)
});
}, [posts]);

return (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4 p-6 pb-32">
Expand Down
13 changes: 7 additions & 6 deletions src/components/layout/Background.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
const Bg = () => {
return (
<div className="absolute top-0 left-0 z-[-1] w-full h-full">
</div>
);
};
import { memo } from "react";

const Bg = memo(() => {
return <div className="absolute top-0 left-0 z-[-1] w-full h-full"></div>;
});

Bg.displayName = "Background";

export default Bg;
40 changes: 22 additions & 18 deletions src/components/layout/MyHead.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useGLTF } from "@react-three/drei";
import { Canvas, useFrame } from "@react-three/fiber";
import { easing } from "maath";
import { useEffect, useRef, useState } from "react";
import { useEffect, useRef, useState, memo } from "react";
import { Object3D, LoopOnce, AnimationMixer } from "three";
function Model({ onLoaded }: { onLoaded: () => void }) {
const gltf = useGLTF("/assets/myHead.gltf");
Expand Down Expand Up @@ -38,33 +38,31 @@ function Model({ onLoaded }: { onLoaded: () => void }) {
};
}, [scene, animations]);

useEffect(() => {
const animate = () => {
requestAnimationFrame(animate);
if (mixer.current) {
mixer.current.update(0.01);
}
};
animate();
}, []);

useEffect(() => {
if (head.current) {
head.current.rotation.set(0, Math.PI, 0);
head.current.position.set(0, -0.5, 0);
}
}, []);

useFrame((state, dt) => {
dummy.lookAt(cursor.x, cursor.y, 1);
dummy.rotation.y += Math.PI;
easing.dampQ(head.current.quaternion, dummy.quaternion, 0.15, dt);
useFrame((state, delta) => {
// Update animation mixer if active
if (mixer.current) {
mixer.current.update(delta);
}

// Update head rotation to follow cursor
if (head.current) {
dummy.lookAt(cursor.x, cursor.y, 1);
dummy.rotation.y += Math.PI;
easing.dampQ(head.current.quaternion, dummy.quaternion, 0.15, delta);
}
});

return <primitive object={head.current} />;
}

const HeadRender = () => {
const HeadRender = memo(() => {
const [loading, setLoading] = useState(true);
return (
<div className="relative w-full h-[600px]">
Expand All @@ -73,12 +71,18 @@ const HeadRender = () => {
<div className="animate-pulse bg-gray-300 rounded-full w-40 h-40" />
</div>
)}
<Canvas camera={{ position: [0, 0, 6], fov: 50 }} id="head" className="w-full h-full">
<Canvas
camera={{ position: [0, 0, 6], fov: 50 }}
id="head"
className="w-full h-full"
>
<ambientLight intensity={0.5} />
<Model onLoaded={() => setLoading(false)} />
</Canvas>
</div>
);
};
});

HeadRender.displayName = "HeadRender";

export default HeadRender;
4 changes: 3 additions & 1 deletion src/components/layout/Nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ const Nav = () => {
(entries) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) return;
setActiveSection(entry.target.id);
// Only update if the active section has actually changed
const newSection = entry.target.id;
setActiveSection((prev) => (prev !== newSection ? newSection : prev));
});
},
{ threshold: 0.01 },
Expand Down
Loading
Loading