Skip to content
Merged
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
6 changes: 4 additions & 2 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ const eslintConfig = [
rules: {
"indent": ["error", 2],
"eol-last": ["error", "always"],
"no-unused-vars": ["error", {
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["error", {
"argsIgnorePattern": "^_",
}],
"no-trailing-spaces": "error",
Expand All @@ -27,7 +28,8 @@ const eslintConfig = [
"object-curly-spacing": ["error", "always"],
"array-bracket-spacing": ["error", "never"],
"quotes": ["error", "double"],
"jsx-quotes": ["error", "prefer-double"]
"jsx-quotes": ["error", "prefer-double"],
"@typescript-eslint/no-empty-object-type": "off"
},
ignores: [
"src/app/(payload)/**",
Expand Down
22 changes: 3 additions & 19 deletions src/app/(frontend)/projects/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import Navbar from "@/components/Navbar";
import Footer from "@/components/Footer";
import { cmsClient } from "@/services/cms-client";
import ProjectCard from "@/components/ProjectCard";
import React from "react";
import ProjectCards from "@/components/ProjectCards";

const Projects = async () => {
const posts = await cmsClient.find({
Expand Down Expand Up @@ -29,24 +30,7 @@ const Projects = async () => {
<p>
Stuff I&apos;ve built, including this website!
</p>
<div className="projects-container">
{posts.docs.length > 0
? posts.docs.map((post) => (
<ProjectCard
key={post.id}
title={post.content.title}
description={post.content.description}
repoLink={post.content.repoLink}
demoLink={post.content.demoLink}
videoLink={post.content.videoLink}
designLink={post.content.designLink}
paperLink={post.content.paperLink}
techStack={post.metadata.tags}
/>
))
: <p>no projects found...</p>
}
</div>
<ProjectCards projects={posts}/>
</section>
</main>

Expand Down
38 changes: 38 additions & 0 deletions src/components/MouseTooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"use client";

import { useEffect, useState } from "react";
import { IBaseChildrenProps } from "@/models";

interface IMouseTooltipProps extends IBaseChildrenProps {
show?: boolean
}

const MouseTooltip = ({ children, show }: IMouseTooltipProps) => {
const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
const [scrollPos, setScrollPos] = useState({ x: 0, y: 0 });
const tooltipPos = { x: mousePos.x + scrollPos.x, y: mousePos.y + scrollPos.y };

useEffect(() => {
const handleMouseMove = (event: MouseEvent) => setMousePos({ x: event.clientX, y: event.clientY });
const handleScrollMove = () => setScrollPos({ x: window.scrollX, y: window.scrollY });

window.addEventListener("mousemove", handleMouseMove);
window.addEventListener("scroll", handleScrollMove);

return () => {
window.removeEventListener("mousemove", handleMouseMove);
window.removeEventListener("scroll", handleScrollMove);
};
}, []);


return (
<div className={`mouse-tooltip ${show ? "hide-on-mobile" : "hidden"}`}
style={{ left: `${tooltipPos.x}px`, top: `${tooltipPos.y}px` }}
>
{children}
</div>
);
};

export default MouseTooltip;
5 changes: 1 addition & 4 deletions src/components/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,7 @@ const NavbarLink = ({ children, className = "", link = "", newTab } : {children?
const Navbar = () => {
const [isBurgerOpened, setIsBurgerOpened] = useState(false);

const handleBurgerClick = () => {
setIsBurgerOpened(!isBurgerOpened);
};
const handleBurgerClick = () => setIsBurgerOpened(!isBurgerOpened);

return (
<nav className="navbar">
Expand All @@ -35,7 +33,6 @@ const Navbar = () => {
<li><NavbarLink className="projects-nav-link" link="/projects">projects</NavbarLink></li>
{/*<li><NavbarButton link="/blogs">blogs</NavbarButton></li>*/}
<li><NavbarLink link="/contact">contact</NavbarLink></li>
{/* <li><NavbarLink newTab link="/silvia-resume.pdf">resume</NavbarLink></li> */}
</ul>
</nav>
);
Expand Down
117 changes: 79 additions & 38 deletions src/components/ProjectCard.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,94 @@
import { IconBrandYoutube, IconBrush, IconCode, IconExternalLink, IconFile, IconPlayerPlay } from "@tabler/icons-react";
import { ReactNode } from "react";
import React, { ReactNode } from "react";
import Link from "next/link";
import { IBaseChildrenProps, IMouseTooltipHandler, NullableString } from "@/models";

const ProjectTag = ({ children }:{children?: ReactNode}) => {
interface IProjectTagProps extends IBaseChildrenProps {}

interface IProjectLinkProps {
link: string,
linkText: string,
icon?: ReactNode,
newTab?: true,
tooltipHandler?: IMouseTooltipHandler,
}

interface IProjectCardProps {
title: string,
description?: NullableString,
repoLink?: NullableString,
demoLink?: NullableString,
videoLink?: NullableString,
designLink?: NullableString,
paperLink?: NullableString,
techStack?: { id?: NullableString, tag?: NullableString }[] | null,
tooltipHandler?: IMouseTooltipHandler
}

const ProjectTag = ({ children }: IProjectTagProps) => {
return (
<div className="project-card-tag">
{children}
</div>
);
};

const ProjectLink = ({ children, link, newTab } : {children?: ReactNode, link: string, newTab?: true}) => {
const LinkContent = ({ children }:{children?: ReactNode}) => {
const ProjectLink = ({ link, linkText, icon, newTab, tooltipHandler }: IProjectLinkProps) => {
const handleShowTooltip = () => {
tooltipHandler?.updateVisibility(true);
tooltipHandler?.updateText(`${linkText}` || "view");
};

const handleHideTooltip = () => {
tooltipHandler?.updateVisibility(false);
};

const LinkContent = () => {
return (
<div className="project-card-link-content">
{children}
{icon}
<span className="hide-on-desktop">{linkText}</span>
</div>
);
};

return (
<Link href={link} target={newTab ? "_blank" : "_self"} className="project-card-link">
<Link href={link} target={newTab ? "_blank" : "_self"} className="project-card-link"
onMouseEnter={handleShowTooltip} onMouseLeave={handleHideTooltip}
>
<div className="project-card-link-content-container">
<LinkContent>{children}</LinkContent> {newTab && <IconExternalLink className="w-4 h-4 hide-on-mobile" />}
<LinkContent /> {newTab && <IconExternalLink className="w-4 h-4 hide-on-mobile" />}
</div>
</Link>
);
};

const ProjectCard = ({ title, description, repoLink, demoLink, videoLink, designLink, paperLink, techStack }: {title: string, description?: string | null, repoLink?: string | null, demoLink?: string | null, videoLink?: string | null, designLink?: string | null, paperLink?: string | null, techStack?: { id?: string | null, tag?: string | null }[] | null }) => {

const ProjectCard = ({ title, description, repoLink, demoLink, videoLink, designLink, paperLink, techStack, tooltipHandler }: IProjectCardProps) => {
enum LinkType {
Video = "video",
Demo = "demo",
Repo = "repo",
Design = "design",
Paper = "paper"
}

interface IProjectCardLink {
type: LinkType,
icon: ReactNode
newTab?: true,
name?: string,
url?: NullableString,
}

const projectCardLinks: IProjectCardLink[] = [
{ type: LinkType.Video, url: videoLink, icon: <IconBrandYoutube />, newTab: true },
{ type: LinkType.Demo, url: demoLink, icon: <IconPlayerPlay />, newTab: true },
{ type: LinkType.Repo, url: repoLink, name: "code", icon: <IconCode />, newTab: true },
{ type: LinkType.Design, url: designLink, icon: <IconBrush />, newTab: true },
{ type: LinkType.Paper, url: paperLink, icon: <IconFile />, newTab: true },
];

return (
<div className="project-card">
<div className="project-card-content">
Expand All @@ -42,36 +102,17 @@ const ProjectCard = ({ title, description, repoLink, demoLink, videoLink, design
)}
<p>{description}</p>
<div className="project-card-links">
{videoLink &&
<ProjectLink link={videoLink} newTab>
<IconBrandYoutube />
<span className="hide-on-desktop">video</span>
</ProjectLink>
}
{demoLink &&
<ProjectLink link={demoLink} newTab>
<IconPlayerPlay />
<span className="hide-on-desktop">demo</span>
</ProjectLink>
}
{repoLink &&
<ProjectLink link={repoLink} newTab>
<IconCode />
<span className="hide-on-desktop">code</span>
</ProjectLink>
}
{designLink &&
<ProjectLink link={designLink} newTab>
<IconBrush />
<span className="hide-on-desktop">design</span>
</ProjectLink>
}
{paperLink &&
<ProjectLink link={paperLink} newTab>
<IconFile />
<span className="hide-on-desktop">paper</span>
</ProjectLink>
}
{projectCardLinks.map(link =>
link.url &&
<ProjectLink
key={link.url}
link={link.url}
linkText={link.name || link.type}
icon={link.icon}
tooltipHandler={tooltipHandler}
newTab={link.newTab}
/>
)}
</div>
</div>
</div>
Expand Down
45 changes: 45 additions & 0 deletions src/components/ProjectCards.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"use client";

import MouseTooltip from "@/components/MouseTooltip";
import React, { useState } from "react";
import ProjectCard from "@/components/ProjectCard";
import { PaginatedDocs } from "payload";
import { IMouseTooltipHandler } from "@/models";


const ProjectCards = ({ projects }: { projects: PaginatedDocs }) => {
const [tooltipText, setTooltipText] = useState("i like kway teow");
const [isTooltipShow, setTooltipShow] = useState(false);

const tooltipHandler: IMouseTooltipHandler = {
updateText: (text: string) => setTooltipText(text),
updateVisibility: (show: boolean) => setTooltipShow(show),
};

return (
<>
<div className="projects-container">
{projects.docs.length > 0
? projects.docs.map((project) => (
<ProjectCard
key={project.id}
title={project.content.title}
description={project.content.description}
repoLink={project.content.repoLink}
demoLink={project.content.demoLink}
videoLink={project.content.videoLink}
designLink={project.content.designLink}
paperLink={project.content.paperLink}
techStack={project.metadata.tags}
tooltipHandler={tooltipHandler}
/>
))
: <p>no projects found...</p>
}
</div>
<MouseTooltip show={isTooltipShow}>{tooltipText}</MouseTooltip>
</>
);
};

export default ProjectCards;
7 changes: 7 additions & 0 deletions src/models/common.types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { ReactElement, ReactNode } from "react";

export interface IBaseChildrenProps {
children?: ReactNode | ReactElement | string,
}

export type NullableString = string | null;
2 changes: 2 additions & 0 deletions src/models/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./common.types";
export * from "./mouse-tooltip.types";
4 changes: 4 additions & 0 deletions src/models/mouse-tooltip.types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface IMouseTooltipHandler {
updateText: (_text: string) => void,
updateVisibility: (_visibility: boolean) => void,
}
16 changes: 16 additions & 0 deletions src/styles/components/mouse-tooltip.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
.mouse-tooltip {
@apply absolute z-50 -ml-6 mt-10
drop-shadow-[0px_0px_4px_rgba(124,45,18,0.5)]
bg-orange-100 px-4 py-2 rounded-lg
dark:bg-orange-900 dark:drop-shadow-[0px_0px_4px_rgba(0,0,0,0.8)];
}

.mouse-tooltip:before {
content: "";
@apply absolute border-solid border-[12px]
border-b-orange-100 dark:border-b-orange-900
border-r-transparent
border-l-transparent
border-t-transparent
top-[-22px];
}
14 changes: 9 additions & 5 deletions src/styles/components/navbar.css
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@
}

.navbar .logo .burger-menu {
@apply font-semibold md:hidden;
@apply font-semibold md:hidden
transition-colors ease-in-out duration-200;

&:hover,
/*&:hover,*/
&.opened {
@apply text-red-600 dark:text-red-300;
}
Expand All @@ -28,18 +29,21 @@
transition-[max-height,margin-top] duration-300;

&:not(&.opened) {
@apply h-0 max-h-0 md:h-full md:max-h-full;
@apply h-0 md:h-full
max-h-0 md:max-h-full
transition-[max-height,margin-top] duration-300;
}

&.opened {
@apply max-h-60 mt-4
@apply h-full
max-h-60 mt-4
md:max-h-full md:mt-0
transition-[max-height,margin-top] duration-300;
}

a.menu-link {
@apply flex gap-1 py-2 px-4 rounded-full
transition ease-in-out duration-200 font-semibold;
transition-colors ease-in-out duration-200 font-semibold;

&.active,
&:hover {
Expand Down
Loading