Compare commits
No commits in common. "devel" and "renovate/gettext-0.x" have entirely different histories.
devel
...
renovate/g
69 changed files with 1647 additions and 2762 deletions
216
.credo.exs
216
.credo.exs
|
@ -1,216 +0,0 @@
|
|||
# This file contains the configuration for Credo and you are probably reading
|
||||
# this after creating it with `mix credo.gen.config`.
|
||||
#
|
||||
# If you find anything wrong or unclear in this file, please report an
|
||||
# issue on GitHub: https://github.com/rrrene/credo/issues
|
||||
#
|
||||
%{
|
||||
#
|
||||
# You can have as many configs as you like in the `configs:` field.
|
||||
configs: [
|
||||
%{
|
||||
#
|
||||
# Run any config using `mix credo -C <name>`. If no config name is given
|
||||
# "default" is used.
|
||||
#
|
||||
name: "default",
|
||||
#
|
||||
# These are the files included in the analysis:
|
||||
files: %{
|
||||
#
|
||||
# You can give explicit globs or simply directories.
|
||||
# In the latter case `**/*.{ex,exs}` will be used.
|
||||
#
|
||||
included: [
|
||||
"lib/",
|
||||
"src/",
|
||||
"test/",
|
||||
"web/",
|
||||
"apps/*/lib/",
|
||||
"apps/*/src/",
|
||||
"apps/*/test/",
|
||||
"apps/*/web/"
|
||||
],
|
||||
excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"]
|
||||
},
|
||||
#
|
||||
# Load and configure plugins here:
|
||||
#
|
||||
plugins: [],
|
||||
#
|
||||
# If you create your own checks, you must specify the source files for
|
||||
# them here, so they can be loaded by Credo before running the analysis.
|
||||
#
|
||||
requires: [],
|
||||
#
|
||||
# If you want to enforce a style guide and need a more traditional linting
|
||||
# experience, you can change `strict` to `true` below:
|
||||
#
|
||||
strict: false,
|
||||
#
|
||||
# To modify the timeout for parsing files, change this value:
|
||||
#
|
||||
parse_timeout: 5000,
|
||||
#
|
||||
# If you want to use uncolored output by default, you can change `color`
|
||||
# to `false` below:
|
||||
#
|
||||
color: false,
|
||||
#
|
||||
# You can customize the parameters of any check by adding a second element
|
||||
# to the tuple.
|
||||
#
|
||||
# To disable a check put `false` as second element:
|
||||
#
|
||||
# {Credo.Check.Design.DuplicatedCode, false}
|
||||
#
|
||||
checks: %{
|
||||
enabled: [
|
||||
#
|
||||
## Consistency Checks
|
||||
#
|
||||
{Credo.Check.Consistency.ExceptionNames, []},
|
||||
{Credo.Check.Consistency.LineEndings, []},
|
||||
{Credo.Check.Consistency.ParameterPatternMatching, []},
|
||||
{Credo.Check.Consistency.SpaceAroundOperators, []},
|
||||
{Credo.Check.Consistency.SpaceInParentheses, []},
|
||||
{Credo.Check.Consistency.TabsOrSpaces, []},
|
||||
|
||||
#
|
||||
## Design Checks
|
||||
#
|
||||
# You can customize the priority of any check
|
||||
# Priority values are: `low, normal, high, higher`
|
||||
#
|
||||
{Credo.Check.Design.AliasUsage,
|
||||
[priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 0]},
|
||||
# You can also customize the exit_status of each check.
|
||||
# If you don't want TODO comments to cause `mix credo` to fail, just
|
||||
# set this value to 0 (zero).
|
||||
#
|
||||
{Credo.Check.Design.TagTODO, [exit_status: 2]},
|
||||
{Credo.Check.Design.TagFIXME, []},
|
||||
|
||||
#
|
||||
## Readability Checks
|
||||
#
|
||||
{Credo.Check.Readability.AliasOrder, []},
|
||||
{Credo.Check.Readability.FunctionNames, []},
|
||||
{Credo.Check.Readability.LargeNumbers, []},
|
||||
{Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]},
|
||||
{Credo.Check.Readability.ModuleAttributeNames, []},
|
||||
{Credo.Check.Readability.ModuleDoc, []},
|
||||
{Credo.Check.Readability.ModuleNames, []},
|
||||
{Credo.Check.Readability.ParenthesesInCondition, []},
|
||||
{Credo.Check.Readability.ParenthesesOnZeroArityDefs, []},
|
||||
{Credo.Check.Readability.PipeIntoAnonymousFunctions, []},
|
||||
{Credo.Check.Readability.PredicateFunctionNames, []},
|
||||
{Credo.Check.Readability.PreferImplicitTry, []},
|
||||
{Credo.Check.Readability.RedundantBlankLines, []},
|
||||
{Credo.Check.Readability.Semicolons, []},
|
||||
{Credo.Check.Readability.SpaceAfterCommas, []},
|
||||
{Credo.Check.Readability.StringSigils, []},
|
||||
{Credo.Check.Readability.TrailingBlankLine, []},
|
||||
{Credo.Check.Readability.TrailingWhiteSpace, []},
|
||||
{Credo.Check.Readability.UnnecessaryAliasExpansion, []},
|
||||
{Credo.Check.Readability.VariableNames, []},
|
||||
{Credo.Check.Readability.WithSingleClause, []},
|
||||
|
||||
#
|
||||
## Refactoring Opportunities
|
||||
#
|
||||
{Credo.Check.Refactor.Apply, []},
|
||||
{Credo.Check.Refactor.CondStatements, []},
|
||||
{Credo.Check.Refactor.CyclomaticComplexity, []},
|
||||
{Credo.Check.Refactor.FunctionArity, []},
|
||||
{Credo.Check.Refactor.LongQuoteBlocks, []},
|
||||
{Credo.Check.Refactor.MatchInCondition, []},
|
||||
{Credo.Check.Refactor.MapJoin, []},
|
||||
{Credo.Check.Refactor.NegatedConditionsInUnless, []},
|
||||
{Credo.Check.Refactor.NegatedConditionsWithElse, []},
|
||||
{Credo.Check.Refactor.Nesting, []},
|
||||
{Credo.Check.Refactor.UnlessWithElse, []},
|
||||
{Credo.Check.Refactor.WithClauses, []},
|
||||
{Credo.Check.Refactor.FilterCount, []},
|
||||
{Credo.Check.Refactor.FilterFilter, []},
|
||||
{Credo.Check.Refactor.RejectReject, []},
|
||||
{Credo.Check.Refactor.RedundantWithClauseResult, []},
|
||||
|
||||
#
|
||||
## Warnings
|
||||
#
|
||||
{Credo.Check.Warning.ApplicationConfigInModuleAttribute, []},
|
||||
{Credo.Check.Warning.BoolOperationOnSameValues, []},
|
||||
{Credo.Check.Warning.Dbg, []},
|
||||
{Credo.Check.Warning.ExpensiveEmptyEnumCheck, []},
|
||||
{Credo.Check.Warning.IExPry, []},
|
||||
{Credo.Check.Warning.IoInspect, []},
|
||||
{Credo.Check.Warning.MissedMetadataKeyInLoggerConfig, []},
|
||||
{Credo.Check.Warning.OperationOnSameValues, []},
|
||||
{Credo.Check.Warning.OperationWithConstantResult, []},
|
||||
{Credo.Check.Warning.RaiseInsideRescue, []},
|
||||
{Credo.Check.Warning.SpecWithStruct, []},
|
||||
{Credo.Check.Warning.WrongTestFileExtension, []},
|
||||
{Credo.Check.Warning.UnusedEnumOperation, []},
|
||||
{Credo.Check.Warning.UnusedFileOperation, []},
|
||||
{Credo.Check.Warning.UnusedKeywordOperation, []},
|
||||
{Credo.Check.Warning.UnusedListOperation, []},
|
||||
{Credo.Check.Warning.UnusedPathOperation, []},
|
||||
{Credo.Check.Warning.UnusedRegexOperation, []},
|
||||
{Credo.Check.Warning.UnusedStringOperation, []},
|
||||
{Credo.Check.Warning.UnusedTupleOperation, []},
|
||||
{Credo.Check.Warning.UnsafeExec, []}
|
||||
],
|
||||
disabled: [
|
||||
#
|
||||
# Checks scheduled for next check update (opt-in for now, just replace `false` with `[]`)
|
||||
|
||||
#
|
||||
# Controversial and experimental checks (opt-in, just move the check to `:enabled`
|
||||
# and be sure to use `mix credo --strict` to see low priority checks)
|
||||
#
|
||||
{Credo.Check.Consistency.MultiAliasImportRequireUse, []},
|
||||
{Credo.Check.Consistency.UnusedVariableNames, []},
|
||||
{Credo.Check.Design.DuplicatedCode, []},
|
||||
{Credo.Check.Design.SkipTestWithoutComment, []},
|
||||
{Credo.Check.Readability.AliasAs, []},
|
||||
{Credo.Check.Readability.BlockPipe, []},
|
||||
{Credo.Check.Readability.ImplTrue, []},
|
||||
{Credo.Check.Readability.MultiAlias, []},
|
||||
{Credo.Check.Readability.NestedFunctionCalls, []},
|
||||
{Credo.Check.Readability.OneArityFunctionInPipe, []},
|
||||
{Credo.Check.Readability.SeparateAliasRequire, []},
|
||||
{Credo.Check.Readability.SingleFunctionToBlockPipe, []},
|
||||
{Credo.Check.Readability.SinglePipe, []},
|
||||
{Credo.Check.Readability.Specs, []},
|
||||
{Credo.Check.Readability.StrictModuleLayout, []},
|
||||
{Credo.Check.Readability.WithCustomTaggedTuple, []},
|
||||
{Credo.Check.Readability.OnePipePerLine, []},
|
||||
{Credo.Check.Refactor.ABCSize, []},
|
||||
{Credo.Check.Refactor.AppendSingleItem, []},
|
||||
{Credo.Check.Refactor.DoubleBooleanNegation, []},
|
||||
{Credo.Check.Refactor.FilterReject, []},
|
||||
{Credo.Check.Refactor.IoPuts, []},
|
||||
{Credo.Check.Refactor.MapMap, []},
|
||||
{Credo.Check.Refactor.ModuleDependencies, []},
|
||||
{Credo.Check.Refactor.NegatedIsNil, []},
|
||||
{Credo.Check.Refactor.PassAsyncInTestCases, []},
|
||||
{Credo.Check.Refactor.PipeChainStart, []},
|
||||
{Credo.Check.Refactor.RejectFilter, []},
|
||||
{Credo.Check.Refactor.VariableRebinding, []},
|
||||
{Credo.Check.Warning.LazyLogging, []},
|
||||
{Credo.Check.Warning.LeakyEnvironment, []},
|
||||
{Credo.Check.Warning.MapGetUnsafePass, []},
|
||||
{Credo.Check.Warning.MixEnv, []},
|
||||
{Credo.Check.Warning.UnsafeToAtom, []}
|
||||
|
||||
# {Credo.Check.Refactor.MapInto, []},
|
||||
|
||||
#
|
||||
# Custom checks can be created using `mix credo.gen.check`.
|
||||
#
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -14,14 +14,10 @@ const plugins = [
|
|||
]
|
||||
|
||||
let opts = {
|
||||
entryPoints: [
|
||||
'js/app.js',
|
||||
'js/public.js'
|
||||
],
|
||||
entryPoints: ['js/app.js', 'js/public.js'],
|
||||
bundle: true,
|
||||
target: 'es2016',
|
||||
target: 'es2017',
|
||||
outdir: '../priv/static/assets',
|
||||
external: ["*.css", "fonts/*", "images/*"],
|
||||
logLevel: 'info',
|
||||
loader,
|
||||
plugins
|
||||
|
|
|
@ -2,247 +2,145 @@
|
|||
@import "tailwindcss/components";
|
||||
@import "tailwindcss/utilities";
|
||||
|
||||
@import "./hljs.css";
|
||||
@import "./reset.css";
|
||||
@import "./gruvbox.css";
|
||||
@import "./lightbox.css";
|
||||
@import "./tablesort.css";
|
||||
|
||||
/*
|
||||
The base layer is for things like reset rules or default styles applied to plain HTML elements.
|
||||
*/
|
||||
@layer base {
|
||||
:root {
|
||||
--color-foreground: 15 23 42;
|
||||
--color-background: 241 245 249;
|
||||
--font-features: "case", "cpsp", "frac", "salt", "ss02", "ccmp";
|
||||
}
|
||||
|
||||
:root {
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-feature-settings: var(--font-features);
|
||||
}
|
||||
@supports (font-variation-settings: normal) {
|
||||
:root {
|
||||
font-family: 'Inter var', sans-serif;
|
||||
font-feature-settings: var(--font-features);
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
--color-primary: 214 93 14; /* orange */
|
||||
--color-primary1: 175 58 3; /* orange faded */
|
||||
|
||||
--color-secondary: 104 157 106; /* aqua */
|
||||
--color-secondary1: 66 123 88; /* aqua faded */
|
||||
|
||||
--color-blue: 69 133 136 ; /* blue */
|
||||
--color-blue1: 7 102 120; /* blue faded */
|
||||
|
||||
--color-purple: 177 98 134 ; /* purple */
|
||||
--color-purple1: 143 63 113; /* purple faded */
|
||||
|
||||
--color-yellow: 215 153 33; /* yellow */
|
||||
--color-yellow1: 181 118 20; /* yellow faded */
|
||||
|
||||
--color-background: 253 244 193; /* light0 */
|
||||
--color-background1: 235 219 178; /* light1 */
|
||||
|
||||
--color-foreground: 60 56 54; /* dark1 */
|
||||
--color-foreground1: 80 73 69; /* dark2 */
|
||||
|
||||
--color-heading: var(--color-secondary);
|
||||
|
||||
@apply selection:bg-theme-primary/50;
|
||||
}
|
||||
|
||||
:root[data-mode=dark] {
|
||||
--color-foreground: 241 245 249;
|
||||
--color-background: 15 23 42;
|
||||
}
|
||||
--color-primary: 214 93 14; /* orange */
|
||||
--color-primary1: 254 128 25; /* orange bright */
|
||||
|
||||
html {
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-feature-settings: "case", "cpsp", "frac", "salt", "ccmp", "cv01", "cv02", "cv03", "cv04", "cv05", "cv06", "cv07", "cv09", "cv10", "cv11";
|
||||
}
|
||||
--color-secondary: 104 157 106; /* aqua */
|
||||
--color-secondary1: 142 192 124; /* aqua bright */
|
||||
|
||||
body#site-body {
|
||||
@apply bg-gradient-to-br from-background from-50% to-rose-100 dark:to-rose-950 text-foreground;
|
||||
}
|
||||
--color-blue: 69 133 136 ; /* blue */
|
||||
--color-blue1: 131 165 152; /* blue bright */
|
||||
|
||||
.stack > * + * {
|
||||
margin-block-start: var(--flow-space, 1em);
|
||||
}
|
||||
--color-purple: 177 98 134 ; /* purple */
|
||||
--color-purple1: 250 189 47; /* purple bright */
|
||||
|
||||
:root .prose {
|
||||
@apply prose-inhji;
|
||||
--color-yellow: 215 153 33; /* yellow */
|
||||
--color-yellow1: 181 118 20; /* yellow bright */
|
||||
|
||||
--color-background: 40 40 40; /* dark0 */
|
||||
--color-background1: 60 56 54; /* dark1 */
|
||||
|
||||
--color-foreground: 235 219 178; /* light1 */
|
||||
--color-foreground1: 213 196 161; /* light2 */
|
||||
|
||||
--color-heading: var(--color-secondary);
|
||||
}
|
||||
|
||||
:root[data-mode=dark] .prose {
|
||||
@apply prose-invert;
|
||||
}
|
||||
/* https://stackoverflow.com/questions/5379752/css-style-external-links */
|
||||
a[href]:not(:where(
|
||||
/* exclude hash only links */
|
||||
[href^="#"],
|
||||
/* exclude relative but not double slash only links */
|
||||
[href^="/"]:not([href^="//"]),
|
||||
/* domains to exclude */
|
||||
[href*="//inhji.de"],
|
||||
/* subdomains to exclude */
|
||||
[href*="//cloud.inhji.de"],
|
||||
)):after {
|
||||
content: '↬';
|
||||
|
||||
.prose a, a.underline-link {
|
||||
@apply underline decoration-1 decoration-theme-primary hover:decoration-theme-base transition;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* ============= SITE LAYOUT =============
|
||||
*/
|
||||
|
||||
/* === SITE HEADER === */
|
||||
|
||||
#site-header {
|
||||
@apply sticky top-0 block px-3 py-6 bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 text-white print:hidden;
|
||||
|
||||
& nav ul {
|
||||
@apply flex;
|
||||
.prose a[href^="http://"], .prose a[href^="https://"] {
|
||||
@apply after:content-['_↗']
|
||||
}
|
||||
|
||||
& a {
|
||||
@apply p-3 rounded transition border border-transparent hover:border-white;
|
||||
.prose em {
|
||||
@apply text-theme-quaternary;
|
||||
}
|
||||
|
||||
& #site-title {
|
||||
@apply font-bold uppercase;
|
||||
}
|
||||
.prose h1, h2, h3, h4 {
|
||||
@apply before:font-light before:text-theme-base/25;
|
||||
}
|
||||
|
||||
/* === PRIMARY SIDEBAR === */
|
||||
|
||||
#primary-sidebar {
|
||||
@apply col-span-1;
|
||||
@apply prose-a:no-underline;
|
||||
|
||||
& nav {
|
||||
@apply flex justify-between md:flex-row lg:flex-col mb-6 lg:mb-0;
|
||||
.prose h1 {
|
||||
@apply before:content-['#'];
|
||||
}
|
||||
|
||||
& h3 {
|
||||
@apply m-0;
|
||||
.prose h2 {
|
||||
@apply before:content-['##'];
|
||||
}
|
||||
|
||||
& a.active {
|
||||
@apply text-primary-500;
|
||||
}
|
||||
.prose h3 {
|
||||
@apply before:content-['###'];
|
||||
}
|
||||
|
||||
/* === SITE CONTENT === */
|
||||
|
||||
#site-content {
|
||||
@apply grid grid-cols-1 lg:grid-cols-5 gap-0 lg:gap-12;
|
||||
@apply mt-6 sm:mt-12 px-6 sm:px-0;
|
||||
|
||||
#content-wrapper {
|
||||
@apply col-span-4;
|
||||
.prose h4 {
|
||||
@apply before:content-['####'];
|
||||
}
|
||||
|
||||
#secondary-sidebar {
|
||||
@apply col-span-1;
|
||||
}
|
||||
.prose pre {
|
||||
@apply p-0;
|
||||
}
|
||||
|
||||
/* === SITE FOOTER === */
|
||||
|
||||
#site-footer {
|
||||
@apply p-8 mt-8 max-w-none bg-foreground/20 border-t border-foreground/10;
|
||||
}
|
||||
|
||||
/* === CONCERNING MULTIPLE LAYOUT BLOCKS === */
|
||||
|
||||
#site-content, #site-footer, #primary-sidebar, #secondary-sidebar {
|
||||
@apply hover:prose-a:text-primary-600 hover:prose-a:transition;
|
||||
}
|
||||
|
||||
/*
|
||||
* ============= PAGE LAYOUT =============
|
||||
*/
|
||||
|
||||
/* === PAGE HEADER === */
|
||||
|
||||
header.page-header {
|
||||
@apply border-b border-foreground/50 mb-6 prose max-w-none;
|
||||
}
|
||||
|
||||
header.page-header h1 {
|
||||
@apply text-3xl leading-loose font-bold;
|
||||
}
|
||||
|
||||
header.page-header p {
|
||||
@apply mb-3;
|
||||
}
|
||||
|
||||
/* === PAGE GRID === */
|
||||
|
||||
section.page-grid {
|
||||
@apply grid grid-cols-1 md:grid-cols-3 gap-6;
|
||||
|
||||
> section {
|
||||
@apply col-span-1 md:col-span-2 last:md:col-span-3;
|
||||
}
|
||||
|
||||
> aside {
|
||||
@apply print:hidden;
|
||||
}
|
||||
.prose img {
|
||||
@apply rounded-lg;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
The components layer is for class-based styles that you want to be able to override with utilities.
|
||||
*/
|
||||
@layer components {
|
||||
.note {
|
||||
.note-link {
|
||||
@apply text-center text-neutral-900 dark:text-neutral-100;
|
||||
}
|
||||
/* Set width and color for identity icons */
|
||||
a[rel] svg {
|
||||
width: 1em;
|
||||
fill: rgb(var(--color-foreground));
|
||||
}
|
||||
|
||||
.divider {
|
||||
@apply flex text-primary-500 items-center my-8 w-full mx-auto last:hidden;
|
||||
@apply before:flex-1 before:content-[''] before:bg-neutral-500/25 before:p-[0.5px] before:mr-2;
|
||||
@apply after:flex-1 after:content-[''] after:bg-neutral-500/25 after:p-[0.5px] after:ml-2;
|
||||
.alert {
|
||||
@apply p-3 mt-3 rounded;
|
||||
}
|
||||
|
||||
.dot {
|
||||
@apply before:content-['·']
|
||||
.alert.alert-danger {
|
||||
@apply bg-red-100 text-red-500 dark:bg-red-950 dark:text-red-500;
|
||||
}
|
||||
|
||||
.tag-bar {
|
||||
& .letter {
|
||||
@apply text-neutral-900 dark:text-neutral-100 capitalize border rounded text-sm px-2 py-1 inline-block mb-2;
|
||||
}
|
||||
.footnotes li p { display: inline; }
|
||||
.footnotes hr { display: none; }
|
||||
.footnote:before { content: '{'; }
|
||||
.footnote:after { content: '}'; }
|
||||
|
||||
& li {
|
||||
@apply mb-2;
|
||||
}
|
||||
|
||||
& a {
|
||||
@apply text-neutral-900 dark:text-neutral-100 border rounded text-sm px-2 py-1 inline-block mb-2 transition;
|
||||
}
|
||||
|
||||
& .count {
|
||||
@apply text-sm font-mono rounded-full py-0.5 px-1 align-middle;
|
||||
}
|
||||
}
|
||||
|
||||
.pagination {
|
||||
@apply flex justify-between mt-6;
|
||||
|
||||
& .pagination-previous, & .pagination-next {
|
||||
@apply block py-2 px-3 bg-neutral-700 text-white rounded;
|
||||
}
|
||||
|
||||
& .pagination-previous.disabled, & .pagination-next.disabled {
|
||||
@apply bg-neutral-500;
|
||||
}
|
||||
|
||||
& .pagination-list {
|
||||
@apply hidden;
|
||||
}
|
||||
}
|
||||
|
||||
/* === CARD LIST (ADMIN) === */
|
||||
|
||||
.card-list {
|
||||
@apply flex flex-col gap-3 mt-6;
|
||||
}
|
||||
|
||||
/* === CARD (ADMIN) === */
|
||||
.card {
|
||||
@apply bg-neutral-100 dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100 p-3 rounded;
|
||||
|
||||
& header {
|
||||
& h2 {
|
||||
@apply text-xl leading-normal;
|
||||
}
|
||||
}
|
||||
|
||||
& footer {
|
||||
@apply flex gap-3 text-sm;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
The utilities layer is for small, single-purpose classes that should always take precedence over any other styles.
|
||||
*/
|
||||
@layer utilities {
|
||||
.hljs {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
a.invalid {
|
||||
@apply line-through;
|
||||
a.button {
|
||||
@apply inline-block text-theme-base px-3 py-2.5 border border-theme-background1 hover:bg-theme-background1 rounded transition font-semibold;
|
||||
}
|
||||
}
|
|
@ -1,148 +0,0 @@
|
|||
@import "tailwindcss/base";
|
||||
@import "tailwindcss/components";
|
||||
@import "tailwindcss/utilities";
|
||||
|
||||
@import "./reset.css";
|
||||
@import "./gruvbox.css";
|
||||
@import "./lightbox.css";
|
||||
@import "./tablesort.css";
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--font-features: "case", "cpsp", "frac", "salt", "ccmp", "cv01", "cv02", "cv03", "cv04", "cv05", "cv06", "cv07", "cv09", "cv10", "cv11";
|
||||
}
|
||||
|
||||
:root {
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-feature-settings: var(--font-features);
|
||||
}
|
||||
@supports (font-variation-settings: normal) {
|
||||
:root {
|
||||
font-family: 'Inter var', sans-serif;
|
||||
font-feature-settings: var(--font-features);
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
--color-primary: 214 93 14; /* orange */
|
||||
--color-primary1: 175 58 3; /* orange faded */
|
||||
|
||||
--color-secondary: 104 157 106; /* aqua */
|
||||
--color-secondary1: 66 123 88; /* aqua faded */
|
||||
|
||||
--color-blue: 69 133 136 ; /* blue */
|
||||
--color-blue1: 7 102 120; /* blue faded */
|
||||
|
||||
--color-purple: 177 98 134 ; /* purple */
|
||||
--color-purple1: 143 63 113; /* purple faded */
|
||||
|
||||
--color-yellow: 215 153 33; /* yellow */
|
||||
--color-yellow1: 181 118 20; /* yellow faded */
|
||||
|
||||
--color-background: 253 244 193; /* light0 */
|
||||
--color-background1: 235 219 178; /* light1 */
|
||||
|
||||
--color-foreground: 60 56 54; /* dark1 */
|
||||
--color-foreground1: 80 73 69; /* dark2 */
|
||||
|
||||
--color-heading: var(--color-secondary);
|
||||
|
||||
@apply selection:bg-theme-primary/50;
|
||||
}
|
||||
|
||||
:root[data-mode=dark] {
|
||||
--color-primary: 214 93 14; /* orange */
|
||||
--color-primary1: 254 128 25; /* orange bright */
|
||||
|
||||
--color-secondary: 104 157 106; /* aqua */
|
||||
--color-secondary1: 142 192 124; /* aqua bright */
|
||||
|
||||
--color-blue: 69 133 136 ; /* blue */
|
||||
--color-blue1: 131 165 152; /* blue bright */
|
||||
|
||||
--color-purple: 177 98 134 ; /* purple */
|
||||
--color-purple1: 250 189 47; /* purple bright */
|
||||
|
||||
--color-yellow: 215 153 33; /* yellow */
|
||||
--color-yellow1: 181 118 20; /* yellow bright */
|
||||
|
||||
--color-background: 40 40 40; /* dark0 */
|
||||
--color-background1: 60 56 54; /* dark1 */
|
||||
|
||||
--color-foreground: 235 219 178; /* light1 */
|
||||
--color-foreground1: 213 196 161; /* light2 */
|
||||
|
||||
--color-heading: var(--color-secondary);
|
||||
}
|
||||
|
||||
:root[data-mode=dark] .prose {
|
||||
@apply prose-invert;
|
||||
}
|
||||
|
||||
.prose a, a.underline-link {
|
||||
@apply underline decoration-1 decoration-theme-primary hover:decoration-theme-base transition;
|
||||
}
|
||||
|
||||
.prose a[href^="http://"], .prose a[href^="https://"] {
|
||||
@apply after:content-['_↗']
|
||||
}
|
||||
|
||||
.prose em {
|
||||
@apply text-theme-quaternary;
|
||||
}
|
||||
|
||||
.prose h2 { @apply before:font-light before:text-theme-secondary; }
|
||||
.prose h3 { @apply before:font-light before:text-theme-tertiary; }
|
||||
.prose h4 { @apply before:font-light before:text-theme-quaternary; }
|
||||
|
||||
|
||||
|
||||
.prose h1 {
|
||||
@apply before:content-['#'];
|
||||
}
|
||||
|
||||
.prose h2 {
|
||||
@apply before:content-['##'];
|
||||
}
|
||||
|
||||
.prose h3 {
|
||||
@apply before:content-['###'];
|
||||
}
|
||||
|
||||
.prose h4 {
|
||||
@apply before:content-['####'];
|
||||
}
|
||||
|
||||
.prose pre {
|
||||
@apply p-0;
|
||||
}
|
||||
|
||||
.prose img {
|
||||
@apply rounded-lg;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
/* Set width and color for identity icons */
|
||||
a[rel] svg {
|
||||
width: 1em;
|
||||
fill: rgb(var(--color-foreground));
|
||||
}
|
||||
|
||||
.alert {
|
||||
@apply p-3 mt-3 rounded;
|
||||
}
|
||||
|
||||
.alert.alert-danger {
|
||||
@apply bg-red-100 text-red-500 dark:bg-red-950 dark:text-red-500;
|
||||
}
|
||||
|
||||
.footnotes li p { display: inline; }
|
||||
.footnotes hr { display: none; }
|
||||
.footnote:before { content: '{'; }
|
||||
.footnote:after { content: '}'; }
|
||||
|
||||
a.button {
|
||||
@apply inline-block text-theme-base px-3 py-2.5 border border-theme-background1 hover:bg-theme-background1 rounded transition font-semibold;
|
||||
}
|
||||
}
|
7
assets/css/gruvbox.css
Normal file
7
assets/css/gruvbox.css
Normal file
|
@ -0,0 +1,7 @@
|
|||
/*!
|
||||
Theme: Gruvbox dark, hard
|
||||
Author: Dawid Kurek (dawikur@gmail.com), morhetz (https://github.com/morhetz/gruvbox)
|
||||
License: ~ MIT (or more permissive) [via base16-schemes-source]
|
||||
Maintainer: @highlightjs/core-team
|
||||
Version: 2021.09.0
|
||||
*/pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{color:#d5c4a1;background:#1d2021}.hljs ::selection,.hljs::selection{background-color:#504945;color:#d5c4a1}.hljs-comment{color:#665c54}.hljs-tag{color:#bdae93}.hljs-operator,.hljs-punctuation,.hljs-subst{color:#d5c4a1}.hljs-operator{opacity:.7}.hljs-bullet,.hljs-deletion,.hljs-name,.hljs-selector-tag,.hljs-template-variable,.hljs-variable{color:#fb4934}.hljs-attr,.hljs-link,.hljs-literal,.hljs-number,.hljs-symbol,.hljs-variable.constant_{color:#fe8019}.hljs-class .hljs-title,.hljs-title,.hljs-title.class_{color:#fabd2f}.hljs-strong{font-weight:700;color:#fabd2f}.hljs-addition,.hljs-code,.hljs-string,.hljs-title.class_.inherited__{color:#b8bb26}.hljs-built_in,.hljs-doctag,.hljs-keyword.hljs-atrule,.hljs-quote,.hljs-regexp{color:#8ec07c}.hljs-attribute,.hljs-function .hljs-title,.hljs-section,.hljs-title.function_,.ruby .hljs-property{color:#83a598}.diff .hljs-meta,.hljs-keyword,.hljs-template-tag,.hljs-type{color:#d3869b}.hljs-emphasis{color:#d3869b;font-style:italic}.hljs-meta,.hljs-meta .hljs-keyword,.hljs-meta .hljs-string{color:#d65d0e}.hljs-meta .hljs-keyword,.hljs-meta-keyword{font-weight:700}
|
|
@ -1,201 +0,0 @@
|
|||
:root {
|
||||
/*!
|
||||
Theme: a11y-light
|
||||
Author: @ericwbailey
|
||||
Maintainer: @ericwbailey
|
||||
|
||||
Based on the Tomorrow Night Eighties theme: https://github.com/isagalaev/highlight.js/blob/master/src/styles/tomorrow-night-eighties.css
|
||||
*/
|
||||
|
||||
& .hljs {
|
||||
background: #fefefe;
|
||||
color: #545454;
|
||||
}
|
||||
|
||||
/* Comment */
|
||||
& .hljs-comment,
|
||||
& .hljs-quote {
|
||||
color: #696969;
|
||||
}
|
||||
|
||||
/* Red */
|
||||
& .hljs-variable,
|
||||
& .hljs-template-variable,
|
||||
& .hljs-tag,
|
||||
& .hljs-name,
|
||||
& .hljs-selector-id,
|
||||
& .hljs-selector-class,
|
||||
& .hljs-regexp,
|
||||
& .hljs-deletion {
|
||||
color: #d91e18;
|
||||
}
|
||||
|
||||
/* Orange */
|
||||
& .hljs-number,
|
||||
& .hljs-built_in,
|
||||
& .hljs-literal,
|
||||
& .hljs-type,
|
||||
& .hljs-params,
|
||||
& .hljs-meta,
|
||||
& .hljs-link {
|
||||
color: #aa5d00;
|
||||
}
|
||||
|
||||
/* Yellow */
|
||||
& .hljs-attribute {
|
||||
color: #aa5d00;
|
||||
}
|
||||
|
||||
/* Green */
|
||||
& .hljs-string,
|
||||
& .hljs-symbol,
|
||||
& .hljs-bullet,
|
||||
& .hljs-addition {
|
||||
color: #008000;
|
||||
}
|
||||
|
||||
/* Blue */
|
||||
& .hljs-title,
|
||||
& .hljs-section {
|
||||
color: #007faa;
|
||||
}
|
||||
|
||||
/* Purple */
|
||||
& .hljs-keyword,
|
||||
& .hljs-selector-tag {
|
||||
color: #7928a1;
|
||||
}
|
||||
|
||||
& .hljs-emphasis {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
& .hljs-strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@media screen and (-ms-high-contrast: active) {
|
||||
& .hljs-addition,
|
||||
& .hljs-attribute,
|
||||
& .hljs-built_in,
|
||||
& .hljs-bullet,
|
||||
& .hljs-comment,
|
||||
& .hljs-link,
|
||||
& .hljs-literal,
|
||||
& .hljs-meta,
|
||||
& .hljs-number,
|
||||
& .hljs-params,
|
||||
& .hljs-string,
|
||||
& .hljs-symbol,
|
||||
& .hljs-type,
|
||||
& .hljs-quote {
|
||||
color: highlight;
|
||||
}
|
||||
|
||||
& .hljs-keyword,
|
||||
& .hljs-selector-tag {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:root[data-mode=dark] {
|
||||
/*!
|
||||
Theme: a11y-dark
|
||||
Author: @ericwbailey
|
||||
Maintainer: @ericwbailey
|
||||
|
||||
Based on the Tomorrow Night Eighties theme: https://github.com/isagalaev/highlight.js/blob/master/src/styles/tomorrow-night-eighties.css
|
||||
*/
|
||||
|
||||
& .hljs {
|
||||
background: #2b2b2b;
|
||||
color: #f8f8f2;
|
||||
}
|
||||
|
||||
/* Comment */
|
||||
& .hljs-comment,
|
||||
& .hljs-quote {
|
||||
color: #d4d0ab;
|
||||
}
|
||||
|
||||
/* Red */
|
||||
& .hljs-variable,
|
||||
& .hljs-template-variable,
|
||||
& .hljs-tag,
|
||||
& .hljs-name,
|
||||
& .hljs-selector-id,
|
||||
& .hljs-selector-class,
|
||||
& .hljs-regexp,
|
||||
& .hljs-deletion {
|
||||
color: #ffa07a;
|
||||
}
|
||||
|
||||
/* Orange */
|
||||
& .hljs-number,
|
||||
& .hljs-built_in,
|
||||
& .hljs-literal,
|
||||
& .hljs-type,
|
||||
& .hljs-params,
|
||||
& .hljs-meta,
|
||||
& .hljs-link {
|
||||
color: #f5ab35;
|
||||
}
|
||||
|
||||
/* Yellow */
|
||||
& .hljs-attribute {
|
||||
color: #ffd700;
|
||||
}
|
||||
|
||||
/* Green */
|
||||
& .hljs-string,
|
||||
& .hljs-symbol,
|
||||
& .hljs-bullet,
|
||||
& .hljs-addition {
|
||||
color: #abe338;
|
||||
}
|
||||
|
||||
/* Blue */
|
||||
& .hljs-title,
|
||||
& .hljs-section {
|
||||
color: #00e0e0;
|
||||
}
|
||||
|
||||
/* Purple */
|
||||
& .hljs-keyword,
|
||||
& .hljs-selector-tag {
|
||||
color: #dcc6e0;
|
||||
}
|
||||
|
||||
& .hljs-emphasis {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
& .hljs-strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@media screen and (-ms-high-contrast: active) {
|
||||
& .hljs-addition,
|
||||
& .hljs-attribute,
|
||||
& .hljs-built_in,
|
||||
& .hljs-bullet,
|
||||
& .hljs-comment,
|
||||
& .hljs-link,
|
||||
& .hljs-literal,
|
||||
& .hljs-meta,
|
||||
& .hljs-number,
|
||||
& .hljs-params,
|
||||
& .hljs-string,
|
||||
& .hljs-symbol,
|
||||
& .hljs-type,
|
||||
& .hljs-quote {
|
||||
color: highlight;
|
||||
}
|
||||
|
||||
& .hljs-keyword,
|
||||
& .hljs-selector-tag {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
52
assets/css/reset.css
Normal file
52
assets/css/reset.css
Normal file
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
1. Use a more-intuitive box-sizing model.
|
||||
*/
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
/*
|
||||
2. Remove default margin
|
||||
*/
|
||||
* {
|
||||
margin: 0;
|
||||
}
|
||||
/*
|
||||
3. Allow percentage-based heights in the application
|
||||
*/
|
||||
html, body {
|
||||
height: 100%;
|
||||
}
|
||||
/*
|
||||
Typographic tweaks!
|
||||
4. Add accessible line-height
|
||||
5. Improve text rendering
|
||||
*/
|
||||
body {
|
||||
line-height: 1.5;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
/*
|
||||
6. Improve media defaults
|
||||
*/
|
||||
img, picture, video, canvas, svg {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
}
|
||||
/*
|
||||
7. Remove built-in form typography styles
|
||||
*/
|
||||
input, button, textarea, select {
|
||||
font: inherit;
|
||||
}
|
||||
/*
|
||||
8. Avoid text overflows
|
||||
*/
|
||||
p, h1, h2, h3, h4, h5, h6 {
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
/*
|
||||
9. Create a root stacking context
|
||||
*/
|
||||
#root, #__next {
|
||||
isolation: isolate;
|
||||
}
|
|
@ -1,117 +0,0 @@
|
|||
/*!
|
||||
Theme: Tokyo-night-Dark
|
||||
origin: https://github.com/enkia/tokyo-night-vscode-theme
|
||||
Description: Original highlight.js style
|
||||
Author: (c) Henri Vandersleyen <hvandersleyen@gmail.com>
|
||||
License: see project LICENSE
|
||||
Touched: 2022
|
||||
*/
|
||||
|
||||
/* Comment */
|
||||
.hljs-meta,
|
||||
.hljs-comment {
|
||||
color: #565f89;
|
||||
}
|
||||
|
||||
/* Red */
|
||||
/*INFO: This keyword, HTML elements, Regex group symbol, CSS units, Terminal Red */
|
||||
.hljs-tag,
|
||||
.hljs-doctag,
|
||||
.hljs-selector-id,
|
||||
.hljs-selector-class,
|
||||
.hljs-regexp,
|
||||
.hljs-template-tag,
|
||||
.hljs-selector-pseudo,
|
||||
.hljs-selector-attr,
|
||||
.hljs-variable.language_,
|
||||
.hljs-deletion {
|
||||
color: #f7768e;
|
||||
}
|
||||
|
||||
/*Orange */
|
||||
/*INFO: Number and Boolean constants, Language support constants */
|
||||
.hljs-variable,
|
||||
.hljs-template-variable,
|
||||
.hljs-number,
|
||||
.hljs-literal,
|
||||
.hljs-type,
|
||||
.hljs-params,
|
||||
.hljs-link {
|
||||
color: #ff9e64;
|
||||
}
|
||||
|
||||
|
||||
/* Yellow */
|
||||
/* INFO: Function parameters, Regex character sets, Terminal Yellow */
|
||||
.hljs-built_in,
|
||||
.hljs-attribute {
|
||||
color: #e0af68;
|
||||
}
|
||||
/* cyan */
|
||||
/* INFO: Language support functions, CSS HTML elements */
|
||||
.hljs-selector-tag {
|
||||
color: #2ac3de;
|
||||
}
|
||||
|
||||
/* light blue */
|
||||
/* INFO: Object properties, Regex quantifiers and flags, Markdown headings, Terminal Cyan, Markdown code, Import/export keywords */
|
||||
.hljs-keyword,
|
||||
.hljs-title.function_,
|
||||
.hljs-title,
|
||||
.hljs-title.class_,
|
||||
.hljs-title.class_.inherited__,
|
||||
.hljs-subst,
|
||||
.hljs-property {color: #7dcfff;}
|
||||
|
||||
/*Green*/
|
||||
/* INFO: Object literal keys, Markdown links, Terminal Green */
|
||||
.hljs-selector-tag { color: #73daca;}
|
||||
|
||||
|
||||
/*Green(er) */
|
||||
/* INFO: Strings, CSS class names */
|
||||
.hljs-quote,
|
||||
.hljs-string,
|
||||
.hljs-symbol,
|
||||
.hljs-bullet,
|
||||
.hljs-addition {
|
||||
color: #9ece6a;
|
||||
}
|
||||
|
||||
/* Blue */
|
||||
/* INFO: Function names, CSS property names, Terminal Blue */
|
||||
.hljs-code,
|
||||
.hljs-formula,
|
||||
.hljs-section {
|
||||
color: #7aa2f7;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Magenta */
|
||||
/*INFO: Control Keywords, Storage Types, Regex symbols and operators, HTML Attributes, Terminal Magenta */
|
||||
.hljs-name,
|
||||
.hljs-keyword,
|
||||
.hljs-operator,
|
||||
.hljs-keyword,
|
||||
.hljs-char.escape_,
|
||||
.hljs-attr {
|
||||
color: #bb9af7;
|
||||
}
|
||||
|
||||
/* white*/
|
||||
/* INFO: Variables, Class names, Terminal White */
|
||||
.hljs-punctuation {color: #c0caf5}
|
||||
|
||||
.hljs {
|
||||
background: #1a1b26;
|
||||
color: #9aa5ce;
|
||||
}
|
||||
|
||||
.hljs-emphasis {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hljs-strong {
|
||||
font-weight: bold;
|
||||
}
|
|
@ -1,10 +1,29 @@
|
|||
// 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 darkmode from "./darkmode"
|
||||
import React from "react"
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import KBar from "./kbar"
|
||||
|
||||
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
|
||||
let liveSocket = new LiveSocket("/live", Socket, { params: { _csrf_token: csrfToken } })
|
||||
|
@ -23,10 +42,27 @@ liveSocket.connect()
|
|||
// >> liveSocket.disableLatencySim()
|
||||
window.liveSocket = liveSocket
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
darkmode()
|
||||
const reactRoot = document.querySelector('#react-root')
|
||||
if (reactRoot) {
|
||||
const root = createRoot(reactRoot);
|
||||
root.render(<KBar/>);
|
||||
}
|
||||
|
||||
document
|
||||
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') {
|
||||
|
@ -43,4 +79,4 @@ document.addEventListener("DOMContentLoaded", function() {
|
|||
this.selectionEnd = start + 1;
|
||||
}
|
||||
}))
|
||||
})
|
||||
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
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')
|
||||
}
|
||||
})
|
||||
}
|
267
assets/js/kbar.js
Normal file
267
assets/js/kbar.js
Normal file
|
@ -0,0 +1,267 @@
|
|||
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,7 +4,6 @@ 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) =>
|
||||
|
@ -12,8 +11,22 @@ document.addEventListener('DOMContentLoaded', (event) => {
|
|||
|
||||
document.querySelectorAll('.prose table').forEach(el =>
|
||||
new Tablesort(el))
|
||||
|
||||
darkmode()
|
||||
|
||||
GLightbox({ selector: '.lightbox' })
|
||||
});
|
||||
|
||||
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')
|
||||
}
|
||||
})
|
||||
|
||||
GLightbox({ selector: '.lightbox' })
|
||||
|
||||
window.hljs = hljs
|
397
assets/package-lock.json
generated
397
assets/package-lock.json
generated
|
@ -8,30 +8,46 @@
|
|||
"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"
|
||||
"esbuild": "^0.18.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.0",
|
||||
"react-dom": "^18.0"
|
||||
}
|
||||
},
|
||||
"../deps/phoenix": {
|
||||
"version": "1.7.7",
|
||||
"version": "1.7.6",
|
||||
"license": "MIT"
|
||||
},
|
||||
"../deps/phoenix_html": {
|
||||
"version": "3.3.2"
|
||||
"version": "3.3.1"
|
||||
},
|
||||
"../deps/phoenix_live_view": {
|
||||
"version": "0.19.5",
|
||||
"version": "0.19.3",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.22.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.6.tgz",
|
||||
"integrity": "sha512-wDb5pWm4WDdF6LFUde3Jl8WzPA+3ZbxYqkC6xAXuD3irdEHN1k0NfTRrJD8ZD378SJ61miMLCqIOXYhd8x+AJQ==",
|
||||
"dependencies": {
|
||||
"regenerator-runtime": "^0.13.11"
|
||||
},
|
||||
"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",
|
||||
"integrity": "sha512-tM8yLeYVe7pRyAu9VMi/Q7aunpLwD139EY1S99xbQkT4/q2qa6eA4ige/WJQYdJ8GBL1K33pPFhPfPdJ/WzT8Q==",
|
||||
"version": "0.18.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.17.tgz",
|
||||
"integrity": "sha512-wHsmJG/dnL3OkpAcwbgoBTTMHVi4Uyou3F5mf58ZtmUyIKfcdA7TROav/6tCzET4A3QW2Q2FC+eFneMU+iyOxg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
|
@ -45,9 +61,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.2.tgz",
|
||||
"integrity": "sha512-lsB65vAbe90I/Qe10OjkmrdxSX4UJDjosDgb8sZUKcg3oefEuW2OT2Vozz8ef7wrJbMcmhvCC+hciF8jY/uAkw==",
|
||||
"version": "0.18.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.17.tgz",
|
||||
"integrity": "sha512-9np+YYdNDed5+Jgr1TdWBsozZ85U1Oa3xW0c7TWqH0y2aGghXtZsuT8nYRbzOMcl0bXZXjOGbksoTtVOlWrRZg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
@ -61,9 +77,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.2.tgz",
|
||||
"integrity": "sha512-qK/TpmHt2M/Hg82WXHRc/W/2SGo/l1thtDHZWqFq7oi24AjZ4O/CpPSu6ZuYKFkEgmZlFoa7CooAyYmuvnaG8w==",
|
||||
"version": "0.18.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.17.tgz",
|
||||
"integrity": "sha512-O+FeWB/+xya0aLg23hHEM2E3hbfwZzjqumKMSIqcHbNvDa+dza2D0yLuymRBQQnC34CWrsJUXyH2MG5VnLd6uw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
@ -77,9 +93,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.2.tgz",
|
||||
"integrity": "sha512-Ora8JokrvrzEPEpZO18ZYXkH4asCdc1DLdcVy8TGf5eWtPO1Ie4WroEJzwI52ZGtpODy3+m0a2yEX9l+KUn0tA==",
|
||||
"version": "0.18.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.17.tgz",
|
||||
"integrity": "sha512-M9uJ9VSB1oli2BE/dJs3zVr9kcCBBsE883prage1NWz6pBS++1oNn/7soPNS3+1DGj0FrkSvnED4Bmlu1VAE9g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
@ -93,9 +109,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.2.tgz",
|
||||
"integrity": "sha512-tP+B5UuIbbFMj2hQaUr6EALlHOIOmlLM2FK7jeFBobPy2ERdohI4Ka6ZFjZ1ZYsrHE/hZimGuU90jusRE0pwDw==",
|
||||
"version": "0.18.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.17.tgz",
|
||||
"integrity": "sha512-XDre+J5YeIJDMfp3n0279DFNrGCXlxOuGsWIkRb1NThMZ0BsrWXoTg23Jer7fEXQ9Ye5QjrvXpxnhzl3bHtk0g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
@ -109,9 +125,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.2.tgz",
|
||||
"integrity": "sha512-YbPY2kc0acfzL1VPVK6EnAlig4f+l8xmq36OZkU0jzBVHcOTyQDhnKQaLzZudNJQyymd9OqQezeaBgkTGdTGeQ==",
|
||||
"version": "0.18.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.17.tgz",
|
||||
"integrity": "sha512-cjTzGa3QlNfERa0+ptykyxs5A6FEUQQF0MuilYXYBGdBxD3vxJcKnzDlhDCa1VAJCmAxed6mYhA2KaJIbtiNuQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
@ -125,9 +141,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.2.tgz",
|
||||
"integrity": "sha512-nSO5uZT2clM6hosjWHAsS15hLrwCvIWx+b2e3lZ3MwbYSaXwvfO528OF+dLjas1g3bZonciivI8qKR/Hm7IWGw==",
|
||||
"version": "0.18.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.17.tgz",
|
||||
"integrity": "sha512-sOxEvR8d7V7Kw8QqzxWc7bFfnWnGdaFBut1dRUYtu+EIRXefBc/eIsiUiShnW0hM3FmQ5Zf27suDuHsKgZ5QrA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
@ -141,9 +157,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.2.tgz",
|
||||
"integrity": "sha512-Odalh8hICg7SOD7XCj0YLpYCEc+6mkoq63UnExDCiRA2wXEmGlK5JVrW50vZR9Qz4qkvqnHcpH+OFEggO3PgTg==",
|
||||
"version": "0.18.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.17.tgz",
|
||||
"integrity": "sha512-2d3Lw6wkwgSLC2fIvXKoMNGVaeY8qdN0IC3rfuVxJp89CRfA3e3VqWifGDfuakPmp90+ZirmTfye1n4ncjv2lg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
|
@ -157,9 +173,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.2.tgz",
|
||||
"integrity": "sha512-ig2P7GeG//zWlU0AggA3pV1h5gdix0MA3wgB+NsnBXViwiGgY77fuN9Wr5uoCrs2YzaYfogXgsWZbm+HGr09xg==",
|
||||
"version": "0.18.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.17.tgz",
|
||||
"integrity": "sha512-c9w3tE7qA3CYWjT+M3BMbwMt+0JYOp3vCMKgVBrCl1nwjAlOMYzEo+gG7QaZ9AtqZFj5MbUc885wuBBmu6aADQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
@ -173,9 +189,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.2.tgz",
|
||||
"integrity": "sha512-mLfp0ziRPOLSTek0Gd9T5B8AtzKAkoZE70fneiiyPlSnUKKI4lp+mGEnQXcQEHLJAcIYDPSyBvsUbKUG2ri/XQ==",
|
||||
"version": "0.18.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.17.tgz",
|
||||
"integrity": "sha512-1DS9F966pn5pPnqXYz16dQqWIB0dmDfAQZd6jSSpiT9eX1NzKh07J6VKR3AoXXXEk6CqZMojiVDSZi1SlmKVdg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
|
@ -189,9 +205,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.2.tgz",
|
||||
"integrity": "sha512-hn28+JNDTxxCpnYjdDYVMNTR3SKavyLlCHHkufHV91fkewpIyQchS1d8wSbmXhs1fiYDpNww8KTFlJ1dHsxeSw==",
|
||||
"version": "0.18.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.17.tgz",
|
||||
"integrity": "sha512-EvLsxCk6ZF0fpCB6w6eOI2Fc8KW5N6sHlIovNe8uOFObL2O+Mr0bflPHyHwLT6rwMg9r77WOAWb2FqCQrVnwFg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
|
@ -205,9 +221,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.2.tgz",
|
||||
"integrity": "sha512-KbXaC0Sejt7vD2fEgPoIKb6nxkfYW9OmFUK9XQE4//PvGIxNIfPk1NmlHmMg6f25x57rpmEFrn1OotASYIAaTg==",
|
||||
"version": "0.18.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.17.tgz",
|
||||
"integrity": "sha512-e0bIdHA5p6l+lwqTE36NAW5hHtw2tNRmHlGBygZC14QObsA3bD4C6sXLJjvnDIjSKhW1/0S3eDy+QmX/uZWEYQ==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
|
@ -221,9 +237,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.2.tgz",
|
||||
"integrity": "sha512-dJ0kE8KTqbiHtA3Fc/zn7lCd7pqVr4JcT0JqOnbj4LLzYnp+7h8Qi4yjfq42ZlHfhOCM42rBh0EwHYLL6LEzcw==",
|
||||
"version": "0.18.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.17.tgz",
|
||||
"integrity": "sha512-BAAilJ0M5O2uMxHYGjFKn4nJKF6fNCdP1E0o5t5fvMYYzeIqy2JdAP88Az5LHt9qBoUa4tDaRpfWt21ep5/WqQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
|
@ -237,9 +253,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.2.tgz",
|
||||
"integrity": "sha512-7Z/jKNFufZ/bbu4INqqCN6DDlrmOTmdw6D0gH+6Y7auok2r02Ur661qPuXidPOJ+FSgbEeQnnAGgsVynfLuOEw==",
|
||||
"version": "0.18.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.17.tgz",
|
||||
"integrity": "sha512-Wh/HW2MPnC3b8BqRSIme/9Zhab36PPH+3zam5pqGRH4pE+4xTrVLx2+XdGp6fVS3L2x+DrsIcsbMleex8fbE6g==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
|
@ -253,9 +269,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.2.tgz",
|
||||
"integrity": "sha512-U+RinR6aXXABFCcAY4gSlv4CL1oOVvSSCdseQmGO66H+XyuQGZIUdhG56SZaDJQcLmrSfRmx5XZOWyCJPRqS7g==",
|
||||
"version": "0.18.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.17.tgz",
|
||||
"integrity": "sha512-j/34jAl3ul3PNcK3pfI0NSlBANduT2UO5kZ7FCaK33XFv3chDhICLY8wJJWIhiQ+YNdQ9dxqQctRg2bvrMlYgg==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
|
@ -269,9 +285,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.2.tgz",
|
||||
"integrity": "sha512-oxzHTEv6VPm3XXNaHPyUTTte+3wGv7qVQtqaZCrgstI16gCuhNOtBXLEBkBREP57YTd68P0VgDgG73jSD8bwXQ==",
|
||||
"version": "0.18.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.17.tgz",
|
||||
"integrity": "sha512-QM50vJ/y+8I60qEmFxMoxIx4de03pGo2HwxdBeFd4nMh364X6TIBZ6VQ5UQmPbQWUVWHWws5MmJXlHAXvJEmpQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
@ -285,9 +301,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.2.tgz",
|
||||
"integrity": "sha512-WNa5zZk1XpTTwMDompZmvQLHszDDDN7lYjEHCUmAGB83Bgs20EMs7ICD+oKeT6xt4phV4NDdSi/8OfjPbSbZfQ==",
|
||||
"version": "0.18.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.17.tgz",
|
||||
"integrity": "sha512-/jGlhWR7Sj9JPZHzXyyMZ1RFMkNPjC6QIAan0sDOtIo2TYk3tZn5UDrkE0XgsTQCxWTTOcMPf9p6Rh2hXtl5TQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
@ -301,9 +317,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.2.tgz",
|
||||
"integrity": "sha512-S6kI1aT3S++Dedb7vxIuUOb3oAxqxk2Rh5rOXOTYnzN8JzW1VzBd+IqPiSpgitu45042SYD3HCoEyhLKQcDFDw==",
|
||||
"version": "0.18.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.17.tgz",
|
||||
"integrity": "sha512-rSEeYaGgyGGf4qZM2NonMhMOP/5EHp4u9ehFiBrg7stH6BYEEjlkVREuDEcQ0LfIl53OXLxNbfuIj7mr5m29TA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
@ -317,9 +333,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.2.tgz",
|
||||
"integrity": "sha512-VXSSMsmb+Z8LbsQGcBMiM+fYObDNRm8p7tkUDMPG/g4fhFX5DEFmjxIEa3N8Zr96SjsJ1woAhF0DUnS3MF3ARw==",
|
||||
"version": "0.18.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.17.tgz",
|
||||
"integrity": "sha512-Y7ZBbkLqlSgn4+zot4KUNYst0bFoO68tRgI6mY2FIM+b7ZbyNVtNbDP5y8qlu4/knZZ73fgJDlXID+ohY5zt5g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
@ -333,9 +349,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.2.tgz",
|
||||
"integrity": "sha512-5NayUlSAyb5PQYFAU9x3bHdsqB88RC3aM9lKDAz4X1mo/EchMIT1Q+pSeBXNgkfNmRecLXA0O8xP+x8V+g/LKg==",
|
||||
"version": "0.18.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.17.tgz",
|
||||
"integrity": "sha512-bwPmTJsEQcbZk26oYpc4c/8PvTY3J5/QK8jM19DVlEsAB41M39aWovWoHtNm78sd6ip6prilxeHosPADXtEJFw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
@ -349,9 +365,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.2.tgz",
|
||||
"integrity": "sha512-47gL/ek1v36iN0wL9L4Q2MFdujR0poLZMJwhO2/N3gA89jgHp4MR8DKCmwYtGNksbfJb9JoTtbkoe6sDhg2QTA==",
|
||||
"version": "0.18.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.17.tgz",
|
||||
"integrity": "sha512-H/XaPtPKli2MhW+3CQueo6Ni3Avggi6hP/YvgkEe1aSaxw+AeO8MFjq8DlgfTd9Iz4Yih3QCZI6YLMoyccnPRg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
|
@ -365,9 +381,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.2.tgz",
|
||||
"integrity": "sha512-tcuhV7ncXBqbt/Ybf0IyrMcwVOAPDckMK9rXNHtF17UTK18OKLpg08glminN06pt2WCoALhXdLfSPbVvK/6fxw==",
|
||||
"version": "0.18.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.17.tgz",
|
||||
"integrity": "sha512-fGEb8f2BSA3CW7riJVurug65ACLuQAzKq0SSqkY2b2yHHH0MzDfbLyKIGzHwOI/gkHcxM/leuSW6D5w/LMNitA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
@ -380,15 +396,101 @@
|
|||
"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",
|
||||
"integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw=="
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.2.tgz",
|
||||
"integrity": "sha512-G6hPax8UbFakEj3hWO0Vs52LQ8k3lnBhxZWomUJDxfz3rZTLqF5k/FCzuNdLx2RbpBiQQF9H9onlDDH1lZsnjg==",
|
||||
"version": "0.18.17",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.17.tgz",
|
||||
"integrity": "sha512-1GJtYnUxsJreHYA0Y+iQz2UEykonY66HNWOb0yXYZi9/kNrORUEHVg87eQsCtqh59PEJ5YVZJO98JHznMJSWjg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"bin": {
|
||||
|
@ -398,28 +500,41 @@
|
|||
"node": ">=12"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/android-arm": "0.19.2",
|
||||
"@esbuild/android-arm64": "0.19.2",
|
||||
"@esbuild/android-x64": "0.19.2",
|
||||
"@esbuild/darwin-arm64": "0.19.2",
|
||||
"@esbuild/darwin-x64": "0.19.2",
|
||||
"@esbuild/freebsd-arm64": "0.19.2",
|
||||
"@esbuild/freebsd-x64": "0.19.2",
|
||||
"@esbuild/linux-arm": "0.19.2",
|
||||
"@esbuild/linux-arm64": "0.19.2",
|
||||
"@esbuild/linux-ia32": "0.19.2",
|
||||
"@esbuild/linux-loong64": "0.19.2",
|
||||
"@esbuild/linux-mips64el": "0.19.2",
|
||||
"@esbuild/linux-ppc64": "0.19.2",
|
||||
"@esbuild/linux-riscv64": "0.19.2",
|
||||
"@esbuild/linux-s390x": "0.19.2",
|
||||
"@esbuild/linux-x64": "0.19.2",
|
||||
"@esbuild/netbsd-x64": "0.19.2",
|
||||
"@esbuild/openbsd-x64": "0.19.2",
|
||||
"@esbuild/sunos-x64": "0.19.2",
|
||||
"@esbuild/win32-arm64": "0.19.2",
|
||||
"@esbuild/win32-ia32": "0.19.2",
|
||||
"@esbuild/win32-x64": "0.19.2"
|
||||
"@esbuild/android-arm": "0.18.17",
|
||||
"@esbuild/android-arm64": "0.18.17",
|
||||
"@esbuild/android-x64": "0.18.17",
|
||||
"@esbuild/darwin-arm64": "0.18.17",
|
||||
"@esbuild/darwin-x64": "0.18.17",
|
||||
"@esbuild/freebsd-arm64": "0.18.17",
|
||||
"@esbuild/freebsd-x64": "0.18.17",
|
||||
"@esbuild/linux-arm": "0.18.17",
|
||||
"@esbuild/linux-arm64": "0.18.17",
|
||||
"@esbuild/linux-ia32": "0.18.17",
|
||||
"@esbuild/linux-loong64": "0.18.17",
|
||||
"@esbuild/linux-mips64el": "0.18.17",
|
||||
"@esbuild/linux-ppc64": "0.18.17",
|
||||
"@esbuild/linux-riscv64": "0.18.17",
|
||||
"@esbuild/linux-s390x": "0.18.17",
|
||||
"@esbuild/linux-x64": "0.18.17",
|
||||
"@esbuild/netbsd-x64": "0.18.17",
|
||||
"@esbuild/openbsd-x64": "0.18.17",
|
||||
"@esbuild/sunos-x64": "0.18.17",
|
||||
"@esbuild/win32-arm64": "0.18.17",
|
||||
"@esbuild/win32-ia32": "0.18.17",
|
||||
"@esbuild/win32-x64": "0.18.17"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
|
@ -435,6 +550,54 @@
|
|||
"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==",
|
||||
"peer": true
|
||||
},
|
||||
"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==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"loose-envify": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/phoenix": {
|
||||
"resolved": "../deps/phoenix",
|
||||
"link": true
|
||||
|
@ -447,10 +610,54 @@
|
|||
"resolved": "../deps/phoenix_live_view",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "18.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
|
||||
"integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "18.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
|
||||
"integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"scheduler": "^0.23.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/regenerator-runtime": {
|
||||
"version": "0.13.11",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
|
||||
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.23.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz",
|
||||
"integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"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,19 @@
|
|||
{
|
||||
"devDependencies": {
|
||||
"esbuild": "^0.19.2"
|
||||
"esbuild": "^0.18.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"classnames": "^2.3.2",
|
||||
"glightbox": "^3.2.0",
|
||||
"highlight.js": "^11.8.0",
|
||||
"kbar": "^0.1.0-beta.40",
|
||||
"phoenix": "file:../deps/phoenix",
|
||||
"phoenix_html": "file:../deps/phoenix_html",
|
||||
"phoenix_live_view": "file:../deps/phoenix_live_view",
|
||||
"tablesort": "^5.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.0",
|
||||
"react-dom": "^18.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,47 +14,59 @@ module.exports = {
|
|||
],
|
||||
darkMode: ['class', '[data-mode="dark"]'],
|
||||
theme: {
|
||||
container: { center: true },
|
||||
extend: {
|
||||
colors: {
|
||||
primary: colors.blue,
|
||||
neutral: colors.slate,
|
||||
foreground: 'rgb(var(--color-foreground) / <alpha-value>)',
|
||||
background: 'rgb(var(--color-background) / <alpha-value>)'
|
||||
},
|
||||
typography: ({ theme }) => ({
|
||||
inhji: {
|
||||
typography: {
|
||||
gruvbox: {
|
||||
css: {
|
||||
'--tw-prose-lead': theme('colors.neutral[700]'),
|
||||
'--tw-prose-links': theme('colors.neutral[900]'),
|
||||
'--tw-prose-counters': theme('colors.neutral[600]'),
|
||||
'--tw-prose-bullets': theme('colors.neutral[400]'),
|
||||
'--tw-prose-hr': theme('colors.neutral[300]'),
|
||||
'--tw-prose-quotes': theme('colors.neutral[900]'),
|
||||
'--tw-prose-quote-borders': theme('colors.neutral[300]'),
|
||||
'--tw-prose-captions': theme('colors.neutral[700]'),
|
||||
'--tw-prose-code': theme('colors.neutral[900]'),
|
||||
'--tw-prose-pre-code': theme('colors.neutral[100]'),
|
||||
'--tw-prose-pre-bg': theme('colors.neutral[200]'),
|
||||
'--tw-prose-th-borders': theme('colors.neutral[300]'),
|
||||
'--tw-prose-td-borders': theme('colors.neutral[200]'),
|
||||
'--tw-prose-invert-lead': theme('colors.neutral[300]'),
|
||||
'--tw-prose-invert-links': theme('colors.white'),
|
||||
'--tw-prose-invert-counters': theme('colors.neutral[400]'),
|
||||
'--tw-prose-invert-bullets': theme('colors.neutral[600]'),
|
||||
'--tw-prose-invert-hr': theme('colors.neutral[700]'),
|
||||
'--tw-prose-invert-quotes': theme('colors.neutral[100]'),
|
||||
'--tw-prose-invert-quote-borders': theme('colors.neutral[700]'),
|
||||
'--tw-prose-invert-captions': theme('colors.neutral[400]'),
|
||||
'--tw-prose-invert-code': theme('colors.white'),
|
||||
'--tw-prose-invert-pre-code': theme('colors.neutral[300]'),
|
||||
'--tw-prose-invert-pre-bg': 'rgb(0 0 0 / 50%)',
|
||||
'--tw-prose-invert-th-borders': theme('colors.neutral[600]'),
|
||||
'--tw-prose-invert-td-borders': theme('colors.neutral[700]'),
|
||||
},
|
||||
},
|
||||
}),
|
||||
'--tw-prose-td-borders': 'rgb(var(--color-background1))',
|
||||
'--tw-prose-th-borders': 'rgb(var(--color-background1))',
|
||||
'--tw-prose-body': 'rgb(var(--color-foreground))',
|
||||
'--tw-prose-links': 'rgb(var(--color-foreground))',
|
||||
'--tw-prose-headings': 'rgb(var(--color-foreground))',
|
||||
'--tw-prose-bold': 'rgb(var(--color-yellow1))',
|
||||
'--tw-prose-quotes': 'rgb(var(--color-foreground))',
|
||||
'--tw-prose-bullets': 'rgb(var(--color-primary))',
|
||||
'--tw-prose-code': 'rgb(var(--color-yellow1))',
|
||||
'--tw-prose-pre-bg': 'rgb(var(--color-background1))',
|
||||
'--tw-prose-quote-borders': 'rgb(var(--color-primary))',
|
||||
'--tw-prose-counters': 'rgb(var(--color-foreground))',
|
||||
'--tw-prose-invert-td-borders': 'rgb(var(--color-background1))',
|
||||
'--tw-prose-invert-th-borders': 'rgb(var(--color-background1))',
|
||||
'--tw-prose-invert-body': 'rgb(var(--color-foreground))',
|
||||
'--tw-prose-invert-links': 'rgb(var(--color-foreground))',
|
||||
'--tw-prose-invert-headings': 'rgb(var(--color-foreground))',
|
||||
'--tw-prose-invert-bold': 'rgb(var(--color-yellow))',
|
||||
'--tw-prose-invert-quotes': 'rgb(var(--color-foreground))',
|
||||
'--tw-prose-invert-bullets': 'rgb(var(--color-primary))',
|
||||
'--tw-prose-invert-quote-borders': 'rgb(var(--color-primary))',
|
||||
'--tw-prose-invert-code': 'rgb(var(--color-yellow))',
|
||||
'--tw-prose-invert-pre-bg': 'rgb(var(--color-background1))',
|
||||
'--tw-prose-invert-counters': 'rgb(var(--color-foreground))',
|
||||
}
|
||||
}
|
||||
},
|
||||
colors: {
|
||||
code: colors.emerald,
|
||||
gray: colors.zinc,
|
||||
theme: {
|
||||
background: 'rgb(var(--color-background) / <alpha-value>)',
|
||||
background1: 'rgb(var(--color-background1) / <alpha-value>)',
|
||||
base: 'rgb(var(--color-foreground) / <alpha-value>)',
|
||||
base1: 'rgb(var(--color-foreground1) / <alpha-value>)',
|
||||
primary: 'rgb(var(--color-primary) / <alpha-value>)',
|
||||
primary1: 'rgb(var(--color-primary1) / <alpha-value>)',
|
||||
secondary: 'rgb(var(--color-secondary) / <alpha-value>)',
|
||||
secondary1: 'rgb(var(--color-secondary1) / <alpha-value>)',
|
||||
tertiary: 'rgb(var(--color-blue) / <alpha-value>)',
|
||||
tertiary1: 'rgb(var(--color-blue1) / <alpha-value>)',
|
||||
quaternary: 'rgb(var(--color-purple) / <alpha-value>)',
|
||||
quaternary1: 'rgb(var(--color-purple1) / <alpha-value>)',
|
||||
quinary: 'rgb(var(--color-yellow) / <alpha-value>)',
|
||||
quinary1: 'rgb(var(--color-yellow1) / <alpha-value>)',
|
||||
heading: 'rgb(var(--color-heading) / <alpha-value>)'
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
require("@tailwindcss/forms"),
|
||||
|
|
|
@ -14,8 +14,9 @@ config :chiya, ChiyaWeb.Endpoint, cache_static_manifest: "priv/static/cache_mani
|
|||
# Configures Swoosh API Client
|
||||
config :swoosh, api_client: Swoosh.ApiClient.Finch, finch_name: Chiya.Finch
|
||||
|
||||
# Do print debug messages in production
|
||||
config :logger, :default_handler, level: :debug
|
||||
# Do not print debug messages in production
|
||||
# config :logger, level: :debug
|
||||
config :logger, :default_handler, level: :info
|
||||
|
||||
config :cors_plug,
|
||||
origin: ["app://obsidian.md"],
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
defmodule Chiya.Flop do
|
||||
use Flop, repo: Chiya.Repo, default_limit: 10
|
||||
end
|
|
@ -12,7 +12,7 @@ defmodule Chiya.Notes do
|
|||
:images,
|
||||
:links_from,
|
||||
:links_to,
|
||||
tags: [:notes],
|
||||
:tags,
|
||||
comments:
|
||||
from(c in NoteComment,
|
||||
order_by: [
|
||||
|
@ -39,34 +39,6 @@ defmodule Chiya.Notes do
|
|||
|> Repo.preload(@preloads)
|
||||
end
|
||||
|
||||
def list_admin_notes(params) do
|
||||
q =
|
||||
Note
|
||||
|> join(:inner, [n], c in assoc(n, :channels), as: :channels)
|
||||
|> order_by([n, c], 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)
|
||||
|> where([n], not is_nil(n.published_at))
|
||||
|> order_by([n], desc: n.updated_at, desc: n.published_at)
|
||||
|> preload(^@preloads)
|
||||
|
||||
Chiya.Flop.validate_and_run(q, params, for: Chiya.Notes.Note)
|
||||
end
|
||||
|
||||
def list_apply_notes(%Chiya.Tags.Tag{} = tag) do
|
||||
Note
|
||||
|> where([n], fragment("? ~ ?", n.name, ^tag.regex))
|
||||
|> or_where([n], fragment("? ~ ?", n.url, ^tag.regex))
|
||||
|> or_where([n], fragment("? ~ ?", n.content, ^tag.regex))
|
||||
|> preload(^@preloads)
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
def list_notes_by_channel(%Chiya.Channels.Channel{} = channel) do
|
||||
list_notes_by_channel_query(channel)
|
||||
|> order_by([n], desc: n.updated_at, desc: n.published_at)
|
||||
|
|
|
@ -11,22 +11,6 @@ defmodule Chiya.Notes.Note do
|
|||
@reserved_slugs ~w(user admin dev api)
|
||||
@note_url_regex ~r/\/note\/([a-z0-9-]+)/
|
||||
|
||||
@derive {
|
||||
Flop.Schema,
|
||||
filterable: [:name, :channels],
|
||||
sortable: [:name],
|
||||
default_limit: 10,
|
||||
max_limit: 100,
|
||||
adapter_opts: [
|
||||
join_fields: [
|
||||
channels: [
|
||||
binding: :channels,
|
||||
field: :name,
|
||||
ecto_type: :string
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
||||
@derive {Jason.Encoder, only: [:id, :name, :content, :slug, :channels, :tags]}
|
||||
schema "notes" do
|
||||
field :content, :string
|
||||
|
@ -72,7 +56,7 @@ defmodule Chiya.Notes.Note do
|
|||
end
|
||||
|
||||
def note_path_admin(note) do
|
||||
~p"/admin/notes/#{note.id}"
|
||||
~p"/admin/notes/#{note.slug}"
|
||||
end
|
||||
|
||||
def note_url(note) do
|
||||
|
@ -114,16 +98,6 @@ defmodule Chiya.Notes.Note do
|
|||
end
|
||||
end
|
||||
|
||||
def note_excerpt(note_content) do
|
||||
if String.contains?(note_content, "<!-- more -->") do
|
||||
note_content
|
||||
|> String.split("<!-- more -->")
|
||||
|> List.first()
|
||||
else
|
||||
String.slice(note_content, 0..150) <> ".."
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(note, attrs) do
|
||||
# if you need to have a preloaded note here,
|
||||
|
|
|
@ -15,6 +15,5 @@ defmodule Chiya.Notes.NoteTag do
|
|||
note_tag
|
||||
|> cast(attrs, [:note_id, :tag_id])
|
||||
|> validate_required([:note_id, :tag_id])
|
||||
|> unique_constraint([:note_id, :tag_id])
|
||||
end
|
||||
end
|
||||
|
|
|
@ -8,7 +8,7 @@ defmodule Chiya.Tags do
|
|||
|
||||
alias Chiya.Tags.Tag
|
||||
|
||||
@preloads [notes: [tags: [:notes]]]
|
||||
@preloads [notes: [:tags]]
|
||||
defp with_preloads(query), do: preload(query, ^@preloads)
|
||||
|
||||
@doc """
|
||||
|
@ -38,17 +38,6 @@ defmodule Chiya.Tags do
|
|||
|> Repo.all()
|
||||
end
|
||||
|
||||
def list_admin_tags() do
|
||||
q =
|
||||
Tag
|
||||
|> order_by(:name)
|
||||
|> with_preloads()
|
||||
|
||||
Repo.all(q)
|
||||
end
|
||||
|
||||
def get_tag!(id), do: Repo.get!(Tag, id) |> preload_tag()
|
||||
|
||||
@doc """
|
||||
Gets a single tag.
|
||||
|
||||
|
|
|
@ -6,10 +6,6 @@ defmodule Chiya.Tags.Tag do
|
|||
import Ecto.Changeset
|
||||
alias Chiya.Tags.TagSlug
|
||||
|
||||
@derive {
|
||||
Flop.Schema,
|
||||
filterable: [], sortable: [], default_limit: 100
|
||||
}
|
||||
@derive {Jason.Encoder, only: [:name]}
|
||||
schema "tags" do
|
||||
field :name, :string
|
||||
|
|
|
@ -8,13 +8,6 @@ defmodule Chiya.Tags.TagUpdater do
|
|||
alias Chiya.{Notes, Tags}
|
||||
alias Chiya.Notes.Note
|
||||
|
||||
@doc """
|
||||
Updates a tag for the given note.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> update_tags({:ok, note}, "foo,bar")
|
||||
"""
|
||||
def update_tags({:ok, %Note{} = note}, attrs) do
|
||||
note
|
||||
|> Notes.preload_note()
|
||||
|
@ -27,19 +20,27 @@ defmodule Chiya.Tags.TagUpdater do
|
|||
{:error, changeset}
|
||||
end
|
||||
|
||||
def update_tags(%Note{} = note, %{tags_string: new_tags} = attrs) when is_map(attrs) do
|
||||
@doc """
|
||||
Updates the tags for the given note
|
||||
|
||||
## Examples
|
||||
|
||||
iex> update_tags(note, "foo,bar")
|
||||
|
||||
"""
|
||||
def update_tags(note, %{tags_string: new_tags} = attrs) when is_map(attrs) do
|
||||
update_tags(note, new_tags)
|
||||
end
|
||||
|
||||
def update_tags(%Note{} = note, %{"tags_string" => new_tags} = attrs) when is_map(attrs) do
|
||||
def update_tags(note, %{"tags_string" => new_tags} = attrs) when is_map(attrs) do
|
||||
update_tags(note, new_tags)
|
||||
end
|
||||
|
||||
def update_tags(%Note{} = note, attrs) when is_map(attrs) do
|
||||
def update_tags(note, attrs) when is_map(attrs) do
|
||||
note
|
||||
end
|
||||
|
||||
def update_tags(%Note{} = note, new_tags) when is_binary(new_tags) do
|
||||
def update_tags(note, new_tags) when is_binary(new_tags) do
|
||||
update_tags(note, split_tags(new_tags))
|
||||
end
|
||||
|
||||
|
@ -50,7 +51,7 @@ defmodule Chiya.Tags.TagUpdater do
|
|||
Enum.map(new_tags, fn tag ->
|
||||
tag
|
||||
|> String.downcase()
|
||||
|> Slugger.slugify(45)
|
||||
|> Slugger.slugify()
|
||||
end)
|
||||
|
||||
Logger.info("Adding tags #{inspect(new_tags -- old_tags)}")
|
||||
|
@ -61,14 +62,6 @@ defmodule Chiya.Tags.TagUpdater do
|
|||
|> remove_tags(old_tags -- new_tags)
|
||||
end
|
||||
|
||||
def add_tags(note, tags) do
|
||||
tags
|
||||
|> Enum.uniq()
|
||||
|> Enum.each(&add_tag(note, &1))
|
||||
|
||||
note
|
||||
end
|
||||
|
||||
defp split_tags(tags_string) when is_binary(tags_string) do
|
||||
tags_string
|
||||
|> String.split(",")
|
||||
|
@ -76,6 +69,14 @@ defmodule Chiya.Tags.TagUpdater do
|
|||
|> Enum.filter(&(String.length(&1) > 0))
|
||||
end
|
||||
|
||||
defp add_tags(note, tags) do
|
||||
tags
|
||||
|> Enum.uniq()
|
||||
|> Enum.each(&add_tag(note, &1))
|
||||
|
||||
note
|
||||
end
|
||||
|
||||
defp add_tag(%{id: note_id} = note, tag) when is_binary(tag) do
|
||||
slug = Slugger.slugify_downcase(tag)
|
||||
|
||||
|
@ -99,14 +100,7 @@ defmodule Chiya.Tags.TagUpdater do
|
|||
tag_id: tag.id
|
||||
}
|
||||
|
||||
case Notes.create_note_tag(attrs) do
|
||||
{:ok, _note_tag} ->
|
||||
true
|
||||
|
||||
{:error, changeset} ->
|
||||
Logger.warning(inspect(changeset))
|
||||
false
|
||||
end
|
||||
{:ok, _note_tag} = Notes.create_note_tag(attrs)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -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"""
|
||||
|
@ -586,9 +586,7 @@ defmodule ChiyaWeb.CoreComponents do
|
|||
<dt class="w-1/4 flex-none text-[0.8125rem] leading-6 text-gray-500 dark:text-gray-300">
|
||||
<%= item.title %>
|
||||
</dt>
|
||||
<dd class="text-sm leading-6 text-gray-700 dark:text-gray-400 overflow-auto">
|
||||
<%= render_slot(item) %>
|
||||
</dd>
|
||||
<dd class="text-sm leading-6 text-gray-700 dark:text-gray-400"><%= render_slot(item) %></dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
|
@ -740,31 +738,6 @@ 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}
|
||||
class="stack"
|
||||
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>
|
||||
</.form>
|
||||
"""
|
||||
end
|
||||
|
||||
## JS Commands
|
||||
|
||||
def show(js \\ %JS{}, selector) do
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
defmodule ChiyaWeb.Layouts do
|
||||
use ChiyaWeb, :html
|
||||
import PhoenixActiveLink
|
||||
|
||||
import ChiyaWeb.PublicComponents, only: [divider: 1, site_header: 1]
|
||||
|
||||
embed_templates "layouts/*"
|
||||
end
|
||||
|
|
|
@ -11,11 +11,6 @@
|
|||
icon: "hero-speaker-wave-solid",
|
||||
name: "Channels"
|
||||
},
|
||||
%{
|
||||
path: ~p"/admin/tags",
|
||||
icon: "hero-document-text-solid",
|
||||
name: "Tags"
|
||||
},
|
||||
%{
|
||||
path: ~p"/admin/identities",
|
||||
icon: "hero-user-solid",
|
||||
|
@ -45,7 +40,9 @@
|
|||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main class="container py-6">
|
||||
<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>
|
||||
|
|
|
@ -1,12 +1,2 @@
|
|||
<.flash_group flash={@flash} />
|
||||
|
||||
<%= if @page_header do %>
|
||||
<header class="page-header">
|
||||
<h1><%= @page_title %></h1>
|
||||
<p><%= Markdown.render(assigns[:content] || "") |> raw %></p>
|
||||
</header>
|
||||
<% end %>
|
||||
|
||||
<section class="page-grid">
|
||||
<%= @inner_content %>
|
||||
</section>
|
||||
<%= @inner_content %>
|
||||
|
|
|
@ -25,7 +25,6 @@
|
|||
</script>
|
||||
<script>
|
||||
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||
console.log("Dark Mode")
|
||||
document.documentElement.dataset["mode"] = "dark"
|
||||
} else {
|
||||
delete document.documentElement.dataset["mode"]
|
||||
|
@ -35,78 +34,56 @@
|
|||
<%= @settings.custom_css %>
|
||||
</style>
|
||||
</head>
|
||||
<body id="site-body" class="h-feed hfeed">
|
||||
<header id="site-header">
|
||||
<nav class="container">
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/" id="site-title">Inhji.de</a>
|
||||
</li>
|
||||
<li class="flex-1"></li>
|
||||
<%= if @current_user do %>
|
||||
<li>
|
||||
<a href="/admin">
|
||||
Admin
|
||||
</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>
|
||||
<body class="text-theme-base bg-theme-background | h-feed hfeed">
|
||||
<aside class="block print:hidden">
|
||||
<%= raw(@settings.custom_html) %>
|
||||
</aside>
|
||||
|
||||
<header class="my-8 block px-3 print:hidden">
|
||||
<.site_header user={@current_user} />
|
||||
</header>
|
||||
|
||||
<main id="site-content" class="container print:hidden">
|
||||
<aside id="primary-sidebar">
|
||||
<nav class="prose max-w-none">
|
||||
<div class="menu">
|
||||
<h3>Pages</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<%= active_link(@conn, "About", to: "/about") %>
|
||||
</li>
|
||||
<li>
|
||||
<%= active_link(@conn, "Wiki", to: "/wiki") %>
|
||||
</li>
|
||||
<li>
|
||||
<%= active_link(@conn, "Bookmarks", to: "/bookmarks") %>
|
||||
</li>
|
||||
<main class="mx-3 md:mx-0">
|
||||
<%= @inner_content %>
|
||||
</main>
|
||||
|
||||
<footer class="max-w-full mt-8 p-8 text-theme-base/75 bg-theme-background1 print:hidden">
|
||||
<section class="max-w-2xl mx-auto flex gap-3">
|
||||
<div class="flex-1">
|
||||
<h2 class="font-bold tracking-wider">Info</h2>
|
||||
|
||||
<ul class="list-disc list-inside">
|
||||
<li>Served by Chiya v<%= Application.spec(:chiya, :vsn) %></li>
|
||||
<li>Made by Inhji</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="menu">
|
||||
<h3>Channels</h3>
|
||||
<ul>
|
||||
<%= for channel <- @channels do %>
|
||||
<li>
|
||||
<%= active_link(@conn, channel.name, to: ~p"/channel/#{channel.slug}") %>
|
||||
</li>
|
||||
<% end %>
|
||||
<div class="flex-1">
|
||||
<h2 class="font-bold tracking-wider">Links</h2>
|
||||
|
||||
<ul class="list-disc list-inside">
|
||||
<li><a href={~p"/wiki"}>Wiki</a></li>
|
||||
<li><a href={~p"/about"}>About</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="menu">
|
||||
<h3>Elsewhere</h3>
|
||||
<ul>
|
||||
<div class="flex-1">
|
||||
<h2 class="font-bold tracking-wider">Elsewhere</h2>
|
||||
|
||||
<ul class="list-disc list-inside">
|
||||
<%= for identity <- @public_identities do %>
|
||||
<li><a href={identity.url}><%= identity.name %></a></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<section id="content-wrapper">
|
||||
<%= @inner_content %>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer id="site-footer" class="container">
|
||||
<p class="text-center">
|
||||
<section class="max-w-2xl mx-auto">
|
||||
<.divider />
|
||||
<div data-dummy="true" />
|
||||
</section>
|
||||
|
||||
<p class="mt-4 max-w-2xl mx-auto text-center">
|
||||
Struggling to make a decent website since 2011
|
||||
</p>
|
||||
</footer>
|
||||
|
@ -122,5 +99,13 @@
|
|||
<span class="p-note"><%= @profile.bio %></span>
|
||||
</section>
|
||||
<% end %>
|
||||
|
||||
<section class="flex h-1 w-full flex-row">
|
||||
<div class="bg-theme-primary w-full"></div>
|
||||
<div class="bg-theme-secondary w-full"></div>
|
||||
<div class="bg-theme-tertiary w-full"></div>
|
||||
<div class="bg-theme-quaternary w-full"></div>
|
||||
<div class="bg-theme-quinary w-full"></div>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -6,6 +6,12 @@ defmodule ChiyaWeb.PublicComponents do
|
|||
router: ChiyaWeb.Router,
|
||||
statics: ChiyaWeb.static_paths()
|
||||
|
||||
import ChiyaWeb.Format
|
||||
import ChiyaWeb.Markdown, only: [render: 1]
|
||||
import Phoenix.HTML, only: [raw: 1]
|
||||
|
||||
import ChiyaWeb.DarkModeToggle
|
||||
|
||||
@doc """
|
||||
Renders a [Hero Icon](https://heroicons.com).
|
||||
|
||||
|
@ -35,6 +41,16 @@ defmodule ChiyaWeb.PublicComponents do
|
|||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders a middot as divider
|
||||
"""
|
||||
attr :class, :string, default: "text-theme-primary"
|
||||
|
||||
def dot(assigns),
|
||||
do: ~H"""
|
||||
<span class={["font-bold", @class]}>·</span>
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Renders a horizontal line
|
||||
"""
|
||||
|
@ -43,6 +59,39 @@ defmodule ChiyaWeb.PublicComponents do
|
|||
<hr class="my-6 border-theme-base/20" />
|
||||
"""
|
||||
|
||||
attr :text, :string, default: "⌘"
|
||||
|
||||
def divider(assigns) do
|
||||
~H"""
|
||||
<div class="flex items-center my-8 text-theme-primary before:flex-1 after:flex-1 before:content-[''] after:content-[''] before:p-[0.5px] after:p-[0.5px] before:bg-theme-base/25 after:bg-theme-base/25 w-full mx-auto last:hidden">
|
||||
<%= assigns.text %>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
attr :note, :map, required: true
|
||||
attr :class_tag, :string, default: ""
|
||||
attr :linked, :boolean, default: true
|
||||
|
||||
def tags(assigns) do
|
||||
~H"""
|
||||
<span class="inline-flex flex-row gap-1">
|
||||
<%= for tag <- @note.tags do %>
|
||||
<%= if assigns.linked do %>
|
||||
<a class={["p-category", @class_tag]} href={~p"/tagged-with/#{tag.slug}"}>
|
||||
<%= tag.name %>
|
||||
</a>
|
||||
<% else %>
|
||||
<span class={["p-category", @class_tag]}>
|
||||
<%= tag.name %>
|
||||
</span>
|
||||
<% end %>
|
||||
<.dot class="text-theme-base/50 last:hidden" />
|
||||
<% end %>
|
||||
</span>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders a note-header with title.
|
||||
"""
|
||||
|
@ -54,17 +103,224 @@ defmodule ChiyaWeb.PublicComponents do
|
|||
|
||||
def header(assigns) do
|
||||
~H"""
|
||||
<header class={[@class]}>
|
||||
<h1 class={["text-3xl leading-10 font-bold text-theme-base1", @class_title]}>
|
||||
<header class={["p-8 rounded bg-theme-background1", @class]}>
|
||||
<h1 class={["text-3xl leading-10 text-theme-base", @class_title]}>
|
||||
<%= render_slot(@inner_block) %>
|
||||
</h1>
|
||||
<p :if={@subtitle != []} class={["mt-4 leading-7 text-theme-base", @class_subtitle]}>
|
||||
<p
|
||||
:if={@subtitle != []}
|
||||
class={["mt-4 leading-7 font-semibold text-theme-base/75", @class_subtitle]}
|
||||
>
|
||||
<%= render_slot(@subtitle) %>
|
||||
</p>
|
||||
</header>
|
||||
"""
|
||||
end
|
||||
|
||||
attr :layout, :atom, default: :list
|
||||
attr :notes, :list, required: true
|
||||
attr :show_content, :boolean, default: true
|
||||
|
||||
def note_list(assigns) do
|
||||
case assigns.layout do
|
||||
:gallery ->
|
||||
note_list_gallery(assigns)
|
||||
|
||||
:microblog ->
|
||||
note_list_microblog(assigns)
|
||||
|
||||
_ ->
|
||||
note_list_default(assigns)
|
||||
end
|
||||
end
|
||||
|
||||
attr :notes, :list, required: true
|
||||
|
||||
@doc """
|
||||
Default note list that renders a list of rounded boxes,
|
||||
which show the note title and an excerpt of the content
|
||||
"""
|
||||
def note_list_default(assigns) do
|
||||
~H"""
|
||||
<section class="note-list default | sm:w-auto flex flex-col gap-3">
|
||||
<%= for note <- assigns.notes do %>
|
||||
<a
|
||||
href={~p"/note/#{note.slug}"}
|
||||
class="block rounded-lg px-6 py-4 border border-theme-background1 hover:bg-theme-background1 transition"
|
||||
>
|
||||
<header class="flex flex-row items-center gap-1">
|
||||
<span class="text-theme-primary text-lg font-semibold leading-8 flex-1">
|
||||
<%= note.name %>
|
||||
</span>
|
||||
<span class="text-theme-base/75 text-sm">
|
||||
<%= pretty_date(note.published_at) %>
|
||||
</span>
|
||||
</header>
|
||||
|
||||
<%= if assigns.show_content do %>
|
||||
<p class="text-theme-base">
|
||||
<%= String.slice(note.content, 0..150) %>
|
||||
</p>
|
||||
<% end %>
|
||||
|
||||
<%= if not Enum.empty?(note.tags) do %>
|
||||
<span class="inline-block">
|
||||
<.tags note={note} linked={false} />
|
||||
</span>
|
||||
<% end %>
|
||||
</a>
|
||||
<% end %>
|
||||
</section>
|
||||
"""
|
||||
end
|
||||
|
||||
attr :notes, :list, required: true
|
||||
|
||||
def note_list_microblog(assigns) do
|
||||
~H"""
|
||||
<section class="note-list microblog | mt-6 text-theme-base">
|
||||
<%= for note <- assigns.notes do %>
|
||||
<article class="mt-4 first:mt-0">
|
||||
<.featured_images note={note} />
|
||||
|
||||
<header class="mt-4 text-lg">
|
||||
<%= if(note.kind == :bookmark) do %>
|
||||
<strong><%= note.name %></strong>
|
||||
<% end %>
|
||||
</header>
|
||||
|
||||
<div class="prose prose-gruvbox mt-4">
|
||||
<%= raw(render(note.content)) %>
|
||||
</div>
|
||||
|
||||
<footer class="mt-4">
|
||||
<a href={~p"/note/#{note.slug}"}>
|
||||
<time class="text-theme-base/75">
|
||||
<%= pretty_datetime(note.published_at) %>
|
||||
</time>
|
||||
</a>
|
||||
<%= if not Enum.empty?(note.tags) do %>
|
||||
<.dot />
|
||||
<.tags note={note} class_tag="text-theme-base/75" />
|
||||
<% end %>
|
||||
</footer>
|
||||
</article>
|
||||
|
||||
<.divider />
|
||||
<% end %>
|
||||
</section>
|
||||
"""
|
||||
end
|
||||
|
||||
attr :notes, :list, required: true
|
||||
|
||||
def note_list_gallery(assigns) do
|
||||
~H"""
|
||||
<section class="note-list gallery | mt-6">
|
||||
<%= for note <- assigns.notes do %>
|
||||
<article>
|
||||
<section class="flex flex-wrap justify-start gap-3">
|
||||
<%= for image <- note.images do %>
|
||||
<a
|
||||
href={ChiyaWeb.Helpers.image_url(image, :full)}
|
||||
class="lightbox | w-28"
|
||||
data-gallery={gallery_name(note)}
|
||||
data-description={ChiyaWeb.Markdown.render(image.content)}
|
||||
>
|
||||
<img src={ChiyaWeb.Helpers.image_url(image, :thumb)} loading="lazy" />
|
||||
</a>
|
||||
<% end %>
|
||||
</section>
|
||||
<a
|
||||
href={~p"/note/#{note.slug}"}
|
||||
class="text-theme-secondary text-lg/10 font-semibold rounded-lg -mx-2 -my-0.5 px-2 py-0.5 hover:bg-theme-secondary/10 transition"
|
||||
>
|
||||
<%= note.name %>
|
||||
<span class="text-theme-base/75 text-sm">
|
||||
<%= pretty_date(note.published_at) %>
|
||||
</span>
|
||||
</a>
|
||||
</article>
|
||||
|
||||
<.line />
|
||||
<% end %>
|
||||
</section>
|
||||
"""
|
||||
end
|
||||
|
||||
attr :user, :map, required: true
|
||||
|
||||
def site_header(assigns) do
|
||||
~H"""
|
||||
<nav class="mx-auto max-w-2xl">
|
||||
<ul class="flex gap-3">
|
||||
<li>
|
||||
<a href="/" class="button">
|
||||
<.icon name="hero-home" />
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/about" class="button">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="currentColor"
|
||||
class="bi bi-umbrella inline"
|
||||
viewBox="0 0 16 16"
|
||||
>
|
||||
<path d="M8 0a.5.5 0 0 1 .5.5v.514C12.625 1.238 16 4.22 16 8c0 0 0 .5-.5.5-.149 0-.352-.145-.352-.145l-.004-.004-.025-.023a3.484 3.484 0 0 0-.555-.394A3.166 3.166 0 0 0 13 7.5c-.638 0-1.178.213-1.564.434a3.484 3.484 0 0 0-.555.394l-.025.023-.003.003s-.204.146-.353.146-.352-.145-.352-.145l-.004-.004-.025-.023a3.484 3.484 0 0 0-.555-.394 3.3 3.3 0 0 0-1.064-.39V13.5H8h.5v.039l-.005.083a2.958 2.958 0 0 1-.298 1.102 2.257 2.257 0 0 1-.763.88C7.06 15.851 6.587 16 6 16s-1.061-.148-1.434-.396a2.255 2.255 0 0 1-.763-.88 2.958 2.958 0 0 1-.302-1.185v-.025l-.001-.009v-.003s0-.002.5-.002h-.5V13a.5.5 0 0 1 1 0v.506l.003.044a1.958 1.958 0 0 0 .195.726c.095.191.23.367.423.495.19.127.466.229.879.229s.689-.102.879-.229c.193-.128.328-.304.424-.495a1.958 1.958 0 0 0 .197-.77V7.544a3.3 3.3 0 0 0-1.064.39 3.482 3.482 0 0 0-.58.417l-.004.004S5.65 8.5 5.5 8.5c-.149 0-.352-.145-.352-.145l-.004-.004a3.482 3.482 0 0 0-.58-.417A3.166 3.166 0 0 0 3 7.5c-.638 0-1.177.213-1.564.434a3.482 3.482 0 0 0-.58.417l-.004.004S.65 8.5.5 8.5C0 8.5 0 8 0 8c0-3.78 3.375-6.762 7.5-6.986V.5A.5.5 0 0 1 8 0zM6.577 2.123c-2.833.5-4.99 2.458-5.474 4.854A4.124 4.124 0 0 1 3 6.5c.806 0 1.48.25 1.962.511a9.706 9.706 0 0 1 .344-2.358c.242-.868.64-1.765 1.271-2.53zm-.615 4.93A4.16 4.16 0 0 1 8 6.5a4.16 4.16 0 0 1 2.038.553 8.688 8.688 0 0 0-.307-2.13C9.434 3.858 8.898 2.83 8 2.117c-.898.712-1.434 1.74-1.731 2.804a8.687 8.687 0 0 0-.307 2.131zm3.46-4.93c.631.765 1.03 1.662 1.272 2.53.233.833.328 1.66.344 2.358A4.14 4.14 0 0 1 13 6.5c.77 0 1.42.23 1.897.477-.484-2.396-2.641-4.355-5.474-4.854z" />
|
||||
</svg>
|
||||
<span class="hidden sm:inline-block">About</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/wiki" class="button">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="currentColor"
|
||||
class="bi bi-journals inline"
|
||||
viewBox="0 0 16 16"
|
||||
>
|
||||
<path d="M5 0h8a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2 2 2 0 0 1-2 2H3a2 2 0 0 1-2-2h1a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1H1a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v9a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1H5a1 1 0 0 0-1 1H3a2 2 0 0 1 2-2z" />
|
||||
<path d="M1 6v-.5a.5.5 0 0 1 1 0V6h.5a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1H1zm0 3v-.5a.5.5 0 0 1 1 0V9h.5a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1H1zm0 2.5v.5H.5a.5.5 0 0 0 0 1h2a.5.5 0 0 0 0-1H2v-.5a.5.5 0 0 0-1 0z" />
|
||||
</svg>
|
||||
<span class="hidden sm:inline">Wiki</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/bookmarks" class="button">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="currentColor"
|
||||
class="bi bi-bookmark-fill inline"
|
||||
viewBox="0 0 16 16"
|
||||
>
|
||||
<path d="M2 2v13.5a.5.5 0 0 0 .74.439L8 13.069l5.26 2.87A.5.5 0 0 0 14 15.5V2a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2z" />
|
||||
</svg>
|
||||
<span class="hidden sm:inline-block">Bookmarks</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="flex-1"></li>
|
||||
<%= if @user do %>
|
||||
<li>
|
||||
<a href="/admin" class="button">
|
||||
<.icon name="hero-beaker" />
|
||||
</a>
|
||||
</li>
|
||||
<% end %>
|
||||
<li>
|
||||
<.darkmode_toggle class="button" />
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
"""
|
||||
end
|
||||
|
||||
attr :note, :map, required: true
|
||||
|
||||
def featured_images(assigns) do
|
||||
|
@ -80,7 +336,7 @@ defmodule ChiyaWeb.PublicComponents do
|
|||
assigns = assign(assigns, :image, List.first(images))
|
||||
|
||||
~H"""
|
||||
<figure class="images-1">
|
||||
<figure>
|
||||
<.featured_image image={assigns.image} size={:full} class="rounded-lg" />
|
||||
</figure>
|
||||
"""
|
||||
|
@ -92,9 +348,9 @@ defmodule ChiyaWeb.PublicComponents do
|
|||
|> assign(:second, Enum.at(images, 1))
|
||||
|
||||
~H"""
|
||||
<figure class="images-2 | flex gap-1">
|
||||
<.featured_image image={assigns.first} size={:full} class="rounded-l flex-1 w-full" />
|
||||
<.featured_image image={assigns.second} size={:full} class="rounded-r flex-1 w-full" />
|
||||
<figure class="flex gap-1">
|
||||
<.featured_image image={assigns.first} size={:thumb} class="rounded-l flex-1 w-full" />
|
||||
<.featured_image image={assigns.second} size={:thumb} class="rounded-r flex-1 w-full" />
|
||||
</figure>
|
||||
"""
|
||||
|
||||
|
@ -106,7 +362,7 @@ defmodule ChiyaWeb.PublicComponents do
|
|||
|> assign(:third, Enum.at(images, 2))
|
||||
|
||||
~H"""
|
||||
<figure class="images-3 | flex gap-1">
|
||||
<figure class="flex gap-1">
|
||||
<.featured_image image={assigns.first} size={:thumb} class="flex-1 w-full rounded-l" />
|
||||
<.featured_image image={assigns.second} size={:thumb} class="flex-1 w-full" />
|
||||
<.featured_image image={assigns.third} size={:thumb} class="flex-1 w-full rounded-r" />
|
||||
|
@ -122,7 +378,7 @@ defmodule ChiyaWeb.PublicComponents do
|
|||
|> assign(:fourth, Enum.at(images, 3))
|
||||
|
||||
~H"""
|
||||
<figure class="images-4 | flex gap-1 flex-col">
|
||||
<figure class="flex gap-1 flex-col">
|
||||
<section class="flex gap-1">
|
||||
<.featured_image image={assigns.first} size={:thumb} class="flex-1 w-full rounded-tl-lg" />
|
||||
<.featured_image image={assigns.second} size={:thumb} class="flex-1 w-full rounded-tr-lg" />
|
||||
|
@ -146,11 +402,12 @@ defmodule ChiyaWeb.PublicComponents do
|
|||
src={ChiyaWeb.Helpers.image_url(assigns.image, assigns.size)}
|
||||
class={assigns.class}
|
||||
title={assigns.image.content}
|
||||
loading="lazy"
|
||||
/>
|
||||
"""
|
||||
end
|
||||
|
||||
defp gallery_name(note), do: "gallery-#{note.id}"
|
||||
|
||||
defp main_images(note),
|
||||
do: Enum.filter(note.images, fn image -> image.featured end)
|
||||
end
|
||||
|
|
|
@ -5,6 +5,29 @@ 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)
|
||||
|
||||
|
@ -31,6 +54,8 @@ defmodule ChiyaWeb.NoteController do
|
|||
|> put_flash(:info, "Note created successfully.")
|
||||
|> redirect(to: ~p"/admin/notes/#{note}")
|
||||
|
||||
# TODO: set channels from changeset when error happened?
|
||||
|
||||
{:error, %Ecto.Changeset{} = changeset} ->
|
||||
render(conn, :new,
|
||||
changeset: changeset,
|
||||
|
@ -42,6 +67,10 @@ 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)
|
||||
|
@ -67,6 +96,8 @@ defmodule ChiyaWeb.NoteController do
|
|||
|> put_flash(:info, "Note updated successfully.")
|
||||
|> redirect(to: ~p"/admin/notes/#{note}")
|
||||
|
||||
# TODO: set channels from changeset when error happened?
|
||||
|
||||
{:error, %Ecto.Changeset{} = changeset} ->
|
||||
render(conn, :edit,
|
||||
note: note,
|
||||
|
@ -191,7 +222,17 @@ defmodule ChiyaWeb.NoteController do
|
|||
)
|
||||
end
|
||||
|
||||
defp get_default_channels(%Plug.Conn{} = conn) do
|
||||
defp with_channels(conn) do
|
||||
assign(
|
||||
conn,
|
||||
:channels,
|
||||
Chiya.Channels.list_channels()
|
||||
|> Chiya.Channels.preload_channel()
|
||||
|> Enum.filter(fn c -> not Enum.empty?(c.notes) end)
|
||||
)
|
||||
end
|
||||
|
||||
defp get_default_channels(conn = %Plug.Conn{}) do
|
||||
if conn.assigns.settings.default_channel do
|
||||
[conn.assigns.settings.default_channel.id]
|
||||
else
|
||||
|
|
|
@ -13,19 +13,15 @@
|
|||
|
||||
<section class="flex flex-row flex-wrap mt-6 -mb-6 gap-3">
|
||||
<a
|
||||
href="#"
|
||||
href={~p"/admin/notes"}
|
||||
class="text-sm dark:text-gray-300 rounded-full bg-gray-100 hover:bg-gray-200 dark:bg-gray-800 dark:hover:bg-gray-700 px-4 py-2 border border-gray-300 dark:border-gray-600 shadow-sm transition"
|
||||
phx-click="update-channel"
|
||||
phx-value-channelid={nil}
|
||||
>
|
||||
All <span class="text-gray-600 dark:text-gray-500">(<%= Enum.count(@notes) %>)</span>
|
||||
</a>
|
||||
<%= for channel <- @channels do %>
|
||||
<a
|
||||
href="#"
|
||||
href={~p"/admin/notes?channel=#{channel.slug}"}
|
||||
class="text-sm dark:text-gray-300 rounded-full bg-gray-100 hover:bg-gray-200 dark:bg-gray-800 dark:hover:bg-gray-700 px-4 py-2 border border-gray-300 dark:border-gray-600 shadow-sm transition"
|
||||
phx-click="update-channel"
|
||||
phx-value-channelid={channel.id}
|
||||
>
|
||||
<%= channel.name %>
|
||||
<span class="text-gray-600 dark:text-gray-500">(<%= Enum.count(channel.notes) %>)</span>
|
||||
|
@ -38,15 +34,3 @@
|
|||
<: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,37 +1,31 @@
|
|||
defmodule ChiyaWeb.PageController do
|
||||
use ChiyaWeb, :controller
|
||||
alias Chiya.Channels
|
||||
|
||||
plug :put_layout, html: {ChiyaWeb.Layouts, :public}
|
||||
plug :put_assigns
|
||||
|
||||
def home(conn, params) do
|
||||
if id = conn.assigns.settings.home_channel_id do
|
||||
channel = Channels.get_channel!(id)
|
||||
{:ok, {notes, meta}} = Chiya.Notes.list_home_notes(channel, params)
|
||||
def home(conn, _params) do
|
||||
settings = conn.assigns.settings
|
||||
|
||||
channel =
|
||||
case settings.home_channel_id do
|
||||
nil -> nil
|
||||
id -> Chiya.Channels.get_channel!(id) |> Chiya.Channels.preload_channel_public()
|
||||
end
|
||||
|
||||
render(conn, :home,
|
||||
channel: channel,
|
||||
notes: notes,
|
||||
meta: meta,
|
||||
page_title: "Home",
|
||||
page_header: false
|
||||
page_title: "Home"
|
||||
)
|
||||
else
|
||||
not_found(conn)
|
||||
end
|
||||
end
|
||||
|
||||
def channel(conn, %{"slug" => channel_slug}) do
|
||||
channel =
|
||||
channel_slug
|
||||
|> Channels.get_channel_by_slug!()
|
||||
|> Channels.preload_channel_public()
|
||||
Chiya.Channels.get_channel_by_slug!(channel_slug)
|
||||
|> Chiya.Channels.preload_channel_public()
|
||||
|
||||
render(conn, :channel,
|
||||
channel: channel,
|
||||
page_title: channel.name,
|
||||
content: channel.content
|
||||
page_title: channel.name
|
||||
)
|
||||
end
|
||||
|
||||
|
@ -40,7 +34,7 @@ defmodule ChiyaWeb.PageController do
|
|||
|
||||
render(conn, :tag,
|
||||
tag: tag,
|
||||
page_title: "Tagged with #{tag.name}"
|
||||
page_title: tag.name
|
||||
)
|
||||
end
|
||||
|
||||
|
@ -48,15 +42,14 @@ defmodule ChiyaWeb.PageController do
|
|||
note = Chiya.Notes.get_note_by_slug_preloaded!(note_slug)
|
||||
changeset = Chiya.Notes.change_note_comment(%Chiya.Notes.NoteComment{}, %{note_id: note.id})
|
||||
|
||||
if note.published_at || conn.assigns.current_user do
|
||||
if is_nil(note.published_at) and is_nil(conn.assigns.current_user) do
|
||||
render_error(conn, :not_found)
|
||||
else
|
||||
render(conn, :note,
|
||||
note: note,
|
||||
changeset: changeset,
|
||||
page_title: note.name,
|
||||
page_header: false
|
||||
changeset: changeset
|
||||
)
|
||||
else
|
||||
not_found(conn)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -64,56 +57,66 @@ defmodule ChiyaWeb.PageController do
|
|||
note = Chiya.Notes.get_note_by_slug_preloaded("about")
|
||||
user = Chiya.Accounts.get_user!(1)
|
||||
|
||||
if note && user do
|
||||
render(conn, :about,
|
||||
note: note,
|
||||
user: user,
|
||||
page_title: user.name
|
||||
page_title: "About"
|
||||
)
|
||||
else
|
||||
not_found(conn)
|
||||
end
|
||||
end
|
||||
|
||||
def wiki(conn, _params) do
|
||||
if id = conn.assigns.settings.wiki_channel_id do
|
||||
[channel, notes_updated, notes_published] =
|
||||
case conn.assigns.settings.wiki_channel_id do
|
||||
nil ->
|
||||
[nil, nil, nil]
|
||||
|
||||
id ->
|
||||
channel = Chiya.Channels.get_channel!(id)
|
||||
notes = Chiya.Notes.list_notes_by_channel_updated(channel, 999)
|
||||
updated = Chiya.Notes.list_notes_by_channel_updated(channel, 5)
|
||||
published = Chiya.Notes.list_notes_by_channel_published(channel, 5)
|
||||
[channel, updated, published]
|
||||
end
|
||||
|
||||
render(conn, :wiki,
|
||||
channel: channel,
|
||||
notes: notes,
|
||||
page_title: channel.name,
|
||||
content: channel.content
|
||||
notes_updated: notes_updated,
|
||||
notes_published: notes_published,
|
||||
page_title: "Wiki"
|
||||
)
|
||||
else
|
||||
not_found(conn)
|
||||
end
|
||||
end
|
||||
|
||||
def bookmarks(conn, _params) do
|
||||
if id = conn.assigns.settings.bookmark_channel_id do
|
||||
[channel, notes, tags] =
|
||||
case conn.assigns.settings.bookmark_channel_id do
|
||||
nil ->
|
||||
[nil, nil]
|
||||
|
||||
id ->
|
||||
channel = Chiya.Channels.get_channel!(id)
|
||||
notes = Chiya.Notes.list_notes_by_channel_published(channel, 999)
|
||||
tags = group_tags(notes)
|
||||
|
||||
[channel, notes, tags]
|
||||
end
|
||||
|
||||
render(conn, :bookmarks,
|
||||
channel: channel,
|
||||
notes: notes,
|
||||
page_title: "#{Enum.count(notes)} Bookmarks"
|
||||
tags: tags,
|
||||
page_title: "Bookmarks"
|
||||
)
|
||||
else
|
||||
not_found(conn)
|
||||
end
|
||||
end
|
||||
|
||||
defp not_found(conn) do
|
||||
conn
|
||||
|> assign(:page_title, "404 Not found")
|
||||
|> render_error(:not_found)
|
||||
end
|
||||
|
||||
defp put_assigns(conn, opts) do
|
||||
conn
|
||||
|> assign(:page_header, true)
|
||||
defp group_tags(notes) do
|
||||
Enum.reduce(notes, [], fn n, acc ->
|
||||
acc ++ n.tags
|
||||
end)
|
||||
|> Enum.uniq_by(fn t -> t.id end)
|
||||
|> Enum.sort_by(fn t -> t.slug end, :asc)
|
||||
|> Enum.group_by(
|
||||
fn n -> String.first(n.name) end,
|
||||
fn n -> n end
|
||||
)
|
||||
|> IO.inspect()
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,47 +1,36 @@
|
|||
defmodule ChiyaWeb.PageHTML do
|
||||
use ChiyaWeb, :html_public
|
||||
import ChiyaWeb.Format, only: [pretty_datetime: 1, pretty_date: 1]
|
||||
import Phoenix.HTML.Tag, only: [content_tag: 3, content_tag: 2]
|
||||
|
||||
embed_templates "page_html/*"
|
||||
|
||||
attr :notes, :list, required: true
|
||||
attr :layout, :atom, default: :default
|
||||
attr :show_content, :boolean, default: true
|
||||
def note_list(assigns)
|
||||
|
||||
attr :notes, :list, required: true
|
||||
attr :show_content, :boolean, default: true
|
||||
def note_list_default(assigns)
|
||||
|
||||
attr :notes, :list, required: true
|
||||
attr :show_content, :boolean, default: true
|
||||
def note_list_microblog(assigns)
|
||||
|
||||
attr :notes, :list, required: true
|
||||
attr :show_content, :boolean, default: true
|
||||
def note_list_gallery(assigns)
|
||||
|
||||
attr :note, :map, required: true
|
||||
attr :linked, :boolean, default: true
|
||||
def tag_list(assigns)
|
||||
def tag_list([]), do: "No Tags"
|
||||
def tag_list(tags), do: Enum.map_join(tags, ", ", fn t -> t.name end)
|
||||
|
||||
def render_outline(note) do
|
||||
ChiyaWeb.OutlineRenderer.render_outline(note.content)
|
||||
note.content
|
||||
|> ChiyaWeb.Outline.get()
|
||||
|> Enum.map(&do_render_outline/1)
|
||||
|> Enum.map(&safe_to_string/1)
|
||||
end
|
||||
|
||||
def has_outline?(note) do
|
||||
ChiyaWeb.OutlineRenderer.has_outline?(note.content)
|
||||
outline_empty =
|
||||
note.content
|
||||
|> ChiyaWeb.Outline.get()
|
||||
|> Enum.empty?()
|
||||
|
||||
!outline_empty
|
||||
end
|
||||
|
||||
def group_tags(notes) do
|
||||
Enum.reduce(notes, [], fn n, acc ->
|
||||
acc ++ n.tags
|
||||
end)
|
||||
|> Enum.uniq_by(fn t -> t.id end)
|
||||
|> Enum.sort_by(fn t -> t.slug end, :asc)
|
||||
|> Enum.group_by(
|
||||
fn n -> String.first(n.name) end,
|
||||
fn n -> n end
|
||||
def do_render_outline(%{text: text, children: children, level: _level}) do
|
||||
slug = Slugger.slugify_downcase(text)
|
||||
|
||||
content_tag(:ul, [class: "m-0"],
|
||||
do: [
|
||||
content_tag(:li, do: content_tag(:a, text, href: "##{slug}")),
|
||||
Enum.map(children, &do_render_outline/1)
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,23 +1,20 @@
|
|||
<section>
|
||||
<section class="max-w-2xl mx-auto">
|
||||
<article class="h-card hcard">
|
||||
<section class="flex gap-3">
|
||||
<img
|
||||
class="rounded-lg block text-center w-28 h-28 | u-photo"
|
||||
src={ChiyaWeb.Uploaders.UserImage.url({@user.user_image, @current_user}, :thumb)}
|
||||
/>
|
||||
<p><%= @user.bio %></p>
|
||||
<.header class="flex-1">
|
||||
<span class="p-name"><%= @user.name %></span>
|
||||
<:subtitle><%= @user.bio %></:subtitle>
|
||||
</.header>
|
||||
</section>
|
||||
|
||||
<section class="prose max-w-none | p-summary e-content">
|
||||
<%= if @note do %>
|
||||
<section class="mx-auto mt-8 prose prose-gruvbox md:prose-lg lg:prose-xl | p-summary e-content">
|
||||
<%= Markdown.render(@note.content) |> raw %>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<aside class="prose max-w-none">
|
||||
<%= if has_outline?(@note) do %>
|
||||
<h3>Outline</h3>
|
||||
|
||||
<section>
|
||||
<%= raw(render_outline(@note)) %>
|
||||
</section>
|
||||
<% end %>
|
||||
</aside>
|
||||
</article>
|
||||
</section>
|
||||
|
|
|
@ -1,7 +1,45 @@
|
|||
<section>
|
||||
<.note_list notes={@notes} layout={@channel.layout} />
|
||||
</section>
|
||||
<%= if @channel do %>
|
||||
<section class="mx-auto max-w-2xl">
|
||||
<.header>
|
||||
Bookmarks
|
||||
<:subtitle><%= Enum.count(@notes) in total %></:subtitle>
|
||||
</.header>
|
||||
</section>
|
||||
|
||||
<aside>
|
||||
<.tag_bar notes={@notes} />
|
||||
</aside>
|
||||
<section class="mt-6 grid grid-cols-1 md:grid-cols-3 gap-3 max-w-4xl mx-auto">
|
||||
<section class="col-span-1 md:col-span-2">
|
||||
<.note_list notes={@notes} layout={@channel.layout} show_content={false} />
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<ul>
|
||||
<%= for {letter, tag_group} <- @tags do %>
|
||||
<li class="mb-2">
|
||||
<span class="capitalize text-theme-primary border border-theme-background1 rounded font-sm px-2 py-1 inline-block mb-2">
|
||||
<%= letter %>
|
||||
</span>
|
||||
|
||||
<%= for tag <- tag_group do %>
|
||||
<a
|
||||
href={~p"/tagged-with/#{tag.slug}"}
|
||||
class="border border-theme-background1 rounded font-sm px-2 py-1 inline-block mb-2 hover:bg-theme-background1 transition"
|
||||
>
|
||||
<%= tag.name %> <%= Enum.count(tag.notes) %>
|
||||
</a>
|
||||
<% end %>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</section>
|
||||
</section>
|
||||
<% else %>
|
||||
<section class="mx-auto max-w-2xl">
|
||||
<.header>
|
||||
Bookmarks
|
||||
</.header>
|
||||
|
||||
<section class="prose prose-gruvbox mt-6">
|
||||
Bookmarks not set up.
|
||||
</section>
|
||||
</section>
|
||||
<% end %>
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
<section>
|
||||
<.note_list notes={@channel.notes} layout={@channel.layout} />
|
||||
</section>
|
||||
<section class="max-w-2xl mx-auto">
|
||||
<.header>
|
||||
<%= @channel.name %>
|
||||
<:subtitle><%= Markdown.render(@channel.content) |> raw %></:subtitle>
|
||||
</.header>
|
||||
|
||||
<aside>
|
||||
<.tag_bar notes={@channel.notes} />
|
||||
</aside>
|
||||
<div class="w-full mt-6 sm:w-auto flex flex-col gap-1.5">
|
||||
<.note_list notes={@channel.notes} layout={@channel.layout} />
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
@ -1,5 +1,29 @@
|
|||
<section>
|
||||
<.note_list notes={@notes} layout={@channel.layout} />
|
||||
<section class="max-w-2xl mx-auto">
|
||||
<.header class_title="text-theme-primary p-name" class_subtitle="p-summary">
|
||||
<%= @settings.title %>
|
||||
<:subtitle><%= @settings.subtitle %></:subtitle>
|
||||
</.header>
|
||||
|
||||
<Flop.Phoenix.pagination meta={@meta} path={~p"/"} />
|
||||
<section class="text-sm my-8">
|
||||
<ul class="flex flex-wrap gap-3">
|
||||
<li>
|
||||
<a href="#" class="button">
|
||||
<.icon name="hero-megaphone" />
|
||||
</a>
|
||||
</li>
|
||||
<%= for channel <- @channels do %>
|
||||
<li>
|
||||
<a href={~p"/channel/#{channel.slug}"} class="button">
|
||||
<%= channel.name %>
|
||||
</a>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<%= if @channel do %>
|
||||
<section class="mt-8">
|
||||
<.note_list notes={@channel.notes} layout={@channel.layout} />
|
||||
</section>
|
||||
<% end %>
|
||||
</section>
|
||||
|
|
|
@ -1,26 +1,84 @@
|
|||
<section>
|
||||
<article class="h-entry hentry note | stack container">
|
||||
<header class="prose max-w-none">
|
||||
<h1 class="p-name">
|
||||
<article class="h-entry hentry">
|
||||
<.header class="max-w-2xl mx-auto" class_title="p-name">
|
||||
<%= @note.name %>
|
||||
</h1>
|
||||
</header>
|
||||
</.header>
|
||||
|
||||
<aside>
|
||||
<%= if @current_user do %>
|
||||
<section class="max-w-2xl mx-auto mt-8 print:hidden">
|
||||
<ul class="flex gap-3">
|
||||
<li>
|
||||
<a href={~p"/admin/notes/#{@note}/edit"} class="button">
|
||||
<.icon name="hero-pencil-square" /> Edit
|
||||
</a>
|
||||
</li>
|
||||
<li><a href={~p"/admin/notes/#{@note}"} class="button">Show in Admin</a></li>
|
||||
</ul>
|
||||
</section>
|
||||
<% end %>
|
||||
|
||||
<aside class="max-w-2xl mx-auto mt-8">
|
||||
<.featured_images note={@note} />
|
||||
</aside>
|
||||
|
||||
<section class="p-summary e-content | prose max-w-none">
|
||||
<%= if has_outline?(@note) do %>
|
||||
<aside class="max-w-2xl mx-auto mt-8 prose prose-gruvbox print:hidden">
|
||||
<div class="bg-theme-background1 rounded p-2">
|
||||
<%= raw(render_outline(@note)) %>
|
||||
</div>
|
||||
</aside>
|
||||
<% end %>
|
||||
|
||||
<section class="mt-8 mx-auto prose prose-gruvbox md:prose-lg lg:prose-xl | p-summary e-content">
|
||||
<%= Markdown.render(@note.content) |> raw %>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<%= if not Enum.empty?(@note.links_to) do %>
|
||||
<section class="mt-8 prose prose-gruvbox max-w-2xl mx-auto">
|
||||
<.divider text="" /> Notes linking here:
|
||||
<ul class="">
|
||||
<%= for link <- @note.links_to do %>
|
||||
<li><a href={~p"/note/#{link.slug}"}><%= link.name %></a></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</section>
|
||||
<% end %>
|
||||
|
||||
<%= if String.length(@note.url || "") > 0 do %>
|
||||
<section class="mt-8 max-w-2xl mx-auto text-center text-lg">
|
||||
<.divider text=" " />
|
||||
<.icon name="hero-link" /> <a href={@note.url}><%= @note.url %></a>
|
||||
</section>
|
||||
<% end %>
|
||||
|
||||
<footer class="max-w-2xl mx-auto mt-8 text-theme-base">
|
||||
<%= if @note.published_at do %>
|
||||
<span>Published</span>
|
||||
<% else %>
|
||||
<span>Unpublished</span>
|
||||
<% end %>
|
||||
<time class="font-semibold | dt-published"><%= pretty_date(@note.published_at) %></time>
|
||||
<.dot />
|
||||
<span>Last Updated</span>
|
||||
<time class="font-semibold" datetime={datetime(@note.updated_at)}>
|
||||
<%= pretty_date(@note.updated_at) %>
|
||||
</time>
|
||||
<%= if not Enum.empty?(@note.tags) do %>
|
||||
<.dot />
|
||||
<.tags note={@note} />
|
||||
<% end %>
|
||||
<%= if @note.kind != :post do %>
|
||||
<.dot />
|
||||
<span><%= @note.kind %></span>
|
||||
<% end %>
|
||||
|
||||
<a href={~p"/"} class="hidden | h-card u-author">Inhji</a>
|
||||
<a href={~p"/note/#{@note.slug}"} class="hidden | u-url u-uid"><%= @note.name %></a>
|
||||
</footer>
|
||||
|
||||
<section>
|
||||
<section class="max-w-2xl mx-auto mt-8">
|
||||
<%= if !Enum.empty?(@note.images) do %>
|
||||
<.line />
|
||||
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<%= for image <- @note.images do %>
|
||||
<a
|
||||
|
@ -38,74 +96,4 @@
|
|||
</div>
|
||||
<% end %>
|
||||
</section>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<aside class="prose max-w-none">
|
||||
<%= if has_outline?(@note) do %>
|
||||
<h3><.icon name="hero-rectangle-stack" /> Outline</h3>
|
||||
|
||||
<section>
|
||||
<%= raw(render_outline(@note)) %>
|
||||
</section>
|
||||
<% end %>
|
||||
|
||||
<h3><.icon name="hero-rectangle-group" /> Properties</h3>
|
||||
|
||||
<section>
|
||||
<ul>
|
||||
<li>
|
||||
<%= if @note.published_at do %>
|
||||
<strong>Published</strong>
|
||||
<% else %>
|
||||
<strong>Unpublished</strong>
|
||||
<% end %>
|
||||
<time class="dt-published"><%= pretty_date(@note.published_at) %></time>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Last updated</strong>
|
||||
<time class="dt-published"><%= pretty_date(@note.published_at) %></time>
|
||||
</li>
|
||||
<li><strong>Tags</strong> <.tag_list note={@note} /></li>
|
||||
<li><strong>Kind</strong> <%= @note.kind %></li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<%= if String.length(@note.url || "") > 0 do %>
|
||||
<h3><.icon name="hero-link" /> URL</h3>
|
||||
|
||||
<section>
|
||||
<ul>
|
||||
<li><a href={@note.url}><%= @note.url %></a></li>
|
||||
</ul>
|
||||
</section>
|
||||
<% end %>
|
||||
|
||||
<%= if not Enum.empty?(@note.links_to) do %>
|
||||
<h3><.icon name="hero-arrow-down-on-square" /> Notes linking here</h3>
|
||||
<section>
|
||||
<ul>
|
||||
<%= for link <- @note.links_to do %>
|
||||
<li><a href={~p"/note/#{link.slug}"}><%= link.name %></a></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</section>
|
||||
<% end %>
|
||||
|
||||
<h3><.icon name="hero-wrench-screwdriver" /> Admin</h3>
|
||||
|
||||
<section>
|
||||
<ul>
|
||||
<li>
|
||||
<a href={~p"/admin/notes/#{@note}/edit"} class="button">
|
||||
Edit
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href={~p"/admin/notes/#{@note}"} class="button">
|
||||
Show in Admin
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</aside>
|
||||
</article>
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
<%= if @layout == :default do %>
|
||||
<.note_list_default notes={@notes} show_content={@show_content} />
|
||||
<% end %>
|
||||
<%= if @layout == :gallery do %>
|
||||
<.note_list_gallery notes={@notes} show_content={@show_content} />
|
||||
<% end %>
|
||||
<%= if @layout == :microblog do %>
|
||||
<.note_list_microblog notes={@notes} show_content={@show_content} />
|
||||
<% end %>
|
|
@ -1,30 +0,0 @@
|
|||
<section class="note-list default stack">
|
||||
<%= for note <- assigns.notes do %>
|
||||
<article class="prose max-w-none">
|
||||
<header class="flex flex-row items-center gap-1">
|
||||
<span class="text-theme-primary text-lg font-semibold leading-8 flex-1">
|
||||
<a href={~p"/note/#{note.slug}"}>
|
||||
<%= note.name %>
|
||||
</a>
|
||||
</span>
|
||||
</header>
|
||||
|
||||
<%= if assigns.show_content do %>
|
||||
<p class="text-theme-base mb-3">
|
||||
<%= Chiya.Notes.Note.note_excerpt(note.content) %>
|
||||
</p>
|
||||
<% end %>
|
||||
|
||||
<footer class="flex">
|
||||
<span class="text-theme-base/75 flex-1">
|
||||
<%= pretty_date(note.published_at) %>
|
||||
</span>
|
||||
<%= if not Enum.empty?(note.tags) do %>
|
||||
<span class="inline-block">
|
||||
<.tag_list note={note} linked={false} />
|
||||
</span>
|
||||
<% end %>
|
||||
</footer>
|
||||
</article>
|
||||
<% end %>
|
||||
</section>
|
|
@ -1,29 +0,0 @@
|
|||
<section class="note-list gallery | stack">
|
||||
<%= for note <- assigns.notes do %>
|
||||
<article>
|
||||
<section class="flex flex-wrap justify-start gap-3">
|
||||
<%= for image <- note.images do %>
|
||||
<a
|
||||
href={ChiyaWeb.Helpers.image_url(image, :full)}
|
||||
class="lightbox | w-28"
|
||||
data-gallery={"gallery-#{note.id}"}
|
||||
data-description={ChiyaWeb.Markdown.render(image.content)}
|
||||
>
|
||||
<img src={ChiyaWeb.Helpers.image_url(image, :thumb)} loading="lazy" />
|
||||
</a>
|
||||
<% end %>
|
||||
</section>
|
||||
<a
|
||||
href={~p"/note/#{note.slug}"}
|
||||
class="text-theme-secondary text-lg/10 font-semibold rounded-lg -mx-2 -my-0.5 px-2 py-0.5 hover:bg-theme-secondary/10 transition"
|
||||
>
|
||||
<%= note.name %>
|
||||
<span class="text-theme-base/75 text-sm">
|
||||
<%= pretty_date(note.published_at) %>
|
||||
</span>
|
||||
</a>
|
||||
</article>
|
||||
|
||||
<.line />
|
||||
<% end %>
|
||||
</section>
|
|
@ -1,31 +0,0 @@
|
|||
<section class="note-list microblog | stack">
|
||||
<%= for note <- assigns.notes do %>
|
||||
<article class="stack">
|
||||
<.featured_images note={note} />
|
||||
|
||||
<header class="text-lg">
|
||||
<%= if(note.kind == :bookmark) do %>
|
||||
<strong><%= note.name %></strong>
|
||||
<% end %>
|
||||
</header>
|
||||
|
||||
<div class="prose max-w-none">
|
||||
<%= raw(Markdown.render(note.content)) %>
|
||||
</div>
|
||||
|
||||
<footer class="prose max-w-none">
|
||||
<a href={~p"/note/#{note.slug}"}>
|
||||
<time>
|
||||
<%= pretty_datetime(note.published_at) %>
|
||||
</time>
|
||||
</a>
|
||||
<%= if not Enum.empty?(note.tags) do %>
|
||||
<span class="dot" />
|
||||
<.tag_list note={note} />
|
||||
<% end %>
|
||||
</footer>
|
||||
</article>
|
||||
|
||||
<div class="divider">⌘</div>
|
||||
<% end %>
|
||||
</section>
|
|
@ -1,7 +1,10 @@
|
|||
<section class="col-span-1 md:col-span-2">
|
||||
<.note_list notes={@tag.notes} />
|
||||
</section>
|
||||
<section class="max-w-2xl mx-auto">
|
||||
<.header>
|
||||
Tagged with “<%= @tag.name %>”
|
||||
<:subtitle><%= @tag.content %></:subtitle>
|
||||
</.header>
|
||||
|
||||
<aside>
|
||||
<.tag_bar notes={@tag.notes} />
|
||||
</aside>
|
||||
<div class="w-full mt-6 sm:w-auto flex flex-col gap-1.5">
|
||||
<.note_list notes={@tag.notes} />
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
<ul class="tag-bar">
|
||||
<%= for {letter, tag_group} <- group_tags(@notes) do %>
|
||||
<li>
|
||||
<span class="letter">
|
||||
<%= letter %>
|
||||
</span>
|
||||
|
||||
<%= for tag <- tag_group do %>
|
||||
<a href={~p"/tagged-with/#{tag.slug}"}>
|
||||
<%= tag.name %>
|
||||
<span class="count">
|
||||
<%= Enum.count(tag.notes) %>
|
||||
</span>
|
||||
</a>
|
||||
<% end %>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
|
@ -1,20 +0,0 @@
|
|||
<span class="tags | inline-flex flex-row gap-1 whitespace-normal">
|
||||
<%= for tag <- @note.tags do %>
|
||||
<%= if @linked do %>
|
||||
<a class="tag | flex gap-0" href={~p"/tagged-with/#{tag.slug}"}>
|
||||
<span>#</span>
|
||||
<span class="p-category">
|
||||
<%= tag.name %>
|
||||
</span>
|
||||
</a>
|
||||
<% else %>
|
||||
<span class="tag | flex gap-0">
|
||||
<span>#</span>
|
||||
<span class="p-category">
|
||||
<%= tag.name %>
|
||||
</span>
|
||||
</span>
|
||||
<% end %>
|
||||
<span class="dot last:hidden" />
|
||||
<% end %>
|
||||
</span>
|
|
@ -1,7 +1,37 @@
|
|||
<section>
|
||||
<.note_list notes={@notes} />
|
||||
</section>
|
||||
<%= if @channel do %>
|
||||
<section class="mx-auto max-w-2xl">
|
||||
<.header>
|
||||
<%= @channel.name %>
|
||||
</.header>
|
||||
</section>
|
||||
|
||||
<aside>
|
||||
<.tag_bar notes={@notes} />
|
||||
</aside>
|
||||
<section class="mt-6 flex flex-col gap-3 max-w-2xl mx-auto">
|
||||
<section class="prose prose-gruvbox md:prose-lg lg:prose-xl max-w-none">
|
||||
<%= Markdown.render(@channel.content) |> raw %>
|
||||
</section>
|
||||
|
||||
<aside class="flex flex-col md:flex-row gap-3 mt-6">
|
||||
<div class="mt-6 flex flex-1 flex-col gap-1.5">
|
||||
<h2 class="text-xl text-theme-base">Recently Updated</h2>
|
||||
|
||||
<.note_list notes={@notes_updated} layout={@channel.layout} show_content={false} />
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex flex-1 flex-col gap-1.5">
|
||||
<h2 class="text-xl text-theme-base">Recently Published</h2>
|
||||
|
||||
<.note_list notes={@notes_published} layout={@channel.layout} show_content={false} />
|
||||
</div>
|
||||
</aside>
|
||||
</section>
|
||||
<% else %>
|
||||
<section class="mx-auto max-w-2xl">
|
||||
<.header>
|
||||
Wiki
|
||||
</.header>
|
||||
|
||||
<section class="prose prose-gruvbox mt-6">
|
||||
Wiki is not set up.
|
||||
</section>
|
||||
</section>
|
||||
<% end %>
|
||||
|
|
|
@ -1,74 +0,0 @@
|
|||
defmodule ChiyaWeb.TagController do
|
||||
use ChiyaWeb, :controller
|
||||
alias Chiya.Tags
|
||||
|
||||
def index(conn, _params) do
|
||||
tags = Chiya.Tags.list_admin_tags()
|
||||
|
||||
render(conn, :index, tags: tags)
|
||||
end
|
||||
|
||||
def show(conn, %{"id" => id}) do
|
||||
tag = Tags.get_tag!(id)
|
||||
render(conn, :show, tag: tag)
|
||||
end
|
||||
|
||||
def apply_prepare(conn, %{"id" => id}) do
|
||||
tag = Tags.get_tag!(id)
|
||||
|
||||
notes =
|
||||
tag
|
||||
|> Chiya.Notes.list_apply_notes()
|
||||
|> Enum.filter(fn note ->
|
||||
exists =
|
||||
note.tags
|
||||
|> Enum.map(fn t -> t.name end)
|
||||
|> Enum.member?(tag.name)
|
||||
|
||||
!exists
|
||||
end)
|
||||
|
||||
render(conn, :apply_prepare, tag: tag, notes: notes)
|
||||
end
|
||||
|
||||
def apply_run(conn, %{"id" => id}) do
|
||||
tag = Tags.get_tag!(id)
|
||||
notes = Chiya.Notes.list_apply_notes(tag)
|
||||
|
||||
Enum.each(notes, fn note ->
|
||||
Chiya.Tags.TagUpdater.add_tags(note, [tag.slug])
|
||||
end)
|
||||
|
||||
notes = Chiya.Notes.list_apply_notes(tag)
|
||||
render(conn, :apply_prepare, tag: tag, notes: notes)
|
||||
end
|
||||
|
||||
def edit(conn, %{"id" => id}) do
|
||||
tag = Tags.get_tag!(id)
|
||||
changeset = Tags.change_tag(tag)
|
||||
|
||||
render(conn, :edit,
|
||||
tag: tag,
|
||||
changeset: changeset,
|
||||
page_title: "EDIT #{tag.name}"
|
||||
)
|
||||
end
|
||||
|
||||
def update(conn, %{"id" => id, "tag" => tag_params}) do
|
||||
tag = Tags.get_tag!(id)
|
||||
|
||||
case Tags.update_tag(tag, tag_params) do
|
||||
{:ok, _tag} ->
|
||||
conn
|
||||
|> put_flash(:info, "Tags updated successfully.")
|
||||
|> redirect(to: ~p"/admin/tags")
|
||||
|
||||
{:error, %Ecto.Changeset{} = changeset} ->
|
||||
render(conn, :edit,
|
||||
tag: tag,
|
||||
changeset: changeset,
|
||||
page_title: "EDIT #{tag.name}"
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,5 +0,0 @@
|
|||
defmodule ChiyaWeb.TagHTML do
|
||||
use ChiyaWeb, :html
|
||||
|
||||
embed_templates "tag_html/*"
|
||||
end
|
|
@ -1,25 +0,0 @@
|
|||
<.header>
|
||||
Apply Tag <%= @tag.name %>
|
||||
<:subtitle><%= Enum.count(@notes) %> notes will get the tag.</:subtitle>
|
||||
<:actions>
|
||||
<.link href={~p"/admin/tags/#{@tag}/apply/run"}>
|
||||
<.button>Apply tag</.button>
|
||||
</.link>
|
||||
</:actions>
|
||||
</.header>
|
||||
|
||||
<section class="card-list">
|
||||
<%= for note <- @notes do %>
|
||||
<article class="card">
|
||||
<header>
|
||||
<h2>
|
||||
<a href={"/admin/notes/#{note.id}"}><%= note.name %></a>
|
||||
</h2>
|
||||
</header>
|
||||
<footer>
|
||||
<span>Updated <%= from_now(note.updated_at) %></span>
|
||||
<span>Published <%= from_now(note.published_at) %></span>
|
||||
</footer>
|
||||
</article>
|
||||
<% end %>
|
||||
</section>
|
|
@ -1,10 +0,0 @@
|
|||
<.header>
|
||||
Edit Tag “<%= @tag.name %>”
|
||||
<:actions>
|
||||
<.link href={~p"/admin/tags"}>
|
||||
<.button><.icon name="hero-arrow-left" /> Back to Tags</.button>
|
||||
</.link>
|
||||
</:actions>
|
||||
</.header>
|
||||
|
||||
<.tag_form changeset={@changeset} action={~p"/admin/tags/#{@tag}"} />
|
|
@ -1,46 +0,0 @@
|
|||
<.header>
|
||||
<.icon name="hero-document-text" /> Tags
|
||||
<:subtitle>Tags are tags.</:subtitle>
|
||||
</.header>
|
||||
|
||||
<section class="card-list mt-8">
|
||||
<h3 class="leading-normal text-xl dark:text-gray-100">With Regex</h3>
|
||||
|
||||
<%= for tag <- Enum.filter(@tags, fn t -> !!t.regex end) do %>
|
||||
<article class="card">
|
||||
<header>
|
||||
<h2>
|
||||
<a href={"/admin/tags/#{tag.id}"}><%= tag.name %></a>
|
||||
</h2>
|
||||
</header>
|
||||
<footer>
|
||||
<span><%= Enum.count(tag.notes) %> notes</span>
|
||||
<span>Updated <%= from_now(tag.updated_at) %></span>
|
||||
<%= if String.length(tag.regex || "") > 0 do %>
|
||||
<span>Regex: <%= tag.regex %></span>
|
||||
<% end %>
|
||||
</footer>
|
||||
</article>
|
||||
<% end %>
|
||||
</section>
|
||||
|
||||
<section class="card-list mt-8">
|
||||
<h3 class="leading-normal text-xl dark:text-gray-100">Without Regex</h3>
|
||||
|
||||
<%= for tag <- Enum.filter(@tags, fn t -> !t.regex end) do %>
|
||||
<article class="card">
|
||||
<header>
|
||||
<h2>
|
||||
<a href={"/admin/tags/#{tag.id}"}><%= tag.name %></a>
|
||||
</h2>
|
||||
</header>
|
||||
<footer>
|
||||
<span><%= Enum.count(tag.notes) %> notes</span>
|
||||
<span>Updated <%= from_now(tag.updated_at) %></span>
|
||||
<%= if String.length(tag.regex || "") > 0 do %>
|
||||
<span>Regex: <%= tag.regex %></span>
|
||||
<% end %>
|
||||
</footer>
|
||||
</article>
|
||||
<% end %>
|
||||
</section>
|
|
@ -1,21 +0,0 @@
|
|||
<.header>
|
||||
Tag <%= @tag.id %>
|
||||
<:subtitle>This is a tag record from your database.</:subtitle>
|
||||
<:actions>
|
||||
<.link href={~p"/admin/tags/#{@tag}/edit"}>
|
||||
<.button>Edit tag</.button>
|
||||
</.link>
|
||||
<.link href={~p"/admin/tags/#{@tag}/apply"}>
|
||||
<.button>Apply tag</.button>
|
||||
</.link>
|
||||
</:actions>
|
||||
</.header>
|
||||
|
||||
<.list>
|
||||
<:item title="Name"><%= @tag.name %></:item>
|
||||
<:item title="Content"><%= @tag.content %></:item>
|
||||
<:item title="Slug"><%= @tag.slug %></:item>
|
||||
<:item title="Regex"><%= @tag.regex %></:item>
|
||||
</.list>
|
||||
|
||||
<.back navigate={~p"/admin/notes"}>Back to notes</.back>
|
|
@ -1,14 +0,0 @@
|
|||
<.simple_form :let={f} for={@changeset} action={@action}>
|
||||
<.error :if={@changeset.action}>
|
||||
Oops, something went wrong! Please check the errors below.
|
||||
</.error>
|
||||
|
||||
<.input field={f[:name]} type="text" />
|
||||
<.input field={f[:content]} type="textarea" label="Content" rows="5" class="font-mono" />
|
||||
<.input field={f[:regex]} type="text" label="Regex" />
|
||||
<.input field={f[:slug]} type="text" label="Slug" />
|
||||
|
||||
<:actions>
|
||||
<.button>Save Tag</.button>
|
||||
</:actions>
|
||||
</.simple_form>
|
|
@ -22,9 +22,18 @@ defmodule ChiyaWeb.Indie.Micropub do
|
|||
end
|
||||
end
|
||||
|
||||
def update_note(note, replace, add, delete) do
|
||||
Logger.info("Updating note..")
|
||||
def find_note(note_url) do
|
||||
slug = Chiya.Notes.Note.note_slug(note_url)
|
||||
note = Chiya.Notes.get_note_by_slug_preloaded(slug)
|
||||
|
||||
if is_nil(note) do
|
||||
{:error, :invalid_request}
|
||||
else
|
||||
{:ok, note}
|
||||
end
|
||||
end
|
||||
|
||||
def update_note(note, replace, add, delete) do
|
||||
with {:ok, note_attrs} <- get_update_attrs(replace, add, delete),
|
||||
{:ok, note} <- Chiya.Notes.update_note(note, note_attrs) do
|
||||
Logger.info("Note updated!")
|
||||
|
@ -38,28 +47,6 @@ defmodule ChiyaWeb.Indie.Micropub do
|
|||
end
|
||||
end
|
||||
|
||||
def find_note(note_url) do
|
||||
Logger.info("Looking up note by url #{note_url}")
|
||||
|
||||
case Chiya.Notes.Note.note_slug(note_url) do
|
||||
{:ok, slug} ->
|
||||
Logger.info("Found note with slug #{slug}, fetching note.")
|
||||
note = Chiya.Notes.get_note_by_slug_preloaded(slug)
|
||||
|
||||
if is_nil(note) do
|
||||
Logger.error("Note with #{note_url} was not found.")
|
||||
{:error, :invalid_request}
|
||||
else
|
||||
Logger.info("Note found!")
|
||||
{:ok, note}
|
||||
end
|
||||
|
||||
_ ->
|
||||
Logger.error("Note with #{note_url} was not found.")
|
||||
{:error, :invalid_request}
|
||||
end
|
||||
end
|
||||
|
||||
defp create_photos(note, properties) do
|
||||
properties
|
||||
|> Props.get_photos()
|
||||
|
@ -111,8 +98,6 @@ defmodule ChiyaWeb.Indie.Micropub do
|
|||
|> Enum.into(replace_attrs)
|
||||
|> Enum.into(add_attrs)
|
||||
|
||||
Logger.info("Update attributes: #{inspect(attrs)}")
|
||||
|
||||
{:ok, attrs}
|
||||
end
|
||||
|
||||
|
@ -139,11 +124,11 @@ defmodule ChiyaWeb.Indie.Micropub do
|
|||
tags = Props.get_tags(source)
|
||||
|
||||
attrs =
|
||||
if Enum.empty?(tags) do
|
||||
attrs
|
||||
else
|
||||
if !Enum.empty?(tags) do
|
||||
tags_string = Enum.join(tags, ",")
|
||||
Map.put(attrs, :tags_string, tags_string)
|
||||
else
|
||||
attrs
|
||||
end
|
||||
|
||||
attrs
|
||||
|
@ -156,9 +141,7 @@ defmodule ChiyaWeb.Indie.Micropub do
|
|||
defp verify_app_token(access_token) do
|
||||
token = Chiya.Accounts.get_app_token("obsidian", "app")
|
||||
|
||||
if is_nil(token) do
|
||||
{:error, :insufficient_scope, "Could not verify app token"}
|
||||
else
|
||||
if not is_nil(token) do
|
||||
token_string =
|
||||
token.token
|
||||
|> :crypto.bytes_to_integer()
|
||||
|
@ -169,6 +152,8 @@ defmodule ChiyaWeb.Indie.Micropub do
|
|||
else
|
||||
{:error, :insufficient_scope, "Could not verify app token"}
|
||||
end
|
||||
else
|
||||
{:error, :insufficient_scope, "Could not verify app token"}
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -178,8 +163,6 @@ defmodule ChiyaWeb.Indie.Micropub do
|
|||
|> get_base_attrs()
|
||||
|> get_channel(channel_id)
|
||||
|
||||
Logger.info("Note attributes: #{inspect(attrs)}")
|
||||
|
||||
{:ok, attrs}
|
||||
end
|
||||
|
||||
|
@ -193,8 +176,6 @@ defmodule ChiyaWeb.Indie.Micropub do
|
|||
|> Map.put_new(:url, url)
|
||||
|> Map.put_new(:kind, :bookmark)
|
||||
|
||||
Logger.info("Bookmark attributes: #{inspect(attrs)}")
|
||||
|
||||
{:ok, attrs}
|
||||
end
|
||||
|
||||
|
@ -215,8 +196,6 @@ defmodule ChiyaWeb.Indie.Micropub do
|
|||
published_at: published_at
|
||||
}
|
||||
|
||||
Logger.info("Base attributes: #{inspect(attrs)}")
|
||||
|
||||
attrs
|
||||
end
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
defmodule ChiyaWeb.Indie.MicropubHandler do
|
||||
@behaviour ChiyaWeb.Plugs.PlugMicropub.HandlerBehaviour
|
||||
@behaviour PlugMicropub.HandlerBehaviour
|
||||
require Logger
|
||||
|
||||
use Phoenix.VerifiedRoutes,
|
||||
|
@ -25,14 +25,6 @@ defmodule ChiyaWeb.Indie.MicropubHandler do
|
|||
%{
|
||||
"type" => "bookmark",
|
||||
"name" => "Bookmark"
|
||||
},
|
||||
%{
|
||||
"type" => "like",
|
||||
"name" => "Like"
|
||||
},
|
||||
%{
|
||||
"type" => "repost",
|
||||
"name" => "Repost"
|
||||
}
|
||||
]
|
||||
|
||||
|
@ -99,7 +91,7 @@ defmodule ChiyaWeb.Indie.MicropubHandler do
|
|||
}
|
||||
|
||||
filtered_note =
|
||||
Map.filter(properties, fn {key, _val} ->
|
||||
Map.filter(note, fn {key, _val} ->
|
||||
Enum.member?(filter_properties, to_string(key))
|
||||
end)
|
||||
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
defmodule ChiyaWeb.Indie.Properties do
|
||||
def get_post_type(properties) do
|
||||
cond do
|
||||
Map.has_key?(properties, "like-of") -> {:ok, :like}
|
||||
Map.has_key?(properties, "bookmark-of") -> {:ok, :bookmark}
|
||||
Map.has_key?(properties, "content") -> {:ok, :note}
|
||||
Map.has_key?(properties, "repost-of") -> {:ok, :repost}
|
||||
Map.has_key?(properties, "read-of") -> {:ok, :read}
|
||||
Map.has_key?(properties, "watch-of") -> {:ok, :watch}
|
||||
Map.has_key?(properties, "listen-of") -> {:ok, :listen}
|
||||
true -> {:error, :unsupported_posttype}
|
||||
Map.has_key?(properties, "like-of") ->
|
||||
{:ok, :like}
|
||||
|
||||
Map.has_key?(properties, "bookmark-of") ->
|
||||
{:ok, :bookmark}
|
||||
|
||||
Map.has_key?(properties, "content") ->
|
||||
{:ok, :note}
|
||||
|
||||
true ->
|
||||
{:error, :unsupported_posttype}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -37,6 +37,8 @@ defmodule ChiyaWeb.AdminHomeLive do
|
|||
|> push_navigate(to: ~p"/note/#{note.slug}")}
|
||||
|
||||
{:error, %Ecto.Changeset{} = changeset} ->
|
||||
IO.inspect(changeset)
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:error, "Could not create note!")
|
||||
|
|
|
@ -1,132 +0,0 @@
|
|||
defmodule ChiyaWeb.NoteEditLive do
|
||||
use ChiyaWeb, :live_view
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<.header>
|
||||
<:actions>
|
||||
<.link href={~p"/admin/notes/#{@note.id}"}>
|
||||
<.button><.icon name="hero-arrow-left" /> Back to Note</.button>
|
||||
</.link>
|
||||
</:actions>
|
||||
</.header>
|
||||
|
||||
<.simple_form for={@note_form} id="note_form" phx-change="validate_note" phx-submit="update_note">
|
||||
<header>
|
||||
<.input
|
||||
field={@note_form[:name]}
|
||||
type="text"
|
||||
class="border-none dark:border-none text-2xl dark:text-2xl"
|
||||
/>
|
||||
|
||||
<.input field={@note_form[:slug]} type="text" class="bg-gray-200 dark:bg-gray-900 font-mono" />
|
||||
</header>
|
||||
|
||||
<section class="grid grid-cols-5 gap-6">
|
||||
<section class="col-span-5 md:col-span-3">
|
||||
<.input
|
||||
field={@note_form[:content]}
|
||||
type="textarea"
|
||||
label="Content"
|
||||
rows="20"
|
||||
class="font-mono"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section class="col-span-5 md:col-span-2 flex flex-col gap-6">
|
||||
<.input field={@note_form[:published_at]} type="datetime-local" label="Published at" />
|
||||
<.input
|
||||
field={@note_form[:kind]}
|
||||
type="select"
|
||||
label="Kind"
|
||||
prompt="Choose a value"
|
||||
options={Ecto.Enum.values(Chiya.Notes.Note, :kind)}
|
||||
/>
|
||||
<.input field={@note_form[:url]} type="text" label="Url" />
|
||||
<.input
|
||||
field={@note_form[:tags_string]}
|
||||
type="text"
|
||||
label="Tags"
|
||||
value={tags_to_string(@note.tags)}
|
||||
/>
|
||||
<.input
|
||||
field={@note_form[:channels]}
|
||||
type="select"
|
||||
label="Channel"
|
||||
multiple={true}
|
||||
options={@channels}
|
||||
value={@selected_channels}
|
||||
/>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<:actions>
|
||||
<.button>Save Note</.button>
|
||||
</:actions>
|
||||
</.simple_form>
|
||||
"""
|
||||
end
|
||||
|
||||
def mount(%{"id" => id}, _session, socket) do
|
||||
note = Chiya.Notes.get_note_preloaded!(id)
|
||||
changeset = Chiya.Notes.change_note(note)
|
||||
selected_channels = Enum.map(note.channels, fn c -> c.id end)
|
||||
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(%{
|
||||
note_form: to_form(changeset),
|
||||
note: note,
|
||||
action: ~p"/admin/notes/#{note}",
|
||||
channels: to_channel_options(),
|
||||
selected_channels: selected_channels
|
||||
})}
|
||||
end
|
||||
|
||||
def handle_event("validate_note", params, socket) do
|
||||
%{"note" => note_params} = params
|
||||
|
||||
note_form =
|
||||
socket.assigns.note
|
||||
|> Chiya.Notes.change_note(note_params)
|
||||
|> Map.put(:action, :validate)
|
||||
|> to_form()
|
||||
|
||||
{:noreply, socket |> assign(:note_form, note_form)}
|
||||
end
|
||||
|
||||
def handle_event("update_note", params, socket) do
|
||||
%{"note" => note_params} = params
|
||||
note_params = from_channel_ids(note_params)
|
||||
note = socket.assigns.note
|
||||
|
||||
case Chiya.Notes.update_note(note, note_params) do
|
||||
{:ok, note} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:info, "Note updated successfully.")
|
||||
|> redirect(to: ~p"/admin/notes/#{note}")}
|
||||
|
||||
{:error, %Ecto.Changeset{} = changeset} ->
|
||||
{:noreply, assign(socket, :note_form, to_form(Map.put(changeset, :action, :update)))}
|
||||
end
|
||||
end
|
||||
|
||||
def tags_to_string(tags), do: Enum.map_join(tags, ", ", fn t -> t.name end)
|
||||
|
||||
defp from_channel_ids(note_params) do
|
||||
selected_ids = Enum.map(note_params["channels"] || [], &String.to_integer/1)
|
||||
|
||||
selected_channels =
|
||||
Chiya.Channels.list_channels()
|
||||
|> Enum.filter(fn c -> Enum.member?(selected_ids, c.id) end)
|
||||
|
||||
Map.put(note_params, "channels", selected_channels)
|
||||
end
|
||||
|
||||
defp to_channel_options(items \\ nil),
|
||||
do:
|
||||
Enum.map(items || Chiya.Channels.list_channels(), fn c ->
|
||||
{Chiya.Channels.Channel.icon(c) <> " " <> c.name, c.id}
|
||||
end)
|
||||
end
|
|
@ -1,95 +0,0 @@
|
|||
defmodule ChiyaWeb.NoteListLive do
|
||||
use ChiyaWeb, :live_view
|
||||
|
||||
@impl true
|
||||
def mount(_params, __session, socket) do
|
||||
channels = Chiya.Channels.list_channels() |> Chiya.Channels.preload_channel()
|
||||
{:ok, {notes, meta}} = Chiya.Notes.list_admin_notes(%{})
|
||||
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(%{
|
||||
channels: channels,
|
||||
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} ->
|
||||
{: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
|
||||
|
||||
defp channel_list(assigns) do
|
||||
channels =
|
||||
assigns.channels
|
||||
|> Enum.map(fn c ->
|
||||
{"#{c.name} (#{Enum.count(c.notes)})", c.name}
|
||||
end)
|
||||
|
||||
[{"All", nil}] ++ channels
|
||||
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],
|
||||
channels: [
|
||||
op: :ilike_and,
|
||||
type: "select",
|
||||
options: channel_list(assigns)
|
||||
]
|
||||
]}
|
||||
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
|
|
@ -37,31 +37,20 @@ defmodule ChiyaWeb.NoteShowLive do
|
|||
</:actions>
|
||||
</.header>
|
||||
|
||||
<section class="mt-4">
|
||||
<div class="select-all font-mono bg-white p-1 rounded">[[<%= @note.slug %>]]</div>
|
||||
</section>
|
||||
|
||||
<section class="grid grid-cols-2">
|
||||
<section class="col-span-1">
|
||||
<.list>
|
||||
<:item title="Published at">
|
||||
<%= pretty_date(@note.published_at) %> <span>(<%= from_now(@note.published_at) %>)</span>
|
||||
</:item>
|
||||
<:item title="Channels"><%= note_channels(@note.channels) %></:item>
|
||||
<:item title="Kind"><%= @note.kind %></:item>
|
||||
<:item title="Url"><a href={@note.url} target="_blank"><%= @note.url %></a></:item>
|
||||
<:item title="Url"><%= @note.url %></:item>
|
||||
<:item title="Tags"><%= note_tags(@note.tags) %></:item>
|
||||
<:item title="Links outgoing"><%= note_links(@note.links_from) %></:item>
|
||||
<:item title="Links incoming"><%= note_links(@note.links_to) %></:item>
|
||||
<:item title="Embed">
|
||||
<pre class="p-1 bg-gray-100 text-black rounded select-all">[[<%= @note.slug %>]]</pre>
|
||||
</:item>
|
||||
</.list>
|
||||
</section>
|
||||
|
||||
<section class="col-span-1 p-6">
|
||||
<section class="prose">
|
||||
<%= raw(Markdown.render(@note.content)) %>
|
||||
</section>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<.line />
|
||||
|
||||
|
|
|
@ -1,38 +0,0 @@
|
|||
defmodule ChiyaWeb.OutlineRenderer do
|
||||
import Phoenix.HTML, only: [safe_to_string: 1]
|
||||
import Phoenix.HTML.Tag, only: [content_tag: 3, content_tag: 2]
|
||||
|
||||
def render_outline(content) do
|
||||
children =
|
||||
content
|
||||
|> ChiyaWeb.Outline.get()
|
||||
|> Enum.map(&do_render_outline/1)
|
||||
|
||||
root = content_tag(:ul, do: children)
|
||||
|
||||
safe_to_string(root)
|
||||
end
|
||||
|
||||
def has_outline?(content) do
|
||||
outline_empty =
|
||||
content
|
||||
|> ChiyaWeb.Outline.get()
|
||||
|> Enum.empty?()
|
||||
|
||||
!outline_empty
|
||||
end
|
||||
|
||||
def do_render_outline(%{text: text, children: children, level: _level}) do
|
||||
slug = Slugger.slugify_downcase(text)
|
||||
list_item = content_tag(:li, do: content_tag(:a, text, href: "##{slug}"))
|
||||
|
||||
if Enum.empty?(children) do
|
||||
[list_item]
|
||||
else
|
||||
[
|
||||
list_item,
|
||||
content_tag(:ul, do: Enum.map(children, &do_render_outline/1))
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,416 +0,0 @@
|
|||
defmodule ChiyaWeb.Plugs.PlugMicropub do
|
||||
@moduledoc """
|
||||
A Plug for building a Micropub server.
|
||||
|
||||
To use:
|
||||
|
||||
"""
|
||||
use Plug.Router
|
||||
require Logger
|
||||
|
||||
plug :match
|
||||
plug :dispatch
|
||||
|
||||
# Plug Callbacks
|
||||
|
||||
@doc false
|
||||
def init(opts) do
|
||||
logging =
|
||||
Keyword.get(opts, :logging) || false
|
||||
|
||||
handler =
|
||||
Keyword.get(opts, :handler) || raise ArgumentError, "Micropub Plug requires :handler option"
|
||||
|
||||
json_encoder =
|
||||
Keyword.get(opts, :json_encoder) ||
|
||||
raise ArgumentError, "Micropub Plug requires :json_encoder option"
|
||||
|
||||
[handler: handler, json_encoder: json_encoder, logging: logging]
|
||||
end
|
||||
|
||||
@doc false
|
||||
def call(conn, opts) do
|
||||
conn = put_private(conn, :plug_micropub, opts)
|
||||
super(conn, opts)
|
||||
end
|
||||
|
||||
# Routes
|
||||
|
||||
post "/" do
|
||||
with {:ok, access_token, conn} <- get_access_token(conn),
|
||||
{:ok, action, conn} <- get_action(conn) do
|
||||
Logger.info("Micropub: Handling action [#{action}]")
|
||||
Logger.info("Micropub: Request Body #{inspect(conn.body_params)}")
|
||||
handle_action(action, access_token, conn)
|
||||
else
|
||||
error -> send_error(conn, error)
|
||||
end
|
||||
end
|
||||
|
||||
get "/" do
|
||||
with {:ok, access_token, conn} <- get_access_token(conn),
|
||||
{:ok, query} <- get_query(conn) do
|
||||
Logger.info("Micropub: Handling query [#{query}]")
|
||||
handle_query(query, access_token, conn)
|
||||
else
|
||||
error -> send_error(conn, error)
|
||||
end
|
||||
end
|
||||
|
||||
post "/media" do
|
||||
handler = conn.private[:plug_micropub][:handler]
|
||||
|
||||
Logger.info("Micropub: Handling media")
|
||||
|
||||
with {:ok, access_token, conn} <- get_access_token(conn),
|
||||
{:ok, file} <- get_file(conn),
|
||||
{:ok, url} <- handler.handle_media(file, access_token) do
|
||||
conn
|
||||
|> put_resp_header("location", url)
|
||||
|> send_resp(:created, "")
|
||||
else
|
||||
error -> send_error(conn, error)
|
||||
end
|
||||
end
|
||||
|
||||
match _ do
|
||||
Logger.warning("Micropub: Unsupported url")
|
||||
send_error(conn, {:error, :invalid_request})
|
||||
end
|
||||
|
||||
# Internal Functions
|
||||
|
||||
defp send_content(conn, content) do
|
||||
json_encoder = conn.private[:plug_micropub][:json_encoder]
|
||||
body = json_encoder.encode!(content)
|
||||
|
||||
conn
|
||||
|> put_resp_content_type("application/json")
|
||||
|> send_resp(:ok, body)
|
||||
end
|
||||
|
||||
defp send_error(conn, {:error, error}) do
|
||||
body = %{error: error}
|
||||
_send_error(conn, body)
|
||||
end
|
||||
|
||||
defp send_error(conn, {:error, error, description}) do
|
||||
body = %{error: error, error_description: description}
|
||||
_send_error(conn, body)
|
||||
end
|
||||
|
||||
defp _send_error(conn, body) do
|
||||
json_encoder = conn.private[:plug_micropub][:json_encoder]
|
||||
|
||||
code = get_error_code(body.error)
|
||||
body = json_encoder.encode!(body)
|
||||
|
||||
Logger.warning("Micropub: Sending error with code #{code}: #{inspect(body)}")
|
||||
|
||||
conn
|
||||
|> put_resp_content_type("application/json")
|
||||
|> send_resp(code, body)
|
||||
end
|
||||
|
||||
defp get_error_code(:insufficient_scope), do: :unauthorized
|
||||
defp get_error_code(:invalid_request), do: :bad_request
|
||||
defp get_error_code(code), do: code
|
||||
|
||||
defp get_action(conn) do
|
||||
{action, body_params} = Map.pop(conn.body_params, "action")
|
||||
conn = %Plug.Conn{conn | body_params: body_params}
|
||||
|
||||
case action do
|
||||
nil ->
|
||||
{:ok, :create, conn}
|
||||
|
||||
action when action in ["delete", "undelete", "update"] ->
|
||||
{:ok, String.to_existing_atom(action), conn}
|
||||
|
||||
_ ->
|
||||
{:error, :invalid_request}
|
||||
end
|
||||
end
|
||||
|
||||
defp get_query(conn) do
|
||||
case Map.fetch(conn.query_params, "q") do
|
||||
{:ok, query} when query in ["config", "source", "syndicate-to", "channel", "category"] ->
|
||||
{:ok, String.to_existing_atom(query)}
|
||||
|
||||
_ ->
|
||||
{:error, :invalid_request}
|
||||
end
|
||||
end
|
||||
|
||||
defp get_file(conn) do
|
||||
case Map.fetch(conn.body_params, "file") do
|
||||
{:ok, file} -> {:ok, file}
|
||||
:error -> {:error, :invalid_request}
|
||||
end
|
||||
end
|
||||
|
||||
defp get_access_token(conn) do
|
||||
{access_token, body_params} = Map.pop(conn.body_params, "access_token")
|
||||
conn = %Plug.Conn{conn | body_params: body_params}
|
||||
|
||||
case access_token do
|
||||
nil -> parse_auth_header(conn)
|
||||
access_token -> {:ok, access_token, conn}
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_auth_header(conn) do
|
||||
with [header] <- get_req_header(conn, "authorization"),
|
||||
"Bearer" <> token <- header,
|
||||
do: {:ok, String.trim(token), conn},
|
||||
else: (_ -> {:error, :unauthorized})
|
||||
end
|
||||
|
||||
defp handle_action(:create, access_token, conn) do
|
||||
Logger.info("Micropub: Handle create")
|
||||
|
||||
content_type = conn |> get_req_header("content-type") |> List.first()
|
||||
Logger.info("Micropub: Content type #{content_type}")
|
||||
handler = conn.private[:plug_micropub][:handler]
|
||||
|
||||
with {:ok, type, properties} <- parse_create_body(content_type, conn.body_params),
|
||||
{:ok, code, url} <- handler.handle_create(type, properties, access_token) do
|
||||
conn
|
||||
|> put_resp_header("location", url)
|
||||
|> send_resp(code, "")
|
||||
else
|
||||
error ->
|
||||
Logger.warning("Micropub: Error while handling create: #{inspect(error)}")
|
||||
send_error(conn, error)
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_action(:update, access_token, conn) do
|
||||
Logger.info("Micropub: Handle update")
|
||||
|
||||
content_type = conn |> get_req_header("content-type") |> List.first()
|
||||
Logger.info("Micropub: Content type #{content_type}")
|
||||
|
||||
with "application/json" <- content_type,
|
||||
{url, properties} when is_binary(url) <- Map.pop(conn.body_params, "url"),
|
||||
{:ok, replace, add, delete} <- parse_update_properties(properties) do
|
||||
do_update(conn, access_token, url, replace, add, delete)
|
||||
else
|
||||
error ->
|
||||
Logger.warning("Micropub: Error while handling update: #{inspect(error)}")
|
||||
send_error(conn, {:error, :invalid_request})
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_action(:delete, access_token, conn) do
|
||||
Logger.info("Micropub: Handle delete")
|
||||
|
||||
case Map.fetch(conn.body_params, "url") do
|
||||
{:ok, url} ->
|
||||
do_delete(conn, access_token, url)
|
||||
|
||||
_ ->
|
||||
send_error(conn, {:error, :invalid_request})
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_action(:undelete, access_token, conn) do
|
||||
Logger.info("Micropub: Handle undelete")
|
||||
|
||||
case Map.fetch(conn.body_params, "url") do
|
||||
{:ok, url} ->
|
||||
do_undelete(conn, access_token, url)
|
||||
|
||||
_ ->
|
||||
send_error(conn, {:error, :invalid_request})
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_query(:config, access_token, conn) do
|
||||
Logger.info("Micropub: Handle config query")
|
||||
|
||||
handler = conn.private[:plug_micropub][:handler]
|
||||
|
||||
case handler.handle_config_query(access_token) do
|
||||
{:ok, content} ->
|
||||
send_content(conn, content)
|
||||
|
||||
error ->
|
||||
Logger.warning("Micropub: Error while handling config: #{inspect(error)}")
|
||||
send_error(conn, error)
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_query(:category, access_token, conn) do
|
||||
Logger.info("Micropub: Handle category query")
|
||||
|
||||
handler = conn.private[:plug_micropub][:handler]
|
||||
|
||||
case handler.handle_category_query(access_token) do
|
||||
{:ok, content} ->
|
||||
send_content(conn, content)
|
||||
|
||||
error ->
|
||||
Logger.warning("Micropub: Error while handling category: #{inspect(error)}")
|
||||
send_error(conn, error)
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_query(:source, access_token, conn) do
|
||||
Logger.info("Micropub: Handle source query")
|
||||
|
||||
case Map.fetch(conn.query_params, "url") do
|
||||
{:ok, url} ->
|
||||
do_source_query(conn, access_token, url)
|
||||
|
||||
error ->
|
||||
Logger.warning("Micropub: Error while handling source: #{inspect(error)}")
|
||||
send_error(conn, {:error, :invalid_request})
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_query(:"syndicate-to", access_token, conn) do
|
||||
Logger.info("Micropub: Handle syndicate-to query")
|
||||
|
||||
handler = conn.private[:plug_micropub][:handler]
|
||||
|
||||
case handler.handle_syndicate_to_query(access_token) do
|
||||
{:ok, content} ->
|
||||
send_content(conn, content)
|
||||
|
||||
error ->
|
||||
Logger.warning("Micropub: Error while handling syndicate-to: #{inspect(error)}")
|
||||
send_error(conn, error)
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_query(:channel, access_token, conn) do
|
||||
Logger.info("Micropub: Handle channel query")
|
||||
|
||||
handler = conn.private[:plug_micropub][:handler]
|
||||
|
||||
case handler.handle_channel_query(access_token) do
|
||||
{:ok, content} ->
|
||||
send_content(conn, content)
|
||||
|
||||
error ->
|
||||
Logger.warning("Micropub: Error while handling channel: #{inspect(error)}")
|
||||
send_error(conn, error)
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_update_properties(properties) do
|
||||
properties = Map.take(properties, ["replace", "add", "delete"])
|
||||
|
||||
valid? =
|
||||
Enum.all?(properties, fn
|
||||
{"delete", prop} when is_list(prop) ->
|
||||
Enum.all?(prop, &is_binary/1)
|
||||
|
||||
{_k, prop} when is_map(prop) ->
|
||||
Logger.debug("Micropub: Parsing Add/Replace maps")
|
||||
|
||||
Enum.all?(prop, fn
|
||||
{_k, v} when is_list(v) ->
|
||||
true
|
||||
|
||||
_ ->
|
||||
Logger.warning("Micropub: Property value of #{prop} is not a list")
|
||||
false
|
||||
end)
|
||||
|
||||
_ ->
|
||||
false
|
||||
end)
|
||||
|
||||
Logger.info("Valid check successful: #{valid?}")
|
||||
|
||||
if valid? do
|
||||
replace = Map.get(properties, "replace", %{})
|
||||
add = Map.get(properties, "add", %{})
|
||||
delete = Map.get(properties, "delete", %{})
|
||||
{:ok, replace, add, delete}
|
||||
else
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
defp do_update(conn, access_token, url, replace, add, delete) do
|
||||
handler = conn.private[:plug_micropub][:handler]
|
||||
|
||||
case handler.handle_update(url, replace, add, delete, access_token) do
|
||||
:ok ->
|
||||
send_resp(conn, :no_content, "")
|
||||
|
||||
{:ok, url} ->
|
||||
conn
|
||||
|> put_resp_header("location", url)
|
||||
|> send_resp(:created, "")
|
||||
|
||||
error ->
|
||||
send_error(conn, error)
|
||||
end
|
||||
end
|
||||
|
||||
defp do_delete(conn, access_token, url) do
|
||||
handler = conn.private[:plug_micropub][:handler]
|
||||
|
||||
case handler.handle_delete(url, access_token) do
|
||||
:ok -> send_resp(conn, :no_content, "")
|
||||
error -> send_error(conn, error)
|
||||
end
|
||||
end
|
||||
|
||||
defp do_undelete(conn, access_token, url) do
|
||||
handler = conn.private[:plug_micropub][:handler]
|
||||
|
||||
case handler.handle_undelete(url, access_token) do
|
||||
:ok ->
|
||||
send_resp(conn, :no_content, "")
|
||||
|
||||
{:ok, url} ->
|
||||
conn
|
||||
|> put_resp_header("location", url)
|
||||
|> send_resp(:created, "")
|
||||
|
||||
error ->
|
||||
send_error(conn, error)
|
||||
end
|
||||
end
|
||||
|
||||
defp do_source_query(conn, access_token, url) do
|
||||
handler = conn.private[:plug_micropub][:handler]
|
||||
properties = Map.get(conn.query_params, "properties", [])
|
||||
|
||||
case handler.handle_source_query(url, properties, access_token) do
|
||||
{:ok, content} -> send_content(conn, content)
|
||||
error -> send_error(conn, error)
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_create_body("application/json", params) do
|
||||
with {:ok, ["h-" <> type]} <- Map.fetch(params, "type"),
|
||||
{:ok, properties} when is_map(properties) <- Map.fetch(params, "properties") do
|
||||
properties = Map.new(properties)
|
||||
|
||||
Logger.info("Micropub: Parsed properties #{inspect(properties)}")
|
||||
{:ok, type, properties}
|
||||
else
|
||||
_ -> {:error, :invalid_request}
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_create_body(_, params) do
|
||||
with {type, params} when is_binary(type) <- Map.pop(params, "h") do
|
||||
properties =
|
||||
params
|
||||
|> Enum.map(fn {k, v} -> {k, List.wrap(v)} end)
|
||||
|> Map.new()
|
||||
|
||||
Logger.info("Micropub: Parsed properties #{inspect(properties)}")
|
||||
{:ok, type, properties}
|
||||
else
|
||||
_ -> {:error, :invalid_request}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,61 +0,0 @@
|
|||
defmodule ChiyaWeb.Plugs.PlugMicropub.HandlerBehaviour do
|
||||
@moduledoc """
|
||||
Behaviour defining the interface for a PlugMicropub Handler
|
||||
"""
|
||||
|
||||
@type access_token :: String.t()
|
||||
@type handler_error_atom :: :invalid_request | :forbidden | :insufficient_scope
|
||||
@type handler_error ::
|
||||
{:error, handler_error_atom} | {:error, handler_error_atom, description :: String.t()}
|
||||
|
||||
@callback handle_create(type :: String.t(), properties :: map, access_token) ::
|
||||
{:ok, :created | :accepted, url :: String.t()}
|
||||
| handler_error
|
||||
|
||||
@callback handle_update(
|
||||
url :: String.t(),
|
||||
replace :: map,
|
||||
add :: map,
|
||||
delete :: map,
|
||||
access_token
|
||||
) ::
|
||||
:ok
|
||||
| {:ok, url :: String.t()}
|
||||
| handler_error
|
||||
|
||||
@callback handle_delete(url :: String.t(), access_token) ::
|
||||
:ok
|
||||
| handler_error
|
||||
|
||||
@callback handle_undelete(url :: String.t(), access_token) ::
|
||||
:ok
|
||||
| {:ok, url :: String.t()}
|
||||
| handler_error
|
||||
|
||||
@callback handle_config_query(access_token) ::
|
||||
{:ok, map}
|
||||
| handler_error
|
||||
|
||||
@callback handle_channel_query(access_token) ::
|
||||
{:ok, map}
|
||||
| handler_error
|
||||
|
||||
@callback handle_category_query(access_token) ::
|
||||
{:ok, map}
|
||||
| handler_error
|
||||
|
||||
@callback handle_syndicate_to_query(access_token) ::
|
||||
{:ok, map}
|
||||
| handler_error
|
||||
|
||||
@callback handle_source_query(
|
||||
url :: String.t(),
|
||||
properties :: [String.t()],
|
||||
access_token
|
||||
) ::
|
||||
{:ok, map}
|
||||
| handler_error
|
||||
|
||||
@callback handle_media(file :: Plug.Upload.t(), access_token) ::
|
||||
{:ok, url :: String.t()} | handler_error
|
||||
end
|
|
@ -76,7 +76,7 @@ defmodule ChiyaWeb.References do
|
|||
end
|
||||
|
||||
defp get_link_class(valid) do
|
||||
if valid, do: "", else: "{:.invalid}"
|
||||
if not valid, do: "{:.invalid}", else: ""
|
||||
end
|
||||
|
||||
defp map_to_tuple([placeholder, note_slug]),
|
||||
|
|
|
@ -40,7 +40,7 @@ defmodule ChiyaWeb.Router do
|
|||
## Indie routes
|
||||
scope "/indie" do
|
||||
forward "/micropub",
|
||||
ChiyaWeb.Plugs.PlugMicropub,
|
||||
PlugMicropub,
|
||||
handler: ChiyaWeb.Indie.MicropubHandler,
|
||||
json_encoder: Jason
|
||||
end
|
||||
|
@ -70,23 +70,16 @@ defmodule ChiyaWeb.Router do
|
|||
live "/", AdminHomeLive, :index
|
||||
|
||||
resources "/channels", ChannelController
|
||||
resources "/notes", NoteController, except: [:show, :index, :edit]
|
||||
resources "/notes", NoteController, except: [:show]
|
||||
resources "/settings", SettingController, singleton: true
|
||||
resources "/identities", IdentityController
|
||||
resources "/comments", CommentController, only: [:index, :show]
|
||||
resources "/tokens", TokenController, only: [:index, :show, :new, :create, :delete]
|
||||
resources "/tags", TagController, only: [:index, :edit, :update, :show]
|
||||
|
||||
get "/tags/:id/apply", TagController, :apply_prepare
|
||||
get "/tags/:id/apply/run", TagController, :apply_run
|
||||
|
||||
get "/notes/import", NoteController, :import_prepare
|
||||
post "/notes/import", NoteController, :import_run
|
||||
|
||||
live "/notes", NoteListLive, :index
|
||||
live "/notes/:id", NoteShowLive, :show
|
||||
live "/notes/:id/edit", NoteEditLive, :edit
|
||||
|
||||
get "/notes/:id/raw", NoteController, :raw
|
||||
get "/notes/:id/publish", NoteController, :publish
|
||||
get "/notes/:id/unpublish", NoteController, :unpublish
|
||||
|
|
6
mix.exs
6
mix.exs
|
@ -35,27 +35,23 @@ defmodule Chiya.MixProject do
|
|||
{:bcrypt_elixir, "~> 3.0"},
|
||||
{:cors_plug, "~> 3.0"},
|
||||
{:coverex, "~> 1.5", only: :test},
|
||||
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
|
||||
{:earmark, "~> 1.4"},
|
||||
{:ecto_autoslug_field, "~> 3.0"},
|
||||
{:ecto_sql, "~> 3.6"},
|
||||
{:finch, "~> 0.16"},
|
||||
{:floki, ">= 0.30.0", only: :test},
|
||||
{:flop, "~> 0.22.1"},
|
||||
{:flop_phoenix, "~> 0.21.1"},
|
||||
{:gettext, "~> 0.23"},
|
||||
{:jason, "~> 1.2"},
|
||||
{:oban, "~> 2.14"},
|
||||
{:phoenix, "~> 1.7.1"},
|
||||
{:phoenix_active_link, "~> 0.3.2"},
|
||||
{:phoenix_ecto, "~> 4.4"},
|
||||
{:phoenix_html, "~> 3.3"},
|
||||
{:phoenix_live_dashboard, "~> 0.8.0"},
|
||||
{:phoenix_live_reload, "~> 1.2", only: :dev},
|
||||
{:phoenix_live_view, "~> 0.19"},
|
||||
{:plug_cowboy, "~> 2.5"},
|
||||
{:plug_micropub, git: "https://git.inhji.de/inhji/plug_micropub.git", branch: "chiya"},
|
||||
{:postgrex, ">= 0.0.0"},
|
||||
{:slugger, "~> 0.3"},
|
||||
{:swoosh, "~> 1.11"},
|
||||
{:tailwind, "~> 0.2.0", runtime: Mix.env() == :dev},
|
||||
{:telemetry_metrics, "~> 0.6"},
|
||||
|
|
42
mix.lock
42
mix.lock
|
@ -1,31 +1,27 @@
|
|||
%{
|
||||
"bcrypt_elixir": {:hex, :bcrypt_elixir, "3.1.0", "0b110a9a6c619b19a7f73fa3004aa11d6e719a67e672d1633dc36b6b2290a0f7", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "2ad2acb5a8bc049e8d5aa267802631912bb80d5f4110a178ae7999e69dca1bf7"},
|
||||
"bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"},
|
||||
"bcrypt_elixir": {:hex, :bcrypt_elixir, "3.0.1", "9be815469e6bfefec40fa74658ecbbe6897acfb57614df1416eeccd4903f602c", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "486bb95efb645d1efc6794c1ddd776a186a9a713abf06f45708a6ce324fb96cf"},
|
||||
"castore": {:hex, :castore, "1.0.3", "7130ba6d24c8424014194676d608cb989f62ef8039efd50ff4b3f33286d06db8", [:mix], [], "hexpm", "680ab01ef5d15b161ed6a95449fac5c6b8f60055677a8e79acf01b27baa4390b"},
|
||||
"certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"},
|
||||
"comeonin": {:hex, :comeonin, "5.4.0", "246a56ca3f41d404380fc6465650ddaa532c7f98be4bda1b4656b3a37cc13abe", [:mix], [], "hexpm", "796393a9e50d01999d56b7b8420ab0481a7538d0caf80919da493b4a6e51faf1"},
|
||||
"certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"},
|
||||
"comeonin": {:hex, :comeonin, "5.3.3", "2c564dac95a35650e9b6acfe6d2952083d8a08e4a89b93a481acb552b325892e", [:mix], [], "hexpm", "3e38c9c2cb080828116597ca8807bb482618a315bfafd98c90bc22a821cc84df"},
|
||||
"cors_plug": {:hex, :cors_plug, "3.0.3", "7c3ac52b39624bc616db2e937c282f3f623f25f8d550068b6710e58d04a0e330", [:mix], [{:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "3f2d759e8c272ed3835fab2ef11b46bddab8c1ab9528167bd463b6452edf830d"},
|
||||
"coverex": {:hex, :coverex, "1.5.0", "a4248302f09562993041f1b056866bfd5688d3a03c429de80c47ea6663989ecc", [:mix], [{:hackney, "~> 1.5", [hex: :hackney, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm", "21a8f6e734a277b86c01b3cc44b7cc8b77cb1886adbebb708eefc6398567dcac"},
|
||||
"cowboy": {:hex, :cowboy, "2.10.0", "ff9ffeff91dae4ae270dd975642997afe2a1179d94b1887863e43f681a203e26", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "3afdccb7183cc6f143cb14d3cf51fa00e53db9ec80cdcd525482f5e99bc41d6b"},
|
||||
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
|
||||
"cowlib": {:hex, :cowlib, "2.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"},
|
||||
"credo": {:hex, :credo, "1.7.0", "6119bee47272e85995598ee04f2ebbed3e947678dee048d10b5feca139435f75", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "6839fcf63d1f0d1c0f450abc8564a57c43d644077ab96f2934563e68b8a769d7"},
|
||||
"db_connection": {:hex, :db_connection, "2.5.0", "bb6d4f30d35ded97b29fe80d8bd6f928a1912ca1ff110831edcd238a1973652c", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c92d5ba26cd69ead1ff7582dbb860adeedfff39774105a4f1c92cbb654b55aa2"},
|
||||
"decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
|
||||
"earmark": {:hex, :earmark, "1.4.40", "ff1a0f8bf3b298113c2a257c4e7a8b29ba9db5d35f5ee6d29291cb8caa09a071", [:mix], [{:earmark_parser, "~> 1.4.35", [hex: :earmark_parser, repo: "hexpm", optional: false]}], "hexpm", "5fb622d5e36046bc313a426211e8bf769ba50db7720744859a21932c6470d75c"},
|
||||
"earmark_parser": {:hex, :earmark_parser, "1.4.35", "437773ca9384edf69830e26e9e7b2e0d22d2596c4a6b17094a3b29f01ea65bb8", [:mix], [], "hexpm", "8652ba3cb85608d0d7aa2d21b45c6fad4ddc9a1f9a1f1b30ca3a246f0acc33f6"},
|
||||
"earmark": {:hex, :earmark, "1.4.39", "acdb2f02c536471029dbcc509fbd6b94b89f40ad7729fb3f68f4b6944843f01d", [:mix], [{:earmark_parser, "~> 1.4.33", [hex: :earmark_parser, repo: "hexpm", optional: false]}], "hexpm", "156c9d8ec3cbeccdbf26216d8247bdeeacc8c76b4d9eee7554be2f1b623ea440"},
|
||||
"earmark_parser": {:hex, :earmark_parser, "1.4.33", "3c3fd9673bb5dcc9edc28dd90f50c87ce506d1f71b70e3de69aa8154bc695d44", [:mix], [], "hexpm", "2d526833729b59b9fdb85785078697c72ac5e5066350663e5be6a1182da61b8f"},
|
||||
"ecto": {:hex, :ecto, "3.10.3", "eb2ae2eecd210b4eb8bece1217b297ad4ff824b4384c0e3fdd28aaf96edd6135", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "44bec74e2364d491d70f7e42cd0d690922659d329f6465e89feb8a34e8cd3433"},
|
||||
"ecto_autoslug_field": {:hex, :ecto_autoslug_field, "3.1.0", "ddf26e814baf3c32c6aebfed56a637f10a097db83f65d71e6f2d1e7faf2e9e51", [:mix], [{:ecto, ">= 3.7.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:slugify, "~> 1.3", [hex: :slugify, repo: "hexpm", optional: false]}], "hexpm", "b6ddd614805263e24b5c169532c934440d0289181cce873061fca3a8e92fd9ff"},
|
||||
"ecto_sql": {:hex, :ecto_sql, "3.10.2", "6b98b46534b5c2f8b8b5f03f126e75e2a73c64f3c071149d32987a5378b0fdbd", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.10.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "68c018debca57cb9235e3889affdaec7a10616a4e3a80c99fa1d01fdafaa9007"},
|
||||
"ecto_autoslug_field": {:hex, :ecto_autoslug_field, "3.0.0", "37fbc2f07e6691136afff246f2cf5b159ad395b665a55d06db918975fd2397db", [:mix], [{:ecto, ">= 3.7.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:slugger, ">= 0.3.0", [hex: :slugger, repo: "hexpm", optional: false]}], "hexpm", "8ec252c7cf85f13132062f56a484d6a0ef1f981f7be9ce4ad7e9546dd8c0cc0f"},
|
||||
"ecto_sql": {:hex, :ecto_sql, "3.10.1", "6ea6b3036a0b0ca94c2a02613fd9f742614b5cfe494c41af2e6571bb034dd94c", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.10.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f6a25bdbbd695f12c8171eaff0851fa4c8e72eec1e98c7364402dda9ce11c56b"},
|
||||
"elixir_make": {:hex, :elixir_make, "0.7.7", "7128c60c2476019ed978210c245badf08b03dbec4f24d05790ef791da11aa17c", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "5bc19fff950fad52bbe5f211b12db9ec82c6b34a9647da0c2224b8b8464c7e6c"},
|
||||
"expo": {:hex, :expo, "0.4.1", "1c61d18a5df197dfda38861673d392e642649a9cef7694d2f97a587b2cfb319b", [:mix], [], "hexpm", "2ff7ba7a798c8c543c12550fa0e2cbc81b95d4974c65855d8d15ba7b37a1ce47"},
|
||||
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
|
||||
"finch": {:hex, :finch, "0.16.0", "40733f02c89f94a112518071c0a91fe86069560f5dbdb39f9150042f44dcfb1a", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f660174c4d519e5fec629016054d60edd822cdfe2b7270836739ac2f97735ec5"},
|
||||
"floki": {:hex, :floki, "0.34.3", "5e2dcaec5d7c228ce5b1d3501502e308b2d79eb655e4191751a1fe491c37feac", [:mix], [], "hexpm", "9577440eea5b97924b4bf3c7ea55f7b8b6dce589f9b28b096cc294a8dc342341"},
|
||||
"flop": {:hex, :flop, "0.22.1", "4eb5dc1c159845e31d33cd8e73e249b84d013d3b9b1fb17619e70d55fbd2b025", [:mix], [{:ecto, "~> 3.10.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:nimble_options, "~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}], "hexpm", "2f212238d92a8fcc2adadf20dcd875c6f8b4e1ce38ca6bc3f805918a5be567e4"},
|
||||
"flop_phoenix": {:hex, :flop_phoenix, "0.21.1", "7b7c143e751f985a94624461a25916d1c604a8a3913253f9a38ae619577853d7", [:mix], [{:flop, "~> 0.22.0", [hex: :flop, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.6.0 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "c44ccdda6d8b8dba4b7c885b7173150dc7388e1f5d63bd834d136fbd538f7b4b"},
|
||||
"gettext": {:hex, :gettext, "0.23.1", "821e619a240e6000db2fc16a574ef68b3bd7fe0167ccc264a81563cc93e67a31", [:mix], [{:expo, "~> 0.4.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "19d744a36b809d810d610b57c27b934425859d158ebd56561bc41f7eeb8795db"},
|
||||
"hackney": {:hex, :hackney, "1.18.2", "d7ff544ddae5e1cb49e9cf7fa4e356d7f41b283989a1c304bfc47a8cc1cf966f", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "af94d5c9f97857db257090a4a10e5426ecb6f4918aa5cc666798566ae14b65fd"},
|
||||
"gettext": {:hex, :gettext, "0.23.0", "8065a6eb5a6c94ef3909b23178ffc064cd43a51d977b6c80babc8bb0c5ebfd6f", [:mix], [{:expo, "~> 0.4.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "09ca27284fe5a82449eb252f2877e551a46b1c9c7d10d72d087211f0bfed9638"},
|
||||
"hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~> 2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"},
|
||||
"hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"},
|
||||
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
|
||||
"jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"},
|
||||
|
@ -35,13 +31,12 @@
|
|||
"mint": {:hex, :mint, "1.5.1", "8db5239e56738552d85af398798c80648db0e90f343c8469f6c6d8898944fb6f", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "4a63e1e76a7c3956abd2c72f370a0d0aecddc3976dea5c27eccbecfa5e7d5b1e"},
|
||||
"nimble_options": {:hex, :nimble_options, "1.0.2", "92098a74df0072ff37d0c12ace58574d26880e522c22801437151a159392270e", [:mix], [], "hexpm", "fd12a8db2021036ce12a309f26f564ec367373265b53e25403f0ee697380f1b8"},
|
||||
"nimble_pool": {:hex, :nimble_pool, "1.0.0", "5eb82705d138f4dd4423f69ceb19ac667b3b492ae570c9f5c900bb3d2f50a847", [:mix], [], "hexpm", "80be3b882d2d351882256087078e1b1952a28bf98d0a287be87e4a24a710b67a"},
|
||||
"oban": {:hex, :oban, "2.15.4", "d49ab4ffb7153010e32f80fe9e56f592706238149ec579eb50f8a4e41d218856", [:mix], [{:ecto_sql, "~> 3.6", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5fce611fdfffb13e9148df883116e5201adf1e731eb302cc88cde0588510079c"},
|
||||
"parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
|
||||
"oban": {:hex, :oban, "2.15.2", "8f934a49db39163633965139c8846d8e24c2beb4180f34a005c2c7c3f69a6aa2", [:mix], [{:ecto_sql, "~> 3.6", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "0f4a579ea48fc7489e0d84facf8b01566e142bdc6542d7dabce32c10e664f1e9"},
|
||||
"parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"},
|
||||
"phoenix": {:hex, :phoenix, "1.7.7", "4cc501d4d823015007ba3cdd9c41ecaaf2ffb619d6fb283199fa8ddba89191e0", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "8966e15c395e5e37591b6ed0bd2ae7f48e961f0f60ac4c733f9566b519453085"},
|
||||
"phoenix_active_link": {:hex, :phoenix_active_link, "0.3.2", "946d013b0839341d85ac4259fadf944d9767186e424a78ef37d310beac92d840", [:mix], [{:phoenix_html, "~> 2.10 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "81fcd4333541d6f1586e1d5fd0c34efed9bc4129573862bea340e234b6f22aeb"},
|
||||
"phoenix_ecto": {:hex, :phoenix_ecto, "4.4.2", "b21bd01fdeffcfe2fab49e4942aa938b6d3e89e93a480d4aee58085560a0bc0d", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "70242edd4601d50b69273b057ecf7b684644c19ee750989fd555625ae4ce8f5d"},
|
||||
"phoenix_html": {:hex, :phoenix_html, "3.3.2", "d6ce982c6d8247d2fc0defe625255c721fb8d5f1942c5ac051f6177bffa5973f", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "44adaf8e667c1c20fb9d284b6b0fa8dc7946ce29e81ce621860aa7e96de9a11d"},
|
||||
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.1", "c4f2a2d3b26e6ca684d162ccf18aaeed8bed2181896e0393d0a2959789482e51", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "1ca0f954274ce1916f771f86b3d49a91d3447e7c32d171660676095c5f30abe9"},
|
||||
"phoenix_html": {:hex, :phoenix_html, "3.3.1", "4788757e804a30baac6b3fc9695bf5562465dd3f1da8eb8460ad5b404d9a2178", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "bed1906edd4906a15fd7b412b85b05e521e1f67c9a85418c55999277e553d0d3"},
|
||||
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.0", "0b3158b5b198aa444473c91d23d79f52fb077e807ffad80dacf88ce078fa8df2", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "87785a54474fed91a67a1227a741097eb1a42c2e49d3c0d098b588af65cd410d"},
|
||||
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.4.1", "2aff698f5e47369decde4357ba91fc9c37c6487a512b41732818f2204a8ef1d3", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "9bffb834e7ddf08467fe54ae58b5785507aaba6255568ae22b4d46e2bb3615ab"},
|
||||
"phoenix_live_view": {:hex, :phoenix_live_view, "0.19.5", "6e730595e8e9b8c5da230a814e557768828fd8dfeeb90377d2d8dbb52d4ec00a", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b2eaa0dd3cfb9bd7fb949b88217df9f25aed915e986a28ad5c8a0d054e7ca9d3"},
|
||||
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"},
|
||||
|
@ -51,14 +46,11 @@
|
|||
"plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"},
|
||||
"plug_micropub": {:git, "https://git.inhji.de/inhji/plug_micropub.git", "1845a4067703fc656fec34b8732b9544967893c0", [branch: "chiya"]},
|
||||
"poison": {:hex, :poison, "4.0.1", "bcb755a16fac91cad79bfe9fc3585bb07b9331e50cfe3420a24bcc2d735709ae", [:mix], [], "hexpm", "ba8836feea4b394bb718a161fc59a288fe0109b5006d6bdf97b6badfcf6f0f25"},
|
||||
"postgrex": {:hex, :postgrex, "0.17.3", "c92cda8de2033a7585dae8c61b1d420a1a1322421df84da9a82a6764580c503d", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "946cf46935a4fdca7a81448be76ba3503cff082df42c6ec1ff16a4bdfbfb098d"},
|
||||
"postgrex": {:hex, :postgrex, "0.17.2", "a3ec9e3239d9b33f1e5841565c4eb200055c52cc0757a22b63ca2d529bbe764c", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "80a918a9e9531d39f7bd70621422f3ebc93c01618c645f2d91306f50041ed90c"},
|
||||
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
|
||||
"scrivener": {:hex, :scrivener, "2.7.2", "1d913c965ec352650a7f864ad7fd8d80462f76a32f33d57d1e48bc5e9d40aba2", [:mix], [], "hexpm", "7866a0ec4d40274efbee1db8bead13a995ea4926ecd8203345af8f90d2b620d9"},
|
||||
"scrivener_html": {:git, "https://github.com/workshops-de/scrivener_html", "80752fe8fcb7cd616adf6b5e78c226f158013ddf", []},
|
||||
"slugger": {:hex, :slugger, "0.3.0", "efc667ab99eee19a48913ccf3d038b1fb9f165fa4fbf093be898b8099e61b6ed", [:mix], [], "hexpm", "20d0ded0e712605d1eae6c5b4889581c3460d92623a930ddda91e0e609b5afba"},
|
||||
"slugify": {:hex, :slugify, "1.3.1", "0d3b8b7e5c1eeaa960e44dce94382bee34a39b3ea239293e457a9c5b47cc6fd3", [:mix], [], "hexpm", "cb090bbeb056b312da3125e681d98933a360a70d327820e4b7f91645c4d8be76"},
|
||||
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
|
||||
"swoosh": {:hex, :swoosh, "1.11.6", "7093531c12a537839c7b0777276aaad9f38a887e61c1d517f3b6a8dc6e9040d9", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7fbb7bd0b674e344dbd3a15c33ca787357663ef534dde2cc5ee74d00f7427fd5"},
|
||||
"swoosh": {:hex, :swoosh, "1.11.4", "9b353f998cba3c5e101a0669559c2fb2757b5d9eb7db058bf08687d82e93e416", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d3390914022a456ae1604bfcb3431bd12509b2afe8c70296bae6c9dca4903d0f"},
|
||||
"tailwind": {:hex, :tailwind, "0.2.1", "83d8eadbe71a8e8f67861fe7f8d51658ecfb258387123afe4d9dc194eddc36b0", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "e8a13f6107c95f73e58ed1b4221744e1eb5a093cd1da244432067e19c8c9a277"},
|
||||
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
|
||||
"telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"},
|
||||
|
@ -68,8 +60,8 @@
|
|||
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
|
||||
"waffle": {:hex, :waffle, "1.1.7", "518f9bdda7b9b3d0958ad6ab16066631ce028f5f12217382822a428895fc4be3", [:mix], [{:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:ex_aws_s3, "~> 2.1", [hex: :ex_aws_s3, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: false]}, {:sweet_xml, "~> 0.6", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "e97e7b10b7f380687b5dc5e65b391538a802eff636605ad183e0bed29b45b0ef"},
|
||||
"waffle_ecto": {:hex, :waffle_ecto, "0.0.12", "e5c17c49b071b903df71861c642093281123142dc4e9908c930db3e06795b040", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:waffle, "~> 1.0", [hex: :waffle, repo: "hexpm", optional: false]}], "hexpm", "585fe6371057066d2e8e3383ddd7a2437ff0668caf3f4cbf5a041e0de9837168"},
|
||||
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
|
||||
"websock_adapter": {:hex, :websock_adapter, "0.5.4", "7af8408e7ed9d56578539594d1ee7d8461e2dd5c3f57b0f2a5352d610ddde757", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "d2c238c79c52cbe223fcdae22ca0bb5007a735b9e933870e241fce66afb4f4ab"},
|
||||
"websock": {:hex, :websock, "0.5.2", "b3c08511d8d79ed2c2f589ff430bd1fe799bb389686dafce86d28801783d8351", [:mix], [], "hexpm", "925f5de22fca6813dfa980fb62fd542ec43a2d1a1f83d2caec907483fe66ff05"},
|
||||
"websock_adapter": {:hex, :websock_adapter, "0.5.3", "4908718e42e4a548fc20e00e70848620a92f11f7a6add8cf0886c4232267498d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "cbe5b814c1f86b6ea002b52dd99f345aeecf1a1a6964e209d208fb404d930d3d"},
|
||||
"yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"},
|
||||
"yaml_elixir": {:hex, :yaml_elixir, "2.9.0", "9a256da867b37b8d2c1ffd5d9de373a4fda77a32a45b452f1708508ba7bbcb53", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "0cb0e7d4c56f5e99a6253ed1a670ed0e39c13fc45a6da054033928607ac08dfc"},
|
||||
"yaml_front_matter": {:hex, :yaml_front_matter, "1.0.0", "9f3310906a9647d04fc4715b6bac5ed177defc9625c53b93430d36d8fd0961db", [:mix], [{:yaml_elixir, "~> 2.0", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "73cf825c18b7a14bfd8fb5856fa4887196035be69d2d45d1c370917aae0572ec"},
|
||||
|
|
|
@ -17,3 +17,4 @@ MIX_ENV=prod mix assets.deploy
|
|||
|
||||
echo "Generating release.."
|
||||
MIX_ENV=prod mix release --overwrite
|
||||
|
||||
|
|
|
@ -33,20 +33,11 @@ defmodule ChiyaWeb.MicropubTest do
|
|||
assert note.content == "replaced content"
|
||||
end
|
||||
|
||||
test "updates a note by replacing name" do
|
||||
note = note_fixture()
|
||||
|
||||
assert {:ok, %Note{} = note} =
|
||||
Micropub.update_note(note, %{"name" => ["replaced name"]}, %{}, %{})
|
||||
|
||||
assert note.name == "replaced name"
|
||||
end
|
||||
|
||||
test "updates a note by adding categories" do
|
||||
note = note_fixture()
|
||||
|
||||
assert {:ok, %Note{} = note} =
|
||||
Micropub.update_note(note, %{"category" => ["foo", "bar"]}, %{}, %{})
|
||||
Micropub.update_note(note, %{}, %{"category" => ["foo", "bar"]}, %{})
|
||||
|
||||
assert Enum.empty?(note.tags) == false
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue