Skip to content

Commit 007d8bb

Browse files
committed
Server-side search/filtering
1 parent 6674eda commit 007d8bb

File tree

8 files changed

+218
-54
lines changed

8 files changed

+218
-54
lines changed

app/Http/Controllers/PuppyController.php

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,23 @@
1010
class PuppyController extends Controller
1111
{
1212

13-
public function index()
13+
public function index(Request $request)
1414
{
15+
$search = $request->search;
16+
1517
return Inertia::render('puppies/index', [
16-
'puppies' => PuppyResource::collection(Puppy::all()->load(['user', 'likedBy'])),
18+
'puppies' => PuppyResource::collection(
19+
Puppy::query()
20+
->when($search, function ($query, $search) {
21+
$query->where('name', 'like', "%{$search}%")
22+
->orWhere('trait', 'like', "%{$search}%");
23+
})
24+
->with(['user', 'likedBy'])
25+
->get()
26+
),
27+
'filters' => [
28+
'search' => $search,
29+
],
1730
]);
1831
}
1932

database/seeders/PuppySeeder.php

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
use App\Models\Puppy;
66
use App\Models\User;
7-
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
87
use Illuminate\Database\Seeder;
98
use Illuminate\Support\Facades\Storage;
109

@@ -25,6 +24,115 @@ public function run(): void
2524
['name' => 'Leia', 'trait' => 'Enjoys naps', 'image' => '3.jpg'],
2625
['name' => 'Chase', 'trait' => 'Very good boi', 'image' => '2.jpg'],
2726
['name' => 'Frisket', 'trait' => 'Mother of all pups', 'image' => '1.jpg'],
27+
['name' => 'Max', 'trait' => 'Loves to play fetch', 'image' => '11.jpg'],
28+
['name' => 'Buddy', 'trait' => 'Loyal companion', 'image' => '12.jpg'],
29+
['name' => 'Charlie', 'trait' => 'Friendly and outgoing', 'image' => '13.jpg'],
30+
['name' => 'Cooper', 'trait' => 'Great with kids', 'image' => '14.jpg'],
31+
['name' => 'Rocky', 'trait' => 'Strong and brave', 'image' => '15.jpg'],
32+
['name' => 'Bear', 'trait' => 'Gentle giant', 'image' => '16.jpg'],
33+
['name' => 'Duke', 'trait' => 'Noble and proud', 'image' => '17.jpg'],
34+
['name' => 'Jack', 'trait' => 'Playful and energetic', 'image' => '18.jpg'],
35+
['name' => 'Tucker', 'trait' => 'Loves treats', 'image' => '19.jpg'],
36+
['name' => 'Oliver', 'trait' => 'Curious explorer', 'image' => '20.jpg'],
37+
['name' => 'Toby', 'trait' => 'Sweet and gentle', 'image' => '21.jpg'],
38+
['name' => 'Buster', 'trait' => 'Full of energy', 'image' => '22.jpg'],
39+
['name' => 'Lucky', 'trait' => 'Always finds treats', 'image' => '1.jpg'],
40+
['name' => 'Milo', 'trait' => 'Loves belly rubs', 'image' => '2.jpg'],
41+
['name' => 'Jake', 'trait' => 'Protective guardian', 'image' => '3.jpg'],
42+
['name' => 'Bruno', 'trait' => 'Loves to swim', 'image' => '4.jpg'],
43+
['name' => 'Oscar', 'trait' => 'Dignified and calm', 'image' => '5.jpg'],
44+
['name' => 'Rusty', 'trait' => 'Loves the outdoors', 'image' => '6.jpg'],
45+
['name' => 'Zeus', 'trait' => 'Mighty and powerful', 'image' => '7.jpg'],
46+
['name' => 'Leo', 'trait' => 'Brave like a lion', 'image' => '8.jpg'],
47+
['name' => 'Finn', 'trait' => 'Loves adventure', 'image' => '9.jpg'],
48+
['name' => 'Ace', 'trait' => 'Top of his class', 'image' => '10.jpg'],
49+
['name' => 'Gus', 'trait' => 'Loves to cuddle', 'image' => '11.jpg'],
50+
['name' => 'Cody', 'trait' => 'Great listener', 'image' => '12.jpg'],
51+
['name' => 'Jasper', 'trait' => 'Loves to dig', 'image' => '13.jpg'],
52+
['name' => 'Bandit', 'trait' => 'Mischievous spirit', 'image' => '14.jpg'],
53+
['name' => 'Scout', 'trait' => 'Always exploring', 'image' => '15.jpg'],
54+
['name' => 'Sammy', 'trait' => 'Loves everyone', 'image' => '16.jpg'],
55+
['name' => 'Dexter', 'trait' => 'Smart and quick', 'image' => '17.jpg'],
56+
['name' => 'Ziggy', 'trait' => 'Playful clown', 'image' => '18.jpg'],
57+
['name' => 'Diesel', 'trait' => 'Strong and sturdy', 'image' => '19.jpg'],
58+
['name' => 'Harley', 'trait' => 'Loves car rides', 'image' => '20.jpg'],
59+
['name' => 'Bentley', 'trait' => 'Sophisticated pup', 'image' => '21.jpg'],
60+
['name' => 'Murphy', 'trait' => 'Loves to chase', 'image' => '22.jpg'],
61+
['name' => 'Riley', 'trait' => 'Happy-go-lucky', 'image' => '1.jpg'],
62+
['name' => 'Baxter', 'trait' => 'Loves to bark', 'image' => '2.jpg'],
63+
['name' => 'Hank', 'trait' => 'Country dog at heart', 'image' => '3.jpg'],
64+
['name' => 'Louie', 'trait' => 'Loves music', 'image' => '4.jpg'],
65+
['name' => 'Ollie', 'trait' => 'Loves to roll over', 'image' => '5.jpg'],
66+
['name' => 'Rocco', 'trait' => 'Tough but sweet', 'image' => '6.jpg'],
67+
['name' => 'Peanut', 'trait' => 'Small but mighty', 'image' => '7.jpg'],
68+
['name' => 'Moose', 'trait' => 'Big and friendly', 'image' => '8.jpg'],
69+
['name' => 'Oreo', 'trait' => 'Black and white beauty', 'image' => '9.jpg'],
70+
['name' => 'Pepper', 'trait' => 'Spicy personality', 'image' => '10.jpg'],
71+
['name' => 'Koda', 'trait' => 'Loves the forest', 'image' => '11.jpg'],
72+
['name' => 'Copper', 'trait' => 'Shiny and bright', 'image' => '12.jpg'],
73+
['name' => 'Marley', 'trait' => 'Loves reggae', 'image' => '13.jpg'],
74+
['name' => 'Benny', 'trait' => 'Always cheerful', 'image' => '14.jpg'],
75+
['name' => 'Teddy', 'trait' => 'Soft and cuddly', 'image' => '15.jpg'],
76+
['name' => 'Winston', 'trait' => 'Distinguished gentleman', 'image' => '16.jpg'],
77+
['name' => 'Gunner', 'trait' => 'Ready for action', 'image' => '17.jpg'],
78+
['name' => 'Ranger', 'trait' => 'Loves the wilderness', 'image' => '18.jpg'],
79+
['name' => 'Boomer', 'trait' => 'Loud and proud', 'image' => '19.jpg'],
80+
['name' => 'Titan', 'trait' => 'Larger than life', 'image' => '20.jpg'],
81+
['name' => 'Champ', 'trait' => 'Winner at everything', 'image' => '21.jpg'],
82+
['name' => 'Blaze', 'trait' => 'Fast as lightning', 'image' => '22.jpg'],
83+
['name' => 'Hunter', 'trait' => 'Great tracker', 'image' => '1.jpg'],
84+
['name' => 'Storm', 'trait' => 'Powerful presence', 'image' => '2.jpg'],
85+
['name' => 'Shadow', 'trait' => 'Follows everywhere', 'image' => '3.jpg'],
86+
['name' => 'Spike', 'trait' => 'Tough exterior, soft heart', 'image' => '4.jpg'],
87+
['name' => 'Dash', 'trait' => 'Super speedy', 'image' => '5.jpg'],
88+
['name' => 'Chief', 'trait' => 'Natural leader', 'image' => '6.jpg'],
89+
['name' => 'Maple', 'trait' => 'Sweet as syrup', 'image' => '7.jpg'],
90+
['name' => 'Daisy', 'trait' => 'Bright and cheerful', 'image' => '8.jpg'],
91+
['name' => 'Rosie', 'trait' => 'Pretty in pink', 'image' => '9.jpg'],
92+
['name' => 'Coco', 'trait' => 'Rich and smooth', 'image' => '10.jpg'],
93+
['name' => 'Honey', 'trait' => 'Sweet disposition', 'image' => '11.jpg'],
94+
['name' => 'Ruby', 'trait' => 'Precious gem', 'image' => '12.jpg'],
95+
['name' => 'Stella', 'trait' => 'Shines bright', 'image' => '13.jpg'],
96+
['name' => 'Zoe', 'trait' => 'Full of life', 'image' => '14.jpg'],
97+
['name' => 'Penny', 'trait' => 'Worth her weight in gold', 'image' => '15.jpg'],
98+
['name' => 'Sadie', 'trait' => 'Sweet and loyal', 'image' => '16.jpg'],
99+
['name' => 'Molly', 'trait' => 'Loves to play', 'image' => '17.jpg'],
100+
['name' => 'Maggie', 'trait' => 'Motherly instincts', 'image' => '18.jpg'],
101+
['name' => 'Sophie', 'trait' => 'Wise beyond her years', 'image' => '19.jpg'],
102+
['name' => 'Chloe', 'trait' => 'Elegant and graceful', 'image' => '20.jpg'],
103+
['name' => 'Lily', 'trait' => 'Pure and innocent', 'image' => '21.jpg'],
104+
['name' => 'Mia', 'trait' => 'Small but fierce', 'image' => '22.jpg'],
105+
['name' => 'Nala', 'trait' => 'Regal and proud', 'image' => '1.jpg'],
106+
['name' => 'Piper', 'trait' => 'Loves to sing', 'image' => '2.jpg'],
107+
['name' => 'Gracie', 'trait' => 'Graceful dancer', 'image' => '3.jpg'],
108+
['name' => 'Hazel', 'trait' => 'Beautiful eyes', 'image' => '4.jpg'],
109+
['name' => 'Ivy', 'trait' => 'Loves to climb', 'image' => '5.jpg'],
110+
['name' => 'Willow', 'trait' => 'Flexible and adaptable', 'image' => '6.jpg'],
111+
['name' => 'Autumn', 'trait' => 'Loves fall leaves', 'image' => '7.jpg'],
112+
['name' => 'Ginger', 'trait' => 'Spicy personality', 'image' => '8.jpg'],
113+
['name' => 'Paisley', 'trait' => 'Unique patterns', 'image' => '9.jpg'],
114+
['name' => 'Roxy', 'trait' => 'Rock and roll spirit', 'image' => '10.jpg'],
115+
['name' => 'Lexi', 'trait' => 'Smart and quick', 'image' => '11.jpg'],
116+
['name' => 'Ellie', 'trait' => 'Loves elephants', 'image' => '12.jpg'],
117+
['name' => 'Abby', 'trait' => 'Loves to hug', 'image' => '13.jpg'],
118+
['name' => 'Kenzie', 'trait' => 'Scottish heritage', 'image' => '14.jpg'],
119+
['name' => 'Lacey', 'trait' => 'Delicate and pretty', 'image' => '15.jpg'],
120+
['name' => 'Misty', 'trait' => 'Mysterious aura', 'image' => '16.jpg'],
121+
['name' => 'Dixie', 'trait' => 'Southern charm', 'image' => '17.jpg'],
122+
['name' => 'Sasha', 'trait' => 'Russian beauty', 'image' => '18.jpg'],
123+
['name' => 'Bonnie', 'trait' => 'Beautiful Scottish lass', 'image' => '19.jpg'],
124+
['name' => 'Trixie', 'trait' => 'Loves tricks', 'image' => '20.jpg'],
125+
['name' => 'Nikki', 'trait' => 'Victory in Greek', 'image' => '21.jpg'],
126+
['name' => 'Sheba', 'trait' => 'Queen of the pack', 'image' => '22.jpg'],
127+
['name' => 'Fiona', 'trait' => 'Fair and beautiful', 'image' => '1.jpg'],
128+
['name' => 'Cassie', 'trait' => 'Loves to explore', 'image' => '2.jpg'],
129+
['name' => 'Josie', 'trait' => 'Cheerful companion', 'image' => '3.jpg'],
130+
['name' => 'Minnie', 'trait' => 'Small and sweet', 'image' => '4.jpg'],
131+
['name' => 'Dolly', 'trait' => 'Loves to dress up', 'image' => '5.jpg'],
132+
['name' => 'Fancy', 'trait' => 'Always dressed to impress', 'image' => '6.jpg'],
133+
['name' => 'Princess', 'trait' => 'Royalty in the making', 'image' => '7.jpg'],
134+
['name' => 'Angel', 'trait' => 'Heavenly disposition', 'image' => '8.jpg'],
135+
['name' => 'Diamond', 'trait' => 'Precious and rare', 'image' => '9.jpg'],
28136
];
29137

30138
$simon = User::first();

package-lock.json

Lines changed: 23 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
"@radix-ui/react-toggle-group": "^1.1.2",
4040
"@radix-ui/react-tooltip": "^1.1.8",
4141
"@tailwindcss/vite": "^4.0.6",
42+
"@types/lodash-es": "^4.17.12",
4243
"@types/react": "^19.0.3",
4344
"@types/react-dom": "^19.0.2",
4445
"@vitejs/plugin-react": "^4.3.4",
@@ -47,6 +48,7 @@
4748
"concurrently": "^9.0.1",
4849
"globals": "^15.14.0",
4950
"laravel-vite-plugin": "^1.0",
51+
"lodash-es": "^4.17.21",
5052
"lucide-react": "^0.475.0",
5153
"react": "^19.0.0",
5254
"react-dom": "^19.0.0",

resources/js/components/PuppiesList.tsx

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
import { type Puppy } from '../types';
22
import { LikeToggle } from './LikeToggle';
33

4-
export function PuppiesList({ searchQuery, puppies }: { searchQuery: string; puppies: Puppy[] }) {
4+
export function PuppiesList({ puppies }: { puppies: Puppy[] }) {
55
return (
66
<ul className="mt-8 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
7-
{puppies
8-
.filter((pup) => pup.trait.toLowerCase().includes(searchQuery.toLowerCase()))
9-
.map((puppy) => (
10-
<PuppyCard key={puppy.id} puppy={puppy} />
11-
))}
7+
{puppies.map((puppy) => (
8+
<PuppyCard key={puppy.id} puppy={puppy} />
9+
))}
1210
</ul>
1311
);
1412
}

resources/js/components/Search.tsx

Lines changed: 54 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,56 @@
1-
import { Delete } from "lucide-react";
2-
import { Dispatch, SetStateAction, useRef } from "react";
1+
import { Filters } from '@/types';
2+
import { router } from '@inertiajs/react';
3+
import { debounce } from 'lodash-es';
4+
import { Delete } from 'lucide-react';
5+
import { useRef } from 'react';
36

4-
export function Search({
5-
searchQuery,
6-
setSearchQuery,
7-
}: {
8-
searchQuery: string;
9-
setSearchQuery: Dispatch<SetStateAction<string>>;
10-
}) {
11-
const inputRef = useRef(null);
12-
return (
13-
<div>
14-
<label htmlFor="search" className="font-medium">
15-
Search for a character trait
16-
</label>
17-
<div className="mt-2 flex items-center gap-4">
18-
<input
19-
ref={inputRef}
20-
value={searchQuery}
21-
onChange={(e) => setSearchQuery(e.target.value)}
22-
placeholder="playful..."
23-
name="search"
24-
id="search"
25-
type="text"
26-
className="w-full max-w-80 bg-white px-4 py-2 ring ring-black/5 focus:ring-2 focus:ring-cyan-500 focus:outline-none"
27-
/>
28-
<button
29-
onClick={() => {
30-
setSearchQuery("");
31-
inputRef.current.focus();
32-
}}
33-
className="inline-block rounded bg-cyan-300 px-4 py-2 !pr-3 !pl-2.5 font-medium text-cyan-900 hover:bg-cyan-200 focus:ring-2 focus:ring-cyan-500 focus:outline-none"
34-
>
35-
<Delete />
36-
</button>
37-
</div>
38-
</div>
39-
);
7+
export function Search({ filters }: { filters: Filters }) {
8+
const inputRef = useRef<HTMLInputElement>(null);
9+
return (
10+
<div>
11+
<label htmlFor="search" className="font-medium">
12+
Search for a character trait
13+
</label>
14+
<div className="mt-2 flex items-center gap-4">
15+
<input
16+
defaultValue={filters.search}
17+
ref={inputRef}
18+
onChange={debounce((e) => {
19+
router.get(
20+
route('home'),
21+
{ search: e.target.value },
22+
{
23+
preserveState: true,
24+
preserveScroll: true,
25+
},
26+
);
27+
}, 300)}
28+
placeholder="playful..."
29+
name="search"
30+
id="search"
31+
type="text"
32+
className="w-full max-w-80 bg-white px-4 py-2 ring ring-black/5 focus:ring-2 focus:ring-cyan-500 focus:outline-none"
33+
/>
34+
<button
35+
onClick={() => {
36+
router.get(
37+
route('home'),
38+
{},
39+
{
40+
preserveState: true,
41+
preserveScroll: true,
42+
onSuccess: () => {
43+
inputRef.current!.value = '';
44+
inputRef.current?.focus();
45+
},
46+
},
47+
);
48+
}}
49+
className="inline-block rounded bg-cyan-300 px-4 py-2 !pr-3 !pl-2.5 font-medium text-cyan-900 hover:bg-cyan-200 focus:ring-2 focus:ring-cyan-500 focus:outline-none"
50+
>
51+
<Delete />
52+
</button>
53+
</div>
54+
</div>
55+
);
4056
}

resources/js/pages/puppies/index.tsx

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,33 +6,32 @@ import { PuppiesList } from '@/components/PuppiesList';
66
import { Search } from '@/components/Search';
77
import { Shortlist } from '@/components/Shortlist';
88

9-
import { Puppy, SharedData } from '@/types';
9+
import { Filters, Puppy, SharedData } from '@/types';
1010
import { usePage } from '@inertiajs/react';
1111
import { useState } from 'react';
1212

13-
export default function App({ puppies }: { puppies: Puppy[] }) {
13+
export default function App({ puppies, filters }: { puppies: Puppy[]; filters: Filters }) {
1414
return (
1515
<PageWrapper>
1616
<Container>
1717
<Header />
18-
<Main inertiaPuppies={puppies} />
18+
<Main inertiaPuppies={puppies} filters={filters} />
1919
</Container>
2020
</PageWrapper>
2121
);
2222
}
2323

24-
function Main({ inertiaPuppies }: { inertiaPuppies: Puppy[] }) {
25-
const [searchQuery, setSearchQuery] = useState('');
24+
function Main({ inertiaPuppies, filters }: { inertiaPuppies: Puppy[]; filters: Filters }) {
2625
const [puppies, setPuppies] = useState<Puppy[]>(inertiaPuppies);
2726
const { auth } = usePage<SharedData>().props;
2827

2928
return (
3029
<main>
3130
<div className="mt-24 grid gap-8 sm:grid-cols-2">
32-
<Search searchQuery={searchQuery} setSearchQuery={setSearchQuery} />
31+
<Search filters={filters} />
3332
{auth.user && <Shortlist puppies={inertiaPuppies} />}
3433
</div>
35-
<PuppiesList puppies={inertiaPuppies} searchQuery={searchQuery} />
34+
<PuppiesList puppies={inertiaPuppies} />
3635
<NewPuppyForm puppies={inertiaPuppies} setPuppies={setPuppies} />
3736
</main>
3837
);

resources/js/types/index.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,8 @@ export interface Puppy {
5050
user: Pick<User, 'id' | 'name'>;
5151
likedBy: User['id'][];
5252
}
53+
54+
export interface Filters {
55+
search?: string;
56+
[key: string]: unknown;
57+
}

0 commit comments

Comments
 (0)