add kbar \o/
This commit is contained in:
parent
4719b4a7c6
commit
6de86fc4de
5 changed files with 247 additions and 255 deletions
|
@ -1,283 +1,263 @@
|
|||
import React from 'react'
|
||||
import React, {useState, useEffect, useMemo} from 'react'
|
||||
import {
|
||||
KBarProvider,
|
||||
KBarPortal,
|
||||
KBarPositioner,
|
||||
KBarAnimator,
|
||||
KBarSearch,
|
||||
KBarResults,
|
||||
useMatches,
|
||||
useRegisterActions,
|
||||
useKBar
|
||||
KBarProvider,
|
||||
KBarPortal,
|
||||
KBarPositioner,
|
||||
KBarAnimator,
|
||||
KBarSearch,
|
||||
KBarResults,
|
||||
useMatches,
|
||||
useRegisterActions,
|
||||
useKBar,
|
||||
createAction
|
||||
} from "kbar";
|
||||
import classNames from 'classnames';
|
||||
|
||||
const searchStyle = {
|
||||
padding: "12px 16px",
|
||||
fontSize: "16px",
|
||||
width: "100%",
|
||||
boxSizing: "border-box",
|
||||
outline: "none",
|
||||
border: "none"
|
||||
padding: "12px 16px",
|
||||
fontSize: "16px",
|
||||
width: "100%",
|
||||
boxSizing: "border-box",
|
||||
outline: "none",
|
||||
border: "none"
|
||||
};
|
||||
|
||||
const animatorStyle = {
|
||||
maxWidth: "600px",
|
||||
width: "100%",
|
||||
borderRadius: "8px",
|
||||
overflow: "hidden"
|
||||
maxWidth: "600px",
|
||||
width: "100%",
|
||||
borderRadius: "8px",
|
||||
overflow: "hidden"
|
||||
};
|
||||
|
||||
const groupNameStyle = {
|
||||
padding: "8px 16px",
|
||||
fontSize: "10px",
|
||||
textTransform: "uppercase",
|
||||
opacity: 0.5,
|
||||
padding: "8px 16px",
|
||||
fontSize: "10px",
|
||||
textTransform: "uppercase",
|
||||
opacity: 0.5,
|
||||
};
|
||||
|
||||
|
||||
function RenderResults() {
|
||||
const { results, rootActionId } = useMatches();
|
||||
const { results, rootActionId } = useMatches();
|
||||
|
||||
return (
|
||||
<KBarResults
|
||||
items={results}
|
||||
onRender={({ item, active }) =>
|
||||
typeof item === "string" ? (
|
||||
<div style={groupNameStyle}>{item}</div>
|
||||
) : (
|
||||
<ResultItem
|
||||
action={item}
|
||||
active={active}
|
||||
currentRootActionId={rootActionId}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<KBarResults
|
||||
items={results}
|
||||
onRender={({ item, active }) =>
|
||||
typeof item === "string" ? (
|
||||
<div style={groupNameStyle}>{item}</div>
|
||||
) : (
|
||||
<ResultItem
|
||||
action={item}
|
||||
active={active}
|
||||
currentRootActionId={rootActionId}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const ResultItem = React.forwardRef(
|
||||
(
|
||||
{
|
||||
action,
|
||||
active,
|
||||
currentRootActionId,
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const ancestors = React.useMemo(() => {
|
||||
if (!currentRootActionId) return action.ancestors;
|
||||
const index = action.ancestors.findIndex(
|
||||
(ancestor) => ancestor.id === currentRootActionId
|
||||
);
|
||||
// +1 removes the currentRootAction; e.g.
|
||||
// if we are on the "Set theme" parent action,
|
||||
// the UI should not display "Set theme… > Dark"
|
||||
// but rather just "Dark"
|
||||
return action.ancestors.slice(index + 1);
|
||||
}, [action.ancestors, currentRootActionId]);
|
||||
(
|
||||
{
|
||||
action,
|
||||
active,
|
||||
currentRootActionId,
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const ancestors = React.useMemo(() => {
|
||||
if (!currentRootActionId) return action.ancestors;
|
||||
const index = action.ancestors.findIndex(
|
||||
(ancestor) => ancestor.id === currentRootActionId
|
||||
);
|
||||
// +1 removes the currentRootAction; e.g.
|
||||
// if we are on the "Set theme" parent action,
|
||||
// the UI should not display "Set theme… > Dark"
|
||||
// but rather just "Dark"
|
||||
return action.ancestors.slice(index + 1);
|
||||
}, [action.ancestors, currentRootActionId]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={classNames(
|
||||
"flex justify-between items-center cursor-pointer px-4 py-3",
|
||||
{
|
||||
"bg-emerald-50": active,
|
||||
"border-l-2 border-emerald-300": active
|
||||
})}
|
||||
>
|
||||
<div className="flex gap-3 items-center text-sm">
|
||||
{action.icon && action.icon}
|
||||
<div className="flex flex-col">
|
||||
<div>
|
||||
{ancestors.length > 0 &&
|
||||
ancestors.map((ancestor) => (
|
||||
<React.Fragment key={ancestor.id}>
|
||||
<span
|
||||
style={{
|
||||
opacity: 0.5,
|
||||
marginRight: 8,
|
||||
}}
|
||||
>
|
||||
{ancestor.name}
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
marginRight: 8,
|
||||
}}
|
||||
>
|
||||
›
|
||||
</span>
|
||||
</React.Fragment>
|
||||
))}
|
||||
<span>{action.name}</span>
|
||||
</div>
|
||||
{action.subtitle && (
|
||||
<span style={{ fontSize: 12 }}>{action.subtitle}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{action.shortcut?.length ? (
|
||||
<div
|
||||
aria-hidden
|
||||
style={{ display: "grid", gridAutoFlow: "column", gap: "4px" }}
|
||||
>
|
||||
{action.shortcut.map((sc) => (
|
||||
<kbd
|
||||
key={sc}
|
||||
style={{
|
||||
padding: "4px 6px",
|
||||
background: "rgba(0 0 0 / .1)",
|
||||
borderRadius: "4px",
|
||||
fontSize: 14,
|
||||
}}
|
||||
>
|
||||
{sc}
|
||||
</kbd>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={classNames(
|
||||
"flex justify-between items-center cursor-pointer px-4 py-3",
|
||||
{
|
||||
"bg-emerald-50": active,
|
||||
"border-l-2 border-emerald-300": active
|
||||
})}
|
||||
>
|
||||
<div className="flex gap-3 items-center text-sm">
|
||||
{action.icon && action.icon}
|
||||
<div className="flex flex-col">
|
||||
<div>
|
||||
{ancestors.length > 0 &&
|
||||
ancestors.map((ancestor) => (
|
||||
<React.Fragment key={ancestor.id}>
|
||||
<span
|
||||
style={{
|
||||
opacity: 0.5,
|
||||
marginRight: 8,
|
||||
}}
|
||||
>
|
||||
{ancestor.name}
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
marginRight: 8,
|
||||
}}
|
||||
>
|
||||
›
|
||||
</span>
|
||||
</React.Fragment>
|
||||
))}
|
||||
<span>{action.name}</span>
|
||||
</div>
|
||||
{action.subtitle && (
|
||||
<span style={{ fontSize: 12 }}>{action.subtitle}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{action.shortcut?.length ? (
|
||||
<div
|
||||
aria-hidden
|
||||
style={{ display: "grid", gridAutoFlow: "column", gap: "4px" }}
|
||||
>
|
||||
{action.shortcut.map((sc) => (
|
||||
<kbd
|
||||
key={sc}
|
||||
style={{
|
||||
padding: "4px 6px",
|
||||
background: "rgba(0 0 0 / .1)",
|
||||
borderRadius: "4px",
|
||||
fontSize: 14,
|
||||
}}
|
||||
>
|
||||
{sc}
|
||||
</kbd>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const actions = [
|
||||
{
|
||||
id: "user",
|
||||
name: "User",
|
||||
shortcut: ["u"],
|
||||
keywords: "profile",
|
||||
perform: () => (window.location.pathname = "user"),
|
||||
},
|
||||
{
|
||||
id: "user.edit",
|
||||
name: "Edit User",
|
||||
shortcut: ["u e"],
|
||||
keywords: "profile edit settings",
|
||||
perform: () => (window.location.pathname = "user/settings"),
|
||||
},
|
||||
{
|
||||
id: "admin",
|
||||
name: "Admin",
|
||||
shortcut: ["a"],
|
||||
keywords: "home",
|
||||
perform: () => (window.location.pathname = "admin"),
|
||||
},
|
||||
{
|
||||
id: "notes",
|
||||
name: "Notes",
|
||||
shortcut: ["n"],
|
||||
keywords: "posts",
|
||||
perform: () => (window.location.pathname = "admin/notes"),
|
||||
},
|
||||
{
|
||||
id: "notes.new",
|
||||
name: "New Note",
|
||||
shortcut: ["n n"],
|
||||
keywords: "create new",
|
||||
perform: () => (window.location.pathname = "admin/notes/new"),
|
||||
},
|
||||
{
|
||||
id: "channels",
|
||||
name: "Channels",
|
||||
shortcut: ["c"],
|
||||
keywords: "channels",
|
||||
perform: () => (window.location.pathname = "admin/channels"),
|
||||
},
|
||||
{
|
||||
id: "channels.new",
|
||||
name: "New Channel",
|
||||
shortcut: ["n c"],
|
||||
keywords: "create new",
|
||||
perform: () => (window.location.pathname = "admin/channels/new"),
|
||||
},
|
||||
{
|
||||
id: "identities",
|
||||
name: "Identities",
|
||||
shortcut: ["i"],
|
||||
keywords: "identities",
|
||||
perform: () => (window.location.pathname = "admin/identities"),
|
||||
},
|
||||
{
|
||||
id: "identities.new",
|
||||
name: "New Identity",
|
||||
shortcut: ["n i"],
|
||||
keywords: "create new",
|
||||
perform: () => (window.location.pathname = "admin/identities/new"),
|
||||
},
|
||||
{
|
||||
id: "settings",
|
||||
name: "Settings",
|
||||
shortcut: ["s"],
|
||||
keywords: "settings",
|
||||
perform: () => (window.location.pathname = "admin/settings"),
|
||||
},
|
||||
{
|
||||
id: "settings.edit",
|
||||
name: "Edit Settings",
|
||||
shortcut: ["s e"],
|
||||
keywords: "settings edit",
|
||||
perform: () => (window.location.pathname = "admin/settings/edit"),
|
||||
}
|
||||
const staticActions = [
|
||||
{
|
||||
id: "user",
|
||||
name: "User",
|
||||
shortcut: ["u"],
|
||||
keywords: "profile",
|
||||
perform: () => (window.location.pathname = "user"),
|
||||
},
|
||||
{
|
||||
id: "user.edit",
|
||||
name: "Edit User",
|
||||
shortcut: ["u e"],
|
||||
keywords: "profile edit settings",
|
||||
perform: () => (window.location.pathname = "user/settings"),
|
||||
},
|
||||
{
|
||||
id: "admin",
|
||||
name: "Admin",
|
||||
shortcut: ["a"],
|
||||
keywords: "home",
|
||||
perform: () => (window.location.pathname = "admin"),
|
||||
},
|
||||
{
|
||||
id: "notes",
|
||||
name: "Notes",
|
||||
shortcut: ["n"],
|
||||
keywords: "posts",
|
||||
perform: () => (window.location.pathname = "admin/notes"),
|
||||
},
|
||||
{
|
||||
id: "notes.new",
|
||||
name: "New Note",
|
||||
shortcut: ["n n"],
|
||||
keywords: "create new",
|
||||
perform: () => (window.location.pathname = "admin/notes/new"),
|
||||
},
|
||||
{
|
||||
id: "channels",
|
||||
name: "Channels",
|
||||
shortcut: ["c"],
|
||||
keywords: "channels",
|
||||
perform: () => (window.location.pathname = "admin/channels"),
|
||||
},
|
||||
{
|
||||
id: "channels.new",
|
||||
name: "New Channel",
|
||||
shortcut: ["n c"],
|
||||
keywords: "create new",
|
||||
perform: () => (window.location.pathname = "admin/channels/new"),
|
||||
},
|
||||
{
|
||||
id: "identities",
|
||||
name: "Identities",
|
||||
shortcut: ["i"],
|
||||
keywords: "identities",
|
||||
perform: () => (window.location.pathname = "admin/identities"),
|
||||
},
|
||||
{
|
||||
id: "identities.new",
|
||||
name: "New Identity",
|
||||
shortcut: ["n i"],
|
||||
keywords: "create new",
|
||||
perform: () => (window.location.pathname = "admin/identities/new"),
|
||||
},
|
||||
{
|
||||
id: "settings",
|
||||
name: "Settings",
|
||||
shortcut: ["s"],
|
||||
keywords: "settings",
|
||||
perform: () => (window.location.pathname = "admin/settings"),
|
||||
},
|
||||
{
|
||||
id: "settings.edit",
|
||||
name: "Edit Settings",
|
||||
shortcut: ["s e"],
|
||||
keywords: "settings edit",
|
||||
perform: () => (window.location.pathname = "admin/settings/edit"),
|
||||
}
|
||||
]
|
||||
|
||||
const dynamicActionsList = {
|
||||
id: "public.home",
|
||||
name: "Go Home",
|
||||
shortcut: ["x"],
|
||||
keywords: "public",
|
||||
perform: () => (window.location.pathname = "/"),
|
||||
}
|
||||
function DynamicResultsProvider() {
|
||||
const [actions, setActions] = useState([])
|
||||
const [notes, setNotes] = useState([])
|
||||
const [rerender, setRerender] = useState(true)
|
||||
|
||||
function DynamicResultsProvider({children}) {
|
||||
//const [search, setSearch] = React.useState("");
|
||||
//useKBar(state => console.log("state"))
|
||||
/*
|
||||
const dynamicActions = React.useMemo(() => {
|
||||
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])
|
||||
useEffect(() => {
|
||||
fetch("/api/admin/notes")
|
||||
.then(resp => resp.json())
|
||||
.then(json => setNotes(json.notes))
|
||||
}, [rerender])
|
||||
|
||||
const { query } = useKBar(state => ({ query: state.query }))
|
||||
const [dynamicActions, setDynamicActions] = React.useState([])
|
||||
const noteActions = useMemo(() => notes.map(note => createAction({
|
||||
id: note.slug,
|
||||
name: note.name,
|
||||
keywords: note.channels.map(c => c.name),
|
||||
perform: () => (window.location.pathname = `/admin/notes/${note.id}`),
|
||||
})), [notes])
|
||||
|
||||
React.useEffect(() => {
|
||||
console.log(query)
|
||||
//fetchUsers(query).then(setUsers)
|
||||
setDynamicActions(dynamicActionsList)
|
||||
}, [query])
|
||||
|
||||
console.log("mount")
|
||||
|
||||
useRegisterActions(dynamicActionsList, [dynamicActions])
|
||||
|
||||
return ([children])
|
||||
useRegisterActions([...staticActions, ...noteActions], [noteActions])
|
||||
}
|
||||
|
||||
export default function KBar() {
|
||||
return (
|
||||
<KBarProvider actions={actions}>
|
||||
<DynamicResultsProvider>
|
||||
<KBarPortal>
|
||||
<KBarPositioner>
|
||||
<KBarAnimator style={animatorStyle} className="bg-gray-50 border border-gray-100 shadow-lg">
|
||||
<KBarSearch style={searchStyle} className="bg-gray-200" />
|
||||
<RenderResults />
|
||||
</KBarAnimator>
|
||||
</KBarPositioner>
|
||||
</KBarPortal>
|
||||
</DynamicResultsProvider>
|
||||
</KBarProvider>
|
||||
)
|
||||
return (
|
||||
<KBarProvider actions={staticActions}>
|
||||
<DynamicResultsProvider />
|
||||
<KBarPortal>
|
||||
<KBarPositioner>
|
||||
<KBarAnimator style={animatorStyle} className="bg-gray-50 border border-gray-100 shadow-lg">
|
||||
<KBarSearch style={searchStyle} className="bg-gray-200" />
|
||||
<RenderResults />
|
||||
</KBarAnimator>
|
||||
</KBarPositioner>
|
||||
</KBarPortal>
|
||||
</KBarProvider>
|
||||
)
|
||||
}
|
|
@ -2,6 +2,7 @@ defmodule Chiya.Channels.Channel do
|
|||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
@derive {Jason.Encoder, only: [:id, :name, :content, :slug, :visibility]}
|
||||
schema "channels" do
|
||||
field :content, :string
|
||||
field :name, :string
|
||||
|
|
|
@ -2,6 +2,7 @@ defmodule Chiya.Notes.Note do
|
|||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
@derive {Jason.Encoder, only: [:id, :name, :content, :slug, :channels]}
|
||||
schema "notes" do
|
||||
field :content, :string
|
||||
field :kind, Ecto.Enum, values: [:post, :bookmark]
|
||||
|
|
8
lib/chiya_web/controllers/api_controller.ex
Normal file
8
lib/chiya_web/controllers/api_controller.ex
Normal 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
|
|
@ -25,9 +25,11 @@ defmodule ChiyaWeb.Router do
|
|||
end
|
||||
|
||||
# Other scopes may use custom stacks.
|
||||
# scope "/api", ChiyaWeb do
|
||||
# pipe_through :api
|
||||
# end
|
||||
scope "/api", ChiyaWeb do
|
||||
pipe_through :api
|
||||
|
||||
get "/admin/notes", ApiController, :notes
|
||||
end
|
||||
|
||||
# Enable LiveDashboard and Swoosh mailbox preview in development
|
||||
if Application.compile_env(:chiya, :dev_routes) do
|
||||
|
|
Loading…
Reference in a new issue