remove kbar, add flop, clean up
This commit is contained in:
parent
0b43c3916c
commit
1331b69013
18 changed files with 209 additions and 620 deletions
|
@ -13,13 +13,21 @@
|
|||
html {
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-feature-settings: "case", "cpsp", "frac", "salt", "ccmp", "cv01", "cv02", "cv03", "cv04", "cv05", "cv06", "cv07", "cv09", "cv10", "cv11";
|
||||
@apply text-slate-800;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply dark:bg-slate-800;
|
||||
}
|
||||
|
||||
.stack > * + * {
|
||||
margin-block-start: var(--flow-space, 1em);
|
||||
}
|
||||
|
||||
:root[data-mode=dark] .prose {
|
||||
@apply prose-invert;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* ============= SITE LAYOUT =============
|
||||
*/
|
||||
|
@ -47,7 +55,7 @@
|
|||
@apply flex md:flex-row lg:flex-col mb-6 lg:mb-0;
|
||||
}
|
||||
|
||||
& ul.menu {
|
||||
& h3 {
|
||||
@apply m-0;
|
||||
}
|
||||
}
|
||||
|
@ -57,14 +65,20 @@
|
|||
#site-content {
|
||||
@apply grid grid-cols-1 lg:grid-cols-5 gap-0 lg:gap-12;
|
||||
@apply px-3 sm:px-0;
|
||||
|
||||
#content-wrapper {
|
||||
@apply col-span-4;
|
||||
}
|
||||
|
||||
#secondary-sidebar {
|
||||
@apply col-span-1;
|
||||
}
|
||||
}
|
||||
|
||||
#content-wrapper {
|
||||
@apply col-span-4;
|
||||
}
|
||||
/* === SITE FOOTER === */
|
||||
|
||||
#secondary-sidebar {
|
||||
@apply col-span-1;
|
||||
#site-footer {
|
||||
@apply mt-8 prose max-w-none;
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
@ -1,29 +1,10 @@
|
|||
// If you want to use Phoenix channels, run `mix help phx.gen.channel`
|
||||
// to get started and then uncomment the line below.
|
||||
// import "./user_socket.js"
|
||||
|
||||
// You can include dependencies in two ways.
|
||||
//
|
||||
// The simplest option is to put them in assets/vendor and
|
||||
// import them using relative paths:
|
||||
//
|
||||
// import "../vendor/some-package.js"
|
||||
//
|
||||
// Alternatively, you can `npm install some-package --prefix assets` and import
|
||||
// them using a path starting with the package name:
|
||||
//
|
||||
// import "some-package"
|
||||
//
|
||||
|
||||
// Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
|
||||
import "phoenix_html"
|
||||
// Establish Phoenix Socket and LiveView configuration.
|
||||
import { Socket } from "phoenix"
|
||||
import { LiveSocket } from "phoenix_live_view"
|
||||
import topbar from "../vendor/topbar"
|
||||
import React from "react"
|
||||
import { createRoot } from 'react-dom'
|
||||
import KBar from "./kbar"
|
||||
import darkmode from "./darkmode"
|
||||
|
||||
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
|
||||
let liveSocket = new LiveSocket("/live", Socket, { params: { _csrf_token: csrfToken } })
|
||||
|
@ -42,41 +23,24 @@ liveSocket.connect()
|
|||
// >> liveSocket.disableLatencySim()
|
||||
window.liveSocket = liveSocket
|
||||
|
||||
const reactRoot = document.querySelector('#react-root')
|
||||
if (reactRoot) {
|
||||
const root = createRoot(reactRoot);
|
||||
root.render(<KBar/>);
|
||||
}
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
darkmode()
|
||||
|
||||
document
|
||||
.querySelector("#dark-mode-toggle")
|
||||
.addEventListener("click", (e) => {
|
||||
e.preventDefault()
|
||||
const data = document.documentElement.dataset
|
||||
if (data["mode"] && data["mode"] == "dark") {
|
||||
delete data["mode"]
|
||||
window.localStorage.removeItem("theme")
|
||||
} else {
|
||||
data["mode"] = "dark"
|
||||
window.localStorage.setItem("theme", "dark")
|
||||
}
|
||||
})
|
||||
document
|
||||
.querySelectorAll('textarea')
|
||||
.forEach(e => e.addEventListener('keydown', function(e) {
|
||||
if (e.key == 'Tab') {
|
||||
e.preventDefault();
|
||||
var start = this.selectionStart;
|
||||
var end = this.selectionEnd;
|
||||
|
||||
document
|
||||
.querySelectorAll('textarea')
|
||||
.forEach(e => e.addEventListener('keydown', function(e) {
|
||||
if (e.key == 'Tab') {
|
||||
e.preventDefault();
|
||||
var start = this.selectionStart;
|
||||
var end = this.selectionEnd;
|
||||
// set textarea value to: text before caret + tab + text after caret
|
||||
this.value = this.value.substring(0, start) +
|
||||
"\t" + this.value.substring(end);
|
||||
|
||||
// set textarea value to: text before caret + tab + text after caret
|
||||
this.value = this.value.substring(0, start) +
|
||||
"\t" + this.value.substring(end);
|
||||
|
||||
// put caret at right position again
|
||||
this.selectionStart =
|
||||
this.selectionEnd = start + 1;
|
||||
}
|
||||
}))
|
||||
|
||||
// put caret at right position again
|
||||
this.selectionStart =
|
||||
this.selectionEnd = start + 1;
|
||||
}
|
||||
}))
|
||||
})
|
||||
|
|
15
assets/js/darkmode.js
Normal file
15
assets/js/darkmode.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
export default function() {
|
||||
document
|
||||
.querySelector('#dark-mode-toggle')
|
||||
.addEventListener('click', (e) => {
|
||||
e.preventDefault()
|
||||
const data = document.documentElement.dataset
|
||||
if (data['mode'] && data['mode'] == 'dark') {
|
||||
delete data['mode']
|
||||
window.localStorage.removeItem('theme')
|
||||
} else {
|
||||
data['mode'] = 'dark'
|
||||
window.localStorage.setItem('theme', 'dark')
|
||||
}
|
||||
})
|
||||
}
|
|
@ -1,267 +0,0 @@
|
|||
import React, {useState, useEffect, useMemo} from 'react'
|
||||
import {
|
||||
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"
|
||||
};
|
||||
|
||||
const animatorStyle = {
|
||||
maxWidth: "600px",
|
||||
width: "100%",
|
||||
borderRadius: "8px",
|
||||
overflow: "hidden"
|
||||
};
|
||||
|
||||
const groupNameStyle = {
|
||||
padding: "8px 16px",
|
||||
fontSize: "10px",
|
||||
textTransform: "uppercase",
|
||||
opacity: 0.75,
|
||||
};
|
||||
|
||||
|
||||
function RenderResults() {
|
||||
const { results, rootActionId } = useMatches();
|
||||
|
||||
return (
|
||||
<KBarResults
|
||||
items={results}
|
||||
onRender={({ item, active }) =>
|
||||
typeof item === "string" ? (
|
||||
<div style={groupNameStyle} className="dark:text-white">{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]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={classNames(
|
||||
"flex justify-between items-center cursor-pointer px-4 py-3 dark:text-white",
|
||||
{
|
||||
"bg-gray-300 dark:bg-gray-600": active,
|
||||
"border-l-2 border-gray-300": active
|
||||
})}
|
||||
>
|
||||
<div className="flex gap-3 items-center text-sm dark:text-white">
|
||||
{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 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"),
|
||||
}
|
||||
].map(function(a) {
|
||||
return {...a, section: "Pages"}
|
||||
})
|
||||
|
||||
function DynamicResultsProvider() {
|
||||
const [actions, setActions] = useState([])
|
||||
const [notes, setNotes] = useState([])
|
||||
const [rerender, setRerender] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/admin/notes")
|
||||
.then(resp => resp.json())
|
||||
.then(json => setNotes(json.notes))
|
||||
}, [rerender])
|
||||
|
||||
|
||||
const noteActions = useMemo(() => notes.map(note => createAction({
|
||||
id: note.slug,
|
||||
name: note.name,
|
||||
subtitle: note.channels.map(c => c.name).join(", "),
|
||||
section: "Notes",
|
||||
keywords: note.channels.map(c => c.name),
|
||||
perform: () => (window.location.pathname = `/admin/notes/${note.id}`),
|
||||
})), [notes])
|
||||
|
||||
useRegisterActions([...staticActions, ...noteActions], [noteActions])
|
||||
}
|
||||
|
||||
export default function KBar() {
|
||||
return (
|
||||
<KBarProvider actions={staticActions}>
|
||||
<DynamicResultsProvider />
|
||||
<KBarPortal>
|
||||
<KBarPositioner>
|
||||
<KBarAnimator style={animatorStyle} className="bg-gray-50 border border-gray-100 shadow-sm dark:bg-gray-900 dark:border-gray-800">
|
||||
<KBarSearch style={searchStyle} className="bg-gray-50 dark:bg-gray-900 border-b border-gray-100 text-gray-900 dark:text-gray-100" />
|
||||
<RenderResults />
|
||||
</KBarAnimator>
|
||||
</KBarPositioner>
|
||||
</KBarPortal>
|
||||
</KBarProvider>
|
||||
)
|
||||
}
|
|
@ -4,6 +4,7 @@ import 'phoenix_html'
|
|||
import hljs from 'highlight.js'
|
||||
import GLightbox from 'glightbox'
|
||||
import Tablesort from 'tablesort'
|
||||
import darkmode from "./darkmode"
|
||||
|
||||
document.addEventListener('DOMContentLoaded', (event) => {
|
||||
document.querySelectorAll('.prose pre code').forEach((el) =>
|
||||
|
@ -11,22 +12,8 @@ document.addEventListener('DOMContentLoaded', (event) => {
|
|||
|
||||
document.querySelectorAll('.prose table').forEach(el =>
|
||||
new Tablesort(el))
|
||||
});
|
||||
|
||||
document
|
||||
.querySelector('#dark-mode-toggle')
|
||||
.addEventListener('click', (e) => {
|
||||
e.preventDefault()
|
||||
const data = document.documentElement.dataset
|
||||
if (data['mode'] && data['mode'] == 'dark') {
|
||||
delete data['mode']
|
||||
window.localStorage.removeItem('theme')
|
||||
} else {
|
||||
data['mode'] = 'dark'
|
||||
window.localStorage.setItem('theme', 'dark')
|
||||
}
|
||||
})
|
||||
darkmode()
|
||||
|
||||
GLightbox({ selector: '.lightbox' })
|
||||
|
||||
window.hljs = hljs
|
||||
GLightbox({ selector: '.lightbox' })
|
||||
});
|
213
assets/package-lock.json
generated
213
assets/package-lock.json
generated
|
@ -8,16 +8,13 @@
|
|||
"classnames": "^2.3.2",
|
||||
"glightbox": "^3.2.0",
|
||||
"highlight.js": "^11.8.0",
|
||||
"kbar": "^0.1.0-beta.43",
|
||||
"phoenix": "file:../deps/phoenix",
|
||||
"phoenix_html": "file:../deps/phoenix_html",
|
||||
"phoenix_live_view": "file:../deps/phoenix_live_view",
|
||||
"tablesort": "^5.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"esbuild": "^0.19.2",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2"
|
||||
"esbuild": "^0.19.2"
|
||||
}
|
||||
},
|
||||
"../deps/phoenix": {
|
||||
|
@ -31,17 +28,6 @@
|
|||
"version": "0.19.5",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.22.15",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.15.tgz",
|
||||
"integrity": "sha512-T0O+aa+4w0u06iNmapipJXMV4HoUir03hpx3/YqXXhu9xim3w+dVphjFWl1OH8NbZHw5Lbm9k45drDkgq2VNNA==",
|
||||
"dependencies": {
|
||||
"regenerator-runtime": "^0.14.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.2.tgz",
|
||||
|
@ -394,92 +380,6 @@
|
|||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-compose-refs": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz",
|
||||
"integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-portal": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.3.tgz",
|
||||
"integrity": "sha512-xLYZeHrWoPmA5mEKEfZZevoVRK/Q43GfzRXkWV6qawIWWK8t6ifIiLQdd7rmQ4Vk1bmI21XhqF9BN3jWf+phpA==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-primitive": "1.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-primitive": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz",
|
||||
"integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-slot": "1.0.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz",
|
||||
"integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-compose-refs": "1.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@reach/observe-rect": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@reach/observe-rect/-/observe-rect-1.2.0.tgz",
|
||||
"integrity": "sha512-Ba7HmkFgfQxZqqaeIWWkNK0rEhpxVQHIoVyW1YDSkGsGIXzcaW4deC8B0pZrNSSyLTdIk7y+5olKt5+g0GmFIQ=="
|
||||
},
|
||||
"node_modules/classnames": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz",
|
||||
|
@ -522,19 +422,6 @@
|
|||
"@esbuild/win32-x64": "0.19.2"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-equals": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-2.0.4.tgz",
|
||||
"integrity": "sha512-caj/ZmjHljPrZtbzJ3kfH5ia/k4mTJe/qSiXAGzxZWRZgsgDV0cvNaQULqUX8t0/JVlzzEdYOwCN5DmzTxoD4w=="
|
||||
},
|
||||
"node_modules/fuse.js": {
|
||||
"version": "6.6.2",
|
||||
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-6.6.2.tgz",
|
||||
"integrity": "sha512-cJaJkxCCxC8qIIcPBF9yGxY0W/tVZS3uEISDxhYIdtk8OL93pe+6Zj7LjCqVV4dzbqcriOZ+kQ/NE4RXZHsIGA==",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/glightbox": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/glightbox/-/glightbox-3.2.0.tgz",
|
||||
|
@ -548,60 +435,6 @@
|
|||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
|
||||
},
|
||||
"node_modules/kbar": {
|
||||
"version": "0.1.0-beta.43",
|
||||
"resolved": "https://registry.npmjs.org/kbar/-/kbar-0.1.0-beta.43.tgz",
|
||||
"integrity": "sha512-MmhhvGuZfmA616X9wuy/iaWCPFmlEi6kGkvce/7GlatWmCSkHZhD8glxUruFUpxSN0HZvW/6e/jvSgpRGnC76w==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-portal": "^1.0.1",
|
||||
"fast-equals": "^2.0.3",
|
||||
"fuse.js": "^6.6.2",
|
||||
"react-virtual": "^2.8.2",
|
||||
"tiny-invariant": "^1.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.0.0 || ^17.0.0 || ^18.0.0",
|
||||
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/kbar/node_modules/react-virtual": {
|
||||
"version": "2.10.4",
|
||||
"resolved": "https://registry.npmjs.org/react-virtual/-/react-virtual-2.10.4.tgz",
|
||||
"integrity": "sha512-Ir6+oPQZTVHfa6+JL9M7cvMILstFZH/H3jqeYeKI4MSUX+rIruVwFC6nGVXw9wqAw8L0Kg2KvfXxI85OvYQdpQ==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/tannerlinsley"
|
||||
],
|
||||
"dependencies": {
|
||||
"@reach/observe-rect": "^1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.6.3 || ^17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||
"dependencies": {
|
||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"loose-envify": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/phoenix": {
|
||||
"resolved": "../deps/phoenix",
|
||||
"link": true
|
||||
|
@ -614,54 +447,10 @@
|
|||
"resolved": "../deps/phoenix_live_view",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz",
|
||||
"integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz",
|
||||
"integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"scheduler": "^0.20.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "17.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/regenerator-runtime": {
|
||||
"version": "0.14.0",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz",
|
||||
"integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA=="
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz",
|
||||
"integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/tablesort": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/tablesort/-/tablesort-5.3.0.tgz",
|
||||
"integrity": "sha512-WkfcZBHsp47gVH9CBHG0ZXopriG01IA87arGrchvIe868d4RiXVvoYPS1zMq9IdW05kBs5iGsqxTABqLyWonbg=="
|
||||
},
|
||||
"node_modules/tiny-invariant": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz",
|
||||
"integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw=="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,11 @@
|
|||
{
|
||||
"devDependencies": {
|
||||
"esbuild": "^0.19.2",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2"
|
||||
"esbuild": "^0.19.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"classnames": "^2.3.2",
|
||||
"glightbox": "^3.2.0",
|
||||
"highlight.js": "^11.8.0",
|
||||
"kbar": "^0.1.0-beta.43",
|
||||
"phoenix": "file:../deps/phoenix",
|
||||
"phoenix_html": "file:../deps/phoenix_html",
|
||||
"phoenix_live_view": "file:../deps/phoenix_live_view",
|
||||
|
|
3
lib/chiya/flop.ex
Normal file
3
lib/chiya/flop.ex
Normal file
|
@ -0,0 +1,3 @@
|
|||
defmodule Chiya.Flop do
|
||||
use Flop, repo: Chiya.Repo, default_limit: 10
|
||||
end
|
|
@ -39,6 +39,14 @@ defmodule Chiya.Notes do
|
|||
|> Repo.preload(@preloads)
|
||||
end
|
||||
|
||||
def list_admin_notes(params) do
|
||||
q =
|
||||
Note
|
||||
|> order_by([n], desc: n.updated_at, desc: n.published_at)
|
||||
|
||||
Chiya.Flop.validate_and_run(q, params, for: Chiya.Notes.Note)
|
||||
end
|
||||
|
||||
def list_home_notes(channel, params) do
|
||||
q =
|
||||
list_notes_by_channel_query(channel)
|
||||
|
@ -46,10 +54,7 @@ defmodule Chiya.Notes do
|
|||
|> order_by([n], desc: n.updated_at, desc: n.published_at)
|
||||
|> preload(^@preloads)
|
||||
|
||||
Flop.validate_and_run(q, params,
|
||||
for: Chiya.Notes.Note,
|
||||
repo: Chiya.Repo
|
||||
)
|
||||
Chiya.Flop.validate_and_run(q, params, for: Chiya.Notes.Note)
|
||||
end
|
||||
|
||||
def list_notes_by_channel(%Chiya.Channels.Channel{} = channel) do
|
||||
|
|
|
@ -17,8 +17,8 @@ defmodule ChiyaWeb.CoreComponents do
|
|||
|
||||
alias Phoenix.LiveView.JS
|
||||
import ChiyaWeb.Gettext
|
||||
|
||||
import ChiyaWeb.DarkModeToggle
|
||||
import Flop.Phoenix
|
||||
|
||||
def favicon(assigns) do
|
||||
~H"""
|
||||
|
@ -738,6 +738,26 @@ defmodule ChiyaWeb.CoreComponents do
|
|||
"""
|
||||
end
|
||||
|
||||
attr :meta, Flop.Meta, required: true
|
||||
attr :id, :string, default: nil
|
||||
attr :on_change, :string, default: "update-filter"
|
||||
attr :target, :string, default: nil
|
||||
attr :fields, :list, default: []
|
||||
|
||||
def filter_form(%{meta: meta} = assigns) do
|
||||
assigns = assign(assigns, form: Phoenix.Component.to_form(meta), meta: nil)
|
||||
|
||||
~H"""
|
||||
<.form for={@form} id={@id} phx-target={@target} phx-change={@on_change} phx-submit={@on_change}>
|
||||
<.filter_fields :let={i} form={@form} fields={@fields}>
|
||||
<.input field={i.field} label={i.label} type={i.type} phx-debounce={120} {i.rest} />
|
||||
</.filter_fields>
|
||||
|
||||
<.button class="button" name="reset">reset</.button>
|
||||
</.form>
|
||||
"""
|
||||
end
|
||||
|
||||
## JS Commands
|
||||
|
||||
def show(js \\ %JS{}, selector) do
|
||||
|
|
|
@ -40,9 +40,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main class="px-4 py-20 sm:px-6 lg:px-8">
|
||||
<div class="mx-auto max-w-2xl">
|
||||
<.flash_group flash={@flash} />
|
||||
<%= @inner_content %>
|
||||
</div>
|
||||
<main class="container py-6">
|
||||
<.flash_group flash={@flash} />
|
||||
<%= @inner_content %>
|
||||
</main>
|
||||
|
|
|
@ -66,6 +66,12 @@
|
|||
</a>
|
||||
</li>
|
||||
<% end %>
|
||||
<li>
|
||||
<a href="#" id="dark-mode-toggle">
|
||||
<span class="hidden dark:inline">🌙</span>
|
||||
<span class="inline dark:hidden">☀️</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
@ -73,23 +79,27 @@
|
|||
<main id="site-content" class="container print:hidden">
|
||||
<aside id="primary-sidebar">
|
||||
<nav class="prose max-w-none">
|
||||
<h3>Channels</h3>
|
||||
<ul class="menu">
|
||||
<%= for channel <- @channels do %>
|
||||
<li>
|
||||
<a href={~p"/channel/#{channel.slug}"}>
|
||||
<%= channel.name %>
|
||||
</a>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<div class="menu">
|
||||
<h3>Channels</h3>
|
||||
<ul>
|
||||
<%= for channel <- @channels do %>
|
||||
<li>
|
||||
<a href={~p"/channel/#{channel.slug}"}>
|
||||
<%= channel.name %>
|
||||
</a>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h3>Elsewhere</h3>
|
||||
<ul class="menu">
|
||||
<%= for identity <- @public_identities do %>
|
||||
<li><a href={identity.url}><%= identity.name %></a></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<div class="menu">
|
||||
<h3>Elsewhere</h3>
|
||||
<ul>
|
||||
<%= for identity <- @public_identities do %>
|
||||
<li><a href={identity.url}><%= identity.name %></a></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
|
@ -98,7 +108,7 @@
|
|||
</section>
|
||||
</main>
|
||||
|
||||
<footer class="max-w-full mt-8 p-8 text-theme-base/75 bg-theme-background1 print:hidden">
|
||||
<footer id="site-footer" class="container">
|
||||
<p class="container text-center">
|
||||
Struggling to make a decent website since 2011
|
||||
</p>
|
||||
|
|
|
@ -5,29 +5,6 @@ defmodule ChiyaWeb.NoteController do
|
|||
alias Chiya.Notes
|
||||
alias Chiya.Notes.{Note, NoteImport}
|
||||
|
||||
def index(conn, %{"channel" => channel_slug}) do
|
||||
channel = Chiya.Channels.get_channel_by_slug!(channel_slug)
|
||||
notes = Notes.list_notes_by_channel(channel)
|
||||
|
||||
conn
|
||||
|> with_channels()
|
||||
|> render(:index,
|
||||
notes: notes,
|
||||
page_title: "Notes"
|
||||
)
|
||||
end
|
||||
|
||||
def index(conn, _params) do
|
||||
notes = Notes.list_notes()
|
||||
|
||||
conn
|
||||
|> with_channels()
|
||||
|> render(:index,
|
||||
notes: notes,
|
||||
page_title: "Notes"
|
||||
)
|
||||
end
|
||||
|
||||
def new(conn, _params) do
|
||||
default_channels = get_default_channels(conn)
|
||||
|
||||
|
@ -67,10 +44,6 @@ defmodule ChiyaWeb.NoteController do
|
|||
end
|
||||
end
|
||||
|
||||
def show(conn, %{"id" => id}) do
|
||||
live_render(conn, NoteShowLive, session: %{"note_id" => id})
|
||||
end
|
||||
|
||||
def edit(conn, %{"id" => id}) do
|
||||
note = Notes.get_note_preloaded!(id)
|
||||
changeset = Notes.change_note(note)
|
||||
|
|
|
@ -34,3 +34,15 @@
|
|||
<:col :let={note} label="Updated at"><%= from_now(note.updated_at) %></:col>
|
||||
<:col :let={note} label="Published at"><%= from_now(note.published_at) %></:col>
|
||||
</.table>
|
||||
|
||||
<.filter_form fields={[:name]} meta={@meta} id="user-filter-form" />
|
||||
|
||||
<Flop.Phoenix.table items={@notes} meta={@meta} path={~p"/admin/notes"}>
|
||||
<:col :let={note} label="Name" field={:name}><%= note.name %></:col>
|
||||
<:col :let={note} label="Updated at" field={:updated_at}><%= from_now(note.updated_at) %></:col>
|
||||
<:col :let={note} label="Published at" field={:published_at}>
|
||||
<%= from_now(note.published_at) %>
|
||||
</:col>
|
||||
</Flop.Phoenix.table>
|
||||
|
||||
<Flop.Phoenix.pagination meta={@meta} path={~p"/admin/notes"} />
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<section class="page-grid">
|
||||
<section>
|
||||
<article class="h-entry hentry | stack container">
|
||||
<header>
|
||||
<h1 class="p-name | text-3xl leading-10 font-bold text-theme-base1">
|
||||
<header class="prose max-w-none">
|
||||
<h1 class="p-name">
|
||||
<%= @note.name %>
|
||||
</h1>
|
||||
</header>
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
<%= raw(Markdown.render(note.content)) %>
|
||||
</div>
|
||||
|
||||
<footer class="text-slate-700">
|
||||
<footer class="prose max-w-none">
|
||||
<a href={~p"/note/#{note.slug}"}>
|
||||
<time>
|
||||
<%= pretty_datetime(note.published_at) %>
|
||||
|
|
67
lib/chiya_web/live/note_list_live.ex
Normal file
67
lib/chiya_web/live/note_list_live.ex
Normal file
|
@ -0,0 +1,67 @@
|
|||
defmodule ChiyaWeb.NoteListLive do
|
||||
use ChiyaWeb, :live_view
|
||||
|
||||
@impl true
|
||||
def mount(_params, __session, socket) do
|
||||
{:ok, {notes, meta}} = Chiya.Notes.list_admin_notes(%{})
|
||||
{:ok, socket |> assign(%{notes: notes, meta: meta})}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_params(params, _uri, socket) do
|
||||
case Chiya.Notes.list_admin_notes(params) do
|
||||
{:ok, {notes, meta}} ->
|
||||
{:noreply, socket |> assign(%{notes: notes, meta: meta})}
|
||||
|
||||
{:error, data} ->
|
||||
IO.inspect(data)
|
||||
{:noreply, socket}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("update-filter", params, socket) do
|
||||
params = Map.delete(params, "_target")
|
||||
{:noreply, push_patch(socket, to: ~p"/admin/notes?#{params}")}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<.header>
|
||||
<.icon name="hero-document-text" /> Notes
|
||||
<:subtitle>Notes are the content, the heart of your site.</:subtitle>
|
||||
<:actions>
|
||||
<.link href={~p"/admin/notes/new"}>
|
||||
<.button><.icon name="hero-plus-small" /> New Note</.button>
|
||||
</.link>
|
||||
<.link href={~p"/admin/notes/import"}>
|
||||
<.button><.icon name="hero-arrow-down-tray" /> Import Note</.button>
|
||||
</.link>
|
||||
</:actions>
|
||||
</.header>
|
||||
|
||||
<section>
|
||||
<.filter_form fields={[name: [op: :ilike_and]]} meta={@meta} id="user-filter-form" />
|
||||
</section>
|
||||
|
||||
<section class="flex flex-col gap-3 mt-6">
|
||||
<%= for note <- @notes do %>
|
||||
<article class="bg-slate-100 dark:bg-slate-800 text-slate-900 dark:text-slate-100 p-3 rounded">
|
||||
<header>
|
||||
<h2 class="text-xl leading-normal">
|
||||
<a href={"/admin/notes/#{note.id}"}><%= note.name %></a>
|
||||
</h2>
|
||||
</header>
|
||||
<footer class="flex gap-3 text-sm ">
|
||||
<span>Updated <%= from_now(note.updated_at) %></span>
|
||||
<span>Published <%= from_now(note.published_at) %></span>
|
||||
</footer>
|
||||
</article>
|
||||
<% end %>
|
||||
</section>
|
||||
|
||||
<Flop.Phoenix.pagination meta={@meta} path={~p"/admin/notes"} />
|
||||
"""
|
||||
end
|
||||
end
|
|
@ -70,7 +70,7 @@ defmodule ChiyaWeb.Router do
|
|||
live "/", AdminHomeLive, :index
|
||||
|
||||
resources "/channels", ChannelController
|
||||
resources "/notes", NoteController, except: [:show]
|
||||
resources "/notes", NoteController, except: [:show, :index]
|
||||
resources "/settings", SettingController, singleton: true
|
||||
resources "/identities", IdentityController
|
||||
resources "/comments", CommentController, only: [:index, :show]
|
||||
|
@ -79,7 +79,9 @@ defmodule ChiyaWeb.Router do
|
|||
get "/notes/import", NoteController, :import_prepare
|
||||
post "/notes/import", NoteController, :import_run
|
||||
|
||||
live "/notes", NoteListLive, :index
|
||||
live "/notes/:id", NoteShowLive, :show
|
||||
|
||||
get "/notes/:id/raw", NoteController, :raw
|
||||
get "/notes/:id/publish", NoteController, :publish
|
||||
get "/notes/:id/unpublish", NoteController, :unpublish
|
||||
|
|
Loading…
Reference in a new issue