Enhance app with multi-CCN support and improved UX

Features:

 Add support for multiple CCNs
🔍 Implement sidebar hiding functionality
🎨 Revamp navigation system for better flow
🖌️ Replace icons with custom-designed assets and improved naming
🚀 Streamline initial setup process
📝 Refine terminology (e.g., "forum thread" → "topic")
🛠️ Enhance forum usability and interaction
This commit is contained in:
Danny Morabito 2025-04-11 22:26:00 +02:00
parent bf3c950da0
commit 9893945f55
Signed by: dannym
GPG key ID: 7CC8056A5A04557E
34 changed files with 792 additions and 308 deletions

View file

@ -1,5 +1,5 @@
import { fileURLToPath, URL } from "node:url";
import { defineConfig, externalizeDepsPlugin } from "electron-vite";
import { fileURLToPath, URL } from "node:url";
import { resolve } from "path";
export default defineConfig({
@ -40,6 +40,7 @@ export default defineConfig({
"@components": fileURLToPath(
new URL("./src/components", import.meta.url)
),
"@assets": fileURLToPath(new URL("./src/assets", import.meta.url)),
"@": fileURLToPath(new URL("./src", import.meta.url)),
},
},

View file

@ -48,6 +48,7 @@
"@nostr/tools": "npm:@jsr/nostr__tools",
"@open-wc/lit-helpers": "^0.7.0",
"@std/encoding": "npm:@jsr/std__encoding",
"iconify-icon": "^2.3.0",
"lit": "^3.2.1",
"markdown-it": "^14.1.0"
}

64
src/arx-icons.ts Normal file
View file

@ -0,0 +1,64 @@
import arborIcon from '@assets/icons/arbor.svg?raw';
import calendarIcon from '@assets/icons/calendar.svg?raw';
import howlIcon from '@assets/icons/howl.svg?raw';
import lettersIcon from '@assets/icons/letters.svg?raw';
import marketIcon from '@assets/icons/market.svg?raw';
import oracleIcon from '@assets/icons/oracle.svg?raw';
import poolIcon from '@assets/icons/pool.svg?raw';
import settingsIcon from '@assets/icons/settings.svg?raw';
import walletIcon from '@assets/icons/wallet.svg?raw';
import { addIcon } from 'iconify-icon';
addIcon('arx:letters', {
width: 52,
height: 46,
body: lettersIcon,
});
addIcon('arx:howl', {
width: 48,
height: 55,
body: howlIcon,
});
addIcon('arx:calendar', {
width: 51,
height: 51,
body: calendarIcon,
});
addIcon('arx:arbor', {
width: 40,
height: 52,
body: arborIcon,
});
addIcon('arx:market', {
width: 48,
height: 44,
body: marketIcon,
});
addIcon('arx:wallet', {
width: 50,
height: 49,
body: walletIcon,
});
addIcon('arx:pool', {
width: 49,
height: 45,
body: poolIcon,
});
addIcon('arx:oracle', {
width: 49,
height: 44,
body: oracleIcon,
});
addIcon('arx:settings', {
width: 42,
height: 44,
body: settingsIcon,
});

View file

@ -0,0 +1,15 @@
<svg
width="40.039608"
height="52.527237"
viewBox="0 0 40.039608 52.527237"
version="1.1"
id="svg1"
xml:space="preserve"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs1" /><g
id="layer1"
transform="translate(-222.26565,-123.96073)"><path
fill="currentColor"
d="m 240.7398,165.76285 v 7.9375 c -2.12881,0 -4.50865,-0.30249 -6.60948,0.0392 -0.82036,0.13342 -1.55334,0.84386 -1.01096,1.69836 0.68907,1.08562 3.09514,0.64369 4.18086,0.64369 3.97936,0 9.38418,0.96805 13.21365,-0.0841 1.23413,-0.33908 1.23405,-1.87398 0,-2.21304 -2.02512,-0.55641 -4.76467,-0.0841 -6.86365,-0.0841 v -7.9375 c 2.82486,-0.0583 5.74458,-0.39114 8.46667,-1.17144 1.70605,-0.48904 3.31189,-1.25448 4.7625,-2.27958 9.47829,-6.69797 4.6268,-18.00061 -0.15904,-25.91773 -2.96729,-4.90873 -8.2379,-12.43543 -14.65763,-12.43338 -6.07697,0.002 -10.76388,7.13175 -13.67667,11.63963 -5.06755,7.84264 -10.47978,20.15759 -0.61083,26.80988 1.55411,1.04756 3.23299,1.75205 5.02708,2.28469 2.52513,0.74967 5.31349,1.01377 7.9375,1.06793 m 2.91042,-2.64575 c 0,-1.79717 -0.53832,-4.58229 0.61327,-6.08154 1.33511,-1.73818 3.39249,-2.96434 4.93788,-4.51221 0.51867,-0.51951 1.33859,-1.26331 1.01689,-2.08664 -0.93033,-2.38103 -5.56187,2.97579 -6.56804,3.68456 v -10.84792 c 0,-1.22749 0.35101,-3.17567 -0.6439,-4.11349 -1.02349,-0.96473 -2.01369,0.15521 -2.20609,1.20329 -0.45803,2.49515 -0.0604,5.40062 -0.0604,7.93729 -0.83903,-0.58408 -1.61249,-1.25107 -2.38125,-1.92763 -0.75509,-0.66453 -1.62186,-1.90747 -2.6446,-2.1436 -1.15689,-0.26711 -1.66045,1.06455 -1.12264,1.90965 1.42696,2.24231 5.0207,3.3653 5.98885,5.87616 1.12987,2.93031 0.15964,7.95856 0.15964,11.10208 -2.26299,-0.006 -4.6865,-0.25665 -6.87916,-0.83499 -1.48053,-0.39051 -2.92423,-0.99456 -4.23334,-1.79145 -8.95699,-5.45228 -3.2979,-17.00133 0.97769,-23.56731 2.43511,-3.73956 6.61801,-10.29751 11.72231,-10.29751 4.94348,0 9.25377,6.67706 11.64086,10.29751 4.54708,6.89642 9.76862,17.84656 0.79456,23.62734 -1.28569,0.8282 -2.75253,1.42344 -4.23333,1.80021 -2.22805,0.56689 -4.58661,0.76594 -6.87917,0.7662"
id="path2" /></g></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -0,0 +1,15 @@
<svg
width="51.278458"
height="51.425667"
viewBox="0 0 51.278458 51.425667"
version="1.1"
id="svg1"
xml:space="preserve"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs1" /><g
id="layer1"
transform="translate(-444.0572,-101.62492)"><path
id="path122"
fill="currentColor"
d="m 454.70902,106.8615 c -3.19761,0 -6.56741,-0.47993 -8.94601,2.12727 -2.21765,2.43078 -1.63732,5.9542 -1.63732,8.98523 v 21.16666 c 0,5.06957 -0.99214,13.56232 6.35,13.75446 10.39108,0.27191 20.82578,0.004 31.22083,0.004 3.78326,0 8.99806,1.00047 11.81927,-2.14137 2.29833,-2.55954 1.67448,-6.33069 1.67448,-9.50029 v -21.96055 c 0,-2.95962 0.71315,-6.93903 -1.01955,-9.52479 -2.10019,-3.13417 -5.44385,-2.91062 -8.77003,-2.91062 0,-1.21181 0.17592,-2.61389 -0.53163,-3.67558 -2.72743,-4.092633 -5.81805,0.83525 -5.81837,3.67558 h -18.25625 c -0.0278,-1.33976 -0.16425,-2.88647 -1.13204,-3.9122 -2.81339,-2.981842 -5.83114,0.91466 -4.95338,3.9122 m 3.17502,-2.38122 0.26458,2.38125 h -1.05833 l 0.26458,-2.38125 h 0.52917 m 24.60625,0 0.26458,2.38125 h -1.05833 l 0.26458,-2.38125 h 0.52917 m -27.78125,5.02708 c 0,1.00729 -0.26764,2.50246 0.27193,3.39937 1.71689,2.85393 2.30559,-2.34217 2.10932,-3.39937 h 25.66458 c 0,4.31724 2.64584,4.35972 2.64584,0 1.44201,0 3.10649,-0.22424 4.49791,0.21273 3.45176,1.08403 2.64584,5.45298 2.64584,8.25394 h -45.77294 c 0,-2.65657 -0.80331,-6.63763 2.11768,-8.05061 1.66857,-0.80714 4.01509,-0.41606 5.81982,-0.41606 m 22.75417,11.11247 v 6.61458 l -2.64584,1.16327 -2.98677,-0.0168 0.59163,2.55765 -0.51527,2.64583 c -0.4492,0.0358 -0.88789,0.0677 -1.32925,0.16782 -4.10143,0.93035 1.4149,3.07357 1.87474,4.5957 0.35894,1.18815 -1.41492,2.96805 -0.44608,3.79644 0.85994,0.73527 1.90867,-0.38477 2.811,-0.4085 0.88052,-0.0232 1.80753,0.60298 2.64584,0.84437 -0.11557,1.46421 0.42271,6.7452 -0.68882,7.62555 -1.2819,1.01528 -5.13339,0.31195 -6.71952,0.31195 h -15.61041 c -2.11941,0 -4.86634,0.28968 -6.47372,-1.41294 -1.87881,-1.99013 -1.1992,-5.60953 -1.1992,-8.11206 v -20.37286 h 30.69167 m 4.7625,6.61458 -2.11667,-0.26458 c 0.003,-3.41657 1.72788,-2.62019 2.11667,0.26458 M 461.3236,124.4469 c -6.03811,1.08442 -10.08007,6.48864 -9.1808,12.57709 0.66984,4.53514 6.43336,9.82676 11.29645,8.84436 1.46845,-0.29664 2.16747,-1.62781 1.62691,-3.02352 -1.40184,-3.61944 -4.00982,-5.76914 -3.07661,-10.05417 0.47148,-2.16489 3.50194,-4.6106 3.3616,-6.61336 -0.13229,-1.88806 -2.66114,-1.9758 -4.02755,-1.7304 m 0.52917,2.52293 c -0.58096,1.38147 -1.53574,2.52775 -2.02255,3.96875 -1.53579,4.54613 0.0978,8.22269 2.28713,12.17083 -5.248,-0.24038 -8.91156,-5.92399 -7.01982,-10.84792 1.1151,-2.9024 3.84279,-4.61753 6.75524,-5.29166 m 26.72292,0.79375 c -0.55031,4.94109 -4.94361,0.27062 0,0 m -9.78959,1.67365 c 7.72196,-1.2965 9.3983,10.09438 1.5875,11.1576 -7.07471,0.96304 -8.75576,-9.95407 -1.5875,-11.1576 m 9.26042,6.52843 v -1.85208 c 3.23527,3.7e-4 3.17789,1.78438 0,1.85208 m 0.79375,6.35 c -3.82709,0.93226 -2.89125,-4.41063 -0.46078,-1.06894 0.24535,0.33733 0.32929,0.68221 0.46078,1.06894 m -6.61458,0.26458 c -0.0608,0.45117 -0.12538,0.88688 -0.23806,1.32925 -0.85928,3.37347 -3.44313,-0.55408 0.23806,-1.32925" /></g></svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

15
src/assets/icons/howl.svg Normal file
View file

@ -0,0 +1,15 @@
<svg
width="48.373081"
height="55.578983"
viewBox="0 0 48.373081 55.578983"
version="1.1"
id="svg1"
xml:space="preserve"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs1" /><g
id="layer1"
transform="translate(-373.7764,-99.170626)"><path
id="path12"
fill="currentColor"
d="m 391.20902,126.44066 c -1.65061,5.81649 -6.57957,9.99022 -7.40833,16.13958 h -0.26458 c -0.70233,-2.574 -1.05834,-4.72503 -1.05834,-7.40833 0,-0.91748 0.30246,-2.56821 -0.42423,-3.27892 -1.98111,-1.93753 -6.02782,3.49441 -6.77444,4.86642 -2.6164,4.80797 -1.82557,11.02536 2.44659,14.69376 5.38053,4.62011 13.77321,3.03332 20.3625,3.03332 1.99816,0 5.59497,0.68633 7.3479,-0.38544 3.2062,-1.96033 -1.00132,-5.98592 -3.11457,-6.49372 1.37462,-3.10568 0.61187,-6.23649 -2.11687,-8.36254 -1.18526,-0.92348 -4.20673,-2.22995 -5.33474,-0.5598 -0.3831,0.56723 -0.24599,1.31264 0.38258,1.61996 2.17754,1.06461 5.34675,1.12394 5.24145,4.54426 -0.0404,1.31071 -1.99403,3.02525 -1.12611,4.2319 0.95602,1.32913 3.15171,1.20121 4.27661,2.75955 h -8.20208 c -2.22213,0 -6.1103,0.75614 -8.14166,-0.15965 -1.68246,-0.7585 -1.15186,-5.21443 -1.11488,-6.71952 0.1643,-6.68824 5.64564,-11.79048 7.48468,-17.97716 0.34158,-1.14911 2.3585,-1.5222 2.04419,-2.89716 -0.22297,-0.97539 -1.33211,-1.05518 -2.12442,-0.88092 -3.64794,0.80236 -6.27697,0.74986 -9.26041,-1.79267 2.5149,-0.6029 5.67696,-2.47893 7.30503,-4.50179 1.11193,-1.38155 1.60271,-2.96203 3.01473,-4.14248 1.92884,-1.61252 4.81542,-1.83106 6.56478,-3.56188 1.23981,-1.22669 2.30228,-2.94073 3.14131,-4.4626 0.34074,-0.61805 0.70504,-1.81659 1.45113,-2.06195 1.74285,-0.57317 2.84227,4.66962 3.0131,5.76612 0.30589,1.9634 -1.3697,7.26806 2.49742,5.72773 0.80851,-0.32204 1.54116,-0.72323 2.38125,-0.96523 -0.30214,3.01128 -2.85342,5.20777 -3.35343,8.20208 -0.59063,3.53693 1.57536,6.1743 1.98172,9.525 0.6255,5.15776 -1.94628,9.08762 0.39585,14.2875 0.60995,1.35414 1.32317,3.2015 2.58806,4.07368 1.57915,1.08889 2.69637,0.73071 3.94405,2.5409 -1.7015,0 -3.94675,0.38139 -5.55605,-0.2364 -2.68845,-1.03206 -4.06019,-5.47157 -4.89091,-7.96568 -0.5215,-1.56574 -0.79029,-6.42303 -2.53171,-6.96859 -2.77029,-0.86787 -0.8155,5.15342 -0.5457,6.17484 1.5426,5.83998 3.90584,11.64166 10.61395,11.64166 1.30735,0 4.73467,0.61552 5.3966,-0.85418 1.84801,-4.10311 -3.05996,-5.32934 -5.04505,-7.1664 -1.15578,-1.06959 -1.74698,-2.94667 -2.19322,-4.41483 -1.17665,-3.87125 0.89951,-7.67037 0.47282,-11.64167 -0.29835,-2.77677 -2.45166,-5.37784 -1.98458,-8.20208 0.60756,-3.67366 5.17031,-7.58113 2.7769,-11.60247 -0.98085,-1.648 -3.00281,-0.0197 -4.18597,0.48997 0,-3.34987 -0.55609,-7.48707 -2.93819,-10.05008 -0.78501,-0.84462 -2.126,-2.105701 -3.41079,-1.837387 -2.15564,0.450182 -3.75492,4.948827 -4.99462,6.595587 -1.81798,2.41493 -4.54008,2.71975 -6.91265,4.44138 -1.8192,1.32009 -2.4261,3.16301 -3.80747,4.79537 -1.40352,1.65855 -3.7454,2.91133 -5.71753,3.76173 -1.02516,0.44206 -2.60665,0.75598 -2.79752,2.08707 -0.16634,1.16002 0.98399,2.17763 1.7402,2.88162 2.46269,2.29261 5.25211,2.67054 8.46565,2.67054 m -11.37708,8.99583 c 0,6.07723 3.14634,10.37028 4.23333,16.13959 -7.70219,-0.67671 -10.2839,-11.67289 -4.23333,-16.13959 m 20.90229,-22.21639 c -2.12454,1.03159 -2.17034,4.79076 0.51753,3.38344 1.75412,-0.91844 1.98561,-4.59887 -0.51753,-3.38344" /></g></svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

View file

@ -0,0 +1,15 @@
<svg
width="52.115841"
height="46.76223"
viewBox="0 0 52.115841 46.76223"
version="1.1"
id="svg1"
xml:space="preserve"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs1" /><g
id="layer1"
transform="translate(-358.17974,-252.02887)"><path
fill="currentColor"
d="m 397.28739,264.99344 c -1.40599,-10.88693 -10.92028,-12.96458 -20.10834,-12.96458 -4.77787,0 -9.23968,0.12302 -13.22916,3.14847 -5.98978,4.54239 -7.06816,14.53774 -4.37808,21.19319 0.87644,2.16838 3.01313,3.76017 3.79277,5.82083 0.76549,2.02328 -1.67974,4.4397 -0.38891,6.55803 1.98627,3.25961 7.47102,-1.61671 9.44088,-2.72993 1.00571,-0.56836 2.59306,-1.07839 3.48777,-0.0398 1.8852,2.18832 2.50599,4.7496 4.9789,6.65502 5.0549,3.89489 10.2043,1.33597 15.61042,2.95757 2.09565,0.62862 5.95608,4.40808 8.16288,2.80671 1.94012,-1.40786 0.0334,-4.3627 0.8403,-6.1534 1.1982,-2.65904 3.37225,-4.44576 4.18413,-7.40833 2.39052,-8.72298 -2.28592,-19.63397 -12.39356,-19.84375 m -32.01457,21.96041 c 0.21828,-1.62096 1.3393,-3.67673 0.93972,-5.29166 -0.47287,-1.91116 -2.7399,-3.24108 -3.63577,-5.02709 -2.33028,-4.64562 -2.1349,-11.31022 0.25785,-15.87499 3.45112,-6.5839 11.61756,-6.5495 18.04862,-6.10909 3.85358,0.2639 7.3102,0.74311 10.30813,3.51206 5.70409,5.26841 4.98496,17.72041 -1.3123,22.23213 -5.28123,3.78378 -10.67902,1.36889 -16.40416,2.44903 -3.06714,0.57866 -5.22896,3.25149 -8.20209,4.10961 m 37.57082,8.73125 c -2.04656,-0.85206 -4.14391,-2.51759 -6.35,-2.85407 -7.08037,-1.07991 -14.21872,1.18216 -17.72709,-7.20009 3.35658,0 6.64649,-0.11779 9.78959,-1.44581 6.67288,-2.81944 8.99583,-9.86083 8.99583,-16.54585 1.75384,0.362 3.49724,0.80936 5.02708,1.77552 5.25432,3.3183 6.33047,11.09689 3.88607,16.48072 -0.86529,1.90581 -3.1352,3.40125 -3.70274,5.29167 -0.37206,1.23929 0.0813,3.17596 0.0813,4.49791"
id="path115" /></g></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View file

@ -0,0 +1,15 @@
<svg
width="48.970749"
height="44.663296"
viewBox="0 0 48.970749 44.663296"
version="1.1"
id="svg1"
xml:space="preserve"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs1" /><g
id="layer1"
transform="translate(25.898366,-196.75692)"><path
fill="currentColor"
d="m -21.117011,197.13272 c -3.45885,0.67086 -3.807992,6.52957 -4.525681,9.40107 -0.902247,3.60992 0.844618,5.38243 1.213694,8.73125 0.58923,5.34638 0.07305,11.3194 -0.106159,16.71387 -0.07531,2.26682 -0.608849,5.71499 0.886026,7.61617 2.102019,2.67332 7.457615,1.59913 10.469619,1.59913 H 7.7225714 c 3.2456176,0 7.1526626,0.59189 10.3187486,-0.14005 4.375573,-1.01153 3.175,-6.53031 3.175,-9.91412 0,-5.16609 -0.609507,-10.75231 0.03512,-15.875 0.201086,-1.59798 1.391367,-2.89721 1.69325,-4.49792 0.558615,-2.96199 -0.854219,-6.16306 -1.562594,-8.99583 -0.299759,-1.19873 -0.470473,-2.69748 -1.334959,-3.64393 -1.975126,-2.16242 -7.549519,-1.11857 -10.2078956,-1.11857 H -10.798262 c -3.301582,0 -7.074319,-0.50534 -10.318749,0.12393 m 11.112499,2.52191 c -2.91e-4,2.63414 -0.290882,5.32196 -0.604703,7.9375 -0.247568,2.06334 -0.542584,3.97835 -2.068896,5.53154 -2.767129,2.81588 -8.186093,2.62792 -10.180536,-1.03363 -1.311467,-2.4077 -0.255178,-5.22629 0.388096,-7.67291 0.295443,-1.12368 0.533001,-3.77403 1.502777,-4.49057 0.728285,-0.53813 2.173364,-0.27193 3.025763,-0.27193 h 7.937499 m 14.2874994,0 c 0.165571,3.85616 1.912711,8.5829 -0.140862,12.17083 -2.656019,4.64049 -9.80335,4.18364 -11.827653,-0.79375 -1.39569,-3.43174 0.316966,-7.79636 0.326849,-11.37708 h 11.641666 m 2.645834,0 H 14.86632 c 0.87204,0 2.285471,-0.25816 3.040056,0.27193 1.044247,0.73359 1.203358,3.07405 1.491341,4.22598 0.605793,2.42317 1.707026,5.26187 0.495686,7.67272 -2.274384,4.52654 -8.827816,4.00979 -11.2503056,2e-4 -0.865515,-1.43257 -0.862148,-2.89417 -1.072827,-4.49792 -0.327119,-2.49012 -0.620658,-5.16136 -0.641449,-7.67291 m -16.404169,13.49372 c 4.260269,5.23092 11.640007,6.34338 15.610416,0 h 0.529166 c 2.837,4.90474 7.4612446,4.7251 12.1708326,3.43959 v 15.875 c 0,1.46606 0.585478,4.59285 -0.6439,5.66118 -1.120709,0.97391 -4.041638,0.42423 -5.441516,0.42423 H -2.0670116 c 0.565368,-3.14441 0.529441,-7.4078 0.112283,-10.58333 -0.138693,-1.05578 0.02283,-2.23507 -0.573263,-3.17091 -1.848815,-2.90259 -11.4023664,-2.98201 -13.4259684,-0.25724 -0.92976,1.25193 -0.665134,3.01952 -0.665134,4.48648 v 9.525 c -1.247905,0 -3.874985,0.49472 -4.830688,-0.46098 -1.048153,-1.04815 -0.460979,-4.24842 -0.460979,-5.62443 v -15.875 c 5.667189,1.54629 8.233166,0.67686 12.4354164,-3.43959 m 14.034551,10.25053 c -1.985647,0.80916 -1.959909,7.20925 -0.873574,8.77066 1.154754,1.65973 6.5150086,1.58136 8.2661896,0.8907 2.147063,-0.84675 2.401343,-7.32205 1.130604,-9.00358 -1.105214,-1.46251 -6.9241566,-1.30942 -8.5232196,-0.65778 m -9.007468,15.14949 h -9.7895824 v -8.99583 l 0.311945,-3.54452 4.4505544,-0.42423 4.602852,0.42423 0.424231,3.27994 v 9.26041 m 15.3458316,-12.96458 0.264584,5.02708 H 5.6059044 v -5.02708 H 10.89757"
id="path10" /></g></svg>

After

Width:  |  Height:  |  Size: 3 KiB

View file

@ -0,0 +1,18 @@
<svg
width="49.721642"
height="44.373489"
viewBox="0 0 49.721642 44.373489"
version="1.1"
id="svg1"
xml:space="preserve"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs1" /><g
id="layer1"
transform="translate(-444.72745,-194.40505)"><path
fill="currentColor"
d="m 491.75067,225.13027 -3.96875,2.38125 c -0.22685,-2.9761 -5.58574,-3.14716 -7.9375,-3.47408 -6.07972,-0.8452 -12.68821,-0.87866 -18.78542,-0.15454 -1.5143,0.17983 -4.89981,0.0374 -5.92168,1.33045 -0.78418,0.99227 0.10538,1.97416 1.16327,1.99438 2.74686,0.0525 5.99388,-0.50795 8.72716,-0.86683 2.60533,-0.34208 5.31206,-0.15246 7.9375,-0.1523 4.07722,2.6e-4 7.93593,0.76801 11.90625,1.5875 v 0.26458 c -11.98028,2.47148 -26.22182,3.49475 -37.30625,-2.64583 3.44524,-2.92695 8.40903,-3.12235 12.7,-3.62862 1.00808,-0.11895 4.0722,0.25753 3.51573,-1.65161 -0.53347,-1.83022 -4.03519,-0.97906 -5.36781,-0.85011 -3.66057,0.35424 -7.59129,0.78401 -10.84792,2.65891 -1.2606,0.72575 -2.81276,1.8729 -2.83671,3.47143 -0.0943,6.29245 6.15471,10.34595 11.56796,11.73966 8.49806,2.18791 18.26142,2.2538 26.72292,-0.17496 3.97842,-1.14195 7.83997,-2.90127 10.13072,-6.53762 4.91681,-7.80494 -5.29097,-10.50363 -11.18906,-11.06757 -1.47074,-0.14062 -5.8316,-1.16661 -6.43941,0.76312 -0.57251,1.81765 2.24448,1.59158 3.26441,1.72551 4.816,0.63234 8.47059,1.42518 12.96459,3.28728 m -43.12709,3.70416 c 3.73629,2.17255 8.46641,2.76565 12.7,3.2189 10.32641,1.10555 19.73081,-0.12344 29.63334,-2.95431 -2.12089,3.00253 -5.80719,4.50058 -9.26042,5.38966 -7.30844,1.88163 -15.09278,1.92904 -22.48958,0.53407 -2.97728,-0.56149 -6.0578,-1.51909 -8.46667,-3.41489 -0.85772,-0.67502 -2.141,-1.62332 -2.11667,-2.77343"
id="path129" /><path
fill="currentColor"
d="m 467.67358,194.55187 c -5.6648,0.81116 -10.88544,3.57401 -15.34583,7.0862 -1.35571,1.0675 -4.01038,2.80903 -4.14413,4.70675 -0.14477,2.05424 3.00037,4.12525 4.40871,5.23592 4.921,3.8809 11.94882,8.02768 18.52084,7.0862 5.88618,-0.84325 11.30586,-3.66932 15.875,-7.40732 1.37344,-1.1236 3.90056,-2.69592 4.07899,-4.65021 0.19818,-2.17061 -3.12808,-4.19348 -4.60816,-5.32229 -5.07524,-3.87072 -12.15861,-7.68414 -18.78542,-6.73525 m 6.61459,3.32632 c 5.27311,1.71872 10.01662,4.63232 14.02291,8.46666 v 0.26459 c -4.92688,4.53757 -11.53209,9.29021 -18.52083,9.51356 -6.96364,0.22257 -14.11713,-4.85812 -18.78542,-9.51356 v -0.26459 c 3.48114,-3.40932 8.55121,-7.351 13.49375,-8.20208 -2.0146,2.87121 -4.06992,4.94266 -3.65824,8.73125 1.05633,9.72119 15.90908,10.10578 17.10319,0.26458 0.2598,-2.14121 -0.0999,-4.27577 -1.30187,-6.08541 -0.70629,-1.06334 -2.05345,-1.93721 -2.35349,-3.175 m -6.08542,1.93906 c 7.71615,-1.35602 9.98382,10.50121 2.38125,11.95993 -7.57672,1.45375 -9.97408,-10.62561 -2.38125,-11.95993"
id="path128" /></g></svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

15
src/assets/icons/pool.svg Normal file
View file

@ -0,0 +1,15 @@
<svg
width="49.911552"
height="45.77253"
viewBox="0 0 49.911552 45.77253"
version="1.1"
id="svg1"
xml:space="preserve"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs1" /><g
id="layer1"
transform="translate(-485.34034,-171.20574)"><path
id="path159"
fill="currentColor"
d="m 510.05511,178.81945 c -1.57359,-3.32099 -3.43658,-6.0272 -7.14375,-7.13251 -1.11516,-0.33251 -4.92197,-1.29014 -4.60856,1.01954 0.1702,1.25432 2.09877,1.18853 3.02106,1.41376 1.96804,0.48062 3.66152,1.67255 4.77148,3.37629 3.0845,4.73458 2.63686,12.30605 2.63686,17.72708 0,1.4274 -0.73794,6.02034 0.61327,6.86774 2.51315,1.5761 2.03256,-3.70262 2.03256,-4.75107 0,-5.95317 -0.8977,-14.27484 2.31796,-19.57917 1.72918,-2.85234 3.96526,-2.76914 6.6491,-3.94036 0.71881,-0.3137 1.17005,-1.06666 0.76352,-1.82023 -0.71508,-1.32557 -2.79882,-0.65876 -3.90975,-0.34504 -3.71421,1.04893 -5.66234,3.83846 -7.14375,7.16397 m 17.21507,-3.92221 c -0.99661,0.13061 -1.18065,1.65229 -1.71837,2.30693 -0.83745,1.01958 -4.12299,2.14557 -1.96375,3.71337 0.66871,0.48554 1.41572,0.70732 1.96375,1.37007 0.50444,0.61004 0.71417,2.44829 1.70776,2.41841 1.04042,-0.0313 1.24734,-1.76087 1.75346,-2.41921 0.8246,-1.07264 4.69432,-1.98951 2.17976,-3.59597 -0.62197,-0.39736 -1.24973,-0.6658 -1.76287,-1.22289 -0.61726,-0.67012 -0.99006,-2.724 -2.15974,-2.57071 m -33.2487,6.61458 -1.42744,2.57152 -2.63155,1.7247 -0.83027,1.16551 2.39063,1.55667 1.93599,2.94595 1.1447,0.48589 1.38049,-2.29061 2.4137,-1.86659 0.83028,-1.04872 -2.61009,-1.99868 -1.4593,-2.72259 -1.13714,-0.52305 m 8.09613,13.29488 c -6.3301,0.83154 -18.56886,1.09803 -16.55646,10.20647 0.26528,1.20067 0.59588,2.33664 1.14734,3.43958 0.72856,1.45711 1.66268,2.77971 2.97473,3.76745 2.77796,2.09133 6.16528,3.04505 9.52398,3.69295 8.31578,1.6041 18.6872,1.56689 26.72291,-1.29353 4.81129,-1.71265 11.53362,-8.26771 8.61039,-14.10437 -2.3416,-4.67538 -11.23232,-5.15563 -15.75414,-5.57605 -1.28286,-0.11929 -4.09721,-0.87491 -5.131,0.17945 -1.84779,1.88454 2.08062,2.30005 3.01434,2.41064 4.05841,0.48069 8.3526,0.63299 12.17083,2.26837 0.96704,0.41418 3.4983,1.39203 2.93125,2.83038 -0.98846,2.50724 -6.19528,3.40554 -8.4875,3.86688 -8.51983,1.71469 -17.67578,1.70394 -26.19375,0 -2.49523,-0.49916 -5.26496,-1.13312 -7.40812,-2.56786 -0.85323,-0.57119 -2.04453,-1.48615 -1.22351,-2.56906 2.26966,-2.99359 9.42041,-3.47696 12.86496,-3.72969 1.24799,-0.0916 3.62291,0.14476 4.6784,-0.55999 0.70002,-0.4674 0.82631,-1.80742 0,-2.21426 -0.98988,-0.48737 -2.82848,-0.1861 -3.88465,-0.0474 m -12.7,12.05853 c 4.04223,2.15276 8.98457,2.83523 13.49375,3.19767 9.99933,0.80373 18.7626,-0.4682 28.31042,-3.19767 -1.59402,3.62953 -5.12554,5.02355 -8.73125,5.91963 -7.23833,1.79889 -15.43116,1.70377 -22.75417,0.4955 -2.77592,-0.45802 -10.89255,-2.45631 -10.31875,-6.41513 m 29.63334,-4.49788 -9.26042,0.786 -8.73125,-0.786 c -0.002,-0.60492 0.16226,-1.68559 -0.42423,-2.08563 -1.15689,-0.78913 -4.90382,0.46519 -4.74412,2.08563 0.21736,2.20568 4.57866,2.52966 6.22668,2.79447 5.28009,0.84841 10.87014,0.90351 16.13959,-0.0466 1.6202,-0.29211 4.2504,-0.44755 5.40374,-1.76307 1.8744,-2.13796 -4.92551,-5.6829 -4.60999,-0.98484" /></g></svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View file

@ -0,0 +1,15 @@
<svg
width="42.581379"
height="44.50452"
viewBox="0 0 42.581379 44.50452"
version="1.1"
id="svg1"
xml:space="preserve"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs1" /><g
id="layer1"
transform="translate(-513.56562,-191.36311)"><path
fill="currentColor"
d="m 532.49652,191.64146 c -4.39731,0.63872 -2.56173,4.78288 -5.56034,6.77403 -2.95736,1.96376 -6.07138,-1.80106 -8.96704,0.62532 -2.11951,1.77602 -5.29879,6.30373 -3.8726,9.1561 0.92264,1.84528 3.27214,2.7776 3.30975,5.02708 0.0453,2.71082 -2.46408,3.38655 -3.49184,5.55625 -1.33779,2.82418 1.47505,6.68912 3.32321,8.70347 2.92261,3.18543 6.27113,-0.65416 9.43394,1.28067 2.11104,1.29141 1.45151,3.5743 2.78568,5.35006 1.6224,2.1594 5.66911,1.84637 8.06632,1.52236 4.52395,-0.61143 2.75201,-5.10792 5.82492,-6.97348 2.81455,-1.70871 5.88816,1.56386 8.71655,-0.72699 2.33704,-1.89287 5.23956,-6.44838 3.60434,-9.41966 -1.04248,-1.89421 -3.32097,-2.59314 -3.32097,-5.0281 0,-2.42603 2.32857,-3.3277 3.31055,-5.29166 1.41947,-2.83893 -1.32225,-7.02785 -3.36753,-8.90581 -2.89674,-2.65977 -5.74302,1.04072 -8.42807,-0.5608 -2.4985,-1.49027 -1.75431,-3.83447 -3.33649,-5.85045 -1.56372,-1.99246 -5.79257,-1.56344 -8.03038,-1.23839 m 0.53323,2.62358 c 1.26588,-0.26796 4.10261,-0.47757 5.15264,0.40055 1.01087,0.84538 0.88937,2.61199 1.406,3.74072 0.64467,1.40842 2.18735,2.31554 3.49144,3.05209 2.55735,1.44442 5.74141,-1.99739 7.73641,0.18435 1.00156,1.0953 2.8298,3.382 2.49333,4.96666 -0.3292,1.55039 -2.09313,2.41535 -2.88959,3.70416 -0.79737,1.29027 -0.6337,3.82596 -0.42281,5.29167 0.31074,2.15969 3.53793,2.95804 3.51553,5.03096 -0.0161,1.49228 -1.81055,3.95999 -2.84611,4.96174 -1.57495,1.52354 -3.45152,-0.16397 -5.20551,-0.37094 -1.48638,-0.17537 -3.04285,0.75743 -4.22925,1.55525 -2.05375,1.3811 -1.64232,5.78247 -3.97773,6.22506 -1.3731,0.26022 -4.37865,0.46774 -5.43683,-0.58348 -0.90976,-0.90377 -0.77784,-2.47932 -1.32332,-3.58923 -0.66636,-1.35586 -2.00913,-2.19111 -3.28912,-2.89999 -2.63062,-1.45688 -6.41482,1.51188 -8.26558,-0.57898 -0.9788,-1.1058 -3.26811,-3.92646 -2.52354,-5.49338 0.98865,-2.08056 3.48183,-2.68094 3.63394,-5.31534 0.087,-1.50666 0.15038,-3.19558 -0.74374,-4.4875 -0.81381,-1.17589 -2.58529,-1.9438 -2.85039,-3.45 -0.17283,-0.98192 0.54289,-1.88054 1.04813,-2.64584 0.65538,-0.99273 1.36908,-2.54787 2.56804,-2.96798 1.77899,-0.62334 3.70635,1.24635 5.54564,1.04874 1.23405,-0.1326 2.45479,-1.05871 3.43857,-1.75879 2.01717,-1.4355 1.62963,-5.52428 3.97385,-6.0205 m 1.58341,11.17888 c -11.03424,0.44841 -9.60319,16.76802 1.32292,16.23614 10.25302,-0.4991 8.86751,-16.65024 -1.32292,-16.23614 m -0.79375,2.84019 c 6.86754,-1.22632 9.37097,9.42319 2.38125,10.67357 -6.97499,1.24773 -9.38056,-9.42375 -2.38125,-10.67357"
id="path126" /></g></svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View file

@ -0,0 +1,15 @@
<svg
width="50.380646"
height="49.466183"
viewBox="0 0 50.380646 49.466183"
version="1.1"
id="svg1"
xml:space="preserve"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs1" /><g
id="layer1"
transform="translate(-376.19943,-188.11239)"><path
fill="currentColor"
d="m 422.95902,228.57028 c 4.14241,-0.83637 4.12926,-10.03343 2.91185,-13.22916 -0.52062,-1.36665 -1.50643,-2.31078 -2.91185,-2.64584 0,-3.09737 0.0819,-6.54769 -3.43958,-7.67291 4.46137,-10.3694 -7.14017,-20.11134 -16.93333,-15.90869 -2.23451,0.95892 -4.08497,2.48283 -5.4113,4.5316 -0.72822,1.12489 -1.11596,3.49858 -2.5407,3.88465 -5.05834,1.37065 -15.21975,-2.97233 -17.72442,3.52369 -1.14751,2.97611 -0.51733,6.9166 -0.51733,10.05416 0,6.47222 -0.44959,13.12572 0.0239,19.57917 0.18713,2.55057 1.61147,4.91294 3.94487,6.04866 2.57935,1.25545 6.45127,0.56592 9.26041,0.56592 h 20.90209 c 2.71375,0 7.40537,0.89471 9.78938,-0.53162 2.99788,-1.79356 2.64603,-5.18964 2.64603,-8.19963 M 411.58196,204.4932 c 0.45959,-1.73781 0.54573,-2.70426 -0.52917,-4.23333 v -0.26459 c 1.0056,-1.8109 0.19175,-3.05081 -1.32291,-4.23333 -0.0447,-1.55397 -1.20998,-1.28905 -1.5875,0 h -0.79375 c -0.24581,-1.53679 -1.34169,-1.53679 -1.5875,0 -2.55964,0.52269 -1.11284,6.36621 -2.11667,8.73125 -1.27255,0 -3.81969,0.48852 -4.89132,-0.27193 -1.19179,-0.84572 -0.99866,-3.49263 -0.90563,-4.75515 0.37831,-5.13388 5.07913,-8.76536 10.03028,-8.73003 4.99877,0.0357 9.54681,3.86879 9.77917,8.99461 0.0553,1.22021 0.22947,3.67552 -0.9191,4.49057 -1.11502,0.79125 -3.82869,0.27193 -5.1559,0.27193 m -5.29171,-4.76293 v -1.85208 c 3.82488,0 3.78187,1.85166 0,1.85208 m -11.1125,0.52917 0.52917,4.23333 c -4.23316,0 -8.46689,0.0205 -12.7,-1.9e-4 -1.39253,-0.007 -3.58331,-0.22971 -3.58331,-2.11648 0,-1.93423 2.16781,-2.11431 3.58331,-2.11666 h 12.17083 m 11.1125,3.175 v -1.85209 c 4.29192,0 4.29192,1.85209 0,1.85209 m 14.02292,10.84791 c -3.5483,0 -7.75573,-0.72841 -10.8469,1.35069 -4.60095,3.09455 -4.4004,10.37749 0.79273,12.79023 2.90295,1.3487 6.93221,0.67575 10.05417,0.67575 0,1.914 0.46958,4.38936 -1.59159,5.40477 -2.31284,1.13937 -6.19823,0.41606 -8.72716,0.41606 h -19.84375 c -2.76357,0 -6.89675,0.77783 -9.24898,-0.96544 -2.41027,-1.7863 -1.86352,-5.14562 -1.86352,-7.76581 v -19.84375 c 3.92504,1.04929 7.85467,0.79375 11.90625,0.79375 h 17.19792 7.67291 c 1.07294,0 2.46288,-0.21416 3.38303,0.46099 1.78508,1.30974 1.11489,4.76059 1.11489,6.68276 m 3.175,2.64584 c 0,2.24401 0.75836,6.18376 -0.31195,8.19045 -0.75367,1.41303 -2.2735,1.33401 -3.6568,1.33455 -2.99197,0.001 -7.22064,0.82449 -9.46845,-1.61241 -2.3274,-2.5232 -0.8511,-6.74842 2.3247,-7.66414 1.19515,-0.34462 2.47492,-0.24845 3.70417,-0.24845 h 7.40833 m -10.57925,2.80793 c -2.62779,0.64098 -1.68539,4.89676 1.05302,4.25048 2.77349,-0.65454 1.67838,-4.91675 -1.05302,-4.25048"
id="path125" /></g></svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View file

@ -60,12 +60,12 @@ export class ArborForumCategory extends LitElement {
<div class="category-header">
<span>${this.title}</span>
<arx-button
label="New Topic"
href="/arbor/new-topic/${this.id}"
label="New Thread"
href="/arbor/new-thread/${this.id}"
></arx-button>
</div>
<slot>
<div class="empty-state">No topics yet...</div>
<div class="empty-state">No threads yet...</div>
</slot>
</div>
`;

View file

@ -12,7 +12,7 @@ import '@components/MarkdownContent';
@customElement('arx-forum-post')
export class ForumPost extends LitElement {
@property({ type: String }) override id = '';
@property({ type: String }) topicId = '';
@property({ type: String }) threadId = '';
@property({ type: String }) npub = '';
@property({ type: Date }) date = new Date();
@property({ type: String }) content = '';
@ -217,12 +217,12 @@ export class ForumPost extends LitElement {
}
private _copyPermalink() {
const permalink = `eve://phora/topics/${this.topicId}#post-${this.id}`;
const permalink = `eve://phora/threads/${this.threadId}#post-${this.id}`;
navigator.clipboard.writeText(permalink);
}
override render() {
const permalink = `eve://phora/topics/${this.topicId}#post-${this.id}`;
const permalink = `eve://phora/threads/${this.threadId}#post-${this.id}`;
const postClasses = {
post: true,

View file

@ -1,14 +1,14 @@
import { LitElement, css, html } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import '@components/EveLink';
import formatDateTime from '@/utils/formatDateTime';
import '@components/EveLink';
@customElement('arx-arbor-forum-topic')
export class ArborForumTopic extends LitElement {
@customElement('arx-arbor-forum-thread')
export class ArborForumThread extends LitElement {
static override styles = [
css`
.topic {
.thread {
display: grid;
grid-template-columns: 3fr 1fr;
padding: 1.75rem;
@ -25,7 +25,7 @@ export class ArborForumTopic extends LitElement {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.topic:hover {
.thread:hover {
transform: translateY(-2px);
box-shadow: calc(var(--depth) * 4px) calc(var(--depth) * 4px)
calc(var(--depth) * 8px)
@ -35,7 +35,7 @@ export class ArborForumTopic extends LitElement {
oklch(from var(--color-base-100) l c h / 0.5);
}
.topic-icon {
.thread-icon {
inline-size: 40px;
block-size: 40px;
background: var(--color-accent);
@ -48,7 +48,7 @@ export class ArborForumTopic extends LitElement {
oklch(from var(--color-base-content) l c h / 0.15);
}
.topic-icon::after {
.thread-icon::after {
content: "";
position: absolute;
inset: 0;
@ -59,13 +59,13 @@ export class ArborForumTopic extends LitElement {
);
}
.topic-info {
.thread-info {
display: flex;
gap: 1.5rem;
align-items: flex-start;
}
.topic-details {
.thread-details {
flex: 1;
}
@ -85,7 +85,7 @@ export class ArborForumTopic extends LitElement {
transform: translateX(4px);
}
.topic-details p {
.thread-details p {
margin: 0;
font-size: 0.975rem;
line-height: 1.6;
@ -156,7 +156,7 @@ export class ArborForumTopic extends LitElement {
}
@media (max-width: 968px) {
.topic {
.thread {
grid-template-columns: 1fr;
gap: 1.5rem;
padding: 1.25rem;
@ -188,13 +188,13 @@ export class ArborForumTopic extends LitElement {
override render() {
return html`
<div class="topic">
<div class="topic-info">
<div class="topic-icon"></div>
<div class="topic-details">
<div class="thread">
<div class="thread-info">
<div class="thread-icon"></div>
<div class="thread-details">
<arx-eve-link
class="${this.status}"
href="/arbor/topics/${this.id}"
href="/arbor/threads/${this.id}"
>
${this.title}
</arx-eve-link>

View file

@ -116,9 +116,12 @@ export class EveDialog extends LitElement {
override connectedCallback() {
super.connectedCallback();
document.addEventListener('keydown', this._handleKeyDown);
this.rootWindow = document.body
.querySelector('arx-eve-router')!
.shadowRoot!.querySelector('.window') as HTMLDivElement;
const router = document.querySelector('arx-eve-router');
if (router) {
this.rootWindow = router.shadowRoot!.querySelector('.window') as HTMLDivElement;
} else {
this.rootWindow = document.body as HTMLDivElement;
}
}
override disconnectedCallback() {
@ -168,7 +171,7 @@ export class EveDialog extends LitElement {
override render() {
return html`
<div
class="${classMap({ active: this.open })}"
class=${classMap({ active: this.open })}
@click=${this._handleOverlayClick}
style="--dialog-width: ${this.width}; --dialog-max-width: ${this.maxWidth};"
>

View file

@ -11,7 +11,7 @@ export class StyledSelect<T> extends LitElement {
@property() name = '';
@property({ type: Boolean }) required = false;
@property() label = '';
@property({ type: Array<T> }) options = [];
@property({ type: Array }) options: T[] = [];
@property() valueMapper?: (option: T) => string;
@property() textMapper?: (option: T) => string;
@ -106,15 +106,17 @@ export class StyledSelect<T> extends LitElement {
private _getOptionValue(option: T) {
if (option === undefined || option === null) return '';
if (this.valueMapper) return this.valueMapper(option);
if (typeof option === 'object' && 'value' in option) return option.value;
return option;
if (typeof option === 'object' && 'value' in (option as object))
return (option as unknown as { value: string }).value;
return String(option);
}
private _getOptionText(option: T) {
if (option === undefined || option === null) return '';
if (this.textMapper) return this.textMapper(option);
if (typeof option === 'object' && 'label' in option) return option.label;
return option;
if (typeof option === 'object' && 'label' in (option as object))
return (option as unknown as { label: string }).label;
return String(option);
}
override render() {
@ -144,7 +146,7 @@ export class StyledSelect<T> extends LitElement {
const optionText = this._getOptionText(option);
return html`
<option
value=${optionValue}
.value=${optionValue}
?selected=${optionValue === this.value}
>
${optionText}

View file

@ -1,5 +1,6 @@
import { LitElement, css, html } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { when } from 'lit/directives/when.js';
import { ArxInputChangeEvent } from './Input';
@ -134,7 +135,7 @@ export class StyledTextarea extends LitElement {
return html`
${when(this.label, () => html`<label for="textarea-${this.name}">${this.label}</label>`)}
<div class="${hasMaxlength ? 'has-maxlength' : ''}">
<div class=${classMap({ 'has-maxlength': hasMaxlength })}>
<textarea
id="textarea-${this.name}"
.value=${this.value}
@ -142,7 +143,7 @@ export class StyledTextarea extends LitElement {
?required=${this.required}
placeholder=${this.placeholder}
rows=${this.rows}
maxlength=${this.maxlength}
.maxlength=${Number.parseInt(this.maxlength) || -1}
name=${this.name}
@input=${this._handleInput}
@focus=${this._handleFocus}

View file

@ -172,7 +172,7 @@ export class Header extends LitElement {
</div>
<div class="nav-buttons">
<button @click=${this._goToWallet}>
<iconify-icon icon="material-symbols:wallet"></iconify-icon>
<iconify-icon icon="arx:wallet"></iconify-icon>
</button>
</div>
</header>
@ -290,11 +290,11 @@ export class Header extends LitElement {
window.location.hash = 'arbor';
break;
case 892:
window.location.hash = `arbor/topics/${suggestion.id}`;
window.location.hash = `arbor/threads/${suggestion.id}`;
break;
case 893: {
const threadId = suggestion.tags.find((tag) => tag[0] === 'E')?.[1]!;
window.location.hash = `arbor/topics/${threadId}`;
window.location.hash = `arbor/threads/${threadId}`;
break;
}
default:

View file

@ -1,10 +1,7 @@
import { ndk, setSigner } from '@/ndk';
import type { ArxInputChangeEvent } from '@components/General/Input';
import { animate } from '@lit-labs/motion';
import { randomBytes } from '@noble/ciphers/webcrypto';
import { NDKEvent, NDKKind, NDKPrivateKeySigner } from '@nostr-dev-kit/ndk';
import * as nip06 from '@nostr/tools/nip06';
import * as nip19 from '@nostr/tools/nip19';
import * as nip49 from '@nostr/tools/nip49';
import * as nostrTools from '@nostr/tools/pure';
import { encodeBase64 } from '@std/encoding/base64';
@ -12,18 +9,23 @@ import { LitElement, css, html } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { when } from 'lit/directives/when.js';
import { ndk, setSigner } from '@/ndk';
import '@components/General/Button';
import '@components/General/Fieldset';
import '@components/General/Input';
import '@components/LoadingView';
import '@components/ProgressSteps';
import '@components/RelayLogs';
import '@components/Setup/CCNList';
import { NDKEvent, NDKKind, NDKPrivateKeySigner, NDKRelay, NDKRelaySet } from '@nostr-dev-kit/ndk';
import { nip19 } from '@nostr/tools';
@customElement('arx-initial-setup')
export class InitialSetup extends LitElement {
@state() private currentPage = 1;
@state() private isAnimating = false;
@state() private seedPhrase = '';
@state() private communityName = '';
@state() private userName = '';
@state() private profileImage = '';
@state() private lightningAddress = '';
@ -32,8 +34,10 @@ export class InitialSetup extends LitElement {
pid: null,
logs: [],
};
@state() private selectedCCN: string | undefined;
@state() private ccnList: { name: string; pubkey: string }[] = [];
private readonly pageLabels = ['Welcome', 'Seed Phrase', 'Relay Setup', 'Profile', 'Complete'];
private readonly pageLabels = ['Welcome', 'Relay Setup', 'Seed Phrase', 'Profile', 'Complete'];
get encryptionPassphrase() {
let encryptionPassphrase = localStorage.getItem('encryption_key');
@ -212,10 +216,30 @@ export class InitialSetup extends LitElement {
}, 300);
}
private nextStep() {
if (this.currentPage === 1) return this.handleNavigation(2);
if (this.currentPage === 2) {
if (this.selectedCCN) return this.handleNavigation(4);
return this.handleNavigation(3);
}
if (this.currentPage === 3) return this.handleNavigation(4);
if (this.currentPage === 4) return this.goToFinalStep();
}
private previousStep() {
if (this.currentPage === 2) return this.handleNavigation(1);
if (this.currentPage === 3) return this.handleNavigation(2);
if (this.currentPage === 4) return this.handleNavigation(3);
}
private onSeedPhraseInput(event: ArxInputChangeEvent) {
this.seedPhrase = event.detail.value;
}
private onCommunityNameInput(event: ArxInputChangeEvent) {
this.communityName = event.detail.value;
}
private generateSeedPhrase() {
this.seedPhrase = nip06.generateSeedWords();
}
@ -227,7 +251,12 @@ export class InitialSetup extends LitElement {
return true;
}
private renderPageOne() {
private isValidCommunityName() {
const trimmedName = this.communityName.trim();
return trimmedName.length > 0;
}
private renderWelcomePage() {
return html`
<main class="welcome-container" ${animate()}>
<arx-progress-steps
@ -253,35 +282,139 @@ export class InitialSetup extends LitElement {
</p>
</section>
<section>
<arx-button
label="Share Feedback"
@click=${() => window.open('https://arx-ccn.com/eve-feedback')}
>
<iconify-icon slot="prefix" icon="mdi:feedback"></iconify-icon>
</arx-button>
<arx-button
variant="secondary"
label="Report a Bug"
@click=${() => window.open('https://arx-ccn.com/report-eve-bug')}
>
<iconify-icon slot="prefix" icon="mdi:bug"></iconify-icon>
</arx-button>
</section>
<div class="navigation">
<span></span>
<arx-button
variant="primary"
label="Next"
@click=${() => this.handleNavigation(2)}
@click=${() => this.nextStep()}
></arx-button>
</div>
</main>
`;
}
private renderPageTwo() {
private renderRelaySetupPage() {
return html`
<main class="welcome-container" ${animate()}>
<arx-progress-steps
.currentPage=${this.currentPage}
.pageLabels=${this.pageLabels}
></arx-progress-steps>
<section>
<h2>Configure Eve Relay</h2>
<p>
During this alpha phase, manual relay configuration is required.
This process will be automated in future releases.
</p>
<p>Please press the button below to start the relay.</p>
<arx-button
variant="primary"
label=${this.relayStatus.running ? 'Relay Running' : 'Start Relay'}
?disabled=${this.relayStatus.running}
@click=${() => this.startRelay()}
>
${when(
this.relayStatus.running,
() => html`<iconify-icon slot="prefix" icon="mdi:check-circle"></iconify-icon>`,
)}
</arx-button>
${when(
this.relayStatus.running,
() => html`
<p class="note">
<iconify-icon icon="mdi:information"></iconify-icon>
Relay is running with PID: ${this.relayStatus.pid}
</p>
<arx-relay-logs .logs=${this.relayStatus.logs}></arx-relay-logs>
<arx-setup-ccn-list @ccn-selected=${this.onCCNSelected} .ccns=${this.ccnList}></arx-setup-ccn-list>`,
)}
<p>
Having trouble? Our team is here to help if you encounter any
issues.
</p>
<p>Click Continue once the relay is running.</p>
</section>
<div class="navigation">
<arx-button
@click=${() => this.previousStep()}
variant="secondary"
label="Back"
>
</arx-button>
<arx-button
@click=${() => this.nextStep()}
variant="primary"
?disabled=${!this.relayStatus.running}
label=${this.selectedCCN ? 'Continue' : 'Create CCN'}
>
</arx-button>
</div>
</main>
`;
}
private onCCNSelected(e: CustomEvent<{ ccn: string }>) {
this.selectedCCN = e.detail.ccn;
}
private async startRelay() {
await window.relay.start(this.encryptionPassphrase);
await this.updateRelayStatus();
await new Promise((resolve) => {
const ws = new WebSocket('ws://localhost:6942');
ws.onopen = () => {
ws.send(JSON.stringify(['CCN', 'LIST']));
};
ws.onmessage = ({ data }) => {
const responseData = JSON.parse(data);
if (responseData[0] !== 'OK' || responseData[1] !== 'CCN LIST' || responseData[2] !== true) return;
this.ccnList = JSON.parse(responseData[3]);
resolve(true);
ws.close();
};
ws.onerror = () => {
resolve(false);
};
});
}
private async updateRelayStatus() {
this.relayStatus = await window.relay.getStatus();
if (this.relayStatus.running) setTimeout(() => this.updateRelayStatus(), 2000);
}
private async createCCN() {
await new Promise((resolve) => {
const ws = new WebSocket('ws://localhost:6942');
ws.onopen = () => {
ws.send(
JSON.stringify([
'CCN',
'CREATE',
{
name: this.communityName,
seed: this.seedPhrase,
},
]),
);
};
ws.onmessage = ({ data }) => {
const responseData = JSON.parse(data);
if (responseData[0] !== 'OK' || responseData[1] !== 'CCN CREATED' || responseData[2] !== true) return;
resolve(true);
ws.close();
this.selectedCCN = JSON.parse(responseData[3]).pubkey;
};
ws.onerror = () => {
resolve(false);
};
});
this.handleNavigation(4);
}
private renderSeedPhrasePage() {
return html`
<main class="welcome-container" ${animate()}>
<arx-progress-steps
@ -334,89 +467,34 @@ export class InitialSetup extends LitElement {
</div>
</section>
<div class="navigation">
<arx-button
@click=${() => this.handleNavigation(1)}
label="Back"
variant="secondary"
>
</arx-button>
<arx-button
@click=${() => this.handleNavigation(3)}
?disabled=${!this.isValidSeedPhrase()}
label="Continue"
variant="primary"
>
</arx-button>
</div>
</main>
`;
}
private async startRelay() {
await window.relay.writeSeed(this.seedPhrase);
await window.relay.start(this.encryptionPassphrase);
this.updateRelayStatus();
}
private async updateRelayStatus() {
this.relayStatus = await window.relay.getStatus();
if (this.relayStatus.running) setTimeout(() => this.updateRelayStatus(), 2000);
}
private renderPageThree() {
return html`
<main class="welcome-container" ${animate()}>
<arx-progress-steps
.currentPage=${this.currentPage}
.pageLabels=${this.pageLabels}
></arx-progress-steps>
<section>
<h2>Configure Eve Relay</h2>
<h3>Community Name</h3>
<p>
During this alpha phase, manual relay configuration is required.
This process will be automated in future releases.
Enter a name for your community. This will be used to identify your
community in Eve
</p>
<p>Please press the button below to start the relay.</p>
<arx-button
variant="primary"
label=${this.relayStatus.running ? 'Relay Running' : 'Start Relay'}
?disabled=${this.relayStatus.running}
@click=${() => this.startRelay()}
>
${when(
this.relayStatus.running,
() => html`<iconify-icon slot="prefix" icon="mdi:check-circle"></iconify-icon>`,
)}
</arx-button>
${when(
this.relayStatus.running,
() => html`
<p class="note">
<iconify-icon icon="mdi:information"></iconify-icon>
Relay is running with PID: ${this.relayStatus.pid}
</p>
<arx-relay-logs .logs=${this.relayStatus.logs}></arx-relay-logs>`,
)}
<p>
Having trouble? Our team is here to help if you encounter any
issues.
</p>
<p>Click Continue once the relay is running.</p>
<arx-input
type="text"
placeholder="Enter Community Name..."
.value=${this.communityName}
@change=${this.onCommunityNameInput}
></arx-input>
</section>
<div class="navigation">
<arx-button
@click=${() => this.handleNavigation(2)}
variant="secondary"
@click=${() => this.previousStep()}
label="Back"
variant="secondary"
>
</arx-button>
<arx-button
@click=${() => this.handleNavigation(4)}
variant="primary"
?disabled=${!this.relayStatus.running}
@click=${() => this.createCCN()}
?disabled=${!this.isValidSeedPhrase() || !this.isValidCommunityName()}
label="Continue"
variant="primary"
>
</arx-button>
</div>
@ -432,7 +510,7 @@ export class InitialSetup extends LitElement {
this.profileImage = e.detail.value;
}
private renderPageFour() {
private renderProfileSetupPage() {
return html`
<main class="welcome-container" ${animate()}>
<arx-progress-steps
@ -473,13 +551,13 @@ export class InitialSetup extends LitElement {
</section>
<div class="navigation">
<arx-button
@click=${() => this.handleNavigation(3)}
@click=${() => this.previousStep()}
variant="secondary"
label="Back"
>
</arx-button>
<arx-button
@click=${() => this.goToFinalStep()}
@click=${() => this.nextStep()}
variant="primary"
label="Next"
>
@ -490,17 +568,34 @@ export class InitialSetup extends LitElement {
}
private async goToFinalStep() {
await new Promise((resolve) => {
const ws = new WebSocket('ws://localhost:6942');
ws.onopen = () => {
ws.send(
JSON.stringify([
'CCN',
'ACTIVATE',
{
pubkey: this.selectedCCN,
},
]),
);
};
ws.onmessage = ({ data }) => {
const responseData = JSON.parse(data);
if (responseData[0] !== 'OK' || responseData[1] !== 'CCN ACTIVATED' || responseData[2] !== true) return;
resolve(true);
};
ws.onerror = () => {
resolve(false);
};
});
const randomPrivateKey = nostrTools.generateSecretKey();
const encryptedNsec = nip49.encrypt(randomPrivateKey, this.encryptionPassphrase);
const npub = nip19.npubEncode(nostrTools.getPublicKey(randomPrivateKey));
this.lightningAddress = `${npub}@npub.cash`;
localStorage.setItem('ncryptsec', encryptedNsec);
setSigner(new NDKPrivateKeySigner(randomPrivateKey));
await ndk.connect(5000);
await ndk.connect(10000);
const event = new NDKEvent(ndk);
event.kind = NDKKind.Metadata;
event.content = JSON.stringify({
@ -509,12 +604,13 @@ export class InitialSetup extends LitElement {
lud16: this.lightningAddress,
});
await event.sign();
await event.publish();
const relaySet = new NDKRelaySet(new Set([new NDKRelay('ws://localhost:6942', undefined, ndk)]), ndk);
await event.publish(relaySet, 10000);
localStorage.setItem('ncryptsec', encryptedNsec);
this.handleNavigation(5);
}
private renderPageFive() {
private renderFinalPage() {
return html`
<main class="welcome-container" ${animate()}>
<arx-progress-steps
@ -539,7 +635,7 @@ export class InitialSetup extends LitElement {
<div class="navigation">
<arx-button
@click=${() => this.handleNavigation(4)}
@click=${() => this.previousStep()}
variant="secondary"
label="Back"
>
@ -571,15 +667,15 @@ export class InitialSetup extends LitElement {
override render() {
switch (this.currentPage) {
case 1:
return this.renderPageOne();
return this.renderWelcomePage();
case 2:
return this.renderPageTwo();
return this.renderRelaySetupPage();
case 3:
return this.renderPageThree();
return this.renderSeedPhrasePage();
case 4:
return this.renderPageFour();
return this.renderProfileSetupPage();
case 5:
return this.renderPageFive();
return this.renderFinalPage();
default:
return html`<div class="welcome-container">
<arx-loading-view></arx-loading-view>

View file

@ -0,0 +1,56 @@
import '@components/General/Button';
import '@components/General/Select';
import { LitElement, css, html } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
@customElement('arx-setup-ccn-list')
export class CCNList extends LitElement {
@property({ type: Array }) ccns: { name: string; pubkey: string }[] = [];
static override styles = css`
:host {
display: block;
}
`;
@state() private selectedCCN: string | undefined;
private _valueMapper = (ccn: { name: string; pubkey: string }) => ccn.pubkey;
private _textMapper = (ccn: { name: string; pubkey: string }) => ccn.name;
private _handleCCNSelected(pubkey: string | undefined) {
this.selectedCCN = pubkey;
this.dispatchEvent(new CustomEvent('ccn-selected', { detail: { ccn: this.selectedCCN } }));
}
override render() {
if (this.ccns.length === 0)
return html`
<p>No CCNs found in the relay. You will be asked to setup a CCN in the next step.</p>
`;
if (this.ccns.length === 1)
return html`
<div>
<h2>CCN Found</h2>
<p>We found one CCN in your relay, which likely means you've used Eve before or there was an update introducing new setup steps. You can use this CCN to proceed with the setup, or create a new one.</p>
<p>CCN Name: ${this.ccns[0].name}</p>
<arx-button variant=${this.selectedCCN === this.ccns[0].pubkey ? 'accent' : 'secondary'} @click="${() => this._handleCCNSelected(this.ccns[0].pubkey)}" label="Use This CCN"></arx-button>
<arx-button variant=${this.selectedCCN === undefined ? 'accent' : 'secondary'} @click="${() => this._handleCCNSelected(undefined)}" label="Create New CCN"></arx-button>
</div>
`;
return html`
<div>
<h2>CCNs</h2>
<p>We found the following CCNs in your relay, which likely means you've used Eve before or there was an update introducing new setup steps. You can quickly choose one of these CCNs to proceed with the setup, or create a new one.</p>
<arx-select
.options="${this.ccns}"
.valueMapper="${this._valueMapper}"
.textMapper="${this._textMapper}"
@change="${(e: CustomEvent<{ value: string }>) => this._handleCCNSelected(e.detail.value)}"
></arx-select>
<arx-button variant=${this.selectedCCN === undefined ? 'accent' : 'secondary'} @click="${() => this._handleCCNSelected(undefined)}" label="Create New CCN"></arx-button>
</div>
`;
}
}

View file

@ -20,31 +20,31 @@ export default class Sidebar extends LitElement {
apps = [
{
id: 1,
href: 'beacon',
name: 'Beacon',
href: 'calendar',
name: 'Calendar',
color: '#FF8C00',
icon: 'fa-solid:sun',
icon: 'arx:calendar',
},
{
id: 2,
href: 'arbor',
name: 'Arbor',
color: '#FF4040',
icon: 'fa-solid:tree',
icon: 'arx:arbor',
},
{
id: 3,
href: 'wallet',
name: 'Wallet',
color: '#1E90FF',
icon: 'fa-solid:spa',
icon: 'arx:wallet',
},
{
id: 4,
href: 'settings',
name: 'Settings',
color: '#7B68EE',
icon: 'fa-solid:tools',
icon: 'arx:settings',
},
];

View file

@ -1,19 +1,10 @@
import path from 'node:path';
import { is, optimizer } from '@electron-toolkit/utils';
import { BrowserWindow, app, ipcMain, shell } from 'electron';
import fs from 'node:fs';
import path from 'node:path';
import { RelayManager } from './relayManager';
const relay = new RelayManager();
ipcMain.handle('relay:writeSeed', async (_, ...args: string[]) => {
if (!args[0]) throw new Error('No seed provided');
const seed = args[0] as string;
const configPath = relay.getRelayConfigPath();
fs.mkdirSync(configPath, { recursive: true });
fs.writeFileSync(path.join(configPath, 'ccn.seed'), seed);
});
ipcMain.handle('relay:start', (_, ...args: string[]) => {
if (!args[0]) throw new Error('No encryption key provided');
const encryptionKey = args[0];

View file

@ -5,7 +5,6 @@ if (process.contextIsolated) {
try {
contextBridge.exposeInMainWorld('electron', electronAPI);
contextBridge.exposeInMainWorld('relay', {
writeSeed: (seed: string) => ipcRenderer.invoke('relay:writeSeed', seed),
start: (encryptionKey: string) => ipcRenderer.invoke('relay:start', encryptionKey),
stop: () => ipcRenderer.invoke('relay:stop'),
getStatus: () => ipcRenderer.invoke('relay:status'),

View file

@ -1,8 +1,8 @@
import { is } from '@electron-toolkit/utils';
import { app } from 'electron';
import { type ChildProcess, spawn } from 'node:child_process';
import { existsSync, rmdirSync } from 'node:fs';
import { join } from 'node:path';
import { is } from '@electron-toolkit/utils';
import { app } from 'electron';
type PackageEnvironment = 'flatpak' | 'appimage' | 'system' | 'mac' | 'dev';
@ -144,20 +144,6 @@ export class RelayManager {
this.process = spawn('nc', ['localhost', '6942']);
this.devRunning = true;
if (this.process.stdout) {
this.process.stdout.on('data', (data: Buffer) => {
const logLine = data.toString().trim();
this.addLog(`[DEV] ${logLine}`);
});
}
if (this.process.stderr) {
this.process.stderr.on('data', (data: Buffer) => {
const logLine = data.toString().trim();
this.addLog(`[DEV] ERROR: ${logLine}`);
});
}
this.process.on('error', (err: Error) => {
this.addLog(`[DEV] Failed to start netcat: ${err.message}`);
this.process = null;
@ -210,9 +196,7 @@ export class RelayManager {
this.process.on('exit', this.handleProcessExit.bind(this));
if (this.process.pid) {
this.restartAttempts = 0;
}
if (this.process.pid) this.restartAttempts = 0;
} catch (error) {
console.error(`Error starting Relay: ${error instanceof Error ? error.message : 'Unknown error'}`);
this.restartProcess();

View file

@ -1,3 +1,4 @@
import '@/arx-icons';
import '@components/Breadcrumbs';
import '@components/ErrorView';
import '@components/Header';
@ -8,6 +9,8 @@ import '@routes/router';
import type EveRouter from '@routes/router';
import './style.css';
let availableCCNs: { name: string; pubkey: string }[] = [];
function checkRelayUp() {
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
@ -19,7 +22,66 @@ function checkRelayUp() {
ws.onopen = () => {
clearTimeout(timeoutId);
ws.close();
ws.send(JSON.stringify(['CCN', 'LIST']));
};
ws.onmessage = async (message) => {
const data = JSON.parse(message.data);
console.log(data);
if (data[0] !== 'OK' || data[1] !== 'CCN LIST' || data[2] !== true) return;
availableCCNs = JSON.parse(data[3]);
if (availableCCNs.length === 0) throw new Error('No CCNs found');
if (availableCCNs.length === 1) {
ws.send(
JSON.stringify([
'CCN',
'ACTIVATE',
{
pubkey: availableCCNs[0].pubkey,
},
]),
);
return resolve(true);
}
if (localStorage.getItem('selectedCCN')) {
const selected = JSON.parse(localStorage.getItem('selectedCCN')!) as { date: number; pubkey: string };
if (Date.now() - selected.date < 1000 * 60 * 60 * 24) {
ws.send(JSON.stringify(['CCN', 'ACTIVATE', { pubkey: selected.pubkey }]));
return resolve(true);
}
}
await new Promise((resolve) => {
let selectedCCN: string | null = availableCCNs[0].pubkey;
const selectBox = document.createElement('arx-select');
selectBox.options = availableCCNs.map((ccn) => ({ label: ccn.name, value: ccn.pubkey }));
selectBox.addEventListener('change', (e) => {
selectedCCN = e.target.value;
});
const okButton = document.createElement('arx-button');
okButton.label = 'OK';
okButton.addEventListener('click', () => {
if (selectedCCN) {
ws.send(
JSON.stringify([
'CCN',
'ACTIVATE',
{
pubkey: selectedCCN,
},
]),
);
localStorage.setItem('selectedCCN', JSON.stringify({ date: Date.now(), pubkey: selectedCCN }));
resolve(true);
document.body.removeChild(dialog);
}
});
const dialog = document.createElement('arx-dialog');
dialog.title = 'Select CCN';
dialog.open = true;
dialog.appendChild(selectBox);
dialog.appendChild(okButton);
document.body.appendChild(dialog);
});
resolve(true);
};

View file

@ -5,7 +5,6 @@ interface RelayStatus {
}
interface RelayBridge {
writeSeed: (seed: string) => Promise<void>;
start: (encryptionKey: string) => Promise<void>;
stop: () => Promise<void>;
getStatus: () => Promise<RelayStatus>;

View file

@ -5,14 +5,14 @@ import { customElement, state } from 'lit/decorators.js';
import { map } from 'lit/directives/map.js';
import { when } from 'lit/directives/when.js';
import '@components/Breadcrumbs';
import '@components/Arbor/ForumTopic';
import '@components/Arbor/ForumCategory';
import '@components/Arbor/ForumThread';
import '@components/Breadcrumbs';
import '@components/General/Prompt';
import type { EvePrompt } from '@components/General/Prompt';
interface ForumTopic {
interface ForumThread {
id: string;
title: string;
author: string;
@ -23,7 +23,7 @@ interface ForumTopic {
interface ForumCategory {
id: string;
name: string;
topics: ForumTopic[];
threads: ForumThread[];
}
@customElement('arx-arbor-home')
@ -33,7 +33,7 @@ export class ArborForum extends LitElement {
private isSaving = false;
private topicsQuery: NDKSubscription | undefined;
private threadsQuery: NDKSubscription | undefined;
@state()
private forum: NDKEvent | null = null;
@ -42,6 +42,10 @@ export class ArborForum extends LitElement {
:host {
display: block;
}
arx-button {
margin-bottom: calc(var(--spacing-sm) / 2);
}
`;
override async connectedCallback() {
@ -92,11 +96,11 @@ export class ArborForum extends LitElement {
{
id: dtag,
name: newCategory,
topics: [],
threads: [],
},
];
this.loadTopics();
this.loadThreads();
} catch (error) {
console.error('Failed to create category:', error);
alert('Failed to create category');
@ -128,17 +132,17 @@ export class ArborForum extends LitElement {
{
id: categoryId,
name: tag[2],
topics: [],
threads: [],
},
];
}
}
this.loadTopics();
this.loadThreads();
}
private async loadTopics() {
if (this.topicsQuery) this.topicsQuery.stop();
this.topicsQuery = ndk
private async loadThreads() {
if (this.threadsQuery) this.threadsQuery.stop();
this.threadsQuery = ndk
.subscribe({
kinds: [892 as NDKKind],
'#d': this.categories.map((category) => `60891:${category.id}`),
@ -148,9 +152,9 @@ export class ArborForum extends LitElement {
(category) => category.id === event.tags.find((tag) => tag[0] === 'd')?.[1].split(':')[1],
);
if (categoryId === -1) return;
if (this.categories[categoryId].topics.find((topic) => topic.id === event.id)) return;
this.categories[categoryId].topics = [
...this.categories[categoryId].topics,
if (this.categories[categoryId].threads.find((thread) => thread.id === event.id)) return;
this.categories[categoryId].threads = [
...this.categories[categoryId].threads,
{
id: event.id,
title: event.tags.find((tag) => tag[0] === 'name')?.[1] || '',
@ -187,18 +191,18 @@ export class ArborForum extends LitElement {
this.categories,
(category) => html`
<arx-arbor-forum-category id=${category.id} title=${category.name}>
${when(category.topics.length === 0, () => html` <div style="padding: 1rem 1.5rem">No topics...</div> `)}
${when(category.threads.length === 0, () => html` <div style="padding: 1rem 1.5rem">No threads...</div> `)}
${map(
category.topics,
(topic) => html`
<arx-arbor-forum-topic
id=${topic.id}
title=${topic.title}
description=${topic.description}
lastPostBy=${topic.author}
lastPostTime=${topic.created_at}
category.threads,
(thread) => html`
<arx-arbor-forum-thread
id=${thread.id}
title=${thread.title}
description=${thread.description}
lastPostBy=${thread.author}
lastPostTime=${thread.created_at}
>
</arx-arbor-forum-topic>
</arx-arbor-forum-thread>
`,
)}
</arx-arbor-forum-category>

View file

@ -9,13 +9,13 @@ import '@components/General/Textarea';
@customElement('arx-arbor-post-creator')
export class ArborPostCreator extends LitElement {
@property({ type: String })
topicId = '';
threadId = '';
@state()
private postContent = '';
@state()
private topic: NDKEvent | null = null;
private thread: NDKEvent | null = null;
@state()
private isCreating = false;
@ -34,7 +34,7 @@ export class ArborPostCreator extends LitElement {
gap: 1rem;
}
.topic-id {
.thread-id {
color: #666;
font-family: monospace;
}
@ -58,24 +58,24 @@ export class ArborPostCreator extends LitElement {
override connectedCallback() {
super.connectedCallback();
this.loadTopic();
this.loadThread();
}
private async loadTopic() {
private async loadThread() {
try {
await getSigner();
this.topic = await ndk.fetchEvent(this.topicId);
if (!this.topic) {
throw new Error('Could not load topic');
this.thread = await ndk.fetchEvent(this.threadId);
if (!this.thread) {
throw new Error('Could not load thread');
}
} catch (error) {
console.error('Failed to load topic:', error);
console.error('Failed to load thread:', error);
}
}
private async doCreatePost() {
if (this.isCreating) return;
if (!this.topic) return;
if (!this.thread) return;
if (this.postContent.length < 10) {
this.error = 'Post content must be at least 10 characters long';
@ -88,9 +88,9 @@ export class ArborPostCreator extends LitElement {
const event = new NDKEvent(ndk);
event.kind = 893;
event.tags = [
['A', this.topic.tags.find((tag) => tag[0] === 'd')?.[1] || ''],
['E', this.topicId],
['p', this.topic.pubkey],
['A', this.thread.tags.find((tag) => tag[0] === 'd')?.[1] || ''],
['E', this.threadId],
['p', this.thread.pubkey],
];
event.content = this.postContent;
@ -117,7 +117,7 @@ export class ArborPostCreator extends LitElement {
override render() {
return html`
<div class="container">
<div class="topic-id">Topic ID: ${this.topicId}</div>
<div class="thread-id">Thread ID: ${this.threadId}</div>
<arx-textarea
placeholder="Post. You can use Markdown here."

View file

@ -8,16 +8,16 @@ import '@components/General/Button';
import '@components/General/Input';
import '@components/General/Textarea';
@customElement('arx-arbor-topic-creator')
export class ArborTopicCreator extends LitElement {
@customElement('arx-arbor-thread-creator')
export class ArborThreadCreator extends LitElement {
@property({ type: String })
categoryId = '';
@state()
private newTopic = '';
private newThread = '';
@state()
private topicContent = '';
private threadContent = '';
@state()
private isCreating = false;
@ -39,16 +39,16 @@ export class ArborTopicCreator extends LitElement {
}
`;
private async doCreateTopic() {
private async doCreateThread() {
if (this.isCreating) return;
if (this.newTopic.length < 3) {
alert('Topic title must be at least 3 characters long');
if (this.newThread.length < 3) {
alert('Thread title must be at least 3 characters long');
return;
}
if (this.topicContent.length < 10) {
alert('Topic content must be at least 10 characters long');
if (this.threadContent.length < 10) {
alert('Thread content must be at least 10 characters long');
return;
}
@ -59,40 +59,27 @@ export class ArborTopicCreator extends LitElement {
const event = new NDKEvent(ndk);
event.kind = 892;
event.tags = [
['name', this.newTopic],
['name', this.newThread],
['d', `60891:${this.categoryId}`],
];
event.content = this.topicContent;
event.content = this.threadContent;
await event.sign();
await event.publish();
this.dispatchEvent(
new CustomEvent('topic-created', {
bubbles: true,
composed: true,
detail: {
topicId: event.id,
categoryId: this.categoryId,
},
}),
);
this.newTopic = '';
this.topicContent = '';
location.hash = `/arbor/${event.id}`;
} catch (error) {
console.error('Failed to create topic:', error);
alert('Failed to create topic');
console.error('Failed to create thread:', error);
alert('Failed to create thread');
} finally {
this.isCreating = false;
}
}
private handleTopicInput(e: ArxInputChangeEvent) {
this.newTopic = e.detail.value;
private handleThreadInput(e: ArxInputChangeEvent) {
this.newThread = e.detail.value;
}
private handleContentInput(e: ArxInputChangeEvent) {
this.topicContent = e.detail.value;
this.threadContent = e.detail.value;
}
override render() {
@ -101,15 +88,15 @@ export class ArborTopicCreator extends LitElement {
<arx-input
type="text"
placeholder="New Topic"
.value=${this.newTopic}
@change=${this.handleTopicInput}
placeholder="New Thread"
.value=${this.newThread}
@change=${this.handleThreadInput}
?disabled=${this.isCreating}
></arx-input>
<arx-textarea
placeholder="Topic. You can use Markdown here."
.value=${this.topicContent}
placeholder="Thread. You can use Markdown here."
.value=${this.threadContent}
@change=${this.handleContentInput}
?disabled=${this.isCreating}
></arx-textarea>
@ -123,7 +110,7 @@ export class ArborTopicCreator extends LitElement {
<arx-button
label=${this.isCreating ? 'Creating...' : 'Create'}
@click=${this.doCreateTopic}
@click=${this.doCreateThread}
?disabled=${this.isCreating}
>
</arx-button>

View file

@ -3,8 +3,8 @@ import type { NDKKind, NDKSubscription } from '@nostr-dev-kit/ndk';
import { LitElement, css, html } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import '@components/Breadcrumbs';
import '@components/Arbor/ForumPost';
import '@components/Breadcrumbs';
import '@components/General/Button';
import { map } from 'lit/directives/map.js';
@ -17,10 +17,10 @@ interface ForumPost {
content: string;
}
@customElement('arx-arbor-topic-view')
export class ArborTopicView extends LitElement {
@customElement('arx-arbor-thread-view')
export class ArborThreadView extends LitElement {
@property({ type: String })
topicId = '';
threadId = '';
@state()
override title = '';
@ -36,7 +36,7 @@ export class ArborTopicView extends LitElement {
margin: 0 auto;
}
.topic-container {
.thread-container {
background: var(--color-base-100);
border-radius: var(--radius-box);
border: var(--border) solid var(--color-base-300);
@ -144,7 +144,7 @@ export class ArborTopicView extends LitElement {
override async connectedCallback() {
super.connectedCallback();
await this.loadTopic();
await this.loadThread();
}
override disconnectedCallback() {
@ -154,13 +154,13 @@ export class ArborTopicView extends LitElement {
}
}
private async loadTopic() {
private async loadThread() {
try {
await getSigner();
const event = await ndk.fetchEvent(this.topicId);
const event = await ndk.fetchEvent(this.threadId);
if (!event) {
throw new Error('Could not load topic');
throw new Error('Could not load thread');
}
this.title = event.tags.find((tag) => tag[0] === 'name')?.[1] || '';
@ -178,7 +178,7 @@ export class ArborTopicView extends LitElement {
this.subscription = ndk
.subscribe({
kinds: [893 as NDKKind],
'#E': [this.topicId],
'#E': [this.threadId],
})
.on('event', (event) => {
this.posts = [
@ -192,8 +192,8 @@ export class ArborTopicView extends LitElement {
];
});
} catch (error) {
console.error('Failed to load topic:', error);
alert('Could not load topic');
console.error('Failed to load thread:', error);
alert('Could not load thread');
}
}
@ -203,9 +203,9 @@ export class ArborTopicView extends LitElement {
return html`
<arx-breadcrumbs .items=${breadcrumbItems}></arx-breadcrumbs>
<div class="topic-container">
<div class="thread-container">
<div class="header">
<span>${this.title || 'Loading topic...'}</span>
<span>${this.title || 'Loading thread...'}</span>
</div>
<div class="posts-container">
@ -218,7 +218,7 @@ export class ArborTopicView extends LitElement {
(post) => html`
<arx-forum-post
id=${post.id}
.topicId=${this.topicId}
.threadId=${this.threadId}
.npub=${post.npub}
.date=${post.date}
.content=${post.content}
@ -229,7 +229,7 @@ export class ArborTopicView extends LitElement {
</div>
<div class="actions">
<arx-button label="New Post" href="/arbor/new-post/${this.topicId}">
<arx-button label="New Post" href="/arbor/new-post/${this.threadId}">
New Post
</arx-button>
</div>

View file

@ -26,56 +26,57 @@ export class Home extends LitElement {
href: 'letters',
name: 'Letters',
color: '#FF3E96',
icon: 'fa-solid:leaf',
icon: 'arx:letters',
},
{
id: 1,
href: 'messages',
name: 'Murmur',
href: 'howl',
name: 'Howl',
color: '#00CD66',
icon: 'fa-solid:seedling',
icon: 'arx:howl',
},
{
id: 2,
href: 'beacon',
name: 'Beacon',
href: 'calendar',
name: 'Calendar',
color: '#FF8C00',
icon: 'fa-solid:sun',
icon: 'arx:calendar',
},
{
id: 3,
href: 'arbor',
name: 'Arbor',
color: '#FF4040',
icon: 'fa-solid:tree',
icon: 'arx:arbor',
},
{
id: 5,
href: 'grove',
name: 'Grove',
href: 'market',
name: 'Market',
color: '#9370DB',
icon: 'fa-solid:store-alt',
icon: 'arx:market',
},
{
id: 6,
href: 'wallet',
name: 'Wallet',
color: '#1E90FF',
icon: 'fa-solid:spa',
icon: 'arx:wallet',
},
{
id: 7,
href: 'oracle',
name: 'Oracle',
color: '#FFD700',
icon: 'bxs:landscape',
href: 'pool',
name: 'Pool',
color: '#7aD700',
icon: 'arx:pool',
},
{
id: 8,
href: 'settings',
name: 'Settings',
color: '#7B68EE',
icon: 'fa-solid:tools',
icon: 'arx:settings',
},
},
];

View file

@ -3,8 +3,8 @@ import '@components/Sidebar';
import '@routes/404Page';
import '@routes/Arbor/Home';
import '@routes/Arbor/NewPost';
import '@routes/Arbor/NewTopic';
import '@routes/Arbor/TopicView';
import '@routes/Arbor/NewThread';
import '@routes/Arbor/ThreadView';
import '@routes/Calendar';
import '@routes/Home';
import '@routes/Profile';
@ -16,6 +16,7 @@ import type { NDKUserProfile } from '@nostr-dev-kit/ndk';
import { spread } from '@open-wc/lit-helpers';
import { LitElement, css } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { keyed } from 'lit/directives/keyed.js';
import { type Ref, createRef, ref } from 'lit/directives/ref.js';
import { when } from 'lit/directives/when.js';
@ -42,7 +43,7 @@ export default class EveRouter extends LitElement {
component: literal`arx-eve-home`,
},
{
pattern: 'beacon',
pattern: 'calendar',
params: {},
component: literal`arx-calendar-route`,
},
@ -57,20 +58,25 @@ export default class EveRouter extends LitElement {
component: literal`arx-arbor-home`,
},
{
pattern: 'arbor/new-topic/:categoryId',
pattern: 'arbor/new-thread/:categoryId',
params: {},
component: literal`arx-arbor-topic-creator`,
component: literal`arx-arbor-thread-creator`,
},
{
pattern: 'arbor/topics/:topicId',
pattern: 'arbor/threads/:threadId',
params: {},
component: literal`arx-arbor-topic-view`,
component: literal`arx-arbor-thread-view`,
},
{
pattern: 'arbor/new-post/:topicId',
pattern: 'arbor/new-post/:threadId',
params: {},
component: literal`arx-arbor-post-creator`,
},
{
pattern: 'howl',
params: {},
component: literal`arx-howl-route`,
},
{
pattern: 'settings',
params: {},
@ -112,6 +118,9 @@ export default class EveRouter extends LitElement {
@state()
private userNpub = '';
@state()
private sidebarVisible = true;
static override styles = css`
:host {
position: fixed;
@ -121,8 +130,13 @@ export default class EveRouter extends LitElement {
height: 100%;
display: grid;
grid-template-rows: auto 1fr;
grid-template-columns: 100px 1fr;
grid-template-columns: auto 1fr;
overflow: hidden;
transition: grid-template-columns 0.3s ease;
}
:host([sidebar-hidden]) {
grid-template-columns: 0 1fr;
}
::-webkit-scrollbar {
@ -173,6 +187,21 @@ export default class EveRouter extends LitElement {
arx-sidebar {
grid-column: 1;
grid-row: 1 / span 2;
transition: width 0.3s ease, opacity 0.3s ease, padding 0.3s ease, transform 0.3s ease;
width: 100px;
opacity: 1;
overflow: hidden;
background: var(--color-base-100);
border-right: 1px solid var(--color-base-300);
}
arx-sidebar.hidden {
width: 0;
opacity: 0;
padding: 0;
pointer-events: none;
transform: translateX(-20px);
border-right: none;
}
.window-content {
@ -187,6 +216,7 @@ export default class EveRouter extends LitElement {
transition: var(--transition);
backface-visibility: hidden;
filter: blur(0px);
grid-row: 1;
}
.window-content::after {
@ -218,6 +248,44 @@ export default class EveRouter extends LitElement {
grid-column: 2;
grid-row: 1;
}
.sidebar-toggle {
position: fixed;
top: 50%;
left: 100px;
transform: translateY(-50%);
z-index: 1000;
background: color-mix(in srgb, var(--color-primary) 70%, transparent);
backdrop-filter: blur(5px);
color: var(--color-base);
border: none;
width: 20px;
height: 50px;
border-top-right-radius: var(--radius-btn);
border-bottom-right-radius: var(--radius-btn);
border-top-left-radius: 0;
border-bottom-left-radius: 0;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
transition: all var(--transition);
box-shadow: 2px 0 5px rgba(0,0,0,0.1);
}
:host([sidebar-hidden]) .sidebar-toggle {
left: 0;
border-top-left-radius: var(--radius-btn);
border-bottom-left-radius: var(--radius-btn);
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.sidebar-toggle:hover {
/* background: var(--color-primary-focus); */
background: color-mix(in srgb, var(--color-primary) 90%, transparent);
}
`;
constructor() {
@ -233,6 +301,7 @@ export default class EveRouter extends LitElement {
if (this.ccnSetup) {
this.loadUserProfile();
}
this.updateSidebarAttribute();
}
override disconnectedCallback(): void {
@ -403,11 +472,36 @@ export default class EveRouter extends LitElement {
}
}
private toggleSidebar() {
this.sidebarVisible = !this.sidebarVisible;
this.updateSidebarAttribute();
}
private updateSidebarAttribute() {
if (this.sidebarVisible) {
this.removeAttribute('sidebar-hidden');
} else {
this.setAttribute('sidebar-hidden', '');
}
}
override render() {
if (!this.ccnSetup) return this.renderSetup();
return html`
<button class="sidebar-toggle" @click=${this.toggleSidebar} title=${when(
this.sidebarVisible,
() => 'Hide Sidebar',
() => 'Show Sidebar',
)}>
${when(
this.sidebarVisible,
() => html`<iconify-icon icon="mdi:chevron-left"></iconify-icon>`,
() => html`<iconify-icon icon="mdi:chevron-right"></iconify-icon>`,
)}
</button>
<arx-sidebar
class=${classMap({ hidden: !this.sidebarVisible })}
.currentPath=${this.currentPath}
.userNpub=${this.userNpub}
.userProfile=${this.userProfile}
@ -422,8 +516,8 @@ export default class EveRouter extends LitElement {
@go-forward=${this.goForward}
title="Eve"
></arx-header>
<div class="window ${this.currentRoute.pattern === 'home' ? 'hide-overflow' : ''}">
<div ${ref(this.windowContentRef)} class="window-content ${this.isTransitioning ? 'transitioning' : ''}">
<div class=${classMap({ window: true, 'hide-overflow': this.currentRoute.pattern === 'home' })}>
<div ${ref(this.windowContentRef)} class=${classMap({ 'window-content': true, transitioning: this.isTransitioning })}>
${when(
this.isTransitioning,
() => html`<arx-loading-view></arx-loading-view>`,

View file

@ -29,7 +29,8 @@
"@routes/*": ["./src/routes/*"],
"@styles/*": ["./src/styles/*"],
"@components/*": ["./src/components/*"],
"@widgets/*": ["./src/components/Widgets/*"]
"@widgets/*": ["./src/components/Widgets/*"],
"@assets/*": ["./src/assets/*"],
}
},
"include": ["./src/**/*"]