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