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
1 change: 0 additions & 1 deletion typesense-sveltekit-search-app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ PUBLIC_TYPESENSE_API_KEY=xxx
PUBLIC_TYPESENSE_HOST=localhost
PUBLIC_TYPESENSE_PORT=8108
PUBLIC_TYPESENSE_PROTOCOL=http
PUBLIC_TYPESENSE_INDEX=books
```

### 4. Project Structure
Expand Down
6 changes: 5 additions & 1 deletion typesense-sveltekit-search-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"test": "vitest run",
"prepare": "svelte-kit sync || echo ''",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
Expand All @@ -19,15 +20,18 @@
"@sveltejs/vite-plugin-svelte": "^7.0.0",
"@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.2.2",
"@testing-library/svelte": "^5.4.2",
"@types/node": "^25.9.1",
"jsdom": "^30.0.1",
"prettier": "^3.8.1",
"prettier-plugin-svelte": "^3.5.1",
"prettier-plugin-tailwindcss": "^0.7.2",
"svelte": "^5.55.2",
"svelte-check": "^4.4.6",
"tailwindcss": "^4.2.2",
"typescript": "^6.0.2",
"vite": "^8.0.7"
"vite": "^8.0.7",
"vitest": "^5.0.0"
},
"dependencies": {
"instantsearch.js": "^4.98.0",
Expand Down
244 changes: 105 additions & 139 deletions typesense-sveltekit-search-app/src/lib/components/BookCard.svelte
Original file line number Diff line number Diff line change
@@ -1,150 +1,116 @@
<script lang="ts">
import type { Book } from '../types';
import type { Book } from '../types';

interface Props {
book: Book;
}
interface Props {
book: Book;
}

let { book }: Props = $props();

let imageError = $state(false);
let { book }: Props = $props();
</script>

<div class="bookCard">
<div class="bookImageContainer">
{#if book.image_url && !imageError}
<img
src={book.image_url}
alt={book.title}
class="bookImage"
onerror={() => imageError = true}
/>
{:else}
<div class="noImage">
<svg class="placeholderIcon" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"></path>
</svg>
<span>No Cover</span>
</div>
{/if}
</div>
<div class="bookInfo">
<h3 class="bookTitle">{book.title}</h3>
<p class="bookAuthor">By: {book.authors?.join(", ") || 'Unknown Author'}</p>
{#if book.publication_year}
<p class="bookYear">Published: {book.publication_year}</p>
{/if}
<div class="ratingContainer">
<div class="starRating">
{"★".repeat(Math.round(book.average_rating || 0))}
{"☆".repeat(5 - Math.round(book.average_rating || 0))}
</div>
<span class="ratingText">
{book.average_rating?.toFixed(1) || '0.0'}
</span>
{#if book.image_url}
<div class="bookImageContainer">
<img src={book.image_url} alt={`Cover of ${book.title}`} class="bookImage" />
</div>
</div>
{/if}
<div class="bookInfo">
<h3 class="bookTitle">{book.title}</h3>
<p class="bookAuthor">{book.authors?.join(', ') || 'Unknown Author'}</p>
<div class="ratingContainer">
<span class="starRating">
{'★'.repeat(Math.round(book.average_rating || 0))}
</span>
<span class="ratingText">
{book.average_rating?.toFixed(1) || '0'} ({book.ratings_count?.toLocaleString() || 0} ratings)
</span>
</div>
{#if book.publication_year}
<p class="bookYear">Published: {book.publication_year}</p>
{/if}
</div>
</div>

<style>
.bookCard {
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;
}

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

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

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

.noImage {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #9ca3af;
font-size: 0.875rem;
gap: 0.5rem;
background: #f3f4f6;
border: 1px dashed #d1d5db;
border-radius: 0.375rem;
}

.placeholderIcon {
width: 2rem;
height: 2rem;
stroke: #9ca3af;
}

.bookInfo {
flex: 1;
display: flex;
flex-direction: column;
}

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

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

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

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

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

.ratingText {
margin-left: 0.5rem;
font-size: 0.75rem;
color: #4b5563;
}
.bookCard {
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;
}

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

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

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

.bookInfo {
flex: 1;
display: flex;
flex-direction: column;
}

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

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

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

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

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

.ratingText {
margin-left: 0.5rem;
font-size: 0.75rem;
color: #4b5563;
}
</style>
48 changes: 48 additions & 0 deletions typesense-sveltekit-search-app/src/lib/components/BookCard.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { cleanup, render, screen } from '@testing-library/svelte';
import { afterEach, describe, expect, it } from 'vitest';
import BookCard from './BookCard.svelte';

afterEach(cleanup);

describe('BookCard', () => {
it('shows the same book details as the Solid card', () => {
render(BookCard, {
props: {
book: {
id: '1',
title: "Harry Potter and the Philosopher's Stone",
authors: ['J.K. Rowling'],
publication_year: 1997,
average_rating: 4.4,
image_url: 'https://example.com/book.jpg',
ratings_count: 4602479
}
}
});

expect(screen.getByAltText("Cover of Harry Potter and the Philosopher's Stone")).toBeTruthy();
expect(screen.getByText('J.K. Rowling')).toBeTruthy();
expect(screen.getByText('4.4 (4,602,479 ratings)')).toBeTruthy();
expect(screen.getByText('Published: 1997')).toBeTruthy();
expect(document.body.textContent).not.toContain('☆');
});

it('omits the cover region when a book has no image', () => {
const { container } = render(BookCard, {
props: {
book: {
id: '2',
title: 'A Book Without a Cover',
authors: ['Example Author'],
publication_year: 2020,
average_rating: 4,
image_url: '',
ratings_count: 10
}
}
});

expect(container.querySelector('img')).toBeNull();
expect(container.querySelector('.bookImageContainer')).toBeNull();
});
});
Loading