Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ code-samples/
├── typesense-astro-search/ # Astro + Typesense search implementation
├── typesense-gin-full-text-search/ # Go (Gin) + Typesense backend implementation
├── typesense-next-search-bar/ # Next.js + Typesense search implementation
├── typesense-nuxt-search-bar/ # Nuxt.js + Typesense search implementation
├── typesense-qwik-js-search/ # Qwik + Typesense search implementation
├── typesense-react-native-search-bar/ # React Native + Typesense search implementation
├── typesense-solid-js-search/ # SolidJS + Typesense search implementation
Expand All @@ -25,6 +26,7 @@ code-samples/
| [typesense-astro-search](./typesense-astro-search) | Astro | A modern search bar with instant search capabilities |
| [typesense-gin-full-text-search](./typesense-gin-full-text-search) | Go (Gin) | Backend API with full-text search using Typesense |
| [typesense-next-search-bar](./typesense-next-search-bar) | Next.js | A modern search bar with instant search capabilities |
| [typesense-nuxt-search-bar](./typesense-nuxt-search-bar) | Nuxt.js | A modern search bar with instant search capabilities |
| [typesense-qwik-js-search](./typesense-qwik-js-search) | Qwik | Resumable search bar with real-time search and modern UI |
| [typesense-react-native-search-bar](./typesense-react-native-search-bar) | React Native | A mobile search bar with instant search capabilities |
| [typesense-solid-js-search](./typesense-solid-js-search) | SolidJS | A modern search bar with instant search capabilities |
Expand Down
5 changes: 5 additions & 0 deletions typesense-nuxt-search-bar/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
NUXT_PUBLIC_TYPESENSE_API_KEY=xyz
NUXT_PUBLIC_TYPESENSE_HOST=localhost
NUXT_PUBLIC_TYPESENSE_PORT=8108
NUXT_PUBLIC_TYPESENSE_PROTOCOL=http
NUXT_PUBLIC_TYPESENSE_INDEX=books
24 changes: 24 additions & 0 deletions typesense-nuxt-search-bar/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist

# Node dependencies
node_modules

# Logs
logs
*.log

# Misc
.DS_Store
.fleet
.idea

# Local env files
.env
.env.*
!.env.example
115 changes: 115 additions & 0 deletions typesense-nuxt-search-bar/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Nuxt.js Search Bar with Typesense

A modern search bar application built with Nuxt.js and Typesense, featuring instant search capabilities.

## Tech Stack

- Nuxt.js
- Vue 3
- Typesense
- typesense-instantsearch-adapter & vue-instantsearch
- Tailwind CSS

## Prerequisites

- Node.js 18+ and npm 9+.
- Docker (for running Typesense locally). Alternatively, you can use a Typesense Cloud cluster.
- Basic knowledge of Vue and Nuxt.js.

## Quick Start

### 1. Clone the repository

```bash
git clone https://github.com/typesense/code-samples.git
cd code-samples/typesense-nuxt-search-bar
```

### 2. Install dependencies

```bash
npm install
```

### 3. Set up Typesense and import data

#### Start Typesense Server (Local Development)

```bash
docker run -d -p 8108:8108 \
-v/tmp/typesense-data:/data typesense/typesense:27.1 \
--data-dir /data --api-key=xyz --enable-cors
```

#### Create Collection and Import Data

The application expects a `books` collection with the following schema:

```json
{
"name": "books",
"fields": [
{"name": "title", "type": "string"},
{"name": "authors", "type": "string[]"},
{"name": "publication_year", "type": "int32"},
{"name": "average_rating", "type": "float"},
{"name": "ratings_count", "type": "int32"},
{"name": "image_url", "type": "string", "optional": true}
]
}
```

### 4. Set up environment variables

Create a `.env` file in the project root (copy from `.env.example`):

```bash
cp .env.example .env
```

Update the values in `.env`:

```env
NUXT_PUBLIC_TYPESENSE_API_KEY=xyz
NUXT_PUBLIC_TYPESENSE_HOST=localhost
NUXT_PUBLIC_TYPESENSE_PORT=8108
NUXT_PUBLIC_TYPESENSE_PROTOCOL=http
NUXT_PUBLIC_TYPESENSE_INDEX=books
```

### 5. Project Structure

```text
├── app
│ └── app.vue
├── components
│ ├── BookCard.vue
│ ├── BookList.vue
│ ├── Heading.vue
│ └── SearchBar.vue
├── types
│ └── Book.ts
├── utils
│ └── instantSearchAdapter.ts
└── nuxt.config.ts
```

### 5. Start the development server

```bash
npm run dev
```

Open [http://localhost:3000](http://localhost:3000) in your browser.

### 6. Deployment

Set env variables to point the app to the Typesense Cluster:

```env
NUXT_PUBLIC_TYPESENSE_API_KEY=xxx
NUXT_PUBLIC_TYPESENSE_HOST=xxx.typesense.net
NUXT_PUBLIC_TYPESENSE_PORT=443
NUXT_PUBLIC_TYPESENSE_PROTOCOL=https
NUXT_PUBLIC_TYPESENSE_INDEX=books
```
72 changes: 72 additions & 0 deletions typesense-nuxt-search-bar/app/app.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<script setup lang="ts">
import { AisInstantSearch } from "vue-instantsearch/vue3/es";
import { createTypesenseAdapter } from "../utils/instantSearchAdapter";
import Heading from "../components/Heading.vue";
import SearchBar from "../components/SearchBar.vue";
import BookList from "../components/BookList.vue";

const config = useRuntimeConfig();
const typesenseConfig = config.public.typesense;

const typesenseAdapter = createTypesenseAdapter({
apiKey: typesenseConfig.apiKey,
host: typesenseConfig.host,
port: typesenseConfig.port,
protocol: typesenseConfig.protocol,
});

useHead({
title: "Nuxt.js Search Bar",
meta: [
{
name: "description",
content: "Search through our collection of books",
},
],
link: [
{
rel: "icon",
type: "image/png",
href: "/favicon.png",
},
],
});
</script>

<template>
<div class="app-container">
<div class="app-content">
<AisInstantSearch
:search-client="typesenseAdapter.searchClient"
:index-name="typesenseConfig.index"
>
<Heading />
<SearchBar />
<BookList />
</AisInstantSearch>
</div>
</div>
</template>

<style>
* {
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
</style>

<style scoped>
.app-container {
min-height: 100vh;
background-color: #f9fafb;
padding: 2rem 1rem;
}

.app-content {
max-width: 80rem;
margin: 0 auto;
}
</style>
137 changes: 137 additions & 0 deletions typesense-nuxt-search-bar/components/BookCard.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
<script setup lang="ts">
import type { Book } from "../types/Book";
import { ref } from "vue";

const props = defineProps<{
book: Book;
}>();

const imageError = ref(false);

const handleImageError = () => {
imageError.value = true;
};
</script>

<template>
<div class="book-card">
<div class="book-image-container">
<img
v-if="book.image_url && !imageError"
:src="book.image_url"
:alt="book.title"
class="book-image"
@error="handleImageError"
/>
<div v-else class="no-image">No Image</div>
</div>
<div class="book-info">
<h3 class="book-title">{{ book.title }}</h3>
<p class="book-author">By: {{ book.authors.join(", ") }}</p>
<p class="book-year">Published: {{ book.publication_year }}</p>
<div class="rating-container">
<div class="star-rating">
{{ "★".repeat(Math.round(book.average_rating)) }}
</div>
<span class="rating-text">
{{ book.average_rating.toFixed(1) }} ({{
book.ratings_count.toLocaleString()
}}
ratings)
</span>
</div>
</div>
</div>
</template>

<style scoped>
.book-card {
display: flex;
gap: 1.5rem;
padding: 1.5rem;
background-color: white;
border-radius: 0.5rem;
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06);
transition: box-shadow 200ms ease-in-out;
}

.book-card:hover {
box-shadow:
0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05);
}

.book-image-container {
flex-shrink: 0;
width: 8rem;
height: 12rem;
background-color: #f3f4f6;
border-radius: 0.375rem;
overflow: hidden;
}

.book-image {
width: 100%;
height: 100%;
object-fit: cover;
}

.no-image {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: #9ca3af;
}

.book-info {
flex: 1;
display: flex;
flex-direction: column;
}

.book-title {
font-size: 1.25rem;
font-weight: 600;
color: #111827;
margin-bottom: 0.5rem;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}

.book-author {
color: #4b5563;
margin-bottom: 0.25rem;
font-size: 0.875rem;
}

.book-year {
color: #6b7280;
font-size: 0.75rem;
margin-bottom: 0.5rem;
}

.rating-container {
margin-top: auto;
padding-top: 0.5rem;
display: flex;
align-items: center;
}

.star-rating {
color: #f59e0b;
font-size: 1.125rem;
line-height: 1;
}

.rating-text {
margin-left: 0.5rem;
font-size: 0.75rem;
color: #4b5563;
}
</style>
Loading