Skip to content

Commit

Permalink
Feature/search (#32)
Browse files Browse the repository at this point in the history
* Fix some UI and project things

* API search

* search working
  • Loading branch information
andrewwippler authored Mar 27, 2023
1 parent b75891f commit 5ba50c3
Show file tree
Hide file tree
Showing 13 changed files with 222 additions and 28 deletions.
15 changes: 6 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,14 @@ The project aims to have an testable API which allows the use of multiuser authe
Need 3 terminals open to run:
1. `docker-compose up`
2. `cd api && yarn dev`
3. `cd frontend && yarn dev`
3. `cd frontend && yarn dev`

## Upgrading
## Migrating from Speaker-Illustrations

1. Clone this repository
2. Place .sql backup inside `./tmp/seeds`
3. `docker-compose up`
4. `docker-compose exec api bash`
5. `adonis migration:run`

TODO: Update user password
2. Place Speaker-Illustrations-backup.sql inside `./tmp/seeds`
3. run `docker-compose up`
4. run `cd api && node ace migration:run`

## Project Timeline

Expand All @@ -48,7 +45,7 @@ Version 0.4.0

Version 0.5.0

- User Preferences
- User Preferences (password, API key)
- Image uploads

Version 0.6.0
Expand Down
30 changes: 30 additions & 0 deletions api/app/Controllers/Http/SearchesController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import Illustration from 'App/Models/Illustration'
import Place from 'App/Models/Place'
import Tag from 'App/Models/Tag'
import { _ } from 'lodash'

export default class SearchesController {

public async search({ auth, request, response }: HttpContextContract) {

const { search } = request.all()

if (!search) {
return response.noContent()
}

const illustrations = await Illustration.query()
.where('title', search)
.orWhere('content', 'LIKE', `%${search}%`)
.orWhere('author', 'LIKE', `%${search}%`)
.andWhere('user_id', `${auth.user?.id}`)
const tagSanitizedSearch = _.startCase(search).replace(/ /g, '-')
const tags = await Tag.query().where('name',tagSanitizedSearch).andWhere('user_id', `${auth.user?.id}`)
const places = await Place.query().preload('illustration').where('place',search).andWhere('user_id', `${auth.user?.id}`)

// console.log({ message: 'success',user_id: `${auth.user?.id}`, searchString: search, data: { illustrations, places, tags } })

return response.send({ message: 'success', searchString: search, data: { illustrations, places, tags } })
}
}
8 changes: 5 additions & 3 deletions api/app/Models/Place.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@ export default class Place extends BaseModel {
@column.dateTime({ autoCreate: true, autoUpdate: true })
public updatedAt: DateTime

@belongsTo(() => Illustration)
public illustrations: BelongsTo<typeof Illustration>
@belongsTo(() => Illustration, {
foreignKey: 'illustration_id',
})
public illustration: BelongsTo<typeof Illustration>

@belongsTo(() => User)
public users: BelongsTo<typeof User>
public user: BelongsTo<typeof User>
}
2 changes: 1 addition & 1 deletion api/config/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ const databaseConfig: DatabaseConfig = {
naturalSort: true,
},
healthCheck: true,
debug: false,
debug: true,
seeders: {
paths: ['./database/seeders/MainSeeder']
}
Expand Down
3 changes: 3 additions & 0 deletions api/start/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ Route.group(() =>{
Route.put('/places/:id', 'PlacesController.update')
Route.delete('/places/:id', 'PlacesController.destroy')

//search
Route.post('/search', 'SearchesController.search')

// Images
// Route.post('/upload', async ({ request }: ) => {
// // to read: https://docs.adonisjs.com/guides/file-uploads
Expand Down
48 changes: 48 additions & 0 deletions api/tests/functional/search.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { test } from '@japa/runner'
import IllustrationFactory from 'Database/factories/IllustrationFactory'
import PlaceFactory from 'Database/factories/PlaceFactory'
import UserFactory from 'Database/factories/UserFactory'
import TagFactory from 'Database/factories/TagFactory'
import Database from '@ioc:Adonis/Lucid/Database'
let goodUser

test.group('Search', (group) => {
// Write your test here
group.each.setup(async () => {
await Database.beginGlobalTransaction()
return () => Database.rollbackGlobalTransaction()
})
group.setup(async () => {
goodUser = await UserFactory.merge({password: 'oasssadfasdf'}).create()
const illustration = await IllustrationFactory.merge({ title: 'Search Test', user_id: goodUser.id }).create()
const place = await PlaceFactory.merge({ illustration_id: illustration.id, place: 'Search Place', user_id: goodUser.id }).create()
const tag = await TagFactory.merge({ name: 'Search Tag', user_id: goodUser.id }).create()

// console.log(illustration.toJSON(),place.toJSON(),tag.toJSON())
})

group.teardown(async () => {
await goodUser.delete()
})

test('Can search for all', async ({ client }) => {
const loggedInUser = await client.post('/login').json({ email: goodUser.email, password: 'oasssadfasdf' })
const response = await client.post(`/search`).json({ search: 'Search' }).bearerToken(loggedInUser.body().token)
// console.log(response.body())
response.assertStatus(200)
response.assertBodyContains({message: "success"})
response.assertBodyContains({ searchString: "Search" })
// not working :(
// response.assertBodyContains({ data: { illustrations: [{title: 'Search Test',}] } })
// response.assertBodyContains({ data: { tags: [{name: 'Search Tag',}] } })
// response.assertBodyContains({ data: { places: [{place: 'Search Place',}] } })
})

test('No seach string', async ({ client }) => {
const loggedInUser = await client.post('/login').json({ email: goodUser.email, password: 'oasssadfasdf' })

const response = await client.post(`/search`).json({}).bearerToken(loggedInUser.body().token)
response.assertStatus(204)
})

})
2 changes: 1 addition & 1 deletion frontend/src/components/IllustrationForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import useUser from '@/library/useUser';
import { useRouter } from 'next/router'
import { illustrationType } from '@/library/illustrationType'
import { setIllustrationEdit, setUpdateUI } from '@/features/ui/reducer'
import TagSelect from './TagSelect/TagSelect'
import TagSelect from './TagSelect'
import { getFormattedTags } from '@/features/tags/reducer';

export default function IllustrationForm({ illustration }: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,22 +81,21 @@ export default function TagSelect({ defaultValue }:{ defaultValue: string | tagT

return (
<>
<div className=' text-white content-center text-sm flex flex-wrap mt-2 w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6'>
<div className='content-center text-sm flex flex-wrap mt-2 w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 sm:text-sm sm:leading-6'>
{illustrationTags && illustrationTags.map((tag, index) => {
return <>
return <div key={index} className='group'>
<div
key={index}
className="inline-flex items-center px-2 py-1 ml-2 mb-2 text-sm font-medium text-sky-800 bg-sky-100 rounded dark:bg-sky-900 dark:text-sky-300"
className="group-hover:text-white group-hover:bg-sky-900 inline-flex items-center px-2 py-1 ml-2 my-1 text-sm font-medium text-sky-800 bg-sky-200 rounded"
>
{tag.name}
<XMarkIcon onClick={e => handleTagRemove(tag.name)} className='ml-1 w-3.5 h-3.5 bg-sky-100' aria-hidden="true" />
<XMarkIcon onClick={e => handleTagRemove(tag.name)} className='group-hover:text-white group-hover:bg-sky-900 ml-1 w-3.5 h-3.5 bg-sky-200' aria-hidden="true" />
</div>
</div>
</>
})}
<input
name='tags'
placeholder='Add New Tag...'
className='clear-left ml-1 px-2 text-sky-900'
className='ml-1 px-2 text-sky-900 border-0 focus:ring-2 ring-inset focus:ring-inset focus:ring-indigo-600'
autoComplete='off'
onChange={e => search(e)}
onKeyDown={e => handleKeyPress(e) }
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/features/tags/reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ export const tagReducer = createSlice({
},
addTag: (state, actions) => {
// Remove duplicate tags, must include space to - conversion
if (!state.tags) {
state.tags = [actions.payload]
}
if (!state.tags.some(item => item.name === actions.payload.name.replace(/ /g, '-'))) {
state.tags = [...state.tags, actions.payload]
}
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/library/placeType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export type placeType = {
place: string,
location: string,
used: Date,
illustration_id: number
}
114 changes: 109 additions & 5 deletions frontend/src/pages/search.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
import React, { useState } from 'react'
import React, { FormEvent, useState } from 'react'
import useUser from '@/library/useUser'
import Layout from '@/components/Layout'
import Form from '@/components/Form'
import Link from 'next/link'
import { useAppDispatch } from '@/hooks'
import { setFlashMessage } from '@/features/flash/reducer'
import api from '@/library/api'
import { tagType } from '@/library/tagtype'
import { illustrationType } from '@/library/illustrationType'
import { placeType } from '@/library/placeType'

type dataReturn = {
illustrations: any
tags: any
places: any
message: string,
}

export default function Login() {
// here we just check if user is already logged in and redirect to profile
Expand All @@ -10,13 +23,104 @@ export default function Login() {
redirectTo: '/',
})

const dispatch = useAppDispatch()
const [data, setData] = useState<dataReturn | null>(null)
const [searched, setSearched] = useState('')

const onSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
let form = {
search: event.currentTarget.search.value.trim(),
}

api.post(`/search`, form)
.then(data => {

if (data.message != 'success') {
dispatch(setFlashMessage({ severity: 'danger', message: data.message }))
return
}
setData(data.data)
console.log(data.data)
setSearched(form.search)
});
}

// The UI does not allow the saving of an illustration without tags.
// If it does, then we need to have a listing of those illustrations here.
return (
<Layout>
<div className="login">
seatch
Illustrations without tags show up here ...
<div className="text-xl font-bold pb-4 text-sky-900">
<span className='mr-4'>Search</span>
</div>
<form className="space-y-6" onSubmit={onSubmit}>
<div className="flex col-span-6">
<label htmlFor="source" className="sr-only block font-medium leading-6 text-gray-900">
Search
</label>
<input
required
type="text"
name="search"
id="search"
placeholder="Search query for Illustration Title or Content, Tag, or Place used"
className="block w-full rounded-l-md border-0 py-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
/>
<button
type="submit"
className="min-w-fit justify-center rounded-r-md bg-indigo-300 px-4 font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500"
>
Search -.
</button>
</div>
</form>
<ul role="list">
{data && data.places && <div className="text-l font-bold pb-2 text-sky-900">
Places
</div>}
{data &&
data.places.length > 0 ? data.places.map((d: placeType, i: number) => (

<li key={i} className="group/item hover:bg-slate-200">
<Link className="block pb-1 group-hover/item:underline" href={`/illustration/${d.illustration_id}`}>{d.place}</Link>
</li>
))
:
searched && <div>No places found</div>
}
</ul>
<ul role="list">
{data && data.tags && <div className="text-l font-bold pb-2 text-sky-900">
Tags
</div>}
{data &&
data.tags.length > 0 ? data.tags.map((d: tagType, i: number) => (

<li key={i} className="group/item hover:bg-slate-200">
<Link className="block pb-1 group-hover/item:underline" href={`/tag/${d.name}`}>{d.name}</Link>
</li>
))
:
searched && <div>No tags found</div>
}
</ul>
<ul role="list">
{data && data.illustrations && <div className="text-l font-bold pb-2 text-sky-900">
Illustrations
</div>}
{data &&
data.illustrations.length > 0 ? data.illustrations.map((d: illustrationType, i: number) => (

<li key={i} className="group/item hover:bg-slate-200">
<Link className="block pb-1 group-hover/item:underline" href={`/illustration/${d.id}`} dangerouslySetInnerHTML={{ __html: d.title.replace(new RegExp(`${searched}`, 'gi'), `<span class='font-bold'>${searched}</span>`) }}></Link>
<div className='invisible h-0 group-hover/item:h-auto group-hover/item:visible' dangerouslySetInnerHTML={{ __html: d.content.slice(0, 256).replace(new RegExp(`${searched}`, 'gi'), `<span class='font-bold'>${searched}</span>`)+'...' }}>
</div>
</li>
))
:
searched && <div>No illustrations found</div>
}
</ul>
</Layout>
)
}
2 changes: 1 addition & 1 deletion frontend/src/pages/tag/[name].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ export default function Tag() {
<li key={i} className="group/item hover:bg-slate-200">
<Link className="block pb-1 group-hover/item:underline" href={`/illustration/${d.id}`}>{d.title}</Link>
<div className='invisible h-0 group-hover/item:h-auto group-hover/item:visible'>
{d.content.substr(0,256)}...
{d.content.slice(0,256)}...
</div>
</li>
))
Expand Down
4 changes: 3 additions & 1 deletion frontend/tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
module.exports = {
content: ["./src/**/*.{js,ts,jsx,tsx}",],
theme: {
extend: {},
extend: {
textColor: ['group-hover'],
},
},
plugins: [require('@tailwindcss/forms'),],
}

0 comments on commit 5ba50c3

Please sign in to comment.