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

Commit c2b4627

Browse files
committed
Catalog: add search support
Add support for fuzzy searching the catalog
1 parent a9a8c9f commit c2b4627

File tree

6 files changed

+211
-29
lines changed

6 files changed

+211
-29
lines changed

elm.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"elm-version": "0.19.1",
77
"dependencies": {
88
"direct": {
9+
"NoRedInk/elm-simple-fuzzy": "1.0.3",
910
"elm/browser": "1.0.2",
1011
"elm/core": "1.0.5",
1112
"elm/html": "1.0.0",

src/Project.elm

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ slug project =
2222
FQN.cons (ownerToString project.owner) project.name
2323

2424

25+
slugString : Project a -> String
26+
slugString project =
27+
project |> slug |> FQN.toString
28+
29+
2530
ownerToString : Owner -> String
2631
ownerToString (Owner o) =
2732
o

src/UnisonShare/Catalog.elm

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import FullyQualifiedName as FQN
55
import Json.Decode as Decode
66
import OrderedDict exposing (OrderedDict)
77
import Project exposing (ProjectListing)
8+
import Simple.Fuzzy as Fuzzy
89
import UnisonShare.Catalog.CatalogMask as CatalogMask exposing (CatalogMask)
910

1011

@@ -86,6 +87,25 @@ fromList items =
8687
-- HELPERS
8788

8889

90+
{-| Fuzzy search through a flattened catalog by project name and category
91+
-}
92+
search : Catalog -> String -> List ( ProjectListing, String )
93+
search catalog_ query =
94+
let
95+
flat ( category, projects ) acc =
96+
acc ++ List.map (\p -> ( p, category )) projects
97+
98+
normalize ( p, c ) =
99+
p
100+
|> Project.slugString
101+
|> (++) c
102+
in
103+
catalog_
104+
|> toList
105+
|> List.foldl flat []
106+
|> Fuzzy.filter normalize query
107+
108+
89109
isEmpty : Catalog -> Bool
90110
isEmpty (Catalog dict) =
91111
OrderedDict.isEmpty dict

src/UnisonShare/Page/Catalog.elm

Lines changed: 64 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ module UnisonShare.Page.Catalog exposing (..)
33
import Api
44
import Env exposing (Env)
55
import FullyQualifiedName as FQN
6-
import Html exposing (Html, a, div, h1, input, strong, text)
6+
import Html exposing (Html, a, div, h1, input, span, strong, text)
77
import Html.Attributes exposing (autofocus, class, href, placeholder)
8-
import Html.Events exposing (onInput)
8+
import Html.Events exposing (onBlur, onFocus, onInput)
99
import Http
1010
import Perspective
1111
import Project exposing (ProjectListing)
@@ -25,6 +25,7 @@ import UnisonShare.Route as Route
2525

2626
type alias LoadedModel =
2727
{ query : String
28+
, hasFocus : Bool
2829
, catalog : Catalog
2930
}
3031

@@ -65,6 +66,7 @@ fetchCatalog env =
6566

6667
type Msg
6768
= UpdateQuery String
69+
| UpdateFocus Bool
6870
| ClearQuery
6971
| NoOp
7072
| FetchCatalogFinished (Result Http.Error Catalog)
@@ -79,7 +81,10 @@ update msg model =
7981
( Failure e, Cmd.none )
8082

8183
Ok catalog ->
82-
( Success { query = "", catalog = catalog }, Cmd.none )
84+
( Success { query = "", hasFocus = True, catalog = catalog }, Cmd.none )
85+
86+
( UpdateFocus hasFocus, Success m ) ->
87+
( Success { m | hasFocus = hasFocus }, Cmd.none )
8388

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

97102

103+
projectUrl : ProjectListing -> String
104+
projectUrl =
105+
Route.forProject >> Route.toUrlString
106+
107+
98108
viewProjectListing : ProjectListing -> Html msg
99109
viewProjectListing project =
100110
let
101111
slug =
102-
FQN.cons (Project.ownerToString project.owner) project.name
103-
104-
url =
105-
project
106-
|> Route.forProject
107-
|> Route.toUrlString
112+
Project.slug project
108113
in
109-
a [ class "project-listing", href url ] [ FQN.view slug ]
114+
a [ class "project-listing", href (projectUrl project) ] [ FQN.view slug ]
110115

111116

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

122127

128+
viewSearchResult : ( ProjectListing, String ) -> Html msg
129+
viewSearchResult ( project, category ) =
130+
a
131+
[ class "search-result", href (projectUrl project) ]
132+
[ project |> Project.slug |> FQN.view
133+
, span [ class "category" ] [ text category ]
134+
]
135+
136+
137+
viewSearchResults : LoadedModel -> Html msg
138+
viewSearchResults model =
139+
if String.length model.query > 3 then
140+
let
141+
results =
142+
model.query
143+
|> Catalog.search model.catalog
144+
|> List.map viewSearchResult
145+
146+
resultsPane =
147+
if List.isEmpty results then
148+
[ div [ class "empty-state" ] [ text ("No matching projects found for \"" ++ model.query ++ "\"") ] ]
149+
150+
else
151+
results
152+
in
153+
div [ class "search-results" ] resultsPane
154+
155+
else
156+
UI.nothing
157+
158+
123159
viewLoaded : LoadedModel -> PageLayout Msg
124160
viewLoaded model =
125161
let
126162
categories =
127163
model.catalog
128164
|> Catalog.toList
129165
|> List.map viewCategory
166+
167+
searchResults =
168+
if model.hasFocus then
169+
viewSearchResults model
170+
171+
else
172+
UI.nothing
130173
in
131174
PageLayout.HeroLayout
132175
{ hero =
@@ -144,13 +187,18 @@ viewLoaded model =
144187
, div [] [ text "Projects, libraries, documention, terms, and types" ]
145188
]
146189
, div [ class "catalog-search" ]
147-
[ Icon.view Icon.search
148-
, input
149-
[ placeholder "Search for projects"
150-
, onInput UpdateQuery
151-
, autofocus True
190+
[ div [ class "search-field" ]
191+
[ Icon.view Icon.search
192+
, input
193+
[ placeholder "Search for projects"
194+
, onInput UpdateQuery
195+
, autofocus True
196+
, onBlur (UpdateFocus False)
197+
, onFocus (UpdateFocus True)
198+
]
199+
[]
152200
]
153-
[]
201+
, searchResults
154202
]
155203
]
156204
)

src/css/unison-share/page/catalog.css

Lines changed: 88 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -56,41 +56,100 @@
5656

5757
.catalog-hero .catalog-search {
5858
width: 50rem;
59-
height: 3.5rem;
6059
background: var(--color-main-bg);
6160
color: var(--color-main-fg);
6261
border-radius: var(--border-radius-base);
6362
position: absolute;
64-
bottom: -1.75rem;
65-
border: 2px solid var(--color-catalog-hero-bg);
63+
top: calc(var(--page-hero-height) - 1.75rem);
64+
border: 2px solid transparent;
6665
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.1);
6766
display: flex;
68-
flex-direction: row;
69-
align-items: center;
70-
padding: 1rem 0 1rem 1rem;
67+
flex-direction: column;
7168
left: 50%;
7269
transform: translateX(-50%);
7370
z-index: var(--layer-base);
7471
transition: all 0.2s;
72+
background: var(--color-gray-lighten-55);
73+
}
74+
75+
.catalog-hero .catalog-search:focus-within {
76+
border-color: var(--color-main-focus-fg);
77+
box-shadow: 0 0 0 2px var(--color-main-focus-outline);
78+
}
79+
80+
.catalog-hero .catalog-search .search-field {
81+
display: flex;
82+
flex-direction: row;
83+
align-items: center;
84+
height: 3.5rem;
85+
padding: 1rem 0 1rem 1rem;
86+
border-radius: var(--border-radius-base);
87+
}
88+
89+
.catalog-hero .catalog-search .search-field:focus-within {
90+
background: var(--color-gray-lighten-60);
91+
}
92+
93+
.catalog-hero .catalog-search .search-field .icon {
94+
font-size: 1.5rem;
95+
color: var(--color-main-subtle-fg);
7596
}
76-
.catalog-hero .catalog-search input {
97+
98+
.catalog-hero .catalog-search .search-field input {
7799
width: 100%;
78100
height: calc(3.5rem - 4px);
79101
margin-left: 0.75rem;
80102
font-size: 1.125rem;
81103
border-radius: var(--border-radius-base);
104+
font-weight: bold;
105+
background: transparent;
82106
}
83-
.catalog-hero .catalog-search input:focus {
107+
108+
.catalog-hero .catalog-search .search-field input::placeholder {
109+
font-weight: normal;
110+
}
111+
112+
.catalog-hero .catalog-search .search-field input:focus {
84113
outline: none;
85114
}
86-
.catalog-hero .catalog-search:focus-within {
87-
border-color: var(--color-main-focus-fg);
88-
box-shadow: 0 0 0 2px var(--color-main-focus-outline);
115+
116+
.catalog-hero .catalog-search .search-results {
117+
background: var(--color-main-bg);
118+
border-top: 1px solid var(--color-gray-lighten-50);
119+
border-radius: 0 0 var(--border-radius-base) var(--border-radius-base);
120+
display: flex;
121+
flex-direction: column;
122+
gap: 0.75rem;
123+
padding: 0.75rem;
89124
}
90125

91-
.catalog-hero .catalog-search .icon {
92-
font-size: 1.5rem;
126+
.catalog-hero .catalog-search .search-results .search-result {
127+
display: flex;
128+
flex-direction: row;
129+
padding: 0.5rem 1rem;
130+
align-items: center;
131+
height: 3rem;
132+
border-radius: var(--border-radius-base);
133+
font-size: 1rem;
134+
}
135+
136+
.catalog-hero .catalog-search .search-results .search-result:hover {
137+
background: var(--color-gray-lighten-55);
138+
text-decoration: none;
139+
}
140+
141+
.catalog-hero .catalog-search .search-results .search-result .category {
93142
color: var(--color-main-subtle-fg);
143+
font-size: var(--font-size-medium);
144+
margin-left: auto;
145+
justify-self: flex-end;
146+
}
147+
148+
.catalog-hero .catalog-search .search-results .empty-state {
149+
font-size: var(--font-size-base);
150+
color: var(--color-main-subtle-fg);
151+
text-align: center;
152+
padding: 1rem 0;
94153
}
95154

96155
.categories {
@@ -103,4 +162,20 @@
103162

104163
.categories .card {
105164
width: 18.75rem;
165+
gap: 0.5rem;
166+
}
167+
168+
.categories .card .project-listing {
169+
display: flex;
170+
flex-direction: row;
171+
align-items: center;
172+
height: 2rem;
173+
padding: 0 0.25rem;
174+
margin-left: -0.25rem;
175+
border-radius: var(--border-radius-base);
176+
}
177+
178+
.categories .card .project-listing:hover {
179+
text-decoration: none;
180+
background: var(--color-gray-lighten-55);
106181
}

tests/UnisonShare/CatalogTests.elm

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,39 @@ toList =
8383
]
8484

8585

86+
search : Test
87+
search =
88+
describe "Catalog.search"
89+
[ test "Fuzzy finds projects in the catalog by project name " <|
90+
\_ ->
91+
let
92+
projectListings_ =
93+
[ baseListing, distributedListing, textExtraListing, nanoidListing ]
94+
95+
catalog_ =
96+
Catalog.catalog catalogMask projectListings_
97+
in
98+
Expect.equal
99+
[ ( baseListing, "Featured" )
100+
, ( distributedListing, "Featured" )
101+
]
102+
(Catalog.search catalog_ "unison")
103+
, test "Fuzzy finds projects in the catalog by category" <|
104+
\_ ->
105+
let
106+
projectListings_ =
107+
[ baseListing, distributedListing, textExtraListing, nanoidListing ]
108+
109+
catalog_ =
110+
Catalog.catalog catalogMask projectListings_
111+
in
112+
Expect.equal
113+
[ ( textExtraListing, "Parsers & Text Manipulation" )
114+
]
115+
(Catalog.search catalog_ "parsers")
116+
]
117+
118+
86119

87120
-- helpers
88121

0 commit comments

Comments
 (0)