I like Cookies and I like Christmas, and hey Datastax is pretty cool too. so lets build something.
Overview
HEY HEY and happy holidays yall. When I heard Datastax was doing a 12 days of Codemas challenge, well you KNOW your boy had to take a crack at it. What better way to do that than a Christmas Cookie Recipe Finder! It’s a web application that allows users to search for cookie recipes. I used Astro (cuz not smart enough for other stuff haha ) for the frontend and Datastax’s own AstraDB as the backend to store and retrieve recipe data. The application is designed to be responsive and user-friendly.
BUT…..
since This is a Christmas build and I didn’t have much to go on, THIS Santa had to call on his Christmas Elves to pull off this Christmas miracle build. So I phone the homies Ryan Furrer and Chris Nowicki.
Key Features
Search Functionality: Users can search for recipes by entering keywords in the title. This feature is crucial for enhancing user experience, as it allows users to quickly find the recipes they are interested in. The search functionality is implemented using a simple form that captures user input and triggers a search query against the database.
Dynamic Results: The application retrieves and displays matching recipes in real-time. This is achieved by leveraging the power of React's state management and the efficient querying capabilities of DataStax Astra. As users type in the search bar, the application dynamically updates the list of recipes displayed, providing immediate feedback.
DataStax Astra Integration: Utilizes the DataStax Astra database to store and query recipe data. The integration with Astra was PRETTY seamless, thanks to its comprehensive API and support for various programming languages, but also thanks to their docs. This allows the application to efficiently store and retrieve large volumes of recipe data, ensuring a smooth user experience.
Development Process
Setting Up the Environment
To get started, ensure you have the following prerequisites:
Node.js (version 14 or higher): Node.js is a JavaScript runtime that allows you to run JavaScript code on the server side. It's essential for setting up the development environment and running the application locally.
npm (Node Package Manager): npm is the default package manager for Node.js, used to install and manage dependencies for the project.
A DataStax Astra account with a database set up: DataStax Astra provides a free tier, making it accessible for developers to experiment with and integrate into their applications.
Clone the repository and install the necessary dependencies:
Running the Application
Clone the Repository:
git clone https://github.com/jasonetorres/proto-pulsar.git cd recipe-search-app
Install Dependencies:
npm i
Start the Development Server:
npm start
Also, in order to run the application, you need to set up environment variables in a .env
file:
Configuring the Database
Create a .env file in the root of the project and add your DataStax Astra connection details:
ASTRA_DB_API_ENDPOINT=https://<your-database-endpoint>
ASTRA_DB_APPLICATION_TOKEN=<your-application-token>
Replace <your-database-endpoint> and <your-application-token> with your actual DataStax Astra credentials, I feel the disclaimer isn’t necessary, but hey ya never know. I am sure we've all done this once haha. This setup ensures that your application can securely connect to the Astra database and perform queries.
Building the Components
SearchBar Component
The SearchBar component is responsible for capturing user input and triggering the search functionality. It uses React's useState hook to manage the search term state. This component is designed to be user-friendly, with a simple input field and a search button. The handleReset function allows users to clear the search term and reset the search results, enhancing the overall user experience.
import { useState } from "react";
export default function Searchbar({
onSubmit,
handleReset,
}: {
onSubmit: any;
handleReset: any;
}) {
const [searchTerm, setSearchTerm] = useState("");
return (
<div className="my-10 w-full flex-col">
<div className="w-full max-w rounded-lg bg-card p-6 shadow-lg">
<form className="flex gap-3" onSubmit={onSubmit}>
<input
type="text"
name="search"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search recipes..."
className="w-full rounded border border-input bg-card px-4 py-2
text-muted-foreground placeholder:text-muted-foreground
focus:outline-none focus:ring-2 focus:ring-ring" />
<button
type="submit"
className="rounded bg-primary px-4 py-2
text-primary-foreground hover:bg-primary/90
focus:outline-none focus:ring-2 focus:ring-ring" />
Search
</button>
</form>
</div>
<div className="mt-4 w-full max-w">
<button
onClick={() => {
setSearchTerm("");
handleReset();
}}
className="w-full rounded bg-primary px-4 py-2
text-primary-foreground hover:bg-primary/90
focus:outline-none focus:ring-2 focus:ring-ring" />
Reset Search
</button>
</div>
</div>
);
}
RecipeIsland Component
“would ya like to spend christmas…..on recipe islaaaaannnd…….”
The RecipeIsland component handles the search logic and displays the search results. It filters recipes based on the search term and manages the state of the filtered recipes. This component is crucial for the application's functionality, as it processes user input, queries the database, and updates the UI with the search results. The use of React's state management ensures that the application remains responsive and efficient. This was a big part that Chris contributed as we had hit a roadblock as to how to effectively query the database
import { useState, type FormEvent } from "react";
import type { Recipe } from "../utils/db";
import Searchbar from "./SearchBar";
import RecipeCard from "./RecipeCard";
export default function RecipeIsland({ recipes }: { recipes: Recipe[] }) {
const [filteredRecipes, setFilteredRecipes] = useState<Recipe[]>([]);
const [searchPerformed, setSearchPerformed] = useState(false);
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const form = e.target as HTMLFormElement;
const formData = new FormData(form);
const searchTerm = formData.get("search") as string;
const searchResults = recipes
.filter(
(recipe) =>
recipe.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
recipe.description.toLowerCase().includes(searchTerm.toLowerCase()),
)
.reduce((uniqueRecipes: Recipe[], recipe) => {
if (!uniqueRecipes.some((r) => r._id === recipe._id)) {
uniqueRecipes.push(recipe);
}
return uniqueRecipes;
}, []);
console.log(searchResults);
setFilteredRecipes(searchResults);
setSearchPerformed(true);
};
const handleReset = () => {
setFilteredRecipes([]);
setSearchPerformed(false);
};
return (
<div className="flex flex-col items-center justify-center">
<Searchbar onSubmit={handleSubmit} handleReset={handleReset} />
{searchPerformed ? (
filteredRecipes.length > 0 ? (
<div className="space-y-8" data-recipes-container>
{filteredRecipes.map((recipe, index) => (
<RecipeCard
key={`recipe-${index}-${recipe.title.toLocaleLowerCase()}`}
cookTime={recipe.cookTime}
description={recipe.description}
difficulty={recipe.difficulty}
ingredients={recipe.ingredients}
instructions={recipe.instructions}
prepTime={recipe.prepTime}
title={recipe.title}
totalTime={recipe.totalTime}
/>
))}
</div>
) : (
<div>
<p>No recipes found.</p>
<button
onClick={handleReset}
className="mt-5 mb-5 p-3 text-white text-center rounded"
style={{ backgroundColor: '#E11D48' }}
>
Back to Search
</button>
</div>
)
) : null}
</div>
);
}
RecipeCard Component
The RecipeCard component is responsible for displaying individual recipe details. It ensures that all necessary information is presented in a user-friendly format. This component is designed to be visually appealing, with a clean layout that highlights the recipe's title, description, ingredients, and instructions. The use of conditional rendering ensures that the component gracefully handles missing data, providing default messages when necessary.
interface RecipeCardProps {
cookTime: string;
description: string;
difficulty: string;
ingredients: string[];
instructions: string[];
prepTime: string;
title: string;
totalTime: string;
}
export default function RecipeCard({cookTime, description, difficulty,
ingredients, instructions, prepTime, title, totalTime}: RecipeCardProps) {
return (
<div className="container mx-auto space-y-[1em] rounded-lg border-border bg-card
p-6 shadow-l">
<h2 style={{ color: '#E11D48', textAlign: 'center'}}>{title || "Recipe Title"}</h2>
<p style={{ textAlign: 'center'}}>{description || "Recipe description"}</p>
<div className="mx-auto w-fit space-y-0 rounded-md border-2 border-dashed
border-primary px-4">
<div className="flex justify-center gap-4 py-4 text-center">
<div>
<p className="text-sm font-bold">Total Time</p>
<p className="text-sm">{totalTime || "Total Time not found"}</p>
</div>
<div>
<p className="text-sm font-bold">Difficulty</p>
<p className="text-sm">{difficulty || "Difficulty not found"}</p>
</div>
</div>
</div>
<p className="text-lg font-bold mx-2">Ingredients:</p>
<ul className="list-disc mx-4">
{!ingredients || ingredients.length === 0 ? (
<li>Ingredients not found</li>
) : (
ingredients.map((ingredient: string) => <li>{ingredient}</li>)
)}
</ul>
<p className="text-lg font-bold mx-2">Instructions:</p>
<ol className="list-decimal mx-4">
{!instructions || instructions.length === 0 ? (
<li>Instructions not found</li>
) : (
instructions.map((instruction: string) => <li>{instruction}</li>)
)}
</ol>
<div className="flex justify-center gap-4 py-4 text-center">
<div>
<p className="text-sm font-bold">Prep Time</p>
<p className="text-sm">{prepTime || "Prep Time not found"}</p>
</div>
<div>
<p className="text-sm font-bold">Cook Time</p>
<p className="text-sm">{cookTime || "Cook Time not found"}</p>
</div>
<div>
<p className="text-sm font-bold">Total Time</p>
<p className="text-sm">{totalTime || "Total Time not found"}</p>
</div>
</div>
</div>
);
}
Now….
this is all well and good and you have a pretty efficiently working search operation but how did we even get the Astradb side setup….well friends I got you.
How to Set Up an Astra DB Database and Use It in Your Project
Astra DB is a serverless database platform optimized for modern workloads like vector-based and generative AI applications. And special shout to them for creating a fun contest around their product to highlight some of the best way to use them!
Step 1: Sign In or Sign Up for Astra DB
Visit the Astra DB homepage.
Log in with your credentials or create a new account.
Once logged in, you will land on the main dashboard that offers various options to manage your databases.
Step 2: Create a New Database
From the dashboard, click on Create Database.
Choose the deployment type:
Serverless (Vector): Recommended for projects using AI/ML or vector-based workloads.
Serverless (Non-Vector): A more traditional database solution without vector capabilities.
Enter a Database Name that is memorable (this cannot be changed later).
Select your cloud provider (e.g., AWS, GCP, Azure) and the desired region.
Click Create Database to initialize your database.
Wait for the status to change to Active in the database list.
Step 3: Explore Database Usage and Metrics
After creating your database, the dashboard provides key metrics:
Read Requests: Number of read operations.
Write Requests: Number of write operations.
Storage Consumed: Space used by your database.
Data Transfer: Amount of data transferred.
These metrics help monitor and optimize database performance.
Step 4: Connect to Your Database
From the database dashboard, click on Connect.
Choose your preferred connection method:
Drivers: Select a programming language (e.g., Python, Node.js, Java).
REST API: For applications needing simple HTTP requests.
GraphQL: Ideal for modern applications using GraphQL APIs.
CQL Console: Directly interact using Cassandra Query Language.
Follow the provided steps to download the secure credentials bundle. This file will be required to authenticate your application.
We built our project in Typescript but the site has the code examples to integrate in multiple other flavors.
TypeScript Example
Install Required Libraries:
npm install cassandra-driver @types/cassandra-driver
Load the Credentials Bundle: Place the downloaded credentials file in your project.
Connect to the Database:
import { Client, auth } from 'cassandra-driver'; const authProvider = new auth.PlainTextAuthProvider('your_client_id', 'your_client_secret'); const client = new Client({ cloud: { secureConnectBundle: 'secure-connect-database_name.zip' }, authProvider }); async function run() { await client.connect(); const result = await client.execute('SELECT release_version FROM system.local'); console.log(`Cassandra Release Version: ${result.rows[0].release_version}`); } run();
Install Required Libraries:
npm install cassandra-driver
Load the Credentials Bundle: Place the downloaded credentials file in your project.
Connect to the Database:
const cassandra = require('cassandra-driver'); const authProvider = new cassandra.auth.PlainTextAuthProvider('your_client_id', 'your_client_secret'); const client = new cassandra.Client({ cloud: { secureConnectBundle: 'secure-connect-database_name.zip' }, authProvider }); async function run() { await client.connect(); const result = await client.execute('SELECT release_version FROM system.local'); console.log(`Cassandra Release Version: ${result.rows[0].release_version}`); } run();
In Closing:
This project demonstrated a well-implemented Christmas cookie recipe search application built with Astro and DataStax AstraDB. Key accomplishments include:
Clean architecture with separate components for search, results display, and recipe cards
Efficient database integration with AstraDB for recipe storage and retrieval
Responsive UI with thoughtful features like search reset and "no results" handling
Comprehensive documentation covering setup, configuration, and database integration
This Christmas cookie recipe finder that makes hunting down holiday treats super easy! Built it using Astro (keeping it simple!) and DataStax AstraDB for storing all those delicious recipes.
The collaboration between myself and Ryan Furrer and Chris Nowicki REALLLLLLY helped overcome technical challenges, particularly with database querying and styling. The project serves as a practical example of building a full-stack application with modern web technologies while maintaining good development practices through proper component separation, error handling, and thorough documentation.
Go ahead and give it a try! There's a sample database of around 20 recipes already loaded up. Down the line, I'm thinking we might spice things up with some Langflow to throw in some AI-generated recipes. Perfect for when you're craving something sweet but not sure what to bake! 🍪
Let me know if you try it out - would love to hear what kinds of cookies you end up making! And hey, if you've got any killer cookie recipes to add to the database, send 'em my way!