diff --git a/eslint.config.mjs b/eslint.config.mjs index 08fe102..f311531 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -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", @@ -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)/**", diff --git a/src/app/(frontend)/projects/page.tsx b/src/app/(frontend)/projects/page.tsx index 361a632..8f9af90 100644 --- a/src/app/(frontend)/projects/page.tsx +++ b/src/app/(frontend)/projects/page.tsx @@ -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({ @@ -29,24 +30,7 @@ const Projects = async () => {

Stuff I've built, including this website!

-
- {posts.docs.length > 0 - ? posts.docs.map((post) => ( - - )) - :

no projects found...

- } -
+ diff --git a/src/components/MouseTooltip.tsx b/src/components/MouseTooltip.tsx new file mode 100644 index 0000000..c61e5c8 --- /dev/null +++ b/src/components/MouseTooltip.tsx @@ -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 ( +
+ {children} +
+ ); +}; + +export default MouseTooltip; diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index b7780dc..d4611c5 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -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 ( ); diff --git a/src/components/ProjectCard.tsx b/src/components/ProjectCard.tsx index 5aa40e1..195c00b 100644 --- a/src/components/ProjectCard.tsx +++ b/src/components/ProjectCard.tsx @@ -1,8 +1,31 @@ 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 (
{children} @@ -10,25 +33,62 @@ const ProjectTag = ({ children }:{children?: ReactNode}) => { ); }; -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 (
- {children} + {icon} + {linkText}
); }; return ( - +
- {children} {newTab && } + {newTab && }
); }; -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: , newTab: true }, + { type: LinkType.Demo, url: demoLink, icon: , newTab: true }, + { type: LinkType.Repo, url: repoLink, name: "code", icon: , newTab: true }, + { type: LinkType.Design, url: designLink, icon: , newTab: true }, + { type: LinkType.Paper, url: paperLink, icon: , newTab: true }, + ]; + return (
@@ -42,36 +102,17 @@ const ProjectCard = ({ title, description, repoLink, demoLink, videoLink, design )}

{description}

- {videoLink && - - - video - - } - {demoLink && - - - demo - - } - {repoLink && - - - code - - } - {designLink && - - - design - - } - {paperLink && - - - paper - - } + {projectCardLinks.map(link => + link.url && + + )}
diff --git a/src/components/ProjectCards.tsx b/src/components/ProjectCards.tsx new file mode 100644 index 0000000..2a84669 --- /dev/null +++ b/src/components/ProjectCards.tsx @@ -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 ( + <> +
+ {projects.docs.length > 0 + ? projects.docs.map((project) => ( + + )) + :

no projects found...

+ } +
+ {tooltipText} + + ); +}; + +export default ProjectCards; diff --git a/src/models/common.types.d.ts b/src/models/common.types.d.ts new file mode 100644 index 0000000..ccf16f2 --- /dev/null +++ b/src/models/common.types.d.ts @@ -0,0 +1,7 @@ +import { ReactElement, ReactNode } from "react"; + +export interface IBaseChildrenProps { + children?: ReactNode | ReactElement | string, +} + +export type NullableString = string | null; diff --git a/src/models/index.ts b/src/models/index.ts new file mode 100644 index 0000000..90195af --- /dev/null +++ b/src/models/index.ts @@ -0,0 +1,2 @@ +export * from "./common.types"; +export * from "./mouse-tooltip.types"; diff --git a/src/models/mouse-tooltip.types.d.ts b/src/models/mouse-tooltip.types.d.ts new file mode 100644 index 0000000..c23b4ca --- /dev/null +++ b/src/models/mouse-tooltip.types.d.ts @@ -0,0 +1,4 @@ +export interface IMouseTooltipHandler { + updateText: (_text: string) => void, + updateVisibility: (_visibility: boolean) => void, +} diff --git a/src/styles/components/mouse-tooltip.css b/src/styles/components/mouse-tooltip.css new file mode 100644 index 0000000..b167aac --- /dev/null +++ b/src/styles/components/mouse-tooltip.css @@ -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]; +} diff --git a/src/styles/components/navbar.css b/src/styles/components/navbar.css index fc08e37..0f11a0b 100644 --- a/src/styles/components/navbar.css +++ b/src/styles/components/navbar.css @@ -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; } @@ -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 { diff --git a/src/styles/components/project-card.css b/src/styles/components/project-card.css index 71bec46..d1e266f 100644 --- a/src/styles/components/project-card.css +++ b/src/styles/components/project-card.css @@ -6,7 +6,8 @@ @apply py-6 px-8 rounded-2xl mb-4 bg-orange-300 bg-opacity-30 transition-all shadow-[inset_0_0_0_0_#9a3412,0_2px_4px_0_#9a3412] hover:shadow-[inset_0_2px_4px_1px_#9a3412,0_0_0_0_#9a3412] - hover:bg-opacity-45; + hover:bg-opacity-45 + ; .project-card-title { @apply text-orange-800 dark:text-orange-300; @@ -44,6 +45,7 @@ @apply transition-shadow shadow-[inset_0_0_0_0_black,0_2px_4px_0_black] hover:shadow-[inset_0_2px_4px_1px_black,0_0_0_0_black] - hover:bg-opacity-15; + hover:bg-opacity-15 + ; } } diff --git a/src/styles/index.css b/src/styles/index.css index 7950503..815d5c9 100644 --- a/src/styles/index.css +++ b/src/styles/index.css @@ -11,3 +11,4 @@ @import "components/footer.css"; @import "components/contact-links.css"; @import "components/project-card.css"; +@import "components/mouse-tooltip.css";