diff --git a/app/about/page.tsx b/app/about/page.tsx index bcfd410..069e2f1 100644 --- a/app/about/page.tsx +++ b/app/about/page.tsx @@ -1,13 +1,12 @@ import React from "react"; -import Base from "../components/layouts/base"; -import ProfileHeader from "../components/ui/header"; -import ContainerLayout from "../components/layouts/container"; +import Base from "@components/layouts/base"; +import ContainerLayout from "@components/layouts/container"; import InProgressImage from "../../public/aboutme-option12.webp"; import Image from "next/image"; const page = () => { return ( - +

About Me diff --git a/app/components/layouts/base.tsx b/app/components/layouts/base.tsx index 15b90e5..b53aed6 100644 --- a/app/components/layouts/base.tsx +++ b/app/components/layouts/base.tsx @@ -1,17 +1,18 @@ import React, { ReactNode } from "react"; -import NavBar from "../ui/nav-bar"; +import SeoBase from "@components/layouts/seo-base"; +import NavBar from "@components/ui/nav-bar/nav-bar"; -interface Props { - children: ReactNode; -} - -const Base = ({ children }: Props) => { +export default function Base({ + title, + subtitle, + pageSlug, + children, +}: BaseLayoutProps) { return ( <> - + +
{children}
); -}; - -export default Base; +} diff --git a/app/components/layouts/seo-base.tsx b/app/components/layouts/seo-base.tsx new file mode 100644 index 0000000..ac82367 --- /dev/null +++ b/app/components/layouts/seo-base.tsx @@ -0,0 +1,26 @@ +import Head from "next/head"; + +// ----------------------------------------------------------------------------- +// Component +// ----------------------------------------------------------------------------- + +export default function SeoBase({ + title = "Kent Miguel", + subtitle, +}: SeoBaseProps) { + const fullTitle = + subtitle && subtitle.length > 0 ? `${title} | ${subtitle}` : title; + + return ( + + {/* + + + */} + + {fullTitle} + + + + ); +} diff --git a/app/components/ui/button/index.tsx b/app/components/ui/button/index.tsx new file mode 100644 index 0000000..d580043 --- /dev/null +++ b/app/components/ui/button/index.tsx @@ -0,0 +1,77 @@ +import Link from 'next/link'; +import { type ForwardedRef, forwardRef } from 'react'; + +import { buttonIconVariants, buttonVariants } from './styles'; +import type { ButtonProps } from './types'; +import { cx } from 'class-variance-authority'; +import { twMerge } from 'tailwind-merge'; + +const Button = forwardRef( + ( + { + className, + size = 'md', + variant = 'primary', + intent = 'none', + disabled = false, + href, + leftIcon, + rightIcon, + newTab, + title, + onClick, + children, + ...rest + }: ButtonProps, + ref: ForwardedRef, + ) => { + const props = { + className: twMerge( + cx( + buttonVariants({ size, variant, intent: !disabled ? intent : undefined, disabled }), + className, + ), + ), + title: title || href || undefined, + 'data-variant': variant, + 'data-disabled': disabled, + 'aria-disabled': disabled, + disabled, + ref, + onClick: newTab && href ? () => window.open(href, '_blank') : onClick, + ...rest, + }; + + if (href && !newTab) { + return ( + + + + ); + } + + return ( + + ); + }, +); + +Button.displayName = 'Button'; + +export default Button; diff --git a/app/components/ui/button/styles.tsx b/app/components/ui/button/styles.tsx new file mode 100644 index 0000000..dcfeec2 --- /dev/null +++ b/app/components/ui/button/styles.tsx @@ -0,0 +1,206 @@ +import { cva } from 'class-variance-authority'; + +export const buttonIconVariants = cva(['flex', 'items-center', 'justify-center'], { + variants: { + size: { + sm: ['w-3', 'h-3'], + md: ['w-4', 'h-4'], + lg: ['w-4', 'h-4'], + xl: ['w-5', 'h-5'], + }, + }, +}); + +export const buttonVariants = cva( + [ + 'rounded', + 'w-fit', + 'flex', + 'justify-center', + 'items-center', + 'font-medium', + 'transition-colors', + 'focus-visible:outline-none', + 'focus-visible:ring-2', + 'focus-visible:ring-blue-9', + ], + { + variants: { + size: { + sm: ['px-2', 'h-6', 'text-xs', 'space-x-1'], + md: ['px-3', 'h-8', 'text-sm', 'space-x-1.5'], + lg: ['px-3', 'h-10', 'text-sm', 'space-x-1.5'], + xl: ['px-4', 'h-12', 'text-base', 'space-x-2'], + }, + variant: { + primary: ['border'], + secondary: [], + outline: ['border'], + ghost: [], + text: ['hover:underline'], + }, + intent: { + none: [ + // Primary + 'data-[variant=primary]:bg-gray-4', + 'data-[variant=primary]:text-gray-12', + 'data-[variant=primary]:border-gray-7', + 'data-[variant=primary]:hover:border-gray-8', + 'data-[variant=primary]:active:bg-gray-5', + // Secondary + 'data-[variant=secondary]:bg-gray-4', + 'data-[variant=secondary]:text-gray-11', + 'data-[variant=secondary]:hover:bg-gray-5', + 'data-[variant=secondary]:active:bg-gray-6', + // Outline + 'data-[variant=outline]:text-gray-11', + 'data-[variant=outline]:border-gray-7', + 'data-[variant=outline]:hover:border-gray-8', + 'data-[variant=outline]:active:bg-gray-3', + // Ghost + 'data-[variant=ghost]:text-gray-11', + 'data-[variant=ghost]:hover:bg-gray-4', + 'data-[variant=ghost]:active:bg-gray-5', + // Text + 'data-[variant=text]:text-gray-11', + ], + primary: [ + // Primary + 'data-[variant=primary]:bg-blue-9', + 'data-[variant=primary]:dark:text-gray-12', + 'data-[variant=primary]:text-gray-1', + 'data-[variant=primary]:border-blue-7', + 'data-[variant=primary]:hover:border-blue-8', + 'data-[variant=primary]:active:bg-blue-10', + // Secondary + 'data-[variant=secondary]:bg-blue-4', + 'data-[variant=secondary]:text-blue-11', + 'data-[variant=secondary]:hover:bg-blue-5', + 'data-[variant=secondary]:active:bg-blue-6', + // Outline + 'data-[variant=outline]:text-blue-9', + 'data-[variant=outline]:border-blue-7', + 'data-[variant=outline]:hover:border-blue-8', + 'data-[variant=outline]:active:bg-blue-3', + // Ghost + 'data-[variant=ghost]:text-blue-9', + 'data-[variant=ghost]:hover:bg-blue-4', + 'data-[variant=ghost]:active:bg-blue-5', + // Text + 'data-[variant=text]:text-blue-9', + ], + success: [ + // Primary + 'data-[variant=primary]:bg-green-9', + 'data-[variant=primary]:dark:text-gray-12', + 'data-[variant=primary]:text-gray-1', + 'data-[variant=primary]:border-green-7', + 'data-[variant=primary]:hover:border-green-8', + 'data-[variant=primary]:active:bg-green-10', + // Secondary + 'data-[variant=secondary]:bg-green-4', + 'data-[variant=secondary]:text-green-11', + 'data-[variant=secondary]:hover:bg-green-5', + 'data-[variant=secondary]:active:bg-green-6', + // Outline + 'data-[variant=outline]:text-green-9', + 'data-[variant=outline]:border-green-7', + 'data-[variant=outline]:hover:border-green-8', + 'data-[variant=outline]:active:bg-green-3', + // Ghost + 'data-[variant=ghost]:text-green-9', + 'data-[variant=ghost]:hover:bg-green-4', + 'data-[variant=ghost]:active:bg-green-5', + // Text + 'data-[variant=text]:text-green-9', + ], + fail: [ + // Primary + 'data-[variant=primary]:bg-red-9', + 'data-[variant=primary]:dark:text-gray-12', + 'data-[variant=primary]:text-gray-1', + 'data-[variant=primary]:border-red-7', + 'data-[variant=primary]:hover:border-red-8', + 'data-[variant=primary]:active:bg-red-10', + // Secondary + 'data-[variant=secondary]:bg-red-4', + 'data-[variant=secondary]:text-red-11', + 'data-[variant=secondary]:hover:bg-red-5', + 'data-[variant=secondary]:active:bg-red-6', + // Outline + 'data-[variant=outline]:text-red-9', + 'data-[variant=outline]:border-red-7', + 'data-[variant=outline]:hover:border-red-8', + 'data-[variant=outline]:active:bg-red-3', + // Ghost + 'data-[variant=ghost]:text-red-9', + 'data-[variant=ghost]:hover:bg-red-4', + 'data-[variant=ghost]:active:bg-red-5', + // Text + 'data-[variant=text]:text-red-9', + ], + warning: [ + // Primary + 'data-[variant=primary]:bg-yellow-9', + 'data-[variant=primary]:dark:text-gray-1', + 'data-[variant=primary]:text-gray-12', + 'data-[variant=primary]:border-yellow-7', + 'data-[variant=primary]:hover:border-yellow-8', + 'data-[variant=primary]:active:bg-yellow-10', + // Secondary + 'data-[variant=secondary]:bg-yellow-4', + 'data-[variant=secondary]:text-yellow-11', + 'data-[variant=secondary]:hover:bg-yellow-5', + 'data-[variant=secondary]:active:bg-yellow-6', + // Outline + 'data-[variant=outline]:text-yellow-9', + 'data-[variant=outline]:border-yellow-7', + 'data-[variant=outline]:hover:border-yellow-8', + 'data-[variant=outline]:active:bg-yellow-3', + // Ghost + 'data-[variant=ghost]:text-yellow-9', + 'data-[variant=ghost]:hover:bg-yellow-4', + 'data-[variant=ghost]:active:bg-yellow-5', + // Text + 'data-[variant=text]:text-yellow-9', + ], + orange: [ + // Primary + 'data-[variant=primary]:bg-orange-9', + 'data-[variant=primary]:dark:text-gray-12', + 'data-[variant=primary]:text-gray-1', + 'data-[variant=primary]:border-orange-7', + 'data-[variant=primary]:hover:border-orange-8', + 'data-[variant=primary]:active:bg-orange-10', + // Secondary + 'data-[variant=secondary]:bg-orange-4', + 'data-[variant=secondary]:text-orange-11', + 'data-[variant=secondary]:hover:bg-orange-5', + 'data-[variant=secondary]:active:bg-orange-6', + // Outline + 'data-[variant=outline]:text-orange-9', + 'data-[variant=outline]:border-orange-7', + 'data-[variant=outline]:hover:border-orange-8', + 'data-[variant=outline]:active:bg-orange-3', + // Ghost + 'data-[variant=ghost]:text-orange-9', + 'data-[variant=ghost]:hover:bg-orange-4', + 'data-[variant=ghost]:active:bg-orange-5', + // Text + 'data-[variant=text]:text-orange-9', + ], + }, + disabled: { + true: 'aria-disabled cursor-not-allowed', + false: '', + }, + }, + compoundVariants: [ + { variant: 'primary', disabled: true, className: 'bg-gray-3 text-gray-7 border-gray-7' }, + { variant: 'secondary', disabled: true, className: 'bg-gray-9 text-gray-11' }, + { variant: 'outline', disabled: true, className: 'text-gray-7 border-gray-7' }, + { variant: 'ghost', disabled: true, className: 'bg-gray-9 text-gray-11' }, + { variant: 'text', disabled: true, className: 'bg-gray-9 text-gray-11' }, + ], + }, +); diff --git a/app/components/ui/button/types.tsx b/app/components/ui/button/types.tsx new file mode 100644 index 0000000..665a3d6 --- /dev/null +++ b/app/components/ui/button/types.tsx @@ -0,0 +1,22 @@ +import type { ReactNode } from 'react'; + +import { buttonVariants } from './styles'; +import type { VariantProps } from 'class-variance-authority'; + +// ----------------------------------------------------------------------------- +// Variant props +// ----------------------------------------------------------------------------- + +export type ButtonVariantProps = VariantProps; + +// ----------------------------------------------------------------------------- +// Component props +// ----------------------------------------------------------------------------- + +export type ButtonProps = JSX.IntrinsicElements['button'] & + ButtonVariantProps & { + href?: string; + leftIcon?: ReactNode; + rightIcon?: ReactNode; + newTab?: boolean; + }; diff --git a/app/components/ui/nav-bar.tsx b/app/components/ui/nav-bar.tsx deleted file mode 100644 index fc9fe14..0000000 --- a/app/components/ui/nav-bar.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import Link from "next/link"; -import React from "react"; - -const NavBar = () => { - return ( -
-
-
- -
    -
  • - Home -
  • - -
  • - Resume -
  • -
  • - About Me -
  • -
-
-
- Kent Miguel -
-
-
    -
  • - Home -
  • - -
  • - Resume -
  • -
  • - About Me -
  • -
-
-
- - {/*
- Button -
*/} -
- ); -}; - -export default NavBar; diff --git a/app/components/ui/nav-bar/nav-bar-desktop.tsx b/app/components/ui/nav-bar/nav-bar-desktop.tsx new file mode 100644 index 0000000..d5faabb --- /dev/null +++ b/app/components/ui/nav-bar/nav-bar-desktop.tsx @@ -0,0 +1,32 @@ +import React from "react"; + +import { NAVBAR_PAGES } from "@/lib/constants/site"; +import Button from "@components/ui/button/index"; +import clsx from "clsx"; + +export default function NavbarDesktop({ selected }: NavBarProps) { + return ( + + ); +} diff --git a/app/components/ui/nav-bar/nav-bar-mobile.tsx b/app/components/ui/nav-bar/nav-bar-mobile.tsx new file mode 100644 index 0000000..473f5f0 --- /dev/null +++ b/app/components/ui/nav-bar/nav-bar-mobile.tsx @@ -0,0 +1,36 @@ +"use client"; + +import React from "react"; + +import { NAVBAR_PAGES } from "@/lib/constants/site"; +import Tooltip from "@components/ui/tooltip"; +import IconButton from "@components/ui/icon-button"; +import clsx from "clsx"; + +export default function NavbarMobile({ selected }: NavBarProps) { + return ( + + ); +} diff --git a/app/components/ui/nav-bar/nav-bar.tsx b/app/components/ui/nav-bar/nav-bar.tsx new file mode 100644 index 0000000..e597814 --- /dev/null +++ b/app/components/ui/nav-bar/nav-bar.tsx @@ -0,0 +1,15 @@ +"use client"; + +import React from "react"; +import { Fragment } from "react"; +import NavbarDesktop from "./nav-bar-desktop"; +import NavbarMobile from "./nav-bar-mobile"; + +export default function Navbar({ selected }: NavBarProps) { + return ( + + + + + ); +} diff --git a/app/global.d.ts b/app/global.d.ts index 2a3545e..38cf63a 100644 --- a/app/global.d.ts +++ b/app/global.d.ts @@ -1,4 +1,6 @@ import { Database as DB } from "@/lib/database.types"; +import { PageSlug } from "@/lib/types/site"; +import type { ReactNode } from "react"; declare global { type Database = DB; @@ -31,4 +33,18 @@ declare global { spaceBefore?: boolean; description?: string; }; + + type NavBarProps = { + selected?: PageSlug; + }; + + export type SeoBaseProps = { + title?: string; + subtitle?: string; + }; + + type BaseLayoutProps = SeoBaseProps & { + pageSlug?: PageSlug; + children?: ReactNode; + }; } diff --git a/app/page.tsx b/app/page.tsx index 1d6af75..503a2da 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,16 +1,15 @@ import Base from "./components/layouts/base"; -import ProfileHeader from "./components/ui/header"; -import ContainerLayout from "./components/layouts/container"; -import Contents from "./components/home/contents"; -import Running from "./components/home/running/running"; +import ProfileHeader from "@components/ui/header"; +import ContainerLayout from "@components/layouts/container"; +import Contents from "@components/home/contents"; export const dynamic = "force-dynamic"; export default async function Home() { return ( <> - + diff --git a/app/projects/page.tsx b/app/projects/page.tsx new file mode 100644 index 0000000..0938031 --- /dev/null +++ b/app/projects/page.tsx @@ -0,0 +1,24 @@ +import React from "react"; +import Base from "@components/layouts/base"; +import ProfileHeader from "@components/ui/header"; +import ContainerLayout from "@components/layouts/container"; +import InProgressImage from "../../public/aboutme-option3.webp"; +import Image from "next/image"; + +export default function AboutMe() { + return ( + + +

+ Projects +

+
Work in progress.
+ image +
+ + ); +} diff --git a/app/resume/components/skills.tsx b/app/resume/components/skills.tsx index fa29c29..e8e1b0d 100644 --- a/app/resume/components/skills.tsx +++ b/app/resume/components/skills.tsx @@ -1,5 +1,5 @@ import React from "react"; -import ContentDisplay from "../../components/layouts/content-display"; +import ContentDisplay from "@components/layouts/content-display"; import { Wand2 } from "lucide-react"; const Skills = () => { @@ -10,7 +10,7 @@ const Skills = () => { name="Skills" description="Tech Stack I usually use" > -
+
  • Coding Languages : Solidity, Web3, Typescript, JavaScript (Next.js, diff --git a/app/resume/page.tsx b/app/resume/page.tsx index 627c1ae..5fc32f4 100644 --- a/app/resume/page.tsx +++ b/app/resume/page.tsx @@ -1,21 +1,17 @@ import React from "react"; -import ProfileHeader from "../components/ui/header"; -import Base from "../components/layouts/base"; -import ContainerLayout from "../components/layouts/container"; -import Image from "next/image"; -import profile from "../../public/profile.jpeg"; -import Contents from "../components/home/contents"; +import ProfileHeader from "@components/ui/header"; +import Base from "@components/layouts/base"; +import ContainerLayout from "@components/layouts/container"; import ResumeContent from "./components/resume-content"; import Skills from "./components/skills"; const page = () => { return ( - + - {/** Accordion */} ); diff --git a/lib/constants/site.tsx b/lib/constants/site.tsx new file mode 100644 index 0000000..8811971 --- /dev/null +++ b/lib/constants/site.tsx @@ -0,0 +1,14 @@ +import { Home, FileText, PencilRuler, Ghost } from "lucide-react"; + +import type { Page } from "@/lib/types/site"; + +/** + * Pages displayed on personal website + * navigation bar. + */ +export const NAVBAR_PAGES: Page[] = [ + { name: "Home", slug: "/", icon: }, + { name: "Resume", slug: "/resume", icon: }, + { name: "Projects", slug: "/projects", icon: }, + { name: "About Me", slug: "/about", icon: }, +]; diff --git a/lib/types/site.ts b/lib/types/site.ts new file mode 100644 index 0000000..611572b --- /dev/null +++ b/lib/types/site.ts @@ -0,0 +1,46 @@ +import type { ReactNode } from "react"; + +/** + * Type for an external link. + * @param name Name describing the link. + * @param href URL of the link. + * @param icon Optional icon to describe/represent the link. + */ +export type ExternalLink = { + name: string; + href: string; + icon?: ReactNode; +}; + +/** + * Type for a page slug + */ +export type PageSlug = + | "/" + | "/resume" + // Design pages + | "/projects" + | "/about"; + +/** + * Type for an external page + * intended to be part of configuration files (e.g. for the navigation bar + * component). + */ +export type PageExternalLink = + | "https://twitter.com/fiveoutofnine" + | "https://github.com/fiveoutofnine"; + +/** + * Type for a page + * intended to be part of configuration files (e.g. for the navigation bar + * component). + * @param name Name describing the page. + * @param slug Slug/URL of the page. + * @param icon Optional icon to describe/represent the page. + */ +export type Page = { + name: string; + slug: PageSlug | PageExternalLink; + icon?: ReactNode; +}; diff --git a/prettier.config.js b/prettier.config.js deleted file mode 100644 index f5661aa..0000000 --- a/prettier.config.js +++ /dev/null @@ -1,22 +0,0 @@ -// prettier.config.js -module.exports = { - bracketSpacing: true, - semi: true, - trailingComma: 'all', - printWidth: 100, - tabWidth: 2, - singleQuote: true, - importOrder: [ - '(^(react/(.*)$)|^(react$))|(^(next/(.*)$)|^(next$))', - '', - '^@/styles/(.*)$', - '^@/public/(.*)$', - '^@/lib/(.*)$', - '^@/components/(.*)$', - '^@/pages/(.*)$', - ], - importOrderSortSpecifiers: true, - importOrderSeparation: true, - importOrderCaseInsensitive: true, - plugins: ['@trivago/prettier-plugin-sort-imports', require('prettier-plugin-tailwindcss')], -}; diff --git a/tsconfig.json b/tsconfig.json index beb39af..a20d921 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,7 +21,8 @@ "baseUrl": ".", "paths": { "@/*": ["./*"], - "@components/*":["./app/components/*"] + "@components/*":["./app/components/*"], + "@/lib/*": ["lib/*"], } },