diff --git a/resources/public/css/output.css b/resources/public/css/output.css new file mode 100644 index 0000000..b67cdfb --- /dev/null +++ b/resources/public/css/output.css @@ -0,0 +1,1487 @@ +/* +! tailwindcss v3.4.9 | MIT License | https://tailwindcss.com +*/ + +/* +1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) +2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) +*/ + +*, +::before, +::after { + box-sizing: border-box; + /* 1 */ + border-width: 0; + /* 2 */ + border-style: solid; + /* 2 */ + border-color: #e5e7eb; + /* 2 */ +} + +::before, +::after { + --tw-content: ''; +} + +/* +1. Use a consistent sensible line-height in all browsers. +2. Prevent adjustments of font size after orientation changes in iOS. +3. Use a more readable tab size. +4. Use the user's configured `sans` font-family by default. +5. Use the user's configured `sans` font-feature-settings by default. +6. Use the user's configured `sans` font-variation-settings by default. +7. Disable tap highlights on iOS +*/ + +html, +:host { + line-height: 1.5; + /* 1 */ + -webkit-text-size-adjust: 100%; + /* 2 */ + -moz-tab-size: 4; + /* 3 */ + -o-tab-size: 4; + tab-size: 4; + /* 3 */ + font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + /* 4 */ + font-feature-settings: normal; + /* 5 */ + font-variation-settings: normal; + /* 6 */ + -webkit-tap-highlight-color: transparent; + /* 7 */ +} + +/* +1. Remove the margin in all browsers. +2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. +*/ + +body { + margin: 0; + /* 1 */ + line-height: inherit; + /* 2 */ +} + +/* +1. Add the correct height in Firefox. +2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) +3. Ensure horizontal rules are visible by default. +*/ + +hr { + height: 0; + /* 1 */ + color: inherit; + /* 2 */ + border-top-width: 1px; + /* 3 */ +} + +/* +Add the correct text decoration in Chrome, Edge, and Safari. +*/ + +abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; +} + +/* +Remove the default font size and weight for headings. +*/ + +h1, +h2, +h3, +h4, +h5, +h6 { + font-size: inherit; + font-weight: inherit; +} + +/* +Reset links to optimize for opt-in styling instead of opt-out. +*/ + +a { + color: inherit; + text-decoration: inherit; +} + +/* +Add the correct font weight in Edge and Safari. +*/ + +b, +strong { + font-weight: bolder; +} + +/* +1. Use the user's configured `mono` font-family by default. +2. Use the user's configured `mono` font-feature-settings by default. +3. Use the user's configured `mono` font-variation-settings by default. +4. Correct the odd `em` font sizing in all browsers. +*/ + +code, +kbd, +samp, +pre { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + /* 1 */ + font-feature-settings: normal; + /* 2 */ + font-variation-settings: normal; + /* 3 */ + font-size: 1em; + /* 4 */ +} + +/* +Add the correct font size in all browsers. +*/ + +small { + font-size: 80%; +} + +/* +Prevent `sub` and `sup` elements from affecting the line height in all browsers. +*/ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* +1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) +2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) +3. Remove gaps between table borders by default. +*/ + +table { + text-indent: 0; + /* 1 */ + border-color: inherit; + /* 2 */ + border-collapse: collapse; + /* 3 */ +} + +/* +1. Change the font styles in all browsers. +2. Remove the margin in Firefox and Safari. +3. Remove default padding in all browsers. +*/ + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; + /* 1 */ + font-feature-settings: inherit; + /* 1 */ + font-variation-settings: inherit; + /* 1 */ + font-size: 100%; + /* 1 */ + font-weight: inherit; + /* 1 */ + line-height: inherit; + /* 1 */ + letter-spacing: inherit; + /* 1 */ + color: inherit; + /* 1 */ + margin: 0; + /* 2 */ + padding: 0; + /* 3 */ +} + +/* +Remove the inheritance of text transform in Edge and Firefox. +*/ + +button, +select { + text-transform: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Remove default button styles. +*/ + +button, +input:where([type='button']), +input:where([type='reset']), +input:where([type='submit']) { + -webkit-appearance: button; + /* 1 */ + background-color: transparent; + /* 2 */ + background-image: none; + /* 2 */ +} + +/* +Use the modern Firefox focus style for all focusable elements. +*/ + +:-moz-focusring { + outline: auto; +} + +/* +Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) +*/ + +:-moz-ui-invalid { + box-shadow: none; +} + +/* +Add the correct vertical alignment in Chrome and Firefox. +*/ + +progress { + vertical-align: baseline; +} + +/* +Correct the cursor style of increment and decrement buttons in Safari. +*/ + +::-webkit-inner-spin-button, +::-webkit-outer-spin-button { + height: auto; +} + +/* +1. Correct the odd appearance in Chrome and Safari. +2. Correct the outline style in Safari. +*/ + +[type='search'] { + -webkit-appearance: textfield; + /* 1 */ + outline-offset: -2px; + /* 2 */ +} + +/* +Remove the inner padding in Chrome and Safari on macOS. +*/ + +::-webkit-search-decoration { + -webkit-appearance: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Change font properties to `inherit` in Safari. +*/ + +::-webkit-file-upload-button { + -webkit-appearance: button; + /* 1 */ + font: inherit; + /* 2 */ +} + +/* +Add the correct display in Chrome and Safari. +*/ + +summary { + display: list-item; +} + +/* +Removes the default spacing and border for appropriate elements. +*/ + +blockquote, +dl, +dd, +h1, +h2, +h3, +h4, +h5, +h6, +hr, +figure, +p, +pre { + margin: 0; +} + +fieldset { + margin: 0; + padding: 0; +} + +legend { + padding: 0; +} + +ol, +ul, +menu { + list-style: none; + margin: 0; + padding: 0; +} + +/* +Reset default styling for dialogs. +*/ + +dialog { + padding: 0; +} + +/* +Prevent resizing textareas horizontally by default. +*/ + +textarea { + resize: vertical; +} + +/* +1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) +2. Set the default placeholder color to the user's configured gray 400 color. +*/ + +input::-moz-placeholder, textarea::-moz-placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +input::placeholder, +textarea::placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +/* +Set the default cursor for buttons. +*/ + +button, +[role="button"] { + cursor: pointer; +} + +/* +Make sure disabled buttons don't get the pointer cursor. +*/ + +:disabled { + cursor: default; +} + +/* +1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) +2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) + This can trigger a poorly considered lint error in some tools but is included by design. +*/ + +img, +svg, +video, +canvas, +audio, +iframe, +embed, +object { + display: block; + /* 1 */ + vertical-align: middle; + /* 2 */ +} + +/* +Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) +*/ + +img, +video { + max-width: 100%; + height: auto; +} + +/* Make elements with the HTML hidden attribute stay hidden by default */ + +[hidden] { + display: none; +} + +:root, +[data-theme] { + background-color: var(--fallback-b1,oklch(var(--b1)/1)); + color: var(--fallback-bc,oklch(var(--bc)/1)); +} + +@supports not (color: oklch(0% 0 0)) { + :root { + color-scheme: light; + --fallback-p: #491eff; + --fallback-pc: #d4dbff; + --fallback-s: #ff41c7; + --fallback-sc: #fff9fc; + --fallback-a: #00cfbd; + --fallback-ac: #00100d; + --fallback-n: #2b3440; + --fallback-nc: #d7dde4; + --fallback-b1: #ffffff; + --fallback-b2: #e5e6e6; + --fallback-b3: #e5e6e6; + --fallback-bc: #1f2937; + --fallback-in: #00b3f0; + --fallback-inc: #000000; + --fallback-su: #00ca92; + --fallback-suc: #000000; + --fallback-wa: #ffc22d; + --fallback-wac: #000000; + --fallback-er: #ff6f70; + --fallback-erc: #000000; + } + + @media (prefers-color-scheme: dark) { + :root { + color-scheme: dark; + --fallback-p: #7582ff; + --fallback-pc: #050617; + --fallback-s: #ff71cf; + --fallback-sc: #190211; + --fallback-a: #00c7b5; + --fallback-ac: #000e0c; + --fallback-n: #2a323c; + --fallback-nc: #a6adbb; + --fallback-b1: #1d232a; + --fallback-b2: #191e24; + --fallback-b3: #15191e; + --fallback-bc: #a6adbb; + --fallback-in: #00b3f0; + --fallback-inc: #000000; + --fallback-su: #00ca92; + --fallback-suc: #000000; + --fallback-wa: #ffc22d; + --fallback-wac: #000000; + --fallback-er: #ff6f70; + --fallback-erc: #000000; + } + } +} + +html { + -webkit-tap-highlight-color: transparent; +} + +* { + scrollbar-color: color-mix(in oklch, currentColor 35%, transparent) transparent; +} + +*:hover { + scrollbar-color: color-mix(in oklch, currentColor 60%, transparent) transparent; +} + +:root { + color-scheme: light; + --in: 72.06% 0.191 231.6; + --su: 64.8% 0.150 160; + --wa: 84.71% 0.199 83.87; + --er: 71.76% 0.221 22.18; + --pc: 89.824% 0.06192 275.75; + --ac: 15.352% 0.0368 183.61; + --inc: 0% 0 0; + --suc: 0% 0 0; + --wac: 0% 0 0; + --erc: 0% 0 0; + --rounded-box: 1rem; + --rounded-btn: 0.5rem; + --rounded-badge: 1.9rem; + --animation-btn: 0.25s; + --animation-input: .2s; + --btn-focus-scale: 0.95; + --border-btn: 1px; + --tab-border: 1px; + --tab-radius: 0.5rem; + --p: 49.12% 0.3096 275.75; + --s: 69.71% 0.329 342.55; + --sc: 98.71% 0.0106 342.55; + --a: 76.76% 0.184 183.61; + --n: 32.1785% 0.02476 255.701624; + --nc: 89.4994% 0.011585 252.096176; + --b1: 100% 0 0; + --b2: 96.1151% 0 0; + --b3: 92.4169% 0.00108 197.137559; + --bc: 27.8078% 0.029596 256.847952; +} + +@media (prefers-color-scheme: dark) { + :root { + color-scheme: dark; + --in: 72.06% 0.191 231.6; + --su: 64.8% 0.150 160; + --wa: 84.71% 0.199 83.87; + --er: 71.76% 0.221 22.18; + --pc: 13.138% 0.0392 275.75; + --sc: 14.96% 0.052 342.55; + --ac: 14.902% 0.0334 183.61; + --inc: 0% 0 0; + --suc: 0% 0 0; + --wac: 0% 0 0; + --erc: 0% 0 0; + --rounded-box: 1rem; + --rounded-btn: 0.5rem; + --rounded-badge: 1.9rem; + --animation-btn: 0.25s; + --animation-input: .2s; + --btn-focus-scale: 0.95; + --border-btn: 1px; + --tab-border: 1px; + --tab-radius: 0.5rem; + --p: 65.69% 0.196 275.75; + --s: 74.8% 0.26 342.55; + --a: 74.51% 0.167 183.61; + --n: 31.3815% 0.021108 254.139175; + --nc: 74.6477% 0.0216 264.435964; + --b1: 25.3267% 0.015896 252.417568; + --b2: 23.2607% 0.013807 253.100675; + --b3: 21.1484% 0.01165 254.087939; + --bc: 74.6477% 0.0216 264.435964; + } +} + +[data-theme=light] { + color-scheme: light; + --in: 72.06% 0.191 231.6; + --su: 64.8% 0.150 160; + --wa: 84.71% 0.199 83.87; + --er: 71.76% 0.221 22.18; + --pc: 89.824% 0.06192 275.75; + --ac: 15.352% 0.0368 183.61; + --inc: 0% 0 0; + --suc: 0% 0 0; + --wac: 0% 0 0; + --erc: 0% 0 0; + --rounded-box: 1rem; + --rounded-btn: 0.5rem; + --rounded-badge: 1.9rem; + --animation-btn: 0.25s; + --animation-input: .2s; + --btn-focus-scale: 0.95; + --border-btn: 1px; + --tab-border: 1px; + --tab-radius: 0.5rem; + --p: 49.12% 0.3096 275.75; + --s: 69.71% 0.329 342.55; + --sc: 98.71% 0.0106 342.55; + --a: 76.76% 0.184 183.61; + --n: 32.1785% 0.02476 255.701624; + --nc: 89.4994% 0.011585 252.096176; + --b1: 100% 0 0; + --b2: 96.1151% 0 0; + --b3: 92.4169% 0.00108 197.137559; + --bc: 27.8078% 0.029596 256.847952; +} + +[data-theme=dark] { + color-scheme: dark; + --in: 72.06% 0.191 231.6; + --su: 64.8% 0.150 160; + --wa: 84.71% 0.199 83.87; + --er: 71.76% 0.221 22.18; + --pc: 13.138% 0.0392 275.75; + --sc: 14.96% 0.052 342.55; + --ac: 14.902% 0.0334 183.61; + --inc: 0% 0 0; + --suc: 0% 0 0; + --wac: 0% 0 0; + --erc: 0% 0 0; + --rounded-box: 1rem; + --rounded-btn: 0.5rem; + --rounded-badge: 1.9rem; + --animation-btn: 0.25s; + --animation-input: .2s; + --btn-focus-scale: 0.95; + --border-btn: 1px; + --tab-border: 1px; + --tab-radius: 0.5rem; + --p: 65.69% 0.196 275.75; + --s: 74.8% 0.26 342.55; + --a: 74.51% 0.167 183.61; + --n: 31.3815% 0.021108 254.139175; + --nc: 74.6477% 0.0216 264.435964; + --b1: 25.3267% 0.015896 252.417568; + --b2: 23.2607% 0.013807 253.100675; + --b3: 21.1484% 0.01165 254.087939; + --bc: 74.6477% 0.0216 264.435964; +} + +*, ::before, ::after { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; + --tw-contain-size: ; + --tw-contain-layout: ; + --tw-contain-paint: ; + --tw-contain-style: ; +} + +::backdrop { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; + --tw-contain-size: ; + --tw-contain-layout: ; + --tw-contain-paint: ; + --tw-contain-style: ; +} + +@media (hover:hover) { + .label a:hover { + --tw-text-opacity: 1; + color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); + } +} + +.btn { + display: inline-flex; + height: 3rem; + min-height: 3rem; + flex-shrink: 0; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + flex-wrap: wrap; + align-items: center; + justify-content: center; + border-radius: var(--rounded-btn, 0.5rem); + border-color: transparent; + border-color: oklch(var(--btn-color, var(--b2)) / var(--tw-border-opacity)); + padding-left: 1rem; + padding-right: 1rem; + text-align: center; + font-size: 0.875rem; + line-height: 1em; + gap: 0.5rem; + font-weight: 600; + text-decoration-line: none; + transition-duration: 200ms; + transition-timing-function: cubic-bezier(0, 0, 0.2, 1); + border-width: var(--border-btn, 1px); + transition-property: color, background-color, border-color, opacity, box-shadow, transform; + --tw-text-opacity: 1; + color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); + --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); + --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); + outline-color: var(--fallback-bc,oklch(var(--bc)/1)); + background-color: oklch(var(--btn-color, var(--b2)) / var(--tw-bg-opacity)); + --tw-bg-opacity: 1; + --tw-border-opacity: 1; +} + +.btn-disabled, + .btn[disabled], + .btn:disabled { + pointer-events: none; +} + +:where(.btn:is(input[type="checkbox"])), +:where(.btn:is(input[type="radio"])) { + width: auto; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; +} + +.btn:is(input[type="checkbox"]):after, +.btn:is(input[type="radio"]):after { + --tw-content: attr(aria-label); + content: var(--tw-content); +} + +.checkbox { + flex-shrink: 0; + --chkbg: var(--fallback-bc,oklch(var(--bc)/1)); + --chkfg: var(--fallback-b1,oklch(var(--b1)/1)); + height: 1.5rem; + width: 1.5rem; + cursor: pointer; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + border-radius: var(--rounded-btn, 0.5rem); + border-width: 1px; + border-color: var(--fallback-bc,oklch(var(--bc)/var(--tw-border-opacity))); + --tw-border-opacity: 0.2; +} + +@media (hover: hover) { + .btm-nav > *.disabled:hover, + .btm-nav > *[disabled]:hover { + pointer-events: none; + --tw-border-opacity: 0; + background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity))); + --tw-bg-opacity: 0.1; + color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); + --tw-text-opacity: 0.2; + } + + .btn:hover { + --tw-border-opacity: 1; + border-color: var(--fallback-b3,oklch(var(--b3)/var(--tw-border-opacity))); + --tw-bg-opacity: 1; + background-color: var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity))); + } + + @supports (color: color-mix(in oklab, black, black)) { + .btn:hover { + background-color: color-mix( + in oklab, + oklch(var(--btn-color, var(--b2)) / var(--tw-bg-opacity, 1)) 90%, + black + ); + border-color: color-mix( + in oklab, + oklch(var(--btn-color, var(--b2)) / var(--tw-border-opacity, 1)) 90%, + black + ); + } + } + + @supports not (color: oklch(0% 0 0)) { + .btn:hover { + background-color: var(--btn-color, var(--fallback-b2)); + border-color: var(--btn-color, var(--fallback-b2)); + } + } + + .btn.glass:hover { + --glass-opacity: 25%; + --glass-border-opacity: 15%; + } + + .btn-outline.btn-warning:hover { + --tw-text-opacity: 1; + color: var(--fallback-wac,oklch(var(--wac)/var(--tw-text-opacity))); + } + + @supports (color: color-mix(in oklab, black, black)) { + .btn-outline.btn-warning:hover { + background-color: color-mix(in oklab, var(--fallback-wa,oklch(var(--wa)/1)) 90%, black); + border-color: color-mix(in oklab, var(--fallback-wa,oklch(var(--wa)/1)) 90%, black); + } + } + + .btn-outline.btn-error:hover { + --tw-text-opacity: 1; + color: var(--fallback-erc,oklch(var(--erc)/var(--tw-text-opacity))); + } + + @supports (color: color-mix(in oklab, black, black)) { + .btn-outline.btn-error:hover { + background-color: color-mix(in oklab, var(--fallback-er,oklch(var(--er)/1)) 90%, black); + border-color: color-mix(in oklab, var(--fallback-er,oklch(var(--er)/1)) 90%, black); + } + } + + .btn-disabled:hover, + .btn[disabled]:hover, + .btn:disabled:hover { + --tw-border-opacity: 0; + background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity))); + --tw-bg-opacity: 0.2; + color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); + --tw-text-opacity: 0.2; + } + + @supports (color: color-mix(in oklab, black, black)) { + .btn:is(input[type="checkbox"]:checked):hover, .btn:is(input[type="radio"]:checked):hover { + background-color: color-mix(in oklab, var(--fallback-p,oklch(var(--p)/1)) 90%, black); + border-color: color-mix(in oklab, var(--fallback-p,oklch(var(--p)/1)) 90%, black); + } + } +} + +.label { + display: flex; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + align-items: center; + justify-content: space-between; + padding-left: 0.25rem; + padding-right: 0.25rem; + padding-top: 0.5rem; + padding-bottom: 0.5rem; +} + +.input { + flex-shrink: 1; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + height: 3rem; + padding-left: 1rem; + padding-right: 1rem; + font-size: 1rem; + line-height: 2; + line-height: 1.5rem; + border-radius: var(--rounded-btn, 0.5rem); + border-width: 1px; + border-color: transparent; + --tw-bg-opacity: 1; + background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity))); +} + +.input[type="number"]::-webkit-inner-spin-button, +.input-md[type="number"]::-webkit-inner-spin-button { + margin-top: -1rem; + margin-bottom: -1rem; + margin-inline-end: -1rem; +} + +.menu li.disabled { + cursor: not-allowed; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + color: var(--fallback-bc,oklch(var(--bc)/0.3)); +} + +.swap { + position: relative; + display: inline-grid; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + place-content: center; + cursor: pointer; +} + +.swap > * { + grid-column-start: 1; + grid-row-start: 1; + transition-duration: 300ms; + transition-timing-function: cubic-bezier(0, 0, 0.2, 1); + transition-property: transform, opacity; +} + +.swap input { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; +} + +.swap .swap-on, +.swap .swap-indeterminate, +.swap input:indeterminate ~ .swap-on { + opacity: 0; +} + +.swap input:checked ~ .swap-off, +.swap-active .swap-off, +.swap input:indeterminate ~ .swap-off { + opacity: 0; +} + +.swap input:checked ~ .swap-on, +.swap-active .swap-on, +.swap input:indeterminate ~ .swap-indeterminate { + opacity: 1; +} + +.textarea { + min-height: 3rem; + flex-shrink: 1; + padding-left: 1rem; + padding-right: 1rem; + padding-top: 0.5rem; + padding-bottom: 0.5rem; + font-size: 0.875rem; + line-height: 1.25rem; + line-height: 2; + border-radius: var(--rounded-btn, 0.5rem); + border-width: 1px; + border-color: transparent; + --tw-bg-opacity: 1; + background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity))); +} + +.btm-nav > *.disabled, + .btm-nav > *[disabled] { + pointer-events: none; + --tw-border-opacity: 0; + background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity))); + --tw-bg-opacity: 0.1; + color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); + --tw-text-opacity: 0.2; +} + +.btm-nav > * .label { + font-size: 1rem; + line-height: 1.5rem; +} + +@media (prefers-reduced-motion: no-preference) { + .btn { + animation: button-pop var(--animation-btn, 0.25s) ease-out; + } +} + +.btn:active:hover, + .btn:active:focus { + animation: button-pop 0s ease-out; + transform: scale(var(--btn-focus-scale, 0.97)); +} + +@supports not (color: oklch(0% 0 0)) { + .btn { + background-color: var(--btn-color, var(--fallback-b2)); + border-color: var(--btn-color, var(--fallback-b2)); + } + + .btn-warning { + --btn-color: var(--fallback-wa); + } + + .btn-error { + --btn-color: var(--fallback-er); + } +} + +@supports (color: color-mix(in oklab, black, black)) { + .btn-outline.btn-warning.btn-active { + background-color: color-mix(in oklab, var(--fallback-wa,oklch(var(--wa)/1)) 90%, black); + border-color: color-mix(in oklab, var(--fallback-wa,oklch(var(--wa)/1)) 90%, black); + } + + .btn-outline.btn-error.btn-active { + background-color: color-mix(in oklab, var(--fallback-er,oklch(var(--er)/1)) 90%, black); + border-color: color-mix(in oklab, var(--fallback-er,oklch(var(--er)/1)) 90%, black); + } +} + +.btn:focus-visible { + outline-style: solid; + outline-width: 2px; + outline-offset: 2px; +} + +@supports (color: oklch(0% 0 0)) { + .btn-warning { + --btn-color: var(--wa); + } + + .btn-error { + --btn-color: var(--er); + } +} + +.btn-warning { + --tw-text-opacity: 1; + color: var(--fallback-wac,oklch(var(--wac)/var(--tw-text-opacity))); + outline-color: var(--fallback-wa,oklch(var(--wa)/1)); +} + +.btn-error { + --tw-text-opacity: 1; + color: var(--fallback-erc,oklch(var(--erc)/var(--tw-text-opacity))); + outline-color: var(--fallback-er,oklch(var(--er)/1)); +} + +.btn.glass { + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); + outline-color: currentColor; +} + +.btn.glass.btn-active { + --glass-opacity: 25%; + --glass-border-opacity: 15%; +} + +.btn-outline.btn-warning { + --tw-text-opacity: 1; + color: var(--fallback-wa,oklch(var(--wa)/var(--tw-text-opacity))); +} + +.btn-outline.btn-warning.btn-active { + --tw-text-opacity: 1; + color: var(--fallback-wac,oklch(var(--wac)/var(--tw-text-opacity))); +} + +.btn-outline.btn-error { + --tw-text-opacity: 1; + color: var(--fallback-er,oklch(var(--er)/var(--tw-text-opacity))); +} + +.btn-outline.btn-error.btn-active { + --tw-text-opacity: 1; + color: var(--fallback-erc,oklch(var(--erc)/var(--tw-text-opacity))); +} + +.btn.btn-disabled, + .btn[disabled], + .btn:disabled { + --tw-border-opacity: 0; + background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity))); + --tw-bg-opacity: 0.2; + color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); + --tw-text-opacity: 0.2; +} + +.btn:is(input[type="checkbox"]:checked), +.btn:is(input[type="radio"]:checked) { + --tw-border-opacity: 1; + border-color: var(--fallback-p,oklch(var(--p)/var(--tw-border-opacity))); + --tw-bg-opacity: 1; + background-color: var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity))); + --tw-text-opacity: 1; + color: var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity))); +} + +.btn:is(input[type="checkbox"]:checked):focus-visible, .btn:is(input[type="radio"]:checked):focus-visible { + outline-color: var(--fallback-p,oklch(var(--p)/1)); +} + +@keyframes button-pop { + 0% { + transform: scale(var(--btn-focus-scale, 0.98)); + } + + 40% { + transform: scale(1.02); + } + + 100% { + transform: scale(1); + } +} + +.checkbox:focus { + box-shadow: none; +} + +.checkbox:focus-visible { + outline-style: solid; + outline-width: 2px; + outline-offset: 2px; + outline-color: var(--fallback-bc,oklch(var(--bc)/1)); +} + +.checkbox:disabled { + border-width: 0px; + cursor: not-allowed; + border-color: transparent; + --tw-bg-opacity: 1; + background-color: var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity))); + opacity: 0.2; +} + +.checkbox:checked, + .checkbox[aria-checked="true"] { + background-repeat: no-repeat; + animation: checkmark var(--animation-input, 0.2s) ease-out; + background-color: var(--chkbg); + background-image: linear-gradient(-45deg, transparent 65%, var(--chkbg) 65.99%), + linear-gradient(45deg, transparent 75%, var(--chkbg) 75.99%), + linear-gradient(-45deg, var(--chkbg) 40%, transparent 40.99%), + linear-gradient( + 45deg, + var(--chkbg) 30%, + var(--chkfg) 30.99%, + var(--chkfg) 40%, + transparent 40.99% + ), + linear-gradient(-45deg, var(--chkfg) 50%, var(--chkbg) 50.99%); +} + +.checkbox:indeterminate { + --tw-bg-opacity: 1; + background-color: var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity))); + background-repeat: no-repeat; + animation: checkmark var(--animation-input, 0.2s) ease-out; + background-image: linear-gradient(90deg, transparent 80%, var(--chkbg) 80%), + linear-gradient(-90deg, transparent 80%, var(--chkbg) 80%), + linear-gradient(0deg, var(--chkbg) 43%, var(--chkfg) 43%, var(--chkfg) 57%, var(--chkbg) 57%); +} + +@keyframes checkmark { + 0% { + background-position-y: 5px; + } + + 50% { + background-position-y: -2px; + } + + 100% { + background-position-y: 0; + } +} + +.input input { + --tw-bg-opacity: 1; + background-color: var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity))); + background-color: transparent; +} + +.input input:focus { + outline: 2px solid transparent; + outline-offset: 2px; +} + +.input[list]::-webkit-calendar-picker-indicator { + line-height: 1em; +} + +.input:focus, + .input:focus-within { + box-shadow: none; + border-color: var(--fallback-bc,oklch(var(--bc)/0.2)); + outline-style: solid; + outline-width: 2px; + outline-offset: 2px; + outline-color: var(--fallback-bc,oklch(var(--bc)/0.2)); +} + +.input:has(> input[disabled]), + .input-disabled, + .input:disabled, + .input[disabled] { + cursor: not-allowed; + --tw-border-opacity: 1; + border-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity))); + --tw-bg-opacity: 1; + background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity))); + color: var(--fallback-bc,oklch(var(--bc)/0.4)); +} + +.input:has(> input[disabled])::-moz-placeholder, .input-disabled::-moz-placeholder, .input:disabled::-moz-placeholder, .input[disabled]::-moz-placeholder { + color: var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity))); + --tw-placeholder-opacity: 0.2; +} + +.input:has(> input[disabled])::placeholder, + .input-disabled::placeholder, + .input:disabled::placeholder, + .input[disabled]::placeholder { + color: var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity))); + --tw-placeholder-opacity: 0.2; +} + +.input:has(> input[disabled]) > input[disabled] { + cursor: not-allowed; +} + +.input::-webkit-date-and-time-value { + text-align: inherit; +} + +.join > :where(*:not(:first-child)):is(.btn) { + margin-inline-start: calc(var(--border-btn) * -1); +} + +.loading { + pointer-events: none; + display: inline-block; + aspect-ratio: 1 / 1; + width: 1.5rem; + background-color: currentColor; + -webkit-mask-size: 100%; + mask-size: 100%; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-image: url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='%23000' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cstyle%3E.spinner_V8m1%7Btransform-origin:center;animation:spinner_zKoa 2s linear infinite%7D.spinner_V8m1 circle%7Bstroke-linecap:round;animation:spinner_YpZS 1.5s ease-out infinite%7D%40keyframes spinner_zKoa%7B100%25%7Btransform:rotate(360deg)%7D%7D%40keyframes spinner_YpZS%7B0%25%7Bstroke-dasharray:0 150;stroke-dashoffset:0%7D47.5%25%7Bstroke-dasharray:42 150;stroke-dashoffset:-16%7D95%25%2C100%25%7Bstroke-dasharray:42 150;stroke-dashoffset:-59%7D%7D%3C%2Fstyle%3E%3Cg class='spinner_V8m1'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3'%3E%3C%2Fcircle%3E%3C%2Fg%3E%3C%2Fsvg%3E"); + mask-image: url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='%23000' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cstyle%3E.spinner_V8m1%7Btransform-origin:center;animation:spinner_zKoa 2s linear infinite%7D.spinner_V8m1 circle%7Bstroke-linecap:round;animation:spinner_YpZS 1.5s ease-out infinite%7D%40keyframes spinner_zKoa%7B100%25%7Btransform:rotate(360deg)%7D%7D%40keyframes spinner_YpZS%7B0%25%7Bstroke-dasharray:0 150;stroke-dashoffset:0%7D47.5%25%7Bstroke-dasharray:42 150;stroke-dashoffset:-16%7D95%25%2C100%25%7Bstroke-dasharray:42 150;stroke-dashoffset:-59%7D%7D%3C%2Fstyle%3E%3Cg class='spinner_V8m1'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3'%3E%3C%2Fcircle%3E%3C%2Fg%3E%3C%2Fsvg%3E"); +} + +.mockup-browser .mockup-browser-toolbar .input { + position: relative; + margin-left: auto; + margin-right: auto; + display: block; + height: 1.75rem; + width: 24rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + --tw-bg-opacity: 1; + background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity))); + padding-left: 2rem; + direction: ltr; +} + +.mockup-browser .mockup-browser-toolbar .input:before { + content: ""; + position: absolute; + left: 0.5rem; + top: 50%; + aspect-ratio: 1 / 1; + height: 0.75rem; + --tw-translate-y: -50%; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); + border-radius: 9999px; + border-width: 2px; + border-color: currentColor; + opacity: 0.6; +} + +.mockup-browser .mockup-browser-toolbar .input:after { + content: ""; + position: absolute; + left: 1.25rem; + top: 50%; + height: 0.5rem; + --tw-translate-y: 25%; + --tw-rotate: -45deg; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); + border-radius: 9999px; + border-width: 1px; + border-color: currentColor; + opacity: 0.6; +} + +@keyframes modal-pop { + 0% { + opacity: 0; + } +} + +@keyframes progress-loading { + 50% { + background-position-x: -115%; + } +} + +@keyframes radiomark { + 0% { + box-shadow: 0 0 0 12px var(--fallback-b1,oklch(var(--b1)/1)) inset, + 0 0 0 12px var(--fallback-b1,oklch(var(--b1)/1)) inset; + } + + 50% { + box-shadow: 0 0 0 3px var(--fallback-b1,oklch(var(--b1)/1)) inset, + 0 0 0 3px var(--fallback-b1,oklch(var(--b1)/1)) inset; + } + + 100% { + box-shadow: 0 0 0 4px var(--fallback-b1,oklch(var(--b1)/1)) inset, + 0 0 0 4px var(--fallback-b1,oklch(var(--b1)/1)) inset; + } +} + +@keyframes rating-pop { + 0% { + transform: translateY(-0.125em); + } + + 40% { + transform: translateY(-0.125em); + } + + 100% { + transform: translateY(0); + } +} + +@keyframes skeleton { + from { + background-position: 150%; + } + + to { + background-position: -50%; + } +} + +.textarea:focus { + box-shadow: none; + border-color: var(--fallback-bc,oklch(var(--bc)/0.2)); + outline-style: solid; + outline-width: 2px; + outline-offset: 2px; + outline-color: var(--fallback-bc,oklch(var(--bc)/0.2)); +} + +.textarea-disabled, + .textarea:disabled, + .textarea[disabled] { + cursor: not-allowed; + --tw-border-opacity: 1; + border-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity))); + --tw-bg-opacity: 1; + background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity))); + color: var(--fallback-bc,oklch(var(--bc)/0.4)); +} + +.textarea-disabled::-moz-placeholder, .textarea:disabled::-moz-placeholder, .textarea[disabled]::-moz-placeholder { + color: var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity))); + --tw-placeholder-opacity: 0.2; +} + +.textarea-disabled::placeholder, + .textarea:disabled::placeholder, + .textarea[disabled]::placeholder { + color: var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity))); + --tw-placeholder-opacity: 0.2; +} + +@keyframes toast-pop { + 0% { + transform: scale(0.9); + opacity: 0; + } + + 100% { + transform: scale(1); + opacity: 1; + } +} + +.join.join-vertical > :where(*:not(:first-child)):is(.btn) { + margin-top: calc(var(--border-btn) * -1); +} + +.join.join-horizontal > :where(*:not(:first-child)):is(.btn) { + margin-inline-start: calc(var(--border-btn) * -1); +} + +.rounded { + border-radius: 0.25rem; +} + +.border-2 { + border-width: 2px; +} + +.filter { + filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); +} diff --git a/resources/public/index.html b/resources/public/index.html new file mode 100644 index 0000000..d3e776f --- /dev/null +++ b/resources/public/index.html @@ -0,0 +1,16 @@ + + + + + + + ied + + + +
+ + + diff --git a/src/ied/config.cljs b/src/ied/config.cljs index 481c258..5811284 100644 --- a/src/ied/config.cljs +++ b/src/ied/config.cljs @@ -2,3 +2,4 @@ (def debug? ^boolean goog.DEBUG) + diff --git a/src/ied/core.cljs b/src/ied/core.cljs index 5418d94..4d67cb6 100644 --- a/src/ied/core.cljs +++ b/src/ied/core.cljs @@ -20,5 +20,6 @@ (defn init [] (routes/start!) (re-frame/dispatch-sync [::events/initialize-db]) + (re-frame/dispatch [::events/connect-to-default-relays]) (dev-setup) (mount-root)) diff --git a/src/ied/db.cljs b/src/ied/db.cljs index d41f915..b24a470 100644 --- a/src/ied/db.cljs +++ b/src/ied/db.cljs @@ -2,7 +2,26 @@ (def default-db {:name "re-frame" + :current-path nil :show-add-event false :events nil :pk nil + :lists nil + :default-relays [{:name "strfry-1" + :uri "http://localhost:7777" + :id (random-uuid) + :status "disconnected"} + {:name "strfry-2" + :uri "http://localhost:7778" + :id (random-uuid) + :status "disconnected"} + {:name "rust-relay" + :uri "http://localhost:4445" + :id (random-uuid) + :status "disconnected"} + ; {:name "damus" + ; :uri "wss://relay.damus.io" + ; :status "disconnected"} + ] + :selected-events nil :sockets []}) diff --git a/src/ied/events.cljs b/src/ied/events.cljs index 75d8c3f..4825544 100644 --- a/src/ied/events.cljs +++ b/src/ied/events.cljs @@ -25,46 +25,100 @@ (fn-traced [{:keys [db]} [_ active-panel]] {:db (assoc db :active-panel active-panel)})) +(re-frame/reg-event-fx + ::set-route + (fn-traced [{:keys [db]} [_ route]] + {:db (assoc db :route route)})) + ;; Database Event? (re-frame/reg-event-fx ::save-event ;; TODO if EOSE retrieved end connection identified by uri ;; TODO make events a set (?) - (fn-traced [{:keys [db]} [_ [uri event]]] - (println uri event) - (when (= (first event) "EVENT") - {:db (update db :events conj event)}))) + (fn-traced [{:keys [db]} [_ [uri raw-event]]] + (let [event (nth raw-event 2 raw-event)] + (println uri raw-event event) + (when (and + (= (first raw-event) "EVENT") + (not (some #(= (:id event) (:id %)) (get db :events {})))) + {:db (update db :events conj event)})))) (defn handlers [ws-uri] {:on-message (fn [e] (re-frame/dispatch [::save-event [ws-uri (-> (.-data e) js/JSON.parse (js->clj :keywordize-keys true))]])) - :on-open #(prn "Opening a new connection") - :on-close #(prn "Closing a connection")}) + ; :on-open #(prn "Opening a new connection") + :on-open #(re-frame/dispatch [::load-events ws-uri]) + :on-close #(prn "Closing a connection") + :on-error (fn [e] (.log js/console "Error with uri: " ws-uri (clj->js e)) + (re-frame/dispatch [::update-ws-connection-status ws-uri "error"]))}) + +(re-frame/reg-event-db + ::update-ws-connection-status + (fn [db [_ ws-uri status]] + (let [target-ws (first (filter #(= ws-uri (:uri %)) (:sockets db)))] + (assoc db + :sockets + (assoc (:sockets db) + (.indexOf (:sockets db) target-ws) + (merge target-ws {:status status})))))) + +(re-frame/reg-event-fx + ::load-events + (fn-traced [cofx [_ ws-uri]] + {::load-events-fx ws-uri})) + +(re-frame/reg-fx + ::load-events-fx + (fn [ws-uri] + (println "loading events") + + (let [sockets (re-frame/subscribe [::subs/sockets]) + target-ws (first (filter #(= ws-uri (:uri %)) @sockets))] + (ws/send (:socket target-ws) ["REQ" "4242" {:kinds [1 30142] + :limit 10}] fmt/json) + ; (ws/close (:socket (first @sockets))) ;; should be handled otherwise (?) + ))) (defn create-socket [uri] + (println "creating socket with uri" uri) (ws/create uri (handlers uri))) (re-frame/reg-event-fx ::create-websocket + ;; ws {:id uuid + ;; :uri + ;; :name + ;; :status connected | disconnected | error} (fn-traced [{:keys [db]} [_ ws]] - {:db (update db :sockets conj (merge ws {:socket (create-socket (:uri ws))}))})) + (if (some #(= (:uri ws) (:uri %)) (:sockets db)) + (doall + (println "uri already there" (:uri ws)) + {:db (assoc db + :sockets + (assoc (:sockets db) + (.indexOf (:sockets db) ws) + (merge ws {:socket (create-socket (:uri ws)) + :status "connected"})))}) + (doall + (println "uri not yet known") + {:db (update db :sockets conj (merge ws {:socket (create-socket (:uri ws)) + :status "connected"}))})))) (re-frame/reg-event-fx ::connect-to-websocket - (fn-traced [{:keys [db]} [_ _]] - {::connect-to-websocket-fx _})) ;; TODO identify the socket to connect to + (fn-traced [{:keys [db]} [_ ws-uri]] + {::connect-to-websocket-fx ws-uri})) (re-frame/reg-fx ::connect-to-websocket-fx - (fn [_] - (let [sockets (re-frame/subscribe [::subs/sockets])] - (println "Sockets: " @sockets) - (ws/send (:socket (first @sockets)) ["REQ" "4242" {:kinds [1 30142] - :limit 10}] fmt/json) - ; (ws/close (:socket (first @sockets))) ;; should be handled otherwise (?) + (fn [ws-uri] + (let [sockets (re-frame/subscribe [::subs/sockets]) + target-ws (first (filter #(= ws-uri (:uri %)) @sockets))] + (re-frame/dispatch [::create-websocket target-ws]) + ; (ws/create (:uri target-ws) (handlers (:uri target-ws))) ))) ;; TODO use id to close socket @@ -72,27 +126,44 @@ ;; render connect / disconnect button based on status (re-frame/reg-event-fx ::close-connection-to-websocket - (fn-traced [{:keys [db]} [_ _]] - {::close-connection-to-websocket-fx _})) + (fn-traced [{:keys [db]} [_ ws-uri]] + {::close-connection-to-websocket-fx ws-uri})) (re-frame/reg-fx ::close-connection-to-websocket-fx - (fn [_] + (fn [ws-uri] (let [sockets (re-frame/subscribe [::subs/sockets])] - (ws/close (:socket (first @sockets)))))) + (ws/close (:socket (first (filter #(= ws-uri (:uri %)) @sockets)))) + (re-frame/dispatch [::update-ws-connection-status ws-uri "disconnected"])))) + +(re-frame/reg-event-fx + ::connect-to-default-relays + (fn-traced [cofx [_]] + (let [default-relays (re-frame/subscribe [::subs/default-relays])] + {::connect-to-default-relays-fx @default-relays}))) + +(re-frame/reg-fx + ::connect-to-default-relays-fx + (fn [default-relays] + (doall + (for [r default-relays] + (re-frame/dispatch [::create-websocket r]))))) ;; TODO (re-frame/reg-event-fx ::send-to-relays (fn-traced [cofx [_ signedEvent]] - {::send-to-relays-fx signedEvent})) + (let [sockets (-> cofx :db :sockets)] + {::send-to-relays-fx [sockets signedEvent]}))) (re-frame/reg-fx ::send-to-relays-fx - (fn [signedEvent] - (let [sockets (re-frame/subscribe [::subs/sockets])] - (.log js/console (clj->js ["EVENT" signedEvent])) - (ws/send (:socket (first @sockets)) ["EVENT" signedEvent] fmt/json)))) + (fn [[sockets signedEvent]] + (println "got relays" sockets) + (let [connected-sockets (filter #(= "connected" (:status %)) sockets)] + (doseq [socket connected-sockets] + (.log js/console "sending to relay") + (ws/send (:socket socket) ["EVENT" signedEvent] fmt/json))))) (re-frame/reg-event-db ::update-websockets @@ -116,6 +187,7 @@ (fn [db _] (assoc db :show-add-event (not (:show-add-event db))))) +;; TODO just pass an event here that is then passed on to signing (re-frame/reg-event-fx ::publish-resource [(re-frame/inject-cofx :now)] @@ -123,15 +195,15 @@ (let [event {:kind 30142 :created_at (:now cofx) :content "hello world" - ::tags [] - ;;:tags [["author" "" (:author resource)]] - }] + :tags [["author" "" (:author resource)]]}] {::publish-resource-fx event}))) +;; TODO maybe we need some validation before publishing (re-frame/reg-fx ::publish-resource-fx - (fn [resource] - (p/let [signedEvent (.nostr.signEvent js/window (clj->js resource))] + (fn [unsignedEvent] + (p/let [_ (js/console.log (clj->js unsignedEvent)) + signedEvent (.nostr.signEvent js/window (clj->js unsignedEvent))] (re-frame/dispatch [::send-to-relays signedEvent])))) (re-frame/reg-event-fx @@ -160,5 +232,69 @@ (fn [cofx _data] ;; _data unused (assoc cofx :now (quot (.now js/Date) 1000)))) -(comment - (quot (.now js/Date) 1000)) +(defn convert-amb-to-nostr-event + [json-string created_at] + (let [parsed-json (js->clj (js/JSON.parse json-string) :keywordize-keys true) + tags (into [["id" (:id parsed-json)] + ["name" (:name parsed-json)] + ["image" (:image parsed-json)]] + cat [(map (fn [e] ["about" (:id e) (-> e :prefLabel :de)]) (:about parsed-json)) + (map (fn [e] ["inLanguage" e]) (:inLanguage parsed-json))]) + event {:kind 30142 + :created_at created_at + :content "Added AMB Resource" + :tags tags}] + event)) + +(re-frame/reg-event-fx + ::convert-amb-and-publish-as-nostr-event + [(re-frame/inject-cofx :now)] + (fn-traced [cofx [_ json-string]] + (let [event (convert-amb-to-nostr-event json-string (:now cofx))] + {::publish-resource-fx event}))) + +(re-frame/reg-event-db + ::toggle-selected-events + (fn [db [_ event]] + (js/console.log "toggling selected event with id" (:id event)) + (if (some #(= event (:id %)) (:selected-events db)) + (assoc db :selected-events (filter #(not= event (:id %)) (:selected-events db))) + (update db :selected-events conj event)))) + +(re-frame/reg-event-fx + ::add-resources-to-list + [(re-frame/inject-cofx :now)] + (fn [cofx [_ [list resources-to-add]]] + (let [tags (into [["d" (:d list) + "name" (:name list)]] + (map (fn [e] (cond + (= 1 (:kind e)) ["e" (:id e)] + (= 30142 (:kind e)) ["a" (str "30142:" (:id e))])) + + resources-to-add)) + _ (.log js/console (clj->js tags)) + event {:kind 30004 + :created_at (:now cofx) + :content "" + :tags tags}] + {::publish-resource-fx event}))) + +(re-frame/reg-event-fx + ::get-lists-for-npub + (fn [cofx [_ npub]] + (let [query-for-lists ["REQ" + (str "lists-for-npub") ;; TODO maybe make this more explicit later + {:authors [(nostr/get-pk-from-npub npub)] + :kinds [30004]}]] + {::request-from-relay query-for-lists}))) + +(re-frame/reg-fx + ::request-from-relay + (fn [query] + (println "requesting from relay this query: " query) + + (let [sockets @(re-frame/subscribe [::subs/sockets])] + (doall + (for [s (filter (fn [s] (= "connected" (:status s))) sockets)] + (ws/send (:socket s) query fmt/json)))))) + diff --git a/src/ied/nostr.cljs b/src/ied/nostr.cljs index 9c46503..9df7796 100644 --- a/src/ied/nostr.cljs +++ b/src/ied/nostr.cljs @@ -1,7 +1,8 @@ (ns ied.nostr (:require [promesa.core :as p] - ["@noble/secp256k1" :as secp])) + ["@noble/secp256k1" :as secp] + ["nostr-tools/nip19" :as nip19])) (defn event-to-serialized-json [m] (let [event [0 @@ -50,3 +51,17 @@ (println serialized-event) (println id))) + (defn string-to-uint8array [s] + (let [encoder (new js/TextEncoder)] + (.encode encoder s))) + +(defn get-npub-from-pk [pk] + (.npubEncode nip19 pk)) + +(defn get-pk-from-npub [npub] + (.decode nip19 npub)) + +(comment + (get-npub-from-pk "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d") + (get-pk-from-npub "npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6" ) + ) diff --git a/src/ied/routes.cljs b/src/ied/routes.cljs index 3b162b1..3298f08 100644 --- a/src/ied/routes.cljs +++ b/src/ied/routes.cljs @@ -10,9 +10,16 @@ (def routes (atom - ["/" {"" :home - "about" :about - "settings" :settings}])) + ["/" + {"" :home + "add-resource" :add-resource + "about" :about + "settings" :settings + ["" [#"npub1[ac-hj-np-z02-9]{58}" :npub]] {"" :npub-view + "/" :npub-view}}])) + +(comment + (parse "/npub1r30l8j4vmppvq8w23umcyvd3vct4zmfpfkn4c7h2h057rmlfcrmq9xt9ma/")) (defn parse [url] @@ -25,7 +32,7 @@ (defn dispatch [route] (let [panel (keyword (str (name (:handler route)) "-panel"))] - (re-frame/dispatch [::events/set-active-panel panel]))) + (re-frame/dispatch [::events/set-route {:route route :panel panel}]))) (defonce history (pushy/pushy dispatch parse)) @@ -42,3 +49,4 @@ :navigate (fn [handler] (navigate! handler))) + diff --git a/src/ied/subs.cljs b/src/ied/subs.cljs index ea1a48f..23ac2c2 100644 --- a/src/ied/subs.cljs +++ b/src/ied/subs.cljs @@ -10,24 +10,50 @@ (re-frame/reg-sub ::active-panel (fn [db _] - (:active-panel db))) + (get-in db [:route :panel]))) (re-frame/reg-sub - ::sockets - (fn [db _] - (:sockets db))) + ::sockets + (fn [db _] + (:sockets db))) (re-frame/reg-sub - ::events - (fn [db _] - (:events db))) - -(re-frame/reg-sub - ::show-add-event - (fn [db _] - (:show-add-event db))) + ::connected-sockets + :<- [::sockets] + (fn [[sockets]] + (filter #(= (:status %) "connected") sockets))) (re-frame/reg-sub - ::pk - (fn [db _] - (:pk db))) + ::events + (fn [db _] + (:events db))) + +(re-frame/reg-sub + ::show-add-event + (fn [db _] + (:show-add-event db))) + +(re-frame/reg-sub + ::pk + (fn [db _] + (:pk db))) + +(re-frame/reg-sub + ::default-relays + (fn [db _] + (:default-relays db))) + +(re-frame/reg-sub + ::selected-events + (fn [db _] + (:selected-events db))) + +(re-frame/reg-sub + ::route-params + (fn [db _] + (get-in db [:route :route :route-params]))) + +(re-frame/reg-sub + ::lists + (fn [db _] + (filter #(= 30004 (:kind %)) (:events db)))) diff --git a/src/ied/views.cljs b/src/ied/views.cljs index d90fd23..a621ea1 100644 --- a/src/ied/views.cljs +++ b/src/ied/views.cljs @@ -4,7 +4,7 @@ [ied.events :as events] [ied.routes :as routes] [ied.subs :as subs] - + ["react" :as react] [reagent.core :as reagent])) ;; add resource form @@ -33,16 +33,27 @@ :value (:author @s) :on-change (fn [e] (swap! s assoc :author (-> e .-target .-value)))}] - [:button {:on-click #(re-frame/dispatch [::events/publish-resource {:name (:name @s) + [:button {:on-click #(re-frame/dispatch [::events/publish-resource {:name (:name @s) ;; TODO this should be sth like build event :uri (:uri @s) :author (:author @s)}])} "Publish Resource"]]))) +(defn add-resource-by-json [] + (let [s (reagent/atom {:json-string ""})] + (fn [] + [:form {:on-submit (fn [e] (.preventDefault e))} + [:textarea {:on-change (fn [e] + (swap! s assoc :json-string (-> e .-target .-value)))}] + [:button {:class "btn btn-warning" + :on-click #(re-frame/dispatch [::events/convert-amb-and-publish-as-nostr-event (:json-string @s)])} + "Publish as Nostr Event"]]))) + ;; events (defn events-panel [] (let [events (re-frame/subscribe [::subs/events]) + selected-events @(re-frame/subscribe [::subs/selected-events]) show-add-event (re-frame/subscribe [::subs/show-add-event])] - [:div + [:div {:class "border-2 rounded"} [:p {:on-click #(re-frame/dispatch [::events/toggle-show-add-event])} (if @show-add-event "X" "Add Resource!")] (when @show-add-event @@ -51,10 +62,16 @@ (if (> (count @events) 0) (doall (for [event @events] - ; [:li (:content (nth event 2 {:content "hello"}))] - [:li {:key (:id event)} (get (nth event 2) :content "")] - )) - [:p "no events there"])])) + [:li {:key (:id event)} (get event :content "") + [:input {:type "checkbox" + :on-click #(re-frame/dispatch [::events/toggle-selected-events event])}]])) + [:p "no events there"]) + [:button {:class "btn" + :disabled (not (boolean (seq selected-events))) + :on-click #(re-frame/dispatch [::events/add-resources-to-list [{:d "unique-id-1" + :name "Test List SC"} + selected-events]])} + "Add To Lists"]])) ;; relays (defn add-relay-form @@ -88,15 +105,27 @@ [:div [add-relay-form] - (when (> (count @sockets) 0) + (if (> (count @sockets) 0) [:ul (doall (for [socket @sockets] [:li {:key (:id socket)} [:span (:name socket)] - [:button {:on-click #(re-frame/dispatch [::events/connect-to-websocket])} "Load events"] - [:button {:on-click #(re-frame/dispatch [::events/close-connection-to-websocket])} "Disconnect"] - [:button {:on-click #(re-frame/dispatch [::events/remove-websocket socket])} "Remove relay"]]))])])) + [:span (:status socket)] + [:button {:class "btn" + :disabled (not= "connected" (:status socket)) + :on-click #(re-frame/dispatch [::events/load-events (:uri socket)])} "Load events"] + (if (not= (:status socket) "connected") + [:button {:class "btn" + :on-click #(re-frame/dispatch [::events/connect-to-websocket (:uri socket)])} "Connect"] + [:button {:class "btn" + :on-click #(re-frame/dispatch [::events/close-connection-to-websocket (:uri socket)])} "Disconnect"]) + + [:button {:class "btn btn-error" + :on-click #(re-frame/dispatch [::events/remove-websocket socket])} "Remove relay"]]))] + [:p "No relays found"] + ;(re-frame/dispatch [::events/connect-to-default-relays]) + )])) ;; Header @@ -106,6 +135,9 @@ [:a {:on-click #(re-frame/dispatch [::events/navigate :home])} "home"] "|" + [:a {:on-click #(re-frame/dispatch [::events/navigate :add-resource])} + "add resource"] + "|" [:a {:on-click #(re-frame/dispatch [::events/navigate :about])} "about"] "|" @@ -116,6 +148,16 @@ [:a {:on-click #(re-frame/dispatch [::events/logout])} "logout"] [:a {:on-click #(re-frame/dispatch [::events/login-with-extension])} "login"])])) +;; Add Resource Panel + +(defn add-resource-panel [] + [:div + [:h1 "Add Resource"] + [add-resource-form] + [add-resource-by-json]]) + +(defmethod routes/panels :add-resource-panel [] [add-resource-panel]) + ;; Settings (defn settings-panel [] [:div @@ -141,19 +183,46 @@ (defn about-panel [] [:div [:h1 "This is the About Page."] - [:div [:a {:on-click #(re-frame/dispatch [::events/navigate :home])} "go to Home Page"]]]) (defmethod routes/panels :about-panel [] [about-panel]) +;; npub +(defn npub-view-panel [] + (let [route-params @(re-frame/subscribe [::subs/route-params]) + lists @(re-frame/subscribe [::subs/lists])] + [:div + [:h1 (str "Hello Npub: " (:npub route-params))] + (if (nil? lists) + [:button {:class "btn" + :on-click + #(re-frame/dispatch [::events/get-lists-for-npub (:npub route-params)])} "Load lists"] + [:div "Got some lists" + (doall + (for [l lists] + [:div {:key (:id l)} + [:li + + [:p (str "ID: " (:id l))] + [:p (str "Name: " (first (filter #(= "d" (first %)) (:tags l))))] + ;; TODO filter tags for already being in events + ;; for all that are not send a query + [:p (str "Tags: " (:tags l))]]]))]) + [:button {:class "btn" + :on-click + #(re-frame/dispatch [::events/get-lists-for-npub (:npub route-params)])} "Load lists"]])) + +(defmethod routes/panels :npub-view-panel [] [npub-view-panel]) + ;; main (defn main-panel [] (let [active-panel (re-frame/subscribe [::subs/active-panel])] [:div [header] - (routes/panels @active-panel)])) + [:div + (routes/panels @active-panel)]])) (comment