Skip to content

Commit

Permalink
Add custom hooks and validation schemas; refactor button components
Browse files Browse the repository at this point in the history
  • Loading branch information
IgorKowalczyk committed Dec 13, 2024
1 parent e4f2b03 commit cc57ed5
Show file tree
Hide file tree
Showing 14 changed files with 214 additions and 316 deletions.
90 changes: 12 additions & 78 deletions app/api/contact/route.ts
Original file line number Diff line number Diff line change
@@ -1,89 +1,23 @@
import isEmail from "validator/lib/isEmail";
import { contactFormSchema } from "@/lib/validator";

export const runtime = "edge";

export async function POST(request: Request) {
const { email, name, message } = await request.clone().json();
const body = await request.json();

if (!email) {
return new Response(
JSON.stringify({
error: true,
type: "email",
message: "Please enter your email address!",
}),
{
status: 400,
headers: {
"Content-Type": "application/json",
},
}
);
}

if (typeof email !== "string" || !isEmail(email)) {
return new Response(
JSON.stringify({
error: true,
type: "email",
message: "Please enter a valid email address!",
}),
{
status: 400,
headers: {
"Content-Type": "application/json",
},
}
);
}
const result = contactFormSchema.safeParse(body);

if (email.trim().length < 5 || email.trim().length > 50) {
return new Response(
JSON.stringify({
error: true,
type: "email",
message: "Email must be between 5 and 50 characters!",
}),
{
status: 400,
headers: {
"Content-Type": "application/json",
},
}
);
}

if (!name || typeof name !== "string" || !name.trim() || name.trim().length < 3 || name.trim().length > 20) {
return new Response(
JSON.stringify({
error: true,
type: "name",
message: "Name must be between 3 and 20 characters!",
}),
{
status: 400,
headers: {
"Content-Type": "application/json",
},
}
);
if (!result.success) {
const errors = result.error.flatten().fieldErrors;
return new Response(JSON.stringify({ error: true, message: Object.values(errors).flat().join(" ") }), {
status: 400,
headers: {
"Content-Type": "application/json",
},
});
}

if (!message || typeof message !== "string" || !message.trim() || message.trim().length < 10 || message.trim().length > 500) {
return new Response(
JSON.stringify({
error: true,
type: "message",
message: "Message must be between 10 and 500 characters!",
}),
{
status: 400,
headers: {
"Content-Type": "application/json",
},
}
);
}
const { name, email, message } = result.data;

const embed = {
title: "📩 New message from igorkowalczyk.dev",
Expand Down
6 changes: 3 additions & 3 deletions app/contact/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ButtonTertiary } from "@/components/Button";
import { Button } from "@/components/Button";
import { ContactForm } from "@/components/client/ContactForm";
import { Description, Header2 } from "@/components/Headers";
import { contact } from "@/config";
Expand All @@ -21,9 +21,9 @@ export default function Page() {
<Description>Or contact me with...</Description>
<div className="mt-4 flex flex-wrap gap-4">
{contact.links.map((element) => (
<ButtonTertiary href={element.href} key={`contact-link-${element.href}`} className="gap-2">
<Button variant="tertiary" href={element.href} key={`contact-link-${element.href}`} className="gap-2">
{element.icon} {element.title}
</ButtonTertiary>
</Button>
))}
</div>
</section>
Expand Down
6 changes: 4 additions & 2 deletions app/not-found.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { ButtonSecondary } from "@/components/Button";
import { Button } from "@/components/Button";

export default function NotFound() {
return (
<div className="mb-16 mt-20">
<h1 className="mx-0 mt-0 bg-gradient-to-r from-[#ff7170] to-[#ffe57f] box-decoration-clone bg-clip-text text-4xl font-black tracking-[-0.03em] text-fill-transparent">404 - Page not found</h1>
<p className="mb-4 mt-2 text-lg text-neutral-700 dark:text-neutral-400">We're sorry — we can't find the page you're looking for.</p>
<ButtonSecondary href="/">Go home</ButtonSecondary>
<Button variant="secondary" href="/">
Go home
</Button>
</div>
);
}
22 changes: 14 additions & 8 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Image from "next/image";
import Link from "next/link";
import { ButtonPrimary, ButtonSecondary, ButtonTertiary } from "../components/Button";
import { Button } from "../components/Button";
import { Icons } from "../components/Icons";
import { ContactForm } from "@/components/client/ContactForm";
import { Description, Header2 } from "@/components/Headers";
Expand All @@ -25,8 +25,12 @@ export default async function HomePage() {
<h1 className="dark:color-black relative m-0 text-4xl font-black tracking-[-0.03em] text-neutral-800 duration-300 dark:text-white md:text-left">Hey, I’m {header.title}</h1>
<p className="mt-2 text-lg text-neutral-700 dark:text-neutral-400">{header.description}</p>
<div className="mt-9 flex flex-row flex-wrap gap-4">
<ButtonPrimary href="/#contact">Contact me</ButtonPrimary>
<ButtonSecondary href="/#about">More about me</ButtonSecondary>
<Button variant="primary" href="/#contact">
Contact me
</Button>
<Button variant="secondary" href="/#about">
More about me
</Button>
</div>
</section>

Expand Down Expand Up @@ -58,11 +62,13 @@ export default async function HomePage() {
</div>

<div className="mt-6 flex flex-row flex-wrap gap-4">
<ButtonPrimary href={`https://github.com/${meta.accounts.github.username}`} rel="noopener noreferrer">
<Button variant="primary" href={`https://github.com/${meta.accounts.github.username}`} rel="noopener noreferrer">
<Icons.Github className="mr-2 size-5 fill-white stroke-2" />
View my Github
</ButtonPrimary>
<ButtonSecondary href="/#contact">Contact me</ButtonSecondary>
</Button>
<Button variant="secondary" href="/#contact">
Contact me
</Button>
</div>
</section>

Expand Down Expand Up @@ -102,9 +108,9 @@ export default async function HomePage() {
<Description>Or contact me with...</Description>
<div className="mt-4 flex flex-wrap gap-4">
{contact.links.map((element) => (
<ButtonTertiary href={element.href} key={`contact-link-${element.href}`} className="gap-2">
<Button variant="tertiary" href={element.href} key={`contact-link-${element.href}`} className="gap-2">
{element.icon} {element.title}
</ButtonTertiary>
</Button>
))}
</div>
</section>
Expand Down
6 changes: 3 additions & 3 deletions app/work/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Fragment } from "react";
import { ButtonPrimary } from "@/components/Button";
import { Button } from "@/components/Button";
import { Description, Header1 } from "@/components/Headers";
import { Icons } from "@/components/Icons";
import { ProjectCard } from "@/components/ProjectCard";
Expand All @@ -26,10 +26,10 @@ export default function Page() {

<Description>Want to see more? Check out my GitHub profile for more projects and contributions.</Description>

<ButtonPrimary href={`https://github.com/${meta.accounts.github.username}`} rel="noopener noreferrer" className="mt-4">
<Button variant="primary" href={`https://github.com/${meta.accounts.github.username}`} rel="noopener noreferrer" className="mt-4">
<Icons.Github className="mr-2 size-5 fill-white stroke-2" />
View my Github
</ButtonPrimary>
</Button>
</div>
);
}
100 changes: 25 additions & 75 deletions components/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,92 +1,42 @@
import { cva, VariantProps } from "class-variance-authority";
import Link from "next/link";
import React from "react";
import { Icons } from "@/components/Icons";
import { cn } from "@/lib/utils";

export interface ButtonProps extends React.HTMLAttributes<HTMLButtonElement | HTMLAnchorElement> {
export const buttonVariants = cva("group flex w-fit items-center rounded-md px-4 py-2 font-medium duration-200 disabled:cursor-not-allowed disabled:opacity-50 motion-reduce:transition-none", {
variants: {
variant: {
primary: "bg-blue-500 text-white hover:bg-blue-600",
secondary: "bg-neutral-200 text-neutral-700 hover:bg-neutral-300 dark:bg-white/10 dark:text-white dark:hover:bg-white/15",
tertiary: "border text-neutral-700 hover:bg-[#f2f3f5] dark:border-neutral-800 dark:bg-[#161617] dark:text-white dark:hover:border-neutral-700 dark:hover:bg-[#202021]",
},
},
defaultVariants: {
variant: "primary",
},
});

export interface ButtonProps extends React.HTMLAttributes<HTMLButtonElement | HTMLAnchorElement>, VariantProps<typeof buttonVariants> {
href?: string;
children: React.ReactNode;
icon?: boolean;
type?: "button" | "submit" | "reset";
disabled?: boolean;
}

export const buttonPrimaryStyles = "group flex w-fit items-center rounded-md bg-blue-500 px-4 py-2 font-medium text-white duration-200 hover:bg-blue-600 disabled:cursor-not-allowed disabled:opacity-50 motion-reduce:transition-none";

export function ButtonPrimary({ href, children, icon = true, ...props }: ButtonProps) {
if (href) {
return (
<Link href={href} {...props} className={cn(buttonPrimaryStyles, props.className || "")}>
{children}
{icon && (
<svg className="ml-2 size-4 duration-200 group-hover:translate-x-1 motion-reduce:transition-none motion-reduce:group-hover:translate-x-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M13 7l5 5m0 0l-5 5m5-5H6" />
</svg>
)}
</Link>
);
}
return (
<button {...props} className={cn(buttonPrimaryStyles, props.className || "")} type="button">
{children}
{icon && (
<svg className="ml-2 size-4 duration-200 group-hover:translate-x-1 motion-reduce:transition-none motion-reduce:group-hover:translate-x-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M13 7l5 5m0 0l-5 5m5-5H6" />
</svg>
)}
</button>
);
type?: HTMLButtonElement["type"];
disabled?: HTMLButtonElement["disabled"];
}

export const buttonSecondaryStyles = "group flex w-fit items-center rounded-md bg-neutral-200 px-4 py-2 font-medium text-neutral-700 duration-200 hover:bg-neutral-300 disabled:cursor-not-allowed disabled:opacity-50 motion-reduce:transition-none dark:bg-white/10 dark:text-white dark:hover:bg-white/15";

export function ButtonSecondary({ href, children, icon = true, ...props }: ButtonProps) {
if (href) {
return (
<Link href={href} {...props} className={cn(buttonSecondaryStyles, props.className || "")}>
{children}
{icon && (
<svg className="ml-2 size-4 duration-200 group-hover:translate-x-1 motion-reduce:transition-none motion-reduce:group-hover:translate-x-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M13 7l5 5m0 0l-5 5m5-5H6" />
</svg>
)}
</Link>
);
} else {
return (
<button {...props} className={cn(buttonSecondaryStyles, props.className || "")} type="button">
{children}
{icon && (
<svg className="ml-2 size-4 duration-200 group-hover:translate-x-1 motion-reduce:transition-none motion-reduce:group-hover:translate-x-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M13 7l5 5m0 0l-5 5m5-5H6" />
</svg>
)}
</button>
);
}
}

export const buttonTertiaryStyles = "group flex w-fit items-center rounded-md border px-4 py-2 font-medium text-neutral-700 duration-200 hover:bg-[#f2f3f5] disabled:cursor-not-allowed disabled:opacity-50 motion-reduce:transition-none dark:border-neutral-800 dark:bg-[#161617] dark:text-white dark:hover:border-neutral-700 dark:hover:bg-[#202021]";

export function ButtonTertiary({ href, children, icon = true, ...props }: ButtonProps) {
export const Button = React.forwardRef<HTMLButtonElement | HTMLAnchorElement, ButtonProps>(({ href, children, icon = true, variant, ...props }, ref) => {
if (href) {
return (
<Link href={href} {...props} className={cn(buttonTertiaryStyles, props.className || "")}>
<Link href={href} {...props} ref={ref as React.Ref<HTMLAnchorElement>} className={cn(buttonVariants({ variant }), props.className || "")}>
{children}
{icon && (
<svg className="ml-2 size-4 duration-200 group-hover:translate-x-1 motion-reduce:transition-none motion-reduce:group-hover:translate-x-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M13 7l5 5m0 0l-5 5m5-5H6" />
</svg>
)}
{icon && <Icons.ArrowRight className="ml-2 size-4 duration-200 group-hover:translate-x-1 motion-reduce:transition-none motion-reduce:group-hover:translate-x-0" />}
</Link>
);
}
return (
<button {...props} className={cn(buttonTertiaryStyles, props.className || "")} type="button">
<button {...props} ref={ref as React.Ref<HTMLButtonElement>} className={cn(buttonVariants({ variant }), props.className || "")}>
{children}
{icon && (
<svg className="ml-2 size-4 duration-200 group-hover:translate-x-1 motion-reduce:transition-none motion-reduce:group-hover:translate-x-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M13 7l5 5m0 0l-5 5m5-5H6" />
</svg>
)}
{icon && <Icons.ArrowRight className="ml-2 size-4 duration-200 group-hover:translate-x-1 motion-reduce:transition-none motion-reduce:group-hover:translate-x-0" />}
</button>
);
}
});
10 changes: 5 additions & 5 deletions components/ProjectCard.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Image from "next/image";
import Link from "next/link";
import { Icons } from "./Icons";
import { ButtonSecondary, ButtonPrimary } from "@/components/Button";
import { Button } from "@/components/Button";
import { type Project } from "@/config";
import { parseISO } from "@/lib/utils";

Expand Down Expand Up @@ -35,16 +35,16 @@ export function ProjectCard({ project }: ProjectCardProps) {
</div>
<div className="mt-6 flex flex-wrap gap-4">
{project.website && (
<ButtonPrimary href={project.website} rel="noopener noreferrer">
<Button variant="primary" href={project.website} rel="noopener noreferrer">
<Icons.Link className="mr-2 size-5 stroke-2" />
Visit website
</ButtonPrimary>
</Button>
)}
{project.github && (
<ButtonSecondary href={project.github} rel="noopener noreferrer">
<Button variant="secondary" href={project.github} rel="noopener noreferrer">
<Icons.Github className="mr-2 size-5 fill-neutral-700 dark:fill-white" />
View on Github
</ButtonSecondary>
</Button>
)}
</div>
</div>
Expand Down
Loading

0 comments on commit cc57ed5

Please sign in to comment.