add kbar \o/

This commit is contained in:
Inhji 2023-03-22 07:31:38 +01:00
parent 4719b4a7c6
commit 6de86fc4de
5 changed files with 247 additions and 255 deletions

View file

@ -1,283 +1,263 @@
import React from 'react' import React, {useState, useEffect, useMemo} from 'react'
import { import {
KBarProvider, KBarProvider,
KBarPortal, KBarPortal,
KBarPositioner, KBarPositioner,
KBarAnimator, KBarAnimator,
KBarSearch, KBarSearch,
KBarResults, KBarResults,
useMatches, useMatches,
useRegisterActions, useRegisterActions,
useKBar useKBar,
createAction
} from "kbar"; } from "kbar";
import classNames from 'classnames'; import classNames from 'classnames';
const searchStyle = { const searchStyle = {
padding: "12px 16px", padding: "12px 16px",
fontSize: "16px", fontSize: "16px",
width: "100%", width: "100%",
boxSizing: "border-box", boxSizing: "border-box",
outline: "none", outline: "none",
border: "none" border: "none"
}; };
const animatorStyle = { const animatorStyle = {
maxWidth: "600px", maxWidth: "600px",
width: "100%", width: "100%",
borderRadius: "8px", borderRadius: "8px",
overflow: "hidden" overflow: "hidden"
}; };
const groupNameStyle = { const groupNameStyle = {
padding: "8px 16px", padding: "8px 16px",
fontSize: "10px", fontSize: "10px",
textTransform: "uppercase", textTransform: "uppercase",
opacity: 0.5, opacity: 0.5,
}; };
function RenderResults() { function RenderResults() {
const { results, rootActionId } = useMatches(); const { results, rootActionId } = useMatches();
return ( return (
<KBarResults <KBarResults
items={results} items={results}
onRender={({ item, active }) => onRender={({ item, active }) =>
typeof item === "string" ? ( typeof item === "string" ? (
<div style={groupNameStyle}>{item}</div> <div style={groupNameStyle}>{item}</div>
) : ( ) : (
<ResultItem <ResultItem
action={item} action={item}
active={active} active={active}
currentRootActionId={rootActionId} currentRootActionId={rootActionId}
/> />
) )
} }
/> />
); );
} }
const ResultItem = React.forwardRef( const ResultItem = React.forwardRef(
( (
{ {
action, action,
active, active,
currentRootActionId, currentRootActionId,
}, },
ref ref
) => { ) => {
const ancestors = React.useMemo(() => { const ancestors = React.useMemo(() => {
if (!currentRootActionId) return action.ancestors; if (!currentRootActionId) return action.ancestors;
const index = action.ancestors.findIndex( const index = action.ancestors.findIndex(
(ancestor) => ancestor.id === currentRootActionId (ancestor) => ancestor.id === currentRootActionId
); );
// +1 removes the currentRootAction; e.g. // +1 removes the currentRootAction; e.g.
// if we are on the "Set theme" parent action, // if we are on the "Set theme" parent action,
// the UI should not display "Set theme… > Dark" // the UI should not display "Set theme… > Dark"
// but rather just "Dark" // but rather just "Dark"
return action.ancestors.slice(index + 1); return action.ancestors.slice(index + 1);
}, [action.ancestors, currentRootActionId]); }, [action.ancestors, currentRootActionId]);
return ( return (
<div <div
ref={ref} ref={ref}
className={classNames( className={classNames(
"flex justify-between items-center cursor-pointer px-4 py-3", "flex justify-between items-center cursor-pointer px-4 py-3",
{ {
"bg-emerald-50": active, "bg-emerald-50": active,
"border-l-2 border-emerald-300": active "border-l-2 border-emerald-300": active
})} })}
> >
<div className="flex gap-3 items-center text-sm"> <div className="flex gap-3 items-center text-sm">
{action.icon && action.icon} {action.icon && action.icon}
<div className="flex flex-col"> <div className="flex flex-col">
<div> <div>
{ancestors.length > 0 && {ancestors.length > 0 &&
ancestors.map((ancestor) => ( ancestors.map((ancestor) => (
<React.Fragment key={ancestor.id}> <React.Fragment key={ancestor.id}>
<span <span
style={{ style={{
opacity: 0.5, opacity: 0.5,
marginRight: 8, marginRight: 8,
}} }}
> >
{ancestor.name} {ancestor.name}
</span> </span>
<span <span
style={{ style={{
marginRight: 8, marginRight: 8,
}} }}
> >
&rsaquo; &rsaquo;
</span> </span>
</React.Fragment> </React.Fragment>
))} ))}
<span>{action.name}</span> <span>{action.name}</span>
</div> </div>
{action.subtitle && ( {action.subtitle && (
<span style={{ fontSize: 12 }}>{action.subtitle}</span> <span style={{ fontSize: 12 }}>{action.subtitle}</span>
)} )}
</div> </div>
</div> </div>
{action.shortcut?.length ? ( {action.shortcut?.length ? (
<div <div
aria-hidden aria-hidden
style={{ display: "grid", gridAutoFlow: "column", gap: "4px" }} style={{ display: "grid", gridAutoFlow: "column", gap: "4px" }}
> >
{action.shortcut.map((sc) => ( {action.shortcut.map((sc) => (
<kbd <kbd
key={sc} key={sc}
style={{ style={{
padding: "4px 6px", padding: "4px 6px",
background: "rgba(0 0 0 / .1)", background: "rgba(0 0 0 / .1)",
borderRadius: "4px", borderRadius: "4px",
fontSize: 14, fontSize: 14,
}} }}
> >
{sc} {sc}
</kbd> </kbd>
))} ))}
</div> </div>
) : null} ) : null}
</div> </div>
); );
} }
); );
const actions = [ const staticActions = [
{ {
id: "user", id: "user",
name: "User", name: "User",
shortcut: ["u"], shortcut: ["u"],
keywords: "profile", keywords: "profile",
perform: () => (window.location.pathname = "user"), perform: () => (window.location.pathname = "user"),
}, },
{ {
id: "user.edit", id: "user.edit",
name: "Edit User", name: "Edit User",
shortcut: ["u e"], shortcut: ["u e"],
keywords: "profile edit settings", keywords: "profile edit settings",
perform: () => (window.location.pathname = "user/settings"), perform: () => (window.location.pathname = "user/settings"),
}, },
{ {
id: "admin", id: "admin",
name: "Admin", name: "Admin",
shortcut: ["a"], shortcut: ["a"],
keywords: "home", keywords: "home",
perform: () => (window.location.pathname = "admin"), perform: () => (window.location.pathname = "admin"),
}, },
{ {
id: "notes", id: "notes",
name: "Notes", name: "Notes",
shortcut: ["n"], shortcut: ["n"],
keywords: "posts", keywords: "posts",
perform: () => (window.location.pathname = "admin/notes"), perform: () => (window.location.pathname = "admin/notes"),
}, },
{ {
id: "notes.new", id: "notes.new",
name: "New Note", name: "New Note",
shortcut: ["n n"], shortcut: ["n n"],
keywords: "create new", keywords: "create new",
perform: () => (window.location.pathname = "admin/notes/new"), perform: () => (window.location.pathname = "admin/notes/new"),
}, },
{ {
id: "channels", id: "channels",
name: "Channels", name: "Channels",
shortcut: ["c"], shortcut: ["c"],
keywords: "channels", keywords: "channels",
perform: () => (window.location.pathname = "admin/channels"), perform: () => (window.location.pathname = "admin/channels"),
}, },
{ {
id: "channels.new", id: "channels.new",
name: "New Channel", name: "New Channel",
shortcut: ["n c"], shortcut: ["n c"],
keywords: "create new", keywords: "create new",
perform: () => (window.location.pathname = "admin/channels/new"), perform: () => (window.location.pathname = "admin/channels/new"),
}, },
{ {
id: "identities", id: "identities",
name: "Identities", name: "Identities",
shortcut: ["i"], shortcut: ["i"],
keywords: "identities", keywords: "identities",
perform: () => (window.location.pathname = "admin/identities"), perform: () => (window.location.pathname = "admin/identities"),
}, },
{ {
id: "identities.new", id: "identities.new",
name: "New Identity", name: "New Identity",
shortcut: ["n i"], shortcut: ["n i"],
keywords: "create new", keywords: "create new",
perform: () => (window.location.pathname = "admin/identities/new"), perform: () => (window.location.pathname = "admin/identities/new"),
}, },
{ {
id: "settings", id: "settings",
name: "Settings", name: "Settings",
shortcut: ["s"], shortcut: ["s"],
keywords: "settings", keywords: "settings",
perform: () => (window.location.pathname = "admin/settings"), perform: () => (window.location.pathname = "admin/settings"),
}, },
{ {
id: "settings.edit", id: "settings.edit",
name: "Edit Settings", name: "Edit Settings",
shortcut: ["s e"], shortcut: ["s e"],
keywords: "settings edit", keywords: "settings edit",
perform: () => (window.location.pathname = "admin/settings/edit"), perform: () => (window.location.pathname = "admin/settings/edit"),
} }
] ]
const dynamicActionsList = { function DynamicResultsProvider() {
id: "public.home", const [actions, setActions] = useState([])
name: "Go Home", const [notes, setNotes] = useState([])
shortcut: ["x"], const [rerender, setRerender] = useState(true)
keywords: "public",
perform: () => (window.location.pathname = "/"),
}
function DynamicResultsProvider({children}) { useEffect(() => {
//const [search, setSearch] = React.useState(""); fetch("/api/admin/notes")
//useKBar(state => console.log("state")) .then(resp => resp.json())
/* .then(json => setNotes(json.notes))
const dynamicActions = React.useMemo(() => { }, [rerender])
const searchQuery = search
//const results = await getResults(search);
//return results.map(r => createAction(...));
console.log(searchQuery)
return dynamicActionsInner
}, [search])
*/
// const {query, search, options} = useKBar((state) => ({ search: state.searchQuery }))
// const dynamicActions = React.useMemo(() =>{
// return dynamicActionsInner
// }, [search])
const { query } = useKBar(state => ({ query: state.query })) const noteActions = useMemo(() => notes.map(note => createAction({
const [dynamicActions, setDynamicActions] = React.useState([]) id: note.slug,
name: note.name,
keywords: note.channels.map(c => c.name),
perform: () => (window.location.pathname = `/admin/notes/${note.id}`),
})), [notes])
React.useEffect(() => { useRegisterActions([...staticActions, ...noteActions], [noteActions])
console.log(query)
//fetchUsers(query).then(setUsers)
setDynamicActions(dynamicActionsList)
}, [query])
console.log("mount")
useRegisterActions(dynamicActionsList, [dynamicActions])
return ([children])
} }
export default function KBar() { export default function KBar() {
return ( return (
<KBarProvider actions={actions}> <KBarProvider actions={staticActions}>
<DynamicResultsProvider> <DynamicResultsProvider />
<KBarPortal> <KBarPortal>
<KBarPositioner> <KBarPositioner>
<KBarAnimator style={animatorStyle} className="bg-gray-50 border border-gray-100 shadow-lg"> <KBarAnimator style={animatorStyle} className="bg-gray-50 border border-gray-100 shadow-lg">
<KBarSearch style={searchStyle} className="bg-gray-200" /> <KBarSearch style={searchStyle} className="bg-gray-200" />
<RenderResults /> <RenderResults />
</KBarAnimator> </KBarAnimator>
</KBarPositioner> </KBarPositioner>
</KBarPortal> </KBarPortal>
</DynamicResultsProvider> </KBarProvider>
</KBarProvider> )
)
} }

View file

@ -2,6 +2,7 @@ defmodule Chiya.Channels.Channel do
use Ecto.Schema use Ecto.Schema
import Ecto.Changeset import Ecto.Changeset
@derive {Jason.Encoder, only: [:id, :name, :content, :slug, :visibility]}
schema "channels" do schema "channels" do
field :content, :string field :content, :string
field :name, :string field :name, :string

View file

@ -2,6 +2,7 @@ defmodule Chiya.Notes.Note do
use Ecto.Schema use Ecto.Schema
import Ecto.Changeset import Ecto.Changeset
@derive {Jason.Encoder, only: [:id, :name, :content, :slug, :channels]}
schema "notes" do schema "notes" do
field :content, :string field :content, :string
field :kind, Ecto.Enum, values: [:post, :bookmark] field :kind, Ecto.Enum, values: [:post, :bookmark]

View file

@ -0,0 +1,8 @@
defmodule ChiyaWeb.ApiController do
use ChiyaWeb, :controller
def notes(conn, _params) do
notes = Chiya.Notes.list_notes()
json(conn, %{notes: notes})
end
end

View file

@ -25,9 +25,11 @@ defmodule ChiyaWeb.Router do
end end
# Other scopes may use custom stacks. # Other scopes may use custom stacks.
# scope "/api", ChiyaWeb do scope "/api", ChiyaWeb do
# pipe_through :api pipe_through :api
# end
get "/admin/notes", ApiController, :notes
end
# Enable LiveDashboard and Swoosh mailbox preview in development # Enable LiveDashboard and Swoosh mailbox preview in development
if Application.compile_env(:chiya, :dev_routes) do if Application.compile_env(:chiya, :dev_routes) do