Skip to content

Commit

Permalink
Merge pull request #58 from saleem-hadad/feat/add-search-for-brands
Browse files Browse the repository at this point in the history
add brand search functionality #feature
  • Loading branch information
saleem-hadad authored Mar 28, 2024
2 parents 4a3f3c4 + 8e10c72 commit 9d1e324
Show file tree
Hide file tree
Showing 9 changed files with 169 additions and 39 deletions.
19 changes: 17 additions & 2 deletions app/Models/Brand.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

namespace App\Models;

use App\Contracts\Searchable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;

class Brand extends Model
class Brand extends Model implements Searchable
{
use HasFactory;

protected $guarded = [];

protected static function booted()
Expand Down Expand Up @@ -39,4 +41,17 @@ public static function findOrCreateNew($name)

return static::create(['name' => $name]);
}

/**
* @param $query
* @return Builder
*/
public static function search($query): Builder
{
return (new static())->newQuery()
->where('name', 'LIKE', "%$query%")
->orWhereHas('category', function($builder) use($query) {
return $builder->where('name', 'LIKE', "%$query%");
});
}
}
64 changes: 64 additions & 0 deletions config/graphiql.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php declare(strict_types=1);

return [
/*
|--------------------------------------------------------------------------
| Routes configuration
|--------------------------------------------------------------------------
|
| Set the key as URI at which the GraphiQL UI can be viewed,
| and add any additional configuration for the route.
|
| You can add multiple routes pointing to different GraphQL endpoints.
|
*/

'routes' => [
'/graphiql' => [
'name' => 'graphiql',
// 'middleware' => ['web']
// 'prefix' => '',
// 'domain' => 'graphql.' . env('APP_DOMAIN', 'localhost'),

/*
|--------------------------------------------------------------------------
| Default GraphQL endpoint
|--------------------------------------------------------------------------
|
| The default endpoint that the GraphiQL UI is set to.
| It assumes you are running GraphQL on the same domain
| as GraphiQL, but can be set to any URL.
|
*/

'endpoint' => '/graphql',

/*
|--------------------------------------------------------------------------
| Subscription endpoint
|--------------------------------------------------------------------------
|
| The default subscription endpoint the GraphiQL UI uses to connect to.
| Tries to connect to the `endpoint` value if `null` as ws://{{endpoint}}
|
| Example: `ws://your-endpoint` or `wss://your-endpoint`
|
*/

'subscription-endpoint' => env('GRAPHIQL_SUBSCRIPTION_ENDPOINT', null),
],
],

/*
|--------------------------------------------------------------------------
| Control GraphiQL availability
|--------------------------------------------------------------------------
|
| Control if the GraphiQL UI is accessible at all.
| This allows you to disable it in certain environments,
| for example you might not want it active in production.
|
*/

'enabled' => env('GRAPHIQL_ENABLED', false),
];
2 changes: 1 addition & 1 deletion graphql/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ type Query {
@orderBy(column: id direction: DESC)

allBrands: [Brand!]! @all
brands: [Brand!]!
brands(search: String @search): [Brand!]!
@paginate(defaultCount: 50)
@lazyLoad(relations: ["category"])
@orderBy(column: id direction: DESC)
Expand Down
2 changes: 1 addition & 1 deletion public/js/app.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion public/mix-manifest.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"/js/app.js": "/js/app.js?id=86df5ca868b7e285c1c332bbe2d96034",
"/js/app.js": "/js/app.js?id=e46970d4146019f17f019679336faf94",
"/css/app.css": "/css/app.css?id=a29af113c48845a119a56553951013db"
}
4 changes: 2 additions & 2 deletions resources/js/Api/brands.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ export const getAllBrands = () => {
.toPromise();
}

export const getBrands = (page) => {
export const getBrands = (page, searchQuery) => {
return client
.query(gql`
query {
brands(page: ${page}) {
brands(search: """${searchQuery}""" page: ${page}) {
data {
id
name
Expand Down
61 changes: 49 additions & 12 deletions resources/js/Pages/Brand/Index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, {useEffect, useMemo, useState} from 'react';
import { PencilAltIcon, TrashIcon } from '@heroicons/react/outline';
import { Head } from '@inertiajs/inertia-react';

Expand All @@ -10,12 +10,14 @@ import Button from '@/Components/Global/Button';
import Delete from '@/Components/Domain/Delete';
import { getAllCategories, getBrands } from '@/Api';
import { animateRowItem } from '@/Utils';
import {debounce} from "lodash";

export default function Index({auth}) {
const [brands, setBrands] = useState([]);
const [allCategories, setAllCategories] = useState([]);
const [currentPage, setCurrentPage] = useState(1);
const [hasMorePages, setHasMorePages] = useState(true);
const [searchQuery, setSearchQuery] = useState('');
const [loading, setLoading] = useState(false);
const [editItem, setEditItem] = useState(null);
const [showCreate, setShowCreate] = useState(false);
Expand All @@ -33,7 +35,7 @@ export default function Index({auth}) {
if(! hasMorePages) return;
setLoading(true);

getBrands(currentPage)
getBrands(currentPage, searchQuery)
.then(({data}) => {
setBrands([...brands, ...data.brands.data])
setHasMorePages(data.brands.paginatorInfo.hasMorePages)
Expand All @@ -42,6 +44,18 @@ export default function Index({auth}) {
.catch(console.error);
}, [currentPage]);

useEffect(() => {
setLoading(true);

getBrands(currentPage, searchQuery)
.then(({data}) => {
setBrands([...brands, ...data.brands.data])
setHasMorePages(data.brands.paginatorInfo.hasMorePages)
setLoading(false);
})
.catch(console.error);
}, [searchQuery]);

const onCreate = (createdItem) => {
setShowCreate(false)
setBrands([createdItem, ...brands])
Expand Down Expand Up @@ -69,17 +83,38 @@ export default function Index({auth}) {
})
}

return (
<Authenticated auth={auth}
header={
<div className='flex justify-between items-center'>
<h2 className="font-semibold text-xl text-gray-800 leading-tight">
Brands
</h2>

<Button children={"Create Brand"} type="button" onClick={() => setShowCreate(true)} />
const performSearchHandler = (e) => {
setBrands([]);
setSearchQuery(e.target.value ?? '');
setCurrentPage(1);
}

const performSearch = useMemo(
() => debounce(performSearchHandler, 300)
, []);

const header = <div className="w-full pb-3 mb-4 px-4 sm:px-0">
<h2 className='text-lg text-gray-600'>Brands</h2>

<div className='flex justify-between items-center mt-2'>
<div>
<div className="relative flex items-center">
<input
type="text"
name="search"
placeholder='🔍 Search'
onChange={performSearch}
className="block w-full rounded-full border-0 py-1.5 pr-14 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-500 sm:text-sm sm:leading-6"
/>
</div>
}>
</div>

<Button children={"Create Brand"} type="button" onClick={() => setShowCreate(true)} />
</div>
</div>

return (
<Authenticated auth={auth}>
<Head title="Brands" />

<Create showCreate={showCreate}
Expand All @@ -103,6 +138,8 @@ export default function Index({auth}) {

<div className="py-12">
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
{header}

<div className="flex flex-col">
{brands.length > 0 && <div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
Expand Down
41 changes: 21 additions & 20 deletions resources/js/Pages/Transaction/Index.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,32 @@ export default function Index({auth}) {
setTransactions([]);
setSearchQuery(e.target.value ?? '');
setCurrentPage(1);
setHasMorePages(true);
}

const performSearch = useMemo(
() => debounce(performSearchHandler, 300)
, []);

const header = <div className="w-full pb-3 mb-4 px-4 sm:px-0">
<h2 className='text-lg text-gray-600'>Transactions</h2>

<div className='flex justify-between items-center mt-2'>
<div>
<div className="relative flex items-center">
<input
type="text"
name="search"
placeholder='🔍 Search'
onChange={performSearch}
className="block w-full rounded-full border-0 py-1.5 pr-14 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-500 sm:text-sm sm:leading-6"
/>
</div>
</div>

<Button children={"Create Transaction"} type="button" onClick={() => setShowCreate(true)} />
</div>
</div>

return (
<Authenticated auth={auth}>
<Head title="Transactions" />
Expand All @@ -117,25 +136,7 @@ export default function Index({auth}) {

<div className="py-12">
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div className="w-full pb-3 mb-4 px-4 sm:px-0">
<h2 className='text-lg text-gray-600'>Transactions</h2>

<div className='flex justify-between items-center mt-2'>
<div>
<div className="relative flex items-center">
<input
type="text"
name="search"
placeholder='🔍 Search'
onChange={performSearch}
className="block w-full rounded-full border-0 py-1.5 pr-14 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-500 sm:text-sm sm:leading-6"
/>
</div>
</div>

<Button children={"Create Transaction"} type="button" onClick={() => setShowCreate(true)} />
</div>
</div>
{header}

<div className="flex flex-col">
{transactions.length > 0 && <div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
Expand Down
13 changes: 13 additions & 0 deletions tests/Unit/Models/Brands/BrandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,17 @@ public function it_has_transactions()

$this->assertCount(1, $sut->transactions);
}

/** @test */
public function is_does_search_about_amount_brand_or_note()
{
Brand::factory()->forCategory(['name' => 'internet'])->create(['name' => 'google']);
Brand::factory()->forCategory(['name' => 'shopping'])->create(['name' => 'ikea']);
Brand::factory()->forCategory(['name' => 'shopping'])->create(['name' => 'lulu']);

$this->assertCount(1, Brand::search('goo')->get());
$this->assertCount(1, Brand::search('internet')->get());
$this->assertCount(2, Brand::search('shopping')->get());
$this->assertCount(0, Brand::search('other')->get());
}
}

0 comments on commit 9d1e324

Please sign in to comment.