Tungsten
- 15 Devlogs
- 34 Total hours
A very simple self-hosted web app that serves as an obsidian alternative, but simpler. Fast and effective! Built with Next.JS!
A very simple self-hosted web app that serves as an obsidian alternative, but simpler. Fast and effective! Built with Next.JS!
Did some quick bug fixes with the demo and we’re ready for launch! Specifically, making new files was completely broken for some reason, and I added the stardance logo to the README. Additionally, the readme is also fetched from github so I can update it on the fly if necessary.
This was last on the list (for some reason), but all network errors are handled with a toast at the top of the screen, which makes it much clearer for everyone what went wrong.
All the themes are based on the Catppuccin color palettes because they are wonderful, and allowed me to focus on other things instead of making a color palette. And it looks phenomenal!
The theme switcher is painfully simple. React’s Context API is so underrated. It saves your theme in localstorage and loads it every time you open the page!
This one was a little tricky to get correct. I had to go through a bunch of the old logic in the sidebar to ensure that no file names collided, and if they did, make sure that state doesn’t get pushed.
Additionally, I had to make creating files look at the rest of the files in the folder and append a 1, 2, 3, etc. to “New File” so the names didn’t collide as soon as you created them.
I was going to put these same restrictions on the server and realized that would take forever with little benefit, as if you break the client to send requests like that, then you’re going to break it. It doesn’t even break that badly, the files work perfectly fine, it’s really just links. After all, these are self-hosted instances, so if you forge some stupid requests I could not care less.
Speaking of links, cross page links are functional again! I procrastinated this one too much but I just needed to make a new helper function, nodeFromPath. It takes an array of strings, being the path to a node, and give you the node (or undefined) that path leads to. I just love working with trees so much, recursive functions are my favorite 
Yay!!! Big updates!!! Almost ready to ship!!! (optimistically)
This is a big one. It also took me forever. But self hosting is fully functional through Docker!
Not displayed is the 5 hours it took me to actually learn Docker… I have used it before but I haven’t actually learned what I was doing. Containers, images, volumes, compose, blah blag. But, it’s worth it because self-hosting is now just one or two commands away.
A lot of the hours listed on this devlog is writing + refining the SELFHOST.md file! It’s a detailed document that describes Tungsten’s stack and how to host it on your hardware. Soon I hope to bring this to an actual website like the landing page for Tungsten.
This is slightly less interesting, but gh actions are set up! It’ll test building the full project, then push a docker image, and do a quick codeql check. I copied most of this from Filatrack, I’ll be honest, but what can I say? It works great!
I tried uploading an image while I was testing stuff and realized that Next.JS limits the body size of server actions quite a lot. Luckily there was an option to change it, and now you can change it to what you want!
Also, the state of which files were unsaved got messed up somehow. When you clicked between an unsaved tab and a saved tab, the saved one would become unsaved! Quick fix but was pretty funny.
Well, my todo list is only getting fuller as the days go on. I pretty much add one or two things for every one I cross off. But all I’ve got left is to fix cross-page links (I broke them last devlog), file name restrictions (like a real filesystem), an options page, and proper error handling. After that (and whatever else I add), we’ll ship!
The landing page is pretty simple. I’m not an artist by any means, so this will do. It’s mostly for you voters/reviewers to get an overview and try the demo!
The demo is super simple. It’s a simple env flag that will change the landing page to what you see now. The demo button simply tries to log you in to the user “[email protected]” with password “demodemo”. This is the only user that will exist in the database, so it will just log in to that. All the server actions have their behavior changed in the demo to not make any changes to the database as well, so whatever you do isn’t saved.
As you can see in the second image, I fixed many things that were bugging me. Namely, the URL bar. That’s how files would be selected, but it caused some really stupid state issues that caused a multitude of bugs and like 2 hours of headaches. That’s gone, and so is autosaving. Everything is now more performant as well!
The editor now has a raw and read only view! The raw view lets you edit the raw markdown, and read only does exactly what you think it does.
I ended up rewriting the logic for saving/loading in the editor, because it got extremely messy and was breaking down with all the features I’ve been adding. You won’t notice anything different, but it’s much more reliable!
We’re getting close to release. I’ve got a couple features I’d like to add and we’ll get going.
Firstly, the logo! I made it myself of a reference image of actual tungsten, cause it’s a cool element. I based the style around the Obsidian logo, with distinct sections each with their own gradients to simulate lighting. My take on this uses all straight lines, as that’s part of the design of the UI. This is my first time really making SVG art, I think it looks pretty decent! 
Next, tables!
and an upgraded menu bar!
Surprisingly, tables were one of the easiest plugins to add. All my CSS work from like a week ago carried over here and it looks great. There might be some changes I do later on however.
And a bunch of unrelated bug fixes that had to be done, as always.
Links to different files work great! There’s a small dialog to edit the link and whatnot and it’ll take you to the specified page!!
As much as I want to use wikilinks for this, that would require a custom plugin I don’t know how to make (I don’t even know if it’s really possible with MDXEditor). I’m gonna leave it at this for now, as the create link dialog works great for making them.
I had to make some pretty major changes to how selecting files works, however. If you look at the URL bar, it now shows the selected file in it. That is now what determines the selected file, which I’ve been meaning to do for a while and makes links infinitely easier.
Math was probably the hardest thing to add due to the lack of extensibility of the MDXEditor component I’m using. The plugin system is overly complex and makes zero sense since there is hardly any documentation on how to actually use it. Luckily, there is a jsxPlugin that lets the user make custom jsx components of your choosing.
The other hard part was finding a suitable editor for the job. After much googling, I found MathLive, which is a currently unmaintained editor that does exactly what I need it to. Internally it uses KaTeX, which is what I was looking for.
Now the problem arises: combining these libraries. This wasn’t easy; it took a lot of trial and error, as the documentation is limited. But the final result is incredibly worth it.
A couple bugs still exist; notably, pressing the left or right arrow into the math editor does not focus your cursor in it; and similarly for moving out of it. I have no clue how to fix these, MDXEditor is not helpful in the slightest. A minor bug I can fix however is the autosaver not saving if only the MathEditor has changed.
This one was so much easier! There’s a built in plugin for it. It even handles image resizing! I just have to handle uploading images to the server. I decided to make a table in the database that simply stores filenames and their associated users, just so you can’t access other user’s images. When you upload a file, it gives the editor a link to api/image/random-uuid.png. The api route checks the user id against the database and gives you the image in response.
Possibly the dumbest thing ever is the fact that the image plugin limits the minimum size of the image at 100x100px. I don’t like this however cause I want to be able to put emojis in my markdown files, with possible support for later uploading custom emojis and using them in the editor. There is no customizing this value, it is hardcoded in the library. Luckily, pnpm natively supports patching node modules, and that’s exactly what I did. The minimum size is now 10x10!
This one took a hot minute but it works really great! It’s so much better than using a modal to do everything!
I didn’t actually know this but the tree component has support for creating files like this out of the box, it’s just not listed in any documentation so it took me a bit to find. It actually starts by creating a file named “New File”, then just immediately renaming it, which is a much cleaner solution than what I was planning 
Renaming is painfully simple, as it handles a bunch of the work too but it’s actually a completely different component it changes to when you start renaming, which ended up being so much easier to work with.
I ran into one of the stupidest bugs ever while implementing renaming. The best way to describe it is “When renaming a file, if it is the open tab, whenever it autosaves it will reset the rename input cause it rerenders.” But basically, the autosaver would save the selected file, causing everything that uses useEditor to rerender, including the sidebar, which makes it re-render the input component for renaming, which resets the text to it’s initialName. It wasn’t hard to fix, I just made the autosaver not save if the content hasn’t changed, which I’ve been meaning to do anyway.
For now, that is most of the hard features done
the last hurdle that I’m not excited to implement is links between pages. I have no clue how that will work, especially with the component I’m using. But that’s next!
A very annoying bug with a solution I can’t believe I missed. Basically, to get large code blocks to work, MDXEditor gives you a codeMirrorPlugin. This plugin lets you pass a list of languages to have in the selector and to have syntax highlighting support for (codemirror provides a very large data set of them, luckily). The problem was, this list was very large and caused lag when typing.
I ended up asking the discussions page and (after an unhelpful AI response), we figured out my onChange handler setting a content state that the markdown editor used was causing the whole thing to re-render every time you typed, and with the large language array, causing a lot of lag. You’ll notice in the video the language selector in the toolbar.
So it was a very, very simple fix. I even went through and implemented auto-saving instead (every 5 seconds). It even stops you from leaving if it hasn’t saved yet!
I also fixed a whole slew of scrolling issues with long markdown files. It was the stupidest thing ever and I’m not proud of the fix, but it works now.
Little more work before the end of the day! (well, it’s past midnight so I guess not)
Large code blocks work and are horribly laggy, will be fixing very soon. They are a pain to style properly since MDXEditor is heavily opinionated and makes everything hard to style.
You can now drag and drop tabs! I only had to install a second library for DnD to make it work! (Drag and drop is the worst thing. Ever.) I love making my bundle size 10% larger with bloated libraries 
The sidebar is fully functional!! You can drag and drop all your files and folders around, nest them as much as you want, and it will retain all of this information in the database. It only took a database rework and many hours of my life 
Let’s talk about it…
So. The database was originally structured with file and folder tables. Both of them had parentId fields and folders had children. Unfortunately this complicated the code more when I tried implementing this as I had to get all the files and folders and combine them into one array. I looked up the proper way to do this because I clearly was not doing it correctly, and switched to just one table, nodes. This simplified just about everything, and this made me split off the file contents as another table as well. I was meaning to do that anyway just to save on memory, as otherwise all the content of every file would be in memory all the time.
Now this node table has all the same fields as before. How do I render this tree? I looked around for good libraries (there’s almost none), and found react-arborist. Looked great, had alright documentation. I started implementing it, and realized I needed to implement the control functions for moving the elements. That’s easy, I said. It wasn’t 
After like an hour it still was just not working. It was really close but was being weird. I looked up the proper way to do it with this library and apparently there’s a class that has a move function that does all the work for you. And yep. It works. For some reason it turned folders back into files but ended up being an easy fix.
So that was a mess but here we are. It works flawlessly. Now I’ve got to implement context menus in this mess, including creating files in folders, etc.
You can now save files! The dot indicator works similarly to basically every IDE ever and has surprisingly complicated logic with the close button as well.
The Markdown editor I’m using is kinda a pain, but a lot better than making one from scratch. I had to implement the key listener on the div container for it. Soon I’ll implement the key listener on the whole page with a special handler that components can listen to.
The only reason I even implemented the save system is to make it easier on the database. Without saving, it would write to the database every key press. I like this style better anyway, I think it’s pretty cool that it feels like an IDE!
Made the sidebar, tabs, and creating files work! You can open tabs for each file you’re working on and see the content of them in the editor.
I actually went to the react documentation to learn some best practices… as I wanted to avoid passing a million props down through the component tree and passing a bunch of callbacks back up. This is called “prop drilling” and it’s the worst thing ever. I learned how to use React’s context system to “teleport” the props I need to the part of the tree that needs them. As you can see, it works flawlessly! Most of the components you see don’t even have props, which makes it so much easier to work with.
Log in page and basic editor UI!
I used Auth.JS for the credentials login. I actually hate that framework and could do it myself but I’m lazy. It probably took longer to set it up than to figure out how JWTs work.
The editor is a WYSIWYG markdown editor called MDXEditor. It’s a struggle to work with cause they use their own stupid styling with no real way to style it other than overriding CSS that I can tell.
Next up is getting pages working in the database!