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 {
|
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,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
›
|
›
|
||||||
</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>
|
)
|
||||||
)
|
|
||||||
}
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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]
|
||||||
|
|
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
|
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
|
||||||
|
|
Loading…
Reference in a new issue