KiraWebs

KiraWebs

Next.jsNotion APIRedis

Web development project featuring a modern landing page for a website creation service, focused on performance, transparency, and a clear user experience for businesses and professionals.


Kirawebs.com is a website developed for a company that provides technological solutions, including web development, consulting, and cloud solutions. 💡 One of the standout features of this project is the implementation of an interactive multi-step form that allows clients to simulate the cost of their website.


Technologies Used

The following technologies were used for the development of this project:

  • Next.js: As the main framework for building the application.
  • React: For creating interactive components.
  • Tailwind CSS: For designing and styling the interface.
  • Shadcn: For implementing modern and accessible UI components.
  • Notion API: To store contact form data in a Notion database.
  • Redis: To implement a rate limiting system, limiting message submissions to 3 per minute.
  • Zod: For data validation in the contact form.
  • ESLint and Prettier: To maintain clean and well-formatted code.
  • Vercel: For deployment and hosting of the application.

Image


Key Features

Interactive Cost Simulation Form

  • Clients can follow a series of steps to get an estimate of their website's cost.
Image
Image

Contact Form with Validation and Rate Limiting

  • An email and a message are requested.
  • Data is validated using Zod.
  • If validation is successful, the data is stored in a Notion database.
  • A rate limiting system was implemented using Redis, limiting message submissions to 3 per minute.
Image
Image

In Notion Database

Image


Code Snippets and Examples

Example of Validation with Zod

const FormSchema = z.object({
	email: z.string().email("Invalid email"),
	description: z
		.string()
		.min(5, "The description must have at least 5 characters")
		.max(1500, "The description must have fewer than 1000 characters"),
})

export async function sendEmail(prevState: unknown, formData: FormData) {
	const clientIp = formData.get("clientIp") as string
	const result = await ratelimit.limit(clientIp)

	if (!result.success) {
		return {
			success: false,
			title: "Submission limit reached",
			details: "Please wait a moment before trying again.",
		}
	}
	const rawData = {
		email: formData.get("email"),
		description: formData.get("description"),
	}
	const validationResult = FormSchema.safeParse(rawData)

	if (!validationResult.success) {
		return {
			success: false,
			title: "Invalid data",
			details:
				"Please verify that your email is valid and that the description has between 5 and 1500 characters.",
		}
	}

	const { email, description } = validationResult.data

Contact Message Storage

 try {
    const response = await notion.pages.create({
      parent: { database_id: DATABASE_IDS.contact! },
      properties: {
        email: {
          title: [{ text: { content: email } }],
        },
        description: {
          rich_text: [{ text: { content: description } }],
        },
      },
    })

    if (!response) {
      return {
        success: false,
        title: "Error sending data",
        details:
          "We were unable to send your data at this time. Please try again later or contact us through another channel.",
      }
    }

    return {
      success: true,
      title: "Message sent!",
      details:
        "Thank you for contacting us. We will get back to you shortly.",
    }
  } catch {
    return {
      success: false,
      title: "Error sending data",
      details:
        "There was a problem processing your request. Please try again later or contact us directly.",
    }
  }
}

Rate Limiting Implementation with Redis

const ratelimit = new Ratelimit({
  redis: redis,
  limiter: Ratelimit.fixedWindow(3, "60 s"),
})
export async function sendEmail(prevState: unknown, formData: FormData) {
  const clientIp = formData.get("clientIp") as string
  const result = await ratelimit.limit(clientIp)

  if (!result.success) {
    return {
      success: false,
      title: "Submission limit reached",
      details: "Please wait a moment before trying again.",
    }
  }

Deployment on Vercel

The application is hosted on Vercel, ensuring optimal performance and easy scalability.


© 2026 Felipe Giraldo