remove kbar, add flop, clean up #308

Merged
inhji merged 2 commits from devel into main 2023-09-10 10:58:40 +02:00
18 changed files with 209 additions and 620 deletions

View file

@ -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;
}
/*

View file

@ -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
View 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')
}
})
}

View file

@ -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,
}}
>
&rsaquo;
</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>
)
}

View file

@ -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
View file

@ -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=="
}
}
}

View file

@ -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
View file

@ -0,0 +1,3 @@
defmodule Chiya.Flop do
use Flop, repo: Chiya.Repo, default_limit: 10
end

View file

@ -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

View file

@ -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

View file

@ -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>

View file

@ -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>

View file

@ -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)

View file

@ -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"} />

View file

@ -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>

View file

@ -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) %>

View 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

View file

@ -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