Skip to content
This repository was archived by the owner on Jul 19, 2022. It is now read-only.

Catalog: add search support #295

Merged
merged 1 commit into from
Jan 5, 2022
Merged
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: 1 addition & 0 deletions elm.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"elm-version": "0.19.1",
"dependencies": {
"direct": {
"NoRedInk/elm-simple-fuzzy": "1.0.3",
"elm/browser": "1.0.2",
"elm/core": "1.0.5",
"elm/html": "1.0.0",
Expand Down
5 changes: 5 additions & 0 deletions src/Project.elm
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ slug project =
FQN.cons (ownerToString project.owner) project.name


slugString : Project a -> String
slugString project =
project |> slug |> FQN.toString


ownerToString : Owner -> String
ownerToString (Owner o) =
o
Expand Down
20 changes: 20 additions & 0 deletions src/UnisonShare/Catalog.elm
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import FullyQualifiedName as FQN
import Json.Decode as Decode
import OrderedDict exposing (OrderedDict)
import Project exposing (ProjectListing)
import Simple.Fuzzy as Fuzzy
import UnisonShare.Catalog.CatalogMask as CatalogMask exposing (CatalogMask)


Expand Down Expand Up @@ -86,6 +87,25 @@ fromList items =
-- HELPERS


{-| Fuzzy search through a flattened catalog by project name and category
-}
search : Catalog -> String -> List ( ProjectListing, String )
search catalog_ query =
let
flat ( category, projects ) acc =
acc ++ List.map (\p -> ( p, category )) projects

normalize ( p, c ) =
p
|> Project.slugString
|> (++) c
in
catalog_
|> toList
|> List.foldl flat []
|> Fuzzy.filter normalize query


isEmpty : Catalog -> Bool
isEmpty (Catalog dict) =
OrderedDict.isEmpty dict
Expand Down
80 changes: 64 additions & 16 deletions src/UnisonShare/Page/Catalog.elm
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ module UnisonShare.Page.Catalog exposing (..)
import Api
import Env exposing (Env)
import FullyQualifiedName as FQN
import Html exposing (Html, a, div, h1, input, strong, text)
import Html exposing (Html, a, div, h1, input, span, strong, text)
import Html.Attributes exposing (autofocus, class, href, placeholder)
import Html.Events exposing (onInput)
import Html.Events exposing (onBlur, onFocus, onInput)
import Http
import Perspective
import Project exposing (ProjectListing)
Expand All @@ -25,6 +25,7 @@ import UnisonShare.Route as Route

type alias LoadedModel =
{ query : String
, hasFocus : Bool
, catalog : Catalog
}

Expand Down Expand Up @@ -65,6 +66,7 @@ fetchCatalog env =

type Msg
= UpdateQuery String
| UpdateFocus Bool
| ClearQuery
| NoOp
| FetchCatalogFinished (Result Http.Error Catalog)
Expand All @@ -79,7 +81,10 @@ update msg model =
( Failure e, Cmd.none )

Ok catalog ->
( Success { query = "", catalog = catalog }, Cmd.none )
( Success { query = "", hasFocus = True, catalog = catalog }, Cmd.none )

( UpdateFocus hasFocus, Success m ) ->
( Success { m | hasFocus = hasFocus }, Cmd.none )

( UpdateQuery query, Success m ) ->
( Success { m | query = query }, Cmd.none )
Expand All @@ -95,18 +100,18 @@ update msg model =
-- VIEW


projectUrl : ProjectListing -> String
projectUrl =
Route.forProject >> Route.toUrlString


viewProjectListing : ProjectListing -> Html msg
viewProjectListing project =
let
slug =
FQN.cons (Project.ownerToString project.owner) project.name

url =
project
|> Route.forProject
|> Route.toUrlString
Project.slug project
in
a [ class "project-listing", href url ] [ FQN.view slug ]
a [ class "project-listing", href (projectUrl project) ] [ FQN.view slug ]


viewCategory : ( String, List ProjectListing ) -> Html msg
Expand All @@ -120,13 +125,51 @@ viewCategory ( category, projects ) =
|> Card.view


viewSearchResult : ( ProjectListing, String ) -> Html msg
viewSearchResult ( project, category ) =
a
[ class "search-result", href (projectUrl project) ]
[ project |> Project.slug |> FQN.view
, span [ class "category" ] [ text category ]
]


viewSearchResults : LoadedModel -> Html msg
viewSearchResults model =
if String.length model.query > 3 then
let
results =
model.query
|> Catalog.search model.catalog
|> List.map viewSearchResult

resultsPane =
if List.isEmpty results then
[ div [ class "empty-state" ] [ text ("No matching projects found for \"" ++ model.query ++ "\"") ] ]

else
results
in
div [ class "search-results" ] resultsPane

else
UI.nothing


viewLoaded : LoadedModel -> PageLayout Msg
viewLoaded model =
let
categories =
model.catalog
|> Catalog.toList
|> List.map viewCategory

searchResults =
if model.hasFocus then
viewSearchResults model

else
UI.nothing
in
PageLayout.HeroLayout
{ hero =
Expand All @@ -144,13 +187,18 @@ viewLoaded model =
, div [] [ text "Projects, libraries, documention, terms, and types" ]
]
, div [ class "catalog-search" ]
[ Icon.view Icon.search
, input
[ placeholder "Search for projects"
, onInput UpdateQuery
, autofocus True
[ div [ class "search-field" ]
[ Icon.view Icon.search
, input
[ placeholder "Search for projects"
, onInput UpdateQuery
, autofocus True
, onBlur (UpdateFocus False)
, onFocus (UpdateFocus True)
]
[]
]
[]
, searchResults
]
]
)
Expand Down
101 changes: 88 additions & 13 deletions src/css/unison-share/page/catalog.css
Original file line number Diff line number Diff line change
Expand Up @@ -56,41 +56,100 @@

.catalog-hero .catalog-search {
width: 50rem;
height: 3.5rem;
background: var(--color-main-bg);
color: var(--color-main-fg);
border-radius: var(--border-radius-base);
position: absolute;
bottom: -1.75rem;
border: 2px solid var(--color-catalog-hero-bg);
top: calc(var(--page-hero-height) - 1.75rem);
border: 2px solid transparent;
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.1);
display: flex;
flex-direction: row;
align-items: center;
padding: 1rem 0 1rem 1rem;
flex-direction: column;
left: 50%;
transform: translateX(-50%);
z-index: var(--layer-base);
transition: all 0.2s;
background: var(--color-gray-lighten-55);
}

.catalog-hero .catalog-search:focus-within {
border-color: var(--color-main-focus-fg);
box-shadow: 0 0 0 2px var(--color-main-focus-outline);
}

.catalog-hero .catalog-search .search-field {
display: flex;
flex-direction: row;
align-items: center;
height: 3.5rem;
padding: 1rem 0 1rem 1rem;
border-radius: var(--border-radius-base);
}

.catalog-hero .catalog-search .search-field:focus-within {
background: var(--color-gray-lighten-60);
}

.catalog-hero .catalog-search .search-field .icon {
font-size: 1.5rem;
color: var(--color-main-subtle-fg);
}
.catalog-hero .catalog-search input {

.catalog-hero .catalog-search .search-field input {
width: 100%;
height: calc(3.5rem - 4px);
margin-left: 0.75rem;
font-size: 1.125rem;
border-radius: var(--border-radius-base);
font-weight: bold;
background: transparent;
}
.catalog-hero .catalog-search input:focus {

.catalog-hero .catalog-search .search-field input::placeholder {
font-weight: normal;
}

.catalog-hero .catalog-search .search-field input:focus {
outline: none;
}
.catalog-hero .catalog-search:focus-within {
border-color: var(--color-main-focus-fg);
box-shadow: 0 0 0 2px var(--color-main-focus-outline);

.catalog-hero .catalog-search .search-results {
background: var(--color-main-bg);
border-top: 1px solid var(--color-gray-lighten-50);
border-radius: 0 0 var(--border-radius-base) var(--border-radius-base);
display: flex;
flex-direction: column;
gap: 0.75rem;
padding: 0.75rem;
}

.catalog-hero .catalog-search .icon {
font-size: 1.5rem;
.catalog-hero .catalog-search .search-results .search-result {
display: flex;
flex-direction: row;
padding: 0.5rem 1rem;
align-items: center;
height: 3rem;
border-radius: var(--border-radius-base);
font-size: 1rem;
}

.catalog-hero .catalog-search .search-results .search-result:hover {
background: var(--color-gray-lighten-55);
text-decoration: none;
}

.catalog-hero .catalog-search .search-results .search-result .category {
color: var(--color-main-subtle-fg);
font-size: var(--font-size-medium);
margin-left: auto;
justify-self: flex-end;
}

.catalog-hero .catalog-search .search-results .empty-state {
font-size: var(--font-size-base);
color: var(--color-main-subtle-fg);
text-align: center;
padding: 1rem 0;
}

.categories {
Expand All @@ -103,4 +162,20 @@

.categories .card {
width: 18.75rem;
gap: 0.5rem;
}

.categories .card .project-listing {
display: flex;
flex-direction: row;
align-items: center;
height: 2rem;
padding: 0 0.25rem;
margin-left: -0.25rem;
border-radius: var(--border-radius-base);
}

.categories .card .project-listing:hover {
text-decoration: none;
background: var(--color-gray-lighten-55);
}
33 changes: 33 additions & 0 deletions tests/UnisonShare/CatalogTests.elm
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,39 @@ toList =
]


search : Test
search =
describe "Catalog.search"
[ test "Fuzzy finds projects in the catalog by project name " <|
\_ ->
let
projectListings_ =
[ baseListing, distributedListing, textExtraListing, nanoidListing ]

catalog_ =
Catalog.catalog catalogMask projectListings_
in
Expect.equal
[ ( baseListing, "Featured" )
, ( distributedListing, "Featured" )
]
(Catalog.search catalog_ "unison")
, test "Fuzzy finds projects in the catalog by category" <|
\_ ->
let
projectListings_ =
[ baseListing, distributedListing, textExtraListing, nanoidListing ]

catalog_ =
Catalog.catalog catalogMask projectListings_
in
Expect.equal
[ ( textExtraListing, "Parsers & Text Manipulation" )
]
(Catalog.search catalog_ "parsers")
]



-- helpers

Expand Down