source-code/
portofolio-neo-gruv
Public
codeCodeinfoIssues 0call_splitPull Requestsplay_circleActions
portofolio-neo-gruv/src/sections/work/ProjectCard.tsx
typescript282 lines13.9 KB
import React from 'react';
import Link from 'next/link';
import type { Project } from '../../types';
import { formatImageUrl } from '../../utils/image';

const TAG_COLORS = [
    'bg-theme-red text-surface-container-lowest',
    'bg-theme-blue text-surface-container-lowest',
    'bg-theme-green text-on-surface',
    'bg-theme-yellow text-on-surface'
];

interface ProjectCardProps {
    project: Project;
    index: number;
    isActive: boolean;
    isVisible: boolean;
    offset: number;
    isRevealed?: boolean;
    onExpand: () => void;
}

export const ProjectCard: React.FC<ProjectCardProps> = ({
    project,
    index,
    isActive,
    isVisible,
    offset,
    isRevealed = true,
    onExpand,
}) => {
    // Pseudo-3D mapping for carousel slots (-2, -1, 0, 1, 2)
    const getCardLayout = (off: number) => {
        switch (off) {
            case 0:
                return { x: 0, scale: 1, zIndex: 30, opacity: 1, blur: 'none' };
            case 1:
                return { x: 105, scale: 0.85, zIndex: 20, opacity: 0.9, blur: 'none' };
            case -1:
                return { x: -105, scale: 0.85, zIndex: 20, opacity: 0.9, blur: 'none' };
            case 2:
                return { x: 52.5, scale: 0.72, zIndex: 10, opacity: 0.45, blur: 'blur(1.5px)' };
            case -2:
                return { x: -52.5, scale: 0.72, zIndex: 10, opacity: 0.45, blur: 'blur(1.5px)' };
            default:
                return { x: off > 0 ? 120 : -120, scale: 0.5, zIndex: 5, opacity: 0, blur: 'blur(4px)' };
        }
    };

    const getRevealClass = (off: number) => {
        if (off < 0) return 'reveal-left';
        if (off > 0) return 'reveal-right';
        return 'reveal-bottom';
    };

    const getRevealDelay = (off: number) => {
        const abs = Math.abs(off);
        if (abs === 0) return '0.1s';
        if (abs === 1) return '0.25s';
        return '0.4s';
    };

    const layout = getCardLayout(offset);
    const revealClass = getRevealClass(offset);
    const revealDelay = getRevealDelay(offset);
    const imageSrc = formatImageUrl(project.coverImage || project.image?.src || '');
 
    if (project.isAllProjectsCard) {
        return (
            <div
                className="absolute w-full max-w-[420px] transition-all duration-500 ease-[cubic-bezier(0.175,0.885,0.32,1.275)]"
                style={{
                    transform: `translateX(${layout.x}%) scale(${layout.scale})`,
                    opacity: isVisible ? layout.opacity : 0,
                    zIndex: layout.zIndex,
                    filter: layout.blur === 'none' ? 'none' : layout.blur,
                    pointerEvents: isActive ? 'auto' : 'none',
                }}
            >
                <div 
                    className={[
                        'group relative',
                        revealClass,
                        isRevealed ? 'reveal-visible' : ''
                    ].join(' ')}
                    style={{ transitionDelay: revealDelay }}
                >
                    {/* Neo shadow behind card */}
                    <div
                        className={[
                            'absolute inset-0 neo-border-heavy translate-x-3 translate-y-3 z-0 transition-all duration-500 ease-[cubic-bezier(0.175,0.885,0.32,1.275)]',
                            isActive
                                ? 'bg-theme-yellow'
                                : 'bg-on-surface/30',
                        ].join(' ')}
                    />
                    {/* Card Body */}
                    <div
                        className={[
                            'relative z-10 neo-border-heavy overflow-hidden flex flex-col transition-all duration-500 h-[380px]',
                            isActive
                                ? 'bg-theme-blue text-surface-container-lowest animate-float-vertical'
                                : 'bg-surface-dim text-on-surface/50',
                        ].join(' ')}
                        style={isActive ? { animationDelay: `${index * 0.4}s` } : {}}
                    >
                        <div className="flex-grow flex flex-col justify-center items-center p-8 text-center">
                            <span className="material-symbols-outlined text-[64px] mb-4 animate-float text-theme-yellow">folder_open</span>
                            <h3 className="font-display-2xl text-[28px] uppercase font-bold tracking-tighter mb-2">
                                ALL PROJECTS
                            </h3>
                            <p className="font-body-md font-bold mb-6 text-sm max-w-[280px]">
                                Explore the complete vault of web breakings, custom APIs, and source repositories.
                            </p>
                            {isActive && (
                                <button
                                    onClick={onExpand}
                                    className="bg-theme-yellow text-on-surface font-label-bold uppercase px-6 py-3 neo-border border-[3px] shadow-[4px_4px_0px_0px_#1e1b19] hover:shadow-none hover:translate-x-[2px] hover:translate-y-[2px] transition-all duration-200"
                                >
                                    SHOW ALL
                                </button>
                            )}
                        </div>
                    </div>
                </div>
            </div>
        );
    }

    return (
        <div
            className="absolute w-full max-w-[420px] transition-all duration-500 ease-[cubic-bezier(0.175,0.885,0.32,1.275)]"
            style={{
                transform: `translateX(${layout.x}%) scale(${layout.scale})`,
                opacity: isVisible ? layout.opacity : 0,
                zIndex: layout.zIndex,
                filter: layout.blur === 'none' ? 'none' : layout.blur,
                pointerEvents: isActive ? 'auto' : 'none',
            }}
        >
            <div 
                className={[
                    'group relative',
                    revealClass,
                    isRevealed ? 'reveal-visible' : ''
                ].join(' ')}
                style={{ transitionDelay: revealDelay }}
            >
                {/* Neo shadow behind card */}
                <div
                    className={[
                        'absolute inset-0 neo-border-heavy translate-x-3 translate-y-3 z-0 transition-all duration-500 ease-[cubic-bezier(0.175,0.885,0.32,1.275)]',
                        isActive
                            ? 'bg-theme-yellow'
                            : 'bg-on-surface/30',
                    ].join(' ')}
                />
                {/* Card */}
                <div
                    className={[
                        'relative z-10 neo-border-heavy overflow-hidden flex flex-col transition-all duration-500',
                        isActive
                            ? 'bg-theme-grey animate-float-vertical'
                            : 'bg-surface-dim',
                    ].join(' ')}
                    style={isActive ? { animationDelay: `${index * 0.4}s` } : {}}
                >
                    {project.featured && (
                        <div className="absolute top-4 left-4 z-20 font-label-bold text-[10px] md:text-xs uppercase bg-theme-red text-surface-container-lowest px-3 py-1 neo-border border-[3px] shadow-[2px_2px_0px_0px_#1e1b19]">
                            Featured
                        </div>
                    )}
                    {/* Image */}
                    {imageSrc && (
                        <div className="h-40 overflow-hidden border-b-[6px] border-on-surface relative">
                            <div className="absolute inset-0 bg-on-surface opacity-0 group-hover:opacity-20 transition-opacity z-10 mix-blend-overlay" />
                            <img
                                alt={project.title}
                                className="w-full h-full object-cover transition-all duration-700 filter-none group-hover:scale-110"
                                src={imageSrc}
                            />
                        </div>
                    )}
                    {/* Content */}
                    <div
                        className={[
                            'p-5 flex-grow flex flex-col justify-between transition-all duration-500',
                            isActive ? 'bg-surface' : 'bg-surface-dim',
                            Math.abs(offset) > 1 ? 'opacity-0 pointer-events-none' : 'opacity-100'
                        ].join(' ')}
                    >
                        <div>
                            <div className="flex gap-2 mb-4 flex-wrap">
                                {project.tags.slice(0, 2).map((tag, tagIndex) => {
                                    const colorClass = TAG_COLORS[tagIndex % TAG_COLORS.length];
                                    return (
                                        <span
                                            key={tagIndex}
                                            className={[
                                                'px-3 py-1 neo-border border-[3px] text-xs font-label-bold uppercase transition-colors duration-500',
                                                isActive
                                                    ? colorClass
                                                    : 'bg-on-surface/20 text-on-surface/50',
                                            ].join(' ')}
                                        >
                                            {tag}
                                        </span>
                                    );
                                })}
                                {project.tags.length > 2 && (
                                    <span
                                        className={[
                                            'px-3 py-1 neo-border border-[3px] text-xs font-label-bold uppercase transition-colors duration-500',
                                            isActive
                                                ? 'bg-surface-variant text-on-surface'
                                                : 'bg-on-surface/20 text-on-surface/50',
                                        ].join(' ')}
                                    >
                                        +{project.tags.length - 2}
                                    </span>
                                )}
                            </div>
                            <h3
                                className={[
                                    'font-display-2xl text-2xl md:text-[28px] leading-tight font-extrabold uppercase mb-2 transition-colors duration-500',
                                    isActive ? 'text-on-surface' : 'text-on-surface/50',
                                ].join(' ')}
                            >
                                {project.title}
                            </h3>
                            <p
                                className={[
                                    'font-body-md font-bold border-t-[3px] border-on-surface pt-3 mt-1.5 transition-colors duration-500',
                                    isActive ? 'text-on-surface-variant' : 'text-on-surface-variant/50',
                                ].join(' ')}
                            >
                                {project.brief}
                            </p>
                        </div>

                        {/* Expand & Action buttons */}
                        {isActive && (
                            <div className="flex gap-2 mt-5">
                                <button
                                    onClick={onExpand}
                                    className="h-11 flex-1 font-label-bold uppercase text-[10px] md:text-xs bg-theme-yellow text-on-surface px-2 md:px-3 neo-border shadow-[2px_2px_0px_0px_#1e1b19] hover:shadow-none hover:translate-x-[2px] hover:translate-y-[2px] transition-all duration-200 inline-flex items-center justify-center gap-1"
                                >
                                    <span className="material-symbols-outlined text-sm">open_in_full</span>
                                    Details
                                </button>
                                
                                {project.hasSourceCode && project.githubRepo && (
                                    <Link
                                        href={`/source-code/${project.githubRepo}`}
                                        className="h-11 w-11 flex-shrink-0 bg-on-surface text-surface neo-border flex items-center justify-center shadow-[2px_2px_0px_0px_#1e1b19] hover:shadow-none hover:translate-x-[2px] hover:translate-y-[2px] transition-all duration-200"
                                        title="Source Code"
                                    >
                                        <span className="material-symbols-outlined text-base">code</span>
                                    </Link>
                                )}

                                {project.liveUrl && (
                                    <a
                                        href={project.liveUrl}
                                        target="_blank"
                                        rel="noopener noreferrer"
                                        className="h-11 flex-1 font-label-bold uppercase text-[10px] md:text-xs bg-theme-blue text-surface px-2 md:px-3 neo-border shadow-[2px_2px_0px_0px_#1e1b19] hover:shadow-none hover:translate-x-[2px] hover:translate-y-[2px] transition-all duration-200 inline-flex items-center justify-center gap-1"
                                        title="Live Demo"
                                    >
                                        <span className="material-symbols-outlined text-sm">public</span>
                                        Live Demo
                                    </a>
                                )}
                            </div>
                        )}
                    </div>
                </div>
            </div>
        </div>
    );
};

About

Custom portfolio frontend designed using retro Neo-Brutalist styling. Features server-rendered pages, persistent codebase layout, interactive file explorer tree, and Shiki code syntax highlighting.

TypeScriptNext.jsReact 19Tailwind CSSShiki

Contributors

1