Skip to content
Open
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
216 changes: 216 additions & 0 deletions app/projects/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
"use client"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Consider Server Component for better SEO and performance.

Using "use client" makes the entire page client-side rendered, which may hurt SEO since project content won't be in the initial HTML. With Next.js 15, consider refactoring to:

  • Keep the page as a Server Component for static content (header, project data)
  • Extract interactive features (ShareLinks, clipboard functionality) into separate Client Components

This approach would improve SEO, reduce JavaScript bundle size, and maintain interactivity where needed.

🤖 Prompt for AI Agents
In app/projects/page.tsx around line 1, the file is marked with "use client"
making the whole page a Client Component which harms SEO and increases bundle
size; refactor by removing "use client" so the page becomes a Server Component
for static content (headers, project data fetching and rendering), and move any
interactive pieces (ShareLinks, clipboard handlers, event-driven UI) into
separate Client Components that import "use client" at their top; ensure data
fetching uses server-side APIs or getServerSideProps/async server component
patterns and pass necessary props to the isolated client components to restore
interactivity without converting the whole page to a client bundle.


import Image from "next/image"
import Link from "next/link"
import { Github, Twitter, Globe, Share2, Linkedin, Copy } from "lucide-react"
import { useMemo } from "react"
import { getAbsoluteUrl } from "@/lib/metadata"

interface Project {
id: string
name: string
description: string
logo?: string
links: {
website?: string
github?: string
twitter?: string
}
}

// Temporary project data. In a real app, this could be fetched from an API or a JSON file.
const projects: Project[] = [
{
id: "stability-nexus",
name: "Stability Nexus",
description:
"Research and publications on stability across AI, blockchain, and finance.",
logo: "/StabilityNexus.svg",
links: {
website: "https://stability.nexus",
github: "https://github.com/StabilityNexus",
twitter: "https://x.com/StabilityNexus",
},
},
{
id: "stable-viewpoints",
name: "Stable Viewpoints",
description:
"Digital publication focused on thoughtful perspectives about stability.",
logo: "/stability-nexus-og.png",
links: {
website: "https://viewpoints.stability.nexus",
github: "https://github.com/StabilityNexus/StableViewpoints",
twitter: "https://x.com/StabilityNexus",
},
},
]

function ShareLinks({ project }: { project: Project }) {
const shareTarget = project.links.website || project.links.github || getAbsoluteUrl("/projects")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix circular reference in fallback logic.

The fallback getAbsoluteUrl("/projects") creates a confusing UX where sharing a project without a website or GitHub link shares the projects listing page itself, rather than something meaningful about the specific project.

Consider these alternatives:

  • Omit the share buttons entirely if no website/GitHub link exists
  • Use a project-specific anchor: getAbsoluteUrl(\/projects#${project.id}`)`
  • Show a disabled state with a tooltip explaining why sharing is unavailable

Apply this diff to hide share functionality when no valid URL exists:

 function ShareLinks({ project }: { project: Project }) {
   const shareTarget = project.links.website || project.links.github || getAbsoluteUrl("/projects")
+  
+  // Don't show share links if no meaningful URL exists
+  if (!project.links.website && !project.links.github) {
+    return null
+  }
+  
   const shareText = encodeURIComponent(`${project.name}`)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const shareTarget = project.links.website || project.links.github || getAbsoluteUrl("/projects")
function ShareLinks({ project }: { project: Project }) {
const shareTarget = project.links.website || project.links.github || getAbsoluteUrl("/projects")
// Don't show share links if no meaningful URL exists
if (!project.links.website && !project.links.github) {
return null
}
const shareText = encodeURIComponent(`${project.name}`)
🤖 Prompt for AI Agents
In app/projects/page.tsx around line 50, the fallback to
getAbsoluteUrl("/projects") creates a circular/share-unhelpful URL; change the
logic to only set shareTarget when project.links.website or project.links.github
exist and otherwise treat sharing as unavailable—update component to omit or
render a disabled share UI (with tooltip) when shareTarget is undefined, ensure
shareTarget is computed as project.links.website || project.links.github ||
undefined, and adjust downstream rendering to check for undefined before showing
share buttons so no projects listing URL is used as a fallback.

const shareText = encodeURIComponent(`${project.name}`)
const encodedUrl = encodeURIComponent(shareTarget)

const twitterUrl = `https://twitter.com/intent/tweet?url=${encodedUrl}&text=${shareText}`
const linkedinUrl = `https://www.linkedin.com/sharing/share-offsite/?url=${encodedUrl}`
const facebookUrl = `https://www.facebook.com/sharer/sharer.php?u=${encodedUrl}`

const copyLink = async () => {
try {
await navigator.clipboard.writeText(shareTarget)
// Optional: You could integrate a toast here, but we'll keep it minimal and accessible.
alert("Link copied to clipboard")
} catch (e) {
alert("Unable to copy link")
}
}
Comment on lines +58 to +66
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add clipboard API availability check and improve UX feedback.

The code has two issues:

  1. navigator.clipboard requires HTTPS and may not be available in all contexts (e.g., older browsers, insecure contexts)
  2. Using alert() is disruptive and not recommended for modern web UX

Apply this diff to add availability check:

   const copyLink = async () => {
+    if (!navigator.clipboard) {
+      alert("Clipboard access not available")
+      return
+    }
     try {
       await navigator.clipboard.writeText(shareTarget)

For better UX, consider integrating a toast notification library (like sonner or react-hot-toast) instead of alert(). This would provide non-blocking feedback that's more consistent with modern UI patterns.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const copyLink = async () => {
try {
await navigator.clipboard.writeText(shareTarget)
// Optional: You could integrate a toast here, but we'll keep it minimal and accessible.
alert("Link copied to clipboard")
} catch (e) {
alert("Unable to copy link")
}
}
const copyLink = async () => {
if (!navigator.clipboard) {
alert("Clipboard access not available")
return
}
try {
await navigator.clipboard.writeText(shareTarget)
// Optional: You could integrate a toast here, but we'll keep it minimal and accessible.
alert("Link copied to clipboard")
} catch (e) {
alert("Unable to copy link")
}
}
🤖 Prompt for AI Agents
In app/projects/page.tsx around lines 58 to 66, the copyLink handler assumes
navigator.clipboard is always available and uses blocking alert() calls; update
it to first check for clipboard API availability (e.g., if (navigator.clipboard
&& navigator.clipboard.writeText)) and handle the unsupported case gracefully
(show a non-blocking fallback such as copying via a hidden input/select fallback
or disabling the copy action), replace alert() with a toast/notification
mechanism (integrate a toast library like sonner or react-hot-toast, or use a
small custom non-blocking message component) to show success and error messages,
and ensure errors are caught and logged for debugging while presenting an
accessible, non-disruptive message to the user.


return (
<div className="flex items-center gap-2">
<a
href={twitterUrl}
target="_blank"
rel="noopener noreferrer"
aria-label={`Share ${project.name} on X/Twitter`}
className="inline-flex p-2 rounded-md border bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
>
<Twitter className="w-4 h-4" />
</a>
<a
href={linkedinUrl}
target="_blank"
rel="noopener noreferrer"
aria-label={`Share ${project.name} on LinkedIn`}
className="inline-flex p-2 rounded-md border bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
>
<Linkedin className="w-4 h-4" />
</a>
<a
href={facebookUrl}
target="_blank"
rel="noopener noreferrer"
aria-label={`Share ${project.name} on Facebook`}
className="inline-flex p-2 rounded-md border bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
>
{/* Using Share2 icon to represent Facebook share without adding another icon lib */}
<Share2 className="w-4 h-4" />
</a>
<button
type="button"
onClick={copyLink}
aria-label={`Copy link to ${project.name}`}
className="inline-flex p-2 rounded-md border bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
>
<Copy className="w-4 h-4" />
</button>
</div>
)
}

function ProjectCard({ project }: { project: Project }) {
const logoSrc = useMemo(() => {
if (!project.logo) return "/placeholder-logo.svg"
return project.logo.startsWith("/") ? project.logo : project.logo
}, [project.logo])
Comment on lines +111 to +114
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Simplify redundant useMemo logic.

The ternary operator doesn't perform any meaningful transformation—it returns project.logo unchanged in both branches:

return project.logo.startsWith("/") ? project.logo : project.logo

Apply this diff to simplify:

   const logoSrc = useMemo(() => {
-    if (!project.logo) return "/placeholder-logo.svg"
-    return project.logo.startsWith("/") ? project.logo : project.logo
+    return project.logo || "/placeholder-logo.svg"
   }, [project.logo])

If you intended to prepend "/" to relative paths, the logic should be:

return project.logo.startsWith("/") ? project.logo : `/${project.logo}`
🤖 Prompt for AI Agents
In app/projects/page.tsx around lines 111-114, the useMemo contains a redundant
ternary that returns project.logo unchanged; replace that logic so that if
project.logo is falsy you return the placeholder, otherwise either return
project.logo directly (to keep current behavior) or, if you intended to
normalize relative paths, return project.logo prefixed with a leading slash when
it does not start with one (i.e. check startsWith("/") and prepend "/" only for
relative paths). Ensure the dependency array remains [project.logo].


return (
<article className="flex flex-col h-full bg-white shadow-lg overflow-hidden transition-all duration-300 hover:shadow-2xl hover:-translate-y-2 border border-gradient-to-r from-[#228B22]/10 to-[#FFBF00]/10 relative rounded-lg">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Invalid Tailwind CSS class will not render correctly.

The className contains border-gradient-to-r from-[#228B22]/10 to-[#FFBF00]/10, but border-gradient-to-r is not a valid Tailwind class. Tailwind doesn't support gradient borders directly via utility classes.

To achieve a gradient border effect, consider one of these approaches:

Option 1: Use a wrapper div with gradient background (recommended)

-    <article className="flex flex-col h-full bg-white shadow-lg overflow-hidden transition-all duration-300 hover:shadow-2xl hover:-translate-y-2 border border-gradient-to-r from-[#228B22]/10 to-[#FFBF00]/10 relative rounded-lg">
+    <article className="flex flex-col h-full bg-white shadow-lg overflow-hidden transition-all duration-300 hover:shadow-2xl hover:-translate-y-2 border border-gray-200 relative rounded-lg">

Option 2: Add gradient as a pseudo-element if gradient border is essential

This requires custom CSS or using a wrapper div with padding and gradient background.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<article className="flex flex-col h-full bg-white shadow-lg overflow-hidden transition-all duration-300 hover:shadow-2xl hover:-translate-y-2 border border-gradient-to-r from-[#228B22]/10 to-[#FFBF00]/10 relative rounded-lg">
<article className="flex flex-col h-full bg-white shadow-lg overflow-hidden transition-all duration-300 hover:shadow-2xl hover:-translate-y-2 border border-gray-200 relative rounded-lg">
🤖 Prompt for AI Agents
In app/projects/page.tsx around line 117, the className uses an invalid Tailwind
class `border-gradient-to-r from-[#228B22]/10 to-[#FFBF00]/10`; remove that
invalid class and implement a proper gradient-border pattern instead — wrap this
article in a thin outer wrapper div that has the gradient background (using
`bg-gradient-to-r from-... to-...`) and padding, then move the current white
background, shadow and rounded corners to an inner element so the inner element
shows the white card while the outer wrapper provides the visible gradient
border; alternatively, if you prefer CSS, replace the invalid utility with a
custom class that creates a gradient border via a pseudo-element or
background-clip and apply that class to the wrapper.

<div className="relative w-full p-6 flex items-center gap-4">
<div className="flex-shrink-0">
<Image
src={logoSrc}
alt={`${project.name} logo`}
width={64}
height={64}
className="w-16 h-16 object-contain"
/>
</div>
<div className="flex-grow">
<h2 className="text-xl font-bold text-gray-900 mb-1">{project.name}</h2>
<p className="text-gray-600 text-sm">{project.description}</p>
</div>
</div>

<div className="px-6 pb-6 mt-auto flex flex-col gap-4">
{/* Primary links row */}
<div className="flex flex-wrap items-center gap-3">
{project.links.website && (
<a
href={project.links.website}
target="_blank"
rel="noopener noreferrer"
aria-label={`${project.name} website`}
className="inline-flex items-center gap-2 px-3 py-2 rounded-md border bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
>
<Globe className="w-4 h-4" />
<span className="text-sm">Website</span>
</a>
)}
{project.links.github && (
<a
href={project.links.github}
target="_blank"
rel="noopener noreferrer"
aria-label={`${project.name} on GitHub`}
className="inline-flex items-center gap-2 px-3 py-2 rounded-md border bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
>
<Github className="w-4 h-4" />
<span className="text-sm">GitHub</span>
</a>
)}
{project.links.twitter && (
<a
href={project.links.twitter}
target="_blank"
rel="noopener noreferrer"
aria-label={`${project.name} on X/Twitter`}
className="inline-flex items-center gap-2 px-3 py-2 rounded-md border bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
>
<Twitter className="w-4 h-4" />
<span className="text-sm">X/Twitter</span>
</a>
)}
</div>

{/* Share row */}
<div className="flex items-center justify-between">
<span className="text-xs text-muted-foreground">Share this project</span>
<ShareLinks project={project} />
</div>
</div>
</article>
)
}

export default function ProjectsPage() {
return (
<div className="min-h-screen bg-gradient-to-br from-green-50 via-yellow-50 to-[#FFC517]/10">
{/* Header */}
<header className="border-b border-gradient-to-r from-[#228B22]/20 to-[#FFBF00]/20 bg-white/90 backdrop-blur-sm sticky top-0 z-50 shadow-sm">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Invalid Tailwind CSS class in header border.

Similar to line 117, the className contains border-gradient-to-r from-[#228B22]/20 to-[#FFBF00]/20, which is not valid Tailwind syntax. Gradient borders require custom CSS or alternative approaches.

Apply this diff to use a standard border:

-      <header className="border-b border-gradient-to-r from-[#228B22]/20 to-[#FFBF00]/20 bg-white/90 backdrop-blur-sm sticky top-0 z-50 shadow-sm">
+      <header className="border-b border-gray-200 bg-white/90 backdrop-blur-sm sticky top-0 z-50 shadow-sm">

If you need a gradient border, consider using a box-shadow or a pseudo-element with a gradient background.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<header className="border-b border-gradient-to-r from-[#228B22]/20 to-[#FFBF00]/20 bg-white/90 backdrop-blur-sm sticky top-0 z-50 shadow-sm">
<header className="border-b border-gray-200 bg-white/90 backdrop-blur-sm sticky top-0 z-50 shadow-sm">
🤖 Prompt for AI Agents
In app/projects/page.tsx around line 189, the header's className uses invalid
Tailwind classes "border-gradient-to-r from-[#228B22]/20 to-[#FFBF00]/20";
replace those with a valid standard border utility (for example a solid border
color like border-b border-gray-200 or border-b border-[rgba(34,139,34,0.2)]
using arbitrary color syntax) and remove the gradient border utilities; if you
require a gradient border instead, implement it via a pseudo-element or wrapper
with a gradient background clipped to the border (or use box-shadow) rather than
using the invalid Tailwind gradient border classes.

<div className="max-w-6xl mx-auto px-4 py-6 flex justify-between items-center">
<div>
<h1 className="text-4xl font-bold font-playfair bg-gradient-to-r from-[#228B22] via-[#5A981A] via-[#91A511] via-[#ADAC0D] via-[#E4B905] to-[#FFBF00] bg-clip-text text-transparent drop-shadow-sm leading-tight pb-2">
Projects
</h1>
<p className="text-gray-600 mt-2 text-base">Explore our projects and organizations.</p>
</div>
<Link
href="/"
className="inline-flex items-center gap-2 bg-gradient-to-r from-[#228B22] to-[#91A511] hover:from-[#3E921E] hover:to-[#ADAC0D] text-white px-6 py-3 font-semibold transition-all duration-300 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5"
>
Back to Articles
</Link>
</div>
</header>

{/* Main Content */}
<main className="max-w-6xl mx-auto px-4 py-12">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8">
{projects.map((p) => (
<ProjectCard key={p.id} project={p} />
))}
</div>
</main>
</div>
)
}